mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Merge pull request #85 from maebert/tests
New unit testing suit using behave and several smaller fixes
This commit is contained in:
commit
0957622d67
24 changed files with 429 additions and 88 deletions
|
@ -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
|
||||
|
|
12
CHANGELOG.md
12
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
|
||||
|
|
36
features/core.feature
Normal file
36
features/core.feature
Normal 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."
|
14
features/data/configs/basic.json
Normal file
14
features/data/configs/basic.json
Normal 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": "@"
|
||||
}
|
14
features/data/configs/encrypted.json
Normal file
14
features/data/configs/encrypted.json
Normal 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": "@"
|
||||
}
|
14
features/data/configs/encrypted_with_pw.json
Normal file
14
features/data/configs/encrypted_with_pw.json
Normal 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": "@"
|
||||
}
|
16
features/data/configs/multiple.json
Normal file
16
features/data/configs/multiple.json
Normal 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": "@"
|
||||
}
|
14
features/data/configs/tags.json
Normal file
14
features/data/configs/tags.json
Normal 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": "@"
|
||||
}
|
BIN
features/data/journals/encrypted.journal
Normal file
BIN
features/data/journals/encrypted.journal
Normal file
Binary file not shown.
5
features/data/journals/simple.journal
Normal file
5
features/data/journals/simple.journal
Normal 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.
|
7
features/data/journals/tags.journal
Normal file
7
features/data/journals/tags.journal
Normal 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.
|
0
features/data/journals/work.journal
Normal file
0
features/data/journals/work.journal
Normal file
29
features/encryption.feature
Normal file
29
features/encryption.feature
Normal 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
29
features/environment.py
Normal 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)
|
36
features/multiple_journals.feature
Normal file
36
features/multiple_journals.feature
Normal 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
106
features/steps/core.py
Normal 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
12
features/tagging.feature
Normal 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
|
||||
"""
|
|
@ -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):
|
||||
|
|
|
@ -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,6 +149,7 @@ 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.
|
||||
if current_entry:
|
||||
current_entry.body += line + "\n"
|
||||
|
||||
# Append last 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 "<Journal with {} entries>".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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
38
jrnl/jrnl.py
38
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()
|
||||
|
||||
|
|
31
jrnl/util.py
31
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.
|
||||
|
|
|
@ -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()
|
Loading…
Add table
Reference in a new issue