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",