Hlavní navigace

Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema

Pavel Tišnovský

Při vývoji aplikací se mnohdy dostaneme do situace, kdy je nutné validovat datové struktury získávané přes REST API, z dokumentových databází apod. V Pythonu lze pro validaci použít knihovny Schemagic a Schema.

Doba čtení: 38 minut

11. Validace map (slovníků)

12. Výsledky páté verze testů

13. Přesnější validace map – kontrola, zda hodnoty odpovídají zadaným kritériím

14. Základní informace o knihovně Schema

15. Instalace knihovny Schema

16. Jednoduchý příklad použití knihovny Schema pro validaci datových struktur

17. Validace obsahu slovníků

18. Druhý příklad používající knihovnu Schema

19. Výsledek druhého příkladu

20. Repositář se všemi demonstračními příklady

21. Odkazy na Internetu

1. Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema

V dnešním článku si popíšeme některé (prozatím základní) možnosti knihoven pojmenovaných Schemagic a Schema. Jedná se o knihovny vytvořené pro ty programátory, kteří používají jazyk Python. Úkolem těchto knihoven je validace prakticky libovolně komplikovaných datových struktur, a to na základě programátorem definovaného schématu. Samotné schéma definované uživatelem (přesněji řečeno programátorem) má dvě úlohy:

  1. Samozřejmě umožňuje samotnou validaci (musíme vědět, jak mají data vypadat)
  2. Současně strukturu dat dokumentuje, a to rigidním způsobem

Validaci dat je možné využít v mnoha oblastech. Představme si například dokumentovou databázi, složitý konfigurační soubor nebo asi nejlépe klasickou webovou službu, která přijme data ve formátu JSON, převede je knihovní funkcí do nativní datové struktury (typicky do slovníku seznamů či hierarchicky uspořádaných slovníků) a následně provede validaci této struktury, ovšem nikoli programově (testováním jednotlivých atributů), ale na základě deklarativního popisu této struktury. Například můžeme specifikovat, že v atributu nazvaném „price“ by mělo být uloženo nezáporné číslo menší než 100000, v atributu pojmenovaném „valid_from“ musí být uložen řetězec odpovídající skutečnému datu (to už nelze otestovat primitivním regulárním výrazem, ale složitějším predikátem) a v atributu „login“ bude buď nick uživatele nebo bude tento atribut obsahovat null/None (popř. alternativně nebude existovat vůbec).

V případě formátu JSON je samozřejmě možné validaci provádět už nad vstupními daty přes JSON Schema, dtto při použití jazyka XML pomocí XML Schema (a dalších podobných nástrojů), ovšem možnosti těchto nástrojů jsou omezené – stále se totiž jedná „pouze“ o DSL, v nichž se složitější kritéria zapisují velmi složitě a většinou i nečitelně.

Poznámka: způsobem validace datových struktur jsme se již na stránkách Rootu zabývali, a to konkrétně ve dvojici článků Validace dat s využitím knihovny spec v Clojure 1.9.0 a Validace dat s využitím knihovny spec v Clojure 1.9.0 (dokončení). Tyto články byly zaměřeny na popis knihovny spec určené pro programovací jazyk Clojure. Dnes se zaměříme výhradně na knihovny použitelné v programovacím jazyku Python.

2. Základní informace o knihovně Schemagic

První knihovnou určenou pro validaci datových struktur v Pythonu, kterou si v dnešním článku alespoň ve stručnosti popíšeme, je knihovna nazvaná Schemagic (název vznikl spojením dvou slov schema + magic). Tato knihovna je samozřejmě k dispozici ve formě balíčku pro pip a její zdrojové kódy naleznete na GitHubu, konkrétně na adrese https://github.com/Mechrop­hile/schemagic. Tato knihovna byla inspirována modulem Schema určeným pro programovací jazyk Clojure, ale nemusíte se bát, že by se při deklaraci schémat či při validaci nepoužívaly idiomy rozšířené v Pythonu – Schema byla jen inspirací, nejedná se o přímou konverzi. Podobně, jako je tomu i u dalších podobně koncipovaných knihoven, je Schemagic založena na deklaraci takzvaného schématu, které popisuje, jak má vypadat datová struktura, která je validována.

Důležité je, že pro zápis schématu se nepoužívá žádný specializovaný DSL (jak je tomu v případě JSONu či XML), ale běžný zdrojový kód naprogramovaný v Pythonu, což s sebou přináší některé příjemné stránky (velmi dobrá podpora v programátorských editorech, integrovaných vývojových prostředích, linterech, minimální doba zaučení atd.), ale i zápory (napadá mě prozatímní neexistence nástroje pro vygenerování nápovědy, popř. pro konverzi schématu do jiného jazyka).

Ve skutečnosti však knihovna Schemagic neprovádí pouhou validaci dat s výsledkem „validní“/„nevalidní“, ale současně umožňuje data konvertovat či dokonce do určité míry transformovat. Je tomu tak z toho důvodu, že se v připravených schématech nepoužívají klasické predikáty, ale konverzní funkce, které pro validní vstup provedou konverzi a pro vstup nevalidní typicky vyhodí výjimku typu ValueError, TypeError atd. (může se však jednat i o další typy výjimek).

Podívejme se na příklad validace – otestujeme, zda je hodnota 42 celým číslem (což evidentně je):

>>> from schemagic import validate_against_schema
>>> validate_against_schema(int, 42)
42

Jak jsme si již řekli v předchozím textu, je validace prováděna s konverzí, takže v následujícím příkladu se celočíselná hodnota 42 převede na číslo s plovoucí řádovou čárkou:

>>> validate_against_schema(float, 42)
42.0

Podobně je možné zvalidovat, zda je řetězec „42“ parsovatelný na hodnotu s plovoucí řádovou čárkou:

>>> validate_against_schema(float, "42")
42.0
Poznámka: prováděné konverze mohou být samozřejmě mnohem složitější, což si ukážeme na příkladu slovníků.

3. Vytvoření projektu využívajícího knihovnu Schemagic

Nejprve si ukažme, jak by mohl vypadat velmi jednoduchý projekt, který bude pro validaci dat využívat knihovnu Schemagic. Struktura tohoto projektu bude prozatím triviální, neboť se bude jednat o pouhé tři soubory:

  • requirements.txt
  • run.sh
  • schemagic_test.py

Nejstručnější je soubor nazvaný requirements.txt [1], neboť ten obsahuje seznam knihoven, na nichž běh projektu závisí. V našem případě projekt závisí pouze na jediné knihovně, takže obsah souboru bude následující:

schemagic

Druhý soubor, který se jmenuje run.sh, bude sloužit pro spuštění testu. Pokud není nastavena proměnná prostředí NOVENV, nastaví se virtuální prostředí Pythonu, do něhož se nainstaluje knihovna Schemagic. Díky tomu, že se instalace provádí do virtuálního prostředí, není ovlivněna globální konfigurace systému a uživatel, který skript spouští, ani nemusí mít práva roota. Pokud naopak nastavíte proměnnou prostředí NOVENV, očekává se, že je knihovna Schemagic již nainstalována pomocí příkazu pip3 install schemagic popř. ještě lépe pomocí příkazu pip3 install –user schemagic:

#! /bin/bash
 
echo "Create Virtualenv for Python deps ..."
function prepare_venv() {
    VIRTUALENV=`which virtualenv`
    if [ $? -eq 1 ]; then
        # python34 which is in CentOS does not have virtualenv binary
        VIRTUALENV=`which virtualenv-3`
    fi
 
    ${VIRTUALENV} -p python3 venv && source venv/bin/activate && python3 `which pip3` install -r requirements.txt
}
 
[ "$NOVENV" == "1" ] || prepare_venv || exit 1
python schemagic_test.py

Při prvním spuštění tohoto skriptu by se mělo inicializovat virtuální prostředí Pythonu s přibližně následujícím výsledkem (povšimněte si toho, že je knihovna Schemagic skutečně nainstalována na základě obsahu souboru requirements.txt se seznamem potřebných knihoven a balíčků):

Create Virtualenv for Python deps ...
Running virtualenv with interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /home/tester/temp/schemagic/schemagic1/venv/bin/python3
Also creating executable in /home/tester/temp/schemagic/schemagic1/venv/bin/python
Installing setuptools, pip, wheel...done.
Collecting schemagic (from -r requirements.txt (line 1))
  Downloading schemagic-0.9.1-py2.py3-none-any.whl
Installing collected packages: schemagic
Successfully installed schemagic-0.9.1

Samotný test uložený v souboru schemagic_test.py bude vypadat následovně:

import sys
import traceback
from schemagic import validate_against_schema
 
 
def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        validate_against_schema(schema, data)
        print("pass")
    except ValueError as e:
        print(e)
        traceback.print_exc(file=sys.stdout)
 
 
integer_list = [int]
string_list = [str]
 
validate(integer_list, [])
validate(integer_list, [1, 2, 3])
validate(integer_list, ["hello", "world", "!"])
 
validate(string_list, [])
validate(string_list, [1, 2, 3, 4])
validate(string_list, ["hello", "world", "!"])

4. Popis jednotlivých řádků testu

Celý skript s testem si nyní podrobněji popíšeme. Nejprve jsou provedeny všechny potřebné importy, zejména pak import funkce nazvané validate_against_schema z modulu pojmenovaného schemagic (později budeme potřebovat naimportovat i další funkce, nyní to však není nutné). Ostatní dva importované moduly jsou použity pro zpracování výjimek, které při validaci nastanou:

import sys
import traceback
from schemagic import validate_against_schema

Následně je v testu definována funkce nazvaná validate, které se předává validační schéma a taktéž data, která mají být oproti schématu validována. Uvnitř této funkce se volá validate_against_schema a očekává se, že pokud validace neproběhne v pořádku, vyhodí tato funkce výjimku typu ValueError (opět platí, že později budeme muset reagovat i na některé další typy výjimek):

def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        validate_against_schema(schema, data)
        print("pass")
    except ValueError as e:
        print(e)
        traceback.print_exc(file=sys.stdout)

Na navazujících řádcích jsou vytvořena dvě schémata popisující seznam celočíselných hodnot a seznam řetězců. Povšimněte si, že samotné schéma v tomto případě vypadá jednoduše – jedná se o specifikaci konverzních funkcí uložených v seznamu. Pozor – skutečně se v případě int a str jedná o běžné konverzní funkce, nikoli i specifikaci datových typů (knihovna Schema popsaná níže v tomto případě pracuje odlišně):

integer_list = [int]
string_list = [str]

A konečně se budeme snažit validovat seznamy s různými hodnotami oproti oběma schématům:

validate(integer_list, [])
validate(integer_list, [1, 2, 3])
validate(integer_list, ["hello", "world", "!"])
 
validate(string_list, [])
validate(string_list, [1, 2, 3, 4])
validate(string_list, ["hello", "world", "!"])

5. Výsledky validace provedené první verzí testů

Podívejme se nyní na výsledky validace, která byla provedena první verzí testů. Samotnou validaci spustíme již výše popsaným skriptem nazvaným run.sh.

Na začátku můžeme vidět průběh inicializace virtuálního prostředí Pythonu, což není v kontextu tohoto článku příliš zajímavé:

Create Virtualenv for Python deps ...
Using base prefix '/usr'
New python executable in /home/tester/temp/schemagic/schemagic1/venv/bin/python3
Not overwriting existing python script /home/tester/temp/schemagic/schemagic1/venv/bin/python (you must use /home/tester/temp/schemagic/schemagic1/venv/bin/python3)
Installing setuptools, pip, wheel...done.
Running virtualenv with interpreter /usr/bin/python3
Requirement already satisfied: schemagic in ./venv/lib/python3.6/site-packages (from -r requirements.txt (line 1))

Validace prázdného seznamu proti schématu očekávajícího seznam celých čísel proběhne v pořádku (jinými slovy – žádný prvek prázdného seznamu se neliší od čísla :-):

[<class 'int'>]
[]
pass

Další validace, tentokrát skutečně seznamu se třemi čísly, podle očekávání proběhne taktéž korektně (nedojde k výjimce):

[<class 'int'>]
[1, 2, 3]
pass

Třetí test skončí s chybou (vyhodí a ihned poté se zachytí výjimka), a to hned u prvního prvku. Povšimněte si přitom, jak vypadá chybové hlášení – chyba vznikla při aplikaci funkce int na řetězec:

[<class 'int'>]
['hello', 'world', '!']
invalid literal for int() with base 10: 'hello'
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    validate_against_schema(schema, data)
  File "/home/tester/temp/schemagic/schemagic1/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/schemagic/schemagic1/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/schemagic/schemagic1/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/schemagic/schemagic1/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/schemagic/schemagic1/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/schemagic/schemagic1/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
ValueError: invalid literal for int() with base 10: 'hello'

Následuje trojice validačních testů používajících jako schéma seznam řetězců. První test s prázdným seznamem na vstupu doběhne korektně, podobně jako tomu bylo u předchozí trojice testů:

[<class 'str'>]
[]
pass

Další test ovšem vrátí na první pohled možná trošku neočekávané výsledky, protože vstupní seznam [1, 2, 3] je korektně zvalidován, i když 1, 2 ani 3 evidentně nejsou hodnoty typu řetězec, ale hodnoty typu celé číslo. Proč tomu tak je? Celá validace probíhá tak, že se na prvky seznamu postupně aplikuje funkce specifikovaná ve schématu (zde konkrétně funkce str) a tato funkce v Pythonu není predikátem, zde je hodnota řetězcem, ale jedná se o funkci konverzní (více viz navazující kapitoly):

[<class 'str'>]
[1, 2, 3, 4]
pass

Třetí test podle očekávání proběhne korektně, neboť funkci str samozřejmě lze aplikovat na řetězec (výsledkem je přitom totožný objekt, tj. žádná konverze se ve skutečnosti ani neprovede – to je výhodné z výkonnostního hlediska):

[<class 'str'>]
['hello', 'world', '!']
pass

6. Automatické konverze prováděné v průběhu validace

V předchozích kapitolách jsme se zmínili o tom, že se v průběhu validace datových struktur ve skutečnosti volají konverzní funkce. Skutečně je tomu tak, protože návratovou hodnotou funkce validate_against_schema je nová datová struktura vzniklá konverzí. Můžeme se o tom snadno přesvědčit nepatrnou úpravou uživatelsky definované funkce validate:

import sys
import traceback
from schemagic import validate_against_schema
 
 
def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        print(validate_against_schema(schema, data))
        print("pass")
    except ValueError as e:
        print(e)
        traceback.print_exc(file=sys.stdout)

Nyní si můžeme vytvořit tři schémata pro seznamy s různými datovými typy:

integer_list = [int]
float_list = [float]
string_list = [str]

Následně je již snadné zjistit, jak ke konverzím dochází:

validate(integer_list, [1, 2, 3])
validate(integer_list, ["hello", "world", "!"])
validate(integer_list, ["1", 1.5])
 
validate(string_list, [1, 2, 3, 4])
validate(string_list, ["hello", "world", "!"])
 
validate(float_list, [1, 2, 3])
validate(float_list, ["hello", "world", "!"])
validate(float_list, ["1", 1.5, "3.1415"])

Výsledky testů (tučně je vypsán řádek s voláním uživatelsky definované validační funkce):

validate(integer_list, [1, 2, 3])
 
[<class 'int'>]
[1, 2, 3]
[1, 2, 3]
pass

Nevalidní data:

validate(integer_list, ["hello", "world", "!"])
 
[<class 'int'>]
['hello', 'world', '!']
invalid literal for int() with base 10: 'hello'
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    print(validate_against_schema(schema, data))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
ValueError: invalid literal for int() with base 10: 'hello'

Zde například dochází ke konverzi řetězce (parsing) i čísla s plovoucí řádovou čárkou (zaokrouhlení):

validate(integer_list, ["1", 1.5])
 
[<class 'int'>]
['1', 1.5]
[1, 1]
pass

Převod celočíselných hodnot na řetězce:

validate(string_list, [1, 2, 3, 4])
 
[<class 'str'>]
[1, 2, 3, 4]
['1', '2', '3', '4']
pass

Zde se vrátí původní řetězce:

validate(string_list, ["hello", "world", "!"])
 
[<class 'str'>]
['hello', 'world', '!']
['hello', 'world', '!']
pass

Konverze celých čísel na hodnoty s plovoucí řádovou čárkou:

validate(float_list, [1, 2, 3])
 
[<class 'float'>]
[1, 2, 3]
[1.0, 2.0, 3.0]
pass

Nevalidní data:

validate(float_list, ["hello", "world", "!"])
 
[<class 'float'>]
['hello', 'world', '!']
could not convert string to float: 'hello'
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    print(validate_against_schema(schema, data))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-2/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
ValueError: could not convert string to float: 'hello'

Zde je opět vidět prováděná konverze:

validate(float_list, ["1", 1.5, "3.1415"])
 
[<class 'float'>]
['1', 1.5, '3.1415']
[1.0, 1.5, 3.1415]
pass

7. Vytvoření vlastních validačních funkcí

Zkusme si nyní vytvořit vlastní validační funkce. Skript s testy začne stejně, jako v předchozích dvou příkladech, tj. importem potřebných modulů a definicí uživatelské funkce validate:

import sys
import traceback
from schemagic import validate_against_schema
 
 
def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        print(validate_against_schema(schema, data))
        print("pass")
    except (ValueError, TypeError, AssertionError) as e:
        print(e)
        traceback.print_exc(file=sys.stdout)
Povšimněte si, že nyní musíme zachytit výjimky typu ValueError, TypeError a navíc i AssertionError. Důvod je patrný níže.

Následuje definice funkce, která zjistí, zda má hodnota předaná do této funkce očekávaný datový typ. Pokud tomu tak není, je vyhozena výjimka AssertionError:

def is_type(value, expected_type):
    assert type(value) is expected_type

Funkci pro zjištění datového typu využijeme v dalších dvou uživatelských funkcích, které v dalších krocích použijeme při deklaraci schématu:

def is_int(value):
    is_type(value, int)
 
 
def is_float(value):
    is_type(value, float)

Nyní již můžeme naše funkce použít ve schématech:

integer_list = [is_int]
float_list = [is_float]

Ve skutečnosti samozřejmě nemusíme explicitně deklarovat pojmenované funkce, ale můžeme použít funkce anonymní (což vede ke kratšímu, ale poněkud méně čitelnému zápisu):

string_list = [lambda x: is_type(x, str)]

Nově definovaná schémata lze snadno otestovat, například následujícím způsobem:

validate(integer_list, [1, 2, 3])
validate(integer_list, ["hello", "world", "!"])
validate(integer_list, ["1", 1.5])
 
validate(float_list, [1, 2, 3])
validate(float_list, ["hello", "world", "!"])
validate(float_list, ["1", 1.5, "3.1415"])
následovně
validate(string_list, [1, 2, 3, 4])
validate(string_list, ["hello", "world", "!"])

8. Výsledky třetí verze testů

Podívejme se nyní na výsledky třetí varianty testů. Povšimněte si především faktu, že uživatelsky definované validační funkce nevrací žádnou hodnotu, takže výsledkem úspěšné validace bude vždy seznam obsahující prvky None:

[<function is_int at 0x7fe523de6c80>]
[1, 2, 3]
[None, None, None]
pass

Nekorektní vstup:

[<function is_int at 0x7fe523de6c80>]
['hello', 'world', '!']
 
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    print(validate_against_schema(schema, data))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
  File "schemagic_test.py", line 23, in is_int
    is_type(value, int)
  File "schemagic_test.py", line 19, in is_type
    assert type(value) is expected_type
AssertionError

Nyní se skutečně testuje datový typ prvků a neprovádí se žádné automatické konverze:

[<function is_int at 0x7fe523de6c80>]
['1', 1.5]
 
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    print(validate_against_schema(schema, data))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
  File "schemagic_test.py", line 23, in is_int
    is_type(value, int)
  File "schemagic_test.py", line 19, in is_type
    assert type(value) is expected_type
AssertionError

Totéž platí i pro test, zda jsou všechny prvky seznamu typu float:

[<function is_float at 0x7fe523de6d08>]
[1, 2, 3]
 
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    print(validate_against_schema(schema, data))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
  File "schemagic_test.py", line 27, in is_float
    is_type(value, float)
  File "schemagic_test.py", line 19, in is_type
    assert type(value) is expected_type
AssertionError

Nekorektní vstup:

[<function is_float at 0x7fe523de6d08>]
['hello', 'world', '!']
 
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    print(validate_against_schema(schema, data))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
  File "schemagic_test.py", line 27, in is_float
    is_type(value, float)
  File "schemagic_test.py", line 19, in is_type
    assert type(value) is expected_type
AssertionError

Jeden z prvků není typu float:

[<function is_float at 0x7fe523de6d08>]
['1', 1.5, '3.1415']
 
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    print(validate_against_schema(schema, data))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
  File "schemagic_test.py", line 27, in is_float
    is_type(value, float)
  File "schemagic_test.py", line 19, in is_type
    assert type(value) is expected_type
AssertionError

Neúspěšná validace s využitím anonymní validační funkce:

[<function <lambda> at 0x7fe523de6d90>]
[1, 2, 3, 4]
 
Traceback (most recent call last):
  File "schemagic_test.py", line 11, in validate
    print(validate_against_schema(schema, data))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 51, in validate_sequence_template
    return list(map(validate_against_schema, itertools.repeat(schema[0], len(value)), value))
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 81, in <lambda>
    validate_against_schema = lambda schema, value: _validate_against_schema(schema, value)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/utils.py", line 67, in _fn
    return dispatch_fn(*args, **kwargs)
  File "/home/tester/temp/python-schema-checks/schemagic-demo-3/venv/lib/python3.6/site-packages/schemagic/core.py", line 80, in <lambda>
    default=lambda schema, value: schema(value))
  File "schemagic_test.py", line 32, in <lambda>
    string_list = [lambda x: is_type(x, str)]
  File "schemagic_test.py", line 19, in is_type
    assert type(value) is expected_type
AssertionError

Úspěšná validace s využitím anonymní validační funkce:

[<function <lambda> at 0x7fe523de6d90>]
['hello', 'world', '!']
[None, None, None]
pass

9. Další vylepšení validačních funkcí

Příklad si můžeme dále upravit do čtvrté varianty. V první řadě vynecháme výpis zásobníkových rámců a ve chvíli, kdy nastane při validaci chyba vypíšeme pouze podrobnou zprávu o tom, jaká chyba nastala:

import sys
import traceback
from schemagic import validate_against_schema
 
 
def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        print(validate_against_schema(schema, data))
        print("pass")
    except (ValueError, TypeError) as e:
        print(e)

Dále upravíme uživatelskou funkci kontrolující datový typ takovým způsobem, aby se ve chvíli, kdy datový typ neodpovídá typu očekávanému, vyhodila výjimka TypeError s podrobnou zprávou:

def is_type(value, expected_type):
    actual_type = type(value)
    if actual_type is not expected_type:
        msg = "Expected type: {expected}, but the value has type {actual}".format(
            expected=expected_type, actual=actual_type)
        raise TypeError(msg)

Zbytek příkladu již zůstane shodný s příkladem předchozím:

def is_int(value):
    is_type(value, int)
 
 
def is_float(value):
    is_type(value, float)
 
 
integer_list = [is_int]
float_list = [is_float]
string_list = [lambda x: is_type(x, str)]
 
validate(integer_list, [1, 2, 3])
validate(integer_list, ["hello", "world", "!"])
validate(integer_list, ["1", 1.5])
 
validate(float_list, [1, 2, 3])
validate(float_list, ["hello", "world", "!"])
validate(float_list, ["1", 1.5, "3.1415"])
 
validate(string_list, [1, 2, 3, 4])
validate(string_list, ["hello", "world", "!"])

10. Výsledky čtvrté verze testů

Výsledky čtvrté verze testů jsou nyní mnohem přehlednější, o čemž se můžeme snadno přesvědčit:

[<function is_int at 0x7fb9dbfebd08>]
[1, 2, 3]
[None, None, None]
pass
 
 
 
[<function is_int at 0x7fb9dbfebd08>]
['hello', 'world', '!']
Expected type: <class 'int'>, but the value has type <class 'str'>
 
 
 
[<function is_int at 0x7fb9dbfebd08>]
['1', 1.5]
Expected type: <class 'int'>, but the value has type <class 'str'>
 
 
 
[<function is_float at 0x7fb9dbfebd90>]
[1, 2, 3]
Expected type: <class 'float'>, but the value has type <class 'int'>
 
 
 
[<function is_float at 0x7fb9dbfebd90>]
['hello', 'world', '!']
Expected type: <class 'float'>, but the value has type <class 'str'>
 
 
 
[<function is_float at 0x7fb9dbfebd90>]
['1', 1.5, '3.1415']
Expected type: <class 'float'>, but the value has type <class 'str'>
 
 
 
[<function <lambda> at 0x7fb9dbfebe18>]
[1, 2, 3, 4]
Expected type: <class 'str'>, but the value has type <class 'int'>
 
 
 
[<function <lambda> at 0x7fb9dbfebe18>]
['hello', 'world', '!']
[None, None, None]
pass

11. Validace map (slovníků)

Velmi důležité je validace map neboli slovníků. Je tomu tak z toho důvodu, že prakticky jakákoli složitější datová struktura je reprezentována slovníkem, seznamem slovníku nebo slovníkem slovníku (popř. různých kombinací). Validační schéma pro kontrolu, zda jsou klíče slovníku řetězci a zda jsou hodnoty celočíselné, může být velmi jednoduché. Samotné schéma je taktéž představováno slovníkem:

string_to_int_map = {str:is_int}

Zabudujme si toto schéma do příkladu. Začátek příkladu je shodný s příkladem předchozím:

import sys
import traceback
from schemagic import validate_against_schema
 
 
def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        print(validate_against_schema(schema, data))
        print("pass")
    except (ValueError, TypeError) as e:
        print(e)
 
 
def is_type(value, expected_type):
    actual_type = type(value)
    if actual_type is not expected_type:
        msg = "Expected type: {expected}, but the value has type {actual}".format(
            expected=expected_type, actual=actual_type)
        raise TypeError(msg)
 
 
def is_int(value):
    is_type(value, int)

Nyní přidáme validační schéma:

string_to_int_map = {str:is_int}

A otestujeme, zda je možné schéma použít a co se stane ve chvíli, kdy není dodrženo:

validate(string_to_int_map, {"prvni": 1, "druha": 2, "treti": 3})
validate(string_to_int_map, {"prvni": 1.5, "druha": "2", "treti": 3})
validate(string_to_int_map, {"prvni": "x", "druha": "y", "treti": "z"})
validate(string_to_int_map, {1: "x", 2: "y", 3: "z"})
validate(string_to_int_map, {1: 1, 2: 2, 3: 3})

12. Výsledky páté verze testů

Z výsledků testů je patrné, že pouze první a poslední slovník obsahuje korektní data. U posledního slovníku je tomu tak z toho důvodu, že se celočíselné hodnoty zkonvertují na řetězec funkcí str:

{<class 'str'>: <function is_int at 0x7f274d98bc80>}
{'prvni': 1, 'druha': 2, 'treti': 3}
{'prvni': None, 'druha': None, 'treti': None}
pass
 
 
 
{<class 'str'>: <function is_int at 0x7f274d98bc80>}
{'prvni': 1.5, 'druha': '2', 'treti': 3}
Expected type: <class 'int'>, but the value has type <class 'float'>
 
 
 
{<class 'str'>: <function is_int at 0x7f274d98bc80>}
{'prvni': 'x', 'druha': 'y', 'treti': 'z'}
Expected type: <class 'int'>, but the value has type <class 'str'>
 
 
 
{<class 'str'>: <function is_int at 0x7f7fa9bccbf8>}
{1: 'x', 2: 'y', 3: 'z'}
Expected type: <class 'int'>, but the value has type <class 'str'>
 
 
 
{<class 'str'>: <function is_int at 0x7f7fa9bccbf8>}
{1: 1, 2: 2, 3: 3}
pass

13. Přesnější validace map – kontrola, zda hodnoty odpovídají zadaným kritériím

Ukažme si ještě poslední příklad demonstrující možnosti knihovny Schemagic. V tomto příkladu jsou definovány dvě uživatelské validační funkce. Jedna slouží pro kontrolu, zda zadaná hodnota odpovídá jménu či příjmení (používáme zde velmi jednoduchý regulární výraz, ten ovšem nemusí být platný pro všechna příjmení):

def name_str(value):
    if not re.fullmatch("[A-Z][a-z]+", value):
        msg = "Proper name expected, but got '{value}' instead".format(value=value)
        raise TypeError(msg)

Druhá funkce otestuje, zda je hodnota celočíselná a současně kladná:

def pos_int(value):
    is_type(value, int)
    if value <= 0:
        msg = "Positive number expected, but got {value} instead".format(value=value)
        raise TypeError(msg)

Tyto dvě validační funkce použijeme v následujícím schématu:

user = {"name": name_str,
        "surname": name_str,
        "id": pos_int}

Toto schéma říká, že prvky slovníku musí být tři, jejich klíče musí znít „name“, „surname“ a „id“ a konečně jakého typu mají být hodnoty uložené pod těmito klíči.

Nezbývá, než si popsané funkce i schéma zabudovat do skriptu s testem:

import sys
import traceback
import re
from schemagic import validate_against_schema
 
 
def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        print(validate_against_schema(schema, data))
        print("pass")
    except (ValueError, TypeError) as e:
        print(e)
 
 
def is_type(value, expected_type):
    actual_type = type(value)
    if actual_type is not expected_type:
        msg = "Expected type: {expected}, but the value has type {actual}".format(
            expected=expected_type, actual=actual_type)
        raise TypeError(msg)
 
 
def name_str(value):
    if not re.fullmatch("[A-Z][a-z]+", value):
        msg = "Proper name expected, but got '{value}' instead".format(value=value)
        raise TypeError(msg)
 
 
def pos_int(value):
    is_type(value, int)
    if value <= 0:
        msg = "Positive number expected, but got {value} instead".format(value=value)
        raise TypeError(msg)
 
 
user = {"name": name_str,
        "surname": name_str,
        "id": pos_int}

Následuje validace různých slovníků:

validate(user, {"name": "Eda",
                "surname": "Wasserfall",
                "id": 1})
 
validate(user, {"name": "eda",
                "surname": "Wasserfall",
                "id": 1})
 
validate(user, {"name": "E",
                "surname": "Wasserfall",
                "id": 1})
 
validate(user, {"name": "Eda",
                "id": 1})
 
validate(user, {"name": "Eda",
                "surname": "Wasserfall",
                "id": 0})

S očekávanými výsledky (povšimněte si zvýrazněných zpráv):

{'name': <function name_str at 0x7f9202b08c80>, 'surname': <function name_str at 0x7f9202b08c80>, 'id': <function pos_int at 0x7f9202b08d08>}
{'name': 'Eda', 'surname': 'Wasserfall', 'id': 1}
{'name': None, 'surname': None, 'id': None}
pass
 
 
 
{'name': <function name_str at 0x7f9202b08c80>, 'surname': <function name_str at 0x7f9202b08c80>, 'id': <function pos_int at 0x7f9202b08d08>}
{'name': 'eda', 'surname': 'Wasserfall', 'id': 1}
Proper name expected, but got 'eda' instead
 
 
 
{'name': <function name_str at 0x7f9202b08c80>, 'surname': <function name_str at 0x7f9202b08c80>, 'id': <function pos_int at 0x7f9202b08d08>}
{'name': 'E', 'surname': 'Wasserfall', 'id': 1}
Proper name expected, but got 'E' instead
 
 
 
{'name': <function name_str at 0x7f9202b08c80>, 'surname': <function name_str at 0x7f9202b08c80>, 'id': <function pos_int at 0x7f9202b08d08>}
{'name': 'Eda', 'id': 1}
Missing keys {'surname'} for value {'name': 'Eda', 'id': 1}
 
 
 
{'name': <function name_str at 0x7f9202b08c80>, 'surname': <function name_str at 0x7f9202b08c80>, 'id': <function pos_int at 0x7f9202b08d08>}
{'name': 'Eda', 'surname': 'Wasserfall', 'id': 0}
Positive number expected, but got 0 instead
Ještě složitější validační kritéria budou ukázána ve druhé a současně i poslední části tohoto článku.

14. Základní informace o knihovně Schema

Druhou knihovnou, o níž se dnes alespoň ve stručnosti zmíníme, je knihovna nazvaná jednoduše Schema. Tato knihovna taktéž slouží k validaci dat, a to do značné míry podobně, jako tomu je u výše popsané knihovny Schemagic – samotné validační schéma je totiž i zde představováno plnohodnotným pythonovským zdrojovým kódem. V následujících pěti kapitolách si ukážeme některé základní možnosti této knihovny, ovšem podrobnějšímu popisu bude věnován samostatný článek.

15. Instalace knihovny Schema

Podívejme se nyní na způsob instalace knihovny Schema. Abychom se trošku odlišili od předchozích příkladů, bude tato knihovna nainstalována nikoli do virtuálního prostředí Pythonu, ale do adresářové struktury viditelné pro všechny procesy spuštěné aktuálně přihlášeným uživatelem (který si knihovnu nainstaloval). Předpokládáme přitom použití Pythonu 3.x; pokud používáte Python 2.x, stačí nahradit pip3 a pip.

Samotná instalace se provede příkazem:

$ pip3 install --user schema
 
Downloading/unpacking schema
  Downloading schema-0.6.7-py2.py3-none-any.whl
Installing collected packages: schema
Successfully installed schema
Cleaning up...

Pro jistotu si můžete nechat vypsat informace o tom, kam (a zda vůbec) byla knihovna nainstalována:

$ pip3 show schema
 
---
Name: schema
Version: 0.6.7
Location: /home/tester/.local/lib/python3.4/site-packages
Requires:

Pokud vše proběhlo v pořádku, můžeme si vyzkoušet základní možnosti této knihovny, a to přímo v interaktivní konzoli programovacího jazyka Python:

$ python3
 
Python 3.4.3 (default, Nov 28 2017, 16:41:13)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from schema import Schema
 
>>> Schema(int).validate(42)
42
 
>>> Schema([int]).validate([42, 1, 2])
[42, 1, 2]
 
>>> Schema((int, )).validate((42, 1, 2))
(42, 1, 2)
 
>>> Schema((int, float, str)).validate((3, "1", 3.14))
(3, '1', 3.14)
Poznámka: pokud jste si knihovnu nainstalovali příkazem pip3, bude dostupná pro interpret Pythonu 3.x. Pokud naopak použijete pip, lze knihovnu použít z Pythonu 2.x (i když na některých systémech může být pip alias pro pip3).

16. Jednoduchý příklad použití knihovny Schema pro validaci datových struktur

V prvním příkladu opět vytvoříme uživatelskou funkci validate, která ovšem nyní bude volat metodu nazvanou Schema.validate a v případě, že validace neproběhne korektně, se bude očekávat výjimka typu SchemaError:

from schema import Schema, SchemaError
 
 
def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        schema.validate(data)
        print("pass")
    except SchemaError as e:
        print(e)

Následuje definice validačních schémat. Povšimněte si syntaktické podobnosti s knihovnou Schemagic:

integer_list = Schema([int])
float_list = Schema([float])
string_list = Schema([str])

Nyní si validaci s využitím všech tří schémat vyzkoušíme:

validate(integer_list, [1, 2, 3])
validate(integer_list, [1.1, 2.2, 3.3])
validate(integer_list, ["1", "2", "3"])
 
validate(float_list, [1, 2, 3])
validate(float_list, [1.1, 2.2, 3.3])
validate(float_list, ["1", "2", "3"])
 
validate(string_list, [1, 2, 3])
validate(string_list, [1.1, 2.2, 3.3])
validate(string_list, ["1", "2", "3"])

I když způsob zápisu schémat je v Schema syntakticky podobný knihovně Schemagic, sémantika je odlišná! Je tomu tak z toho důvodu, že nyní int, str apod. představuje zápis datových typů a nikoli konverzních funkcí. Ostatně se o tom lze snadno přesvědčit spuštěním příkladu.

První validace je shodná:

Schema([<class 'int'>])
[1, 2, 3]
pass

Nyní ovšem přichází rozdíl – žádné automatické konverze, ale test elementů na datový typ:

Schema([<class 'int'>])
[1.1, 2.2, 3.3]
Or(<class 'int'>) did not validate 1.1
1.1 should be instance of 'int'
 
 
 
Schema([<class 'int'>])
['1', '2', '3']
Or(<class 'int'>) did not validate '1'
'1' should be instance of 'int'
 
 
 
Schema([<class 'float'>])
[1, 2, 3]
Or(<class 'float'>) did not validate 1
1 should be instance of 'float'
 
 
 
Schema([<class 'float'>])
[1.1, 2.2, 3.3]
pass
 
 
 
Schema([<class 'float'>])
['1', '2', '3']
Or(<class 'float'>) did not validate '1'
'1' should be instance of 'float'
 
 
 
Schema([<class 'str'>])
[1, 2, 3]
Or(<class 'str'>) did not validate 1
1 should be instance of 'str'
 
 
 
Schema([<class 'str'>])
[1.1, 2.2, 3.3]
Or(<class 'str'>) did not validate 1.1
1.1 should be instance of 'str'
 
 
 
Schema([<class 'str'>])
['1', '2', '3']
pass

17. Validace obsahu slovníků

Samozřejmě, že i v knihovně Schema je možné kontrolovat obsahy slovníků. Pro zajímavost si to vyzkoušejme v interaktivním prostředí Pythonu:

$ python3

Naimportujeme potřebný modul a nadefinujeme validační schéma:

>>> from schema import Schema
>>> s1 = Schema({"name": str, "surname": str})

Nyní můžeme schéma snadno použít pro různé slovníky. Povšimněte si, že se kontroluje i existence všech dvojic klíč+hodnota:

>>> s1.validate({"name": "Eda", "surname": "Wasserfall"})
{'name': 'Eda', 'surname': 'Wasserfall'}
>>> s1.validate({"name": "Eda", "surname": ""})
{'name': 'Eda', 'surname': ''}
>>> s1.validate({"name": "Eda"})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/tester/.local/lib/python3.4/site-packages/schema.py", line 290, in validate
    SchemaMissingKeyError('Missing keys: ' + s_missing_keys, e)
schema.SchemaMissingKeyError: Missing keys: 'surname'

To však zdaleka není vše, protože pro jednu hodnotu (řekněme pro klíče) můžeme použít více validačních kritérií. Ty je zapotřebí spojit klauzulí And. I tu si musíme naimportovat:

>>> from schema import And
>>> s2 = Schema({"name": And(str, len), "surname": And(str, len)})

Zápis And(str, len) znamená: pod klíčem „name“ má být uložena hodnota typu řetězec, jehož délka musí být nenulová (připomeňme si, že nula v logických výrazech odpovídá hodnotě False).

Opět si nové validační schéma odzkoušíme:

>>> s2.validate({"name": "Eda", "surname": ""})
Traceback (most recent call last):
  File "/home/tester/.local/lib/python3.4/site-packages/schema.py", line 316, in validate
    return s.validate(data)
  File "/home/tester/.local/lib/python3.4/site-packages/schema.py", line 96, in validate
    data = s.validate(data)
  File "/home/tester/.local/lib/python3.4/site-packages/schema.py", line 334, in validate
    raise SchemaError('%s(%r) should evaluate to True' % (f, data), e)
schema.SchemaError: len('') should evaluate to True
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "/home/tester/.local/lib/python3.4/site-packages/schema.py", line 276, in validate
    ignore_extra_keys=i).validate(value)
  File "/home/tester/.local/lib/python3.4/site-packages/schema.py", line 318, in validate
    raise SchemaError([None] + x.autos, [e] + x.errors)
schema.SchemaError: len('') should evaluate to True
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/tester/.local/lib/python3.4/site-packages/schema.py", line 279, in validate
    raise SchemaError([k] + x.autos, [e] + x.errors)
schema.SchemaError: Key 'surname' error:
len('') should evaluate to True

Nyní je vše v pořádku:

>>> s2.validate({"name": "Eda", "surname": "Wasserfall"})
{'name': 'Eda', 'surname': 'Wasserfall'}

18. Druhý příklad používající knihovnu Schema

Ve druhém příkladu, který používá knihovnu Schema jsou ukázány některé další možnosti nabízené touto knihovnou. Zejména si povšimněte, jak se zapisují uživatelské validační funkce – ty totiž nemusí (a nemají) vyhazovat výjimku, ale pouze vracet pravdivostní hodnotu (jedná se tedy o skutečné predikáty):

def pos(value):
    return type(value) is int and value > 0

Predikáty lze samozřejmě zapsat i formou anonymní funkce a celý zápis tak podstatně zkrátit:

validate(Schema(lambda value: value < 0), 42)

V tomto příkladu taktéž validujeme prvky slovníků tak, jak jsme si to ukázali v předchozí kapitole.

Úplný zdrojový kód vypadá následovně:

MIF18_Michálek

from schema import Schema, SchemaError
 
 
def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        schema.validate(data)
        print("pass")
    except SchemaError as e:
        print(e)
 
 
def pos(value):
    return type(value) is int and value > 0
 
 
number_list = Schema([int, float, complex])
 
validate(number_list, [1, 2, 3])
validate(number_list, [1.1, 2.2, 3.3])
validate(number_list, [1+2j, 3+4j, 5j])
validate(number_list, ["1", "2", "3"])
 
binary_numbers = Schema([0, 1])
validate(binary_numbers, [0, 0, 0])
validate(binary_numbers, [1, 1, 0])
validate(binary_numbers, [1, 2, 3])
 
validate(Schema(pos), 42)
validate(Schema(pos), 0)
validate(Schema(pos), -1)
validate(Schema(pos), 1.5)
 
validate(Schema(lambda value: value < 0), 42)
validate(Schema(lambda value: value < 0), 0)
validate(Schema(lambda value: value < 0), -1)
 
user = Schema({"name": str,
               "surname": str,
               "id": pos})
 
validate(user, {"name": "Eda",
                "surname": "Wasserfall",
                "id": 1})
 
validate(user, {"name": "Eda",
                "id": 1})
 
validate(user, {"name": "Eda",
                "surname": "Wasserfall",
                "id": 0})

19. Výsledek druhého příkladu

Druhý příklad by měl po svém spuštění vypsat na standardní výstup následující zprávy odpovídající jednotlivým po sobě jdoucím validacím:

Schema([<class 'int'>, <class 'float'>, <class 'complex'>])
[1, 2, 3]
pass
 
 
 
Schema([<class 'int'>, <class 'float'>, <class 'complex'>])
[1.1, 2.2, 3.3]
pass
 
 
 
Schema([<class 'int'>, <class 'float'>, <class 'complex'>])
[(1+2j), (3+4j), 5j]
pass
 
 
 
Schema([<class 'int'>, <class 'float'>, <class 'complex'>])
['1', '2', '3']
Or(<class 'int'>, <class 'float'>, <class 'complex'>) did not validate '1'
'1' should be instance of 'complex'
 
 
 
Schema([0, 1])
[0, 0, 0]
pass
 
 
 
Schema([0, 1])
[1, 1, 0]
pass
 
 
 
Schema([0, 1])
[1, 2, 3]
Or(0, 1) did not validate 2
1 does not match 2
 
 
 
Schema(<function pos at 0x7f45c4172bf8>)
42
pass
 
 
 
Schema(<function pos at 0x7f45c4172bf8>)
0
pos(0) should evaluate to True
 
 
 
Schema(<function pos at 0x7f45c4172bf8>)
-1
pos(-1) should evaluate to True
 
 
 
Schema(<function pos at 0x7f45c4172bf8>)
1.5
pos(1.5) should evaluate to True
 
 
 
Schema(<function <lambda> at 0x7f45c40ff730>)
42
<lambda>(42) should evaluate to True
 
 
 
Schema(<function <lambda> at 0x7f45c40ff730>)
0
<lambda>(0) should evaluate to True
 
 
 
Schema(<function <lambda> at 0x7f45c40ff730>)
-1
pass
 
 
 
Schema({'surname': <class 'str'>, 'id': <function pos at 0x7f45c4172bf8>, 'name': <class 'str'>})
{'surname': 'Wasserfall', 'id': 1, 'name': 'Eda'}
pass
 
 
 
Schema({'surname': <class 'str'>, 'id': <function pos at 0x7f45c4172bf8>, 'name': <class 'str'>})
{'id': 1, 'name': 'Eda'}
Missing keys: 'surname'
 
 
 
Schema({'surname': <class 'str'>, 'id': <function pos at 0x7f45c4172bf8>, 'name': <class 'str'>})
{'surname': 'Wasserfall', 'id': 0, 'name': 'Eda'}
Key 'id' error:
pos(0) should evaluate to True

20. Repositář se všemi demonstračními příklady

Všech osm demonstračních projektů, které jsme si v dnešním článku popsali, bylo uloženo do repositáře, který naleznete na adrese https://github.com/tisnik/python-schema-checks. V tabulce pod tímto odstavcem jsou vypsány odkazy na všechny tyto projekty:

Projekt Stručný popis Cesta
schemagic-demo-1 základní vlastnosti knihovny Schemagic https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-1
schemagic-demo-2 konverze prováděné při validaci https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-2
schemagic-demo-3 vlastní validační funkce https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-3
schemagic-demo-4 vylepšení předchozího příkladu https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-4
schemagic-demo-5 validace slovníků https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-5
schemagic-demo-6 validace slovníků podruhé https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-6
     
schema-demo-1 základní vlastnosti knihovny Scheme https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-1
schema-demo-2 validace slovníků a dalších typů https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-2

21. Odkazy na Internetu

  1. schemagic 0.9.1 (na PyPi)
    https://pypi.python.org/py­pi/schemagic/0.9.1
  2. Schemagic / Schemagic.web (na GitHubu)
    https://github.com/Mechrop­hile/schemagic
  3. schema 0.6.7 (na PyPi)
    https://pypi.python.org/pypi/schema
  4. schema (na GitHubu)
    https://github.com/keleshev/schema
  5. XML Schema validator and data conversion library for Python
    https://github.com/brunato/xmlschema
  6. xmlschema 0.9.7
    https://pypi.python.org/py­pi/xmlschema/0.9.7
  7. jsonschema 2.6.0
    https://pypi.python.org/py­pi/jsonschema
  8. warlock 1.3.0
    https://pypi.python.org/pypi/warlock
  9. Python Virtual Environments – A Primer
    https://realpython.com/python-virtual-environments-a-primer/
  10. pip 1.1 documentation: Requirements files
    https://pip.readthedocs.i­o/en/1.1/requirements.html
  11. unittest.mock — mock object library
    https://docs.python.org/3­.5/library/unittest.mock.html
  12. mock 2.0.0
    https://pypi.python.org/pypi/mock
  13. An Introduction to Mocking in Python
    https://www.toptal.com/python/an-introduction-to-mocking-in-python
  14. Unit testing (Wikipedia)
    https://en.wikipedia.org/wi­ki/Unit_testing
  15. Unit testing
    https://cs.wikipedia.org/wi­ki/Unit_testing
  16. Test-driven development (Wikipedia)
    https://en.wikipedia.org/wiki/Test-driven_development
  17. Pip (dokumentace)
    https://pip.pypa.io/en/stable/
  18. 5 Differences between clojure.spec and Schema
    https://lispcast.com/clojure.spec-vs-schema/
  19. Schema: Clojure(Script) library for declarative data description and validation
    https://github.com/plumatic/schema
  20. clojure.spec – Rationale and Overview
    https://clojure.org/about/spec
Našli jste v článku chybu?