diff --git a/.travis.yml b/.travis.yml
index 35f34a63..93c1f82b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,8 +5,8 @@ python:
- "3.3"
- "3.4"
install:
- - "pip install -e . --use-mirrors"
- - "pip install pycrypto>=2.6 --use-mirrors"
+ - "pip install -e ."
+ - "pip install pycrypto>=2.6"
- "pip install -q behave"
# command to run tests
script:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 699d7c08..4dd5e1d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ Changelog
### 1.9 (July 21, 2014)
+* __1.9.8__ Fixes a problem with temporary files on windows
+* __1.9.7__ Fixes writing non-ascii entries on the prompt
+* __1.9.6__ Fuzzy time parsing improvements (thanks to @pcarranza)
+* __1.9.5__ Multi-word tags for DayOne Journals
+* __1.9.4__ Fixed: Order of journal entries in file correct after --edit'ing
+* __1.9.3__ Fixed: Tags at the beginning of lines
* __1.9.2__ Fixed: Tag search ignores email-addresses (thanks to @mjhoffman65)
* __1.9.1__ Fixed: Dates in the future can be parsed as well.
* __1.9.0__ Improved: Greatly improved date parsing. Also added an `-on` option for filtering
diff --git a/docs/_themes/jrnl/static/img/favicon-152.png b/docs/_themes/jrnl/static/img/favicon-152.png
index ac658d9c..539d40cc 100644
Binary files a/docs/_themes/jrnl/static/img/favicon-152.png and b/docs/_themes/jrnl/static/img/favicon-152.png differ
diff --git a/docs/_themes/jrnl/static/img/favicon.ico b/docs/_themes/jrnl/static/img/favicon.ico
index 7c9c2c1e..d6197b35 100644
Binary files a/docs/_themes/jrnl/static/img/favicon.ico and b/docs/_themes/jrnl/static/img/favicon.ico differ
diff --git a/docs/_themes/jrnl/static/img/logo.png b/docs/_themes/jrnl/static/img/logo.png
index 1ea79cf1..900ebac6 100644
Binary files a/docs/_themes/jrnl/static/img/logo.png and b/docs/_themes/jrnl/static/img/logo.png differ
diff --git a/docs/_themes/jrnl/static/img/logo@2x.png b/docs/_themes/jrnl/static/img/logo@2x.png
index 9cc3d76b..2ef28d1a 100644
Binary files a/docs/_themes/jrnl/static/img/logo@2x.png and b/docs/_themes/jrnl/static/img/logo@2x.png differ
diff --git a/docs/encryption.rst b/docs/encryption.rst
index 37d341a8..8ca1faba 100644
--- a/docs/encryption.rst
+++ b/docs/encryption.rst
@@ -30,7 +30,12 @@ A note on security
While jrnl follows best practises, true security is an illusion. Specifically, jrnl will leave traces in your memory and your shell history -- it's meant to keep journals secure in transit, for example when storing it on an `untrusted `_ services such as Dropbox. If you're concerned about security, disable history logging for journal in your ``.bashrc`` ::
- HISTINGNORE="jrnl *"
+ HISTIGNORE="jrnl *"
+
+If you are using zsh instead of bash, you can get the same behaviour adding this to your ``zshrc`` ::
+
+ setopt HIST_IGNORE_SPACE
+ alias jrnl=" jrnl"
Manual decryption
-----------------
diff --git a/docs/installation.rst b/docs/installation.rst
index 46df9b3a..9cadd548 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -6,7 +6,11 @@ Getting started
Installation
------------
-Install *jrnl* using pip ::
+On OS X, the easiest way to install *jrnl* is using `Homebrew `_ ::
+
+ brew install jrnl
+
+On other platforms, install *jrnl* using pip ::
pip install jrnl
diff --git a/features/data/configs/multiple_without_default.json b/features/data/configs/multiple_without_default.json
new file mode 100644
index 00000000..042e843a
--- /dev/null
+++ b/features/data/configs/multiple_without_default.json
@@ -0,0 +1,16 @@
+{
+ "default_hour": 9,
+ "timeformat": "%Y-%m-%d %H:%M",
+ "linewrap": 80,
+ "encrypt": false,
+ "editor": "",
+ "default_minute": 0,
+ "highlight": true,
+ "password": "",
+ "journals": {
+ "simple": "features/journals/simple.journal",
+ "work": "features/journals/work.journal",
+ "ideas": "features/journals/nothing.journal"
+ },
+ "tagsymbols": "@"
+}
diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature
index 0510209b..7c77ff72 100644
--- a/features/multiple_journals.feature
+++ b/features/multiple_journals.feature
@@ -34,3 +34,8 @@ Feature: Multiple journals
Then journal "ideas" should not exist
When we run "jrnl ideas 23 july 2012: sell my junk on ebay and make lots of money"
Then journal "ideas" should have 1 entry
+
+ Scenario: Gracefully handle a config without a default journal
+ Given we use the config "multiple_without_default.json"
+ When we run "jrnl fork this repo and fix something"
+ Then we should see the message "You have not specified a journal. Either provide a default journal in your config file, or specify one of your journals on the command line."
diff --git a/features/regression.feature b/features/regression.feature
index 1672afb4..f975a4b1 100644
--- a/features/regression.feature
+++ b/features/regression.feature
@@ -59,3 +59,10 @@ Feature: Zapped bugs should stay dead.
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
| I'm feeling sore because I forgot to stretch.
"""
+
+ Scenario: Writing an entry at the prompt with non-ascii characters
+ # https://github.com/maebert/jrnl/issues/295
+ Given we use the config "basic.json"
+ When we run "jrnl" and enter "Crème brûlée & Mötorhead"
+ Then we should get no error
+ and the journal should contain "Crème brûlée & Mötorhead"
diff --git a/features/steps/core.py b/features/steps/core.py
index c4aa2f59..d0ea8460 100644
--- a/features/steps/core.py
+++ b/features/steps/core.py
@@ -2,9 +2,8 @@ from behave import *
from jrnl import cli, Journal, util
from dateutil import parser as date_parser
import os
-import sys
+import codecs
import json
-import pytz
import keyring
keyring.set_keyring(keyring.backends.file.PlaintextKeyring())
try:
@@ -30,7 +29,7 @@ def _parse_args(command):
def read_journal(journal_name="default"):
with open(cli.CONFIG_PATH) as config_file:
config = json.load(config_file)
- with open(config['journals'][journal_name]) as journal_file:
+ with codecs.open(config['journals'][journal_name], 'r', 'utf-8') as journal_file:
journal = journal_file.read()
return journal
@@ -57,7 +56,7 @@ def run_with_input(context, command, inputs=None):
buffer = StringIO(text.strip())
util.STDIN = buffer
try:
- cli.run(args or None)
+ cli.run(args)
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
@@ -66,7 +65,7 @@ def run_with_input(context, command, inputs=None):
def run(context, command):
args = _parse_args(command)
try:
- cli.run(args or None)
+ cli.run(args)
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
@@ -124,10 +123,8 @@ def check_output(context, text=None):
def check_output_time_inline(context, text):
out = context.stdout_capture.getvalue()
local_tz = tzlocal.get_localzone()
- utc_time = date_parser.parse(text)
- date = utc_time + local_tz._utcoffset
- local_date = date.strftime("%Y-%m-%d %H:%M")
- assert local_date in out, local_date
+ local_time = date_parser.parse(text).astimezone(local_tz).strftime("%Y-%m-%d %H:%M")
+ assert local_time in out, local_time
@then('the output should contain "{text}"')
def check_output_inline(context, text):
@@ -186,7 +183,7 @@ def config_var(context, key, value, journal=None):
@then('the journal should have {number:d} entry')
@then('journal "{journal_name}" should have {number:d} entries')
@then('journal "{journal_name}" should have {number:d} entry')
-def check_journal_content(context, number, journal_name="default"):
+def check_num_entries(context, number, journal_name="default"):
journal = open_journal(journal_name)
assert len(journal.entries) == number
diff --git a/features/tagging.feature b/features/tagging.feature
index 649ff9bf..4eba8470 100644
--- a/features/tagging.feature
+++ b/features/tagging.feature
@@ -31,12 +31,22 @@ Feature: Tagging
@c++ : 1
@c# : 1
"""
- Scenario: An email should not be a tag
- Given we use the config "tags-237.json"
- When we run "jrnl --tags"
- Then we should get no error
- and the output should be
- """
- @newline : 1
- @email : 1
- """
\ No newline at end of file
+ Scenario: An email should not be a tag
+ Given we use the config "tags-237.json"
+ When we run "jrnl --tags"
+ Then we should get no error
+ and the output should be
+ """
+ @newline : 1
+ @email : 1
+ """
+
+ Scenario: Entry cans start and end with tags
+ Given we use the config "basic.json"
+ When we run "jrnl today: @foo came over, we went to a @bar"
+ When we run "jrnl --tags"
+ Then the output should be
+ """
+ @foo : 1
+ @bar : 1
+ """
diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py
index d5649eb3..6d91a9bd 100644
--- a/jrnl/DayOneJournal.py
+++ b/jrnl/DayOneJournal.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# encoding: utf-8
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
from . import Entry
from . import Journal
import os
@@ -65,7 +65,7 @@ class DayOne(Journal.Journal):
'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]
+ 'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags]
}
plistlib.writePlist(entry_plist, filename)
for entry in self._deleted_entries:
@@ -75,7 +75,7 @@ class DayOne(Journal.Journal):
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])
+ return "\n".join(["# {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."""
diff --git a/jrnl/Entry.py b/jrnl/Entry.py
index fb92c3b6..66b32f6c 100755
--- a/jrnl/Entry.py
+++ b/jrnl/Entry.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python
# encoding: utf-8
+from __future__ import unicode_literals
import re
import textwrap
from datetime import datetime
@@ -16,9 +17,15 @@ class Entry:
self.starred = starred
self.modified = False
+ @staticmethod
+ def tag_regex(tagsymbols):
+ pattern = r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
+ return re.compile( pattern, re.UNICODE )
+
def parse_tags(self):
- fulltext = " ".join([self.title, self.body]).lower()
- tags = re.findall(r'(?u)\s([{tags}][-+*#/\w]+)'.format(tags=self.journal.config['tagsymbols']), fulltext, re.UNICODE)
+ fulltext = " " + " ".join([self.title, self.body]).lower()
+ tagsymbols = self.journal.config['tagsymbols']
+ tags = re.findall( Entry.tag_regex(tagsymbols), fulltext )
self.tags = tags
return set(tags)
@@ -28,7 +35,7 @@ class Entry:
title = date_str + " " + self.title.rstrip("\n ")
if self.starred:
title += " *"
- return u"{title}{sep}{body}\n".format(
+ return "{title}{sep}{body}\n".format(
title=title,
sep="\n" if self.body.rstrip("\n ") else "",
body=self.body.rstrip("\n ")
@@ -58,7 +65,7 @@ class Entry:
if short:
return title
else:
- return u"{title}{sep}{body}\n".format(
+ return "{title}{sep}{body}\n".format(
title=title,
sep="\n" if has_body else "",
body=body if has_body else "",
@@ -95,7 +102,7 @@ class Entry:
space = "\n"
md_head = "###"
- return u"{md} {date}, {title} {body} {space}".format(
+ return "{md} {date}, {title} {body} {space}".format(
md=md_head,
date=date_str,
title=self.title,
diff --git a/jrnl/Journal.py b/jrnl/Journal.py
index 2dab065a..92a6774f 100644
--- a/jrnl/Journal.py
+++ b/jrnl/Journal.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# encoding: utf-8
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
from . import Entry
from . import util
from . import time
@@ -165,9 +165,9 @@ class Journal(object):
lambda match: util.colorize(match.group(0)),
pp, re.UNICODE)
else:
- pp = re.sub(r"(?u)([{tags}]\w+)".format(tags=self.config['tagsymbols']),
- lambda match: util.colorize(match.group(0)),
- pp)
+ pp = re.sub( Entry.Entry.tag_regex(self.config['tagsymbols']),
+ lambda match: util.colorize(match.group(0)),
+ pp)
return pp
def __repr__(self):
@@ -176,7 +176,7 @@ class Journal(object):
def write(self, filename=None):
"""Dumps the journal into the config file, overwriting it"""
filename = filename or self.config['journal']
- journal = u"\n".join([e.__unicode__() for e in self.entries])
+ journal = "\n".join([e.__unicode__() for e in self.entries])
if self.config['encrypt']:
journal = self._encrypt(journal)
with open(filename, 'wb') as journal_file:
@@ -269,7 +269,7 @@ class Journal(object):
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([e.__unicode__() for e in self.entries])
+ return "\n".join([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."""
diff --git a/jrnl/__init__.py b/jrnl/__init__.py
index 8d23f05e..44bf8eac 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.9.2'
+__version__ = '1.9.8'
__author__ = 'Manuel Ebert'
__license__ = 'MIT License'
__copyright__ = 'Copyright 2013 - 2014 Manuel Ebert'
diff --git a/jrnl/cli.py b/jrnl/cli.py
index af60bbe4..35764734 100644
--- a/jrnl/cli.py
+++ b/jrnl/cli.py
@@ -7,7 +7,7 @@
license: MIT, see LICENSE for more details.
"""
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
from . import Journal
from . import DayOneJournal
from . import util
@@ -17,16 +17,19 @@ import jrnl
import os
import argparse
import sys
+import logging
xdg_config = os.environ.get('XDG_CONFIG_HOME')
CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config')
PYCRYPTO = install.module_exists("Crypto")
+log = logging.getLogger(__name__)
def parse_args(args=None):
parser = argparse.ArgumentParser()
parser.add_argument('-v', '--version', dest='version', action="store_true", help="prints version information and exits")
parser.add_argument('-ls', dest='ls', action="store_true", help="displays accessible journals")
+ parser.add_argument('-d', '--debug', dest='debug', action='store_true', help='execute in debug mode')
composing = parser.add_argument_group('Composing', 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."')
composing.add_argument('text', metavar='', nargs="*")
@@ -61,7 +64,7 @@ def guess_mode(args, config):
elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred)):
# Any sign of displaying stuff?
compose = False
- elif args.text and all(word[0] in config['tagsymbols'] for word in u" ".join(args.text).split()):
+ elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
# No date and only tags?
compose = False
@@ -90,6 +93,7 @@ def decrypt(journal, filename=None):
def touch_journal(filename):
"""If filename does not exist, touch the file"""
if not os.path.exists(filename):
+ log.debug('Creating journal file %s', filename)
util.prompt("[Journal created at {0}]".format(filename))
open(filename, 'a').close()
@@ -114,8 +118,15 @@ def update_config(config, new_config, scope, force_local=False):
config.update(new_config)
+def configure_logger(debug=False):
+ logging.basicConfig(level=logging.DEBUG if debug else logging.INFO,
+ format='%(levelname)-8s %(name)-12s %(message)s')
+ logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging
+
+
def run(manual_args=None):
args = parse_args(manual_args)
+ configure_logger(args.debug)
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
if args.version:
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__)
@@ -123,8 +134,10 @@ def run(manual_args=None):
sys.exit(0)
if not os.path.exists(CONFIG_PATH):
+ log.debug('Configuration file not found, installing jrnl...')
config = install.install_jrnl(CONFIG_PATH)
else:
+ log.debug('Reading configuration from file %s', CONFIG_PATH)
config = util.load_and_fix_json(CONFIG_PATH)
install.upgrade_config(config, config_path=CONFIG_PATH)
@@ -132,6 +145,7 @@ def run(manual_args=None):
print(util.py2encode(list_journals(config)))
sys.exit(0)
+ log.debug('Using configuration "%s"', config)
original_config = config.copy()
# check if the configuration is supported by available modules
if config['encrypt'] and not PYCRYPTO:
@@ -151,15 +165,34 @@ def run(manual_args=None):
except:
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
+
+ if config['journal'] is None:
+ util.prompt("You have not specified a journal. Either provide a default journal in your config file, or specify one of your journals on the command line.")
+ sys.exit(1)
+
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
touch_journal(config['journal'])
+ log.debug('Using journal path %(journal)s', config)
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("[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,22 +216,12 @@ 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()
if util.PY2 and type(raw) is not unicode:
raw = raw.decode(sys.getfilesystemencoding())
+ log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
journal.new_entry(raw)
util.prompt("[Entry added to {0} journal]".format(journal_name))
journal.write()
@@ -233,20 +256,20 @@ def run(manual_args=None):
elif args.encrypt is not False:
encrypt(journal, filename=args.encrypt)
# Not encrypting to a separate file: update config!
- if not args.encrypt:
+ if not args.encrypt or args.encrypt == config['journal']:
update_config(original_config, {"encrypt": True}, journal_name, force_local=True)
install.save_config(original_config, config_path=CONFIG_PATH)
elif args.decrypt is not False:
decrypt(journal, filename=args.decrypt)
# Not decrypting to a separate file: update config!
- if not args.decrypt:
+ if not args.decrypt or args.decrypt == config['journal']:
update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
install.save_config(original_config, config_path=CONFIG_PATH)
elif args.edit:
if not config['editor']:
- util.prompt(u"[You need to specify an editor in {0} to use the --edit function.]".format(CONFIG_PATH))
+ util.prompt("[You need to specify an editor in {0} to use the --edit function.]".format(CONFIG_PATH))
sys.exit(1)
other_entries = [e for e in old_entries if e not in journal.entries]
# Edit
@@ -259,10 +282,11 @@ def run(manual_args=None):
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"))
+ prompts.append("{0} {1} modified".format(num_edited, "entry" if num_edited == 1 else "entries"))
if prompts:
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
journal.entries += other_entries
+ journal.sort()
journal.write()
if __name__ == "__main__":
diff --git a/jrnl/exporters.py b/jrnl/exporters.py
index 71919f34..ad2c3186 100644
--- a/jrnl/exporters.py
+++ b/jrnl/exporters.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# encoding: utf-8
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
import os
import json
from .util import u, slugify
@@ -29,7 +29,7 @@ def to_tag_list(journal):
elif min(tag_counts)[0] == 0:
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
result += '[Removed tags that appear only once.]\n'
- result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
+ result += "\n".join("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
return result
@@ -81,7 +81,7 @@ def export(journal, format, output=None):
"markdown": to_md
}
if format not in maps:
- return u"[ERROR: can't export to '{0}'. Valid options are 'md', 'txt', and 'json']".format(format)
+ return "[ERROR: can't export to '{0}'. Valid options are 'md', 'txt', and 'json']".format(format)
if output and os.path.isdir(output): # multiple files
return write_files(journal, output, format)
else:
@@ -90,9 +90,9 @@ def export(journal, format, output=None):
try:
with codecs.open(output, "w", "utf-8") as f:
f.write(content)
- return u"[Journal exported to {0}]".format(output)
+ return "[Journal exported to {0}]".format(output)
except IOError as e:
- return u"[ERROR: {0} {1}]".format(e.filename, e.strerror)
+ return "[ERROR: {0} {1}]".format(e.filename, e.strerror)
else:
return content
@@ -111,4 +111,4 @@ def write_files(journal, path, format):
content = e.__unicode__()
with codecs.open(full_path, "w", "utf-8") as f:
f.write(content)
- return u"[Journal exported individual files in {0}]".format(path)
+ return "[Journal exported individual files in {0}]".format(path)
diff --git a/jrnl/time.py b/jrnl/time.py
index 531293de..378d4c92 100644
--- a/jrnl/time.py
+++ b/jrnl/time.py
@@ -48,7 +48,10 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
return None
if flag is 1: # Date found, but no time. Use the default time.
- date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0)
+ date = datetime(*date[:3],
+ hour=23 if inclusive else default_hour or 0,
+ minute=59 if inclusive else default_minute or 0,
+ second=59 if inclusive else 0)
else:
date = datetime(*date[:6])
diff --git a/jrnl/util.py b/jrnl/util.py
index b06113c2..e9df0fb1 100644
--- a/jrnl/util.py
+++ b/jrnl/util.py
@@ -13,6 +13,7 @@ import tempfile
import subprocess
import codecs
import unicodedata
+import logging
PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2
@@ -22,6 +23,8 @@ STDOUT = sys.stdout
TEST = False
__cached_tz = None
+log = logging.getLogger(__name__)
+
def getpass(prompt="Password: "):
if not TEST:
@@ -71,16 +74,19 @@ def py2encode(s):
def prompt(msg):
"""Prints a message to the std err stream defined in util."""
+ if not msg:
+ return
if not msg.endswith("\n"):
msg += "\n"
STDERR.write(u(msg))
def py23_input(msg=""):
- STDERR.write(u(msg))
- return STDIN.readline().strip()
+ prompt(msg)
+ return u(STDIN.readline()).strip()
def py23_read(msg=""):
- return STDIN.read()
+ prompt(msg)
+ return u(STDIN.read())
def yesno(prompt, default=True):
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
@@ -93,27 +99,34 @@ def load_and_fix_json(json_path):
"""
with open(json_path) as f:
json_str = f.read()
- config = fixed = None
+ log.debug('Configuration file %s read correctly', json_path)
+ config = None
try:
return json.loads(json_str)
except ValueError as e:
+ log.debug('Could not parse configuration %s: %s', json_str, e,
+ exc_info=True)
# Attempt to fix extra ,
json_str = re.sub(r",[ \n]*}", "}", json_str)
# Attempt to fix missing ,
json_str = re.sub(r"([^{,]) *\n *(\")", r"\1,\n \2", json_str)
try:
+ log.debug('Attempting to reload automatically fixed configuration file %s',
+ json_str)
config = json.loads(json_str)
with open(json_path, 'w') as f:
json.dump(config, f, indent=2)
+ log.debug('Fixed configuration saved in file %s', json_path)
prompt("[Some errors in your jrnl config have been fixed for you.]")
return config
except ValueError as e:
+ log.debug('Could not load fixed configuration: %s', e, exc_info=True)
prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(json_path, e.message))
prompt("[Entry was NOT added to your journal]")
sys.exit(1)
def get_text_from_editor(config, template=""):
- tmpfile = os.path.join(tempfile.mktemp(prefix="jrnl"))
+ _, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
with codecs.open(tmpfile, 'w', "utf-8") as f:
if template:
f.write(template)
diff --git a/setup.py b/setup.py
index 1578b452..0208acdf 100644
--- a/setup.py
+++ b/setup.py
@@ -51,13 +51,8 @@ except ImportError:
readline_available = False
-if sys.argv[-1] == 'publish':
- os.system("python setup.py sdist upload")
- sys.exit()
-
base_dir = os.path.dirname(os.path.abspath(__file__))
-
def get_version(filename="jrnl/__init__.py"):
with open(os.path.join(base_dir, filename)) as initfile:
for line in initfile.readlines():
@@ -65,6 +60,71 @@ def get_version(filename="jrnl/__init__.py"):
if m:
return m.group(1)
+
+def get_changelog(filename="CHANGELOG.md"):
+ changelog = {}
+ current_version = None
+ with open(os.path.join(base_dir, filename)) as changelog_file:
+ for line in changelog_file.readlines():
+ if line.startswith("* __"):
+ parts = line.strip("* ").split(" ", 1)
+ if len(parts) == 2:
+ current_version, changes = parts[0].strip("_\n"), parts[1]
+ changelog[current_version] = [changes.strip()]
+ else:
+ current_version = parts[0].strip("_\n")
+ changelog[current_version] = []
+ elif line.strip() and current_version and not line.startswith("#"):
+ changelog[current_version].append(line.strip(" *\n"))
+ return changelog
+
+def dist_pypi():
+ os.system("python setup.py sdist upload")
+ sys.exit()
+
+def dist_github():
+ """Creates a release on the maebert/jrnl repository on github"""
+ import requests
+ import keyring
+ import getpass
+ version = get_version()
+ version_tuple = version.split(".")
+ changes_since_last_version = ["* __{}__: {}".format(key, "\n".join(changes)) for key, changes in get_changelog().items() if key.startswith("{}.{}".format(*version_tuple))]
+ changes_since_last_version = "\n".join(sorted(changes_since_last_version, reverse=True))
+ payload = {
+ "tag_name": version,
+ "target_commitish": "master",
+ "name": version,
+ "body": "Changes in Version {}.{}: \n\n{}".format(version_tuple[0], version_tuple[1], changes_since_last_version)
+ }
+ print("Preparing release {}...".format(version))
+ username = keyring.get_password("github", "__default_user") or raw_input("Github username: ")
+ password = keyring.get_password("github", username) or getpass.getpass()
+ otp = raw_input("One Time Token: ")
+ response = requests.post("https://api.github.com/repos/maebert/jrnl/releases", headers={"X-GitHub-OTP": otp}, json=payload, auth=(username, password))
+ if response.status_code in (403, 404):
+ print("Authentication error.")
+ else:
+ keyring.set_password("github", "__default_user", username)
+ keyring.set_password("github", username, password)
+ if response.status_code > 299:
+ if "message" in response.json():
+ print("Error: {}".format(response.json()['message']))
+ for error_dict in response.json().get('errors', []):
+ print("*", error_dict)
+ else:
+ print("Unkown error")
+ print(response.text)
+ else:
+ print("Release created.")
+ sys.exit()
+
+if sys.argv[-1] == 'publish':
+ dist_pypi()
+
+if sys.argv[-1] == 'github_release':
+ dist_github()
+
conditional_dependencies = {
"pyreadline>=2.0": not readline_available and "win32" in sys.platform,
"readline>=6.2": not readline_available and "win32" not in sys.platform,
@@ -92,24 +152,24 @@ setup(
},
long_description=__doc__,
entry_points={
- 'console_scripts': [
- 'jrnl = jrnl:run',
- ],
+ "console_scripts": [
+ "jrnl = jrnl:run",
+ ]
},
classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Environment :: Console',
- 'Intended Audience :: End Users/Desktop',
- 'License :: OSI Approved :: MIT License',
- 'Natural Language :: English',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2.6',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: 3.4',
- 'Topic :: Office/Business :: News/Diary',
- 'Topic :: Text Processing'
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Intended Audience :: End Users/Desktop",
+ "License :: OSI Approved :: MIT License",
+ "Natural Language :: English",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Topic :: Office/Business :: News/Diary",
+ "Topic :: Text Processing"
],
# metadata for upload to PyPI
author = "Manuel Ebert",