Skip to content

Commit 49e7676

Browse files
authored
Merge pull request #244 from realpython/python-yaml
Supplementary materials for the YAML tutorial
2 parents 3d8e1b7 + 5999441 commit 49e7676

File tree

11 files changed

+522
-0
lines changed

11 files changed

+522
-0
lines changed

python-yaml/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# YAML: The Missing Battery in Python
2+
3+
Python scripts and sample data for the [YAML: The Missing Battery in Python](https://realpython.com/python-yaml/) tutorial on Real Python.
4+
5+
## Installation
6+
7+
This code requires the following libraries:
8+
9+
- PyYAML
10+
- FastAPI
11+
- uvicorn
12+
13+
To install them, activate your [virtual environment](https://realpython.com/python-virtual-environments-a-primer/), and type the following command:
14+
15+
```shell
16+
(venv) $ python -m pip install -r requirements.txt
17+
```
18+
19+
Additionally, you can install optional dependencies that were mentioned in the tutorial:
20+
21+
```shell
22+
(venv) $ python -m pip install yamllint yq shyaml
23+
```
24+
25+
## Usage
26+
27+
Print a sample YAML document with syntax highlighting:
28+
29+
```console
30+
$ cat data.yaml | python colorize.py
31+
```
32+
33+
Print a static HTML preview of a sample YAML document:
34+
35+
```console
36+
$ cat data.yaml | python yaml2html.py
37+
```
38+
39+
Print an interactive HTML preview of a sample YAML document:
40+
41+
```console
42+
$ cat data.yaml | python tree.py
43+
```
44+
45+
Start an interactive demo of a YAML formatter at <http://localhost:8000/>:
46+
47+
```console
48+
$ cd formatter/
49+
$ uvicorn server:app
50+
```

python-yaml/colorize.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# colorize.py
2+
3+
import sys
4+
5+
import yaml
6+
7+
8+
def colorize(text):
9+
colors = {
10+
yaml.KeyToken: lambda x: f"\033[34;1m{x}\033[0m",
11+
yaml.ValueToken: lambda x: f"\033[36m{x}\033[0m",
12+
yaml.TagToken: lambda x: f"\033[31m{x}\033[0m",
13+
}
14+
15+
for start, end, token in reversed(list(tokenize(text))):
16+
color = colors.get(type(token), lambda text: text)
17+
text = text[:start] + color(text[start:end]) + text[end:]
18+
19+
return text
20+
21+
22+
def tokenize(text, loader=yaml.SafeLoader):
23+
last_token = yaml.ValueToken(None, None)
24+
for token in yaml.scan(text, loader):
25+
start = token.start_mark.index
26+
end = token.end_mark.index
27+
if isinstance(token, yaml.TagToken):
28+
yield start, end, token
29+
elif isinstance(token, yaml.ScalarToken):
30+
yield start, end, last_token
31+
elif isinstance(token, (yaml.KeyToken, yaml.ValueToken)):
32+
last_token = token
33+
34+
35+
if __name__ == "__main__":
36+
print(colorize("".join(sys.stdin.readlines())))

python-yaml/data.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
message:
2+
date: 2022-01-16 12:46:17Z
3+
4+
to:
5+
6+
7+
cc:
8+
9+
subject: Friendly reminder
10+
content: |
11+
Dear XYZ,
12+
13+
Lorem ipsum dolor sit amet...
14+
attachments:
15+
image1.gif: !!binary
16+
R0lGODdhCAAIAPAAAAIGAfr4+SwAA
17+
AAACAAIAAACDIyPeWCsClxDMsZ3CgA7

python-yaml/formatter/server.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Running the backend:
3+
$ uvicorn formatter:app
4+
5+
Viewing the frontend:
6+
http://127.0.0.1:8000/
7+
"""
8+
9+
from typing import Optional
10+
11+
import yaml
12+
from fastapi import FastAPI
13+
from fastapi.responses import JSONResponse, HTMLResponse
14+
from fastapi.staticfiles import StaticFiles
15+
from pydantic import BaseModel
16+
17+
TEST_DATA = {
18+
"person": {
19+
"name_latin": "Ivan",
20+
"name": "Иван",
21+
"age": 42,
22+
}
23+
}
24+
25+
26+
class Parameters(BaseModel):
27+
# Boolean flags:
28+
allow_unicode: bool = False
29+
canonical: bool = False
30+
default_flow_style: bool = False
31+
explicit_end: bool = False
32+
explicit_start: bool = False
33+
sort_keys: bool = True
34+
35+
# Valued parameters:
36+
indent: Optional[int] = None
37+
width: Optional[int] = None
38+
default_style: Optional[str] = None
39+
encoding: Optional[str] = None
40+
line_break: Optional[str] = None
41+
version: Optional[list] = None
42+
43+
44+
app = FastAPI()
45+
app.mount("/static", StaticFiles(directory="static/"), name="static")
46+
47+
48+
@app.get("/", response_class=HTMLResponse)
49+
async def index():
50+
with open("views/index.html", "rb") as file:
51+
return file.read()
52+
53+
54+
@app.post("/")
55+
async def serialize(parameters: Parameters):
56+
try:
57+
serialized = yaml.dump(TEST_DATA, **vars(parameters))
58+
if isinstance(serialized, bytes):
59+
return repr(serialized)
60+
return serialized
61+
except Exception as ex:
62+
return JSONResponse(str(ex), status_code=400)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
document.addEventListener("DOMContentLoaded", async () => {
2+
3+
const allowUnicode = document.querySelector("#allow_unicode")
4+
const canonical = document.querySelector("#canonical")
5+
const content = document.querySelector("#content")
6+
const defaultFlowStyle = document.querySelector("#default_flow_style")
7+
const defaultStyle = document.querySelector("#default_style")
8+
const encoding = document.querySelector("#encoding")
9+
const explicitEnd = document.querySelector("#explicit_end")
10+
const explicitStart = document.querySelector("#explicit_start")
11+
const form = document.querySelector("form")
12+
const indent = document.querySelector("#indent")
13+
const lineBreak = document.querySelector("#line_break")
14+
const sortKeys = document.querySelector("#sort_keys")
15+
const version = document.querySelector("#version")
16+
const width = document.querySelector("#width")
17+
18+
const getPayload = () => {
19+
return {
20+
21+
// Boolean flags:
22+
allow_unicode: allowUnicode.checked,
23+
canonical: canonical.checked,
24+
default_flow_style: defaultFlowStyle.checked,
25+
explicit_end: explicitEnd.checked,
26+
explicit_start: explicitStart.checked,
27+
sort_keys: sortKeys.checked,
28+
29+
// Valued parameters:
30+
indent: parseInt(indent.value),
31+
width: parseInt(width.value),
32+
default_style: defaultStyle.value,
33+
encoding: encoding.value || null,
34+
line_break: lineBreak.value,
35+
version: (version.value && version.value.split(".").map(x => parseInt(x))) || null,
36+
}
37+
}
38+
39+
const reformat = async () => {
40+
try {
41+
const response = await fetch("/", {
42+
method: "POST",
43+
headers: {
44+
"Content-Type": "application/json",
45+
},
46+
body: JSON.stringify(getPayload()),
47+
})
48+
content.value = await response.json()
49+
} catch (error) {
50+
alert(`Error: ${error}`)
51+
}
52+
}
53+
54+
function showRangeValue() {
55+
this.previousElementSibling.innerText = this.value
56+
}
57+
58+
form.addEventListener("change", reformat)
59+
indent.addEventListener("change", showRangeValue)
60+
width.addEventListener("change", showRangeValue)
61+
62+
showRangeValue.call(indent)
63+
showRangeValue.call(width)
64+
await reformat()
65+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
* {
2+
box-sizing: border-box;
3+
font-family: "Roboto Condensed", sans-serif;
4+
font-size: 18pt;
5+
}
6+
7+
.column {
8+
float: left;
9+
margin: 15px;
10+
}
11+
12+
.row:after {
13+
content: "";
14+
display: table;
15+
clear: both;
16+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>YAML Formatter</title>
7+
<link rel="preconnect" href="https://fonts.googleapis.com">
8+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9+
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed&display=swap" rel="stylesheet"/>
10+
<link rel="stylesheet" href="/static/style.css">
11+
</head>
12+
<body>
13+
<form>
14+
<div class="row">
15+
<div class="column">
16+
<textarea id="content" readonly rows="15" cols="50"></textarea>
17+
</div>
18+
<div class="column">
19+
<h2>Boolean Flags</h2>
20+
<p>
21+
<input type="checkbox" id="allow_unicode" name="allow_unicode">
22+
<label for="allow_unicode">allow_unicode</label>
23+
</p>
24+
<p>
25+
<input type="checkbox" id="canonical" name="canonical">
26+
<label for="canonical">canonical</label>
27+
</p>
28+
<p>
29+
<input type="checkbox" id="default_flow_style" name="default_flow_style">
30+
<label for="default_flow_style">default_flow_style</label>
31+
</p>
32+
<p>
33+
<input type="checkbox" id="explicit_end" name="explicit_end">
34+
<label for="explicit_end">explicit_end</label>
35+
</p>
36+
<p>
37+
<input type="checkbox" id="explicit_start" name="explicit_start">
38+
<label for="explicit_start">explicit_start</label>
39+
</p>
40+
<p>
41+
<input type="checkbox" id="sort_keys" name="sort_keys" checked>
42+
<label for="sort_keys">sort_keys</label>
43+
</p>
44+
</div>
45+
<div class="column">
46+
<h2>Valued Parameters</h2>
47+
<p>
48+
<label for="indent">indent</label>
49+
<span></span>
50+
<input type="range" id="indent" name="indent" min="2" max="9" value="2">
51+
</p>
52+
<p>
53+
<label for="width">width</label>
54+
<span></span>
55+
<input type="range" id="width" name="width" min="2" max="50" value="50">
56+
</p>
57+
<p>
58+
<label for="default_style">default_style</label>
59+
<select id="default_style" name="default_style">
60+
<option value="">None</option>
61+
<option value="'">Single-quote '</option>
62+
<option value="&#34;">Double-quote "</option>
63+
</select>
64+
</p>
65+
<p>
66+
<label for="encoding">encoding</label>
67+
<select id="encoding" name="encoding">
68+
<option value="">None</option>
69+
<option value="utf-8">UTF-8</option>
70+
<option value="utf-16">UTF-16</option>
71+
<option value="utf-32">UTF-32</option>
72+
<option value="iso-8859-1">ISO-8859-1</option>
73+
<option value="iso-8859-2">ISO-8859-2</option>
74+
</select>
75+
</p>
76+
<p>
77+
<label for="line_break">line_break</label>
78+
<select id="line_break" name="line_break">
79+
<option value="&#13;">macOS \r</option>
80+
<option value="&#10;" selected>Linux \n</option>
81+
<option value="&#13;&#10;">Windows \r\n</option>
82+
</select>
83+
</p>
84+
<p>
85+
<label for="version">version</label>
86+
<select id="version" name="version">
87+
<option value="">None</option>
88+
<option value="1.1">1.1</option>
89+
<option value="1.2">1.2</option>
90+
<option value="1.99">1.99</option>
91+
<option value="2.0">2.0</option>
92+
</select>
93+
</p>
94+
</div>
95+
</div>
96+
</form>
97+
<script src="/static/app.js"></script>
98+
</body>
99+
</html>

python-yaml/models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# models.py
2+
3+
import codecs
4+
from dataclasses import dataclass
5+
6+
7+
@dataclass
8+
class Person:
9+
first_name: str
10+
last_name: str
11+
12+
13+
class User:
14+
__slots__ = ["name"]
15+
16+
def __init__(self, name):
17+
self.name = name
18+
19+
def __setstate__(self, state):
20+
self.name = codecs.decode(state["name"], "rot13")

python-yaml/requirements.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
anyio==3.5.0
2+
asgiref==3.5.0
3+
click==8.0.4
4+
fastapi==0.75.0
5+
h11==0.13.0
6+
idna==3.3
7+
pydantic==1.9.0
8+
PyYAML==6.0
9+
sniffio==1.2.0
10+
starlette==0.17.1
11+
typing_extensions==4.1.1
12+
uvicorn==0.17.6

0 commit comments

Comments
 (0)