Merge pull request #164 from maebert/1.8

Official support for python 3.4
This commit is contained in:
Manuel Ebert 2014-05-22 13:49:31 -07:00
commit 9618b74f8d
9 changed files with 164 additions and 140 deletions

View file

@ -3,7 +3,7 @@ python:
- "2.6"
- "2.7"
- "3.3"
# - "3.4" # Not available on Travis yet, see https://github.com/travis-ci/travis-ci/issues/1989
- "3.4"
install:
- "pip install -e . --use-mirrors"
- "pip install pycrypto>=2.6 --use-mirrors"

View file

@ -2,6 +2,10 @@ Changelog
=========
### 1.8 (May 22, 2014)
* __1.8.0__ Official support for python 3.4
### 1.7 (December 22, 2013)
* __1.7.22__ Fixed an issue with writing files when exporting entries containing non-ascii characters.

View file

@ -1,4 +1,4 @@
<?xxml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@ -23,7 +23,7 @@
<key>Location</key>
<dict>
<key>Administrative Area</key>
<string>Ãstergötlands län</string>
<string>Östergötlands län</string>
<key>Country</key>
<string>Sverige</string>
<key>Latitude</key>

View file

@ -19,7 +19,7 @@
<key>Location</key>
<dict>
<key>Administrative Area</key>
<string>Ãstergötlands län</string>
<string>Östergötlands län</string>
<key>Country</key>
<string>Sverige</string>
<key>Latitude</key>

137
jrnl/DayOneJournal.py Normal file
View file

@ -0,0 +1,137 @@
#!/usr/bin/env python
# encoding: utf-8
from __future__ import absolute_import
from . import Entry
from . import Journal
import os
import re
from datetime import datetime
import time
import plistlib
import pytz
import uuid
import tzlocal
from xml.parsers.expat import ExpatError
class DayOne(Journal.Journal):
"""A special Journal handling DayOne files"""
# InvalidFileException was added to plistlib in Python3.4
PLIST_EXCEPTIONS = (ExpatError, plistlib.InvalidFileException) if hasattr(plistlib, "InvalidFileException") else ExpatError
def __init__(self, **kwargs):
self.entries = []
self._deleted_entries = []
super(DayOne, self).__init__(**kwargs)
def open(self):
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
self.entries = []
for filename in filenames:
with open(filename, 'rb') as plist_entry:
try:
dict_entry = plistlib.readPlist(plist_entry)
except self.PLIST_EXCEPTIONS:
pass
else:
try:
timezone = pytz.timezone(dict_entry['Time Zone'])
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
timezone = tzlocal.get_localzone()
date = dict_entry['Creation Date']
date = date + timezone.utcoffset(date, is_dst=False)
raw = dict_entry['Entry Text']
sep = re.search("\n|[\?!.]+ +\n?", raw)
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "")
entry = Entry.Entry(self, date, title, body, starred=dict_entry["Starred"])
entry.uuid = dict_entry["UUID"]
entry.tags = [self.config['tagsymbols'][0] + tag for tag in dict_entry.get("Tags", [])]
self.entries.append(entry)
self.sort()
def write(self):
"""Writes only the entries that have been modified into plist files."""
for entry in self.entries:
if entry.modified:
if not hasattr(entry, "uuid"):
entry.uuid = uuid.uuid1().hex
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
entry_plist = {
'Creation Date': utc_time,
'Starred': entry.starred if hasattr(entry, 'starred') else False,
'Entry Text': entry.title + "\n" + entry.body,
'Time Zone': str(tzlocal.get_localzone()),
'UUID': entry.uuid,
'Tags': [tag.strip(self.config['tagsymbols']) for tag in entry.tags]
}
plistlib.writePlist(entry_plist, filename)
for entry in self._deleted_entries:
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
os.remove(filename)
def editable_str(self):
"""Turns the journal into a string of entries that can be edited
manually and later be parsed with eslf.parse_editable_str."""
return u"\n".join([u"# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries])
def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates it's entries."""
# Method: create a new list of entries from the edited text, then match
# UUIDs of the new entries against self.entries, updating the entries
# if the edited entries differ, and deleting entries from self.entries
# if they don't show up in the edited entries anymore.
date_length = len(datetime.today().strftime(self.config['timeformat']))
# Initialise our current entry
entries = []
current_entry = None
for line in edited.splitlines():
# try to parse line as UUID => new entry begins
line = line.rstrip()
m = re.match("# *([a-f0-9]+) *$", line.lower())
if m:
if current_entry:
entries.append(current_entry)
current_entry = Entry.Entry(self)
current_entry.modified = False
current_entry.uuid = m.group(1).lower()
else:
try:
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
if line.endswith("*"):
current_entry.starred = True
line = line[:-1]
current_entry.title = line[date_length + 1:]
current_entry.date = new_date
except ValueError:
if current_entry:
current_entry.body += line + "\n"
# Append last entry
if current_entry:
entries.append(current_entry)
# Now, update our current entries if they changed
for entry in entries:
entry.parse_tags()
matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid]
if matched_entries:
# This entry is an existing entry
match = matched_entries[0]
if match != entry:
self.entries.remove(match)
entry.modified = True
self.entries.append(entry)
else:
# This entry seems to be new... save it.
entry.modified = True
self.entries.append(entry)
# Remove deleted entries
edited_uuids = [e.uuid for e in entries]
self._deleted_entries = [e for e in self.entries if e.uuid not in edited_uuids]
self.entries[:] = [e for e in self.entries if e.uuid in edited_uuids]
return entries

View file

@ -5,13 +5,11 @@ from __future__ import absolute_import
from . import Entry
from . import util
import codecs
import os
try: import parsedatetime.parsedatetime_consts as pdt
except ImportError: import parsedatetime as pdt
import re
from datetime import datetime
import dateutil
import time
import sys
try:
from Crypto.Cipher import AES
@ -20,11 +18,6 @@ try:
except ImportError:
crypto_installed = False
import hashlib
import plistlib
import pytz
import uuid
import tzlocal
from xml.parsers.expat import ExpatError
class Journal(object):
@ -328,121 +321,3 @@ class Journal(object):
for entry in mod_entries:
entry.modified = not any(entry == old_entry for old_entry in self.entries)
self.entries = mod_entries
class DayOne(Journal):
"""A special Journal handling DayOne files"""
def __init__(self, **kwargs):
self.entries = []
self._deleted_entries = []
super(DayOne, self).__init__(**kwargs)
def open(self):
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
self.entries = []
for filename in filenames:
with open(filename, 'rb') as plist_entry:
try:
dict_entry = plistlib.readPlist(plist_entry)
except ExpatError:
pass
else:
try:
timezone = pytz.timezone(dict_entry['Time Zone'])
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
timezone = tzlocal.get_localzone()
date = dict_entry['Creation Date']
date = date + timezone.utcoffset(date, is_dst=False)
raw = dict_entry['Entry Text']
sep = re.search("\n|[\?!.]+ +\n?", raw)
title, body = (raw[:sep.end()], raw[sep.end():]) if sep else (raw, "")
entry = Entry.Entry(self, date, title, body, starred=dict_entry["Starred"])
entry.uuid = dict_entry["UUID"]
entry.tags = [self.config['tagsymbols'][0] + tag for tag in dict_entry.get("Tags", [])]
self.entries.append(entry)
self.sort()
def write(self):
"""Writes only the entries that have been modified into plist files."""
for entry in self.entries:
if entry.modified:
if not hasattr(entry, "uuid"):
entry.uuid = uuid.uuid1().hex
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
entry_plist = {
'Creation Date': utc_time,
'Starred': entry.starred if hasattr(entry, 'starred') else False,
'Entry Text': entry.title+"\n"+entry.body,
'Time Zone': str(tzlocal.get_localzone()),
'UUID': entry.uuid,
'Tags': [tag.strip(self.config['tagsymbols']) for tag in entry.tags]
}
plistlib.writePlist(entry_plist, filename)
for entry in self._deleted_entries:
filename = os.path.join(self.config['journal'], "entries", entry.uuid+".doentry")
os.remove(filename)
def editable_str(self):
"""Turns the journal into a string of entries that can be edited
manually and later be parsed with eslf.parse_editable_str."""
return u"\n".join([u"# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries])
def parse_editable_str(self, edited):
"""Parses the output of self.editable_str and updates it's entries."""
# Method: create a new list of entries from the edited text, then match
# UUIDs of the new entries against self.entries, updating the entries
# if the edited entries differ, and deleting entries from self.entries
# if they don't show up in the edited entries anymore.
date_length = len(datetime.today().strftime(self.config['timeformat']))
# Initialise our current entry
entries = []
current_entry = None
for line in edited.splitlines():
# try to parse line as UUID => new entry begins
line = line.rstrip()
m = re.match("# *([a-f0-9]+) *$", line.lower())
if m:
if current_entry:
entries.append(current_entry)
current_entry = Entry.Entry(self)
current_entry.modified = False
current_entry.uuid = m.group(1).lower()
else:
try:
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
if line.endswith("*"):
current_entry.starred = True
line = line[:-1]
current_entry.title = line[date_length+1:]
current_entry.date = new_date
except ValueError:
if current_entry:
current_entry.body += line + "\n"
# Append last entry
if current_entry:
entries.append(current_entry)
# Now, update our current entries if they changed
for entry in entries:
entry.parse_tags()
matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid]
if matched_entries:
# This entry is an existing entry
match = matched_entries[0]
if match != entry:
self.entries.remove(match)
entry.modified = True
self.entries.append(entry)
else:
# This entry seems to be new... save it.
entry.modified = True
self.entries.append(entry)
# Remove deleted entries
edited_uuids = [e.uuid for e in entries]
self._deleted_entries = [e for e in self.entries if e.uuid not in edited_uuids]
self.entries[:] = [e for e in self.entries if e.uuid in edited_uuids]
return entries

View file

@ -8,7 +8,7 @@ jrnl is a simple journal application for your command line.
from __future__ import absolute_import
__title__ = 'jrnl'
__version__ = '1.7.22'
__version__ = '1.8.0'
__author__ = 'Manuel Ebert'
__license__ = 'MIT License'
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'

View file

@ -9,10 +9,10 @@
from __future__ import absolute_import
from . import Journal
from . import DayOneJournal
from . import util
from . import exporters
from . import install
from . import __version__
import jrnl
import os
import argparse
@ -43,12 +43,13 @@ def parse_args(args=None):
exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences')
exporting.add_argument('--export', metavar='TYPE', dest='export', help='Export your journal to Markdown, JSON or Text', default=False, const=None)
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='The output of the file can be provided when using with --export', default=False, const=None)
exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', 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('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', 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('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true")
return parser.parse_args(args)
def guess_mode(args, config):
"""Guesses the mode (compose, read or export) from the given arguments"""
compose = True
@ -65,6 +66,7 @@ def guess_mode(args, config):
return compose, export
def encrypt(journal, filename=None):
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
password = util.getpass("Enter new password: ")
@ -75,6 +77,7 @@ def encrypt(journal, filename=None):
util.set_keychain(journal.name, password)
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
@ -82,20 +85,21 @@ def decrypt(journal, filename=None):
journal.write(filename)
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):
util.prompt("[Journal created at {0}]".format(filename))
open(filename, 'a').close()
def list_journals(config):
"""List the journals specified in the configuration file"""
sep = "\n"
journal_list = sep.join(config['journals'])
return journal_list
def update_config(config, new_config, scope, force_local=False):
"""Updates a config dict with new values - either global if scope is None
or config['journals'][scope] is just a string pointing to a journal file,
@ -108,6 +112,7 @@ def update_config(config, new_config, scope, force_local=False):
else:
config.update(new_config)
def run(manual_args=None):
args = parse_args(manual_args)
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
@ -158,7 +163,7 @@ def run(manual_args=None):
if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or \
"entries" in os.listdir(config['journal']):
journal = Journal.DayOne(**config)
journal = DayOneJournal.DayOne(**config)
else:
util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
sys.exit(1)
@ -178,7 +183,7 @@ def run(manual_args=None):
elif config['editor']:
raw = util.get_text_from_editor(config)
else:
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n")
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n")
if raw:
args.text = [raw]
else:
@ -189,7 +194,7 @@ def run(manual_args=None):
raw = " ".join(args.text).strip()
if util.PY2 and type(raw) is not unicode:
raw = raw.decode(sys.getfilesystemencoding())
entry = journal.new_entry(raw)
journal.new_entry(raw)
util.prompt("[Entry added to {0} journal]".format(journal_name))
journal.write()
else:
@ -244,8 +249,10 @@ def run(manual_args=None):
num_deleted = old_num_entries - len(journal)
num_edited = len([e for e in journal.entries if e.modified])
prompts = []
if num_deleted: prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
if num_edited: prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
if num_deleted:
prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
if num_edited:
prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
if prompts:
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
journal.entries += other_entries

View file

@ -72,6 +72,7 @@ setup(
install_requires = [
"parsedatetime>=1.2",
"pytz>=2013b",
"six>=1.6.1",
"tzlocal>=1.1",
"keyring>=3.3",
"python-dateutil>=2.2"