Merge branch 'pr/211' into 2.0-rc1

This commit is contained in:
Manuel Ebert 2014-06-27 14:51:03 +02:00
commit 800a373462
7 changed files with 138 additions and 56 deletions

View file

@ -4,6 +4,10 @@ Changelog
### 1.8 (May 22, 2014)
* __1.8.4__ Improved: using external editors (thanks to @chrissexton)
* __1.8.3__ Fixed: export to text files and improves help (thanks to @igniteflow and @mpe)
* __1.8.2__ Better integration with environment variables (thanks to @ajaam and @matze)
* __1.8.1__ Minor bug fixes
* __1.8.0__ Official support for python 3.4
### 1.7 (December 22, 2013)

View file

@ -48,6 +48,15 @@ Text export
Pretty-prints your entire journal.
XML export
-----------
::
jrnl --export xml
Why anyone would want to export stuff to XML is beyond me, but here you go.
Export to files
---------------

View file

@ -79,26 +79,3 @@ class Entry:
def __ne__(self, other):
return not self.__eq__(other)
def to_dict(self):
return {
'title': self.title,
'body': self.body,
'date': self.date.strftime("%Y-%m-%d"),
'time': self.date.strftime("%H:%M"),
'starred': self.starred
}
def to_md(self):
date_str = self.date.strftime(self.journal.config['timeformat'])
body_wrapper = "\n\n" if self.body else ""
body = body_wrapper + self.body
space = "\n"
md_head = "###"
return u"{md} {date}, {title} {body} {space}".format(
md=md_head,
date=date_str,
title=self.title,
body=body,
space=space
)

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.8.1'
__version__ = '1.8.4'
__author__ = 'Manuel Ebert'
__license__ = 'MIT License'
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'

View file

@ -153,17 +153,6 @@ def run(manual_args=None):
touch_journal(config['journal'])
mode_compose, mode_export = guess_mode(args, config)
# open journal file or folder
if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or \
"entries" in os.listdir(config['journal']):
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)
else:
journal = Journal.Journal(journal_name, **config)
# How to quit writing?
if "win32" in sys.platform:
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
@ -183,6 +172,17 @@ def run(manual_args=None):
else:
mode_compose = False
# open journal file or folder
if os.path.isdir(config['journal']):
if config['journal'].strip("/").endswith(".dayone") or \
"entries" in os.listdir(config['journal']):
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)
else:
journal = Journal.Journal(journal_name, **config)
# Writing mode
if mode_compose:
raw = " ".join(args.text).strip()

View file

@ -6,6 +6,7 @@ import os
import json
from .util import u, slugify
import codecs
from xml.dom import minidom
def get_tags_count(journal):
@ -14,12 +15,12 @@ def get_tags_count(journal):
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
tags = [tag
for entry in journal.entries
for tag in set(entry.tags)
]
for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = set([(tags.count(tag), tag) for tag in tags])
return tag_counts
def to_tag_list(journal):
"""Prints a list of all tags and the number of occurrences."""
tag_counts = get_tags_count(journal)
@ -32,15 +33,82 @@ def to_tag_list(journal):
result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
return result
def entry_to_dict(entry):
return {
'title': entry.title,
'body': entry.body,
'date': entry.date.strftime("%Y-%m-%d"),
'time': entry.date.strftime("%H:%M"),
'starred': entry.starred
}
def to_json(journal):
"""Returns a JSON representation of the Journal."""
tags = get_tags_count(journal)
result = {
"tags": dict((tag, count) for count, tag in tags),
"entries": [e.to_dict() for e in journal.entries]
"entries": [entry_to_dict(e) for e in journal.entries]
}
return json.dumps(result, indent=2)
def entry_to_xml(entry, doc=None):
"""Turns an entry into an XML representation.
If doc is not given, it will return a full XML document.
Otherwise, it will only return a new 'entry' elemtent for
a given doc."""
doc_el = doc or minidom.Document()
entry_el = doc_el.createElement('entry')
for key, value in entry_to_dict(entry).items():
elem = doc_el.createElement(key)
elem.appendChild(doc_el.createTextNode(u(value)))
entry_el.appendChild(elem)
if not doc:
doc_el.appendChild(entry_el)
return doc_el.toprettyxml()
else:
return entry_el
def to_xml(journal):
"""Returns a XML representation of the Journal."""
tags = get_tags_count(journal)
doc = minidom.Document()
xml = doc.createElement('journal')
tags_el = doc.createElement('tags')
entries_el = doc.createElement('entries')
for tag in tags:
tag_el = doc.createElement('tag')
tag_el.setAttribute('name', tag[1])
count_node = doc.createTextNode(u(tag[0]))
tag.appendChild(count_node)
tags_el.appendChild(tag)
for entry in journal.entries:
entries_el.appendChild(entry_to_xml(entry, doc))
xml.appendChild(entries_el)
xml.appendChild(tags_el)
doc.appendChild(xml)
return doc.toprettyxml()
def entry_to_md(entry):
date_str = entry.date.strftime(entry.journal.config['timeformat'])
body_wrapper = "\n\n" if entry.body else ""
body = body_wrapper + entry.body
space = "\n"
md_head = "###"
return u"{md} {date}, {title} {body} {space}".format(
md=md_head,
date=date_str,
title=entry.title,
body=body,
space=space
)
def to_md(journal):
"""Returns a markdown representation of the Journal"""
out = []
@ -58,24 +126,29 @@ def to_md(journal):
result = "\n".join(out)
return result
def to_txt(journal):
"""Returns the complete text of the Journal."""
return journal.pprint()
def export(journal, format, output=None):
"""Exports the journal to various formats.
format should be one of json, txt, text, md, markdown.
format should be one of json, xml, txt, text, md, markdown.
If output is None, returns a unicode representation of the output.
If output is a directory, exports entries into individual files.
Otherwise, exports to the given output file.
"""
maps = {
"json": to_json,
"xml": to_xml,
"txt": to_txt,
"text": to_txt,
"md": to_md,
"markdown": to_md
}
if format not in maps:
return u"[ERROR: can't export to '{0}'. Valid options are 'md', 'txt', 'xml', and 'json']".format(format)
if output and os.path.isdir(output): # multiple files
return write_files(journal, output, format)
else:
@ -84,24 +157,27 @@ def export(journal, format, output=None):
try:
with codecs.open(output, "w", "utf-8") as f:
f.write(content)
return "[Journal exported to {0}]".format(output)
return u"[Journal exported to {0}]".format(output)
except IOError as e:
return "[ERROR: {0} {1}]".format(e.filename, e.strerror)
return u"[ERROR: {0} {1}]".format(e.filename, e.strerror)
else:
return content
def write_files(journal, path, format):
"""Turns your journal into separate files for each entry.
Format should be either json, md or txt."""
Format should be either json, xml, md or txt."""
make_filename = lambda entry: e.date.strftime("%C-%m-%d_{0}.{1}".format(slugify(u(e.title)), format))
for e in journal.entries:
full_path = os.path.join(path, make_filename(e))
if format == 'json':
content = json.dumps(e.to_dict(), indent=2) + "\n"
elif format == 'md':
content = e.to_md()
elif format == 'txt':
content = u(e)
content = json.dumps(entry_to_dict(e), indent=2) + "\n"
elif format in ('md', 'markdown'):
content = entry_to_md(e)
elif format in 'xml':
content = entry_to_xml(e)
elif format in ('txt', 'text'):
content = e.__unicode__()
with codecs.open(full_path, "w", "utf-8") as f:
f.write(content)
return "[Journal exported individual files in {0}]".format(path)
return u"[Journal exported individual files in {0}]".format(path)

View file

@ -2,10 +2,8 @@
# encoding: utf-8
import sys
import os
from tzlocal import get_localzone
import getpass as gp
import keyring
import pytz
import json
if "win32" in sys.platform:
import colorama
@ -24,12 +22,14 @@ STDOUT = sys.stdout
TEST = False
__cached_tz = None
def getpass(prompt="Password: "):
if not TEST:
return gp.getpass(prompt)
else:
return py23_input(prompt)
def get_password(validator, keychain=None, max_attempts=3):
pwd_from_keychain = keychain and get_keychain(keychain)
password = pwd_from_keychain or getpass()
@ -49,9 +49,11 @@ def get_password(validator, keychain=None, max_attempts=3):
prompt("Extremely wrong password.")
sys.exit(1)
def get_keychain(journal_name):
return keyring.get_password('jrnl', journal_name)
def set_keychain(journal_name, password):
if password is None:
try:
@ -61,40 +63,51 @@ def set_keychain(journal_name, password):
elif not TEST:
keyring.set_password('jrnl', journal_name, password)
def u(s):
"""Mock unicode function for python 2 and 3 compatibility."""
return s if PY3 or type(s) is unicode else unicode(s.encode('string-escape'), "unicode_escape")
if PY3:
return str(s)
elif isinstance(s, basestring) and type(s) is not unicode:
return unicode(s.encode('string-escape'), "unicode_escape")
return unicode(s)
def py2encode(s):
"""Encode in Python 2, but not in python 3."""
return s.encode("utf-8") if PY2 and type(s) is unicode else s
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 py23_read(msg=""):
STDERR.write(u(msg))
return STDIN.read()
def yesno(prompt, default=True):
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
raw = py23_input(prompt)
return {'y': True, 'n': False}.get(raw.lower(), default)
def load_and_fix_json(json_path):
"""Tries to load a json object from a file.
If that fails, tries to fix common errors (no or extra , at end of the line).
"""
with open(json_path) as f:
json_str = f.read()
config = fixed = None
config = None
try:
return json.loads(json_str)
except ValueError as e:
@ -113,8 +126,9 @@ def load_and_fix_json(json_path):
prompt("[Entry was NOT added to your journal]")
sys.exit(1)
def get_text_from_editor(config, template=""):
tmpfile = os.path.join(tempfile.gettempdir(), "jrnl")
tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl"))
with codecs.open(tmpfile, 'w', "utf-8") as f:
if template:
f.write(template)
@ -126,10 +140,12 @@ def get_text_from_editor(config, template=""):
prompt('[Nothing saved to file]')
return raw
def colorize(string):
"""Returns the string wrapped in cyan ANSI escape"""
return u"\033[36m{}\033[39m".format(string)
def slugify(string):
"""Slugifies a string.
Based on public domain code from https://github.com/zacharyvoase/slugify
@ -141,6 +157,7 @@ def slugify(string):
slug = re.sub(r'[-\s]+', '-', no_punctuation)
return u(slug)
def int2byte(i):
"""Converts an integer to a byte.
This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3."""
@ -151,4 +168,3 @@ def byte2int(b):
"""Converts a byte to an integer.
This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3."""
return ord(b)if PY2 else b