phml.core
1from __future__ import annotations 2 3import os 4import sys 5from contextlib import contextmanager 6from importlib import import_module 7from pathlib import Path 8from typing import TYPE_CHECKING, Any, NoReturn, overload 9 10if TYPE_CHECKING: 11 from collections.abc import Iterator 12 13from .compiler import HypertextMarkupCompiler 14from .components import ComponentManager, ComponentType 15from .embedded import __FROM_IMPORTS__, __IMPORTS__, Module 16from .helpers import PHMLTryCatch 17from .nodes import AST, Node, Parent 18from .parser import HypertextMarkupParser 19 20 21class HypertextManager: 22 parser: HypertextMarkupParser 23 """PHML parser.""" 24 compiler: HypertextMarkupCompiler 25 """PHML compiler to HTML.""" 26 components: ComponentManager 27 """PHML component parser and manager.""" 28 context: dict[str, Any] 29 """PHML global variables to expose to each phml file compiled with this instance. 30 This is the highest scope and is overridden by more specific scoped variables. 31 """ 32 33 def __init__(self) -> None: 34 self.parser = HypertextMarkupParser() 35 self.compiler = HypertextMarkupCompiler() 36 self.components = ComponentManager() 37 self.context = {"Module": Module} 38 self._ast: AST | None = None 39 self._from_path = None 40 self._from_file = None 41 self._to_file = None 42 43 @staticmethod 44 @contextmanager 45 def open( 46 _from: str | Path, 47 _to: str | Path | None = None, 48 ) -> Iterator[HypertextManager]: 49 with PHMLTryCatch(): 50 core = HypertextManager() 51 core._from_path = Path(_from) 52 core._from_path.parent.mkdir(parents=True, exist_ok=True) 53 core._from_file = Path(_from).open("r", encoding="utf-8") 54 if _to is not None: 55 output = Path(_to) 56 output.parent.mkdir(parents=True, exist_ok=True) 57 core._to_file = output.open("+w", encoding="utf-8") 58 59 core.parse() 60 61 yield core 62 63 core._from_path = None 64 core._from_file.close() 65 if core._to_file is not None: 66 core._to_file.close() 67 68 @property 69 def imports(self) -> dict: 70 return dict(__IMPORTS__) 71 72 @property 73 def from_imports(self) -> dict: 74 return dict(__FROM_IMPORTS__) 75 76 def add_module( 77 self, 78 module: str, 79 *, 80 name: str | None = None, 81 imports: list[str] | None = None, 82 ) -> NoReturn: 83 """Pass and imported a python file as a module. The modules are imported and added 84 to phml's cached imports. These modules are **ONLY** exposed to the python elements. 85 To use them in the python elements or the other scopes in the files you must use the python 86 import syntax `import <module>` or `from <module> import <...objects>`. PHML will parse 87 the imports first and remove them from the python elements. It then checks it's cache of 88 python modules that have been imported and adds those imported modules to the local context 89 for each embedded python execution. 90 91 Note: 92 - All imports will have a `.` prefixed to the module name. For example `current/file.py` gets the module 93 name `.current.file`. This helps seperate and indicate what imports are injected with this method. 94 Module import syntax will retain it's value, For example suppose the module `..module.name.here` 95 is added. It is in directory `module/` which is in a sibling directory to `current/`. The path 96 would look like `parent/ -> module/ -> name/ -> here.py` and the module would keep the name of 97 `..module.name.here`. 98 99 - All paths are resolved with the cwd in mind. 100 101 Args: 102 module (str): Absolute or relative path to a module, or module syntax reference to a module. 103 name (str): Optional name for the module after it is imported. 104 imports (list[str]): Optional list of objects to import from the module. Turns the import to 105 `from <module> import <...objects>` from `import <module>`. 106 107 Returns: 108 str: Name of the imported module. The key to use for indexing imported modules 109 """ 110 111 if module.startswith("~"): 112 module = module.replace("~", str(Path.home())) 113 114 mod = None 115 file = Path(module).with_suffix(".py") 116 cwd = os.getcwd() 117 118 if file.is_file(): 119 current = Path(cwd).as_posix() 120 path = file.resolve().as_posix() 121 122 cwd_p = current.split("/") 123 path_p = path.split("/") 124 index = 0 125 for cp, pp in zip(cwd_p, path_p): 126 if cp != pp: 127 break 128 index += 1 129 130 name = "/".join(path_p[index:]).rsplit(".", 1)[0].replace("/", ".") 131 path = "/".join(path_p[:index]) 132 133 # Make the path that is imported form the only path in sys.path 134 # this will prevent module conflicts and garuntees the correct module is imported 135 sys_path = list(sys.path) 136 sys.path = [path] 137 mod = import_module(name) 138 sys.path = sys_path 139 140 name = f".{name}" 141 else: 142 if module.startswith(".."): 143 current = Path(os.getcwd()).as_posix() 144 cwd_p = current.split("/") 145 146 path = "/".join(cwd_p[:-1]) 147 148 sys_path = list(sys.path) 149 sys.path = [path] 150 mod = import_module(module.lstrip("..")) 151 sys.path = sys_path 152 else: 153 mod = import_module(module) 154 name = f".{module.lstrip('..')}" 155 156 # Add imported module or module objects to appropriate collection 157 if imports is not None and len(imports) > 0: 158 for _import in imports: 159 if name not in __FROM_IMPORTS__: 160 __FROM_IMPORTS__[name] = {} 161 __FROM_IMPORTS__[name].update({_import: getattr(mod, _import)}) 162 else: 163 __IMPORTS__[name] = mod 164 165 return name 166 167 def remove_module(self, module: str, imports: list[str] | None = None): 168 if not module.startswith("."): 169 module = f".{module}" 170 if module in __IMPORTS__ and len(imports or []) == 0: 171 __IMPORTS__.pop(module, None) 172 if module in __FROM_IMPORTS__: 173 if imports is not None and len(imports) > 0: 174 for _import in imports: 175 __FROM_IMPORTS__[module].pop(_import, None) 176 if len(__FROM_IMPORTS__[module]) == 0: 177 __FROM_IMPORTS__.pop(module, None) 178 else: 179 __FROM_IMPORTS__.pop(module, None) 180 181 return self 182 183 @property 184 def ast(self) -> AST: 185 """The current ast that has been parsed. Defaults to None.""" 186 return self._ast or AST() 187 188 def load(self, path: str | Path): 189 """Loads the contents of a file and sets the core objects ast 190 to the results after parsing. 191 """ 192 with PHMLTryCatch(), Path(path).open("r", encoding="utf-8") as file: 193 self._from_path = path 194 self._ast = self.parser.parse(file.read()) 195 return self 196 197 def parse(self, data: str | dict | None = None): 198 """Parse a given phml string or dict into a phml ast. 199 200 Returns: 201 Instance of the core object for method chaining. 202 """ 203 204 if data is None and self._from_file is None: 205 raise ValueError( 206 "Must either provide a phml str/dict to parse or use parse in the open context manager", 207 ) 208 209 with PHMLTryCatch(self._from_path, "phml:__parse__"): 210 if isinstance(data, dict): 211 ast = Node.from_dict(data) 212 if not isinstance(ast, AST) and ast is not None: 213 ast = AST([ast]) 214 self._ast = ast 215 elif data is not None: 216 self._ast = self.parser.parse(data) 217 elif self._from_file is not None: 218 self._ast = self.parser.parse(self._from_file.read()) 219 self._from_file.seek(0) 220 221 return self 222 223 def format( 224 self, 225 *, 226 code: str = "", 227 file: str | Path | None = None, 228 compress: bool = False, 229 ) -> str | None: 230 """Format a phml str or file. 231 232 Args: 233 code (str, optional): The phml str to format. 234 235 Kwargs: 236 file (str, optional): Path to a phml file. Can be used instead of 237 `code` to parse and format a phml file. 238 compress (bool, optional): Flag to compress the file and remove new lines. Defaults to False. 239 240 Note: 241 If both `code` and `file` are passed in then both will be formatted with the formatted `code` 242 bing returned as a string and the formatted `file` being written to the files original location. 243 244 Returns: 245 str: When a phml str is passed in 246 None: When a file path is passed in. Instead the resulting formatted string is written back to the file. 247 """ 248 249 result = None 250 if code != "": 251 self.parse(code) 252 result = self.compiler.render( 253 self._ast, 254 compress, 255 ) 256 257 if file is not None: 258 self.load(file) 259 with Path(file).open("+w", encoding="utf-8") as phml_file: 260 phml_file.write( 261 self.compiler.render( 262 self._ast, 263 compress, 264 ), 265 ) 266 267 return result 268 269 def compile(self, **context: Any) -> Parent: 270 """Compile the python blocks, python attributes, and phml components and return the resulting ast. 271 The resulting ast replaces the core objects ast. 272 """ 273 context = {**self.context, **context, "_phml_path_": self._from_path} 274 if self._ast is not None: 275 with PHMLTryCatch(self._from_path, "phml:__compile__"): 276 ast = self.compiler.compile(self._ast, self.components, **context) 277 return ast 278 raise ValueError("Must first parse a phml file before compiling to an AST") 279 280 def render(self, _compress: bool = False, **context: Any) -> str: 281 """Renders the phml ast into an html string. If currently in a context manager 282 the resulting string will also be output to an associated file. 283 """ 284 context = {**self.context, **context, "_phml_path_": self._from_path} 285 if self._ast is not None: 286 with PHMLTryCatch(self._from_path, "phml:__render"): 287 result = self.compiler.render( 288 self.compile(**context), 289 _compress, 290 ) 291 292 if self._to_file is not None: 293 self._to_file.write(result) 294 elif self._from_file is not None and self._from_path is not None: 295 self._to_file = ( 296 Path(self._from_path) 297 .with_suffix(".html") 298 .open("+w", encoding="utf-8") 299 ) 300 self._to_file.write(result) 301 return result 302 raise ValueError("Must first parse a phml file before rendering a phml AST") 303 304 def write(self, _path: str | Path, _compress: bool = False, **context: Any): 305 """Render and write the current ast to a file. 306 307 Args: 308 path (str): The output path for the rendered html. 309 compress (bool): Whether to compress the output. Defaults to False. 310 """ 311 path = Path(_path).with_suffix(".html") 312 path.parent.mkdir(parents=True, exist_ok=True) 313 314 with path.open("+w", encoding="utf-8") as file: 315 file.write(self.compiler.render(self.compile(**context), _compress)) 316 return self 317 318 @overload 319 def add(self, file: str | Path, *, ignore: str = ""): 320 """Add a component to the component manager with a file path. Also, componetes can be added to 321 the component manager with a name and str or an already parsed component dict. 322 323 Args: 324 file (str): The file path to the component. 325 ignore (str): The path prefix to remove before creating the comopnent name. 326 name (str): The name of the component. This is the index/key in the component manager. 327 This is also the name of the element in phml. Ex: `Some.Component` == `<Some.Component />` 328 data (str | ComponentType): This is the data that is assigned in the manager. It can be a string 329 representation of the component, or an already parsed component type dict. 330 """ 331 ... 332 333 @overload 334 def add(self, *, name: str, data: str | ComponentType): 335 """Add a component to the component manager with a file path. Also, componetes can be added to 336 the component manager with a name and str or an already parsed component dict. 337 338 Args: 339 file (str): The file path to the component. 340 ignore (str): The path prefix to remove before creating the comopnent name. 341 name (str): The name of the component. This is the index/key in the component manager. 342 This is also the name of the element in phml. Ex: `Some.Component` == `<Some.Component />` 343 data (str | ComponentType): This is the data that is assigned in the manager. It can be a string 344 representation of the component, or an already parsed component type dict. 345 """ 346 ... 347 348 def add( 349 self, 350 file: str | Path | None = None, 351 *, 352 name: str | None = None, 353 data: ComponentType | str | None = None, 354 ignore: str = "", 355 ): 356 """Add a component to the component manager. The components are used by the compiler 357 when generating html files from phml. 358 """ 359 with PHMLTryCatch(file or name or "_cmpt_"): 360 self.components.add(file, name=name, data=data, ignore=ignore) 361 362 def remove(self, key: str): 363 """Remove a component from the component manager based on the components name/tag.""" 364 self.components.remove(key) 365 366 def expose(self, _context: dict[str, Any] | None = None, **context: Any): 367 """Expose global variables to each phml file compiled with this instance. 368 This data is the highest scope and will be overridden by more specific 369 scoped variables with equivelant names. 370 """ 371 372 if _context: 373 self.context.update(_context or {}) 374 self.context.update(context) 375 376 def redact(self, *keys: str): 377 """Remove global variable from this instance.""" 378 for key in keys: 379 self.context.pop(key, None)
22class HypertextManager: 23 parser: HypertextMarkupParser 24 """PHML parser.""" 25 compiler: HypertextMarkupCompiler 26 """PHML compiler to HTML.""" 27 components: ComponentManager 28 """PHML component parser and manager.""" 29 context: dict[str, Any] 30 """PHML global variables to expose to each phml file compiled with this instance. 31 This is the highest scope and is overridden by more specific scoped variables. 32 """ 33 34 def __init__(self) -> None: 35 self.parser = HypertextMarkupParser() 36 self.compiler = HypertextMarkupCompiler() 37 self.components = ComponentManager() 38 self.context = {"Module": Module} 39 self._ast: AST | None = None 40 self._from_path = None 41 self._from_file = None 42 self._to_file = None 43 44 @staticmethod 45 @contextmanager 46 def open( 47 _from: str | Path, 48 _to: str | Path | None = None, 49 ) -> Iterator[HypertextManager]: 50 with PHMLTryCatch(): 51 core = HypertextManager() 52 core._from_path = Path(_from) 53 core._from_path.parent.mkdir(parents=True, exist_ok=True) 54 core._from_file = Path(_from).open("r", encoding="utf-8") 55 if _to is not None: 56 output = Path(_to) 57 output.parent.mkdir(parents=True, exist_ok=True) 58 core._to_file = output.open("+w", encoding="utf-8") 59 60 core.parse() 61 62 yield core 63 64 core._from_path = None 65 core._from_file.close() 66 if core._to_file is not None: 67 core._to_file.close() 68 69 @property 70 def imports(self) -> dict: 71 return dict(__IMPORTS__) 72 73 @property 74 def from_imports(self) -> dict: 75 return dict(__FROM_IMPORTS__) 76 77 def add_module( 78 self, 79 module: str, 80 *, 81 name: str | None = None, 82 imports: list[str] | None = None, 83 ) -> NoReturn: 84 """Pass and imported a python file as a module. The modules are imported and added 85 to phml's cached imports. These modules are **ONLY** exposed to the python elements. 86 To use them in the python elements or the other scopes in the files you must use the python 87 import syntax `import <module>` or `from <module> import <...objects>`. PHML will parse 88 the imports first and remove them from the python elements. It then checks it's cache of 89 python modules that have been imported and adds those imported modules to the local context 90 for each embedded python execution. 91 92 Note: 93 - All imports will have a `.` prefixed to the module name. For example `current/file.py` gets the module 94 name `.current.file`. This helps seperate and indicate what imports are injected with this method. 95 Module import syntax will retain it's value, For example suppose the module `..module.name.here` 96 is added. It is in directory `module/` which is in a sibling directory to `current/`. The path 97 would look like `parent/ -> module/ -> name/ -> here.py` and the module would keep the name of 98 `..module.name.here`. 99 100 - All paths are resolved with the cwd in mind. 101 102 Args: 103 module (str): Absolute or relative path to a module, or module syntax reference to a module. 104 name (str): Optional name for the module after it is imported. 105 imports (list[str]): Optional list of objects to import from the module. Turns the import to 106 `from <module> import <...objects>` from `import <module>`. 107 108 Returns: 109 str: Name of the imported module. The key to use for indexing imported modules 110 """ 111 112 if module.startswith("~"): 113 module = module.replace("~", str(Path.home())) 114 115 mod = None 116 file = Path(module).with_suffix(".py") 117 cwd = os.getcwd() 118 119 if file.is_file(): 120 current = Path(cwd).as_posix() 121 path = file.resolve().as_posix() 122 123 cwd_p = current.split("/") 124 path_p = path.split("/") 125 index = 0 126 for cp, pp in zip(cwd_p, path_p): 127 if cp != pp: 128 break 129 index += 1 130 131 name = "/".join(path_p[index:]).rsplit(".", 1)[0].replace("/", ".") 132 path = "/".join(path_p[:index]) 133 134 # Make the path that is imported form the only path in sys.path 135 # this will prevent module conflicts and garuntees the correct module is imported 136 sys_path = list(sys.path) 137 sys.path = [path] 138 mod = import_module(name) 139 sys.path = sys_path 140 141 name = f".{name}" 142 else: 143 if module.startswith(".."): 144 current = Path(os.getcwd()).as_posix() 145 cwd_p = current.split("/") 146 147 path = "/".join(cwd_p[:-1]) 148 149 sys_path = list(sys.path) 150 sys.path = [path] 151 mod = import_module(module.lstrip("..")) 152 sys.path = sys_path 153 else: 154 mod = import_module(module) 155 name = f".{module.lstrip('..')}" 156 157 # Add imported module or module objects to appropriate collection 158 if imports is not None and len(imports) > 0: 159 for _import in imports: 160 if name not in __FROM_IMPORTS__: 161 __FROM_IMPORTS__[name] = {} 162 __FROM_IMPORTS__[name].update({_import: getattr(mod, _import)}) 163 else: 164 __IMPORTS__[name] = mod 165 166 return name 167 168 def remove_module(self, module: str, imports: list[str] | None = None): 169 if not module.startswith("."): 170 module = f".{module}" 171 if module in __IMPORTS__ and len(imports or []) == 0: 172 __IMPORTS__.pop(module, None) 173 if module in __FROM_IMPORTS__: 174 if imports is not None and len(imports) > 0: 175 for _import in imports: 176 __FROM_IMPORTS__[module].pop(_import, None) 177 if len(__FROM_IMPORTS__[module]) == 0: 178 __FROM_IMPORTS__.pop(module, None) 179 else: 180 __FROM_IMPORTS__.pop(module, None) 181 182 return self 183 184 @property 185 def ast(self) -> AST: 186 """The current ast that has been parsed. Defaults to None.""" 187 return self._ast or AST() 188 189 def load(self, path: str | Path): 190 """Loads the contents of a file and sets the core objects ast 191 to the results after parsing. 192 """ 193 with PHMLTryCatch(), Path(path).open("r", encoding="utf-8") as file: 194 self._from_path = path 195 self._ast = self.parser.parse(file.read()) 196 return self 197 198 def parse(self, data: str | dict | None = None): 199 """Parse a given phml string or dict into a phml ast. 200 201 Returns: 202 Instance of the core object for method chaining. 203 """ 204 205 if data is None and self._from_file is None: 206 raise ValueError( 207 "Must either provide a phml str/dict to parse or use parse in the open context manager", 208 ) 209 210 with PHMLTryCatch(self._from_path, "phml:__parse__"): 211 if isinstance(data, dict): 212 ast = Node.from_dict(data) 213 if not isinstance(ast, AST) and ast is not None: 214 ast = AST([ast]) 215 self._ast = ast 216 elif data is not None: 217 self._ast = self.parser.parse(data) 218 elif self._from_file is not None: 219 self._ast = self.parser.parse(self._from_file.read()) 220 self._from_file.seek(0) 221 222 return self 223 224 def format( 225 self, 226 *, 227 code: str = "", 228 file: str | Path | None = None, 229 compress: bool = False, 230 ) -> str | None: 231 """Format a phml str or file. 232 233 Args: 234 code (str, optional): The phml str to format. 235 236 Kwargs: 237 file (str, optional): Path to a phml file. Can be used instead of 238 `code` to parse and format a phml file. 239 compress (bool, optional): Flag to compress the file and remove new lines. Defaults to False. 240 241 Note: 242 If both `code` and `file` are passed in then both will be formatted with the formatted `code` 243 bing returned as a string and the formatted `file` being written to the files original location. 244 245 Returns: 246 str: When a phml str is passed in 247 None: When a file path is passed in. Instead the resulting formatted string is written back to the file. 248 """ 249 250 result = None 251 if code != "": 252 self.parse(code) 253 result = self.compiler.render( 254 self._ast, 255 compress, 256 ) 257 258 if file is not None: 259 self.load(file) 260 with Path(file).open("+w", encoding="utf-8") as phml_file: 261 phml_file.write( 262 self.compiler.render( 263 self._ast, 264 compress, 265 ), 266 ) 267 268 return result 269 270 def compile(self, **context: Any) -> Parent: 271 """Compile the python blocks, python attributes, and phml components and return the resulting ast. 272 The resulting ast replaces the core objects ast. 273 """ 274 context = {**self.context, **context, "_phml_path_": self._from_path} 275 if self._ast is not None: 276 with PHMLTryCatch(self._from_path, "phml:__compile__"): 277 ast = self.compiler.compile(self._ast, self.components, **context) 278 return ast 279 raise ValueError("Must first parse a phml file before compiling to an AST") 280 281 def render(self, _compress: bool = False, **context: Any) -> str: 282 """Renders the phml ast into an html string. If currently in a context manager 283 the resulting string will also be output to an associated file. 284 """ 285 context = {**self.context, **context, "_phml_path_": self._from_path} 286 if self._ast is not None: 287 with PHMLTryCatch(self._from_path, "phml:__render"): 288 result = self.compiler.render( 289 self.compile(**context), 290 _compress, 291 ) 292 293 if self._to_file is not None: 294 self._to_file.write(result) 295 elif self._from_file is not None and self._from_path is not None: 296 self._to_file = ( 297 Path(self._from_path) 298 .with_suffix(".html") 299 .open("+w", encoding="utf-8") 300 ) 301 self._to_file.write(result) 302 return result 303 raise ValueError("Must first parse a phml file before rendering a phml AST") 304 305 def write(self, _path: str | Path, _compress: bool = False, **context: Any): 306 """Render and write the current ast to a file. 307 308 Args: 309 path (str): The output path for the rendered html. 310 compress (bool): Whether to compress the output. Defaults to False. 311 """ 312 path = Path(_path).with_suffix(".html") 313 path.parent.mkdir(parents=True, exist_ok=True) 314 315 with path.open("+w", encoding="utf-8") as file: 316 file.write(self.compiler.render(self.compile(**context), _compress)) 317 return self 318 319 @overload 320 def add(self, file: str | Path, *, ignore: str = ""): 321 """Add a component to the component manager with a file path. Also, componetes can be added to 322 the component manager with a name and str or an already parsed component dict. 323 324 Args: 325 file (str): The file path to the component. 326 ignore (str): The path prefix to remove before creating the comopnent name. 327 name (str): The name of the component. This is the index/key in the component manager. 328 This is also the name of the element in phml. Ex: `Some.Component` == `<Some.Component />` 329 data (str | ComponentType): This is the data that is assigned in the manager. It can be a string 330 representation of the component, or an already parsed component type dict. 331 """ 332 ... 333 334 @overload 335 def add(self, *, name: str, data: str | ComponentType): 336 """Add a component to the component manager with a file path. Also, componetes can be added to 337 the component manager with a name and str or an already parsed component dict. 338 339 Args: 340 file (str): The file path to the component. 341 ignore (str): The path prefix to remove before creating the comopnent name. 342 name (str): The name of the component. This is the index/key in the component manager. 343 This is also the name of the element in phml. Ex: `Some.Component` == `<Some.Component />` 344 data (str | ComponentType): This is the data that is assigned in the manager. It can be a string 345 representation of the component, or an already parsed component type dict. 346 """ 347 ... 348 349 def add( 350 self, 351 file: str | Path | None = None, 352 *, 353 name: str | None = None, 354 data: ComponentType | str | None = None, 355 ignore: str = "", 356 ): 357 """Add a component to the component manager. The components are used by the compiler 358 when generating html files from phml. 359 """ 360 with PHMLTryCatch(file or name or "_cmpt_"): 361 self.components.add(file, name=name, data=data, ignore=ignore) 362 363 def remove(self, key: str): 364 """Remove a component from the component manager based on the components name/tag.""" 365 self.components.remove(key) 366 367 def expose(self, _context: dict[str, Any] | None = None, **context: Any): 368 """Expose global variables to each phml file compiled with this instance. 369 This data is the highest scope and will be overridden by more specific 370 scoped variables with equivelant names. 371 """ 372 373 if _context: 374 self.context.update(_context or {}) 375 self.context.update(context) 376 377 def redact(self, *keys: str): 378 """Remove global variable from this instance.""" 379 for key in keys: 380 self.context.pop(key, None)
34 def __init__(self) -> None: 35 self.parser = HypertextMarkupParser() 36 self.compiler = HypertextMarkupCompiler() 37 self.components = ComponentManager() 38 self.context = {"Module": Module} 39 self._ast: AST | None = None 40 self._from_path = None 41 self._from_file = None 42 self._to_file = None
PHML global variables to expose to each phml file compiled with this instance. This is the highest scope and is overridden by more specific scoped variables.
44 @staticmethod 45 @contextmanager 46 def open( 47 _from: str | Path, 48 _to: str | Path | None = None, 49 ) -> Iterator[HypertextManager]: 50 with PHMLTryCatch(): 51 core = HypertextManager() 52 core._from_path = Path(_from) 53 core._from_path.parent.mkdir(parents=True, exist_ok=True) 54 core._from_file = Path(_from).open("r", encoding="utf-8") 55 if _to is not None: 56 output = Path(_to) 57 output.parent.mkdir(parents=True, exist_ok=True) 58 core._to_file = output.open("+w", encoding="utf-8") 59 60 core.parse() 61 62 yield core 63 64 core._from_path = None 65 core._from_file.close() 66 if core._to_file is not None: 67 core._to_file.close()
77 def add_module( 78 self, 79 module: str, 80 *, 81 name: str | None = None, 82 imports: list[str] | None = None, 83 ) -> NoReturn: 84 """Pass and imported a python file as a module. The modules are imported and added 85 to phml's cached imports. These modules are **ONLY** exposed to the python elements. 86 To use them in the python elements or the other scopes in the files you must use the python 87 import syntax `import <module>` or `from <module> import <...objects>`. PHML will parse 88 the imports first and remove them from the python elements. It then checks it's cache of 89 python modules that have been imported and adds those imported modules to the local context 90 for each embedded python execution. 91 92 Note: 93 - All imports will have a `.` prefixed to the module name. For example `current/file.py` gets the module 94 name `.current.file`. This helps seperate and indicate what imports are injected with this method. 95 Module import syntax will retain it's value, For example suppose the module `..module.name.here` 96 is added. It is in directory `module/` which is in a sibling directory to `current/`. The path 97 would look like `parent/ -> module/ -> name/ -> here.py` and the module would keep the name of 98 `..module.name.here`. 99 100 - All paths are resolved with the cwd in mind. 101 102 Args: 103 module (str): Absolute or relative path to a module, or module syntax reference to a module. 104 name (str): Optional name for the module after it is imported. 105 imports (list[str]): Optional list of objects to import from the module. Turns the import to 106 `from <module> import <...objects>` from `import <module>`. 107 108 Returns: 109 str: Name of the imported module. The key to use for indexing imported modules 110 """ 111 112 if module.startswith("~"): 113 module = module.replace("~", str(Path.home())) 114 115 mod = None 116 file = Path(module).with_suffix(".py") 117 cwd = os.getcwd() 118 119 if file.is_file(): 120 current = Path(cwd).as_posix() 121 path = file.resolve().as_posix() 122 123 cwd_p = current.split("/") 124 path_p = path.split("/") 125 index = 0 126 for cp, pp in zip(cwd_p, path_p): 127 if cp != pp: 128 break 129 index += 1 130 131 name = "/".join(path_p[index:]).rsplit(".", 1)[0].replace("/", ".") 132 path = "/".join(path_p[:index]) 133 134 # Make the path that is imported form the only path in sys.path 135 # this will prevent module conflicts and garuntees the correct module is imported 136 sys_path = list(sys.path) 137 sys.path = [path] 138 mod = import_module(name) 139 sys.path = sys_path 140 141 name = f".{name}" 142 else: 143 if module.startswith(".."): 144 current = Path(os.getcwd()).as_posix() 145 cwd_p = current.split("/") 146 147 path = "/".join(cwd_p[:-1]) 148 149 sys_path = list(sys.path) 150 sys.path = [path] 151 mod = import_module(module.lstrip("..")) 152 sys.path = sys_path 153 else: 154 mod = import_module(module) 155 name = f".{module.lstrip('..')}" 156 157 # Add imported module or module objects to appropriate collection 158 if imports is not None and len(imports) > 0: 159 for _import in imports: 160 if name not in __FROM_IMPORTS__: 161 __FROM_IMPORTS__[name] = {} 162 __FROM_IMPORTS__[name].update({_import: getattr(mod, _import)}) 163 else: 164 __IMPORTS__[name] = mod 165 166 return name
Pass and imported a python file as a module. The modules are imported and added
to phml's cached imports. These modules are ONLY exposed to the python elements.
To use them in the python elements or the other scopes in the files you must use the python
import syntax import <module>
or from <module> import <...objects>
. PHML will parse
the imports first and remove them from the python elements. It then checks it's cache of
python modules that have been imported and adds those imported modules to the local context
for each embedded python execution.
Note
All imports will have a
.
prefixed to the module name. For examplecurrent/file.py
gets the module name.current.file
. This helps seperate and indicate what imports are injected with this method. Module import syntax will retain it's value, For example suppose the module..module.name.here
is added. It is in directorymodule/
which is in a sibling directory tocurrent/
. The path would look likeparent/ -> module/ -> name/ -> here.py
and the module would keep the name of..module.name.here
.All paths are resolved with the cwd in mind.
Args
- module (str): Absolute or relative path to a module, or module syntax reference to a module.
- name (str): Optional name for the module after it is imported.
- imports (list[str]): Optional list of objects to import from the module. Turns the import to
from <module> import <...objects>
fromimport <module>
.
Returns
str: Name of the imported module. The key to use for indexing imported modules
168 def remove_module(self, module: str, imports: list[str] | None = None): 169 if not module.startswith("."): 170 module = f".{module}" 171 if module in __IMPORTS__ and len(imports or []) == 0: 172 __IMPORTS__.pop(module, None) 173 if module in __FROM_IMPORTS__: 174 if imports is not None and len(imports) > 0: 175 for _import in imports: 176 __FROM_IMPORTS__[module].pop(_import, None) 177 if len(__FROM_IMPORTS__[module]) == 0: 178 __FROM_IMPORTS__.pop(module, None) 179 else: 180 __FROM_IMPORTS__.pop(module, None) 181 182 return self
189 def load(self, path: str | Path): 190 """Loads the contents of a file and sets the core objects ast 191 to the results after parsing. 192 """ 193 with PHMLTryCatch(), Path(path).open("r", encoding="utf-8") as file: 194 self._from_path = path 195 self._ast = self.parser.parse(file.read()) 196 return self
Loads the contents of a file and sets the core objects ast to the results after parsing.
198 def parse(self, data: str | dict | None = None): 199 """Parse a given phml string or dict into a phml ast. 200 201 Returns: 202 Instance of the core object for method chaining. 203 """ 204 205 if data is None and self._from_file is None: 206 raise ValueError( 207 "Must either provide a phml str/dict to parse or use parse in the open context manager", 208 ) 209 210 with PHMLTryCatch(self._from_path, "phml:__parse__"): 211 if isinstance(data, dict): 212 ast = Node.from_dict(data) 213 if not isinstance(ast, AST) and ast is not None: 214 ast = AST([ast]) 215 self._ast = ast 216 elif data is not None: 217 self._ast = self.parser.parse(data) 218 elif self._from_file is not None: 219 self._ast = self.parser.parse(self._from_file.read()) 220 self._from_file.seek(0) 221 222 return self
Parse a given phml string or dict into a phml ast.
Returns
Instance of the core object for method chaining.
224 def format( 225 self, 226 *, 227 code: str = "", 228 file: str | Path | None = None, 229 compress: bool = False, 230 ) -> str | None: 231 """Format a phml str or file. 232 233 Args: 234 code (str, optional): The phml str to format. 235 236 Kwargs: 237 file (str, optional): Path to a phml file. Can be used instead of 238 `code` to parse and format a phml file. 239 compress (bool, optional): Flag to compress the file and remove new lines. Defaults to False. 240 241 Note: 242 If both `code` and `file` are passed in then both will be formatted with the formatted `code` 243 bing returned as a string and the formatted `file` being written to the files original location. 244 245 Returns: 246 str: When a phml str is passed in 247 None: When a file path is passed in. Instead the resulting formatted string is written back to the file. 248 """ 249 250 result = None 251 if code != "": 252 self.parse(code) 253 result = self.compiler.render( 254 self._ast, 255 compress, 256 ) 257 258 if file is not None: 259 self.load(file) 260 with Path(file).open("+w", encoding="utf-8") as phml_file: 261 phml_file.write( 262 self.compiler.render( 263 self._ast, 264 compress, 265 ), 266 ) 267 268 return result
Format a phml str or file.
Args
- code (str, optional): The phml str to format.
Kwargs
file (str, optional): Path to a phml file. Can be used instead of
code
to parse and format a phml file. compress (bool, optional): Flag to compress the file and remove new lines. Defaults to False.
Note
If both
code
andfile
are passed in then both will be formatted with the formattedcode
bing returned as a string and the formattedfile
being written to the files original location.
Returns
str: When a phml str is passed in None: When a file path is passed in. Instead the resulting formatted string is written back to the file.
270 def compile(self, **context: Any) -> Parent: 271 """Compile the python blocks, python attributes, and phml components and return the resulting ast. 272 The resulting ast replaces the core objects ast. 273 """ 274 context = {**self.context, **context, "_phml_path_": self._from_path} 275 if self._ast is not None: 276 with PHMLTryCatch(self._from_path, "phml:__compile__"): 277 ast = self.compiler.compile(self._ast, self.components, **context) 278 return ast 279 raise ValueError("Must first parse a phml file before compiling to an AST")
Compile the python blocks, python attributes, and phml components and return the resulting ast. The resulting ast replaces the core objects ast.
281 def render(self, _compress: bool = False, **context: Any) -> str: 282 """Renders the phml ast into an html string. If currently in a context manager 283 the resulting string will also be output to an associated file. 284 """ 285 context = {**self.context, **context, "_phml_path_": self._from_path} 286 if self._ast is not None: 287 with PHMLTryCatch(self._from_path, "phml:__render"): 288 result = self.compiler.render( 289 self.compile(**context), 290 _compress, 291 ) 292 293 if self._to_file is not None: 294 self._to_file.write(result) 295 elif self._from_file is not None and self._from_path is not None: 296 self._to_file = ( 297 Path(self._from_path) 298 .with_suffix(".html") 299 .open("+w", encoding="utf-8") 300 ) 301 self._to_file.write(result) 302 return result 303 raise ValueError("Must first parse a phml file before rendering a phml AST")
Renders the phml ast into an html string. If currently in a context manager the resulting string will also be output to an associated file.
305 def write(self, _path: str | Path, _compress: bool = False, **context: Any): 306 """Render and write the current ast to a file. 307 308 Args: 309 path (str): The output path for the rendered html. 310 compress (bool): Whether to compress the output. Defaults to False. 311 """ 312 path = Path(_path).with_suffix(".html") 313 path.parent.mkdir(parents=True, exist_ok=True) 314 315 with path.open("+w", encoding="utf-8") as file: 316 file.write(self.compiler.render(self.compile(**context), _compress)) 317 return self
Render and write the current ast to a file.
Args
- path (str): The output path for the rendered html.
- compress (bool): Whether to compress the output. Defaults to False.
349 def add( 350 self, 351 file: str | Path | None = None, 352 *, 353 name: str | None = None, 354 data: ComponentType | str | None = None, 355 ignore: str = "", 356 ): 357 """Add a component to the component manager. The components are used by the compiler 358 when generating html files from phml. 359 """ 360 with PHMLTryCatch(file or name or "_cmpt_"): 361 self.components.add(file, name=name, data=data, ignore=ignore)
Add a component to the component manager. The components are used by the compiler when generating html files from phml.
363 def remove(self, key: str): 364 """Remove a component from the component manager based on the components name/tag.""" 365 self.components.remove(key)
Remove a component from the component manager based on the components name/tag.
367 def expose(self, _context: dict[str, Any] | None = None, **context: Any): 368 """Expose global variables to each phml file compiled with this instance. 369 This data is the highest scope and will be overridden by more specific 370 scoped variables with equivelant names. 371 """ 372 373 if _context: 374 self.context.update(_context or {}) 375 self.context.update(context)
Expose global variables to each phml file compiled with this instance. This data is the highest scope and will be overridden by more specific scoped variables with equivelant names.