Skip to content

Commit b2f8d87

Browse files
authored
fix: fix default cx deps path for 3.13 (#1833)
* fix default cx deps path for 313 * make log debug * very explicit * fix unit tests * create separate method for adding cx deps to sys path * add in azure env check * lint * lint * fix tests * fix tests
1 parent 7fde853 commit b2f8d87

File tree

4 files changed

+270
-2
lines changed

4 files changed

+270
-2
lines changed

workers/proxy_worker/utils/common.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from typing import Callable, Optional
88

99
from proxy_worker.utils.constants import (
10+
AZURE_CONTAINER_NAME,
11+
AZURE_WEBSITE_INSTANCE_ID,
1012
PYTHON_SCRIPT_FILE_NAME,
1113
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
1214
PYTHON_EOL_DATES,
@@ -115,3 +117,9 @@ def check_python_eol():
115117
logger.warning(f"Python {version} will reach EOL on "
116118
f"{eol_date.strftime('%Y-%m')}. Consider upgrading to "
117119
f"a supported version: aka.ms/supported-python-versions")
120+
121+
122+
def is_azure_environment():
123+
"""Check if the function app is running on the cloud"""
124+
return (AZURE_CONTAINER_NAME in os.environ
125+
or AZURE_WEBSITE_INSTANCE_ID in os.environ)

workers/proxy_worker/utils/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
# Container constants
99
CONTAINER_NAME = "CONTAINER_NAME"
10+
AZURE_WEBSITE_INSTANCE_ID = "WEBSITE_INSTANCE_ID"
11+
AZURE_CONTAINER_NAME = "CONTAINER_NAME"
1012
AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot"
1113

1214
# new programming model default script file name

workers/proxy_worker/utils/dependency.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import List, Optional
88

99
from ..logging import logger
10-
from .common import is_envvar_true
10+
from .common import is_envvar_true, is_azure_environment
1111
from .constants import AZURE_WEBJOBS_SCRIPT_ROOT, CONTAINER_NAME
1212

1313

@@ -136,7 +136,7 @@ def prioritize_customer_dependencies(cls, cx_working_dir=None):
136136

137137
cls._remove_from_sys_path(cls.worker_deps_path)
138138
cls._add_to_sys_path(cls.worker_deps_path, True)
139-
cls._add_to_sys_path(cls.cx_deps_path, True)
139+
cls._add_cx_deps_to_sys_path(cls.cx_deps_path, True)
140140
cls._add_to_sys_path(working_directory, False)
141141

142142
logger.info(
@@ -170,6 +170,45 @@ def _add_to_sys_path(cls, path: str, add_to_first: bool):
170170
# defined in sys.path
171171
cls._clear_path_importer_cache_and_modules(path)
172172

173+
@classmethod
174+
def _add_cx_deps_to_sys_path(cls, path: str, add_to_first: bool):
175+
"""This will ensure no duplicated path are added into sys.path and
176+
clear importer cache. No action if path already exists in sys.path.
177+
178+
Parameters
179+
----------
180+
path: str
181+
The path needs to be added into sys.path.
182+
If the path is an empty string, no action will be taken.
183+
add_to_first: bool
184+
Should the path added to the first entry (highest priority)
185+
"""
186+
187+
# Customer dependencies path has not been identified & app is not in
188+
# Azure environment -> app is running locally with an environment not
189+
# in Function App level
190+
if not path and not is_azure_environment():
191+
default_path = next((p for p in sys.path if 'site-packages' in p), '')
192+
logger.info("No customer dependencies path found, using default: %s",
193+
default_path)
194+
if default_path not in sys.path:
195+
sys.path.insert(0, default_path)
196+
# Don't duplicate paths - move to front without clearing cache
197+
else:
198+
sys.path.remove(default_path)
199+
sys.path.insert(0, default_path)
200+
201+
# Otherwise, continue with normal flow
202+
elif path and path not in sys.path:
203+
if add_to_first:
204+
sys.path.insert(0, path)
205+
else:
206+
sys.path.append(path)
207+
208+
# Only clear path importer and sys.modules cache if path is not
209+
# defined in sys.path
210+
cls._clear_path_importer_cache_and_modules(path)
211+
173212
@classmethod
174213
def _remove_from_sys_path(cls, path: str):
175214
"""This will remove path from sys.path and clear importer cache.

workers/tests/unittest_proxy/test_dependency.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,222 @@ def test_prioritize_customer_dependencies(mock_logger, mock_env, mock_linux,
5858
"Finished prioritize_customer_dependencies" in str(call[0][0])
5959
for call in mock_logger.info.call_args_list
6060
)
61+
62+
63+
@patch.dict(os.environ, {"AzureWebJobsScriptRoot": "/home/site/wwwroot"})
64+
def test_get_cx_deps_path_with_matching_prefix():
65+
"""Test _get_cx_deps_path returns customer path when prefix matches."""
66+
original_sys_path = sys.path.copy()
67+
try:
68+
sys.path = [
69+
"/home/site/wwwroot/.python_packages/lib/site-packages",
70+
"/usr/local/lib/python3.11/site-packages",
71+
"/home/site/wwwroot"
72+
]
73+
result = DependencyManager._get_cx_deps_path()
74+
75+
assert result == "/home/site/wwwroot/.python_packages/lib/site-packages"
76+
finally:
77+
sys.path = original_sys_path
78+
79+
80+
@patch.dict(os.environ, {"AzureWebJobsScriptRoot": "/home/site/wwwroot"})
81+
def test_get_cx_deps_path_no_matching_prefix_returns_empty():
82+
"""Test _get_cx_deps_path returns empty string when no prefix match."""
83+
original_sys_path = sys.path.copy()
84+
try:
85+
sys.path = [
86+
"/usr/local/lib/python3.11/site-packages",
87+
"/some/other/path",
88+
"/home/site/wwwroot"
89+
]
90+
result = DependencyManager._get_cx_deps_path()
91+
92+
# When no cx_paths match, return empty string (not the first site-packages)
93+
assert result == ""
94+
finally:
95+
sys.path = original_sys_path
96+
97+
98+
@patch.dict(os.environ, {}, clear=True)
99+
def test_get_cx_deps_path_no_prefix_env_returns_empty():
100+
"""Test _get_cx_deps_path returns empty string when no env var set."""
101+
original_sys_path = sys.path.copy()
102+
try:
103+
sys.path = [
104+
"/usr/local/lib/python3.11/site-packages",
105+
"/some/other/path"
106+
]
107+
result = DependencyManager._get_cx_deps_path()
108+
109+
# When env var is not set, prefix is None and cx_paths is empty
110+
assert result == ""
111+
finally:
112+
sys.path = original_sys_path
113+
114+
115+
@patch.dict(os.environ, {"AzureWebJobsScriptRoot": "/home/site/wwwroot"})
116+
def test_get_cx_deps_path_no_site_packages_returns_empty():
117+
"""Test _get_cx_deps_path returns empty string when no site-packages found."""
118+
original_sys_path = sys.path.copy()
119+
try:
120+
sys.path = [
121+
"/home/site/wwwroot",
122+
"/some/other/path"
123+
]
124+
result = DependencyManager._get_cx_deps_path()
125+
126+
# When no paths with site-packages match the prefix, return empty string
127+
assert result == ""
128+
finally:
129+
sys.path = original_sys_path
130+
131+
132+
@patch.dict(os.environ, {"AzureWebJobsScriptRoot": "/home/site/wwwroot"})
133+
def test_get_cx_deps_path_multiple_matches_returns_first():
134+
"""Test _get_cx_deps_path returns first match when multiple cx paths exist."""
135+
original_sys_path = sys.path.copy()
136+
try:
137+
sys.path = [
138+
"/home/site/wwwroot/.python_packages/lib/site-packages",
139+
"/home/site/wwwroot/venv/lib/site-packages",
140+
"/usr/local/lib/python3.11/site-packages"
141+
]
142+
result = DependencyManager._get_cx_deps_path()
143+
144+
# When multiple paths match, return the first one
145+
expected_path = "/home/site/wwwroot/.python_packages/lib/site-packages"
146+
assert result == expected_path
147+
finally:
148+
sys.path = original_sys_path
149+
150+
151+
@patch(
152+
"proxy_worker.utils.dependency.DependencyManager."
153+
"_clear_path_importer_cache_and_modules"
154+
)
155+
def test_add_cx_deps_to_sys_path_adds_to_first(mock_clear):
156+
"""Test _add_cx_deps_to_sys_path adds path to first position."""
157+
sys.path = ["/original/path", "/another/path"]
158+
159+
DependencyManager._add_cx_deps_to_sys_path(
160+
"/new/cx/path", add_to_first=True
161+
)
162+
163+
assert sys.path[0] == "/new/cx/path"
164+
assert "/original/path" in sys.path
165+
mock_clear.assert_called_once_with("/new/cx/path")
166+
167+
168+
@patch(
169+
"proxy_worker.utils.dependency.DependencyManager."
170+
"_clear_path_importer_cache_and_modules"
171+
)
172+
def test_add_cx_deps_to_sys_path_appends_to_end(mock_clear):
173+
"""Test _add_cx_deps_to_sys_path appends path to end."""
174+
sys.path = ["/original/path", "/another/path"]
175+
176+
DependencyManager._add_cx_deps_to_sys_path(
177+
"/new/cx/path", add_to_first=False
178+
)
179+
180+
assert sys.path[-1] == "/new/cx/path"
181+
assert sys.path[0] == "/original/path"
182+
mock_clear.assert_called_once_with("/new/cx/path")
183+
184+
185+
def test_add_cx_deps_to_sys_path_no_duplicate():
186+
"""Test _add_cx_deps_to_sys_path does not add duplicate paths."""
187+
sys.path = ["/existing/path", "/another/path"]
188+
original_length = len(sys.path)
189+
190+
DependencyManager._add_cx_deps_to_sys_path(
191+
"/existing/path", add_to_first=True
192+
)
193+
194+
# Path should not be added again
195+
assert len(sys.path) == original_length
196+
assert sys.path.count("/existing/path") == 1
197+
198+
199+
@patch("proxy_worker.utils.dependency.is_azure_environment",
200+
return_value=False)
201+
@patch("proxy_worker.utils.dependency.logger")
202+
def test_add_cx_deps_to_sys_path_empty_path_with_default(
203+
mock_logger, mock_is_azure
204+
):
205+
"""Test _add_cx_deps_to_sys_path uses default when path is empty
206+
in local environment."""
207+
sys.path = ["/usr/local/lib/python3.11/site-packages", "/original/path"]
208+
209+
DependencyManager._add_cx_deps_to_sys_path("", add_to_first=True)
210+
211+
# Should insert the first site-packages path to position 0
212+
assert sys.path[0] == "/usr/local/lib/python3.11/site-packages"
213+
mock_logger.info.assert_called_once_with(
214+
"No customer dependencies path found, using default: %s",
215+
"/usr/local/lib/python3.11/site-packages"
216+
)
217+
mock_is_azure.assert_called_once()
218+
219+
220+
@patch("proxy_worker.utils.dependency.is_azure_environment",
221+
return_value=False)
222+
@patch("proxy_worker.utils.dependency.logger")
223+
def test_add_cx_deps_to_sys_path_empty_path_no_site_packages(
224+
mock_logger, mock_is_azure
225+
):
226+
"""Test _add_cx_deps_to_sys_path handles empty path with no
227+
site-packages in local environment."""
228+
sys.path = ["/some/path", "/another/path"]
229+
230+
DependencyManager._add_cx_deps_to_sys_path("", add_to_first=True)
231+
232+
# Should insert empty string at position 0 when no site-packages found
233+
assert sys.path[0] == ""
234+
mock_logger.info.assert_called_once_with(
235+
"No customer dependencies path found, using default: %s",
236+
""
237+
)
238+
mock_is_azure.assert_called_once()
239+
240+
241+
@patch("proxy_worker.utils.dependency.is_azure_environment",
242+
return_value=True)
243+
@patch("proxy_worker.utils.dependency.logger")
244+
def test_add_cx_deps_to_sys_path_empty_path_in_azure(
245+
mock_logger, mock_is_azure
246+
):
247+
"""Test _add_cx_deps_to_sys_path takes no action when path is empty
248+
in Azure environment."""
249+
sys.path = ["/usr/local/lib/python3.11/site-packages", "/original/path"]
250+
original_sys_path = sys.path.copy()
251+
252+
DependencyManager._add_cx_deps_to_sys_path("", add_to_first=True)
253+
254+
# sys.path should remain unchanged in Azure environment
255+
assert sys.path == original_sys_path
256+
mock_logger.info.assert_not_called()
257+
mock_is_azure.assert_called_once()
258+
259+
260+
@patch("proxy_worker.utils.dependency.is_azure_environment",
261+
return_value=True)
262+
@patch(
263+
"proxy_worker.utils.dependency.DependencyManager."
264+
"_clear_path_importer_cache_and_modules"
265+
)
266+
def test_add_cx_deps_to_sys_path_none_path_no_action(
267+
mock_clear, mock_is_azure
268+
):
269+
"""Test _add_cx_deps_to_sys_path takes no action for None path
270+
in Azure environment."""
271+
sys.path = ["/original/path"]
272+
original_sys_path = sys.path.copy()
273+
274+
DependencyManager._add_cx_deps_to_sys_path(None, add_to_first=True)
275+
276+
# sys.path should remain unchanged
277+
assert sys.path == original_sys_path
278+
mock_clear.assert_not_called()
279+
mock_is_azure.assert_called_once()

0 commit comments

Comments
 (0)