From 78eda12287794b9aac7a79ef24d2f3ae8562d101 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 22 May 2014 11:56:28 -0700 Subject: Official support for python 3.4 --- .travis.yml | 2 +- CHANGELOG.md | 4 ++++ jrnl/__init__.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 278906eb..35f34a63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eb1a0be..cb89a41a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 1e1dc0ec..91b75919 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -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' -- cgit v1.2.3 From 09066ee64d05a63b61f0c2575074d3c457147d2b Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 22 May 2014 12:16:26 -0700 Subject: Fix encoding in tests --- .../bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry | 2 +- .../bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D512.doentry | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry b/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry index 745a08df..79ac2401 100644 --- a/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry +++ b/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry @@ -23,7 +23,7 @@ Location Administrative Area - Östergötlands län + Östergötlands län Country Sverige Latitude diff --git a/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D512.doentry b/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D512.doentry index f005a1ff..ea3efec5 100644 --- a/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D512.doentry +++ b/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D512.doentry @@ -19,7 +19,7 @@ Location Administrative Area - Östergötlands län + Östergötlands län Country Sverige Latitude -- cgit v1.2.3 From e592e81f6f03982d5605e252691b83204a78be63 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 22 May 2014 12:17:54 -0700 Subject: Split DayOne into separate file --- jrnl/DayOneJournal.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++ jrnl/Journal.py | 125 ----------------------------------------------- jrnl/cli.py | 27 ++++++---- 3 files changed, 150 insertions(+), 135 deletions(-) create mode 100644 jrnl/DayOneJournal.py diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py new file mode 100644 index 00000000..b3dbf0ea --- /dev/null +++ b/jrnl/DayOneJournal.py @@ -0,0 +1,133 @@ +#!/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""" + 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 diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 74afecf7..35bdc395 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -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 diff --git a/jrnl/cli.py b/jrnl/cli.py index 417398a4..4aa16588 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -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 -- cgit v1.2.3 From 726d259289d19b66739ba226761065296200fe78 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 22 May 2014 12:18:00 -0700 Subject: add six to requirements --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 477735f9..6b48675a 100644 --- a/setup.py +++ b/setup.py @@ -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" -- cgit v1.2.3 From 9265c3f85089ceb929a16a5992c7f6dfb4a680d8 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 22 May 2014 13:24:04 -0700 Subject: Fixes xml header --- .../bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry b/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry index 79ac2401..066821bb 100644 --- a/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry +++ b/features/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry @@ -1,4 +1,4 @@ - + -- cgit v1.2.3 From 2948f6dc69b34f27f5ea84583466ec2466012069 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 22 May 2014 13:24:19 -0700 Subject: Catch proper exceptions in python3.4 --- jrnl/DayOneJournal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py index b3dbf0ea..d5649eb3 100644 --- a/jrnl/DayOneJournal.py +++ b/jrnl/DayOneJournal.py @@ -17,6 +17,10 @@ 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 = [] @@ -29,7 +33,7 @@ class DayOne(Journal.Journal): with open(filename, 'rb') as plist_entry: try: dict_entry = plistlib.readPlist(plist_entry) - except ExpatError: + except self.PLIST_EXCEPTIONS: pass else: try: -- cgit v1.2.3