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

Toto cvičení je věnováno základům pro reprodukovatelnou a izolovanou vývojařinu. Uvidíte, jak můžeme zajistit, že práce na projektu – který vyžaduje instalaci závislostí – nepotřebuje instalaci žádného systémového balíčku ani další zásah do systému jako takového.

Nezapomeňte, že Čtení před cvičením je povinné a je z něj kvíz, který musíte vyplnit před cvičením.

Čtení před cvičením

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 jen málokdy commitnut do Gitového repozitáře. Místo toho commitujete konfigurační soubor, které popisuje, jak se prostředí připraví. 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 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ší 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, vždy se 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í (pískoviště) 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í skript source (nebo .) aby byly změněny aktuální 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ů pro daný jazyk.
  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á 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á.

Kvíz před cvičením

Soubor s kvízem je ve složce 11 v tomto GitLabím projektu.

Zkopírujte si správnou jazykovou mutaci do vašeho projektu jako 11/before.md (tj. budete muset soubor přejmenovat).

Otázky i prostor pro odpovědi jsou v souboru, odpovědi vyplňte mezi značky **[A1]** a **[/A1]**.

Pipeline before-11 na GitLabu zkontroluje, že jste odevzdali odpovědi ve správném formátu. Ze zřejmých důvodů nemůže zkontrolovat skutečnou správnost.

Odevzdejte kvízy před začátkem cvičení 11.

Virtual environment for Python (a.k.a. virtualenv or venv)

To try installing Python packages safely, we will first setup a virtual environment for our project. Fortunately, Python has built-in support for creating a virtual environment.

We will demonstrate this on following example:

#!/usr/bin/env python3

import sys
import dateparser


def main():
    input_date = ' '.join(sys.argv[1:])
    if input_date == '':
        input_date = 'now'

    date = dateparser.parse(input_date)
    if not date:
        print(f"Invalid date specification (`{input_date}').", file=sys.stderr)
        sys.exit(1)

    print(date.strftime('%Y-%m-%dT%H:%M:%S'))


if __name__ == '__main__':
    main()

Save this snippet into timestamp2iso.py and set the executable bit. Note that dateparser.parse() is able to parse various time specification into the native Python date format. The time specification can be even text such as three days ago.

Make sure you understand the whole program before continuing.

Try running the timestamp2iso.py program.

Unless you have already installed the python3-dateparser package system-wide, it should fail with ModuleNotFoundError: No module named 'dateparser'. The chances are that you do not have that module installed.

If you have installed the python3-dateparser, uninstall it now and try again (just for this demo). But double-check that you would not remove some other program that may require it.

We could now install the python3-dateparser with DNF but we already described why that is a bad idea. We could also install it with pip globally but that is not the best course of action either.

Instead, we will create a new virtual environment for it.

python -m venv my-venv

The above command creates a new directory my-venv that contains a bare installation of Python. Feel free to investigate the contents of this directory.

We now need to activate the environment.

source my-venv/bin/activate

Your prompt should have changed: it is prefixed by (my-venv) now.

Running timestamp2iso.py will still terminate with ModuleNotFoundError.

We will now install the dependency:

pip install dateparser

This will take some time as Python will also download transitive dependencies of this library (and their dependencies etc.). Once the installation finishes, run timestamp2iso.py again.

This time, it should work.

./timestamp2iso.py three days ago

Once we are finished with the development, we can deactivate the environment by calling deactivate (this time, without sourcing anything).

Running timestamp2iso.py outside the environment shall again terminate with ModuleNotFoundError.

How does it work?

Python virtual environment uses two tricks in its implementation.

First, the activate script extends $PATH with the my-venv/bin directory. That means that calling python will prefer the application from the virtualenv’s directory (e.g. my-venv/bin/python).

Try this yourself: print $PATH before and after you activate a virtualenv.

This also explains why we should always specify /usr/bin/env python in the shebang instead of /usr/bin/python.

You can also view the activate script and see how this is implemented. Note that deactivate is actually a function.

Why is the activate script not executable? Hint.

The second trick is that Python searches for modules (i.e., for files implementing an imported module) relative to the path of the python binary. Hence, when python is inside my-venv/bin, Python will look for the modules inside my-venv/lib. That is the location where your locally installed files will be placed.

You can check this by executing the following one-liner that prints Python search directories (again, before and after activation):

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

This behaviour is actually not hard-wired in the Python interpreter. When Python starts up, it automatically imports a module called site. This module contains site-specific setup: it adjusts sys.path to include all directories where your distribution installs Python modules. It also detects virtual environments by looking for the pyvenv.cfg file in the grandparent directory of the python binary. In our case, this configuration file contains include-system-site-packages=false, which tells the site module to skip distribution’s module directories. You can see that the principle is very simple and the interpreter itself needs to know nothing about virtual environments.

Installing Python-specific packages with pip

pip VS. python -m pip?

Generally, it is recommended to use python -m pip, rather than raw pip. Reasons behind these additional 10 key strokes are well described in Why you should use python -m pip. However, in order to make the following text more readable, we will use the shorter pip variant.

We have already seen one usage of pip in practice, but pip can do much more. The nice walkthrough over all pip capabilities can be found in Using Python’s pip to Manage Your Projects' Dependencies.

Here we provide a brief summary of the most important concepts and commands.

By default pip install is searching through the package registry PyPI, in order to install package specified in command-line. We wouldn’t be far from truth, by saying that all packages inside this registry are just archived directories, which contains Python source code organized in a prescribed way.

If you would like to change this default package registry you can use --index-url argument.

As you are already familiar with GitLab, you could be interested in GitLab PyPI Package Registry Support.

In later section, we will learn how to turn a directory with code into proper Python package. Assuming that we have already done it, we can that package directly (without archiving/packing) by running pip install /path/to/python_package.

For example, imagine a situation where you are interested in third-party open-source package. This package is available in remote git repository (typically on GitHub or GitLab), but it is NOT packed and published in PyPI. You can simply clone the repository and run pip install .. However, thanks to pip VCS Support you can avoid the cloning phase and install the package directly with:

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

In order to upgrade a specific package you run pip install --upgrade [packages].

Finally, for removing package you run pip uninstall [packages].

Dependency versioning

We have already mentioned Semantic Versioning 2.0.0. Python uses more or less compatible versioning, which is described in PEP 440 – Version Identification and Dependency Specification.

When you install dependencies from package registry, you can specify this version.

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

Truth is that a version specifier consists of a series of version clauses, separated by commas. Therefore you can type:

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

Dependency versioning

Sometimes it is helpful to save a list of all currently installed packages (including transitive dependencies). For example, you have recently noticed a new bug in you project and you would like to keep record of precise version of currently installed dependencies, so you co-worker can reproduce it.

In order to do that, it is possible to use pip freeze and create a list that sets specific versions, ensuring the same environment for every developer.

It is recommended to store these in requirements.txt file.

# Generationg requirements file
pip freeze > requirements.txt`

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

Packaging Python Projects

Let’s say that you come up with a super cool algorithm and you want to enrich the world by sharing it. Python official documentation offers step-by-step tutorial how to achieve it.

In following text, we are going to use setuptools for building the python projects. Historically, this was the only options how to a python package. Recently, Python developers decided to open gates for alternatives and so you may also build a python package with Poetry, flit or others. The description of these tools is out of the scope of this course.

Python Package Directory Structure

The very first step, before you can publish it, is to transform it into a proper Python package. We need to files called pyproject.toml and setup.cfg. These files contain information about the project, a list of dependencies, and also information for project installation.

Not long ago, it was usual to have setup.py script, rather that setup.cfg and pyproject.toml. Therefore, in many repositories/tutorials you can still find usage of it. The content is more or less 1:1, but there are certain cases, in which you are forced to use setup.py. Fortunately, this is not applicable for our usecase and so we have decided to describe the modern variant with static configuration files.
As is written in setuptools Quickstart, since version 61.0.0, setuptools offeres the experimental usage of having only a pyproject.toml. This approach is also used by Poetry, but in following text we will stay with stable combination setup.cfg and pyproject.tom.

In timestamp2iso you can find Python package with the same functionality as our previous timestamp2iso.py script.

Please study carefully the directory structure as well as the content of setup.cfg.

One may notice that the necessary dependencies are duplicated in setup.cfg and in requirements.txt. Actually, this is not a mistake. In setup.cfg you should use the most possible relaxed version of the dependency, whereas in requirements.txt we need to specify all dependencies with precise version. There are also the transitive dependencies, which should NOT be present in setup.cfg.

For more details see install_requires vs requirements file.

Try to install this package with VCS Support with following command:

pip install git+http://gitlab.mff.cuni.cz/teaching/nswi177/2022/common/timestamp2iso.git

You perhaps noticed that the setup.cfg contained section [options.entry_points]. This section specifies what are actual scripts of your project. Note that after running the above command, you can execute timestamp2iso command directly. Pip created a wrapper script for you and added it to the sandbox $PATH.

timestamp2iso three days ago

Now uninstall the package with:

pip uninstall matfyz-nswi177-timestamp2iso

Clone the repository to you local machine and change directory to it. Now run:

pip install -e .

pip install -e produces an editable installation for easy debugging. Instead of copying your code to the virtual environment, it installs only a symlink-like thing (actually, an timestamp2iso.egg-link file which has a similar effect on Python’s mechanism for finding modules) referring to the directory with your source files.

Add some nice prefix just before the ISO print statement and run timestamp2iso three days ago again.

Building Python Package

Now, when we already have the proper directory structure, we are only two step from publishing it to Package Registry.

Now, we prepare distribution packages for our code. Firstly, we install the build package by invoking pip install build. Then we can run

python -m build

Two files are created in the dist subdirectory:

  • matfyz-nswi177-timestamp2iso-0.0.1.tar.gz – a source code archive

  • matfyz_nswi177_timestamp2iso-0.0.1-py3-none-any.whl – a wheel file, which is the built package (py3 is the Python version required, none and any tell that this is a platform-independent package).

Note that wheel file is nothing more that a simple Zip archive.

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

$ unzip -l dist/matfyz_nswi177_timestamp2iso-0.0.1-py3-none-any.whl
Archive:  dist/matfyz_nswi177_timestamp2iso-0.0.1-py3-none-any.whl
  Length      Date    Time    Name
---------  ---------- -----   ----
       57  04-23-2022 17:26   timestamp2iso/__init__.py
      597  04-23-2022 17:33   timestamp2iso/main.py
      402  04-23-2022 17:31   timestamp2iso/timestamp2iso.py
     1067  04-23-2022 19:36   matfyz_nswi177_timestamp2iso-0.0.1.dist-info/LICENSE
      917  04-23-2022 19:36   matfyz_nswi177_timestamp2iso-0.0.1.dist-info/METADATA
       92  04-23-2022 19:36   matfyz_nswi177_timestamp2iso-0.0.1.dist-info/WHEEL
       58  04-23-2022 19:36   matfyz_nswi177_timestamp2iso-0.0.1.dist-info/entry_points.txt
       14  04-23-2022 19:36   matfyz_nswi177_timestamp2iso-0.0.1.dist-info/top_level.txt
      849  04-23-2022 19:36   matfyz_nswi177_timestamp2iso-0.0.1.dist-info/RECORD
---------                     -------
     4053                     9 files

You may wonder, why there are two archives with very similar content. The answer can be found in What Are Python Wheels and Why Should You Care?.

You can now switch to a different virtualenv and install the package using pip install package.whl.

Publishing Python Package

If you think that the package could be useful to other people, you can publish it in the Python Package Index. This is usually accomplished using the twine tool. The precise steps are described in Uploading the distribution archives.

Creating distribution packages (e.g. for DNF)

While the work for creating the project files may seem to complicate things a lot, it actually saves time in the long run.

Virtually any Python developer would be now able to install your program and have a clear starting point when investigating other details.

Note that if you have installed some program via DNF system-wide and that program was written in Python, somewhere inside it was setup.cfg that looked very similar to the one you have just seen. Only instead of installing the script into your virtual environment, it was installed globally.

There is really no other magic behind it.

Note that for example Ranger is written in Python and this script describes its installation (it is a script for creating packages for DNF). Note that the %py3_install is a macro that actually calls setup.py install.

Higher-level tools

We can think of the pip and virtualenv as low-level tools. However, there are also tools that combine both of them and bring more comfort to package management. In Python there are at least two favorite choices, namely Poetry and Pipenv.

Internally, these tools use pip and venv, so you are still able to have independent working spaces as well as the possibility to install a specific package from the Python Package Index (PyPI).

The complete introduction of these tools is out of the scope for this course. Generally, they follow the same principles, but they add some extra functions that are nice to have. Briefly, the major differences are:

  • They can freeze specific versions of dependencies, so that the project builds the same on all machines (using poetry.lock file).
  • Packages can be removed together with their dependencies.
  • It is easier to initialize a new project.

Other languages

Other languages have their own tools with similar functions:

Excercise

Setup program from examples repository (11/last_commit) to be a proper Python project.

Hodnocené úlohy (deadline: 12. května)

11/tapsum2json (100 bodů)

Napište program, který vytvoří souhrn TAP testů ve formátu JSON.

TAP – neboli Protokol pro testování čehokoliv – Test Anything Protocol je univerzální formát pro výsledky testů. Používá ho BATS a také pipelines v GitLabu.

1..4
ok 1 One
ok 2 Two
ok 3 Three
not ok 4 Four
#
# -- Report --
# filename:77:26: note: Something is wrong here.
# --
#

Váš program dostane seznam argumentů – názvy soubor – a přečte je pomocí tzv. TAP consumeru. Každý ze souborů bude samostatným TAP výsledkem (tj. to co třeba BATS vytiskne s parametrem -t). Neexistující soubory budou přeskočeny a započteny jako soubory bez testů.

Program pak vytiskne souhrn testů v následujícím formátu.

{
  "summary": [
    {
      "filename": "filename1.tap",
      "total": 12,
      "passed": 8,
      "skipped": 3,
      "failed": 1
    },
    {
      ...
    }
  ]
}

Při řešení musíte použít knihovnu pro čtení TAP soubor: tap.py je určitě rozumná volba, ale můžete zkusit najít nějakou lepší.

Vaše řešení musí obshovat pyproject.toml, setup.cfg a requirements.txt se seznamem knihoven, na kterých váš program závisí a které mohou být předány do pip install. Vaše řešení musí jít nainstalovat pomocí setup.cfg a vytvoří spustitelný tapsum2json soubor v $PATH. Toto je povinná součást testů, protože tak budeme vaše řešení testovat (podívejte se do testů).

Uložte vaše řešení do podadresáře 11/tapsum2json.

Pokud chcete pustit automatické testy na svém počítači, budete potřebovat prográmek json_reformat z DNF balíčku yajl (sudo dnf install yajl). Testy přeformátují JSONový výstup, aby ho šlo jednoduše porovnávat. Nevyžadujeme, abyste formátovali výstup ve vašem programu, ale předání indent=True do funkce json.dump určitě zjednoduší ladění.

Učební výstupy

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, co jsou závislosti (ve smyslu požadovaných knihoven)

  • vysvětlit, proč instalace závislostí globálně do systému nemusí dobře fungovat pro více projektů

  • vysvětlit, jak fungují virtuální prostředí (sandboxing) (hlavní principy)

  • 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 …

  • vytvořit nové virtuální prostředí (pro Python)

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

  • spustit/otestovat Pythoní projekt pomocí virtualenv (s setup.cfg a pyproject.toml)

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

  • nainstalovat nové závislosti pro projekt

  • aktualizovat seznam závislostí

  • nastavit projekt pro instalaci (volitelné)