python-frontmatter 1.0.1__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/MANIFEST.in +1 -0
- {python-frontmatter-1.0.1/python_frontmatter.egg-info → python_frontmatter-1.2.0}/PKG-INFO +27 -7
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/README.md +3 -3
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/frontmatter/__init__.py +82 -42
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/frontmatter/conftest.py +3 -1
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/frontmatter/default_handlers.py +55 -34
- python_frontmatter-1.2.0/frontmatter/py.typed +1 -0
- python_frontmatter-1.2.0/frontmatter/util.py +31 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0/python_frontmatter.egg-info}/PKG-INFO +27 -7
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/SOURCES.txt +1 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/requires.txt +3 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/setup.py +7 -3
- python-frontmatter-1.0.1/frontmatter/util.py +0 -15
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/LICENSE +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/dependency_links.txt +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/not-zip-safe +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/top_level.txt +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/setup.cfg +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/empty-frontmatter.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/empty-frontmatter.txt +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/no-frontmatter.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/no-frontmatter.txt +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/json/hello-json.md +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/json/hello-json.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/stub_tests.py +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/test_docs.py +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/test_files.py +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/toml/hello-toml.md +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/toml/hello-toml.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/unit_test.py +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/chinese.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/chinese.txt +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/extra-dash.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/extra-dash.txt +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/extra-space.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/extra-space.txt +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/hello-markdown.md +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/hello-markdown.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/hello-world.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/hello-world.txt +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/network-diagrams.md +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/network-diagrams.result.json +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/unpretty.md +0 -0
- {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/unpretty.result.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-frontmatter
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Parse and manage posts with YAML (or other) frontmatter
|
|
5
5
|
Home-page: https://github.com/eyeseast/python-frontmatter
|
|
6
6
|
Author: Chris Amico
|
|
@@ -12,15 +12,35 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: PyYAML
|
|
21
23
|
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Requires-Dist: toml; extra == "test"
|
|
26
|
+
Requires-Dist: pyaml; extra == "test"
|
|
27
|
+
Requires-Dist: mypy; extra == "test"
|
|
28
|
+
Requires-Dist: types-PyYAML; extra == "test"
|
|
29
|
+
Requires-Dist: types-toml; extra == "test"
|
|
22
30
|
Provides-Extra: docs
|
|
23
|
-
|
|
31
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: provides-extra
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: summary
|
|
24
44
|
|
|
25
45
|
# Python Frontmatter
|
|
26
46
|
|
|
@@ -134,10 +154,10 @@ Well, hello there, world.
|
|
|
134
154
|
Or write to a file (or file-like object):
|
|
135
155
|
|
|
136
156
|
```python
|
|
137
|
-
>>> from io import
|
|
138
|
-
>>> f =
|
|
157
|
+
>>> from io import StringIO
|
|
158
|
+
>>> f = StringIO()
|
|
139
159
|
>>> frontmatter.dump(post, f)
|
|
140
|
-
>>> print(f.getvalue()
|
|
160
|
+
>>> print(f.getvalue()) # doctest: +NORMALIZE_WHITESPACE
|
|
141
161
|
---
|
|
142
162
|
excerpt: tl;dr
|
|
143
163
|
layout: post
|
|
@@ -110,10 +110,10 @@ Well, hello there, world.
|
|
|
110
110
|
Or write to a file (or file-like object):
|
|
111
111
|
|
|
112
112
|
```python
|
|
113
|
-
>>> from io import
|
|
114
|
-
>>> f =
|
|
113
|
+
>>> from io import StringIO
|
|
114
|
+
>>> f = StringIO()
|
|
115
115
|
>>> frontmatter.dump(post, f)
|
|
116
|
-
>>> print(f.getvalue()
|
|
116
|
+
>>> print(f.getvalue()) # doctest: +NORMALIZE_WHITESPACE
|
|
117
117
|
---
|
|
118
118
|
excerpt: tl;dr
|
|
119
119
|
layout: post
|
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Python Frontmatter: Parse and manage posts with YAML frontmatter
|
|
4
4
|
"""
|
|
5
|
+
from __future__ import annotations
|
|
5
6
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
7
|
+
import io
|
|
8
|
+
import pathlib
|
|
9
|
+
from os import PathLike
|
|
10
|
+
from typing import TYPE_CHECKING, Iterable, TextIO
|
|
8
11
|
|
|
12
|
+
from .default_handlers import JSONHandler, TOMLHandler, YAMLHandler
|
|
13
|
+
from .util import can_open, is_readable, is_writable, u
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .default_handlers import BaseHandler
|
|
12
18
|
|
|
13
19
|
|
|
14
20
|
__all__ = ["parse", "load", "loads", "dump", "dumps"]
|
|
@@ -22,7 +28,7 @@ handlers = [
|
|
|
22
28
|
]
|
|
23
29
|
|
|
24
30
|
|
|
25
|
-
def detect_format(text, handlers):
|
|
31
|
+
def detect_format(text: str, handlers: Iterable[BaseHandler]) -> BaseHandler | None:
|
|
26
32
|
"""
|
|
27
33
|
Figure out which handler to use, based on metadata.
|
|
28
34
|
Returns a handler instance or None.
|
|
@@ -40,7 +46,12 @@ def detect_format(text, handlers):
|
|
|
40
46
|
return None
|
|
41
47
|
|
|
42
48
|
|
|
43
|
-
def parse(
|
|
49
|
+
def parse(
|
|
50
|
+
text: str,
|
|
51
|
+
encoding: str = "utf-8",
|
|
52
|
+
handler: BaseHandler | None = None,
|
|
53
|
+
**defaults: object,
|
|
54
|
+
) -> tuple[dict[str, object], str]:
|
|
44
55
|
"""
|
|
45
56
|
Parse text with frontmatter, return metadata and content.
|
|
46
57
|
Pass in optional metadata defaults as keyword args.
|
|
@@ -79,14 +90,14 @@ def parse(text, encoding="utf-8", handler=None, **defaults):
|
|
|
79
90
|
return metadata, text
|
|
80
91
|
|
|
81
92
|
# parse, now that we have frontmatter
|
|
82
|
-
|
|
83
|
-
if isinstance(
|
|
84
|
-
metadata.update(
|
|
93
|
+
fm_data = handler.load(fm)
|
|
94
|
+
if isinstance(fm_data, dict):
|
|
95
|
+
metadata.update(fm_data)
|
|
85
96
|
|
|
86
97
|
return metadata, content.strip()
|
|
87
98
|
|
|
88
99
|
|
|
89
|
-
def check(fd, encoding="utf-8"):
|
|
100
|
+
def check(fd: TextIO | PathLike[str] | str, encoding: str = "utf-8") -> bool:
|
|
90
101
|
"""
|
|
91
102
|
Check if a file-like object or filename has a frontmatter,
|
|
92
103
|
return True if exists, False otherwise.
|
|
@@ -99,17 +110,21 @@ def check(fd, encoding="utf-8"):
|
|
|
99
110
|
True
|
|
100
111
|
|
|
101
112
|
"""
|
|
102
|
-
if
|
|
113
|
+
if is_readable(fd):
|
|
103
114
|
text = fd.read()
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
with
|
|
116
|
+
elif can_open(fd):
|
|
117
|
+
with open(fd, "r", encoding=encoding) as f:
|
|
107
118
|
text = f.read()
|
|
108
119
|
|
|
120
|
+
else:
|
|
121
|
+
# no idea what we're dealing with
|
|
122
|
+
return False
|
|
123
|
+
|
|
109
124
|
return checks(text, encoding)
|
|
110
125
|
|
|
111
126
|
|
|
112
|
-
def checks(text, encoding="utf-8"):
|
|
127
|
+
def checks(text: str, encoding: str = "utf-8") -> bool:
|
|
113
128
|
"""
|
|
114
129
|
Check if a text (binary or unicode) has a frontmatter,
|
|
115
130
|
return True if exists, False otherwise.
|
|
@@ -127,7 +142,12 @@ def checks(text, encoding="utf-8"):
|
|
|
127
142
|
return detect_format(text, handlers) != None
|
|
128
143
|
|
|
129
144
|
|
|
130
|
-
def load(
|
|
145
|
+
def load(
|
|
146
|
+
fd: str | io.IOBase | pathlib.Path,
|
|
147
|
+
encoding: str = "utf-8",
|
|
148
|
+
handler: BaseHandler | None = None,
|
|
149
|
+
**defaults: object,
|
|
150
|
+
) -> Post:
|
|
131
151
|
"""
|
|
132
152
|
Load and parse a file-like object or filename,
|
|
133
153
|
return a :py:class:`post <frontmatter.Post>`.
|
|
@@ -139,18 +159,26 @@ def load(fd, encoding="utf-8", handler=None, **defaults):
|
|
|
139
159
|
... post = frontmatter.load(f)
|
|
140
160
|
|
|
141
161
|
"""
|
|
142
|
-
if
|
|
162
|
+
if is_readable(fd):
|
|
143
163
|
text = fd.read()
|
|
144
164
|
|
|
145
|
-
|
|
146
|
-
with
|
|
165
|
+
elif can_open(fd):
|
|
166
|
+
with open(fd, "r", encoding=encoding) as f:
|
|
147
167
|
text = f.read()
|
|
148
168
|
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Cannot open filename using type {type(fd)}")
|
|
171
|
+
|
|
149
172
|
handler = handler or detect_format(text, handlers)
|
|
150
173
|
return loads(text, encoding, handler, **defaults)
|
|
151
174
|
|
|
152
175
|
|
|
153
|
-
def loads(
|
|
176
|
+
def loads(
|
|
177
|
+
text: str,
|
|
178
|
+
encoding: str = "utf-8",
|
|
179
|
+
handler: BaseHandler | None = None,
|
|
180
|
+
**defaults: object,
|
|
181
|
+
) -> Post:
|
|
154
182
|
"""
|
|
155
183
|
Parse text (binary or unicode) and return a :py:class:`post <frontmatter.Post>`.
|
|
156
184
|
|
|
@@ -166,18 +194,24 @@ def loads(text, encoding="utf-8", handler=None, **defaults):
|
|
|
166
194
|
return Post(content, handler, **metadata)
|
|
167
195
|
|
|
168
196
|
|
|
169
|
-
def dump(
|
|
197
|
+
def dump(
|
|
198
|
+
post: Post,
|
|
199
|
+
fd: str | PathLike[str] | TextIO,
|
|
200
|
+
encoding: str = "utf-8",
|
|
201
|
+
handler: BaseHandler | None = None,
|
|
202
|
+
**kwargs: object,
|
|
203
|
+
) -> None:
|
|
170
204
|
"""
|
|
171
205
|
Serialize :py:class:`post <frontmatter.Post>` to a string and write to a file-like object.
|
|
172
206
|
Text will be encoded on the way out (utf-8 by default).
|
|
173
207
|
|
|
174
208
|
::
|
|
175
209
|
|
|
176
|
-
>>> from io import
|
|
210
|
+
>>> from io import StringIO
|
|
177
211
|
>>> post = frontmatter.load('tests/yaml/hello-world.txt')
|
|
178
|
-
>>> f =
|
|
212
|
+
>>> f = StringIO()
|
|
179
213
|
>>> frontmatter.dump(post, f)
|
|
180
|
-
>>> print(f.getvalue()
|
|
214
|
+
>>> print(f.getvalue())
|
|
181
215
|
---
|
|
182
216
|
layout: post
|
|
183
217
|
title: Hello, world!
|
|
@@ -188,11 +222,11 @@ def dump(post, fd, encoding="utf-8", handler=None, **kwargs):
|
|
|
188
222
|
|
|
189
223
|
.. testcode::
|
|
190
224
|
|
|
191
|
-
from io import
|
|
225
|
+
from io import StringIO
|
|
192
226
|
post = frontmatter.load('tests/yaml/hello-world.txt')
|
|
193
|
-
f =
|
|
227
|
+
f = StringIO()
|
|
194
228
|
frontmatter.dump(post, f)
|
|
195
|
-
print(f.getvalue()
|
|
229
|
+
print(f.getvalue())
|
|
196
230
|
|
|
197
231
|
.. testoutput::
|
|
198
232
|
|
|
@@ -205,15 +239,18 @@ def dump(post, fd, encoding="utf-8", handler=None, **kwargs):
|
|
|
205
239
|
|
|
206
240
|
"""
|
|
207
241
|
content = dumps(post, handler, **kwargs)
|
|
208
|
-
if
|
|
209
|
-
fd.write(content
|
|
242
|
+
if is_writable(fd):
|
|
243
|
+
fd.write(content)
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
with
|
|
245
|
+
elif can_open(fd):
|
|
246
|
+
with open(fd, "w", encoding=encoding) as f:
|
|
213
247
|
f.write(content)
|
|
214
248
|
|
|
249
|
+
else:
|
|
250
|
+
raise ValueError(f"Cannot open filename using type {type(fd)}")
|
|
251
|
+
|
|
215
252
|
|
|
216
|
-
def dumps(post, handler=None, **kwargs):
|
|
253
|
+
def dumps(post: Post, handler: BaseHandler | None = None, **kwargs: object) -> str:
|
|
217
254
|
"""
|
|
218
255
|
Serialize a :py:class:`post <frontmatter.Post>` to a string and return text.
|
|
219
256
|
This always returns unicode text, which can then be encoded.
|
|
@@ -252,6 +289,7 @@ def dumps(post, handler=None, **kwargs):
|
|
|
252
289
|
if handler is None:
|
|
253
290
|
handler = getattr(post, "handler", None) or YAMLHandler()
|
|
254
291
|
|
|
292
|
+
assert handler is not None
|
|
255
293
|
return handler.format(post, **kwargs)
|
|
256
294
|
|
|
257
295
|
|
|
@@ -265,46 +303,48 @@ class Post(object):
|
|
|
265
303
|
For convenience, metadata values are available as proxied item lookups.
|
|
266
304
|
"""
|
|
267
305
|
|
|
268
|
-
def __init__(
|
|
306
|
+
def __init__(
|
|
307
|
+
self, content: str, handler: BaseHandler | None = None, **metadata: object
|
|
308
|
+
) -> None:
|
|
269
309
|
self.content = str(content)
|
|
270
310
|
self.metadata = metadata
|
|
271
311
|
self.handler = handler
|
|
272
312
|
|
|
273
|
-
def __getitem__(self, name):
|
|
313
|
+
def __getitem__(self, name: str) -> object:
|
|
274
314
|
"Get metadata key"
|
|
275
315
|
return self.metadata[name]
|
|
276
316
|
|
|
277
|
-
def __contains__(self, item):
|
|
317
|
+
def __contains__(self, item: object) -> bool:
|
|
278
318
|
"Check metadata contains key"
|
|
279
319
|
return item in self.metadata
|
|
280
320
|
|
|
281
|
-
def __setitem__(self, name, value):
|
|
321
|
+
def __setitem__(self, name: str, value: object) -> None:
|
|
282
322
|
"Set a metadata key"
|
|
283
323
|
self.metadata[name] = value
|
|
284
324
|
|
|
285
|
-
def __delitem__(self, name):
|
|
325
|
+
def __delitem__(self, name: str) -> None:
|
|
286
326
|
"Delete a metadata key"
|
|
287
327
|
del self.metadata[name]
|
|
288
328
|
|
|
289
|
-
def __bytes__(self):
|
|
329
|
+
def __bytes__(self) -> bytes:
|
|
290
330
|
return self.content.encode("utf-8")
|
|
291
331
|
|
|
292
|
-
def __str__(self):
|
|
332
|
+
def __str__(self) -> str:
|
|
293
333
|
return self.content
|
|
294
334
|
|
|
295
|
-
def get(self, key, default=None):
|
|
335
|
+
def get(self, key: str, default: object = None) -> object:
|
|
296
336
|
"Get a key, fallback to default"
|
|
297
337
|
return self.metadata.get(key, default)
|
|
298
338
|
|
|
299
|
-
def keys(self):
|
|
339
|
+
def keys(self) -> Iterable[str]:
|
|
300
340
|
"Return metadata keys"
|
|
301
341
|
return self.metadata.keys()
|
|
302
342
|
|
|
303
|
-
def values(self):
|
|
343
|
+
def values(self) -> Iterable[object]:
|
|
304
344
|
"Return metadata values"
|
|
305
345
|
return self.metadata.values()
|
|
306
346
|
|
|
307
|
-
def to_dict(self):
|
|
347
|
+
def to_dict(self) -> dict[str, object]:
|
|
308
348
|
"Post as a dict, for serializing"
|
|
309
349
|
d = self.metadata.copy()
|
|
310
350
|
d["content"] = self.content
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
@pytest.fixture(autouse=True)
|
|
5
|
-
def add_globals(doctest_namespace):
|
|
7
|
+
def add_globals(doctest_namespace: dict[str, object]) -> None:
|
|
6
8
|
import frontmatter
|
|
7
9
|
|
|
8
10
|
doctest_namespace["frontmatter"] = frontmatter
|
|
@@ -8,8 +8,8 @@ By default, ``frontmatter`` reads and writes YAML metadata. But maybe
|
|
|
8
8
|
you don't like YAML. Maybe enjoy writing metadata in JSON, or TOML, or
|
|
9
9
|
some other exotic markup not yet invented. For this, there are handlers.
|
|
10
10
|
|
|
11
|
-
This module includes handlers for YAML, JSON and TOML, as well as a
|
|
12
|
-
:py:class:`BaseHandler <frontmatter.default_handlers.BaseHandler>` that
|
|
11
|
+
This module includes handlers for YAML, JSON and TOML, as well as a
|
|
12
|
+
:py:class:`BaseHandler <frontmatter.default_handlers.BaseHandler>` that
|
|
13
13
|
outlines the basic API and can be subclassed to deal with new formats.
|
|
14
14
|
|
|
15
15
|
**Note**: The TOML handler is only available if the `toml <https://pypi.org/project/toml/>`_
|
|
@@ -32,10 +32,10 @@ A handler needs to do four things:
|
|
|
32
32
|
|
|
33
33
|
An example:
|
|
34
34
|
|
|
35
|
-
Calling :py:func:`frontmatter.load <frontmatter.load>` (or :py:func:`loads <frontmatter.loads>`)
|
|
36
|
-
with the ``handler`` argument tells frontmatter which handler to use.
|
|
37
|
-
The handler instance gets saved as an attribute on the returned post
|
|
38
|
-
object. By default, calling :py:func:`frontmatter.dumps <frontmatter.dumps>`
|
|
35
|
+
Calling :py:func:`frontmatter.load <frontmatter.load>` (or :py:func:`loads <frontmatter.loads>`)
|
|
36
|
+
with the ``handler`` argument tells frontmatter which handler to use.
|
|
37
|
+
The handler instance gets saved as an attribute on the returned post
|
|
38
|
+
object. By default, calling :py:func:`frontmatter.dumps <frontmatter.dumps>`
|
|
39
39
|
on the post will use the attached handler.
|
|
40
40
|
|
|
41
41
|
|
|
@@ -67,7 +67,7 @@ on the post will use the attached handler.
|
|
|
67
67
|
<BLANKLINE>
|
|
68
68
|
And this shouldn't break.
|
|
69
69
|
|
|
70
|
-
Passing a new handler to :py:func:`frontmatter.dumps <frontmatter.dumps>`
|
|
70
|
+
Passing a new handler to :py:func:`frontmatter.dumps <frontmatter.dumps>`
|
|
71
71
|
(or :py:func:`dump <frontmatter.dump>`) changes the export format:
|
|
72
72
|
|
|
73
73
|
::
|
|
@@ -116,11 +116,19 @@ All handlers use the interface defined on ``BaseHandler``. Each handler needs to
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
"""
|
|
119
|
+
from __future__ import annotations
|
|
119
120
|
|
|
120
121
|
import json
|
|
121
122
|
import re
|
|
122
123
|
import yaml
|
|
123
124
|
|
|
125
|
+
from types import ModuleType
|
|
126
|
+
from typing import TYPE_CHECKING, Any, Type
|
|
127
|
+
|
|
128
|
+
SafeDumper: Type[yaml.CDumper] | Type[yaml.SafeDumper]
|
|
129
|
+
SafeLoader: Type[yaml.CSafeLoader] | Type[yaml.SafeLoader]
|
|
130
|
+
toml: ModuleType | None
|
|
131
|
+
|
|
124
132
|
try:
|
|
125
133
|
from yaml import CSafeDumper as SafeDumper
|
|
126
134
|
from yaml import CSafeLoader as SafeLoader
|
|
@@ -136,6 +144,10 @@ except ImportError:
|
|
|
136
144
|
from .util import u
|
|
137
145
|
|
|
138
146
|
|
|
147
|
+
if TYPE_CHECKING:
|
|
148
|
+
from frontmatter import Post
|
|
149
|
+
|
|
150
|
+
|
|
139
151
|
__all__ = ["BaseHandler", "YAMLHandler", "JSONHandler"]
|
|
140
152
|
|
|
141
153
|
if toml:
|
|
@@ -159,11 +171,16 @@ class BaseHandler:
|
|
|
159
171
|
All default handlers are subclassed from BaseHandler.
|
|
160
172
|
"""
|
|
161
173
|
|
|
162
|
-
FM_BOUNDARY = None
|
|
163
|
-
START_DELIMITER = None
|
|
164
|
-
END_DELIMITER = None
|
|
174
|
+
FM_BOUNDARY: re.Pattern[str] | None = None
|
|
175
|
+
START_DELIMITER: str | None = None
|
|
176
|
+
END_DELIMITER: str | None = None
|
|
165
177
|
|
|
166
|
-
def __init__(
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
fm_boundary: re.Pattern[str] | None = None,
|
|
181
|
+
start_delimiter: str | None = None,
|
|
182
|
+
end_delimiter: str | None = None,
|
|
183
|
+
):
|
|
167
184
|
self.FM_BOUNDARY = fm_boundary or self.FM_BOUNDARY
|
|
168
185
|
self.START_DELIMITER = start_delimiter or self.START_DELIMITER
|
|
169
186
|
self.END_DELIMITER = end_delimiter or self.END_DELIMITER
|
|
@@ -176,7 +193,7 @@ class BaseHandler:
|
|
|
176
193
|
)
|
|
177
194
|
)
|
|
178
195
|
|
|
179
|
-
def detect(self, text):
|
|
196
|
+
def detect(self, text: str) -> bool:
|
|
180
197
|
"""
|
|
181
198
|
Decide whether this handler can parse the given ``text``,
|
|
182
199
|
and return True or False.
|
|
@@ -184,30 +201,32 @@ class BaseHandler:
|
|
|
184
201
|
Note that this is *not* called when passing a handler instance to
|
|
185
202
|
:py:func:`frontmatter.load <frontmatter.load>` or :py:func:`loads <frontmatter.loads>`.
|
|
186
203
|
"""
|
|
204
|
+
assert self.FM_BOUNDARY is not None
|
|
187
205
|
if self.FM_BOUNDARY.match(text):
|
|
188
206
|
return True
|
|
189
207
|
return False
|
|
190
208
|
|
|
191
|
-
def split(self, text):
|
|
209
|
+
def split(self, text: str) -> tuple[str, str]:
|
|
192
210
|
"""
|
|
193
211
|
Split text into frontmatter and content
|
|
194
212
|
"""
|
|
213
|
+
assert self.FM_BOUNDARY is not None
|
|
195
214
|
_, fm, content = self.FM_BOUNDARY.split(text, 2)
|
|
196
215
|
return fm, content
|
|
197
216
|
|
|
198
|
-
def load(self, fm):
|
|
217
|
+
def load(self, fm: str) -> dict[str, Any]:
|
|
199
218
|
"""
|
|
200
219
|
Parse frontmatter and return a dict
|
|
201
220
|
"""
|
|
202
221
|
raise NotImplementedError
|
|
203
222
|
|
|
204
|
-
def export(self, metadata, **kwargs):
|
|
223
|
+
def export(self, metadata: dict[str, object], **kwargs: object) -> str:
|
|
205
224
|
"""
|
|
206
225
|
Turn metadata back into text
|
|
207
226
|
"""
|
|
208
227
|
raise NotImplementedError
|
|
209
228
|
|
|
210
|
-
def format(self, post, **kwargs):
|
|
229
|
+
def format(self, post: Post, **kwargs: object) -> str:
|
|
211
230
|
"""
|
|
212
231
|
Turn a post into a string, used in ``frontmatter.dumps``
|
|
213
232
|
"""
|
|
@@ -233,14 +252,14 @@ class YAMLHandler(BaseHandler):
|
|
|
233
252
|
FM_BOUNDARY = re.compile(r"^-{3,}\s*$", re.MULTILINE)
|
|
234
253
|
START_DELIMITER = END_DELIMITER = "---"
|
|
235
254
|
|
|
236
|
-
def load(self, fm, **kwargs):
|
|
255
|
+
def load(self, fm: str, **kwargs: object) -> Any:
|
|
237
256
|
"""
|
|
238
257
|
Parse YAML front matter. This uses yaml.SafeLoader by default.
|
|
239
258
|
"""
|
|
240
259
|
kwargs.setdefault("Loader", SafeLoader)
|
|
241
|
-
return yaml.load(fm, **kwargs)
|
|
260
|
+
return yaml.load(fm, **kwargs) # type: ignore[arg-type]
|
|
242
261
|
|
|
243
|
-
def export(self, metadata, **kwargs):
|
|
262
|
+
def export(self, metadata: dict[str, object], **kwargs: object) -> str:
|
|
244
263
|
"""
|
|
245
264
|
Export metadata as YAML. This uses yaml.SafeDumper by default.
|
|
246
265
|
"""
|
|
@@ -248,8 +267,8 @@ class YAMLHandler(BaseHandler):
|
|
|
248
267
|
kwargs.setdefault("default_flow_style", False)
|
|
249
268
|
kwargs.setdefault("allow_unicode", True)
|
|
250
269
|
|
|
251
|
-
|
|
252
|
-
return u(
|
|
270
|
+
metadata_str = yaml.dump(metadata, **kwargs).strip() # type: ignore[call-overload]
|
|
271
|
+
return u(metadata_str) # ensure unicode
|
|
253
272
|
|
|
254
273
|
|
|
255
274
|
class JSONHandler(BaseHandler):
|
|
@@ -263,23 +282,24 @@ class JSONHandler(BaseHandler):
|
|
|
263
282
|
START_DELIMITER = ""
|
|
264
283
|
END_DELIMITER = ""
|
|
265
284
|
|
|
266
|
-
def split(self, text):
|
|
285
|
+
def split(self, text: str) -> tuple[str, str]:
|
|
286
|
+
assert self.FM_BOUNDARY is not None
|
|
267
287
|
_, fm, content = self.FM_BOUNDARY.split(text, 2)
|
|
268
288
|
return "{" + fm + "}", content
|
|
269
289
|
|
|
270
|
-
def load(self, fm, **kwargs):
|
|
271
|
-
return json.loads(fm, **kwargs)
|
|
290
|
+
def load(self, fm: str, **kwargs: object) -> Any:
|
|
291
|
+
return json.loads(fm, **kwargs) # type: ignore[arg-type]
|
|
272
292
|
|
|
273
|
-
def export(self, metadata, **kwargs):
|
|
293
|
+
def export(self, metadata: dict[str, object], **kwargs: object) -> str:
|
|
274
294
|
"Turn metadata into JSON"
|
|
275
295
|
kwargs.setdefault("indent", 4)
|
|
276
|
-
|
|
277
|
-
return u(
|
|
296
|
+
metadata_str = json.dumps(metadata, **kwargs) # type: ignore[arg-type]
|
|
297
|
+
return u(metadata_str)
|
|
278
298
|
|
|
279
299
|
|
|
280
300
|
if toml:
|
|
281
301
|
|
|
282
|
-
class TOMLHandler(BaseHandler):
|
|
302
|
+
class TOMLHandler(BaseHandler): # pyright: ignore
|
|
283
303
|
"""
|
|
284
304
|
Load and export TOML metadata.
|
|
285
305
|
|
|
@@ -289,14 +309,15 @@ if toml:
|
|
|
289
309
|
FM_BOUNDARY = re.compile(r"^\+{3,}\s*$", re.MULTILINE)
|
|
290
310
|
START_DELIMITER = END_DELIMITER = "+++"
|
|
291
311
|
|
|
292
|
-
def load(self, fm, **kwargs):
|
|
312
|
+
def load(self, fm: str, **kwargs: object) -> Any:
|
|
313
|
+
assert toml is not None
|
|
293
314
|
return toml.loads(fm, **kwargs)
|
|
294
315
|
|
|
295
|
-
def export(self, metadata, **kwargs):
|
|
316
|
+
def export(self, metadata: dict[str, object], **kwargs: object) -> str:
|
|
296
317
|
"Turn metadata into TOML"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
318
|
+
assert toml is not None
|
|
319
|
+
metadata_str = toml.dumps(metadata)
|
|
320
|
+
return u(metadata_str)
|
|
300
321
|
|
|
301
322
|
else:
|
|
302
|
-
TOMLHandler = None
|
|
323
|
+
TOMLHandler: Type[TOMLHandler] | None = None # type: ignore[no-redef]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561. This package uses inline types.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Utilities for handling unicode and other repetitive bits
|
|
4
|
+
"""
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from typing import TypeGuard, TextIO
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_readable(fd: object) -> TypeGuard[TextIO]:
|
|
10
|
+
return callable(getattr(fd, "read", None))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_writable(fd: object) -> TypeGuard[TextIO]:
|
|
14
|
+
return callable(getattr(fd, "write", None))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def can_open(fd: object) -> TypeGuard[str | PathLike[str]]:
|
|
18
|
+
return isinstance(fd, str) or isinstance(fd, PathLike)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def u(text: str | bytes, encoding: str = "utf-8") -> str:
|
|
22
|
+
"Return unicode text, no matter what"
|
|
23
|
+
|
|
24
|
+
if isinstance(text, bytes):
|
|
25
|
+
text_str: str = text.decode(encoding)
|
|
26
|
+
else:
|
|
27
|
+
text_str = str(text)
|
|
28
|
+
|
|
29
|
+
# it's already unicode
|
|
30
|
+
text_str = text_str.replace("\r\n", "\n")
|
|
31
|
+
return text_str
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-frontmatter
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Parse and manage posts with YAML (or other) frontmatter
|
|
5
5
|
Home-page: https://github.com/eyeseast/python-frontmatter
|
|
6
6
|
Author: Chris Amico
|
|
@@ -12,15 +12,35 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: PyYAML
|
|
21
23
|
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Requires-Dist: toml; extra == "test"
|
|
26
|
+
Requires-Dist: pyaml; extra == "test"
|
|
27
|
+
Requires-Dist: mypy; extra == "test"
|
|
28
|
+
Requires-Dist: types-PyYAML; extra == "test"
|
|
29
|
+
Requires-Dist: types-toml; extra == "test"
|
|
22
30
|
Provides-Extra: docs
|
|
23
|
-
|
|
31
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: provides-extra
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: summary
|
|
24
44
|
|
|
25
45
|
# Python Frontmatter
|
|
26
46
|
|
|
@@ -134,10 +154,10 @@ Well, hello there, world.
|
|
|
134
154
|
Or write to a file (or file-like object):
|
|
135
155
|
|
|
136
156
|
```python
|
|
137
|
-
>>> from io import
|
|
138
|
-
>>> f =
|
|
157
|
+
>>> from io import StringIO
|
|
158
|
+
>>> f = StringIO()
|
|
139
159
|
>>> frontmatter.dump(post, f)
|
|
140
|
-
>>> print(f.getvalue()
|
|
160
|
+
>>> print(f.getvalue()) # doctest: +NORMALIZE_WHITESPACE
|
|
141
161
|
---
|
|
142
162
|
excerpt: tl;dr
|
|
143
163
|
layout: post
|
|
@@ -11,7 +11,7 @@ with open("README.md") as f:
|
|
|
11
11
|
readme = f.read()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
VERSION = "1.0
|
|
14
|
+
VERSION = "1.2.0"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
setup(
|
|
@@ -24,9 +24,13 @@ setup(
|
|
|
24
24
|
author_email="eyeseast@gmail.com",
|
|
25
25
|
url="https://github.com/eyeseast/python-frontmatter",
|
|
26
26
|
packages=["frontmatter"],
|
|
27
|
+
package_data={"frontmatter": ["py.typed"]},
|
|
27
28
|
include_package_data=True,
|
|
28
29
|
install_requires=["PyYAML"],
|
|
29
|
-
extras_require={
|
|
30
|
+
extras_require={
|
|
31
|
+
"test": ["pytest", "toml", "pyaml", "mypy", "types-PyYAML", "types-toml"],
|
|
32
|
+
"docs": ["sphinx"],
|
|
33
|
+
},
|
|
30
34
|
tests_require=["python-frontmatter[test]"],
|
|
31
35
|
license="MIT",
|
|
32
36
|
zip_safe=False,
|
|
@@ -37,11 +41,11 @@ setup(
|
|
|
37
41
|
"License :: OSI Approved :: MIT License",
|
|
38
42
|
"Natural Language :: English",
|
|
39
43
|
"Programming Language :: Python :: 3",
|
|
40
|
-
"Programming Language :: Python :: 3.8",
|
|
41
44
|
"Programming Language :: Python :: 3.9",
|
|
42
45
|
"Programming Language :: Python :: 3.10",
|
|
43
46
|
"Programming Language :: Python :: 3.11",
|
|
44
47
|
"Programming Language :: Python :: 3.12",
|
|
48
|
+
"Programming Language :: Python :: 3.13",
|
|
45
49
|
],
|
|
46
50
|
test_suite="test",
|
|
47
51
|
)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
Utilities for handling unicode and other repetitive bits
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def u(text, encoding="utf-8"):
|
|
8
|
-
"Return unicode text, no matter what"
|
|
9
|
-
|
|
10
|
-
if isinstance(text, bytes):
|
|
11
|
-
text = text.decode(encoding)
|
|
12
|
-
|
|
13
|
-
# it's already unicode
|
|
14
|
-
text = text.replace("\r\n", "\n")
|
|
15
|
-
return text
|
|
File without changes
|
|
File without changes
|
{python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
{python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/empty-frontmatter.result.json
RENAMED
|
File without changes
|
|
File without changes
|
{python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/no-frontmatter.result.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/network-diagrams.result.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|