├── VERSION ├── PROJECT_NAME ├── strictyaml ├── ruamel │ ├── py.typed │ ├── configobjwalker.py │ ├── anchor.py │ ├── scalarbool.py │ ├── timestamp.py │ ├── __init__.py │ └── loader.py ├── constants.py ├── __init__.py ├── any_validator.py ├── dumper.py └── exceptions.py ├── .gitattributes ├── hitch ├── hitchreqs.in ├── hitchreqs.txt ├── debugrequirements.txt ├── printstats.py ├── story │ ├── strictyaml.story │ ├── bugs.story │ ├── handle-exceptions.story │ ├── dirty-load.story │ ├── email-url.story │ ├── merge-documents.story │ ├── non-schema-validation.story │ ├── scalar-string.story │ ├── scalar-hexadecimal-integer.story │ ├── single-value.story │ ├── validator-repr.story │ ├── duplicatekeys.story │ ├── enum-with-item-validation.story │ ├── build-yaml-document-from-scratch.story │ ├── unique-sequence.story │ ├── revalidation.story │ ├── scalar-integer.story │ ├── fixed-sequence.story │ ├── decimal.story │ ├── mapping-representation.story │ ├── regexp.story │ ├── optional-with-defaults.story │ ├── nested-map.story │ ├── or.story │ ├── whatline.story │ ├── empty.story │ └── enum.story ├── Dockerfile-hitch ├── self-env-test.sh ├── self_env_test.py └── envirotest.py ├── setup.py ├── docs ├── public │ ├── using │ │ ├── alpha │ │ │ ├── index.md │ │ │ ├── restrictions │ │ │ │ ├── index.md │ │ │ │ ├── loading-dirty-yaml.md │ │ │ │ └── duplicate-keys.md │ │ │ ├── howto │ │ │ │ ├── index.md │ │ │ │ ├── merge-yaml-documents.md │ │ │ │ ├── label-exceptions.md │ │ │ │ ├── without-a-schema.md │ │ │ │ ├── build-yaml-document.md │ │ │ │ ├── revalidation.md │ │ │ │ ├── what-line.md │ │ │ │ └── either-or-validation.md │ │ │ ├── scalar │ │ │ │ ├── index.md │ │ │ │ ├── hexadecimal-integer.md │ │ │ │ ├── email-and-url.md │ │ │ │ ├── string.md │ │ │ │ ├── integer.md │ │ │ │ ├── regular-expressions.md │ │ │ │ ├── decimal.md │ │ │ │ └── enum.md │ │ │ └── compound │ │ │ │ ├── index.md │ │ │ │ ├── mapping-with-slug-keys.md │ │ │ │ ├── optional-keys-with-defaults.md │ │ │ │ ├── sequences-of-unique-items.md │ │ │ │ ├── optional-keys.md │ │ │ │ ├── fixed-length-sequences.md │ │ │ │ ├── mapping-yaml-object.md │ │ │ │ └── update.md │ │ └── index.md │ ├── why-not │ │ ├── toml-indentation-1.png │ │ ├── ini.md │ │ ├── hocon.md │ │ ├── json.md │ │ ├── json5.md │ │ ├── pykwalify.md │ │ ├── index.md │ │ ├── sdlang.md │ │ ├── xml.md │ │ ├── ordinary-yaml.md │ │ ├── hjson.md │ │ ├── json-schema.md │ │ ├── python-schema.md │ │ └── turing-complete-code.md │ ├── redirects.yml │ ├── when-to-use-validators.md │ ├── why │ │ ├── duplicate-keys-disallowed.md │ │ ├── explicit-tags-removed.md │ │ ├── only-parse-strings-not-files.md │ │ ├── not-parse-direct-representations-of-python-objects.md │ │ ├── flow-style-removed.md │ │ ├── index.md │ │ ├── turing-complete-schema.md │ │ └── speed-not-a-priority.md │ ├── what-is-yaml.md │ ├── comparison │ │ └── table.yml │ └── features-removed.md └── src │ ├── using │ ├── alpha │ │ ├── index.md │ │ ├── howto │ │ │ └── index.md │ │ ├── restrictions │ │ │ └── index.md │ │ ├── scalar │ │ │ └── index.md │ │ └── compound │ │ │ └── index.md │ └── index.md │ ├── why-not │ ├── toml-indentation-1.png │ ├── index.md │ ├── ini.md │ ├── hocon.md │ ├── json5.md │ ├── json.md │ ├── pykwalify.md │ ├── sdlang.md │ ├── xml.md │ ├── ordinary-yaml.md │ ├── hjson.md │ ├── json-schema.md │ ├── python-schema.md │ └── turing-complete-code.md │ ├── redirects.yml │ ├── dirtemplate.yml │ ├── why │ ├── index.md │ ├── duplicate-keys-disallowed.md │ ├── explicit-tags-removed.md │ ├── only-parse-strings-not-files.md │ ├── not-parse-direct-representations-of-python-objects.md │ ├── flow-style-removed.md │ ├── turing-complete-schema.md │ └── speed-not-a-priority.md │ ├── when-to-use-validators.md │ ├── template │ ├── step.jinja2 │ └── story.jinja2 │ ├── what-is-yaml.md │ ├── comparison │ └── table.yml │ └── features-removed.md ├── .github ├── ISSUE_TEMPLATE.md ├── workflows │ ├── regression.yml │ └── envirotest.yml └── PULL_REQUEST_TEMPLATE.md ├── MANIFEST.in ├── .gitignore ├── LICENSE.txt └── pyproject.toml /VERSION: -------------------------------------------------------------------------------- 1 | 1.7.3 2 | -------------------------------------------------------------------------------- /PROJECT_NAME: -------------------------------------------------------------------------------- 1 | strictyaml 2 | -------------------------------------------------------------------------------- /strictyaml/ruamel/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.story linguist-language=YAML 2 | -------------------------------------------------------------------------------- /hitch/hitchreqs.in: -------------------------------------------------------------------------------- 1 | hitchpylibrarytoolkit==0.6.17 2 | -------------------------------------------------------------------------------- /hitch/hitchreqs.txt: -------------------------------------------------------------------------------- 1 | hitchpylibrarytoolkit==0.6.17 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /docs/public/using/alpha/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using StrictYAML v0.x.x 3 | --- -------------------------------------------------------------------------------- /docs/src/using/alpha/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using StrictYAML v0.x.x 3 | --- 4 | -------------------------------------------------------------------------------- /docs/src/why-not/toml-indentation-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crdoconnor/strictyaml/HEAD/docs/src/why-not/toml-indentation-1.png -------------------------------------------------------------------------------- /docs/public/why-not/toml-indentation-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crdoconnor/strictyaml/HEAD/docs/public/why-not/toml-indentation-1.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Feel free to raise an issue for any of the following: 2 | 3 | * Support request 4 | * Feature request 5 | * Argument 6 | * Bug 7 | 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include VERSION LICENSE.txt README.md CHANGELOG.md 2 | recursive-include strictyaml/ 3 | global-exclude __pycache__ 4 | global-exclude *.py[co] 5 | -------------------------------------------------------------------------------- /hitch/debugrequirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | ipython #==1.2.1 3 | pyzmq 4 | path.py 5 | q 6 | ipykernel 7 | sure 8 | ensure 9 | python-slugify 10 | pytest 11 | -------------------------------------------------------------------------------- /hitch/printstats.py: -------------------------------------------------------------------------------- 1 | import pstats 2 | import sys 3 | import os 4 | 5 | if os.path.exists(sys.argv[1]): 6 | p = pstats.Stats(sys.argv[1]) 7 | p.sort_stats("cumulative") 8 | p.print_stats() 9 | -------------------------------------------------------------------------------- /docs/public/using/alpha/restrictions/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Restrictions 3 | --- 4 | 5 | - [Disallowed YAML](disallowed-yaml) 6 | - [Duplicate keys](duplicate-keys) 7 | - [Dirty load](loading-dirty-yaml) 8 | -------------------------------------------------------------------------------- /hitch/story/strictyaml.story: -------------------------------------------------------------------------------- 1 | strictyaml: 2 | given: 3 | python version: (( python version )) 4 | ruamel version: (( ruamel version )) 5 | with: 6 | python version: 2.7.14 7 | ruamel version: 0.17.5 8 | -------------------------------------------------------------------------------- /docs/src/using/alpha/howto/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to... 3 | --- 4 | 5 | {% for dirfile in (thisdir.is_not_dir() - thisdir.named("index.md"))|sort() -%} 6 | - [{{ title(dirfile) }}]({{ dirfile.name.splitext()[0] }}) 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /strictyaml/constants.py: -------------------------------------------------------------------------------- 1 | TRUE_VALUES = ["yes", "true", "on", "1", "y"] 2 | 3 | FALSE_VALUES = ["no", "false", "off", "0", "n"] 4 | 5 | BOOL_VALUES = TRUE_VALUES + FALSE_VALUES 6 | 7 | REGEXES = { 8 | "email": r".+?\@.+?", 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/using/alpha/restrictions/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Restrictions 3 | --- 4 | 5 | {% for dirfile in (thisdir.is_not_dir() - thisdir.named("index.md"))|sort() -%} 6 | - [{{ title(dirfile) }}]({{ dirfile.name.splitext()[0] }}) 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /docs/src/using/alpha/scalar/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scalar StrictYAML Validators 3 | --- 4 | 5 | {% for dirfile in (thisdir.is_not_dir() - thisdir.named("index.md"))|sort() -%} 6 | - [{{ title(dirfile) }}]({{ dirfile.name.splitext()[0] }}) 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | .cache 4 | strictyaml.egg-info/ 5 | tests/.hitch 6 | state/ 7 | __pycache__ 8 | *.pyc 9 | *.kate-swp 10 | hitch/gen 11 | hitch/personalsettings.yml 12 | temp/ 13 | .vscode/ 14 | hitch/devenv.yml 15 | hitchpylibrarytoolkit 16 | -------------------------------------------------------------------------------- /docs/src/using/alpha/compound/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Compound StrictYAML Validators 3 | --- 4 | 5 | {% for dirfile in (thisdir.is_not_dir() - thisdir.named("index.md"))|sort() -%} 6 | - [{{ title(dirfile) }}]({{ dirfile.name.splitext()[0] }}) 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /docs/public/redirects.yml: -------------------------------------------------------------------------------- 1 | /why-not/syntax-typing: /why/syntax-typing-bad 2 | /new-faq: / 3 | /using/alpha/compound/either-or-validation: /using/alpha/howto/either-or-validation/ 4 | /why/care-about-yaml: /why 5 | /why/binary-data-removed: /why/not-parse-direct-representations-of-python-objects 6 | -------------------------------------------------------------------------------- /docs/src/redirects.yml: -------------------------------------------------------------------------------- 1 | /why-not/syntax-typing: /why/syntax-typing-bad 2 | /new-faq: / 3 | /using/alpha/compound/either-or-validation: /using/alpha/howto/either-or-validation/ 4 | /why/care-about-yaml: /why 5 | /why/binary-data-removed: /why/not-parse-direct-representations-of-python-objects 6 | -------------------------------------------------------------------------------- /strictyaml/ruamel/configobjwalker.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import warnings 4 | 5 | from strictyaml.ruamel.util import configobj_walker as new_configobj_walker 6 | 7 | if False: # MYPY 8 | from typing import Any # NOQA 9 | 10 | 11 | def configobj_walker(cfg): 12 | # type: (Any) -> Any 13 | warnings.warn( 14 | "configobj_walker has moved to strictyaml.ruamel.util, please update your code" 15 | ) 16 | return new_configobj_walker(cfg) 17 | -------------------------------------------------------------------------------- /docs/src/why-not/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not X? 3 | --- 4 | 5 | There are a number of formats and approaches that can achieve more or 6 | less the same purpose as StrictYAML. Below is a series of comparisons 7 | with some of the more famous ones: 8 | 9 | {% for dirfile in (thisdir.ext("md") - thisdir.named("index.md"))|sort() -%} 10 | - [{{ title(dirfile) }}]({{ dirfile.basename().splitext()[0] }}) 11 | {% endfor %} 12 | 13 | If you'd like to write or link to a rebuttal to any argument raised 14 | here, feel free to raise a ticket. 15 | -------------------------------------------------------------------------------- /docs/public/using/alpha/howto/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to... 3 | --- 4 | 5 | - [Build a YAML document from scratch in code](build-yaml-document) 6 | - [Either/or schema validation of different, equally valid different kinds of YAML](either-or-validation) 7 | - [Labeling exceptions](label-exceptions) 8 | - [Merge YAML documents](merge-yaml-documents) 9 | - [Revalidate an already validated document](revalidation) 10 | - [Reading in YAML, editing it and writing it back out](roundtripping) 11 | - [Get line numbers of YAML elements](what-line) 12 | - [Parsing YAML without a schema](without-a-schema) 13 | -------------------------------------------------------------------------------- /strictyaml/ruamel/anchor.py: -------------------------------------------------------------------------------- 1 | if False: # MYPY 2 | from typing import Any, Dict, Optional, List, Union, Optional, Iterator # NOQA 3 | 4 | anchor_attrib = "_yaml_anchor" 5 | 6 | 7 | class Anchor(object): 8 | __slots__ = "value", "always_dump" 9 | attrib = anchor_attrib 10 | 11 | def __init__(self): 12 | # type: () -> None 13 | self.value = None 14 | self.always_dump = False 15 | 16 | def __repr__(self): 17 | # type: () -> Any 18 | ad = ", (always dump)" if self.always_dump else "" 19 | return "Anchor({!r}{})".format(self.value, ad) 20 | -------------------------------------------------------------------------------- /hitch/Dockerfile-hitch: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | ENV LC_ALL=C.UTF-8 5 | ENV LANG=C.UTF-8 6 | 7 | RUN apt-get update && apt-get install \ 8 | libuv1-dev libncurses5-dev libncursesw5-dev xz-utils \ 9 | liblzma-dev tk-dev build-essential \ 10 | libreadline-dev libffi-dev libsqlite3-dev \ 11 | zlib1g-dev zlib1g libbz2-dev libssl-dev \ 12 | curl git make llvm wget \ 13 | python3-virtualenv virtualenv python3.8-venv \ 14 | python3-dev python-openssl -y \ 15 | && apt-get clean && rm -rf /var/lib/apt/lists/* 16 | 17 | RUN mkdir /src 18 | WORKDIR /src 19 | -------------------------------------------------------------------------------- /docs/src/dirtemplate.yml: -------------------------------------------------------------------------------- 1 | base templates: template 2 | templated: 3 | - template/story.jinja2: 4 | content: yes 5 | filename: yes 6 | - why/index.md: 7 | content: yes 8 | - why-not/index.md: 9 | content: yes 10 | - using/alpha/compound/index.md: 11 | content: yes 12 | - using/alpha/howto/index.md: 13 | content: yes 14 | - using/alpha/restrictions/index.md: 15 | content: yes 16 | - using/alpha/scalar/index.md: 17 | content: yes 18 | - using/index.md: 19 | content: yes 20 | - using/alpha/index.md: 21 | content: yes 22 | - using/alpha/index.md: 23 | content: yes 24 | - index.md: 25 | content: yes 26 | -------------------------------------------------------------------------------- /docs/public/using/alpha/scalar/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scalar StrictYAML Validators 3 | --- 4 | 5 | - [Boolean (Bool)](boolean) 6 | - [Parsing comma separated items (CommaSeparated)](comma-separated) 7 | - [Datetimes (Datetime)](datetime) 8 | - [Decimal numbers (Decimal)](decimal) 9 | - [Email and URL validators](email-and-url) 10 | - [Empty key validation](empty) 11 | - [Enumerated scalars (Enum)](enum) 12 | - [Floating point numbers (Float)](float) 13 | - [Hexadecimal Integers (HexInt)](hexadecimal-integer) 14 | - [Integers (Int)](integer) 15 | - [Validating strings with regexes (Regex)](regular-expressions) 16 | - [Parsing strings (Str)](string) 17 | -------------------------------------------------------------------------------- /hitch/self-env-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # This is for running the tests in a self managed environment 4 | # It is intended only for software packagers 5 | # It assumes strictyaml is already installed 6 | 7 | THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) 8 | 9 | VIRTUALENV_BIN="${VIRTUALENV_BIN:-virtualenv}" 10 | PYTHON_BIN="${PYTHON_BIN:-python}" 11 | 12 | rm -rf /tmp/testvenv/ 13 | rm -rf /tmp/testgen/ 14 | mkdir /tmp/testgen/ 15 | $VIRTUALENV_BIN /tmp/testvenv 16 | /tmp/testvenv/bin/pip install -r $THIS_DIR/hitchreqs.txt 17 | 18 | cd $THIS_DIR/ 19 | /tmp/testvenv/bin/python $THIS_DIR/self_env_test.py $PYTHON_BIN 20 | -------------------------------------------------------------------------------- /hitch/self_env_test.py: -------------------------------------------------------------------------------- 1 | from hitchstory import StoryCollection 2 | from engine import Engine 3 | from pathquery import pathquery 4 | from path import Path 5 | import sys 6 | 7 | PROJECT_DIR = Path(__file__).parent.parent 8 | 9 | class Directories: 10 | gen = Path("/tmp/testgen") 11 | key = PROJECT_DIR / "hitch" 12 | project = PROJECT_DIR 13 | 14 | DIR = Directories() 15 | 16 | result = StoryCollection( 17 | pathquery(DIR.key / "story").ext("story"), 18 | Engine(DIR, python_path=sys.argv[1]) 19 | ).only_uninherited().ordered_by_name().play() 20 | 21 | 22 | if not result.all_passed: 23 | print("FAILURE") 24 | sys.exit(1) 25 | -------------------------------------------------------------------------------- /.github/workflows/regression.yml: -------------------------------------------------------------------------------- 1 | name: Regression 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | regression: 12 | timeout-minutes: 30 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout repo 16 | uses: actions/checkout@v2 17 | 18 | - name: build 19 | run: | 20 | mkdir -p ~/.ssh/ 21 | touch ~/.ssh/id_rsa 22 | touch ~/.ssh/id_rsa.pub 23 | echo test | podman secret create pypitoken - 24 | ./key.sh make 25 | 26 | - name: regression 27 | run: | 28 | ./key.sh regression 29 | -------------------------------------------------------------------------------- /.github/workflows/envirotest.yml: -------------------------------------------------------------------------------- 1 | name: Envirotest 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # Run every friday evening, to catch before weekend 7 | - cron: '0 17 * * Fri' 8 | 9 | jobs: 10 | envirotest: 11 | timeout-minutes: 30 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: build 18 | run: | 19 | mkdir -p ~/.ssh/ 20 | touch ~/.ssh/id_rsa 21 | touch ~/.ssh/id_rsa.pub 22 | echo test | podman secret create pypitoken - 23 | ./key.sh make 24 | 25 | - name: envirotest 26 | run: | 27 | ./key.sh envirotest full 28 | -------------------------------------------------------------------------------- /docs/src/why/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Design Justifications 3 | --- 4 | 5 | StrictYAML is the result of some carefully considered, although 6 | controversial design decisions. These are justified here. 7 | 8 | {% for dirfile in (thisdir.is_not_dir() - thisdir.named("index.md"))|sort() -%} 9 | - [{{ title(dirfile) }}]({{ dirfile.name.splitext()[0] }}) 10 | {% endfor %} 11 | 12 | If you have seen a relevant counterargument to you'd like to link 13 | to addressed at StrictYAML, please create a pull request and 14 | link to it in the relevant document. 15 | 16 | If you'd like to write your own rebuttal to any argument raised 17 | here, raise a ticket and issue a pull request linking to it at 18 | the end of the document. 19 | -------------------------------------------------------------------------------- /docs/src/why-not/ini.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use INI files? 3 | --- 4 | 5 | INI is a very old and quite readable configuration format for small configuration files. 6 | It is still used by many programs today and it has some advantages due to this - e.g. 7 | python has inbuilt parser for it. 8 | 9 | Unfortunately it suffers from two major problems: 10 | 11 | - Different parsers will operate in subtly different ways that can lead to often obscure edge case bugs regarding the way whitespace is used, case sensitivity, comments and escape characters. 12 | - It doesn't let you represent hierarchical data. 13 | 14 | [TOML](../toml) is a configuration format designed to address these two concerns, 15 | although it also suffers from obscure edge case bugs. 16 | -------------------------------------------------------------------------------- /docs/public/when-to-use-validators.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: When should I use a validator and when should I not? 3 | --- 4 | 5 | When starting out on greenfield projects it's much quicker not to create a validator. In such cases it's often more prudent to just parse the YAML and convert the strings explicitly on the fly (e.g. int(yaml['key'])). 6 | 7 | If the YAML is also going to be largely under the control of the developer it also might not make sense to write a validator either. 8 | 9 | If you have written software that is going to parse YAML from a source you do *not* control - especially by somebody who might make a mistake - then it probably does make sense to write a validator. 10 | 11 | You can start off without using a validator and then add one later. 12 | -------------------------------------------------------------------------------- /docs/public/why-not/ini.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use INI files? 3 | --- 4 | 5 | INI is a very old and quite readable configuration format for small configuration files. 6 | It is still used by many programs today and it has some advantages due to this - e.g. 7 | python has inbuilt parser for it. 8 | 9 | Unfortunately it suffers from two major problems: 10 | 11 | - Different parsers will operate in subtly different ways that can lead to often obscure edge case bugs regarding the way whitespace is used, case sensitivity, comments and escape characters. 12 | - It doesn't let you represent hierarchical data. 13 | 14 | [TOML](../toml) is a configuration format designed to address these two concerns, 15 | although it also suffers from obscure edge case bugs. 16 | -------------------------------------------------------------------------------- /docs/src/when-to-use-validators.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: When should I use a validator and when should I not? 3 | --- 4 | 5 | When starting out on greenfield projects it's much quicker not to create a validator. In such cases it's often more prudent to just parse the YAML and convert the strings explicitly on the fly (e.g. int(yaml['key'])). 6 | 7 | If the YAML is also going to be largely under the control of the developer it also might not make sense to write a validator either. 8 | 9 | If you have written software that is going to parse YAML from a source you do *not* control - especially by somebody who might make a mistake - then it probably does make sense to write a validator. 10 | 11 | You can start off without using a validator and then add one later. 12 | -------------------------------------------------------------------------------- /docs/src/why/duplicate-keys-disallowed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is wrong with duplicate keys? 3 | --- 4 | 5 | Duplicate keys are allowed in regular YAML - as parsed by pyyaml, ruamel.yaml and poyo: 6 | 7 | ```yaml 8 | x: cow 9 | y: dog 10 | x: bull 11 | ``` 12 | 13 | Not only is it unclear whether x should be "cow" or "bull" (the parser will decide 'bull', but did you know that?), 14 | if there are 200 lines between x: cow and x: bull, a user might very likely change the *first* x and erroneously believe that the resulting value of x has been changed - when it hasn't. 15 | 16 | In order to avoid all possible confusion, StrictYAML will simply refuse to parse this and will *only* accept associative arrays where all of the keys are unique. It will throw a DuplicateKeysDisallowed exception. 17 | -------------------------------------------------------------------------------- /docs/public/why/duplicate-keys-disallowed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is wrong with duplicate keys? 3 | --- 4 | 5 | Duplicate keys are allowed in regular YAML - as parsed by pyyaml, ruamel.yaml and poyo: 6 | 7 | ```yaml 8 | x: cow 9 | y: dog 10 | x: bull 11 | ``` 12 | 13 | Not only is it unclear whether x should be "cow" or "bull" (the parser will decide 'bull', but did you know that?), 14 | if there are 200 lines between x: cow and x: bull, a user might very likely change the *first* x and erroneously believe that the resulting value of x has been changed - when it hasn't. 15 | 16 | In order to avoid all possible confusion, StrictYAML will simply refuse to parse this and will *only* accept associative arrays where all of the keys are unique. It will throw a DuplicateKeysDisallowed exception. 17 | -------------------------------------------------------------------------------- /docs/src/template/step.jinja2: -------------------------------------------------------------------------------- 1 | {% if step['in_interpreter'] %} 2 | ```python 3 | {% for line in step['code'].rstrip('\n').split('\n') %}>>> {{ line }} 4 | {% endfor -%} 5 | {{ step['will_output'] }} 6 | ``` 7 | {% else %} 8 | ```python 9 | {{ step['code'] }} 10 | ``` 11 | {% if 'will_output' in step %} 12 | ```yaml 13 | {{ step['will_output'] }} 14 | ``` 15 | {% endif %} 16 | {% if 'raises' in step %} 17 | ```python 18 | {% if 'in python 3' in step['raises']['type'] -%} 19 | {{ step['raises']['type']['in python 3'] }}: 20 | {%- else %}{{ step['raises']['type'] }}:{% endif -%} 21 | {%- if 'in python 3' in step['raises']['message'] -%} 22 | {{ step['raises']['message']['in python 3'] }}: 23 | ``` 24 | {% else %} 25 | {{ step['raises']['message'] }} 26 | ``` 27 | {% endif %} 28 | {% endif %} 29 | {% endif %} 30 | -------------------------------------------------------------------------------- /docs/public/using/alpha/compound/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Compound StrictYAML Validators 3 | --- 4 | 5 | - [Fixed length sequences (FixedSeq)](fixed-length-sequences) 6 | - [Mappings combining defined and undefined keys (MapCombined)](map-combined) 7 | - [Mappings with arbitrary key names (MapPattern)](map-pattern) 8 | - [Mapping with defined keys and a custom key validator (Map)](mapping-with-slug-keys) 9 | - [Using a YAML object of a parsed mapping](mapping-yaml-object) 10 | - [Mappings with defined keys (Map)](mapping) 11 | - [Optional keys with defaults (Map/Optional)](optional-keys-with-defaults) 12 | - [Validating optional keys in mappings (Map)](optional-keys) 13 | - [Sequences of unique items (UniqueSeq)](sequences-of-unique-items) 14 | - [Sequence/list validator (Seq)](sequences) 15 | - [Updating document with a schema](update) 16 | -------------------------------------------------------------------------------- /hitch/story/bugs.story: -------------------------------------------------------------------------------- 1 | Revalidation with an or breaks lookup: 2 | based on: strictyaml 3 | given: 4 | yaml_snippet: | 5 | x: 6 | a: b 7 | setup: | 8 | from strictyaml import load, Any, Int, Str, Map 9 | loose_schema = Map({"x": Any()}) 10 | strict_schema = Str() | Map({"a": Str()}) 11 | steps: 12 | - run: | 13 | parsed = load(yaml_snippet, loose_schema) 14 | parsed['x'].revalidate(strict_schema) 15 | parsed['x']['a'] = "x" 16 | assert parsed['x']['a'] == "x" 17 | 18 | Parsing string with data shouldn't resolve to ruamel data structures: 19 | based on: strictyaml 20 | given: 21 | yaml_snippet: 'x: |\n x' 22 | setup: | 23 | from strictyaml import load 24 | from strictyaml.ruamel.scalarstring import ScalarString 25 | steps: 26 | - run: | 27 | assert not isinstance(load("- >\n hello").data[0], ScalarString) 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Qualities of an ideal pull request: 2 | 3 | Code changes come with a new story or an amendment to an existing story 4 | in order to exercise the code. 5 | 6 | Commit messages that adhere to the following pattern: 7 | 8 | TYPEOFCOMMIT : Quick explanation of what changed and why. 9 | 10 | Where TYPEOFCOMMIT is one of the following: 11 | 12 | * FEATURE - for new features and accompanying stories. 13 | * BUG - for bugfixes and accompanying stories (or amendments to an existing story). 14 | * DOCS - for changes to README, etc. and non-functional changes to stories. 15 | * MISC - Anything else. 16 | 17 | Ideal code qualities: 18 | 19 | * Loosely coupled 20 | * DRY 21 | * Clear and straightforward is preferable to clever 22 | * Docstrings that explain why rather than what 23 | * Clearly disambiguated Variable/method/class names 24 | * Passes flake8 linting with < 100 character lines 25 | -------------------------------------------------------------------------------- /docs/public/why-not/hocon.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not HOCON? 3 | --- 4 | 5 | [HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md) is another "redesigned" JSON, ironically enough, taking JSON and making it even more complicated. 6 | 7 | Along with JSON's [syntax typing](../../why/syntax-typing-bad) - a downside of most non-YAML alternatives, HOCON makes the following mistakes in its design: 8 | 9 | - It does not fail loudly on duplicate keys. 10 | - It has a confusing rules for deciding on concatenations and substitutions. 11 | - It has a mechanism for substitutions similar to [YAML's node anchor feature](../why/node-anchors-and-references-removed.md) - which, unless used extremely sparingly, can create confusing markup that, ironically, is *not* human optimized. 12 | 13 | In addition, its attempt at using "less pedantic" syntax creates a system of rules which makes the behavior of the parser much less obvious and edge cases more frequent. 14 | -------------------------------------------------------------------------------- /docs/src/why-not/hocon.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not HOCON? 3 | --- 4 | 5 | [HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md) is another "redesigned" JSON, ironically enough, taking JSON and making it even more complicated. 6 | 7 | Along with JSON's [syntax typing](../../why/syntax-typing-bad) - a downside of most non-YAML alternatives, HOCON makes the following mistakes in its design: 8 | 9 | - It does not fail loudly on duplicate keys. 10 | - It has a confusing rules for deciding on concatenations and substitutions. 11 | - It has a mechanism for substitutions similar to [YAML's node anchor feature](../why/node-anchors-and-references-removed.md) - which, unless used extremely sparingly, can create confusing markup that, ironically, is *not* human optimized. 12 | 13 | In addition, its attempt at using "less pedantic" syntax creates a system of rules which makes the behavior of the parser much less obvious and edge cases more frequent. 14 | -------------------------------------------------------------------------------- /docs/src/why-not/json5.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not JSON5? 3 | --- 4 | 5 | [JSON5](http://json5.org/) is also a proposed extension to JSON to make it more readable. 6 | 7 | Its main criticism of YAML is:: 8 | 9 | There are other formats that are human-friendlier, like YAML, but changing from JSON to a completely different format is undesirable in many cases. 10 | 11 | This is, I believe, mistaken. It is better if a language is not subtly different if you are going to use it as such. Subtle differences invite mistakes brought on by confusion. 12 | 13 | JSON5 looks like a hybrid of YAML and JSON:: 14 | 15 | ```json 16 | { 17 | foo: 'bar', 18 | while: true, 19 | } 20 | ``` 21 | 22 | It has weaknesses similar to TOML: 23 | 24 | - The noisiness of the delimiters that supplant significant whitespace make it less readable and editable. 25 | - The use of [syntax typing](../../why/syntax-typing-bad) is neither necessary, nor an aid to stricter typing if you have a schema. 26 | -------------------------------------------------------------------------------- /docs/public/why-not/json.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not JSON for simple configuration files? 3 | --- 4 | 5 | JSON is an *ideal* format for REST APIs and other forms of data intended for machine exchange and it probably always will be because: 6 | 7 | - It's a simple spec. 8 | - It has all the basic types which map on to all programming languages - number, string, list, mapping, boolean *and no more*. 9 | - Its syntax contains a built in level of error detection - cut a JSON request in half and it is no longer still valid, eliminating an entire class of obscure and problematic bugs. 10 | - If pretty-printed correctly, it's more or less readable - for the purposes of debugging, anyway. 11 | 12 | However, while it is eminently suitable for REST APIs it is less suitable for configuration since: 13 | 14 | - The same syntax which gives it decent error detection (commas, curly brackets) makes it tricky for humans to edit. 15 | - It's not especially readable. 16 | - It doesn't allow comments. 17 | -------------------------------------------------------------------------------- /docs/public/why-not/json5.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not JSON5? 3 | --- 4 | 5 | [JSON5](http://json5.org/) is also a proposed extension to JSON to make it more readable. 6 | 7 | Its main criticism of YAML is:: 8 | 9 | There are other formats that are human-friendlier, like YAML, but changing from JSON to a completely different format is undesirable in many cases. 10 | 11 | This is, I believe, mistaken. It is better if a language is not subtly different if you are going to use it as such. Subtle differences invite mistakes brought on by confusion. 12 | 13 | JSON5 looks like a hybrid of YAML and JSON:: 14 | 15 | ```json 16 | { 17 | foo: 'bar', 18 | while: true, 19 | } 20 | ``` 21 | 22 | It has weaknesses similar to TOML: 23 | 24 | - The noisiness of the delimiters that supplant significant whitespace make it less readable and editable. 25 | - The use of [syntax typing](../../why/syntax-typing-bad) is neither necessary, nor an aid to stricter typing if you have a schema. 26 | -------------------------------------------------------------------------------- /docs/src/why-not/json.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not JSON for simple configuration files? 3 | --- 4 | 5 | JSON is an *ideal* format for REST APIs and other forms of data intended for machine exchange and it probably always will be because: 6 | 7 | - It's a simple spec. 8 | - It has all the basic types which map on to all programming languages - number, string, list, mapping, boolean *and no more*. 9 | - Its syntax contains a built in level of error detection - cut a JSON request in half and it is no longer still valid, eliminating an entire class of obscure and problematic bugs. 10 | - If pretty-printed correctly, it's more or less readable - for the purposes of debugging, anyway. 11 | 12 | However, while it is eminently suitable for REST APIs it is less suitable for configuration since: 13 | 14 | - The same syntax which gives it decent error detection (commas, curly brackets) makes it tricky for humans to edit. 15 | - It's not especially readable. 16 | - It doesn't allow comments. 17 | -------------------------------------------------------------------------------- /docs/src/why/explicit-tags-removed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is wrong with explicit tags? 3 | --- 4 | 5 | Explicit tags are tags that have an explicit type attached that is used to determine what type to convert the data to when it is parsed. 6 | 7 | For example, if it were to be applied to "fix" the Godfather movie script parsing issue described above, it would look like this: 8 | 9 | ```yaml 10 | - Don Corleone: Do you have faith in my judgment? 11 | - Clemenza: !!str Yes 12 | - Don Corleone: Do I have your loyalty? 13 | ``` 14 | 15 | Explicit typecasts in YAML markup are slightly confusing for non-programmers, much like the concept of 'types' in general. StrictYAML's philosophy is that types should be kept strictly separated from data, so this 'feature' of YAML is switched off. 16 | 17 | If tags are seen in a YAML file it will raise a special TagTokenDisallowed exception. 18 | 19 | 20 | ## Counterpoints 21 | 22 | - [Valid usage in AWS cloudformation syntax?](https://github.com/crdoconnor/strictyaml/issues/37) 23 | -------------------------------------------------------------------------------- /docs/public/why-not/pykwalify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use kwalify with standard YAML to validate my YAML? 3 | --- 4 | 5 | Kwalify is a schema validation language that is written *in* YAML. 6 | 7 | It is a descriptive schema language suitable for validating simple YAML. 8 | 9 | Kwalify compiles to the strictyaml equivalent but is able to do less. You cannot, for example: 10 | 11 | - Plug generated lists that come from outside of the spec (e.g. a list of country code from pycountry). 12 | - Validate parts of the schema which can be either one thing *or* another - e.g. a list *or* a single string. 13 | - Plug sub-validators of a document into larger validators. 14 | 15 | If your schema is very simple and small, there is no point to using kwalify. 16 | 17 | If your schema needs to be shared with a 3rd party - especially a third party using another language, it may be helpful to use it. 18 | 19 | If your schema validation requirements are more complicated - e.g. like what is described above - it's best *not* to use it. 20 | -------------------------------------------------------------------------------- /docs/public/why/explicit-tags-removed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is wrong with explicit tags? 3 | --- 4 | 5 | Explicit tags are tags that have an explicit type attached that is used to determine what type to convert the data to when it is parsed. 6 | 7 | For example, if it were to be applied to "fix" the Godfather movie script parsing issue described above, it would look like this: 8 | 9 | ```yaml 10 | - Don Corleone: Do you have faith in my judgment? 11 | - Clemenza: !!str Yes 12 | - Don Corleone: Do I have your loyalty? 13 | ``` 14 | 15 | Explicit typecasts in YAML markup are slightly confusing for non-programmers, much like the concept of 'types' in general. StrictYAML's philosophy is that types should be kept strictly separated from data, so this 'feature' of YAML is switched off. 16 | 17 | If tags are seen in a YAML file it will raise a special TagTokenDisallowed exception. 18 | 19 | 20 | ## Counterpoints 21 | 22 | - [Valid usage in AWS cloudformation syntax?](https://github.com/crdoconnor/strictyaml/issues/37) 23 | -------------------------------------------------------------------------------- /docs/src/why-not/pykwalify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use kwalify with standard YAML to validate my YAML? 3 | --- 4 | 5 | Kwalify is a schema validation language that is written *in* YAML. 6 | 7 | It is a descriptive schema language suitable for validating simple YAML. 8 | 9 | Kwalify compiles to the strictyaml equivalent but is able to do less. You cannot, for example: 10 | 11 | - Plug generated lists that come from outside of the spec (e.g. a list of country code from pycountry). 12 | - Validate parts of the schema which can be either one thing *or* another - e.g. a list *or* a single string. 13 | - Plug sub-validators of a document into larger validators. 14 | 15 | If your schema is very simple and small, there is no point to using kwalify. 16 | 17 | If your schema needs to be shared with a 3rd party - especially a third party using another language, it may be helpful to use it. 18 | 19 | If your schema validation requirements are more complicated - e.g. like what is described above - it's best *not* to use it. 20 | -------------------------------------------------------------------------------- /docs/src/using/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using StrictYAML 3 | --- 4 | 5 | How to: 6 | 7 | {% for dirfile in (subdir("using/alpha/howto/").is_not_dir() - subdir("using/alpha/howto/").named("index.md"))|sort() -%} 8 | - [{{ title(dirfile) }}](alpha/howto/{{ dirfile.name.splitext()[0] }}) 9 | {% endfor %} 10 | 11 | Compound validators: 12 | 13 | {% for dirfile in (subdir("using/alpha/compound/").is_not_dir() - subdir("using/alpha/compound/").named("index.md"))|sort() -%} 14 | - [{{ title(dirfile) }}](alpha/compound/{{ dirfile.name.splitext()[0] }}) 15 | {% endfor %} 16 | 17 | Scalar validators: 18 | 19 | {% for dirfile in (subdir("using/alpha/scalar/").is_not_dir() - subdir("using/alpha/scalar/").named("index.md"))|sort() -%} 20 | - [{{ title(dirfile) }}](alpha/scalar/{{ dirfile.name.splitext()[0] }}) 21 | {% endfor %} 22 | Restrictions: 23 | 24 | {% for dirfile in (subdir("using/alpha/restrictions/").is_not_dir() - subdir("using/alpha/restrictions/").named("index.md"))|sort() -%} 25 | - [{{ title(dirfile) }}](alpha/restrictions/{{ dirfile.name.splitext()[0] }}) 26 | {% endfor %} 27 | -------------------------------------------------------------------------------- /hitch/story/handle-exceptions.story: -------------------------------------------------------------------------------- 1 | Labeling exceptions: 2 | docs: howto/label-exceptions 3 | based on: strictyaml 4 | description: | 5 | When raising exceptions, you can add a label that will replace 6 | with whatever you want. 7 | given: 8 | setup: | 9 | from strictyaml import Map, Int, load, YAMLValidationError 10 | yaml_snippet: | 11 | a: 1 12 | b: 13 | - 1 14 | - 2 15 | variations: 16 | Label myfilename: 17 | steps: 18 | - Run: 19 | code: | 20 | load(yaml_snippet, Map({"a": Int(), "b": Map({"x": Int(), "y": Int()})}), label="myfilename") 21 | raises: 22 | type: strictyaml.exceptions.YAMLValidationError 23 | message: |- 24 | when expecting a mapping 25 | in "myfilename", line 2, column 1: 26 | b: 27 | ^ (line: 2) 28 | found a sequence 29 | in "myfilename", line 4, column 1: 30 | - '2' 31 | ^ (line: 4) 32 | -------------------------------------------------------------------------------- /docs/src/why/only-parse-strings-not-files.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does StrictYAML only parse from strings and not files? 3 | --- 4 | 5 | While other parsers will take strings, file handles and file names, 6 | StrictYAML will only parse YAML strings. 7 | 8 | This is done deliberately to reduce the module's remit, with the 9 | intention of reducing both the potential bug surface and the number 10 | of exceptions that StrictYAML has to deal with - things like 11 | nonexistent files, file system errors, bad reads, unknown file 12 | extensions, etc. become the problem of some other module - ideally 13 | one more focused on handling those kinds of issues. 14 | 15 | If you want a quick and easy one liner way to get text from a file, 16 | I recommend that you pip install path.py and and use .text() on the 17 | Path object: 18 | 19 | ```python 20 | >>> from path import Path 21 | >>> from strictyaml import load 22 | >>> parsed_data = load(Path("myfile.yaml").text()).data 23 | >>> print(parsed_data) 24 | [ parsed yaml ] 25 | ``` 26 | 27 | 28 | ## Counterarguments 29 | 30 | - 31 | -------------------------------------------------------------------------------- /docs/public/why/only-parse-strings-not-files.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does StrictYAML only parse from strings and not files? 3 | --- 4 | 5 | While other parsers will take strings, file handles and file names, 6 | StrictYAML will only parse YAML strings. 7 | 8 | This is done deliberately to reduce the module's remit, with the 9 | intention of reducing both the potential bug surface and the number 10 | of exceptions that StrictYAML has to deal with - things like 11 | nonexistent files, file system errors, bad reads, unknown file 12 | extensions, etc. become the problem of some other module - ideally 13 | one more focused on handling those kinds of issues. 14 | 15 | If you want a quick and easy one liner way to get text from a file, 16 | I recommend that you pip install path.py and and use .text() on the 17 | Path object: 18 | 19 | ```python 20 | >>> from path import Path 21 | >>> from strictyaml import load 22 | >>> parsed_data = load(Path("myfile.yaml").text()).data 23 | >>> print(parsed_data) 24 | [ parsed yaml ] 25 | ``` 26 | 27 | 28 | ## Counterarguments 29 | 30 | - 31 | -------------------------------------------------------------------------------- /hitch/story/dirty-load.story: -------------------------------------------------------------------------------- 1 | Dirty load: 2 | docs: restrictions/loading-dirty-yaml 3 | based on: strictyaml 4 | description: | 5 | StrictYAML refuses to parse flow style and node anchors 6 | by default, but since there have since been 7 | [some requests](https://github.com/crdoconnor/strictyaml/issues/38) 8 | to parse flow style, this now allowed with the "dirty_load" method. 9 | If allow_flow_style is True, Map indentation is not checked for 10 | consistency, as the indentation level is dependent on the map key length. 11 | given: 12 | setup: | 13 | from strictyaml import Map, Int, MapPattern, Seq, Str, Any, dirty_load 14 | 15 | schema = Map({"foo": Map({"a": Any(), "b": Any(), "c": Any()}), "y": MapPattern(Str(), Str()), "z": Seq(Str())}) 16 | variations: 17 | Flow style mapping: 18 | given: 19 | yaml_snippet: | 20 | foo: { a: 1, b: 2, c: 3 } 21 | y: {} 22 | z: [] 23 | steps: 24 | - Run: | 25 | assert dirty_load(yaml_snippet, schema, allow_flow_style=True) == {"foo": {"a": "1", "b": "2", "c": "3"}, "y": {}, "z": []} 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Colm O'Connor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/public/using/alpha/howto/merge-yaml-documents.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Merge YAML documents 3 | --- 4 | 5 | 6 | Loaded YAML can be combined and dumped with the comments intact. 7 | 8 | 9 | 10 | 11 | ```python 12 | from strictyaml import Map, MapPattern, Str, Seq, Int, load 13 | 14 | schema_1 = Map({ 15 | "a": Str(), 16 | "b": Map({"x": Int(), "y": Int()}), 17 | "c": Seq(MapPattern(Str(), Str())), 18 | }) 19 | 20 | schema_2 = Map({"x": Int(), "y": Int()}) 21 | 22 | yaml_1 = load(yaml_snippet_1, schema_1) 23 | yaml_2 = load(yaml_snippet_2, schema_2) 24 | 25 | yaml_1['b'] = yaml_2 26 | 27 | ``` 28 | 29 | 30 | 31 | ```python 32 | print(yaml_1.as_yaml()) 33 | ``` 34 | 35 | ```yaml 36 | # Some comment 37 | 38 | a: â # value comment 39 | 40 | # Another comment 41 | b: 42 | x: 8 43 | 44 | # y is now 9 45 | y: 9 46 | c: 47 | - a: 1 48 | - b: 2 49 | ``` 50 | 51 | 52 | 53 | 54 | 55 | 56 | !!! note "Executable specification" 57 | 58 | Documentation automatically generated from 59 | merge-documents.story 60 | storytests. -------------------------------------------------------------------------------- /docs/public/using/alpha/howto/label-exceptions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Labeling exceptions 3 | --- 4 | 5 | 6 | When raising exceptions, you can add a label that will replace 7 | with whatever you want. 8 | 9 | 10 | Example yaml_snippet: 11 | 12 | ```yaml 13 | a: 1 14 | b: 15 | - 1 16 | - 2 17 | 18 | ``` 19 | 20 | 21 | ```python 22 | from strictyaml import Map, Int, load, YAMLValidationError 23 | 24 | ``` 25 | 26 | 27 | 28 | Label myfilename: 29 | 30 | 31 | ```python 32 | load(yaml_snippet, Map({"a": Int(), "b": Map({"x": Int(), "y": Int()})}), label="myfilename") 33 | 34 | ``` 35 | 36 | 37 | ```python 38 | strictyaml.exceptions.YAMLValidationError: 39 | when expecting a mapping 40 | in "myfilename", line 2, column 1: 41 | b: 42 | ^ (line: 2) 43 | found a sequence 44 | in "myfilename", line 4, column 1: 45 | - '2' 46 | ^ (line: 4) 47 | ``` 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | !!! note "Executable specification" 56 | 57 | Documentation automatically generated from 58 | handle-exceptions.story 59 | storytests. -------------------------------------------------------------------------------- /docs/src/what-is-yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is YAML? 3 | --- 4 | 5 | YAML is a simple, human readable format for representing associative and hierarchical data. 6 | 7 | Example from wikipedia page on YAML: 8 | 9 | ```yaml 10 | receipt: Oz-Ware Purchase Invoice 11 | date: 2012-08-06 12 | customer: 13 | first name: Harry 14 | family name: Potter 15 | address: |- 16 | 4 Privet Drive, 17 | Little Whinging, 18 | England 19 | items: 20 | - part_no: A4786 21 | description: Water Bucket (Filled) 22 | price: 1.47 23 | quantity: 4 24 | 25 | - part_no: E1628 26 | description: High Heeled "Ruby" Slippers 27 | size: 8 28 | price: 133.7 29 | quantity: 1 30 | ``` 31 | 32 | Key features: 33 | 34 | - Things which are associated with other things - delimited by the colon (:). 35 | - Ordered lists of things - delimited by the prepended dash (-). 36 | - Multi-line strings - delimited by the bar (|) if there is another newline at the end of the string, or bar + dash (|-) if not. 37 | - Indentation describing the hierarchy of data. 38 | - Maps directly to data types common to most high level languages - lists, dicts, scalars. 39 | 40 | You don't need to know much more than this. 41 | -------------------------------------------------------------------------------- /docs/public/what-is-yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is YAML? 3 | --- 4 | 5 | YAML is a simple, human readable format for representing associative and hierarchical data. 6 | 7 | Example from wikipedia page on YAML: 8 | 9 | ```yaml 10 | receipt: Oz-Ware Purchase Invoice 11 | date: 2012-08-06 12 | customer: 13 | first name: Harry 14 | family name: Potter 15 | address: |- 16 | 4 Privet Drive, 17 | Little Whinging, 18 | England 19 | items: 20 | - part_no: A4786 21 | description: Water Bucket (Filled) 22 | price: 1.47 23 | quantity: 4 24 | 25 | - part_no: E1628 26 | description: High Heeled "Ruby" Slippers 27 | size: 8 28 | price: 133.7 29 | quantity: 1 30 | ``` 31 | 32 | Key features: 33 | 34 | - Things which are associated with other things - delimited by the colon (:). 35 | - Ordered lists of things - delimited by the prepended dash (-). 36 | - Multi-line strings - delimited by the bar (|) if there is another newline at the end of the string, or bar + dash (|-) if not. 37 | - Indentation describing the hierarchy of data. 38 | - Maps directly to data types common to most high level languages - lists, dicts, scalars. 39 | 40 | You don't need to know much more than this. 41 | -------------------------------------------------------------------------------- /docs/public/why-not/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not X? 3 | --- 4 | 5 | There are a number of formats and approaches that can achieve more or 6 | less the same purpose as StrictYAML. Below is a series of comparisons 7 | with some of the more famous ones: 8 | 9 | - [Why avoid using environment variables as configuration?](environment-variables-as-config) 10 | - [Why not use HJSON?](hjson) 11 | - [Why not HOCON?](hocon) 12 | - [Why not use INI files?](ini) 13 | - [Why not use JSON Schema for validation?](json-schema) 14 | - [Why not JSON for simple configuration files?](json) 15 | - [Why not JSON5?](json5) 16 | - [Why not use the YAML 1.2 standard? - we don't need a new standard!](ordinary-yaml) 17 | - [Why not use kwalify with standard YAML to validate my YAML?](pykwalify) 18 | - [Why not use Python's schema library (or similar) for validation?](python-schema) 19 | - [Why not use SDLang?](sdlang) 20 | - [What is wrong with TOML?](toml) 21 | - [Why shouldn't I just use Python code for configuration?](turing-complete-code) 22 | - [Why not use XML for configuration or DSLs?](xml) 23 | 24 | 25 | If you'd like to write or link to a rebuttal to any argument raised 26 | here, feel free to raise a ticket. -------------------------------------------------------------------------------- /docs/public/using/alpha/scalar/hexadecimal-integer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hexadecimal Integers (HexInt) 3 | --- 4 | 5 | 6 | StrictYAML can interpret a hexadecimal integer 7 | preserving its value 8 | 9 | 10 | Example yaml_snippet: 11 | 12 | ```yaml 13 | x: 0x1a 14 | 15 | ``` 16 | 17 | 18 | ```python 19 | from strictyaml import Map, HexInt, load 20 | from ensure import Ensure 21 | 22 | schema = Map({"x": HexInt()}) 23 | 24 | parsed = load(yaml_snippet, schema) 25 | 26 | ``` 27 | 28 | 29 | 30 | Parsed correctly: 31 | 32 | 33 | ```python 34 | Ensure(parsed).equals({"x": 26}) 35 | Ensure(parsed.as_yaml()).equals("x: 0x1a\n") 36 | 37 | ``` 38 | 39 | 40 | 41 | 42 | Uppercase: 43 | 44 | ```yaml 45 | x: 0X1A 46 | 47 | ``` 48 | 49 | 50 | ```python 51 | Ensure(load(yaml_snippet, schema).data).equals({"x": 26}) 52 | Ensure(load(yaml_snippet, schema).as_yaml()).equals("x: 0X1A\n") 53 | 54 | ``` 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | !!! note "Executable specification" 63 | 64 | Documentation automatically generated from 65 | scalar-hexadecimal-integer.story 66 | storytests. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | packages = ["strictyaml", "strictyaml.ruamel"] 7 | 8 | [project] 9 | name = "strictyaml" 10 | authors = [ 11 | {name = "Colm O'Connor", email = "colm.oconnor.github@gmail.com"}, 12 | ] 13 | description = "Strict, typed YAML parser" 14 | license = {text = "MIT"} 15 | requires-python = ">=3.7.0" 16 | keywords = ["yaml"] 17 | classifiers = [ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Topic :: Text Processing :: Markup", 21 | "Topic :: Software Development :: Libraries", 22 | "Natural Language :: English", 23 | ] 24 | dependencies = [ 25 | "python-dateutil>=2.6.0" 26 | ] 27 | dynamic = ["version", "readme"] 28 | 29 | [project.urls] 30 | homepage = "https://hitchdev.com/strictyaml" 31 | documentation = "https://hitchdev.com/strictyaml/using" 32 | repository = "https://github.com/crdoconnor/strictyaml" 33 | changelog = "https://hitchdev.com/strictyaml/changelog" 34 | 35 | [tool.setuptools.dynamic] 36 | readme = {file = ["README.md",], content-type = "text/markdown"} 37 | version = {file = "VERSION"} 38 | -------------------------------------------------------------------------------- /docs/public/why/not-parse-direct-representations-of-python-objects.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does StrictYAML not parse direct representations of Python objects? 3 | --- 4 | 5 | Regular YAML allows the direct representation of Python objects. 6 | 7 | For example: 8 | 9 | ```yaml 10 | --- !python/hash:UserObject 11 | email: evilhacker@hacker.com 12 | password: passwordtoset 13 | type: admin 14 | ``` 15 | 16 | If we load this YAML formatted string in, we get a user object. This was 17 | how YAML was intended to work since it allows the ability to write objects 18 | to and read them from, say, a database. 19 | 20 | By itself, this behavior isn't necessarily capable of enacting a successful 21 | attack, so not all code that parses untrusted YAML is insecure, 22 | but it can be used, especially in conjunction with metaprogramming to execute 23 | arbitrary code on your system. 24 | 25 | This shares a lot in common with the pickle module's behavior, which is why 26 | its use with [untrusted input is strongly recommended against in the Python 27 | docs](https://docs.python.org/3/library/pickle.html). 28 | 29 | This anti-feature led to Ruby on Rails' spectacular [security fail](https://codeclimate.com/blog/rails-remote-code-execution-vulnerability-explained/). 30 | -------------------------------------------------------------------------------- /docs/src/why/not-parse-direct-representations-of-python-objects.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does StrictYAML not parse direct representations of Python objects? 3 | --- 4 | 5 | Regular YAML allows the direct representation of Python objects. 6 | 7 | For example: 8 | 9 | ```yaml 10 | --- !python/hash:UserObject 11 | email: evilhacker@hacker.com 12 | password: passwordtoset 13 | type: admin 14 | ``` 15 | 16 | If we load this YAML formatted string in, we get a user object. This was 17 | how YAML was intended to work since it allows the ability to write objects 18 | to and read them from, say, a database. 19 | 20 | By itself, this behavior isn't necessarily capable of enacting a successful 21 | attack, so not all code that parses untrusted YAML is insecure, 22 | but it can be used, especially in conjunction with metaprogramming to execute 23 | arbitrary code on your system. 24 | 25 | This shares a lot in common with the pickle module's behavior, which is why 26 | its use with [untrusted input is strongly recommended against in the Python 27 | docs](https://docs.python.org/3/library/pickle.html). 28 | 29 | This anti-feature led to Ruby on Rails' spectacular [security fail](https://codeclimate.com/blog/rails-remote-code-execution-vulnerability-explained/). 30 | -------------------------------------------------------------------------------- /docs/src/template/story.jinja2: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ story.name }} 3 | --- 4 | {% if story.info['experimental'] %} 5 | !!! warning "Experimental" 6 | 7 | This feature is in alpha. The API may change on a minor version increment. 8 | {% endif %} 9 | 10 | {{ story.info['description'] }} 11 | 12 | {% if 'yaml_snippet' in story.given %}Example yaml_snippet: 13 | 14 | ```yaml 15 | {{ story.given['yaml_snippet'] }} 16 | ``` 17 | {% endif %} 18 | 19 | {% if 'setup' in story.given -%} 20 | ```python 21 | {{ story.given['setup'] }} 22 | ``` 23 | {%- endif %} 24 | 25 | {% if story.variations %} 26 | {% for variation in story.variations %} 27 | {{ variation.child_name }}: 28 | {% if'yaml_snippet' in variation.data.get('given', {}).keys() %} 29 | ```yaml 30 | {{ variation.given['yaml_snippet'] }} 31 | ``` 32 | {% endif %} 33 | {% with step = variation.steps[0] %}{% include "step.jinja2" %}{% endwith %} 34 | {% endfor %} 35 | {% else %} 36 | {% with step = story.steps[0] %}{% include "step.jinja2" %}{% endwith %} 37 | {% endif %} 38 | 39 | 40 | !!! note "Executable specification" 41 | 42 | Documentation automatically generated from 43 | {{ story.filename.basename() }} 44 | storytests. 45 | -------------------------------------------------------------------------------- /docs/public/why-not/sdlang.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use SDLang? 3 | --- 4 | 5 | [SDLang](http://sdlang.org/) or "simple declarative language" is a proposed configuration language with an XML-like structure inspired by C. 6 | 7 | Example: 8 | 9 | ``` 10 | // This is a node with a single string value 11 | title "Hello, World" 12 | 13 | // Multiple values are supported, too 14 | bookmarks 12 15 188 1234 15 | 16 | // Nodes can have attributes 17 | author "Peter Parker" email="peter@example.org" active=true 18 | 19 | // Nodes can be arbitrarily nested 20 | contents { 21 | section "First section" { 22 | paragraph "This is the first paragraph" 23 | paragraph "This is the second paragraph" 24 | } 25 | } 26 | 27 | // Anonymous nodes are supported 28 | "This text is the value of an anonymous node!" 29 | 30 | // This makes things like matrix definitions very convenient 31 | matrix { 32 | 1 0 0 33 | 0 1 0 34 | 0 0 1 35 | } 36 | ``` 37 | 38 | Advantages: 39 | 40 | - Relatively more straightforward than other serialization languages. 41 | 42 | Disadvantages: 43 | 44 | - Syntax typing - leading to noisy syntax. 45 | - The distinction between properties and values is not entirely clear. 46 | - Instead of having one obvious way to describe property:value mappings 47 | - Niche 48 | -------------------------------------------------------------------------------- /docs/src/why-not/sdlang.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use SDLang? 3 | --- 4 | 5 | [SDLang](http://sdlang.org/) or "simple declarative language" is a proposed configuration language with an XML-like structure inspired by C. 6 | 7 | Example: 8 | 9 | ``` 10 | // This is a node with a single string value 11 | title "Hello, World" 12 | 13 | // Multiple values are supported, too 14 | bookmarks 12 15 188 1234 15 | 16 | // Nodes can have attributes 17 | author "Peter Parker" email="peter@example.org" active=true 18 | 19 | // Nodes can be arbitrarily nested 20 | contents { 21 | section "First section" { 22 | paragraph "This is the first paragraph" 23 | paragraph "This is the second paragraph" 24 | } 25 | } 26 | 27 | // Anonymous nodes are supported 28 | "This text is the value of an anonymous node!" 29 | 30 | // This makes things like matrix definitions very convenient 31 | matrix { 32 | 1 0 0 33 | 0 1 0 34 | 0 0 1 35 | } 36 | ``` 37 | 38 | Advantages: 39 | 40 | - Relatively more straightforward than other serialization languages. 41 | 42 | Disadvantages: 43 | 44 | - Syntax typing - leading to noisy syntax. 45 | - The distinction between properties and values is not entirely clear. 46 | - Instead of having one obvious way to describe property:value mappings 47 | - Niche 48 | -------------------------------------------------------------------------------- /hitch/story/email-url.story: -------------------------------------------------------------------------------- 1 | Email and URL validators: 2 | based on: strictyaml 3 | docs: scalar/email-and-url 4 | description: | 5 | StrictYAML can validate emails (using a simplified regex) and 6 | URLs. 7 | given: 8 | setup: | 9 | from strictyaml import Email, Url, Map, load 10 | from ensure import Ensure 11 | 12 | schema = Map({"a": Email(), "b": Url()}) 13 | variations: 14 | Parsed: 15 | given: 16 | yaml_snippet: | 17 | a: billg@microsoft.com 18 | b: https://user:pass@example.com:443/path?k=v#frag 19 | steps: 20 | - Run: | 21 | Ensure(load(yaml_snippet, schema)).equals({"a": "billg@microsoft.com", "b": "https://user:pass@example.com:443/path?k=v#frag"}) 22 | 23 | Exception: 24 | given: 25 | yaml_snippet: | 26 | a: notanemail 27 | b: notaurl 28 | steps: 29 | - Run: 30 | code: load(yaml_snippet, schema) 31 | raises: 32 | type: strictyaml.exceptions.YAMLValidationError 33 | message: |- 34 | when expecting an email address 35 | found non-matching string 36 | in "", line 1, column 1: 37 | a: notanemail 38 | ^ (line: 1) 39 | -------------------------------------------------------------------------------- /docs/public/using/alpha/restrictions/loading-dirty-yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dirty load 3 | --- 4 | 5 | 6 | StrictYAML refuses to parse flow style and node anchors 7 | by default, but since there have since been 8 | [some requests](https://github.com/crdoconnor/strictyaml/issues/38) 9 | to parse flow style, this now allowed with the "dirty_load" method. 10 | If allow_flow_style is True, Map indentation is not checked for 11 | consistency, as the indentation level is dependent on the map key length. 12 | 13 | 14 | 15 | 16 | ```python 17 | from strictyaml import Map, Int, MapPattern, Seq, Str, Any, dirty_load 18 | 19 | schema = Map({"foo": Map({"a": Any(), "b": Any(), "c": Any()}), "y": MapPattern(Str(), Str()), "z": Seq(Str())}) 20 | 21 | ``` 22 | 23 | 24 | 25 | Flow style mapping: 26 | 27 | ```yaml 28 | foo: { a: 1, b: 2, c: 3 } 29 | y: {} 30 | z: [] 31 | 32 | ``` 33 | 34 | 35 | ```python 36 | assert dirty_load(yaml_snippet, schema, allow_flow_style=True) == {"foo": {"a": "1", "b": "2", "c": "3"}, "y": {}, "z": []} 37 | 38 | ``` 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | !!! note "Executable specification" 47 | 48 | Documentation automatically generated from 49 | dirty-load.story 50 | storytests. -------------------------------------------------------------------------------- /hitch/story/merge-documents.story: -------------------------------------------------------------------------------- 1 | Merge YAML documents: 2 | docs: howto/merge-yaml-documents 3 | based on: strictyaml 4 | description: | 5 | Loaded YAML can be combined and dumped with the comments intact. 6 | given: 7 | yaml_snippet_1: | 8 | # Some comment 9 | 10 | a: â # value comment 11 | 12 | # Another comment 13 | b: 14 | x: 4 15 | y: 5 16 | c: 17 | - a: 1 18 | - b: 2 19 | yaml_snippet_2: | 20 | x: 8 21 | 22 | # y is now 9 23 | y: 9 24 | setup: | 25 | from strictyaml import Map, MapPattern, Str, Seq, Int, load 26 | 27 | schema_1 = Map({ 28 | "a": Str(), 29 | "b": Map({"x": Int(), "y": Int()}), 30 | "c": Seq(MapPattern(Str(), Str())), 31 | }) 32 | 33 | schema_2 = Map({"x": Int(), "y": Int()}) 34 | 35 | yaml_1 = load(yaml_snippet_1, schema_1) 36 | yaml_2 = load(yaml_snippet_2, schema_2) 37 | 38 | yaml_1['b'] = yaml_2 39 | steps: 40 | - Run: 41 | code: print(yaml_1.as_yaml()) 42 | will output: |- 43 | # Some comment 44 | 45 | a: â # value comment 46 | 47 | # Another comment 48 | b: 49 | x: 8 50 | 51 | # y is now 9 52 | y: 9 53 | c: 54 | - a: 1 55 | - b: 2 56 | -------------------------------------------------------------------------------- /docs/public/using/alpha/scalar/email-and-url.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Email and URL validators 3 | --- 4 | 5 | 6 | StrictYAML can validate emails (using a simplified regex) and 7 | URLs. 8 | 9 | 10 | 11 | 12 | ```python 13 | from strictyaml import Email, Url, Map, load 14 | from ensure import Ensure 15 | 16 | schema = Map({"a": Email(), "b": Url()}) 17 | 18 | ``` 19 | 20 | 21 | 22 | Parsed: 23 | 24 | ```yaml 25 | a: billg@microsoft.com 26 | b: https://user:pass@example.com:443/path?k=v#frag 27 | 28 | ``` 29 | 30 | 31 | ```python 32 | Ensure(load(yaml_snippet, schema)).equals({"a": "billg@microsoft.com", "b": "https://user:pass@example.com:443/path?k=v#frag"}) 33 | 34 | ``` 35 | 36 | 37 | 38 | 39 | Exception: 40 | 41 | ```yaml 42 | a: notanemail 43 | b: notaurl 44 | 45 | ``` 46 | 47 | 48 | ```python 49 | load(yaml_snippet, schema) 50 | ``` 51 | 52 | 53 | ```python 54 | strictyaml.exceptions.YAMLValidationError: 55 | when expecting an email address 56 | found non-matching string 57 | in "", line 1, column 1: 58 | a: notanemail 59 | ^ (line: 1) 60 | ``` 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | !!! note "Executable specification" 69 | 70 | Documentation automatically generated from 71 | email-url.story 72 | storytests. -------------------------------------------------------------------------------- /docs/public/why/flow-style-removed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is wrong with flow-style YAML? 3 | --- 4 | 5 | Flow style is essentially JSON embedded in YAML - making use of curly { } and square brackets to denote lists and mappings. 6 | 7 | Example: 8 | 9 | ```yaml 10 | a: 1 11 | b: {c: 3, d: 4} 12 | ``` 13 | 14 | This use of JSONesque { and } is also ugly and hampers readability - *especially* when { and } are used for other purposes (e.g. templating) and the human reader/writer of YAML has to give themselves a headache figuring out what *kind* of curly bracket it is. 15 | 16 | The *first* question in the FAQ of pyyaml actually subtly indicates that this feature wasn't a good idea - see "[why does my YAML look wrong?](http://pyyaml.org/wiki/PyYAMLDocumentation#Dictionarieswithoutnestedcollectionsarenotdumpedcorrectly)". 17 | 18 | To take a real life example, use of flow style in [this saltstack YAML definition](https://github.com/saltstack-formulas/mysql-formula/blob/master/mysql/server.sls#L27) which blurs the distinction between flow style and jinja2, 19 | confusing the reader. 20 | 21 | 22 | ## Parsing 'dirty' YAML with flow style 23 | 24 | To parse YAML with flow style, you can use [dirty load](../../using/alpha/restrictions/loading-dirty-yaml). 25 | 26 | 27 | ## Counterarguments 28 | 29 | - 30 | - 31 | -------------------------------------------------------------------------------- /docs/src/why/flow-style-removed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is wrong with flow-style YAML? 3 | --- 4 | 5 | Flow style is essentially JSON embedded in YAML - making use of curly { } and square brackets to denote lists and mappings. 6 | 7 | Example: 8 | 9 | ```yaml 10 | a: 1 11 | b: {c: 3, d: 4} 12 | ``` 13 | 14 | This use of JSONesque { and } is also ugly and hampers readability - *especially* when { and } are used for other purposes (e.g. templating) and the human reader/writer of YAML has to give themselves a headache figuring out what *kind* of curly bracket it is. 15 | 16 | The *first* question in the FAQ of pyyaml actually subtly indicates that this feature wasn't a good idea - see "[why does my YAML look wrong?](http://pyyaml.org/wiki/PyYAMLDocumentation#Dictionarieswithoutnestedcollectionsarenotdumpedcorrectly)". 17 | 18 | To take a real life example, use of flow style in [this saltstack YAML definition](https://github.com/saltstack-formulas/mysql-formula/blob/master/mysql/server.sls#L27) which blurs the distinction between flow style and jinja2, 19 | confusing the reader. 20 | 21 | 22 | ## Parsing 'dirty' YAML with flow style 23 | 24 | To parse YAML with flow style, you can use [dirty load](../../using/alpha/restrictions/loading-dirty-yaml). 25 | 26 | 27 | ## Counterarguments 28 | 29 | - 30 | - 31 | -------------------------------------------------------------------------------- /docs/public/why-not/xml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use XML for configuration or DSLs? 3 | --- 4 | 5 | XML suffers from overcomplication much like vanilla YAML does - although to an ever greater degree, thanks to the committee-driven design. 6 | Doctypes and namespaces are horrendous additions to the language, for instance. 7 | XML is not only not really human readable (beyond a very basic subset of the language), it's often barely *programmer* readable despite being less expressive than most Turing-complete languages. 8 | It's a flagrant violation of the [rule of least power](https://en.wikipedia.org/wiki/Rule_of_least_power). 9 | 10 | The language was, in fact, *so* overcomplicated that it ended up increasing the attack surface of the parser itself to the point that it led to parsers with [security vulnerabilities](https://en.wikipedia.org/wiki/Billion_laughs). 11 | 12 | Unlike JSON and YAML, XML's structure also does not map well on to the default data types used by most languages, often requiring a *third* language to act as a go between - e.g. either XQuery or XPath. 13 | 14 | XML's decline in favor of JSON as a default API format is largely due to these complications and the lack of any real benefit drawn from them. 15 | The associated technologies (e.g. XSLT) also suffered from design by committee. 16 | 17 | Using it as a configuration language will all but ensure that you need to write extra boilerplate code to manage its quirks. 18 | -------------------------------------------------------------------------------- /docs/src/why-not/xml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use XML for configuration or DSLs? 3 | --- 4 | 5 | XML suffers from overcomplication much like vanilla YAML does - although to an ever greater degree, thanks to the committee-driven design. 6 | Doctypes and namespaces are horrendous additions to the language, for instance. 7 | XML is not only not really human readable (beyond a very basic subset of the language), it's often barely *programmer* readable despite being less expressive than most Turing-complete languages. 8 | It's a flagrant violation of the [rule of least power](https://en.wikipedia.org/wiki/Rule_of_least_power). 9 | 10 | The language was, in fact, *so* overcomplicated that it ended up increasing the attack surface of the parser itself to the point that it led to parsers with [security vulnerabilities](https://en.wikipedia.org/wiki/Billion_laughs). 11 | 12 | Unlike JSON and YAML, XML's structure also does not map well on to the default data types used by most languages, often requiring a *third* language to act as a go between - e.g. either XQuery or XPath. 13 | 14 | XML's decline in favor of JSON as a default API format is largely due to these complications and the lack of any real benefit drawn from them. 15 | The associated technologies (e.g. XSLT) also suffered from design by committee. 16 | 17 | Using it as a configuration language will all but ensure that you need to write extra boilerplate code to manage its quirks. 18 | -------------------------------------------------------------------------------- /docs/public/why/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Design Justifications 3 | --- 4 | 5 | StrictYAML is the result of some carefully considered, although 6 | controversial design decisions. These are justified here. 7 | 8 | - [What is wrong with duplicate keys?](duplicate-keys-disallowed) 9 | - [What is wrong with explicit tags?](explicit-tags-removed) 10 | - [What is wrong with flow-style YAML?](flow-style-removed) 11 | - [The Norway Problem - why StrictYAML refuses to do implicit typing and so should you](implicit-typing-removed) 12 | - [What is wrong with node anchors and references?](node-anchors-and-references-removed) 13 | - [Why does StrictYAML not parse direct representations of Python objects?](not-parse-direct-representations-of-python-objects) 14 | - [Why does StrictYAML only parse from strings and not files?](only-parse-strings-not-files) 15 | - [Why is parsing speed not a high priority for StrictYAML?](speed-not-a-priority) 16 | - [What is syntax typing?](syntax-typing-bad) 17 | - [Why does StrictYAML make you define a schema in Python - a Turing-complete language?](turing-complete-schema) 18 | 19 | 20 | If you have seen a relevant counterargument to you'd like to link 21 | to addressed at StrictYAML, please create a pull request and 22 | link to it in the relevant document. 23 | 24 | If you'd like to write your own rebuttal to any argument raised 25 | here, raise a ticket and issue a pull request linking to it at 26 | the end of the document. -------------------------------------------------------------------------------- /docs/public/why/turing-complete-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does StrictYAML make you define a schema in Python - a Turing-complete language? 3 | --- 4 | 5 | StrictYAML defines schemas in Python (i.e. Turing-complete) code. For example: 6 | 7 | ```python 8 | Map({"name": Str(), "email": Str()}) 9 | ``` 10 | 11 | Instead of: 12 | 13 | ```yaml 14 | type: map 15 | mapping: 16 | "name": 17 | type: str 18 | required: yes 19 | "email": 20 | type: str 21 | required: yes 22 | ``` 23 | 24 | There are some trade offs here: 25 | 26 | Schema definition in a non-Turing-complete language like YAML makes 27 | the schema programming language independent and gives it more 28 | potential for being read and understood by non-programmers. 29 | 30 | However, schema definition in a non-Turing-complete language also 31 | restricts and makes certain use cases impossible or awkward. 32 | 33 | Some use cases I came across included: 34 | 35 | - Being able to import pycountry's country list and restrict "country: " to valid country names. 36 | - Being able to implement a schema that validated date/time scalar values against the specific date/time parser I wanted. 37 | - Being able to revalidate sections of the document on a 'second pass' that used new data - e.g. a list in one part of the document is restricted to items which come from another part. 38 | 39 | 40 | ## Counterarguments 41 | 42 | - 43 | -------------------------------------------------------------------------------- /docs/src/comparison/table.yml: -------------------------------------------------------------------------------- 1 | json: 2 | syntax to data ratio: 3 | % length of JSON: 100 4 | syntax typing: yes 5 | well defined: yes 6 | comments: no 7 | hierarchical: yes 8 | duplicate keys: no 9 | multiline strings: no 10 | meaningful whitespace: no 11 | complex features: no 12 | 13 | strictyaml: 14 | syntax to data ratio: 15 | % length of JSON: 16 | syntax typing: no 17 | well defined: yes 18 | comments: yes 19 | syntactic noise ratio: 20 | hierarchical: yes 21 | duplicate keys: no 22 | multiline strings: yes 23 | meaningful whitespace: yes 24 | complex features: no 25 | 26 | yaml: 27 | syntax to data ratio: 28 | % length of JSON: 29 | syntax typing: yes 30 | well defined: yes 31 | comments: yes 32 | hierarchical: yes 33 | duplicate keys: yes 34 | multiline strings: yes 35 | meaningful whitespace: no 36 | complex features: yes 37 | 38 | ini: 39 | syntactic noise ratio: 40 | % length of JSON: 41 | syntax typing: no 42 | well defined: no 43 | comments: yes 44 | hierarchical: no 45 | duplicate keys: yes 46 | multiline strings: no 47 | meaningful whitespace: no 48 | complex features: no 49 | 50 | toml: 51 | syntax to data ratio: 52 | % length of JSON: 53 | syntax typing: no 54 | well defined: no 55 | comments: yes 56 | hierarchical: yes 57 | duplicate keys: no 58 | multiline strings: yes 59 | meaningful whitespace: no 60 | complex features: yes 61 | 62 | json5: 63 | -------------------------------------------------------------------------------- /docs/src/why/turing-complete-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why does StrictYAML make you define a schema in Python - a Turing-complete language? 3 | --- 4 | 5 | StrictYAML defines schemas in Python (i.e. Turing-complete) code. For example: 6 | 7 | ```python 8 | Map({"name": Str(), "email": Str()}) 9 | ``` 10 | 11 | Instead of: 12 | 13 | ```yaml 14 | type: map 15 | mapping: 16 | "name": 17 | type: str 18 | required: yes 19 | "email": 20 | type: str 21 | required: yes 22 | ``` 23 | 24 | There are some trade offs here: 25 | 26 | Schema definition in a non-Turing-complete language like YAML makes 27 | the schema programming language independent and gives it more 28 | potential for being read and understood by non-programmers. 29 | 30 | However, schema definition in a non-Turing-complete language also 31 | restricts and makes certain use cases impossible or awkward. 32 | 33 | Some use cases I came across included: 34 | 35 | - Being able to import pycountry's country list and restrict "country: " to valid country names. 36 | - Being able to implement a schema that validated date/time scalar values against the specific date/time parser I wanted. 37 | - Being able to revalidate sections of the document on a 'second pass' that used new data - e.g. a list in one part of the document is restricted to items which come from another part. 38 | 39 | 40 | ## Counterarguments 41 | 42 | - 43 | -------------------------------------------------------------------------------- /docs/public/comparison/table.yml: -------------------------------------------------------------------------------- 1 | json: 2 | syntax to data ratio: 3 | % length of JSON: 100 4 | syntax typing: yes 5 | well defined: yes 6 | comments: no 7 | hierarchical: yes 8 | duplicate keys: no 9 | multiline strings: no 10 | meaningful whitespace: no 11 | complex features: no 12 | 13 | strictyaml: 14 | syntax to data ratio: 15 | % length of JSON: 16 | syntax typing: no 17 | well defined: yes 18 | comments: yes 19 | syntactic noise ratio: 20 | hierarchical: yes 21 | duplicate keys: no 22 | multiline strings: yes 23 | meaningful whitespace: yes 24 | complex features: no 25 | 26 | yaml: 27 | syntax to data ratio: 28 | % length of JSON: 29 | syntax typing: yes 30 | well defined: yes 31 | comments: yes 32 | hierarchical: yes 33 | duplicate keys: yes 34 | multiline strings: yes 35 | meaningful whitespace: no 36 | complex features: yes 37 | 38 | ini: 39 | syntactic noise ratio: 40 | % length of JSON: 41 | syntax typing: no 42 | well defined: no 43 | comments: yes 44 | hierarchical: no 45 | duplicate keys: yes 46 | multiline strings: no 47 | meaningful whitespace: no 48 | complex features: no 49 | 50 | toml: 51 | syntax to data ratio: 52 | % length of JSON: 53 | syntax typing: no 54 | well defined: no 55 | comments: yes 56 | hierarchical: yes 57 | duplicate keys: no 58 | multiline strings: yes 59 | meaningful whitespace: no 60 | complex features: yes 61 | 62 | json5: 63 | -------------------------------------------------------------------------------- /docs/src/why-not/ordinary-yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use the YAML 1.2 standard? - we don't need a new standard! 3 | --- 4 | 5 | ![Standards](https://imgs.xkcd.com/comics/standards.png "Fortunately the configuration one has been solved now that we have Strict uh... wait, no it hasn't...") 6 | 7 | StrictYAML is composed of two parts: 8 | 9 | - A new YAML specification which parses a restricted subset of the [YAML 1.2 specification](https://yaml.org/spec/1.2.2/) 10 | and *only* parses to ordered dict, list or string. 11 | - An optional validator (which will, as requested, validate and cast parse some of those scalar string values to ints, floats, datetimes, etc.). 12 | 13 | Note that StrictYAML is *not* a new standard. If you have a syntax highlighter or editor or anything else that recognizes 14 | or reads YAML it will recognize StrictYAML in the same way. 15 | 16 | While not all YAML output by other programs will be readable by StrictYAML (it is, after all, stricter), a lot will be. 17 | 18 | The features removed from the YAML spec, and their rationales are as follows: 19 | 20 | - [Implicit Typing](../../why/implicit-typing-removed) 21 | - [Direct representations of objects](../../why/not-parse-direct-representations-of-python-objects) 22 | - [Explicit tags](../../why/explicit-tags-removed) 23 | - [Node anchors and refs](../../why/node-anchors-and-references-removed) 24 | - [Flow style](../../why/flow-style-removed) 25 | - [Duplicate Keys Disallowed](../../why/duplicate-keys-disallowed) 26 | 27 | -------------------------------------------------------------------------------- /docs/public/why-not/ordinary-yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use the YAML 1.2 standard? - we don't need a new standard! 3 | --- 4 | 5 | ![Standards](https://imgs.xkcd.com/comics/standards.png "Fortunately the configuration one has been solved now that we have Strict uh... wait, no it hasn't...") 6 | 7 | StrictYAML is composed of two parts: 8 | 9 | - A new YAML specification which parses a restricted subset of the [YAML 1.2 specification](https://yaml.org/spec/1.2.2/) 10 | and *only* parses to ordered dict, list or string. 11 | - An optional validator (which will, as requested, validate and cast parse some of those scalar string values to ints, floats, datetimes, etc.). 12 | 13 | Note that StrictYAML is *not* a new standard. If you have a syntax highlighter or editor or anything else that recognizes 14 | or reads YAML it will recognize StrictYAML in the same way. 15 | 16 | While not all YAML output by other programs will be readable by StrictYAML (it is, after all, stricter), a lot will be. 17 | 18 | The features removed from the YAML spec, and their rationales are as follows: 19 | 20 | - [Implicit Typing](../../why/implicit-typing-removed) 21 | - [Direct representations of objects](../../why/not-parse-direct-representations-of-python-objects) 22 | - [Explicit tags](../../why/explicit-tags-removed) 23 | - [Node anchors and refs](../../why/node-anchors-and-references-removed) 24 | - [Flow style](../../why/flow-style-removed) 25 | - [Duplicate Keys Disallowed](../../why/duplicate-keys-disallowed) 26 | 27 | -------------------------------------------------------------------------------- /docs/public/why-not/hjson.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use HJSON? 3 | --- 4 | 5 | !!! note "No longer supported" 6 | 7 | HJSON is no longer supported. 8 | 9 | 10 | [HJSON](http://hjson.org/) is an attempt at fixing the aforementioned lack of readability of JSON. 11 | 12 | It has the following criticisms of YAML: 13 | 14 | - JSON is easier to explain (compare the JSON and YAML specs). 15 | 16 | - JSON is not bloated (it does not have anchors, substitutions or concatenation). 17 | 18 | As with TOML's criticism, these are spot on. However, strictyaml fixes this by *cutting out those parts of the spec*, leaving something that is actually simpler than HJSON. 19 | 20 | It has another criticism: 21 | 22 | - JSON does not suffer from significant whitespace. 23 | 24 | This is not a valid criticism. 25 | 26 | Whitespace and indentation is meaningful to people parsing any kind of code and markup (why else would code which *doesn't* have meaningful whitespace use indentation as well?) so it *should* be meaningful to computers parsing. 27 | 28 | There is an initial 'usability hump' for first time users of languages which have significant whitespace *that were previously not used to significant whitespace* but this isn't especially hard to overcome - especially if you have a propery configured decent editor which is explicit about the use of whitespace. 29 | 30 | Python users often report this being a problem, but after using the language for a while usually come to prefer it since it keeps the code shorter and makes its intent clearer. 31 | -------------------------------------------------------------------------------- /docs/src/why-not/hjson.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use HJSON? 3 | --- 4 | 5 | !!! note "No longer supported" 6 | 7 | HJSON is no longer supported. 8 | 9 | 10 | [HJSON](http://hjson.org/) is an attempt at fixing the aforementioned lack of readability of JSON. 11 | 12 | It has the following criticisms of YAML: 13 | 14 | - JSON is easier to explain (compare the JSON and YAML specs). 15 | 16 | - JSON is not bloated (it does not have anchors, substitutions or concatenation). 17 | 18 | As with TOML's criticism, these are spot on. However, strictyaml fixes this by *cutting out those parts of the spec*, leaving something that is actually simpler than HJSON. 19 | 20 | It has another criticism: 21 | 22 | - JSON does not suffer from significant whitespace. 23 | 24 | This is not a valid criticism. 25 | 26 | Whitespace and indentation is meaningful to people parsing any kind of code and markup (why else would code which *doesn't* have meaningful whitespace use indentation as well?) so it *should* be meaningful to computers parsing. 27 | 28 | There is an initial 'usability hump' for first time users of languages which have significant whitespace *that were previously not used to significant whitespace* but this isn't especially hard to overcome - especially if you have a propery configured decent editor which is explicit about the use of whitespace. 29 | 30 | Python users often report this being a problem, but after using the language for a while usually come to prefer it since it keeps the code shorter and makes its intent clearer. 31 | -------------------------------------------------------------------------------- /docs/public/why-not/json-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use JSON Schema for validation? 3 | --- 4 | 5 | JSON schema can also be used to validate YAML. This presumes that 6 | you might want to use jsonschema and yaml together. 7 | 8 | StrictYAML was inspired by the frustration of trying to use 9 | pyyaml with [pykwalify](https://pykwalify.readthedocs.io/), 10 | in fact. 11 | 12 | ## Loss of line numbers 13 | 14 | Because parsing first with pyyaml and then passing the result 15 | to pykwalify loses line numbers, validation errors lose the 16 | line number where the validation error occurred. 17 | 18 | This makes tracking down the location of errors tricky. If 19 | a schema is a little repetitive it can make tracking down 20 | the exact location of the error hellish. 21 | 22 | # Simpler errors in StrictYAML 23 | 24 | StrictYAML has an emphasis on the friendliness of schema 25 | validation errors. Ideally every schema validation error 26 | should be extremely obvious and show only the information 27 | necessary. 28 | 29 | # StrictYAML schemas are more flexible 30 | 31 | Because schemas are written in python and strictyaml allows 32 | revalidation, strictyaml schemas are much more flexible: 33 | 34 | Example: 35 | 36 | ```python 37 | from strictyaml import load, Seq, Enum 38 | import pycountry # updated list of country codes 39 | 40 | 41 | load( 42 | strictyaml_string, 43 | Map( 44 | { 45 | "countries": Seq(Enum([country.alpha_2 for country in pycountry.countries])) 46 | } 47 | ) 48 | ) 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/src/why-not/json-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use JSON Schema for validation? 3 | --- 4 | 5 | JSON schema can also be used to validate YAML. This presumes that 6 | you might want to use jsonschema and yaml together. 7 | 8 | StrictYAML was inspired by the frustration of trying to use 9 | pyyaml with [pykwalify](https://pykwalify.readthedocs.io/), 10 | in fact. 11 | 12 | ## Loss of line numbers 13 | 14 | Because parsing first with pyyaml and then passing the result 15 | to pykwalify loses line numbers, validation errors lose the 16 | line number where the validation error occurred. 17 | 18 | This makes tracking down the location of errors tricky. If 19 | a schema is a little repetitive it can make tracking down 20 | the exact location of the error hellish. 21 | 22 | # Simpler errors in StrictYAML 23 | 24 | StrictYAML has an emphasis on the friendliness of schema 25 | validation errors. Ideally every schema validation error 26 | should be extremely obvious and show only the information 27 | necessary. 28 | 29 | # StrictYAML schemas are more flexible 30 | 31 | Because schemas are written in python and strictyaml allows 32 | revalidation, strictyaml schemas are much more flexible: 33 | 34 | Example: 35 | 36 | ```python 37 | from strictyaml import load, Seq, Enum 38 | import pycountry # updated list of country codes 39 | 40 | 41 | load( 42 | strictyaml_string, 43 | Map( 44 | { 45 | "countries": Seq(Enum([country.alpha_2 for country in pycountry.countries])) 46 | } 47 | ) 48 | ) 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/src/why/speed-not-a-priority.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why is parsing speed not a high priority for StrictYAML? 3 | --- 4 | 5 | JSON and StrictYAML are essentially complementary formats. They both allow 6 | a relatively loose representation of data that just contains, mappings and 7 | sequences. They are serialization formats that are relatively straightforward 8 | for both humans and machines to read and write. 9 | 10 | The main difference is simply one of degree: 11 | 12 | JSON is primarily optimized for *machine* readability and writability, while 13 | still maintaining human readability. 14 | 15 | YAML is optimized for *human* readability and writability, while maintaining 16 | machine readability and writability. 17 | 18 | This means that the two formats are better suited to slightly different applications. 19 | For instance, JSON is better suited as a format for use with REST APIs while 20 | YAML is better suited as a format for use by configuration languages and DSLs. 21 | 22 | If you are using YAML primarily as a readable medium to express a markup language 23 | or represent configuration in, this probably means that 1) what you are reading is 24 | probably relatively short (e.g. < 1,000 lines) and 2) it will be read/written 25 | infrequently (e.g. once, when a program starts). 26 | 27 | For this reason, it is assumed that for most StrictYAML applications, parsing 28 | speed is of a lower importance than strictness, readability and ease of use. 29 | 30 | That being said, any requests that improve parsing or writing speed are welcome. 31 | -------------------------------------------------------------------------------- /docs/public/why/speed-not-a-priority.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why is parsing speed not a high priority for StrictYAML? 3 | --- 4 | 5 | JSON and StrictYAML are essentially complementary formats. They both allow 6 | a relatively loose representation of data that just contains, mappings and 7 | sequences. They are serialization formats that are relatively straightforward 8 | for both humans and machines to read and write. 9 | 10 | The main difference is simply one of degree: 11 | 12 | JSON is primarily optimized for *machine* readability and writability, while 13 | still maintaining human readability. 14 | 15 | YAML is optimized for *human* readability and writability, while maintaining 16 | machine readability and writability. 17 | 18 | This means that the two formats are better suited to slightly different applications. 19 | For instance, JSON is better suited as a format for use with REST APIs while 20 | YAML is better suited as a format for use by configuration languages and DSLs. 21 | 22 | If you are using YAML primarily as a readable medium to express a markup language 23 | or represent configuration in, this probably means that 1) what you are reading is 24 | probably relatively short (e.g. < 1,000 lines) and 2) it will be read/written 25 | infrequently (e.g. once, when a program starts). 26 | 27 | For this reason, it is assumed that for most StrictYAML applications, parsing 28 | speed is of a lower importance than strictness, readability and ease of use. 29 | 30 | That being said, any requests that improve parsing or writing speed are welcome. 31 | -------------------------------------------------------------------------------- /hitch/story/non-schema-validation.story: -------------------------------------------------------------------------------- 1 | Parsing YAML without a schema: 2 | docs: howto/without-a-schema 3 | based on: strictyaml 4 | description: | 5 | When using strictyaml you do not have to specify a schema. If 6 | you do this, the validator "Any" is used which will accept any 7 | mapping and any list and any scalar values (which will always be 8 | interpreted as a string, unlike regular YAML). 9 | 10 | This is the recommended approach when rapidly prototyping and the 11 | desired schema is fluid. 12 | 13 | When your prototype code is parsing YAML that has a more fixed 14 | structure, we recommend that you 'lock it down' with a schema. 15 | 16 | The Any validator can be used inside fixed structures as well. 17 | given: 18 | setup: | 19 | from strictyaml import Str, Any, MapPattern, load 20 | from ensure import Ensure 21 | yaml_snippet: | 22 | a: 23 | x: 9 24 | y: 8 25 | b: 2 26 | c: 3 27 | variations: 28 | Parse without validator: 29 | steps: 30 | - Run: | 31 | Ensure(load(yaml_snippet)).equals({"a": {"x": "9", "y": "8"}, "b": "2", "c": "3"}) 32 | 33 | Parse with any validator - equivalent: 34 | steps: 35 | - Run: | 36 | Ensure(load(yaml_snippet, Any())).equals({"a": {"x": "9", "y": "8"}, "b": "2", "c": "3"}) 37 | 38 | Fix higher levels of schema: 39 | steps: 40 | - Run: | 41 | Ensure(load(yaml_snippet, MapPattern(Str(), Any()))).equals({"a": {"x": "9", "y": "8"}, "b": "2", "c": "3"}) 42 | -------------------------------------------------------------------------------- /hitch/story/scalar-string.story: -------------------------------------------------------------------------------- 1 | Parsing strings (Str): 2 | docs: scalar/string 3 | based on: strictyaml 4 | description: | 5 | StrictYAML parses to a YAML object, not 6 | the value directly to give you more flexibility 7 | and control over what you can do with the YAML. 8 | 9 | This is what that can object can do - in most 10 | cases if parsed as a string, it will behave in 11 | the same way. 12 | given: 13 | setup: | 14 | from strictyaml import Str, Map, load 15 | from ensure import Ensure 16 | 17 | schema = Map({"a": Str(), "b": Str(), "c": Str(), "d": Str()}) 18 | 19 | parsed = load(yaml_snippet, schema) 20 | yaml_snippet: | 21 | a: 1 22 | b: yes 23 | c: â string 24 | d: | 25 | multiline string 26 | variations: 27 | Parses correctly: 28 | steps: 29 | - Run: 30 | code: | 31 | Ensure(parsed).equals( 32 | {"a": "1", "b": "yes", "c": u"â string", "d": "multiline string\n"} 33 | ) 34 | 35 | Dict lookup cast to string: 36 | steps: 37 | - Run: 38 | code: Ensure(str(parsed["a"])).equals("1") 39 | 40 | Dict lookup cast to int: 41 | steps: 42 | - Run: 43 | code: | 44 | Ensure(int(parsed["a"])).equals(1) 45 | 46 | Dict lookup cast to bool impossible: 47 | steps: 48 | - Run: 49 | code: bool(parsed["a"]) 50 | raises: 51 | message: |- 52 | Cannot cast 'YAML(1)' to bool. 53 | Use bool(yamlobj.data) or bool(yamlobj.text) instead. 54 | 55 | -------------------------------------------------------------------------------- /hitch/story/scalar-hexadecimal-integer.story: -------------------------------------------------------------------------------- 1 | Hexadecimal Integers (HexInt): 2 | docs: scalar/hexadecimal-integer 3 | based on: strictyaml 4 | description: | 5 | StrictYAML can interpret a hexadecimal integer 6 | preserving its value 7 | given: 8 | yaml_snippet: | 9 | x: 0x1a 10 | setup: | 11 | from strictyaml import Map, HexInt, load 12 | from ensure import Ensure 13 | 14 | schema = Map({"x": HexInt()}) 15 | 16 | parsed = load(yaml_snippet, schema) 17 | 18 | variations: 19 | Parsed correctly: 20 | steps: 21 | - Run: | 22 | Ensure(parsed).equals({"x": 26}) 23 | Ensure(parsed.as_yaml()).equals("x: 0x1a\n") 24 | 25 | Uppercase: 26 | given: 27 | yaml_snippet: | 28 | x: 0X1A 29 | steps: 30 | - Run: 31 | code: | 32 | Ensure(load(yaml_snippet, schema).data).equals({"x": 26}) 33 | Ensure(load(yaml_snippet, schema).as_yaml()).equals("x: 0X1A\n") 34 | 35 | Invalid scalar hexadecimal integer: 36 | based on: strictyaml 37 | given: 38 | yaml_snippet: | 39 | x: some_string 40 | setup: | 41 | from strictyaml import Map, HexInt, load 42 | 43 | schema = Map({"x": HexInt()}) 44 | steps: 45 | - Run: 46 | code: load(yaml_snippet, schema) 47 | raises: 48 | type: strictyaml.exceptions.YAMLValidationError 49 | message: |- 50 | when expecting a hexadecimal integer 51 | found arbitrary text 52 | in "", line 1, column 1: 53 | x: some_string 54 | ^ (line: 1) 55 | -------------------------------------------------------------------------------- /hitch/story/single-value.story: -------------------------------------------------------------------------------- 1 | Single value: 2 | based on: strictyaml 3 | description: | 4 | The minimal YAML document that is parsed by StrictYAML is 5 | a string of characters which parses by default to a string 6 | unless a scalar validator is used. 7 | 8 | Where standard YAML implicitly converts certain strings 9 | to other types, StrictYAML will only parse to strings 10 | unless otherwise directed. 11 | given: 12 | setup: | 13 | from strictyaml import Str, Int, load 14 | variations: 15 | Raise exception on None: 16 | steps: 17 | - Run: 18 | code: load(None, Str()) 19 | raises: 20 | message: StrictYAML can only read a string of valid YAML. 21 | 22 | String of 1: 23 | steps: 24 | - Run: 25 | code: | 26 | assert load("1", Str()) == "1" 27 | 28 | Int of 1: 29 | steps: 30 | - Run: 31 | code: | 32 | assert load("1", Int()) == 1 33 | 34 | Empty value parsed as blank string by default: 35 | steps: 36 | - Run: 37 | code: | 38 | assert load("x:") == {"x": ""} 39 | 40 | Empty document parsed as blank string by default: 41 | steps: 42 | - Run: 43 | code: | 44 | assert load("", Str()) == "" 45 | 46 | Null parsed as string null by default: 47 | steps: 48 | - Run: 49 | code: | 50 | assert load("null: null") == {"null": "null"} 51 | 52 | #Single value with comment: 53 | #steps: 54 | #- Run: 55 | #code: | 56 | #assert load("# ought not to be parsed\nstring") == "string" 57 | -------------------------------------------------------------------------------- /docs/public/using/alpha/scalar/string.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Parsing strings (Str) 3 | --- 4 | 5 | 6 | StrictYAML parses to a YAML object, not 7 | the value directly to give you more flexibility 8 | and control over what you can do with the YAML. 9 | 10 | This is what that can object can do - in most 11 | cases if parsed as a string, it will behave in 12 | the same way. 13 | 14 | 15 | Example yaml_snippet: 16 | 17 | ```yaml 18 | a: 1 19 | b: yes 20 | c: â string 21 | d: | 22 | multiline string 23 | 24 | ``` 25 | 26 | 27 | ```python 28 | from strictyaml import Str, Map, load 29 | from ensure import Ensure 30 | 31 | schema = Map({"a": Str(), "b": Str(), "c": Str(), "d": Str()}) 32 | 33 | parsed = load(yaml_snippet, schema) 34 | 35 | ``` 36 | 37 | 38 | 39 | Parses correctly: 40 | 41 | 42 | ```python 43 | Ensure(parsed).equals( 44 | {"a": "1", "b": "yes", "c": u"â string", "d": "multiline string\n"} 45 | ) 46 | 47 | ``` 48 | 49 | 50 | 51 | 52 | Dict lookup cast to string: 53 | 54 | 55 | ```python 56 | Ensure(str(parsed["a"])).equals("1") 57 | ``` 58 | 59 | 60 | 61 | 62 | Dict lookup cast to int: 63 | 64 | 65 | ```python 66 | Ensure(int(parsed["a"])).equals(1) 67 | 68 | ``` 69 | 70 | 71 | 72 | 73 | Dict lookup cast to bool impossible: 74 | 75 | 76 | ```python 77 | bool(parsed["a"]) 78 | ``` 79 | 80 | 81 | ```python 82 | : 83 | Cannot cast 'YAML(1)' to bool. 84 | Use bool(yamlobj.data) or bool(yamlobj.text) instead. 85 | ``` 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | !!! note "Executable specification" 94 | 95 | Documentation automatically generated from 96 | scalar-string.story 97 | storytests. -------------------------------------------------------------------------------- /docs/public/using/alpha/compound/mapping-with-slug-keys.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mapping with defined keys and a custom key validator (Map) 3 | --- 4 | 5 | !!! warning "Experimental" 6 | 7 | This feature is in alpha. The API may change on a minor version increment. 8 | 9 | 10 | A typical mapping except that the key values are determined 11 | by the value provided by the validator. 12 | 13 | 14 | Example yaml_snippet: 15 | 16 | ```yaml 17 | Name: United Kingdom 18 | country-code: GB 19 | DIAL CODE: +44 20 | official languages: 21 | - English 22 | - Welsh 23 | 24 | ``` 25 | 26 | 27 | ```python 28 | from collections import OrderedDict 29 | from strictyaml import Map, Optional, Str, Seq, load, ScalarValidator 30 | from ensure import Ensure 31 | 32 | # This example uses slugify from the "python-slugify" package 33 | from slugify import slugify 34 | 35 | class Slug(ScalarValidator): 36 | def validate_scalar(self, chunk): 37 | return slugify(unicode(chunk.contents)) 38 | 39 | schema = Map({ 40 | "name": Str(), 41 | Optional("country-code"): Str(), 42 | "dial-code": Str(), 43 | "official-languages": Seq(Str()) 44 | }, key_validator=Slug()) 45 | 46 | ``` 47 | 48 | 49 | 50 | ```python 51 | Ensure(load(yaml_snippet, schema).data).equals( 52 | { 53 | "name": "United Kingdom", 54 | "country-code": "GB", 55 | "dial-code": "+44", 56 | "official-languages": ["English", "Welsh"], 57 | } 58 | ) 59 | 60 | ``` 61 | 62 | 63 | 64 | 65 | 66 | 67 | !!! note "Executable specification" 68 | 69 | Documentation automatically generated from 70 | map-with-key-validator.story 71 | storytests. -------------------------------------------------------------------------------- /docs/public/why-not/python-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use Python's schema library (or similar) for validation? 3 | --- 4 | 5 | Python's 'schema' (as well as similar libraries) can also be used to validate 6 | the structure of objects. Validating YAML is even [cited as a reason on their 7 | README](https://github.com/keleshev/schema). 8 | 9 | Using a schema for validation requires running the YAML through a parser 10 | first which and then taking the output (usually a data structure like a dict) 11 | and passing it through the schema. 12 | 13 | Unfortunately there are a number of problems with this approach: 14 | 15 | 16 | ## You still have [the Norway Problem](../../why/implicit-typing-removed) 17 | 18 | If the standard YAML parser parses 'NO' as false or [empty string as 19 | None](https://github.com/Grokzen/pykwalify/issues/77) then it doesn't 20 | really matter if the schema says an empty string or the text 'NO' is 21 | okay, it will be seeing a 'None' or a 'False' which will cause a failure. 22 | 23 | 24 | ## You can't get line numbers and snippets for the validation errors 25 | 26 | Assuming you've successfully circumvented the Norway problem, parsing 27 | and feeding the output to schema is still problematic. If you pass a 28 | parsed dict to schema, schema can't tell which line number the failure 29 | happened on and can't give you a code snippet highlighting where it 30 | happened. 31 | 32 | 33 | ## Roundtripping becomes very very difficult if not impossible 34 | 35 | Due to the loss of metadata about parsed YAML being lost when it 36 | is fed into a generic schema validator, it also becomes impossible to 37 | to *change* the data and serialize it without losing critical 38 | details (i.e. mapping ordering, comments or validation structures). 39 | -------------------------------------------------------------------------------- /docs/src/why-not/python-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why not use Python's schema library (or similar) for validation? 3 | --- 4 | 5 | Python's 'schema' (as well as similar libraries) can also be used to validate 6 | the structure of objects. Validating YAML is even [cited as a reason on their 7 | README](https://github.com/keleshev/schema). 8 | 9 | Using a schema for validation requires running the YAML through a parser 10 | first which and then taking the output (usually a data structure like a dict) 11 | and passing it through the schema. 12 | 13 | Unfortunately there are a number of problems with this approach: 14 | 15 | 16 | ## You still have [the Norway Problem](../../why/implicit-typing-removed) 17 | 18 | If the standard YAML parser parses 'NO' as false or [empty string as 19 | None](https://github.com/Grokzen/pykwalify/issues/77) then it doesn't 20 | really matter if the schema says an empty string or the text 'NO' is 21 | okay, it will be seeing a 'None' or a 'False' which will cause a failure. 22 | 23 | 24 | ## You can't get line numbers and snippets for the validation errors 25 | 26 | Assuming you've successfully circumvented the Norway problem, parsing 27 | and feeding the output to schema is still problematic. If you pass a 28 | parsed dict to schema, schema can't tell which line number the failure 29 | happened on and can't give you a code snippet highlighting where it 30 | happened. 31 | 32 | 33 | ## Roundtripping becomes very very difficult if not impossible 34 | 35 | Due to the loss of metadata about parsed YAML being lost when it 36 | is fed into a generic schema validator, it also becomes impossible to 37 | to *change* the data and serialize it without losing critical 38 | details (i.e. mapping ordering, comments or validation structures). 39 | -------------------------------------------------------------------------------- /strictyaml/ruamel/scalarbool.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import print_function, absolute_import, division, unicode_literals 4 | 5 | """ 6 | You cannot subclass bool, and this is necessary for round-tripping anchored 7 | bool values (and also if you want to preserve the original way of writing) 8 | 9 | bool.__bases__ is type 'int', so that is what is used as the basis for ScalarBoolean as well. 10 | 11 | You can use these in an if statement, but not when testing equivalence 12 | """ 13 | 14 | from strictyaml.ruamel.anchor import Anchor 15 | 16 | if False: # MYPY 17 | from typing import Text, Any, Dict, List # NOQA 18 | 19 | __all__ = ["ScalarBoolean"] 20 | 21 | # no need for no_limit_int -> int 22 | 23 | 24 | class ScalarBoolean(int): 25 | def __new__(cls, *args, **kw): 26 | # type: (Any, Any, Any) -> Any 27 | anchor = kw.pop("anchor", None) # type: ignore 28 | b = int.__new__(cls, *args, **kw) # type: ignore 29 | if anchor is not None: 30 | b.yaml_set_anchor(anchor, always_dump=True) 31 | return b 32 | 33 | @property 34 | def anchor(self): 35 | # type: () -> Any 36 | if not hasattr(self, Anchor.attrib): 37 | setattr(self, Anchor.attrib, Anchor()) 38 | return getattr(self, Anchor.attrib) 39 | 40 | def yaml_anchor(self, any=False): 41 | # type: (bool) -> Any 42 | if not hasattr(self, Anchor.attrib): 43 | return None 44 | if any or self.anchor.always_dump: 45 | return self.anchor 46 | return None 47 | 48 | def yaml_set_anchor(self, value, always_dump=False): 49 | # type: (Any, bool) -> None 50 | self.anchor.value = value 51 | self.anchor.always_dump = always_dump 52 | -------------------------------------------------------------------------------- /hitch/story/validator-repr.story: -------------------------------------------------------------------------------- 1 | Validator repr: 2 | based on: strictyaml 3 | description: | 4 | When repr(x) is called on validators it should print an 5 | executable representation of the object. 6 | given: 7 | setup: | 8 | import strictyaml as sy 9 | 10 | variations: 11 | Int: 12 | steps: 13 | - Run: 14 | code: | 15 | assert repr(sy.Map({"a": sy.Int()})) == """Map({'a': Int()})""" 16 | 17 | Optional: 18 | steps: 19 | - Run: 20 | code: | 21 | assert repr(sy.Map({sy.Optional("a"): sy.Int()})) == """Map({Optional("a"): Int()})""" 22 | 23 | Sequence: 24 | steps: 25 | - Run: 26 | code: | 27 | assert repr(sy.Seq(sy.Str())) == """Seq(Str())""" 28 | 29 | Empty: 30 | steps: 31 | - Run: 32 | code: | 33 | assert repr(sy.FixedSeq([sy.EmptyNone(), sy.EmptyDict(), sy.EmptyList()])) == """FixedSeq([EmptyNone(), EmptyDict(), EmptyList()])""" 34 | 35 | UniqueSeq Decimal: 36 | steps: 37 | - Run: 38 | code: | 39 | assert repr(sy.UniqueSeq(sy.Decimal())) == """UniqueSeq(Decimal())""" 40 | 41 | MapPattern Bool Enum: 42 | steps: 43 | - Run: 44 | code: | 45 | assert repr(sy.MapPattern(sy.Bool(), sy.Enum(["x", "y"]))) == "MapPattern(Bool(), Enum(['x', 'y']))" 46 | 47 | Seq Datetime Any Or: 48 | steps: 49 | - Run: 50 | code: | 51 | assert repr(sy.Seq(sy.Datetime() | sy.Any())) == """Seq(Datetime() | Any())""" 52 | 53 | Comma Separated Float: 54 | steps: 55 | - Run: 56 | code: | 57 | assert repr(sy.Map({"x": sy.CommaSeparated(sy.Float())})) == "Map({'x': CommaSeparated(Float())})" 58 | 59 | -------------------------------------------------------------------------------- /docs/public/using/alpha/howto/without-a-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Parsing YAML without a schema 3 | --- 4 | 5 | 6 | When using strictyaml you do not have to specify a schema. If 7 | you do this, the validator "Any" is used which will accept any 8 | mapping and any list and any scalar values (which will always be 9 | interpreted as a string, unlike regular YAML). 10 | 11 | This is the recommended approach when rapidly prototyping and the 12 | desired schema is fluid. 13 | 14 | When your prototype code is parsing YAML that has a more fixed 15 | structure, we recommend that you 'lock it down' with a schema. 16 | 17 | The Any validator can be used inside fixed structures as well. 18 | 19 | 20 | Example yaml_snippet: 21 | 22 | ```yaml 23 | a: 24 | x: 9 25 | y: 8 26 | b: 2 27 | c: 3 28 | 29 | ``` 30 | 31 | 32 | ```python 33 | from strictyaml import Str, Any, MapPattern, load 34 | from ensure import Ensure 35 | 36 | ``` 37 | 38 | 39 | 40 | Parse without validator: 41 | 42 | 43 | ```python 44 | Ensure(load(yaml_snippet)).equals({"a": {"x": "9", "y": "8"}, "b": "2", "c": "3"}) 45 | 46 | ``` 47 | 48 | 49 | 50 | 51 | Parse with any validator - equivalent: 52 | 53 | 54 | ```python 55 | Ensure(load(yaml_snippet, Any())).equals({"a": {"x": "9", "y": "8"}, "b": "2", "c": "3"}) 56 | 57 | ``` 58 | 59 | 60 | 61 | 62 | Fix higher levels of schema: 63 | 64 | 65 | ```python 66 | Ensure(load(yaml_snippet, MapPattern(Str(), Any()))).equals({"a": {"x": "9", "y": "8"}, "b": "2", "c": "3"}) 67 | 68 | ``` 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | !!! note "Executable specification" 77 | 78 | Documentation automatically generated from 79 | non-schema-validation.story 80 | storytests. -------------------------------------------------------------------------------- /docs/public/using/alpha/compound/optional-keys-with-defaults.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Optional keys with defaults (Map/Optional) 3 | --- 4 | 5 | !!! warning "Experimental" 6 | 7 | This feature is in alpha. The API may change on a minor version increment. 8 | 9 | 10 | 11 | 12 | Example yaml_snippet: 13 | 14 | ```yaml 15 | a: 1 16 | 17 | ``` 18 | 19 | 20 | ```python 21 | from strictyaml import Map, Int, Str, Bool, EmptyNone, Optional, load, as_document 22 | from collections import OrderedDict 23 | from ensure import Ensure 24 | 25 | schema = Map({"a": Int(), Optional("b", default=False): Bool(), }) 26 | 27 | ``` 28 | 29 | 30 | 31 | When parsed the result will include the optional value: 32 | 33 | 34 | ```python 35 | Ensure(load(yaml_snippet, schema).data).equals(OrderedDict([("a", 1), ("b", False)])) 36 | 37 | ``` 38 | 39 | 40 | 41 | 42 | If parsed and then output to YAML again the default data won't be there: 43 | 44 | 45 | ```python 46 | print(load(yaml_snippet, schema).as_yaml()) 47 | ``` 48 | 49 | ```yaml 50 | a: 1 51 | ``` 52 | 53 | 54 | 55 | 56 | When default data is output to YAML it is removed: 57 | 58 | 59 | ```python 60 | print(as_document({"a": 1, "b": False}, schema).as_yaml()) 61 | 62 | ``` 63 | 64 | ```yaml 65 | a: 1 66 | ``` 67 | 68 | 69 | 70 | 71 | When you want a key to stay and default to None: 72 | 73 | 74 | ```python 75 | schema = Map({"a": Int(), Optional("b", default=None, drop_if_none=False): EmptyNone() | Bool(), }) 76 | Ensure(load(yaml_snippet, schema).data).equals(OrderedDict([("a", 1), ("b", None)])) 77 | 78 | ``` 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | !!! note "Executable specification" 87 | 88 | Documentation automatically generated from 89 | optional-with-defaults.story 90 | storytests. -------------------------------------------------------------------------------- /hitch/envirotest.py: -------------------------------------------------------------------------------- 1 | import pyenv 2 | import sys 3 | import random 4 | 5 | 6 | def run_test( 7 | pyenv_build, 8 | pyproject_toml, 9 | test_package, 10 | prerequisites, 11 | strategy_name, 12 | _storybook, 13 | _doctests, 14 | ): 15 | if strategy_name == "latest": 16 | strategies = [ 17 | lambda versions: versions[-1], 18 | ] 19 | elif strategy_name == "earliest": 20 | strategies = [ 21 | lambda versions: versions[0], 22 | ] 23 | elif strategy_name in ("full", "maxi"): 24 | strategies = ( 25 | [ 26 | lambda versions: versions[0], 27 | ] 28 | + [random.choice] * 2 29 | if strategy_name == "full" 30 | else 5 + [lambda versions: versions[-1]] 31 | ) 32 | else: 33 | raise Exception(f"Strategy name {strategy_name} not found") 34 | 35 | for strategy in strategies: 36 | envirotestvenv = pyenv.EnvirotestVirtualenv( 37 | pyenv_build=pyenv_build, 38 | pyproject_toml=pyproject_toml, 39 | picker=strategy, 40 | test_package=test_package, 41 | prerequisites=prerequisites, 42 | ) 43 | envirotestvenv.build() 44 | 45 | python_path = envirotestvenv.venv.python_path 46 | 47 | _doctests(python_path) 48 | results = ( 49 | _storybook(python_path=python_path) 50 | .only_uninherited() 51 | .ordered_by_name() 52 | .play() 53 | ) 54 | if not results.all_passed: 55 | print("FAILED") 56 | print("COPY the following into hitch/devenv.yml:\n\n") 57 | print("python version: {}".format(envirotestvenv.python_version)) 58 | print("packages:") 59 | for package, version in envirotestvenv.picked_versions.items(): 60 | print(" {}: {}".format(package, version)) 61 | sys.exit(1) 62 | -------------------------------------------------------------------------------- /docs/public/using/alpha/restrictions/duplicate-keys.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Duplicate keys 3 | --- 4 | 5 | 6 | Duplicate keys are allowed in regular YAML - as parsed by pyyaml, ruamel.yaml and poyo: 7 | 8 | Not only is it unclear whether x should be "cow" or "bull" (the parser will decide 'bull', but did you know that?), 9 | if there are 200 lines between x: cow and x: bull, a user might very likely change the *first* x and erroneously believe 10 | that the resulting value of x has been changed - when it has not. 11 | 12 | In order to avoid all possible confusion, StrictYAML will simply refuse to parse this and will only accept associative 13 | arrays where all of the keys are unique. It will throw a DuplicateKeysDisallowed exception. 14 | 15 | 16 | Example yaml_snippet: 17 | 18 | ```yaml 19 | a: cow 20 | a: bull 21 | 22 | ``` 23 | 24 | 25 | ```python 26 | from strictyaml import load, DuplicateKeysDisallowed 27 | 28 | ``` 29 | 30 | 31 | 32 | Nameless exception: 33 | 34 | 35 | ```python 36 | load(yaml_snippet) 37 | ``` 38 | 39 | 40 | ```python 41 | strictyaml.exceptions.DuplicateKeysDisallowed: 42 | While parsing 43 | in "", line 2, column 1: 44 | a: bull 45 | ^ (line: 2) 46 | Duplicate key 'a' found 47 | in "", line 2, column 2: 48 | a: bull 49 | ^ (line: 2) 50 | ``` 51 | 52 | 53 | 54 | 55 | Named exception: 56 | 57 | 58 | ```python 59 | load(yaml_snippet, label="mylabel") 60 | ``` 61 | 62 | 63 | ```python 64 | strictyaml.exceptions.DuplicateKeysDisallowed: 65 | While parsing 66 | in "mylabel", line 2, column 1: 67 | a: bull 68 | ^ (line: 2) 69 | Duplicate key 'a' found 70 | in "mylabel", line 2, column 2: 71 | a: bull 72 | ^ (line: 2) 73 | ``` 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | !!! note "Executable specification" 82 | 83 | Documentation automatically generated from 84 | duplicatekeys.story 85 | storytests. -------------------------------------------------------------------------------- /hitch/story/duplicatekeys.story: -------------------------------------------------------------------------------- 1 | Duplicate keys: 2 | docs: restrictions/duplicate-keys 3 | based on: strictyaml 4 | description: | 5 | Duplicate keys are allowed in regular YAML - as parsed by pyyaml, ruamel.yaml and poyo: 6 | 7 | Not only is it unclear whether x should be "cow" or "bull" (the parser will decide 'bull', but did you know that?), 8 | if there are 200 lines between x: cow and x: bull, a user might very likely change the *first* x and erroneously believe 9 | that the resulting value of x has been changed - when it has not. 10 | 11 | In order to avoid all possible confusion, StrictYAML will simply refuse to parse this and will only accept associative 12 | arrays where all of the keys are unique. It will throw a DuplicateKeysDisallowed exception. 13 | given: 14 | setup: | 15 | from strictyaml import load, DuplicateKeysDisallowed 16 | yaml_snippet: | 17 | a: cow 18 | a: bull 19 | variations: 20 | Nameless exception: 21 | steps: 22 | - Run: 23 | code: load(yaml_snippet) 24 | raises: 25 | type: strictyaml.exceptions.DuplicateKeysDisallowed 26 | message: |- 27 | While parsing 28 | in "", line 2, column 1: 29 | a: bull 30 | ^ (line: 2) 31 | Duplicate key 'a' found 32 | in "", line 2, column 2: 33 | a: bull 34 | ^ (line: 2) 35 | 36 | 37 | Named exception: 38 | steps: 39 | - Run: 40 | code: load(yaml_snippet, label="mylabel") 41 | raises: 42 | type: strictyaml.exceptions.DuplicateKeysDisallowed 43 | message: |- 44 | While parsing 45 | in "mylabel", line 2, column 1: 46 | a: bull 47 | ^ (line: 2) 48 | Duplicate key 'a' found 49 | in "mylabel", line 2, column 2: 50 | a: bull 51 | ^ (line: 2) 52 | -------------------------------------------------------------------------------- /strictyaml/__init__.py: -------------------------------------------------------------------------------- 1 | # The all important loader 2 | from strictyaml.parser import load 3 | from strictyaml.parser import dirty_load 4 | 5 | # Document builder 6 | from strictyaml.parser import as_document 7 | 8 | # YAML object 9 | from strictyaml.representation import YAML 10 | 11 | # Validators 12 | from strictyaml.validators import Validator 13 | from strictyaml.validators import OrValidator 14 | from strictyaml.any_validator import Any 15 | from strictyaml.scalar import ScalarValidator 16 | from strictyaml.scalar import Enum 17 | from strictyaml.scalar import Regex 18 | from strictyaml.scalar import Email 19 | from strictyaml.scalar import Url 20 | from strictyaml.scalar import Str 21 | from strictyaml.scalar import Int 22 | from strictyaml.scalar import HexInt 23 | from strictyaml.scalar import Bool 24 | from strictyaml.scalar import Float 25 | from strictyaml.scalar import Decimal 26 | from strictyaml.scalar import Datetime 27 | from strictyaml.scalar import CommaSeparated 28 | from strictyaml.scalar import NullNone 29 | from strictyaml.scalar import EmptyNone 30 | from strictyaml.scalar import EmptyDict 31 | from strictyaml.scalar import EmptyList 32 | from strictyaml.compound import Optional 33 | from strictyaml.compound import Map 34 | from strictyaml.compound import MapPattern 35 | from strictyaml.compound import MapCombined 36 | from strictyaml.compound import Seq 37 | from strictyaml.compound import UniqueSeq 38 | from strictyaml.compound import FixedSeq 39 | 40 | # Base exception from strictyaml.ruamel (all exceptions inherit from this) 41 | from strictyaml.ruamel import YAMLError 42 | 43 | # Exceptions 44 | from strictyaml.exceptions import StrictYAMLError 45 | from strictyaml.exceptions import YAMLValidationError 46 | 47 | # Disallowed token exceptions 48 | from strictyaml.exceptions import DisallowedToken 49 | 50 | from strictyaml.exceptions import TagTokenDisallowed 51 | from strictyaml.exceptions import FlowMappingDisallowed 52 | from strictyaml.exceptions import AnchorTokenDisallowed 53 | from strictyaml.exceptions import DuplicateKeysDisallowed 54 | from strictyaml import exceptions 55 | 56 | __version__ = "1.6.2" 57 | -------------------------------------------------------------------------------- /hitch/story/enum-with-item-validation.story: -------------------------------------------------------------------------------- 1 | Enum with item validation: 2 | docs: compound/map-pattern 3 | based on: strictyaml 4 | description: | 5 | See also: enum validation. 6 | 7 | Your enums can be a transformed string or something other than a string 8 | if you use an item validator. 9 | given: 10 | setup: | 11 | from strictyaml import Map, Enum, Int, MapPattern, YAMLValidationError, load 12 | from ensure import Ensure 13 | 14 | schema = Map({"a": Enum([1, 2, 3], item_validator=Int())}) 15 | variations: 16 | Parse correctly: 17 | given: 18 | yaml_snippet: 'a: 1' 19 | steps: 20 | - Run: 21 | code: | 22 | Ensure(load(yaml_snippet, schema)).equals({"a": 1}) 23 | 24 | Invalid because D is not an integer: 25 | given: 26 | yaml_snippet: 'a: D' 27 | steps: 28 | - Run: 29 | code: load(yaml_snippet, schema) 30 | raises: 31 | type: strictyaml.exceptions.YAMLValidationError 32 | message: |- 33 | when expecting an integer 34 | found arbitrary text 35 | in "", line 1, column 1: 36 | a: D 37 | ^ (line: 1) 38 | 39 | Invalid because 4 is not in enum: 40 | given: 41 | yaml_snippet: 'a: 4' 42 | steps: 43 | - Run: 44 | code: load(yaml_snippet, schema) 45 | raises: 46 | type: strictyaml.exceptions.YAMLValidationError 47 | message: "when expecting one of: 1, 2, 3\nfound an arbitrary integer\n\ 48 | \ in \"\", line 1, column 1:\n a: '4'\n ^ (line:\ 49 | \ 1)" 50 | Invalid because blank string is not in enum: 51 | given: 52 | yaml_snippet: 'a:' 53 | steps: 54 | - Run: 55 | code: load(yaml_snippet, schema) 56 | raises: 57 | type: strictyaml.exceptions.YAMLValidationError 58 | message: "when expecting an integer\nfound a blank string\n in \"\", line 1, column 1:\n a: ''\n ^ (line: 1)" 60 | -------------------------------------------------------------------------------- /strictyaml/ruamel/timestamp.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import print_function, absolute_import, division, unicode_literals 4 | 5 | import datetime 6 | import copy 7 | 8 | # ToDo: at least on PY3 you could probably attach the tzinfo correctly to the object 9 | # a more complete datetime might be used by safe loading as well 10 | 11 | if False: # MYPY 12 | from typing import Any, Dict, Optional, List # NOQA 13 | 14 | 15 | class TimeStamp(datetime.datetime): 16 | def __init__(self, *args, **kw): 17 | # type: (Any, Any) -> None 18 | self._yaml = dict(t=False, tz=None, delta=0) # type: Dict[Any, Any] 19 | 20 | def __new__(cls, *args, **kw): # datetime is immutable 21 | # type: (Any, Any) -> Any 22 | return datetime.datetime.__new__(cls, *args, **kw) # type: ignore 23 | 24 | def __deepcopy__(self, memo): 25 | # type: (Any) -> Any 26 | ts = TimeStamp( 27 | self.year, self.month, self.day, self.hour, self.minute, self.second 28 | ) 29 | ts._yaml = copy.deepcopy(self._yaml) 30 | return ts 31 | 32 | def replace( 33 | self, 34 | year=None, 35 | month=None, 36 | day=None, 37 | hour=None, 38 | minute=None, 39 | second=None, 40 | microsecond=None, 41 | tzinfo=True, 42 | fold=None, 43 | ): 44 | if year is None: 45 | year = self.year 46 | if month is None: 47 | month = self.month 48 | if day is None: 49 | day = self.day 50 | if hour is None: 51 | hour = self.hour 52 | if minute is None: 53 | minute = self.minute 54 | if second is None: 55 | second = self.second 56 | if microsecond is None: 57 | microsecond = self.microsecond 58 | if tzinfo is True: 59 | tzinfo = self.tzinfo 60 | if fold is None: 61 | fold = self.fold 62 | ts = type(self)( 63 | year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold 64 | ) 65 | ts._yaml = copy.deepcopy(self._yaml) 66 | return ts 67 | -------------------------------------------------------------------------------- /docs/public/why-not/turing-complete-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why shouldn't I just use Python code for configuration? 3 | --- 4 | 5 | It is relatively common for many applications to avoid the use of a different 6 | markup language for configuration and simply allow configuration to be 7 | done using code. One famous and unapologetic example of this is 8 | [Django](https://docs.djangoproject.com/en/1.10/ref/settings), which 9 | requires all configuration to be in a "settings.py" or similar file. 10 | 11 | This seems like a great idea from the outset - Python is more flexible 12 | than any configuration language, so, for instance, if you wanted to 13 | use a list comprehension or read a file or call an API to fill a value, 14 | you can. 15 | 16 | However, with this flexibility comes many traps and unsightly 17 | pitfalls. The Django pitfalls in particular are 18 | [cogently summed up by Ned Bachelder on 19 | his blog](http://nedbatchelder.com/blog/201112/duplicitous_django_settings.html) - 20 | pitfalls which have been the cause of countless bugs over the 21 | years. 22 | 23 | The language expressiveness trade off applies at every level in code 24 | 25 | - [We need less powerful languages](http://lukeplant.me.uk/blog/posts/less-powerful-languages/). 26 | - [Rule of least power (wikipedia)](https://en.wikipedia.org/wiki/Rule_of_least_power). 27 | - [Principle of least power by Tim Berners Lee](https://www.w3.org/DesignIssues/Principles.html#PLP). 28 | - [Principle of least power by Jeff Atwood (coding horror blogger / stack overflow founder)](https://blog.codinghorror.com/the-principle-of-least-power/). 29 | 30 | A good way of refactoring, in fact, is to take a large chunk of Turing-complete Python code that *can* be transformed directly into StrictYAML with no loss in expressiveness and and to transform it - for example, a list of translation strings, countries or parameters. 31 | 32 | It also makes it easier to have the markup generated by another program or a templating language. 33 | While you technically *can* do this with Turing-complete code, it will often lead to a debugging nightmare - [just ask C++ programmers](https://stackoverflow.com/questions/622659/what-are-the-good-and-bad-points-of-c-templates>)! 34 | -------------------------------------------------------------------------------- /docs/src/why-not/turing-complete-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why shouldn't I just use Python code for configuration? 3 | --- 4 | 5 | It is relatively common for many applications to avoid the use of a different 6 | markup language for configuration and simply allow configuration to be 7 | done using code. One famous and unapologetic example of this is 8 | [Django](https://docs.djangoproject.com/en/1.10/ref/settings), which 9 | requires all configuration to be in a "settings.py" or similar file. 10 | 11 | This seems like a great idea from the outset - Python is more flexible 12 | than any configuration language, so, for instance, if you wanted to 13 | use a list comprehension or read a file or call an API to fill a value, 14 | you can. 15 | 16 | However, with this flexibility comes many traps and unsightly 17 | pitfalls. The Django pitfalls in particular are 18 | [cogently summed up by Ned Bachelder on 19 | his blog](http://nedbatchelder.com/blog/201112/duplicitous_django_settings.html) - 20 | pitfalls which have been the cause of countless bugs over the 21 | years. 22 | 23 | The language expressiveness trade off applies at every level in code 24 | 25 | - [We need less powerful languages](http://lukeplant.me.uk/blog/posts/less-powerful-languages/). 26 | - [Rule of least power (wikipedia)](https://en.wikipedia.org/wiki/Rule_of_least_power). 27 | - [Principle of least power by Tim Berners Lee](https://www.w3.org/DesignIssues/Principles.html#PLP). 28 | - [Principle of least power by Jeff Atwood (coding horror blogger / stack overflow founder)](https://blog.codinghorror.com/the-principle-of-least-power/). 29 | 30 | A good way of refactoring, in fact, is to take a large chunk of Turing-complete Python code that *can* be transformed directly into StrictYAML with no loss in expressiveness and and to transform it - for example, a list of translation strings, countries or parameters. 31 | 32 | It also makes it easier to have the markup generated by another program or a templating language. 33 | While you technically *can* do this with Turing-complete code, it will often lead to a debugging nightmare - [just ask C++ programmers](https://stackoverflow.com/questions/622659/what-are-the-good-and-bad-points-of-c-templates>)! 34 | -------------------------------------------------------------------------------- /docs/public/using/alpha/compound/sequences-of-unique-items.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sequences of unique items (UniqueSeq) 3 | --- 4 | 5 | 6 | UniqueSeq validates sequences which contain no duplicate 7 | values. 8 | 9 | 10 | Example yaml_snippet: 11 | 12 | ```yaml 13 | - A 14 | - B 15 | - C 16 | 17 | ``` 18 | 19 | 20 | ```python 21 | from strictyaml import UniqueSeq, Str, load, as_document 22 | from ensure import Ensure 23 | 24 | schema = UniqueSeq(Str()) 25 | 26 | ``` 27 | 28 | 29 | 30 | Valid: 31 | 32 | 33 | ```python 34 | Ensure(load(yaml_snippet, schema)).equals(["A", "B", "C", ]) 35 | 36 | ``` 37 | 38 | 39 | 40 | 41 | Parsing with one dupe raises an exception: 42 | 43 | ```yaml 44 | - A 45 | - B 46 | - B 47 | 48 | ``` 49 | 50 | 51 | ```python 52 | load(yaml_snippet, schema) 53 | ``` 54 | 55 | 56 | ```python 57 | strictyaml.exceptions.YAMLValidationError: 58 | while parsing a sequence 59 | in "", line 1, column 1: 60 | - A 61 | ^ (line: 1) 62 | duplicate found 63 | in "", line 3, column 1: 64 | - B 65 | ^ (line: 3) 66 | ``` 67 | 68 | 69 | 70 | 71 | Parsing all dupes raises an exception: 72 | 73 | ```yaml 74 | - 3 75 | - 3 76 | - 3 77 | 78 | ``` 79 | 80 | 81 | ```python 82 | load(yaml_snippet, schema) 83 | ``` 84 | 85 | 86 | ```python 87 | strictyaml.exceptions.YAMLValidationError: 88 | while parsing a sequence 89 | in "", line 1, column 1: 90 | - '3' 91 | ^ (line: 1) 92 | duplicate found 93 | in "", line 3, column 1: 94 | - '3' 95 | ^ (line: 3) 96 | ``` 97 | 98 | 99 | 100 | 101 | Serializing with dupes raises an exception: 102 | 103 | 104 | ```python 105 | as_document(["A", "B", "B"], schema) 106 | 107 | ``` 108 | 109 | 110 | ```python 111 | strictyaml.exceptions.YAMLSerializationError: 112 | Expecting all unique items, but duplicates were found in '['A', 'B', 'B']'. 113 | ``` 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | !!! note "Executable specification" 122 | 123 | Documentation automatically generated from 124 | unique-sequence.story 125 | storytests. -------------------------------------------------------------------------------- /docs/public/using/alpha/scalar/integer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Integers (Int) 3 | --- 4 | 5 | 6 | StrictYAML parses to a YAML object, not 7 | the value directly to give you more flexibility 8 | and control over what you can do with the YAML. 9 | 10 | This is what that can object can do - in many 11 | cases if parsed as a integer, it will behave in 12 | the same way. 13 | 14 | 15 | Example yaml_snippet: 16 | 17 | ```yaml 18 | a: 1 19 | b: 5 20 | 21 | ``` 22 | 23 | 24 | ```python 25 | from strictyaml import Map, Int, load 26 | from ensure import Ensure 27 | 28 | schema = Map({"a": Int(), "b": Int()}) 29 | 30 | parsed = load(yaml_snippet, schema) 31 | 32 | ``` 33 | 34 | 35 | 36 | Parsed correctly: 37 | 38 | 39 | ```python 40 | Ensure(parsed).equals({"a": 1, "b": 5}) 41 | 42 | ``` 43 | 44 | 45 | 46 | 47 | Has underscores: 48 | 49 | ```yaml 50 | a: 10_000_000 51 | b: 10_0_0 52 | 53 | ``` 54 | 55 | 56 | ```python 57 | Ensure(load(yaml_snippet, schema).data).equals({"a": 10000000, "b": 1000}) 58 | 59 | ``` 60 | 61 | 62 | 63 | 64 | Cast with str: 65 | 66 | 67 | ```python 68 | Ensure(str(parsed["a"])).equals("1") 69 | ``` 70 | 71 | 72 | 73 | 74 | Cast with float: 75 | 76 | 77 | ```python 78 | Ensure(float(parsed["a"])).equals(1.0) 79 | ``` 80 | 81 | 82 | 83 | 84 | Greater than: 85 | 86 | 87 | ```python 88 | Ensure(parsed["a"] > 0).equals(True) 89 | ``` 90 | 91 | 92 | 93 | 94 | Less than: 95 | 96 | 97 | ```python 98 | Ensure(parsed["a"] < 2).equals(True) 99 | ``` 100 | 101 | 102 | 103 | 104 | To get actual int, use .data: 105 | 106 | 107 | ```python 108 | Ensure(type(load(yaml_snippet, schema)["a"].data) is int).equals(True) 109 | ``` 110 | 111 | 112 | 113 | 114 | Cannot cast to bool: 115 | 116 | 117 | ```python 118 | bool(load(yaml_snippet, schema)['a']) 119 | ``` 120 | 121 | 122 | ```python 123 | : 124 | Cannot cast 'YAML(1)' to bool. 125 | Use bool(yamlobj.data) or bool(yamlobj.text) instead. 126 | ``` 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | !!! note "Executable specification" 135 | 136 | Documentation automatically generated from 137 | scalar-integer.story 138 | storytests. -------------------------------------------------------------------------------- /hitch/story/build-yaml-document-from-scratch.story: -------------------------------------------------------------------------------- 1 | Build a YAML document from scratch in code: 2 | docs: howto/build-yaml-document 3 | based on: strictyaml 4 | description: | 5 | YAML documents can be built from combinations of dicts, 6 | lists and strings if no schema is used. 7 | given: 8 | setup: | 9 | from ensure import Ensure 10 | from strictyaml import as_document 11 | from collections import OrderedDict 12 | 13 | # Can also use regular dict if an arbitrary ordering is ok 14 | yaml = as_document(OrderedDict( 15 | [(u"â", 'yes'), ("b", "hâllo"), ("c", ["1", "2", "3"])] 16 | )) 17 | variations: 18 | Then dump: 19 | steps: 20 | - Run: 21 | code: print(yaml.as_yaml()) 22 | will output: |- 23 | â: yes 24 | b: hâllo 25 | c: 26 | - 1 27 | - 2 28 | - 3 29 | 30 | However, any type that is not a string, dict or list cannot be parsed without a schema: 31 | steps: 32 | - run: 33 | code: | 34 | class RandomClass(object): 35 | def __repr__(self): 36 | return 'some random object' 37 | 38 | as_document({"x": RandomClass()}) 39 | raises: 40 | type: strictyaml.exceptions.YAMLSerializationError 41 | message: |- 42 | 'some random object' is not a string 43 | 44 | Empty dicts also cannot be serialized without a schema: 45 | steps: 46 | - run: 47 | code: | 48 | as_document({'hello': {}}) 49 | raises: 50 | type: strictyaml.exceptions.YAMLSerializationError 51 | message: Empty dicts are not serializable to StrictYAML unless schema 52 | is used. 53 | 54 | Neither can lists: 55 | steps: 56 | - run: 57 | code: | 58 | as_document({'hello': []}) 59 | raises: 60 | type: strictyaml.exceptions.YAMLSerializationError 61 | message: Empty lists are not serializable to StrictYAML unless schema 62 | is used. 63 | 64 | You can grab line numbers from the object that is serialized: 65 | steps: 66 | - Run: 67 | code: | 68 | Ensure(yaml.start_line).equals(1) 69 | -------------------------------------------------------------------------------- /hitch/story/unique-sequence.story: -------------------------------------------------------------------------------- 1 | Sequences of unique items (UniqueSeq): 2 | docs: compound/sequences-of-unique-items 3 | based on: strictyaml 4 | description: | 5 | UniqueSeq validates sequences which contain no duplicate 6 | values. 7 | given: 8 | yaml_snippet: | 9 | - A 10 | - B 11 | - C 12 | setup: | 13 | from strictyaml import UniqueSeq, Str, load, as_document 14 | 15 | schema = UniqueSeq(Str()) 16 | variations: 17 | Valid: 18 | steps: 19 | - Run: 20 | code: | 21 | assert load(yaml_snippet, schema) == ["A", "B", "C", ] 22 | 23 | Parsing with one dupe raises an exception: 24 | given: 25 | yaml_snippet: | 26 | - A 27 | - B 28 | - B 29 | steps: 30 | - Run: 31 | code: load(yaml_snippet, schema) 32 | raises: 33 | type: strictyaml.exceptions.YAMLValidationError 34 | message: |- 35 | while parsing a sequence 36 | in "", line 1, column 1: 37 | - A 38 | ^ (line: 1) 39 | duplicate found 40 | in "", line 3, column 1: 41 | - B 42 | ^ (line: 3) 43 | 44 | Parsing all dupes raises an exception: 45 | given: 46 | yaml_snippet: | 47 | - 3 48 | - 3 49 | - 3 50 | steps: 51 | - Run: 52 | code: load(yaml_snippet, schema) 53 | raises: 54 | type: strictyaml.exceptions.YAMLValidationError 55 | message: |- 56 | while parsing a sequence 57 | in "", line 1, column 1: 58 | - '3' 59 | ^ (line: 1) 60 | duplicate found 61 | in "", line 3, column 1: 62 | - '3' 63 | ^ (line: 3) 64 | 65 | Serializing with dupes raises an exception: 66 | steps: 67 | - Run: 68 | code: | 69 | as_document(["A", "B", "B"], schema) 70 | raises: 71 | type: strictyaml.exceptions.YAMLSerializationError 72 | message: Expecting all unique items, but duplicates were found in '['A', 73 | 'B', 'B']'. 74 | -------------------------------------------------------------------------------- /docs/public/using/alpha/compound/optional-keys.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Validating optional keys in mappings (Map) 3 | --- 4 | 5 | 6 | Not every key in a YAML mapping will be required. If 7 | you use the "Optional('key')" validator with YAML, 8 | you can signal that a key/value pair is not required. 9 | 10 | 11 | 12 | 13 | ```python 14 | from strictyaml import Map, Int, Str, Bool, Optional, load 15 | from ensure import Ensure 16 | 17 | schema = Map({"a": Int(), Optional("b"): Bool(), }) 18 | 19 | ``` 20 | 21 | 22 | 23 | Valid example 1: 24 | 25 | ```yaml 26 | a: 1 27 | b: yes 28 | 29 | ``` 30 | 31 | 32 | ```python 33 | Ensure(load(yaml_snippet, schema)).equals({"a": 1, "b": True}) 34 | 35 | ``` 36 | 37 | 38 | 39 | 40 | Valid example 2: 41 | 42 | ```yaml 43 | a: 1 44 | b: no 45 | 46 | ``` 47 | 48 | 49 | ```python 50 | Ensure(load(yaml_snippet, schema)).equals({"a": 1, "b": False}) 51 | 52 | ``` 53 | 54 | 55 | 56 | 57 | Valid example missing key: 58 | 59 | ```yaml 60 | a: 1 61 | ``` 62 | 63 | 64 | ```python 65 | Ensure(load(yaml_snippet, schema)).equals({"a": 1}) 66 | 67 | ``` 68 | 69 | 70 | 71 | 72 | Invalid 1: 73 | 74 | ```yaml 75 | a: 1 76 | b: 2 77 | 78 | ``` 79 | 80 | 81 | ```python 82 | load(yaml_snippet, schema) 83 | ``` 84 | 85 | 86 | ```python 87 | strictyaml.exceptions.YAMLValidationError: 88 | when expecting a boolean value (one of "yes", "true", "on", "1", "y", "no", "false", "off", "0", "n") 89 | found an arbitrary integer 90 | in "", line 2, column 1: 91 | b: '2' 92 | ^ (line: 2) 93 | ``` 94 | 95 | 96 | 97 | 98 | Invalid 2: 99 | 100 | ```yaml 101 | a: 1 102 | b: yes 103 | c: 3 104 | 105 | ``` 106 | 107 | 108 | ```python 109 | load(yaml_snippet, schema) 110 | ``` 111 | 112 | 113 | ```python 114 | strictyaml.exceptions.YAMLValidationError: 115 | while parsing a mapping 116 | unexpected key not in schema 'c' 117 | in "", line 3, column 1: 118 | c: '3' 119 | ^ (line: 3) 120 | ``` 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | !!! note "Executable specification" 129 | 130 | Documentation automatically generated from 131 | optional.story 132 | storytests. -------------------------------------------------------------------------------- /docs/public/using/alpha/howto/build-yaml-document.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Build a YAML document from scratch in code 3 | --- 4 | 5 | 6 | YAML documents can be built from combinations of dicts, 7 | lists and strings if no schema is used. 8 | 9 | 10 | 11 | 12 | ```python 13 | from ensure import Ensure 14 | from strictyaml import as_document 15 | from collections import OrderedDict 16 | 17 | # Can also use regular dict if an arbitrary ordering is ok 18 | yaml = as_document(OrderedDict( 19 | [(u"â", 'yes'), ("b", "hâllo"), ("c", ["1", "2", "3"])] 20 | )) 21 | 22 | ``` 23 | 24 | 25 | 26 | Then dump: 27 | 28 | 29 | ```python 30 | print(yaml.as_yaml()) 31 | ``` 32 | 33 | ```yaml 34 | â: yes 35 | b: hâllo 36 | c: 37 | - 1 38 | - 2 39 | - 3 40 | ``` 41 | 42 | 43 | 44 | 45 | However, any type that is not a string, dict or list cannot be parsed without a schema: 46 | 47 | 48 | ```python 49 | class RandomClass(object): 50 | def __repr__(self): 51 | return 'some random object' 52 | 53 | as_document({"x": RandomClass()}) 54 | 55 | ``` 56 | 57 | 58 | ```python 59 | strictyaml.exceptions.YAMLSerializationError: 60 | 'some random object' is not a string 61 | ``` 62 | 63 | 64 | 65 | 66 | Empty dicts also cannot be serialized without a schema: 67 | 68 | 69 | ```python 70 | as_document({'hello': {}}) 71 | 72 | ``` 73 | 74 | 75 | ```python 76 | strictyaml.exceptions.YAMLSerializationError: 77 | Empty dicts are not serializable to StrictYAML unless schema is used. 78 | ``` 79 | 80 | 81 | 82 | 83 | Neither can lists: 84 | 85 | 86 | ```python 87 | as_document({'hello': []}) 88 | 89 | ``` 90 | 91 | 92 | ```python 93 | strictyaml.exceptions.YAMLSerializationError: 94 | Empty lists are not serializable to StrictYAML unless schema is used. 95 | ``` 96 | 97 | 98 | 99 | 100 | You can grab line numbers from the object that is serialized: 101 | 102 | 103 | ```python 104 | Ensure(yaml.start_line).equals(1) 105 | 106 | ``` 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | !!! note "Executable specification" 115 | 116 | Documentation automatically generated from 117 | build-yaml-document-from-scratch.story 118 | storytests. -------------------------------------------------------------------------------- /docs/public/using/alpha/howto/revalidation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Revalidate an already validated document 3 | --- 4 | 5 | 6 | When parsing a YAML document you may wish to do more than one validation 7 | pass over a document. 8 | 9 | This is needed when: 10 | 11 | * It simplifies your code to apply validation in stages. 12 | * You want to validate recursively. 13 | * One part of the document validation depends upon another (this is the example given below). 14 | 15 | 16 | Example yaml_snippet: 17 | 18 | ```yaml 19 | capitals: 20 | UK: 1 21 | Germany: 2 22 | countries: 23 | - Germany 24 | - UK 25 | 26 | ``` 27 | 28 | 29 | ```python 30 | from strictyaml import Str, Int, Map, Seq, Any, load 31 | from ensure import Ensure 32 | 33 | overall_schema = Map({"capitals": Any(), "countries": Seq(Str())}) 34 | parsed = load(yaml_snippet, overall_schema) 35 | 36 | ``` 37 | 38 | 39 | 40 | Reparse mapping: 41 | 42 | 43 | ```python 44 | Ensure(parsed.data['capitals']['UK']).equals("1") 45 | parsed['capitals'].revalidate(Map({capital: Int() for capital in parsed.data['countries']})) 46 | Ensure(parsed.data['capitals']['UK']).equals(1) 47 | 48 | ``` 49 | 50 | 51 | 52 | 53 | Reparse scalar: 54 | 55 | 56 | ```python 57 | Ensure(parsed.data['capitals']['UK']).equals("1") 58 | parsed['capitals']['UK'].revalidate(Int()) 59 | 60 | Ensure(parsed.data['capitals']['UK']).equals(1) 61 | Ensure(parsed['capitals']['UK'].data).is_an(int) 62 | 63 | ``` 64 | 65 | 66 | 67 | 68 | Parse error: 69 | 70 | ```yaml 71 | capitals: 72 | UK: 1 73 | Germany: 2 74 | France: 3 75 | countries: 76 | - Germany 77 | - UK 78 | 79 | ``` 80 | 81 | 82 | ```python 83 | parsed['capitals'].revalidate(Map({capital: Int() for capital in parsed.data['countries']})) 84 | 85 | ``` 86 | 87 | 88 | ```python 89 | strictyaml.exceptions.YAMLValidationError: 90 | while parsing a mapping 91 | unexpected key not in schema 'France' 92 | in "", line 4, column 1: 93 | France: '3' 94 | ^ (line: 4) 95 | ``` 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | !!! note "Executable specification" 104 | 105 | Documentation automatically generated from 106 | revalidation.story 107 | storytests. -------------------------------------------------------------------------------- /hitch/story/revalidation.story: -------------------------------------------------------------------------------- 1 | Revalidate an already validated document: 2 | docs: howto/revalidation 3 | based on: strictyaml 4 | description: | 5 | When parsing a YAML document you may wish to do more than one validation 6 | pass over a document. 7 | 8 | This is needed when: 9 | 10 | * It simplifies your code to apply validation in stages. 11 | * You want to validate recursively. 12 | * One part of the document validation depends upon another (this is the example given below). 13 | given: 14 | setup: | 15 | from strictyaml import Str, Int, Map, Seq, Any, load 16 | from ensure import Ensure 17 | 18 | overall_schema = Map({"capitals": Any(), "countries": Seq(Str())}) 19 | parsed = load(yaml_snippet, overall_schema) 20 | 21 | yaml_snippet: | 22 | capitals: 23 | UK: 1 24 | Germany: 2 25 | countries: 26 | - Germany 27 | - UK 28 | variations: 29 | Reparse mapping: 30 | steps: 31 | - Run: 32 | code: | 33 | Ensure(parsed.data['capitals']['UK']).equals("1") 34 | parsed['capitals'].revalidate(Map({capital: Int() for capital in parsed.data['countries']})) 35 | Ensure(parsed.data['capitals']['UK']).equals(1) 36 | 37 | Reparse scalar: 38 | steps: 39 | - Run: 40 | code: | 41 | Ensure(parsed.data['capitals']['UK']).equals("1") 42 | parsed['capitals']['UK'].revalidate(Int()) 43 | 44 | Ensure(parsed.data['capitals']['UK']).equals(1) 45 | Ensure(parsed['capitals']['UK'].data).is_an(int) 46 | 47 | Parse error: 48 | given: 49 | yaml_snippet: | 50 | capitals: 51 | UK: 1 52 | Germany: 2 53 | France: 3 54 | countries: 55 | - Germany 56 | - UK 57 | steps: 58 | - Run: 59 | code: | 60 | parsed['capitals'].revalidate(Map({capital: Int() for capital in parsed.data['countries']})) 61 | raises: 62 | type: strictyaml.exceptions.YAMLValidationError 63 | message: |- 64 | while parsing a mapping 65 | unexpected key not in schema 'France' 66 | in "", line 4, column 1: 67 | France: '3' 68 | ^ (line: 4) 69 | -------------------------------------------------------------------------------- /strictyaml/any_validator.py: -------------------------------------------------------------------------------- 1 | from strictyaml.ruamel.comments import CommentedSeq, CommentedMap 2 | from strictyaml.compound import FixedSeq, Map 3 | from strictyaml.validators import Validator 4 | from strictyaml.exceptions import YAMLSerializationError 5 | from strictyaml.scalar import Bool, EmptyDict, EmptyList, Float, Int, Str 6 | 7 | 8 | def schema_from_document(document): 9 | if isinstance(document, CommentedMap): 10 | return Map( 11 | {key: schema_from_document(value) for key, value in document.items()} 12 | ) 13 | elif isinstance(document, CommentedSeq): 14 | return FixedSeq([schema_from_document(item) for item in document]) 15 | else: 16 | return Str() 17 | 18 | 19 | def schema_from_data(data, allow_empty): 20 | if isinstance(data, dict): 21 | if len(data) == 0: 22 | if allow_empty: 23 | return EmptyDict() 24 | raise YAMLSerializationError( 25 | "Empty dicts are not serializable to StrictYAML unless schema is used." 26 | ) 27 | return Map( 28 | {key: schema_from_data(value, allow_empty) for key, value in data.items()} 29 | ) 30 | elif isinstance(data, list): 31 | if len(data) == 0: 32 | if allow_empty: 33 | return EmptyList() 34 | raise YAMLSerializationError( 35 | "Empty lists are not serializable to StrictYAML unless schema is used." 36 | ) 37 | return FixedSeq([schema_from_data(item, allow_empty) for item in data]) 38 | elif isinstance(data, bool): 39 | return Bool() 40 | elif isinstance(data, int): 41 | return Int() 42 | elif isinstance(data, float): 43 | return Float() 44 | else: 45 | return Str() 46 | 47 | 48 | class Any(Validator): 49 | """ 50 | Validates any YAML and returns simple dicts/lists of strings. 51 | """ 52 | 53 | def validate(self, chunk): 54 | return schema_from_document(chunk.contents)(chunk) 55 | 56 | def to_yaml(self, data, allow_empty=False): 57 | """ 58 | Args: 59 | allow_empty (bool): True to allow EmptyDict and EmptyList in the 60 | schema generated from the data. 61 | """ 62 | return schema_from_data(data, allow_empty=allow_empty).to_yaml(data) 63 | 64 | @property 65 | def key_validator(self): 66 | return Str() 67 | -------------------------------------------------------------------------------- /docs/public/using/alpha/compound/fixed-length-sequences.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fixed length sequences (FixedSeq) 3 | --- 4 | 5 | 6 | Sequences of fixed length can be validated with a series 7 | of different (or the same) types. 8 | 9 | 10 | 11 | 12 | ```python 13 | from strictyaml import FixedSeq, Str, Map, Int, Float, YAMLValidationError, load 14 | from ensure import Ensure 15 | 16 | schema = FixedSeq([Int(), Map({"x": Str()}), Float()]) 17 | 18 | ``` 19 | 20 | 21 | 22 | Equivalent list: 23 | 24 | ```yaml 25 | - 1 26 | - x: 5 27 | - 2.5 28 | 29 | ``` 30 | 31 | 32 | ```python 33 | Ensure(load(yaml_snippet, schema)).equals([1, {"x": "5"}, 2.5, ]) 34 | 35 | ``` 36 | 37 | 38 | 39 | 40 | Invalid list 1: 41 | 42 | ```yaml 43 | a: 1 44 | b: 2 45 | c: 3 46 | 47 | ``` 48 | 49 | 50 | ```python 51 | load(yaml_snippet, schema) 52 | ``` 53 | 54 | 55 | ```python 56 | strictyaml.exceptions.YAMLValidationError: 57 | when expecting a sequence of 3 elements 58 | in "", line 1, column 1: 59 | a: '1' 60 | ^ (line: 1) 61 | found a mapping 62 | in "", line 3, column 1: 63 | c: '3' 64 | ^ (line: 3) 65 | ``` 66 | 67 | 68 | 69 | 70 | Invalid list 2: 71 | 72 | ```yaml 73 | - 2 74 | - a 75 | - a: 76 | - 1 77 | - 2 78 | 79 | ``` 80 | 81 | 82 | ```python 83 | load(yaml_snippet, schema) 84 | ``` 85 | 86 | 87 | ```python 88 | strictyaml.exceptions.YAMLValidationError: 89 | when expecting a mapping 90 | found arbitrary text 91 | in "", line 2, column 1: 92 | - a 93 | ^ (line: 2) 94 | ``` 95 | 96 | 97 | 98 | 99 | Invalid list 3: 100 | 101 | ```yaml 102 | - 1 103 | - a 104 | 105 | ``` 106 | 107 | 108 | ```python 109 | load(yaml_snippet, schema) 110 | ``` 111 | 112 | 113 | ```python 114 | strictyaml.exceptions.YAMLValidationError: 115 | when expecting a sequence of 3 elements 116 | in "", line 1, column 1: 117 | - '1' 118 | ^ (line: 1) 119 | found a sequence of 2 elements 120 | in "", line 2, column 1: 121 | - a 122 | ^ (line: 2) 123 | ``` 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | !!! note "Executable specification" 132 | 133 | Documentation automatically generated from 134 | fixed-sequence.story 135 | storytests. -------------------------------------------------------------------------------- /strictyaml/ruamel/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import print_function, absolute_import, division, unicode_literals 4 | 5 | if False: # MYPY 6 | from typing import Dict, Any # NOQA 7 | 8 | _package_data = dict( 9 | full_package_name="strictyaml.ruamel", 10 | version_info=(0, 16, 13), 11 | __version__="0.16.13", 12 | author="Anthon van der Neut", 13 | author_email="a.van.der.neut@ruamel.eu", 14 | description="strictyaml.ruamel is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order", # NOQA 15 | entry_points=None, 16 | since=2014, 17 | extras_require={ 18 | ':platform_python_implementation=="CPython" and python_version<="2.7"': [ 19 | "ruamel.ordereddict" 20 | ], # NOQA 21 | ':platform_python_implementation=="CPython" and python_version<"3.10"': [ 22 | "strictyaml.ruamel.clib>=0.1.2" 23 | ], # NOQA 24 | "jinja2": ["strictyaml.ruamel.jinja2>=0.2"], 25 | "docs": ["ryd"], 26 | }, 27 | classifiers=[ 28 | "Programming Language :: Python :: 2.7", 29 | "Programming Language :: Python :: 3.5", 30 | "Programming Language :: Python :: 3.6", 31 | "Programming Language :: Python :: 3.7", 32 | "Programming Language :: Python :: 3.8", 33 | "Programming Language :: Python :: Implementation :: CPython", 34 | "Programming Language :: Python :: Implementation :: PyPy", 35 | "Programming Language :: Python :: Implementation :: Jython", 36 | "Topic :: Software Development :: Libraries :: Python Modules", 37 | "Topic :: Text Processing :: Markup", 38 | "Typing :: Typed", 39 | ], 40 | keywords="yaml 1.2 parser round-trip preserve quotes order config", 41 | read_the_docs="yaml", 42 | supported=[(2, 7), (3, 5)], # minimum 43 | tox=dict( 44 | env="*", # remove 'pn', no longer test narrow Python 2.7 for unicode patterns and PyPy 45 | deps="ruamel.std.pathlib", 46 | fl8excl="_test/lib", 47 | ), 48 | universal=True, 49 | rtfd="yaml", 50 | ) # type: Dict[Any, Any] 51 | 52 | 53 | version_info = _package_data["version_info"] 54 | __version__ = _package_data["__version__"] 55 | 56 | try: 57 | from .cyaml import * # NOQA 58 | 59 | __with_libyaml__ = True 60 | except (ImportError, ValueError): # for Jython 61 | __with_libyaml__ = False 62 | 63 | from strictyaml.ruamel.main import * # NOQA 64 | -------------------------------------------------------------------------------- /docs/public/using/alpha/howto/what-line.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Get line numbers of YAML elements 3 | --- 4 | 5 | 6 | Line numbers, the text of an item and text of surrounding lines 7 | can be grabbed from returned YAML objects - using .start_line, 8 | .end_line, lines(), lines_before(x) and lines_after(x). 9 | 10 | 11 | Example yaml_snippet: 12 | 13 | ```yaml 14 | y: p 15 | # Some comment 16 | 17 | a: | 18 | x 19 | 20 | # Another comment 21 | b: y 22 | c: a 23 | 24 | d: b 25 | 26 | ``` 27 | 28 | 29 | ```python 30 | from strictyaml import Map, Str, YAMLValidationError, load 31 | from ensure import Ensure 32 | 33 | schema = Map({"y": Str(), "a": Str(), "b": Str(), "c": Str(), "d": Str()}) 34 | 35 | snippet = load(yaml_snippet, schema) 36 | 37 | ``` 38 | 39 | 40 | 41 | If there is preceding comment for an item the start line includes it: 42 | 43 | 44 | ```python 45 | Ensure(snippet["a"].start_line).equals(3) 46 | Ensure(snippet["d"].start_line).equals(9) 47 | 48 | ``` 49 | 50 | 51 | 52 | 53 | If there is a trailing comment the end line includes it: 54 | 55 | 56 | ```python 57 | Ensure(snippet["a"].end_line).equals(6) 58 | Ensure(snippet["d"].end_line).equals(10) 59 | 60 | ``` 61 | 62 | 63 | 64 | 65 | You can grab the start line of a key: 66 | 67 | 68 | ```python 69 | Ensure(snippet.keys()[1].start_line).equals(3) 70 | 71 | ``` 72 | 73 | 74 | 75 | 76 | Start line and end line of whole snippet: 77 | 78 | 79 | ```python 80 | Ensure(snippet.start_line).equals(1) 81 | Ensure(snippet.end_line).equals(10) 82 | 83 | ``` 84 | 85 | 86 | 87 | 88 | Grabbing a line before an item: 89 | 90 | 91 | ```python 92 | Ensure(snippet['a'].lines_before(1)).equals("# Some comment") 93 | 94 | ``` 95 | 96 | 97 | 98 | 99 | Grabbing a line after an item: 100 | 101 | 102 | ```python 103 | Ensure(snippet['a'].lines_after(4)).equals("b: y\nc: a\n\nd: b") 104 | 105 | ``` 106 | 107 | 108 | 109 | 110 | Grabbing the lines of an item including surrounding comments: 111 | 112 | 113 | ```python 114 | print(load(yaml_snippet, schema)['a'].lines()) 115 | 116 | ``` 117 | 118 | ```yaml 119 | a: | 120 | x 121 | 122 | # Another comment 123 | ``` 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | !!! note "Executable specification" 132 | 133 | Documentation automatically generated from 134 | whatline.story 135 | storytests. -------------------------------------------------------------------------------- /docs/public/using/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using StrictYAML 3 | --- 4 | 5 | How to: 6 | 7 | - [Build a YAML document from scratch in code](alpha/howto/build-yaml-document) 8 | - [Either/or schema validation of different, equally valid different kinds of YAML](alpha/howto/either-or-validation) 9 | - [Labeling exceptions](alpha/howto/label-exceptions) 10 | - [Merge YAML documents](alpha/howto/merge-yaml-documents) 11 | - [Revalidate an already validated document](alpha/howto/revalidation) 12 | - [Reading in YAML, editing it and writing it back out](alpha/howto/roundtripping) 13 | - [Get line numbers of YAML elements](alpha/howto/what-line) 14 | - [Parsing YAML without a schema](alpha/howto/without-a-schema) 15 | 16 | 17 | Compound validators: 18 | 19 | - [Fixed length sequences (FixedSeq)](alpha/compound/fixed-length-sequences) 20 | - [Mappings combining defined and undefined keys (MapCombined)](alpha/compound/map-combined) 21 | - [Mappings with arbitrary key names (MapPattern)](alpha/compound/map-pattern) 22 | - [Mapping with defined keys and a custom key validator (Map)](alpha/compound/mapping-with-slug-keys) 23 | - [Using a YAML object of a parsed mapping](alpha/compound/mapping-yaml-object) 24 | - [Mappings with defined keys (Map)](alpha/compound/mapping) 25 | - [Optional keys with defaults (Map/Optional)](alpha/compound/optional-keys-with-defaults) 26 | - [Validating optional keys in mappings (Map)](alpha/compound/optional-keys) 27 | - [Sequences of unique items (UniqueSeq)](alpha/compound/sequences-of-unique-items) 28 | - [Sequence/list validator (Seq)](alpha/compound/sequences) 29 | - [Updating document with a schema](alpha/compound/update) 30 | 31 | 32 | Scalar validators: 33 | 34 | - [Boolean (Bool)](alpha/scalar/boolean) 35 | - [Parsing comma separated items (CommaSeparated)](alpha/scalar/comma-separated) 36 | - [Datetimes (Datetime)](alpha/scalar/datetime) 37 | - [Decimal numbers (Decimal)](alpha/scalar/decimal) 38 | - [Email and URL validators](alpha/scalar/email-and-url) 39 | - [Empty key validation](alpha/scalar/empty) 40 | - [Enumerated scalars (Enum)](alpha/scalar/enum) 41 | - [Floating point numbers (Float)](alpha/scalar/float) 42 | - [Hexadecimal Integers (HexInt)](alpha/scalar/hexadecimal-integer) 43 | - [Integers (Int)](alpha/scalar/integer) 44 | - [Validating strings with regexes (Regex)](alpha/scalar/regular-expressions) 45 | - [Parsing strings (Str)](alpha/scalar/string) 46 | 47 | Restrictions: 48 | 49 | - [Disallowed YAML](alpha/restrictions/disallowed-yaml) 50 | - [Duplicate keys](alpha/restrictions/duplicate-keys) 51 | - [Dirty load](alpha/restrictions/loading-dirty-yaml) 52 | -------------------------------------------------------------------------------- /hitch/story/scalar-integer.story: -------------------------------------------------------------------------------- 1 | Integers (Int): 2 | docs: scalar/integer 3 | based on: strictyaml 4 | description: | 5 | StrictYAML parses to a YAML object, not 6 | the value directly to give you more flexibility 7 | and control over what you can do with the YAML. 8 | 9 | This is what that can object can do - in many 10 | cases if parsed as a integer, it will behave in 11 | the same way. 12 | given: 13 | yaml_snippet: | 14 | a: 1 15 | b: 5 16 | setup: | 17 | from strictyaml import Map, Int, load 18 | from ensure import Ensure 19 | 20 | schema = Map({"a": Int(), "b": Int()}) 21 | 22 | parsed = load(yaml_snippet, schema) 23 | variations: 24 | Parsed correctly: 25 | steps: 26 | - Run: | 27 | Ensure(parsed).equals({"a": 1, "b": 5}) 28 | 29 | Has underscores: 30 | given: 31 | yaml_snippet: | 32 | a: 10_000_000 33 | b: 10_0_0 34 | steps: 35 | - Run: 36 | code: | 37 | Ensure(load(yaml_snippet, schema).data).equals({"a": 10000000, "b": 1000}) 38 | 39 | Cast with str: 40 | steps: 41 | - Run: Ensure(str(parsed["a"])).equals("1") 42 | 43 | Cast with float: 44 | steps: 45 | - Run: Ensure(float(parsed["a"])).equals(1.0) 46 | 47 | Greater than: 48 | steps: 49 | - Run: Ensure(parsed["a"] > 0).equals(True) 50 | 51 | Less than: 52 | steps: 53 | - Run: Ensure(parsed["a"] < 2).equals(True) 54 | 55 | To get actual int, use .data: 56 | steps: 57 | - Run: Ensure(type(load(yaml_snippet, schema)["a"].data) is int).equals(True) 58 | 59 | Cannot cast to bool: 60 | steps: 61 | - Run: 62 | code: bool(load(yaml_snippet, schema)['a']) 63 | raises: 64 | type: TypeError 65 | message: |- 66 | Cannot cast 'YAML(1)' to bool. 67 | Use bool(yamlobj.data) or bool(yamlobj.text) instead. 68 | 69 | 70 | Invalid scalar integer: 71 | based on: strictyaml 72 | given: 73 | yaml_snippet: | 74 | a: string 75 | b: 2 76 | setup: | 77 | from strictyaml import Map, Int, load 78 | 79 | schema = Map({"a": Int(), "b": Int()}) 80 | steps: 81 | - Run: 82 | code: load(yaml_snippet, schema) 83 | raises: 84 | type: strictyaml.exceptions.YAMLValidationError 85 | message: |- 86 | when expecting an integer 87 | found arbitrary text 88 | in "", line 1, column 1: 89 | a: string 90 | ^ (line: 1) 91 | -------------------------------------------------------------------------------- /strictyaml/dumper.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from strictyaml.ruamel.representer import RoundTripRepresenter 6 | from strictyaml.ruamel.scalarstring import ScalarString 7 | from strictyaml.ruamel.emitter import Emitter 8 | from strictyaml.ruamel.serializer import Serializer 9 | from strictyaml.ruamel.resolver import BaseResolver 10 | import sys 11 | 12 | if sys.version_info[0] == 3: 13 | RoundTripRepresenter.add_representer( 14 | ScalarString, RoundTripRepresenter.represent_str 15 | ) 16 | else: 17 | RoundTripRepresenter.add_representer( 18 | ScalarString, RoundTripRepresenter.represent_unicode 19 | ) 20 | 21 | 22 | class StrictYAMLResolver(BaseResolver): 23 | def __init__(self, version=None, loader=None): 24 | BaseResolver.__init__(self, loader) 25 | 26 | 27 | class StrictYAMLDumper(Emitter, Serializer, RoundTripRepresenter, StrictYAMLResolver): 28 | def __init__( 29 | self, 30 | stream, 31 | default_style=None, 32 | default_flow_style=None, 33 | canonical=None, 34 | indent=None, 35 | width=None, 36 | allow_unicode=None, 37 | line_break=None, 38 | encoding=None, 39 | explicit_start=None, 40 | explicit_end=None, 41 | version=None, 42 | tags=None, 43 | block_seq_indent=None, 44 | top_level_colon_align=None, 45 | prefix_colon=None, 46 | ): 47 | # type: (Any, StreamType, Any, bool, Union[None, int], Union[None, int], bool, Any, Any, Union[None, bool], Union[None, bool], Any, Any, Any, Any, Any) -> None # NOQA 48 | Emitter.__init__( 49 | self, 50 | stream, 51 | canonical=canonical, 52 | indent=indent, 53 | width=width, 54 | allow_unicode=allow_unicode, 55 | line_break=line_break, 56 | block_seq_indent=block_seq_indent, 57 | top_level_colon_align=top_level_colon_align, 58 | prefix_colon=prefix_colon, 59 | dumper=self, 60 | ) 61 | Serializer.__init__( 62 | self, 63 | encoding=encoding, 64 | explicit_start=explicit_start, 65 | explicit_end=explicit_end, 66 | version=version, 67 | tags=tags, 68 | dumper=self, 69 | ) 70 | RoundTripRepresenter.__init__( 71 | self, 72 | default_style=default_style, 73 | default_flow_style=default_flow_style, 74 | dumper=self, 75 | ) 76 | StrictYAMLResolver.__init__(self, loader=self) 77 | -------------------------------------------------------------------------------- /hitch/story/fixed-sequence.story: -------------------------------------------------------------------------------- 1 | Fixed length sequences (FixedSeq): 2 | docs: compound/fixed-length-sequences 3 | based on: strictyaml 4 | description: | 5 | Sequences of fixed length can be validated with a series 6 | of different (or the same) types. 7 | given: 8 | setup: | 9 | from strictyaml import FixedSeq, Str, Map, Int, Float, YAMLValidationError, load 10 | from ensure import Ensure 11 | 12 | schema = FixedSeq([Int(), Map({"x": Str()}), Float()]) 13 | variations: 14 | Equivalent list: 15 | given: 16 | yaml_snippet: | 17 | - 1 18 | - x: 5 19 | - 2.5 20 | steps: 21 | - Run: 22 | code: | 23 | Ensure(load(yaml_snippet, schema)).equals([1, {"x": "5"}, 2.5, ]) 24 | 25 | Invalid list 1: 26 | given: 27 | yaml_snippet: | 28 | a: 1 29 | b: 2 30 | c: 3 31 | steps: 32 | - Run: 33 | code: load(yaml_snippet, schema) 34 | raises: 35 | type: strictyaml.exceptions.YAMLValidationError 36 | message: |- 37 | when expecting a sequence of 3 elements 38 | in "", line 1, column 1: 39 | a: '1' 40 | ^ (line: 1) 41 | found a mapping 42 | in "", line 3, column 1: 43 | c: '3' 44 | ^ (line: 3) 45 | 46 | Invalid list 2: 47 | given: 48 | yaml_snippet: | 49 | - 2 50 | - a 51 | - a: 52 | - 1 53 | - 2 54 | steps: 55 | - Run: 56 | code: load(yaml_snippet, schema) 57 | raises: 58 | type: strictyaml.exceptions.YAMLValidationError 59 | message: |- 60 | when expecting a mapping 61 | found arbitrary text 62 | in "", line 2, column 1: 63 | - a 64 | ^ (line: 2) 65 | 66 | Invalid list 3: 67 | given: 68 | yaml_snippet: | 69 | - 1 70 | - a 71 | steps: 72 | - Run: 73 | code: load(yaml_snippet, schema) 74 | raises: 75 | type: strictyaml.exceptions.YAMLValidationError 76 | message: |- 77 | when expecting a sequence of 3 elements 78 | in "", line 1, column 1: 79 | - '1' 80 | ^ (line: 1) 81 | found a sequence of 2 elements 82 | in "", line 2, column 1: 83 | - a 84 | ^ (line: 2) 85 | -------------------------------------------------------------------------------- /hitch/story/decimal.story: -------------------------------------------------------------------------------- 1 | Decimal numbers (Decimal): 2 | docs: scalar/decimal 3 | based on: strictyaml 4 | description: | 5 | StrictYAML parses to a YAML object representing 6 | a decimal, not the value directly to give you more 7 | flexibility and control over what you can do with the 8 | YAML. 9 | 10 | This is what that can object can do - in many 11 | cases if parsed as a decimal, it will behave in 12 | the same way. 13 | 14 | To get a python decimal.Decimal object, use .data. 15 | 16 | Parsing and validating as a Decimal is best for 17 | values which require precision, like prices. 18 | given: 19 | setup: | 20 | from strictyaml import Map, Decimal, load 21 | from decimal import Decimal as Dec 22 | from ensure import Ensure 23 | 24 | schema = Map({"a": Decimal(), "b": Decimal()}) 25 | 26 | yaml_snippet: | 27 | a: 1.00000000000000000001 28 | b: 5.4135 29 | variations: 30 | .data to get Decimal object: 31 | steps: 32 | - Run: Ensure(type(load(yaml_snippet, schema)["a"].data) is Dec).is_true() 33 | 34 | Valid: 35 | steps: 36 | - Run: | 37 | Ensure(load(yaml_snippet, schema)).equals({"a": Dec('1.00000000000000000001'), "b": Dec('5.4135')}) 38 | 39 | Cast to str: 40 | steps: 41 | - Run: 42 | Ensure(str(load(yaml_snippet, schema)['a'])).equals("1.00000000000000000001") 43 | 44 | 45 | Cast to float: 46 | steps: 47 | - Run: 48 | Ensure(float(load(yaml_snippet, schema)["a"])).equals(1.0) 49 | 50 | Greater than: 51 | steps: 52 | - Run: 53 | Ensure(load(yaml_snippet, schema)["a"] > Dec('1.0')).is_true() 54 | 55 | Less than which would not work for float: 56 | steps: 57 | - Run: 58 | Ensure(load(yaml_snippet, schema)["a"] < Dec('1.00000000000000000002')).is_true() 59 | 60 | Cannot cast to bool: 61 | steps: 62 | - Run: 63 | code: bool(load(yaml_snippet, schema)['a']) 64 | raises: 65 | type: TypeError 66 | message: |- 67 | Cannot cast 'YAML(1.00000000000000000001)' to bool. 68 | Use bool(yamlobj.data) or bool(yamlobj.text) instead. 69 | 70 | Invalid: 71 | given: 72 | yaml_snippet: | 73 | a: string 74 | b: 2 75 | steps: 76 | - Run: 77 | code: load(yaml_snippet, schema) 78 | raises: 79 | type: strictyaml.exceptions.YAMLValidationError 80 | message: |- 81 | when expecting a decimal 82 | found arbitrary text 83 | in "", line 1, column 1: 84 | a: string 85 | ^ (line: 1) 86 | -------------------------------------------------------------------------------- /docs/public/using/alpha/scalar/regular-expressions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Validating strings with regexes (Regex) 3 | --- 4 | 5 | 6 | StrictYAML can validate regular expressions and return a 7 | string. If the regular expression does not match, an 8 | exception is raised. 9 | 10 | 11 | 12 | 13 | ```python 14 | from strictyaml import Regex, Map, load, as_document 15 | from collections import OrderedDict 16 | from ensure import Ensure 17 | 18 | schema = Map({"a": Regex(u"[1-4]"), "b": Regex(u"[5-9]")}) 19 | 20 | ``` 21 | 22 | 23 | 24 | Parsed correctly: 25 | 26 | ```yaml 27 | a: 1 28 | b: 5 29 | 30 | ``` 31 | 32 | 33 | ```python 34 | Ensure(load(yaml_snippet, schema)).equals({"a": "1", "b": "5"}) 35 | 36 | ``` 37 | 38 | 39 | 40 | 41 | Non-matching: 42 | 43 | ```yaml 44 | a: 5 45 | b: 5 46 | 47 | ``` 48 | 49 | 50 | ```python 51 | load(yaml_snippet, schema) 52 | ``` 53 | 54 | 55 | ```python 56 | strictyaml.exceptions.YAMLValidationError: 57 | when expecting string matching [1-4] 58 | found non-matching string 59 | in "", line 1, column 1: 60 | a: '5' 61 | ^ (line: 1) 62 | ``` 63 | 64 | 65 | 66 | 67 | Non-matching suffix: 68 | 69 | ```yaml 70 | a: 1 Hello 71 | b: 5 72 | 73 | ``` 74 | 75 | 76 | ```python 77 | load(yaml_snippet, schema) 78 | ``` 79 | 80 | 81 | ```python 82 | strictyaml.exceptions.YAMLValidationError: 83 | when expecting string matching [1-4] 84 | found non-matching string 85 | in "", line 1, column 1: 86 | a: 1 Hello 87 | ^ (line: 1) 88 | ``` 89 | 90 | 91 | 92 | 93 | Serialized successfully: 94 | 95 | 96 | ```python 97 | print(as_document(OrderedDict([("a", "1"), ("b", "5")]), schema).as_yaml()) 98 | 99 | ``` 100 | 101 | ```yaml 102 | a: 1 103 | b: 5 104 | ``` 105 | 106 | 107 | 108 | 109 | Serialization failure non matching regex: 110 | 111 | 112 | ```python 113 | as_document(OrderedDict([("a", "x"), ("b", "5")]), schema) 114 | 115 | ``` 116 | 117 | 118 | ```python 119 | strictyaml.exceptions.YAMLSerializationError: 120 | when expecting string matching [1-4] found 'x' 121 | ``` 122 | 123 | 124 | 125 | 126 | Serialization failure not a string: 127 | 128 | 129 | ```python 130 | as_document(OrderedDict([("a", 1), ("b", "5")]), schema) 131 | 132 | ``` 133 | 134 | 135 | ```python 136 | strictyaml.exceptions.YAMLSerializationError: 137 | when expecting string matching [1-4] got '1' of type int. 138 | ``` 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | !!! note "Executable specification" 147 | 148 | Documentation automatically generated from 149 | regexp.story 150 | storytests. -------------------------------------------------------------------------------- /hitch/story/mapping-representation.story: -------------------------------------------------------------------------------- 1 | Using a YAML object of a parsed mapping: 2 | docs: compound/mapping-yaml-object 3 | based on: strictyaml 4 | description: | 5 | When a YAML document with mappings is parsed, it is not parsed 6 | as a dict but as a YAML object which behaves very similarly to 7 | a dict, but with some extra capabilities. 8 | 9 | You can use .items(), .keys(), .values(), look up items with 10 | square bracket notation, .get(key, with_default_if_nonexistent) 11 | and use "x in y" notation to determine key membership. 12 | 13 | To retrieve the equivalent dict (containing just other dicts, lists 14 | and strings/ints/etc.) use .data. 15 | given: 16 | setup: | 17 | from strictyaml import Map, Int, load 18 | from ensure import Ensure 19 | 20 | schema = Map({"a": Int(), "b": Int(), "c": Int()}) 21 | yaml_snippet: | 22 | a: 1 23 | b: 2 24 | c: 3 25 | variations: 26 | .is_mapping(): 27 | steps: 28 | - Run: 29 | code: | 30 | Ensure(load(yaml_snippet, schema).is_mapping()).is_true() 31 | 32 | Equivalence with equivalent plain dict: 33 | steps: 34 | - Run: 35 | code: | 36 | Ensure(load(yaml_snippet, schema)).equals({"a": 1, "b": 2, "c": 3}) 37 | 38 | .items(): 39 | steps: 40 | - Run: 41 | code: | 42 | Ensure(load(yaml_snippet, schema).items()).equals([("a", 1), ("b", 2), ("c", 3)]) 43 | 44 | Use in to detect presence of a key: 45 | steps: 46 | - Run: 47 | code: | 48 | Ensure("a" in load(yaml_snippet, schema)).is_true() 49 | 50 | .values(): 51 | steps: 52 | - Run: 53 | code: | 54 | Ensure(load(yaml_snippet, schema).values()).equals([1, 2, 3]) 55 | 56 | .keys(): 57 | steps: 58 | - Run: 59 | code: | 60 | Ensure(load(yaml_snippet, schema).keys()).equals(["a", "b", "c"]) 61 | 62 | Key lookup: 63 | steps: 64 | - Run: 65 | code: | 66 | yaml = load(yaml_snippet, schema) 67 | Ensure(yaml[yaml.keys()[0]]).equals(1) 68 | 69 | Dict lookup: 70 | steps: 71 | - Run: 72 | code: | 73 | Ensure(load(yaml_snippet, schema)["a"]).equals(1) 74 | 75 | .get(): 76 | steps: 77 | - Run: 78 | code: | 79 | Ensure(load(yaml_snippet, schema).get("a")).equals(1) 80 | 81 | .get() nonexistent: 82 | steps: 83 | - Run: 84 | code: | 85 | Ensure(load(yaml_snippet, schema).get("nonexistent")).equals(None) 86 | 87 | len(): 88 | steps: 89 | - Run: 90 | code: | 91 | Ensure(len(load(yaml_snippet, schema))).equals(3) 92 | -------------------------------------------------------------------------------- /hitch/story/regexp.story: -------------------------------------------------------------------------------- 1 | Validating strings with regexes (Regex): 2 | docs: scalar/regular-expressions 3 | based on: strictyaml 4 | description: | 5 | StrictYAML can validate regular expressions and return a 6 | string. If the regular expression does not match, an 7 | exception is raised. 8 | given: 9 | setup: | 10 | from strictyaml import Regex, Map, load, as_document 11 | from collections import OrderedDict 12 | from ensure import Ensure 13 | 14 | schema = Map({"a": Regex(u"[1-4]"), "b": Regex(u"[5-9]")}) 15 | variations: 16 | Parsed correctly: 17 | given: 18 | yaml_snippet: | 19 | a: 1 20 | b: 5 21 | steps: 22 | - Run: 23 | code: | 24 | Ensure(load(yaml_snippet, schema)).equals({"a": "1", "b": "5"}) 25 | 26 | Non-matching: 27 | given: 28 | yaml_snippet: | 29 | a: 5 30 | b: 5 31 | steps: 32 | - Run: 33 | code: load(yaml_snippet, schema) 34 | raises: 35 | type: strictyaml.exceptions.YAMLValidationError 36 | message: |- 37 | when expecting string matching [1-4] 38 | found non-matching string 39 | in "", line 1, column 1: 40 | a: '5' 41 | ^ (line: 1) 42 | 43 | Non-matching suffix: 44 | given: 45 | yaml_snippet: | 46 | a: 1 Hello 47 | b: 5 48 | steps: 49 | - Run: 50 | code: load(yaml_snippet, schema) 51 | raises: 52 | type: strictyaml.exceptions.YAMLValidationError 53 | message: |- 54 | when expecting string matching [1-4] 55 | found non-matching string 56 | in "", line 1, column 1: 57 | a: 1 Hello 58 | ^ (line: 1) 59 | 60 | Serialized successfully: 61 | steps: 62 | - Run: 63 | code: | 64 | print(as_document(OrderedDict([("a", "1"), ("b", "5")]), schema).as_yaml()) 65 | will output: |- 66 | a: 1 67 | b: 5 68 | 69 | Serialization failure non matching regex: 70 | steps: 71 | - Run: 72 | code: | 73 | as_document(OrderedDict([("a", "x"), ("b", "5")]), schema) 74 | raises: 75 | type: strictyaml.exceptions.YAMLSerializationError 76 | message: when expecting string matching [1-4] found 'x' 77 | 78 | Serialization failure not a string: 79 | steps: 80 | - Run: 81 | code: | 82 | as_document(OrderedDict([("a", 1), ("b", "5")]), schema) 83 | raises: 84 | type: strictyaml.exceptions.YAMLSerializationError 85 | message: when expecting string matching [1-4] got '1' of type int. 86 | -------------------------------------------------------------------------------- /hitch/story/optional-with-defaults.story: -------------------------------------------------------------------------------- 1 | Optional keys with defaults (Map/Optional): 2 | docs: compound/optional-keys-with-defaults 3 | experimental: yes 4 | based on: strictyaml 5 | about: | 6 | Not every key in a YAML mapping will be required. If 7 | you use the "Optional('key')" validator with YAML, 8 | you can signal that a key/value pair is not required. 9 | given: 10 | yaml_snippet: | 11 | a: 1 12 | setup: | 13 | from strictyaml import Map, Int, Str, Bool, EmptyNone, Optional, load, as_document 14 | from collections import OrderedDict 15 | from ensure import Ensure 16 | 17 | schema = Map({"a": Int(), Optional("b", default=False): Bool(), }) 18 | variations: 19 | When parsed the result will include the optional value: 20 | steps: 21 | - Run: | 22 | Ensure(load(yaml_snippet, schema).data).equals(OrderedDict([("a", 1), ("b", False)])) 23 | 24 | If parsed and then output to YAML again the default data won't be there: 25 | steps: 26 | - Run: 27 | code: print(load(yaml_snippet, schema).as_yaml()) 28 | will output: |- 29 | a: 1 30 | 31 | When default data is output to YAML it is removed: 32 | steps: 33 | - Run: 34 | code: | 35 | print(as_document({"a": 1, "b": False}, schema).as_yaml()) 36 | will output: |- 37 | a: 1 38 | 39 | When you want a key to stay and default to None: 40 | steps: 41 | - Run: 42 | code: | 43 | schema = Map({"a": Int(), Optional("b", default=None, drop_if_none=False): EmptyNone() | Bool(), }) 44 | Ensure(load(yaml_snippet, schema).data).equals(OrderedDict([("a", 1), ("b", None)])) 45 | 46 | 47 | Optional keys with bad defaults: 48 | based on: Optional keys with defaults (Map/Optional) 49 | steps: 50 | - Run: 51 | code: | 52 | Map({"a": Int(), Optional("b", default="nonsense"): Bool(), }) 53 | raises: 54 | type: strictyaml.exceptions.InvalidOptionalDefault 55 | message: "Optional default for 'b' failed validation:\n Not a boolean" 56 | 57 | 58 | Optional keys revalidation bug: 59 | based on: Optional keys with defaults (Map/Optional) 60 | given: 61 | yaml_snippet: | 62 | content: 63 | subitem: 64 | a: 1 65 | steps: 66 | - Run: 67 | code: | 68 | from strictyaml import MapPattern, Any 69 | 70 | loose_schema = Map({"content": Any()}) 71 | strict_schema = Map({"subitem": Map({"a": Int(), Optional("b", default=False): Bool()})}) 72 | 73 | myyaml = load(yaml_snippet, loose_schema) 74 | myyaml['content'].revalidate(strict_schema) 75 | assert myyaml.data == {"content": {"subitem": {"a": 1, "b": False}}}, myyaml.data 76 | print(myyaml.data.__repr__()) 77 | will output: "{'content': {'subitem': {'a': 1, 'b': False}}}" 78 | -------------------------------------------------------------------------------- /hitch/story/nested-map.story: -------------------------------------------------------------------------------- 1 | Nested mapping validation: 2 | based on: strictyaml 3 | description: | 4 | Mappings can be nested within one another, which 5 | will be parsed as a dict within a dict. 6 | given: 7 | setup: | 8 | from strictyaml import Map, Int, load 9 | from ensure import Ensure 10 | 11 | schema = Map({"a": Map({"x": Int(), "y": Int()}), "b": Int(), "c": Int()}) 12 | variations: 13 | Valid nested mapping: 14 | given: 15 | yaml_snippet: | 16 | a: 17 | x: 9 18 | y: 8 19 | b: 2 20 | c: 3 21 | steps: 22 | - Run: 23 | code: | 24 | Ensure(load(yaml_snippet, schema)).equals({"a": {"x": 9, "y": 8}, "b": 2, "c": 3}) 25 | 26 | 27 | Invalid nested mapping: 28 | given: 29 | yaml_snippet: | 30 | a: 31 | x: 9 32 | z: 8 33 | b: 2 34 | d: 3 35 | steps: 36 | - Run: 37 | code: load(yaml_snippet, schema) 38 | raises: 39 | type: strictyaml.exceptions.YAMLValidationError 40 | message: |- 41 | while parsing a mapping 42 | unexpected key not in schema 'z' 43 | in "", line 3, column 1: 44 | z: '8' 45 | ^ (line: 3) 46 | 47 | No nested mapping where expected: 48 | given: 49 | yaml_snippet: | 50 | a: 11 51 | b: 2 52 | d: 3 53 | steps: 54 | - Run: 55 | code: load(yaml_snippet, schema) 56 | raises: 57 | type: strictyaml.exceptions.YAMLValidationError 58 | message: |- 59 | when expecting a mapping 60 | found an arbitrary integer 61 | in "", line 1, column 1: 62 | a: '11' 63 | ^ (line: 1) 64 | Modify nested map: 65 | given: 66 | yaml_snippet: | 67 | a: 68 | x: 9 69 | y: 70 | - 1 71 | - 2 72 | - 3 73 | b: 2 74 | c: 3 75 | setup: | 76 | from strictyaml import Map, Int, load, Seq 77 | from collections import OrderedDict 78 | 79 | schema = Map({"a": Map({"x": Int(), "y": Seq(Int())}), "b": Int(), "c": Int()}) 80 | 81 | yaml = load(yaml_snippet, schema) 82 | 83 | # Non-ordered dict would also work, but would yield an indeterminate order of keys 84 | yaml['a'] = OrderedDict([("x", 5), ("y", [4, 5, 6])]) 85 | yaml['a']['x'] = 99 86 | steps: 87 | - Run: 88 | code: print(yaml.as_yaml()) 89 | will output: |- 90 | a: 91 | x: 99 92 | y: 93 | - 4 94 | - 5 95 | - 6 96 | b: 2 97 | c: 3 98 | 99 | -------------------------------------------------------------------------------- /docs/public/using/alpha/scalar/decimal.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Decimal numbers (Decimal) 3 | --- 4 | 5 | 6 | StrictYAML parses to a YAML object representing 7 | a decimal, not the value directly to give you more 8 | flexibility and control over what you can do with the 9 | YAML. 10 | 11 | This is what that can object can do - in many 12 | cases if parsed as a decimal, it will behave in 13 | the same way. 14 | 15 | To get a python decimal.Decimal object, use .data. 16 | 17 | Parsing and validating as a Decimal is best for 18 | values which require precision, like prices. 19 | 20 | 21 | Example yaml_snippet: 22 | 23 | ```yaml 24 | a: 1.00000000000000000001 25 | b: 5.4135 26 | 27 | ``` 28 | 29 | 30 | ```python 31 | from strictyaml import Map, Decimal, load 32 | from decimal import Decimal as Dec 33 | from ensure import Ensure 34 | 35 | schema = Map({"a": Decimal(), "b": Decimal()}) 36 | 37 | ``` 38 | 39 | 40 | 41 | .data to get Decimal object: 42 | 43 | 44 | ```python 45 | Ensure(type(load(yaml_snippet, schema)["a"].data) is Dec).is_true() 46 | ``` 47 | 48 | 49 | 50 | 51 | Valid: 52 | 53 | 54 | ```python 55 | Ensure(load(yaml_snippet, schema)).equals({"a": Dec('1.00000000000000000001'), "b": Dec('5.4135')}) 56 | 57 | ``` 58 | 59 | 60 | 61 | 62 | Cast to str: 63 | 64 | 65 | ```python 66 | Ensure(str(load(yaml_snippet, schema)['a'])).equals("1.00000000000000000001") 67 | ``` 68 | 69 | 70 | 71 | 72 | Cast to float: 73 | 74 | 75 | ```python 76 | Ensure(float(load(yaml_snippet, schema)["a"])).equals(1.0) 77 | ``` 78 | 79 | 80 | 81 | 82 | Greater than: 83 | 84 | 85 | ```python 86 | Ensure(load(yaml_snippet, schema)["a"] > Dec('1.0')).is_true() 87 | ``` 88 | 89 | 90 | 91 | 92 | Less than which would not work for float: 93 | 94 | 95 | ```python 96 | Ensure(load(yaml_snippet, schema)["a"] < Dec('1.00000000000000000002')).is_true() 97 | ``` 98 | 99 | 100 | 101 | 102 | Cannot cast to bool: 103 | 104 | 105 | ```python 106 | bool(load(yaml_snippet, schema)['a']) 107 | ``` 108 | 109 | 110 | ```python 111 | : 112 | Cannot cast 'YAML(1.00000000000000000001)' to bool. 113 | Use bool(yamlobj.data) or bool(yamlobj.text) instead. 114 | ``` 115 | 116 | 117 | 118 | 119 | Invalid: 120 | 121 | ```yaml 122 | a: string 123 | b: 2 124 | 125 | ``` 126 | 127 | 128 | ```python 129 | load(yaml_snippet, schema) 130 | ``` 131 | 132 | 133 | ```python 134 | strictyaml.exceptions.YAMLValidationError: 135 | when expecting a decimal 136 | found arbitrary text 137 | in "", line 1, column 1: 138 | a: string 139 | ^ (line: 1) 140 | ``` 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | !!! note "Executable specification" 149 | 150 | Documentation automatically generated from 151 | decimal.story 152 | storytests. -------------------------------------------------------------------------------- /docs/public/using/alpha/howto/either-or-validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Either/or schema validation of different, equally valid different kinds of YAML 3 | --- 4 | 5 | 6 | StrictYAML can be directed to parse two different elements or 7 | blocks of YAML. 8 | 9 | If the first thing does not parse correctly, it attempts to 10 | parse the second. If the second does not parse correctly, 11 | it raises an exception. 12 | 13 | 14 | 15 | 16 | ```python 17 | from strictyaml import Map, Seq, Bool, Int, Str, YAMLValidationError, load 18 | from ensure import Ensure 19 | 20 | schema = Str() | Map({"a": Bool() | Int()}) 21 | 22 | ``` 23 | 24 | 25 | 26 | Boolean first choice true: 27 | 28 | ```yaml 29 | a: yes 30 | ``` 31 | 32 | 33 | ```python 34 | Ensure(load(yaml_snippet, schema)).equals({"a": True}) 35 | 36 | ``` 37 | 38 | 39 | 40 | 41 | Boolean first choice false: 42 | 43 | ```yaml 44 | a: no 45 | ``` 46 | 47 | 48 | ```python 49 | Ensure(load(yaml_snippet, schema)).equals({"a": False}) 50 | 51 | ``` 52 | 53 | 54 | 55 | 56 | Int second choice: 57 | 58 | ```yaml 59 | a: 5 60 | ``` 61 | 62 | 63 | ```python 64 | Ensure(load(yaml_snippet, schema)).equals({"a": 5}) 65 | 66 | ``` 67 | 68 | 69 | 70 | 71 | Invalid not bool or int: 72 | 73 | ```yaml 74 | a: A 75 | ``` 76 | 77 | 78 | ```python 79 | load(yaml_snippet, schema) 80 | ``` 81 | 82 | 83 | ```python 84 | strictyaml.exceptions.YAMLValidationError: 85 | when expecting an integer 86 | found arbitrary text 87 | in "", line 1, column 1: 88 | a: A 89 | ^ (line: 1) 90 | ``` 91 | 92 | 93 | 94 | 95 | Invalid combinations of more than one map: 96 | 97 | ```yaml 98 | a: x 99 | ``` 100 | 101 | 102 | ```python 103 | load(yaml_snippet, Map({"a": Str()}) | Map({"b": Str()})) 104 | 105 | ``` 106 | 107 | 108 | ```python 109 | strictyaml.exceptions.InvalidValidatorError: 110 | You tried to Or ('|') together 2 Map validators. Try using revalidation instead. 111 | ``` 112 | 113 | 114 | 115 | 116 | Invalid combinations of more than one seq: 117 | 118 | ```yaml 119 | - 1 120 | - 2 121 | 122 | ``` 123 | 124 | 125 | ```python 126 | load(yaml_snippet, Seq(Int()) | Seq(Str())) 127 | 128 | ``` 129 | 130 | 131 | ```python 132 | strictyaml.exceptions.InvalidValidatorError: 133 | You tried to Or ('|') together 2 Seq validators. Try using revalidation instead. 134 | ``` 135 | 136 | 137 | 138 | 139 | Change item after validated: 140 | 141 | ```yaml 142 | a: yes 143 | ``` 144 | 145 | 146 | ```python 147 | yaml = load(yaml_snippet, schema) 148 | yaml['a'] = 5 149 | assert yaml['a'] == 5 150 | 151 | ``` 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | !!! note "Executable specification" 160 | 161 | Documentation automatically generated from 162 | or.story 163 | storytests. -------------------------------------------------------------------------------- /docs/public/using/alpha/compound/mapping-yaml-object.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using a YAML object of a parsed mapping 3 | --- 4 | 5 | 6 | When a YAML document with mappings is parsed, it is not parsed 7 | as a dict but as a YAML object which behaves very similarly to 8 | a dict, but with some extra capabilities. 9 | 10 | You can use .items(), .keys(), .values(), look up items with 11 | square bracket notation, .get(key, with_default_if_nonexistent) 12 | and use "x in y" notation to determine key membership. 13 | 14 | To retrieve the equivalent dict (containing just other dicts, lists 15 | and strings/ints/etc.) use .data. 16 | 17 | 18 | Example yaml_snippet: 19 | 20 | ```yaml 21 | a: 1 22 | b: 2 23 | c: 3 24 | 25 | ``` 26 | 27 | 28 | ```python 29 | from strictyaml import Map, Int, load 30 | from ensure import Ensure 31 | 32 | schema = Map({"a": Int(), "b": Int(), "c": Int()}) 33 | 34 | ``` 35 | 36 | 37 | 38 | .is_mapping(): 39 | 40 | 41 | ```python 42 | Ensure(load(yaml_snippet, schema).is_mapping()).is_true() 43 | 44 | ``` 45 | 46 | 47 | 48 | 49 | Equivalence with equivalent plain dict: 50 | 51 | 52 | ```python 53 | Ensure(load(yaml_snippet, schema)).equals({"a": 1, "b": 2, "c": 3}) 54 | 55 | ``` 56 | 57 | 58 | 59 | 60 | .items(): 61 | 62 | 63 | ```python 64 | Ensure(load(yaml_snippet, schema).items()).equals([("a", 1), ("b", 2), ("c", 3)]) 65 | 66 | ``` 67 | 68 | 69 | 70 | 71 | Use in to detect presence of a key: 72 | 73 | 74 | ```python 75 | Ensure("a" in load(yaml_snippet, schema)).is_true() 76 | 77 | ``` 78 | 79 | 80 | 81 | 82 | .values(): 83 | 84 | 85 | ```python 86 | Ensure(load(yaml_snippet, schema).values()).equals([1, 2, 3]) 87 | 88 | ``` 89 | 90 | 91 | 92 | 93 | .keys(): 94 | 95 | 96 | ```python 97 | Ensure(load(yaml_snippet, schema).keys()).equals(["a", "b", "c"]) 98 | 99 | ``` 100 | 101 | 102 | 103 | 104 | Key lookup: 105 | 106 | 107 | ```python 108 | yaml = load(yaml_snippet, schema) 109 | Ensure(yaml[yaml.keys()[0]]).equals(1) 110 | 111 | ``` 112 | 113 | 114 | 115 | 116 | Dict lookup: 117 | 118 | 119 | ```python 120 | Ensure(load(yaml_snippet, schema)["a"]).equals(1) 121 | 122 | ``` 123 | 124 | 125 | 126 | 127 | .get(): 128 | 129 | 130 | ```python 131 | Ensure(load(yaml_snippet, schema).get("a")).equals(1) 132 | 133 | ``` 134 | 135 | 136 | 137 | 138 | .get() nonexistent: 139 | 140 | 141 | ```python 142 | Ensure(load(yaml_snippet, schema).get("nonexistent")).equals(None) 143 | 144 | ``` 145 | 146 | 147 | 148 | 149 | len(): 150 | 151 | 152 | ```python 153 | Ensure(len(load(yaml_snippet, schema))).equals(3) 154 | 155 | ``` 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | !!! note "Executable specification" 164 | 165 | Documentation automatically generated from 166 | mapping-representation.story 167 | storytests. -------------------------------------------------------------------------------- /hitch/story/or.story: -------------------------------------------------------------------------------- 1 | Either/or schema validation of different, equally valid different kinds of YAML: 2 | docs: howto/either-or-validation 3 | description: | 4 | StrictYAML can be directed to parse two different elements or 5 | blocks of YAML. 6 | 7 | If the first thing does not parse correctly, it attempts to 8 | parse the second. If the second does not parse correctly, 9 | it raises an exception. 10 | based on: strictyaml 11 | given: 12 | setup: | 13 | from strictyaml import Map, Seq, Bool, Int, Str, YAMLValidationError, load 14 | from ensure import Ensure 15 | 16 | schema = Str() | Map({"a": Bool() | Int()}) 17 | variations: 18 | Boolean first choice true: 19 | given: 20 | yaml_snippet: 'a: yes' 21 | steps: 22 | - Run: 23 | code: | 24 | Ensure(load(yaml_snippet, schema)).equals({"a": True}) 25 | 26 | Boolean first choice false: 27 | given: 28 | yaml_snippet: 'a: no' 29 | steps: 30 | - Run: 31 | code: | 32 | Ensure(load(yaml_snippet, schema)).equals({"a": False}) 33 | 34 | Int second choice: 35 | given: 36 | yaml_snippet: 'a: 5' 37 | steps: 38 | - Run: 39 | code: | 40 | Ensure(load(yaml_snippet, schema)).equals({"a": 5}) 41 | 42 | Invalid not bool or int: 43 | given: 44 | yaml_snippet: 'a: A' 45 | steps: 46 | - Run: 47 | code: load(yaml_snippet, schema) 48 | raises: 49 | type: strictyaml.exceptions.YAMLValidationError 50 | message: |- 51 | when expecting an integer 52 | found arbitrary text 53 | in "", line 1, column 1: 54 | a: A 55 | ^ (line: 1) 56 | 57 | Invalid combinations of more than one map: 58 | given: 59 | yaml_snippet: 'a: x' 60 | steps: 61 | - Run: 62 | code: | 63 | load(yaml_snippet, Map({"a": Str()}) | Map({"b": Str()})) 64 | raises: 65 | type: strictyaml.exceptions.InvalidValidatorError 66 | message: You tried to Or ('|') together 2 Map validators. Try using revalidation 67 | instead. 68 | 69 | Invalid combinations of more than one seq: 70 | given: 71 | yaml_snippet: | 72 | - 1 73 | - 2 74 | steps: 75 | - Run: 76 | code: | 77 | load(yaml_snippet, Seq(Int()) | Seq(Str())) 78 | raises: 79 | type: strictyaml.exceptions.InvalidValidatorError 80 | message: You tried to Or ('|') together 2 Seq validators. Try using revalidation 81 | instead. 82 | 83 | Change item after validated: 84 | given: 85 | yaml_snippet: 'a: yes' 86 | steps: 87 | - Run: 88 | code: | 89 | yaml = load(yaml_snippet, schema) 90 | yaml['a'] = 5 91 | assert yaml['a'] == 5 92 | -------------------------------------------------------------------------------- /docs/public/features-removed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What YAML features does StrictYAML remove? 3 | --- 4 | 5 | StrictYAML restricts you from parsing a number of things which 6 | the YAML specification says should be parsed. An issue has 7 | been [raised](https://github.com/yaml/YAML2/issues/8) by 8 | [David Seaward](https://github.com/lofidevops) about this critique 9 | on the official YAML repository. 10 | 11 | This document lists those of those features: 12 | 13 | 14 | ## Implicit Typing ([Why?](../why/implicit-typing-removed)) 15 | 16 | ```yaml 17 | x: yes 18 | y: null 19 | ``` 20 | 21 | Example pyyaml/ruamel/poyo: 22 | 23 | ```python 24 | load(yaml) == {"x": True, "y": None} 25 | ``` 26 | 27 | Example StrictYAML without schema: 28 | 29 | ```python 30 | load(yaml) == {"x": "yes", "y": "null"} 31 | ``` 32 | 33 | Example StrictYAML with schema: 34 | 35 | ```python 36 | load(yaml, Map({"x": Bool(), "y": Str()})) == {"x": True, "y": "null"} 37 | ``` 38 | 39 | 40 | ## Direct representations of objects ([Why?](../why/not-parse-direct-representations-of-python-objects)) 41 | 42 | ```yaml 43 | --- !python/hash:UnsafeUserObject 44 | email: evilhacker@hacker.com 45 | password: passwordtoset 46 | type: admin 47 | ``` 48 | 49 | Example pyyaml/ruamel: 50 | 51 | ```python 52 | load(yaml) == {'evil': b'z\xf8\xa5u\xabZ'} 53 | ``` 54 | 55 | Example StrictYAML 56 | 57 | ```python 58 | raises TagTokenDisallowed 59 | ``` 60 | 61 | 62 | ## Duplicate Keys Disallowed ([Why?](../why/duplicate-keys-disallowed)) 63 | 64 | ```yaml 65 | x: 1 66 | x: 2 67 | ``` 68 | 69 | Example pyyaml/poyo: 70 | 71 | ```python 72 | load(yaml) == {'x': 2} 73 | ``` 74 | 75 | Example StrictYAML 76 | 77 | ```python 78 | raises DuplicateKeysDisallowed 79 | ``` 80 | 81 | 82 | ## Explicit tags ([Why?](../why/explicit-tags-removed)) 83 | 84 | ```yaml 85 | x: !!int 5 86 | ``` 87 | 88 | Example pyyaml/ruamel/poyo: 89 | 90 | ```python 91 | load(yaml) == load(yaml) == {"x": 5} 92 | ``` 93 | 94 | Example StrictYAML 95 | 96 | ```python 97 | raises TagTokenDisallowed 98 | ``` 99 | 100 | 101 | ## Node anchors and refs ([Why?](../why/node-anchors-and-references-removed)) 102 | 103 | ```yaml 104 | x: &id001 105 | a: 1 106 | y: *id001 107 | ``` 108 | 109 | Example pyyaml/ruamel/poyo: 110 | 111 | ```python 112 | load(yaml) == {'x': {'a': 1}, 'y': {'a': 1}} 113 | ``` 114 | 115 | Example StrictYAML 116 | 117 | ```python 118 | raises NodeAnchorDisallowed 119 | ``` 120 | 121 | To parse the above YAML *literally* in StrictYAML do: 122 | 123 | ```yaml 124 | x: '&id001' 125 | a: 1 126 | y: '*id001' 127 | ``` 128 | 129 | 130 | ## Flow style ([Why?](../why/flow-style-removed)) 131 | 132 | ```yaml 133 | x: 1 134 | b: {c: 3, d: 4} 135 | ``` 136 | 137 | Example pyyaml/ruamel/poyo: 138 | 139 | ```python 140 | load(yaml) == {'x': 1, 'b': {'c': 3, 'd': 4}} 141 | ``` 142 | 143 | Example StrictYAML 144 | 145 | ```python 146 | raises FlowStyleDisallowed 147 | ``` 148 | -------------------------------------------------------------------------------- /docs/src/features-removed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What YAML features does StrictYAML remove? 3 | --- 4 | 5 | StrictYAML restricts you from parsing a number of things which 6 | the YAML specification says should be parsed. An issue has 7 | been [raised](https://github.com/yaml/YAML2/issues/8) by 8 | [David Seaward](https://github.com/lofidevops) about this critique 9 | on the official YAML repository. 10 | 11 | This document lists those of those features: 12 | 13 | 14 | ## Implicit Typing ([Why?](../why/implicit-typing-removed)) 15 | 16 | ```yaml 17 | x: yes 18 | y: null 19 | ``` 20 | 21 | Example pyyaml/ruamel/poyo: 22 | 23 | ```python 24 | load(yaml) == {"x": True, "y": None} 25 | ``` 26 | 27 | Example StrictYAML without schema: 28 | 29 | ```python 30 | load(yaml) == {"x": "yes", "y": "null"} 31 | ``` 32 | 33 | Example StrictYAML with schema: 34 | 35 | ```python 36 | load(yaml, Map({"x": Bool(), "y": Str()})) == {"x": True, "y": "null"} 37 | ``` 38 | 39 | 40 | ## Direct representations of objects ([Why?](../why/not-parse-direct-representations-of-python-objects)) 41 | 42 | ```yaml 43 | --- !python/hash:UnsafeUserObject 44 | email: evilhacker@hacker.com 45 | password: passwordtoset 46 | type: admin 47 | ``` 48 | 49 | Example pyyaml/ruamel: 50 | 51 | ```python 52 | load(yaml) == {'evil': b'z\xf8\xa5u\xabZ'} 53 | ``` 54 | 55 | Example StrictYAML 56 | 57 | ```python 58 | raises TagTokenDisallowed 59 | ``` 60 | 61 | 62 | ## Duplicate Keys Disallowed ([Why?](../why/duplicate-keys-disallowed)) 63 | 64 | ```yaml 65 | x: 1 66 | x: 2 67 | ``` 68 | 69 | Example pyyaml/poyo: 70 | 71 | ```python 72 | load(yaml) == {'x': 2} 73 | ``` 74 | 75 | Example StrictYAML 76 | 77 | ```python 78 | raises DuplicateKeysDisallowed 79 | ``` 80 | 81 | 82 | ## Explicit tags ([Why?](../why/explicit-tags-removed)) 83 | 84 | ```yaml 85 | x: !!int 5 86 | ``` 87 | 88 | Example pyyaml/ruamel/poyo: 89 | 90 | ```python 91 | load(yaml) == load(yaml) == {"x": 5} 92 | ``` 93 | 94 | Example StrictYAML 95 | 96 | ```python 97 | raises TagTokenDisallowed 98 | ``` 99 | 100 | 101 | ## Node anchors and refs ([Why?](../why/node-anchors-and-references-removed)) 102 | 103 | ```yaml 104 | x: &id001 105 | a: 1 106 | y: *id001 107 | ``` 108 | 109 | Example pyyaml/ruamel/poyo: 110 | 111 | ```python 112 | load(yaml) == {'x': {'a': 1}, 'y': {'a': 1}} 113 | ``` 114 | 115 | Example StrictYAML 116 | 117 | ```python 118 | raises NodeAnchorDisallowed 119 | ``` 120 | 121 | To parse the above YAML *literally* in StrictYAML do: 122 | 123 | ```yaml 124 | x: '&id001' 125 | a: 1 126 | y: '*id001' 127 | ``` 128 | 129 | 130 | ## Flow style ([Why?](../why/flow-style-removed)) 131 | 132 | ```yaml 133 | x: 1 134 | b: {c: 3, d: 4} 135 | ``` 136 | 137 | Example pyyaml/ruamel/poyo: 138 | 139 | ```python 140 | load(yaml) == {'x': 1, 'b': {'c': 3, 'd': 4}} 141 | ``` 142 | 143 | Example StrictYAML 144 | 145 | ```python 146 | raises FlowStyleDisallowed 147 | ``` 148 | -------------------------------------------------------------------------------- /hitch/story/whatline.story: -------------------------------------------------------------------------------- 1 | Get line numbers of YAML elements: 2 | based on: strictyaml 3 | docs: howto/what-line 4 | description: | 5 | Line numbers, the text of an item and text of surrounding lines 6 | can be grabbed from returned YAML objects - using .start_line, 7 | .end_line, lines(), lines_before(x) and lines_after(x). 8 | given: 9 | yaml_snippet: | 10 | y: p 11 | # Some comment 12 | 13 | a: | 14 | x 15 | 16 | # Another comment 17 | b: y 18 | c: a 19 | 20 | d: b 21 | 22 | setup: | 23 | from strictyaml import Map, Str, YAMLValidationError, load 24 | 25 | schema = Map({"y": Str(), "a": Str(), "b": Str(), "c": Str(), "d": Str()}) 26 | 27 | snippet = load(yaml_snippet, schema) 28 | 29 | variations: 30 | If there is preceding comment for an item the start line includes it: 31 | steps: 32 | - Run: 33 | code: | 34 | assert snippet["a"].start_line == 3 35 | assert snippet["d"].start_line == 9 36 | 37 | If there is a trailing comment the end line includes it: 38 | steps: 39 | - Run: 40 | code: | 41 | assert snippet["a"].end_line == 6 42 | assert snippet["d"].end_line == 10 43 | 44 | You can grab the start line of a key: 45 | steps: 46 | - Run: 47 | code: | 48 | assert snippet.keys()[1].start_line == 3 49 | 50 | Start line and end line of whole snippet: 51 | steps: 52 | - Run: 53 | code: | 54 | assert snippet.start_line == 1 55 | assert snippet.end_line == 10 56 | 57 | Grabbing a line before an item: 58 | steps: 59 | - Run: 60 | code: | 61 | assert snippet['a'].lines_before(1) == "# Some comment" 62 | 63 | Grabbing a line after an item: 64 | steps: 65 | - Run: 66 | code: | 67 | assert snippet['a'].lines_after(4) == "b: y\nc: a\n\nd: b" 68 | 69 | Grabbing the lines of an item including surrounding comments: 70 | steps: 71 | - Run: 72 | code: | 73 | print(load(yaml_snippet, schema)['a'].lines()) 74 | will output: |- 75 | a: | 76 | x 77 | 78 | # Another comment 79 | 80 | Start line of YAML with list: 81 | based on: strictyaml 82 | description: | 83 | Actually, this should probably be 6, not 4. This is likely a 84 | bug in ruamel.yaml however. 85 | 86 | TODO: Come back to this test. 87 | given: 88 | yaml_snippet: | 89 | a: 90 | b: 91 | - 1 92 | # comment 93 | # second comment 94 | - 2 95 | - 3 96 | - 4 97 | setup: | 98 | from strictyaml import load 99 | steps: 100 | - Run: 101 | code: |- 102 | assert load(yaml_snippet)['a']['b'][1].start_line == 4 103 | assert load(yaml_snippet)['a']['b'][1].end_line == 4 104 | -------------------------------------------------------------------------------- /strictyaml/exceptions.py: -------------------------------------------------------------------------------- 1 | from strictyaml.ruamel.error import MarkedYAMLError 2 | from strictyaml.ruamel.dumper import RoundTripDumper 3 | from strictyaml.ruamel import dump 4 | 5 | try: 6 | from strictyaml.ruamel.error import Mark as StringMark 7 | except ImportError: 8 | from strictyaml.ruamel.error import StringMark 9 | 10 | 11 | class StrictYAMLError(MarkedYAMLError): 12 | pass 13 | 14 | 15 | class InvalidValidatorError(StrictYAMLError): 16 | pass 17 | 18 | 19 | class CannotBuildDocumentFromInvalidData(StrictYAMLError): 20 | pass 21 | 22 | 23 | class CannotBuildDocumentsFromEmptyDictOrList(StrictYAMLError): 24 | pass 25 | 26 | 27 | class YAMLSerializationError(StrictYAMLError): 28 | pass 29 | 30 | 31 | class InvalidOptionalDefault(YAMLSerializationError): 32 | pass 33 | 34 | 35 | class YAMLValidationError(StrictYAMLError): 36 | def __init__(self, context, problem, chunk): 37 | self.context = context 38 | self.problem = problem 39 | self._chunk = chunk 40 | self.note = None 41 | 42 | @property 43 | def context_mark(self): 44 | context_line = self._chunk.start_line() - 1 45 | str_document = dump(self._chunk.whole_document, Dumper=RoundTripDumper) 46 | context_index = len("\n".join(str_document.split("\n")[:context_line])) 47 | return StringMark( 48 | self._chunk.label, 49 | context_index, 50 | context_line, 51 | 0, 52 | str_document, 53 | context_index + 1, 54 | ) 55 | 56 | @property 57 | def problem_mark(self): 58 | problem_line = self._chunk.end_line() - 1 59 | str_document = dump(self._chunk.whole_document, Dumper=RoundTripDumper) 60 | problem_index = len("\n".join(str_document.split("\n")[:problem_line])) 61 | return StringMark( 62 | self._chunk.label, 63 | problem_index, 64 | problem_line, 65 | 0, 66 | str_document, 67 | problem_index + 1, 68 | ) 69 | 70 | 71 | class DisallowedToken(StrictYAMLError): 72 | MESSAGE = "Disallowed token" 73 | 74 | 75 | class TagTokenDisallowed(DisallowedToken): 76 | MESSAGE = "Tag tokens not allowed" 77 | 78 | 79 | class FlowMappingDisallowed(DisallowedToken): 80 | MESSAGE = "Flow mapping tokens not allowed" 81 | 82 | 83 | class AnchorTokenDisallowed(DisallowedToken): 84 | MESSAGE = "Anchor tokens not allowed" 85 | 86 | 87 | class DuplicateKeysDisallowed(DisallowedToken): 88 | MESSAGE = "Duplicate keys not allowed" 89 | 90 | 91 | class InconsistentIndentationDisallowed(DisallowedToken): 92 | MESSAGE = "Inconsistent indentation not allowed" 93 | 94 | 95 | def raise_type_error(yaml_object, to_type, alternatives): 96 | raise TypeError( 97 | ("Cannot cast {0} to {1}.\n" "Use {2} instead.").format( 98 | repr(yaml_object), to_type, alternatives 99 | ) 100 | ) 101 | -------------------------------------------------------------------------------- /hitch/story/empty.story: -------------------------------------------------------------------------------- 1 | Empty key validation: 2 | docs: scalar/empty 3 | based on: strictyaml 4 | description: | 5 | Sometimes you may wish to not specify a value or specify 6 | that it does not exist. 7 | 8 | Using StrictYAML you can accept this as a valid value and 9 | have it parsed to one of three things - None, {} (empty dict), 10 | or [] (empty list). 11 | given: 12 | setup: | 13 | from strictyaml import Map, Str, Enum, EmptyNone, EmptyDict, EmptyList, NullNone, load, as_document 14 | from ensure import Ensure 15 | yaml_snippet: 'a:' 16 | variations: 17 | EmptyNone with empty value: 18 | steps: 19 | - Run: | 20 | Ensure(load(yaml_snippet, Map({"a": EmptyNone() | Enum(["A", "B",])}))).equals({"a": None}) 21 | 22 | EmptyDict: 23 | steps: 24 | - Run: | 25 | Ensure(load(yaml_snippet, Map({"a": EmptyDict() | Enum(["A", "B",])}))).equals({"a": {}}) 26 | 27 | EmptyList: 28 | steps: 29 | - Run: | 30 | Ensure(load(yaml_snippet, Map({"a": EmptyList() | Enum(["A", "B",])}))).equals({"a": []}) 31 | 32 | NullNone: 33 | steps: 34 | - Run: | 35 | Ensure(load("a: null", Map({"a": NullNone() | Enum(["A", "B",])}))).equals({"a": None}) 36 | 37 | EmptyNone no empty value: 38 | given: 39 | yaml_snippet: 'a: A' 40 | steps: 41 | - Run: | 42 | Ensure(load(yaml_snippet, Map({"a": EmptyNone() | Enum(["A", "B",])}))).equals({"a": "A"}) 43 | 44 | Combine Str with EmptyNone and Str is evaluated first: 45 | steps: 46 | - Run: | 47 | Ensure(load(yaml_snippet, Map({"a": Str() | EmptyNone()}))).equals({"a": ""}) 48 | 49 | 50 | Combine EmptyNone with Str and Str is evaluated last: 51 | steps: 52 | - Run: | 53 | Ensure(load(yaml_snippet, Map({"a": EmptyNone() | Str()}))).equals({"a": None}) 54 | 55 | Non-empty value: 56 | given: 57 | yaml_snippet: 'a: C' 58 | steps: 59 | - Run: 60 | code: | 61 | load(yaml_snippet, Map({"a": Enum(["A", "B",]) | EmptyNone()})) 62 | raises: 63 | type: strictyaml.exceptions.YAMLValidationError 64 | message: |- 65 | when expecting an empty value 66 | found arbitrary text 67 | in "", line 1, column 1: 68 | a: C 69 | ^ (line: 1) 70 | 71 | Serialize empty dict: 72 | steps: 73 | - Run: 74 | code: | 75 | print(as_document({"a": {}}, Map({"a": EmptyDict() | Str()})).as_yaml()) 76 | will output: 'a:' 77 | 78 | Serialize empty list: 79 | steps: 80 | - Run: 81 | code: | 82 | print(as_document({"a": []}, Map({"a": EmptyList() | Str()})).as_yaml()) 83 | will output: 'a:' 84 | 85 | Serialize None: 86 | steps: 87 | - Run: 88 | code: | 89 | print(as_document({"a": None}, Map({"a": EmptyNone() | Str()})).as_yaml()) 90 | will output: 'a:' 91 | -------------------------------------------------------------------------------- /docs/public/using/alpha/scalar/enum.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Enumerated scalars (Enum) 3 | --- 4 | 5 | 6 | StrictYAML allows you to ensure that a scalar 7 | value can only be one of a set number of items. 8 | 9 | It will throw an exception if any strings not 10 | in the list are found. 11 | 12 | 13 | 14 | 15 | ```python 16 | from strictyaml import Map, Enum, MapPattern, YAMLValidationError, load 17 | from collections import OrderedDict 18 | from ensure import Ensure 19 | 20 | schema = Map({"a": Enum(["A", "B", "C"])}) 21 | 22 | ``` 23 | 24 | 25 | 26 | Valid because it contains 'A': 27 | 28 | ```yaml 29 | a: A 30 | ``` 31 | 32 | 33 | ```python 34 | Ensure(load(yaml_snippet, schema)).equals({"a": "A"}) 35 | 36 | ``` 37 | 38 | 39 | 40 | 41 | Get .data from enum: 42 | 43 | ```yaml 44 | a: A 45 | ``` 46 | 47 | 48 | ```python 49 | assert isinstance(load(yaml_snippet, schema)['a'].data, str) 50 | 51 | ``` 52 | 53 | 54 | 55 | 56 | Valid because it contains 'B': 57 | 58 | ```yaml 59 | a: B 60 | ``` 61 | 62 | 63 | ```python 64 | Ensure(load(yaml_snippet, schema)).equals({"a": "B"}) 65 | 66 | ``` 67 | 68 | 69 | 70 | 71 | Valid because it contains 'C': 72 | 73 | ```yaml 74 | a: C 75 | ``` 76 | 77 | 78 | ```python 79 | Ensure(load(yaml_snippet, schema)).equals({"a": "C"}) 80 | 81 | ``` 82 | 83 | 84 | 85 | 86 | Invalid because D is not in enum: 87 | 88 | ```yaml 89 | a: D 90 | ``` 91 | 92 | 93 | ```python 94 | load(yaml_snippet, schema) 95 | ``` 96 | 97 | 98 | ```python 99 | strictyaml.exceptions.YAMLValidationError: 100 | when expecting one of: A, B, C 101 | found arbitrary text 102 | in "", line 1, column 1: 103 | a: D 104 | ^ (line: 1) 105 | ``` 106 | 107 | 108 | 109 | 110 | Invalid because blank string is not in enum: 111 | 112 | ```yaml 113 | a: 114 | ``` 115 | 116 | 117 | ```python 118 | load(yaml_snippet, schema) 119 | ``` 120 | 121 | 122 | ```python 123 | strictyaml.exceptions.YAMLValidationError: 124 | when expecting one of: A, B, C 125 | found a blank string 126 | in "", line 1, column 1: 127 | a: '' 128 | ^ (line: 1) 129 | ``` 130 | 131 | 132 | 133 | 134 | Successful serialization: 135 | 136 | ```yaml 137 | a: A 138 | ``` 139 | 140 | 141 | ```python 142 | yaml = load(yaml_snippet, schema) 143 | yaml['a'] = "B" 144 | print(yaml.as_yaml()) 145 | 146 | ``` 147 | 148 | ```yaml 149 | a: B 150 | ``` 151 | 152 | 153 | 154 | 155 | Invalid serialization: 156 | 157 | ```yaml 158 | a: A 159 | ``` 160 | 161 | 162 | ```python 163 | yaml = load(yaml_snippet, schema) 164 | yaml['a'] = "D" 165 | print(yaml.as_yaml()) 166 | 167 | ``` 168 | 169 | 170 | ```python 171 | strictyaml.exceptions.YAMLSerializationError: 172 | Got 'D' when expecting one of: A, B, C 173 | ``` 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | !!! note "Executable specification" 182 | 183 | Documentation automatically generated from 184 | enum.story 185 | storytests. -------------------------------------------------------------------------------- /hitch/story/enum.story: -------------------------------------------------------------------------------- 1 | Enumerated scalars (Enum): 2 | docs: scalar/enum 3 | based on: strictyaml 4 | description: | 5 | StrictYAML allows you to ensure that a scalar 6 | value can only be one of a set number of items. 7 | 8 | It will throw an exception if any strings not 9 | in the list are found. 10 | given: 11 | setup: | 12 | from strictyaml import Map, Enum, MapPattern, YAMLValidationError, load 13 | from collections import OrderedDict 14 | from ensure import Ensure 15 | 16 | schema = Map({"a": Enum(["A", "B", "C"])}) 17 | variations: 18 | Valid because it contains 'A': 19 | given: 20 | yaml_snippet: 'a: A' 21 | steps: 22 | - Run: | 23 | Ensure(load(yaml_snippet, schema)).equals({"a": "A"}) 24 | 25 | Get .data from enum: 26 | given: 27 | yaml_snippet: 'a: A' 28 | steps: 29 | - Run: | 30 | assert isinstance(load(yaml_snippet, schema)['a'].data, str) 31 | 32 | Valid because it contains 'B': 33 | given: 34 | yaml_snippet: 'a: B' 35 | steps: 36 | - Run: 37 | code: | 38 | Ensure(load(yaml_snippet, schema)).equals({"a": "B"}) 39 | 40 | Valid because it contains 'C': 41 | given: 42 | yaml_snippet: 'a: C' 43 | steps: 44 | - Run: 45 | code: | 46 | Ensure(load(yaml_snippet, schema)).equals({"a": "C"}) 47 | 48 | Invalid because D is not in enum: 49 | given: 50 | yaml_snippet: 'a: D' 51 | steps: 52 | - Run: 53 | code: load(yaml_snippet, schema) 54 | raises: 55 | type: strictyaml.exceptions.YAMLValidationError 56 | message: |- 57 | when expecting one of: A, B, C 58 | found arbitrary text 59 | in "", line 1, column 1: 60 | a: D 61 | ^ (line: 1) 62 | 63 | Invalid because blank string is not in enum: 64 | given: 65 | yaml_snippet: 'a:' 66 | steps: 67 | - Run: 68 | code: load(yaml_snippet, schema) 69 | raises: 70 | type: strictyaml.exceptions.YAMLValidationError 71 | message: |- 72 | when expecting one of: A, B, C 73 | found a blank string 74 | in "", line 1, column 1: 75 | a: '' 76 | ^ (line: 1) 77 | 78 | Successful serialization: 79 | given: 80 | yaml_snippet: 'a: A' 81 | steps: 82 | - Run: 83 | code: | 84 | yaml = load(yaml_snippet, schema) 85 | yaml['a'] = "B" 86 | print(yaml.as_yaml()) 87 | will output: 'a: B' 88 | 89 | Invalid serialization: 90 | given: 91 | yaml_snippet: 'a: A' 92 | steps: 93 | - Run: 94 | code: | 95 | yaml = load(yaml_snippet, schema) 96 | yaml['a'] = "D" 97 | print(yaml.as_yaml()) 98 | raises: 99 | type: strictyaml.exceptions.YAMLSerializationError 100 | message: "Got 'D' when expecting one of: A, B, C" 101 | -------------------------------------------------------------------------------- /docs/public/using/alpha/compound/update.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Updating document with a schema 3 | --- 4 | 5 | 6 | When StrictYAML loads a document with a schema, it checks that future 7 | updates to that document follow the original schema. 8 | 9 | 10 | 11 | 12 | ```python 13 | import strictyaml as s 14 | from ensure import Ensure 15 | 16 | ``` 17 | 18 | 19 | 20 | GitHub \#72: 21 | 22 | 23 | ```python 24 | doc = s.load('a: 9', s.Map({ 25 | 'a': s.Str(), 26 | s.Optional('b'): s.Int(), 27 | })) 28 | doc['b'] = 9 29 | assert doc['b'] == 9 30 | ``` 31 | 32 | 33 | 34 | 35 | Works on empty mapping: 36 | 37 | 38 | ```python 39 | doc = s.load('', s.EmptyDict() | s.Map({ 40 | 'a': s.Int(), 41 | })) 42 | doc['a'] = 9 43 | assert doc['a'] == 9, doc.as_yaml() 44 | ``` 45 | 46 | 47 | 48 | 49 | Works on complex types: 50 | 51 | 52 | ```python 53 | doc = s.load('a: 8', s.Map({'a': s.Int() | s.Float()})) 54 | assert type(doc['a'].data) == int, repr(doc.data) 55 | doc['a'] = '5.' 56 | assert type(doc['a'].data) == float, repr(doc.data) 57 | assert doc['a'] == 5. 58 | ``` 59 | 60 | 61 | 62 | 63 | Will not work on empty sequence: 64 | 65 | 66 | ```python 67 | doc = s.load('', s.EmptyList() | s.Seq(s.Int())) 68 | doc[0] = 9 69 | 70 | ``` 71 | 72 | 73 | ```python 74 | strictyaml.exceptions.YAMLSerializationError: 75 | cannot extend list via __setitem__. Instead, replace whole list on parent node. 76 | ``` 77 | 78 | 79 | 80 | 81 | Works on map with setting, updating, and then setting multiple keys (regression): 82 | 83 | 84 | ```python 85 | doc = s.load('', s.EmptyDict() | s.MapPattern( 86 | s.Str(), 87 | s.EmptyDict() | s.Map({ 88 | s.Optional('b'): s.Seq(s.Int()), 89 | }) 90 | )) 91 | doc['a'] = {} 92 | doc['a']['b'] = ['9'] 93 | assert doc.data == {'a': {'b': [9]}}, doc.data 94 | assert doc.as_yaml() == 'a:\n b:\n - 9\n', doc.as_yaml() 95 | # Second assignment doesn't occur... 96 | doc['a']['b'] = ['9', '10'] 97 | assert doc.data == {'a': {'b': [9, 10]}}, doc.data 98 | assert doc.as_yaml() == 'a:\n b:\n - 9\n - 10\n', doc.as_yaml() 99 | # If and only if another node is overwritten. This was a bug due 100 | # to mismatched _ruamelparsed objects. 101 | doc['b'] = {'b': ['11']} 102 | assert doc['a']['b'].data == [9, 10], doc.data 103 | assert doc['b']['b'].data == [11], doc.data 104 | assert doc.as_yaml() == 'a:\n b:\n - 9\n - 10\nb:\n b:\n - 11\n', doc.as_yaml() 105 | 106 | ``` 107 | 108 | 109 | 110 | 111 | For empty sequence, must instead assign whole sequence as key: 112 | 113 | 114 | ```python 115 | doc = s.load('a:', s.Map({'a': s.EmptyList() | s.Seq(s.Int())})) 116 | doc['a'] = [1, 2, 3] 117 | assert doc['a'].data == [1, 2, 3], repr(doc.data) 118 | ``` 119 | 120 | 121 | 122 | 123 | Can assign from string: 124 | 125 | 126 | ```python 127 | doc = s.load('a: 9', s.Map({ 128 | 'a': s.Str(), 129 | s.Optional('b'): s.Int(), 130 | })) 131 | doc['b'] = '9' 132 | assert doc['b'] == 9 133 | ``` 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | !!! note "Executable specification" 142 | 143 | Documentation automatically generated from 144 | update-with-schema.story 145 | storytests. -------------------------------------------------------------------------------- /strictyaml/ruamel/loader.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | 6 | from strictyaml.ruamel.reader import Reader 7 | from strictyaml.ruamel.scanner import Scanner, RoundTripScanner 8 | from strictyaml.ruamel.parser import Parser, RoundTripParser 9 | from strictyaml.ruamel.composer import Composer 10 | from strictyaml.ruamel.constructor import ( 11 | BaseConstructor, 12 | SafeConstructor, 13 | Constructor, 14 | RoundTripConstructor, 15 | ) 16 | from strictyaml.ruamel.resolver import VersionedResolver 17 | 18 | if False: # MYPY 19 | from typing import Any, Dict, List, Union, Optional # NOQA 20 | from strictyaml.ruamel.compat import StreamTextType, VersionType # NOQA 21 | 22 | __all__ = ["BaseLoader", "SafeLoader", "Loader", "RoundTripLoader"] 23 | 24 | 25 | class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, VersionedResolver): 26 | def __init__(self, stream, version=None, preserve_quotes=None): 27 | # type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None 28 | Reader.__init__(self, stream, loader=self) 29 | Scanner.__init__(self, loader=self) 30 | Parser.__init__(self, loader=self) 31 | Composer.__init__(self, loader=self) 32 | BaseConstructor.__init__(self, loader=self) 33 | VersionedResolver.__init__(self, version, loader=self) 34 | 35 | 36 | class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, VersionedResolver): 37 | def __init__(self, stream, version=None, preserve_quotes=None): 38 | # type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None 39 | Reader.__init__(self, stream, loader=self) 40 | Scanner.__init__(self, loader=self) 41 | Parser.__init__(self, loader=self) 42 | Composer.__init__(self, loader=self) 43 | SafeConstructor.__init__(self, loader=self) 44 | VersionedResolver.__init__(self, version, loader=self) 45 | 46 | 47 | class Loader(Reader, Scanner, Parser, Composer, Constructor, VersionedResolver): 48 | def __init__(self, stream, version=None, preserve_quotes=None): 49 | # type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None 50 | Reader.__init__(self, stream, loader=self) 51 | Scanner.__init__(self, loader=self) 52 | Parser.__init__(self, loader=self) 53 | Composer.__init__(self, loader=self) 54 | Constructor.__init__(self, loader=self) 55 | VersionedResolver.__init__(self, version, loader=self) 56 | 57 | 58 | class RoundTripLoader( 59 | Reader, 60 | RoundTripScanner, 61 | RoundTripParser, 62 | Composer, 63 | RoundTripConstructor, 64 | VersionedResolver, 65 | ): 66 | def __init__(self, stream, version=None, preserve_quotes=None): 67 | # type: (StreamTextType, Optional[VersionType], Optional[bool]) -> None 68 | # self.reader = Reader.__init__(self, stream) 69 | Reader.__init__(self, stream, loader=self) 70 | RoundTripScanner.__init__(self, loader=self) 71 | RoundTripParser.__init__(self, loader=self) 72 | Composer.__init__(self, loader=self) 73 | RoundTripConstructor.__init__( 74 | self, preserve_quotes=preserve_quotes, loader=self 75 | ) 76 | VersionedResolver.__init__(self, version, loader=self) 77 | --------------------------------------------------------------------------------