From b2b842711d7f9c9bdc7f4e71105ca5bdbe5fbee8 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 18 Jul 2013 22:48:46 +0200 Subject: [PATCH 01/21] Ability to parse in args manually --- jrnl/jrnl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 3377b686..e1fa134f 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -29,7 +29,7 @@ xdg_config = os.environ.get('XDG_CONFIG_HOME') CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config') PYCRYPTO = install.module_exists("Crypto") -def parse_args(): +def parse_args(args=None): parser = argparse.ArgumentParser() composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"') @@ -51,7 +51,7 @@ def parse_args(): exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None) exporting.add_argument('--delete-last', dest='delete_last', help='Deletes the last entry from your journal file.', action="store_true") - return parser.parse_args() + return parser.parse_args(args) def guess_mode(args, config): """Guesses the mode (compose, read or export) from the given arguments""" @@ -114,7 +114,7 @@ def update_config(config, new_config, scope): config.update(new_config) -def cli(): +def cli(manual_args=None): if not os.path.exists(CONFIG_PATH): config = install.install_jrnl(CONFIG_PATH) else: @@ -133,7 +133,7 @@ def cli(): print("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.") sys.exit(-1) - args = parse_args() + args = parse_args(manual_args) # If the first textual argument points to a journal file, # use this! From 47c90b6d405bbae2cf1335c4aa969c1fafe6bed1 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 18 Jul 2013 22:49:22 +0200 Subject: [PATCH 02/21] Core testing --- features/configs/basic.json | 14 ++++++++++++++ features/core.feature | 14 ++++++++++++++ features/journals/simple.journal | 5 +++++ features/steps/core.py | 24 ++++++++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 features/configs/basic.json create mode 100644 features/core.feature create mode 100644 features/journals/simple.journal create mode 100644 features/steps/core.py diff --git a/features/configs/basic.json b/features/configs/basic.json new file mode 100644 index 00000000..2dc11d73 --- /dev/null +++ b/features/configs/basic.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/simple.journal" + }, + "tagsymbols": "@" +} diff --git a/features/core.feature b/features/core.feature new file mode 100644 index 00000000..45aa5e26 --- /dev/null +++ b/features/core.feature @@ -0,0 +1,14 @@ +Feature: Basic reading and writing to a journal + + Scenario: Loading a sample journal + Given we use "basic.json" + When we run "jrnl -n 2" + Then we should get no error + Then the output should be + """ + 2013-06-09 15:39 My first entry. + | Everything is alright + + 2013-06-10 15:40 Life is good. + | But I'm better. + """ diff --git a/features/journals/simple.journal b/features/journals/simple.journal new file mode 100644 index 00000000..66d8439c --- /dev/null +++ b/features/journals/simple.journal @@ -0,0 +1,5 @@ +2013-06-09 15:39 My first entry. +Everything is alright + +2013-06-10 15:40 Life is good. +But I'm better. diff --git a/features/steps/core.py b/features/steps/core.py new file mode 100644 index 00000000..44893d41 --- /dev/null +++ b/features/steps/core.py @@ -0,0 +1,24 @@ +from behave import * +from jrnl import Journal, jrnl +import os + +@given('we use "{config_file}"') +def set_config(context, config_file): + full_path = os.path.join("features/configs", config_file) + jrnl.CONFIG_PATH = os.path.abspath(full_path) + +@when('we run "{command}"') +def run(context, command): + args = command.split()[1:] + jrnl.cli(args) + +@then('we should get no error') +def no_error(context): + assert context.failed is False + +@then('the output should be') +def check_output(context): + text = context.text.strip().splitlines() + out = context.stdout_capture.getvalue().strip().splitlines() + for line_text, line_out in zip(text, out): + assert line_text.strip() == line_out.strip() From cd67ad73eabac9b1da6171070bbfe3e453d7b0f3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 11:36:29 +0200 Subject: [PATCH 03/21] Backup and restore config and journal files every time --- features/environment.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 features/environment.py diff --git a/features/environment.py b/features/environment.py new file mode 100644 index 00000000..f374c25a --- /dev/null +++ b/features/environment.py @@ -0,0 +1,22 @@ +from behave import * +import shutil +import os + +def before_scenario(context, scenario): + """Before each scenario, backup all config and journal test data.""" + for folder in ("configs", "journals"): + original = os.path.join("features", folder) + backup = os.path.join("features", folder+"_backup") + if not os.path.exists(backup): + os.mkdir(backup) + for filename in os.listdir(original): + shutil.copy2(os.path.join(original, filename), backup) + +def after_scenario(context, scenario): + """After each scenario, restore all test data and remove backups.""" + for folder in ("configs", "journals"): + original = os.path.join("features", folder) + backup = os.path.join("features", folder+"_backup") + for filename in os.listdir(backup): + shutil.copy2(os.path.join(backup, filename), original) + shutil.rmtree(backup) From af16165159f33d03dda86da35a524c9aafe034d9 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 11:36:39 +0200 Subject: [PATCH 04/21] Tests for writing entries from the command line --- features/core.feature | 11 +++++++++-- features/steps/core.py | 8 +++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/core.feature b/features/core.feature index 45aa5e26..466a1851 100644 --- a/features/core.feature +++ b/features/core.feature @@ -1,10 +1,10 @@ Feature: Basic reading and writing to a journal Scenario: Loading a sample journal - Given we use "basic.json" + Given we use the config "basic.json" When we run "jrnl -n 2" Then we should get no error - Then the output should be + And the output should be """ 2013-06-09 15:39 My first entry. | Everything is alright @@ -12,3 +12,10 @@ Feature: Basic reading and writing to a journal 2013-06-10 15:40 Life is good. | But I'm better. """ + + Scenario: Writing an entry from command line + Given we use the config "basic.json" + When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa." + Then the output should contain "Entry added" + When we run "jrnl -n 1" + Then the output should contain "2013-07-23 09:00 A cold and stormy day." diff --git a/features/steps/core.py b/features/steps/core.py index 44893d41..a4fa5c55 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -2,7 +2,7 @@ from behave import * from jrnl import Journal, jrnl import os -@given('we use "{config_file}"') +@given('we use the config "{config_file}"') def set_config(context, config_file): full_path = os.path.join("features/configs", config_file) jrnl.CONFIG_PATH = os.path.abspath(full_path) @@ -22,3 +22,9 @@ def check_output(context): out = context.stdout_capture.getvalue().strip().splitlines() for line_text, line_out in zip(text, out): assert line_text.strip() == line_out.strip() + +@then('the output should contain "{text}"') +def check_output(context, text): + out = context.stdout_capture.getvalue() + print out + assert text in out From cb9beac711a589feff3d8643c961462d5090be5a Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 11:46:02 +0200 Subject: [PATCH 05/21] Emoji support --- features/core.feature | 8 ++++++++ jrnl/jrnl.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/features/core.feature b/features/core.feature index 466a1851..b56c2f4a 100644 --- a/features/core.feature +++ b/features/core.feature @@ -19,3 +19,11 @@ Feature: Basic reading and writing to a journal Then the output should contain "Entry added" When we run "jrnl -n 1" Then the output should contain "2013-07-23 09:00 A cold and stormy day." + + Scenario: Emoji support + Given we use the config "basic.json" + When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘" + Then the output should contain "Entry added" + When we run "jrnl -n 1" + Then the output should contain "🌞" + and the output should contain "🐘" diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index e1fa134f..45df7a9c 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -171,8 +171,9 @@ def cli(manual_args=None): # Writing mode if mode_compose: raw = " ".join(args.text).strip() - unicode_raw = raw.decode(sys.getfilesystemencoding()) - entry = journal.new_entry(unicode_raw, args.date) + if type(raw) is not unicode: + raw = raw.decode(sys.getfilesystemencoding()) + entry = journal.new_entry(raw, args.date) entry.starred = args.star print("[Entry added to {0} journal]".format(journal_name)) journal.write() From e75c24c69625527a6052b49fc7d5dc04f7901f6a Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 12:43:58 +0200 Subject: [PATCH 06/21] Update travis --- .travis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ecfc9bde..15cf9c92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,13 @@ python: - "2.6" - "2.7" - "3.3" -install: "pip install -r requirements.txt --use-mirrors" +install: + - "pip install -q -r requirements.txt --use-mirrors" + - "pip install -q behave" # command to run tests -script: nosetests +script: + - python --version + - behave matrix: allow_failures: # python 3 support for travis is shaky.... - python: 3.3 From 13f8e668dc00c459f77bab1a9170195b053a6101 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 12:44:39 +0200 Subject: [PATCH 07/21] Mock stdin --- features/core.feature | 9 ++++++++- features/steps/core.py | 33 ++++++++++++++++++++++++++++++--- jrnl/jrnl.py | 2 -- jrnl/util.py | 10 +++++----- tests/test_jrnl.py | 33 --------------------------------- 5 files changed, 43 insertions(+), 44 deletions(-) delete mode 100644 tests/test_jrnl.py diff --git a/features/core.feature b/features/core.feature index b56c2f4a..7e495ef9 100644 --- a/features/core.feature +++ b/features/core.feature @@ -4,7 +4,7 @@ Feature: Basic reading and writing to a journal Given we use the config "basic.json" When we run "jrnl -n 2" Then we should get no error - And the output should be + and the output should be """ 2013-06-09 15:39 My first entry. | Everything is alright @@ -27,3 +27,10 @@ Feature: Basic reading and writing to a journal When we run "jrnl -n 1" Then the output should contain "🌞" and the output should contain "🐘" + + Scenario: Writing an entry at the prompt + Given we use the config "basic.json" + When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive." + Then we should get no error + and the journal should contain "2013-07-25 09:00 I saw Elvis." + and the journal should contain "He's alive." diff --git a/features/steps/core.py b/features/steps/core.py index a4fa5c55..5687e3c9 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -1,16 +1,36 @@ from behave import * -from jrnl import Journal, jrnl +from jrnl import jrnl import os +import sys +import json +import StringIO + +def read_journal(journal_name="default"): + with open(jrnl.CONFIG_PATH) as config_file: + config = json.load(config_file) + with open(config['journals'][journal_name]) as journal_file: + journal = journal_file.read() + return journal @given('we use the config "{config_file}"') def set_config(context, config_file): full_path = os.path.join("features/configs", config_file) jrnl.CONFIG_PATH = os.path.abspath(full_path) +@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 + args = command.split()[1:] + buffer = StringIO.StringIO(text.strip()) + jrnl.util.STDIN = buffer + jrnl.cli(args) + @when('we run "{command}"') def run(context, command): args = command.split()[1:] - jrnl.cli(args) + jrnl.cli(args or None) + @then('we should get no error') def no_error(context): @@ -24,7 +44,14 @@ def check_output(context): assert line_text.strip() == line_out.strip() @then('the output should contain "{text}"') -def check_output(context, text): +def check_output_inline(context, text): out = context.stdout_capture.getvalue() print out assert text in out + +@then('the journal should contain "{text}"') +@then('journal {journal_name} should contain "{text}"') +def check_journal_content(context, text, journal_name="default"): + journal = read_journal(journal_name) + assert text in journal + diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 45df7a9c..3eba8700 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -149,8 +149,6 @@ def cli(manual_args=None): mode_compose, mode_export = guess_mode(args, config) # open journal file or folder - - if os.path.isdir(config['journal']) and ( config['journal'].endswith(".dayone") or \ config['journal'].endswith(".dayone/")): journal = Journal.DayOne(**config) diff --git a/jrnl/util.py b/jrnl/util.py index 9ed4e1f6..5f7f0c52 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,14 +4,14 @@ import sys import os from tzlocal import get_localzone +STDIN = sys.stdin +STDOUT = sys.stdout + __cached_tz = None def py23_input(msg): - if sys.version_info[0] == 3: - try: return input(msg) - except SyntaxError: return "" - else: - return raw_input(msg) + STDOUT.write(msg) + return STDIN.readline().strip() def get_local_timezone(): """Returns the Olson identifier of the local timezone. diff --git a/tests/test_jrnl.py b/tests/test_jrnl.py deleted file mode 100644 index 8280fe6e..00000000 --- a/tests/test_jrnl.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -import unittest - -class TestClasses(unittest.TestCase): - """Test the behavior of the classes. - - tests related to the Journal and the Entry Classes which can - be tested withouth command-line interaction - """ - - def setUp(self): - pass - - def test_colon_in_textbody(self): - """colons should not cause problems in the text body""" - pass - - -class TestCLI(unittest.TestCase): - """test the command-line interaction part of the program""" - - def setUp(self): - pass - - def test_something(self): - """first test""" - pass - - -if __name__ == '__main__': - unittest.main() From 29005c0e0799f5daac55231254e7c20bcf80fdb7 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 13:03:27 +0200 Subject: [PATCH 08/21] Better Python2.6 compatibility --- features/steps/core.py | 1 - jrnl/Entry.py | 2 +- jrnl/Journal.py | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 5687e3c9..4b57b47e 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -46,7 +46,6 @@ def check_output(context): @then('the output should contain "{text}"') def check_output_inline(context, text): out = context.stdout_capture.getvalue() - print out assert text in out @then('the journal should contain "{text}"') diff --git a/jrnl/Entry.py b/jrnl/Entry.py index a8ec0557..e8da761b 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -15,7 +15,7 @@ class Entry: def parse_tags(self): fulltext = " ".join([self.title, self.body]).lower() - tags = re.findall(r'(?u)([{}]\w+)'.format(self.journal.config['tagsymbols']), fulltext, re.UNICODE) + tags = re.findall(r'(?u)([{tags}]\w+)'.format(tags=self.journal.config['tagsymbols']), fulltext, re.UNICODE) return set(tags) def __unicode__(self): diff --git a/jrnl/Journal.py b/jrnl/Journal.py index d4e733ea..60118184 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -152,7 +152,8 @@ class Journal(object): except ValueError: # Happens when we can't parse the start of the line as an date. # In this case, just append line to our body. - current_entry.body += line + "\n" + if current_entry: + current_entry.body += line + "\n" # Append last entry if current_entry: @@ -173,7 +174,7 @@ class Journal(object): lambda match: self._colorize(match.group(0)), pp, re.UNICODE) else: - pp = re.sub(r"(?u)([{}]\w+)".format(self.config['tagsymbols']), + pp = re.sub(r"(?u)([{tags}]\w+)".format(tags=self.config['tagsymbols']), lambda match: self._colorize(match.group(0)), pp) return pp From 03c1395c0153a075f6661864d48fc81dc25404da Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 13:09:33 +0200 Subject: [PATCH 09/21] Python 3 compatibility for tests --- features/steps/core.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 4b57b47e..130fe5b4 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -3,7 +3,10 @@ from jrnl import jrnl import os import sys import json -import StringIO +try: + from io import StringIO +except ImportError: + from cStringIO import StringIO def read_journal(journal_name="default"): with open(jrnl.CONFIG_PATH) as config_file: @@ -22,7 +25,7 @@ def set_config(context, config_file): def run_with_input(context, command, inputs=None): text = inputs or context.text args = command.split()[1:] - buffer = StringIO.StringIO(text.strip()) + buffer = StringIO(text.strip()) jrnl.util.STDIN = buffer jrnl.cli(args) From f9bdc13210fad65109b41b01eac94fe78d61a85a Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 13:24:18 +0200 Subject: [PATCH 10/21] Python 3 improvements --- jrnl/Journal.py | 5 ++++- jrnl/exporters.py | 9 ++++++--- jrnl/jrnl.py | 4 ++-- jrnl/util.py | 7 ++++++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 60118184..4c49ad39 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -179,13 +179,16 @@ class Journal(object): pp) return pp + def pprint(self): + return self.__unicode__() + def __repr__(self): return "".format(len(self.entries)) def write(self, filename=None): """Dumps the journal into the config file, overwriting it""" filename = filename or self.config['journal'] - journal = "\n".join([unicode(e) for e in self.entries]) + journal = "\n".join([e.__unicode__() for e in self.entries]) if self.config['encrypt']: journal = self._encrypt(journal) with open(filename, 'wb') as journal_file: diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 5bd687cf..4e2a9492 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -7,6 +7,9 @@ try: from slugify import slugify except ImportError: import slugify try: import simplejson as json except ImportError: import json +try: from .util import u +except (SystemError, ValueError): from util import u + def get_tags_count(journal): """Returns a set of tuples (count, tag) for all tags present in the journal.""" @@ -60,7 +63,7 @@ def to_md(journal): def to_txt(journal): """Returns the complete text of the Journal.""" - return unicode(journal) + return journal.pprint() def export(journal, format, output=None): """Exports the journal to various formats. @@ -93,7 +96,7 @@ def export(journal, format, output=None): def write_files(journal, path, format): """Turns your journal into separate files for each entry. Format should be either json, md or txt.""" - make_filename = lambda entry: e.date.strftime("%C-%m-%d_{}.{}".format(slugify(unicode(e.title)), format)) + make_filename = lambda entry: e.date.strftime("%C-%m-%d_{}.{}".format(slugify(u(e.title)), format)) for e in journal.entries: full_path = os.path.join(path, make_filename(e)) if format == 'json': @@ -101,7 +104,7 @@ def write_files(journal, path, format): elif format == 'md': content = e.to_md() elif format == 'txt': - content = unicode(e) + content = u(e) with open(full_path, 'w') as f: f.write(content) return "[Journal exported individual files in {}]".format(path) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 3eba8700..37a310b7 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -169,7 +169,7 @@ def cli(manual_args=None): # Writing mode if mode_compose: raw = " ".join(args.text).strip() - if type(raw) is not unicode: + if util.PY2 and type(raw) is not unicode: raw = raw.decode(sys.getfilesystemencoding()) entry = journal.new_entry(raw, args.date) entry.starred = args.star @@ -183,7 +183,7 @@ def cli(manual_args=None): strict=args.strict, short=args.short) journal.limit(args.limit) - print(unicode(journal)) + print(journal.pprint()) # Various export modes elif args.tags: diff --git a/jrnl/util.py b/jrnl/util.py index 5f7f0c52..4279bab0 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -3,10 +3,15 @@ import sys import os from tzlocal import get_localzone +PY3 = sys.version_info[0] == 3 +PY2 = sys.version_info[0] == 2 + +def u(s): + """Mock unicode function for python 2 and 3 compatibility.""" + return s if PY3 else unicode(s, "unicode_escape") STDIN = sys.stdin STDOUT = sys.stdout - __cached_tz = None def py23_input(msg): From 4b9b5e827b7df031fde586ecc29f8401f15619dd Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 10:11:37 +0200 Subject: [PATCH 11/21] Tests for multiple journals --- features/configs/multiple.json | 16 +++++++++++++ features/environment.py | 2 ++ features/journals/work.journal | 0 features/multiple_journals.feature | 36 ++++++++++++++++++++++++++++++ features/steps/core.py | 34 ++++++++++++++++++++++++++-- jrnl/Journal.py | 1 - 6 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 features/configs/multiple.json create mode 100644 features/journals/work.journal create mode 100644 features/multiple_journals.feature diff --git a/features/configs/multiple.json b/features/configs/multiple.json new file mode 100644 index 00000000..af7a3e15 --- /dev/null +++ b/features/configs/multiple.json @@ -0,0 +1,16 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/simple.journal", + "work": "features/journals/work.journal", + "ideas": "features/journals/nothing.journal" + }, + "tagsymbols": "@" +} diff --git a/features/environment.py b/features/environment.py index f374c25a..59763616 100644 --- a/features/environment.py +++ b/features/environment.py @@ -17,6 +17,8 @@ def after_scenario(context, scenario): for folder in ("configs", "journals"): original = os.path.join("features", folder) backup = os.path.join("features", folder+"_backup") + for filename in os.listdir(original): + os.remove(os.path.join(original, filename)) for filename in os.listdir(backup): shutil.copy2(os.path.join(backup, filename), original) shutil.rmtree(backup) diff --git a/features/journals/work.journal b/features/journals/work.journal new file mode 100644 index 00000000..e69de29b diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature new file mode 100644 index 00000000..fb026d2e --- /dev/null +++ b/features/multiple_journals.feature @@ -0,0 +1,36 @@ +Feature: Multiple journals + + Scenario: Loading a config with two journals + Given we use the config "multiple.json" + Then journal "default" should have 2 entries + and journal "work" should have 0 entries + + Scenario: Write to default config by default + Given we use the config "multiple.json" + When we run "jrnl this goes to default" + Then journal "default" should have 3 entries + and journal "work" should have 0 entries + + Scenario: Write to specified journal + Given we use the config "multiple.json" + When we run "jrnl work a long day in the office" + Then journal "default" should have 2 entries + and journal "work" should have 1 entry + + Scenario: Tell user which journal was used + Given we use the config "multiple.json" + When we run "jrnl work a long day in the office" + Then the output should contain "Entry added to work journal" + + Scenario: Write to specified journal with a timestamp + Given we use the config "multiple.json" + When we run "jrnl work 23 july 2012: a long day in the office" + Then journal "default" should have 2 entries + and journal "work" should have 1 entry + and journal "work" should contain "2012-07-23" + + Scenario: Create new journals as required + Given we use the config "multiple.json" + Then journal "ideas" should not exist + When we run "jrnl ideas 23 july 2012: sell my junk on ebay and make lots of money" + Then journal "ideas" should have 1 entry diff --git a/features/steps/core.py b/features/steps/core.py index 130fe5b4..36369edc 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -1,5 +1,5 @@ from behave import * -from jrnl import jrnl +from jrnl import jrnl, Journal import os import sys import json @@ -15,6 +15,16 @@ def read_journal(journal_name="default"): journal = journal_file.read() return journal +def open_journal(journal_name="default"): + with open(jrnl.CONFIG_PATH) as config_file: + config = json.load(config_file) + journals = config['journals'] + if type(journals) is dict: # We can override the default config on a by-journal basis + config['journal'] = journals.get(journal_name) + else: # But also just give them a string to point to the journal file + config['journal'] = journal + return Journal.Journal(**config) + @given('we use the config "{config_file}"') def set_config(context, config_file): full_path = os.path.join("features/configs", config_file) @@ -52,8 +62,28 @@ def check_output_inline(context, text): assert text in out @then('the journal should contain "{text}"') -@then('journal {journal_name} should contain "{text}"') +@then('journal "{journal_name}" should contain "{text}"') def check_journal_content(context, text, journal_name="default"): journal = read_journal(journal_name) assert text in journal +@then('journal "{journal_name}" should not exist') +def journal_doesnt_exist(context, journal_name="default"): + with open(jrnl.CONFIG_PATH) as config_file: + config = json.load(config_file) + journal_path = config['journals'][journal_name] + print journal_path + assert not os.path.exists(journal_path) + +@then('the journal should have {number:d} entries') +@then('the journal should have {number:d} entry') +@then('journal "{journal_name}" should have {number:d} entries') +@then('journal "{journal_name}" should have {number:d} entry') +def check_journal_content(context, number, journal_name="default"): + journal = open_journal(journal_name) + assert len(journal.entries) == number + +@then('fail') +def debug_fail(context): + assert False + diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 4c49ad39..9deb2a38 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -50,7 +50,6 @@ class Journal(object): 'linewrap': 80, } self.config.update(kwargs) - # Set up date parser consts = pdt.Constants(usePyICU=False) consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday From d3edbfd53b36aa3a7af596a4a9f2b3264d9f16ad Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 12:04:01 +0200 Subject: [PATCH 12/21] Uses stderr for prompts instead stdout --- CHANGELOG.md | 4 ++++ jrnl/Journal.py | 10 +++++----- jrnl/__init__.py | 2 +- jrnl/jrnl.py | 21 +++++++++++---------- jrnl/util.py | 5 +++++ 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a44755dc..1d1fb676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +### 1.3.2 + +* [Improved] Everything that is not direct output of jrnl will be written stderr to improve integration + ### 1.3.0 * [New] Export to multiple files diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 9deb2a38..827c9551 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -3,8 +3,8 @@ try: from . import Entry except (SystemError, ValueError): import Entry -try: from .util import get_local_timezone -except (SystemError, ValueError): from util import get_local_timezone +try: from .util import get_local_timezone, prompt +except (SystemError, ValueError): from util import get_local_timezone, prompt import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -76,7 +76,7 @@ class Journal(object): try: plain = crypto.decrypt(cipher[16:]) except ValueError: - print("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") + prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") sys.exit(-1) if plain[-1] != " ": # Journals are always padded return None @@ -118,9 +118,9 @@ class Journal(object): attempts += 1 self.config['password'] = None # This password doesn't work. if attempts < 3: - print("Wrong password, try again.") + prompt("Wrong password, try again.") else: - print("Extremely wrong password.") + prompt("Extremely wrong password.") sys.exit(-1) journal = decrypted else: diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 7ca6b98e..d17a3b01 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.3.1' +__version__ = '1.3.2' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 37a310b7..ef78840b 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -29,6 +29,7 @@ xdg_config = os.environ.get('XDG_CONFIG_HOME') CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config') PYCRYPTO = install.module_exists("Crypto") + def parse_args(args=None): parser = argparse.ArgumentParser() composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') @@ -77,7 +78,7 @@ def get_text_from_editor(config): raw = f.read() os.remove(tmpfile) else: - print('[Nothing saved to file]') + util.prompt('[Nothing saved to file]') raw = '' return raw @@ -89,19 +90,19 @@ def encrypt(journal, filename=None): journal.make_key(prompt="Enter new password:") journal.config['encrypt'] = True journal.write(filename) - print("Journal encrypted to {0}.".format(filename or journal.config['journal'])) + util.prompt("Journal encrypted to {0}.".format(filename or journal.config['journal'])) def decrypt(journal, filename=None): """ Decrypts into new file. If filename is not set, we encrypt the journal file itself. """ journal.config['encrypt'] = False journal.config['password'] = "" journal.write(filename) - print("Journal decrypted to {0}.".format(filename or journal.config['journal'])) + util.prompt("Journal decrypted to {0}.".format(filename or journal.config['journal'])) def touch_journal(filename): """If filename does not exist, touch the file""" if not os.path.exists(filename): - print("[Journal created at {0}]".format(filename)) + util.prompt("[Journal created at {0}]".format(filename)) open(filename, 'a').close() def update_config(config, new_config, scope): @@ -122,15 +123,15 @@ def cli(manual_args=None): try: config = json.load(f) except ValueError as e: - print("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message)) - print("[Entry was NOT added to your journal]") + util.prompt("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message)) + util.prompt("[Entry was NOT added to your journal]") sys.exit(-1) install.update_config(config, config_path=CONFIG_PATH) original_config = config.copy() # check if the configuration is supported by available modules if config['encrypt'] and not PYCRYPTO: - print("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.") + util.prompt("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.") sys.exit(-1) args = parse_args(manual_args) @@ -173,7 +174,7 @@ def cli(manual_args=None): raw = raw.decode(sys.getfilesystemencoding()) entry = journal.new_entry(raw, args.date) entry.starred = args.star - print("[Entry added to {0} journal]".format(journal_name)) + util.prompt("[Entry added to {0} journal]".format(journal_name)) journal.write() # Reading mode @@ -193,7 +194,7 @@ def cli(manual_args=None): print(exporters.export(journal, args.export, args.output)) elif (args.encrypt is not False or args.decrypt is not False) and not PYCRYPTO: - print("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.") + util.prompt("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.") elif args.encrypt is not False: encrypt(journal, filename=args.encrypt) @@ -211,7 +212,7 @@ def cli(manual_args=None): elif args.delete_last: last_entry = journal.entries.pop() - print("[Deleted Entry:]") + util.prompt("[Deleted Entry:]") print(last_entry) journal.write() diff --git a/jrnl/util.py b/jrnl/util.py index 4279bab0..ac432fbf 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -11,9 +11,14 @@ def u(s): return s if PY3 else unicode(s, "unicode_escape") STDIN = sys.stdin +STDERR = sys.stderr STDOUT = sys.stdout __cached_tz = None +def prompt(msg): + """Prints a message to the std err stream defined in util.""" + print(msg, file=STDERR) + def py23_input(msg): STDOUT.write(msg) return STDIN.readline().strip() From 279547c35092a36347aec78b6dffb7a06f61be96 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 12:04:32 +0200 Subject: [PATCH 13/21] Tests for using stderr prompts --- features/core.feature | 4 ++-- features/environment.py | 9 +++++++++ features/multiple_journals.feature | 2 +- features/steps/core.py | 6 +++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/features/core.feature b/features/core.feature index 7e495ef9..d5a6fa7f 100644 --- a/features/core.feature +++ b/features/core.feature @@ -16,14 +16,14 @@ Feature: Basic reading and writing to a journal Scenario: Writing an entry from command line Given we use the config "basic.json" When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa." - Then the output should contain "Entry added" + Then we should see the message "Entry added" When we run "jrnl -n 1" Then the output should contain "2013-07-23 09:00 A cold and stormy day." Scenario: Emoji support Given we use the config "basic.json" When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘" - Then the output should contain "Entry added" + Then we should see the message "Entry added" When we run "jrnl -n 1" Then the output should contain "🌞" and the output should contain "🐘" diff --git a/features/environment.py b/features/environment.py index 59763616..bebc2150 100644 --- a/features/environment.py +++ b/features/environment.py @@ -1,9 +1,16 @@ from behave import * import shutil import os +from jrnl 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 for folder in ("configs", "journals"): original = os.path.join("features", folder) backup = os.path.join("features", folder+"_backup") @@ -14,6 +21,8 @@ def before_scenario(context, scenario): def after_scenario(context, scenario): """After each scenario, restore all test data and remove backups.""" + context.messages.close() + context.messages = None for folder in ("configs", "journals"): original = os.path.join("features", folder) backup = os.path.join("features", folder+"_backup") diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature index fb026d2e..0510209b 100644 --- a/features/multiple_journals.feature +++ b/features/multiple_journals.feature @@ -20,7 +20,7 @@ Feature: Multiple journals Scenario: Tell user which journal was used Given we use the config "multiple.json" When we run "jrnl work a long day in the office" - Then the output should contain "Entry added to work journal" + Then we should see the message "Entry added to work journal" Scenario: Write to specified journal with a timestamp Given we use the config "multiple.json" diff --git a/features/steps/core.py b/features/steps/core.py index 36369edc..bab0ee96 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -61,6 +61,11 @@ def check_output_inline(context, text): out = context.stdout_capture.getvalue() assert text in out +@then('we should see the message "{text}"') +def check_message(context, text): + out = context.messages.getvalue() + assert text in out + @then('the journal should contain "{text}"') @then('journal "{journal_name}" should contain "{text}"') def check_journal_content(context, text, journal_name="default"): @@ -72,7 +77,6 @@ def journal_doesnt_exist(context, journal_name="default"): with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) journal_path = config['journals'][journal_name] - print journal_path assert not os.path.exists(journal_path) @then('the journal should have {number:d} entries') From a84713e99ac65dbc938ab3b3074a32d99c2e7e52 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 20:08:41 +0200 Subject: [PATCH 14/21] Allows getpass to get bypassed by reading from stdin --- jrnl/Journal.py | 9 +++++---- jrnl/util.py | 25 ++++++++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 827c9551..43d1d2e6 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -3,8 +3,8 @@ try: from . import Entry except (SystemError, ValueError): import Entry -try: from .util import get_local_timezone, prompt -except (SystemError, ValueError): from util import get_local_timezone, prompt +try: from .util import get_local_timezone, prompt, getpass +except (SystemError, ValueError): from util import get_local_timezone, prompt, getpass import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -25,7 +25,6 @@ except ImportError: if "win32" in sys.platform: import pyreadline as readline else: import readline import hashlib -import getpass try: import colorama colorama.init() @@ -89,6 +88,7 @@ class Journal(object): sys.exit("Error: PyCrypto is not installed.") atfork() # A seed for PyCrypto iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16)) + print("iv", iv, len(iv)) crypto = AES.new(self.key, AES.MODE_CBC, iv) if len(plain) % 16 != 0: plain += " " * (16 - len(plain) % 16) @@ -98,7 +98,8 @@ class Journal(object): def make_key(self, prompt="Password: "): """Creates an encryption key from the default password or prompts for a new password.""" - password = self.config['password'] or getpass.getpass(prompt) + password = self.config['password'] or getpass(prompt) + print("GOT PWD", password) self.key = hashlib.sha256(password.encode('utf-8')).digest() def open(self, filename=None): diff --git a/jrnl/util.py b/jrnl/util.py index ac432fbf..54a60c66 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -3,24 +3,35 @@ import sys import os from tzlocal import get_localzone +import getpass as gp + 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 + +def getpass(prompt): + if not TEST: + return gp.getpass(prompt) + else: + return py23_input(prompt) + def u(s): """Mock unicode function for python 2 and 3 compatibility.""" return s if PY3 else unicode(s, "unicode_escape") -STDIN = sys.stdin -STDERR = sys.stderr -STDOUT = sys.stdout -__cached_tz = None - def prompt(msg): """Prints a message to the std err stream defined in util.""" - print(msg, file=STDERR) + if not msg.endswith("\n"): + msg += "\n" + STDERR.write(u(msg)) def py23_input(msg): - STDOUT.write(msg) + STDERR.write(u(msg)) return STDIN.readline().strip() def get_local_timezone(): From c0733f36c55120451bf1651b5520b403570719b5 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 20:08:53 +0200 Subject: [PATCH 15/21] Tests for encryption --- features/configs/encrypted.json | 14 ++++++++++++++ features/encryption.feature | 24 ++++++++++++++++++++++++ features/environment.py | 1 + features/journals/encrypted.journal | 3 +++ features/steps/core.py | 14 ++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 features/configs/encrypted.json create mode 100644 features/encryption.feature create mode 100644 features/journals/encrypted.journal diff --git a/features/configs/encrypted.json b/features/configs/encrypted.json new file mode 100644 index 00000000..a498974b --- /dev/null +++ b/features/configs/encrypted.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": true, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/encrypted.journal" + }, + "tagsymbols": "@" +} diff --git a/features/encryption.feature b/features/encryption.feature new file mode 100644 index 00000000..c7f94d62 --- /dev/null +++ b/features/encryption.feature @@ -0,0 +1,24 @@ + Feature: Multiple journals + + Scenario: Loading an encrypted journal + Given we use the config "encrypted.json" + When we run "jrnl -n 1" and enter "bad doggie no biscuit" + Then we should see the message "Password" + and the output should contain "2013-06-10 15:40 Life is good" + + Scenario: Decrypting a journal + Given we use the config "encrypted.json" + When we run "jrnl --decrypt" and enter "bad doggie no biscuit" + Then we should see the message "Journal decrypted" + and the journal should have 2 entries + and the config should have "encrypt" set to "bool:False" + + Scenario: Encrypting a journal + Given we use the config "basic.json" + When we run "jrnl --encrypt" and enter "swordfish" + Then we should see the message "Journal encrypted" + and the config should have "encrypt" set to "bool:True" + When we run "jrnl -n 1" and enter "swordish" + Then we should see the message "Password" + and the output should contain "2013-06-10 15:40 Life is good" + diff --git a/features/environment.py b/features/environment.py index bebc2150..a25d2fff 100644 --- a/features/environment.py +++ b/features/environment.py @@ -11,6 +11,7 @@ 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 for folder in ("configs", "journals"): original = os.path.join("features", folder) backup = os.path.join("features", folder+"_backup") diff --git a/features/journals/encrypted.journal b/features/journals/encrypted.journal new file mode 100644 index 00000000..1c40a799 --- /dev/null +++ b/features/journals/encrypted.journal @@ -0,0 +1,3 @@ +~|5\< +hqFCZ[\ELxy +eowW( O4;p[fD$K7 4C{&;duj|Z@?WGݕW ,z2 \ No newline at end of file diff --git a/features/steps/core.py b/features/steps/core.py index bab0ee96..531410aa 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -79,6 +79,20 @@ def journal_doesnt_exist(context, journal_name="default"): journal_path = config['journals'][journal_name] assert not os.path.exists(journal_path) +@then('the config should have "{key}" set to "{value}"') +def config_var(context, key, value): + t, value = value.split(":") + value = { + "bool": lambda v: v.lower() == "true", + "int": int, + "str": str + }[t](value) + with open(jrnl.CONFIG_PATH) as config_file: + config = json.load(config_file) + assert key in config + print key, config[key], type(config[key]), value, type(value) + assert config[key] == value + @then('the journal should have {number:d} entries') @then('the journal should have {number:d} entry') @then('journal "{journal_name}" should have {number:d} entries') From dbf5caa971a1ad8797ae9ccbf7018a7b29ef7888 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 21:19:30 +0200 Subject: [PATCH 16/21] Changes cleaning strategy --- features/{ => data}/configs/basic.json | 0 features/{ => data}/configs/encrypted.json | 0 features/{ => data}/configs/multiple.json | 0 features/data/journals/encrypted.journal | Bin 0 -> 128 bytes features/{ => data}/journals/simple.journal | 0 features/{ => data}/journals/work.journal | 0 features/environment.py | 21 ++++++++------------ features/journals/encrypted.journal | 3 --- 8 files changed, 8 insertions(+), 16 deletions(-) rename features/{ => data}/configs/basic.json (100%) rename features/{ => data}/configs/encrypted.json (100%) rename features/{ => data}/configs/multiple.json (100%) create mode 100644 features/data/journals/encrypted.journal rename features/{ => data}/journals/simple.journal (100%) rename features/{ => data}/journals/work.journal (100%) delete mode 100644 features/journals/encrypted.journal diff --git a/features/configs/basic.json b/features/data/configs/basic.json similarity index 100% rename from features/configs/basic.json rename to features/data/configs/basic.json diff --git a/features/configs/encrypted.json b/features/data/configs/encrypted.json similarity index 100% rename from features/configs/encrypted.json rename to features/data/configs/encrypted.json diff --git a/features/configs/multiple.json b/features/data/configs/multiple.json similarity index 100% rename from features/configs/multiple.json rename to features/data/configs/multiple.json diff --git a/features/data/journals/encrypted.journal b/features/data/journals/encrypted.journal new file mode 100644 index 0000000000000000000000000000000000000000..339b47baf9671f4550efeb9b6a0cfcd5032255d6 GIT binary patch literal 128 zcmV-`0Du3(bJIGVsY(mXmoW-2hF&*L`0NbJTYlTUr8*^Qm97}8E^3^1bZ$P^M literal 0 HcmV?d00001 diff --git a/features/journals/simple.journal b/features/data/journals/simple.journal similarity index 100% rename from features/journals/simple.journal rename to features/data/journals/simple.journal diff --git a/features/journals/work.journal b/features/data/journals/work.journal similarity index 100% rename from features/journals/work.journal rename to features/data/journals/work.journal diff --git a/features/environment.py b/features/environment.py index a25d2fff..89125fca 100644 --- a/features/environment.py +++ b/features/environment.py @@ -13,22 +13,17 @@ def before_scenario(context, scenario): jrnl.util.STDERR = context.messages jrnl.util.TEST = True for folder in ("configs", "journals"): - original = os.path.join("features", folder) - backup = os.path.join("features", folder+"_backup") - if not os.path.exists(backup): - os.mkdir(backup) + original = os.path.join("features", "data", folder) + working_dir = os.path.join("features", folder) + if not os.path.exists(working_dir): + os.mkdir(working_dir) for filename in os.listdir(original): - shutil.copy2(os.path.join(original, filename), backup) + shutil.copy2(os.path.join(original, filename), working_dir) def after_scenario(context, scenario): - """After each scenario, restore all test data and remove backups.""" + """After each scenario, restore all test data and remove working_dirs.""" context.messages.close() context.messages = None for folder in ("configs", "journals"): - original = os.path.join("features", folder) - backup = os.path.join("features", folder+"_backup") - for filename in os.listdir(original): - os.remove(os.path.join(original, filename)) - for filename in os.listdir(backup): - shutil.copy2(os.path.join(backup, filename), original) - shutil.rmtree(backup) + working_dir = os.path.join("features", folder) + shutil.rmtree(working_dir) diff --git a/features/journals/encrypted.journal b/features/journals/encrypted.journal deleted file mode 100644 index 1c40a799..00000000 --- a/features/journals/encrypted.journal +++ /dev/null @@ -1,3 +0,0 @@ -~|5\< -hqFCZ[\ELxy -eowW( O4;p[fD$K7 4C{&;duj|Z@?WGݕW ,z2 \ No newline at end of file From 84556c178a59538c7c98bfbba6a31eadc9e69ca0 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 21:24:19 +0200 Subject: [PATCH 17/21] Unifies encryption between python versions --- CHANGELOG.md | 4 ++++ jrnl/Journal.py | 37 +++++++++++++++++-------------------- jrnl/__init__.py | 2 +- jrnl/util.py | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1fb676..e27814bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +### 1.4.0 + +* [Improved] Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`). + ### 1.3.2 * [Improved] Everything that is not direct output of jrnl will be written stderr to improve integration diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 43d1d2e6..7c680b21 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -3,8 +3,8 @@ try: from . import Entry except (SystemError, ValueError): import Entry -try: from .util import get_local_timezone, prompt, getpass -except (SystemError, ValueError): from util import get_local_timezone, prompt, getpass +try: from . import util +except (SystemError, ValueError): import util import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -18,7 +18,7 @@ import sys import glob try: from Crypto.Cipher import AES - from Crypto.Random import random, atfork + from Crypto import Random crypto_installed = True except ImportError: crypto_installed = False @@ -75,32 +75,29 @@ class Journal(object): try: plain = crypto.decrypt(cipher[16:]) except ValueError: - prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") + util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") sys.exit(-1) - if plain[-1] != " ": # Journals are always padded + padding = " ".encode("utf-8") + if not plain.endswith(padding): # Journals are always padded return None else: - return plain + return plain.decode("utf-8") def _encrypt(self, plain): """Encrypt a plaintext string using self.key as the key""" if not crypto_installed: sys.exit("Error: PyCrypto is not installed.") - atfork() # A seed for PyCrypto - iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16)) - print("iv", iv, len(iv)) + Random.atfork() # A seed for PyCrypto + iv = Random.new().read(AES.block_size) crypto = AES.new(self.key, AES.MODE_CBC, iv) - if len(plain) % 16 != 0: - plain += " " * (16 - len(plain) % 16) - else: # Always pad so we can detect properly decrypted files :) - plain += " " * 16 + plain = plain.encode("utf-8") + plain += b" " * (AES.block_size - len(plain) % AES.block_size) return iv + crypto.encrypt(plain) def make_key(self, prompt="Password: "): """Creates an encryption key from the default password or prompts for a new password.""" - password = self.config['password'] or getpass(prompt) - print("GOT PWD", password) - self.key = hashlib.sha256(password.encode('utf-8')).digest() + password = self.config['password'] or util.getpass(prompt) + self.key = hashlib.sha256(password.encode("utf-8")).digest() def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. @@ -119,9 +116,9 @@ class Journal(object): attempts += 1 self.config['password'] = None # This password doesn't work. if attempts < 3: - prompt("Wrong password, try again.") + util.prompt("Wrong password, try again.") else: - prompt("Extremely wrong password.") + util.prompt("Extremely wrong password.") sys.exit(-1) journal = decrypted else: @@ -318,7 +315,7 @@ class DayOne(Journal): try: timezone = pytz.timezone(dict_entry['Time Zone']) except pytz.exceptions.UnknownTimeZoneError: - timezone = pytz.timezone(get_local_timezone()) + timezone = pytz.timezone(util.get_local_timezone()) date = dict_entry['Creation Date'] date = date + timezone.utcoffset(date) entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False) @@ -344,7 +341,7 @@ class DayOne(Journal): 'Creation Date': utc_time, 'Starred': entry.starred if hasattr(entry, 'starred') else False, 'Entry Text': entry.title+"\n"+entry.body, - 'Time Zone': get_local_timezone(), + 'Time Zone': util.get_local_timezone(), 'UUID': new_uuid } plistlib.writePlist(entry_plist, filename) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index d17a3b01..d41e846b 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.3.2' +__version__ = '1.4.0' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/jrnl/util.py b/jrnl/util.py index 54a60c66..28499933 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -22,7 +22,7 @@ def getpass(prompt): def u(s): """Mock unicode function for python 2 and 3 compatibility.""" - return s if PY3 else unicode(s, "unicode_escape") + return s if PY3 or type(s) is unicode else unicode(s, "unicode_escape") def prompt(msg): """Prints a message to the std err stream defined in util.""" From 95e766015914d244a658a53bc7863cb95e04b419 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 21:24:25 +0200 Subject: [PATCH 18/21] Fixes encryption tests --- features/encryption.feature | 2 +- features/steps/core.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/features/encryption.feature b/features/encryption.feature index c7f94d62..a18e2477 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -18,7 +18,7 @@ When we run "jrnl --encrypt" and enter "swordfish" Then we should see the message "Journal encrypted" and the config should have "encrypt" set to "bool:True" - When we run "jrnl -n 1" and enter "swordish" + When we run "jrnl -n 1" and enter "swordfish" Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" diff --git a/features/steps/core.py b/features/steps/core.py index 531410aa..b3675c1c 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -90,7 +90,6 @@ def config_var(context, key, value): with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) assert key in config - print key, config[key], type(config[key]), value, type(value) assert config[key] == value @then('the journal should have {number:d} entries') From 19f6fd3672d74376109c429434ee5ed4d0494f1f Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 21:26:21 +0200 Subject: [PATCH 19/21] Test for decrypting journals when password is saved in config --- features/data/configs/encrypted_with_pw.json | 14 ++++++++++++++ features/encryption.feature | 5 +++++ 2 files changed, 19 insertions(+) create mode 100644 features/data/configs/encrypted_with_pw.json diff --git a/features/data/configs/encrypted_with_pw.json b/features/data/configs/encrypted_with_pw.json new file mode 100644 index 00000000..1a277240 --- /dev/null +++ b/features/data/configs/encrypted_with_pw.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": true, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "bad doggie no biscuit", + "journals": { + "default": "features/journals/encrypted.journal" + }, + "tagsymbols": "@" +} diff --git a/features/encryption.feature b/features/encryption.feature index a18e2477..d134c3bb 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -6,6 +6,11 @@ Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" + Scenario: Loading an encrypted journal with password in config + Given we use the config "encrypted_with_pw.json" + When we run "jrnl -n 1" + Then the output should contain "2013-06-10 15:40 Life is good" + Scenario: Decrypting a journal Given we use the config "encrypted.json" When we run "jrnl --decrypt" and enter "bad doggie no biscuit" From db118037912757ce997baaaa18cd10228773b426 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 23 Jul 2013 21:01:57 -0700 Subject: [PATCH 20/21] Tagging fixes --- CHANGELOG.md | 4 ++++ jrnl/Entry.py | 1 + jrnl/__init__.py | 2 +- jrnl/exporters.py | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e27814bf..e9563f6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +### 1.4.1 + +* [Fixed] Tagging works again + ### 1.4.0 * [Improved] Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`). diff --git a/jrnl/Entry.py b/jrnl/Entry.py index e8da761b..5fbdeb15 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -16,6 +16,7 @@ class Entry: def parse_tags(self): fulltext = " ".join([self.title, self.body]).lower() tags = re.findall(r'(?u)([{tags}]\w+)'.format(tags=self.journal.config['tagsymbols']), fulltext, re.UNICODE) + self.tags = tags return set(tags) def __unicode__(self): diff --git a/jrnl/__init__.py b/jrnl/__init__.py index d41e846b..e6c3253f 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.4.0' +__version__ = '1.4.1' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 4e2a9492..42126802 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -32,7 +32,7 @@ def to_tag_list(journal): elif min(tag_counts)[0] == 0: tag_counts = filter(lambda x: x[0] > 1, tag_counts) result += '[Removed tags that appear only once.]\n' - result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=False)) + result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) return result def to_json(journal): From 1dfbfc2eaa7a2131d1024f1cacebf5372218d65b Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 23 Jul 2013 21:02:03 -0700 Subject: [PATCH 21/21] Tests for tagging --- features/data/configs/tags.json | 14 ++++++++++++++ features/data/journals/tags.journal | 7 +++++++ features/tagging.feature | 12 ++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 features/data/configs/tags.json create mode 100644 features/data/journals/tags.journal create mode 100644 features/tagging.feature diff --git a/features/data/configs/tags.json b/features/data/configs/tags.json new file mode 100644 index 00000000..dc69950c --- /dev/null +++ b/features/data/configs/tags.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/tags.journal" + }, + "tagsymbols": "@" +} diff --git a/features/data/journals/tags.journal b/features/data/journals/tags.journal new file mode 100644 index 00000000..7b5cdf04 --- /dev/null +++ b/features/data/journals/tags.journal @@ -0,0 +1,7 @@ +2013-06-09 15:39 I have an @idea: +(1) write a command line @journal software +(2) ??? +(3) PROFIT! + +2013-06-10 15:40 I met with @dan. +As alway's he shared his latest @idea on how to rule the world with me. diff --git a/features/tagging.feature b/features/tagging.feature new file mode 100644 index 00000000..a030d610 --- /dev/null +++ b/features/tagging.feature @@ -0,0 +1,12 @@ +Feature: Tagging + + Scenario: Displaying tags + Given we use the config "tags.json" + When we run "jrnl --tags" + Then we should get no error + and the output should be + """ + @idea : 2 + @journal : 1 + @dan : 1 + """