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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a44755dc..e9563f6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ 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`). + +### 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/features/core.feature b/features/core.feature new file mode 100644 index 00000000..d5a6fa7f --- /dev/null +++ b/features/core.feature @@ -0,0 +1,36 @@ +Feature: Basic reading and writing to a journal + + Scenario: Loading a sample 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 + """ + 2013-06-09 15:39 My first entry. + | Everything is alright + + 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 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 we should see the message "Entry added" + 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/data/configs/basic.json b/features/data/configs/basic.json new file mode 100644 index 00000000..2dc11d73 --- /dev/null +++ b/features/data/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/data/configs/encrypted.json b/features/data/configs/encrypted.json new file mode 100644 index 00000000..a498974b --- /dev/null +++ b/features/data/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/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/data/configs/multiple.json b/features/data/configs/multiple.json new file mode 100644 index 00000000..af7a3e15 --- /dev/null +++ b/features/data/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/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/encrypted.journal b/features/data/journals/encrypted.journal new file mode 100644 index 00000000..339b47ba Binary files /dev/null and b/features/data/journals/encrypted.journal differ diff --git a/features/data/journals/simple.journal b/features/data/journals/simple.journal new file mode 100644 index 00000000..66d8439c --- /dev/null +++ b/features/data/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/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/data/journals/work.journal b/features/data/journals/work.journal new file mode 100644 index 00000000..e69de29b diff --git a/features/encryption.feature b/features/encryption.feature new file mode 100644 index 00000000..d134c3bb --- /dev/null +++ b/features/encryption.feature @@ -0,0 +1,29 @@ + 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: 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" + 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 "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/environment.py b/features/environment.py new file mode 100644 index 00000000..89125fca --- /dev/null +++ b/features/environment.py @@ -0,0 +1,29 @@ +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 + jrnl.util.TEST = True + for folder in ("configs", "journals"): + 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), 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) + shutil.rmtree(working_dir) diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature new file mode 100644 index 00000000..0510209b --- /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 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" + 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 new file mode 100644 index 00000000..b3675c1c --- /dev/null +++ b/features/steps/core.py @@ -0,0 +1,106 @@ +from behave import * +from jrnl import jrnl, Journal +import os +import sys +import json +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: + config = json.load(config_file) + with open(config['journals'][journal_name]) as journal_file: + 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) + 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(text.strip()) + jrnl.util.STDIN = buffer + jrnl.cli(args) + +@when('we run "{command}"') +def run(context, command): + args = command.split()[1:] + jrnl.cli(args or None) + + +@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() + +@then('the output should contain "{text}"') +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"): + 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] + 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 + 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') +@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/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 + """ diff --git a/jrnl/Entry.py b/jrnl/Entry.py index a8ec0557..5fbdeb15 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -15,7 +15,8 @@ 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) + self.tags = tags return set(tags) def __unicode__(self): diff --git a/jrnl/Journal.py b/jrnl/Journal.py index d4e733ea..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 -except (SystemError, ValueError): from util import get_local_timezone +try: from . import util +except (SystemError, ValueError): import util import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -18,14 +18,13 @@ 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 if "win32" in sys.platform: import pyreadline as readline else: import readline import hashlib -import getpass try: import colorama colorama.init() @@ -50,7 +49,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 @@ -77,30 +75,29 @@ 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?") + 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)) + 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.getpass(prompt) - 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: - print("Wrong password, try again.") + util.prompt("Wrong password, try again.") else: - print("Extremely wrong password.") + util.prompt("Extremely wrong password.") sys.exit(-1) journal = decrypted else: @@ -152,7 +149,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,18 +171,21 @@ 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 + 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: @@ -314,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) @@ -340,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 7ca6b98e..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.3.1' +__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 5bd687cf..42126802 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.""" @@ -29,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): @@ -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 3377b686..ef78840b 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -29,7 +29,8 @@ 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 +52,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""" @@ -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): @@ -114,7 +115,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: @@ -122,18 +123,18 @@ def cli(): 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() + args = parse_args(manual_args) # If the first textual argument points to a journal file, # use this! @@ -149,8 +150,6 @@ def cli(): 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) @@ -171,10 +170,11 @@ def cli(): # 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 util.PY2 and 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)) + util.prompt("[Entry added to {0} journal]".format(journal_name)) journal.write() # Reading mode @@ -184,7 +184,7 @@ def cli(): strict=args.strict, short=args.short) journal.limit(args.limit) - print(unicode(journal)) + print(journal.pprint()) # Various export modes elif args.tags: @@ -194,7 +194,7 @@ def cli(): 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) @@ -212,7 +212,7 @@ def cli(): 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 9ed4e1f6..28499933 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -3,15 +3,36 @@ 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 py23_input(msg): - if sys.version_info[0] == 3: - try: return input(msg) - except SyntaxError: return "" +def getpass(prompt): + if not TEST: + return gp.getpass(prompt) else: - return raw_input(msg) + return py23_input(prompt) + + +def u(s): + """Mock unicode function for python 2 and 3 compatibility.""" + 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.""" + if not msg.endswith("\n"): + msg += "\n" + STDERR.write(u(msg)) + +def py23_input(msg): + STDERR.write(u(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()