From 0830c023b9150e889fc9f971237703a025b0ef4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 19 Mar 2020 13:42:23 +0100 Subject: [PATCH] :memo: Add docs section, build a package from scratch (#71) * :memo: Add docs section, build a package from scratch * :memo: Update Package docs * :pencil2: Fix internal links --- docs/tutorial/package.md | 642 +++++++++++++++++++++++++++++++++++ docs/tutorial/using-click.md | 2 +- mkdocs.yml | 1 + 3 files changed, 644 insertions(+), 1 deletion(-) create mode 100644 docs/tutorial/package.md diff --git a/docs/tutorial/package.md b/docs/tutorial/package.md new file mode 100644 index 0000000000..672104efb0 --- /dev/null +++ b/docs/tutorial/package.md @@ -0,0 +1,642 @@ +When you create a CLI program with **Typer** you probably want to create your own Python package. + +That's what allows your users to install it and have it as an independent program that they can use in their terminal. + +And that's also required for shell auto completion to work (unless you use your program through [Typer CLI](../typer-cli.md){.internal-link target=_blank}). + +Nowadays, there are several ways and tools to create Python packages (what you install with `pip install something`). + +You might even have your favorite already. + +Here's a very opinionated, short guide, showing one of the alternative ways of creating a Python package with a **Typer** app, from scratch. + +!!! tip + If you already have a favorite way of creating Python packages, feel free to skip this. + +## Prerequisites + +For this guide we'll use Poetry. + +Poetry's docs are great, so go ahead, check them and install it. + +## Create a project + +Let's say we want to create a CLI application called `portal-gun`. + +To make sure your package doesn't collide with the package created by someone else, we'll name it with a prefix of your name. + +So, if your name is Rick, we'll call it `rick-portal-gun`. + +Create a project with Poetry: + +
+ +```console +$ poetry new rick-portal-gun + +Created package rick_portal_gun in rick-portal-gun + +// Enter the new project directory +cd ./rick-portal-gun +``` + +
+ +## Dependencies and environment + +Add `typer[all]` to your dependencies: + +
+ +```console +$ poetry add typer[all] + +// It creates a virtual environment for your project +Creating virtualenv rick-portal-gun-w31dJa0b-py3.6 in /home/rick/.cache/pypoetry/virtualenvs +Using version ^0.1.0 for typer + +Updating dependencies +Resolving dependencies... (1.2s) + +Writing lock file + +---> 100% + +Package operations: 15 installs, 0 updates, 0 removals + + - Installing zipp (3.1.0) + - Installing importlib-metadata (1.5.0) + - Installing pyparsing (2.4.6) + - Installing six (1.14.0) + - Installing attrs (19.3.0) + - Installing click (7.1.1) + - Installing colorama (0.4.3) + - Installing more-itertools (8.2.0) + - Installing packaging (20.3) + - Installing pluggy (0.13.1) + - Installing py (1.8.1) + - Installing shellingham (1.3.2) + - Installing wcwidth (0.1.8) + - Installing pytest (5.4.1) + - Installing typer (0.0.11) + +// Activate that new virtual environment +$ poetry shell + +Spawning shell within /home/rick/.cache/pypoetry/virtualenvs/rick-portal-gun-w31dJa0b-py3.6 + +// Open an editor using this new environment, for example VS Code +$ code ./ +``` + +
+ +You can see that you have a generated project structure that looks like: + +``` +. +├── poetry.lock +├── pyproject.toml +├── README.rst +├── rick_portal_gun +│   └── __init__.py +└── tests + ├── __init__.py + └── test_rick_portal_gun.py +``` + +## Create your app + +Now let's create an extremely simple **Typer** app. + +Create a file `rick_portal_gun/main.py` with: + +```Python +import typer + + +app = typer.Typer() + + +@app.callback() +def callback(): + """ + Awesome Portal Gun + """ + + +@app.command() +def shoot(): + """ + Shoot the portal gun + """ + typer.echo("Shooting portal gun") + + +@app.command() +def load(): + """ + Load the portal gun + """ + typer.echo("Loading portal gun") +``` + +!!! tip + As we are creating an installable Python package, there's no need to add a section with `if __name__ == "__main__:`. + +## Modify the README + +Let's change the README. By default it's a file `README.rst`. + +Let's change it to `README.md`. So, change the extension from `.rst` to `.md`. + +So that we can use Markdown instead of reStructuredText. + +And change the file to have something like: + +```Markdown +# Portal Gun + +The awesome Portal Gun +``` + +## Modify your project metadata + +Edit your file `pyproject.toml`. + +It would look something like: + +```TOML +[tool.poetry] +name = "rick-portal-gun" +version = "0.1.0" +description = "" +authors = ["Rick Sanchez "] + +[tool.poetry.dependencies] +python = "^3.6" +typer = {extras = ["all"], version = "^0.1.0"} + +[tool.poetry.dev-dependencies] +pytest = "^5.2" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" +``` + +We changed the default README, so let's make it use the new `README.md`. + +Add the line: + +```TOML hl_lines="6" +[tool.poetry] +name = "rick-portal-gun" +version = "0.1.0" +description = "" +authors = ["Rick Sanchez "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.6" +typer = {extras = ["all"], version = "^0.1.0"} + +[tool.poetry.dev-dependencies] +pytest = "^5.2" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" +``` + +## Add a "script" + +We are creating a Python package that can be installed with `pip install`. + +But we want it to provide a CLI program that can be executed in the shell. + +To do that, we add a configuration to the `pyproject.toml` in the section `[tool.poetry.scripts]`: + +```TOML hl_lines="8 9" +[tool.poetry] +name = "rick-portal-gun" +version = "0.1.0" +description = "" +authors = ["Rick Sanchez "] +readme = "README.md" + +[tool.poetry.scripts] +rick-portal-gun = "rick_portal_gun.main:app" + +[tool.poetry.dependencies] +python = "^3.6" +typer = {extras = ["all"], version = "^0.1.0"} + +[tool.poetry.dev-dependencies] +pytest = "^5.2" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" +``` + +Here's what that line means: + +`rick-portal-gun`: will be the name of the CLI program. That's how we will call it in the terminal once it is installed. Like: + +
+ +```console +$ rick-portal-gun + +// Something happens here ✨ +``` + +
+ +`rick_portal_gun.main`, in the part `"rick_portal_gun.main:app"`, with underscores, refers to the Python module to import. That's what someone would use in a section like: + +```Python +from rick_portal_gun.main import # something goes here +``` + +The `app` in `"rick_portal_gun.main:app"` is the thing to import from the module, and to call as a function, like: + +```Python +from rick_portal_gun.main import app +app() +``` + +That config section tells Poetry that when this package is installed we want it to create a command line program called `rick-portal-gun`. + +And that the object to call (like a function) is the one in the variable `app` inside of the module `rick_portal_gun.main`. + +## Install your package + +That's what we need to create a package. + +You can now install it: + +
+ +```console +$ poetry install + +Installing dependencies from lock file + +No dependencies to install or update + + - Installing rick-portal-gun (0.1.0) +``` + +
+ +## Try your CLI program + +Your package is installed in the environment created by Poetry, but you can already use it. + +
+ +```console +// You can use the which program to check which rick-portal-gun program is available (if any) +$ which rick-portal-gun + +// You get the one from your environment +/home/rick/.cache/pypoetry/virtualenvs/rick-portal-gun-w31dJa0b-py3.6/bin/rick-portal-gun + +// Try it +$ rick-portal-gun + +// You get all the standard help +Usage: rick-portal-gun [OPTIONS] COMMAND [ARGS]... + + Awesome Portal Gun + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + + --help Show this message and exit. + +Commands: + load Load the portal gun + shoot Shoot the portal gun +``` + +
+ +## Create a wheel package + +Python packages have a standard format called a "wheel". It's a file that ends in `.whl`. + +You can create a wheel with Poetry: + +
+ +```console +$ poetry build + +Building rick-portal-gun (0.1.0) + - Building sdist + - Built rick-portal-gun-0.1.0.tar.gz + + - Building wheel + - Built rick_portal_gun-0.1.0-py3-none-any.whl +``` + +
+ +After that, if you check in your project directory, you should now have a couple of extra files at `./dist/`: + +``` hl_lines="3 4" +. +├── dist +│   ├── rick_portal_gun-0.1.0-py3-none-any.whl +│   └── rick-portal-gun-0.1.0.tar.gz +├── pyproject.toml +├── README.md +├── ... +``` + +The `.whl` is the wheel file. You can send that wheel file to anyone and they can use it to install your program (we'll see how to upload it to PyPI in a bit). + +## Test your wheel package + +Now you can open another terminal and install that package from the file for your own user with: + +
+ +```console +$ pip install --user /home/rock/code/rick-portal-gun/dist/rick_portal_gun-0.1.0-py3-none-any.whl + +---> 100% +``` + +
+ +!!! warning + The `--user` is important, that ensures you install it in your user's directory and not in the global system. + + If you installed it in the global system (e.g. with `sudo`) you could install a version of a library (e.g. a sub-dependency) that is incompatible with your system. + +!!! tip + Bonus points if you use `pipx` to install it while keeping an isolated environment for your Python CLI programs 🚀 + +Now you have your CLI program installed. And you can use it freely: + +
+ +```console +$ rick-portal-gun shoot + +// It works 🎉 +Shooting portal gun +``` + +
+ +Having it installed globally (and not in a single environment), you can now install completion globally for it: + +
+ +```console +$ rick-portal-gun --install-completion + +zsh completion installed in /home/user/.zshrc. +Completion will take effect once you restart the terminal. +``` + +
+ +!!! tip + If you want to remove completion you can just delete the added line in that file. + +And after you restart the terminal you will get completion for your new CLI program: + +
+ +```console +$ rick-portal-gun [TAB][TAB] + +// You get completion for your CLI program ✨ +load -- Load the portal gun +shoot -- Shoot the portal gun +``` + +
+ +## Publish to PyPI (optional) + +You can publish that new package to PyPI to make it public, so others can install it easily. + +So, go ahead and create an account there (it's free). + +### PyPI API token + +To do it, you first need to configure a PyPI auth token. + +Login to PyPI. + +And then go to https://pypi.org/manage/account/token/ to create a new token. + +Let's say your new API token is: + +``` +pypi-wubalubadubdub-deadbeef1234 +``` + +Now configure Poetry to use this token with the command `poetry config pypi-token.pypi`: + +
+ +```console +$ poetry config pypi-token.pypi pypi-wubalubadubdub-deadbeef1234 +// It won't show any output, but it's already configured +``` + +
+ +### Publish to PyPI + +Now you can publish your package with Poetry. + +You could build the package (as we did above) and then publish later, or you could tell poetry to build it before publishing in one go: + +
+ +```console +$ poetry publish --build + +# There are 2 files ready for publishing. Build anyway? (yes/no) [no] $ yes + +---> 100% + +Building rick-portal-gun (0.1.0) + - Building sdist + - Built rick-portal-gun-0.1.0.tar.gz + + - Building wheel + - Built rick_portal_gun-0.1.0-py3-none-any.whl + +Publishing rick-portal-gun (0.1.0) to PyPI + - Uploading rick-portal-gun-0.1.0.tar.gz 100% + - Uploading rick_portal_gun-0.1.0-py3-none-any.whl 100% +``` + +
+ +Now you can go to PyPI and check your projects at https://pypi.org/manage/projects/. + +You should now see your new "rick-portal-gun" package. + +### Install from PyPI + +Now to see that we can install it form PyPI, open another terminal, and uninstall the currently installed package. + +
+ +```console +$ pip uninstall rick-portal-gun + +Found existing installation: rick-portal-gun 0.1.0 +Uninstalling rick-portal-gun-0.1.0: + Would remove: + /home/user/.local/bin/rick-portal-gun + /home/user/.local/lib/python3.6/site-packages/rick_portal_gun-0.1.0.dist-info/* + /home/user/.local/lib/python3.6/site-packages/rick_portal_gun/* +# Proceed (y/n)? $ y + Successfully uninstalled rick-portal-gun-0.1.0 +``` + +
+ +And now install it again, but this time using just the name, so that `pip` pulls it from PyPI: + +
+ +```console +$ pip install --user rick-portal-gun + +// Notice that it says "Downloading" 🚀 +Collecting rick-portal-gun + Downloading rick_portal_gun-0.1.0-py3-none-any.whl (1.8 kB) +Requirement already satisfied: typer[all]<0.0.12,>=0.0.11 in ./.local/lib/python3.6/site-packages (from rick-portal-gun) (0.0.11) +Requirement already satisfied: click<7.2.0,>=7.1.1 in ./anaconda3/lib/python3.6/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (7.1.1) +Requirement already satisfied: colorama; extra == "all" in ./anaconda3/lib/python3.6/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (0.4.3) +Requirement already satisfied: shellingham; extra == "all" in ./anaconda3/lib/python3.6/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (1.3.1) +Installing collected packages: rick-portal-gun +Successfully installed rick-portal-gun-0.1.0 +``` + +
+ +And now test the newly installed package from PyPI: + +
+ +```console +$ rick-portal-gun load + +// It works! 🎉 +Loading portal gun +``` + +
+ +## Generate docs with **Typer CLI** (optional) + +You can install and use [Typer CLI](../typer-cli.md){.internal-link target=_blank} to generate docs for your package. + +After installing it, you can use it to generate a new `README.md`: + +
+ +```console +$ typer rick_portal_gun.main utils docs --output README.md --name rick-portal-gun + +Docs saved to: README.md +``` + +
+ +You just have to pass it the module to import (`rick_portal_gun.main`) and it will detect the `typer.Typer` app automatically. + +By specifying the `--name` of the program it will be able to use it while generating the docs. + +### Publish a new version with the docs + +Now you can publish a new version with the updated docs. + +For that you need to first increase the version in `pyproject.toml`: + +```TOML hl_lines="3" +[tool.poetry] +name = "rick-portal-gun" +version = "0.2.0" +description = "" +authors = ["Rick Sanchez "] +readme = "README.md" + +[tool.poetry.scripts] +rick-portal-gun = "rick_portal_gun.main:app" + +[tool.poetry.dependencies] +python = "^3.6" +typer = {extras = ["all"], version = "^0.1.0"} + +[tool.poetry.dev-dependencies] +pytest = "^5.2" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" +``` + +And in the file `rick_portal_gun/__init__.py`: + +```Python +__version__ = '0.2.0' +``` + +And then build and publish again: + +
+ +```console +$ poetry publish --build + +---> 100% + +Building rick-portal-gun (0.2.0) + - Building sdist + - Built rick-portal-gun-0.2.0.tar.gz + + - Building wheel + - Built rick_portal_gun-0.2.0-py3-none-any.whl + +Publishing rick-portal-gun (0.2.0) to PyPI + - Uploading rick-portal-gun-0.2.0.tar.gz 100% + - Uploading rick_portal_gun-0.2.0-py3-none-any.whl 100% +``` + +
+ +And now you can go to PyPI, to the project page, and reload it, and it will now have your new generated docs. + +## What's next + +This is a very simple guide. You could add many more steps. + +For example, you should use Git, the version control system, to save your code. + +You can add a lot of extra metadata to your `pyproject.toml`, check the docs for Poetry: Libraries. + +You could use `pipx` to manage your installed CLI Python programs in isolated environments. + +Maybe use automatic formatting with Black. + +You'll probably want to publish your code as open source to GitHub. + +And then you could integrate a CI tool to run your tests and deploy your package automatically. + +And there's a long etc. But now you have the basics and you can continue on your own 🚀. \ No newline at end of file diff --git a/docs/tutorial/using-click.md b/docs/tutorial/using-click.md index 6aa9e181de..010aa28fdc 100644 --- a/docs/tutorial/using-click.md +++ b/docs/tutorial/using-click.md @@ -185,6 +185,6 @@ Most of the functionality provided by decorators in Click has an alternative way For example, to access the context, you can just declare a function parameter of type `typer.Context`. !!! tip - You can read more about using the context in the docs: [Commands: Using the Context](./commands/using-the-context.md){.internal-link target=_blank} + You can read more about using the context in the docs: [Commands: Using the Context](commands/context.md){.internal-link target=_blank} But if you need to use something based on Click decorators, you can always generate a Click object using the methods described above, and use it as you would normally use Click. diff --git a/mkdocs.yml b/mkdocs.yml index cb467ab14a..3bd553f30a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ nav: - Launching Applications: 'tutorial/launch.md' - Testing: 'tutorial/testing.md' - Using Click: 'tutorial/using-click.md' + - Building a Package: 'tutorial/package.md' - Typer CLI - completion for small scripts: 'typer-cli.md' - Alternatives, Inspiration and Comparisons: 'alternatives.md' - Help Typer - Get Help: 'help-typer.md'