Cvičení: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14.

Na tomto cvičení se podíváme na dva užitečné prográmky, xargs a find. A také si rozšíříme znalosti o o SSH, protože si povíme o tzv. port forwardingu.

Ale hlavní náplní bude vývoj projektů v jazyce Python ve virtuálním (izolovaném) prostředí, které lze snadno distribuovat mezi jednotlivé vývojáře ve velkém softwarovém týmu. A také se podíváme, jak se programy chystají na další distribuci.

Části o find a xargs jsou částečně propojeny, jinak jsou tři témata (find/xargs, SSH port forwarding a práce v izolovaném prostředí) v podstatě nezávislá a lze je číst v libovolném pořadí. Znalost o pokročilém vývoji v Pythonu se bude hodit pro poslední domácí úlohu.

Předstartovní kontrola

  • Víte, co je SSH.
  • Víte, co jsou TCP porty.
  • Víte, co je to obraz disku.
  • Pamatujete si, k čemu se používají shellové wildcardy (zástupné znaky).
  • Víte, že řetězce v C jsou zakončeny nulovým bajtem.
  • Víte, jak se vytvářejí a jak jsou organizovány moduly v Pythonu.

Nástroje xargs (a parallel)

xargs ve své nejjednodušší podobě čte standardní vstup a převádí jej na argumenty pro uživatelem zadaný program.

Předpokládejme, že existují následující soubory:

2024-04-16.txt  2024-04-24.txt  2024-05-02.txt  2024-05-10.txt
2024-04-17.txt  2024-04-25.txt  2024-05-03.txt  2024-05-11.txt
2024-04-18.txt  2024-04-26.txt  2024-05-04.txt  2024-05-12.txt
2024-04-19.txt  2024-04-27.txt  2024-05-05.txt  2024-05-13.txt
2024-04-20.txt  2024-04-28.txt  2024-05-06.txt  2024-05-14.txt
2024-04-21.txt  2024-04-29.txt  2024-05-07.txt  2024-05-15.txt
2024-04-22.txt  2024-04-30.txt  2024-05-08.txt
2024-04-23.txt  2024-05-01.txt  2024-05-09.txt

Jako malé cvičení napište jednořádkový příkaz shellu, který tyto soubory vytvoří.

Řešení.

Naším úkolem je odstranit soubory starší než 20 dní. V této verzi pouze provedeme echo příkazu, abychom nemuseli soubory při ladění našeho řešení znovu vytvářet.

cutoff_date="$( date -d "20 days ago" '+%Y%m%d' )"
for filename in 202[0-9]-[01][0-9]-[0-3][0-9].txt; do
    date_num="$( basename "$filename" .txt | tr -d '-' )"
    if [ "$date_num" -lt "$cutoff_date" ]; then
        echo rm "$filename"
    fi
done

To znamená, že program rm by byl volán několikrát, přičemž by vždy odstranil jen jeden soubor. Režie spojená se spouštěním nového procesu by se mohla stát vážným úzkým hrdlem pro větší skripty (uvažujte například o tisících souborů).

Bylo by mnohem lepší, kdybychom rm zavolali jen jednou a zadali mu seznam souborů, které má odstranit (tj. jako více argumentů).

Zde je řešením xargs. Trochu program upravíme:

cutoff_date="$( date -d "20 days ago" '+%Y%m%d' )"
for filename in 202[0-9]-[01][0-9]-[0-3][0-9].txt; do
    date_num="$( basename "$filename" .txt | tr -d '-' )"
    if [ "$date_num" -lt "$cutoff_date" ]; then
        echo "$filename"
    fi
done | xargs echo rm

Místo toho, abychom soubor hned odstranili, vypíšeme pouze jeho název a celou smyčku přesměrujeme rourou do xargs, kde všechny jeho běžné argumenty odkazují na program, který má být spuštěn.

Místo mnoha řádků s rm ... uvidíme jen jeden dlouhý řádek s jediným voláním rm.

Další situací, kdy se xargs může hodit, je sestavování složitého příkazového řádku nebo situace, kdy by použití nahrazování příkazů ($( ... )) způsobilo nečitelnost skriptu.

Záludné názvy souborů mohou samozřejmě stále způsobovat problémy, protože xargs předpokládá, že argumenty jsou odděleny bílými znaky. (Všimněte si, že výše jsme byli v bezpečí, protože názvy souborů byly rozumné.) To lze změnit pomocí --delimiter.

Pokud do xargs přesměrováváte výstup z vašeho programu, zvažte oddělení položek nulovým bajtem (tj. terminátorem řetězce v jazyce C, \0). Vzpomeňte si, co jste měli v kurzu Arduina o řetězcích C – a jak se ukončují. To je nejbezpečnější možnost, protože tento znak se nemůže objevit nikde uvnitř žádného argumentu. A informujte o tom xargs pomocí -0 nebo --null.

Poznamenejme, že xargs je dostatečně chytrý na to, aby si uvědomil, kdy by byl příkazový řádek příliš dlouhý, a automaticky jej rozdělí (podrobnosti viz manuál).

Je také dobré mít na paměti, že xargs může příkaz provádět paralelně (tj. rozdělit stdin na více částí a zavolat program vícekrát s různými částmi) prostřednictvím -P. Pokud jsou vaše shellové skripty pomalé, ale máte dostatek procesorového výkonu, může vám to práci docela urychlit.

parallel

Tento program lze použít k paralelnímu provádění více příkazů, čímž se urychlí jejich provádění.

parallel se chová téměř stejně jako xargs, ale má mnohem lepší podporu pro souběžné provádění jednotlivých úloh (nemíchání jejich výstupů, provádění na vzdáleném stroji atd. atd.).

Rozdíly jsou poměrně dobře popsány v dokumentaci příkazu parallel.

Další podrobnosti naleznete v parallel_tutorial(1) (ano, je to manuálová stránka) a v parallel(1).

find

I když jsou ls(1) a wildcardy dosti mocné, někdy potřebujeme vybrat soubory pomocí složitějších kritérií. Zde je užitečný program find(1).

Bez argumentů vypíše všechny soubory v aktuálním adresáři, včetně souborů ve vnořených adresářích.

Nespouštějte jej v kořenovém adresáři (/), pokud nevíte, co děláte (a rozhodně ne na sdíleném počítači linux.ms.mff.cuni.cz).

Pomocí parametru -name můžete omezit vyhledávání na soubory odpovídající zadanému wildcardu.

Následující příkaz vyhledá všechny soubory alpha.txt v aktuálním adresáři a v libovolném podadresáři (bez ohledu na hloubku).

find -name alpha.txt

Proč následující příkaz pro vyhledání všech souborů *.txt nebude fungovat?

find -name *.txt

Nápověda. Odpověď.

find má mnoho voleb – nebudeme zde opakovat jeho manuálovou stránku, ale zmíníme se o těch, které stojí za zapamatování.

-delete okamžitě odstraní nalezené soubory. Velmi užitečné a velmi nebezpečné.

-exec spustí daný program pro každý nalezený soubor. Musíte použít {} pro zadání názvu nalezeného souboru a příkaz musíte ukončit pomocí ; (protože ; ukončuje příkazy i v shellu, budete ho muset escapovat).

find -name '*.md' -exec wc -l {} \;

Všimněte si, že pro každý nalezený soubor dojde k novému vyvolání příkazu wc. To lze změnit změnou terminátoru příkazu (\;) na +. Podívejte se na rozdíl mezi voláním následujících dvou příkazů:

find -name '*.md' -exec echo {} \;
find -name '*.md' -exec echo {} +

Upozornění

Ve výchozím nastavení vypíše find jeden název souboru na řádek. Jméno souboru však může obsahovat i znak nového řádku (!), a proto následující idiom není 100% bezpečný.

find -options-for-find | while read filename; do
    do_some_complicated_things_with "$filename"
done

Pokud chcete být opravdu bezpeční, použijte -print0 a IFS= read -r -d $'\0' filename, neboť tak se použije jediný bezpečný oddělovač – \0. Alternativně můžete výstup find -print0 přesměrovat do xargs --null.

Pokud však pracujete s vlastními soubory nebo je vzor bezpečný, výše uvedená smyčka je v pořádku (jen nezapomeňte, že adresáře jsou také soubory a mohou ve svém názvu obsahovat \n).

Shell také umožňuje exportovat funkci a volat ji zevnitř xargs. Vypadá to ošklivě, ale je to bezpečný přístup, pokud chcete provádět složité operace nad nalezenými soubory.

my_callback_function() {
    echo ""
    echo "\$0 = $0"
    echo "\$@ =" "$@"
}
export -f my_callback_function

find . -print0 | xargs -0 -n 1 bash -c 'my_callback_function "$@"' arg_zero arg_one
Pochopitelně, vy si funkci pojmenujte podle toho, co dělá. Námi zvolené jméno má jedinou výhodu – je dobře vidět na všech třech místech, kde je použité.

Připomeňme, že funkce lze definovat přímo v shellu a výše uvedené lze skutečně vytvořit interaktivně, aniž by se cokoliv muselo ukládat jako skript.

Přesměrování portů přes SSH (SSH port forwarding)

Obecně platí, že služby poskytované počítačem by neměly být vystaveny na síti, aby si s nimi mohli hrát náhodní “bezpečnostní výzkumníci”. Proto je brána firewall obvykle nakonfigurována tak, aby kontrolovala přístup k počítači ze sítě.

Pokud má být služba poskytována pouze lokálně, je ještě jednodušší nechat ji poslouchat pouze na zařízení zpětné smyčky (loopback). Tímto způsobem k ní mohou přistupovat pouze místní uživatelé (včetně uživatelů připojených k počítači přes SSH).

Pro příklad si můžete povšimnout, že na linux.ms.mff.cuni.cz je na portu 8080 naslouchající webový server. Tento webový server není dostupný, pokud se k němu pokusíte přistupovat přes linux.ms.mff.cuni.cz, ale lokální přístup k němu (při přihlášení k linux.ms.mff.cuni.cz) funguje.

you@laptop$ curl http://linux.ms.mff.cuni.cz:8080                # Fails
you@laptop$ ssh linux.ms.mff.cuni.cz curl --silent http://localhost:8080  # Works

Přístup k tomuto webovému serveru pomocí cURL je sice možný, ale není to uživatelsky nejpřívětivější způsob prohlížení webových stránek.

Přesměrování portů přes SSH (SSH local port forwarding)

SSH lze použít k vytvoření bezpečného tunelu, přes který je lokální port přesměrován na port přístupný ze vzdáleného stroje. V podstatě se připojíte ke zpětné smyčce na svém počítači a SSH tuto komunikaci přesměruje na vzdálený server, čímž efektivně zpřístupní vzdálený port.

Následující příkaz způsobí, že se lokální port 8888 bude chovat jako port 8080 na vzdáleném počítači. Část 127.0.0.1 odkazuje na zpětnou smyčku na vzdáleném serveru (můžete tam napsat i localhost)

ssh -L 8888:127.0.0.1:8080 -N linux.ms.mff.cuni.cz

Vždy nejprve uvedete, který lokální port chcete přesměrovat (8888), a poté cíl, jako byste se připojovali ze vzdáleného počítače (172.0.0.1:8080).

Pomocí -N je toto připojení použitelné pouze pro přesměrování – pro jeho ukončení použijte Ctrl-C (bez něj se přihlásíte i ke vzdálenému počítači).

Otevřete http://localhost:8888 v prohlížeči a zkontrolujte, zda vidíte stejný obsah jako při použití výše uvedeného příkazu ssh linux.ms.mff.cuni.cz curl http://localhost:8080.

Často budete přesměrovávat (lokální) port N na stejný (vzdálený) port N, proto je velmi snadné zapomenout na správné pořadí. Pořadí parametrů -L je ale důležité a záměna čísel (např. 8888:127.0.0.1:9090 místo 9090:127.0.0.1:8888) způsobí přesměrování jiných portů (většinou se to však rychle naučíte).

Nemějte však obavy, pokud si je nemůžete zapamatovat. Proto máte k dispozici manuálové stránky, které používají i každodenní uživatelé Linuxu. Není to nic, za co byste se měli stydět nebo čeho byste se měli bát :-).

Reverzní port forward (SSH remote/reverse port forwarding)

SSH umožňuje vytvořit také tzv. reverzní port forward.

V podstatě umožňuje otevřít spojení ze vzdáleného serveru k místnímu počítači (proti směru spojení SSH).

Prakticky můžete nastavit reverzní přesměrování portů například připojením se ze stolního počítače, který máte doma, na počítač v IMPAKTu/Rotundě a poté se pomocí tohoto reverzního přesměrování připojit z IMPAKTu/Rotundy zpět na svůj domácí počítač.

Tato vlastnost bude fungovat i v případě, že je váš počítač za NAT, což znemožňuje přímé připojení zvenčí.

Následující příkaz nastaví reverzní přesměrování portů tak, že připojení k portu 2222 na vzdáleném počítači bude převedeno na připojení k portu 22 (ssh) na lokálním počítači:

ssh -N -R 2222:127.0.0.1:22 u-plN.ms.mff.cuni.cz

Nejprve uvedete vzdálený port k přesměrování (2222) a poté cíl, jako byste se připojovali z lokálního stroje (127.0.0.1:22).

Při zkoušení tohoto postupu se ujistěte, že je spuštěn démon sshd (vzpomeňte si na lab 10 a příkaz systemctl), a použijte jiný port než 2222, abyste zabránili kolizím.

Abyste se mohli připojit k vašemu počítači přes tento přesměrovaný port, musíte tak učinit z laboratoře IMPAKTu/Rotundy pomocí následujícího příkazu.

ssh -p 2222 your-desktop-login@localhost

Používáme localhost, protože připojení je vázáno pouze na rozhraní loopback, nikoli na skutečný síťový adaptér dostupný na laboratorních počítačích. (Ve skutečnosti ssh umožňuje navázat port forward na veřejnou IP adresu, ale správce to často z bezpečnostních důvodů zakazuje.)

Vývoj softwaru ve virtuálním prostředí

Během předchozích cvičení jsme viděli, že doporučovaným způsobem instalace aplikací (a knihoven nebo datových souborů) na Linuxu je skrze správce balíčků. Ten nainstaluje aplikaci pro všechny uživatele, umožní upgrade celého systému a celkově udržuje systém v rozumném stavu.

Nicméně, ne vždy se systémová instalace hodí. Typický příklad jsou závislosti pro konkrétní projekt. Ty se obvykle neinstalují do systému a to hlavně z následujících důvodů:

  • Potřebujete různé verze závislostí pro různé projekty.
  • Nechcete si pamatovat, co všechno máte odinstalovat, když na projektu přestanete pracovat.
  • Potřebujete určit, kdy dojde k jejich upgradu: upgrade OS by neměl ovlivnit váš projekt.
  • Verze, které potřebujete, jsou jiné, než které zná správce balíčků.
  • A nebo nejsou vůbec ve správci balíčků dostupné.

Z výše uvedených důvodů je mnohem lepší vytvořit instalaci určenou pro jeden projekt, která je lépe oddělena (izolována) od zbytku systému. V tomto ohledu instalace pro uživatele (tj. někam do $HOME) nemusí poskytnout takovou míru odstínění, jakou chceme.

Tento přístup podporuje většina rozumných programovacích jazyků a obvykle je najdeme pod názvy jako virtual environment, local repository, sandbox nebo něco podobného (koncepty nejsou 1:1 ve všech jazycích a nástrojích, ale základní myšlenka je stejná).

S virtuálním prostředím (virtual environment) jsou závislosti obvykle instalovány do určeného adresáře ve vašem projektu. Tento adresář potom neverzujeme (Gitem). Překladač/interpret je pak nasměrován na tento adresář.

Instalace do jednoho adresáře nezanese váš systém. Umožní vám pracovat na více projektech s nekompatibilními závislostmi, protože jsou kompletně oddělené.

Instalační adresář je zřídkakdy nahráván do Gitového repozitáře. Místo toho se nahraje konfigurační soubor, který určuje, jak prostředí připravit.

Každý vývojář si pak může prostředí vytvořit, aniž by do repozitáře přibývaly soubory, které jsou specifické pro danou distribuci nebo operační systém. Ale konfigurační soubor zajistí, že všichni budou pracovat s identických prostředím (tj. stejné verze všech závislostí).

Také to znamená, že noví členové softwarového týmu si mohou prostředí jednoduše připravit právě s pomocí takového konfiguračního souboru.

Instalace závislostí

Uvnitř virtuálního prostředí projektu se obvykle nepoužívají správci balíčků (jako DNF). Místo toho jsou závislosti instalovány správci balíčků daného programovacího jazyka.

Ty jsou obvykle platformě nezávislé a používají vlastní repozitáře. Tyto repozitáře pak obsahují jen knihovny pro daný programovací jazyk. Opět může být těchto repozitářů více a je na vývojářích, jak si vše nastaví

Technicky vzato, i jazykoví správci balíčků umí nainstalovat balíčky globálně do systému. Čímž zasahují do práce klasických správců balíčků. Je na administrátorovi, aby to nějak rozumně uřídil. Obvykle to znamená, že existuje hranice, kam instaluje systémový správce balíčků a kam správce balíčků daného jazyka.

V našem případě budeme vždy instalovat jen do adresářů virtuálního prostředí a do systému jako takového zasahovat nebudeme.

Instalační adresáře

Na typickém Linuxovém systému existuje několik míst, kam může být software nainstalován:

  • /usr – systémové balíčky jak je instaluje správce balíčků dané distribuce
  • /usr/local – software instalovaný lokálně administrátorem; sem obvykle instalují svoje balíčky správci balíčků daného jazyka, pokud je instalují globálně (system-wide)
  • /opt/$PACKAGE – velké balíčky, které neinstaluje správce balíčků, mají často celý vlastní adresář uvnitř /opt.
  • $HOME (obvykle /home/$USER/) – jazykoví správci balíčků spuštění ne-rootem mohou balíčky nainstalovat do domovských adresářů (přesné umístění záleží na jazyce).
  • $HOME/.local je obvyklé místo pro lokální instalaci, které kopíruje /usr/local, ale je jen pro jednoho uživatele (spustitelné soubory jsou pak v $HOME/.local/bin)
  • virtuální prostředí pro každý projekt

Python Package Index (PyPI)

Zbytek textu se bude zabývat Pythoními nástroji. Podobné nástroje jsou ale dostupné i pro další jazyky: věříme, že ukázky nad Pythonem jsou dostačující pro demonstraci principů v praxi.

Python má repozitář zvaný Python Package Index (PyPI), kde kdokoliv může zveřejnit své Pythonovské knihovny či programy.

Repozitář je možné používat buď skrz webový prohlížeč, nebo přes klienta na příkazové řádce, který se jmenuje pip.

pip se chová podobně jako DNF. Můžete s ním instalovat, upgradovat nebo odinstalovat Pythoní moduly.

Pokud ho spustíte s právy superuživatele, může instalovat balíčky do systému. Nedělejte to, pokud si nejste jistí tím, co chcete, a zda chápete možné důsledky.

Problémy s důvěryhodností

Všechny balíčky z hlavního repozitáře vaší distribuce jsou obvykle kontrolovány někým z bezpečnostního týmu pro danou distribuci. To bohužel není pravda pro repozitáře, jako je PyPI. Takže jako vývojář musíte být opatrnější, když budete instalovat software z těchto zdrojů.

Ne všechny balíčky dělají to, co slibují. Některé jejich chyby jsou jen neškodné hlouposti, ale některé jsou záměrně škodlivé. Používání existujícího kódu je obvykle dobrý nápad, ale měli byste se zamyslet nad důvěryhodností autora. Konec konců, kód poběží na vašem účtu, až jej budete spouštět nebo instalovat.

Typickým příkladem je tzv. typosquatting, kdy lumpové zveřejní svůj zákeřný balíček a jeho jméno se liší jen překlepem od jiného známého balíčku. Můžeme doporučit tenhle článek, ale na webu jich lze najít mnohem víc.

Na druhou stranu, mnoho PyPI balíčků je také dostupných jako balíčky pro vaši distribuci (klidně si zkuste dnf search python3- na vašem Fedořím stroji). Takže byly pravděpodobně už některým ze správců distribuce zkontrolovány a jsou v pořádku. Pro balíčky, které takto dostupné nejsou, se vždy dívejte na znaky normálního a zákeřného projektu. Oblíbenost repozitáře se zdrojáky. Uživatelské aktivita. Jak reagují na bug reporty? Kvalita dokumentace. Atd. atd.

Nezapomeňte, že dnešní software je málokdy stavěn na zelené louce. Nebojte se zkoumat, co už je hotové. Zkontrolujte si to. A pak to použijte :-).

Obvyklý průběh práce

Zatímco konkrétní nástroje se liší podle programovacího jazyka, základní kroky pro vývoj projektu uvnitř virtuálního projektu jsou v podstatě vždy stejné.

  1. Vývojář naklonuje projekt (třeba z Gitového repozitáře).
  2. Virtuální prostředí (sandbox) je inicializováno. To obvykle znamená vytvoření nového adresáře s čistým prostředím pro daný jazyk.
  3. Virtuální prostředí musí být aktivováno. Obvykle virtuální prostředí musí změnit $PATH (nebo nějaký podobný ekvivalent, který je používán pro hledání knihoven a modulů), takže vývojář musí source (nebo .) nějaký aktivační skript, aby byly změněny příslušné proměnné.
  4. Potom může vývojář nainstalovat závislosti. Obvykle jsou uloženy v souboru, který lze přímo předat správci balíčků (daného programovacího jazyka).
  5. Teprve teď může vývojář začít na projektu pracovat. Projekt je kompletně izolován, odstraněním adresáře s virtuálním prostředím odstraníme i veškeré stopy po instalovaných balíčcích.

Každodenní práce pak zahrnuje jen krok č. 3 (nějaký způsob aktivace) a krok 5 (vlastní vývoj).

Poznamenejme, že aktivace virtuálního prostředí obvykle odstraní přístup ke globálně (systémově) nainstalovaným knihovnám. Uvnitř virtuálního prostředí vývojář začíná s čistým štítem: jen s holým překladačem. To je ale velmi rozumné rozhodnutí: zajistí, že libovolná systémová instalace neovlivní nastavení projektu.

Jinými slovy to zlepší opakovatelnost (reproducibility) celého nastavení. Také to znamená, že vývojář musí uvést každou závislost do konfiguračního souboru. I pokud je to závislost, která je obvykle vždy nainstalovaná.

Virtuální prostředí pro Python (také známé jako virtualenv nebo venv)

Abychom si mohli bezpečně vyzkoušet instalaci balíčků Pythonu, nastavíme nejprve virtuální prostředí pro náš projekt. Python má naštěstí vestavěnou podporu pro vytvoření virtuálního prostředí.

Ukážeme si to na následujícím příkladu:

#!/usr/bin/env python3

import argparse
import shutil
import sys

import fs

class FsCatException(Exception):
    pass

def fs_cat(filesystem, filename, target):
    try:
        with fs.open_fs(filesystem) as my_fs:
            try:
                with my_fs.open(filename, 'rb') as my_file:
                    shutil.copyfileobj(my_file, target)
            except fs.errors.FileExpected as e:
                raise FsCatException(f"{filename} on {filesystem} is not a regular file") from e
            except fs.errors.ResourceNotFound as e:
                raise FsCatException(f"{filename} does not exist on {filesystem}") from e
    except Exception as e:
        if isinstance(e, FsCatException):
            raise e
        raise FsCatException(f"unable to read {filesystem}, perhaps misspelled path or protocol ({e})?") from e


def main():
    args = argparse.ArgumentParser(description='Filesystem cat')
    args.add_argument(
        'filesystem',
        nargs=1,
        metavar='FILESYSTEM',
        help='Filesystem specification, e.g. tar://path/to/file.tar'
    )
    args.add_argument(
        'filename',
        nargs=1,
        metavar='FILENAME',
        help='File path on FILESYSTEM, e.g. /README.md'
    )

    config = args.parse_args()

    try:
        fs_cat(config.filesystem[0], config.filename[0], sys.stdout.buffer)
    except FsCatException as e:
        print(f"Fatal: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

Uložte tento kód do souboru fscat.py a nastavte mu spustitelný bit. Všimněte si, že fs.open_fs dokáže otevírat různé souborové systémy a přistupovat k souborům v nich, jako byste použili vestavěný pythonovský open.

V našem programu zadáme cestu k souborovému systému a souboru (z tohoto filesystému), který má být vypsán na obrazovku (odtud název fscat, protože simuluje cat uvnitř jiného souborového systému).

Než budete pokračovat, ujistěte se, že celému programu rozumíte.

Zkuste spustit program fscat.py.

Pokud nemáte v systému nainstalovaný balíček python3-fs, mělo by dojít k chybě ModuleNotFoundError: No module named 'fs'. Je pravděpodobné, že tento modul nainstalovaný nemáte.

Pokud jste již nainstalovali python3-fs, odinstalujte jej a zkuste to znovu (jen pro tuto ukázku). Ale dvakrát zkontrolujte, zda byste neodstranili nějaký jiný program, který jej může vyžadovat.

Nyní bychom mohli nainstalovat python3-fs pomocí DNF, ale již jsme vysvětlili, proč to není dobrý nápad. Mohli bychom jej také nainstalovat globálně pomocí pip, ale ani to není nejlepší postup.

Místo toho pro něj vytvoříme nové virtuální prostředí.

python3 -m venv my-venv

Výše uvedený příkaz vytvoří nový adresář my-venv, který obsahuje čistou instalaci Pythonu. Neváhejte a prozkoumejte obsah tohoto adresáře.

Nyní je třeba toto prostředí aktivovat.

source my-venv/bin/activate

Váš prompt by se měl změnit: nyní začíná (my-venv).

Spuštění souboru fscat.py přesto skončí s chybou ModuleNotFoundError.

Nyní nainstalujeme závislost:

pip install fs

To bude chvíli trvat, protože Python bude stahovat také tranzitivní závislosti této knihovny (a jejich závislosti atd.). Po dokončení instalace spusťte znovu soubor fscat.py.

Tentokrát by to mělo fungovat.

./fscat.py

Dobře, vypsalo to chybovou zprávu o požadovaných argumentech. Stáhněte si tento archív a spusťte skript následujícím způsobem:

./fscat.py tar://test.tar.gz testdir/test.txt

Měl by vypsat Test string, protože je schopen pracovat i s tar archívy jako se souborovými systémy a vypsat na nich soubory (ověřte, zda je soubor skutečně k dispozici, buď pomocí atool, MC, nebo přímo pomocí tar).

Jakmile jsme s vývojem hotovi, můžeme prostředí deaktivovat voláním deactivate (tentokrát bez zadávání zdrojů).

Spuštění souboru fscat.py mimo prostředí opět skončí chybou ModuleNotFoundError.

Jak to funguje?

Virtuální prostředí Pythonu používá při své implementaci dva triky.

Nejprve skript activate rozšíří $PATH o adresář my-venv/bin. To znamená, že volání python3 bude preferovat aplikaci z adresáře virtuálního prostředí (např. my-venv/bin/python3).

Vyzkoušejte si to sami: vypište $PATH před a po aktivaci virtuálního prostředí.

To také vysvětluje, proč bychom měli v shebangu vždy zadávat /usr/bin/env python3 místo /usr/bin/python3. env se bude řídit proměnnou $PATH, která byla změněna aktivací virtuálního prostředí.

Můžete si také prohlédnout skript activate a zjistit, jak je implementován. Všimněte si, že deactivate je ve skutečnosti funkce.

Proč není skript activate spustitelný? Nápověda.

Druhý trik spočívá v tom, že Python hledá moduly (tj. soubory implementující importovaný modul) vzhledem k cestě k binárnímu souboru python3. Pokud je tedy python3 uvnitř my-venv/bin, Python bude hledat moduly uvnitř my-venv/lib. To je adresář, kde budou umístěny lokálně nainstalované soubory.

To si můžete ověřit spuštěním následujícího jednořádkového příkazu, který vypíše prohledávané adresáře Pythonu (opět před a po aktivaci):

python3 -c 'import sys; print(sys.path)'

Toto chování ve skutečnosti není v interpretu jazyka Python pevně zabudováno. Při spuštění Pythonu se automaticky importuje modul site. Tento modul obsahuje nastavení specifické pro dané umístění: upraví soubor sys.path tak, aby obsahoval všechny adresáře, do kterých vaše distribuce instaluje moduly Pythonu. Detekuje také virtuální prostředí tak, že hledá soubor pyvenv.cfg v prarodičovském adresáři binárního souboru python3. V našem případě tento konfigurační soubor obsahuje include-system-site-packages=false, což modulu site říká, aby adresáře s moduly distribuce vynechal. Vidíte, že princip je velmi jednoduchý a samotný interpret nemusí o virtuálních prostředích nic vědět.

Instalace balíčků specifických pro Python pomocí pip

pip VS. python3 -m pip?

Obecně se doporučuje používat python3 -m pip, nikoli surový pip. Důvody, které vedly k těmto dalším deseti stiskům kláves, jsou dobře popsány v článku Why you should use python3 -m pip. Aby však byl následující text čitelnější, budeme používat kratší variantu pip.

Jedno použití pipu v praxi jsme si již ukázali, ale pip toho umí mnohem více. Pěkný přehled všech možností pipu najdete v článku Using Python’s pip to Manage Your Projects' Dependencies.

Zde uvádíme stručný přehled nejdůležitějších pojmů a příkazů.

Ve výchozím nastavení pip install prohledává registr balíčků PyPI, aby nainstaloval balíček zadaný na příkazovém řádku. Nebyli bychom daleko od pravdy, kdybychom řekli, že všechny balíčky uvnitř tohoto registru jsou jen archivované adresáře, které obsahují předepsaným způsobem uspořádaný zdrojový kód v jazyce Python.

Pokud chcete tento výchozí registr balíčků změnit, můžete použít argument --index-url.

Protože jste již s GitLabem obeznámeni, mohl by vás zajímat GitLab PyPI Package Registry Support.

V další části se naučíme, jak z adresáře s kódem vytvořit správný balíček Pythonu. Za předpokladu, že jsme to již udělali, můžeme tento balíček nainstalovat přímo (bez archivace/balení) příkazem pip install /path/to/python_package.

Představte si například situaci, kdy máte zájem o open-source balíček třetí strany. Tento balíček je k dispozici ve vzdáleném repozitáři git (typicky na GitHubu nebo GitLabu), ale NENÍ zabalen a zveřejněn v PyPI. Můžete jednoduše klonovat repozitář a spustit pip install .. Díky pip VCS Support se však můžete fázi klonování vyhnout a balíček nainstalovat přímo pomocí:

pip install git+https://git.example.com/MyProject

Chcete-li aktualizovat konkrétní balíček, spusťte příkaz pip install --upgrade [packages].

Nakonec pro odstranění balíčku spustíte pip uninstall [packages].

Verzování závislostí

Možná jste už slyšeli o sémantickém verzování. Python používá víceméně kompatibilní verzování, které je popsáno v PEP 440 – Version Identification and Dependency Specification.

Při instalaci závislostí z registru balíčků můžete tuto verzi zadat.

pkgname          # latest version
pkgname == 4.2   # specific version
pkgname >= 4.2   # minimal version
pkgname ~= 4.2   # equivalent to >= 4.2, == 4.*

Pravdou je, že specifikátor verze se skládá z řady klauzulí o verzi oddělených čárkami. Proto můžete zadat:

pkgname >= 1.0, != 1.3.4.*, < 2.0

Někdy je užitečné uložit seznam všech aktuálně nainstalovaných balíčků (včetně tranzitivních závislostí). Například jste si nedávno všimli nové chyby ve svém projektu a rádi byste si uchovali přesné verze aktuálně nainstalovaných závislostí, aby váš spolupracovník mohl chybu reprodukovat.

Za tímto účelem je možné použít pip freeze a vytvořit seznam, který nastaví konkrétní verze a zajistí tak stejné prostředí pro každého vývojáře.

Doporučujeme je uložit do souboru requirements.txt.

# Generating requirements file
pip freeze > requirements.txt

# Installing package from it
pip install -r requirements.txt

Balení Pythoních projektů

Řekněme, že jste přišli na super skvělý algoritmus a chcete obohatit svět tím, že se o něj podělíte. Oficiální dokumentace Pythonu nabízí návod krok za krokem, jak toho dosáhnout.

V následujícím textu budeme používat setuptools pro sestavování projektů v jazyce Python. Historicky to byla jediná možnost, jak sestavit Pythoní balíček. V poslední době se vývojáři Pythonu rozhodli otevřít brány alternativám, a tak můžete balíček Pythonu sestavit také pomocí Poetry, flit nebo dalších. Popis těchto nástrojů je mimo rámec tohoto kurzu.

Struktura adresáře balíčků Pythonu

Úplně prvním krokem, než jej můžete publikovat, je jeho transformace do správného balíčku Pythonu. Musíme vytvořit soubory pyproject.toml a setup.cfg. Tyto soubory obsahují informace o projektu, seznam závislostí a také informace pro instalaci projektu.

Ještě nedávno bylo obvyklé mít skript setup.py, nikoli setup.cfg a pyproject.toml. Proto v mnoha repozitářích/návodech stále můžete najít jeho použití. Obsah je víceméně 1:1, ale jsou určité případy, kdy jste nuceni použít setup.py. To naštěstí pro náš usecase neplatí, a proto jsme se rozhodli popsat moderní variantu se statickými konfiguračními soubory.
Jak se píše v setuptools Quickstart, od verze 61.0.0 nabízí setuptools možnost experimentálního použití pouze pyproject.toml. Tento přístup používá také Poetry, ale v následujícím textu zůstaneme u stabilní kombinace setup.cfg a pyproject.toml.

V repozitáři fscat najdete balíček Pythonu se stejnou funkčností, jakou má náš předchozí skript fscat.py.

Pečlivě si prostudujte adresářovou strukturu a obsah souboru setup.cfg.

Můžeme si všimnout, že potřebné závislosti jsou v souboru setup.cfg a v souboru requirements.txt duplicitní. Ve skutečnosti to není chyba. V souboru setup.cfg je třeba použít co nejslabší verzi závislosti, zatímco v souboru requirements.txt musíme všechny závislosti uvést s přesnou verzí. Existují také tranzitivní závislosti, které by se v souboru setup.cfg NEMĚLY vyskytovat.

Další podrobnosti naleznete v části install_requires vs requirements file.

Zkuste si tento balíček (s pomocí podpory VCS) následujícím příkazem nainstalovat:

pip install git+http://gitlab.mff.cuni.cz/teaching/nswi177/2024/common/fscat.git

Možná jste si všimli, že soubor setup.cfg obsahuje sekci [options.entry_points]. Tato sekce určuje, jaké jsou skripty váš projekt aktuálně obsahuje. Všimněte si, že po spuštění výše uvedeného příkazu můžete spustit příkaz fscat přímo. Pip pro vás vytvořil obalový skript a přidal jej do $PATH virtuálního prostředí.

fscat tar://tests/test.tar.gz testdir/test.txt

Nyní balíček odinstalujte pomocí:

pip uninstall matfyz-nswi177-fscat

Naklonujte repozitář do místního počítače a vstupte do jeho adresáře. Nyní spusťte:

pip install -e .

pip install -e vytvoří editovatelnou instalaci pro snadné ladění. Namísto kopírování vašeho kódu do virtuálního prostředí nainstaluje pouze věc podobnou symlinku (ve skutečnosti soubor fscat.egg-link, který má podobný účinek na mechanismus Pythonu pro vyhledávání modulů) odkazující na adresář s vašimi zdrojovými soubory.

Sestavení Pythoního balíčku

Když už máme správnou adresářovou strukturu, zbývají nám už jen dva kroky k jejímu zveřejnění v registru balíčků.

Nyní připravíme distribuční balíčky pro náš kód. Nejprve nainstalujeme balíček build příkazem pip install build. Poté můžeme spustit příkaz

python3 -m build

V podadresáři dist se vytvoří dva soubory:

  • matfyz-nswi177-fscat-0.0.1.tar.gz – archiv zdrojového kódu

  • matfyz_nswi177_fscat-0.0.1-py3-none-any.whlwheel soubor, což je sestavený balíček (py3 je požadovaná verze Pythonu, none a any říkají, že se jedná o balíček nezávislý na platformě).

Všimněte si, že wheel soubor není nic jiného než jednoduchý zip archiv.

$ file dist/matfyz_nswi177_fscat-0.0.1-py3-none-any.whl
dist/matfyz_nswi177_fscat-0.0.1-py3-none-any.whl: Zip archive data, at least v2.0 to extract, compression method=deflate

$ unzip -l dist/matfyz_nswi177_fscat-0.0.1-py3-none-any.whl
Archive:  dist/matfyz_nswi177_fscat-0.0.1-py3-none-any.whl
  Length      Date    Time    Name
---------  ---------- -----   ----
       51  2024-04-24 10:48   fscat/__init__.py
      837  2024-04-24 10:48   fscat/fscat.py
      777  2024-04-24 10:48   fscat/main.py
     1075  2024-04-24 10:53   matfyz_nswi177_fscat-0.0.1.dist-info/LICENSE
     1173  2024-04-24 10:53   matfyz_nswi177_fscat-0.0.1.dist-info/METADATA
       92  2024-04-24 10:53   matfyz_nswi177_fscat-0.0.1.dist-info/WHEEL
       42  2024-04-24 10:53   matfyz_nswi177_fscat-0.0.1.dist-info/entry_points.txt
        6  2024-04-24 10:53   matfyz_nswi177_fscat-0.0.1.dist-info/top_level.txt
      769  2024-04-24 10:53   matfyz_nswi177_fscat-0.0.1.dist-info/RECORD
---------                     -------
     4822                     9 files

Možná vás zajímá, proč existují dva archivy s velmi podobným obsahem. Odpověď najdete v článku What Are Python Wheels and Why Should You Care?.

Nyní se můžete přepnout do jiného virtuálního prostředí a nainstalovat balíček pomocí pip install package.whl.

Publikování Pythoního balíčku

Pokud si myslíte, že by balíček mohl být užitečný i pro další lidi, můžete jej zveřejnit v Python Package Index. To se obvykle provádí pomocí nástroje twine. Přesný postup je popsán v části Uploading the distribution archives.

Vytváření distribučních balíčků (např. pro DNF)

I když se může zdát, že práce s vytvářením projektových souborů je velmi komplikovaná, ve skutečnosti z dlouhodobého hlediska šetří čas.

Prakticky každý vývojář Pythonu by nyní mohl nainstalovat váš program a mít jasný výchozí bod při zkoumání dalších detailů.

Všimněte si, že pokud jste instalovali nějaký program prostřednictvím DNF a tento program byl napsán v jazyce Python, někde uvnitř byl soubor setup.cfg, který vypadal velmi podobně jako ten, který jste právě viděli. Jen místo toho, aby se skript nainstaloval do vašeho virtuálního prostředí, byl nainstalován globálně.

Žádné jiné kouzlo za tím opravdu není.

Všimněte si, že například Ranger je napsán v jazyce Python a tento skript popisuje jeho instalaci (jedná se o skript pro vytváření balíčků pro DNF). Všimněte si, že %py3_install je makro, které ve skutečnosti volá setup.py install.

Nástroje vyšší úrovně

Na pip a virtualenv můžeme pohlížet jako na nízkoúrovňové nástroje. Existují však i nástroje, které oba tyto nástroje kombinují a přinášejí větší komfort při správě balíčků. V jazyce Python existují přinejmenším dvě oblíbené možnosti, a to Poetry a Pipenv.

Interně tyto nástroje používají pip a venv, takže stále můžete mít nezávislé pracovní prostory a také možnost nainstalovat konkrétní balíček z Python Package Index (PyPI).

Úplné představení těchto nástrojů je mimo rámec tohoto kurzu. Obecně se řídí stejnými principy, ale přidávají některé funkce navíc, které je hezké mít. Ve stručnosti lze uvést tyto hlavní rozdíly:

  • Mohou zmrazit určité verze závislostí, takže se projekt sestaví na všech počítačích stejně (pomocí souboru poetry.lock).
  • Balíčky lze odstranit spolu s jejich závislostmi.
  • Inicializace nového projektu je jednodušší.

Ostatní jazyky

Ostatní jazyky mají vlastní nástroje s podobnými funkcemi:

Úlohy k ověření vašich znalostí

Očekáváme, že následující úlohy vyřešíte ještě před příchodem na cvičení, takže se budeme moci o vašich řešeních na cvičení pobavit.

Zkomprimujte pomocí GZ (gzip) všechny soubory .csv, které jsou větší než 1M.

Nepřepisujte existující soubory bez vyzvání.

Nápověda.

Řešení.

Vzpomeňte si na generátor statických stránek ze Cvičení 8. V něm jsme měli následující smyčku pro generování HTML souborů ze zdrojů v Markdownu.

generate_web() {
    local page
    for page in src/*.md; do
        if ! [ -f "$page" ]; then
            continue
        fi
        build_markdown_page "$page"
    done
}

Aktualizujte implementaci tak, aby podporovala i vnořené podadresáře.

Řešení.

Napište program v jazyce Python, který pomocí balíčků roman a dateparser vypíše uživatelem zadané datum římskými číslicemi.

Program nejlépe vystihují níže uvedené příklady (za předpokladu, že byly provedeny 24. dubna 2024).

./romandate.py
XXIV.IV.MMXXIV
./romandate.py 2021-01-01
I.I.MMXXI
./romandate.py 40 years ago
XXIV.IV.MCMLXXXIV

Testy předpokládají, že jsou již spuštěny ve virtuálním prostředí, kde jsou nainstalovány výše uvedené balíčky (při spuštění na GitLabu se tyto dva balíčky nainstalují automaticky).

Nezapomeňte zkontrolovat, že vaše řešení také funguje

  • když je spuštěno bez parametrů (čas nyní, tedy now)
  • když je spuštěno s relativními daty jako třeba 5 days ago

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 12/romandate.py a commitněte ho (pushněte) do GitLabu.

Připravte Pythoní balíček, který poskytuje příkaz get-project-name, který se pokusí automaticky určit název projektu.

Program hledá v souborech README.md a README první neprázdný řádek (odstraní přebytečné bílé znaky a úvodní # v souborech *.md).

Pokud není k dispozici README.md ani README, program se pokusí najít nejvyšší adresář projektu Git (zvažte použití parametru konstruktoru search_parent_directories=True a vlastnosti working_tree_dir u typu Repo z GitPython) a vypíše jeho jméno (bez cesty).

Pokud aktuální adresář není součástí projektu Git, program vypíše jméno aktuálního adresáře (opět bez cesty).

Očekáváme, že následující postup bude fungovat (pravděpodobně nejlépe ve virtuálním prostředí) v kořenu vašeho repozitáře.


get-project-name
# Prints 'NSWI177 Tasks Repository'
cd 01
get-project-name
# Prints 'NSWI177 Tasks Repository'
cd ../../
get-project-name
# Prints directory name of the parent directory of your submission repository clone

Očekáváme, že si vytvoříte správný podadresář src a správně uspořádáte svůj balíček pomocí souboru setup.cfg atd.

Automatické testy vždy vytvoří nové virtuální prostředí pro každý testovaný případ. Což je fajn pro závěrečnou kontrolu. Ale je též možné testy spustit v aktivovaném virtuálním prostředí, kdy očekávají, že příkaz get-project-name už je nainstalován. A sice přes nastavení NSWI177_LAB12_NO_INSTALL=true (takže pak přeskočí část pip install 12/get-project-name, což je docela dost zrychlí):

env NSWI177_LAB12_NO_INSTALL=true ./bin/run_tests.sh 11-post/project_name

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 12/get-project-name/ a commitněte ho (pushněte) do GitLabu.

Učební výstupy

Učební výstupy podávají zhuštěný souhrn základních konceptů a dovedností, které byste měli umět vysvětlit a/nebo použít po každém cvičení. Také obsahují absolutní minimum, které je potřebné pro pochopení navazujících cvičení (a dalších předmětů).

Znalosti konceptů

Znalost konceptů znamená, že rozumíte významu a kontextu daného tématu a jste schopni témata zasadit do většího rámce. Takže, jste schopni …

  • vysvětlit rozdíl mezi (normálním) přesměrování SSH portu a tzv. reverzním přesměrováním

  • vysvětlit, co jsou požadavky (závislosti na knihovnách)

  • vysvětlit základy sémantického verzování

  • vysvětlit, jaké jsou výhody a nevýhody instalace závislostí pro celý systém vs jejich instalaci ve virtuálním prostředí

  • poskytnout vysokoúrovňový přehled virtuálních prostředí

  • vysvětlit výhod a nevýhody uvedení tranzitivních závislostí ve srovnání s uvedením jen přímých

  • vysvětlit výhody a nevýhody uvedení přesné verze nebo jen minimální

Praktické dovednosti

Praktické dovednosti se obvykle týkají použití daných programů pro vyřešení různých úloh. Takže, dokážete …

  • používat xargs

  • použít find se základními predikáty (-name, -type) a akcemi (-exec, -delete)

  • použít přesměrování portu SSH pro přístup ke službě dostupné na zařízení loopback

  • použít reverzní předávání portů SSH pro připojení k počítači za NAT

  • vytvořit nové virtuální prostředí (pro Python) pomocí python3 -m venv

  • aktivovat a deaktivovat virtuální prostředí

  • nainstalovat závislosti projektu do virtuálního prostředí pomocí pip

  • vyvíjet program uvnitř virtuálního prostředí (s projektem používajícím setup.cfg a pyproject.toml)

  • nainstalovat projekt, který používá setup.cfg

  • volitelné: připravit projekt Pythonu pro instalaci