Use implicit namespace plugins for import and export (#1216)

* behavior outline

* FIrst pass at allow external plugins

* remove template exporter

* Add listing of active plugins to '--version' output

* Documentation for plugins

* [Docs] add custom imports and exporters to site TOC

* [Docs] better linewrapping

* enforce positive initial linewrap

Check column widths

update gitignore

throw error when linewrap too small

simply check for large enough linewrap value

* delete unused error message

* PR feedback

make exception more informative

update check_linewrap signature in src and test

make check_linewrap a free function

* delete unused function

* delete else..pass block

* newline for make format

* Include dates_exporter

* Use Base classes for importer and exporters.

* [Docs] improve documentation of custom Importers and Exporters

* [Testing] separate run with external plugin!

* basic behavior test

* prototype unittest for JSON Exporter

test for unimplemented method

* make format

delete unused imports

* Remove 'importer' or 'exporter' from filenames where not needed

* [Test] run different tests with or without the external plugins installed

* [Test] move test rot13 plugin into git tree

from 0dc912af82

* consolidate demo plugins to common package

* [Docs] name page for plugins

* [Docs] include the sample plug in code files directly

* style fixes

* [test] determine whether to run external plug in tests based on installed packages

* improved code documentation

* style fixes for GitHub actions

* Convert "short" and "pretty" (and "default") formaters to plugins

further to https://github.com/jrnl-org/jrnl/pull/1177

* more code clean up

tests pass locally...now for GitHub...

* [tests] dynamically determine jrnl version for plugin tests

* [GitHub Actions] direct install of testing plugins

* Remove template code

* [plugins] meta --> collector

* [Docs] create scripted entries using an custom importer

* (closer to) being able to run behave tests outside project root directory

* We already know when exporter to use
Don't re-calculate it!

* [Tests] don't name test plugin 'testing"
If so named, pip won't install it.

* [Test] run behave tests with test plugins outside project root

* [Test] behave tests pass locally

* [Docs] fix typo

* [GitHub Actions] run test commands from poetry's shell

* black-ify code

* [GitHub Actions] move downstream (rather than up) to run tests

* [GitHub Actions] set shell to poetry

* [GitHub Workflows] Manually activate virtual environment

* [GitHub Actions] Skip Windows & Python 3.8
Can't seem to find Python exe?

* [GiotHub Actions] explicitly use virtual env

* [GitHub Actions] create virutal env directly

* [GitHub Actions] better activate of Windows virtual env

* [GitHub Actions] create virtual env on Mac

* [Github Actions] install wheel and upgrade pip

* [GitHub Actions] skip virtual environments altogether

* [GitHub Actions] change directory for behave test

* Remove Windows exclusions from CI as per note -- they should be working now

Co-authored-by: Suhas <sugas182@gmail.com>
Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
MinchinWeb 2021-05-29 18:21:45 -06:00 committed by GitHub
parent 104fa2caca
commit 1a7e8d8ea2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1025 additions and 409 deletions

View file

@ -12,9 +12,9 @@ from .commands import postconfig_list
from .commands import preconfig_diagnostic
from .commands import preconfig_version
from .output import deprecated_cmd
from .plugins import EXPORT_FORMATS
from .plugins import IMPORT_FORMATS
from .plugins import util
from .plugins.collector import EXPORT_FORMATS
from .plugins.collector import IMPORT_FORMATS
class WrappingFormatter(argparse.RawTextHelpFormatter):

47
jrnl/behave_testing.py Normal file
View file

@ -0,0 +1,47 @@
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
"""
Certain functions to support the *behave* test suite.
They are placed here so they are importable in multiple places, as otherwise
imports fail when running the suite outside of the project's root folder.
"""
import jrnl.time
def _mock_getpass(inputs):
def prompt_return(prompt=""):
if type(inputs) == str:
return inputs
try:
return next(inputs)
except StopIteration:
raise KeyboardInterrupt
return prompt_return
def _mock_input(inputs):
def prompt_return(prompt=""):
try:
val = next(inputs)
print(prompt, val)
return val
except StopIteration:
raise KeyboardInterrupt
return prompt_return
def _mock_time_parse(context):
original_parse = jrnl.time.parse
if "now" not in context:
return original_parse
def wrapper(input, *args, **kwargs):
input = context.now if input == "now" else input
return original_parse(input, *args, **kwargs)
return wrapper

View file

@ -28,6 +28,12 @@ def preconfig_diagnostic(_):
def preconfig_version(_):
from jrnl import __title__
from jrnl import __version__
from jrnl.plugins.collector import (
IMPORT_FORMATS,
EXPORT_FORMATS,
get_exporter,
get_importer,
)
version_str = f"""{__title__} version {__version__}
@ -37,6 +43,22 @@ This is free software, and you are welcome to redistribute it under certain
conditions; for details, see: https://www.gnu.org/licenses/gpl-3.0.html"""
print(version_str)
print()
print("Active Plugins:")
print(" Importers:")
for importer in IMPORT_FORMATS:
importer_class = get_importer(importer)
print(
f" {importer} : {importer_class.version} from",
f"{importer_class().class_path()}",
)
print(" Exporters:")
for exporter in EXPORT_FORMATS:
exporter_class = get_exporter(exporter)
# print(f" {exporter} : {exporter_class.version} from {exporter_class().class_path()}")
print(f" {exporter} : ", end="")
print(f"{exporter_class.version} from ", end="")
print(f"{exporter_class().class_path()}")
def postconfig_list(config, **kwargs):
@ -47,7 +69,7 @@ def postconfig_list(config, **kwargs):
def postconfig_import(args, config, **kwargs):
from .Journal import open_journal
from .plugins import get_importer
from .plugins.collector import get_importer
# Requires opening the journal
journal = open_journal(args.journal_name, config)

View file

View file

View file

@ -324,19 +324,17 @@ def _delete_search_results(journal, old_entries, **kwargs):
def _display_search_results(args, journal, **kwargs):
if args.short or args.export == "short":
print(journal.pprint(short=True))
elif args.export == "pretty":
print(journal.pprint())
print(plugins.collector.get_exporter("short").export(journal))
elif args.tags:
print(plugins.get_exporter("tags").export(journal))
print(plugins.collector.get_exporter("tags").export(journal))
elif args.export:
exporter = plugins.get_exporter(args.export)
exporter = plugins.collector.get_exporter(args.export)
print(exporter.export(journal, args.filename))
elif kwargs["config"].get("display_format"):
exporter = plugins.get_exporter(kwargs["config"]["display_format"])
exporter = plugins.collector.get_exporter(kwargs["config"]["display_format"])
print(exporter.export(journal, args.filename))
else:
print(journal.pprint())
# print(journal.pprint())
print(plugins.collector.get_exporter("default").export(journal))

View file

@ -1,49 +1,5 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from .fancy_exporter import FancyExporter
from .jrnl_importer import JRNLImporter
from .json_exporter import JSONExporter
from .markdown_exporter import MarkdownExporter
from .tag_exporter import TagExporter
from .dates_exporter import DatesExporter
from .template_exporter import __all__ as template_exporters
from .text_exporter import TextExporter
from .xml_exporter import XMLExporter
from .yaml_exporter import YAMLExporter
__exporters = [
JSONExporter,
MarkdownExporter,
TagExporter,
DatesExporter,
TextExporter,
XMLExporter,
YAMLExporter,
FancyExporter,
] + template_exporters
__importers = [JRNLImporter]
__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
__exporter_types["pretty"] = None
__exporter_types["short"] = None
__importer_types = {name: plugin for plugin in __importers for name in plugin.names}
EXPORT_FORMATS = sorted(__exporter_types.keys())
IMPORT_FORMATS = sorted(__importer_types.keys())
def get_exporter(format):
for exporter in __exporters:
if hasattr(exporter, "names") and format in exporter.names:
return exporter
return None
def get_importer(format):
for importer in __importers:
if hasattr(importer, "names") and format in importer.names:
return importer
return None

View file

@ -3,6 +3,11 @@
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
"""
Base class for Importers and Exporters.
"""
import os
import re
import unicodedata
@ -11,16 +16,36 @@ from jrnl.color import ERROR_COLOR
from jrnl.color import RESET_COLOR
class TextExporter:
"""This Exporter can convert entries and journals into text files."""
class BaseImporter:
"""Base Importer class (to sub-class)"""
names = ["text", "txt"]
extension = "txt"
# names = ["jrnl"]
# version = __version__
@classmethod
def class_path(cls):
return cls.__module__
@staticmethod
def import_(journal, input=None):
raise NotImplementedError
class BaseExporter:
"""Base Exporter class (to sub-class)"""
# names = ["text", "txt"]
# extension = "txt"
# version = __version__
@classmethod
def class_path(cls):
return cls.__module__
@classmethod
def export_entry(cls, entry):
"""Returns a string representation of a single entry."""
return str(entry)
raise NotImplementedError
@classmethod
def export_journal(cls, journal):
@ -33,9 +58,16 @@ class TextExporter:
try:
with open(path, "w", encoding="utf-8") as f:
f.write(cls.export_journal(journal))
return f"[Journal exported to {path}]"
return (
f"[Journal '{journal.name}' exported (as a single file) to {path}]"
)
except IOError as e:
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
except NotImplementedError:
return (
f"[{ERROR_COLOR}ERROR{RESET_COLOR}: This exporter doesn't support "
"exporting as a single file.]"
)
@classmethod
def make_filename(cls, entry):
@ -46,16 +78,23 @@ class TextExporter:
@classmethod
def write_files(cls, journal, path):
"""Exports a journal into individual files for each entry."""
for entry in journal.entries:
try:
full_path = os.path.join(path, cls.make_filename(entry))
with open(full_path, "w", encoding="utf-8") as f:
f.write(cls.export_entry(entry))
except IOError as e:
return "[{2}ERROR{3}: {0} {1}]".format(
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
)
return "[Journal exported to {}]".format(path)
try:
for entry in journal.entries:
try:
full_path = os.path.join(path, cls.make_filename(entry))
with open(full_path, "w", encoding="utf-8") as f:
f.write(cls.export_entry(entry))
except IOError as e:
return "[{2}ERROR{3}: {0} {1}]".format(
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
)
except NotImplementedError:
return (
f"[{ERROR_COLOR}ERROR{RESET_COLOR}: This exporter doesn't support "
"exporting as individual files.]"
)
else:
return f"[Journal '{journal.name}' exported (as multiple files) to {path}]"
def _slugify(string):
"""Slugifies a string.

106
jrnl/plugins/collector.py Normal file
View file

@ -0,0 +1,106 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
"""
Code relating to the collecting of plugins and distributing calls to them.
In particular, the code here collects the list of imports and exporters, both
internal and external, and tells the main program which plugins are available.
Actual calling of the plugins is done directly and works because given plugin
functions are importable/callable at predetermined (code) locations.
Internal plugins are located in the `jrnl.plugins` namespace, and external
plugins are located in the `jrnl.contrib` namespace.
This file was originally called "meta", using that title in the reflexive sense;
i.e. it is the collection of code that allows plugins to deal with themselves.
"""
import importlib
import pkgutil
import jrnl.contrib.exporter
import jrnl.contrib.importer
import jrnl.plugins.exporter
import jrnl.plugins.importer
__exporters_builtin = list(
pkgutil.iter_modules(
jrnl.plugins.exporter.__path__, jrnl.plugins.exporter.__name__ + "."
)
)
__exporters_contrib = list(
pkgutil.iter_modules(
jrnl.contrib.exporter.__path__, jrnl.contrib.exporter.__name__ + "."
)
)
__importers_builtin = list(
pkgutil.iter_modules(
jrnl.plugins.importer.__path__, jrnl.plugins.importer.__name__ + "."
)
)
__importers_contrib = list(
pkgutil.iter_modules(
jrnl.contrib.importer.__path__, jrnl.contrib.importer.__name__ + "."
)
)
__exporter_types_builtin = {
name: importlib.import_module(plugin.name)
for plugin in __exporters_builtin
for name in importlib.import_module(plugin.name).Exporter.names
}
__exporter_types_contrib = {
name: importlib.import_module(plugin.name)
for plugin in __exporters_contrib
for name in importlib.import_module(plugin.name).Exporter.names
}
__importer_types_builtin = {
name: importlib.import_module(plugin.name)
for plugin in __importers_builtin
for name in importlib.import_module(plugin.name).Importer.names
}
__importer_types_contrib = {
name: importlib.import_module(plugin.name)
for plugin in __importers_contrib
for name in importlib.import_module(plugin.name).Importer.names
}
__exporter_types = {
**__exporter_types_builtin,
**__exporter_types_contrib,
}
__importer_types = {
**__importer_types_builtin,
**__importer_types_contrib,
}
EXPORT_FORMATS = sorted(__exporter_types.keys())
"""list of stings: all available export formats."""
IMPORT_FORMATS = sorted(__importer_types.keys())
"""list of stings: all available import formats."""
def get_exporter(format):
"""
Given an export format, returns the (callable) class of the corresponding exporter.
"""
try:
return __exporter_types[format].Exporter
except (AttributeError, KeyError):
return None
def get_importer(format):
"""
Given an import format, returns the (callable) class of the corresponding importer.
"""
try:
return __importer_types[format].Importer
except (AttributeError, KeyError):
return None

View file

@ -2,16 +2,20 @@
# encoding: utf-8
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from collections import Counter
from .text_exporter import TextExporter
from jrnl.plugins.base import BaseExporter
from ... import __version__
class DatesExporter(TextExporter):
class Exporter(BaseExporter):
"""This Exporter lists dates and their respective counts, for heatingmapping etc."""
names = ["dates"]
extension = "dates"
version = __version__
@classmethod
def export_entry(cls, entry):

View file

@ -3,17 +3,21 @@
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from jrnl.exception import JrnlError
from textwrap import TextWrapper
from .text_exporter import TextExporter
from jrnl.plugins.base import BaseExporter
from jrnl.plugins.util import check_provided_linewrap_viability
from ... import __version__
class FancyExporter(TextExporter):
class Exporter(BaseExporter):
"""This Exporter can convert entries and journals into text with unicode box drawing characters."""
names = ["fancy", "boxed"]
extension = "txt"
version = __version__
# Top border of the card
border_a = ""
@ -80,14 +84,3 @@ class FancyExporter(TextExporter):
def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal."""
return "\n".join(cls.export_entry(entry) for entry in journal)
def check_provided_linewrap_viability(linewrap, card, journal):
if len(card[0]) > linewrap:
width_violation = len(card[0]) - linewrap
raise JrnlError(
"LineWrapTooSmallForDateFormat",
config_linewrap=linewrap,
columns=width_violation,
journal=journal,
)

View file

@ -5,15 +5,18 @@
import json
from .text_exporter import TextExporter
from .util import get_tags_count
from jrnl.plugins.base import BaseExporter
from jrnl.plugins.util import get_tags_count
from ... import __version__
class JSONExporter(TextExporter):
class Exporter(BaseExporter):
"""This Exporter can convert entries and journals into json."""
names = ["json"]
extension = "json"
version = __version__
@classmethod
def entry_to_dict(cls, entry):

View file

@ -9,15 +9,17 @@ import sys
from jrnl.color import RESET_COLOR
from jrnl.color import WARNING_COLOR
from jrnl.plugins.base import BaseExporter
from .text_exporter import TextExporter
from ... import __version__
class MarkdownExporter(TextExporter):
class Exporter(BaseExporter):
"""This Exporter can convert entries and journals into Markdown."""
names = ["md", "markdown"]
extension = "md"
version = __version__
@classmethod
def export_entry(cls, entry, to_multifile=True):

View file

@ -0,0 +1,20 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from jrnl.plugins.base import BaseExporter
from ... import __version__
class Exporter(BaseExporter):
"""Pretty print journal"""
names = ["pretty", "default"]
extension = "txt"
version = __version__
@classmethod
def export_journal(cls, journal):
return journal.pprint()

View file

@ -0,0 +1,20 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from jrnl.plugins.base import BaseExporter
from ... import __version__
class Exporter(BaseExporter):
"""Short export -- i.e. single line date and title"""
names = ["short"]
extension = "txt"
version = __version__
@classmethod
def export_journal(cls, journal):
return journal.pprint(short=True)

View file

@ -3,15 +3,19 @@
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from .text_exporter import TextExporter
from .util import get_tags_count
from jrnl.plugins.base import BaseExporter
from jrnl.plugins.util import get_tags_count
from ... import __version__
class TagExporter(TextExporter):
class Exporter(BaseExporter):
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
names = ["tags"]
extension = "tags"
version = __version__
@classmethod
def export_entry(cls, entry):

View file

@ -0,0 +1,21 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from jrnl.plugins.base import BaseExporter
from ... import __version__
class Exporter(BaseExporter):
"""This Exporter can convert entries and journals into text files."""
names = ["text", "txt"]
extension = "txt"
version = __version__
@classmethod
def export_entry(cls, entry):
"""Returns a string representation of a single entry."""
return str(entry)

View file

@ -5,30 +5,18 @@
from xml.dom import minidom
from .json_exporter import JSONExporter
from .util import get_tags_count
from jrnl.plugins.base import BaseExporter
from jrnl.plugins.util import get_tags_count
from ... import __version__
class XMLExporter(JSONExporter):
class Exporter(BaseExporter):
"""This Exporter can convert entries and journals into XML."""
names = ["xml"]
extension = "xml"
@classmethod
def export_entry(cls, entry, doc=None):
"""Returns an XML representation of a single entry."""
doc_el = doc or minidom.Document()
entry_el = doc_el.createElement("entry")
for key, value in cls.entry_to_dict(entry).items():
elem = doc_el.createElement(key)
elem.appendChild(doc_el.createTextNode(value))
entry_el.appendChild(elem)
if not doc:
doc_el.appendChild(entry_el)
return doc_el.toprettyxml()
else:
return entry_el
version = __version__
@classmethod
def entry_to_xml(cls, entry, doc):
@ -45,6 +33,21 @@ class XMLExporter(JSONExporter):
entry_el.appendChild(doc.createTextNode(entry.fulltext))
return entry_el
@classmethod
def export_entry(cls, entry, doc=None):
"""Returns an XML representation of a single entry."""
doc_el = doc or minidom.Document()
entry_el = doc_el.createElement("entry")
for key, value in cls.entry_to_dict(entry).items():
elem = doc_el.createElement(key)
elem.appendChild(doc_el.createTextNode(value))
entry_el.appendChild(elem)
if not doc:
doc_el.appendChild(entry_el)
return doc_el.toprettyxml()
else:
return entry_el
@classmethod
def export_journal(cls, journal):
"""Returns an XML representation of an entire journal."""

View file

@ -10,15 +10,17 @@ import sys
from jrnl.color import ERROR_COLOR
from jrnl.color import RESET_COLOR
from jrnl.color import WARNING_COLOR
from jrnl.plugins.base import BaseExporter
from .text_exporter import TextExporter
from ... import __version__
class YAMLExporter(TextExporter):
class Exporter(BaseExporter):
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
names = ["yaml"]
extension = "md"
version = __version__
@classmethod
def export_entry(cls, entry, to_multifile=True):
@ -133,9 +135,10 @@ class YAMLExporter(TextExporter):
def export_journal(cls, journal):
"""Returns an error, as YAML export requires a directory as a target."""
print(
"{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(
ERROR_COLOR, RESET_COLOR
(
f"[{ERROR_COLOR}ERROR{RESET_COLOR}: YAML export must be to "
"individual files. Please specify a directory to export to.]"
),
file=sys.stderr,
)
return
raise NotImplementedError

View file

@ -5,11 +5,16 @@
import sys
from jrnl.plugins.base import BaseImporter
class JRNLImporter:
from ... import __version__
class Importer(BaseImporter):
"""This plugin imports entries from other jrnl files."""
names = ["jrnl"]
version = __version__
@staticmethod
def import_(journal, input=None):
@ -28,7 +33,9 @@ class JRNLImporter:
journal.import_(other_journal_txt)
new_cnt = len(journal.entries)
print(
"[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name),
"[{} entries imported to '{}' journal]".format(
new_cnt - old_cnt, journal.name
),
file=sys.stderr,
)
journal.write()

View file

@ -1,142 +0,0 @@
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
import re
import yaml
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
PRINT_RE = r"{{ *(.+?) *}}"
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
END_BLOCK_RE = r"{% *end(for|if) *%}"
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE)
IF_RE = r"{% *if +(.+?) *%}"
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
INCLUDE_RE = r"{% *include +(.+?) *%}"
class Template:
def __init__(self, template):
self.template = template
self.clean_template = None
self.blocks = {}
@classmethod
def from_file(cls, filename):
with open(filename) as f:
front_matter, body = f.read().strip("-\n").split("---", 2)
front_matter = yaml.load(front_matter, Loader=yaml.FullLoader)
template = cls(body)
template.__dict__.update(front_matter)
return template
def render(self, **vars):
if self.clean_template is None:
self._get_blocks()
return self._expand(self.clean_template, **vars)
def render_block(self, block, **vars):
if self.clean_template is None:
self._get_blocks()
return self._expand(self.blocks[block], **vars)
def _eval_context(self, vars):
import asteval
e = asteval.Interpreter(use_numpy=False, writer=None)
e.symtable.update(vars)
e.symtable["__last_iteration"] = vars.get("__last_iteration", False)
return e
def _get_blocks(self):
def s(match):
name, contents = match.groups()
self.blocks[name] = self._strip_single_nl(contents)
return ""
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
def _expand(self, template, **vars):
stack = sorted(
[
(m.start(), 1, m.groups()[0])
for m in re.finditer(START_BLOCK_RE, template)
]
+ [
(m.end(), -1, m.groups()[0])
for m in re.finditer(END_BLOCK_RE, template)
]
)
last_nesting, nesting = 0, 0
start = 0
result = ""
block_type = None
if not stack:
return self._expand_vars(template, **vars)
for pos, indent, typ in stack:
nesting += indent
if nesting == 1 and last_nesting == 0:
block_type = typ
result += self._expand_vars(template[start:pos], **vars)
start = pos
elif nesting == 0 and last_nesting == 1:
if block_type == "if":
result += self._expand_cond(template[start:pos], **vars)
elif block_type == "for":
result += self._expand_loops(template[start:pos], **vars)
elif block_type == "block":
result += self._save_block(template[start:pos], **vars)
start = pos
last_nesting = nesting
result += self._expand_vars(template[stack[-1][0] :], **vars)
return result
def _expand_vars(self, template, **vars):
safe_eval = self._eval_context(vars)
expanded = re.sub(
INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template
)
return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
def _expand_cond(self, template, **vars):
start_block = re.search(IF_RE, template, re.M)
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
expression = start_block.groups()[0]
sub_template = self._strip_single_nl(
template[start_block.end() : end_block.start()]
)
safe_eval = self._eval_context(vars)
if safe_eval(expression):
return self._expand(sub_template)
return ""
def _strip_single_nl(self, template, strip_r=True):
if template[0] == "\n":
template = template[1:]
if strip_r and template[-1] == "\n":
template = template[:-1]
return template
def _expand_loops(self, template, **vars):
start_block = re.search(FOR_RE, template, re.M)
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
var_name, iterator = start_block.groups()
sub_template = self._strip_single_nl(
template[start_block.end() : end_block.start()], strip_r=False
)
safe_eval = self._eval_context(vars)
result = ""
items = safe_eval(iterator)
for idx, var in enumerate(items):
vars[var_name] = var
vars["__last_iteration"] = idx == len(items) - 1
result += self._expand(sub_template, **vars)
del vars[var_name]
return self._strip_single_nl(result)

View file

@ -1,44 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from glob import glob
import os
from .template import Template
from .text_exporter import TextExporter
class GenericTemplateExporter(TextExporter):
"""This Exporter can convert entries and journals into text files."""
@classmethod
def export_entry(cls, entry):
"""Returns a string representation of a single entry."""
vars = {"entry": entry, "tags": entry.tags}
return cls.template.render_block("entry", **vars)
@classmethod
def export_journal(cls, journal):
"""Returns a string representation of an entire journal."""
vars = {"journal": journal, "entries": journal.entries, "tags": journal.tags}
return cls.template.render_block("journal", **vars)
def __exporter_from_file(template_file):
"""Create a template class from a file"""
name = os.path.basename(template_file).replace(".template", "")
template = Template.from_file(template_file)
return type(
str(f"{name.title()}Exporter"),
(GenericTemplateExporter,),
{"names": [name], "extension": template.extension, "template": template},
)
__all__ = []
# Factory pattern to create Exporter classes for all available templates
for template_file in glob("jrnl/templates/*.template"):
__all__.append(__exporter_from_file(template_file))

View file

@ -3,11 +3,13 @@
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from jrnl.exception import JrnlError
def get_tags_count(journal):
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
# Astute reader: should the following line leave you as puzzled as me the first time
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
# I came across this construction, worry not and embrace the ensuing moment of enlightenment.
tags = [tag for entry in journal.entries for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = {(tags.count(tag), tag) for tag in tags}
@ -25,3 +27,14 @@ def oxford_list(lst):
return lst[0] + " or " + lst[1]
else:
return ", ".join(lst[:-1]) + ", or " + lst[-1]
def check_provided_linewrap_viability(linewrap, card, journal):
if len(card[0]) > linewrap:
width_violation = len(card[0]) - linewrap
raise JrnlError(
"LineWrapTooSmallForDateFormat",
config_linewrap=linewrap,
columns=width_violation,
journal=journal,
)

View file

@ -1,18 +0,0 @@
---
extension: txt
---
{% block journal %}
{% for entry in entries %}
{% include entry %}
{% endfor %}
{% endblock %}
{% block entry %}
{{ entry.title }}
{{ "-" * len(entry.title) }}
{{ entry.body }}
{% endblock %}