Obsah
1. Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema
2. Základní informace o knihovně Schemagic
3. Vytvoření projektu využívajícího knihovnu Schemagic
4. Popis jednotlivých řádků testu
5. Výsledky validace provedené první verzí testů
6. Automatické konverze prováděné v průběhu validace
7. Vytvoření vlastních validačních funkcí
9. Další vylepšení validačních funkcí
10. Výsledky čtvrté 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
16. Jednoduchý příklad použití knihovny Schema pro validaci datových struktur
18. Druhý příklad používající knihovnu Schema
20. Repositář se všemi demonstračními příklady
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:
- Samozřejmě umožňuje samotnou validaci (musíme vědět, jak mají data vypadat)
- 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ě.
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/Mechrophile/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
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)
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
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)
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ě:
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
- schemagic 0.9.1 (na PyPi)
https://pypi.python.org/pypi/schemagic/0.9.1 - Schemagic / Schemagic.web (na GitHubu)
https://github.com/Mechrophile/schemagic - schema 0.6.7 (na PyPi)
https://pypi.python.org/pypi/schema - schema (na GitHubu)
https://github.com/keleshev/schema - XML Schema validator and data conversion library for Python
https://github.com/brunato/xmlschema - xmlschema 0.9.7
https://pypi.python.org/pypi/xmlschema/0.9.7 - jsonschema 2.6.0
https://pypi.python.org/pypi/jsonschema - warlock 1.3.0
https://pypi.python.org/pypi/warlock - Python Virtual Environments – A Primer
https://realpython.com/python-virtual-environments-a-primer/ - pip 1.1 documentation: Requirements files
https://pip.readthedocs.io/en/1.1/requirements.html - unittest.mock — mock object library
https://docs.python.org/3.5/library/unittest.mock.html - mock 2.0.0
https://pypi.python.org/pypi/mock - An Introduction to Mocking in Python
https://www.toptal.com/python/an-introduction-to-mocking-in-python - Unit testing (Wikipedia)
https://en.wikipedia.org/wiki/Unit_testing - Unit testing
https://cs.wikipedia.org/wiki/Unit_testing - Test-driven development (Wikipedia)
https://en.wikipedia.org/wiki/Test-driven_development - Pip (dokumentace)
https://pip.pypa.io/en/stable/ - 5 Differences between clojure.spec and Schema
https://lispcast.com/clojure.spec-vs-schema/ - Schema: Clojure(Script) library for declarative data description and validation
https://github.com/plumatic/schema - clojure.spec – Rationale and Overview
https://clojure.org/about/spec