remove py2 remnants and use mocks in tests

This commit is contained in:
Peter Schmidbauer 2019-10-31 21:12:55 +01:00
parent ef23d7eabe
commit 34f8f858f1
25 changed files with 155 additions and 246 deletions

View file

@ -2,7 +2,7 @@
Scenario: Loading an encrypted journal
Given we use the config "encrypted.yaml"
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"
Scenario: Decrypting a journal
@ -14,16 +14,16 @@
Scenario: Encrypting a journal
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"
and the config for journal "default" should have "encrypt" set to "bool:True"
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"
Scenario: Storing a password in Keychain
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"
Then the config for journal "simple" should have "encrypt" set to "bool:True"
When we run "jrnl simple -n 1"

View file

@ -1,25 +1,15 @@
from behave import *
import shutil
import os
import jrnl
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
def before_scenario(context, scenario):
"""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
for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder)
if os.path.exists(working_dir):
shutil.rmtree(working_dir)
for folder in ("configs", "journals"):
original = os.path.join("features", "data", folder)
working_dir = os.path.join("features", folder)
@ -32,10 +22,9 @@ def before_scenario(context, scenario):
else:
shutil.copy2(source, working_dir)
def after_scenario(context, scenario):
"""After each scenario, restore all test data and remove working_dirs."""
context.messages.close()
context.messages = None
for folder in ("configs", "journals"):
working_dir = os.path.join("features", folder)
if os.path.exists(working_dir):

View file

@ -42,5 +42,5 @@ Feature: Multiple journals
Scenario: Don't crash if no file exists for a configured encrypted journal
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"

View file

@ -1,5 +1,4 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from unittest.mock import patch
from behave import given, when, then
from jrnl import cli, install, Journal, util, plugins
@ -10,10 +9,13 @@ import os
import json
import yaml
import keyring
import tzlocal
import shlex
import sys
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
keys = defaultdict(dict)
@ -31,15 +33,6 @@ class TestKeyring(keyring.backend.KeyringBackend):
keyring.set_keyring(TestKeyring())
try:
from io import StringIO
except ImportError:
from cStringIO import StringIO
import tzlocal
import shlex
import sys
def ushlex(command):
if sys.version_info[0] == 3:
return shlex.split(command)
@ -73,18 +66,41 @@ def set_config(context, config_file):
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 "{inputs}"')
def run_with_input(context, command, inputs=None):
text = inputs or context.text
@when('we run "{command}" and enter "{inputs1}"')
@when('we run "{command}" and enter "{inputs1}" and "{inputs2}"')
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:]
buffer = StringIO(text.strip())
util.STDIN = buffer
try:
cli.run(args or [])
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input:
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:
cli.run(args or [])
context.exit_status = 0
except SystemExit as e:
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}"')
@ -190,28 +206,24 @@ def check_output_time_inline(context, text):
def check_output_inline(context, text=None):
text = text or context.text
out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
assert text in out, text
@then('the output should not contain "{text}"')
def check_output_not_inline(context, text):
out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
assert text not in out
@then('we should see the message "{text}"')
def check_message(context, text):
out = context.messages.getvalue()
out = context.stderr_capture.getvalue()
assert text in out, [text, out]
@then('we should not see the message "{text}"')
def check_not_message(context, text):
out = context.messages.getvalue()
out = context.stderr_capture.getvalue()
assert text not in out, [text, out]

View file

@ -13,11 +13,11 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
Scenario: Upgrading a journal encrypted with jrnl 1.x
Given we use the config "encrypted_old.json"
When we run "jrnl -n 1" and enter
When we run "jrnl -n 1" and enter
"""
Y
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"

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import Entry
from . import Journal
from . import time as jrnl_time
@ -83,7 +82,7 @@ class DayOne(Journal.Journal):
def editable_str(self):
"""Turns the journal into a string of entries that can be edited
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(["# {0}\n{1}".format(e.uuid, str(e)) for e in self.entries])
def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates its entries."""

View file

@ -8,14 +8,13 @@ from cryptography.hazmat.backends import default_backend
import sys
import os
import base64
import getpass
import logging
log = logging.getLogger()
def make_key(password):
password = util.bytes(password)
password = password.encode("utf-8")
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
@ -48,9 +47,9 @@ class EncryptedJournal(Journal.Journal):
self.config['password'] = password
text = ""
self._store(filename, text)
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
print("[Journal '{0}' created at {1}]".format(self.name, filename), file=sys.stderr)
else:
util.prompt("No password supplied for encrypted journal")
print("No password supplied for encrypted journal", file=sys.stderr)
sys.exit(1)
else:
text = self._load(filename)
@ -59,7 +58,6 @@ class EncryptedJournal(Journal.Journal):
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
return self
def _load(self, filename, password=None):
"""Loads an encrypted journal from a file and tries to decrypt it.
If password is not provided, will look for password in the keychain

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import unicode_literals
import re
import textwrap
from datetime import datetime
@ -52,13 +51,13 @@ class Entry:
@staticmethod
def tag_regex(tagsymbols):
pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
return re.compile(pattern, re.UNICODE)
return re.compile(pattern)
def _parse_tags(self):
tagsymbols = self.journal.config['tagsymbols']
return set(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."""
date_str = self.date.strftime(self.journal.config['timeformat'])
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import Entry
from . import util
from . import time
@ -15,7 +14,7 @@ import logging
log = logging.getLogger(__name__)
class Tag(object):
class Tag:
def __init__(self, name, count=0):
self.name = name
self.count = count
@ -27,7 +26,7 @@ class Tag(object):
return "<Tag '{}'>".format(self.name)
class Journal(object):
class Journal:
def __init__(self, name='default', **kwargs):
self.config = {
'journal': "journal.txt",
@ -72,7 +71,7 @@ class Journal(object):
filename = filename or self.config['journal']
if not os.path.exists(filename):
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
print("[Journal '{0}' created at {1}]".format(self.name, filename), file=sys.stderr)
self._create(filename)
text = self._load(filename)
@ -96,7 +95,7 @@ class Journal(object):
return True
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):
raise NotImplementedError
@ -140,9 +139,6 @@ class Journal(object):
entry._parse_text()
return entries
def __unicode__(self):
return self.pprint()
def pprint(self, short=False):
"""Prettyprints the journal's entries"""
sep = "\n"
@ -153,7 +149,7 @@ class Journal(object):
tagre = re.compile(re.escape(tag), re.IGNORECASE)
pp = re.sub(tagre,
lambda match: util.colorize(match.group(0)),
pp, re.UNICODE)
pp)
else:
pp = re.sub(
Entry.Entry.tag_regex(self.config['tagsymbols']),
@ -162,6 +158,9 @@ class Journal(object):
)
return pp
def __str__(self):
return self.pprint()
def __repr__(self):
return "<Journal with {0} entries>".format(len(self.entries))
@ -254,7 +253,7 @@ class Journal(object):
def editable_str(self):
"""Turns the journal into a string of entries that can be edited
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):
"""Parses the output of self.editable_str and updates it's entries."""
@ -347,8 +346,9 @@ def open_journal(name, config, legacy=False):
from . import DayOneJournal
return DayOneJournal.DayOne(**config).open()
else:
util.prompt(
u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])
print(
u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']),
file=sys.stderr
)
sys.exit(1)

View file

@ -1,6 +1,5 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from . import cli

View file

@ -7,8 +7,6 @@
license: MIT, see LICENSE for more details.
"""
from __future__ import unicode_literals
from __future__ import absolute_import
from . import Journal
from . import util
from . import install
@ -91,7 +89,7 @@ def encrypt(journal, filename=None):
if util.yesno("Do you want to store the password in your keychain?", default=True):
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 {0}.".format(filename or new_journal.config['journal']), file=sys.stderr)
def decrypt(journal, filename=None):
@ -102,7 +100,7 @@ def decrypt(journal, filename=None):
new_journal = Journal.PlainJournal(filename, **journal.config)
new_journal.entries = journal.entries
new_journal.write(filename)
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
print("Journal decrypted to {0}.".format(filename or new_journal.config['journal']), file=sys.stderr)
def list_journals(config):
@ -138,20 +136,19 @@ def configure_logger(debug=False):
def run(manual_args=None):
args = parse_args(manual_args)
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:
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__)
print(util.py2encode(version_str))
print(version_str)
sys.exit(0)
try:
config = install.load_or_install_jrnl()
except UserAbort as err:
util.prompt("\n{}".format(err))
print("\n{}".format(err), file=sys.stderr)
sys.exit(1)
if args.ls:
util.prnt(list_journals(config))
print(list_journals(config))
sys.exit(0)
log.debug('Using configuration "%s"', config)
@ -164,8 +161,8 @@ def run(manual_args=None):
if journal_name is not 'default':
args.text = args.text[1:]
elif "default" not in config['journals']:
util.prompt("No default journal configured.")
util.prompt(list_journals(config))
print("No default journal configured.", file=sys.stderr)
print(list_journals(config), file=sys.stderr)
sys.exit(1)
config = util.scope_config(config, journal_name)
@ -175,12 +172,12 @@ def run(manual_args=None):
try:
args.limit = int(args.text[0].lstrip("-"))
args.text = args.text[1:]
except:
except ValueError:
pass
log.debug('Using journal "%s"', journal_name)
mode_compose, mode_export, mode_import = guess_mode(args, config)
# How to quit writing?
if "win32" in sys.platform:
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
@ -190,21 +187,22 @@ def run(manual_args=None):
if mode_compose and not args.text:
if not sys.stdin.isatty():
# Piping data into jrnl
raw = util.py23_read()
raw = sys.stdin.read()
elif config['editor']:
template = ""
if config['template']:
try:
template = open(config['template']).read()
except:
util.prompt("[Could not read template at '']".format(config['template']))
except IOError:
print("[Could not read template at '']".format(config['template']), file=sys.stderr)
sys.exit(1)
raw = util.get_text_from_editor(config, template)
else:
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:
util.prompt("[Entry NOT saved to journal.]")
print("[Entry NOT saved to journal.]", file=sys.stderr)
sys.exit(0)
if raw:
args.text = [raw]
@ -215,7 +213,7 @@ def run(manual_args=None):
try:
journal = Journal.open_journal(journal_name, config)
except KeyboardInterrupt:
util.prompt("[Interrupted while opening journal]".format(journal_name))
print("[Interrupted while opening journal]".format(journal_name), file=sys.stderr)
sys.exit(1)
# Import mode
@ -225,11 +223,9 @@ def run(manual_args=None):
# Writing mode
elif mode_compose:
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)
journal.new_entry(raw)
util.prompt("[Entry added to {0} journal]".format(journal_name))
print("[Entry added to {0} journal]".format(journal_name), file=sys.stderr)
journal.write()
if not mode_compose:
@ -246,14 +242,14 @@ def run(manual_args=None):
# Reading mode
if not mode_compose and not mode_export and not mode_import:
print(util.py2encode(journal.pprint()))
print(journal.pprint())
# Various export modes
elif args.short:
print(util.py2encode(journal.pprint(short=True)))
print(journal.pprint(short=True))
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:
exporter = plugins.get_exporter(args.export)
@ -275,7 +271,8 @@ def run(manual_args=None):
elif args.edit:
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)
other_entries = [e for e in old_entries if e not in journal.entries]
# Edit
@ -290,7 +287,7 @@ def run(manual_args=None):
if num_edited:
prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
if prompts:
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
print("[{0}]".format(", ".join(prompts).capitalize()), file=sys.stderr)
journal.entries += other_entries
journal.sort()
journal.write()

View file

@ -1,15 +1,14 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .util import ERROR_COLOR, RESET_COLOR
from .util import slugify, u
from .template import Template
from .util import slugify
from .plugins.template import Template
import os
import codecs
class Exporter(object):
class Exporter:
"""This Exporter can convert entries and journals into text files."""
def __init__(self, format):
with open("jrnl/templates/" + format + ".template") as f:
@ -17,8 +16,8 @@ class Exporter(object):
self.template = Template(body)
def export_entry(self, entry):
"""Returns a unicode representation of a single entry."""
return entry.__unicode__()
"""Returns a string representation of a single entry."""
return str(entry)
def _get_vars(self, journal):
return {
@ -28,7 +27,7 @@ class Exporter(object):
}
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))
def write_file(self, journal, path):
@ -41,7 +40,7 @@ class Exporter(object):
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
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_{0}.{1}".format(slugify(entry.title), self.extension))
def write_files(self, journal, path):
"""Exports a journal into individual files for each entry."""
@ -57,7 +56,7 @@ class Exporter(object):
def export(self, journal, format="text", output=None):
"""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
representation as unicode if output is None."""
representation as string if output is None."""
if output and os.path.isdir(output): # multiple files
return self.write_files(journal, output)
elif output: # single file

View file

@ -69,7 +69,7 @@ def upgrade_config(config):
for key in missing_keys:
config[key] = default_config[key]
save_config(config)
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH))
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH), file=sys.stderr)
def save_config(config):
@ -91,10 +91,10 @@ def load_or_install_jrnl():
try:
upgrade.upgrade_jrnl_if_necessary(config_path)
except upgrade.UpgradeValidationException:
util.prompt("Aborting upgrade.")
util.prompt("Please tell us about this problem at the following URL:")
util.prompt("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException")
util.prompt("Exiting.")
print("Aborting upgrade.", file=sys.stderr)
print("Please tell us about this problem at the following URL:", file=sys.stderr)
print("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException", file=sys.stderr)
print("Exiting.", file=sys.stderr)
sys.exit(1)
upgrade_config(config)
@ -121,7 +121,7 @@ def install():
# Where to create the journal?
path_query = 'Path to your journal file (leave blank for {}): '.format(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))
path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it
@ -139,7 +139,7 @@ def install():
else:
util.set_keychain("default", None)
EncryptedJournal._create(default_config['journals']['default'], password)
print("Journal will be encrypted.")
print("Journal will be encrypted.", file=sys.stderr)
else:
PlainJournal._create(default_config['journals']['default'])

View file

@ -1,8 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
from .jrnl_importer import JRNLImporter
from .json_exporter import JSONExporter

View file

@ -1,12 +1,11 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import codecs
import sys
from .. import util
class JRNLImporter(object):
class JRNLImporter:
"""This plugin imports entries from other jrnl files."""
names = ["jrnl"]
@ -21,11 +20,11 @@ class JRNLImporter(object):
other_journal_txt = f.read()
else:
try:
other_journal_txt = util.py23_read()
other_journal_txt = sys.stdin.read()
except KeyboardInterrupt:
util.prompt("[Entries NOT imported into journal.]")
print("[Entries NOT imported into journal.]", file=sys.stderr)
sys.exit(0)
journal.import_(other_journal_txt)
new_cnt = len(journal.entries)
util.prompt("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name))
print("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr)
journal.write()

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
import json
from .util import get_tags_count

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter
import os
import re

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
from .util import get_tags_count

View file

@ -13,7 +13,7 @@ BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
INCLUDE_RE = r"{% *include +(.+?) *%}"
class Template(object):
class Template:
def __init__(self, template):
self.template = template
self.clean_template = None

View file

@ -1,8 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .text_exporter import TextExporter
from .template import Template
import os
@ -14,7 +12,7 @@ class GenericTemplateExporter(TextExporter):
@classmethod
def export_entry(cls, entry):
"""Returns a unicode representation of a single entry."""
"""Returns a string representation of a single entry."""
vars = {
'entry': entry,
'tags': entry.tags
@ -23,7 +21,7 @@ class GenericTemplateExporter(TextExporter):
@classmethod
def export_journal(cls, journal):
"""Returns a unicode representation of an entire journal."""
"""Returns a string representation of an entire journal."""
vars = {
'journal': journal,
'entries': journal.entries,

View file

@ -1,26 +1,25 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import codecs
from ..util import u, slugify
from ..util import slugify
import os
from ..util import ERROR_COLOR, RESET_COLOR
class TextExporter(object):
class TextExporter:
"""This Exporter can convert entries and journals into text files."""
names = ["text", "txt"]
extension = "txt"
@classmethod
def export_entry(cls, entry):
"""Returns a unicode representation of a single entry."""
return entry.__unicode__()
"""Returns a string representation of a single entry."""
return str(entry)
@classmethod
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)
@classmethod
@ -35,7 +34,7 @@ class TextExporter(object):
@classmethod
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_{0}.{1}".format(slugify(str(entry.title)), cls.extension))
@classmethod
def write_files(cls, journal, path):
@ -53,7 +52,7 @@ class TextExporter(object):
def export(cls, journal, output=None):
"""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
representation as unicode if output is None."""
representation as string if output is None."""
if output and os.path.isdir(output): # multiple files
return cls.write_files(journal, output)
elif output: # single file

View file

@ -1,10 +1,8 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
from .json_exporter import JSONExporter
from .util import get_tags_count
from ..util import u
from xml.dom import minidom
@ -20,7 +18,7 @@ class XMLExporter(JSONExporter):
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(u(value)))
elem.appendChild(doc_el.createTextNode(value))
entry_el.appendChild(elem)
if not doc:
doc_el.appendChild(entry_el)
@ -33,8 +31,8 @@ class XMLExporter(JSONExporter):
entry_el = doc.createElement('entry')
entry_el.setAttribute('date', entry.date.isoformat())
if hasattr(entry, "uuid"):
entry_el.setAttribute('uuid', u(entry.uuid))
entry_el.setAttribute('starred', u(entry.starred))
entry_el.setAttribute('uuid', entry.uuid)
entry_el.setAttribute('starred', entry.starred)
entry_el.appendChild(doc.createTextNode(entry.fulltext))
return entry_el
@ -49,7 +47,7 @@ class XMLExporter(JSONExporter):
for count, tag in tags:
tag_el = doc.createElement('tag')
tag_el.setAttribute('name', tag)
count_node = doc.createTextNode(u(count))
count_node = doc.createTextNode(str(count))
tag_el.appendChild(count_node)
tags_el.appendChild(tag_el)
for entry in journal.entries:

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import, unicode_literals, print_function
from .text_exporter import TextExporter
import os
import re
@ -27,7 +26,7 @@ class YAMLExporter(TextExporter):
tagsymbols = entry.journal.config['tagsymbols']
# 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'''
newbody = ''

View file

@ -1,4 +1,4 @@
from __future__ import absolute_import, unicode_literals
import sys
from . import __version__
from . import Journal
@ -10,7 +10,7 @@ import codecs
def backup(filename, binary=False):
util.prompt(" Created a backup at {}.backup".format(filename))
print(" Created a backup at {}.backup".format(filename), file=sys.stderr)
filename = os.path.expanduser(os.path.expandvars(filename))
with open(filename, 'rb' if binary else 'r') as original:
contents = original.read()
@ -26,7 +26,7 @@ def upgrade_jrnl_if_necessary(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
okay - jrnl will now upgrade your configuration and journal files. Afterwards
@ -63,19 +63,19 @@ older versions of jrnl anymore.
longest_journal_name = max([len(journal) for journal in config['journals']])
if encrypted_journals:
util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__))
print("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__), file=sys.stderr)
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:
util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__))
print("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__), file=sys.stderr)
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:
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():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
try:
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
@ -85,13 +85,13 @@ older versions of jrnl anymore.
raise UserAbort("jrnl NOT upgraded, exiting.")
for journal_name, path in encrypted_journals.items():
util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path))
print("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path), file=sys.stderr)
backup(path, binary=True)
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
all_journals.append(EncryptedJournal.from_journal(old_journal))
for journal_name, path in plain_journals.items():
util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path))
print("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path), file=sys.stderr)
backup(path)
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
@ -100,8 +100,9 @@ older versions of jrnl anymore.
failed_journals = [j for j in all_journals if not j.validate_parsing()]
if len(failed_journals) > 0:
util.prompt("\nThe following journal{} failed to upgrade:\n{}".format(
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals))
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)),
file=sys.stderr
)
raise UpgradeValidationException
@ -110,10 +111,11 @@ older versions of jrnl anymore.
for j in all_journals:
j.write()
util.prompt("\nUpgrading config...")
print("\nUpgrading config...", file=sys.stderr)
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.".format(config_path), file=sys.stderr)
class UpgradeValidationException(Exception):
"""Raised when the contents of an upgraded journal do not match the old journal"""

View file

@ -1,9 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import unicode_literals
from __future__ import absolute_import
import sys
import os
import getpass as gp
@ -21,15 +18,6 @@ import logging
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"
ERROR_COLOR = "\033[31m"
RESET_COLOR = "\033[0m"
@ -44,18 +32,14 @@ SENTENCE_SPLITTER = re.compile(r"""
\s+ # a sequence of required spaces.
| # Otherwise,
\n # a sentence also terminates newlines.
)""", re.UNICODE | re.VERBOSE)
)""", re.VERBOSE)
class UserAbort(Exception):
pass
def getpass(prompt="Password: "):
if not TEST:
return gp.getpass(bytes(prompt))
else:
return py23_input(prompt)
getpass = gp.getpass
def get_password(validator, keychain=None, max_attempts=3):
@ -67,14 +51,14 @@ def get_password(validator, keychain=None, max_attempts=3):
set_keychain(keychain, None)
attempt = 1
while result is None and attempt < max_attempts:
prompt("Wrong password, try again.")
password = getpass()
print("Wrong password, try again.", file=sys.stderr)
password = gp.getpass()
result = validator(password)
attempt += 1
if result is not None:
return result
else:
prompt("Extremely wrong password.")
print("Extremely wrong password.", file=sys.stderr)
sys.exit(1)
@ -88,57 +72,16 @@ def set_keychain(journal_name, password):
if password is None:
try:
keyring.delete_password('jrnl', journal_name)
except:
except RuntimeError:
pass
elif not TEST:
else:
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):
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
raw = py23_input(prompt)
return {'y': True, 'n': False}.get(raw.lower(), default)
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'}"
response = input(prompt)
return {"y": True, "n": False}.get(response.lower(), default)
def load_config(config_path):
@ -176,7 +119,7 @@ def get_text_from_editor(config, template=""):
os.close(filehandle)
os.remove(tmpfile)
if not raw:
prompt('[Nothing saved to file]')
print('[Nothing saved to file]', file=sys.stderr)
return raw
@ -188,27 +131,11 @@ def colorize(string):
def slugify(string):
"""Slugifies a string.
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)
ascii_string = str(unicodedata.normalize('NFKD', string).encode('ascii', 'ignore'))
if PY3:
ascii_string = ascii_string[1:] # removed the leading 'b'
no_punctuation = re.sub(r'[^\w\s-]', '', ascii_string).strip().lower()
normalized_string = str(unicodedata.normalize('NFKD', string))
no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower()
slug = re.sub(r'[-\s]+', '-', no_punctuation)
return u(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
return slug
def split_title(text):