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.6"
- "2.7" - "2.7"
- "3.3" - "3.3"
# - "3.4" # Not available on Travis yet, see https://github.com/travis-ci/travis-ci/issues/1989 - "3.4"
install: install:
- "pip install -e . --use-mirrors" - "pip install -e . --use-mirrors"
- "pip install pycrypto>=2.6 --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 (December 22, 2013)
* __1.7.22__ Fixed an issue with writing files when exporting entries containing non-ascii characters. * __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" <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd"> "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
@ -23,7 +23,7 @@
<key>Location</key> <key>Location</key>
<dict> <dict>
<key>Administrative Area</key> <key>Administrative Area</key>
<string>Ãstergötlands län</string> <string>Östergötlands län</string>
<key>Country</key> <key>Country</key>
<string>Sverige</string> <string>Sverige</string>
<key>Latitude</key> <key>Latitude</key>

View file

@ -19,7 +19,7 @@
<key>Location</key> <key>Location</key>
<dict> <dict>
<key>Administrative Area</key> <key>Administrative Area</key>
<string>Ãstergötlands län</string> <string>Östergötlands län</string>
<key>Country</key> <key>Country</key>
<string>Sverige</string> <string>Sverige</string>
<key>Latitude</key> <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 Entry
from . import util from . import util
import codecs import codecs
import os
try: import parsedatetime.parsedatetime_consts as pdt try: import parsedatetime.parsedatetime_consts as pdt
except ImportError: import parsedatetime as pdt except ImportError: import parsedatetime as pdt
import re import re
from datetime import datetime from datetime import datetime
import dateutil import dateutil
import time
import sys import sys
try: try:
from Crypto.Cipher import AES from Crypto.Cipher import AES
@ -20,11 +18,6 @@ try:
except ImportError: except ImportError:
crypto_installed = False crypto_installed = False
import hashlib import hashlib
import plistlib
import pytz
import uuid
import tzlocal
from xml.parsers.expat import ExpatError
class Journal(object): class Journal(object):
@ -328,121 +321,3 @@ class Journal(object):
for entry in mod_entries: for entry in mod_entries:
entry.modified = not any(entry == old_entry for old_entry in self.entries) entry.modified = not any(entry == old_entry for old_entry in self.entries)
self.entries = mod_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 from __future__ import absolute_import
__title__ = 'jrnl' __title__ = 'jrnl'
__version__ = '1.7.22' __version__ = '1.8.0'
__author__ = 'Manuel Ebert' __author__ = 'Manuel Ebert'
__license__ = 'MIT License' __license__ = 'MIT License'
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert' __copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'

View file

@ -9,10 +9,10 @@
from __future__ import absolute_import from __future__ import absolute_import
from . import Journal from . import Journal
from . import DayOneJournal
from . import util from . import util
from . import exporters from . import exporters
from . import install from . import install
from . import __version__
import jrnl import jrnl
import os import os
import argparse import argparse
@ -49,6 +49,7 @@ def parse_args(args=None):
return parser.parse_args(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"""
compose = True compose = True
@ -65,6 +66,7 @@ def guess_mode(args, config):
return compose, export return compose, export
def encrypt(journal, filename=None): def encrypt(journal, filename=None):
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """ """ Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
password = util.getpass("Enter new password: ") password = util.getpass("Enter new password: ")
@ -75,6 +77,7 @@ def encrypt(journal, filename=None):
util.set_keychain(journal.name, password) util.set_keychain(journal.name, password)
util.prompt("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
@ -82,20 +85,21 @@ def decrypt(journal, filename=None):
journal.write(filename) journal.write(filename)
util.prompt("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):
util.prompt("[Journal created at {0}]".format(filename)) util.prompt("[Journal created at {0}]".format(filename))
open(filename, 'a').close() open(filename, 'a').close()
def list_journals(config): def list_journals(config):
"""List the journals specified in the configuration file""" """List the journals specified in the configuration file"""
sep = "\n" sep = "\n"
journal_list = sep.join(config['journals']) journal_list = sep.join(config['journals'])
return journal_list return journal_list
def update_config(config, new_config, scope, force_local=False): def update_config(config, new_config, scope, force_local=False):
"""Updates a config dict with new values - either global if scope is None """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, 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: else:
config.update(new_config) config.update(new_config)
def run(manual_args=None): def run(manual_args=None):
args = parse_args(manual_args) 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] 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 os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or \ if config['journal'].strip("/").endswith(".dayone") or \
"entries" in os.listdir(config['journal']): "entries" in os.listdir(config['journal']):
journal = Journal.DayOne(**config) journal = DayOneJournal.DayOne(**config)
else: else:
util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']))
sys.exit(1) sys.exit(1)
@ -189,7 +194,7 @@ def run(manual_args=None):
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()
if util.PY2 and type(raw) is not unicode: if util.PY2 and type(raw) is not unicode:
raw = raw.decode(sys.getfilesystemencoding()) raw = raw.decode(sys.getfilesystemencoding())
entry = journal.new_entry(raw) journal.new_entry(raw)
util.prompt("[Entry added to {0} journal]".format(journal_name)) util.prompt("[Entry added to {0} journal]".format(journal_name))
journal.write() journal.write()
else: else:
@ -244,8 +249,10 @@ def run(manual_args=None):
num_deleted = old_num_entries - len(journal) num_deleted = old_num_entries - len(journal)
num_edited = len([e for e in journal.entries if e.modified]) num_edited = len([e for e in journal.entries if e.modified])
prompts = [] prompts = []
if num_deleted: prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries")) if num_deleted:
if num_edited: prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries")) 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: if prompts:
util.prompt("[{0}]".format(", ".join(prompts).capitalize())) util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
journal.entries += other_entries journal.entries += other_entries

View file

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