Merge pull request #85 from maebert/tests

New unit testing suit using behave and several smaller fixes
This commit is contained in:
Manuel Ebert 2013-07-23 21:14:06 -07:00
commit 0957622d67
24 changed files with 429 additions and 88 deletions

View file

@ -3,9 +3,13 @@ python:
- "2.6" - "2.6"
- "2.7" - "2.7"
- "3.3" - "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 # command to run tests
script: nosetests script:
- python --version
- behave
matrix: matrix:
allow_failures: # python 3 support for travis is shaky.... allow_failures: # python 3 support for travis is shaky....
- python: 3.3 - python: 3.3

View file

@ -1,6 +1,18 @@
Changelog 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 ### 1.3.0
* [New] Export to multiple files * [New] Export to multiple files

36
features/core.feature Normal file
View file

@ -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."

View file

@ -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": "@"
}

View file

@ -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": "@"
}

View file

@ -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": "@"
}

View file

@ -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": "@"
}

View file

@ -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": "@"
}

Binary file not shown.

View file

@ -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.

View file

@ -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.

View file

View file

@ -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"

29
features/environment.py Normal file
View file

@ -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)

View file

@ -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

106
features/steps/core.py Normal file
View file

@ -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

12
features/tagging.feature Normal file
View file

@ -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
"""

View file

@ -15,7 +15,8 @@ class Entry:
def parse_tags(self): def parse_tags(self):
fulltext = " ".join([self.title, self.body]).lower() 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) return set(tags)
def __unicode__(self): def __unicode__(self):

View file

@ -3,8 +3,8 @@
try: from . import Entry try: from . import Entry
except (SystemError, ValueError): import Entry except (SystemError, ValueError): import Entry
try: from .util import get_local_timezone try: from . import util
except (SystemError, ValueError): from util import get_local_timezone except (SystemError, ValueError): import util
import codecs import codecs
import os import os
try: import parsedatetime.parsedatetime_consts as pdt try: import parsedatetime.parsedatetime_consts as pdt
@ -18,14 +18,13 @@ import sys
import glob import glob
try: try:
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Random import random, atfork from Crypto import Random
crypto_installed = True crypto_installed = True
except ImportError: except ImportError:
crypto_installed = False crypto_installed = False
if "win32" in sys.platform: import pyreadline as readline if "win32" in sys.platform: import pyreadline as readline
else: import readline else: import readline
import hashlib import hashlib
import getpass
try: try:
import colorama import colorama
colorama.init() colorama.init()
@ -50,7 +49,6 @@ class Journal(object):
'linewrap': 80, 'linewrap': 80,
} }
self.config.update(kwargs) self.config.update(kwargs)
# Set up date parser # Set up date parser
consts = pdt.Constants(usePyICU=False) consts = pdt.Constants(usePyICU=False)
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
@ -77,30 +75,29 @@ class Journal(object):
try: try:
plain = crypto.decrypt(cipher[16:]) plain = crypto.decrypt(cipher[16:])
except ValueError: 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) 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 return None
else: else:
return plain return plain.decode("utf-8")
def _encrypt(self, plain): def _encrypt(self, plain):
"""Encrypt a plaintext string using self.key as the key""" """Encrypt a plaintext string using self.key as the key"""
if not crypto_installed: if not crypto_installed:
sys.exit("Error: PyCrypto is not installed.") sys.exit("Error: PyCrypto is not installed.")
atfork() # A seed for PyCrypto Random.atfork() # A seed for PyCrypto
iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16)) iv = Random.new().read(AES.block_size)
crypto = AES.new(self.key, AES.MODE_CBC, iv) crypto = AES.new(self.key, AES.MODE_CBC, iv)
if len(plain) % 16 != 0: plain = plain.encode("utf-8")
plain += " " * (16 - len(plain) % 16) plain += b" " * (AES.block_size - len(plain) % AES.block_size)
else: # Always pad so we can detect properly decrypted files :)
plain += " " * 16
return iv + crypto.encrypt(plain) return iv + crypto.encrypt(plain)
def make_key(self, prompt="Password: "): def make_key(self, prompt="Password: "):
"""Creates an encryption key from the default password or prompts for a new 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 util.getpass(prompt)
self.key = hashlib.sha256(password.encode('utf-8')).digest() self.key = hashlib.sha256(password.encode("utf-8")).digest()
def open(self, filename=None): def open(self, filename=None):
"""Opens the journal file defined in the config and parses it into a list of Entries. """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 attempts += 1
self.config['password'] = None # This password doesn't work. self.config['password'] = None # This password doesn't work.
if attempts < 3: if attempts < 3:
print("Wrong password, try again.") util.prompt("Wrong password, try again.")
else: else:
print("Extremely wrong password.") util.prompt("Extremely wrong password.")
sys.exit(-1) sys.exit(-1)
journal = decrypted journal = decrypted
else: else:
@ -152,6 +149,7 @@ class Journal(object):
except ValueError: except ValueError:
# Happens when we can't parse the start of the line as an date. # Happens when we can't parse the start of the line as an date.
# In this case, just append line to our body. # In this case, just append line to our body.
if current_entry:
current_entry.body += line + "\n" current_entry.body += line + "\n"
# Append last entry # Append last entry
@ -173,18 +171,21 @@ class Journal(object):
lambda match: self._colorize(match.group(0)), lambda match: self._colorize(match.group(0)),
pp, re.UNICODE) pp, re.UNICODE)
else: 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)), lambda match: self._colorize(match.group(0)),
pp) pp)
return pp return pp
def pprint(self):
return self.__unicode__()
def __repr__(self): def __repr__(self):
return "<Journal with {} entries>".format(len(self.entries)) return "<Journal with {} entries>".format(len(self.entries))
def write(self, filename=None): def write(self, filename=None):
"""Dumps the journal into the config file, overwriting it""" """Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal'] 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']: if self.config['encrypt']:
journal = self._encrypt(journal) journal = self._encrypt(journal)
with open(filename, 'wb') as journal_file: with open(filename, 'wb') as journal_file:
@ -314,7 +315,7 @@ class DayOne(Journal):
try: try:
timezone = pytz.timezone(dict_entry['Time Zone']) timezone = pytz.timezone(dict_entry['Time Zone'])
except pytz.exceptions.UnknownTimeZoneError: except pytz.exceptions.UnknownTimeZoneError:
timezone = pytz.timezone(get_local_timezone()) timezone = pytz.timezone(util.get_local_timezone())
date = dict_entry['Creation Date'] date = dict_entry['Creation Date']
date = date + timezone.utcoffset(date) date = date + timezone.utcoffset(date)
entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False) entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False)
@ -340,7 +341,7 @@ class DayOne(Journal):
'Creation Date': utc_time, 'Creation Date': utc_time,
'Starred': entry.starred if hasattr(entry, 'starred') else False, 'Starred': entry.starred if hasattr(entry, 'starred') else False,
'Entry Text': entry.title+"\n"+entry.body, 'Entry Text': entry.title+"\n"+entry.body,
'Time Zone': get_local_timezone(), 'Time Zone': util.get_local_timezone(),
'UUID': new_uuid 'UUID': new_uuid
} }
plistlib.writePlist(entry_plist, filename) plistlib.writePlist(entry_plist, filename)

View file

@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line.
""" """
__title__ = 'jrnl' __title__ = 'jrnl'
__version__ = '1.3.1' __version__ = '1.4.1'
__author__ = 'Manuel Ebert' __author__ = 'Manuel Ebert'
__license__ = 'MIT License' __license__ = 'MIT License'
__copyright__ = 'Copyright 2013 Manuel Ebert' __copyright__ = 'Copyright 2013 Manuel Ebert'

View file

@ -7,6 +7,9 @@ try: from slugify import slugify
except ImportError: import slugify except ImportError: import slugify
try: import simplejson as json try: import simplejson as json
except ImportError: import json except ImportError: import json
try: from .util import u
except (SystemError, ValueError): from util import u
def get_tags_count(journal): def get_tags_count(journal):
"""Returns a set of tuples (count, tag) for all tags present in the journal.""" """Returns a set of tuples (count, tag) for all tags present in the journal."""
@ -29,7 +32,7 @@ def to_tag_list(journal):
elif min(tag_counts)[0] == 0: elif min(tag_counts)[0] == 0:
tag_counts = filter(lambda x: x[0] > 1, tag_counts) tag_counts = filter(lambda x: x[0] > 1, tag_counts)
result += '[Removed tags that appear only once.]\n' result += '[Removed tags that appear only once.]\n'
result += "\n".join(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 return result
def to_json(journal): def to_json(journal):
@ -60,7 +63,7 @@ def to_md(journal):
def to_txt(journal): def to_txt(journal):
"""Returns the complete text of the Journal.""" """Returns the complete text of the Journal."""
return unicode(journal) return journal.pprint()
def export(journal, format, output=None): def export(journal, format, output=None):
"""Exports the journal to various formats. """Exports the journal to various formats.
@ -93,7 +96,7 @@ def export(journal, format, output=None):
def write_files(journal, path, format): def write_files(journal, path, format):
"""Turns your journal into separate files for each entry. """Turns your journal into separate files for each entry.
Format should be either json, md or txt.""" 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: for e in journal.entries:
full_path = os.path.join(path, make_filename(e)) full_path = os.path.join(path, make_filename(e))
if format == 'json': if format == 'json':
@ -101,7 +104,7 @@ def write_files(journal, path, format):
elif format == 'md': elif format == 'md':
content = e.to_md() content = e.to_md()
elif format == 'txt': elif format == 'txt':
content = unicode(e) content = u(e)
with open(full_path, 'w') as f: with open(full_path, 'w') as f:
f.write(content) f.write(content)
return "[Journal exported individual files in {}]".format(path) return "[Journal exported individual files in {}]".format(path)

View file

@ -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') CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config')
PYCRYPTO = install.module_exists("Crypto") PYCRYPTO = install.module_exists("Crypto")
def parse_args():
def parse_args(args=None):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') 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"') 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('--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") 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): def guess_mode(args, config):
"""Guesses the mode (compose, read or export) from the given arguments""" """Guesses the mode (compose, read or export) from the given arguments"""
@ -77,7 +78,7 @@ def get_text_from_editor(config):
raw = f.read() raw = f.read()
os.remove(tmpfile) os.remove(tmpfile)
else: else:
print('[Nothing saved to file]') util.prompt('[Nothing saved to file]')
raw = '' raw = ''
return raw return raw
@ -89,19 +90,19 @@ def encrypt(journal, filename=None):
journal.make_key(prompt="Enter new password:") journal.make_key(prompt="Enter new password:")
journal.config['encrypt'] = True journal.config['encrypt'] = True
journal.write(filename) 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): def decrypt(journal, filename=None):
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """ """ Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
journal.config['encrypt'] = False journal.config['encrypt'] = False
journal.config['password'] = "" journal.config['password'] = ""
journal.write(filename) 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): def touch_journal(filename):
"""If filename does not exist, touch the file""" """If filename does not exist, touch the file"""
if not os.path.exists(filename): 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() open(filename, 'a').close()
def update_config(config, new_config, scope): def update_config(config, new_config, scope):
@ -114,7 +115,7 @@ def update_config(config, new_config, scope):
config.update(new_config) config.update(new_config)
def cli(): def cli(manual_args=None):
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
config = install.install_jrnl(CONFIG_PATH) config = install.install_jrnl(CONFIG_PATH)
else: else:
@ -122,18 +123,18 @@ def cli():
try: try:
config = json.load(f) config = json.load(f)
except ValueError as e: except ValueError as e:
print("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message)) util.prompt("[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("[Entry was NOT added to your journal]")
sys.exit(-1) sys.exit(-1)
install.update_config(config, config_path=CONFIG_PATH) install.update_config(config, config_path=CONFIG_PATH)
original_config = config.copy() original_config = config.copy()
# check if the configuration is supported by available modules # check if the configuration is supported by available modules
if config['encrypt'] and not PYCRYPTO: 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) sys.exit(-1)
args = parse_args() args = parse_args(manual_args)
# If the first textual argument points to a journal file, # If the first textual argument points to a journal file,
# use this! # use this!
@ -149,8 +150,6 @@ def cli():
mode_compose, mode_export = guess_mode(args, config) mode_compose, mode_export = guess_mode(args, config)
# open journal file or folder # open journal file or folder
if os.path.isdir(config['journal']) and ( config['journal'].endswith(".dayone") or \ if os.path.isdir(config['journal']) and ( config['journal'].endswith(".dayone") or \
config['journal'].endswith(".dayone/")): config['journal'].endswith(".dayone/")):
journal = Journal.DayOne(**config) journal = Journal.DayOne(**config)
@ -171,10 +170,11 @@ def cli():
# Writing mode # Writing mode
if mode_compose: if mode_compose:
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()
unicode_raw = raw.decode(sys.getfilesystemencoding()) if util.PY2 and type(raw) is not unicode:
entry = journal.new_entry(unicode_raw, args.date) raw = raw.decode(sys.getfilesystemencoding())
entry = journal.new_entry(raw, args.date)
entry.starred = args.star 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() journal.write()
# Reading mode # Reading mode
@ -184,7 +184,7 @@ def cli():
strict=args.strict, strict=args.strict,
short=args.short) short=args.short)
journal.limit(args.limit) journal.limit(args.limit)
print(unicode(journal)) print(journal.pprint())
# Various export modes # Various export modes
elif args.tags: elif args.tags:
@ -194,7 +194,7 @@ def cli():
print(exporters.export(journal, args.export, args.output)) print(exporters.export(journal, args.export, args.output))
elif (args.encrypt is not False or args.decrypt is not False) and not PYCRYPTO: 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: elif args.encrypt is not False:
encrypt(journal, filename=args.encrypt) encrypt(journal, filename=args.encrypt)
@ -212,7 +212,7 @@ def cli():
elif args.delete_last: elif args.delete_last:
last_entry = journal.entries.pop() last_entry = journal.entries.pop()
print("[Deleted Entry:]") util.prompt("[Deleted Entry:]")
print(last_entry) print(last_entry)
journal.write() journal.write()

View file

@ -3,15 +3,36 @@
import sys import sys
import os import os
from tzlocal import get_localzone 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 __cached_tz = None
def py23_input(msg): def getpass(prompt):
if sys.version_info[0] == 3: if not TEST:
try: return input(msg) return gp.getpass(prompt)
except SyntaxError: return ""
else: 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(): def get_local_timezone():
"""Returns the Olson identifier of the local timezone. """Returns the Olson identifier of the local timezone.

View file

@ -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()