Obsah
1. Validace datových struktur v Pythonu (2. část)
2. Dokončení popisu možností nabízených knihovnou Schema
3. Třetí demonstrační příklad – kontrola platu a pracovní pozice zaměstnanců
4. Výsledky běhu třetího demonstračního příkladu
5. Čtvrtý demonstrační příklad – použití klauzulí And a Or
6. Výsledky běhu čtvrtého demonstračního příkladu
8. Použití regulárních výrazů pro kontrolu dat ve slovnících
9. Zdrojový kód pátého demonstračního příkladu a jeho výsledky
10. Použití třídy pro kontrolu údajů o platu a o pracovní pozici
11. Kontrola unikátnosti ID zaměstnance
12. Zdrojový kód šestého demonstračního příkladu
13. Výsledky běhu šestého demonstračního příkladu
15. Ukázka základních možností knihovny Voluptuous v REPLu
16. První příklad – validace obsahu seznamů knihovnou Voluptuous
17. Tři varianty validace celých kladných čísel implementované ve třech knihovnách
18. Druhý příklad – validace obsahu slovníků
19. Repositář s demonstračními příklady
1. Validace datových struktur v Pythonu (2. část)
V předchozím článku jsme se seznámili s dvojicí knihoven určených pro validaci datových struktur v Pythonu. Připomeňme si, že se jedná o knihovny pojmenované Schemagic a Schema. Obě tyto knihovny jsou založeny na tom, že samotný popis validačních schémat je nadeklarován přímo v Pythonu, takže se případní uživatelé těchto knihoven (což jsou většinou programátoři) nemusí učit nový DSL (doménově specifický jazyk). Knihovna Schemagic je zvláštní tím, že kromě vlastní validace provádí i konverzi dat. Ve stručnosti je možné říci, že validační kritéria jsou představována konverzními funkcemi, které buď konverzi provedou nebo vyhodí výjimku typu ValueError nebo TypeError. Naproti tomu knihovna Schema skutečně provádí pouze validaci datových struktur a proto jsou jinak zapisována i validační kritéria – ta jsou tvořena predikáty, tj. funkcemi vracejícími pravdivostní hodnotu True či False.
Příklad (ten nejjednodušší možný) validace knihovnou Schemagic:
>>> from schemagic import validate_against_schema >>> validate_against_schema(int, 42) 42
Příklad validace knihovnou Schema:
>>> from schema import Schema >>> s1 = Schema({"name": str, "surname": str}) >>> s1.validate({"name": "Eda", "surname": "Wasserfall"}) {'name': 'Eda', 'surname': 'Wasserfall'} >>> 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'
2. Dokončení popisu možností nabízených knihovnou Schema
V první polovině dnešního článku nejprve dokončíme popis knihovny Schema. Pravděpodobně se jedná o nejčastěji používanou knihovnu tohoto typu (alespoň v Pythonu), ovšem to může být způsobeno tím, že třetí knihovna Voluptuous má těžko zapamatovatelné jméno, i když jsou některé její vlastnosti lepší. U knihovny Schema si ukážeme použití klauzulí And, Or a Optional i to, jakým způsobem je možné validační schéma vylepšit (a současně zjednodušit) s využitím vlastních tříd implementujících metodu validate. Díky tomu, že se pro validaci používají třídy, je totiž možné implementovat i poměrně sofistikované testy, například zjištění, zda údaje ve všech slovnících mají unikátní ID popř. že je někde ID zduplikováno (samozřejmě lze vymyslet i další testy, v nichž se využívají kontextové informace uložené do objektů).
3. Třetí demonstrační příklad – kontrola platu a pracovní pozice zaměstnanců
Již minule jsme si ukázali některé možnosti nabízené knihovnou Schema. Víme již, jak pomocí predikátů kontrolovat například i hodnoty uložené do slovníků atd. Nyní si tyto vlastnosti otestujeme na dalším příkladu, v němž budeme validovat záznamy (slovníky) obsahující informace o zaměstnancích. V těchto záznamech se budou vyskytovat údaje o platu a taktéž pracovní pozici (ta bude později kontrolována oproti číselníku). Základní validační schéma může vypadat takto:
employee = Schema({"name": str, "surname": str, "id": positive_integer, "salary": salary, "position": str})
Vidíme, že tři hodnoty musí být typu řetězec, ID zaměstnance bude celé kladné číslo a jeho plat bude kontrolován predikátem salary. Oba dva námi vytvořené predikáty jsou prozatím dosti jednoduché:
def positive_integer(value): return type(value) is int and value > 0 def salary(value): return type(value) is float and value > 10000.0 and value < 99999.9
Ve druhém predikátu kontrolujeme jak datový typ, tak i to, zda hodnota leží v určeném rozsahu.
Úplný zdrojový kód v pořadí třetího příkladu, v němž zkoušíme možnosti knihovny Schema, vypadá následovně:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from sys import argv from schema import Schema, SchemaError def validate(schema, data, verbose_mode=False): try: print("\n\n") if verbose_mode: print(schema) print(data) schema.validate(data) print("pass") except SchemaError as e: print(e) def positive_integer(value): return type(value) is int and value > 0 def salary(value): return type(value) is float and value > 10000.0 and value < 99999.9 employee = Schema({"name": str, "surname": str, "id": positive_integer, "salary": salary, "position": str}) verbose_mode = "-v" in argv validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": -15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 1000000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "id": 1, "salary": 100000000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 45000.0, "position": ""}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 45000.0, "position": "tovarnik"}, verbose_mode) validate(employee, {"name": "", "surname": "", "id": 1, "salary": 25000.0, "position": ""}, verbose_mode)
4. Výsledky běhu třetího demonstračního příkladu
Podívejme se nyní na výsledky běhu třetího demonstračního příkladu, především na zprávy oznamující úspěšnou popř. neúspěšnou validaci:
{'salary': 15000.0, 'position': 'QA', 'name': 'Eda', 'id': 1, 'surname': 'Wasserfall'} pass {'salary': 15000, 'position': 'QA', 'name': 'Eda', 'id': 1, 'surname': 'Wasserfall'} Key 'salary' error: salary(15000) should evaluate to True {'salary': -15000.0, 'position': 'QA', 'name': 'Eda', 'id': 1, 'surname': 'Wasserfall'} Key 'salary' error: salary(-15000.0) should evaluate to True {'salary': 1000000.0, 'position': 'QA', 'name': 'Eda', 'id': 1, 'surname': 'Wasserfall'} Key 'salary' error: salary(1000000.0) should evaluate to True {'salary': 15000.0, 'position': 'QA', 'name': 'Eda', 'id': 1, 'surname': 'Wasserfall'} pass {'salary': 100000000.0, 'position': 'QA', 'name': 'Eda', 'id': 1} Key 'salary' error: salary(100000000.0) should evaluate to True {'salary': 45000.0, 'position': '', 'name': 'Eda', 'id': 1, 'surname': 'Wasserfall'} pass {'salary': 45000.0, 'position': 'tovarnik', 'name': 'Eda', 'id': 1, 'surname': 'Wasserfall'} pass {'salary': 25000.0, 'position': '', 'name': '', 'id': 1, 'surname': ''} pass
5. Čtvrtý demonstrační příklad – použití klauzulí And a Or
Při pohledu na předchozí příklad je patrné, že validace ještě v žádném případě není dokonalá. Především se může stát, že plat bude zapsán formou celého čísla, vůbec nekontrolujeme, zda není jméno/příjmení prázdný řetězec a dále není kontrolována ani pracovní pozice. Vylepšení bude provedeno s pomocí klauzulí And a Or, které dokážou spojit více predikátů logickou spojkou. Nejprve si však vytvoříme nové predikáty, které použijeme (bez dalšího popisu – jsou prajednoduché):
def positive_integer(value): return type(value) is int and value > 0 def positive_float(value): return type(value) is float and value > 0
Dále si vytvoříme jednoduchý číselník se jmény pracovních pozic:
POSITIONS = ["QA", "DevOps", "Admin", "Docs", "HR"]
A konečně můžeme napsat nové validační schéma, tentokrát pomocí spojek And a Or:
employee = Schema({"name": And(str, len), "surname": And(str, len), "id": positive_integer, "salary": Or(positive_integer, positive_float, lambda x: x > 10000.0 and x < 99999.0), "position": And(str, lambda s: s in POSITIONS)})
Úplný zdrojový kód v pořadí již čtvrtého příkladu, v němž zkoušíme možnosti knihovny Schema, vypadá následovně:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from sys import argv from schema import Schema, SchemaError, And, Or def validate(schema, data, verbose_mode=False): try: print("\n\n") if verbose_mode: print(schema) print(data) schema.validate(data) print("pass") except SchemaError as e: print(e) def positive_integer(value): return type(value) is int and value > 0 def positive_float(value): return type(value) is float and value > 0 POSITIONS = ["QA", "DevOps", "Admin", "Docs", "HR"] employee = Schema({"name": And(str, len), "surname": And(str, len), "id": positive_integer, "salary": Or(positive_integer, positive_float, lambda x: x > 10000.0 and x < 99999.0), "position": And(str, lambda s: s in POSITIONS)}) verbose_mode = "-v" in argv validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": -15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 1000000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "id": 1, "salary": 100000000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 45000.0, "position": ""}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 45000.0, "position": "tovarnik"}, verbose_mode) validate(employee, {"name": "", "surname": "", "id": 1, "salary": 25000.0, "position": ""}, verbose_mode)
6. Výsledky běhu čtvrtého demonstračního příkladu
Pokud předchozí příklad spustíme, měly by se na standardní výstup vypsat následující zprávy o úspěchu či neúspěchu validace jednotlivých informací o zaměstnancích:
{'position': 'QA', 'id': 1, 'salary': 15000.0, 'name': 'Eda', 'surname': 'Wasserfall'} pass {'position': 'QA', 'id': 1, 'salary': 15000, 'name': 'Eda', 'surname': 'Wasserfall'} pass {'position': 'QA', 'id': 1, 'salary': -15000.0, 'name': 'Eda', 'surname': 'Wasserfall'} Key 'salary' error: Or(<function positive_integer at 0x7fa41594fbf8>, <function positive_float at 0x7fa4158df730>, <function <lambda> at 0x7fa4158df7b8>) did not validate -15000.0 <lambda>(-15000.0) should evaluate to True {'position': 'QA', 'id': 1, 'salary': 1000000.0, 'name': 'Eda', 'surname': 'Wasserfall'} pass {'position': 'QA', 'id': 1, 'salary': 15000.0, 'name': 'Eda', 'surname': 'Wasserfall'} pass {'id': 1, 'salary': 100000000.0, 'name': 'Eda', 'position': 'QA'} Missing keys: 'surname' {'position': '', 'id': 1, 'salary': 45000.0, 'name': 'Eda', 'surname': 'Wasserfall'} Key 'position' error: <lambda>('') should evaluate to True {'position': 'tovarnik', 'id': 1, 'salary': 45000.0, 'name': 'Eda', 'surname': 'Wasserfall'} Key 'position' error: <lambda>('tovarnik') should evaluate to True {'position': '', 'id': 1, 'salary': 25000.0, 'name': '', 'surname': ''} Key 'position' error: <lambda>('') should evaluate to True
Povšimněte si, jak se projevilo nekorektně napsané validační kritérium pro „salary“.
7. Použití klauzule Optional
Další velmi užitečnou klauzulí je klauzule Optional, která se zapisuje u klíčů. Označujeme jí ty klíče, resp. ty dvojice klíč+hodnota, které jsou nepovinné. Příklad použití pro dvě nepovinné položky „salary“ a „position“ zapsané do validačního kritéria:
employee = Schema({"name": And(str, len), "surname": And(str, len), "id": positive_integer, Optional("salary"): And(Or(positive_integer, positive_float), lambda x: x > 10000.0 and x < 99999.0), Optional("position"): And(str, lambda s: s in POSITIONS)})
8. Použití regulárních výrazů pro kontrolu dat ve slovnících
Ukažme si ještě jednoduchý predikát sloužící 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): return re.fullmatch("[A-Z][a-z]+", value)
Způsob zakomponování tohoto predikátu do validačního schématu:
employee = Schema({"name": And(str, len, name_str), "surname": And(str, len, name_str), "id": positive_integer, Optional("salary"): And(Or(positive_integer, positive_float), lambda x: x > 10000.0 and x < 99999.0), Optional("position"): And(str, lambda s: s in POSITIONS)})
Je použita klauzule And, takže pro jméno a příjmení je nutné, aby následující predikáty platily v uvedeném pořadí:
- str – jedná se o řetězec
- len – řetězec není prázdný
- name_str – řetězec odpovídá regulárnímu výrazu „[A-Z][a-z]+“
9. Zdrojový kód pátého demonstračního příkladu a jeho výsledky
Úplný zdrojový kód pátého příkladu vypadá následovně:
#!/usr/bin/env python # vim: set fileencoding=utf-8 import re from sys import argv from schema import Schema, SchemaError, And, Or, Optional def validate(schema, data, verbose_mode=False): try: print("\n\n") if verbose_mode: print(schema) print(data) schema.validate(data) print("pass") except SchemaError as e: print(e) def positive_integer(value): return type(value) is int and value > 0 def positive_float(value): return type(value) is float and value > 0 def name_str(value): return re.fullmatch("[A-Z][a-z]+", value) POSITIONS = ["QA", "DevOps", "Admin", "Docs", "HR"] employee = Schema({"name": And(str, len, name_str), "surname": And(str, len, name_str), "id": positive_integer, Optional("salary"): And(Or(positive_integer, positive_float), lambda x: x > 10000.0 and x < 99999.0), Optional("position"): And(str, lambda s: s in POSITIONS)}) verbose_mode = "-v" in argv validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": -15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 1000000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "id": 1, "salary": 100000000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 45000.0, "position": ""}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 45000.0, "position": "tovarnik"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 25000.0,}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "position": "DevOps"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1}, verbose_mode) validate(employee, {"name": "eda", "surname": "Wasserfall", "id": 1, "salary": 45000.0, "position": "HR"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "wasserfall", "id": 1, "salary": 45000.0, "position": "HR"}, verbose_mode)
Následuje výpis výsledků validace:
{'position': 'QA', 'id': 1, 'name': 'Eda', 'surname': 'Wasserfall', 'salary': 15000.0} pass {'position': 'QA', 'id': 1, 'name': 'Eda', 'surname': 'Wasserfall', 'salary': 15000} pass {'position': 'QA', 'id': 1, 'name': 'Eda', 'surname': 'Wasserfall', 'salary': -15000.0} Key 'salary' error: Or(<function positive_integer at 0x7f671c0779d8>, <function positive_float at 0x7f671c085730>) did not validate -15000.0 positive_float(-15000.0) should evaluate to True {'position': 'QA', 'id': 1, 'name': 'Eda', 'surname': 'Wasserfall', 'salary': 1000000.0} Key 'salary' error: <lambda>(1000000.0) should evaluate to True {'position': 'QA', 'id': 1, 'name': 'Eda', 'surname': 'Wasserfall', 'salary': 15000.0} pass {'position': 'QA', 'id': 1, 'name': 'Eda', 'salary': 100000000.0} Key 'salary' error: <lambda>(100000000.0) should evaluate to True {'position': '', 'id': 1, 'name': 'Eda', 'surname': 'Wasserfall', 'salary': 45000.0} Key 'position' error: <lambda>('') should evaluate to True {'position': 'tovarnik', 'id': 1, 'name': 'Eda', 'surname': 'Wasserfall', 'salary': 45000.0} Key 'position' error: <lambda>('tovarnik') should evaluate to True {'id': 1, 'name': 'Eda', 'surname': 'Wasserfall', 'salary': 25000.0} pass {'position': 'DevOps', 'id': 1, 'name': 'Eda', 'surname': 'Wasserfall'} pass {'id': 1, 'name': 'Eda', 'surname': 'Wasserfall'} pass {'position': 'HR', 'id': 1, 'name': 'eda', 'surname': 'Wasserfall', 'salary': 45000.0} Key 'name' error: name_str('eda') should evaluate to True {'position': 'HR', 'id': 1, 'name': 'Eda', 'surname': 'wasserfall', 'salary': 45000.0} Key 'surname' error: name_str('wasserfall') should evaluate to True
10. Použití třídy pro kontrolu údajů o platu a o pracovní pozici
Knihovna Schema programátorům nabízí ještě jednu velmi zajímavou možnost definice validačních kritérií. Kritérium totiž může být implementováno třídou, u které se očekává existence metody validate, která pro neúspěšnou validaci vyhazuje výjimku typu SchemaError. Ukažme si to na tom nejjednodušším příkladu – kontrole platu:
class Salary: def validate(self, value): if not is_integer(value) and not is_float(value): raise SchemaError("Salary has unexpected type {t}".format(t=type(value))) elif value <= 10000: raise SchemaError("Salary {s} is too low".format(s=value)) elif value >= 99999.9: raise SchemaError("Salary {s} is too high".format(s=value))
Zařazení do schématu se provede takto (vytvoří se instance třídy):
employee = Schema({"name": And(str, len, name_str)), "surname": And(str, len, name_str)), "id": ..., Optional("salary"): Salary(), Optional("position"): ...})
Podobná třída již v knihovně existuje – jedná se o třídu Regex, kterou taktéž můžeme použít. Při vytváření objektů se do konstruktoru přímo předá regulární výraz:
employee = Schema({"name": And(str, len, Regex("[A-Z][a-z]+")), "surname": And(str, len, Regex("[A-Z][a-z]+")), "id": ..., Optional("salary"): Salary(), Optional("position"): ...})
Podobně lze realizovat třídu pro kontrolu pracovní pozice:
class Position: POSITIONS = ["QA", "DevOps", "Admin", "Docs", "HR"] def validate(self, value): if value not in Position.POSITIONS: raise SchemaError("Unknown position '{p}'".format(p=value))
Zařazení do schématu se provede takto (vytvoří se instance třídy):
employee = Schema({"name": And(str, len, Regex("[A-Z][a-z]+")), "surname": And(str, len, Regex("[A-Z][a-z]+")), "id": ..., Optional("salary"): Salary(), Optional("position"): And(str, Position())})
11. Kontrola unikátnosti ID zaměstnance
Vzhledem k tomu, že se do schématu předává instance třídy provádějící validaci, můžeme naše validační schéma ještě více vylepšit. Můžeme totiž provést kontrolu, zda je ID zaměstnance unikátní, tj. zda neexistují nějaké duplicity. Provede se to jednoduše – do nové třídy pro kontrolu unikátnosti ID přidáme atribut _ids obsahující množinu již validovaných ID. Musí přitom platit podmínka, že ID právě kontrolovaného záznamu se v této množině nesmí vyskytovat. Implementace je snadná:
class UniqueId: def __init__(self): self._ids = set() def validate(self, value): if value in self._ids: raise SchemaError("ID {id} is not unique".format(id=value)) self._ids.add(value)
Zařazení do schématu se provede takto:
employee = Schema({"name": And(str, len, Regex("[A-Z][a-z]+")), "surname": And(str, len, Regex("[A-Z][a-z]+")), "id": UniqueId(), Optional("salary"): Salary(), Optional("position"): And(str, Position())})
12. Zdrojový kód šestého demonstračního příkladu
Následuje výpis úplného zdrojového kódu šestého příkladu použití knihovny Schema:
#!/usr/bin/env python # vim: set fileencoding=utf-8 import re from sys import argv from schema import Schema, SchemaError, And, Or, Optional, Regex def validate(schema, data, verbose_mode=False): try: print("\n\n") if verbose_mode: print(schema) print(data) schema.validate(data) print("pass") except SchemaError as e: print(e) def is_integer(value): return type(value) is int def is_float(value): return type(value) is float class Salary: def validate(self, value): if not is_integer(value) and not is_float(value): raise SchemaError("Salary has unexpected type {t}".format(t=type(value))) elif value <= 10000: raise SchemaError("Salary {s} is too low".format(s=value)) elif value >= 99999.9: raise SchemaError("Salary {s} is too high".format(s=value)) class Position: POSITIONS = ["QA", "DevOps", "Admin", "Docs", "HR"] def validate(self, value): if value not in Position.POSITIONS: raise SchemaError("Unknown position '{p}'".format(p=value)) class UniqueId: def __init__(self): self._ids = set() def validate(self, value): if value in self._ids: raise SchemaError("ID {id} is not unique".format(id=value)) self._ids.add(value) employee = Schema({"name": And(str, len, Regex("[A-Z][a-z]+")), "surname": And(str, len, Regex("[A-Z][a-z]+")), "id": UniqueId(), Optional("salary"): Salary(), Optional("position"): And(str, Position())}) verbose_mode = "-v" in argv validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 1, "salary": 15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 2, "salary": 15000, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 3, "salary": -15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 4, "salary": 1000000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 5, "salary": 15000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "id": 6, "salary": 100000000.0, "position": "QA"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 7, "salary": 45000.0, "position": ""}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 8, "salary": 45000.0, "position": "tovarnik"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 9, "salary": 25000.0}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 10, "position": "DevOps"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Wasserfall", "id": 11}, verbose_mode) validate(employee, {"id": 12}, verbose_mode) validate(employee, {"name": "eda", "surname": "Wasserfall", "id": 13, "salary": 45000.0, "position": "HR"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "wasserfall", "id": 14, "salary": 45000.0, "position": "HR"}, verbose_mode) validate(employee, {"name": "Eda", "surname": "Unique", "id": 1, "salary": 45000.0, "position": "HR"}, verbose_mode)
13. Výsledky běhu šestého demonstračního příkladu
Po spuštění příkladu by se na standardní výstup měly vypsat tyto informace. Povšimněte si, že nyní jsou chybové zprávy velmi dobře čitelné:
{'name': 'Eda', 'salary': 15000.0, 'id': 1, 'surname': 'Wasserfall', 'position': 'QA'} pass {'name': 'Eda', 'salary': 15000, 'id': 2, 'surname': 'Wasserfall', 'position': 'QA'} pass {'name': 'Eda', 'salary': -15000.0, 'id': 3, 'surname': 'Wasserfall', 'position': 'QA'} Key 'salary' error: Salary -15000.0 is too low {'name': 'Eda', 'salary': 1000000.0, 'id': 4, 'surname': 'Wasserfall', 'position': 'QA'} Key 'salary' error: Salary 1000000.0 is too high {'name': 'Eda', 'salary': 15000.0, 'id': 5, 'surname': 'Wasserfall', 'position': 'QA'} pass {'name': 'Eda', 'salary': 100000000.0, 'id': 6, 'position': 'QA'} Key 'salary' error: Salary 100000000.0 is too high {'name': 'Eda', 'salary': 45000.0, 'id': 7, 'surname': 'Wasserfall', 'position': ''} Key 'position' error: Unknown position '' {'name': 'Eda', 'salary': 45000.0, 'id': 8, 'surname': 'Wasserfall', 'position': 'tovarnik'} Key 'position' error: Unknown position 'tovarnik' {'name': 'Eda', 'salary': 25000.0, 'id': 9, 'surname': 'Wasserfall'} pass {'name': 'Eda', 'id': 10, 'surname': 'Wasserfall', 'position': 'DevOps'} pass {'name': 'Eda', 'id': 11, 'surname': 'Wasserfall'} pass {'id': 12} Missing keys: 'name', 'surname' {'name': 'eda', 'salary': 45000.0, 'id': 13, 'surname': 'Wasserfall', 'position': 'HR'} Key 'name' error: Regex('[A-Z][a-z]+') does not match 'eda' {'name': 'Eda', 'salary': 45000.0, 'id': 14, 'surname': 'wasserfall', 'position': 'HR'} Key 'surname' error: Regex('[A-Z][a-z]+') does not match 'wasserfall' {'name': 'Eda', 'salary': 45000.0, 'id': 1, 'surname': 'Unique', 'position': 'HR'} Key 'id' error: ID 1 is not unique
14. Knihovna Voluptuous
Poslední knihovnou určenou pro kontrolu struktury složitějších dat, s níž se v dnešním článku alespoň ve stručnosti seznámíme, je knihovna s dosti neobvyklým názvem Voluptuous, jejíž zdrojové kódy nalezneme na adrese https://github.com/alecthomas/voluptuous. Tato knihovna je postavena na podobných principech, s jakými jsme se již dříve setkali u knihoven Schemagic i Schema – samotná struktura dat je popsána přímo v Pythonu (jako klasická nativní pythonovská datová struktura, většinou seznam či slovník) a není tedy zapotřebí používat žádný doménově specifický jazyk (DSL – Domain Specific Language). I z tohoto důvodu bude základní popis této knihovny relativně stručný, protože si její vlastnosti a rozdíly ukážeme na několika demonstračních příkladech, které budou v mnoha ohledech podobné již popsaným příkladům.
Knihovnu Voluptuous (schválně, kdo dokáže to jméno opsat bez chyby?) si nejdříve nainstalujeme, a to konkrétně s využitím nástroje pip3 (nebo pip), protože tato knihovna je samozřejmě registrována i na PyPI (Python Package Index). Pro jednoduchost provedeme instalaci jen pro právě aktivního uživatele:
$ pip3 install --user voluptuous Collecting voluptuous Downloading voluptuous-0.11.1-py2.py3-none-any.whl Installing collected packages: voluptuous Successfully installed voluptuous-0.11.1
15. Ukázka základních možností knihovny Voluptuous v REPLu
Nyní si můžeme základní vlastnosti této knihovny otestovat v interaktivní smyčce REPL programovacího jazyka Python. REPL spustíme klasicky:
$ python3 Python 3.6.3 (default, Oct 9 2017, 12:11:29) [GCC 7.2.1 20170915 (Red Hat 7.2.1-2)] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
Naimportujeme objekt Schema z této knihovny:
>>> from voluptuous import Schema >>>
Nyní si již můžeme nadeklarovat jednoduché schéma:
user = Schema({"name": str, "surname": str, "id": int})
Předchozí volání konstruktoru vytvořilo objekt typu Schema, o čemž se můžeme přesvědčit velmi snadno:
>>> user <Schema({'name': <class 'str'>, 'surname': <class 'str'>, 'id': <class 'int'>}, extra=PREVENT_EXTRA, required=False) object at 0x7fa88cb93eb8>
Následuje ukázka použití schématu pro validaci (korektních) dat:
user({"name": "Eda", "surname": "Wasserfall", "id": 1}) {'name': 'Eda', 'surname': 'Wasserfall', 'id': 1}
Povšimněte si malé změny: zde se přímo volá user(data) (jakoby user byla běžná funkce a ne objekt) a nikoli user.validate(data), což je konvence, se kterou jsme se setkali v knihovně Schema. V knihovně Voluptuous je zkrácené volání implementováno díky překrytí metody __call__ vlastní implementací.
Příklad chybového výstupu pro nekorektní data bude vypadat následovně:
user({"name": 42, "id": "X"}) Traceback (most recent call last): File "<stdin>", line 2, in <module> File "/home/tester/.local/lib/python3.6/site-packages/voluptuous/schema_builder.py", line 267, in __call__ return self._compiled([], data) File "/home/tester/.local/lib/python3.6/site-packages/voluptuous/schema_builder.py", line 587, in validate_dict return base_validate(path, iteritems(data), out) File "/home/tester/.local/lib/python3.6/site-packages/voluptuous/schema_builder.py", line 425, in validate_mapping raise er.MultipleInvalid(errors) voluptuous.error.MultipleInvalid: expected str for dictionary value @ data['name'] >>>
16. První příklad – validace obsahu seznamů knihovnou Voluptuous
Ukažme si nyní demonstrační příklad se základním použitím této knihovny. Příklad obsahuje jediný soubor napsaný v Pythonu, podobně jako tomu bylo i ve všech předchozích demonstračních příkladech. V příkladu používáme již nám známou uživatelskou funkci validate:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from voluptuous import Schema from voluptuous import Invalid def validate(schema, data): try: print("\n\n") print(schema) print(data) schema(data) print("pass") except Exception as e: print(e) def pos(value): if type(value) is not int or value <= 0: raise Invalid("positive integer value expected, but got {v} instead".format(v=value)) 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)
U seznamů deklarujeme, jaké vlastnosti jsou očekávány u všech prvků. Mezi vlastnostmi se implicitně používá operace „or“:
# seznam čísel libovolného typu number_list = Schema([int, float, complex]) # seznam binárních číslic binary_numbers = Schema([0, 1])
Podívejme se nyní na výsledky běhu tohoto demonstračního příkladu. První část, tj. kontrola typů prvků seznamů dopadla následovně. Pouze poslední seznam obsahující řetězce byl vyhodnocen jako nekorektní (podle očekávání):
[<class 'int'>, <class 'float'>, <class 'complex'>] [1, 2, 3] pass [<class 'int'>, <class 'float'>, <class 'complex'>] [1.1, 2.2, 3.3] pass [<class 'int'>, <class 'float'>, <class 'complex'>] [(1+2j), (3+4j), 5j] pass [<class 'int'>, <class 'float'>, <class 'complex'>] ['1', '2', '3'] expected complex @ data[0]
Dále testujeme, jestli prvky seznamu tvoří řetězec binárních číslic, opět s očekávaným výsledkem:
[0, 1] [0, 0, 0] pass [0, 1] [1, 1, 0] pass [0, 1] [1, 2, 3] not a valid value @ data[1]
A nakonec test, zda jsou hodnoty celými kladnými čísly:
<function pos at 0x7f421daff0d0> 42 pass <function pos at 0x7f421daff0d0> 0 positive integer value expected, but got 0 instead <function pos at 0x7f421daff0d0> -1 positive integer value expected, but got -1 instead <function pos at 0x7f421daff0d0> 1.5 positive integer value expected, but got 1.5 instead
17. Tři varianty validace celých kladných čísel implementované ve třech knihovnách
Zajímavá je především definice funkce pos kontrolující, zda je jejím parametrem celé kladné číslo. Tato funkce musí v případě nekorektní hodnoty vyhodit výjimku typu Invalid (třídu s touto výjimkou jsme importovali na začátku). Je pro osvěžení si porovnejme implementaci této funkce pro všechny tři popsané knihovny:
Schemagic
def positive_integer(value): if type(value) is not int or value <= 0: raise TypeError("positive integer value expected, but got {v} instead".format(v=value))
V případě nevalidních dat se vyhazuje výjimka typu TypeError nebo ValueError.
Schema
def positive_integer(value): return type(value) is int and value > 0
Predikát, který u nevalidních dat vrací pravdivostní hodnotu False.
Voluptuous
def positive_integer(value): if type(value) is not int or value <= 0: raise Invalid("positive integer value expected, but got {v} instead".format(v=value))
V případě nevalidních dat se vyhazuje výjimka typu Invalid.
18. Druhý příklad – validace obsahu seznamů
Ve druhém příkladu založeném na knihovně Voluptuous si ukážeme nám již dobře známý problém – verifikaci, jestli předaný slovník obsahuje všechny očekávané klíče a zda jsou hodnoty na tyto klíče navázané očekávaného typu. Zápis validačních kritérií vypadá naprosto stejně, jako u předchozí popisované knihovny Schema:
user = Schema({"name": str, "surname": str, "id": pos})
Následně toto schéma použijeme pro validaci slovníků s různým obsahem, například:
validate(user, {"name": "Eda", "surname": "Wasserfall", "id": 1}) validate(user, {"name": "Eda", "id": 1})
Úplný kód tohoto demonstračního příkladu vypadá následovně:
#!/usr/bin/env python # vim: set fileencoding=utf-8 from voluptuous import Schema from voluptuous import Invalid def validate(schema, data): try: print("\n\n") print(schema) print(data) schema(data) print("pass") except Exception as e: print(e) def pos(value): if type(value) is not int or value <= 0: raise Invalid("positive integer value expected, but got {v} instead".format(v=value)) 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) 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})
Opět se podívejme na výstup vyprodukovaný tímto příkladem:
[<class 'int'>, <class 'float'>, <class 'complex'>] [1, 2, 3] pass [<class 'int'>, <class 'float'>, <class 'complex'>] [1.1, 2.2, 3.3] pass [<class 'int'>, <class 'float'>, <class 'complex'>] [(1+2j), (3+4j), 5j] pass [<class 'int'>, <class 'float'>, <class 'complex'>] ['1', '2', '3'] expected complex @ data[0] [0, 1] [0, 0, 0] pass [0, 1] [1, 1, 0] pass [0, 1] [1, 2, 3] not a valid value @ data[1] <function pos at 0x7f2fb79b50d0> 42 pass <function pos at 0x7f2fb79b50d0> 0 positive integer value expected, but got 0 instead <function pos at 0x7f2fb79b50d0> -1 positive integer value expected, but got -1 instead <function pos at 0x7f2fb79b50d0> 1.5 positive integer value expected, but got 1.5 instead {'name': <class 'str'>, 'surname': <class 'str'>, 'id': <function pos at 0x7f2fb79b50d0>} {'name': 'Eda', 'surname': 'Wasserfall', 'id': 1} pass {'name': <class 'str'>, 'surname': <class 'str'>, 'id': <function pos at 0x7f2fb79b50d0>} {'name': 'Eda', 'id': 1} pass {'name': <class 'str'>, 'surname': <class 'str'>, 'id': <function pos at 0x7f2fb79b50d0>} {'name': 'Eda', 'surname': 'Wasserfall', 'id': 0} positive integer value expected, but got 0 instead for dictionary value @ data['id']
19. Repositář s demonstračními příklady
Všechny demonstrační projekty, které jsme si v dnešním článku popsali, byly uloženy do Git 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 projekty rozdělené podle použité knihovny. Z tohoto důvodu zde naleznete i projekty zmíněné minule.
Schemagic
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
Projekt | Stručný popis | Cesta |
---|---|---|
schema-demo-1 | základní vlastnosti knihovny Schema | 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 |
schema-demo-3 | validace slovníků a dalších typů | https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-3 |
schema-demo-4 | validace slovníků a dalších typů | https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-4 |
schema-demo-5 | validace slovníků a dalších typů | https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-5 |
schema-demo-6 | validace slovníků a dalších typů | https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-6 |
Voluptuous
Projekt | Stručný popis | Cesta |
---|---|---|
voluptuous-demo-1 | základní vlastnosti knihovny Voluptuous | https://github.com/tisnik/python-schema-checks/tree/master/voluptuous-demo-1 |
voluptuous-demo-2 | validace obsahu slovníků | https://github.com/tisnik/python-schema-checks/tree/master/voluptuous-demo-2 |
20. Odkazy na Internetu
- 7 Best Python Libraries for Validating Data
https://www.yeahhub.com/7-best-python-libraries-validating-data/ - voluptuous na (na PyPi)
https://pypi.python.org/pypi/voluptuous - voluptuous (na GitHubu)
https://github.com/alecthomas/voluptuous - 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