|
| 1 | +#Set up main functions |
| 2 | + |
| 3 | +As we've seen throughout this guide, there can be quite a bit of setup around a main function: loading modules, defining scopes, injecting dependencies, loading profiles, etc. This becomes repetitive when you have multiple entry points in your project (CLI commands, etc.). |
| 4 | + |
| 5 | +To solve this,`python-injection` provides**entrypoints**, a way to create custom decorators that encapsulate all your setup logic using a builder pattern. |
| 6 | + |
| 7 | +##Creating an entrypoint |
| 8 | + |
| 9 | +Use the`@entrypointmaker` decorator to define your setup logic once: |
| 10 | +```python |
| 11 | +from injectionimport adefine_scope |
| 12 | +from injection.entrypointimport AsyncEntrypoint, Entrypoint, entrypointmaker |
| 13 | +from injection.loadersimport PythonModuleLoader |
| 14 | + |
| 15 | +@entrypointmaker |
| 16 | +def entrypoint[**P, T](self: AsyncEntrypoint[P, T])-> Entrypoint[P, T]: |
| 17 | +import src |
| 18 | + |
| 19 | + module_loader= PythonModuleLoader.endswith("_impl") |
| 20 | +return ( |
| 21 | +self.inject() |
| 22 | + .decorate(adefine_scope("lifespan",kind="shared")) |
| 23 | + .async_to_sync() |
| 24 | + .load_modules(module_loader, src) |
| 25 | + ) |
| 26 | +``` |
| 27 | + |
| 28 | +Now you can use your custom`@entrypoint` decorator on any function: |
| 29 | +```python |
| 30 | +@entrypoint |
| 31 | +asyncdefmain(dependency: Dependency): |
| 32 | +# All setup is automatically applied |
| 33 | +... |
| 34 | + |
| 35 | +if__name__=="__main__": |
| 36 | + main() |
| 37 | +``` |
| 38 | + |
| 39 | +!!! info "Builder execution order" |
| 40 | + The builder instructions execute in**reverse order**. Each method call re-decorates the main function, so the last instruction in the chain is call first. In the example above, modules are loaded first, then the function is converted to sync, then the scope is defined, and finally dependencies are injected. |
| 41 | + |
| 42 | +###Automatic execution |
| 43 | + |
| 44 | +The entrypoint decorator accepts an optional`autocall` parameter. When set to`True`, the decorated function is automatically called: |
| 45 | +```python |
| 46 | +@entrypoint(autocall=True) |
| 47 | +asyncdefmain(dependency: Dependency): |
| 48 | +# This function runs automatically when the module is executed |
| 49 | +... |
| 50 | +``` |
| 51 | + |
| 52 | +This is particularly convenient for scripts and CLI commands where you want the entry point to execute immediately. |
| 53 | + |
| 54 | +##Integrating with ProfileLoader |
| 55 | + |
| 56 | +If you're using a[`ProfileLoader`](profiles.md#profileloader) in your project, pass it to`@entrypointmaker` using the`profile_loader` parameter: |
| 57 | +```python |
| 58 | +from injection.entrypointimport Entrypoint, entrypointmaker |
| 59 | +from injection.loadersimport ProfileLoader, PythonModuleLoader |
| 60 | + |
| 61 | +profile_loader= ProfileLoader(...) |
| 62 | + |
| 63 | +@entrypointmaker(profile_loader=profile_loader) |
| 64 | +def entrypoint[**P, T](self: Entrypoint[P, T])-> Entrypoint[P, T]: |
| 65 | +import src |
| 66 | + |
| 67 | + module_loader= PythonModuleLoader.endswith("_impl") |
| 68 | +return ( |
| 69 | +self.inject() |
| 70 | + .load_profile(Profile.DEV)# Load a specific profile |
| 71 | + .load_modules(module_loader, src) |
| 72 | + ) |
| 73 | +``` |
| 74 | + |
| 75 | +The`load_profile` method accepts a profile name and loads it before the main function executes. |
| 76 | + |
| 77 | +##Resolving dependencies in the setup |
| 78 | + |
| 79 | +You can resolve dependencies from the setup function parameters. These dependencies must be registered in the default module (not in a profile-specific module) and should preferably be transient or constant. This is particularly useful for resolving configuration to determine which profile to load: |
| 80 | +```python |
| 81 | +from dataclassesimport dataclass |
| 82 | +from injectionimport constant |
| 83 | +from injection.entrypointimport Entrypoint, entrypointmaker |
| 84 | +from injection.loadersimport PythonModuleLoader |
| 85 | +from osimport getenv |
| 86 | + |
| 87 | +@dataclass |
| 88 | +classConfig: |
| 89 | + profile: Profile |
| 90 | + |
| 91 | +@constant |
| 92 | +def_config_factory() -> Config: |
| 93 | + profile= Profile(getenv("PROFILE","development")) |
| 94 | +return Config(profile) |
| 95 | + |
| 96 | +@entrypointmaker(profile_loader=profile_loader) |
| 97 | +def entrypoint[**P, T](self: Entrypoint[P, T], config: Config)-> Entrypoint[P, T]: |
| 98 | +import src |
| 99 | + |
| 100 | + profile= config.profile# Use config to determine profile |
| 101 | + suffixes=self.profile_loader.required_module_names(profile) |
| 102 | + module_loader= PythonModuleLoader.endswith(*suffixes) |
| 103 | +return ( |
| 104 | +self.inject() |
| 105 | + .load_profile(profile) |
| 106 | + .load_modules(module_loader, src) |
| 107 | + ) |
| 108 | +``` |
| 109 | + |
| 110 | +In this example,`config` is resolved from the default module and used to dynamically load the appropriate profile. |
| 111 | + |
| 112 | +!!! warning |
| 113 | + Dependencies resolved in the entrypoint setup function must be registered in the default module (not in a profile-specific module) and should be transient or constant to avoid state issues. |