|
| 1 | +--- |
| 2 | +jupytext: |
| 3 | + text_representation: |
| 4 | + extension: .md |
| 5 | + format_name: myst |
| 6 | + format_version: 0.13 |
| 7 | + jupytext_version: 1.16.4 |
| 8 | +kernelspec: |
| 9 | + display_name: Python 3 (ipykernel) |
| 10 | + language: python |
| 11 | + name: python3 |
| 12 | +--- |
| 13 | + |
| 14 | +# How to Execute a Python Package |
| 15 | + |
| 16 | +In [How to Execute a Python Script](./execute-script) you learned about two primary ways to execute a stand-alone Python script. |
| 17 | +There are two other ways to execute Python code from the command line, both of which work for code that has been formatted as a package. |
| 18 | + |
| 19 | +1. You can [**execute modules**](executable-modules) using their import name |
| 20 | +2. You can [**execute packages**](executable-packages) using a `__main__.py` file |
| 21 | +3. You can [**execute functions**](named-commands) named commands using project scripts |
| 22 | + |
| 23 | +(executable-modules)= |
| 24 | +## 1. Executable modules |
| 25 | + |
| 26 | +In the [Pass your script to the Python command lesson](execute-script-pass-to-python) you learned that the `python` command can |
| 27 | +be passed a file path for execution. Alternatively you can also pass the name of a module, exactly as would be used after an `import`. |
| 28 | +In this case, Python will look up the module referenced in the |
| 29 | +[currently active environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#create-and-use-virtual-environments), |
| 30 | +and will execute it as a script. |
| 31 | + |
| 32 | +This execution mode is performed with the `-m` flag, as in `python -m site`. It can be used in place of a file |
| 33 | +path, but cannot be used in combination with a path, as there can only be one executing module. |
| 34 | + |
| 35 | +:::{tip} |
| 36 | +These commands both do the same thing, but the latter is easier to remember and much more portable, as it doesn't rely |
| 37 | +on the local machine's file system details. |
| 38 | + |
| 39 | +```bash |
| 40 | +python ./.venv/lib/python3.12/site-packages/pip/__main__.py |
| 41 | +``` |
| 42 | + |
| 43 | +```bash |
| 44 | +python -m pip |
| 45 | +``` |
| 46 | +::: |
| 47 | + |
| 48 | +### Further exploration |
| 49 | + |
| 50 | +On your own or in small groups: |
| 51 | + |
| 52 | +* Install the `my_program.py` module from the [last lesson](execute-script-launch-command) |
| 53 | +* Try to get the same greeting as before using `python -m my_program`. |
| 54 | + |
| 55 | +**my_program.py** which prints out `✨ Hello from Python ✨` when executed as a script. |
| 56 | + |
| 57 | +```python |
| 58 | +#!/usr/bin/env python |
| 59 | + |
| 60 | +def shiny_hello(): |
| 61 | + print("\N{Sparkles} Hello from Python \N{Sparkles}") |
| 62 | + |
| 63 | +if __name__ == "__main__": |
| 64 | + shiny_hello() |
| 65 | +``` |
| 66 | + |
| 67 | +(executable-packages)= |
| 68 | +## 2. Executable packages |
| 69 | + |
| 70 | +The `-m` flag as described above only works for Python modules (files), but does not work for Python (sub-)packages (directories). |
| 71 | +This means that you cannot execute a command using only the name of your package when it is structured to use directories |
| 72 | + |
| 73 | +Once your package grows, the top-level name `my_program` turns into a directory. |
| 74 | +(See [Python Package Structure](https://www.pyopensci.org/python-package-guide/package-structure-code/python-package-structure.html) |
| 75 | +for when and how to create a package structure). |
| 76 | + |
| 77 | +``` |
| 78 | +project/ |
| 79 | +└── src/ |
| 80 | + └── my_program/ |
| 81 | + ├── __init__.py |
| 82 | + └── greeting/ |
| 83 | + ├── __init__.py |
| 84 | + └── hello.py |
| 85 | +``` |
| 86 | + |
| 87 | +However, the directory is not executable. |
| 88 | + |
| 89 | +```bash |
| 90 | +python -m my_program |
| 91 | +python: No module named my_program.__main__; 'my_program' is a package and cannot be directly executed |
| 92 | +``` |
| 93 | + |
| 94 | +Initially, Python seems to tell you that the directory names, including your top-level package name, |
| 95 | +cannot be directly executed. But the error message contains the hint that you need to make this run properly. |
| 96 | + |
| 97 | +[Earlier you learned](execute-script-name-eq-main) that `if __name__ == "__main__":` can protect parts of your |
| 98 | +Python file from executing when it is imported, making that conditional change the file's behavior when used as |
| 99 | +a script vs when used as a module. There is a very similar concept that can be applied to Python directories. |
| 100 | + |
| 101 | +You may already know that a directory that contains an [`__init__.py` module](https://www.pyopensci.org/python-package-guide/tutorials/installable-code.html#what-is-an-init-py-file) |
| 102 | +becomes a valid `import` target and that whenever the directory is imported, the code in the `__init__.py` is executed. |
| 103 | +There is another special file Python directories can contain: [`__main__.py` module](https://docs.python.org/3/library/__main__.html#module-__main__). |
| 104 | +Any package that contains a `__main__.py` can be execued with `python -m` exactly like a Python module. When a |
| 105 | +Python package (directory) is executed, the code in `__main__.py` is executed, as if it was the target of the `-m`. |
| 106 | + |
| 107 | +In this way a Python directory can segment its import behaviour from its command behaviour by using both |
| 108 | +`__init__.py` and `__main__.py` in a very similar way to how a Python file segments this behaviour using |
| 109 | +`if __name__ == "__main__":`. |
| 110 | + |
| 111 | +If we add to the earlier package structure, we can make the original execution command work. |
| 112 | + |
| 113 | +``` |
| 114 | +project/ |
| 115 | +└── src/ |
| 116 | + └── my_program/ <-- directories with init and main can be imported or executed |
| 117 | + ├── __init__.py |
| 118 | + ├── __main__.py |
| 119 | + └── greeting/ <-- directories with init an no main can be imported but not executed |
| 120 | + ├── __init__.py |
| 121 | + └── hello.py |
| 122 | +``` |
| 123 | + |
| 124 | +```python |
| 125 | +# project/src/my_program/__main__.py |
| 126 | +from .greeting.hello import shiny_hello |
| 127 | + |
| 128 | +shiny_hello() |
| 129 | +``` |
| 130 | + |
| 131 | +```bash |
| 132 | +python -m my_program |
| 133 | +# ✨ Hello from Python ✨ |
| 134 | +``` |
| 135 | + |
| 136 | +:::{note} |
| 137 | +The `__main__.py` file typically doesn't have an `if __name__ == "__main__":` conditional in it, as its execution |
| 138 | +is already separated out from the rest of the package. |
| 139 | +::: |
| 140 | + |
| 141 | +### Further exploration |
| 142 | + |
| 143 | +- Try to separate `my_program` into a package containing two files, including one `__main__.py` |
| 144 | +- Try to get `python -m my_program` to work as before |
| 145 | +- Does importing `my_program` still work [as before the separation](execute-script-name-eq-main)? |
| 146 | + |
| 147 | +```python |
| 148 | +import my_program |
| 149 | + |
| 150 | +def guess_my_number(): |
| 151 | + my_program.shiny_hello() |
| 152 | + print("Was your number 42?") |
| 153 | + |
| 154 | +guess_my_number() |
| 155 | +# ✨ Hello from Python ✨ |
| 156 | +# Was your number 42? |
| 157 | +``` |
| 158 | + |
| 159 | +:::{attention} |
| 160 | +Don't forget to (re)install your package after creating this file! |
| 161 | + |
| 162 | +Unless you used an [editable install](https://www.pyopensci.org/python-package-guide/tutorials/installable-code.html#step-5-install-your-package-locally) |
| 163 | +any additional files or changes you make won't be picked up by Python. |
| 164 | +::: |
| 165 | + |
| 166 | +(named-commands)= |
| 167 | +## 3. Named Commands |
| 168 | + |
| 169 | +The final way to make Python code executable directly from the command line is to include a special [entrypoint](https://packaging.python.org/en/latest/specifications/entry-points/) |
| 170 | +into the package metadata. Entrypoints are a general purpose plug-in system for Python packages, but the |
| 171 | +[`console_scripts`](https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts) |
| 172 | +entry is specifically targeted at creating executable commands on systems that install the package. |
| 173 | + |
| 174 | +These entrypoints and their commands are configured in your project's [`pyproject.toml`](https://www.pyopensci.org/python-package-guide/tutorials/pyproject-toml.html#what-is-a-pyproject-toml-file) file in the [`[project.scripts]`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#creating-executable-scripts) table. |
| 175 | + |
| 176 | +```toml |
| 177 | +[project.scripts] |
| 178 | +shiny = "my_program.greetings.hello:shiny_hello" |
| 179 | +``` |
| 180 | + |
| 181 | +In the above example `shiny` is the name of the command that will be made available in the shell after installation, |
| 182 | +`my_program.greetings.hello` is the path of import required to access the necessary function (which may contain |
| 183 | +`.subpackage` parts, depending on how you structured your package), and `:shiny_hello` is the function (proceeded with |
| 184 | +`:`) that will be called, **without arguments**. |
| 185 | + |
| 186 | +A script target of `"my_program.greetings.hello:shiny_hello"` is logically equivilant to |
| 187 | +```python |
| 188 | +import my_program.greetings.hello |
| 189 | + |
| 190 | +my_program.greetings.hello.shiny_hello() |
| 191 | +``` |
| 192 | + |
| 193 | +If this package was installed, the command would be made avalible in your shell |
| 194 | + |
| 195 | +```bash |
| 196 | +shiny |
| 197 | +# ✨ Hello from Python ✨ |
| 198 | +``` |
| 199 | + |
| 200 | +### Further exploration |
| 201 | + |
| 202 | +On your own or in small groups: |
| 203 | + |
| 204 | +- List some advantages of making a Python package executable over providing a script entry point. |
| 205 | +- List some disadvantages of making a Python package executable over providing a script entry point. |
| 206 | +- Review the Pros section from [How to Execute a Python Script](execute-script-comparison) |
| 207 | + - Do you see any similarities between executable packages and executable script files? |
| 208 | + - Do you notice any similarities between entrypoint scripts and executable script files? |
0 commit comments