⚠️ Viikon tehtävien palautuksen deadline on tiistai 9.4. klo 23:59. Tehtävät on tarkoitus tehdä joko pajassa tai omatoimisesti.

Tehtävät palautetaan GitHubin avulla Labtooliin rekisteröimääsi repositorioon. Muista pushata tehtävät GitHubiin ennen palautuksen deadlinea! Klo 00 jälkeen tulevia repositorion päivityksiä ei huomioida pisteytyksessä, eli ne tuovat 0 pistettä.

Palautuksesta saadut pisteet ja palaute löytyy Labtoolista viimeistään deadlinea vastaavan viikon loppuun mennessä. Muista tarkistaa saamasi pisteet ja palaute. Jos pisteytyksestä herää kysymyksiä, lähetä viesti Labtoolin kautta.

Tämän viikon tehtävien palautuksesta on tarjolla 1 piste ja harjoitustyön palautuksesta 2 pistettä.

Tee palautettavia tehtäviä varten repositorion sisällä olevaan hakemistoon laskarit uusi alihakemisto viikko3.

UML

Ohjelmistojen dokumentoinnissa ja sovelluksen suunnittelun yhteydessä on usein tapana visualisoida ohjelman rakennetta ja toimintaa UML-kaavioilla.

UML tarjoaa lukuisia erilaisia kaaviotyyppejä, hyödynnämme kurssilla kuitenkin näistä ainoastaan kolmea.

Luokkakaaviot

Kurssilla Tietokantojen perusteet olet saattanut jo tutustua luokkakaavioiden käyttöön. Luokkakaavioiden käyttötarkoitus on ohjelman luokkien ja niiden välisten suhteiden kuvailu. Todo-sovelluksen oleellista tietosisältöä edustavat käyttäjää vastaava luokka User:

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

ja tehtävää vastaava luokka Todo:

import uuid

class Todo:
    def __init__(self, content, done=False, user=None, todo_id=None):
        self.content = content
        self.done = done
        self.user = user
        self.id = todo_id or str(uuid.uuid4())

    def set_done(self):
        self.done = True

Jokaiseen todoon liittyy yksi käyttäjä, ja yksittäiseen käyttäjään liittyviä todoja voi olla useita. Tilannetta kuvaa seuraava luokkakaavio

Luokkakaavio

Luokkakaavioon on nyt merkitty molempien luokkien oliomuuttujat sekä metodit. Yleensä ei ole mielekästä kuvata luokkia tällä tarkkuudella, eli luokkakaavioihin riittää merkitä luokan nimi:

Luokkien tarkemmat yksityiskohdat selviävät koodia katsomalla tai docstring-dokumentoinnista, johon tutustutaan viikolla 6.

Riippuvuus

UML-kaavioissa olevat “viivat” kuvaavat luokkien olioiden välistä pysyvää yhteyttä. Joissain tilanteissa on mielekästä merkata kaavioihin myös ei-pysyvää suhdetta kuvaava katkoviiva, eli riippuvuus.

Eräs tällainen tilanne voisi olla Unicafe-ruokalan kassapäätteen toiminnallisuudesta vastaava koodi. Koodissa on kaksi luokkaa Maksukortti ja Kassapaate, joiden välillä ei ole pysyvää yhteyttä.

Maksukortin koodi on seuraava:

class Maksukortti:
  def __init__(self, saldo):
      self.saldo = saldo

  def lataa_rahaa(self, lisays):
      self.saldo += lisays

  def ota_rahaa(self, maara):
      if self.saldo < maara:
          return False

      self.saldo -= maara

      return True

Kuten huomataan, koodissa ei mainita kassapäätettä millään tavalla.

Kassapäätteen hieman lyhennetty koodi on seuraava:

EDULLISEN_HINTA = 2.5
MAUKKAAN_HINTA = 4.3

class Kassapaate:
    def __init__():
        self.edulliset = 0
        self.maukkaat = 0

    def syo_edullisesti(self, kortti):
        if kortti.saldo() < EDULLISEN_HINTA:
            return False

        kortti.ota_rahaa(EDULLISEN_HINTA);
        self.edulliset += 1
        return True

    def syo_maukkaasti(self, kortti):
        # ...

    def lataa_rahaa_korttille(self, kortti, summa):
        if summa < 0:
            return

        kortti.lataa_rahaa(summa)
        self.rahaa += summa

Kassapääte käyttää maksukortteja hetkellisesti lounaiden maksamisen ja rahan lataamisen yhteydessä. Kassapääte ei kuitenkaan muista pysyvästi yksittäisiä maksukortteja. Tämän takia kassapäätteellä on riippuvuus maksukortteihin, mutta ei kuitenkaan normaalia yhteyttä, sillä UML-kaavioon merkattu yhteys viittaa pysyvään, ajallisesti pidempikestoiseen suhteeseen.

Tilannetta kuvaava luokkakaavio on seuraava:

Riippuvuus siis kuvataan katkoviivallisena nuolena, joka kohdistuu siihen luokkaan mistä ollaan riippuvaisia. Riippuvuuteen ei merkitä numeroa toisin kuin yhteyteen.

Tarkastellaan toisena esimerkkinä riippuvuudesta todo-sovelluksen sovelluslogiikasta vastaavaa luokkaa TodoService, jonka koodi hieman lyhennettynä näyttää seuraavalta:

class TodoService:
    def __init__(self, todo_repository, user_repository):
        self._user = None
        self._todo_repository = todo_repository
        self._user_repository = user_repository

    def create_todo(self, content):
        todo = Todo(content=content, user=self._user)

        return self._todo_repository.create(todo)

    def get_undone_todos(self):
        if not self._user:
            return []

        todos = self._todo_repository.find_by_username(self._user.username)
        undone_todos = filter(lambda todo: not todo.done, todos)

        return list(undone_todos)

    # ...

Sovelluslogiikkaa hoitava olio tuntee kirjautuneen käyttäjän, mutta pääsee käsiksi kirjautuneen käyttäjän todoihin ainoastaan todo_repository-olion välityksellä. Tämän takia luokalla ei ole yhteyttä luokkaan Todo, luokkien välillä on kuitenkin riippuvuus, sillä sovelluslogiikka käsittelee metodeissaan todo-olioita.

Merkitään luokkakaavioon seuraavasti:

Luokkakaavio

Riippuvuuksien merkitseminen luokkakaavioihin ei ole välttämättä kovin oleellinen asia, niitä kannattaa merkitä jos ne tuovat esille tilanteen kannalta jotain oleellista.

Perintä

Luokkien perintähierarkian ilmaisemisessa käytetään nuolia, joissa on valkoiset päät. Esim. jos Todo-sovelluksessa olisi normaalin käyttäjän eli luokan User perivää ylläpitäjää kuvaava luokka SuperUser, merkattaisiin se luokkakaavioon seuraavasti:

Työkaluja luokkakaavioiden piirtämiseen

GitHubin markdown-tiedostoissa erilaisia kaavioita voi toteuttaa kätevästi Mermaid-syntaksin avulla. Kaavioista ei tarvitse erillisiä kuvatiedostoja, vaan ne voi määritellä suoraan markdown-tiedostoon:

## Sovelluslogiikka

Sovelluksen loogisen tietomallin muodostavat luokat User ja Todo, jotka kuvaavat käyttäjiä ja käyttäjien tehtäviä:

```mermaid
 classDiagram
      Todo "*" --> "1" User
      class User{
          username
          password
      }
      class Todo{
          id
          content
          done
      }
```

Dokumentaation Luokkakaavio-osiosta löytyy tarkemmat ohjeet luokkakaavioiden toteuttamiseen. Mallia kaavioiden käyttämisessä markdown-tiedostossa voi ottaa referenssisovelluksen arkkitehtuuri-dokumentaatiosta. Vaihtoehtoisesti, luokkakaavioiden toteuttamiseen sopii myös esimerkiksi https://app.diagrams.net/.


Tehtävä 1: Monopoli

Monopoli on varmasti kaikkien tuntema lautapeli.

Tehdään luokkakaavio, joka kuvaa pelissä olevia asioita ja niiden suhteita. Aloitetaan sellaisella, joka ei kuvaa peliä vielä kokonaisuudessaan vaan sisältää vasta seuraavat elementit:

Monopolia pelataan käyttäen kahta noppaa. Pelaajia on vähintään 2 ja enintään 8. Peliä pelataan pelilaudalla joita on yksi. Pelilauta sisältää 40 ruutua. Kukin ruutu tietää, mikä on sitä seuraava ruutu pelilaudalla. Kullakin pelaajalla on yksi pelinappula. Pelinappula sijaitsee aina yhdessä ruudussa.

Tämä alustava kaavio näyttää seuraavalta:

(Luokkien keskinäisiä suhteita voisi kuvata hieman tarkemminkin, mutta tämä taso riittää meille.) Kaavio on toteutettu markdown-tiedostoon Mermaid-syntaksin mukaisesti seuraavaan tapaan:

## Monopoli, alustava luokkakaavio

```mermaid
 classDiagram
    Monopolipeli "1" -- "2" Noppa
    Monopolipeli "1" -- "1" Pelilauta
    Pelilauta "1" -- "40" Ruutu
    Ruutu "1" -- "1" Ruutu : seuraava
    Ruutu "1" -- "0..8" Pelinappula
    Pelinappula "1" -- "1" Pelaaja
    Pelaaja "2..8" -- "1" Monopolipeli
```

Laajennetaan nyt luokkakaaviota tuomalla esiin seuraavat asiat:

Ruutuja on useampaa eri tyyppiä:

  • Aloitusruutu
  • Vankila
  • Sattuma ja yhteismaa
  • Asemat ja laitokset
  • Normaalit kadut (joihin liittyy nimi)

Monopolipelin täytyy tuntea sekä aloitusruudun että vankilan sijainti.

Jokaiseen ruutuun liittyy jokin toiminto.

Sattuma- ja yhteismaaruutuihin liittyy kortteja, joihin kuhunkin liittyy joku toiminto.

Toimintoja on useanlaisia. Ei ole vielä tarvetta tarkentaa toiminnon laatua.

Normaaleille kaduille voi rakentaa korkeintaan 4 taloa tai yhden hotellin. Kadun voi omistaa joku pelaajista. Pelaajilla on rahaa.

Lisää tämän viikon tehtäviä varten repositoriosi laskarit hakemistoon hakemisto viikko3 ja lisää toteuttamasi kaavio sinne. Jos hyödynnät Mermaid-syntaksia kaavion toteuttamisessa, voit sijoittaa kaavion markdown-tiedostoon.

Pakkauskaavio

Todo-sovelluksen koodi on sijoitettu hakemistoihin seuraavasti:

Hakemistorakennetta voidaan kuvata UML:ssä pakkauskaaviolla:

Pakkausten välille on merkitty riippuvuudet katkoviivalla. Pakkaus ui riippuu pakkauksesta services sillä ui-pakkauksen luokat käyttävät services-pakkauksen luokkaa TodoService, joka vastaa sovelluksen sovelluslogiikasta.

Vastaavasti pakkaus services riippuu pakkauksesta repositories sillä sen luokka TodoService käyttää repositories-pakkauksen luokkia TodoRepository ja UserRepository.

Pakkauskaavioihin on myös mahdollista merkitä pakkausten sisältönä olevia luokkia normaalin luokkakaaviosyntaksin mukaan:

Sovelluksen koodi on organisoitu kerrosarkkitehtuurin periaatteiden mukaan. Asiasta lisää hieman täällä. Myös Ohjelmoinnin jatkokurssin osa 10 luku Laajemman sovelluksen kehittäminen voi olla hyödyllinen.

Sekvenssikaaviot

Luokka- ja pakkauskaaviot kuvaavat ohjelman rakennetta. Ohjelman toiminta ei kuitenkaan tule niistä ilmi millään tavalla.

Esimerkiksi Unicafe-ruokalan maksukortin ja kassapäätteen välistä suhdetta kuvaava luokkakaavio voisi näyttää seuraavalta:

Luokkakaavio

Vaikka kaavioon on merkitty metodien nimet, ei ohjelman toimintalogiikka, esimerkiksi mitä tapahtuu kun kortilla ostetaan edullinen lounas, selviä kaaviosta millään tavalla.

Sekvenssikaaviot on alunperin kehitetty kuvaamaan verkossa olevien ohjelmien keskinäisen kommunikoinnin etenemistä. Sekvenssikaaviot sopivat kohtuullisen hyvin kuvaamaan myös sitä, miten ohjelman oliot kutsuvat toistensa metodeja suorituksen aikana.

Koodia katsomalla näemme, että lounaan maksaminen tapahtuu siten, että ensin kassapääte kysyy kortin saldoa ja jos se on riittävä, vähentää kassapääte lounaan hinnan kortilta ja palauttaa True:

EDULLISEN_HINTA = 2.5

class Kassapaate:
    # ...

    def syo_edullisesti(self, kortti):
        if kortti.saldo < EDULLISEN_HINTA:
            return False

        kortti.ota_rahaa(EDULLISEN_HINTA)
        self.edulliset += 1
        return True

    # ...

Sekvenssikaaviona kuvattuna tilanne näyttää seuraavalta:

Sekvenssikaaviossa oliot kuvataan laatikoina, joista lähtee alaspäin olion “elämänlanka”. Kaaviossa aika etenee ylhäältä alas. Metodikutsut kuvataan nuolena, joka yhdistää kutsujan ja kutsutun olion elämänlangat. Paluuarvo merkitään katkoviivalla. Attribuutin arvon lukeminen tai asettaminen voidaan kuvata kaaviossa metodikutsun tavoin. Tästä esimerkkinä kaavion saldo-attribuutin lukeminen.

Jos saldo ei riitä, etenee suoritus seuraavan sekvenssikaavion tapaan:

Tarkastellaan hieman monimutkaisempaa tapausta, yrityksen palkanhallinnasta vastaavaa ohjelmaa:

class Henkilo:
    def __init__(self, nimi, palkka, tilinumero):
        self.nimi = nimi
        self.palkka = palkka
        self.tilinumero = tilinumero

class Henkilostorekisteri:
    def __init__(self):
        self._henkilot = {}
        self._pankki = PankkiRajapinta()

    def lisaa(self, henkilo):
        self._henkilot[henkilo.nimi] = henkilo

    def suorita_palkanmaksu(self):
        for nimi in self._henkilot:
            henkilo = self._henkilot[nimi]
            self._pankki.maksa_palkka(henkilo.tilinumero, henkilo.palkka)

    def aseta_palkka(self, nimi, uusi_palkka):
        henkilo = self._henkilot[nimi]
        henkilo.palkka = uusi_palkka

class PankkiRajapinta:
    # ...

    def maksa_palkka(tilinumero, summa):
        # suorittaa maksun verkkopankin internet-rajapinnan avulla
        # yksityiskohdat piilotettu

Sekvenssikaaviot siis kuvaavat yksittäisten suoritusskenaarioiden aikana tapahtuvia asioita. Kuvataan nyt seuraavan pääohjelman aikaansaamat tapahtumat:

def main():
    rekisteri = Henkilostorekisteri()

    arto = Henkilo("Hellas", 1200, "1234-12345")
    rekisteri.lisaa(arto)

    sasu = Henkilo("Tarkoma", 6500, "4455-123123")
    rekisteri.lisaa(sasu)

    rekisteri.aseta_palkka("Hellas", 3500)

    rekisteri.suorita_palkanmaksu()

Sekvenssikaavio on seuraavassa:

Kaavio alkaa tilanteesta, jossa Henkilostorekisteri-luokan olio on jo luotu, mutta henkilöolioita ei vielä ole olemassa.

Toiminta alkaa siitä, kun pääohjelma eli main luo henkilön nimeltä arto. Seuraavaksi main-funktiosta kutsutaan rekisterin metodia lisaa, jolle annetaan argumentiksi luotu henkilöolio. Vastaava toistuu kun main luo uuden henkilön ja lisää sen rekisteriin.

Seuraavana toimenpiteenä main kasvattaa arton palkkaa kutsumalla rekisterin metodia aseta_palkka. Tämä saa aikaan sen, että rekisteri asettaa arto-olion palkka-attribuutille uuden arvon. Rekisterin viivaan on merkitty paksunnus, joka korostaa, että attribuutille on asetettu arvo. Huomaa, että olion attribuutin asettamista voidaan kuvata metodikutsun tavoin.

Viimeinen ja monimutkaisin toiminnoista käynnistyy, kun main kutsuu rekisterin metodia suorita_palkanmaksu. Rekisteri kysyy ensin arton tilinumeroa ja palkkaa ja kutsuu paluuarvoina olevilla tiedoilla pankin metodia maksa_palkka ja sama toistuu sasu-olion kohdalla.

Sekvenssikaaviot eivät ole optimaalinen tapa ohjelman suorituslogiikan kuvaamiseen. Ne sopivat jossain määrin olio-ohjelmien toiminnan kuvaamiseen, mutta esim. funktionaalisella tyylillä tehtyjen ohjelmien kuvaamisessa ne ovat varsin heikkoja.

Tietynlaisten tilanteiden kuvaamiseen ohjelmoinnin perusteissakin käsitellyt vuokaaviot voivat sopia paremmin.

Voit halutessasi lukea lisää sekvenssikaavioista kurssin vanhan version materiaalista.

Työkaluja sekvenssikaavioiden piirtämiseen

Myös sekvenssikaavioiden toteuttaminen onnistuu kätevästi Mermaid-syntaksin avulla. Dokumentaation Sekvenssikaavio-osiosta löytyy tarkemmat ohjeet sekvenssikaavioiden toteuttamiseen. Mallia kaavioiden käyttämisessä markdown-tiedostossa voi ottaa referenssisovelluksen arkkitehtuuri-dokumentaatiosta. Vaihtoehtoisesti, sekvenssikaavioiden toteuttamiseen sopii myös esimerkiksi https://www.websequencediagrams.com/.


Tehtävä 2: sekvenssikaavio

Tarkastellaan HSL-matkakorttien hallintaan käytettävää koodia.

Kuvaa sekvenssikaaviona koodin main-funktion suorituksen aikaansaama toiminnallisuus.

Muista, että sekvenssikaaviossa tulee tulla ilmi kaikki mainin suorituksen aikaansaamat olioiden luomiset ja metodien kutsut!

class Kioski:
    def osta_matkakortti(self, nimi, arvo = None):
        uusi_kortti = Matkakortti(nimi)

        if arvo:
            uusi_kortti.kasvata_arvoa(arvo)

        return uusi_kortti

class Matkakortti:
    def __init__(self, omistaja):
        self.omistaja = omistaja
        self.pvm = 0
        self.kk = 0
        self.arvo = 0

    def kasvata_arvoa(self, maara):
        self.arvo += maara

    def vahenna_arvoa(self, maara):
        self.arvo -= maara

    def uusi_aika(self, pvm, kk):
        self.pvm = pvm
        self.kk = kk

class Lataajalaite:
    def lataa_arvoa(self, kortti, maara):
        kortti.kasvata_arvoa(maara)

    def lataa_aikaa(self, kortti, pvm, kk):
        kortti.uusi_aika(pvm, kk)

RATIKKA = 1.5
HKL = 2.1
SEUTU = 3.5

class Lukijalaite:
    def osta_lippu(self, kortti, tyyppi):
        hinta = 0

        if tyyppi == 0:
            hinta = RATIKKA
        elif tyyppi == 1:
            hinta = HKL
        else:
            hinta = SEUTU

        if kortti.arvo < hinta:
            return False

        kortti.vahenna_arvoa(hinta)

        return True

class HKLLaitehallinto:
    def __init__(self):
        self._lataajat = []
        self._lukijat = []

    def lisaa_lataaja(self, lataaja):
        self._lataajat.append(lataaja)

    def lisaa_lukija(self, lukija):
        self._lukijat.append(lukija)

def main():
    laitehallinto = HKLLaitehallinto()

    rautatietori = Lataajalaite()
    ratikka6 = Lukijalaite()
    bussi244 = Lukijalaite()

    laitehallinto.lisaa_lataaja(rautatietori)
    laitehallinto.lisaa_lukija(ratikka6)
    laitehallinto.lisaa_lukija(bussi244)

    lippu_luukku = Kioski()
    kallen_kortti = lippu_luukku.osta_matkakortti("Kalle")

    rautatietori.lataa_arvoa(kallen_kortti, 3)

    ratikka6.osta_lippu(kallen_kortti, 0)
    bussi244.osta_lippu(kallen_kortti, 2)

if __name__ == "__main__":
    main()

Lisää toteuttamasi kaavio repositoriosi laskarit/viikko3-hakemistoon. Jos hyödynnät Mermaid-syntaksia kaavion toteuttamisessa, voit sijoittaa kaavion markdown-tiedostoon.

Tehtävien suorittaminen ja Invoke

Projekteissa on useimmiten monia toistuvasti suoritettavia tehtäviä, joita suoritetaan terminaalissa annettavien komentojen muodossa. Luultavasti tärkein näistä tehtävistä on sovelluksen käynnistäminen, joka saattaa tapahtua esimerkiksi komennolla python3 src/index.py.

Tehtäviin liittyvien komentojen kirjoittaminen käsin käy helposti työlääksi. Tämä tulee ilmi etenkin tilanteissa, joissa komennot ovat monimutkaisia, tai vaativat useampien komentojen suorittamista. Ongelman ratkaisemiseksi on kehitetty työkaluja, joiden avulla tehtäviä voi määritellä ja suorittaa terminaalissa helposti. Tutustutaan seuraavaksi erääseen tähän käyttötarkoitukseen soveltuvaan työkaluun nimeltä Invoke.

Asennus

Invoken asennus projektiin onnistuu komennolla:

poetry add invoke

Tehtävien määritteleminen

Invoken avulla määritellyt tehtävät toteutetaan projektin juurihakemiston tasks.py-tiedostoon. Tehtävät ovat funktioita, joissa käytetään @task-dekoraattoria. Toteutetaan esimerkkinä tasks.py-tiedostoon tehtävä nimeltä foo, joka tulostaa tekstin “bar”:

from invoke import task

@task
def foo(ctx):
    print("bar")

Tämän erittäin hyödyllisen tehtävän voi suorittaa terminaalissa komennolla:

poetry run invoke foo

Komennon suorittamisen pitäisi tulostaa komentoriville teksti “bar”. Tehtävät voi siis suorittaa terminaalissa komennolla, joka on muotoa poetry run invoke <tehtävä>. Huomaa, että poetry run-komennon ansiosta tehtävät suoritetaan virtuaaliympäristössä.

Toteutetaan seuraavaksi foo-tehtävän lisäksi tehtävä, josta on oikeasti hyötyä. Tarvitsemme tehtävän, joka suorittaa sovelluksemme komennolla python3 src/index.py. Annetaan tälle tehtävälle nimeksi start:

from invoke import task

@task
def foo(ctx):
    print("bar")

@task
def start(ctx):
    ctx.run("python3 src/index.py", pty=True)

Voimme suorittaa tehtävässä komentorivikomennon käyttämällä parametrina saadun Context-olion metodia run. Tehtävän suorittaminen onnistuu komennolla poetry run invoke start. Huomaa, että pty=True-argumentti on erityisen tärkeä komentorivikäyttöliittymässä, jotta sovelluksen syötteet ja tulosteet toimivat odotetulla tavalla.

Voimme listata kaikki projektissa käytössä olevat tehtävät komennolla:

poetry run invoke --list

Huomioita tehtävien nimeämisestä

Jos tehtävän määrittelevän funktion nimi on snake_case-formaatissa, on komentoriviltä suoritettavan tehtävän nimi kebab-case-formaatissa. Esimerkiksi seuraavasti nimetty tehtävä:

from invoke import task

@task
def lorem_ipsum(ctx):
    print("Lorem ipsum")

Suoritettaisiin komennolla poetry run invoke lorem-ipsum. Jos olet epävarma käytössä olevien tehtävien nimistä, voit aina listata ne komennolla poetry run invoke --list.

Toisistaan riippuvaiset tehtävät

Coverage-ohjeissa tutustuimme testikattavuuden keräämiseen ja raportin muodostamiseen sen perusteella. Jos haluamme muodostaa testikattavuusraportin, tulee testikattavuus olla ensin kerätty. Käyttötarkoitukseen soveltuvilla tehtävillä voisi olla määrittelyt seuraavasti:

from invoke import task

@task
def coverage(ctx):
    ctx.run("coverage run --branch -m pytest", pty=True)

@task()
def coverage_report(ctx):
    ctx.run("coverage html", pty=True)

Jos suoritamme tehtävän coverage-report ennen coverage-tehtävän suorittamista, raportti sisältää joko vanhat testikattavuustiedot, tai kohtaamme virheen, joka valittaa testikattavuustietojen puutosta. Voisimme suorittaa komennot peräkkäin komennolla:

poetry run invoke coverage coverage-report

Helpompaa on kuitenkin määritellä coverage-report-tehtävä riippuvaiseksi coverage-tehtävästä. Tämä onnistuu antamalla @task-dekoraattorille argumentiksi coverage-tehtävän funktio:

from invoke import task

@task
def coverage(ctx):
    ctx.run("coverage run --branch -m pytest", pty=True)

@task(coverage)
def coverage_report(ctx):
    ctx.run("coverage html", pty=True)

Nyt komento poetry run invoke coverage-report suorittaa ensin tehtävän coverage, jonka jälkeen suoritetaan itse tehtävä coverage-report.

Jos haluat, että oletus työpöytäsovelluksesi avaisisi joka kertaa uudelleen tuodun raportin, voit laajentaa coverage-report task:in näin:

from subprocess import call
from sys import platform

@task(coverage)
def coverage_report(ctx):
    ctx.run("coverage html", pty=True)
    if platform != "win32":
        call(("xdg-open", "htmlcov/index.html"))

Harjoitustyö

Tämän viikon aikana aloitetaan harjoitustyön toteutus ja testaaminen. Ohjelman tulee edistyä jokaisella viikolla tasaisesti. Jos ohjelma tulee valmiiksi jo ennen loppupalautusta valmistaudu laajentamaan sitä saadaksesi ohjelman edistymisestä pisteet. Tarkoitus on edistää projektia tasaisesti kurssiviikkojen aikana.

Tämän viikon harjoitustyön palautuksesta on tarjolla 2 pistettä. Viikkopisteiden lisäksi kannattaa pitää mielessä harjoitustyön lopullisen palautuksen arvosteluperusteet.

Varoitus: pip

Olet kenties saattanut aiemmin asentaa Pythonin tarvitsemia riippuvuuksia pip-komennolla. Älä kuitenkaan käytä pipiä tällä kurssilla sillä jos teet niin, teet 99.9% todennäköisyydellä jotain väärin. Asenna riippuvuudet tällä kurssilla Poetryn avulla.

Harjoitustyö 1: Poetry projektin alustaminen

Alusta repositoriosi juureen Poetry-projekti edellisen viikon Poetry-ohjeiden mukaisesti. Repositorion rakenne tulee olla seuraava:

laskarit/
  ...
dokumentaatio/
  ...
src/
  ...
pyproject.toml
poetry.lock
README.md
...

Projektin koodi tulee sijoittaa repositorion src-hakemistoon. Koodia kannattaa tarpeen mukaan jakaa hakemiston sisällä alihakemistoihin. Mallia voi ottaa referenssisovelluksesta.

Voit myös halutessasi alustaa projektin haluamaasi alihakemistoon, esimerkiksi seuraavasti:

laskarit/
  ...
todo-app/
  dokumentaatio/
    ...
  src/
    ...
  pyproject.toml
  poetry.lock
  ...
README.md
...

HUOM: src-hakemiston alahakemistoissa (ei siis itse src-hakemistossa) tulee olla tyhjät __init__.py-tiedostot, jotta mm. import-lauseet toimivat halutulla tavalla. Lisää aiheesta voi lukea Pythonin dokumentaatiosta ja mallia voi ottaa referenssisovelluksesta.

Harjoitustyö 2: Toiminnallisuuden toteutus

Toteuta ainakin osa jostain edellisellä viikolla tekemäsi määrittelydokumentin toiminnallisuudesta. Pelkät tyhjät luokat tai funktiot ilman toiminnallisuutta eivät tuo pisteitä.

Toteutukseen liittyviä ohjeita löydät täältä. Jos olet toteuttamassa peliä, kannattaa yleisten ohjeiden lisäksi tutustua pygame-ohjeeseen.

Harjoitustyö 3: Testaamisen aloittaminen

Sovelluksella on oltava vähintään yksi testi. Testin tulee olla mielekäs, eli sen on testattava jotain ohjelman kannalta merkityksellistä asiaa. Testin tulee myös mennä läpi. Lisää testejä varten src hakemistoon hakemisto tests ja lisää testitiedostot sinne:

src/
  tests/
    __init__.py
    ...
  ...

Kertaa edellisen viikon unittest-ohjeet, jos tämä tuottaa hankaluuksia.

Harjoitustyö 4: Testikattavuusraportti

Ohjelmalle tulee pystyä generoimaan coverage-työkalun avulla testikattavuusraportti. Projektin juurihakemistossa (samassa hakemistossa, missä pyproject.toml-tiedosto sijaitsee) tulee olla .coveragerc-tiedosto, jossa määritellään, mistä hakemistosta testikattavuus kerätään. Testeihin liittyvä koodi tulee jättää testikattavuusraportin ulkopuolelle:

[run]
source = src
omit = src/**/__init__.py,src/tests/**

Kertaa edellisen viikon coverage-ohjeet, jos tämä tuottaa hankaluuksia. Mallia coveragen konfigurointiin voi tarvittaessa ottaa referenssisovelluksesta.

Harjoitustyö 5: Invoke-tehtävät

Toteuta projektille seuraavat Invoke-tehtävät:

  • poetry run invoke start käynnistää ohjelman
  • poetry run invoke test suorittaa testit pytestin avulla
  • poetry run invoke coverage-report kerää coveragen avulla testikattavuuden ja muodostaa sen perusteella selaimessa avattavan, HTML-muotoisen testikattavuusraportin

Mallia Invoke-tehtävien toteutukseen voi ottaa tarvittaessa referenssisovelluksesta. Voit halutessasi lisätä myös muita tehtäviä, joita koet projektisi kannalta hyödylliseksi.

Harjoitustyö 6: Changelog

Changelogin ylläpitäminen on yleinen tapa dokumentoida merkittävät muutokset, joita ohjelmistoprojektissa tapahtuu sen kehityksen edetessä. Lisää projektin dokumentaatio-hakemistoon tiedosto changelog.md ja dokumentoi siihen jokaisen viikon aikana tapahtuneet merkittävät muutokset. Merkittäviä muutoksia ovat esimerkiksi uudet käyttäjälle näkyvät toiminnallisuudet, suuremmat arkkitehtuuriset muutokset (esimerkiksi uudet luokat ja niiden vastuualueet) ja uudet testauksen kohteet. Esimerkiksi referenssisovelluksessa tämän viikon changelog-merkintä on seuraava:

## Viikko 3

- Käyttäjä näkee listan kaikista tehtävistä
- Lisätty TodoRepository-luokka, joka vastaa tehtävien tallennuksesta CSV-tiedostoon
- Lisätty TodoService-luokka, joka vastaa sovelluslogiikan koodista
- Testattu, että TodoRepository-luokka palauttaa kaikki tehtävät

Lisää README.md-tiedostoon linkki, joka vie lisäämääsi changelog.md-tiedostoon.

Harjoitustyö 7: Muuta

Varmista vielä, että seuraavat asiat ovat kunnossa:

  • Tuntikirjanpito on ajantasalla
    • Tuntikirjanpitoon ei merkitä laskareihin käytettyä aikaa
  • Viikolle on tehty changelog-merkintä changelog.md-tiedostoon
  • Repositorion README.md-tiedosto kunnossa
    • Tiedosto on kurssin tämän vaiheen osalta relevantin sisällön suhteen samankaltainen kuin referenssisovelluksen README.md-tiedosto
    • Kaikki ylimääräinen, mm. linkit laskareihin on poistettu
  • Repositorio on siisti
    • Ei ylimääräistä tavaraa (esim. pytest-, tai coverage-komentojen generoimia hakemistoja ja tiedostoja)
    • Laskarit jätetään hakemiston laskarit alle
    • Järkevä .gitignore-tiedosto olemassa. Mallia voi ottaa referenssisovelluksesta

Harjoitustyön toimivuus

HUOM: Saadaksesi harjoitustyöstä viikkokohtaiset pisteet, sovelluksen tulee toimia laitoksen tietokoneella ja ohjaajien pitää pystyä se niiltä aukaisemaan! Voit testata tätä millä tahansa Cubbli-tietokoneella, kuten fuksiläppärillä, tai laitoksen tietokoneluokkien tietokoneilla. Testaus onnistuu myös virtuaalityöasemassa joko selaimen tai VMWare Horizon -asiakasohjelman avulla.

Virtuaalityöasemassa oman sovelluksen testaaminen onnistuu selaimen avulla seuraavasti:

  1. Kirjaudu virtuaalityöasemaan ja valitse Cubbli Linux
  2. Käynnistä terminaali ja tarkista käytössäoleva Python-versio komennolla python3 --version. Jos versio on alle 3.8, päivitä versio tämän ohjeen avulla
  3. Varmista, että Poetry on asennettu suorittamalla komento poetry --version. Jos asennus puuttuu, seuraa näitä Linux-asennuksen ohjeita
  4. Kloonaa repositoriosi haluamaasi hakemistoon git clone-komennolla
  5. Siirry repositoriosi hakemistoon ja asenna riippuvuudet komennolla poetry install. Huomaa, että komento tulee suorittaa hakemistossa, jossa pyproject.toml-tiedosto sijaitsee

Mikäli yhteys virtuaalityöasemaan pätkii, kannattaa kokeilla toista selainta. Käyttäjät ovat raportoineet ainakin Google Chromen toimivan varsin hyvin. Myös VMWare Horizon Clientin asentaminen saattaa auttaa.

HUOM: Jos suoritat SQLite-tietokantaa käyttävää sovellusta virtuaalityöasemassa, saatat törmätä virheeseen database is locked. Ongelma ratkeaa luultavasti tämän ohjeen avulla.

Älä plagioi tai riko tekijänoikeuksia

Plagiointi

Kurssilla seurataan Helsingin yliopiston opintokäytäntöjä. Plagiarismi ja opintovilppi, eli esimerkiksi netissä olevien tai kaverilta saatujen vastausten kopiointi ja niiden palauttaminen omana työnä on kiellettyä. Todettu opintovilppi johtaa kurssisuorituksen hylkäämiseen ja toistuva opintovilppi voi johtaa opinto-oikeuden määräaikaiseen menettämiseen.

Mitä plagiointi tarkoittaa harjoitustyön yhteydessä? Koodin suora kopioiminen on kiellettyä poikkeuksena muutaman rivin mittaiset algoritmit ja ChatGPT:n tai vastaavien välineiden generoima koodi (ks. alla). Myös koodin rakenteen suora kopioiminen esim. siten että muuttujien ja funktioiden nimet muutetaan lasketaan plagioinniksi. Toisaalta esim. verkosta löytyneitä kuvia saa käyttää, jos tähän on oikeus (ks. kohta tekijänoikeudet), mutta näin tehtävessä tulee työn dokumentaatioon tehdä “lähdeviite”, eli mainita mistä lainaus on tehty.

Samat plagiaattisäännöt koskevat työn dokumentaatiota ja erityisen kiellettyä on copy pasteta referenssisovelluksen dokumentaatiota.

ChatGPT ja vastaavat

Myös ChatGPT:n ja vastaavien tekoälyyn perustuvien välineiden (kuten esim. Bing Chatin, Google Bardin tai GitHub Copilotin) generoiman koodin tai tekstin esittäminen itse kirjoitetuksi on plagiointia. Generoidun koodin käyttö on kurssilla sallittua, mutta “ympäröi” aina tällainen koodi kommenteilla # generoitu koodi alkaa ja # generoitu koodi päättyy. Tee näin myös silloin, jos olet tehnyt generoituun koodiin vain vähäisiä muutoksia (vaihtanut muuttujien ja funktioiden nimiä tms.).

Muistathan, että ChatGPT:n ja vastaavien välineiden käyttö yksikkötestien koodin generointiin on kurssilla kiellettyä (muuten saa kyllä käyttää testailtaessakin).

Tekijänoikeudet

Kunnioita muutenkin tekijänoikeuksia ja muita immateriaalioikeuksia. Muista, että et saa käyttää mitä tahansa verkosta löytynyttä omassa työssäsi. Tämä koskee monenlaista materiaalia ohjelmakoodista kuviin ja teksteihin. Tarkista siis aina, että onko käyttö sallittua sen lisenssin mukaan, jolla materiaali mahdollisesti on jaettu. Muista, että harjoitustyöt ovat lähtökohtaisesti julkisia GitHubissa. On omalla vastuullasi, ettet riko tekijänoikeuksia.