Skip to content

Commit e0fe05a

Browse files
committed
Adds a check to Python executable validation.
Adds a check to determine if the provided path is an file or a directory. If the path is directory, an RSConnectException is thrown. In addition: - The error message for each existing validation is updated to inform the user that a Python executable is expected. - Refactors the which_python method, eliminating an unused parameter, and providing additional None safety.
1 parent 4838b11 commit e0fe05a

File tree

3 files changed

+46
-20
lines changed

3 files changed

+46
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010

1111
- The `--cacert` option now supports certificate files encoded in the Distinguished Encoding Rules (DER) binary format. Certificate files with DER encoding must end in a `.cer` or `.der` suffix.
12+
- The `--python` option now provides additional user guidance when an invalid path is provided.
1213

1314
### Changed
1415

rsconnect/bundle.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,19 +1075,24 @@ def are_apis_supported_on_server(connect_details):
10751075
return connect_details["python"]["api_enabled"]
10761076

10771077

1078-
def which_python(python, env=os.environ):
1079-
"""Determine which python binary should be used.
1080-
1081-
In priority order:
1082-
* --python specified on the command line
1083-
* the python binary running this script
1084-
"""
1085-
if python:
1086-
if not (exists(python) and os.access(python, os.X_OK)):
1087-
raise RSConnectException('The file, "%s", does not exist or is not executable.' % python)
1088-
return python
1089-
1090-
return sys.executable
1078+
def which_python(python: typing.Optional[str] = None):
1079+
"""Determines which Python executable to use.
1080+
1081+
If the :param python: is provided, then validation is performed to check if the path is an executable file. If
1082+
None, the invoking system Python executable location is returned.
1083+
1084+
:param python: (Optional) path to a python executable.
1085+
:return: :param python: or `sys.executable`.
1086+
"""
1087+
if python is None:
1088+
return sys.executable
1089+
if not exists(python):
1090+
raise RSConnectException(f"The path '{python}' does not exist. Expected a Python executable.")
1091+
if isdir(python):
1092+
raise RSConnectException(f"The path '{python}' is a directory. Expected a Python executable.")
1093+
if not os.access(python, os.X_OK):
1094+
raise RSConnectException(f"The path '{python}' is not executable. Expected a Python executable")
1095+
return python
10911096

10921097

10931098
def inspect_environment(

tests/test_bundle.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -646,13 +646,6 @@ def test_validate_entry_point(self):
646646
finally:
647647
shutil.rmtree(directory)
648648

649-
def test_which_python(self):
650-
with self.assertRaises(RSConnectException):
651-
which_python("fake.file")
652-
653-
self.assertEqual(which_python(sys.executable), sys.executable)
654-
self.assertEqual(which_python(None), sys.executable)
655-
656649
def test_default_title(self):
657650
self.assertEqual(_default_title("testing.txt"), "testing")
658651
self.assertEqual(_default_title("this.is.a.test.ext"), "this.is.a.test")
@@ -781,3 +774,30 @@ def fake_inspect_environment(
781774

782775
assert python == expected_python
783776
assert environment == expected_environment
777+
778+
779+
class WhichPythonTestCase(TestCase):
780+
def test_default(self):
781+
self.assertEqual(which_python(), sys.executable)
782+
783+
def test_none(self):
784+
self.assertEqual(which_python(None), sys.executable)
785+
786+
def test_sys(self):
787+
self.assertEqual(which_python(sys.executable), sys.executable)
788+
789+
def test_does_not_exist(self):
790+
with tempfile.NamedTemporaryFile() as tmpfile:
791+
name = tmpfile.name
792+
with self.assertRaises(RSConnectException):
793+
which_python(name)
794+
795+
def test_is_directory(self):
796+
with tempfile.TemporaryDirectory() as tmpdir:
797+
with self.assertRaises(RSConnectException):
798+
which_python(tmpdir)
799+
800+
def test_is_not_executable(self):
801+
with tempfile.NamedTemporaryFile() as tmpfile:
802+
with self.assertRaises(RSConnectException):
803+
which_python(tmpfile.name)

0 commit comments

Comments
 (0)