Merge remote-tracking branch 'maebert/2.0-rc1' into 2.0-rc1

Conflicts:
	setup.py
This commit is contained in:
flight16 2015-04-13 08:46:43 +09:00
commit 53e3bd334c
23 changed files with 320 additions and 164 deletions

View file

@ -39,7 +39,7 @@ Feature: Basic reading and writing to a journal
Given we use the config "basic.yaml"
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 "[2013-07-25 09:00] I saw Elvis."
and the journal should contain "He's alive."
Scenario: Displaying the version number

View file

@ -0,0 +1,11 @@
{
"default_hour": 9,
"timeformat": "%Y-%m-%d %H:%M",
"linewrap": 80,
"encrypt": false,
"editor": "",
"default_minute": 0,
"highlight": true,
"journals": {"default": "features/journals/simple_jrnl-1-9-5.journal"},
"tagsymbols": "@"
}

View file

@ -1 +1 @@
gAAAAABVH4F009PRK-vz0bGa2elPRuNWvQOFjDt_TQtTbgHDBCiWgEzsTF7c4Vy-iqm-MYOh2UUrh_kUX7vTzsj3R-OJsKEYRy060yUaOH3cfBB1QHmMBhefV2XSJ-A5u_PryN137rf7kbV5Xk0jSDi2GbRuIRT6yRER1y-MAn4RDs0jfpxfeskZ65ykaB9-5Rm-lA_1ygHM9Uwrcu3HyrMJei1C6kl23w==
gAAAAABVIHB7tnwKExG7aC5ZbAbBL9SG2oY2GENeoOJ22i1PZigOvCYvrQN3kpsu0KGr7ay5K-_46R5YFlqJvtQ8anPH2FSITsaZy-l5Lz_5quw3rmzhLwAR1tc0icgtR4MEpXEdsuQ7cyb12Xq-JLDrnATs0id5Vow9Ri_tE7Xe4BXgXaySn3aRPwWKoninVxVPVvETY3MXHSUEXV9OZ-pH5kYBLGYbLA==

View file

@ -1,5 +1,5 @@
2013-06-09 15:39 My first entry.
[2013-06-09 15:39] My first entry.
Everything is alright
2013-06-10 15:40 Life is good.
[2013-06-10 15:40] Life is good.
But I'm better.

View file

@ -0,0 +1,3 @@
2010-06-10 15:00 A life without chocolate is like a bad analogy.
2013-06-10 15:40 He said "[this] is the best time to be alive".

View file

@ -1,2 +1,2 @@
2013-06-10 15:40 I programmed for @OS/2.
[2013-06-10 15:40] I programmed for @OS/2.
Almost makes me want to go back to @C++, though. (Still better than @C#).

View file

@ -1,3 +1,3 @@
2014-07-22 11:11 This entry has an email.
[2014-07-22 11:11] This entry has an email.
@Newline tag should show as a tag.
Kyla's @email is kyla@clevelandunderdog.org and Guinness's is guinness@fortheloveofpits.org.

View file

@ -1,8 +1,8 @@
2013-04-09 15:39 I have an @idea:
[2013-04-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.
[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.
inst

View file

@ -36,6 +36,7 @@
"""
Y
bad doggie no biscuit
bad doggie no biscuit
"""
Then we should see the message "Password"
and the output should contain "2013-06-10 15:40 Life is good"

View file

@ -20,3 +20,9 @@ Feature: Exporting a Journal
and "tags" in the json output should contain "@journal"
and "tags" in the json output should not contain "@dan"
Scenario: Exporting dayone to json
Given we use the config "dayone.yaml"
When we run "jrnl --export json"
Then we should get no error
and the output should be parsable as json
and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1"

View file

@ -19,14 +19,14 @@ Feature: Zapped bugs should stay dead.
Given we use the config "basic.yaml"
When we run "jrnl 2013-11-30 15:42: Project Started."
Then we should see the message "Entry added"
and the journal should contain "2013-11-30 15:42 Project Started."
and the journal should contain "[2013-11-30 15:42] Project Started."
Scenario: Date in the future should be parsed correctly
# https://github.com/maebert/jrnl/issues/185
Given we use the config "basic.yaml"
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
Then we should see the message "Entry added"
and the journal should contain "2019-06-26 09:00 Planet?"
and the journal should contain "[2019-06-26 09:00] Planet?"
Scenario: Loading entry with ambiguous time stamp
#https://github.com/maebert/jrnl/issues/153
@ -38,16 +38,26 @@ Feature: Zapped bugs should stay dead.
2013-10-27 03:27 Some text.
"""
Scenario: Title with an embedded period.
Given we use the config "basic.yaml"
When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic."
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should be
"""
2014-04-24 09:00 Created a new website - empty.com.
| Hope to get a lot of traffic.
"""
Scenario: Title with an embedded period.
Given we use the config "basic.yaml"
When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic."
Then we should see the message "Entry added"
When we run "jrnl -1"
Then the output should be
"""
2014-04-24 09:00 Created a new website - empty.com.
| Hope to get a lot of traffic.
"""
Scenario: Upgrade and parse journals with square brackets
Given we use the config "upgrade_from_195.json"
When we run "jrnl -2" and enter "Y"
Then the output should contain
"""
2010-06-10 15:00 A life without chocolate is like a bad analogy.
2013-06-10 15:40 He said "[this] is the best time to be alive".
"""
Scenario: Title with an embedded period on DayOne journal
Given we use the config "dayone.yaml"

View file

@ -4,7 +4,7 @@ Feature: Starring entries
Given we use the config "basic.yaml"
When we run "jrnl 20 july 2013 *: Best day of my life!"
Then we should see the message "Entry added"
and the journal should contain "2013-07-20 09:00 Best day of my life! *"
and the journal should contain "[2013-07-20 09:00] Best day of my life! *"
Scenario: Filtering by starred entries
Given we use the config "basic.yaml"

View file

@ -126,6 +126,22 @@ def check_output_field_key(context, field, key):
assert key in out_json[field]
@then('the json output should contain {path} = "{value}"')
def check_json_output_path(context, path, value):
""" E.g.
the json output should contain entries.0.title = "hello"
"""
out = context.stdout_capture.getvalue()
struct = json.loads(out)
for node in path.split('.'):
try:
struct = struct[int(node)]
except ValueError:
struct = struct[node]
assert struct == value, struct
@then('the output should be')
@then('the output should be "{text}"')
def check_output(context, text=None):
@ -146,12 +162,14 @@ def check_output_time_inline(context, text):
assert local_date in out, local_date
@then('the output should contain')
@then('the output should contain "{text}"')
def check_output_inline(context, text):
def check_output_inline(context, text=None):
text = text or context.text
out = context.stdout_capture.getvalue()
if isinstance(out, bytes):
out = out.decode('utf-8')
assert text in out
assert text in out, text
@then('the output should not contain "{text}"')

View file

@ -1,19 +1,20 @@
from . import Journal, util
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import hashlib
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
import base64
def make_key(password):
if type(password) is unicode:
password = password.encode('utf-8')
password = util.bytes(password)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
# Salt is hard-coded
salt='\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8',
salt=b'\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8',
iterations=100000,
backend=default_backend()
)
@ -32,13 +33,15 @@ class EncryptedJournal(Journal.Journal):
and otherwise ask the user to enter a password up to three times.
If the password is provided but wrong (or corrupt), this will simply
return None."""
with open(filename) as f:
with open(filename, 'rb') as f:
journal_encrypted = f.read()
def validate_password(password):
key = make_key(password)
try:
return Fernet(key).decrypt(journal_encrypted).decode('utf-8')
plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8')
self.config['password'] = password
return plain
except (InvalidToken, IndexError):
return None
if password:
@ -48,7 +51,7 @@ class EncryptedJournal(Journal.Journal):
def _store(self, filename, text):
key = make_key(self.config['password'])
journal = Fernet(key).encrypt(text.encode('utf-8'))
with open(filename, 'w') as f:
with open(filename, 'wb') as f:
f.write(journal)
@classmethod
@ -57,3 +60,35 @@ class EncryptedJournal(Journal.Journal):
dummy = Fernet(key).encrypt("")
with open(filename, 'w') as f:
f.write(dummy)
class LegacyEncryptedJournal(Journal.LegacyJournal):
"""Legacy class to support opening journals encrypted with the jrnl 1.x
standard. You'll not be able to save these journals anymore."""
def __init__(self, name='default', **kwargs):
super(LegacyEncryptedJournal, self).__init__(name, **kwargs)
self.config['encrypt'] = True
def _load(self, filename, password=None):
with open(filename, 'rb') as f:
journal_encrypted = f.read()
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
def validate_password(password):
decryption_key = hashlib.sha256(password.encode('utf-8')).digest()
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
try:
plain_padded = decryptor.update(cipher) + decryptor.finalize()
self.config['password'] = password
if plain_padded[-1] in (" ", 32):
# Ancient versions of jrnl. Do not judge me.
return plain_padded.decode('utf-8').rstrip(" ")
else:
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plain = unpadder.update(plain_padded) + unpadder.finalize()
return plain.decode('utf-8')
except ValueError:
return None
if password:
return validate_password(password)
return util.get_password(keychain=self.name, validator=validate_password)

View file

@ -20,19 +20,19 @@ class Entry:
@staticmethod
def tag_regex(tagsymbols):
pattern = r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
return re.compile( pattern, re.UNICODE )
return re.compile(pattern, re.UNICODE)
def parse_tags(self):
fulltext = " " + " ".join([self.title, self.body]).lower()
fulltext = " " + " ".join([self.title, self.body]).lower()
tagsymbols = self.journal.config['tagsymbols']
tags = re.findall( Entry.tag_regex(tagsymbols), fulltext )
tags = re.findall(Entry.tag_regex(tagsymbols), fulltext)
self.tags = tags
return set(tags)
def __unicode__(self):
"""Returns a string representation of the entry to be written into a journal file."""
date_str = self.date.strftime(self.journal.config['timeformat'])
title = date_str + " " + self.title.rstrip("\n ")
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
if self.starred:
title += " *"
return "{title}{sep}{body}\n".format(
@ -48,13 +48,14 @@ class Entry:
if not short and self.journal.config['linewrap']:
title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
body = "\n".join([
textwrap.fill((line + " ") if (len(line) == 0) else line,
self.journal.config['linewrap'],
initial_indent="| ",
subsequent_indent="| ",
drop_whitespace=False)
for line in self.body.rstrip(" \n").splitlines()
])
textwrap.fill(
(line + " ") if (len(line) == 0) else line,
self.journal.config['linewrap'],
initial_indent="| ",
subsequent_indent="| ",
drop_whitespace=False)
for line in self.body.rstrip(" \n").splitlines()
])
else:
title = date_str + " " + self.title.rstrip("\n ")
body = self.body.rstrip("\n ")
@ -83,7 +84,7 @@ class Entry:
or self.body.rstrip() != other.body.rstrip() \
or self.date != other.date \
or self.starred != other.starred:
return False
return False
return True
def __ne__(self, other):

View file

@ -10,6 +10,9 @@ import sys
import codecs
import re
from datetime import datetime
import logging
log = logging.getLogger(__name__)
class Journal(object):
@ -33,6 +36,15 @@ class Journal(object):
"""Returns the number of entries"""
return len(self.entries)
@classmethod
def from_journal(cls, other):
"""Creates a new journal by copying configuration and entries from
another journal object"""
new_journal = cls(other.name, **other.config)
new_journal.entries = other.entries
log.debug("Imported %d entries from %s to %s", len(new_journal), other.__class__.__name__, cls.__name__)
return new_journal
def import_(self, other_journal_txt):
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
self.sort()
@ -41,9 +53,15 @@ class Journal(object):
"""Opens the journal file defined in the config and parses it into a list of Entries.
Entries have the form (date, title, body)."""
filename = filename or self.config['journal']
if not os.path.exists(filename):
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
self._create(filename)
text = self._load(filename)
self.entries = self._parse(text)
self.sort()
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
return self
def write(self, filename=None):
@ -64,37 +82,36 @@ class Journal(object):
def _parse(self, journal_txt):
"""Parses a journal that's stored in a string and returns a list of entries"""
# Entries start with a line that looks like 'date title' - let's figure out how
# long the date will be by constructing one
date_length = len(datetime.today().strftime(self.config['timeformat']))
# Initialise our current entry
entries = []
current_entry = None
date_blob_re = re.compile("^\[[^\\]]+\] ")
for line in journal_txt.splitlines():
line = line.rstrip()
try:
# try to parse line as date => new entry begins
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
date_blob = date_blob_re.findall(line)
if date_blob:
date_blob = date_blob[0]
new_date = time.parse(date_blob.strip(" []"))
if new_date:
# Found a date at the start of the line: This is a new entry.
if current_entry:
entries.append(current_entry)
# parsing successful => save old entry and create new one
if new_date and current_entry:
entries.append(current_entry)
if line.endswith("*"):
starred = True
line = line[:-1]
else:
starred = False
if line.endswith("*"):
starred = True
line = line[:-1]
else:
starred = False
current_entry = Entry.Entry(self, date=new_date, title=line[date_length + 1:], starred=starred)
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"
current_entry = Entry.Entry(
self,
date=new_date,
title=line[len(date_blob):],
starred=starred
)
elif current_entry:
# Didn't find a date - keep on feeding to current entry.
current_entry.body += line + "\n"
# Append last entry
if current_entry:
@ -226,9 +243,6 @@ class Journal(object):
class PlainJournal(Journal):
def __init__(self, name='default', **kwargs):
super(PlainJournal, self).__init__(name, **kwargs)
@classmethod
def _create(cls, filename):
with codecs.open(filename, "a", "utf-8"):
@ -243,10 +257,70 @@ class PlainJournal(Journal):
f.write(text)
def open_journal(name, config):
class LegacyJournal(Journal):
"""Legacy class to support opening journals formatted with the jrnl 1.x
standard. Main difference here is that in 1.x, timestamps were not cuddled
by square brackets, and the line break between the title and the rest of
the entry was not enforced. You'll not be able to save these journals anymore."""
def _load(self, filename):
with codecs.open(filename, "r", "utf-8") as f:
return f.read()
def _parse(self, journal_txt):
"""Parses a journal that's stored in a string and returns a list of entries"""
# Entries start with a line that looks like 'date title' - let's figure out how
# long the date will be by constructing one
date_length = len(datetime.today().strftime(self.config['timeformat']))
# Initialise our current entry
entries = []
current_entry = None
for line in journal_txt.splitlines():
line = line.rstrip()
try:
# try to parse line as date => new entry begins
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
# parsing successful => save old entry and create new one
if new_date and current_entry:
entries.append(current_entry)
if line.endswith("*"):
starred = True
line = line[:-1]
else:
starred = False
current_entry = Entry.Entry(self, date=new_date, title=line[date_length + 1:], starred=starred)
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 + u"\n"
# Append last entry
if current_entry:
entries.append(current_entry)
for entry in entries:
entry.parse_tags()
return entries
def open_journal(name, config, legacy=False):
"""
Creates a normal, encrypted or DayOne journal based on the passed config.
If legacy is True, it will open Journals with legacy classes build for
backwards compatibility with jrnl 1.x
"""
config = config.copy()
journal_conf = config['journals'].get(name)
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
log.debug('Updating configuration with specific journal overrides %s', journal_conf)
config.update(journal_conf)
else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
from . import DayOneJournal
@ -256,7 +330,11 @@ def open_journal(name, config):
sys.exit(1)
if not config['encrypt']:
if legacy:
return LegacyJournal(name, **config).open()
return PlainJournal(name, **config).open()
else:
from . import EncryptedJournal
if legacy:
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
return EncryptedJournal.EncryptedJournal(name, **config).open()

View file

@ -13,7 +13,6 @@ from . import util
from . import install
from . import plugins
import jrnl
import os
import argparse
import sys
import logging
@ -102,13 +101,6 @@ def decrypt(journal, filename=None):
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
def touch_journal(filename):
"""If filename does not exist, touch the file"""
if not os.path.exists(filename):
util.prompt("[Journal created at {0}]".format(filename))
Journal.PlainJournal._create(filename)
def list_journals(config):
"""List the journals specified in the configuration file"""
sep = "\n"
@ -147,7 +139,6 @@ def run(manual_args=None):
sys.exit(0)
config = install.load_or_install_jrnl()
if args.ls:
util.prnt(u"Journals defined in {}".format(install.CONFIG_FILE_PATH))
ml = min(max(len(k) for k in config['journals']), 20)
@ -163,6 +154,7 @@ def run(manual_args=None):
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
if journal_name is not 'default':
args.text = args.text[1:]
# If the first remaining argument looks like e.g. '-3', interpret that as a limiter
if not args.limit and args.text and args.text[0].startswith("-"):
try:
@ -172,16 +164,7 @@ def run(manual_args=None):
pass
log.debug('Using journal "%s"', journal_name)
journal_conf = config['journals'].get(journal_name)
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
log.debug('Updating configuration with specific jourlnal overrides %s', journal_conf)
config.update(journal_conf)
else: # But also just give them a string to point to the journal file
config['journal'] = journal_conf
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
touch_journal(config['journal'])
mode_compose, mode_export, mode_import = guess_mode(args, config)
log.debug('Using journal path %(journal)s', config)
# How to quit writing?
if "win32" in sys.platform:
@ -206,6 +189,7 @@ def run(manual_args=None):
else:
mode_compose = False
# This is where we finally open the journal!
journal = Journal.open_journal(journal_name, config)
# Import mode

View file

@ -65,7 +65,7 @@ def upgrade_config(config):
for key in missing_keys:
config[key] = default_config[key]
save_config(config)
print("[.jrnl_conf updated to newest version at {}]".format(CONFIG_FILE_PATH))
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH))
def save_config(config):

View file

@ -32,15 +32,10 @@ class PluginMeta(type):
else:
return ', '.join(plugin_names[:-1]) + ", or " + plugin_names[-1]
class BaseExporter(object):
__metaclass__ = PluginMeta
names = []
class BaseImporter(object):
__metaclass__ = PluginMeta
names = []
# This looks a bit arcane, but is basically bilingual speak for defining a
# class with meta class 'PluginMeta' for both Python 2 and 3.
BaseExporter = PluginMeta(str('BaseExporter'), (), {'names': []})
BaseImporter = PluginMeta(str('BaseImporter'), (), {'names': []})
for module in glob.glob(os.path.dirname(__file__) + "/*.py"):

View file

@ -14,13 +14,16 @@ class JSONExporter(TextExporter):
@classmethod
def entry_to_dict(cls, entry):
return {
entry_dict = {
'title': entry.title,
'body': entry.body,
'date': entry.date.strftime("%Y-%m-%d"),
'time': entry.date.strftime("%H:%M"),
'starred': entry.starred
}
if hasattr(entry, "uuid"):
entry_dict['uuid'] = entry.uuid
return entry_dict
@classmethod
def export_entry(cls, entry):

View file

@ -1,37 +1,19 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import hashlib
import util
from __future__ import absolute_import, unicode_literals
from . import __version__
from . import EncryptedJournal
from . import Journal
from . import util
from .EncryptedJournal import EncryptedJournal
import sys
from cryptography.fernet import Fernet
import os
def upgrade_encrypted_journal(filename, key_plain):
"""Decrypts a journal in memory using the jrnl 1.x encryption scheme
and returns it in plain text."""
with open(filename) as f:
iv_cipher = f.read()
iv, cipher = iv_cipher[:16], iv_cipher[16:]
decryption_key = hashlib.sha256(key_plain.encode('utf-8')).digest()
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
try:
plain_padded = decryptor.update(cipher) + decryptor.finalize()
if plain_padded[-1] == " ":
# Ancient versions of jrnl. Do not judge me.
plain = plain_padded.rstrip(" ")
else:
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plain = unpadder.update(plain_padded) + unpadder.finalize()
except ValueError:
return None
key = EncryptedJournal.make_key(key_plain)
journal = Fernet(key).encrypt(plain)
with open(filename, 'w') as f:
f.write(journal)
return plain
def backup(filename, binary=False):
util.prompt(" Created a backup at {}.backup".format(filename))
with open(filename, 'rb' if binary else 'r') as original:
contents = original.read()
with open(filename + ".backup", 'wb' if binary else 'w') as backup:
backup.write(contents)
def upgrade_jrnl_if_necessary(config_path):
@ -57,41 +39,63 @@ Please note that jrnl 1.x is NOT forward compatible with this version of jrnl.
If you choose to proceed, you will not be able to use your journals with
older versions of jrnl anymore.
""".format(__version__))
encrypted_journals = {}
plain_journals = {}
for journal, journal_conf in config['journals'].items():
other_journals = {}
for journal_name, journal_conf in config['journals'].items():
if isinstance(journal_conf, dict):
if journal_conf.get("encrypt"):
encrypted_journals[journal] = journal_conf.get("journal")
else:
plain_journals[journal] = journal_conf.get("journal")
path = journal_conf.get("journal")
encrypt = journal_conf.get("encrypt")
else:
if config.get('encrypt'):
encrypted_journals[journal] = journal_conf
else:
plain_journals[journal] = journal_conf
encrypt = config.get('encrypt')
path = journal_conf
if encrypt:
encrypted_journals[journal_name] = path
elif os.path.isdir(path):
other_journals[journal_name] = path
else:
plain_journals[journal_name] = path
longest_journal_name = max([len(journal) for journal in config['journals']])
if encrypted_journals:
longest_journal_name = max([len(journal) for journal in config['journals']])
util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__))
for journal, path in encrypted_journals.items():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
if plain_journals:
util.prompt("\nFollowing plain text journals will be not be touched:")
for journal, path in plain_journals.items():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
cont = util.yesno("Continue upgrading jrnl?", default=False)
if plain_journals:
util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__))
for journal, path in plain_journals.items():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
if other_journals:
util.prompt("\nFollowing journals will be not be touched:")
for journal, path in other_journals.items():
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
if not cont:
util.prompt("jrnl NOT upgraded, exiting.")
sys.exit(1)
for journal, path in encrypted_journals.items():
util.prompt("Enter password for {} journal (stored in {}).".format(journal, path))
util.get_password(keychain=journal, validator=lambda pwd: upgrade_encrypted_journal(path, pwd))
for journal_name, path in encrypted_journals.items():
util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path))
backup(path, binary=True)
old_journal = Journal.open_journal(journal_name, config, legacy=True)
new_journal = EncryptedJournal.from_journal(old_journal)
new_journal.write()
util.prompt(" Done.")
with open(config_path + ".backup", 'w') as config_backup:
config_backup.write(config_file)
for journal_name, path in plain_journals.items():
util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path))
backup(path)
old_journal = Journal.open_journal(journal_name, config, legacy=True)
new_journal = Journal.PlainJournal.from_journal(old_journal)
new_journal.write()
util.prompt(" Done.")
util.prompt("""\n\nYour old config has been backed up to {}.backup.
We're all done here and you can start enjoying jrnl 2.""".format(config_path))
util.prompt("\nUpgrading config...")
backup(config_path)
util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path))

View file

@ -75,10 +75,17 @@ def u(s):
def py2encode(s):
"""Encode in Python 2, but not in python 3."""
"""Encodes to UTF-8 in Python 2 but not r."""
return s.encode("utf-8") if PY2 and type(s) is unicode else s
def bytes(s):
"""Returns bytes, no matter what."""
if PY3:
return s.encode("utf-8") if type(s) is not bytes else s
return s.encode("utf-8") if type(s) is unicode else s
def prnt(s):
"""Encode and print a string"""
STDOUT.write(u(s + "\n"))
@ -92,7 +99,7 @@ def prompt(msg):
def py23_input(msg=""):
STDERR.write(u(msg))
prompt(msg)
return STDIN.readline().strip()
@ -114,7 +121,7 @@ def load_config(config_path):
def get_text_from_editor(config, template=""):
tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl"))
tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl", suffix=".txt"))
with codecs.open(tmpfile, 'w', "utf-8") as f:
if template:
f.write(template)

View file

@ -75,11 +75,11 @@ conditional_dependencies = {
setup(
name = "jrnl",
version = get_version(),
description = "A command line journal application that stores your journal in a plain text file",
packages = ['jrnl'],
install_requires = [
name="jrnl",
version=get_version(),
description="A command line journal application that stores your journal in a plain text file",
packages=['jrnl'],
install_requires=[
"pyxdg>=0.19",
"parsedatetime>=1.2",
"pytz>=2013b",
@ -115,9 +115,9 @@ setup(
'Topic :: Text Processing'
],
# metadata for upload to PyPI
author = "Manuel Ebert",
author_email = "manuel@1450.me",
author="Manuel Ebert",
author_email="manuel@1450.me",
license="LICENSE",
keywords = "journal todo todo.txt jrnl".split(),
url = "http://www.jrnl.sh",
keywords="journal todo todo.txt jrnl".split(),
url="http://www.jrnl.sh",
)