Repository: StackStorm/st2

Issue:
Packs that have actions in sub-directories importing modules in same sub-directory fail
## SUMMARY

I have observed in a couple of packs when using StackStorm 3.4dev that they now get ModuleNotFoundError when the pack has a module actions/lib/moduleA.py and that moduleA.py has an import moduleB where moduleB is in the same library.
These packs were working in previous versions.
Observed in both ActiveDirectory and Excel packs.

### STACKSTORM VERSION

st2 3.4dev (c422f0029), on Python 3.6.8

##### OS, environment, install method

CentOS 8.2, single line bash installer

## Steps to reproduce the problem

Observed with following actions:

- activedirectory.get_ad_user
- excel.get_keys_for_rows

## Expected Results

Actions to work as before and not throw ModuleNotFoundError

## Actual Results

Excel Stack Trace:
```
{
  "stdout": "",
  "stderr": "Traceback (most recent call last):
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 238, in _get_action_instance
    actions_cls = action_loader.register_plugin(Action, self._file_path)
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/st2common/util/loader.py\", line 166, in register_plugin
    module = imp.load_source(module_name, plugin_abs_file_path)
  File \"/opt/stackstorm/virtualenvs/excel/lib64/python3.6/imp.py\", line 172, in load_source
    module = _load(spec)
  File \"<frozen importlib._bootstrap>\", line 684, in _load
  File \"<frozen importlib._bootstrap>\", line 665, in _load_unlocked
  File \"<frozen importlib._bootstrap_external>\", line 678, in exec_module
  File \"<frozen importlib._bootstrap>\", line 219, in _call_with_frames_removed
  File \"/opt/stackstorm/packs/excel/actions/get_keys_for_rows.py\", line 14, in <module>
    from lib import excel_action, excel_reader
  File \"/opt/stackstorm/packs/excel/actions/lib/excel_reader.py\", line 16, in <module>
    import string_converter
ModuleNotFoundError: No module named 'string_converter'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 334, in <module>
    obj.run()
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 192, in run
    action = self._get_action_instance()
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 245, in _get_action_instance
    raise exc_cls(msg)
ModuleNotFoundError: Failed to load action class from file \"/opt/stackstorm/packs/excel/actions/get_keys_for_rows.py\" (action file most likely doesn't exist or contains invalid syntax): No module named 'string_converter'

Traceback (most recent call last):
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 238, in _get_action_instance
    actions_cls = action_loader.register_plugin(Action, self._file_path)
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/st2common/util/loader.py\", line 166, in register_plugin
    module = imp.load_source(module_name, plugin_abs_file_path)
  File \"/opt/stackstorm/virtualenvs/excel/lib64/python3.6/imp.py\", line 172, in load_source
    module = _load(spec)
  File \"<frozen importlib._bootstrap>\", line 684, in _load
  File \"<frozen importlib._bootstrap>\", line 665, in _load_unlocked
  File \"<frozen importlib._bootstrap_external>\", line 678, in exec_module
  File \"<frozen importlib._bootstrap>\", line 219, in _call_with_frames_removed
  File \"/opt/stackstorm/packs/excel/actions/get_keys_for_rows.py\", line 14, in <module>
    from lib import excel_action, excel_reader
  File \"/opt/stackstorm/packs/excel/actions/lib/excel_reader.py\", line 16, in <module>
    import string_converter
ModuleNotFoundError: No module named 'string_converter'

",
  "exit_code": 1,
  "result": "None"
}
```

Active Directory StackTrace:
```
{
  "stdout": "",
  "stderr": "Traceback (most recent call last):
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 238, in _get_action_instance
    actions_cls = action_loader.register_plugin(Action, self._file_path)
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/st2common/util/loader.py\", line 166, in register_plugin
    module = imp.load_source(module_name, plugin_abs_file_path)
  File \"/opt/stackstorm/virtualenvs/activedirectory/lib64/python3.6/imp.py\", line 172, in load_source
    module = _load(spec)
  File \"<frozen importlib._bootstrap>\", line 684, in _load
  File \"<frozen importlib._bootstrap>\", line 665, in _load_unlocked
  File \"<frozen importlib._bootstrap_external>\", line 678, in exec_module
  File \"<frozen importlib._bootstrap>\", line 219, in _call_with_frames_removed
  File \"/opt/stackstorm/packs/activedirectory/actions/run_cmdlet.py\", line 1, in <module>
    from lib import action
  File \"/opt/stackstorm/packs/activedirectory/actions/lib/action.py\", line 1, in <module>
    from winrm_connection import WinRmConnection
ModuleNotFoundError: No module named 'winrm_connection'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 334, in <module>
    obj.run()
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 192, in run
    action = self._get_action_instance()
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 245, in _get_action_instance
    raise exc_cls(msg)
ModuleNotFoundError: Failed to load action class from file \"/opt/stackstorm/packs/activedirectory/actions/run_cmdlet.py\" (action file most likely doesn't exist or contains invalid syntax): No module named 'winrm_connection'

Traceback (most recent call last):
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py\", line 238, in _get_action_instance
    actions_cls = action_loader.register_plugin(Action, self._file_path)
  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/st2common/util/loader.py\", line 166, in register_plugin
    module = imp.load_source(module_name, plugin_abs_file_path)
  File \"/opt/stackstorm/virtualenvs/activedirectory/lib64/python3.6/imp.py\", line 172, in load_source
    module = _load(spec)
  File \"<frozen importlib._bootstrap>\", line 684, in _load
  File \"<frozen importlib._bootstrap>\", line 665, in _load_unlocked
  File \"<frozen importlib._bootstrap_external>\", line 678, in exec_module
  File \"<frozen importlib._bootstrap>\", line 219, in _call_with_frames_removed
  File \"/opt/stackstorm/packs/activedirectory/actions/run_cmdlet.py\", line 1, in <module>
    from lib import action
  File \"/opt/stackstorm/packs/activedirectory/actions/lib/action.py\", line 1, in <module>
    from winrm_connection import WinRmConnection
ModuleNotFoundError: No module named 'winrm_connection'

",
  "exit_code": 1,
  "result": "None"
}
```

Workarounds were possible to get this to work, e.g. change the ActiveDirectory to use "from .winrm_connection import WinRm"



Discussion:
1. Create fresh CentOS 8.2 VM and install STackStorm 3.3. Install excel pack. Put in place blank spreadsheet. Run excel.get_keys_for_rows -> all ok
2. Create fresh CentOS 8.2 VM and install StackStorm unstable. Repeat above -> get the import error. (So reproduced it on 2 different CentOS 8.2 VMs now - just in case I was going mad!)

e.g. just run:
`st2 run excel.get_keys_for_rows sheet=<sheetname> excel_file=<spreadsheet>`

In fact don't even need to have a valid spreadsheet e.g.
```
$ st2 run excel.get_keys_for_rows sheet=rubbish excel_file=/opt/stackstorm/rubbish.xlsx
.
id: 601055e7509b8ab7a3c1b0d7
action.ref: excel.get_keys_for_rows
context.user: st2admin
parameters: 
  excel_file: /opt/stackstorm/rubbish.xlsx
  sheet: rubbish
status: failed
start_timestamp: Tue, 26 Jan 2021 17:48:23 UTC
end_timestamp: Tue, 26 Jan 2021 17:48:24 UTC
result: 
  exit_code: 1
  result: None
  stderr: "Traceback (most recent call last):
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py", line 238, in _get_action_instance
    actions_cls = action_loader.register_plugin(Action, self._file_path)
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/st2common/util/loader.py", line 166, in register_plugin
    module = imp.load_source(module_name, plugin_abs_file_path)
  File "/opt/stackstorm/virtualenvs/excel/lib64/python3.6/imp.py", line 172, in load_source
    module = _load(spec)
  File "<frozen importlib._bootstrap>", line 684, in _load
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/opt/stackstorm/packs/excel/actions/get_keys_for_rows.py", line 14, in <module>
    from lib import excel_action, excel_reader
  File "/opt/stackstorm/packs/excel/actions/lib/excel_reader.py", line 16, in <module>
    import string_converter
ModuleNotFoundError: No module named 'string_converter'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py", line 334, in <module>
    obj.run()
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py", line 192, in run
    action = self._get_action_instance()
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py", line 245, in _get_action_instance
    raise exc_cls(msg)
ModuleNotFoundError: Failed to load action class from file "/opt/stackstorm/packs/excel/actions/get_keys_for_rows.py" (action file most likely doesn't exist or contains invalid syntax): No module named 'string_converter'

Traceback (most recent call last):
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/python_runner/python_action_wrapper.py", line 238, in _get_action_instance
    actions_cls = action_loader.register_plugin(Action, self._file_path)
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/st2common/util/loader.py", line 166, in register_plugin
    module = imp.load_source(module_name, plugin_abs_file_path)
  File "/opt/stackstorm/virtualenvs/excel/lib64/python3.6/imp.py", line 172, in load_source
    module = _load(spec)
  File "<frozen importlib._bootstrap>", line 684, in _load
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/opt/stackstorm/packs/excel/actions/get_keys_for_rows.py", line 14, in <module>
    from lib import excel_action, excel_reader
  File "/opt/stackstorm/packs/excel/actions/lib/excel_reader.py", line 16, in <module>
    import string_converter
ModuleNotFoundError: No module named 'string_converter'

"
  stdout: ''
```
I believe that this is related to the `PYTHONPATH` changes in #5100. I'm working on a fix for this now.

Resolving patch:
diff --git a/st2common/st2common/models/db/execution.py b/st2common/st2common/models/db/execution.py
--- a/st2common/st2common/models/db/execution.py
+++ b/st2common/st2common/models/db/execution.py
@@ -97,7 +97,7 @@ class ActionExecutionDB(stormbase.StormFoundationDB):
     }
 
     def get_uid(self):
-        # TODO Construct od from non id field:
+        # TODO Construct id from non id field:
         uid = [self.RESOURCE_TYPE, str(self.id)]  # pylint: disable=no-member
         return ':'.join(uid)
 
diff --git a/st2common/st2common/util/sandboxing.py b/st2common/st2common/util/sandboxing.py
--- a/st2common/st2common/util/sandboxing.py
+++ b/st2common/st2common/util/sandboxing.py
@@ -20,6 +20,7 @@
 
 from __future__ import absolute_import
 
+import fnmatch
 import os
 import sys
 from distutils.sysconfig import get_python_lib
@@ -27,6 +28,7 @@
 from oslo_config import cfg
 
 from st2common.constants.pack import SYSTEM_PACK_NAMES
+from st2common.content.utils import get_pack_base_path
 
 __all__ = [
     'get_sandbox_python_binary_path',
@@ -132,10 +134,45 @@ def get_sandbox_python_path_for_python_action(pack, inherit_from_parent=True,
     Same as get_sandbox_python_path() function, but it's intended to be used for Python runner
     actions.
     """
-    return get_sandbox_python_path(
+    sandbox_python_path = get_sandbox_python_path(
         inherit_from_parent=inherit_from_parent,
         inherit_parent_virtualenv=inherit_parent_virtualenv)
 
+    pack_base_path = get_pack_base_path(pack_name=pack)
+    virtualenv_path = get_sandbox_virtualenv_path(pack=pack)
+
+    if virtualenv_path and os.path.isdir(virtualenv_path):
+        pack_virtualenv_lib_path = os.path.join(virtualenv_path, 'lib')
+
+        virtualenv_directories = os.listdir(pack_virtualenv_lib_path)
+        virtualenv_directories = [dir_name for dir_name in virtualenv_directories if
+                                  fnmatch.fnmatch(dir_name, 'python*')]
+
+        # Add the pack's lib directory (lib/python3.x) in front of the PYTHONPATH.
+        pack_actions_lib_paths = os.path.join(pack_base_path, 'actions', 'lib')
+        pack_virtualenv_lib_path = os.path.join(virtualenv_path, 'lib')
+        pack_venv_lib_directory = os.path.join(pack_virtualenv_lib_path, virtualenv_directories[0])
+
+        # Add the pack's site-packages directory (lib/python3.x/site-packages)
+        # in front of the Python system site-packages This is important because
+        # we want Python 3 compatible libraries to be used from the pack virtual
+        # environment and not system ones.
+        pack_venv_site_packages_directory = os.path.join(pack_virtualenv_lib_path,
+                                                         virtualenv_directories[0],
+                                                         'site-packages')
+
+        full_sandbox_python_path = [
+            # NOTE: Order here is very important for imports to function correctly
+            pack_venv_lib_directory,
+            pack_venv_site_packages_directory,
+            pack_actions_lib_paths,
+            sandbox_python_path,
+        ]
+
+        sandbox_python_path = ':'.join(full_sandbox_python_path)
+
+    return sandbox_python_path
+
 
 def get_sandbox_virtualenv_path(pack):
     """


Test patch:
diff --git a/st2common/tests/unit/test_util_sandboxing.py b/st2common/tests/unit/test_util_sandboxing.py
--- a/st2common/tests/unit/test_util_sandboxing.py
+++ b/st2common/tests/unit/test_util_sandboxing.py
@@ -38,12 +38,12 @@
 
 
 class SandboxingUtilsTestCase(unittest.TestCase):
+    maxDiff = None
+
     def setUp(self):
         super(SandboxingUtilsTestCase, self).setUp()
 
-        # Restore PATH and other variables before each test case
-        os.environ['PATH'] = self.old_path
-        os.environ['PYTHONPATH'] = self.old_python_path
+        # Restore the virtualenv before each test case
         set_virtualenv_prefix(self.old_virtualenv_prefix)
 
     @classmethod
@@ -51,16 +51,22 @@ def setUpClass(cls):
         tests_config.parse_args()
 
         # Store original values so we can restore them in setUp
-        cls.old_path = os.environ.get('PATH', '')
-        cls.old_python_path = os.environ.get('PYTHONPATH', '')
         cls.old_virtualenv_prefix = get_virtualenv_prefix()
 
     @classmethod
     def tearDownClass(cls):
-        os.environ['PATH'] = cls.old_path
-        os.environ['PYTHONPATH'] = cls.old_python_path
         set_virtualenv_prefix(cls.old_virtualenv_prefix)
 
+    def assertEndsWith(self, string, ending_substr, msg=None):
+        msg = msg or "'{string}'' does not end with '{ending_substr}'"
+        try:
+            assert string.endswith(ending_substr) is True
+        except AssertionError as e:
+            print(dir(e))
+            print(e.args)
+            e.args = (msg.format(string=string, ending_substr=ending_substr),)
+            raise e
+
     def test_get_sandbox_python_binary_path(self):
         # Non-system content pack, should use pack specific virtualenv binary
         result = get_sandbox_python_binary_path(pack='mapack')
@@ -72,12 +78,13 @@ def test_get_sandbox_python_binary_path(self):
         self.assertEqual(result, sys.executable)
 
     def test_get_sandbox_path(self):
+        virtualenv_path = '/home/venv/test'
+
         # Mock the current PATH value
-        os.environ['PATH'] = '/home/path1:/home/path2:/home/path3:'
+        with mock.patch.dict(os.environ, {'PATH': '/home/path1:/home/path2:/home/path3:'}):
+            result = get_sandbox_path(virtualenv_path=virtualenv_path)
 
-        virtualenv_path = '/home/venv/test'
-        result = get_sandbox_path(virtualenv_path=virtualenv_path)
-        self.assertEqual(result, '/home/venv/test/bin/:/home/path1:/home/path2:/home/path3')
+        self.assertEqual(result, f'{virtualenv_path}/bin/:/home/path1:/home/path2:/home/path3')
 
     @mock.patch('st2common.util.sandboxing.get_python_lib')
     def test_get_sandbox_python_path(self, mock_get_python_lib):
@@ -88,31 +95,35 @@ def test_get_sandbox_python_path(self, mock_get_python_lib):
 
         # Inherit python path from current process
         # Mock the current process python path
-        os.environ['PYTHONPATH'] = ':/data/test1:/data/test2'
+        with mock.patch.dict(os.environ, {'PYTHONPATH': ':/data/test1:/data/test2'}):
+            python_path = get_sandbox_python_path(inherit_from_parent=True,
+                                                  inherit_parent_virtualenv=False)
 
-        python_path = get_sandbox_python_path(inherit_from_parent=True,
-                                              inherit_parent_virtualenv=False)
         self.assertEqual(python_path, ':/data/test1:/data/test2')
 
         # Inherit from current process and from virtualenv (not running inside virtualenv)
         clear_virtualenv_prefix()
 
-        python_path = get_sandbox_python_path(inherit_from_parent=True,
-                                              inherit_parent_virtualenv=False)
+        with mock.patch.dict(os.environ, {'PYTHONPATH': ':/data/test1:/data/test2'}):
+            python_path = get_sandbox_python_path(inherit_from_parent=True,
+                                                  inherit_parent_virtualenv=False)
+
         self.assertEqual(python_path, ':/data/test1:/data/test2')
 
         # Inherit from current process and from virtualenv (running inside virtualenv)
         sys.real_prefix = '/usr'
-        mock_get_python_lib.return_value = sys.prefix + '/virtualenvtest'
-        python_path = get_sandbox_python_path(inherit_from_parent=True,
-                                              inherit_parent_virtualenv=True)
-        self.assertEqual(python_path, ':/data/test1:/data/test2:%s/virtualenvtest' %
-                         (sys.prefix))
+        mock_get_python_lib.return_value = f'{sys.prefix}/virtualenvtest'
+
+        with mock.patch.dict(os.environ, {'PYTHONPATH': ':/data/test1:/data/test2'}):
+            python_path = get_sandbox_python_path(inherit_from_parent=True,
+                                                  inherit_parent_virtualenv=True)
+
+        self.assertEqual(python_path, f':/data/test1:/data/test2:{sys.prefix}/virtualenvtest')
 
     @mock.patch('os.path.isdir', mock.Mock(return_value=True))
-    @mock.patch('os.listdir', mock.Mock(return_value=['python2.7']))
+    @mock.patch('os.listdir', mock.Mock(return_value=['python3.6']))
     @mock.patch('st2common.util.sandboxing.get_python_lib')
-    def test_get_sandbox_python_path_for_python_action_python2_used_for_venv(self,
+    def test_get_sandbox_python_path_for_python_action_no_inheritance(self,
             mock_get_python_lib):
 
         # No inheritance
@@ -120,27 +131,106 @@ def test_get_sandbox_python_path_for_python_action_python2_used_for_venv(self,
                                                                 inherit_from_parent=False,
                                                                 inherit_parent_virtualenv=False)
 
-        self.assertEqual(python_path, ':')
+        actual_path = python_path.strip(':').split(':')
+        self.assertEqual(len(actual_path), 3)
+
+        # First entry should be lib/python3 dir from venv
+        self.assertEndsWith(actual_path[0], 'virtualenvs/dummy_pack/lib/python3.6')
+        # Second entry should be python3 site-packages dir from venv
+        self.assertEndsWith(actual_path[1], 'virtualenvs/dummy_pack/lib/python3.6/site-packages')
+        # Third entry should be actions/lib dir from pack root directory
+        self.assertEndsWith(actual_path[2], 'packs/dummy_pack/actions/lib')
+
+    @mock.patch('os.path.isdir', mock.Mock(return_value=True))
+    @mock.patch('os.listdir', mock.Mock(return_value=['python3.6']))
+    @mock.patch('st2common.util.sandboxing.get_python_lib')
+    def test_get_sandbox_python_path_for_python_action_inherit_from_parent_process_only(self,
+            mock_get_python_lib):
 
         # Inherit python path from current process
         # Mock the current process python path
-        os.environ['PYTHONPATH'] = ':/data/test1:/data/test2'
+        with mock.patch.dict(os.environ, {'PYTHONPATH': ':/data/test1:/data/test2'}):
+            python_path = get_sandbox_python_path(inherit_from_parent=True,
+                                                  inherit_parent_virtualenv=False)
+
+            self.assertEqual(python_path, ':/data/test1:/data/test2')
+
+            python_path = get_sandbox_python_path_for_python_action(pack='dummy_pack',
+                                                                    inherit_from_parent=True,
+                                                                    inherit_parent_virtualenv=False)
+
+        actual_path = python_path.strip(':').split(':')
+        self.assertEqual(len(actual_path), 6)
+
+        # First entry should be lib/python3 dir from venv
+        self.assertEndsWith(actual_path[0], 'virtualenvs/dummy_pack/lib/python3.6')
+        # Second entry should be python3 site-packages dir from venv
+        self.assertEndsWith(actual_path[1], 'virtualenvs/dummy_pack/lib/python3.6/site-packages')
+        # Third entry should be actions/lib dir from pack root directory
+        self.assertEndsWith(actual_path[2], 'packs/dummy_pack/actions/lib')
+        # And the rest of the paths from get_sandbox_python_path
+        self.assertEqual(actual_path[3], '')
+        self.assertEqual(actual_path[4], '/data/test1')
+        self.assertEqual(actual_path[5], '/data/test2')
 
-        python_path = get_sandbox_python_path(inherit_from_parent=True,
-                                              inherit_parent_virtualenv=False)
-        self.assertEqual(python_path, ':/data/test1:/data/test2')
+    @mock.patch('os.path.isdir', mock.Mock(return_value=True))
+    @mock.patch('os.listdir', mock.Mock(return_value=['python3.6']))
+    @mock.patch('st2common.util.sandboxing.get_python_lib')
+    def test_get_sandbox_python_path_for_python_action_inherit_from_parent_process_and_venv(self,
+            mock_get_python_lib):
 
         # Inherit from current process and from virtualenv (not running inside virtualenv)
         clear_virtualenv_prefix()
 
-        python_path = get_sandbox_python_path(inherit_from_parent=True,
-                                              inherit_parent_virtualenv=False)
-        self.assertEqual(python_path, ':/data/test1:/data/test2')
+        # Inherit python path from current process
+        # Mock the current process python path
+        with mock.patch.dict(os.environ, {'PYTHONPATH': ':/data/test1:/data/test2'}):
+            python_path = get_sandbox_python_path(inherit_from_parent=True,
+                                                  inherit_parent_virtualenv=False)
+
+            self.assertEqual(python_path, ':/data/test1:/data/test2')
+
+            python_path = get_sandbox_python_path_for_python_action(pack='dummy_pack',
+                                                                    inherit_from_parent=True,
+                                                                    inherit_parent_virtualenv=True)
+
+        actual_path = python_path.strip(':').split(':')
+        self.assertEqual(len(actual_path), 6)
+
+        # First entry should be lib/python3 dir from venv
+        self.assertEndsWith(actual_path[0], 'virtualenvs/dummy_pack/lib/python3.6')
+        # Second entry should be python3 site-packages dir from venv
+        self.assertEndsWith(actual_path[1], 'virtualenvs/dummy_pack/lib/python3.6/site-packages')
+        # Third entry should be actions/lib dir from pack root directory
+        self.assertEndsWith(actual_path[2], 'packs/dummy_pack/actions/lib')
+        # And the rest of the paths from get_sandbox_python_path
+        self.assertEqual(actual_path[3], '')
+        self.assertEqual(actual_path[4], '/data/test1')
+        self.assertEqual(actual_path[5], '/data/test2')
 
         # Inherit from current process and from virtualenv (running inside virtualenv)
         sys.real_prefix = '/usr'
-        mock_get_python_lib.return_value = sys.prefix + '/virtualenvtest'
-        python_path = get_sandbox_python_path(inherit_from_parent=True,
-                                              inherit_parent_virtualenv=True)
-        self.assertEqual(python_path, ':/data/test1:/data/test2:%s/virtualenvtest' %
-                         (sys.prefix))
+        mock_get_python_lib.return_value = f'{sys.prefix}/virtualenvtest'
+
+        # Inherit python path from current process
+        # Mock the current process python path
+        with mock.patch.dict(os.environ, {'PYTHONPATH': ':/data/test1:/data/test2'}):
+            python_path = get_sandbox_python_path_for_python_action(pack='dummy_pack',
+                                                                    inherit_from_parent=True,
+                                                                    inherit_parent_virtualenv=True)
+
+        actual_path = python_path.strip(':').split(':')
+        self.assertEqual(len(actual_path), 7)
+
+        # First entry should be lib/python3 dir from venv
+        self.assertEndsWith(actual_path[0], 'virtualenvs/dummy_pack/lib/python3.6')
+        # Second entry should be python3 site-packages dir from venv
+        self.assertEndsWith(actual_path[1], 'virtualenvs/dummy_pack/lib/python3.6/site-packages')
+        # Third entry should be actions/lib dir from pack root directory
+        self.assertEndsWith(actual_path[2], 'packs/dummy_pack/actions/lib')
+        # The paths from get_sandbox_python_path
+        self.assertEqual(actual_path[3], '')
+        self.assertEqual(actual_path[4], '/data/test1')
+        self.assertEqual(actual_path[5], '/data/test2')
+        # And the parent virtualenv
+        self.assertEqual(actual_path[6], f'{sys.prefix}/virtualenvtest')