Skip to content

Pythonprojecten met uv

In de vorige hoofdstukken heb je gewerkt met een eigen virtual environment zodat je jouw pythonomgeving mooi gescheiden kan houden van andere projecten waar je aan werkt. Dit is echt de oplossing voor alle problemen waarbij volledige Pythoninstallaties onbruikbaar kunnen worden — waarna je alles opnieuw moet installeren. Dit kan gebeuren als je — vanwege al je verschillende projecten — zoveel packages installeert dat die met elkaar in conflict komen.

Voor ieder project nieuwe environments aanmaken heeft wel een nadeel: je moet alle packages die je nodig hebt opnieuw installeren. Welke waren dat ook alweer? Vast numpy, en matplotlib, en…? Niet handig. Als je code gaat delen met elkaar krijg je regelmatig te maken met een ImportError of ModuleNotFoundError omdat je niet precies weet wat er nodig is, waarna je weer één of ander package moet installeren.

Nu pythondaq netjes is uitgesplitst in een MVC-structuur en de wijzigingen met Git worden bijgehouden, ga je er een package van maken zodat je het ook met anderen kan delen. Daarin staan alle benodigdheden duidelijk omschreven zodat gebruikers daar verder niet over hoeven na te denken.

Packages op PyPI (de standaardplek waar Python packages gepubliceerd worden) geven altijd hun dependencies op. Dat zijn de packages die verder nog nodig zijn om alles te laten werken. Installeer je matplotlib, dan krijg je er six, python-dateutil, pyparsing, pillow, numpy, kiwisolver, cycler automatisch bij. Maar alleen de namen van packages zijn niet genoeg. Welke versies van numpy werken met de huidige versie van matplotlib? Allemaal zaken die je — als je een package schrijft — zelf moet bijhouden. Het voordeel is dat jouw gebruikers alleen maar jouw package hoeven te installeren — de rest gaat vanzelf.

En… hoe test je je package zodat je zeker weet dat hij het bij een ander ook doet? Heel vaak werkt het bij jou wel, maar vergeet je een bestand mee te sturen dat wel echt nodig is.1 Of: bij jou werkt import my_new_cool_app.gui wel, maar bij een ander geeft hij een ImportError of ModuleNotFoundError. De bestanden zijn er wel, maar worden verkeerd geïmporteerd.

Hoe krijg je eigenlijk je code bij iemand anders? Liefst als één bestand, of zelfs met pip install my_new_cool_app; dat zou wel mooi zijn.

Ook daarvoor gebruiken we uv.

Info

Voorgaande jaren leerden we studenten om Poetry te gebruiken. Heel populair, maar uv is de afgelopen anderhalf jaar nog veel populairder geworden. En terecht.

Er zijn meerdere tools ontwikkeld om dezelfde problemen op te lossen. uv is in korte tijd heel populair geworden. Het richt zich op het officiële ecosysteem: standaard Python packages, ofwel PyPI en pip; niet conda (zie meer hierover in paragraaf pip vs conda). Dit zorgt er voor dat iedereen mét of zónder Anaconda je package kan installeren. Omdat uv ook in staat is zelf verschillende versies van Python te installeren hebben we Anaconda niet meer nodig. De installer van Anaconda is bijna 1 Gb groot en bevat heel veel Python packages die je nooit gebruikt. De installer van uv is nog geen 20 Mb en kun je gebruiken om precies te installeren wat je nodig hebt.

Werken in een terminal

uv is een tool die je enkel en alleen in de terminal kunt gebruiken. Het heeft alleen een command-line interface (CLI). Ben je nog niet zo bekend met het navigeren in een terminal dan kun je als oefening de Terminal Adventure Game spelen.

We gaan uv bedienen door commando's te geven in de terminal van Visual Studio Code. We laten de terminal weten welk programma wij willen gaan besturen, door uv in te typen. En daarachter wat we willen dat uv gaat doen. We kunnen bijvoorbeeld kijken welke commando's allemaal beschikbaar zijn met uv help. Dat geeft een vrij lange lijst die je terug kunt scrollen in de terminal, maar je kunt ook uv help | more intypen om de tekst per pagina weer te geven.2

> uv help | more 

Info

Zoals je ziet heeft uv dus heel veel verschillende commando's. uv is een Zwitsers zakmes: het bevat heel veel tools voor wie dat nodig heeft. Wij hebben lang niet alles nodig dus laat je daardoor niet uit het veld slaan. In de rest van dit hoofdstuk vertellen we precies wat je wel nodig hebt. Als je meer wilt weten kun je het beste de documentatie lezen.

Nieuw uv project

Info

We gaan werken met modules en packages. Ben je daar nog niet zo bekend mee, zorg dan dat je paragraaf Modules en paragraaf packages gemaakt hebt.

Stel je wilt een package schrijven met wat handige functies om veelgebruikte statistische berekeningen makkelijk uit te voeren. Je noemt het easystat. Het doel is eerst om het in al je eigen analyses makkelijk te kunnen gebruiken (import easystat) maar je wilt het ook op GitHub zetten en wie weet vinden anderen het ook handig! Je wilt het dus ook netjes doen. En niet later van anderen horen: leuk, maar bij mij werkt het niet!

Easystat uv project aanmaken

  1. Open Github Desktop en ga naar het dropdownmenu File. Kies hier voor New repository .... Geef de repository de naam easystat en zet de repository in de map ECPC. Vink Initialize this repository with a README aan en kies bij Git ignore voor Python.
  2. Open de repository easystat in Visual Studio Code.
  3. Open een Terminal in je Visual Studio Code-omgeving (Menu > Terminal > New Terminal). Maak het uv project aan met:
    Terminal
    uv init --package
    
  4. Je bekijkt de nieuw gemaakte mappenstructuur en ziet dat het overeenkomt met de mappenstructuur zoals hieronder weergegeven:

    ECPC
    ├── oefenopdrachten
    ├── pythondaq
    ├── easystat
               ├── src
                          └── easystat
                                     └── __init__.py
               ├── .gitattributes
               ├── .gitignore
               ├── .python-version
               ├── pyproject.toml
               └── README.md
    └── •••

  5. Commit in GitHub Desktop de wijzigingen die uv init heeft gedaan.

src-layout

Door het project in een source layout (src-layout) te bouwen (easystat zit in een mapje src) staat al je Pythoncode netjes bij elkaar weggestopt. Dit maakt het makkelijker om te testen of het installeren goed werkt zodat je zeker weet dat andere mensen met jouw code aan de slag kunnen.

Testcode

(ECPC) > uv init --package 

Checkpunten:

  • De projectmap easystat staat in de map ECPC.
  • In de projectmap easystat staat een map src.
  • In de map src staat een package map easystat

Projecttraject

  • Easystat uv project aanmaken
  • Easystat virtual environment aanmaken
  • Easystat shortcuts.py, measurements.py en try_measurements.py aanmaken
  • Easystat shortcuts.py testen
  • Easystat dependencies toevoegen
  • Easystat package imports fixen

Laten we één voor één kijken welke mappen en bestanden uv heeft aangemaakt. We hadden al een README.md in de projectmap staan. Hierin komt een algemene beschrijving van ons project.3

Dan komt de src-map. Daarin komt ons nieuwe package easystat4 te staan. Er is alvast voor ons een __init__.py aangemaakt. Handig! De bestanden .gitattributes en .gitignore bewaren wat instellingen voor git, en .python-version bewaart het versienummer van Python dat uv gebruikt. Vul je daar 3.12 in? Dan installeert uv Python 3.12 in je virtual environment.

En als laatste… een pyproject.toml5 waarin alle informatie over je project wordt bijgehouden. Ook staat er in dit bestand informatie voor de verschillende tools die je kunt gebruiken. De inhoud van het bestand ziet er ongeveer zo uit:

[project]
name = "easystat"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
    { name = "David Fokkema", email = "davidfokkema@icloud.com" }
]
requires-python = ">=3.13"
dependencies = []

[project.scripts]
easystat = "easystat:main"

[build-system]
requires = ["uv_build>=0.8.4,<0.9.0"]
build-backend = "uv_build"

Het bestand is in het TOML-formaat.7 Tussen de vierkante haken staan de koppen van de verschillende secties in dit configuratiebestand. In de eerste sectie staat informatie over ons project. Je kunt daar bijvoorbeeld een beschrijving toevoegen of het versienummer aanpassen. Ook bevat die sectie de dependencies. Dit zijn alle Pythonpackages die ons project nodig heeft. Op dit moment is dat nog niets. Ook het versienummer van Python is belangrijk. Hier is dat groter of gelijk aan 3.13. Dit kan belangrijk zijn. Gebruikers met een iets oudere versie van Python — bijvoorbeeld versie 3.11 — kunnen nu het package niet installeren. Als je niet per se de nieuwste snufjes van Python 3.13 nodig hebt kun je aangeven dat een iets oudere versie van Python ook prima is. Op moment van schrijven — zomer 2025 — is Python 3.13 de nieuwste versie. Het is dus prima om minimaal 3.12 te vragen — die versie is inmiddels bijna twee jaar oud.

Python versie in je project

Het is heel handig om als je requires-python = ">=3.12" invult, ofwel 'minstens 3.12', dat je dan in .python-version ook 3.12 invult omdat je anders niet zeker weet dat je code ook echt werkt met 3.12 (omdat jij zelf dan bijvoorbeeld met 3.13 werkt en het dus nooit getest hebt met 3.12).

De sectie [project.scripts] zorgt ervoor dat we ons script kunnen aanroepen door easystat in de terminal in te typen en de sectie [build-system] zorgt ervoor dat we een package kunnen maken en uploaden naar de Python Package Index (PyPI). De [build-system] sectie is nu nog niet belangrijk.

Synchroniseren van virtual environments

  1. Hoewel we hierboven beweerden dat je easystat kunt intypen in de terminal en dat er dan een scriptje draait, werkt dat (nog) niet. Probeer maar eens! Het werkt ook niet als je een nieuwe terminal opent. En... er staat niets tussen haakjes aan het begin van de opdrachtprompt. Blijkbaar is er nog geen virtual environment actief.
  2. Open src/eaystat/__init__.py. Rechtsonderin zie je inderdaad Select Interpreter. Als je daarop klikt zie je alleen niet Python 3.x.x (easystat) in het rijtje staan... Druk op Esc om het menu te verlaten.
  3. In een terminal in VS Code, type in:
    > uv sync 
    
    Wat dit gedaan heeft is het automatisch aanmaken van het virtual environment op basis van je projectinstellingen. Dus de Pythonversie die in .python-version staat en eventuele dependencies die gedefinieerd zijn in je pyproject.toml.
  4. Kies het nieuwe virtual environment.
  5. Open een nieuwe terminal en type easystat. Als het goed is werkt het nu wél!
  6. Commit in GitHub Desktop de wijzigingen die uv sync heeft gedaan (een uv.lock file, zie later).

Checkpunten:

  • Rechtsonderin staat je Python environment geselecteerd (3.12.x (easystat)).
  • In de terminal staat (easystat) vooraan de opdrachtprompt.
  • Het commando easystat runt zonder problemen.

Projecttraject

  • Easystat uv project aanmaken
  • Easystat virtual environment aanmaken
  • Easystat shortcuts.py, measurements.py en try_measurements.py aanmaken
  • Easystat shortcuts.py testen
  • Easystat dependencies toevoegen
  • Easystat package imports fixen

Maken van de easystat-package

We starten met ons package. We gaan een aantal ModuleNotFoundErrors tegenkomen, maar dat lossen we ook weer op. Stel, we berekenen vaak de standaarddeviatie van het gemiddelde en maken daarvoor een handige shortcut in shortcuts.py. Nu willen we deze shortcut ook in een ander script measurements.py gebruiken, die op basis van een aantal metingen het gemiddelde mét een onzekerheid geeft. Dit kunnen we doen door de module te importeren in het nieuwe script zodat we de functie stdev_of_mean daar ook kunnen gebruiken. We maken uiteindelijk een script try_measurements.py om dit allemaal te testen, en die zetten we expres niet in het package, maar in een nieuwe map tests. Het testscript hoort immers niet bij de code van het easystat package.

Easystat shortcuts.py en try_shortcuts.py aanmaken

Maak zoals hieronder aangegeven de bestanden shortcuts.py, measurements.py en try_measurements.py aan, waarbij je let op in welke map de bestanden moeten staan (je moet nog een map zelf aanmaken):

shortcuts.py
import numpy as np 


def stdev_of_mean(values):
    """Calculate the standard deviation of the mean"""
    return np.std(values) / np.sqrt(len(values))    
measurements.py
import numpy as np
from shortcuts import stdev_of_mean


def result_with_uncertainty(values):
    """Return result with uncertainty from list of measurements."""
    return np.mean(values), stdev_of_mean(values)
try_measurements.py
from measurements import result_with_uncertainty

measurements = [1, 2, 2, 2, 3]
result, uncertainty = result_with_uncertainty(measurements)

print(f"{measurements=}")
print(f"Result of measurements is: {result:.2f} +- {uncertainty:.2f}.")
easystat
├── src
           ├── easystat
                      ├── __init__.py
                      ├── measurements.py
                      └── shortcuts.py
├── tests
           └── try_measurements.py
├── pyproject.toml
└── readme.md

Import numpy could not be resolved

Misschien is het je al opgevallen dat VS Code een oranje kringeltje onder numpy zet in de eerste regels van twee scripts, en ook onder shortcuts en measurements. Als je daar je muiscursor op plaatst krijg je een popup met de melding Import numpy could not be resolved. Daar moeten we misschien wat mee... In de volgende opdrachten gaan we die problemen één voor één oplossen.

Checkpunten:

  • De map easystat/src/easystat bevat het bestand measurements.py.
  • De map easystat/src/easystat bevat het bestand shortcuts.py.
  • In de projectmap easystat staat een map tests.
  • De map easystat/tests bevat het bestand try_measurements.py.

Projecttraject

  • Easystat uv project aanmaken
  • Easystat virtual environment aanmaken
  • Easystat shortcuts.py, measurements.py en try_measurements.py aanmaken
  • Easystat shortcuts.py testen
  • Easystat dependencies toevoegen
  • Easystat package imports fixen

In de eerste regel van try_measurements.py importeren we de functie uit het nieuwe package om uit te proberen. In de eerste print-regel gebruiken we een handige functie van f-strings.6

Easystat shortcuts.py testen

Je bent heel benieuwd of je package al werkt. Je runt als eerste het bestand shortcuts.py en krijgt een foutmelding...

Testcode

shortcuts.py
import numpy as np


def stdev_of_mean(values):
    """Calculate the standard deviation of the mean"""
    return np.std(values) / np.sqrt(len(values))
(easystat) > python .\src\easystat\shortcuts.py

Checkpunten:

  • Je hebt de juiste virtual environment geactiveerd.
  • Je runt het bestand shortcuts.py.
  • Je krijgt een foutmelding ModuleNotFoundError: No module named 'NumPy'

Projecttraject

  • Easystat uv project aanmaken
  • Easystat virtual environment aanmaken
  • Easystat shortcuts.py, measurements.py en try_measurements.py aanmaken
  • Easystat shortcuts.py testen
  • Easystat dependencies toevoegen
  • Easystat package imports fixen

De beloofde ModuleNotFoundError! Ons package heeft NumPy nodig en dat hebben we nog niet geïnstalleerd. Dat was de reden voor de kriebeltjes onder numpy. Het installeren kunnen we handmatig doen maar dan hebben andere gebruikers een probleem. Veel beter is het om netjes aan te geven dat ons package NumPy nodig heeft — als dependency.

Dependencies toevoegen

Om een dependency aan te geven vertellen we uv dat hij deze moet toevoegen met:

(easystat) > uv add numpy 

Easystat dependencies toevoegen

Je voegt numpy als dependency toe aan het project easystat met het commando uv add numpy. Je kijkt in de pyproject.toml en warempel daar staat numpy nu bij de dependencies! Je vraagt je af of numpy nu ook in het virtual environment easystat is geïnstalleerd en controleert dit met uv pip list en waarachtig numpy staat in de lijst . Weer ga je shortcuts.py draaien en ditmaal krijg geen foutmelding! Commit de wijzigingen.

Checkpunten:

  • Je hebt de juiste virtual environment geacitveerd.
  • Je hebt numpy als dependency toegevoegd.
  • Je krijgt geen foutmelding als je het bestand shortcuts.py runt.

Projecttraject

  • Easystat uv project aanmaken
  • Easystat virtual environment aanmaken
  • Easystat shortcuts.py, measurements.py en try_measurements.py aanmaken
  • Easystat shortcuts.py testen
  • Easystat dependencies toevoegen
  • Easystat package imports fixen

Fijn! uv heeft NumPy nu toegevoegd aan de environment easystat. Gewone package managers als Pip en Conda zullen geen packages toevoegen aan je uv project als je pip/conda install package aanroept. Gebruik daarom altijd uv add package als je met uv aan een package werkt. Sterker nog, als je met Pip handmatig packages extra installeert zal uv sync deze packages als overbodig herkennen en ze prompt weer verwijderen. Heb je iets verkeerds toegevoegd? Het verwijderen van dependency PACKAGE gaat met uv remove PACKAGE.

Info

Als we de code in ons package aanpassen dan hoeven we het environment niet opnieuw te synchroniseren met uv sync, maar als we met de hand iets wijzigen in de pyproject.toml dan moet dat wel. Als je een ImportError of ModuleNotFoundError krijgt voor je eigen package — bijvoorbeeld als je nieuwe mappen of bestanden hebt aangemaakt — probeer dan eerst voor de zekerheid uv sync.

uv.lock

uv.lock

Na het toevoegen van NumPy is er ook een grote wijziging in het bestand uv.lock bijgekomen. Hierin staan de exacte versies van alle geïnstalleerde packages. Vaak wordt dit bestand gecommit zodat collega-ontwikkelaars van hetzelfde project exact dezelfde versies installeren zodra ze uv sync aanroepen. Ook als er nieuwere versies van NumPy bijkomen blijven alle ontwikkelaars precies dezelfde NumPy-versie gebruiken totdat uv.lock geüpdatet wordt. Niets is zo vervelend als "oh, bij mij werkt het wel" dus hoe meer dingen precies hetzelfde zijn, hoe minder problemen je tegenkomt. Updaten naar nieuwere versies kan natuurlijk wel, maar alleen op het moment dat je er klaar voor bent om te testen of dan alles nog netjes werkt.

Upgrade uv lockfile

  1. Clone de repository NatuurkundePracticumAmsterdam/upgrade-uv-lock.
  2. Open de repository in Visual Studio Code en open een nieuwe terminal.
  3. Installeer de dependencies in één keer met uv sync.
  4. Waarvoor gebruikt uv de lockfile (uv.lock)? Welke versies van NumPy en matplotlib worden geïnstalleerd?
  5. Sinds het maken van de repository zijn er nieuwere versies van NumPy en matplotlib uitgekomen. Die worden nu nog niet geïnstalleerd, hoewel er in pyproject.toml staat: dependencies = ["matplotlib>=3.10.3", "numpy>=2.2.6"] (het mag dus wel!). Controleer dat in pyproject.toml.
  6. Nu willen we de nieuwe versies hebben. Je kunt de lockfile updaten met uv lock --upgrade. De nieuwere versies worden genoemd en verwerkt in de lockfile. Controleer in GitHub Desktop dat uv.lock gewijzigd is.
  7. Update je virtual environment met uv sync en controleer dat de nieuwere versies inderdaad geïnstalleerd worden.

Upgrade dependencies

De stappen uv lock --upgrade en uv sync kunnen in één keer met uv sync --upgrade. Die zul je dus vaker gebruiken.

Absolute imports

We hebben een uv project, dependencies toegevoegd maar nog niet alle code getest. Dat gaan we nu doen!

Easystat package testen

Je probeert nog een keer shortcuts.py te runnen en ziet dat dat gewoon werkt. Daarna probeer je measurements.py. Werkt ook, maar wel gek dat er golfjes onder from shortcuts import stdev_of_mean staan, hij doet het toch gewoon? Je kijkt even welke waarschuwing daarbij gegeven wordt door je muiscursor op de golfjes te schuiven. Daarna probeer je try_measurements.py. Hier gaan dingen mis: daarom zette VS Code kriebeltjes onder measurements (en onder shortcuts vanwege een vergelijkbare reden).

Testcode

(easystat) > python tests/try_measurements.py 

We willen dus de module measurements importeren, maar Python kan hem niet vinden. Dat is ook wel een klein beetje logisch, want try_measurements.py staat in de map tests maar measurements.py staat in de map src/easystat. Dus we moeten Python vertellen wáár hij die module kan vinden, namelijk in ons nieuwe package easystat. Doordat we een package gemaakt hebben hoeven we dus niet precies te vertellen in welke map alles te vinden is, maar hoeven we alleen de naam van het package te gebruiken. Dus niet map.op.computer.easystat.src.easystat maar gewoon easystat. Wel zo makkelijk.

Import aanpassen: easystat package gebruiken

Je past from measurements ... aan naar from easystat.measurements .... Je test de code opnieuw. Verdorie, weer een error. Overleg met elkaar wat deze error betekent. Waarom kregen we die error niet toen we measurements.py testten?

Testcode

(easystat) > python tests/try_measurements.py 

Het probleem is dat wanneer je met Python een script runt en je importeert iets, dat Python eerst in de map kijkt waar het script staat (hier tests) en daarna zoekt in de lijst met geïnstalleerde packages. De module shortcuts staat niet in tests. Toen we measurements.py draaiden kon hij die wél vinden want measurements.py en shortcuts.py staan in dezelfde map. Dus afhankelijk van welk script we draaien kan hij de modules soms wel vinden, soms niet. Dat is natuurlijk niet zo handig. De oplossing: absolute imports: geef bij alle imports altijd de naam van je package op.

Import aanpassen: absolute imports

Je past in het bestand measurements.py de regel from shortcuts ... aan door de naam van het package toe te voegen en ziet dat de oranje kriebeltjes ook verdwijnen. Je test de code in try_measurements.py opnieuw. Gelukt!

Testcode

(easystat) > python tests/try_measurements.py 

Checkpunten:

  • Je hebt de import in try_measurements.py aangepast.
  • Je hebt de import in measurements.py aangepast.
  • Je krijgt geen foutmelding als je het bestand try_measurements.py runt.

Projecttraject

  • Easystat uv project aanmaken
  • Easystat virtual environment aanmaken
  • Easystat shortcuts.py, measurements.py en try_measurements.py aanmaken
  • Easystat shortcuts.py testen
  • Easystat dependencies toevoegen
  • Easystat package imports fixen
Wheels

Wheels

Wanneer we klaar zijn om ons package te delen met andere gebruikers gebruiken we het commando build om zogeheten wheels te bouwen. Wheels zijn de bestanden die uv en pip downloaden en installeren wanneer je zegt pip install numpy. Het is een soort ingepakte installer met alles wat er nodig is om het package te gebruiken.

Bouw een wheel

  1. Bouw het wheel van easystat met uv build.
  2. Bekijk de namen van de bestanden in de nieuwe map easystat/dist, welke extensie hebben ze?

(easystat) > uv build 

Een .tar.gz-bestand is een soort zipbestand met daarin de broncode van ons package (een source distribution). De tests worden daar niet in meegenomen. Een wheel is een soort bestand dat direct geïnstalleerd kan worden met pip. Zogenaamde pure-python packages bevatten alleen Pythoncode — en geen C-code die gecompileerd moet worden voor verschillende besturingssystemen of hardwareplatforms. Je herkent ze aan none-any in de bestandsnaam. None voor niet-OS-specifiek en any voor draait op elk hardwareplatform. We kunnen dit bestand als download neerzetten op een website of aan anderen mailen. Zij kunnen het dan installeren met pip install.

Test wheel

Laten we het wheel uitproberen. We gaan straks een nieuw virtual environment aanmaken, installeren het wheel en proberen het testscript te runnen — één keer vóór het installeren van het wheel en één keer ná het installeren, als volgt:

  1. Maak een nieuw, leeg, virtual environment.
  2. Draai tests/try_measurements.py en bekijk de foutmelding.
  3. Installeer het wheel met uv pip install .\dist\easystat-0.1.0-py3-none-any.whl.
  4. Draai tests/try_measurements.py en bekijk de uitkomst.

Het werkt! Je ziet dat pip install niet alleen ons package easystat installeert, maar ook de dependency numpy. Dat is precies wat we willen.

Het is belangrijk om de wheels niet in je GitHub repository te committen. Je repository is voor broncode, waarmee wheels gebouwd kunnen worden. Als je de stappen voor het aanmaken van de repository netjes gevolgd hebt dan heb je een .gitignore toegevoegd met Python-specifieke bestandsnamen en directories die genegeerd worden door Git en GitHub.

uv gebruiken voor een bestaand project: pythondaq

Natuurlijk willen we uv ook gaan gebruiken bij pythondaq. We maken nu alleen geen nieuw project, maar gaan uv toevoegen aan een bestaand project. Daarvoor moeten we twee dingen doen. Als eerste gaan we uv initialiseren in de pythondaq repository en dan moeten we de code in de src-structuur plaatsen.

Pythondaq: uv

  1. Je project pythondaq is zo tof aan het worden dat je het met uv gaat beheren zodat jij en anderen het gemakkelijk kunnen installeren en gebruiken. Als eerste open je de repository in GitHub Desktop en Visual Studio Code en open je een nieuwe terminal. Je test voor de zekerheid run_experiment.py nog even uit zodat je zeker weet dat alles nu nog werkt. Vervolgens maak je een uv project . Dan kies je op twee plaatsen dat je Python 3.12 wilt gebruiken. Dan synchroniseer je je virtual environment en commit je je wijzigingen.
  2. Test run_experiment.py en voeg alle benodigde dependencies toe totdat alles werkt en je opnieuw het lampje ziet gaan branden en de resultaten van je experiment krijgt. Commit je wijzigingen.

Checkpunten:

  • Je hebt uv geïnitialiseerd in de Pythondaq projectmap.
  • Na het initialiseren van uv is er een pyproject.toml en een .python-version in de projectmap aangemaakt.
  • Wanneer met uv sync een nieuwe virtual environment met Python 3.12 wordt aangemaakt werkt run_experiment.py daarna in die nieuwe omgeving naar behoren.

Projecttraject

  • Pythondaq: docstrings
  • Pythondaq: uv
  • Pythondaq: src-layout
  • Pythondaq: test imports
  • Pythondaq: applicatie

Pythondaq: src-layout

Nu de code in principe werkt, gaan we die in een src-layout zetten zoals hiernaast. Je test run_experiment.py en die moet werken.
pythondaq
├──src
          └──pythondaq
                    ├──__init__.py
                    ├──arduino_device.py
                    ├──diode_experiment.py
                    └──run_experiment.py
├──.gitattributes
├──.gitignore
├──.python-version
├──pyproject.toml
├──README.md
└──uv.lock

Checkpunten:

  • Je 'oude' code staat nu allemaan in src/pythondaq.
  • run_experiment.py draait zonder problemen.

Projecttraject

  • Pythondaq: docstrings
  • Pythondaq: uv
  • Pythondaq: src-layout
  • Pythondaq: test imports
  • Pythondaq: applicatie
Model, view, controller packages

In grotere projecten is het gebruikelijk om model, view, controller niet alleen uit te splitsen in verschillende scripts, maar ook in aparte packages te zetten.

  1. Maak 3 extra packages in de pythondaq package. models, views en controllers.
  2. Zet de modules in de juiste packages.
  3. Test je code zodat alle imports weer werken.

Van script naar applicatie

Om onze python code te testen heb je tot nu toe waarschijnlijk op de run-knop in Visual Studio Code gedrukt. Of je hebt in de terminal aan python gevraagd om het script.py te runnen:

Terminal
python script.py
Je moet dan wel in Visual Studio Code de juiste map geopend hebben zodat Python het bestand kan vinden. En als je de run-knop gebruikt moet wel het bestandje open staan dat je wilt runnen. Kortom, best een beetje gedoe. Maar als we programma's zoals uv, Conda of Python willen gebruiken hoeven we helemaal niet het juiste bestandje op te zoeken en te runnen. We hoeven alleen maar een commando in de terminal te geven — bijvoorbeeld python of conda — en de computer start automatisch het juiste programma op.

Dat willen wij ook voor onze programma's! En omdat we uv gebruiken kunnen we dat heel eenvoudig doen. We gaan even in een andere test-repository een commando toevoegen om de module uit te voeren waarvan je de code in paragraaf Modules kunt vinden. De twee bestanden square.py en count_count.py hebben we voor jullie netjes in een package geplaats in de repository AnneliesVlaar/just_count met de volgende structuur:

just_count/
    src/
        just_count/
            __init__.py
            square.py
            count_count.py
    .python-version
    pyproject.toml
    README.md
    uv.lock

De bestanden square.py en count_count.py zien er hetzelfde uit als in paragraaf Modules:

def square(x):
    return x**2


if __name__ == "__main__":
    print(f"The square of 4 is {square(4)}")
import square

print(f"The square of 5 is {square.square(5)}")

We kunnen uv niet vragen om een script te runnen, maar wel om een functie in een module uit te voeren. Een nieuw uv project krijgt automatisch al een voorbeeldscript. Daar gaan we even naar kijken en daarna passen we het aan voor eigen gebruik.

Voorbeeldscript

Je cloned de repository just_count in GitHub desktop en opent het daarna vanuit GitHub Desktop in Visual Studio Code. Je ziet een pyproject.toml en een uv.lock in de repository staan, dus je maakt meteen een virtual environment aan . Je opent een terminal en voert de opdracht just-count uit. De code hiervoor staat in src/just_count/__init__.py. Dit is overigens niet de beste plek, maar werkt prima als eenvoudig voorbeeld. Je bekijkt de code en ziet dat de bewuste code in een functie main() staat.

Testcode

__init__.py
def main() -> None:
    print("Hello from just-count!")
(just_count) > just-count

Als wij willen dat onze eigen code draait als we just-count intypen, dan moeten we zorgen dat onze code ook in een functie gezet wordt.

Main functie toevoegen

Je opent het hoofdbestand count_count.py en zet de body van de module in een functie main(). Daarna pas je het bestand aan zodat de functie nog steeds wordt uitgevoerd wanneer je het bestand count_count.py runt.

Testcode

count_count.py
import square

def main():
    print(f"The square of 5 is {square.square(5)}")

if __name__ == '__main__':
    main()
(just_count) > python .\src\just_count\count_count.py

Checkpunten:

  • Er is een functie main() in het bestand count_count.py
  • Het runnen van het bestand count_count.py geeft de output The square of 5 is 25

Projecttraject

  • main functie toevoegen
  • commando toevoegen
  • commando testen

In pyproject.toml kunnen we nu het commando toe gaan voegen. In de scripts-sectie kunnen we aangeven met welk commando een functie uit een module wordt uitgevoerd. In pyproject.toml staat al zo'n kopje:

[project.scripts]
just-count = "just_count:main"
De vorm van die laatste regel is als volgt:
naam_commando = "package.module:naam_functie"
Hier is naam_commando het commando dat je in moet typen in de terminal, package is de naam van het Python package waar de code staat, module is de naam van de module waar de code staat, en naam_functie is de naam van de functie waarin de code staat. Als je module weglaat, dan kijkt uv in __init__.py.

Om de wijzigingen aan pyproject.toml door te voeren moet je je virtual environment wel opnieuw synchroniseren. uv installeert dan jouw package ook opnieuw.

commando toevoegen

Je voegt in de pyproject.toml onder het kopje [project.scripts] een nieuw commando square toe. Deze verwijst naar de functie main() welke in de module count_count.py staat die ondergebracht is in de package just_count. Omdat je handmatig het toml-bestand hebt aangepast synchroniseer je je virtual environment opnieuw .

Pseudo-code

pyproject.toml
[project.scripts]
square = "just_count.count_count:main"

Checkpunten:

  • De naam van het commando is square.
  • De verwijzing na het = teken begint met twee aanhalingstekens gevolgd door het package just_count gevolgt door een punt.
  • Na de punt staat de naam van de module count_count.py zonder de extensie .py gevolgd door een dubbele punt.
  • Na de dubbele punt staat de naam van de functie main() zonder haakjes ().
  • Achter de functie staan weer dubble aanhalingstekens om de verwijzing te sluiten.
  • Na het opslaan van de pyproject.toml is het package opnieuw geïnstalleerd.

Projecttraject

  • main functie toevoegen
  • commando toevoegen
  • commando testen

Commando testen

Nu je het commando square hebt aangemaakt ga je deze testen in een terminal. Er verschijnt een error ModuleNotFoundError: No module named 'square'. Je leest het info-blokje hieronder.

Je runt het commando square opnieuw en je ziet de tekst The square of 5 is 25 verschijnen. Je vraagt je af of het commando ook werkt als de terminal in een andere map zit. Met het commando cd.. ga je naar een bovenliggende map. Je test het commando square en ziet weer de tekst The square of 5 is 25 verschijnen. Je concludeert dat het commando nu overal werkt zolang het juiste virtual environment is geactiveerd. Dat test je uit door het virtual environment te deactiveren en het commando square nogmaal te proberen. Je krijgt een error en hebt daarmee je vermoeden bewezen. Tevreden ga je door naar de volgende opdracht.

ModuleNotFoundError: No module named 'square'

Als je de Traceback leest zie je dat het probleem ontstaat in de module count_count.py. Zoiets hebben we al eerder gezien toen we werkten met het easystat package... Pas het import statement aan naar from just_count import square.

Pseudo-code

(just-count) > square 

Checkpunten:

  • Het import statement in count_count.py is genoteerd beginnend vanuit de map src.
  • Het commando square werkt als het juiste virtual environment is geactiveerd.
  • Het commando square werkt nog steeds nadat je met het commando cd.. naar een bovenliggende map bent gegaan.
  • Het commando square werkt niet als het virtual environment is gedeactiveerd.

Projecttraject

  • main functie toevoegen
  • commando toevoegen
  • commando testen
Error analysis

Als extra oefening gaan we met uv een commando maken om een ander script uit te laten voeren. De package is al aangemaakt, maar werkt nog niet naar behoren. Los in de volgende opdrachten de errors op om het script data_analysis.py te laten runnen.

  1. Ga naar GitHub en clone AnneliesVlaar/erroranalysis in GitHub Desktop en open de repository daarna in Visual Studio Code.
  2. Natuurlijk maak je gelijk een nieuw virtual environment aan , voordat we dit package gaan testen.
  3. Snuffel door de bestanden en mappen, en open src/erroranalysis/data_analysis.py. Dit is het script wat moet kunnen runnen.
  4. Run het script data_analysis.py en los de errors één voor één op.

Om erachter te komen of de problemen die we hierboven hadden écht zijn opgelost maak je een nieuw leeg virtual environment aan en test je dat het script niet werkt. Dan installeer je het package en run je het script opnieuw. Werkt alles? Mooi! Dan gaan we nu een commando aanmaken om de functie table() aan te roepen.

  1. Open pyproject.toml en zoek het kopje voor scripts. Het formaat was:
    [project.scripts]
    naam_commando = "package.module:naam_functie"
    
    pas de regel aan zodat jouw commando de functie table() aanroept in src/erroranalysis/data_analysis.py. Je mag de naam van het commando zelf kiezen.
  2. Ga naar de terminal en kijk of het werkt!
    (erroranalysis) > naam_commando 
    
    

Pythondaq: test imports

Bij het uitbouwen van de applicatie ga je mogelijk onderdelen uit de pythonpackage importeren. Daarom is het verstandig om, net als met de opdracht Easystat package testen, het importeren uit de package te testen. Maak daarvoor een tests-map met test_imports.py in de repository pythondaq.
test_imports.py
import pythondaq.run_experiment
Je runt het bestand test_imports.py en lost de errors op. Daarna werkt je package ook als je het aanroept van buiten de map met broncode. Je pythondaq-repository is nu een volledig project dat je met andere gebruikers van Python kunt delen, bijvoorbeeld via een wheel.
pythondaq
├──src
          └──pythondaq
                    ├──__init__.py
                    ├──arduino_device.py
                    ├──diode_experiment.py
                    └──run_experiment.py
├──tests
          └──test_imports.py
├──.python-version
├──pyproject.toml
├──README.md
└──uv.lock

Pseudocode

run_experiment.py
# define from which package the module diode_experiment should be imported
...
Testcode
test_imports.py
import pythondaq.view
(ECPC) > python test_imports.py

Checkpunten:

  • Er is een map tests in de repository pythondaq.
  • De import statements in de modules in het package pythondaq zijn aangepast zodat het bestand test_imports runt zonder problemen.

Projecttraject

  • Pythondaq: docstrings
  • Pythondaq: uv
  • Pythondaq: src-layout
  • Pythondaq: test imports
  • Pythondaq: applicatie

Applicaties runnen in virtual environments

Inmiddels ben je misschien gewend dat je virtual environments altijd moet activeren voordat je applicaties kunt runnen die daarin geïnstalleerd zijn. In Visual Studio Code gaat dat automatisch als je het environment goed geselecteerd hebt. Maar... hoe moet dat zonder VS Code? Omdat uv virtual environments in je projectmap neerzet (de .venv-map) moet je met je terminal wel in de goede map zitten. Daarna kun je ervoor kiezen om het commando te runnen met uv (dan is activeren niet nodig) of zonder uv (dan is activeren wel nodig). Voer de volgende opdrachten uit om dat te proberen.

  1. Open in GitHub Desktop nog een keer je easystat repository.
  2. Klik op Menu > Repository > Open in Command Prompt om een nieuwe terminal te openen, in je projectmap.
  3. Run het commando easystat. Dit werkt niet.
  4. Run het commando uv run easystat. Dit werkt wel.
  5. Activeer het environment door het volgende commando te runnen: .venv\Scripts\activate.
  6. Controleer dat (easystat) aan het begin van de opdrachtprompt staat.
  7. Run het commando easystat. Dit werkt nu wel.

Je mag zelf kiezen welke methode je fijn vindt.

Pythondaq: applicatie

Je maakt een commando om het script run_experiment.py uit de repository pythondaq te starten . Wanneer je het commando aanroept gaat het LED-lampje branden, en verschijnt er even later een IU-plot op het scherm. Je test of het commando ook buiten Visual Studio Code werkt door vanuit GitHub Menu > Repository > Open in Command Prompt een nieuwe terminal te openen. Je test het commando met uv run en ook door het juiste virtual environment te activeren . Je ziet dat ook dan het commando werkt, zonder uv run. Wat een feest! Je hebt nu een applicatie geschreven die een Arduino aanstuurt om een ledje te laten branden. En je kunt je applicatie gewoon vanuit de terminal aanroepen!

Pseudo-code

run_experiment.py
# import statements

# def function
    # code to start a measurement
pyproject.toml
[project.scripts]
naam_commando = "package.module:naam_functie"

Checkpunten:

  • De functie in run_experiment.py bevat alle code die uitgevoerd moet worden om een meting te starten.
  • Het commando in de pyproject.toml verwijst op de correcte manier naar de functie in run_experiment.py.
  • Het aanroepen van het commando zorgt ervoor dat een meting gestart wordt.
  • Het commando werkt ook in een losse zolang het juiste virtual environment actief is, óf uv run wordt gebruikt.

Projecttraject

  • Pythondaq: docstrings
  • Pythondaq: uv
  • Pythondaq: src-layout
  • Pythondaq: test imports
  • Pythondaq: applicatie
Versie 2.0.0

In de pyproject.toml kan je ook de versie aangeven van je package. Maar wanneer hoog je nu welk cijfertje op? Wanneer wordt iets versie 2.0.0? Daar zijn conventies voor. Bug fixes gaan op het laatste cijfer, wijzigingen en nieuwe features gaan op het middelste cijfer. Wanneer de applicatie dusdanig verandert dat je bijvoorbeeld bestanden die je met oude versie hebt gemaakt niet met de nieuwe versie kunt openen, dan verander je het eerste cijfer. Je start vaak met versie 0.1.0 en blijft tijdens het bouwen van je project ophogen naar 0.2.0 en soms zelfs 0.83.0. Wanneer je project min of meer klaar is voor eerste gebruik, dan kies je er vaak voor om versie 1.0.0 te releasen.


  1. Echt gebeurd: meerdere studenten leverden hun grafische applicatie in voor een beoordeling. We konden het niet draaien, want er misten bestanden. Bij de student werkte het wel, maar bij ons echt niet. 

  2. more is een programma die aangeleverde tekst per pagina laat zien, waar je met Space een volgende pagina te zien krijgt. Met Enter krijg je maar één regel extra en met Q sluit je het programma meteen af. Het | karakter stuurt output door. Dus uv help | more stuurt de output van uv help door naar het programma more

  3. Wanneer de repository op GitHub wordt geplaatst wordt deze README automatisch op de hoofdpagina van de repository getoond, onder de code. 

  4. Ja er is een map easystat met daarin een map src met daarin weer een map easystat — dat kan nog wel eens verwarrend zijn. Het is conventie om de projectmap dezelfde naam te geven als je package. Het pad is dus eigenlijk project/src/package en dat wordt dan, in ons geval, easystat/src/easystat

  5. Vroeger was er een setup.py maar Python schakelt nu langzaam over naar dit nieuwe bestand. 

  6. In f-strings kunnen tussen de accolades variabelen of functieaanroepen staan. Voeg daar het =-teken aan toe en je krijgt niet alleen de waarde, maar ook de variabele of aanroep zelf te zien. Bijvoorbeeld: als je definieert name = "Alice", dan geeft print(f"{name}") als uitkomst Alice. Maar voeg je het =-teken toe zoals in print(f"{name=")} wordt de uitvoer name='Alice'. Je ziet dan dus ook meteen de naam van de variabele en dat kan handig zijn. 

  7. Tom Preston-Werner, Pradyun Gedam, and others. Tom's obvious, minimal language. URL: https://github.com/toml-lang/toml