Project Management with PDM#

Part 1 of notes from talk given to the WSU Python Working Group on February 8, 2023. See Part 2 here.

In this chapter, I show the basics of project management with PDM: how to create a project, add dependencies, and add development dependencies.

This tutorial is primarily aimed at macOS and Linux users, although the commands for Windows should mostly translate.

Note

A companion repository with the example project created in these notes is available here.

Requirements#

  • The ability to get python executables of different versions, such as with pyenv or conda

  • PDM available globally

These notes were last updated:

date
Sat May 20 13:24:18 PDT 2023

The version of PDM used is:

pdm --version
PDM, version 2.6.1

Set the python version and initialize the project#

First, create the project directory and cd into it:

mkdir eeskew-pwg-test-000
cd eeskew-pwg-test-000

Important

Because this is a throwaway test project, it is important that you give your project a name that won’t conflict with any other package on PyPI or TestPyPI. Adding your name and some numbers is a good way to ensure this.

Here, we’ll use python version 3.11, but you may change this to be whatever you like. I’ll cover two methods of setting the python version: using pyenv, and using conda.

Install python 3.11 if it is not already (see installed versions with pyenv versions):

pyenv install 3.11

Set the local python version for this project and initialize using that version:

pyenv local 3.11
pdm init --python python

pyenv local creates a file .python-version, which pyenv reads and redirects the command python to the installed python3.11. Thus, we only need to tell pdm to use the usual python executable.

PDM can use conda to create your virtual environment. To do this simply, we create the virtual environment before initializing the project, so that we can pass the right python executable to pdm init.

pdm venv create -w conda 3.11
pdm init --python .venv/bin/python

pdm init options#

For

Would you like to create a virtualenv with <path-to-python>? [y/n] (y):

Ensure you select y. Otherwise, PDM will operate in “PEP 582 mode” - see note on this below.

For

Is the project a library that is installable?
If yes, we will need to ask a few more questions to include the project name and build backend [y/n] (n):

select y.

If you want to use pdm-bump in the test-publish script described in Part 2, you’ll need to use the now-deprecated pdm-pep517 backend, so for:

Which build backend to use?
0. pdm-backend
1. setuptools
2. flit-core
3. hatchling
4. pdm-pep517
Please select (0):

select 4.

Otherwise, all the default options should be good, except where you want to fill in your own information (project description, email, etc).

The PDM project#

Let’s take a look at what we’ve created:

ls -a
.   .git        .pdm-python      .venv      pyproject.toml
..  .gitignore  .python-version  README.md

The relevant files created are the README.md; .pdm.toml, which holds local configuration for this PDM project; and pyproject.toml, which holds project tool configuration and package metadata.

See also

PDM has also created a virtual environment for us in the .venv directory. This is where our package and its dependencies will be installed. If you are unfamiliar with virtual environments, I recommend this article.

Note

PDM supports the rejected Python Enhancement Proposal (PEP) 582. An alternative to virtual environments, PEP 582 would automatically get dependencies from a __pypackages__ directory in the project root, without having to activate a virtualenv. See the PDM docs on PEP 582 here. We will not be using PEP 582 mode in this tutorial.

The pyproject.toml file#

The central location of our project configuration is pyproject.toml:

# pyproject.toml

[project]
name = "eeskew-pwg-test-000"
version = "0.1.0"
description = "A test project for presentation to the WSU Python Working Group."
authors = [
    {name = "Edward Eskew", email = "edward.eskew@wsu.edu"},
]
dependencies = []
requires-python = ">=3.11"
readme = "README.md"
license = {text = "MIT"}

[build-system]
requires = ["pdm-pep517>=1.0"]
build-backend = "pdm.pep517.api"

This file is written in .toml format, which stands for Tom’s Obvious Minimal Language.

The project table contains the metadata needed to install our project. Its values thus far were set by the options we chose while running pdm init.

The build-system section tells the build frontend (e.g. pip) what build backend to use - the build backend is what will actually create the distribution artifacts (wheels and sdists), which we’ll see later. See PEP 517 for more information.

See also

See the PDM docs on writing pyproject.toml for more on what can be specified in this file.

Adding code#

First we create our package directory in src layout, with an empty (for now) __init__.py file to indicate that it is a python package:

mkdir src
mkdir src/eeskew_pwg_test_000
touch src/eeskew_pwg_test_000/__init__.py

Note

By convention, we use snake_case for the package name, while we use kebab-case for the repository name.

Add a module#

Let’s add some code in src/eeskew_pwg_test_000/sarcasm.py:

# src/eeskew_pwg_test_000/sarcasm.py
def sarcasm(s):
    """Convert string `s` to sArCaSm TeXt."""
    out = ""
    for i, c in enumerate(s):
        if i % 2 == 0:
            out += c.lower()

        else:
            out += c.upper()

    return out

The actual content of this code is not too important for the purposes of these notes, but for completeness, all it does is capitalize and lowercase alternating letters in a string.

Install the project#

To make our code available in the virtual environment, we have to install it:

pdm install
Lock file does not exist
Updating the lock file...
?25l Fetching hashes for resolved packages...
🔒 Lock successful
Changes are written to pdm.lock.
?25l Fetching hashes for resolved packages...
All packages are synced to date, nothing to do.
Installing the project as an editable package...
   Install eeskew-pwg-test-000 0.1.0 successful

🎉 All complete!

?25h

Now we can import our package:

pdm run python -c 'from eeskew_pwg_test_000.sarcasm import sarcasm; print(sarcasm("Hello world!"))'
hElLo wOrLd!

Note we have to type pdm run before our command for it to be run within our project environment.

Tip

If you don’t want to type pdm run every time before a command to be run in the project virtual environment, you can activate the environment, which will modify your sys.prefix to point to the .venv directory. See the python venv docs here for more on how virtual environments work.

pdm will print the command to activate the project virtual environment with the command pdm venv activate. You can copy and paste that output, or, if you want to activate the environment in one line, use:

eval $(pdm venv activate)

To simplify things further, add this as an alias to your ~/.bashrc or ~/.bash_profile (and don’t forget to restart your shell or source ~/.bashrc after):

# ~/.bashrc
alias pdm-activate='eval $(pdm venv activate)'

This will let you activate the environment with the command pdm-activate.

You can deactivate an active virtual environment with the command deactivate. See also the PDM docs on virtualenv activation.

The pdm.lock file#

Running pdm install also created a new file, pdm.lock:

ls
README.md  pdm.lock  pyproject.toml  src
# pdm.lock
# This file is @generated by PDM.
# It is not intended for manual editing.

[metadata]
lock_version = "4.2"
cross_platform = true
groups = ["default"]
content_hash = "sha256:86165c41f17b4b263a688544a3ebc55eccc1713dd177c40649b2e936dab66751"

[metadata.files]

This is a lockfile, which will contain the exact versions of each project dependency we install. It is useful for creating a perfect reproduction of the project virtual environment, which keeps our development reproducible over time and across different machines.

Right now, we have not installed anything other than the project itself, so it is essentially empty.

Add a dependency#

Let’s add a dependency to our project, cowsay:

pdm add cowsay
Adding packages to default dependencies: cowsay
 Fetching hashes for resolved packages...
🔒 Lock successful
Changes are written to pyproject.toml.
?25l Fetching hashes for resolved packages...
Synchronizing working set with lock file: 1 to add, 0 to update, 0 to remove

   Install cowsay 5.0 successful
Installing the project as an editable package...
   Update eeskew-pwg-test-000 0.1.0 -> 0.1.0 successful

🎉 All complete!

?25h

What did pdm add do?#

cowsay now appears as a dependency in pyproject.toml:

# pyproject.toml
...

authors = [
    {name = "Edward Eskew", email = "edward.eskew@wsu.edu"},
]
dependencies = [
    "cowsay>=5.0",
]
requires-python = ">=3.11"
readme = "README.md"
license = {text = "MIT"}

...

We’ve also updated pdm.lock to include cowsay:

# pdm.lock
# This file is @generated by PDM.
# It is not intended for manual editing.

[[package]]
name = "cowsay"
version = "5.0"
summary = "The famous cowsay for GNU/Linux is now available for python"

[metadata]
lock_version = "4.2"
cross_platform = true
groups = ["default"]
content_hash = "sha256:a04c8eb9409090bc9acb94e5cec5ef19afdecacb2c169628142af5ca472135f0"

[metadata.files]
"cowsay 5.0" = [
    {url = "https://files.pythonhosted.org/packages/6b/b8/9f497fd045d74fe21d91cbe8debae0b451229989e35b539d218547d79fc6/cowsay-5.0.tar.gz", hash = "sha256:c00e02444f5bc7332826686bd44d963caabbaba9a804a63153822edce62bbbf3"},
]

We can now import cowsay:

pdm run python -c 'import cowsay; cowsay.cow("moo!")'
  ____
| moo! |
  ====
    \
     \
       ^__^
       (oo)\_______
       (__)\       )\/\
           ||----w |
           ||     ||

See also

See the PDM docs on managing dependencies for more information.

Adding more code#

Let’s add a new function to src/eeskew_pwg_test_000/sarcasm.py:

# src/eeskew_pwg_test_000/sarcasm.py
import cowsay


...


def sarcastic_cowsay(s):
    """Cowsay `s`, sArCaStIcAlLy."""
    sarcastic_s = sarcasm(s)
    cowsay.cow(sarcastic_s)

We can now run this new function:

pdm run python -c 'from eeskew_pwg_test_000.sarcasm import sarcastic_cowsay; sarcastic_cowsay("mooo!")'
  _____
| mOoO! |
  =====
     \
      \
        ^__^
        (oo)\_______
        (__)\       )\/\
            ||----w |
            ||     ||

Note

We didn’t have to re-run pdm install to use our new function - this is because PDM installs our eeskew_pwg_test_000 package in “editable mode”, which acts sort of like a symlink between the source code and the installed files in the .venv directory.

Add a development dependency#

The dependencies listed in the project.dependencies section of pyproject.toml will all be installed when someone runs pip install eeskew-pwg-test-000. What if we have dependencies we only want in our development environment?

Let’s add black, a tool to automatically format our code:

pdm add -d black
Adding group dev to lockfile
Adding packages to dev dev-dependencies: black
 Fetching hashes for resolved packages...
🔒 Lock successful
Changes are written to pyproject.toml.
?25l Fetching hashes for resolved packages...
Synchronizing working set with lock file: 6 to add, 0 to update, 0 to remove

   Installing black 23.3.0...
   Installing black 23.3.0...         
   Installing click 8.1.3...          
   Installing black 23.3.0...         
   Installing click 8.1.3...          
   Installing mypy-extensions 1.0.0...
   Installing black 23.3.0...         
   Installing click 8.1.3...          
   Installing mypy-extensions 1.0.0...
   Installing packaging 23.1...       
   Installing black 23.3.0...         
   Installing click 8.1.3...          
   Installing mypy-extensions 1.0.0...
   Installing packaging 23.1...       
   Installing pathspec 0.11.1...      
   Install mypy-extensions 1.0.0 successful
   Installing black 23.3.0...         
   Installing click 8.1.3...          
   Installing mypy-extensions 1.0.0...
   Installing packaging 23.1...       
   Installing pathspec 0.11.1...      
   Installing black 23.3.0...      
   Installing click 8.1.3...       
   Installing packaging 23.1...    
   Installing pathspec 0.11.1...   
   Install platformdirs 3.5.1 successful
   Installing black 23.3.0...      
   Installing click 8.1.3...       
   Installing packaging 23.1...    
   Installing pathspec 0.11.1...   
   Install pathspec 0.11.1 successful
   Installing black 23.3.0...      
   Installing click 8.1.3...       
   Installing packaging 23.1...    
   Installing pathspec 0.11.1...   
   Install packaging 23.1 successful
   Installing black 23.3.0...      
   Installing click 8.1.3...       
   Installing packaging 23.1...    
   Installing pathspec 0.11.1...   
   Install click 8.1.3 successful
   Installing black 23.3.0...      
   Installing click 8.1.3...       
   Installing packaging 23.1...    
   Installing pathspec 0.11.1...   
   Install black 23.3.0 successful
   Installing black 23.3.0...      
   Installing click 8.1.3...       
   Installing packaging 23.1...    
   Installing pathspec 0.11.1...   

🎉 All complete!
   Installing black 23.3.0...      
   Installing click 8.1.3...       
   Installing packaging 23.1...    
   Installing pathspec 0.11.1...   

?25h

What did pdm add -d do?#

We’ve added a new [tool.pdm.dev-dependencies] table to pyproject.toml, which contains black:

# pyproject.toml
...

requires = ["pdm-pep517>=1.0"]
build-backend = "pdm.pep517.api"

[tool.pdm.dev-dependencies]
dev = [
    "black>=23.3.0",
]

When we run pdm install, by default, PDM will install dependencies from here in addition to the dependencies listed in [project.dependencies]. A different tool like pip, however, will not.

See also

See the PDM docs on adding development dependencies for more information on development dependencies.

The lockfile has also been updated:

cat pdm.lock
# This file is @generated by PDM.
# It is not intended for manual editing.

[[package]]
name = "black"
version = "23.3.0"
requires_python = ">=3.7"
summary = "The uncompromising code formatter."
dependencies = [
    "click>=8.0.0",
    "mypy-extensions>=0.4.3",
    "packaging>=22.0",
    "pathspec>=0.9.0",
    "platformdirs>=2",
]

[[package]]
name = "click"
version = "8.1.3"
requires_python = ">=3.7"
summary = "Composable command line interface toolkit"
dependencies = [
    "colorama; platform_system == \"Windows\"",
]

[[package]]
name = "colorama"
version = "0.4.6"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
summary = "Cross-platform colored terminal text."

[[package]]
name = "cowsay"
version = "5.0"
summary = "The famous cowsay for GNU/Linux is now available for python"

[[package]]
name = "mypy-extensions"
version = "1.0.0"
requires_python = ">=3.5"
summary = "Type system extensions for programs checked with the mypy type checker."

[[package]]
name = "packaging"
version = "23.1"
requires_python = ">=3.7"
summary = "Core utilities for Python packages"

[[package]]
name = "pathspec"
version = "0.11.1"
requires_python = ">=3.7"
summary = "Utility library for gitignore style pattern matching of file paths."

[[package]]
name = "platformdirs"
version = "3.5.1"
requires_python = ">=3.7"
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."

[metadata]
lock_version = "4.2"
cross_platform = true
groups = ["default", "dev"]
content_hash = "sha256:715ad39cb3ddf659e0afedfb18dcade3de83c16696c31ed976bbf46bb61aaa3a"

[metadata.files]
"black 23.3.0" = [
    {url = "https://files.pythonhosted.org/packages/06/1e/273d610249f0335afb1ddb03664a03223f4826e3d1a95170a0142cb19fb4/black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
    {url = "https://files.pythonhosted.org/packages/12/4b/99c71d1cf1353edd5aff2700b8960f92e9b805c9dab72639b67dbb449d3a/black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
    {url = "https://files.pythonhosted.org/packages/13/0a/ed8b66c299e896780e4528eed4018f5b084da3b9ba4ee48328550567d866/black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
    {url = "https://files.pythonhosted.org/packages/13/25/cfa06788d0a936f2445af88f13604b5bcd5c9d050db618c718e6ebe66f74/black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
    {url = "https://files.pythonhosted.org/packages/21/14/d5a2bec5fb15f9118baab7123d344646fac0b1c6939d51c2b05259cd2d9c/black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
    {url = "https://files.pythonhosted.org/packages/24/eb/2d2d2c27cb64cfd073896f62a952a802cd83cf943a692a2f278525b57ca9/black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
    {url = "https://files.pythonhosted.org/packages/27/70/07aab2623cfd3789786f17e051487a41d5657258c7b1ef8f780512ffea9c/black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
    {url = "https://files.pythonhosted.org/packages/29/b1/b584fc863c155653963039664a592b3327b002405043b7e761b9b0212337/black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
    {url = "https://files.pythonhosted.org/packages/3c/d7/85f3d79f9e543402de2244c4d117793f262149e404ea0168841613c33e07/black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
    {url = "https://files.pythonhosted.org/packages/3f/0d/81dd4194ce7057c199d4f28e4c2a885082d9d929e7a55c514b23784f7787/black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
    {url = "https://files.pythonhosted.org/packages/49/36/15d2122f90ff1cd70f06892ebda777b650218cf84b56b5916a993dc1359a/black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
    {url = "https://files.pythonhosted.org/packages/49/d7/f3b7da6c772800f5375aeb050a3dcf682f0bbeb41d313c9c2820d0156e4e/black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
    {url = "https://files.pythonhosted.org/packages/69/49/7e1f0cf585b0d607aad3f971f95982cc4208fc77f92363d632d23021ee57/black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
    {url = "https://files.pythonhosted.org/packages/6d/b4/0f13ab7f5e364795ff82b76b0f9a4c9c50afda6f1e2feeb8b03fdd7ec57d/black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
    {url = "https://files.pythonhosted.org/packages/ad/e7/4642b7f462381799393fbad894ba4b32db00870a797f0616c197b07129a9/black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
    {url = "https://files.pythonhosted.org/packages/c0/53/42e312c17cfda5c8fc4b6b396a508218807a3fcbb963b318e49d3ddd11d5/black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
    {url = "https://files.pythonhosted.org/packages/ca/44/eb41edd3f558a6139f09eee052dead4a7a464e563b822ddf236f5a8ee286/black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
    {url = "https://files.pythonhosted.org/packages/ce/f4/2b0c6ac9e1f8584296747f66dd511898b4ebd51d6510dba118279bff53b6/black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
    {url = "https://files.pythonhosted.org/packages/d1/6e/5810b6992ed70403124c67e8b3f62858a32b35405177553f1a78ed6b6e31/black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
    {url = "https://files.pythonhosted.org/packages/d6/36/66370f5017b100225ec4950a60caeef60201a10080da57ddb24124453fba/black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
    {url = "https://files.pythonhosted.org/packages/d7/6f/d3832960a3b646b333b7f0d80d336a3c123012e9d9d5dba4a622b2b6181d/black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
    {url = "https://files.pythonhosted.org/packages/db/f4/7908f71cc71da08df1317a3619f002cbf91927fb5d3ffc7723905a2113f7/black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
    {url = "https://files.pythonhosted.org/packages/de/b4/76f152c5eb0be5471c22cd18380d31d188930377a1a57969073b89d6615d/black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
    {url = "https://files.pythonhosted.org/packages/eb/a5/17b40bfd9b607b69fa726b0b3a473d14b093dcd5191ea1a1dd664eccfee3/black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
    {url = "https://files.pythonhosted.org/packages/fd/5b/fc2d7922c1a6bb49458d424b5be71d251f2d0dc97be9534e35d171bdc653/black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
]
"click 8.1.3" = [
    {url = "https://files.pythonhosted.org/packages/59/87/84326af34517fca8c58418d148f2403df25303e02736832403587318e9e8/click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
    {url = "https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
]
"colorama 0.4.6" = [
    {url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
    {url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
"cowsay 5.0" = [
    {url = "https://files.pythonhosted.org/packages/6b/b8/9f497fd045d74fe21d91cbe8debae0b451229989e35b539d218547d79fc6/cowsay-5.0.tar.gz", hash = "sha256:c00e02444f5bc7332826686bd44d963caabbaba9a804a63153822edce62bbbf3"},
]
"mypy-extensions 1.0.0" = [
    {url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
    {url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
"packaging 23.1" = [
    {url = "https://files.pythonhosted.org/packages/ab/c3/57f0601a2d4fe15de7a553c00adbc901425661bf048f2a22dfc500caf121/packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
    {url = "https://files.pythonhosted.org/packages/b9/6c/7c6658d258d7971c5eb0d9b69fa9265879ec9a9158031206d47800ae2213/packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
]
"pathspec 0.11.1" = [
    {url = "https://files.pythonhosted.org/packages/95/60/d93628975242cc515ab2b8f5b2fc831d8be2eff32f5a1be4776d49305d13/pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
    {url = "https://files.pythonhosted.org/packages/be/c8/551a803a6ebb174ec1c124e68b449b98a0961f0b737def601e3c1fbb4cfd/pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
]
"platformdirs 3.5.1" = [
    {url = "https://files.pythonhosted.org/packages/89/7e/c6ff9ddcf93b9b36c90d88111c4db354afab7f9a58c7ac3257fa717f1268/platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"},
    {url = "https://files.pythonhosted.org/packages/9c/0e/ae9ef1049d4b5697e79250c4b2e72796e4152228e67733389868229c92bb/platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"},
]

Many new packages now exist in the lockfile, not just cowsay and black. This is because (unlike cowsay), black itself has dependencies that we needed to install to get it to work. This lock file records exactly the versions of those sub-dependencies that we’ve now installed into our project virtual environment.

When we run pdm install, if the lock file exists (and pyproject.toml hasn’t been changed since the lockfile was last updated), PDM will install precisely the packages listed in the lockfile, so we’ll always be working in the same virtual environment. This is useful for developing and testing the code, so you should always include the lockfile in your project version control.

However, we don’t want to impose these restrictions on users of our library, or our project would rapidly become impossible to install due to other packages requiring different versions of the packages in the lockfile. The only thing that we care about is that users have the right versions of the dependencies we directly use, which are listed in the project.dependencies array in pyproject.toml. This is why pip install does not care about the existence of the lockfile.

See also

See the PDM docs on version control for more on best practices for version-controlling a PDM project.

Using black#

We can now run black within our environment.

Let’s re-write src/eeskew_pwg_test_000/sarcasm.py with deliberately poor formatting:

# src/eeskew_pwg_test_000/sarcasm.py
import cowsay
def sarcasm(
            s
          ):
    """Convert string `s` to sArCaSm TeXt."""
    out = ''
    for i,c in \
        enumerate( s ):

        if i% 2 ==0: out +=c.lower()

        else:



            out+= c.upper()

    return out

def sarcastic_cowsay(s):
    """Cowsay `s`, sArCaStIcAlLy."""
    sarcastic_s = sarcasm(s); cowsay.cow(sarcastic_s)

This is bad formatting! Rather than fix it manually, we can run black on our code, which will auto-impose a reasonable style.

pdm run black src/
reformatted /Users/Ed/python/eds-notes/repos/_tmp_pwg_presentation_02-08-2023_part1/eeskew-pwg-test-000/src/eeskew_pwg_test_000/sarcasm.py

All done! ✨ 🍰 ✨
1 file reformatted, 1 file left unchanged.

What did black do?#

black has automatically re-formatted src/eeskew_pwg_test_000/sarcasm.py, fixing the poor formatting we introduced earlier:

# src/eeskew_pwg_test_000/sarcasm.py
import cowsay


def sarcasm(s):
    """Convert string `s` to sArCaSm TeXt."""
    out = ""
    for i, c in enumerate(s):
        if i % 2 == 0:
            out += c.lower()

        else:
            out += c.upper()

    return out


def sarcastic_cowsay(s):
    """Cowsay `s`, sArCaStIcAlLy."""
    sarcastic_s = sarcasm(s)
    cowsay.cow(sarcastic_s)

See also

See the black documentation for more information.

Conclusion#

That’s all for Part 1! The examples here should be enough to get you started using PDM to manage your own python projects. For more advanced usage, check out the PDM documentation on:

In Part 2, we’ll cover how to publish a PDM project on PyPI, so that it can be installed with a simple pip install.