mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
remove py2 remnants and use mocks in tests
fstring wip Run pyupgrade fix broken pyupgrade fstring run pyupgrade on plugin dir fixup! remove py2 remnants and use mocks in tests small print bugfix The file=sys.stderr was part of the format(), so an error got printed to stdout Drop use of codecs package Use builtins.open() instead fixup! remove py2 remnants and use mocks in tests
This commit is contained in:
parent
b7e2e91af3
commit
9d8d6a83ae
28 changed files with 211 additions and 321 deletions
|
@ -2,7 +2,7 @@
|
||||||
Scenario: Loading an encrypted journal
|
Scenario: Loading an encrypted journal
|
||||||
Given we use the config "encrypted.yaml"
|
Given we use the config "encrypted.yaml"
|
||||||
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
|
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
|
||||||
Then we should see the message "Password"
|
Then the output should contain "Password"
|
||||||
and the output should contain "2013-06-10 15:40 Life is good"
|
and the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
||||||
Scenario: Decrypting a journal
|
Scenario: Decrypting a journal
|
||||||
|
@ -14,16 +14,16 @@
|
||||||
|
|
||||||
Scenario: Encrypting a journal
|
Scenario: Encrypting a journal
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl --encrypt" and enter "swordfish"
|
When we run "jrnl --encrypt" and enter "swordfish" and "n"
|
||||||
Then we should see the message "Journal encrypted"
|
Then we should see the message "Journal encrypted"
|
||||||
and the config for journal "default" should have "encrypt" set to "bool:True"
|
and the config for journal "default" should have "encrypt" set to "bool:True"
|
||||||
When we run "jrnl -n 1" and enter "swordfish"
|
When we run "jrnl -n 1" and enter "swordfish"
|
||||||
Then we should see the message "Password"
|
Then the output should contain "Password"
|
||||||
and the output should contain "2013-06-10 15:40 Life is good"
|
and the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
||||||
Scenario: Storing a password in Keychain
|
Scenario: Storing a password in Keychain
|
||||||
Given we use the config "multiple.yaml"
|
Given we use the config "multiple.yaml"
|
||||||
When we run "jrnl simple --encrypt" and enter "sabertooth"
|
When we run "jrnl simple --encrypt" and enter "sabertooth" and "y"
|
||||||
When we set the keychain password of "simple" to "sabertooth"
|
When we set the keychain password of "simple" to "sabertooth"
|
||||||
Then the config for journal "simple" should have "encrypt" set to "bool:True"
|
Then the config for journal "simple" should have "encrypt" set to "bool:True"
|
||||||
When we run "jrnl simple -n 1"
|
When we run "jrnl simple -n 1"
|
||||||
|
|
|
@ -1,25 +1,15 @@
|
||||||
from behave import *
|
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
import jrnl
|
|
||||||
try:
|
|
||||||
from io import StringIO
|
|
||||||
except ImportError:
|
|
||||||
from cStringIO import StringIO
|
|
||||||
|
|
||||||
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."""
|
||||||
context.messages = StringIO()
|
|
||||||
jrnl.util.STDERR = context.messages
|
|
||||||
jrnl.util.TEST = True
|
|
||||||
|
|
||||||
# Clean up in case something went wrong
|
# Clean up in case something went wrong
|
||||||
for folder in ("configs", "journals"):
|
for folder in ("configs", "journals"):
|
||||||
working_dir = os.path.join("features", folder)
|
working_dir = os.path.join("features", folder)
|
||||||
if os.path.exists(working_dir):
|
if os.path.exists(working_dir):
|
||||||
shutil.rmtree(working_dir)
|
shutil.rmtree(working_dir)
|
||||||
|
|
||||||
|
|
||||||
for folder in ("configs", "journals"):
|
for folder in ("configs", "journals"):
|
||||||
original = os.path.join("features", "data", folder)
|
original = os.path.join("features", "data", folder)
|
||||||
working_dir = os.path.join("features", folder)
|
working_dir = os.path.join("features", folder)
|
||||||
|
@ -32,10 +22,9 @@ def before_scenario(context, scenario):
|
||||||
else:
|
else:
|
||||||
shutil.copy2(source, working_dir)
|
shutil.copy2(source, working_dir)
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
context.messages.close()
|
|
||||||
context.messages = None
|
|
||||||
for folder in ("configs", "journals"):
|
for folder in ("configs", "journals"):
|
||||||
working_dir = os.path.join("features", folder)
|
working_dir = os.path.join("features", folder)
|
||||||
if os.path.exists(working_dir):
|
if os.path.exists(working_dir):
|
||||||
|
|
|
@ -42,5 +42,5 @@ Feature: Multiple journals
|
||||||
|
|
||||||
Scenario: Don't crash if no file exists for a configured encrypted journal
|
Scenario: Don't crash if no file exists for a configured encrypted journal
|
||||||
Given we use the config "multiple.yaml"
|
Given we use the config "multiple.yaml"
|
||||||
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes"
|
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes" and "y"
|
||||||
Then we should see the message "Journal 'new_encrypted' created"
|
Then we should see the message "Journal 'new_encrypted' created"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from __future__ import unicode_literals
|
from unittest.mock import patch
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from behave import given, when, then
|
from behave import given, when, then
|
||||||
from jrnl import cli, install, Journal, util, plugins
|
from jrnl import cli, install, Journal, util, plugins
|
||||||
|
@ -10,10 +9,13 @@ import os
|
||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
import keyring
|
import keyring
|
||||||
|
import tzlocal
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class TestKeyring(keyring.backend.KeyringBackend):
|
class TestKeyring(keyring.backend.KeyringBackend):
|
||||||
"""A test keyring that just stores its valies in a hash"""
|
"""A test keyring that just stores its values in a hash"""
|
||||||
|
|
||||||
priority = 1
|
priority = 1
|
||||||
keys = defaultdict(dict)
|
keys = defaultdict(dict)
|
||||||
|
@ -31,15 +33,6 @@ class TestKeyring(keyring.backend.KeyringBackend):
|
||||||
keyring.set_keyring(TestKeyring())
|
keyring.set_keyring(TestKeyring())
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from io import StringIO
|
|
||||||
except ImportError:
|
|
||||||
from cStringIO import StringIO
|
|
||||||
import tzlocal
|
|
||||||
import shlex
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def ushlex(command):
|
def ushlex(command):
|
||||||
if sys.version_info[0] == 3:
|
if sys.version_info[0] == 3:
|
||||||
return shlex.split(command)
|
return shlex.split(command)
|
||||||
|
@ -73,19 +66,42 @@ def set_config(context, config_file):
|
||||||
cf.write("version: {}".format(__version__))
|
cf.write("version: {}".format(__version__))
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_getpass(inputs):
|
||||||
|
def prompt_return(prompt="Password: "):
|
||||||
|
print(prompt)
|
||||||
|
return next(inputs)
|
||||||
|
return prompt_return
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_input(inputs):
|
||||||
|
def prompt_return(prompt=""):
|
||||||
|
val = next(inputs)
|
||||||
|
print(prompt, val)
|
||||||
|
return val
|
||||||
|
return prompt_return
|
||||||
|
|
||||||
|
|
||||||
@when('we run "{command}" and enter')
|
@when('we run "{command}" and enter')
|
||||||
@when('we run "{command}" and enter "{inputs}"')
|
@when('we run "{command}" and enter "{inputs1}"')
|
||||||
def run_with_input(context, command, inputs=None):
|
@when('we run "{command}" and enter "{inputs1}" and "{inputs2}"')
|
||||||
text = inputs or context.text
|
def run_with_input(context, command, inputs1="", inputs2=""):
|
||||||
|
# create an iterator through all inputs. These inputs will be fed one by one
|
||||||
|
# to the mocked calls for 'input()', 'util.getpass()' and 'sys.stdin.read()'
|
||||||
|
text = iter((inputs1, inputs2)) if inputs1 else iter(context.text.split("\n"))
|
||||||
args = ushlex(command)[1:]
|
args = ushlex(command)[1:]
|
||||||
buffer = StringIO(text.strip())
|
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input:
|
||||||
util.STDIN = buffer
|
with patch("jrnl.util.getpass", side_effect=_mock_getpass(text)) as mock_getpass:
|
||||||
|
with patch("sys.stdin.read", side_effect=text) as mock_read:
|
||||||
try:
|
try:
|
||||||
cli.run(args or [])
|
cli.run(args or [])
|
||||||
context.exit_status = 0
|
context.exit_status = 0
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
context.exit_status = e.code
|
context.exit_status = e.code
|
||||||
|
|
||||||
|
# assert at least one of the mocked input methods got called
|
||||||
|
assert mock_input.called or mock_getpass.called or mock_read.called
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@when('we run "{command}"')
|
@when('we run "{command}"')
|
||||||
def run(context, command):
|
def run(context, command):
|
||||||
|
@ -190,28 +206,24 @@ def check_output_time_inline(context, text):
|
||||||
def check_output_inline(context, text=None):
|
def check_output_inline(context, text=None):
|
||||||
text = text or context.text
|
text = text or context.text
|
||||||
out = context.stdout_capture.getvalue()
|
out = context.stdout_capture.getvalue()
|
||||||
if isinstance(out, bytes):
|
|
||||||
out = out.decode('utf-8')
|
|
||||||
assert text in out, text
|
assert text in out, text
|
||||||
|
|
||||||
|
|
||||||
@then('the output should not contain "{text}"')
|
@then('the output should not contain "{text}"')
|
||||||
def check_output_not_inline(context, text):
|
def check_output_not_inline(context, text):
|
||||||
out = context.stdout_capture.getvalue()
|
out = context.stdout_capture.getvalue()
|
||||||
if isinstance(out, bytes):
|
|
||||||
out = out.decode('utf-8')
|
|
||||||
assert text not in out
|
assert text not in out
|
||||||
|
|
||||||
|
|
||||||
@then('we should see the message "{text}"')
|
@then('we should see the message "{text}"')
|
||||||
def check_message(context, text):
|
def check_message(context, text):
|
||||||
out = context.messages.getvalue()
|
out = context.stderr_capture.getvalue()
|
||||||
assert text in out, [text, out]
|
assert text in out, [text, out]
|
||||||
|
|
||||||
|
|
||||||
@then('we should not see the message "{text}"')
|
@then('we should not see the message "{text}"')
|
||||||
def check_not_message(context, text):
|
def check_not_message(context, text):
|
||||||
out = context.messages.getvalue()
|
out = context.stderr_capture.getvalue()
|
||||||
assert text not in out, [text, out]
|
assert text not in out, [text, out]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,5 +19,5 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
|
||||||
bad doggie no biscuit
|
bad doggie no biscuit
|
||||||
bad doggie no biscuit
|
bad doggie no biscuit
|
||||||
"""
|
"""
|
||||||
Then we should see the message "Password"
|
Then the output should contain "Password"
|
||||||
and the output should contain "2013-06-10 15:40 Life is good"
|
and the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from . import Entry
|
from . import Entry
|
||||||
from . import Journal
|
from . import Journal
|
||||||
from . import time as jrnl_time
|
from . import time as jrnl_time
|
||||||
|
@ -26,7 +24,7 @@ class DayOne(Journal.Journal):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.entries = []
|
self.entries = []
|
||||||
self._deleted_entries = []
|
self._deleted_entries = []
|
||||||
super(DayOne, self).__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
|
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
|
||||||
|
@ -83,7 +81,7 @@ class DayOne(Journal.Journal):
|
||||||
def editable_str(self):
|
def editable_str(self):
|
||||||
"""Turns the journal into a string of entries that can be edited
|
"""Turns the journal into a string of entries that can be edited
|
||||||
manually and later be parsed with eslf.parse_editable_str."""
|
manually and later be parsed with eslf.parse_editable_str."""
|
||||||
return "\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries])
|
return "\n".join([f"# {e.uuid}\n{str(e)}" for e in self.entries])
|
||||||
|
|
||||||
def parse_editable_str(self, edited):
|
def parse_editable_str(self, edited):
|
||||||
"""Parses the output of self.editable_str and updates its entries."""
|
"""Parses the output of self.editable_str and updates its entries."""
|
||||||
|
@ -107,7 +105,7 @@ class DayOne(Journal.Journal):
|
||||||
current_entry.modified = False
|
current_entry.modified = False
|
||||||
current_entry.uuid = m.group(1).lower()
|
current_entry.uuid = m.group(1).lower()
|
||||||
else:
|
else:
|
||||||
date_blob_re = re.compile("^\[[^\\]]+\] ")
|
date_blob_re = re.compile("^\\[[^\\]]+\\] ")
|
||||||
date_blob = date_blob_re.findall(line)
|
date_blob = date_blob_re.findall(line)
|
||||||
if date_blob:
|
if date_blob:
|
||||||
date_blob = date_blob[0]
|
date_blob = date_blob[0]
|
||||||
|
|
|
@ -8,14 +8,13 @@ from cryptography.hazmat.backends import default_backend
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import base64
|
import base64
|
||||||
import getpass
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
def make_key(password):
|
def make_key(password):
|
||||||
password = util.bytes(password)
|
password = password.encode("utf-8")
|
||||||
kdf = PBKDF2HMAC(
|
kdf = PBKDF2HMAC(
|
||||||
algorithm=hashes.SHA256(),
|
algorithm=hashes.SHA256(),
|
||||||
length=32,
|
length=32,
|
||||||
|
@ -30,7 +29,7 @@ def make_key(password):
|
||||||
|
|
||||||
class EncryptedJournal(Journal.Journal):
|
class EncryptedJournal(Journal.Journal):
|
||||||
def __init__(self, name='default', **kwargs):
|
def __init__(self, name='default', **kwargs):
|
||||||
super(EncryptedJournal, self).__init__(name, **kwargs)
|
super().__init__(name, **kwargs)
|
||||||
self.config['encrypt'] = True
|
self.config['encrypt'] = True
|
||||||
|
|
||||||
def open(self, filename=None):
|
def open(self, filename=None):
|
||||||
|
@ -48,9 +47,9 @@ class EncryptedJournal(Journal.Journal):
|
||||||
self.config['password'] = password
|
self.config['password'] = password
|
||||||
text = ""
|
text = ""
|
||||||
self._store(filename, text)
|
self._store(filename, text)
|
||||||
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
|
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
util.prompt("No password supplied for encrypted journal")
|
print("No password supplied for encrypted journal", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
text = self._load(filename)
|
text = self._load(filename)
|
||||||
|
@ -59,7 +58,6 @@ class EncryptedJournal(Journal.Journal):
|
||||||
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
def _load(self, filename, password=None):
|
def _load(self, filename, password=None):
|
||||||
"""Loads an encrypted journal from a file and tries to decrypt it.
|
"""Loads an encrypted journal from a file and tries to decrypt it.
|
||||||
If password is not provided, will look for password in the keychain
|
If password is not provided, will look for password in the keychain
|
||||||
|
@ -99,7 +97,7 @@ class LegacyEncryptedJournal(Journal.LegacyJournal):
|
||||||
"""Legacy class to support opening journals encrypted with the jrnl 1.x
|
"""Legacy class to support opening journals encrypted with the jrnl 1.x
|
||||||
standard. You'll not be able to save these journals anymore."""
|
standard. You'll not be able to save these journals anymore."""
|
||||||
def __init__(self, name='default', **kwargs):
|
def __init__(self, name='default', **kwargs):
|
||||||
super(LegacyEncryptedJournal, self).__init__(name, **kwargs)
|
super().__init__(name, **kwargs)
|
||||||
self.config['encrypt'] = True
|
self.config['encrypt'] = True
|
||||||
|
|
||||||
def _load(self, filename, password=None):
|
def _load(self, filename, password=None):
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -51,14 +49,14 @@ class Entry:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tag_regex(tagsymbols):
|
def tag_regex(tagsymbols):
|
||||||
pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
|
pattern = fr'(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)'
|
||||||
return re.compile(pattern, re.UNICODE)
|
return re.compile(pattern)
|
||||||
|
|
||||||
def _parse_tags(self):
|
def _parse_tags(self):
|
||||||
tagsymbols = self.journal.config['tagsymbols']
|
tagsymbols = self.journal.config['tagsymbols']
|
||||||
return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text))
|
return {tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)}
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
"""Returns a string representation of the entry to be written into a journal file."""
|
"""Returns a string representation of the entry to be written into a journal file."""
|
||||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||||
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
||||||
|
@ -106,7 +104,7 @@ class Entry:
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
return "<Entry '{}' on {}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.__repr__())
|
return hash(self.__repr__())
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from . import Entry
|
from . import Entry
|
||||||
from . import util
|
from . import util
|
||||||
from . import time
|
from . import time
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import codecs
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
@ -15,7 +12,7 @@ import logging
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Tag(object):
|
class Tag:
|
||||||
def __init__(self, name, count=0):
|
def __init__(self, name, count=0):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.count = count
|
self.count = count
|
||||||
|
@ -24,10 +21,10 @@ class Tag(object):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Tag '{}'>".format(self.name)
|
return f"<Tag '{self.name}'>"
|
||||||
|
|
||||||
|
|
||||||
class Journal(object):
|
class Journal:
|
||||||
def __init__(self, name='default', **kwargs):
|
def __init__(self, name='default', **kwargs):
|
||||||
self.config = {
|
self.config = {
|
||||||
'journal': "journal.txt",
|
'journal': "journal.txt",
|
||||||
|
@ -72,7 +69,7 @@ class Journal(object):
|
||||||
filename = filename or self.config['journal']
|
filename = filename or self.config['journal']
|
||||||
|
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
|
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
||||||
self._create(filename)
|
self._create(filename)
|
||||||
|
|
||||||
text = self._load(filename)
|
text = self._load(filename)
|
||||||
|
@ -96,7 +93,7 @@ class Journal(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _to_text(self):
|
def _to_text(self):
|
||||||
return "\n".join([e.__unicode__() for e in self.entries])
|
return "\n".join([str(e) for e in self.entries])
|
||||||
|
|
||||||
def _load(self, filename):
|
def _load(self, filename):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -118,7 +115,7 @@ class Journal(object):
|
||||||
# Initialise our current entry
|
# Initialise our current entry
|
||||||
entries = []
|
entries = []
|
||||||
|
|
||||||
date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ")
|
date_blob_re = re.compile("(?:^|\n)\\[([^\\]]+)\\] ")
|
||||||
last_entry_pos = 0
|
last_entry_pos = 0
|
||||||
for match in date_blob_re.finditer(journal_txt):
|
for match in date_blob_re.finditer(journal_txt):
|
||||||
date_blob = match.groups()[0]
|
date_blob = match.groups()[0]
|
||||||
|
@ -140,9 +137,6 @@ class Journal(object):
|
||||||
entry._parse_text()
|
entry._parse_text()
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return self.pprint()
|
|
||||||
|
|
||||||
def pprint(self, short=False):
|
def pprint(self, short=False):
|
||||||
"""Prettyprints the journal's entries"""
|
"""Prettyprints the journal's entries"""
|
||||||
sep = "\n"
|
sep = "\n"
|
||||||
|
@ -153,7 +147,7 @@ class Journal(object):
|
||||||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||||
pp = re.sub(tagre,
|
pp = re.sub(tagre,
|
||||||
lambda match: util.colorize(match.group(0)),
|
lambda match: util.colorize(match.group(0)),
|
||||||
pp, re.UNICODE)
|
pp)
|
||||||
else:
|
else:
|
||||||
pp = re.sub(
|
pp = re.sub(
|
||||||
Entry.Entry.tag_regex(self.config['tagsymbols']),
|
Entry.Entry.tag_regex(self.config['tagsymbols']),
|
||||||
|
@ -162,8 +156,11 @@ class Journal(object):
|
||||||
)
|
)
|
||||||
return pp
|
return pp
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.pprint()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Journal with {0} entries>".format(len(self.entries))
|
return f"<Journal with {len(self.entries)} entries>"
|
||||||
|
|
||||||
def sort(self):
|
def sort(self):
|
||||||
"""Sorts the Journal's entries by date"""
|
"""Sorts the Journal's entries by date"""
|
||||||
|
@ -183,7 +180,7 @@ class Journal(object):
|
||||||
for entry in self.entries
|
for entry in self.entries
|
||||||
for tag in set(entry.tags)]
|
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 = set([(tags.count(tag), tag) for tag in tags])
|
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||||
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)]
|
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)]
|
||||||
|
|
||||||
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, exclude=[]):
|
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, exclude=[]):
|
||||||
|
@ -200,8 +197,8 @@ class Journal(object):
|
||||||
|
|
||||||
exclude is a list of the tags which should not appear in the results.
|
exclude is a list of the tags which should not appear in the results.
|
||||||
entry is kept if any tag is present, unless they appear in exclude."""
|
entry is kept if any tag is present, unless they appear in exclude."""
|
||||||
self.search_tags = set([tag.lower() for tag in tags])
|
self.search_tags = {tag.lower() for tag in tags}
|
||||||
excluded_tags = set([tag.lower() for tag in exclude])
|
excluded_tags = {tag.lower() for tag in exclude}
|
||||||
end_date = time.parse(end_date, inclusive=True)
|
end_date = time.parse(end_date, inclusive=True)
|
||||||
start_date = time.parse(start_date)
|
start_date = time.parse(start_date)
|
||||||
|
|
||||||
|
@ -226,7 +223,7 @@ class Journal(object):
|
||||||
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
|
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
|
||||||
starred = False
|
starred = False
|
||||||
# Split raw text into title and body
|
# Split raw text into title and body
|
||||||
sep = re.search("\n|[\?!.]+ +\n?", raw)
|
sep = re.search(r"\n|[?!.]+ +\n?", raw)
|
||||||
first_line = raw[:sep.end()].strip() if sep else raw
|
first_line = raw[:sep.end()].strip() if sep else raw
|
||||||
starred = False
|
starred = False
|
||||||
|
|
||||||
|
@ -254,7 +251,7 @@ class Journal(object):
|
||||||
def editable_str(self):
|
def editable_str(self):
|
||||||
"""Turns the journal into a string of entries that can be edited
|
"""Turns the journal into a string of entries that can be edited
|
||||||
manually and later be parsed with eslf.parse_editable_str."""
|
manually and later be parsed with eslf.parse_editable_str."""
|
||||||
return "\n".join([e.__unicode__() for e in self.entries])
|
return "\n".join([str(e) for e in self.entries])
|
||||||
|
|
||||||
def parse_editable_str(self, edited):
|
def parse_editable_str(self, edited):
|
||||||
"""Parses the output of self.editable_str and updates it's entries."""
|
"""Parses the output of self.editable_str and updates it's entries."""
|
||||||
|
@ -270,15 +267,15 @@ class Journal(object):
|
||||||
class PlainJournal(Journal):
|
class PlainJournal(Journal):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create(cls, filename):
|
def _create(cls, filename):
|
||||||
with codecs.open(filename, "a", "utf-8"):
|
with open(filename, "a", encoding="utf-8"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _load(self, filename):
|
def _load(self, filename):
|
||||||
with codecs.open(filename, "r", "utf-8") as f:
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
def _store(self, filename, text):
|
def _store(self, filename, text):
|
||||||
with codecs.open(filename, 'w', "utf-8") as f:
|
with open(filename, 'w', encoding="utf-8") as f:
|
||||||
f.write(text)
|
f.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
@ -287,7 +284,7 @@ class LegacyJournal(Journal):
|
||||||
standard. Main difference here is that in 1.x, timestamps were not cuddled
|
standard. Main difference here is that in 1.x, timestamps were not cuddled
|
||||||
by square brackets. You'll not be able to save these journals anymore."""
|
by square brackets. You'll not be able to save these journals anymore."""
|
||||||
def _load(self, filename):
|
def _load(self, filename):
|
||||||
with codecs.open(filename, "r", "utf-8") as f:
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
def _parse(self, journal_txt):
|
def _parse(self, journal_txt):
|
||||||
|
@ -323,7 +320,7 @@ class LegacyJournal(Journal):
|
||||||
# escaping for the new format).
|
# escaping for the new format).
|
||||||
line = new_date_format_regex.sub(r' \1', line)
|
line = new_date_format_regex.sub(r' \1', line)
|
||||||
if current_entry:
|
if current_entry:
|
||||||
current_entry.text += line + u"\n"
|
current_entry.text += line + "\n"
|
||||||
|
|
||||||
# Append last entry
|
# Append last entry
|
||||||
if current_entry:
|
if current_entry:
|
||||||
|
@ -347,8 +344,9 @@ def open_journal(name, config, legacy=False):
|
||||||
from . import DayOneJournal
|
from . import DayOneJournal
|
||||||
return DayOneJournal.DayOne(**config).open()
|
return DayOneJournal.DayOne(**config).open()
|
||||||
else:
|
else:
|
||||||
util.prompt(
|
print(
|
||||||
u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])
|
f"[Error: {config['journal']} is a directory, but doesn't seem to be a DayOne journal either.",
|
||||||
|
file=sys.stderr
|
||||||
)
|
)
|
||||||
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from . import cli
|
from . import cli
|
||||||
|
|
||||||
|
|
||||||
|
|
58
jrnl/cli.py
58
jrnl/cli.py
|
@ -1,5 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
jrnl
|
jrnl
|
||||||
|
@ -7,8 +6,6 @@
|
||||||
license: MIT, see LICENSE for more details.
|
license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from __future__ import absolute_import
|
|
||||||
from . import Journal
|
from . import Journal
|
||||||
from . import util
|
from . import util
|
||||||
from . import install
|
from . import install
|
||||||
|
@ -91,7 +88,7 @@ def encrypt(journal, filename=None):
|
||||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||||
util.set_keychain(journal.name, journal.config['password'])
|
util.set_keychain(journal.name, journal.config['password'])
|
||||||
|
|
||||||
util.prompt("Journal encrypted to {0}.".format(filename or new_journal.config['journal']))
|
print("Journal encrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def decrypt(journal, filename=None):
|
def decrypt(journal, filename=None):
|
||||||
|
@ -102,12 +99,12 @@ def decrypt(journal, filename=None):
|
||||||
new_journal = Journal.PlainJournal(filename, **journal.config)
|
new_journal = Journal.PlainJournal(filename, **journal.config)
|
||||||
new_journal.entries = journal.entries
|
new_journal.entries = journal.entries
|
||||||
new_journal.write(filename)
|
new_journal.write(filename)
|
||||||
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
|
print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def list_journals(config):
|
def list_journals(config):
|
||||||
"""List the journals specified in the configuration file"""
|
"""List the journals specified in the configuration file"""
|
||||||
result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH)
|
result = f"Journals defined in {install.CONFIG_FILE_PATH}\n"
|
||||||
ml = min(max(len(k) for k in config['journals']), 20)
|
ml = min(max(len(k) for k in config['journals']), 20)
|
||||||
for journal, cfg in config['journals'].items():
|
for journal, cfg in config['journals'].items():
|
||||||
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
|
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
|
||||||
|
@ -138,20 +135,19 @@ def configure_logger(debug=False):
|
||||||
def run(manual_args=None):
|
def run(manual_args=None):
|
||||||
args = parse_args(manual_args)
|
args = parse_args(manual_args)
|
||||||
configure_logger(args.debug)
|
configure_logger(args.debug)
|
||||||
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
|
|
||||||
if args.version:
|
if args.version:
|
||||||
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__)
|
version_str = f"{jrnl.__title__} version {jrnl.__version__}"
|
||||||
print(util.py2encode(version_str))
|
print(version_str)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = install.load_or_install_jrnl()
|
config = install.load_or_install_jrnl()
|
||||||
except UserAbort as err:
|
except UserAbort as err:
|
||||||
util.prompt("\n{}".format(err))
|
print(f"\n{err}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.ls:
|
if args.ls:
|
||||||
util.prnt(list_journals(config))
|
print(list_journals(config))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
log.debug('Using configuration "%s"', config)
|
log.debug('Using configuration "%s"', config)
|
||||||
|
@ -161,11 +157,11 @@ def run(manual_args=None):
|
||||||
# use this!
|
# use this!
|
||||||
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
|
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
|
||||||
|
|
||||||
if journal_name is not 'default':
|
if journal_name != 'default':
|
||||||
args.text = args.text[1:]
|
args.text = args.text[1:]
|
||||||
elif "default" not in config['journals']:
|
elif "default" not in config['journals']:
|
||||||
util.prompt("No default journal configured.")
|
print("No default journal configured.", file=sys.stderr)
|
||||||
util.prompt(list_journals(config))
|
print(list_journals(config), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
config = util.scope_config(config, journal_name)
|
config = util.scope_config(config, journal_name)
|
||||||
|
@ -175,7 +171,7 @@ def run(manual_args=None):
|
||||||
try:
|
try:
|
||||||
args.limit = int(args.text[0].lstrip("-"))
|
args.limit = int(args.text[0].lstrip("-"))
|
||||||
args.text = args.text[1:]
|
args.text = args.text[1:]
|
||||||
except:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
log.debug('Using journal "%s"', journal_name)
|
log.debug('Using journal "%s"', journal_name)
|
||||||
|
@ -190,21 +186,22 @@ def run(manual_args=None):
|
||||||
if mode_compose and not args.text:
|
if mode_compose and not args.text:
|
||||||
if not sys.stdin.isatty():
|
if not sys.stdin.isatty():
|
||||||
# Piping data into jrnl
|
# Piping data into jrnl
|
||||||
raw = util.py23_read()
|
raw = sys.stdin.read()
|
||||||
elif config['editor']:
|
elif config['editor']:
|
||||||
template = ""
|
template = ""
|
||||||
if config['template']:
|
if config['template']:
|
||||||
try:
|
try:
|
||||||
template = open(config['template']).read()
|
template = open(config['template']).read()
|
||||||
except:
|
except OSError:
|
||||||
util.prompt("[Could not read template at '']".format(config['template']))
|
print(f"[Could not read template at '{config['template']}']", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
raw = util.get_text_from_editor(config, template)
|
raw = util.get_text_from_editor(config, template)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n")
|
print("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", file=sys.stderr)
|
||||||
|
raw = sys.stdin.read()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
util.prompt("[Entry NOT saved to journal.]")
|
print("[Entry NOT saved to journal.]", file=sys.stderr)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if raw:
|
if raw:
|
||||||
args.text = [raw]
|
args.text = [raw]
|
||||||
|
@ -215,7 +212,7 @@ def run(manual_args=None):
|
||||||
try:
|
try:
|
||||||
journal = Journal.open_journal(journal_name, config)
|
journal = Journal.open_journal(journal_name, config)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
util.prompt("[Interrupted while opening journal]".format(journal_name))
|
print(f"[Interrupted while opening journal]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Import mode
|
# Import mode
|
||||||
|
@ -225,11 +222,9 @@ def run(manual_args=None):
|
||||||
# Writing mode
|
# Writing mode
|
||||||
elif mode_compose:
|
elif mode_compose:
|
||||||
raw = " ".join(args.text).strip()
|
raw = " ".join(args.text).strip()
|
||||||
if util.PY2 and type(raw) is not unicode:
|
|
||||||
raw = raw.decode(sys.getfilesystemencoding())
|
|
||||||
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
|
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
|
||||||
journal.new_entry(raw)
|
journal.new_entry(raw)
|
||||||
util.prompt("[Entry added to {0} journal]".format(journal_name))
|
print(f"[Entry added to {journal_name} journal]", file=sys.stderr)
|
||||||
journal.write()
|
journal.write()
|
||||||
|
|
||||||
if not mode_compose:
|
if not mode_compose:
|
||||||
|
@ -246,14 +241,14 @@ def run(manual_args=None):
|
||||||
|
|
||||||
# Reading mode
|
# Reading mode
|
||||||
if not mode_compose and not mode_export and not mode_import:
|
if not mode_compose and not mode_export and not mode_import:
|
||||||
print(util.py2encode(journal.pprint()))
|
print(journal.pprint())
|
||||||
|
|
||||||
# Various export modes
|
# Various export modes
|
||||||
elif args.short:
|
elif args.short:
|
||||||
print(util.py2encode(journal.pprint(short=True)))
|
print(journal.pprint(short=True))
|
||||||
|
|
||||||
elif args.tags:
|
elif args.tags:
|
||||||
print(util.py2encode(plugins.get_exporter("tags").export(journal)))
|
print(plugins.get_exporter("tags").export(journal))
|
||||||
|
|
||||||
elif args.export is not False:
|
elif args.export is not False:
|
||||||
exporter = plugins.get_exporter(args.export)
|
exporter = plugins.get_exporter(args.export)
|
||||||
|
@ -275,7 +270,8 @@ def run(manual_args=None):
|
||||||
|
|
||||||
elif args.edit:
|
elif args.edit:
|
||||||
if not config['editor']:
|
if not config['editor']:
|
||||||
util.prompt("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR))
|
print("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]"
|
||||||
|
.format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
other_entries = [e for e in old_entries if e not in journal.entries]
|
other_entries = [e for e in old_entries if e not in journal.entries]
|
||||||
# Edit
|
# Edit
|
||||||
|
@ -286,11 +282,11 @@ def run(manual_args=None):
|
||||||
num_edited = len([e for e in journal.entries if e.modified])
|
num_edited = len([e for e in journal.entries if e.modified])
|
||||||
prompts = []
|
prompts = []
|
||||||
if num_deleted:
|
if num_deleted:
|
||||||
prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
|
prompts.append("{} {} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
|
||||||
if num_edited:
|
if num_edited:
|
||||||
prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
|
prompts.append("{} {} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
|
||||||
if prompts:
|
if prompts:
|
||||||
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
|
print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr)
|
||||||
journal.entries += other_entries
|
journal.entries += other_entries
|
||||||
journal.sort()
|
journal.sort()
|
||||||
journal.write()
|
journal.write()
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from .util import ERROR_COLOR, RESET_COLOR
|
from .util import ERROR_COLOR, RESET_COLOR
|
||||||
from .util import slugify, u
|
from .util import slugify
|
||||||
from .template import Template
|
from .plugins.template import Template
|
||||||
import os
|
import os
|
||||||
import codecs
|
|
||||||
|
|
||||||
|
|
||||||
class Exporter(object):
|
class Exporter:
|
||||||
"""This Exporter can convert entries and journals into text files."""
|
"""This Exporter can convert entries and journals into text files."""
|
||||||
def __init__(self, format):
|
def __init__(self, format):
|
||||||
with open("jrnl/templates/" + format + ".template") as f:
|
with open("jrnl/templates/" + format + ".template") as f:
|
||||||
|
@ -17,8 +14,8 @@ class Exporter(object):
|
||||||
self.template = Template(body)
|
self.template = Template(body)
|
||||||
|
|
||||||
def export_entry(self, entry):
|
def export_entry(self, entry):
|
||||||
"""Returns a unicode representation of a single entry."""
|
"""Returns a string representation of a single entry."""
|
||||||
return entry.__unicode__()
|
return str(entry)
|
||||||
|
|
||||||
def _get_vars(self, journal):
|
def _get_vars(self, journal):
|
||||||
return {
|
return {
|
||||||
|
@ -28,36 +25,36 @@ class Exporter(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def export_journal(self, journal):
|
def export_journal(self, journal):
|
||||||
"""Returns a unicode representation of an entire journal."""
|
"""Returns a string representation of an entire journal."""
|
||||||
return self.template.render_block("journal", **self._get_vars(journal))
|
return self.template.render_block("journal", **self._get_vars(journal))
|
||||||
|
|
||||||
def write_file(self, journal, path):
|
def write_file(self, journal, path):
|
||||||
"""Exports a journal into a single file."""
|
"""Exports a journal into a single file."""
|
||||||
try:
|
try:
|
||||||
with codecs.open(path, "w", "utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
f.write(self.export_journal(journal))
|
f.write(self.export_journal(journal))
|
||||||
return "[Journal exported to {0}]".format(path)
|
return f"[Journal exported to {path}]"
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||||
|
|
||||||
def make_filename(self, entry):
|
def make_filename(self, entry):
|
||||||
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), self.extension))
|
return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(entry.title), self.extension))
|
||||||
|
|
||||||
def write_files(self, journal, path):
|
def write_files(self, journal, path):
|
||||||
"""Exports a journal into individual files for each entry."""
|
"""Exports a journal into individual files for each entry."""
|
||||||
for entry in journal.entries:
|
for entry in journal.entries:
|
||||||
try:
|
try:
|
||||||
full_path = os.path.join(path, self.make_filename(entry))
|
full_path = os.path.join(path, self.make_filename(entry))
|
||||||
with codecs.open(full_path, "w", "utf-8") as f:
|
with open(full_path, "w", encoding="utf-8") as f:
|
||||||
f.write(self.export_entry(entry))
|
f.write(self.export_entry(entry))
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||||
return "[Journal exported to {0}]".format(path)
|
return f"[Journal exported to {path}]"
|
||||||
|
|
||||||
def export(self, journal, format="text", output=None):
|
def export(self, journal, format="text", output=None):
|
||||||
"""Exports to individual files if output is an existing path, or into
|
"""Exports to individual files if output is an existing path, or into
|
||||||
a single file if output is a file name, or returns the exporter's
|
a single file if output is a file name, or returns the exporter's
|
||||||
representation as unicode if output is None."""
|
representation as string if output is None."""
|
||||||
if output and os.path.isdir(output): # multiple files
|
if output and os.path.isdir(output): # multiple files
|
||||||
return self.write_files(journal, output)
|
return self.write_files(journal, output)
|
||||||
elif output: # single file
|
elif output: # single file
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
import readline
|
import readline
|
||||||
import glob
|
import glob
|
||||||
import getpass
|
import getpass
|
||||||
|
@ -69,7 +67,7 @@ def upgrade_config(config):
|
||||||
for key in missing_keys:
|
for key in missing_keys:
|
||||||
config[key] = default_config[key]
|
config[key] = default_config[key]
|
||||||
save_config(config)
|
save_config(config)
|
||||||
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH))
|
print(f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def save_config(config):
|
def save_config(config):
|
||||||
|
@ -91,10 +89,10 @@ def load_or_install_jrnl():
|
||||||
try:
|
try:
|
||||||
upgrade.upgrade_jrnl_if_necessary(config_path)
|
upgrade.upgrade_jrnl_if_necessary(config_path)
|
||||||
except upgrade.UpgradeValidationException:
|
except upgrade.UpgradeValidationException:
|
||||||
util.prompt("Aborting upgrade.")
|
print("Aborting upgrade.", file=sys.stderr)
|
||||||
util.prompt("Please tell us about this problem at the following URL:")
|
print("Please tell us about this problem at the following URL:", file=sys.stderr)
|
||||||
util.prompt("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException")
|
print("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException", file=sys.stderr)
|
||||||
util.prompt("Exiting.")
|
print("Exiting.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
upgrade_config(config)
|
upgrade_config(config)
|
||||||
|
@ -120,8 +118,8 @@ def install():
|
||||||
readline.set_completer(autocomplete)
|
readline.set_completer(autocomplete)
|
||||||
|
|
||||||
# Where to create the journal?
|
# Where to create the journal?
|
||||||
path_query = 'Path to your journal file (leave blank for {}): '.format(JOURNAL_FILE_PATH)
|
path_query = f'Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): '
|
||||||
journal_path = util.py23_input(path_query).strip() or JOURNAL_FILE_PATH
|
journal_path = input(path_query).strip() or JOURNAL_FILE_PATH
|
||||||
default_config['journals']['default'] = os.path.expanduser(os.path.expandvars(journal_path))
|
default_config['journals']['default'] = os.path.expanduser(os.path.expandvars(journal_path))
|
||||||
|
|
||||||
path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it
|
path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it
|
||||||
|
@ -139,7 +137,7 @@ def install():
|
||||||
else:
|
else:
|
||||||
util.set_keychain("default", None)
|
util.set_keychain("default", None)
|
||||||
EncryptedJournal._create(default_config['journals']['default'], password)
|
EncryptedJournal._create(default_config['journals']['default'], password)
|
||||||
print("Journal will be encrypted.")
|
print("Journal will be encrypted.", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
PlainJournal._create(default_config['journals']['default'])
|
PlainJournal._create(default_config['journals']['default'])
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
from .jrnl_importer import JRNLImporter
|
from .jrnl_importer import JRNLImporter
|
||||||
from .json_exporter import JSONExporter
|
from .json_exporter import JSONExporter
|
||||||
|
@ -15,8 +13,8 @@ from .template_exporter import __all__ as template_exporters
|
||||||
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters
|
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters
|
||||||
__importers =[JRNLImporter]
|
__importers =[JRNLImporter]
|
||||||
|
|
||||||
__exporter_types = dict([(name, plugin) for plugin in __exporters for name in plugin.names])
|
__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
|
||||||
__importer_types = dict([(name, plugin) for plugin in __importers for name in plugin.names])
|
__importer_types = {name: plugin for plugin in __importers for name in plugin.names}
|
||||||
|
|
||||||
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
||||||
IMPORT_FORMATS = sorted(__importer_types.keys())
|
IMPORT_FORMATS = sorted(__importer_types.keys())
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
import codecs
|
|
||||||
import sys
|
import sys
|
||||||
from .. import util
|
from .. import util
|
||||||
|
|
||||||
class JRNLImporter(object):
|
class JRNLImporter:
|
||||||
"""This plugin imports entries from other jrnl files."""
|
"""This plugin imports entries from other jrnl files."""
|
||||||
names = ["jrnl"]
|
names = ["jrnl"]
|
||||||
|
|
||||||
|
@ -17,15 +15,15 @@ class JRNLImporter(object):
|
||||||
old_cnt = len(journal.entries)
|
old_cnt = len(journal.entries)
|
||||||
old_entries = journal.entries
|
old_entries = journal.entries
|
||||||
if input:
|
if input:
|
||||||
with codecs.open(input, "r", "utf-8") as f:
|
with open(input, "r", encoding="utf-8") as f:
|
||||||
other_journal_txt = f.read()
|
other_journal_txt = f.read()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
other_journal_txt = util.py23_read()
|
other_journal_txt = sys.stdin.read()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
util.prompt("[Entries NOT imported into journal.]")
|
print("[Entries NOT imported into journal.]", file=sys.stderr)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
journal.import_(other_journal_txt)
|
journal.import_(other_journal_txt)
|
||||||
new_cnt = len(journal.entries)
|
new_cnt = len(journal.entries)
|
||||||
util.prompt("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name))
|
print("[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr)
|
||||||
journal.write()
|
journal.write()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
import json
|
import json
|
||||||
from .util import get_tags_count
|
from .util import get_tags_count
|
||||||
|
@ -35,7 +34,7 @@ class JSONExporter(TextExporter):
|
||||||
"""Returns a json representation of an entire journal."""
|
"""Returns a json representation of an entire journal."""
|
||||||
tags = get_tags_count(journal)
|
tags = get_tags_count(journal)
|
||||||
result = {
|
result = {
|
||||||
"tags": dict((tag, count) for count, tag in tags),
|
"tags": {tag: count for count, tag in tags},
|
||||||
"entries": [cls.entry_to_dict(e) for e in journal.entries]
|
"entries": [cls.entry_to_dict(e) for e in journal.entries]
|
||||||
}
|
}
|
||||||
return json.dumps(result, indent=2)
|
return json.dumps(result, indent=2)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals, print_function
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -51,15 +50,11 @@ class MarkdownExporter(TextExporter):
|
||||||
newbody = newbody + previous_line # add very last line
|
newbody = newbody + previous_line # add very last line
|
||||||
|
|
||||||
if warn_on_heading_level is True:
|
if warn_on_heading_level is True:
|
||||||
print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr)
|
print(f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
||||||
|
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
return "{md} {date} {title}\n{body} {space}".format(
|
return f"{heading} {date_str} {entry.title}\n{newbody} "
|
||||||
md=heading,
|
|
||||||
date=date_str,
|
|
||||||
title=entry.title,
|
|
||||||
body=newbody,
|
|
||||||
space=""
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
from .util import get_tags_count
|
from .util import get_tags_count
|
||||||
|
|
||||||
|
@ -26,5 +25,5 @@ class TagExporter(TextExporter):
|
||||||
elif min(tag_counts)[0] == 0:
|
elif min(tag_counts)[0] == 0:
|
||||||
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
|
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
|
||||||
result += '[Removed tags that appear only once.]\n'
|
result += '[Removed tags that appear only once.]\n'
|
||||||
result += "\n".join("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
|
result += "\n".join("{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -13,7 +13,7 @@ BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
|
||||||
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
||||||
|
|
||||||
|
|
||||||
class Template(object):
|
class Template:
|
||||||
def __init__(self, template):
|
def __init__(self, template):
|
||||||
self.template = template
|
self.template = template
|
||||||
self.clean_template = None
|
self.clean_template = None
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
from .template import Template
|
from .template import Template
|
||||||
import os
|
import os
|
||||||
|
@ -14,7 +12,7 @@ class GenericTemplateExporter(TextExporter):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry):
|
def export_entry(cls, entry):
|
||||||
"""Returns a unicode representation of a single entry."""
|
"""Returns a string representation of a single entry."""
|
||||||
vars = {
|
vars = {
|
||||||
'entry': entry,
|
'entry': entry,
|
||||||
'tags': entry.tags
|
'tags': entry.tags
|
||||||
|
@ -23,7 +21,7 @@ class GenericTemplateExporter(TextExporter):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns a unicode representation of an entire journal."""
|
"""Returns a string representation of an entire journal."""
|
||||||
vars = {
|
vars = {
|
||||||
'journal': journal,
|
'journal': journal,
|
||||||
'entries': journal.entries,
|
'entries': journal.entries,
|
||||||
|
@ -36,7 +34,7 @@ def __exporter_from_file(template_file):
|
||||||
"""Create a template class from a file"""
|
"""Create a template class from a file"""
|
||||||
name = os.path.basename(template_file).replace(".template", "")
|
name = os.path.basename(template_file).replace(".template", "")
|
||||||
template = Template.from_file(template_file)
|
template = Template.from_file(template_file)
|
||||||
return type(str("{}Exporter".format(name.title())), (GenericTemplateExporter, ), {
|
return type(str(f"{name.title()}Exporter"), (GenericTemplateExporter, ), {
|
||||||
"names": [name],
|
"names": [name],
|
||||||
"extension": template.extension,
|
"extension": template.extension,
|
||||||
"template": template
|
"template": template
|
||||||
|
|
|
@ -1,41 +1,39 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
from ..util import slugify
|
||||||
import codecs
|
|
||||||
from ..util import u, slugify
|
|
||||||
import os
|
import os
|
||||||
from ..util import ERROR_COLOR, RESET_COLOR
|
from ..util import ERROR_COLOR, RESET_COLOR
|
||||||
|
|
||||||
|
|
||||||
class TextExporter(object):
|
class TextExporter:
|
||||||
"""This Exporter can convert entries and journals into text files."""
|
"""This Exporter can convert entries and journals into text files."""
|
||||||
names = ["text", "txt"]
|
names = ["text", "txt"]
|
||||||
extension = "txt"
|
extension = "txt"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry):
|
def export_entry(cls, entry):
|
||||||
"""Returns a unicode representation of a single entry."""
|
"""Returns a string representation of a single entry."""
|
||||||
return entry.__unicode__()
|
return str(entry)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns a unicode representation of an entire journal."""
|
"""Returns a string 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)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_file(cls, journal, path):
|
def write_file(cls, journal, path):
|
||||||
"""Exports a journal into a single file."""
|
"""Exports a journal into a single file."""
|
||||||
try:
|
try:
|
||||||
with codecs.open(path, "w", "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 "[Journal exported to {0}]".format(path)
|
return f"[Journal exported to {path}]"
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_filename(cls, entry):
|
def make_filename(cls, entry):
|
||||||
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension))
|
return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_files(cls, journal, path):
|
def write_files(cls, journal, path):
|
||||||
|
@ -43,17 +41,17 @@ class TextExporter(object):
|
||||||
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))
|
||||||
with codecs.open(full_path, "w", "utf-8") as f:
|
with open(full_path, "w", encoding="utf-8") as f:
|
||||||
f.write(cls.export_entry(entry))
|
f.write(cls.export_entry(entry))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
||||||
return "[Journal exported to {0}]".format(path)
|
return "[Journal exported to {}]".format(path)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export(cls, journal, output=None):
|
def export(cls, journal, output=None):
|
||||||
"""Exports to individual files if output is an existing path, or into
|
"""Exports to individual files if output is an existing path, or into
|
||||||
a single file if output is a file name, or returns the exporter's
|
a single file if output is a file name, or returns the exporter's
|
||||||
representation as unicode if output is None."""
|
representation as string if output is None."""
|
||||||
if output and os.path.isdir(output): # multiple files
|
if output and os.path.isdir(output): # multiple files
|
||||||
return cls.write_files(journal, output)
|
return cls.write_files(journal, output)
|
||||||
elif output: # single file
|
elif output: # single file
|
||||||
|
|
|
@ -10,7 +10,7 @@ def get_tags_count(journal):
|
||||||
for entry in journal.entries
|
for entry in journal.entries
|
||||||
for tag in set(entry.tags)]
|
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 = set([(tags.count(tag), tag) for tag in tags])
|
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||||
return tag_counts
|
return tag_counts
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from .json_exporter import JSONExporter
|
from .json_exporter import JSONExporter
|
||||||
from .util import get_tags_count
|
from .util import get_tags_count
|
||||||
from ..util import u
|
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +18,7 @@ class XMLExporter(JSONExporter):
|
||||||
entry_el = doc_el.createElement('entry')
|
entry_el = doc_el.createElement('entry')
|
||||||
for key, value in cls.entry_to_dict(entry).items():
|
for key, value in cls.entry_to_dict(entry).items():
|
||||||
elem = doc_el.createElement(key)
|
elem = doc_el.createElement(key)
|
||||||
elem.appendChild(doc_el.createTextNode(u(value)))
|
elem.appendChild(doc_el.createTextNode(value))
|
||||||
entry_el.appendChild(elem)
|
entry_el.appendChild(elem)
|
||||||
if not doc:
|
if not doc:
|
||||||
doc_el.appendChild(entry_el)
|
doc_el.appendChild(entry_el)
|
||||||
|
@ -33,8 +31,8 @@ class XMLExporter(JSONExporter):
|
||||||
entry_el = doc.createElement('entry')
|
entry_el = doc.createElement('entry')
|
||||||
entry_el.setAttribute('date', entry.date.isoformat())
|
entry_el.setAttribute('date', entry.date.isoformat())
|
||||||
if hasattr(entry, "uuid"):
|
if hasattr(entry, "uuid"):
|
||||||
entry_el.setAttribute('uuid', u(entry.uuid))
|
entry_el.setAttribute('uuid', entry.uuid)
|
||||||
entry_el.setAttribute('starred', u(entry.starred))
|
entry_el.setAttribute('starred', entry.starred)
|
||||||
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
||||||
return entry_el
|
return entry_el
|
||||||
|
|
||||||
|
@ -49,7 +47,7 @@ class XMLExporter(JSONExporter):
|
||||||
for count, tag in tags:
|
for count, tag in tags:
|
||||||
tag_el = doc.createElement('tag')
|
tag_el = doc.createElement('tag')
|
||||||
tag_el.setAttribute('name', tag)
|
tag_el.setAttribute('name', tag)
|
||||||
count_node = doc.createTextNode(u(count))
|
count_node = doc.createTextNode(str(count))
|
||||||
tag_el.appendChild(count_node)
|
tag_el.appendChild(count_node)
|
||||||
tags_el.appendChild(tag_el)
|
tags_el.appendChild(tag_el)
|
||||||
for entry in journal.entries:
|
for entry in journal.entries:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals, print_function
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -18,7 +17,8 @@ class YAMLExporter(TextExporter):
|
||||||
def export_entry(cls, entry, to_multifile=True):
|
def export_entry(cls, entry, to_multifile=True):
|
||||||
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
||||||
if to_multifile is False:
|
if to_multifile is False:
|
||||||
print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format("\033[31m", "\033[0m", file=sys.stderr))
|
print("{}ERROR{}: YAML export must be to individual files. "
|
||||||
|
"Please specify a directory to export to.".format("\033[31m", "\033[0m"), file=sys.stderr)
|
||||||
return
|
return
|
||||||
|
|
||||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||||
|
@ -27,7 +27,7 @@ class YAMLExporter(TextExporter):
|
||||||
|
|
||||||
tagsymbols = entry.journal.config['tagsymbols']
|
tagsymbols = entry.journal.config['tagsymbols']
|
||||||
# see also Entry.Entry.rag_regex
|
# see also Entry.Entry.rag_regex
|
||||||
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols), re.UNICODE)
|
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols))
|
||||||
|
|
||||||
'''Increase heading levels in body text'''
|
'''Increase heading levels in body text'''
|
||||||
newbody = ''
|
newbody = ''
|
||||||
|
|
|
@ -51,7 +51,7 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if flag is 1: # Date found, but no time. Use the default time.
|
if flag == 1: # Date found, but no time. Use the default time.
|
||||||
date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0)
|
date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0)
|
||||||
else:
|
else:
|
||||||
date = datetime(*date[:6])
|
date = datetime(*date[:6])
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from __future__ import absolute_import, unicode_literals
|
import sys
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from . import Journal
|
from . import Journal
|
||||||
|
@ -6,11 +6,10 @@ from . import util
|
||||||
from .EncryptedJournal import EncryptedJournal
|
from .EncryptedJournal import EncryptedJournal
|
||||||
from .util import UserAbort
|
from .util import UserAbort
|
||||||
import os
|
import os
|
||||||
import codecs
|
|
||||||
|
|
||||||
|
|
||||||
def backup(filename, binary=False):
|
def backup(filename, binary=False):
|
||||||
util.prompt(" Created a backup at {}.backup".format(filename))
|
print(f" Created a backup at {filename}.backup", file=sys.stderr)
|
||||||
filename = os.path.expanduser(os.path.expandvars(filename))
|
filename = os.path.expanduser(os.path.expandvars(filename))
|
||||||
with open(filename, 'rb' if binary else 'r') as original:
|
with open(filename, 'rb' if binary else 'r') as original:
|
||||||
contents = original.read()
|
contents = original.read()
|
||||||
|
@ -19,14 +18,14 @@ def backup(filename, binary=False):
|
||||||
|
|
||||||
|
|
||||||
def upgrade_jrnl_if_necessary(config_path):
|
def upgrade_jrnl_if_necessary(config_path):
|
||||||
with codecs.open(config_path, "r", "utf-8") as f:
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
config_file = f.read()
|
config_file = f.read()
|
||||||
if not config_file.strip().startswith("{"):
|
if not config_file.strip().startswith("{"):
|
||||||
return
|
return
|
||||||
|
|
||||||
config = util.load_config(config_path)
|
config = util.load_config(config_path)
|
||||||
|
|
||||||
util.prompt("""Welcome to jrnl {}.
|
print("""Welcome to jrnl {}.
|
||||||
|
|
||||||
It looks like you've been using an older version of jrnl until now. That's
|
It looks like you've been using an older version of jrnl until now. That's
|
||||||
okay - jrnl will now upgrade your configuration and journal files. Afterwards
|
okay - jrnl will now upgrade your configuration and journal files. Afterwards
|
||||||
|
@ -65,19 +64,19 @@ older versions of jrnl anymore.
|
||||||
|
|
||||||
longest_journal_name = max([len(journal) for journal in config['journals']])
|
longest_journal_name = max([len(journal) for journal in config['journals']])
|
||||||
if encrypted_journals:
|
if encrypted_journals:
|
||||||
util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__))
|
print(f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", file=sys.stderr)
|
||||||
for journal, path in encrypted_journals.items():
|
for journal, path in encrypted_journals.items():
|
||||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||||
|
|
||||||
if plain_journals:
|
if plain_journals:
|
||||||
util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__))
|
print(f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", file=sys.stderr)
|
||||||
for journal, path in plain_journals.items():
|
for journal, path in plain_journals.items():
|
||||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||||
|
|
||||||
if other_journals:
|
if other_journals:
|
||||||
util.prompt("\nFollowing journals will be not be touched:")
|
print("\nFollowing journals will be not be touched:", file=sys.stderr)
|
||||||
for journal, path in other_journals.items():
|
for journal, path in other_journals.items():
|
||||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
||||||
|
@ -87,13 +86,13 @@ older versions of jrnl anymore.
|
||||||
raise UserAbort("jrnl NOT upgraded, exiting.")
|
raise UserAbort("jrnl NOT upgraded, exiting.")
|
||||||
|
|
||||||
for journal_name, path in encrypted_journals.items():
|
for journal_name, path in encrypted_journals.items():
|
||||||
util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path))
|
print(f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...", file=sys.stderr)
|
||||||
backup(path, binary=True)
|
backup(path, binary=True)
|
||||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
||||||
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
||||||
|
|
||||||
for journal_name, path in plain_journals.items():
|
for journal_name, path in plain_journals.items():
|
||||||
util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path))
|
print(f"\nUpgrading plain text '{journal_name}' journal stored in {path}...", file=sys.stderr)
|
||||||
backup(path)
|
backup(path)
|
||||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
||||||
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
|
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
|
||||||
|
@ -102,8 +101,9 @@ older versions of jrnl anymore.
|
||||||
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
||||||
|
|
||||||
if len(failed_journals) > 0:
|
if len(failed_journals) > 0:
|
||||||
util.prompt("\nThe following journal{} failed to upgrade:\n{}".format(
|
print("\nThe following journal{} failed to upgrade:\n{}".format(
|
||||||
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals))
|
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)),
|
||||||
|
file=sys.stderr
|
||||||
)
|
)
|
||||||
|
|
||||||
raise UpgradeValidationException
|
raise UpgradeValidationException
|
||||||
|
@ -112,10 +112,11 @@ older versions of jrnl anymore.
|
||||||
for j in all_journals:
|
for j in all_journals:
|
||||||
j.write()
|
j.write()
|
||||||
|
|
||||||
util.prompt("\nUpgrading config...")
|
print("\nUpgrading config...", file=sys.stderr)
|
||||||
backup(config_path)
|
backup(config_path)
|
||||||
|
|
||||||
util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path))
|
print("\nWe're all done here and you can start enjoying jrnl 2.", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
class UpgradeValidationException(Exception):
|
class UpgradeValidationException(Exception):
|
||||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||||
|
|
109
jrnl/util.py
109
jrnl/util.py
|
@ -1,8 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
@ -14,22 +10,12 @@ if "win32" in sys.platform:
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
import subprocess
|
import subprocess
|
||||||
import codecs
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import shlex
|
import shlex
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
PY2 = sys.version_info[0] == 2
|
|
||||||
STDIN = sys.stdin
|
|
||||||
STDERR = sys.stderr
|
|
||||||
STDOUT = sys.stdout
|
|
||||||
TEST = False
|
|
||||||
__cached_tz = None
|
|
||||||
|
|
||||||
WARNING_COLOR = "\033[33m"
|
WARNING_COLOR = "\033[33m"
|
||||||
ERROR_COLOR = "\033[31m"
|
ERROR_COLOR = "\033[31m"
|
||||||
RESET_COLOR = "\033[0m"
|
RESET_COLOR = "\033[0m"
|
||||||
|
@ -44,18 +30,14 @@ SENTENCE_SPLITTER = re.compile(r"""
|
||||||
\s+ # a sequence of required spaces.
|
\s+ # a sequence of required spaces.
|
||||||
| # Otherwise,
|
| # Otherwise,
|
||||||
\n # a sentence also terminates newlines.
|
\n # a sentence also terminates newlines.
|
||||||
)""", re.UNICODE | re.VERBOSE)
|
)""", re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
class UserAbort(Exception):
|
class UserAbort(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def getpass(prompt="Password: "):
|
getpass = gp.getpass
|
||||||
if not TEST:
|
|
||||||
return gp.getpass(bytes(prompt))
|
|
||||||
else:
|
|
||||||
return py23_input(prompt)
|
|
||||||
|
|
||||||
|
|
||||||
def get_password(validator, keychain=None, max_attempts=3):
|
def get_password(validator, keychain=None, max_attempts=3):
|
||||||
|
@ -67,14 +49,14 @@ def get_password(validator, keychain=None, max_attempts=3):
|
||||||
set_keychain(keychain, None)
|
set_keychain(keychain, None)
|
||||||
attempt = 1
|
attempt = 1
|
||||||
while result is None and attempt < max_attempts:
|
while result is None and attempt < max_attempts:
|
||||||
prompt("Wrong password, try again.")
|
print("Wrong password, try again.", file=sys.stderr)
|
||||||
password = getpass()
|
password = gp.getpass()
|
||||||
result = validator(password)
|
result = validator(password)
|
||||||
attempt += 1
|
attempt += 1
|
||||||
if result is not None:
|
if result is not None:
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
prompt("Extremely wrong password.")
|
print("Extremely wrong password.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,57 +70,16 @@ def set_keychain(journal_name, password):
|
||||||
if password is None:
|
if password is None:
|
||||||
try:
|
try:
|
||||||
keyring.delete_password('jrnl', journal_name)
|
keyring.delete_password('jrnl', journal_name)
|
||||||
except:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
elif not TEST:
|
else:
|
||||||
keyring.set_password('jrnl', journal_name, password)
|
keyring.set_password('jrnl', journal_name, password)
|
||||||
|
|
||||||
|
|
||||||
def u(s):
|
|
||||||
"""Mock unicode function for python 2 and 3 compatibility."""
|
|
||||||
if not isinstance(s, str):
|
|
||||||
s = str(s)
|
|
||||||
return s if PY3 or type(s) is unicode else s.decode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def py2encode(s):
|
|
||||||
"""Encodes to UTF-8 in Python 2 but not in Python 3."""
|
|
||||||
return s.encode("utf-8") if PY2 and type(s) is unicode else s
|
|
||||||
|
|
||||||
|
|
||||||
def bytes(s):
|
|
||||||
"""Returns bytes, no matter what."""
|
|
||||||
if PY3:
|
|
||||||
return s.encode("utf-8") if type(s) is not bytes else s
|
|
||||||
return s.encode("utf-8") if type(s) is unicode else s
|
|
||||||
|
|
||||||
|
|
||||||
def prnt(s):
|
|
||||||
"""Encode and print a string"""
|
|
||||||
STDOUT.write(u(s + "\n"))
|
|
||||||
|
|
||||||
|
|
||||||
def prompt(msg):
|
|
||||||
"""Prints a message to the std err stream defined in util."""
|
|
||||||
if not msg.endswith("\n"):
|
|
||||||
msg += "\n"
|
|
||||||
STDERR.write(u(msg))
|
|
||||||
|
|
||||||
|
|
||||||
def py23_input(msg=""):
|
|
||||||
prompt(msg)
|
|
||||||
return STDIN.readline().strip()
|
|
||||||
|
|
||||||
|
|
||||||
def py23_read(msg=""):
|
|
||||||
print(msg)
|
|
||||||
return STDIN.read()
|
|
||||||
|
|
||||||
|
|
||||||
def yesno(prompt, default=True):
|
def yesno(prompt, default=True):
|
||||||
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
|
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
|
||||||
raw = py23_input(prompt)
|
response = input(prompt)
|
||||||
return {'y': True, 'n': False}.get(raw.lower(), default)
|
return {"y": True, "n": False}.get(response.lower(), default)
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path):
|
def load_config(config_path):
|
||||||
|
@ -164,51 +105,35 @@ def scope_config(config, journal_name):
|
||||||
|
|
||||||
def get_text_from_editor(config, template=""):
|
def get_text_from_editor(config, template=""):
|
||||||
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
||||||
with codecs.open(tmpfile, 'w', "utf-8") as f:
|
with open(tmpfile, 'w', encoding="utf-8") as f:
|
||||||
if template:
|
if template:
|
||||||
f.write(template)
|
f.write(template)
|
||||||
try:
|
try:
|
||||||
subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile])
|
subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile])
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
subprocess.call(config['editor'] + [tmpfile])
|
subprocess.call(config['editor'] + [tmpfile])
|
||||||
with codecs.open(tmpfile, "r", "utf-8") as f:
|
with open(tmpfile, "r", encoding="utf-8") as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
os.close(filehandle)
|
os.close(filehandle)
|
||||||
os.remove(tmpfile)
|
os.remove(tmpfile)
|
||||||
if not raw:
|
if not raw:
|
||||||
prompt('[Nothing saved to file]')
|
print('[Nothing saved to file]', file=sys.stderr)
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|
||||||
def colorize(string):
|
def colorize(string):
|
||||||
"""Returns the string wrapped in cyan ANSI escape"""
|
"""Returns the string wrapped in cyan ANSI escape"""
|
||||||
return u"\033[36m{}\033[39m".format(string)
|
return f"\033[36m{string}\033[39m"
|
||||||
|
|
||||||
|
|
||||||
def slugify(string):
|
def slugify(string):
|
||||||
"""Slugifies a string.
|
"""Slugifies a string.
|
||||||
Based on public domain code from https://github.com/zacharyvoase/slugify
|
Based on public domain code from https://github.com/zacharyvoase/slugify
|
||||||
and ported to deal with all kinds of python 2 and 3 strings
|
|
||||||
"""
|
"""
|
||||||
string = u(string)
|
normalized_string = str(unicodedata.normalize('NFKD', string))
|
||||||
ascii_string = str(unicodedata.normalize('NFKD', string).encode('ascii', 'ignore'))
|
no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower()
|
||||||
if PY3:
|
|
||||||
ascii_string = ascii_string[1:] # removed the leading 'b'
|
|
||||||
no_punctuation = re.sub(r'[^\w\s-]', '', ascii_string).strip().lower()
|
|
||||||
slug = re.sub(r'[-\s]+', '-', no_punctuation)
|
slug = re.sub(r'[-\s]+', '-', no_punctuation)
|
||||||
return u(slug)
|
return slug
|
||||||
|
|
||||||
|
|
||||||
def int2byte(i):
|
|
||||||
"""Converts an integer to a byte.
|
|
||||||
This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3."""
|
|
||||||
return chr(i) if PY2 else bytes((i,))
|
|
||||||
|
|
||||||
|
|
||||||
def byte2int(b):
|
|
||||||
"""Converts a byte to an integer.
|
|
||||||
This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3."""
|
|
||||||
return ord(b)if PY2 else b
|
|
||||||
|
|
||||||
|
|
||||||
def split_title(text):
|
def split_title(text):
|
||||||
|
|
Loading…
Add table
Reference in a new issue