Skip to content

Commit ac01b5b

Browse files
author
Chris Amico
authored
Merge pull request #111 from tusharsadhwani/types
Add types to codebase
2 parents c842f53 + 5aa3109 commit ac01b5b

8 files changed

Lines changed: 119 additions & 56 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ jobs:
3131
- name: Run tests
3232
run: |
3333
pytest . --doctest-modules --doctest-glob "README.md"
34+
- name: Run type checking
35+
run: |
36+
mypy .

frontmatter/__init__.py

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22
"""
33
Python Frontmatter: Parse and manage posts with YAML frontmatter
44
"""
5+
from __future__ import annotations
56

67
import codecs
7-
import re
8-
8+
import io
9+
from typing import TYPE_CHECKING, Iterable
910

1011
from .util import u
1112
from .default_handlers import YAMLHandler, JSONHandler, TOMLHandler
1213

1314

15+
if TYPE_CHECKING:
16+
from .default_handlers import BaseHandler
17+
18+
1419
__all__ = ["parse", "load", "loads", "dump", "dumps"]
1520

1621

@@ -22,7 +27,7 @@
2227
]
2328

2429

25-
def detect_format(text, handlers):
30+
def detect_format(text: str, handlers: Iterable[BaseHandler]) -> BaseHandler | None:
2631
"""
2732
Figure out which handler to use, based on metadata.
2833
Returns a handler instance or None.
@@ -40,7 +45,12 @@ def detect_format(text, handlers):
4045
return None
4146

4247

43-
def parse(text, encoding="utf-8", handler=None, **defaults):
48+
def parse(
49+
text: str,
50+
encoding: str = "utf-8",
51+
handler: BaseHandler | None = None,
52+
**defaults: object,
53+
) -> tuple[dict[str, object], str]:
4454
"""
4555
Parse text with frontmatter, return metadata and content.
4656
Pass in optional metadata defaults as keyword args.
@@ -79,14 +89,14 @@ def parse(text, encoding="utf-8", handler=None, **defaults):
7989
return metadata, text
8090

8191
# parse, now that we have frontmatter
82-
fm = handler.load(fm)
83-
if isinstance(fm, dict):
84-
metadata.update(fm)
92+
fm_data = handler.load(fm)
93+
if isinstance(fm_data, dict):
94+
metadata.update(fm_data)
8595

8696
return metadata, content.strip()
8797

8898

89-
def check(fd, encoding="utf-8"):
99+
def check(fd: str | io.IOBase, encoding: str = "utf-8") -> bool:
90100
"""
91101
Check if a file-like object or filename has a frontmatter,
92102
return True if exists, False otherwise.
@@ -109,7 +119,7 @@ def check(fd, encoding="utf-8"):
109119
return checks(text, encoding)
110120

111121

112-
def checks(text, encoding="utf-8"):
122+
def checks(text: str, encoding: str = "utf-8") -> bool:
113123
"""
114124
Check if a text (binary or unicode) has a frontmatter,
115125
return True if exists, False otherwise.
@@ -127,7 +137,12 @@ def checks(text, encoding="utf-8"):
127137
return detect_format(text, handlers) != None
128138

129139

130-
def load(fd, encoding="utf-8", handler=None, **defaults):
140+
def load(
141+
fd: str | io.IOBase,
142+
encoding: str = "utf-8",
143+
handler: BaseHandler | None = None,
144+
**defaults: object,
145+
) -> Post:
131146
"""
132147
Load and parse a file-like object or filename,
133148
return a :py:class:`post <frontmatter.Post>`.
@@ -150,7 +165,12 @@ def load(fd, encoding="utf-8", handler=None, **defaults):
150165
return loads(text, encoding, handler, **defaults)
151166

152167

153-
def loads(text, encoding="utf-8", handler=None, **defaults):
168+
def loads(
169+
text: str,
170+
encoding: str = "utf-8",
171+
handler: BaseHandler | None = None,
172+
**defaults: object,
173+
) -> Post:
154174
"""
155175
Parse text (binary or unicode) and return a :py:class:`post <frontmatter.Post>`.
156176
@@ -166,7 +186,13 @@ def loads(text, encoding="utf-8", handler=None, **defaults):
166186
return Post(content, handler, **metadata)
167187

168188

169-
def dump(post, fd, encoding="utf-8", handler=None, **kwargs):
189+
def dump(
190+
post: Post,
191+
fd: str | io.IOBase,
192+
encoding: str = "utf-8",
193+
handler: BaseHandler | None = None,
194+
**kwargs: object,
195+
) -> None:
170196
"""
171197
Serialize :py:class:`post <frontmatter.Post>` to a string and write to a file-like object.
172198
Text will be encoded on the way out (utf-8 by default).
@@ -213,7 +239,7 @@ def dump(post, fd, encoding="utf-8", handler=None, **kwargs):
213239
f.write(content)
214240

215241

216-
def dumps(post, handler=None, **kwargs):
242+
def dumps(post: Post, handler: BaseHandler | None = None, **kwargs: object) -> str:
217243
"""
218244
Serialize a :py:class:`post <frontmatter.Post>` to a string and return text.
219245
This always returns unicode text, which can then be encoded.
@@ -265,46 +291,48 @@ class Post(object):
265291
For convenience, metadata values are available as proxied item lookups.
266292
"""
267293

268-
def __init__(self, content, handler=None, **metadata):
294+
def __init__(
295+
self, content: str, handler: BaseHandler | None = None, **metadata: object
296+
) -> None:
269297
self.content = str(content)
270298
self.metadata = metadata
271299
self.handler = handler
272300

273-
def __getitem__(self, name):
301+
def __getitem__(self, name: str) -> object:
274302
"Get metadata key"
275303
return self.metadata[name]
276304

277-
def __contains__(self, item):
305+
def __contains__(self, item: object) -> bool:
278306
"Check metadata contains key"
279307
return item in self.metadata
280308

281-
def __setitem__(self, name, value):
309+
def __setitem__(self, name: str, value: object) -> None:
282310
"Set a metadata key"
283311
self.metadata[name] = value
284312

285-
def __delitem__(self, name):
313+
def __delitem__(self, name: str) -> None:
286314
"Delete a metadata key"
287315
del self.metadata[name]
288316

289-
def __bytes__(self):
317+
def __bytes__(self) -> bytes:
290318
return self.content.encode("utf-8")
291319

292-
def __str__(self):
320+
def __str__(self) -> str:
293321
return self.content
294322

295-
def get(self, key, default=None):
323+
def get(self, key: str, default: object = None) -> object:
296324
"Get a key, fallback to default"
297325
return self.metadata.get(key, default)
298326

299-
def keys(self):
327+
def keys(self) -> Iterable[str]:
300328
"Return metadata keys"
301329
return self.metadata.keys()
302330

303-
def values(self):
331+
def values(self) -> Iterable[object]:
304332
"Return metadata values"
305333
return self.metadata.values()
306334

307-
def to_dict(self):
335+
def to_dict(self) -> dict[str, object]:
308336
"Post as a dict, for serializing"
309337
d = self.metadata.copy()
310338
d["content"] = self.content

frontmatter/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from __future__ import annotations
2+
13
import pytest
24

35

46
@pytest.fixture(autouse=True)
5-
def add_globals(doctest_namespace):
7+
def add_globals(doctest_namespace: dict[str, object]) -> None:
68
import frontmatter
79

810
doctest_namespace["frontmatter"] = frontmatter

frontmatter/default_handlers.py

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,19 @@
116116
117117
118118
"""
119+
from __future__ import annotations
119120

120121
import json
121122
import re
122123
import yaml
123124

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+
124132
try:
125133
from yaml import CSafeDumper as SafeDumper
126134
from yaml import CSafeLoader as SafeLoader
@@ -136,6 +144,10 @@
136144
from .util import u
137145

138146

147+
if TYPE_CHECKING:
148+
from frontmatter import Post
149+
150+
139151
__all__ = ["BaseHandler", "YAMLHandler", "JSONHandler"]
140152

141153
if toml:
@@ -159,11 +171,16 @@ class BaseHandler:
159171
All default handlers are subclassed from BaseHandler.
160172
"""
161173

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
165177

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+
):
167184
self.FM_BOUNDARY = fm_boundary or self.FM_BOUNDARY
168185
self.START_DELIMITER = start_delimiter or self.START_DELIMITER
169186
self.END_DELIMITER = end_delimiter or self.END_DELIMITER
@@ -176,38 +193,40 @@ def __init__(self, fm_boundary=None, start_delimiter=None, end_delimiter=None):
176193
)
177194
)
178195

179-
def detect(self, text):
196+
def detect(self, text: str) -> bool:
180197
"""
181198
Decide whether this handler can parse the given ``text``,
182199
and return True or False.
183200
184201
Note that this is *not* called when passing a handler instance to
185202
:py:func:`frontmatter.load <frontmatter.load>` or :py:func:`loads <frontmatter.loads>`.
186203
"""
204+
assert self.FM_BOUNDARY is not None
187205
if self.FM_BOUNDARY.match(text):
188206
return True
189207
return False
190208

191-
def split(self, text):
209+
def split(self, text: str) -> tuple[str, str]:
192210
"""
193211
Split text into frontmatter and content
194212
"""
213+
assert self.FM_BOUNDARY is not None
195214
_, fm, content = self.FM_BOUNDARY.split(text, 2)
196215
return fm, content
197216

198-
def load(self, fm):
217+
def load(self, fm: str) -> dict[str, Any]:
199218
"""
200219
Parse frontmatter and return a dict
201220
"""
202221
raise NotImplementedError
203222

204-
def export(self, metadata, **kwargs):
223+
def export(self, metadata: dict[str, object], **kwargs: object) -> str:
205224
"""
206225
Turn metadata back into text
207226
"""
208227
raise NotImplementedError
209228

210-
def format(self, post, **kwargs):
229+
def format(self, post: Post, **kwargs: object) -> str:
211230
"""
212231
Turn a post into a string, used in ``frontmatter.dumps``
213232
"""
@@ -233,23 +252,23 @@ class YAMLHandler(BaseHandler):
233252
FM_BOUNDARY = re.compile(r"^-{3,}\s*$", re.MULTILINE)
234253
START_DELIMITER = END_DELIMITER = "---"
235254

236-
def load(self, fm, **kwargs):
255+
def load(self, fm: str, **kwargs: object) -> Any:
237256
"""
238257
Parse YAML front matter. This uses yaml.SafeLoader by default.
239258
"""
240259
kwargs.setdefault("Loader", SafeLoader)
241-
return yaml.load(fm, **kwargs)
260+
return yaml.load(fm, **kwargs) # type: ignore[arg-type]
242261

243-
def export(self, metadata, **kwargs):
262+
def export(self, metadata: dict[str, object], **kwargs: object) -> str:
244263
"""
245264
Export metadata as YAML. This uses yaml.SafeDumper by default.
246265
"""
247266
kwargs.setdefault("Dumper", SafeDumper)
248267
kwargs.setdefault("default_flow_style", False)
249268
kwargs.setdefault("allow_unicode", True)
250269

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
253272

254273

255274
class JSONHandler(BaseHandler):
@@ -263,18 +282,18 @@ class JSONHandler(BaseHandler):
263282
START_DELIMITER = ""
264283
END_DELIMITER = ""
265284

266-
def split(self, text):
285+
def split(self, text: str) -> tuple[str, str]:
267286
_, fm, content = self.FM_BOUNDARY.split(text, 2)
268287
return "{" + fm + "}", content
269288

270-
def load(self, fm, **kwargs):
271-
return json.loads(fm, **kwargs)
289+
def load(self, fm: str, **kwargs: object) -> Any:
290+
return json.loads(fm, **kwargs) # type: ignore[arg-type]
272291

273-
def export(self, metadata, **kwargs):
292+
def export(self, metadata: dict[str, object], **kwargs: object) -> str:
274293
"Turn metadata into JSON"
275294
kwargs.setdefault("indent", 4)
276-
metadata = json.dumps(metadata, **kwargs)
277-
return u(metadata)
295+
metadata_str = json.dumps(metadata, **kwargs) # type: ignore[arg-type]
296+
return u(metadata_str)
278297

279298

280299
if toml:
@@ -289,14 +308,15 @@ class TOMLHandler(BaseHandler):
289308
FM_BOUNDARY = re.compile(r"^\+{3,}\s*$", re.MULTILINE)
290309
START_DELIMITER = END_DELIMITER = "+++"
291310

292-
def load(self, fm, **kwargs):
311+
def load(self, fm: str, **kwargs: object) -> Any:
312+
assert toml is not None
293313
return toml.loads(fm, **kwargs)
294314

295-
def export(self, metadata, **kwargs):
315+
def export(self, metadata: dict[str, object], **kwargs: object) -> str:
296316
"Turn metadata into TOML"
297-
metadata = toml.dumps(metadata)
298-
return u(metadata)
299-
317+
assert toml is not None
318+
metadata_str = toml.dumps(metadata)
319+
return u(metadata_str)
300320

301321
else:
302-
TOMLHandler = None
322+
TOMLHandler: Type[TOMLHandler] | None = None # type: ignore[no-redef]

frontmatter/py.typed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Marker file for PEP 561. This package uses inline types.

0 commit comments

Comments
 (0)