1515class GlobalContext :
1616 """Define class for global variables and trigger context."""
1717
18- def __init__ (self , name , global_sym_table = None , manager = None ):
18+ def __init__ (self , name , global_sym_table = None , manager = None , rel_import_path = None ):
1919 """Initialize GlobalContext."""
2020 self .name = name
2121 self .global_sym_table = global_sym_table if global_sym_table else {}
@@ -25,6 +25,7 @@ def __init__(self, name, global_sym_table=None, manager=None):
2525 self .manager = manager
2626 self .auto_start = False
2727 self .module = None
28+ self .rel_import_path = rel_import_path
2829
2930 def trigger_register (self , func ):
3031 """Register a trigger function; return True if start now."""
@@ -50,11 +51,12 @@ def start(self):
5051 self .triggers_delay_start = set ()
5152
5253 def stop (self ):
53- """Stop all triggers."""
54+ """Stop all triggers and auto_start ."""
5455 for func in self .triggers :
5556 func .trigger_stop ()
5657 self .triggers = set ()
5758 self .triggers_delay_start = set ()
59+ self .set_auto_start (False )
5860
5961 def get_name (self ):
6062 """Return the global context name."""
@@ -68,27 +70,97 @@ def get_trig_info(self, name, trig_args):
6870 """Return a new trigger info instance with the given args."""
6971 return TrigInfo (name , trig_args , self )
7072
71- async def module_import (self , module_name ):
72- """Import a module from the pyscript/modules folder."""
73- mod_ctx_name = f"module.{ module_name } "
74- mod_ctx = self .manager .get (mod_ctx_name )
75- if not mod_ctx :
76-
77- def check_isfile (filepath ):
78- return os .path .isfile (filepath )
79-
80- filepath = Function .hass .config .path (FOLDER ) + f"/modules/{ module_name } .py"
81- if await Function .hass .async_add_executor_job (check_isfile , filepath ):
82- mod = ModuleType (module_name )
83- global_ctx = GlobalContext (mod_ctx_name , global_sym_table = mod .__dict__ , manager = self .manager )
84- global_ctx .set_auto_start (True )
85- await self .manager .load_file (filepath , global_ctx )
86- global_ctx .module = mod
87- mod_ctx = self .manager .get (mod_ctx_name )
88-
89- if mod_ctx and mod_ctx .module :
90- return mod_ctx .module
91- return None
73+ async def module_import (self , module_name , import_level ):
74+ """Import a pyscript module from the pyscript/modules or apps folder."""
75+
76+ pyscript_dir = Function .hass .config .path (FOLDER )
77+ module_path = module_name .replace ("." , "/" )
78+ filepaths = []
79+
80+ def find_first_file (filepaths ):
81+ for ctx_name , path , rel_path in filepaths :
82+ abs_path = os .path .join (pyscript_dir , path )
83+ if os .path .isfile (abs_path ):
84+ return [ctx_name , abs_path , rel_path ]
85+ return None
86+
87+ #
88+ # first build a list of potential import files
89+ #
90+ if import_level > 0 :
91+ if self .rel_import_path is None :
92+ raise ImportError ("attempted relative import with no known parent package" )
93+ path = self .rel_import_path
94+ ctx_name = self .name
95+ for _ in range (import_level - 1 ):
96+ path = os .path .basename (path )
97+ idx = ctx_name .rfind ("." )
98+ if path .find ("/" ) < 0 or idx < 0 :
99+ raise ImportError ("attempted relative import above parent package" )
100+ ctx_name = ctx_name [0 :idx ]
101+ idx = ctx_name .rfind ("." )
102+ if idx >= 0 :
103+ ctx_name = f"{ ctx_name [0 :idx ]} .{ module_name } "
104+ filepaths .append ([ctx_name , f"{ path } /{ module_path } .py" , path ])
105+ path += f"/{ module_path } "
106+ filepaths .append ([f"{ ctx_name } .__init__" , f"{ path } /__init__.py" , path ])
107+ module_name = ctx_name [ctx_name .find ("." ) + 1 :]
108+
109+ else :
110+ if self .rel_import_path is not None and self .rel_import_path .startswith ("apps/" ):
111+ ctx_name = f"apps.{ module_name } "
112+ filepaths .append ([ctx_name , f"apps/{ module_path } .py" , f"apps/{ module_path } " ])
113+ filepaths .append (
114+ [f"{ ctx_name } .__init__" , f"apps/{ module_path } /__init__.py" , f"apps/{ module_path } " ]
115+ )
116+
117+ ctx_name = f"modules.{ module_name } "
118+ filepaths .append ([ctx_name , f"modules/{ module_path } .py" , None ])
119+ filepaths .append (
120+ [f"{ ctx_name } .__init__" , f"modules/{ module_path } /__init__.py" , f"modules/{ module_path } " ]
121+ )
122+
123+ #
124+ # now see if we have loaded it already
125+ #
126+ for ctx_name , _ , _ in filepaths :
127+ mod_ctx = self .manager .get (ctx_name )
128+ if mod_ctx and mod_ctx .module :
129+ _LOGGER .debug (
130+ "module_import: returning existing module %s, ctx = %s, mod = %s" ,
131+ module_name ,
132+ ctx_name ,
133+ mod_ctx .module ,
134+ )
135+ return [mod_ctx .module , None ]
136+
137+ #
138+ # not loaded already, so try to find and import it
139+ #
140+ _LOGGER .debug ("module_import: for module %s, searching %s" , module_name , filepaths )
141+ file_info = await Function .hass .async_add_executor_job (find_first_file , filepaths )
142+ if not file_info :
143+ return [None , None ]
144+
145+ [ctx_name , filepath , rel_import_path ] = file_info
146+
147+ mod = ModuleType (module_name )
148+ global_ctx = GlobalContext (
149+ ctx_name , global_sym_table = mod .__dict__ , manager = self .manager , rel_import_path = rel_import_path
150+ )
151+ global_ctx .set_auto_start (True )
152+ error_ctx = await self .manager .load_file (filepath , global_ctx )
153+ if error_ctx :
154+ _LOGGER .debug (
155+ "module_import: failed to load module %s, ctx = %s, path = %s" ,
156+ module_name ,
157+ ctx_name ,
158+ filepath ,
159+ )
160+ return [None , error_ctx ]
161+ global_ctx .module = mod
162+ _LOGGER .debug ("module_import: imported %s, ctx = %s, mod = %s" , module_name , ctx_name , mod )
163+ return [mod , None ]
92164
93165
94166class GlobalContextMgr :
@@ -185,7 +257,7 @@ def new_name(cls, root):
185257
186258 @classmethod
187259 async def load_file (cls , filepath , global_ctx ):
188- """Load, parse and run the given script file."""
260+ """Load, parse and run the given script file; returns error ast_ctx on error, or None if ok ."""
189261
190262 def read_file (path ):
191263 try :
@@ -201,7 +273,7 @@ def read_file(path):
201273 source = await Function .hass .async_add_executor_job (read_file , filepath )
202274
203275 if source is None :
204- return
276+ return False
205277
206278 ast_ctx = AstEval (global_ctx .get_name (), global_ctx )
207279 Function .install_ast_funcs (ast_ctx )
@@ -210,11 +282,12 @@ def read_file(path):
210282 exc = ast_ctx .get_exception_long ()
211283 ast_ctx .get_logger ().error (exc )
212284 global_ctx .stop ()
213- return
285+ return ast_ctx
214286 await ast_ctx .eval ()
215287 exc = ast_ctx .get_exception_long ()
216288 if exc is not None :
217289 ast_ctx .get_logger ().error (exc )
218290 global_ctx .stop ()
219- return
291+ return ast_ctx
220292 cls .set (global_ctx .get_name (), global_ctx )
293+ return None
0 commit comments