Skip to content

Commit 9511ba8

Browse files
authored
Merge pull request #97 from ucodery/running
feat(lesson): Add execute python code lessons
2 parents af5cc9c + 1bbf1b4 commit 9511ba8

File tree

6 files changed

+495
-7
lines changed

6 files changed

+495
-7
lines changed

CONTRIBUTING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,23 +193,23 @@ There are different sessions in nox related to building the docs: `docs`, `docs-
193193
* `docs`: this session builds the guide and opens it in your browser.
194194

195195
```bash
196-
nox -e docs
196+
nox -s docs
197197
```
198198

199199
To see the guide built locally, open the file `_build/html/index.html` in your browser.
200200

201201
* `docs-test`: this session runs the tests for the guide.
202202

203203
```bash
204-
nox -e docs-test
204+
nox -s docs-test
205205
```
206206

207207
If the tests fail, you will see an error message in your terminal. You need to fix the errors before submitting your pull request.
208208

209209
* `docs-live`: this session builds the guide and opens it in your browser with live reloading.
210210

211211
```bash
212-
nox -e docs-live
212+
nox -s docs-live
213213
```
214214

215215
open the local version of the guide in your browser at ``localhost`` shown in the terminal.
@@ -219,7 +219,7 @@ There are different sessions in nox related to building the docs: `docs`, `docs-
219219
Before submitting your pull request, make sure to run the tests and check the formatting of your code.
220220

221221
```bash
222-
nox -e docs-test
222+
nox -s docs-test
223223
```
224224

225225
If the tests fail, you will see an error message in your terminal. You need to fix the errors before submitting your pull request.

conf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@
137137
"_build",
138138
"Thumbs.db",
139139
".DS_Store",
140+
".direnv",
140141
".github",
141142
".nox",
143+
".venv",
142144
"README.md",
143145
"**/README.md",
144146
"styles/*",
@@ -203,4 +205,4 @@
203205
"codeautolink.match_block",
204206
"codeautolink.match_name",
205207
"codeautolink.failed_resolve",
206-
]
208+
]

index.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,23 @@
3434
* [Write Conditionals to redirect code](conditionals)
3535
* [Common Python exceptions](common-exceptions)
3636

37-
<!--
37+
<!--
3838
TODO: let's merge this with the conditional lesson
39-
* [Conditionals with alternatives](conditionals-alternatives)
39+
* [Conditionals with alternatives](conditionals-alternatives)
4040
-->
4141
:::
4242
::::
4343

44+
::::{grid-item}
45+
:::{card} [✿ Running Code ✿](running-code/intro)
46+
:class-card: left-aligned
47+
48+
* [Execute a Python Package](running-code/execute-package)
49+
* [Execute a Python Script](running-code/execute-script)
50+
51+
:::
52+
::::
53+
4454
::::{grid-item}
4555
:::{card} [✿ Share Code ✿](publish-share-code/intro)
4656
:class-card: left-aligned
@@ -86,6 +96,15 @@ Clean Code <clean-modular-code/intro-clean-code>
8696
Optimize Code <code-workflow-logic/intro>
8797
:::
8898

99+
:::{toctree}
100+
:hidden:
101+
:caption: Running Code
102+
:maxdepth: 2
103+
104+
Package Code <running-code/intro>
105+
:::
106+
107+
89108
:::{toctree}
90109
:hidden:
91110
:caption: Share Code

running-code/execute-package.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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

Comments
 (0)