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.
Files changed (44) hide show
  1. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/MANIFEST.in +1 -0
  2. {python-frontmatter-1.0.1/python_frontmatter.egg-info → python_frontmatter-1.2.0}/PKG-INFO +27 -7
  3. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/README.md +3 -3
  4. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/frontmatter/__init__.py +82 -42
  5. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/frontmatter/conftest.py +3 -1
  6. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/frontmatter/default_handlers.py +55 -34
  7. python_frontmatter-1.2.0/frontmatter/py.typed +1 -0
  8. python_frontmatter-1.2.0/frontmatter/util.py +31 -0
  9. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0/python_frontmatter.egg-info}/PKG-INFO +27 -7
  10. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/SOURCES.txt +1 -0
  11. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/requires.txt +3 -0
  12. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/setup.py +7 -3
  13. python-frontmatter-1.0.1/frontmatter/util.py +0 -15
  14. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/LICENSE +0 -0
  15. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/dependency_links.txt +0 -0
  16. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/not-zip-safe +0 -0
  17. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/python_frontmatter.egg-info/top_level.txt +0 -0
  18. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/setup.cfg +0 -0
  19. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/empty-frontmatter.result.json +0 -0
  20. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/empty-frontmatter.txt +0 -0
  21. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/no-frontmatter.result.json +0 -0
  22. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/empty/no-frontmatter.txt +0 -0
  23. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/json/hello-json.md +0 -0
  24. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/json/hello-json.result.json +0 -0
  25. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/stub_tests.py +0 -0
  26. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/test_docs.py +0 -0
  27. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/test_files.py +0 -0
  28. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/toml/hello-toml.md +0 -0
  29. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/toml/hello-toml.result.json +0 -0
  30. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/unit_test.py +0 -0
  31. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/chinese.result.json +0 -0
  32. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/chinese.txt +0 -0
  33. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/extra-dash.result.json +0 -0
  34. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/extra-dash.txt +0 -0
  35. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/extra-space.result.json +0 -0
  36. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/extra-space.txt +0 -0
  37. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/hello-markdown.md +0 -0
  38. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/hello-markdown.result.json +0 -0
  39. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/hello-world.result.json +0 -0
  40. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/hello-world.txt +0 -0
  41. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/network-diagrams.md +0 -0
  42. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/network-diagrams.result.json +0 -0
  43. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/unpretty.md +0 -0
  44. {python-frontmatter-1.0.1 → python_frontmatter-1.2.0}/tests/yaml/unpretty.result.json +0 -0
@@ -1,5 +1,6 @@
1
1
  include LICENSE
2
2
  include README.md
3
+ include frontmatter/py.typed
3
4
 
4
5
  recursive-include tests *
5
6
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-frontmatter
3
- Version: 1.0.1
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
- License-File: LICENSE
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 BytesIO
138
- >>> f = BytesIO()
157
+ >>> from io import StringIO
158
+ >>> f = StringIO()
139
159
  >>> frontmatter.dump(post, f)
140
- >>> print(f.getvalue().decode('utf-8')) # doctest: +NORMALIZE_WHITESPACE
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 BytesIO
114
- >>> f = BytesIO()
113
+ >>> from io import StringIO
114
+ >>> f = StringIO()
115
115
  >>> frontmatter.dump(post, f)
116
- >>> print(f.getvalue().decode('utf-8')) # doctest: +NORMALIZE_WHITESPACE
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 codecs
7
- import re
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
- from .util import u
11
- from .default_handlers import YAMLHandler, JSONHandler, TOMLHandler
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(text, encoding="utf-8", handler=None, **defaults):
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
- fm = handler.load(fm)
83
- if isinstance(fm, dict):
84
- metadata.update(fm)
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 hasattr(fd, "read"):
113
+ if is_readable(fd):
103
114
  text = fd.read()
104
115
 
105
- else:
106
- with codecs.open(fd, "r", encoding) as f:
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(fd, encoding="utf-8", handler=None, **defaults):
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 hasattr(fd, "read"):
162
+ if is_readable(fd):
143
163
  text = fd.read()
144
164
 
145
- else:
146
- with codecs.open(fd, "r", encoding) as f:
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(text, encoding="utf-8", handler=None, **defaults):
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(post, fd, encoding="utf-8", handler=None, **kwargs):
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 BytesIO
210
+ >>> from io import StringIO
177
211
  >>> post = frontmatter.load('tests/yaml/hello-world.txt')
178
- >>> f = BytesIO()
212
+ >>> f = StringIO()
179
213
  >>> frontmatter.dump(post, f)
180
- >>> print(f.getvalue().decode('utf-8'))
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 BytesIO
225
+ from io import StringIO
192
226
  post = frontmatter.load('tests/yaml/hello-world.txt')
193
- f = BytesIO()
227
+ f = StringIO()
194
228
  frontmatter.dump(post, f)
195
- print(f.getvalue().decode('utf-8'))
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 hasattr(fd, "write"):
209
- fd.write(content.encode(encoding))
242
+ if is_writable(fd):
243
+ fd.write(content)
210
244
 
211
- else:
212
- with codecs.open(fd, "w", encoding) as f:
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__(self, content, handler=None, **metadata):
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__(self, fm_boundary=None, start_delimiter=None, end_delimiter=None):
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
- metadata = yaml.dump(metadata, **kwargs).strip()
252
- return u(metadata) # ensure unicode
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
- metadata = json.dumps(metadata, **kwargs)
277
- return u(metadata)
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
- metadata = toml.dumps(metadata)
298
- return u(metadata)
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
1
+ Metadata-Version: 2.4
2
2
  Name: python-frontmatter
3
- Version: 1.0.1
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
- License-File: LICENSE
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 BytesIO
138
- >>> f = BytesIO()
157
+ >>> from io import StringIO
158
+ >>> f = StringIO()
139
159
  >>> frontmatter.dump(post, f)
140
- >>> print(f.getvalue().decode('utf-8')) # doctest: +NORMALIZE_WHITESPACE
160
+ >>> print(f.getvalue()) # doctest: +NORMALIZE_WHITESPACE
141
161
  ---
142
162
  excerpt: tl;dr
143
163
  layout: post
@@ -5,6 +5,7 @@ setup.py
5
5
  frontmatter/__init__.py
6
6
  frontmatter/conftest.py
7
7
  frontmatter/default_handlers.py
8
+ frontmatter/py.typed
8
9
  frontmatter/util.py
9
10
  python_frontmatter.egg-info/PKG-INFO
10
11
  python_frontmatter.egg-info/SOURCES.txt
@@ -7,3 +7,6 @@ sphinx
7
7
  pytest
8
8
  toml
9
9
  pyaml
10
+ mypy
11
+ types-PyYAML
12
+ types-toml
@@ -11,7 +11,7 @@ with open("README.md") as f:
11
11
  readme = f.read()
12
12
 
13
13
 
14
- VERSION = "1.0.1"
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={"test": ["pytest", "toml", "pyaml"], "docs": ["sphinx"]},
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