diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f48607..dd2c033 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,4 +25,5 @@ jobs: run: | pip install -e '.[test]' - name: Run tests - run: pytest + run: | + pytest . --doctest-modules --doctest-glob "README.md" diff --git a/README.md b/README.md index af3bdfd..618870b 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,15 @@ This is a small package to load and parse files (or just text) with YAML front m ## Usage: ```python -import frontmatter +>>> import frontmatter + ``` Load a post from a filename: ```python -post = frontmatter.load('tests/yaml/hello-world.txt') +>>> post = frontmatter.load('tests/yaml/hello-world.txt') + ``` Or a file (or file-like object): diff --git a/docs/handlers.rst b/docs/handlers.rst index c6dc6be..b82002d 100644 --- a/docs/handlers.rst +++ b/docs/handlers.rst @@ -1,8 +1,6 @@ Customizing input and output ============================ -.. module:: frontmatter - .. automodule:: frontmatter.default_handlers .. autoclass:: frontmatter.default_handlers.BaseHandler diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/content/reversed.txt b/examples/content/reversed.txt new file mode 100644 index 0000000..bbb952d --- /dev/null +++ b/examples/content/reversed.txt @@ -0,0 +1,28 @@ +This is txt format to prevent reformatting +============================================ + +Dextra tempore deus +------------------- + +Lorem markdownum est dicere pariter es dat si non, praesignis Styge, non +Maenalon magnae miserrimus. Corpora frustra committere insuetum et fecit +**Hippothousque arbore solio** inopem utraque concepit illa comantem me mortis +epulis protinus putares! Piceis *manibus*. Erinys et parum morsusque repugnat +ore corna sacris, pollice movet currus gestamina. + +Genitoris forti circumfuso videbit fertur vulnere simillima +----------------------------------------------------------- + +Audit enim, est illa nervis loco inque hoc, et rigido! Monstris vatibus laetos +contemptor Calydonia. Et visa capillo referens regia: usus: odiique nostro. +**Vim** sensit inpulit virginis metuens secum cogit, corpus. + +Humus ater Dromas est honorem, Titanida glandibus sinit, e terras capillos +cremet retinentibus male. Tertia et cedit eliso flectere haec, cute nihil +marmore armo. Mihi [Olympi](http://que.org/saepepoenas), iam sustinet addidit +humana similis. + +--- +title: Front matter, reversed +ref: https://github.com/eyeseast/python-frontmatter/issues/67 +--- diff --git a/examples/reversed.py b/examples/reversed.py new file mode 100644 index 0000000..56715f1 --- /dev/null +++ b/examples/reversed.py @@ -0,0 +1,76 @@ +import frontmatter + +POST_TEMPLATE = """\ +{content} + +{start_delimiter} +{metadata} +{end_delimiter} +""" + + +class ReverseYAMLHandler(frontmatter.YAMLHandler): + """ + This is an example of using Handler.parse and Handler.format to move Frontmatter to the bottom + of a file, both for parsing and output. + + >>> with open("examples/content/reversed.txt") as f: + ... text = f.read() + >>> handler = ReverseYAMLHandler() + >>> post = frontmatter.loads(text, handler=handler) + >>> print(post['title']) + Front matter, reversed + >>> print(post['ref']) + https://github.com/eyeseast/python-frontmatter/issues/67 + >>> print(frontmatter.dumps(post, handler=handler)) + This is txt format to prevent reformatting + ============================================ + + Dextra tempore deus + ------------------- + + Lorem markdownum est dicere pariter es dat si non, praesignis Styge, non + Maenalon magnae miserrimus. Corpora frustra committere insuetum et fecit + **Hippothousque arbore solio** inopem utraque concepit illa comantem me mortis + epulis protinus putares! Piceis *manibus*. Erinys et parum morsusque repugnat + ore corna sacris, pollice movet currus gestamina. + + Genitoris forti circumfuso videbit fertur vulnere simillima + ----------------------------------------------------------- + + Audit enim, est illa nervis loco inque hoc, et rigido! Monstris vatibus laetos + contemptor Calydonia. Et visa capillo referens regia: usus: odiique nostro. + **Vim** sensit inpulit virginis metuens secum cogit, corpus. + + Humus ater Dromas est honorem, Titanida glandibus sinit, e terras capillos + cremet retinentibus male. Tertia et cedit eliso flectere haec, cute nihil + marmore armo. Mihi [Olympi](http://que.org/saepepoenas), iam sustinet addidit + humana similis. + + --- + ref: https://github.com/eyeseast/python-frontmatter/issues/67 + title: Front matter, reversed + --- + """ + + # FM_BOUNDARY as a string, so we can rsplit + FM_BOUNDARY = "---" + + def split(self, text): + """ + Split text into frontmatter and content + """ + content, fm, _ = text.rsplit(self.FM_BOUNDARY, 2) + return fm, content + + def format(self, post, **kwargs): + start_delimiter = kwargs.pop("start_delimiter", self.START_DELIMITER) + end_delimiter = kwargs.pop("end_delimiter", self.END_DELIMITER) + metadata = self.export(post.metadata, **kwargs) + + return POST_TEMPLATE.format( + content=post.content, + metadata=metadata, + start_delimiter=start_delimiter, + end_delimiter=end_delimiter, + ).strip() diff --git a/frontmatter/__init__.py b/frontmatter/__init__.py index b4b290c..92b6323 100644 --- a/frontmatter/__init__.py +++ b/frontmatter/__init__.py @@ -13,13 +13,6 @@ __all__ = ["parse", "load", "loads", "dump", "dumps"] -POST_TEMPLATE = """\ -{start_delimiter} -{metadata} -{end_delimiter} - -{content} -""" # global handlers handlers = { @@ -57,11 +50,11 @@ def parse(text, encoding="utf-8", handler=None, **defaults): .. testsetup:: * - import frontmatter + >>> import frontmatter .. doctest:: - >>> with open('../tests/yaml/hello-world.txt') as f: + >>> with open('tests/yaml/hello-world.txt') as f: ... metadata, content = frontmatter.parse(f.read()) >>> print(metadata['title']) Hello, world! @@ -102,7 +95,7 @@ def check(fd, encoding="utf-8"): .. doctest:: - >>> frontmatter.check('../tests/yaml/hello-world.txt') + >>> frontmatter.check('tests/yaml/hello-world.txt') True """ @@ -125,7 +118,7 @@ def checks(text, encoding="utf-8"): .. doctest:: - >>> with open('../tests/yaml/hello-world.txt') as f: + >>> with open('tests/yaml/hello-world.txt') as f: ... frontmatter.checks(f.read()) True @@ -141,8 +134,8 @@ def load(fd, encoding="utf-8", handler=None, **defaults): .. doctest:: - >>> post = frontmatter.load('../tests/yaml/hello-world.txt') - >>> with open('../tests/yaml/hello-world.txt') as f: + >>> post = frontmatter.load('tests/yaml/hello-world.txt') + >>> with open('tests/yaml/hello-world.txt') as f: ... post = frontmatter.load(f) """ @@ -163,7 +156,7 @@ def loads(text, encoding="utf-8", handler=None, **defaults): .. doctest:: - >>> with open('../tests/yaml/hello-world.txt') as f: + >>> with open('tests/yaml/hello-world.txt') as f: ... post = frontmatter.loads(f.read()) """ @@ -181,7 +174,7 @@ def dump(post, fd, encoding="utf-8", handler=None, **kwargs): :: >>> from io import BytesIO - >>> post = frontmatter.load('../tests/yaml/hello-world.txt') + >>> post = frontmatter.load('tests/yaml/hello-world.txt') >>> f = BytesIO() >>> frontmatter.dump(post, f) >>> print(f.getvalue().decode('utf-8')) @@ -189,14 +182,14 @@ def dump(post, fd, encoding="utf-8", handler=None, **kwargs): layout: post title: Hello, world! --- - + Well, hello there, world. .. testcode:: from io import BytesIO - post = frontmatter.load('../tests/yaml/hello-world.txt') + post = frontmatter.load('tests/yaml/hello-world.txt') f = BytesIO() frontmatter.dump(post, f) print(f.getvalue().decode('utf-8')) @@ -207,7 +200,7 @@ def dump(post, fd, encoding="utf-8", handler=None, **kwargs): layout: post title: Hello, world! --- - + Well, hello there, world. """ @@ -232,18 +225,18 @@ def dumps(post, handler=None, **kwargs): :: - >>> post = frontmatter.load('../tests/yaml/hello-world.txt') + >>> post = frontmatter.load('tests/yaml/hello-world.txt') >>> print(frontmatter.dumps(post)) # doctest: +NORMALIZE_WHITESPACE --- layout: post title: Hello, world! --- - + Well, hello there, world. .. testcode:: - post = frontmatter.load('../tests/yaml/hello-world.txt') + post = frontmatter.load('tests/yaml/hello-world.txt') print(frontmatter.dumps(post)) .. testoutput:: @@ -259,17 +252,7 @@ def dumps(post, handler=None, **kwargs): if handler is None: handler = getattr(post, "handler", None) or YAMLHandler() - start_delimiter = kwargs.pop("start_delimiter", handler.START_DELIMITER) - end_delimiter = kwargs.pop("end_delimiter", handler.END_DELIMITER) - - metadata = handler.export(post.metadata, **kwargs) - - return POST_TEMPLATE.format( - metadata=metadata, - content=post.content, - start_delimiter=start_delimiter, - end_delimiter=end_delimiter, - ).strip() + return handler.format(post, **kwargs) class Post(object): diff --git a/frontmatter/conftest.py b/frontmatter/conftest.py new file mode 100644 index 0000000..e804a6e --- /dev/null +++ b/frontmatter/conftest.py @@ -0,0 +1,8 @@ +import pytest + + +@pytest.fixture(autouse=True) +def add_globals(doctest_namespace): + import frontmatter + + doctest_namespace["frontmatter"] = frontmatter diff --git a/frontmatter/default_handlers.py b/frontmatter/default_handlers.py index 46b40be..07ecbb5 100644 --- a/frontmatter/default_handlers.py +++ b/frontmatter/default_handlers.py @@ -40,7 +40,7 @@ >>> import frontmatter >>> from frontmatter.default_handlers import YAMLHandler, TOMLHandler - >>> post = frontmatter.load('tests/toml/hello-toml.markdown', handler=TOMLHandler()) + >>> post = frontmatter.load('tests/toml/hello-toml.md', handler=TOMLHandler()) >>> post.handler #doctest: +ELLIPSIS @@ -97,15 +97,10 @@ # set YAML format when dumping, but the old handler attached >>> t1 = frontmatter.dumps(post, handler=YAMLHandler()) - - # set a new handler, changing all future exports - >>> post.handler = YAMLHandler() + >>> post.handler = YAMLHandler() # set a new handler, changing all future exports >>> t2 = frontmatter.dumps(post) - - # remove handler, defaulting back to YAML - >>> post.handler = None + >>> post.handler = None # remove handler, defaulting back to YAML >>> t3 = frontmatter.dumps(post) - >>> t1 == t2 == t3 True @@ -114,6 +109,7 @@ - split metadata and content, based on a boundary pattern (``handler.split``) - parse plain text metadata into a Python dictionary (``handler.load``) - export a dictionary back into plain text (``handler.export``) +- format exported metadata and content into a single string (``handler.format``) """ @@ -143,7 +139,16 @@ __all__.append("TOMLHandler") -class BaseHandler(object): +DEFAULT_POST_TEMPLATE = """\ +{start_delimiter} +{metadata} +{end_delimiter} + +{content} +""" + + +class BaseHandler: """ BaseHandler lays out all the steps to detecting, splitting, parsing and exporting front matter metadata. @@ -199,6 +204,22 @@ def export(self, metadata, **kwargs): """ raise NotImplementedError + def format(self, post, **kwargs): + """ + Turn a post into a string, used in ``frontmatter.dumps`` + """ + start_delimiter = kwargs.pop("start_delimiter", self.START_DELIMITER) + end_delimiter = kwargs.pop("end_delimiter", self.END_DELIMITER) + + metadata = self.export(post.metadata, **kwargs) + + return DEFAULT_POST_TEMPLATE.format( + metadata=metadata, + content=post.content, + start_delimiter=start_delimiter, + end_delimiter=end_delimiter, + ).strip() + class YAMLHandler(BaseHandler): """