11from azure_functions_worker .utils .common import is_true_like
22from typing import List , Optional
3+ from types import ModuleType
34import importlib
45import inspect
56import os
910from ..logging import logger
1011from ..constants import (
1112 AZURE_WEBJOBS_SCRIPT_ROOT ,
13+ CONTAINER_NAME ,
1214 PYTHON_ISOLATE_WORKER_DEPENDENCIES ,
1315 PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT ,
1416 PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_39
@@ -66,6 +68,10 @@ def initialize(cls):
6668 cls .cx_working_dir = cls ._get_cx_working_dir ()
6769 cls .worker_deps_path = cls ._get_worker_deps_path ()
6870
71+ @classmethod
72+ def is_in_linux_consumption (cls ):
73+ return CONTAINER_NAME in os .environ
74+
6975 @classmethod
7076 @enable_feature_by (
7177 flag = PYTHON_ISOLATE_WORKER_DEPENDENCIES ,
@@ -87,6 +93,11 @@ def use_worker_dependencies(cls):
8793 # The following log line will not show up in core tools but should
8894 # work in kusto since core tools only collects gRPC logs. This function
8995 # is executed even before the gRPC logging channel is ready.
96+ logger .info (f'Applying use_worker_dependencies:'
97+ f' worker_dependencies: { cls .worker_deps_path } ,'
98+ f' customer_dependencies: { cls .cx_deps_path } ,'
99+ f' working_directory: { cls .cx_working_dir } ' )
100+
90101 cls ._remove_from_sys_path (cls .cx_deps_path )
91102 cls ._remove_from_sys_path (cls .cx_working_dir )
92103 cls ._add_to_sys_path (cls .worker_deps_path , True )
@@ -101,7 +112,7 @@ def use_worker_dependencies(cls):
101112 PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT
102113 )
103114 )
104- def prioritize_customer_dependencies (cls ):
115+ def prioritize_customer_dependencies (cls , cx_working_dir = None ):
105116 """Switch the sys.path and ensure the customer's code import are loaded
106117 from CX's deppendencies.
107118
@@ -112,24 +123,50 @@ def prioritize_customer_dependencies(cls):
112123 As for Linux Consumption, this will only remove worker_deps_path,
113124 but the customer's path will be loaded in function_environment_reload.
114125
115- The search order of a module name in customer frame is:
126+ The search order of a module name in customer's paths is:
116127 1. cx_deps_path
117- 2. cx_working_dir
128+ 2. worker_deps_path
129+ 3. cx_working_dir
118130 """
131+ # Try to get the latest customer's working directory
132+ # cx_working_dir => cls.cx_working_dir => AzureWebJobsScriptRoot
133+ working_directory : str = ''
134+ if cx_working_dir :
135+ working_directory : str = os .path .abspath (cx_working_dir )
136+ if not working_directory :
137+ working_directory = cls .cx_working_dir
138+ if not working_directory :
139+ working_directory = os .getenv (AZURE_WEBJOBS_SCRIPT_ROOT , '' )
140+
141+ # Try to get the latest customer's dependency path
142+ cx_deps_path : str = cls ._get_cx_deps_path ()
143+ if not cx_deps_path :
144+ cx_deps_path = cls .cx_deps_path
145+
146+ logger .info ('Applying prioritize_customer_dependencies:'
147+ f' worker_dependencies: { cls .worker_deps_path } ,'
148+ f' customer_dependencies: { cx_deps_path } ,'
149+ f' working_directory: { working_directory } ' )
150+
119151 cls ._remove_from_sys_path (cls .worker_deps_path )
120152 cls ._add_to_sys_path (cls .cx_deps_path , True )
121- cls ._add_to_sys_path (cls .cx_working_dir , False )
122153
123154 # Deprioritize worker dependencies but don't completely remove it
124155 # Otherwise, it will break some really old function apps, those
125156 # don't have azure-functions module in .python_packages
126157 # https://github.com/Azure/azure-functions-core-tools/pull/1498
127158 cls ._add_to_sys_path (cls .worker_deps_path , False )
128159
129- logger .info (f'Start using customer dependencies { cls .cx_deps_path } ' )
160+ # The modules defined in customer's working directory should have the
161+ # least priority since we uses the new folder structure.
162+ # Please check the "Message to customer" section in the following PR:
163+ # https://github.com/Azure/azure-functions-python-worker/pull/726
164+ cls ._add_to_sys_path (working_directory , False )
165+
166+ logger .info ('Finished prioritize_customer_dependencies' )
130167
131168 @classmethod
132- def reload_azure_google_namespace (cls , cx_working_dir : str ):
169+ def reload_customer_libraries (cls , cx_working_dir : str ):
133170 """Reload azure and google namespace, this including any modules in
134171 this namespace, such as azure-functions, grpcio, grpcio-tools etc.
135172
@@ -152,7 +189,7 @@ def reload_azure_google_namespace(cls, cx_working_dir: str):
152189 use_new = is_true_like (use_new_env )
153190
154191 if use_new :
155- cls .reload_all_namespaces_from_customer_deps (cx_working_dir )
192+ cls .prioritize_customer_dependencies (cx_working_dir )
156193 else :
157194 cls .reload_azure_google_namespace_from_worker_deps ()
158195
@@ -189,33 +226,6 @@ def reload_azure_google_namespace_from_worker_deps(cls):
189226 logger .info ('Unable to reload azure.functions. '
190227 'Using default. Exception:\n {}' .format (ex ))
191228
192- @classmethod
193- def reload_all_namespaces_from_customer_deps (cls , cx_working_dir : str ):
194- """This is a new implementation of reloading azure and google
195- namespace from customer's .python_packages folder. Only intended to be
196- used in Linux Consumption scenario.
197-
198- Parameters
199- ----------
200- cx_working_dir: str
201- The path which contains customer's project file (e.g. wwwroot).
202- """
203- # Specialized working directory needs to be added
204- working_directory : str = os .path .abspath (cx_working_dir )
205-
206- # Switch to customer deps and clear out all module cache in worker deps
207- cls ._remove_from_sys_path (cls .worker_deps_path )
208- cls ._add_to_sys_path (cls .cx_deps_path , True )
209- cls ._add_to_sys_path (working_directory , False )
210-
211- # Deprioritize worker dependencies but don't completely remove it
212- # Otherwise, it will break some really old function apps, those
213- # don't have azure-functions module in .python_packages
214- # https://github.com/Azure/azure-functions-core-tools/pull/1498
215- cls ._add_to_sys_path (cls .worker_deps_path , False )
216-
217- logger .info ('Reloaded all namespaces from customer dependencies' )
218-
219229 @classmethod
220230 def _add_to_sys_path (cls , path : str , add_to_first : bool ):
221231 """This will ensure no duplicated path are added into sys.path and
@@ -325,7 +335,18 @@ def _get_worker_deps_path() -> str:
325335 if worker_deps_paths :
326336 return worker_deps_paths [0 ]
327337
328- # 2. If it fails to find one, try to find one from the parent path
338+ # 2. Try to find module spec of azure.functions without actually
339+ # importing it (e.g. lib/site-packages/azure/functions/__init__.py)
340+ try :
341+ azf_spec = importlib .util .find_spec ('azure.functions' )
342+ if azf_spec and azf_spec .origin :
343+ return os .path .abspath (
344+ os .path .join (os .path .dirname (azf_spec .origin ), '..' , '..' )
345+ )
346+ except ModuleNotFoundError :
347+ logger .warning ('Cannot locate built-in azure.functions module' )
348+
349+ # 3. If it fails to find one, try to find one from the parent path
329350 # This is used for handling the CI/localdev environment
330351 return os .path .abspath (
331352 os .path .join (os .path .dirname (__file__ ), '..' , '..' )
@@ -341,18 +362,34 @@ def _remove_module_cache(path: str):
341362 path: str
342363 The module cache to be removed if it is imported from this path.
343364 """
344- all_modules = set (sys .modules .keys ()) - set (sys .builtin_module_names )
345- for module_name in all_modules :
346- module = sys .modules [module_name ]
347- # Module path can be actual file path or a pure namespace path
348- # For actual files: use __file__ attribute to retrieve module path
349- # For namespace: use __path__[0] to retrieve module path
350- module_path = ''
351- if getattr (module , '__file__' , None ):
352- module_path = os .path .dirname (module .__file__ )
353- elif getattr (module , '__path__' , None ) and getattr (
354- module .__path__ , '_path' , None ):
355- module_path = module .__path__ ._path [0 ]
356-
357- if module_path .startswith (path ):
358- sys .modules .pop (module_name )
365+ if not path :
366+ return
367+
368+ not_builtin = set (sys .modules .keys ()) - set (sys .builtin_module_names )
369+
370+ # Don't reload azure_functions_worker
371+ to_be_cleared_from_cache = set ([
372+ module_name for module_name in not_builtin
373+ if not module_name .startswith ('azure_functions_worker' )
374+ ])
375+
376+ for module_name in to_be_cleared_from_cache :
377+ module = sys .modules .get (module_name )
378+ if not isinstance (module , ModuleType ):
379+ continue
380+
381+ # Module path can be actual file path or a pure namespace path.
382+ # Both of these has the module path placed in __path__ property
383+ # The property .__path__ can be None or does not exist in module
384+ try :
385+ module_paths = set (getattr (module , '__path__' , None ) or [])
386+ if hasattr (module , '__file__' ) and module .__file__ :
387+ module_paths .add (module .__file__ )
388+
389+ if any ([p for p in module_paths if p .startswith (path )]):
390+ sys .modules .pop (module_name )
391+ except Exception as e :
392+ logger .warning (
393+ f'Attempt to remove module cache for { module_name } but'
394+ f' failed with { e } . Using the original module cache.'
395+ )
0 commit comments