Skip to content

Commit

Permalink
expand on __main__ and scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
ucodery committed Dec 13, 2024
1 parent a064e5e commit 7304135
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 21 deletions.
82 changes: 64 additions & 18 deletions running-code/execute-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ kernelspec:
In [How to Execute a Python Script](./execute-script) you learned about two primary ways to execute a stand-alone Python script.
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.

1. You can [**execute modules**](#executable-modules) using their import name
2. You can [**execute packages**](#executable-packages) using a `__main__.py` file
3. A package can provide arbitraty command names that execute parts of themselves
1. You can [**execute modules**](executable-modules) using their import name
2. You can [**execute packages**](executable-packages) using a `__main__.py` file
3. You can [**execute functions**](named-commands) named commands using project scripts

(executable-modules)=
## 1. Executable modules

In the [Pass your script to the Python command lesson](execute-script-pass-to-python) you learned that the `python` command can
Expand Down Expand Up @@ -55,8 +56,6 @@ On your own or in small groups:

```python
#!/usr/bin/env python
# The above line is a shebang and can take the place of typing python on the command line
# This comment is below, because shebangs must be the first line!

def shiny_hello():
print("\N{Sparkles} Hello from Python \N{Sparkles}")
Expand All @@ -65,6 +64,7 @@ if __name__ == "__main__":
shiny_hello()
```

(executable-packages)=
## 2. Executable packages

The `-m` flag as described above only works for Python modules (files), but does not work for Python (sub-)packages (directories).
Expand All @@ -73,15 +73,19 @@ This means that you cannot execute a command using only the name of your package
Once your package grows, the top-level name `my_program` turns into a directory.
(See [Python Package Structure](https://www.pyopensci.org/python-package-guide/package-structure-code/python-package-structure.html)
for when and how to create a package structure).

```
project/
└── src/
└── my_program/
├── __init__.py
└── greeting.py
└── greeting/
├── __init__.py
└── hello.py
```

However, you can never execute a directory.
However, the directory is not executable.

```bash
python -m my_program
python: No module named my_program.__main__; 'my_program' is a package and cannot be directly executed
Expand All @@ -91,11 +95,44 @@ Initially, Python seems to tell you that the directory names, including your top
cannot be directly executed. But the error message contains the hint that you need to make this run properly.

[Earlier you learned](execute-script-name-eq-main) that `if __name__ == "__main__":` can protect parts of your
script from executing when it is imported, making that conditional change the file's behavior when used as a script.
There is a very similar concept that can be applied to Python packages.
Python file from executing when it is imported, making that conditional change the file's behavior when used as
a script vs when used as a module. There is a very similar concept that can be applied to Python directories.

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)
becomes a valid `import` target and that whenever the directory is imported, the code in the `__init__.py` is executed.
There is another special file Python directories can contain: [`__main__.py` module](https://docs.python.org/3/library/__main__.html#module-__main__).
Any package that contains a `__main__.py` can be execued with `python -m` exactly like a Python module. When a
Python package (directory) is executed, the code in `__main__.py` is executed, as if it was the target of the `-m`.

In this way a Python directory can segment its import behaviour from its command behaviour by using both
`__init__.py` and `__main__.py` in a very similar way to how a Python file segments this behaviour using
`if __name__ == "__main__":`.

Any package that contains a [`__main__.py` module](https://docs.python.org/3/library/__main__.html#module-__main__)
can be executed directly using the `python` command, without reference to any specific module files.
If we add to the earlier package structure, we can make the original execution command work.

```
project/
└── src/
└── my_program/ <-- directories with init and main can be imported or executed
├── __init__.py
├── __main__.py
└── greeting/ <-- directories with init an no main can be imported but not executed
├── __init__.py
└── hello.py
```

```python
# project/src/my_program/__main__.py
from .greeting.hello import shiny_hello

# notice top-level evaluations are not guarded by any conditional
shiny_hello()
```

```bash
python -m my_program
# ✨ Hello from Python ✨
```

:::{note}
The `__main__.py` file typically doesn't have an `if __name__ == "__main__":` conditional in it, as its execution
Expand All @@ -122,8 +159,12 @@ guess_my_number()

:::{attention}
Don't forget to (re)install your package after creating this file!

Unless you used an [editable install](https://www.pyopensci.org/python-package-guide/tutorials/installable-code.html#step-5-install-your-package-locally)
any additional files or changes you make won't be picked up by Python.
:::

(named-commands)=
## 3. Named Commands

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/)
Expand All @@ -135,23 +176,28 @@ These commands are configured in your project's [`pyproject.toml`](https://www.p

```toml
[project.scripts]
shiny = "my_program.greetings:shiny_hello"
shiny = "my_program.greetings.hello:shiny_hello"
```

In the above example `shiny` is the name of the command that will be made available in the shell after installation,
`my_program` is the name of your top-level package import, `greetings` is the name of the sub-package (optional, or may be
repeated as necessary to access the correct sub-package), and `shiny_hello` is the function that will be called.
`my_program.greetings.hello` is the path of import required to access the necessary function (which may contain
`.subpackage` parts, depending on how you structured your package), and `:shiny_hello` is the function (proceeded with
`:`) that will be called, **without arguments**.

A script target of `"my_program.greetings.hello:shiny_hello"` is logically equivilant to
```python
import my_program.greetings.hello

If this package was installed, the command would be made avalibel in your shell
my_program.greetings.hello.shiny_hello()
```

If this package was installed, the command would be made avalible in your shell

```bash
shiny
# ✨ Hello from Python ✨
```

The target of each `scripts` definition should always be one function within your package, which will be directly executed (without arguments)
when the command is invoked in the shell. The target function can live anywhere; it does not have to be in a `__main__.py` or under a `if __name__ == "__main__":`.

### Further exploration

On your own or in small groups:
Expand Down
6 changes: 3 additions & 3 deletions running-code/execute-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ kernelspec:

There are two primary ways to execute a Python script.

1. You can call Python from your shell and pass in the script path
2. You can call your script directly as a command from your shell
1. You can [pass your script](execute-script-pass-to-python) to Python in your shell
2. You can [call your script](execute-script-launch-command) directly as a command in your shell

(execute-script-pass-to-python)=
## 1. Pass your script to the Python command
Expand All @@ -39,7 +39,7 @@ def report_error():
print("\N{Sparkles} Hello from Python \N{Sparkles}")
```

Note that only one line is printed when this script is run.
Note that only one line is printed when this script is run.

```bash
python my_program.py
Expand Down

0 comments on commit 7304135

Please sign in to comment.