diff --git a/docs/asyncio_blocking_operations.md b/docs/asyncio_blocking_operations.md index 3a49d3b6..f2c68d02 100644 --- a/docs/asyncio_blocking_operations.md +++ b/docs/asyncio_blocking_operations.md @@ -53,15 +53,7 @@ When an `open` call running in the event loop is fixed, all the blocking reads a #### import_module -When importing a module, the import machinery has to read the module from disk which does blocking I/O. Importing modules is both CPU-intensive and involves blocking I/O, so it is crucial to ensure these operations are executed in the executor. - -Importing code in [cpython is not thread-safe](https://github.com/python/cpython/issues/83065). If the module will only ever be imported in a single place, the standard executor calls can be used. If there's a possibility of the same module being imported concurrently in different parts of the application, use the thread-safe `homeassistant.helpers.importlib.import_module` helper. - -Example: - -```python -platform = await async_import_module(hass, f"homeassistant.components.homeassistant.triggers.{platform_name}") -``` +See [Importing code with asyncio](asyncio_imports.md) #### sleep diff --git a/docs/asyncio_imports.md b/docs/asyncio_imports.md new file mode 100644 index 00000000..4774143e --- /dev/null +++ b/docs/asyncio_imports.md @@ -0,0 +1,60 @@ +--- +title: "Importing code with asyncio" +--- + +Determining when it is safe to import code when using asyncio can be tricky because two constraints need to be considered: + +- Importing code can do blocking I/O to load the files from the disk +- Importing code in [cpython is not thread-safe](https://github.com/python/cpython/issues/83065) + +## Module level imports + +If your imports are at the **module level** (also called **top-level imports**) and all the necessary modules are imported in `__init__.py`, Home Assistant will load your integration either **before the event loop starts** or in a background thread using the **import executor**. + +In this scenario, your imports are generally handled safely, so you **don’t need to worry** about whether they’re event-loop safe. + +## Imports outside of module level + +If your imports are not happening at module level, you must carefully consider each import, as the import machinery has to read the module from disk which does blocking I/O. If possible, it's usually best to change to a module level import, as it avoids much complexity and the risk of mistakes. Importing modules is both CPU-intensive and involves blocking I/O, so it is crucial to ensure these operations are executed in the executor. + +If you can be sure that the modules have already been imported, using a bare [`import`](https://docs.python.org/3/reference/simple_stmts.html#import) statement is safe since Python will not load the modules again. + +If the integration will always use the module, it's usually best to include a module-level import in `__init__.py` to ensure the module is loaded. However, if this creates a circular import, one of the solutions below will need to be used instead. + +If the module is only used conditionally, and will only ever be imported in a single place, the standard executor calls can be used: + +- For imports inside of Home Assistant `hass.async_add_executor_job(_function_that_does_late_import)` +- For imports outside of Home Assistant: [`loop.run_in_executor(None, _function_that_does_late_import)`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) +If the same module may be imported concurrently in different parts of the application, use the thread-safe `homeassistant.helpers.importlib.import_module` helper. + +If it's possible the module may be imported from multiple different paths, use `async_import_module`: +Example: + +```python +from homeassistant.helpers.importlib import async_import_module + +platform = await async_import_module(hass, f"homeassistant.components.homeassistant.triggers.{platform_name}") +``` + +## Determining if a module is already loaded + +If you are unsure if a module is already loaded, you can check if the module is already in [`sys.modules`](https://docs.python.org/3/library/sys.html#sys.modules). You should know that the module will appear in `sys.modules` as soon as it begins loading, and [cpython imports are not thread-safe](https://github.com/python/cpython/issues/83065). For this reason, it's important to consider race conditions when code may be imported from multiple paths. + +## Avoiding imports that are only used for type-checking + +If an imported module is only used for type checking, it is recommended to guard it with an `if TYPE_CHECKING:` block to avoid it being imported at runtime. + +```python +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from some_module import SomeClass # Only imported for type checking + +def some_function() -> SomeClass: + # Function implementation + pass +``` + +## Avoid importing code that is rarely used + +Importing modules can be both CPU and I/O intensive, so it’s important to avoid importing code that will rarely be used. While importing code outside the module level does add some runtime overhead, this approach is often more efficient when the code is only needed occasionally. By deferring imports, you ensure that resources are only used when necessary, reducing unnecessary processing and improving overall performance. diff --git a/sidebars.js b/sidebars.js index 207e1a05..abfc72e6 100644 --- a/sidebars.js +++ b/sidebars.js @@ -318,6 +318,7 @@ module.exports = { "asyncio_working_with_async", "asyncio_thread_safety", "asyncio_blocking_operations", + "asyncio_imports", ], }, ],