mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
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:
parent
cef3a98b4e
commit
4392e29742
45 changed files with 1021 additions and 383 deletions
57
.github/workflows/testing_external_plugins.yaml
vendored
Normal file
57
.github/workflows/testing_external_plugins.yaml
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
name: Testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ develop, release ]
|
||||||
|
paths:
|
||||||
|
- 'jrnl/**'
|
||||||
|
- 'features/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'poetry.lock'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
pull_request:
|
||||||
|
branches: [ develop ]
|
||||||
|
paths:
|
||||||
|
- 'jrnl/**'
|
||||||
|
- 'features/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'poetry.lock'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-namespace-plugins:
|
||||||
|
if: >
|
||||||
|
! contains(github.event.head_commit.message, '[ci skip]')
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [ 3.7, 3.8, 3.9 ]
|
||||||
|
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||||
|
exclude: # Added for GitHub Actions PR problem 2020-12-19 -- remove later!
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: 3.9
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install pip setuptools wheel --upgrade
|
||||||
|
python -m pip install .
|
||||||
|
python -m pip install ./tests/external_plugins_src/
|
||||||
|
python -m pip install pytest behave
|
||||||
|
# installed test plugins aren't recognized by "behave" if run from the
|
||||||
|
# project's root folder
|
||||||
|
|
||||||
|
- name: Test with pytest
|
||||||
|
if: success() || failure()
|
||||||
|
run: pytest --junitxml=reports/pytest/results.xml
|
||||||
|
|
||||||
|
- name: Test with behave
|
||||||
|
if: success() || failure()
|
||||||
|
run: cd features && behave --no-skipped --format progress2 --junit --junit-directory ../reports/behave
|
|
@ -13,7 +13,10 @@ used alone (e.g. `jrnl --format json`) to display all entries from the selected
|
||||||
|
|
||||||
This page shows examples of all the built-in formats, but since `jrnl` supports adding
|
This page shows examples of all the built-in formats, but since `jrnl` supports adding
|
||||||
more formats through plugins, you may have more available on your system. Please see
|
more formats through plugins, you may have more available on your system. Please see
|
||||||
`jrnl --help` for a list of which formats are available on your system.
|
`jrnl --version` for a list of which formats are available on your system. Note
|
||||||
|
that plugins can also override built-in formats, so review your installed
|
||||||
|
plugins if your output does not match what is listed here. You can also [write
|
||||||
|
your own plugins](./plugins.md) to create custom formats.
|
||||||
|
|
||||||
Any of these formats can be used interchangeably, and are only grouped into "display",
|
Any of these formats can be used interchangeably, and are only grouped into "display",
|
||||||
"data", and "report" formats below for convenience.
|
"data", and "report" formats below for convenience.
|
||||||
|
|
181
docs/plugins.md
Normal file
181
docs/plugins.md
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
<!-- Copyright (C) 2012-2021 jrnl contributors
|
||||||
|
License: https://www.gnu.org/licenses/gpl-3.0.html -->
|
||||||
|
|
||||||
|
# Extending jrnl
|
||||||
|
|
||||||
|
*jrnl* can be extended with custom importers and exporters.
|
||||||
|
|
||||||
|
Note that custom importers and exporters can be given the same name as a
|
||||||
|
built-in importer or exporter to override it.
|
||||||
|
|
||||||
|
Custom Importers and Exporters are traditional Python packages, and are
|
||||||
|
installed (into *jrnl*) simply by installing them so they are available to the
|
||||||
|
Python interpreter that is running *jrnl*.
|
||||||
|
|
||||||
|
Exporter are also used as "formatters" when entries are written to the command
|
||||||
|
line.
|
||||||
|
|
||||||
|
## Rational
|
||||||
|
|
||||||
|
I added this feature because *jrnl* was overall working well for me, but I
|
||||||
|
found myself maintaining a private fork so I could have a slightly customized
|
||||||
|
export format. Implementing (import and) export plugins was seen as a way to
|
||||||
|
maintain my custom exporter without the need to maintaining my private fork.
|
||||||
|
|
||||||
|
This implementation tries to keep plugins as light as possible, and as free of
|
||||||
|
boilerplate code as reasonable. As well, internal importers and exporters are
|
||||||
|
implemented in almost exactly the same way as custom importers and exporters,
|
||||||
|
and so it is hoped that plugins can be moved from "contributed" to "internal"
|
||||||
|
easily, or that internal plugins can serve as a base and/or a demonstration for
|
||||||
|
external plugins.
|
||||||
|
|
||||||
|
-- @MinchinWeb, May 2021
|
||||||
|
|
||||||
|
## Entry Class
|
||||||
|
|
||||||
|
Both the Importers and the Exporters work on the `Entry` class. Below is a
|
||||||
|
(selective) description of the class, it's properties and functions:
|
||||||
|
|
||||||
|
- **Entry** (class) at `jrnl.Entry.Entry`.
|
||||||
|
- **title** (string): a single line that represents a entry's title.
|
||||||
|
- **date** (datetime.datetime): the date and time assigned to an entry.
|
||||||
|
- **body** (string): the main body of the entry. Can be basically any
|
||||||
|
length. *jrnl* assumes no particular structure here.
|
||||||
|
- **starred** (boolean): is an entry starred? Presumably, starred entries
|
||||||
|
are of particular importance.
|
||||||
|
- **tags** (list of strings): the tags attached to an entry. Each tag
|
||||||
|
includes the pre-facing "tag symbol".
|
||||||
|
- **\_\_init\_\_(journal, date=None, text="", starred=False)**: contractor
|
||||||
|
method
|
||||||
|
- **journal** (*jrnl.Journal.Journal*): a link to an existing Journal
|
||||||
|
class. Mainly used to access it's configuration.
|
||||||
|
- **date** (datetime.datetime)
|
||||||
|
- **text** (string): assumed to include both the title and the body.
|
||||||
|
When the title, body, or tags of an entry are requested, this text
|
||||||
|
will the parsed to determine the tree.
|
||||||
|
- **starred** (boolean)
|
||||||
|
|
||||||
|
Entries also have "advanced" metadata if they are using the DayOne backend, but
|
||||||
|
we'll ignore that for the purposes of this demo.
|
||||||
|
|
||||||
|
## Custom Importer
|
||||||
|
|
||||||
|
If you have a (custom) datasource that you want to import into your jrnl
|
||||||
|
(perhaps like a blog export), you can write a custom importer to do this.
|
||||||
|
|
||||||
|
An importer takes the source data, turns it into Entries and then appends those
|
||||||
|
entries to a Journal. Here is a basic Importer, assumed to be provided with a
|
||||||
|
nicely formatted JSON file:
|
||||||
|
|
||||||
|
~~~ python
|
||||||
|
{%
|
||||||
|
include-markdown "../tests/external_plugins_src/jrnl/contrib/importer/simple_json.py"
|
||||||
|
comments=false
|
||||||
|
%}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Note that the above is very minimal, doesn't do any error checking, and doesn't
|
||||||
|
try to import all possible entry metadata.
|
||||||
|
|
||||||
|
Another potential use of a custom importer is to effectively create a scripted
|
||||||
|
entry creator. For example, maybe each day you want to create a journal entry
|
||||||
|
that contains the answers to specific questions; you could create a custom
|
||||||
|
"importer" that would ask you the questions, and then create an entry containing
|
||||||
|
the answers provided.
|
||||||
|
|
||||||
|
Some implementation notes:
|
||||||
|
|
||||||
|
- The importer class must be named **Importer**, and should sub-class
|
||||||
|
**jrnl.plugins.base.BaseImporter**.
|
||||||
|
- The importer module must be within the **jrnl.contrib.importer** namespace.
|
||||||
|
- The importer must not have any `__init__.py` files in the base directories
|
||||||
|
(but you can have one for your importer base directory if it is in a
|
||||||
|
directory rather than a single file).
|
||||||
|
- The importer must be installed as a Python package available to the same
|
||||||
|
Python interpreter running jrnl.
|
||||||
|
- The importer must expose at least the following the following members:
|
||||||
|
- **version** (string): the version of the plugin. Displayed to help the
|
||||||
|
user debug their installations.
|
||||||
|
- **names** (list of strings): these are the "names" that can be passed to
|
||||||
|
the CLI to involve your importer. If you specify one used by a built-in
|
||||||
|
plugin, it will overwrite it (effectively making the built-in one
|
||||||
|
unavailable).
|
||||||
|
- **import_(journal, input=None)**: the actual importer. Must append
|
||||||
|
entries to the journal passed to it. It is recommended to accept either a
|
||||||
|
filename or standard input as a source.
|
||||||
|
|
||||||
|
## Custom Exporter
|
||||||
|
|
||||||
|
Custom exporters are useful to make *jrnl*'s data available to other programs.
|
||||||
|
One common usecase would to generate the input to be used by a static site
|
||||||
|
generator or blogging engine.
|
||||||
|
|
||||||
|
An exporter take either a whole journal or a specific entry and exports it.
|
||||||
|
Below is a basic JSON Exporter; note that a more extensive JSON exporter is
|
||||||
|
included in *jrnl* and so this (if installed) would override the built in
|
||||||
|
exporter.
|
||||||
|
|
||||||
|
~~~ python
|
||||||
|
{%
|
||||||
|
include-markdown "../tests/external_plugins_src/jrnl/contrib/exporter/custom_json.py"
|
||||||
|
comments=false
|
||||||
|
%}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Note that the above is very minimal, doesn't do any error checking, and doesn't
|
||||||
|
export all entry metadata.
|
||||||
|
|
||||||
|
Some implementation notes:
|
||||||
|
|
||||||
|
- the exporter class must be named **Exporter** and should sub-class
|
||||||
|
**jrnl.plugins.base.BaseExporter**.
|
||||||
|
- the exporter module must be within the **jrnl.contrib.exporter** namespace.
|
||||||
|
- The exporter must not have any `__init__.py` files in the base directories
|
||||||
|
(but you can have one for your exporter base directory if it is in a
|
||||||
|
directory rather than a single file).
|
||||||
|
- The exporter must be installed as a Python package available to the same
|
||||||
|
Python interpreter running jrnl.
|
||||||
|
- the exporter should expose at least the following the following members
|
||||||
|
(there are a few more you will need to define if you don't subclass
|
||||||
|
`jrnl.plugins.base.BaseExporter`):
|
||||||
|
- **version** (string): the version of the plugin. Displayed to help the
|
||||||
|
user debug their installations.
|
||||||
|
- **names** (list of strings): these are the "names" that can be passed to
|
||||||
|
the CLI to invole your exporter. If you specific one used by a built-in
|
||||||
|
plugin, it will overwrite it (effectively making the built-in one
|
||||||
|
unavailable).
|
||||||
|
- **extension** (string): the file extention used on exported entries.
|
||||||
|
- **export_entry(entry)**: given an entry, returns a string of the formatted,
|
||||||
|
exported entry.
|
||||||
|
- **export_journal(journal)**: (optional) given a journal, returns a string
|
||||||
|
of the formatted, exported entries of the journal. If not implemented,
|
||||||
|
*jrnl* will call **export_entry()** on each entry in turn and then
|
||||||
|
concatenate the results together.
|
||||||
|
|
||||||
|
### Special Exporters
|
||||||
|
|
||||||
|
There are a few "special" exporters, in that they are called by *jrnl* in
|
||||||
|
situations other than a traditional export. They are:
|
||||||
|
|
||||||
|
- **short** -- called by `jrnl --short`. Displays each entry on a single line.
|
||||||
|
The default is to print the timestamp of the entry, followed by the title.
|
||||||
|
The built-in (default) plugin is at `jrnl.plugins.exporter.short`.
|
||||||
|
- **default** -- called when a different format is not specified. The built-in
|
||||||
|
(default) plugin is at `jrnl.plugins.exporter.pretty`.
|
||||||
|
|
||||||
|
## Development Tips
|
||||||
|
|
||||||
|
- Editable installs (`pip install -e ...`) don't seem to play nice with
|
||||||
|
the namespace layout. If your plugin isn't appearing, try a non-editable
|
||||||
|
install of both *jrnl* and your plugin.
|
||||||
|
- If you run *jrnl* from the main project root directory (the one that contains
|
||||||
|
*jrnl*'s source code), namespace plugins won't be recognized. This is (I
|
||||||
|
suspect) because the Python interpreter will find your *jrnl* source directory
|
||||||
|
(which doesn't contain your namespace plugins) before it find your
|
||||||
|
"site-packages" directory (i.e. installed packages, which will recognize
|
||||||
|
namespace packages).
|
||||||
|
- Don't name your plugin file "testing.py" or it won't be installed (at least
|
||||||
|
automatically) by pip.
|
||||||
|
- For examples, you can look to the *jrnl*'s internal importers and exporters.
|
||||||
|
As well, there are some basic external examples included in *jrnl*'s git repo
|
||||||
|
at `tests/external_plugins_src` (including the example code above).
|
|
@ -1 +1,2 @@
|
||||||
mkdocs==1.1
|
mkdocs==1.1.2
|
||||||
|
mkdocs-include-markdown-plugin==2.8.0
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from jrnl.os_compat import on_windows
|
from jrnl.os_compat import on_windows
|
||||||
|
|
||||||
|
try:
|
||||||
|
from jrnl.contrib.exporter import flag as testing_exporter
|
||||||
|
except ImportError:
|
||||||
|
testing_exporter = None
|
||||||
|
|
||||||
CWD = os.getcwd()
|
CWD = os.getcwd()
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
TARGET_CWD = HERE.parent # project root folder
|
||||||
|
|
||||||
# @see https://behave.readthedocs.io/en/latest/tutorial.html#debug-on-error-in-case-of-step-failures
|
# @see https://behave.readthedocs.io/en/latest/tutorial.html#debug-on-error-in-case-of-step-failures
|
||||||
BEHAVE_DEBUG_ON_ERROR = False
|
BEHAVE_DEBUG_ON_ERROR = False
|
||||||
|
@ -15,6 +23,8 @@ def setup_debug_on_error(userdata):
|
||||||
|
|
||||||
|
|
||||||
def before_all(context):
|
def before_all(context):
|
||||||
|
# always start in project root directory
|
||||||
|
os.chdir(TARGET_CWD)
|
||||||
setup_debug_on_error(context.config.userdata)
|
setup_debug_on_error(context.config.userdata)
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,10 +37,10 @@ def before_all(context):
|
||||||
|
|
||||||
|
|
||||||
def clean_all_working_dirs():
|
def clean_all_working_dirs():
|
||||||
if os.path.exists("test.txt"):
|
if os.path.exists(HERE / "test.txt"):
|
||||||
os.remove("test.txt")
|
os.remove(HERE / "test.txt")
|
||||||
for folder in ("configs", "journals", "cache"):
|
for folder in ("configs", "journals", "cache"):
|
||||||
working_dir = os.path.join("features", folder)
|
working_dir = HERE / folder
|
||||||
if os.path.exists(working_dir):
|
if os.path.exists(working_dir):
|
||||||
shutil.rmtree(working_dir)
|
shutil.rmtree(working_dir)
|
||||||
|
|
||||||
|
@ -46,20 +56,28 @@ def before_feature(context, feature):
|
||||||
feature.skip("Skipping on Windows")
|
feature.skip("Skipping on Windows")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if "skip_only_with_external_plugins" in feature.tags and testing_exporter is None:
|
||||||
|
feature.skip("Requires test external plugins installed")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "skip_no_external_plugins" in feature.tags and testing_exporter:
|
||||||
|
feature.skip("Skipping with external plugins installed")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def before_scenario(context, scenario):
|
def before_scenario(context, scenario):
|
||||||
"""Before each scenario, backup all config and journal test data."""
|
"""Before each scenario, backup all config and journal test data."""
|
||||||
# Clean up in case something went wrong
|
# Clean up in case something went wrong
|
||||||
clean_all_working_dirs()
|
clean_all_working_dirs()
|
||||||
for folder in ("configs", "journals"):
|
for folder in ("configs", "journals"):
|
||||||
original = os.path.join("features", "data", folder)
|
original = HERE / "data" / folder
|
||||||
working_dir = os.path.join("features", folder)
|
working_dir = HERE / folder
|
||||||
if not os.path.exists(working_dir):
|
if not os.path.exists(working_dir):
|
||||||
os.mkdir(working_dir)
|
os.mkdir(working_dir)
|
||||||
for filename in os.listdir(original):
|
for filename in os.listdir(original):
|
||||||
source = os.path.join(original, filename)
|
source = original / filename
|
||||||
if os.path.isdir(source):
|
if os.path.isdir(source):
|
||||||
shutil.copytree(source, os.path.join(working_dir, filename))
|
shutil.copytree(source, (working_dir / filename))
|
||||||
else:
|
else:
|
||||||
shutil.copy2(source, working_dir)
|
shutil.copy2(source, working_dir)
|
||||||
|
|
||||||
|
@ -73,11 +91,22 @@ def before_scenario(context, scenario):
|
||||||
scenario.skip("Skipping on Windows")
|
scenario.skip("Skipping on Windows")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
"skip_only_with_external_plugins" in scenario.effective_tags
|
||||||
|
and testing_exporter is None
|
||||||
|
):
|
||||||
|
scenario.skip("Requires test external plugins installed")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "skip_no_external_plugins" in scenario.effective_tags and testing_exporter:
|
||||||
|
scenario.skip("Skipping with external plugins installed")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def after_scenario(context, scenario):
|
def after_scenario(context, scenario):
|
||||||
"""After each scenario, restore all test data and remove working_dirs."""
|
"""After each scenario, restore all test data and remove working_dirs."""
|
||||||
if os.getcwd() != CWD:
|
if os.getcwd() != TARGET_CWD:
|
||||||
os.chdir(CWD)
|
os.chdir(TARGET_CWD)
|
||||||
|
|
||||||
# only clean up if debugging is off and the scenario passed
|
# only clean up if debugging is off and the scenario passed
|
||||||
if BEHAVE_DEBUG_ON_ERROR and scenario.status != "failed":
|
if BEHAVE_DEBUG_ON_ERROR and scenario.status != "failed":
|
||||||
|
|
|
@ -26,6 +26,7 @@ Feature: Custom formats
|
||||||
| basic_folder |
|
| basic_folder |
|
||||||
| basic_dayone |
|
| basic_dayone |
|
||||||
|
|
||||||
|
@skip_no_external_plugins
|
||||||
Scenario Outline: JSON format
|
Scenario Outline: JSON format
|
||||||
Given we use the config "<config>.yaml"
|
Given we use the config "<config>.yaml"
|
||||||
And we use the password "test" if prompted
|
And we use the password "test" if prompted
|
||||||
|
@ -48,6 +49,7 @@ Feature: Custom formats
|
||||||
| basic_folder |
|
| basic_folder |
|
||||||
| basic_dayone |
|
| basic_dayone |
|
||||||
|
|
||||||
|
@skip_no_external_plugins
|
||||||
Scenario: Exporting dayone to json
|
Scenario: Exporting dayone to json
|
||||||
Given we use the config "dayone.yaml"
|
Given we use the config "dayone.yaml"
|
||||||
When we run "jrnl --export json"
|
When we run "jrnl --export json"
|
||||||
|
@ -91,6 +93,7 @@ Feature: Custom formats
|
||||||
| basic_folder |
|
| basic_folder |
|
||||||
| basic_dayone |
|
| basic_dayone |
|
||||||
|
|
||||||
|
@skip_no_external_plugins
|
||||||
Scenario Outline: Exporting using filters should only export parts of the journal
|
Scenario Outline: Exporting using filters should only export parts of the journal
|
||||||
Given we use the config "<config>.yaml"
|
Given we use the config "<config>.yaml"
|
||||||
And we use the password "test" if prompted
|
And we use the password "test" if prompted
|
||||||
|
@ -112,6 +115,7 @@ Feature: Custom formats
|
||||||
| basic_folder |
|
| basic_folder |
|
||||||
| basic_dayone |
|
| basic_dayone |
|
||||||
|
|
||||||
|
@skip # template exporters have been removed
|
||||||
Scenario Outline: Exporting using custom templates
|
Scenario Outline: Exporting using custom templates
|
||||||
Given we use the config "<config>.yaml"
|
Given we use the config "<config>.yaml"
|
||||||
And we load template "sample.template"
|
And we load template "sample.template"
|
||||||
|
|
86
features/plugins.feature
Normal file
86
features/plugins.feature
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
Feature: Functionality of Importer and Exporter Plugins
|
||||||
|
|
||||||
|
@skip_no_external_plugins
|
||||||
|
Scenario Outline: List buildin plugin names in --version
|
||||||
|
Given We use the config "basic_onefile.yaml"
|
||||||
|
When We run "jrnl --version"
|
||||||
|
Then the output should contain pyproject.toml version
|
||||||
|
And The output should contain "<plugin_name> : <version> from jrnl.<source>.<type>.<filename>"
|
||||||
|
And the output should not contain ".contrib."
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| plugin_name | version | source | type | filename |
|
||||||
|
| jrnl | <pyproject.toml version> | plugins | importer | jrnl |
|
||||||
|
| boxed | <pyproject.toml version> | plugins | exporter | fancy |
|
||||||
|
| dates | <pyproject.toml version> | plugins | exporter | dates |
|
||||||
|
| default | <pyproject.toml version> | plugins | exporter | pretty |
|
||||||
|
| fancy | <pyproject.toml version> | plugins | exporter | fancy |
|
||||||
|
| json | <pyproject.toml version> | plugins | exporter | json |
|
||||||
|
| markdown | <pyproject.toml version> | plugins | exporter | markdown |
|
||||||
|
| md | <pyproject.toml version> | plugins | exporter | markdown |
|
||||||
|
| pretty | <pyproject.toml version> | plugins | exporter | pretty |
|
||||||
|
| short | <pyproject.toml version> | plugins | exporter | short |
|
||||||
|
| tags | <pyproject.toml version> | plugins | exporter | tag |
|
||||||
|
| text | <pyproject.toml version> | plugins | exporter | text |
|
||||||
|
| txt | <pyproject.toml version> | plugins | exporter | text |
|
||||||
|
| xml | <pyproject.toml version> | plugins | exporter | xml |
|
||||||
|
| yaml | <pyproject.toml version> | plugins | exporter | yaml |
|
||||||
|
|
||||||
|
@skip_only_with_external_plugins
|
||||||
|
Scenario Outline: List external plugin names in --version
|
||||||
|
Given We use the config "basic_onefile.yaml"
|
||||||
|
When We run "jrnl --version"
|
||||||
|
Then the output should contain pyproject.toml version
|
||||||
|
And The output should contain "<plugin_name> : <version> from jrnl.<source>.<type>.<filename>"
|
||||||
|
Examples:
|
||||||
|
| plugin_name | version | source | type | filename |
|
||||||
|
| jrnl | <pyproject.toml version> | plugins | importer | jrnl |
|
||||||
|
| json | v1.0.0 | contrib | importer | simple_json |
|
||||||
|
| boxed | <pyproject.toml version> | plugins | exporter | fancy |
|
||||||
|
| dates | <pyproject.toml version> | plugins | exporter | dates |
|
||||||
|
| default | <pyproject.toml version> | plugins | exporter | pretty |
|
||||||
|
| fancy | <pyproject.toml version> | plugins | exporter | fancy |
|
||||||
|
| json | v1.0.0 | contrib | exporter | custom_json |
|
||||||
|
| markdown | <pyproject.toml version> | plugins | exporter | markdown |
|
||||||
|
| md | <pyproject.toml version> | plugins | exporter | markdown |
|
||||||
|
| pretty | <pyproject.toml version> | plugins | exporter | pretty |
|
||||||
|
| rot13 | v1.0.0 | contrib | exporter | rot13 |
|
||||||
|
| short | <pyproject.toml version> | plugins | exporter | short |
|
||||||
|
| tags | <pyproject.toml version> | plugins | exporter | tag |
|
||||||
|
| testing | v0.0.1 | contrib | exporter | flag |
|
||||||
|
| text | <pyproject.toml version> | plugins | exporter | text |
|
||||||
|
| txt | v1.0.0 | contrib | exporter | rot13 |
|
||||||
|
| xml | <pyproject.toml version> | plugins | exporter | xml |
|
||||||
|
| yaml | <pyproject.toml version> | plugins | exporter | yaml |
|
||||||
|
|
||||||
|
@skip_only_with_external_plugins
|
||||||
|
Scenario Outline: Do not list overridden plugin names in --version
|
||||||
|
Given We use the config "basic_onefile.yaml"
|
||||||
|
When We run "jrnl --version"
|
||||||
|
Then the output should contain pyproject.toml version
|
||||||
|
And the output should not contain "<plugin_name> : <version> from jrnl.<source>.<type>.<filename>"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| plugin_name | version | source | type | filename |
|
||||||
|
| json | <pyproject.toml version> | plugins | exporter | json |
|
||||||
|
| txt | <pyproject.toml version> | plugins | exporter | text |
|
||||||
|
|
||||||
|
|
||||||
|
@skip_only_with_external_plugins
|
||||||
|
Scenario Outline: JSON format
|
||||||
|
Given we use the config "<config>.yaml"
|
||||||
|
And we use the password "test" if prompted
|
||||||
|
When we run "jrnl --format json"
|
||||||
|
Then we should get no error
|
||||||
|
And the output should be parsable as json
|
||||||
|
And "entries" in the json output should have 3 elements
|
||||||
|
And entry 1 should not have an array "tags"
|
||||||
|
And entry 2 should not have an array "tags"
|
||||||
|
And entry 3 should not have an array "tags"
|
||||||
|
|
||||||
|
Examples: configs
|
||||||
|
| config |
|
||||||
|
| basic_onefile |
|
||||||
|
| basic_encrypted |
|
||||||
|
| basic_folder |
|
||||||
|
| basic_dayone |
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from jrnl.args import parse_args
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
@ -14,20 +13,23 @@ from behave import given
|
||||||
from behave import then
|
from behave import then
|
||||||
from behave import when
|
from behave import when
|
||||||
import keyring
|
import keyring
|
||||||
|
|
||||||
import toml
|
import toml
|
||||||
import yaml
|
import yaml
|
||||||
from yaml.loader import SafeLoader
|
from yaml.loader import SafeLoader
|
||||||
|
|
||||||
|
|
||||||
import jrnl.time
|
|
||||||
from jrnl import Journal
|
from jrnl import Journal
|
||||||
from jrnl import __version__
|
from jrnl import __version__
|
||||||
from jrnl import plugins
|
from jrnl import plugins
|
||||||
|
from jrnl.args import parse_args
|
||||||
|
from jrnl.behave_testing import _mock_getpass
|
||||||
|
from jrnl.behave_testing import _mock_input
|
||||||
|
from jrnl.behave_testing import _mock_time_parse
|
||||||
from jrnl.cli import cli
|
from jrnl.cli import cli
|
||||||
from jrnl.config import load_config
|
from jrnl.config import load_config
|
||||||
from jrnl.os_compat import split_args
|
from jrnl.os_compat import split_args
|
||||||
from jrnl.override import apply_overrides, _recursively_apply
|
from jrnl.override import _recursively_apply
|
||||||
|
from jrnl.override import apply_overrides
|
||||||
|
import jrnl.time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import parsedatetime.parsedatetime_consts as pdt
|
import parsedatetime.parsedatetime_consts as pdt
|
||||||
|
@ -279,42 +281,6 @@ def extension_editor_file(context, suffix):
|
||||||
assert filename_suffix == suffix
|
assert filename_suffix == suffix
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@when('we run "{command}" and enter')
|
@when('we run "{command}" and enter')
|
||||||
@when('we run "{command}" and enter nothing')
|
@when('we run "{command}" and enter nothing')
|
||||||
@when('we run "{command}" and enter "{inputs}"')
|
@when('we run "{command}" and enter "{inputs}"')
|
||||||
|
@ -461,8 +427,9 @@ def run(context, command, text=""):
|
||||||
@given('we load template "{filename}"')
|
@given('we load template "{filename}"')
|
||||||
def load_template(context, filename):
|
def load_template(context, filename):
|
||||||
full_path = os.path.join("features/data/templates", filename)
|
full_path = os.path.join("features/data/templates", filename)
|
||||||
|
|
||||||
exporter = plugins.template_exporter.__exporter_from_file(full_path)
|
exporter = plugins.template_exporter.__exporter_from_file(full_path)
|
||||||
plugins.__exporter_types[exporter.names[0]] = exporter
|
plugins.collector.__exporter_types[exporter.names[0]] = exporter
|
||||||
|
|
||||||
|
|
||||||
@when('we set the keyring password of "{journal}" to "{password}"')
|
@when('we set the keyring password of "{journal}" to "{password}"')
|
||||||
|
@ -540,6 +507,11 @@ def check_output_version_inline(context):
|
||||||
@then('the output should contain "{text}" or "{text2}"')
|
@then('the output should contain "{text}" or "{text2}"')
|
||||||
def check_output_inline(context, text=None, text2=None):
|
def check_output_inline(context, text=None, text2=None):
|
||||||
text = text or context.text
|
text = text or context.text
|
||||||
|
if "<pyproject.toml version>" in text:
|
||||||
|
pyproject = (Path(__file__) / ".." / ".." / ".." / "pyproject.toml").resolve()
|
||||||
|
pyproject_contents = toml.load(pyproject)
|
||||||
|
pyproject_version = pyproject_contents["tool"]["poetry"]["version"]
|
||||||
|
text = text.replace("<pyproject.toml version>", pyproject_version)
|
||||||
out = context.stdout_capture.getvalue()
|
out = context.stdout_capture.getvalue()
|
||||||
assert (text and text in out) or (text2 and text2 in out)
|
assert (text and text in out) or (text2 and text2 in out)
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,14 @@ def entry_array_count(context, entry_number, name, items_number):
|
||||||
assert len(out_json["entries"][entry_number - 1][name]) == items_number
|
assert len(out_json["entries"][entry_number - 1][name]) == items_number
|
||||||
|
|
||||||
|
|
||||||
|
@then('entry {entry_number:d} should not have an array "{name}"')
|
||||||
|
def entry_not_array_item(context, entry_number, name):
|
||||||
|
# note that entry_number is 1-indexed.
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
out_json = json.loads(out)
|
||||||
|
assert name not in out_json["entries"][entry_number - 1]
|
||||||
|
|
||||||
|
|
||||||
@then("the output should be a valid XML string")
|
@then("the output should be a valid XML string")
|
||||||
def assert_valid_xml_string(context):
|
def assert_valid_xml_string(context):
|
||||||
output = context.stdout_capture.getvalue()
|
output = context.stdout_capture.getvalue()
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from jrnl.jrnl import run
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
# from __future__ import with_statement
|
|
||||||
from jrnl.args import parse_args
|
|
||||||
from behave import then
|
from behave import then
|
||||||
|
|
||||||
from features.steps.core import _mock_getpass, _mock_time_parse
|
from jrnl.args import parse_args
|
||||||
|
from jrnl.behave_testing import _mock_getpass
|
||||||
|
from jrnl.behave_testing import _mock_time_parse
|
||||||
|
from jrnl.jrnl import run
|
||||||
|
|
||||||
|
|
||||||
@then("the editor {editor} should have been called")
|
@then("the editor {editor} should have been called")
|
||||||
|
|
|
@ -183,6 +183,8 @@ Feature: Writing new entries.
|
||||||
And we run "jrnl -until 1980"
|
And we run "jrnl -until 1980"
|
||||||
Then the output should be "1979-05-01 09:00 Being born hurts."
|
Then the output should be "1979-05-01 09:00 Being born hurts."
|
||||||
|
|
||||||
|
# the testing plugins override the JSON exporter
|
||||||
|
@skip_no_external_plugins
|
||||||
Scenario: Writing into Dayone adds extended metadata
|
Scenario: Writing into Dayone adds extended metadata
|
||||||
Given we use the config "dayone.yaml"
|
Given we use the config "dayone.yaml"
|
||||||
When we run "jrnl 01 may 1979: Being born hurts."
|
When we run "jrnl 01 may 1979: Being born hurts."
|
||||||
|
|
|
@ -12,9 +12,9 @@ from .commands import postconfig_list
|
||||||
from .commands import preconfig_diagnostic
|
from .commands import preconfig_diagnostic
|
||||||
from .commands import preconfig_version
|
from .commands import preconfig_version
|
||||||
from .output import deprecated_cmd
|
from .output import deprecated_cmd
|
||||||
from .plugins import EXPORT_FORMATS
|
|
||||||
from .plugins import IMPORT_FORMATS
|
|
||||||
from .plugins import util
|
from .plugins import util
|
||||||
|
from .plugins.collector import EXPORT_FORMATS
|
||||||
|
from .plugins.collector import IMPORT_FORMATS
|
||||||
|
|
||||||
|
|
||||||
class WrappingFormatter(argparse.RawTextHelpFormatter):
|
class WrappingFormatter(argparse.RawTextHelpFormatter):
|
||||||
|
|
47
jrnl/behave_testing.py
Normal file
47
jrnl/behave_testing.py
Normal 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
|
|
@ -28,6 +28,12 @@ def preconfig_diagnostic(_):
|
||||||
def preconfig_version(_):
|
def preconfig_version(_):
|
||||||
from jrnl import __title__
|
from jrnl import __title__
|
||||||
from jrnl import __version__
|
from jrnl import __version__
|
||||||
|
from jrnl.plugins.collector import (
|
||||||
|
IMPORT_FORMATS,
|
||||||
|
EXPORT_FORMATS,
|
||||||
|
get_exporter,
|
||||||
|
get_importer,
|
||||||
|
)
|
||||||
|
|
||||||
version_str = f"""{__title__} version {__version__}
|
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"""
|
conditions; for details, see: https://www.gnu.org/licenses/gpl-3.0.html"""
|
||||||
|
|
||||||
print(version_str)
|
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):
|
def postconfig_list(config, **kwargs):
|
||||||
|
@ -47,7 +69,7 @@ def postconfig_list(config, **kwargs):
|
||||||
|
|
||||||
def postconfig_import(args, config, **kwargs):
|
def postconfig_import(args, config, **kwargs):
|
||||||
from .Journal import open_journal
|
from .Journal import open_journal
|
||||||
from .plugins import get_importer
|
from .plugins.collector import get_importer
|
||||||
|
|
||||||
# Requires opening the journal
|
# Requires opening the journal
|
||||||
journal = open_journal(args.journal_name, config)
|
journal = open_journal(args.journal_name, config)
|
||||||
|
|
0
jrnl/contrib/exporter/.gitkeep
Normal file
0
jrnl/contrib/exporter/.gitkeep
Normal file
0
jrnl/contrib/importer/.gitkeep
Normal file
0
jrnl/contrib/importer/.gitkeep
Normal file
14
jrnl/jrnl.py
14
jrnl/jrnl.py
|
@ -324,19 +324,17 @@ def _delete_search_results(journal, old_entries, **kwargs):
|
||||||
|
|
||||||
def _display_search_results(args, journal, **kwargs):
|
def _display_search_results(args, journal, **kwargs):
|
||||||
if args.short or args.export == "short":
|
if args.short or args.export == "short":
|
||||||
print(journal.pprint(short=True))
|
print(plugins.collector.get_exporter("short").export(journal))
|
||||||
|
|
||||||
elif args.export == "pretty":
|
|
||||||
print(journal.pprint())
|
|
||||||
|
|
||||||
elif args.tags:
|
elif args.tags:
|
||||||
print(plugins.get_exporter("tags").export(journal))
|
print(plugins.collector.get_exporter("tags").export(journal))
|
||||||
|
|
||||||
elif args.export:
|
elif args.export:
|
||||||
exporter = plugins.get_exporter(args.export)
|
exporter = plugins.collector.get_exporter(args.export)
|
||||||
print(exporter.export(journal, args.filename))
|
print(exporter.export(journal, args.filename))
|
||||||
elif kwargs["config"].get("display_format"):
|
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))
|
print(exporter.export(journal, args.filename))
|
||||||
else:
|
else:
|
||||||
print(journal.pprint())
|
# print(journal.pprint())
|
||||||
|
print(plugins.collector.get_exporter("default").export(journal))
|
||||||
|
|
|
@ -1,48 +1,4 @@
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
# Copyright (C) 2012-2021 jrnl contributors
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# 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
|
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
# Copyright (C) 2012-2021 jrnl contributors
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base class for Importers and Exporters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
@ -10,16 +15,36 @@ from jrnl.color import ERROR_COLOR
|
||||||
from jrnl.color import RESET_COLOR
|
from jrnl.color import RESET_COLOR
|
||||||
|
|
||||||
|
|
||||||
class TextExporter:
|
class BaseImporter:
|
||||||
"""This Exporter can convert entries and journals into text files."""
|
"""Base Importer class (to sub-class)"""
|
||||||
|
|
||||||
names = ["text", "txt"]
|
# names = ["jrnl"]
|
||||||
extension = "txt"
|
# 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
|
@classmethod
|
||||||
def export_entry(cls, entry):
|
def export_entry(cls, entry):
|
||||||
"""Returns a string representation of a single entry."""
|
"""Returns a string representation of a single entry."""
|
||||||
return str(entry)
|
raise NotImplementedError
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
|
@ -32,9 +57,16 @@ class TextExporter:
|
||||||
try:
|
try:
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
f.write(cls.export_journal(journal))
|
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:
|
except IOError as e:
|
||||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
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
|
@classmethod
|
||||||
def make_filename(cls, entry):
|
def make_filename(cls, entry):
|
||||||
|
@ -45,6 +77,7 @@ class TextExporter:
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_files(cls, journal, path):
|
def write_files(cls, journal, path):
|
||||||
"""Exports a journal into individual files for each entry."""
|
"""Exports a journal into individual files for each entry."""
|
||||||
|
try:
|
||||||
for entry in journal.entries:
|
for entry in journal.entries:
|
||||||
try:
|
try:
|
||||||
full_path = os.path.join(path, cls.make_filename(entry))
|
full_path = os.path.join(path, cls.make_filename(entry))
|
||||||
|
@ -54,7 +87,13 @@ class TextExporter:
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(
|
return "[{2}ERROR{3}: {0} {1}]".format(
|
||||||
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
||||||
)
|
)
|
||||||
return "[Journal exported to {}]".format(path)
|
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):
|
def _slugify(string):
|
||||||
"""Slugifies a string.
|
"""Slugifies a string.
|
106
jrnl/plugins/collector.py
Normal file
106
jrnl/plugins/collector.py
Normal 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
|
|
@ -1,16 +1,20 @@
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
# Copyright (C) 2012-2021 jrnl contributors
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from collections import Counter
|
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."""
|
"""This Exporter lists dates and their respective counts, for heatingmapping etc."""
|
||||||
|
|
||||||
names = ["dates"]
|
names = ["dates"]
|
||||||
extension = "dates"
|
extension = "dates"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry):
|
def export_entry(cls, entry):
|
|
@ -2,17 +2,21 @@
|
||||||
# Copyright (C) 2012-2021 jrnl contributors
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
from jrnl.exception import JrnlError
|
|
||||||
from textwrap import TextWrapper
|
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."""
|
"""This Exporter can convert entries and journals into text with unicode box drawing characters."""
|
||||||
|
|
||||||
names = ["fancy", "boxed"]
|
names = ["fancy", "boxed"]
|
||||||
extension = "txt"
|
extension = "txt"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
# Top border of the card
|
# Top border of the card
|
||||||
border_a = "┎"
|
border_a = "┎"
|
||||||
|
@ -79,14 +83,3 @@ class FancyExporter(TextExporter):
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns a unicode representation of an entire journal."""
|
"""Returns a unicode representation of an entire journal."""
|
||||||
return "\n".join(cls.export_entry(entry) for entry in 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,
|
|
||||||
)
|
|
|
@ -4,15 +4,18 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .text_exporter import TextExporter
|
from jrnl.plugins.base import BaseExporter
|
||||||
from .util import get_tags_count
|
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."""
|
"""This Exporter can convert entries and journals into json."""
|
||||||
|
|
||||||
names = ["json"]
|
names = ["json"]
|
||||||
extension = "json"
|
extension = "json"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def entry_to_dict(cls, entry):
|
def entry_to_dict(cls, entry):
|
|
@ -8,15 +8,17 @@ import sys
|
||||||
|
|
||||||
from jrnl.color import RESET_COLOR
|
from jrnl.color import RESET_COLOR
|
||||||
from jrnl.color import WARNING_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."""
|
"""This Exporter can convert entries and journals into Markdown."""
|
||||||
|
|
||||||
names = ["md", "markdown"]
|
names = ["md", "markdown"]
|
||||||
extension = "md"
|
extension = "md"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry, to_multifile=True):
|
def export_entry(cls, entry, to_multifile=True):
|
20
jrnl/plugins/exporter/pretty.py
Normal file
20
jrnl/plugins/exporter/pretty.py
Normal 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()
|
20
jrnl/plugins/exporter/short.py
Normal file
20
jrnl/plugins/exporter/short.py
Normal 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)
|
|
@ -2,15 +2,19 @@
|
||||||
# Copyright (C) 2012-2021 jrnl contributors
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# 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."""
|
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
|
||||||
|
|
||||||
names = ["tags"]
|
names = ["tags"]
|
||||||
extension = "tags"
|
extension = "tags"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry):
|
def export_entry(cls, entry):
|
21
jrnl/plugins/exporter/text.py
Normal file
21
jrnl/plugins/exporter/text.py
Normal 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)
|
|
@ -4,30 +4,18 @@
|
||||||
|
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
from .json_exporter import JSONExporter
|
from jrnl.plugins.base import BaseExporter
|
||||||
from .util import get_tags_count
|
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."""
|
"""This Exporter can convert entries and journals into XML."""
|
||||||
|
|
||||||
names = ["xml"]
|
names = ["xml"]
|
||||||
extension = "xml"
|
extension = "xml"
|
||||||
|
version = __version__
|
||||||
@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
|
@classmethod
|
||||||
def entry_to_xml(cls, entry, doc):
|
def entry_to_xml(cls, entry, doc):
|
||||||
|
@ -44,6 +32,21 @@ class XMLExporter(JSONExporter):
|
||||||
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
||||||
return entry_el
|
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
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns an XML representation of an entire journal."""
|
"""Returns an XML representation of an entire journal."""
|
|
@ -9,15 +9,17 @@ import sys
|
||||||
from jrnl.color import ERROR_COLOR
|
from jrnl.color import ERROR_COLOR
|
||||||
from jrnl.color import RESET_COLOR
|
from jrnl.color import RESET_COLOR
|
||||||
from jrnl.color import WARNING_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."""
|
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
||||||
|
|
||||||
names = ["yaml"]
|
names = ["yaml"]
|
||||||
extension = "md"
|
extension = "md"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry, to_multifile=True):
|
def export_entry(cls, entry, to_multifile=True):
|
||||||
|
@ -132,9 +134,10 @@ class YAMLExporter(TextExporter):
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns an error, as YAML export requires a directory as a target."""
|
"""Returns an error, as YAML export requires a directory as a target."""
|
||||||
print(
|
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,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
return
|
raise NotImplementedError
|
|
@ -4,11 +4,16 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from jrnl.plugins.base import BaseImporter
|
||||||
|
|
||||||
class JRNLImporter:
|
from ... import __version__
|
||||||
|
|
||||||
|
|
||||||
|
class Importer(BaseImporter):
|
||||||
"""This plugin imports entries from other jrnl files."""
|
"""This plugin imports entries from other jrnl files."""
|
||||||
|
|
||||||
names = ["jrnl"]
|
names = ["jrnl"]
|
||||||
|
version = __version__
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def import_(journal, input=None):
|
def import_(journal, input=None):
|
||||||
|
@ -27,7 +32,9 @@ class JRNLImporter:
|
||||||
journal.import_(other_journal_txt)
|
journal.import_(other_journal_txt)
|
||||||
new_cnt = len(journal.entries)
|
new_cnt = len(journal.entries)
|
||||||
print(
|
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,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
journal.write()
|
journal.write()
|
|
@ -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.SafeLoader)
|
|
||||||
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)
|
|
|
@ -1,43 +0,0 @@
|
||||||
# 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))
|
|
|
@ -2,11 +2,13 @@
|
||||||
# Copyright (C) 2012-2021 jrnl contributors
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
from jrnl.exception import JrnlError
|
||||||
|
|
||||||
|
|
||||||
def get_tags_count(journal):
|
def get_tags_count(journal):
|
||||||
"""Returns a set of tuples (count, tag) for all tags present in the 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
|
# 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)]
|
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]
|
# 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}
|
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||||
|
@ -24,3 +26,14 @@ def oxford_list(lst):
|
||||||
return lst[0] + " or " + lst[1]
|
return lst[0] + " or " + lst[1]
|
||||||
else:
|
else:
|
||||||
return ", ".join(lst[:-1]) + ", or " + lst[-1]
|
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,
|
||||||
|
)
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -12,6 +12,9 @@ extra_css:
|
||||||
- assets/highlight.css
|
- assets/highlight.css
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition
|
- admonition
|
||||||
|
plugins:
|
||||||
|
- include-markdown
|
||||||
|
- search
|
||||||
repo_url: https://github.com/jrnl-org/jrnl/
|
repo_url: https://github.com/jrnl-org/jrnl/
|
||||||
edit_uri: edit/develop/docs/
|
edit_uri: edit/develop/docs/
|
||||||
site_author: jrnl contributors
|
site_author: jrnl contributors
|
||||||
|
@ -24,4 +27,5 @@ nav:
|
||||||
- Privacy and Security: privacy-and-security.md
|
- Privacy and Security: privacy-and-security.md
|
||||||
- Formats: formats.md
|
- Formats: formats.md
|
||||||
- Advanced Usage: advanced.md
|
- Advanced Usage: advanced.md
|
||||||
|
- Custom Importers & Exporters: plugins.md
|
||||||
- Recipes: recipes.md
|
- Recipes: recipes.md
|
||||||
|
|
16
poetry.lock
generated
16
poetry.lock
generated
|
@ -447,6 +447,18 @@ watchdog = ">=2.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
i18n = ["babel (>=2.9.0)"]
|
i18n = ["babel (>=2.9.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mkdocs-include-markdown-plugin"
|
||||||
|
version = "2.8.0"
|
||||||
|
description = "Mkdocs Markdown includer plugin."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["bump2version (==1.0.1)", "flake8 (==3.8.4)", "flake8-implicit-str-concat (==0.2.0)", "flake8-print (==4.0.0)", "isort (==5.6.4)", "pre-commit (==2.9.2)", "pytest (==6.1.2)", "pytest-cov (==2.10.1)", "pyupgrade (==2.9.0)", "yamllint (==1.25.0)"]
|
||||||
|
test = ["pytest (==6.1.2)", "pytest-cov (==2.10.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy-extensions"
|
name = "mypy-extensions"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
@ -1139,6 +1151,10 @@ mkdocs = [
|
||||||
{file = "mkdocs-1.2.1-py3-none-any.whl", hash = "sha256:11141126e5896dd9d279b3e4814eb488e409a0990fb638856255020406a8e2e7"},
|
{file = "mkdocs-1.2.1-py3-none-any.whl", hash = "sha256:11141126e5896dd9d279b3e4814eb488e409a0990fb638856255020406a8e2e7"},
|
||||||
{file = "mkdocs-1.2.1.tar.gz", hash = "sha256:6e0ea175366e3a50d334597b0bc042b8cebd512398cdd3f6f34842d0ef524905"},
|
{file = "mkdocs-1.2.1.tar.gz", hash = "sha256:6e0ea175366e3a50d334597b0bc042b8cebd512398cdd3f6f34842d0ef524905"},
|
||||||
]
|
]
|
||||||
|
mkdocs-include-markdown-plugin = [
|
||||||
|
{file = "mkdocs_include_markdown_plugin-2.8.0-py3-none-any.whl", hash = "sha256:29b7d40da2945414f4dcb4c39eac004da6a644433f10d9da0dd5e331e50a5dbf"},
|
||||||
|
{file = "mkdocs_include_markdown_plugin-2.8.0.tar.gz", hash = "sha256:a4171b1f8a5cb4e2e05f2989ca47f4825ed0723021af7a3a871f8abe7cb91ba0"},
|
||||||
|
]
|
||||||
mypy-extensions = [
|
mypy-extensions = [
|
||||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||||
|
|
9
tests/external_plugins_src/README.md
Normal file
9
tests/external_plugins_src/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Rot13 Custom Exporter for Jrnl
|
||||||
|
|
||||||
|
This is a custom exporter to demostrate how to write customer exporters for
|
||||||
|
[jrnl](https://github.com/jrnl-org/jrnl). It is also used by *jrnl* in its
|
||||||
|
tests to ensure the feature works as expected.
|
||||||
|
|
||||||
|
This plugin applies a [Caeser
|
||||||
|
cipher](https://en.wikipedia.org/wiki/Caesar_cipher) (specifically the
|
||||||
|
[ROT13](https://en.wikipedia.org/wiki/ROT13)) to output text.
|
|
@ -0,0 +1,37 @@
|
||||||
|
# pelican\contrib\exporter\custom_json.py
|
||||||
|
import json
|
||||||
|
|
||||||
|
from jrnl.plugins.base import BaseExporter
|
||||||
|
|
||||||
|
__version__ = "v1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
class Exporter(BaseExporter):
|
||||||
|
"""
|
||||||
|
This basic Exporter can convert entries and journals into JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
|
names = ["json"]
|
||||||
|
extension = "json"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def entry_to_dict(cls, entry):
|
||||||
|
return {
|
||||||
|
"title": entry.title,
|
||||||
|
"body": entry.body,
|
||||||
|
"date": entry.date.strftime("%Y-%m-%d"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_entry(cls, entry):
|
||||||
|
"""Returns a json representation of a single entry."""
|
||||||
|
return json.dumps(cls.entry_to_dict(entry), indent=2) + "\n"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_journal(cls, journal):
|
||||||
|
"""Returns a json representation of an entire journal."""
|
||||||
|
result = {
|
||||||
|
"entries": [cls.entry_to_dict(e) for e in journal.entries],
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2)
|
25
tests/external_plugins_src/jrnl/contrib/exporter/flag.py
Normal file
25
tests/external_plugins_src/jrnl/contrib/exporter/flag.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
# Copyright (C) 2012-2021 jrnl contributors
|
||||||
|
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
|
"""
|
||||||
|
Exporter for testing and experimentation purposes.
|
||||||
|
|
||||||
|
It is not called "testing" because then it's not installed.
|
||||||
|
|
||||||
|
The presence of this plugin is also used as a "switch" by the test suite to
|
||||||
|
decide on whether or not to run the "vanilla" test suite, or the test suite
|
||||||
|
for external plugins.
|
||||||
|
|
||||||
|
The `export_entry` and `export_journal` methods are both purposely not
|
||||||
|
implemented to confirm behavior on plugins that don't implement them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from jrnl.plugins.base import BaseExporter
|
||||||
|
|
||||||
|
|
||||||
|
class Exporter(BaseExporter):
|
||||||
|
names = ["testing", "test"]
|
||||||
|
version = "v0.0.1"
|
||||||
|
extension = "test"
|
15
tests/external_plugins_src/jrnl/contrib/exporter/rot13.py
Normal file
15
tests/external_plugins_src/jrnl/contrib/exporter/rot13.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
from jrnl.plugins.base import BaseExporter
|
||||||
|
|
||||||
|
__version__ = "v1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
class Exporter(BaseExporter):
|
||||||
|
names = ["rot13", "txt"]
|
||||||
|
extension = "txt"
|
||||||
|
version = __version__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_entry(cls, entry):
|
||||||
|
return codecs.encode(str(entry), "rot_13")
|
|
@ -0,0 +1,50 @@
|
||||||
|
# pelican\contrib\importer\sample_json.py
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from jrnl import Entry
|
||||||
|
from jrnl.plugins.base import BaseImporter
|
||||||
|
|
||||||
|
__version__ = "v1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
class Importer(BaseImporter):
|
||||||
|
"""JSON Importer for jrnl."""
|
||||||
|
|
||||||
|
names = ["json"]
|
||||||
|
version = __version__
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def import_(journal, input=None):
|
||||||
|
"""
|
||||||
|
Given a nicely formatted JSON file, will add the
|
||||||
|
contained Entries to the journal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
old_cnt = len(journal.entries)
|
||||||
|
if input:
|
||||||
|
with open(input, "r", encoding="utf-8") as f:
|
||||||
|
data = json.loads(f)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data = sys.stdin.read()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(
|
||||||
|
"[Entries NOT imported into journal.]",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
for json_entry in data:
|
||||||
|
raw = json_entry["title"] + "/n" + json_entry["body"]
|
||||||
|
date = json_entry["date"]
|
||||||
|
entry = Entry.Entry(journal, date, raw)
|
||||||
|
journal.entries.append(entry)
|
||||||
|
|
||||||
|
new_cnt = len(journal.entries)
|
||||||
|
print(
|
||||||
|
"[{} entries imported to '{}' journal]".format(
|
||||||
|
new_cnt - old_cnt, journal.name
|
||||||
|
),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
35
tests/external_plugins_src/setup.py
Normal file
35
tests/external_plugins_src/setup.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
try:
|
||||||
|
from setuptools import setup
|
||||||
|
except ImportError:
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(filename="jrnl/contrib/exporter/rot13.py"):
|
||||||
|
with open(os.path.join(base_dir, filename), encoding="utf-8") as initfile:
|
||||||
|
for line in initfile.readlines():
|
||||||
|
m = re.match("__version__ *= *['\"](.*)['\"]", line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="jrnl-demo-plugins",
|
||||||
|
version=get_version(),
|
||||||
|
description="Demonstration custom plugins for jrnl",
|
||||||
|
long_description="\n\n".join([open(os.path.join(base_dir, "README.md")).read()]),
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
author="W. Minchin",
|
||||||
|
author_email="w_minchin@hotmail.com",
|
||||||
|
url="https://github.com/jrnl-org/jrnl/tree/develop/tests/external_plugins_src",
|
||||||
|
packages=["jrnl", "jrnl.contrib", "jrnl.contrib.exporter", "jrnl.contrib.importer"],
|
||||||
|
include_package_data=True,
|
||||||
|
install_requires=[
|
||||||
|
"jrnl",
|
||||||
|
],
|
||||||
|
zip_safe=False, # use wheels instead
|
||||||
|
)
|
46
tests/test_plugin.py
Normal file
46
tests/test_plugin.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from datetime import date
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from jrnl import Entry
|
||||||
|
from jrnl import Journal
|
||||||
|
from jrnl.plugins.exporter import json as json_exporter
|
||||||
|
|
||||||
|
try:
|
||||||
|
from jrnl.contrib.exporter import testing as testing_exporter
|
||||||
|
except:
|
||||||
|
testing_exporter = None
|
||||||
|
|
||||||
|
|
||||||
|
if testing_exporter:
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def create_entry():
|
||||||
|
entry = Entry.Entry(
|
||||||
|
journal=Journal.Journal(),
|
||||||
|
text="This is the entry text",
|
||||||
|
date=date(year=2001, month=1, day=1),
|
||||||
|
starred=True,
|
||||||
|
)
|
||||||
|
yield entry
|
||||||
|
|
||||||
|
class TestBaseExporter(testing_exporter.Exporter):
|
||||||
|
def test_unimplemented_export(self, create_entry):
|
||||||
|
entry = create_entry
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
self.export_entry(entry)
|
||||||
|
|
||||||
|
class TestJsonExporter(json_exporter.Exporter):
|
||||||
|
def test_json_exporter_name(self):
|
||||||
|
assert "json" in self.names
|
||||||
|
|
||||||
|
def test_export_entry(self, create_entry):
|
||||||
|
entry = create_entry
|
||||||
|
fake_uuid = "ewqf09-432p9p0433-243209" # generated by mashing keys
|
||||||
|
entry.uuid = fake_uuid
|
||||||
|
exported = self.export_entry(entry)
|
||||||
|
deserialized_export = json.loads(exported)
|
||||||
|
assert deserialized_export["title"] == "This is the entry text"
|
||||||
|
assert deserialized_export["date"] == "2001-01-01"
|
||||||
|
assert "uuid" in deserialized_export.keys()
|
|
@ -9,7 +9,7 @@ from jrnl.jrnl import _display_search_results
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
# see: https://github.com/psf/black/issues/664
|
# see: https://github.com/psf/black/issues/664
|
||||||
@pytest.mark.parametrize("export_format", [ "pretty", "short","markdown"])
|
@pytest.mark.parametrize("export_format", [ "pretty", "short", "markdown"])
|
||||||
#fmt: on
|
#fmt: on
|
||||||
@mock.patch.object(argparse, "Namespace", return_value={"export": "markdown", "filename": "irrele.vant"})
|
@mock.patch.object(argparse, "Namespace", return_value={"export": "markdown", "filename": "irrele.vant"})
|
||||||
def test_export_format(mock_args, export_format):
|
def test_export_format(mock_args, export_format):
|
||||||
|
|
Loading…
Add table
Reference in a new issue