diff --git a/features/encryption.feature b/features/encryption.feature index 82d971eb3..d30c48b31 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -2,7 +2,7 @@ Scenario: Loading an encrypted journal Given we use the config "encrypted.yaml" When we run "jrnl -n 1" and enter "bad doggie no biscuit" - Then we should see the message "Password" + Then the output should contain "Password" and the output should contain "2013-06-10 15:40 Life is good" Scenario: Decrypting a journal @@ -14,16 +14,16 @@ Scenario: Encrypting a journal Given we use the config "basic.yaml" - When we run "jrnl --encrypt" and enter "swordfish" + When we run "jrnl --encrypt" and enter "swordfish" and "n" Then we should see the message "Journal encrypted" and the config for journal "default" should have "encrypt" set to "bool:True" When we run "jrnl -n 1" and enter "swordfish" - Then we should see the message "Password" + Then the output should contain "Password" and the output should contain "2013-06-10 15:40 Life is good" Scenario: Storing a password in Keychain Given we use the config "multiple.yaml" - When we run "jrnl simple --encrypt" and enter "sabertooth" + When we run "jrnl simple --encrypt" and enter "sabertooth" and "y" When we set the keychain password of "simple" to "sabertooth" Then the config for journal "simple" should have "encrypt" set to "bool:True" When we run "jrnl simple -n 1" diff --git a/features/environment.py b/features/environment.py index 6f9ac5dfb..7a918feb7 100644 --- a/features/environment.py +++ b/features/environment.py @@ -1,25 +1,15 @@ -from behave import * import shutil import os -import jrnl -try: - from io import StringIO -except ImportError: - from cStringIO import StringIO + def before_scenario(context, scenario): """Before each scenario, backup all config and journal test data.""" - context.messages = StringIO() - jrnl.util.STDERR = context.messages - jrnl.util.TEST = True - # Clean up in case something went wrong for folder in ("configs", "journals"): working_dir = os.path.join("features", folder) if os.path.exists(working_dir): shutil.rmtree(working_dir) - for folder in ("configs", "journals"): original = os.path.join("features", "data", folder) working_dir = os.path.join("features", folder) @@ -32,10 +22,9 @@ def before_scenario(context, scenario): else: shutil.copy2(source, working_dir) + def after_scenario(context, scenario): """After each scenario, restore all test data and remove working_dirs.""" - context.messages.close() - context.messages = None for folder in ("configs", "journals"): working_dir = os.path.join("features", folder) if os.path.exists(working_dir): diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature index 1d4943ee4..28587f960 100644 --- a/features/multiple_journals.feature +++ b/features/multiple_journals.feature @@ -42,5 +42,5 @@ Feature: Multiple journals Scenario: Don't crash if no file exists for a configured encrypted journal Given we use the config "multiple.yaml" - When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes" + When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes" and "y" Then we should see the message "Journal 'new_encrypted' created" diff --git a/features/steps/core.py b/features/steps/core.py index 83981d138..9217f77f3 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -1,5 +1,4 @@ -from __future__ import unicode_literals -from __future__ import absolute_import +from unittest.mock import patch from behave import given, when, then from jrnl import cli, install, Journal, util, plugins @@ -10,10 +9,13 @@ import json import yaml import keyring +import tzlocal +import shlex +import sys class TestKeyring(keyring.backend.KeyringBackend): - """A test keyring that just stores its valies in a hash""" + """A test keyring that just stores its values in a hash""" priority = 1 keys = defaultdict(dict) @@ -31,15 +33,6 @@ def delete_password(self, servicename, username, password): keyring.set_keyring(TestKeyring()) -try: - from io import StringIO -except ImportError: - from cStringIO import StringIO -import tzlocal -import shlex -import sys - - def ushlex(command): if sys.version_info[0] == 3: return shlex.split(command) @@ -73,18 +66,41 @@ def set_config(context, config_file): cf.write("version: {}".format(__version__)) +def _mock_getpass(inputs): + def prompt_return(prompt="Password: "): + print(prompt) + return next(inputs) + return prompt_return + + +def _mock_input(inputs): + def prompt_return(prompt=""): + val = next(inputs) + print(prompt, val) + return val + return prompt_return + + @when('we run "{command}" and enter') -@when('we run "{command}" and enter "{inputs}"') -def run_with_input(context, command, inputs=None): - text = inputs or context.text +@when('we run "{command}" and enter "{inputs1}"') +@when('we run "{command}" and enter "{inputs1}" and "{inputs2}"') +def run_with_input(context, command, inputs1="", inputs2=""): + # create an iterator through all inputs. These inputs will be fed one by one + # to the mocked calls for 'input()', 'util.getpass()' and 'sys.stdin.read()' + text = iter((inputs1, inputs2)) if inputs1 else iter(context.text.split("\n")) args = ushlex(command)[1:] - buffer = StringIO(text.strip()) - util.STDIN = buffer - try: - cli.run(args or []) - context.exit_status = 0 - except SystemExit as e: - context.exit_status = e.code + with patch("builtins.input", side_effect=_mock_input(text)) as mock_input: + with patch("jrnl.util.getpass", side_effect=_mock_getpass(text)) as mock_getpass: + with patch("sys.stdin.read", side_effect=text) as mock_read: + try: + cli.run(args or []) + context.exit_status = 0 + except SystemExit as e: + context.exit_status = e.code + + # assert at least one of the mocked input methods got called + assert mock_input.called or mock_getpass.called or mock_read.called + @when('we run "{command}"') @@ -190,28 +206,24 @@ def check_output_time_inline(context, text): def check_output_inline(context, text=None): text = text or context.text out = context.stdout_capture.getvalue() - if isinstance(out, bytes): - out = out.decode('utf-8') assert text in out, text @then('the output should not contain "{text}"') def check_output_not_inline(context, text): out = context.stdout_capture.getvalue() - if isinstance(out, bytes): - out = out.decode('utf-8') assert text not in out @then('we should see the message "{text}"') def check_message(context, text): - out = context.messages.getvalue() + out = context.stderr_capture.getvalue() assert text in out, [text, out] @then('we should not see the message "{text}"') def check_not_message(context, text): - out = context.messages.getvalue() + out = context.stderr_capture.getvalue() assert text not in out, [text, out] diff --git a/features/upgrade.feature b/features/upgrade.feature index bce026b8c..ddcce4943 100644 --- a/features/upgrade.feature +++ b/features/upgrade.feature @@ -13,11 +13,11 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x Scenario: Upgrading a journal encrypted with jrnl 1.x Given we use the config "encrypted_old.json" - When we run "jrnl -n 1" and enter + When we run "jrnl -n 1" and enter """ Y bad doggie no biscuit bad doggie no biscuit """ - Then we should see the message "Password" + Then the output should contain "Password" and the output should contain "2013-06-10 15:40 Life is good" diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py index 9e988f786..59314c4b9 100644 --- a/jrnl/DayOneJournal.py +++ b/jrnl/DayOneJournal.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import, unicode_literals from . import Entry from . import Journal from . import time as jrnl_time @@ -26,7 +24,7 @@ class DayOne(Journal.Journal): def __init__(self, **kwargs): self.entries = [] self._deleted_entries = [] - super(DayOne, self).__init__(**kwargs) + super().__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"))] @@ -83,7 +81,7 @@ def write(self): 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 "\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries]) + return "\n".join([f"# {e.uuid}\n{str(e)}" for e in self.entries]) def parse_editable_str(self, edited): """Parses the output of self.editable_str and updates its entries.""" @@ -107,7 +105,7 @@ def parse_editable_str(self, edited): current_entry.modified = False current_entry.uuid = m.group(1).lower() else: - date_blob_re = re.compile("^\[[^\\]]+\] ") + date_blob_re = re.compile("^\\[[^\\]]+\\] ") date_blob = date_blob_re.findall(line) if date_blob: date_blob = date_blob[0] diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index a83651e45..1cce66b81 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -8,14 +8,13 @@ import sys import os import base64 -import getpass import logging log = logging.getLogger() def make_key(password): - password = util.bytes(password) + password = password.encode("utf-8") kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, @@ -30,7 +29,7 @@ def make_key(password): class EncryptedJournal(Journal.Journal): def __init__(self, name='default', **kwargs): - super(EncryptedJournal, self).__init__(name, **kwargs) + super().__init__(name, **kwargs) self.config['encrypt'] = True def open(self, filename=None): @@ -48,9 +47,9 @@ def open(self, filename=None): self.config['password'] = password text = "" self._store(filename, text) - util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename)) + print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) else: - util.prompt("No password supplied for encrypted journal") + print("No password supplied for encrypted journal", file=sys.stderr) sys.exit(1) else: text = self._load(filename) @@ -59,7 +58,6 @@ def open(self, filename=None): log.debug("opened %s with %d entries", self.__class__.__name__, len(self)) return self - def _load(self, filename, password=None): """Loads an encrypted journal from a file and tries to decrypt it. If password is not provided, will look for password in the keychain @@ -99,7 +97,7 @@ class LegacyEncryptedJournal(Journal.LegacyJournal): """Legacy class to support opening journals encrypted with the jrnl 1.x standard. You'll not be able to save these journals anymore.""" def __init__(self, name='default', **kwargs): - super(LegacyEncryptedJournal, self).__init__(name, **kwargs) + super().__init__(name, **kwargs) self.config['encrypt'] = True def _load(self, filename, password=None): diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 1306cef5a..ca80b231e 100755 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import unicode_literals import re import textwrap from datetime import datetime @@ -51,14 +49,14 @@ def tags(self): @staticmethod def tag_regex(tagsymbols): - pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols) - return re.compile(pattern, re.UNICODE) + pattern = fr'(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)' + return re.compile(pattern) def _parse_tags(self): tagsymbols = self.journal.config['tagsymbols'] - return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)) + return {tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)} - def __unicode__(self): + def __str__(self): """Returns a string representation of the entry to be written into a journal file.""" date_str = self.date.strftime(self.journal.config['timeformat']) title = "[{}] {}".format(date_str, self.title.rstrip("\n ")) @@ -106,7 +104,7 @@ def pprint(self, short=False): ) def __repr__(self): - return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) + return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) def __hash__(self): return hash(self.__repr__()) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 72fe94b18..f130823d6 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -1,13 +1,10 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import, unicode_literals from . import Entry from . import util from . import time import os import sys -import codecs import re from datetime import datetime import logging @@ -15,7 +12,7 @@ log = logging.getLogger(__name__) -class Tag(object): +class Tag: def __init__(self, name, count=0): self.name = name self.count = count @@ -24,10 +21,10 @@ def __str__(self): return self.name def __repr__(self): - return "".format(self.name) + return f"" -class Journal(object): +class Journal: def __init__(self, name='default', **kwargs): self.config = { 'journal': "journal.txt", @@ -72,7 +69,7 @@ def open(self, filename=None): filename = filename or self.config['journal'] if not os.path.exists(filename): - util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename)) + print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) self._create(filename) text = self._load(filename) @@ -96,7 +93,7 @@ def validate_parsing(self): return True def _to_text(self): - return "\n".join([e.__unicode__() for e in self.entries]) + return "\n".join([str(e) for e in self.entries]) def _load(self, filename): raise NotImplementedError @@ -118,7 +115,7 @@ def _parse(self, journal_txt): # Initialise our current entry entries = [] - date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ") + date_blob_re = re.compile("(?:^|\n)\\[([^\\]]+)\\] ") last_entry_pos = 0 for match in date_blob_re.finditer(journal_txt): date_blob = match.groups()[0] @@ -140,9 +137,6 @@ def _parse(self, journal_txt): entry._parse_text() return entries - def __unicode__(self): - return self.pprint() - def pprint(self, short=False): """Prettyprints the journal's entries""" sep = "\n" @@ -153,7 +147,7 @@ def pprint(self, short=False): tagre = re.compile(re.escape(tag), re.IGNORECASE) pp = re.sub(tagre, lambda match: util.colorize(match.group(0)), - pp, re.UNICODE) + pp) else: pp = re.sub( Entry.Entry.tag_regex(self.config['tagsymbols']), @@ -162,8 +156,11 @@ def pprint(self, short=False): ) return pp + def __str__(self): + return self.pprint() + def __repr__(self): - return "".format(len(self.entries)) + return f"" def sort(self): """Sorts the Journal's entries by date""" @@ -183,7 +180,7 @@ def tags(self): for entry in self.entries 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]) + tag_counts = {(tags.count(tag), tag) for tag in tags} return [Tag(tag, count=count) for count, tag in sorted(tag_counts)] def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, exclude=[]): @@ -200,8 +197,8 @@ def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict= exclude is a list of the tags which should not appear in the results. entry is kept if any tag is present, unless they appear in exclude.""" - self.search_tags = set([tag.lower() for tag in tags]) - excluded_tags = set([tag.lower() for tag in exclude]) + self.search_tags = {tag.lower() for tag in tags} + excluded_tags = {tag.lower() for tag in exclude} end_date = time.parse(end_date, inclusive=True) start_date = time.parse(start_date) @@ -226,7 +223,7 @@ def new_entry(self, raw, date=None, sort=True): raw = raw.replace('\\n ', '\n').replace('\\n', '\n') starred = False # Split raw text into title and body - sep = re.search("\n|[\?!.]+ +\n?", raw) + sep = re.search(r"\n|[?!.]+ +\n?", raw) first_line = raw[:sep.end()].strip() if sep else raw starred = False @@ -254,7 +251,7 @@ def new_entry(self, raw, date=None, sort=True): 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 "\n".join([e.__unicode__() for e in self.entries]) + return "\n".join([str(e) for e in self.entries]) def parse_editable_str(self, edited): """Parses the output of self.editable_str and updates it's entries.""" @@ -270,15 +267,15 @@ def parse_editable_str(self, edited): class PlainJournal(Journal): @classmethod def _create(cls, filename): - with codecs.open(filename, "a", "utf-8"): + with open(filename, "a", encoding="utf-8"): pass def _load(self, filename): - with codecs.open(filename, "r", "utf-8") as f: + with open(filename, "r", encoding="utf-8") as f: return f.read() def _store(self, filename, text): - with codecs.open(filename, 'w', "utf-8") as f: + with open(filename, 'w', encoding="utf-8") as f: f.write(text) @@ -287,7 +284,7 @@ class LegacyJournal(Journal): standard. Main difference here is that in 1.x, timestamps were not cuddled by square brackets. You'll not be able to save these journals anymore.""" def _load(self, filename): - with codecs.open(filename, "r", "utf-8") as f: + with open(filename, "r", encoding="utf-8") as f: return f.read() def _parse(self, journal_txt): @@ -323,7 +320,7 @@ def _parse(self, journal_txt): # escaping for the new format). line = new_date_format_regex.sub(r' \1', line) if current_entry: - current_entry.text += line + u"\n" + current_entry.text += line + "\n" # Append last entry if current_entry: @@ -347,8 +344,9 @@ def open_journal(name, config, legacy=False): from . import DayOneJournal return DayOneJournal.DayOne(**config).open() else: - util.prompt( - u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']) + print( + f"[Error: {config['journal']} is a directory, but doesn't seem to be a DayOne journal either.", + file=sys.stderr ) sys.exit(1) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 57664dbb5..1905b1959 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# encoding: utf-8 import pkg_resources diff --git a/jrnl/__main__.py b/jrnl/__main__.py index 73e08b337..3ce867303 100644 --- a/jrnl/__main__.py +++ b/jrnl/__main__.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import, unicode_literals from . import cli diff --git a/jrnl/cli.py b/jrnl/cli.py index 65a535169..3cf670309 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# encoding: utf-8 """ jrnl @@ -7,8 +6,6 @@ license: MIT, see LICENSE for more details. """ -from __future__ import unicode_literals -from __future__ import absolute_import from . import Journal from . import util from . import install @@ -91,7 +88,7 @@ def encrypt(journal, filename=None): if util.yesno("Do you want to store the password in your keychain?", default=True): util.set_keychain(journal.name, journal.config['password']) - util.prompt("Journal encrypted to {0}.".format(filename or new_journal.config['journal'])) + print("Journal encrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr) def decrypt(journal, filename=None): @@ -102,12 +99,12 @@ def decrypt(journal, filename=None): new_journal = Journal.PlainJournal(filename, **journal.config) new_journal.entries = journal.entries new_journal.write(filename) - util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal'])) + print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr) def list_journals(config): """List the journals specified in the configuration file""" - result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH) + result = f"Journals defined in {install.CONFIG_FILE_PATH}\n" ml = min(max(len(k) for k in config['journals']), 20) for journal, cfg in config['journals'].items(): result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg) @@ -138,20 +135,19 @@ def configure_logger(debug=False): 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__) - print(util.py2encode(version_str)) + version_str = f"{jrnl.__title__} version {jrnl.__version__}" + print(version_str) sys.exit(0) try: config = install.load_or_install_jrnl() except UserAbort as err: - util.prompt("\n{}".format(err)) + print(f"\n{err}", file=sys.stderr) sys.exit(1) if args.ls: - util.prnt(list_journals(config)) + print(list_journals(config)) sys.exit(0) log.debug('Using configuration "%s"', config) @@ -161,11 +157,11 @@ def run(manual_args=None): # use this! journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default' - if journal_name is not 'default': + if journal_name != 'default': args.text = args.text[1:] elif "default" not in config['journals']: - util.prompt("No default journal configured.") - util.prompt(list_journals(config)) + print("No default journal configured.", file=sys.stderr) + print(list_journals(config), file=sys.stderr) sys.exit(1) config = util.scope_config(config, journal_name) @@ -175,12 +171,12 @@ def run(manual_args=None): try: args.limit = int(args.text[0].lstrip("-")) args.text = args.text[1:] - except: + except ValueError: pass log.debug('Using journal "%s"', journal_name) mode_compose, mode_export, mode_import = guess_mode(args, config) - + # How to quit writing? if "win32" in sys.platform: _exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter" @@ -190,21 +186,22 @@ def run(manual_args=None): if mode_compose and not args.text: if not sys.stdin.isatty(): # Piping data into jrnl - raw = util.py23_read() + raw = sys.stdin.read() elif config['editor']: template = "" if config['template']: try: template = open(config['template']).read() - except: - util.prompt("[Could not read template at '']".format(config['template'])) + except OSError: + print(f"[Could not read template at '{config['template']}']", file=sys.stderr) sys.exit(1) raw = util.get_text_from_editor(config, template) else: try: - raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n") + print("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", file=sys.stderr) + raw = sys.stdin.read() except KeyboardInterrupt: - util.prompt("[Entry NOT saved to journal.]") + print("[Entry NOT saved to journal.]", file=sys.stderr) sys.exit(0) if raw: args.text = [raw] @@ -215,7 +212,7 @@ def run(manual_args=None): try: journal = Journal.open_journal(journal_name, config) except KeyboardInterrupt: - util.prompt("[Interrupted while opening journal]".format(journal_name)) + print(f"[Interrupted while opening journal]", file=sys.stderr) sys.exit(1) # Import mode @@ -225,11 +222,9 @@ def run(manual_args=None): # Writing mode elif 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)) + print(f"[Entry added to {journal_name} journal]", file=sys.stderr) journal.write() if not mode_compose: @@ -246,14 +241,14 @@ def run(manual_args=None): # Reading mode if not mode_compose and not mode_export and not mode_import: - print(util.py2encode(journal.pprint())) + print(journal.pprint()) # Various export modes elif args.short: - print(util.py2encode(journal.pprint(short=True))) + print(journal.pprint(short=True)) elif args.tags: - print(util.py2encode(plugins.get_exporter("tags").export(journal))) + print(plugins.get_exporter("tags").export(journal)) elif args.export is not False: exporter = plugins.get_exporter(args.export) @@ -275,7 +270,8 @@ def run(manual_args=None): elif args.edit: if not config['editor']: - util.prompt("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR)) + print("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]" + .format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR), file=sys.stderr) sys.exit(1) other_entries = [e for e in old_entries if e not in journal.entries] # Edit @@ -286,11 +282,11 @@ def run(manual_args=None): 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")) + prompts.append("{} {} 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("{} {} modified".format(num_edited, "entry" if num_deleted == 1 else "entries")) if prompts: - util.prompt("[{0}]".format(", ".join(prompts).capitalize())) + print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr) journal.entries += other_entries journal.sort() journal.write() diff --git a/jrnl/export.py b/jrnl/export.py index d48733148..1ee4e6ffa 100644 --- a/jrnl/export.py +++ b/jrnl/export.py @@ -1,15 +1,12 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import, unicode_literals from .util import ERROR_COLOR, RESET_COLOR -from .util import slugify, u -from .template import Template +from .util import slugify +from .plugins.template import Template import os -import codecs -class Exporter(object): +class Exporter: """This Exporter can convert entries and journals into text files.""" def __init__(self, format): with open("jrnl/templates/" + format + ".template") as f: @@ -17,8 +14,8 @@ def __init__(self, format): self.template = Template(body) def export_entry(self, entry): - """Returns a unicode representation of a single entry.""" - return entry.__unicode__() + """Returns a string representation of a single entry.""" + return str(entry) def _get_vars(self, journal): return { @@ -28,36 +25,36 @@ def _get_vars(self, journal): } def export_journal(self, journal): - """Returns a unicode representation of an entire journal.""" + """Returns a string representation of an entire journal.""" return self.template.render_block("journal", **self._get_vars(journal)) def write_file(self, journal, path): """Exports a journal into a single file.""" try: - with codecs.open(path, "w", "utf-8") as f: + with open(path, "w", encoding="utf-8") as f: f.write(self.export_journal(journal)) - return "[Journal exported to {0}]".format(path) - except IOError as e: - return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) + return f"[Journal exported to {path}]" + except OSError as e: + return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]" def make_filename(self, entry): - return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), self.extension)) + return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(entry.title), self.extension)) def write_files(self, journal, path): """Exports a journal into individual files for each entry.""" for entry in journal.entries: try: full_path = os.path.join(path, self.make_filename(entry)) - with codecs.open(full_path, "w", "utf-8") as f: + with open(full_path, "w", encoding="utf-8") as f: f.write(self.export_entry(entry)) - except IOError as e: - return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) - return "[Journal exported to {0}]".format(path) + except OSError as e: + return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]" + return f"[Journal exported to {path}]" def export(self, journal, format="text", output=None): """Exports to individual files if output is an existing path, or into a single file if output is a file name, or returns the exporter's - representation as unicode if output is None.""" + representation as string if output is None.""" if output and os.path.isdir(output): # multiple files return self.write_files(journal, output) elif output: # single file diff --git a/jrnl/install.py b/jrnl/install.py index 5a80562fb..c104b46b2 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import import readline import glob import getpass @@ -69,7 +67,7 @@ def upgrade_config(config): for key in missing_keys: config[key] = default_config[key] save_config(config) - print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH)) + print(f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", file=sys.stderr) def save_config(config): @@ -91,10 +89,10 @@ def load_or_install_jrnl(): try: upgrade.upgrade_jrnl_if_necessary(config_path) except upgrade.UpgradeValidationException: - util.prompt("Aborting upgrade.") - util.prompt("Please tell us about this problem at the following URL:") - util.prompt("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException") - util.prompt("Exiting.") + print("Aborting upgrade.", file=sys.stderr) + print("Please tell us about this problem at the following URL:", file=sys.stderr) + print("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException", file=sys.stderr) + print("Exiting.", file=sys.stderr) sys.exit(1) upgrade_config(config) @@ -120,8 +118,8 @@ def autocomplete(text, state): readline.set_completer(autocomplete) # Where to create the journal? - path_query = 'Path to your journal file (leave blank for {}): '.format(JOURNAL_FILE_PATH) - journal_path = util.py23_input(path_query).strip() or JOURNAL_FILE_PATH + path_query = f'Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): ' + journal_path = input(path_query).strip() or JOURNAL_FILE_PATH default_config['journals']['default'] = os.path.expanduser(os.path.expandvars(journal_path)) path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it @@ -139,7 +137,7 @@ def autocomplete(text, state): else: util.set_keychain("default", None) EncryptedJournal._create(default_config['journals']['default'], password) - print("Journal will be encrypted.") + print("Journal will be encrypted.", file=sys.stderr) else: PlainJournal._create(default_config['journals']['default']) diff --git a/jrnl/plugins/__init__.py b/jrnl/plugins/__init__.py index 64d7b3ba2..8d59b5562 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -1,8 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals - from .text_exporter import TextExporter from .jrnl_importer import JRNLImporter from .json_exporter import JSONExporter @@ -15,8 +13,8 @@ __exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters __importers =[JRNLImporter] -__exporter_types = dict([(name, plugin) for plugin in __exporters for name in plugin.names]) -__importer_types = dict([(name, plugin) for plugin in __importers for name in plugin.names]) +__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names} +__importer_types = {name: plugin for plugin in __importers for name in plugin.names} EXPORT_FORMATS = sorted(__exporter_types.keys()) IMPORT_FORMATS = sorted(__importer_types.keys()) diff --git a/jrnl/plugins/jrnl_importer.py b/jrnl/plugins/jrnl_importer.py index 85615e75b..83341cd94 100644 --- a/jrnl/plugins/jrnl_importer.py +++ b/jrnl/plugins/jrnl_importer.py @@ -1,12 +1,10 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals -import codecs import sys from .. import util -class JRNLImporter(object): +class JRNLImporter: """This plugin imports entries from other jrnl files.""" names = ["jrnl"] @@ -17,15 +15,15 @@ def import_(journal, input=None): old_cnt = len(journal.entries) old_entries = journal.entries if input: - with codecs.open(input, "r", "utf-8") as f: + with open(input, "r", encoding="utf-8") as f: other_journal_txt = f.read() else: try: - other_journal_txt = util.py23_read() + other_journal_txt = sys.stdin.read() except KeyboardInterrupt: - util.prompt("[Entries NOT imported into journal.]") + print("[Entries NOT imported into journal.]", file=sys.stderr) sys.exit(0) journal.import_(other_journal_txt) new_cnt = len(journal.entries) - util.prompt("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name)) + print("[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr) journal.write() diff --git a/jrnl/plugins/json_exporter.py b/jrnl/plugins/json_exporter.py index 5abaf9161..e368a300b 100644 --- a/jrnl/plugins/json_exporter.py +++ b/jrnl/plugins/json_exporter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals from .text_exporter import TextExporter import json from .util import get_tags_count @@ -35,7 +34,7 @@ def export_journal(cls, journal): """Returns a json representation of an entire journal.""" tags = get_tags_count(journal) result = { - "tags": dict((tag, count) for count, tag in tags), + "tags": {tag: count for count, tag in tags}, "entries": [cls.entry_to_dict(e) for e in journal.entries] } return json.dumps(result, indent=2) diff --git a/jrnl/plugins/markdown_exporter.py b/jrnl/plugins/markdown_exporter.py index 19b5404d6..da2b57482 100644 --- a/jrnl/plugins/markdown_exporter.py +++ b/jrnl/plugins/markdown_exporter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals, print_function from .text_exporter import TextExporter import os import re @@ -51,15 +50,11 @@ def export_entry(cls, entry, to_multifile=True): newbody = newbody + previous_line # add very last line if warn_on_heading_level is True: - print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr) + print(f"{WARNING_COLOR}WARNING{RESET_COLOR}: " + f"Headings increased past H6 on export - {date_str} {entry.title}", + file=sys.stderr) - return "{md} {date} {title}\n{body} {space}".format( - md=heading, - date=date_str, - title=entry.title, - body=newbody, - space="" - ) + return f"{heading} {date_str} {entry.title}\n{newbody} " @classmethod def export_journal(cls, journal): diff --git a/jrnl/plugins/tag_exporter.py b/jrnl/plugins/tag_exporter.py index 439bac7c8..f5453ced8 100644 --- a/jrnl/plugins/tag_exporter.py +++ b/jrnl/plugins/tag_exporter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals from .text_exporter import TextExporter from .util import get_tags_count @@ -26,5 +25,5 @@ def export_journal(cls, 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("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) + result += "\n".join("{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) return result diff --git a/jrnl/plugins/template.py b/jrnl/plugins/template.py index 21fb28963..7f72e2f80 100644 --- a/jrnl/plugins/template.py +++ b/jrnl/plugins/template.py @@ -13,7 +13,7 @@ INCLUDE_RE = r"{% *include +(.+?) *%}" -class Template(object): +class Template: def __init__(self, template): self.template = template self.clean_template = None diff --git a/jrnl/plugins/template_exporter.py b/jrnl/plugins/template_exporter.py index 85aa2236d..f15328f23 100644 --- a/jrnl/plugins/template_exporter.py +++ b/jrnl/plugins/template_exporter.py @@ -1,8 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals - from .text_exporter import TextExporter from .template import Template import os @@ -14,7 +12,7 @@ class GenericTemplateExporter(TextExporter): @classmethod def export_entry(cls, entry): - """Returns a unicode representation of a single entry.""" + """Returns a string representation of a single entry.""" vars = { 'entry': entry, 'tags': entry.tags @@ -23,7 +21,7 @@ def export_entry(cls, entry): @classmethod def export_journal(cls, journal): - """Returns a unicode representation of an entire journal.""" + """Returns a string representation of an entire journal.""" vars = { 'journal': journal, 'entries': journal.entries, @@ -36,7 +34,7 @@ def __exporter_from_file(template_file): """Create a template class from a file""" name = os.path.basename(template_file).replace(".template", "") template = Template.from_file(template_file) - return type(str("{}Exporter".format(name.title())), (GenericTemplateExporter, ), { + return type(str(f"{name.title()}Exporter"), (GenericTemplateExporter, ), { "names": [name], "extension": template.extension, "template": template diff --git a/jrnl/plugins/text_exporter.py b/jrnl/plugins/text_exporter.py index dbb54d04e..ce2e71de0 100644 --- a/jrnl/plugins/text_exporter.py +++ b/jrnl/plugins/text_exporter.py @@ -1,41 +1,39 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals -import codecs -from ..util import u, slugify +from ..util import slugify import os from ..util import ERROR_COLOR, RESET_COLOR -class TextExporter(object): +class TextExporter: """This Exporter can convert entries and journals into text files.""" names = ["text", "txt"] extension = "txt" @classmethod def export_entry(cls, entry): - """Returns a unicode representation of a single entry.""" - return entry.__unicode__() + """Returns a string representation of a single entry.""" + return str(entry) @classmethod def export_journal(cls, journal): - """Returns a unicode representation of an entire journal.""" + """Returns a string representation of an entire journal.""" return "\n".join(cls.export_entry(entry) for entry in journal) @classmethod def write_file(cls, journal, path): """Exports a journal into a single file.""" try: - with codecs.open(path, "w", "utf-8") as f: + with open(path, "w", encoding="utf-8") as f: f.write(cls.export_journal(journal)) - return "[Journal exported to {0}]".format(path) + return f"[Journal exported to {path}]" except IOError as e: - return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) + return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]" @classmethod def make_filename(cls, entry): - return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension)) + return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension)) @classmethod def write_files(cls, journal, path): @@ -43,17 +41,17 @@ def write_files(cls, journal, path): for entry in journal.entries: try: full_path = os.path.join(path, cls.make_filename(entry)) - with codecs.open(full_path, "w", "utf-8") as f: + with open(full_path, "w", encoding="utf-8") as f: f.write(cls.export_entry(entry)) except IOError as e: return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) - return "[Journal exported to {0}]".format(path) + return "[Journal exported to {}]".format(path) @classmethod def export(cls, journal, output=None): """Exports to individual files if output is an existing path, or into a single file if output is a file name, or returns the exporter's - representation as unicode if output is None.""" + representation as string if output is None.""" if output and os.path.isdir(output): # multiple files return cls.write_files(journal, output) elif output: # single file diff --git a/jrnl/plugins/util.py b/jrnl/plugins/util.py index 0a642cb2e..a056b19ab 100644 --- a/jrnl/plugins/util.py +++ b/jrnl/plugins/util.py @@ -10,7 +10,7 @@ def get_tags_count(journal): for entry in journal.entries 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]) + tag_counts = {(tags.count(tag), tag) for tag in tags} return tag_counts diff --git a/jrnl/plugins/xml_exporter.py b/jrnl/plugins/xml_exporter.py index 0af2ed477..2783663b5 100644 --- a/jrnl/plugins/xml_exporter.py +++ b/jrnl/plugins/xml_exporter.py @@ -1,10 +1,8 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals from .json_exporter import JSONExporter from .util import get_tags_count -from ..util import u from xml.dom import minidom @@ -20,7 +18,7 @@ def export_entry(cls, entry, doc=None): entry_el = doc_el.createElement('entry') for key, value in cls.entry_to_dict(entry).items(): elem = doc_el.createElement(key) - elem.appendChild(doc_el.createTextNode(u(value))) + elem.appendChild(doc_el.createTextNode(value)) entry_el.appendChild(elem) if not doc: doc_el.appendChild(entry_el) @@ -33,8 +31,8 @@ def entry_to_xml(cls, entry, doc): entry_el = doc.createElement('entry') entry_el.setAttribute('date', entry.date.isoformat()) if hasattr(entry, "uuid"): - entry_el.setAttribute('uuid', u(entry.uuid)) - entry_el.setAttribute('starred', u(entry.starred)) + entry_el.setAttribute('uuid', entry.uuid) + entry_el.setAttribute('starred', entry.starred) entry_el.appendChild(doc.createTextNode(entry.fulltext)) return entry_el @@ -49,7 +47,7 @@ def export_journal(cls, journal): for count, tag in tags: tag_el = doc.createElement('tag') tag_el.setAttribute('name', tag) - count_node = doc.createTextNode(u(count)) + count_node = doc.createTextNode(str(count)) tag_el.appendChild(count_node) tags_el.appendChild(tag_el) for entry in journal.entries: diff --git a/jrnl/plugins/yaml_exporter.py b/jrnl/plugins/yaml_exporter.py index c07358110..4a75667f7 100644 --- a/jrnl/plugins/yaml_exporter.py +++ b/jrnl/plugins/yaml_exporter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals, print_function from .text_exporter import TextExporter import os import re @@ -18,7 +17,8 @@ class YAMLExporter(TextExporter): def export_entry(cls, entry, to_multifile=True): """Returns a markdown representation of a single entry, with YAML front matter.""" if to_multifile is False: - print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format("\033[31m", "\033[0m", file=sys.stderr)) + print("{}ERROR{}: YAML export must be to individual files. " + "Please specify a directory to export to.".format("\033[31m", "\033[0m"), file=sys.stderr) return date_str = entry.date.strftime(entry.journal.config['timeformat']) @@ -27,7 +27,7 @@ def export_entry(cls, entry, to_multifile=True): tagsymbols = entry.journal.config['tagsymbols'] # see also Entry.Entry.rag_regex - multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols), re.UNICODE) + multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols)) '''Increase heading levels in body text''' newbody = '' diff --git a/jrnl/time.py b/jrnl/time.py index 9ff125aa9..eee3fc207 100644 --- a/jrnl/time.py +++ b/jrnl/time.py @@ -51,7 +51,7 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None): except TypeError: return None - if flag is 1: # Date found, but no time. Use the default time. + if flag == 1: # Date found, but no time. Use the default time. date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0) else: date = datetime(*date[:6]) diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index f2b80af39..216987a0e 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +import sys from . import __version__ from . import Journal @@ -6,11 +6,10 @@ from .EncryptedJournal import EncryptedJournal from .util import UserAbort import os -import codecs def backup(filename, binary=False): - util.prompt(" Created a backup at {}.backup".format(filename)) + print(f" Created a backup at {filename}.backup", file=sys.stderr) filename = os.path.expanduser(os.path.expandvars(filename)) with open(filename, 'rb' if binary else 'r') as original: contents = original.read() @@ -19,14 +18,14 @@ def backup(filename, binary=False): def upgrade_jrnl_if_necessary(config_path): - with codecs.open(config_path, "r", "utf-8") as f: + with open(config_path, "r", encoding="utf-8") as f: config_file = f.read() if not config_file.strip().startswith("{"): return config = util.load_config(config_path) - util.prompt("""Welcome to jrnl {}. + print("""Welcome to jrnl {}. It looks like you've been using an older version of jrnl until now. That's okay - jrnl will now upgrade your configuration and journal files. Afterwards @@ -63,19 +62,19 @@ def upgrade_jrnl_if_necessary(config_path): longest_journal_name = max([len(journal) for journal in config['journals']]) if encrypted_journals: - util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__)) + print(f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", file=sys.stderr) for journal, path in encrypted_journals.items(): - util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) + print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) if plain_journals: - util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__)) + print(f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", file=sys.stderr) for journal, path in plain_journals.items(): - util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) + print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) if other_journals: - util.prompt("\nFollowing journals will be not be touched:") + print("\nFollowing journals will be not be touched:", file=sys.stderr) for journal, path in other_journals.items(): - util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) + print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) try: cont = util.yesno("\nContinue upgrading jrnl?", default=False) @@ -85,13 +84,13 @@ def upgrade_jrnl_if_necessary(config_path): raise UserAbort("jrnl NOT upgraded, exiting.") for journal_name, path in encrypted_journals.items(): - util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path)) + print(f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...", file=sys.stderr) backup(path, binary=True) old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True) all_journals.append(EncryptedJournal.from_journal(old_journal)) for journal_name, path in plain_journals.items(): - util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path)) + print(f"\nUpgrading plain text '{journal_name}' journal stored in {path}...", file=sys.stderr) backup(path) old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True) all_journals.append(Journal.PlainJournal.from_journal(old_journal)) @@ -100,8 +99,9 @@ def upgrade_jrnl_if_necessary(config_path): failed_journals = [j for j in all_journals if not j.validate_parsing()] if len(failed_journals) > 0: - util.prompt("\nThe following journal{} failed to upgrade:\n{}".format( - 's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)) + print("\nThe following journal{} failed to upgrade:\n{}".format( + 's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)), + file=sys.stderr ) raise UpgradeValidationException @@ -110,10 +110,11 @@ def upgrade_jrnl_if_necessary(config_path): for j in all_journals: j.write() - util.prompt("\nUpgrading config...") + print("\nUpgrading config...", file=sys.stderr) backup(config_path) - util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path)) + print("\nWe're all done here and you can start enjoying jrnl 2.", file=sys.stderr) + class UpgradeValidationException(Exception): """Raised when the contents of an upgraded journal do not match the old journal""" diff --git a/jrnl/util.py b/jrnl/util.py index bc36ba9b4..959d63a17 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -1,8 +1,4 @@ #!/usr/bin/env python -# encoding: utf-8 - -from __future__ import unicode_literals -from __future__ import absolute_import import sys import os @@ -14,22 +10,12 @@ import re import tempfile import subprocess -import codecs import unicodedata import shlex import logging log = logging.getLogger(__name__) - -PY3 = sys.version_info[0] == 3 -PY2 = sys.version_info[0] == 2 -STDIN = sys.stdin -STDERR = sys.stderr -STDOUT = sys.stdout -TEST = False -__cached_tz = None - WARNING_COLOR = "\033[33m" ERROR_COLOR = "\033[31m" RESET_COLOR = "\033[0m" @@ -44,18 +30,14 @@ \s+ # a sequence of required spaces. | # Otherwise, \n # a sentence also terminates newlines. -)""", re.UNICODE | re.VERBOSE) +)""", re.VERBOSE) class UserAbort(Exception): pass -def getpass(prompt="Password: "): - if not TEST: - return gp.getpass(bytes(prompt)) - else: - return py23_input(prompt) +getpass = gp.getpass def get_password(validator, keychain=None, max_attempts=3): @@ -67,14 +49,14 @@ def get_password(validator, keychain=None, max_attempts=3): set_keychain(keychain, None) attempt = 1 while result is None and attempt < max_attempts: - prompt("Wrong password, try again.") - password = getpass() + print("Wrong password, try again.", file=sys.stderr) + password = gp.getpass() result = validator(password) attempt += 1 if result is not None: return result else: - prompt("Extremely wrong password.") + print("Extremely wrong password.", file=sys.stderr) sys.exit(1) @@ -88,57 +70,16 @@ def set_keychain(journal_name, password): if password is None: try: keyring.delete_password('jrnl', journal_name) - except: + except RuntimeError: pass - elif not TEST: + else: keyring.set_password('jrnl', journal_name, password) -def u(s): - """Mock unicode function for python 2 and 3 compatibility.""" - if not isinstance(s, str): - s = str(s) - return s if PY3 or type(s) is unicode else s.decode("utf-8") - - -def py2encode(s): - """Encodes to UTF-8 in Python 2 but not in Python 3.""" - return s.encode("utf-8") if PY2 and type(s) is unicode else s - - -def bytes(s): - """Returns bytes, no matter what.""" - if PY3: - return s.encode("utf-8") if type(s) is not bytes else s - return s.encode("utf-8") if type(s) is unicode else s - - -def prnt(s): - """Encode and print a string""" - STDOUT.write(u(s + "\n")) - - -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=""): - prompt(msg) - return STDIN.readline().strip() - - -def py23_read(msg=""): - print(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) + prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} " + response = input(prompt) + return {"y": True, "n": False}.get(response.lower(), default) def load_config(config_path): @@ -164,51 +105,35 @@ def scope_config(config, journal_name): def get_text_from_editor(config, template=""): filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt") - with codecs.open(tmpfile, 'w', "utf-8") as f: + with open(tmpfile, 'w', encoding="utf-8") as f: if template: f.write(template) try: subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile]) except AttributeError: subprocess.call(config['editor'] + [tmpfile]) - with codecs.open(tmpfile, "r", "utf-8") as f: + with open(tmpfile, "r", encoding="utf-8") as f: raw = f.read() os.close(filehandle) os.remove(tmpfile) if not raw: - prompt('[Nothing saved to file]') + print('[Nothing saved to file]', file=sys.stderr) return raw def colorize(string): """Returns the string wrapped in cyan ANSI escape""" - return u"\033[36m{}\033[39m".format(string) + return f"\033[36m{string}\033[39m" def slugify(string): """Slugifies a string. Based on public domain code from https://github.com/zacharyvoase/slugify - and ported to deal with all kinds of python 2 and 3 strings """ - string = u(string) - ascii_string = str(unicodedata.normalize('NFKD', string).encode('ascii', 'ignore')) - if PY3: - ascii_string = ascii_string[1:] # removed the leading 'b' - no_punctuation = re.sub(r'[^\w\s-]', '', ascii_string).strip().lower() + normalized_string = str(unicodedata.normalize('NFKD', string)) + no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower() 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.""" - return chr(i) if PY2 else bytes((i,)) - - -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 + return slug def split_title(text):