From d8f422151d8d4fb068b6fcf365182b16da93fefc Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Sat, 7 Dec 2019 12:32:55 -0700 Subject: [PATCH 1/4] Apply black formatter to code --- features/steps/core.py | 101 +++++----- jrnl/DayOneJournal.py | 64 +++++-- jrnl/EncryptedJournal.py | 51 ++--- jrnl/Entry.py | 70 ++++--- jrnl/Journal.py | 106 +++++++---- jrnl/__init__.py | 3 +- jrnl/cli.py | 307 +++++++++++++++++++++++------- jrnl/export.py | 13 +- jrnl/install.py | 102 ++++++---- jrnl/plugins/__init__.py | 13 +- jrnl/plugins/fancy_exporter.py | 66 ++++--- jrnl/plugins/jrnl_importer.py | 7 +- jrnl/plugins/json_exporter.py | 15 +- jrnl/plugins/markdown_exporter.py | 37 ++-- jrnl/plugins/tag_exporter.py | 9 +- jrnl/plugins/template.py | 35 +++- jrnl/plugins/template_exporter.py | 22 +-- jrnl/plugins/text_exporter.py | 11 +- jrnl/plugins/util.py | 6 +- jrnl/plugins/xml_exporter.py | 21 +- jrnl/plugins/yaml_exporter.py | 76 +++++--- jrnl/time.py | 25 ++- jrnl/upgrade.py | 71 +++++-- jrnl/util.py | 51 +++-- 24 files changed, 843 insertions(+), 439 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 57b958f82..641a272e4 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -5,8 +5,11 @@ from jrnl import __version__ from dateutil import parser as date_parser from collections import defaultdict -try: import parsedatetime.parsedatetime_consts as pdt -except ImportError: import parsedatetime as pdt + +try: + import parsedatetime.parsedatetime_consts as pdt +except ImportError: + import parsedatetime as pdt import time import os import json @@ -17,7 +20,7 @@ import sys consts = pdt.Constants(usePyICU=False) -consts.DOWParseStyle = -1 # Prefers past weekdays +consts.DOWParseStyle = -1 # Prefers past weekdays CALENDAR = pdt.Calendar(consts) @@ -44,23 +47,25 @@ def delete_password(self, servicename, username): def ushlex(command): if sys.version_info[0] == 3: return shlex.split(command) - return map(lambda s: s.decode('UTF8'), shlex.split(command.encode('utf8'))) + return map(lambda s: s.decode("UTF8"), shlex.split(command.encode("utf8"))) def read_journal(journal_name="default"): config = util.load_config(install.CONFIG_FILE_PATH) - with open(config['journals'][journal_name]) as journal_file: + with open(config["journals"][journal_name]) as journal_file: journal = journal_file.read() return journal def open_journal(journal_name="default"): config = util.load_config(install.CONFIG_FILE_PATH) - journal_conf = config['journals'][journal_name] - if type(journal_conf) is dict: # We can override the default config on a by-journal basis + journal_conf = config["journals"][journal_name] + if ( + type(journal_conf) is dict + ): # We can override the default config on a by-journal basis config.update(journal_conf) else: # But also just give them a string to point to the journal file - config['journal'] = journal_conf + config["journal"] = journal_conf return Journal.open_journal(journal_name, config) @@ -70,14 +75,15 @@ def set_config(context, config_file): install.CONFIG_FILE_PATH = os.path.abspath(full_path) if config_file.endswith("yaml"): # Add jrnl version to file for 2.x journals - with open(install.CONFIG_FILE_PATH, 'a') as cf: + with open(install.CONFIG_FILE_PATH, "a") as cf: cf.write("version: {}".format(__version__)) @when('we open the editor and enter ""') @when('we open the editor and enter "{text}"') def open_editor_and_enter(context, text=""): - text = (text or context.text) + text = text or context.text + def _mock_editor_function(command): tmpfile = command[-1] with open(tmpfile, "w+") as f: @@ -88,7 +94,7 @@ def _mock_editor_function(command): return tmpfile - with patch('subprocess.call', side_effect=_mock_editor_function): + with patch("subprocess.call", side_effect=_mock_editor_function): run(context, "jrnl") @@ -96,6 +102,7 @@ def _mock_getpass(inputs): def prompt_return(prompt="Password: "): print(prompt) return next(inputs) + return prompt_return @@ -104,6 +111,7 @@ def prompt_return(prompt=""): val = next(inputs) print(prompt, val) return val + return prompt_return @@ -119,24 +127,26 @@ def run_with_input(context, command, inputs=""): text = iter([inputs]) args = ushlex(command)[1:] + # fmt: off + # black needs the 'on' and 'off' to be at the same indentation level with patch("builtins.input", side_effect=_mock_input(text)) as mock_input,\ patch("getpass.getpass", side_effect=_mock_getpass(text)) as mock_getpass,\ 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 - - # at least one of the mocked input methods got called - assert mock_input.called or mock_getpass.called or mock_read.called - # all inputs were used - try: - next(text) - assert False, "Not all inputs were consumed" - except StopIteration: - pass - + try: + cli.run(args or []) + context.exit_status = 0 + except SystemExit as e: + context.exit_status = e.code + + # at least one of the mocked input methods got called + assert mock_input.called or mock_getpass.called or mock_read.called + # all inputs were used + try: + next(text) + assert False, "Not all inputs were consumed" + except StopIteration: + pass + # fmt: on @when('we run "{command}"') @@ -158,20 +168,20 @@ def load_template(context, filename): @when('we set the keychain password of "{journal}" to "{password}"') def set_keychain(context, journal, password): - keyring.set_password('jrnl', journal, password) + keyring.set_password("jrnl", journal, password) -@then('we should get an error') +@then("we should get an error") def has_error(context): assert context.exit_status != 0, context.exit_status -@then('we should get no error') +@then("we should get no error") def no_error(context): assert context.exit_status == 0, context.exit_status -@then('the output should be parsable as json') +@then("the output should be parsable as json") def check_output_json(context): out = context.stdout_capture.getvalue() assert json.loads(out), out @@ -210,7 +220,7 @@ def check_json_output_path(context, path, value): out = context.stdout_capture.getvalue() struct = json.loads(out) - for node in path.split('.'): + for node in path.split("."): try: struct = struct[int(node)] except ValueError: @@ -218,14 +228,19 @@ def check_json_output_path(context, path, value): assert struct == value, struct -@then('the output should be') +@then("the output should be") @then('the output should be "{text}"') def check_output(context, text=None): text = (text or context.text).strip().splitlines() out = context.stdout_capture.getvalue().strip().splitlines() - assert len(text) == len(out), "Output has {} lines (expected: {})".format(len(out), len(text)) + assert len(text) == len(out), "Output has {} lines (expected: {})".format( + len(out), len(text) + ) for line_text, line_out in zip(text, out): - assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()] + assert line_text.strip() == line_out.strip(), [ + line_text.strip(), + line_out.strip(), + ] @then('the output should contain "{text}" in the local time') @@ -233,11 +248,11 @@ def check_output_time_inline(context, text): out = context.stdout_capture.getvalue() local_tz = tzlocal.get_localzone() date, flag = CALENDAR.parse(text) - output_date = time.strftime("%Y-%m-%d %H:%M",date) + output_date = time.strftime("%Y-%m-%d %H:%M", date) assert output_date in out, output_date -@then('the output should contain') +@then("the output should contain") @then('the output should contain "{text}"') def check_output_inline(context, text=None): text = text or context.text @@ -274,7 +289,7 @@ def check_journal_content(context, text, journal_name="default"): def journal_doesnt_exist(context, journal_name="default"): with open(install.CONFIG_FILE_PATH) as config_file: config = yaml.load(config_file, Loader=yaml.FullLoader) - journal_path = config['journals'][journal_name] + journal_path = config["journals"][journal_name] assert not os.path.exists(journal_path) @@ -282,11 +297,7 @@ def journal_doesnt_exist(context, journal_name="default"): @then('the config for journal "{journal}" should have "{key}" set to "{value}"') def config_var(context, key, value, journal=None): t, value = value.split(":") - value = { - "bool": lambda v: v.lower() == "true", - "int": int, - "str": str - }[t](value) + value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](value) config = util.load_config(install.CONFIG_FILE_PATH) if journal: config = config["journals"][journal] @@ -294,8 +305,8 @@ def config_var(context, key, value, journal=None): assert config[key] == value -@then('the journal should have {number:d} entries') -@then('the journal should have {number:d} entry') +@then("the journal should have {number:d} entries") +@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_entries(context, number, journal_name="default"): @@ -303,6 +314,6 @@ def check_journal_entries(context, number, journal_name="default"): assert len(journal.entries) == number -@then('fail') +@then("fail") def debug_fail(context): assert False diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py index 59314c4b9..83eb67887 100644 --- a/jrnl/DayOneJournal.py +++ b/jrnl/DayOneJournal.py @@ -19,7 +19,11 @@ class DayOne(Journal.Journal): """A special Journal handling DayOne files""" # InvalidFileException was added to plistlib in Python3.4 - PLIST_EXCEPTIONS = (ExpatError, plistlib.InvalidFileException) if hasattr(plistlib, "InvalidFileException") else ExpatError + PLIST_EXCEPTIONS = ( + (ExpatError, plistlib.InvalidFileException) + if hasattr(plistlib, "InvalidFileException") + else ExpatError + ) def __init__(self, **kwargs): self.entries = [] @@ -27,28 +31,39 @@ def __init__(self, **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"))] + filenames = [ + os.path.join(self.config["journal"], "entries", f) + for f in os.listdir(os.path.join(self.config["journal"], "entries")) + ] filenames = [] - for root, dirnames, f in os.walk(self.config['journal']): - for filename in fnmatch.filter(f, '*.doentry'): + for root, dirnames, f in os.walk(self.config["journal"]): + for filename in fnmatch.filter(f, "*.doentry"): filenames.append(os.path.join(root, filename)) self.entries = [] for filename in filenames: - with open(filename, 'rb') as plist_entry: + with open(filename, "rb") as plist_entry: try: dict_entry = plistlib.readPlist(plist_entry) except self.PLIST_EXCEPTIONS: pass else: try: - timezone = pytz.timezone(dict_entry['Time Zone']) + timezone = pytz.timezone(dict_entry["Time Zone"]) except (KeyError, pytz.exceptions.UnknownTimeZoneError): timezone = tzlocal.get_localzone() - date = dict_entry['Creation Date'] + date = dict_entry["Creation Date"] date = date + timezone.utcoffset(date, is_dst=False) - entry = Entry.Entry(self, date, text=dict_entry['Entry Text'], starred=dict_entry["Starred"]) + entry = Entry.Entry( + self, + date, + text=dict_entry["Entry Text"], + starred=dict_entry["Starred"], + ) entry.uuid = dict_entry["UUID"] - entry._tags = [self.config['tagsymbols'][0] + tag.lower() for tag in dict_entry.get("Tags", [])] + entry._tags = [ + self.config["tagsymbols"][0] + tag.lower() + for tag in dict_entry.get("Tags", []) + ] self.entries.append(entry) self.sort() @@ -58,24 +73,33 @@ def write(self): """Writes only the entries that have been modified into plist files.""" for entry in self.entries: if entry.modified: - utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple())) + utc_time = datetime.utcfromtimestamp( + time.mktime(entry.date.timetuple()) + ) if not hasattr(entry, "uuid"): entry.uuid = uuid.uuid1().hex - filename = os.path.join(self.config['journal'], "entries", entry.uuid.upper() + ".doentry") - + filename = os.path.join( + self.config["journal"], "entries", entry.uuid.upper() + ".doentry" + ) + entry_plist = { - 'Creation Date': utc_time, - 'Starred': entry.starred if hasattr(entry, 'starred') else False, - 'Entry Text': entry.title + "\n" + entry.body, - 'Time Zone': str(tzlocal.get_localzone()), - 'UUID': entry.uuid.upper(), - 'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags] + "Creation Date": utc_time, + "Starred": entry.starred if hasattr(entry, "starred") else False, + "Entry Text": entry.title + "\n" + entry.body, + "Time Zone": str(tzlocal.get_localzone()), + "UUID": entry.uuid.upper(), + "Tags": [ + tag.strip(self.config["tagsymbols"]).replace("_", " ") + for tag in entry.tags + ], } plistlib.writePlist(entry_plist, filename) for entry in self._deleted_entries: - filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry") + filename = os.path.join( + self.config["journal"], "entries", entry.uuid + ".doentry" + ) os.remove(filename) def editable_str(self): @@ -113,7 +137,7 @@ def parse_editable_str(self, edited): if line.endswith("*"): current_entry.starred = True line = line[:-1] - current_entry.title = line[len(date_blob) - 1:] + current_entry.title = line[len(date_blob) - 1 :] current_entry.date = new_date elif current_entry: current_entry.body += line + "\n" diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index f3ff63c60..73d942693 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -19,32 +19,34 @@ def make_key(password): algorithm=hashes.SHA256(), length=32, # Salt is hard-coded - salt=b'\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8', + salt=b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8", iterations=100000, - backend=default_backend() + backend=default_backend(), ) key = kdf.derive(password) return base64.urlsafe_b64encode(key) class EncryptedJournal(Journal.Journal): - def __init__(self, name='default', **kwargs): + def __init__(self, name="default", **kwargs): super().__init__(name, **kwargs) - self.config['encrypt'] = True + self.config["encrypt"] = True def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" - filename = filename or self.config['journal'] + filename = filename or self.config["journal"] if not os.path.exists(filename): password = util.create_password() if password: - if util.yesno("Do you want to store the password in your keychain?", default=True): + if util.yesno( + "Do you want to store the password in your keychain?", default=True + ): util.set_keychain(self.name, password) else: util.set_keychain(self.name, None) - self.config['password'] = password + self.config["password"] = password text = "" self._store(filename, text) print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) @@ -64,62 +66,67 @@ def _load(self, filename, password=None): and otherwise ask the user to enter a password up to three times. If the password is provided but wrong (or corrupt), this will simply return None.""" - with open(filename, 'rb') as f: + with open(filename, "rb") as f: journal_encrypted = f.read() def validate_password(password): key = make_key(password) try: - plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8') - self.config['password'] = password + plain = Fernet(key).decrypt(journal_encrypted).decode("utf-8") + self.config["password"] = password return plain except (InvalidToken, IndexError): return None + if password: return validate_password(password) return util.get_password(keychain=self.name, validator=validate_password) def _store(self, filename, text): - key = make_key(self.config['password']) - journal = Fernet(key).encrypt(text.encode('utf-8')) - with open(filename, 'wb') as f: + key = make_key(self.config["password"]) + journal = Fernet(key).encrypt(text.encode("utf-8")) + with open(filename, "wb") as f: f.write(journal) @classmethod def _create(cls, filename, password): key = make_key(password) dummy = Fernet(key).encrypt(b"") - with open(filename, 'wb') as f: + with open(filename, "wb") as f: f.write(dummy) 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): + + def __init__(self, name="default", **kwargs): super().__init__(name, **kwargs) - self.config['encrypt'] = True + self.config["encrypt"] = True def _load(self, filename, password=None): - with open(filename, 'rb') as f: + with open(filename, "rb") as f: journal_encrypted = f.read() iv, cipher = journal_encrypted[:16], journal_encrypted[16:] def validate_password(password): - decryption_key = hashlib.sha256(password.encode('utf-8')).digest() - decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor() + decryption_key = hashlib.sha256(password.encode("utf-8")).digest() + decryptor = Cipher( + algorithms.AES(decryption_key), modes.CBC(iv), default_backend() + ).decryptor() try: plain_padded = decryptor.update(cipher) + decryptor.finalize() - self.config['password'] = password + self.config["password"] = password if plain_padded[-1] in (" ", 32): # Ancient versions of jrnl. Do not judge me. - return plain_padded.decode('utf-8').rstrip(" ") + return plain_padded.decode("utf-8").rstrip(" ") else: unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() plain = unpadder.update(plain_padded) + unpadder.finalize() - return plain.decode('utf-8') + return plain.decode("utf-8") except ValueError: return None + if password: return validate_password(password) return util.get_password(keychain=self.name, validator=validate_password) diff --git a/jrnl/Entry.py b/jrnl/Entry.py index ca80b231e..80754e05f 100755 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -49,72 +49,84 @@ def tags(self): @staticmethod def tag_regex(tagsymbols): - pattern = fr'(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)' + pattern = fr"(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)" return re.compile(pattern) def _parse_tags(self): - tagsymbols = self.journal.config['tagsymbols'] - return {tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)} + tagsymbols = self.journal.config["tagsymbols"] + return { + tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text) + } 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']) + date_str = self.date.strftime(self.journal.config["timeformat"]) title = "[{}] {}".format(date_str, self.title.rstrip("\n ")) if self.starred: title += " *" return "{title}{sep}{body}\n".format( title=title, sep="\n" if self.body.rstrip("\n ") else "", - body=self.body.rstrip("\n ") + body=self.body.rstrip("\n "), ) def pprint(self, short=False): """Returns a pretty-printed version of the entry. If short is true, only print the title.""" - date_str = self.date.strftime(self.journal.config['timeformat']) - if self.journal.config['indent_character']: - indent = self.journal.config['indent_character'].rstrip() + " " + date_str = self.date.strftime(self.journal.config["timeformat"]) + if self.journal.config["indent_character"]: + indent = self.journal.config["indent_character"].rstrip() + " " else: indent = "" - if not short and self.journal.config['linewrap']: - title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap']) - body = "\n".join([ - textwrap.fill( - line, - self.journal.config['linewrap'], - initial_indent=indent, - subsequent_indent=indent, - drop_whitespace=True) or indent - for line in self.body.rstrip(" \n").splitlines() - ]) + if not short and self.journal.config["linewrap"]: + title = textwrap.fill( + date_str + " " + self.title, self.journal.config["linewrap"] + ) + body = "\n".join( + [ + textwrap.fill( + line, + self.journal.config["linewrap"], + initial_indent=indent, + subsequent_indent=indent, + drop_whitespace=True, + ) + or indent + for line in self.body.rstrip(" \n").splitlines() + ] + ) else: title = date_str + " " + self.title.rstrip("\n ") body = self.body.rstrip("\n ") # Suppress bodies that are just blanks and new lines. - has_body = len(self.body) > 20 or not all(char in (" ", "\n") for char in self.body) + has_body = len(self.body) > 20 or not all( + char in (" ", "\n") for char in self.body + ) if short: return title else: return "{title}{sep}{body}\n".format( - title=title, - sep="\n" if has_body else "", - body=body if has_body else "", + title=title, sep="\n" if has_body else "", body=body if has_body else "" ) 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__()) def __eq__(self, other): - if not isinstance(other, Entry) \ - or self.title.strip() != other.title.strip() \ - or self.body.rstrip() != other.body.rstrip() \ - or self.date != other.date \ - or self.starred != other.starred: + if ( + not isinstance(other, Entry) + or self.title.strip() != other.title.strip() + or self.body.rstrip() != other.body.rstrip() + or self.date != other.date + or self.starred != other.starred + ): return False return True diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 0fd41c221..01a7bf4e5 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -25,17 +25,17 @@ def __repr__(self): class Journal: - def __init__(self, name='default', **kwargs): + def __init__(self, name="default", **kwargs): self.config = { - 'journal': "journal.txt", - 'encrypt': False, - 'default_hour': 9, - 'default_minute': 0, - 'timeformat': "%Y-%m-%d %H:%M", - 'tagsymbols': '@', - 'highlight': True, - 'linewrap': 80, - 'indent_character': '|', + "journal": "journal.txt", + "encrypt": False, + "default_hour": 9, + "default_minute": 0, + "timeformat": "%Y-%m-%d %H:%M", + "tagsymbols": "@", + "highlight": True, + "linewrap": 80, + "indent_character": "|", } self.config.update(kwargs) # Set up date parser @@ -56,17 +56,24 @@ def from_journal(cls, other): another journal object""" new_journal = cls(other.name, **other.config) new_journal.entries = other.entries - log.debug("Imported %d entries from %s to %s", len(new_journal), other.__class__.__name__, cls.__name__) + log.debug( + "Imported %d entries from %s to %s", + len(new_journal), + other.__class__.__name__, + cls.__name__, + ) return new_journal def import_(self, other_journal_txt): - self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt))) + self.entries = list( + frozenset(self.entries) | frozenset(self._parse(other_journal_txt)) + ) self.sort() def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" - filename = filename or self.config['journal'] + filename = filename or self.config["journal"] if not os.path.exists(filename): print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) @@ -80,7 +87,7 @@ def open(self, filename=None): def write(self, filename=None): """Dumps the journal into the config file, overwriting it""" - filename = filename or self.config['journal'] + filename = filename or self.config["journal"] text = self._to_text() self._store(filename, text) @@ -127,7 +134,7 @@ def _parse(self, journal_txt): if new_date: if entries: - entries[-1].text = journal_txt[last_entry_pos:match.start()] + entries[-1].text = journal_txt[last_entry_pos : match.start()] last_entry_pos = match.end() entries.append(Entry.Entry(self, date=new_date)) @@ -146,18 +153,16 @@ def pprint(self, short=False): """Prettyprints the journal's entries""" sep = "\n" pp = sep.join([e.pprint(short=short) for e in self.entries]) - if self.config['highlight']: # highlight tags + if self.config["highlight"]: # highlight tags if self.search_tags: for tag in self.search_tags: tagre = re.compile(re.escape(tag), re.IGNORECASE) - pp = re.sub(tagre, - lambda match: util.colorize(match.group(0)), - pp) + pp = re.sub(tagre, lambda match: util.colorize(match.group(0)), pp) else: pp = re.sub( - Entry.Entry.tag_regex(self.config['tagsymbols']), + Entry.Entry.tag_regex(self.config["tagsymbols"]), lambda match: util.colorize(match.group(0)), - pp + pp, ) return pp @@ -181,14 +186,21 @@ def tags(self): """Returns a set of tuples (count, tag) for all tags present in the journal.""" # Astute reader: should the following line leave you as puzzled as me the first time # I came across this construction, worry not and embrace the ensuing moment of enlightment. - tags = [tag - for entry in self.entries - for tag in set(entry.tags)] + tags = [tag 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 = {(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=[]): + def filter( + self, + tags=[], + start_date=None, + end_date=None, + starred=False, + strict=False, + short=False, + exclude=[], + ): """Removes all entries from the journal that don't match the filter. tags is a list of tags, each being a string that starts with one of the @@ -211,7 +223,8 @@ def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict= tagged = self.search_tags.issubset if strict else self.search_tags.intersection excluded = lambda tags: len([tag for tag in tags if tag in excluded_tags]) > 0 result = [ - entry for entry in self.entries + entry + for entry in self.entries if (not tags or tagged(entry.tags)) and (not starred or entry.starred) and (not start_date or entry.date >= start_date) @@ -225,11 +238,11 @@ def new_entry(self, raw, date=None, sort=True): """Constructs a new entry from some raw text input. If a date is given, it will parse and use this, otherwise scan for a date in the input first.""" - raw = raw.replace('\\n ', '\n').replace('\\n', '\n') + raw = raw.replace("\\n ", "\n").replace("\\n", "\n") starred = False # Split raw text into title and body sep = re.search(r"\n|[?!.]+ +\n?", raw) - first_line = raw[:sep.end()].strip() if sep else raw + first_line = raw[: sep.end()].strip() if sep else raw starred = False if not date: @@ -237,12 +250,12 @@ def new_entry(self, raw, date=None, sort=True): if colon_pos > 0: date = time.parse( raw[:colon_pos], - default_hour=self.config['default_hour'], - default_minute=self.config['default_minute'] + default_hour=self.config["default_hour"], + default_minute=self.config["default_minute"], ) if date: # Parsed successfully, strip that from the raw text starred = raw[:colon_pos].strip().endswith("*") - raw = raw[colon_pos + 1:].strip() + raw = raw[colon_pos + 1 :].strip() starred = starred or first_line.startswith("*") or first_line.endswith("*") if not date: # Still nothing? Meh, just live in the moment. date = time.parse("now") @@ -280,7 +293,7 @@ def _load(self, filename): return f.read() def _store(self, filename, text): - with open(filename, 'w', encoding="utf-8") as f: + with open(filename, "w", encoding="utf-8") as f: f.write(text) @@ -288,6 +301,7 @@ class LegacyJournal(Journal): """Legacy class to support opening journals formatted with the jrnl 1.x 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 open(filename, "r", encoding="utf-8") as f: return f.read() @@ -296,17 +310,19 @@ def _parse(self, journal_txt): """Parses a journal that's stored in a string and returns a list of entries""" # Entries start with a line that looks like 'date title' - let's figure out how # long the date will be by constructing one - date_length = len(datetime.today().strftime(self.config['timeformat'])) + date_length = len(datetime.today().strftime(self.config["timeformat"])) # Initialise our current entry entries = [] current_entry = None - new_date_format_regex = re.compile(r'(^\[[^\]]+\].*?$)') + new_date_format_regex = re.compile(r"(^\[[^\]]+\].*?$)") for line in journal_txt.splitlines(): line = line.rstrip() try: # try to parse line as date => new entry begins - new_date = datetime.strptime(line[:date_length], self.config['timeformat']) + new_date = datetime.strptime( + line[:date_length], self.config["timeformat"] + ) # parsing successful => save old entry and create new one if new_date and current_entry: @@ -318,12 +334,14 @@ def _parse(self, journal_txt): else: starred = False - current_entry = Entry.Entry(self, date=new_date, text=line[date_length + 1:], starred=starred) + current_entry = Entry.Entry( + self, date=new_date, text=line[date_length + 1 :], starred=starred + ) except ValueError: # Happens when we can't parse the start of the line as an date. # In this case, just append line to our body (after some # escaping for the new format). - line = new_date_format_regex.sub(r' \1', line) + line = new_date_format_regex.sub(r" \1", line) if current_entry: current_entry.text += line + "\n" @@ -342,26 +360,30 @@ def open_journal(name, config, legacy=False): backwards compatibility with jrnl 1.x """ config = config.copy() - config['journal'] = os.path.expanduser(os.path.expandvars(config['journal'])) + config["journal"] = os.path.expanduser(os.path.expandvars(config["journal"])) - if os.path.isdir(config['journal']): - if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']): + if os.path.isdir(config["journal"]): + if config["journal"].strip("/").endswith(".dayone") or "entries" in os.listdir( + config["journal"] + ): from . import DayOneJournal + return DayOneJournal.DayOne(**config).open() else: print( f"[Error: {config['journal']} is a directory, but doesn't seem to be a DayOne journal either.", - file=sys.stderr + file=sys.stderr, ) sys.exit(1) - if not config['encrypt']: + if not config["encrypt"]: if legacy: return LegacyJournal(name, **config).open() return PlainJournal(name, **config).open() else: from . import EncryptedJournal + if legacy: return EncryptedJournal.LegacyEncryptedJournal(name, **config).open() return EncryptedJournal.EncryptedJournal(name, **config).open() diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 1905b1959..5a7b8568a 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -2,7 +2,6 @@ import pkg_resources -dist = pkg_resources.get_distribution('jrnl') +dist = pkg_resources.get_distribution("jrnl") __title__ = dist.project_name __version__ = dist.version - diff --git a/jrnl/cli.py b/jrnl/cli.py index 945a728f8..1946cbf51 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -22,32 +22,152 @@ 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="*") - - reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal') - reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date') - reading.add_argument('-until', '-to', dest='end_date', metavar="DATE", help='View entries before this date') - reading.add_argument('-on', dest='on_date', metavar="DATE", help='View entries on this date') - reading.add_argument('-and', dest='strict', action="store_true", help='Filter by tags using AND (default: OR)') - reading.add_argument('-starred', dest='starred', action="store_true", help='Show only starred entries') - reading.add_argument('-n', dest='limit', default=None, metavar="N", help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", nargs="?", type=int) - reading.add_argument('-not', dest='excluded', nargs='+', default=[], metavar="E", help="Exclude entries with these tags") - - exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal') - exporting.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags') - exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences') - exporting.add_argument('--export', metavar='TYPE', dest='export', choices=plugins.EXPORT_FORMATS, help='Export your journal. TYPE can be {}.'.format(plugins.util.oxford_list(plugins.EXPORT_FORMATS)), default=False, const=None) - exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.', default=False, const=None) - exporting.add_argument('--import', metavar='TYPE', dest='import_', choices=plugins.IMPORT_FORMATS, help='Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.'.format(plugins.util.oxford_list(plugins.IMPORT_FORMATS)), default=False, const='jrnl', nargs='?') - exporting.add_argument('-i', metavar='INPUT', dest='input', help='Optionally specifies input file when using --import.', default=False, const=None) - exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None) - exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None) - exporting.add_argument('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true") + 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="*") + + reading = parser.add_argument_group( + "Reading", + "Specifying either of these parameters will display posts of your journal", + ) + reading.add_argument( + "-from", dest="start_date", metavar="DATE", help="View entries after this date" + ) + reading.add_argument( + "-until", + "-to", + dest="end_date", + metavar="DATE", + help="View entries before this date", + ) + reading.add_argument( + "-on", dest="on_date", metavar="DATE", help="View entries on this date" + ) + reading.add_argument( + "-and", + dest="strict", + action="store_true", + help="Filter by tags using AND (default: OR)", + ) + reading.add_argument( + "-starred", + dest="starred", + action="store_true", + help="Show only starred entries", + ) + reading.add_argument( + "-n", + dest="limit", + default=None, + metavar="N", + help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", + nargs="?", + type=int, + ) + reading.add_argument( + "-not", + dest="excluded", + nargs="+", + default=[], + metavar="E", + help="Exclude entries with these tags", + ) + + exporting = parser.add_argument_group( + "Export / Import", "Options for transmogrifying your journal" + ) + exporting.add_argument( + "-s", + "--short", + dest="short", + action="store_true", + help="Show only titles or line containing the search tags", + ) + exporting.add_argument( + "--tags", + dest="tags", + action="store_true", + help="Returns a list of all tags and number of occurences", + ) + exporting.add_argument( + "--export", + metavar="TYPE", + dest="export", + choices=plugins.EXPORT_FORMATS, + help="Export your journal. TYPE can be {}.".format( + plugins.util.oxford_list(plugins.EXPORT_FORMATS) + ), + default=False, + const=None, + ) + exporting.add_argument( + "-o", + metavar="OUTPUT", + dest="output", + help="Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.", + default=False, + const=None, + ) + exporting.add_argument( + "--import", + metavar="TYPE", + dest="import_", + choices=plugins.IMPORT_FORMATS, + help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format( + plugins.util.oxford_list(plugins.IMPORT_FORMATS) + ), + default=False, + const="jrnl", + nargs="?", + ) + exporting.add_argument( + "-i", + metavar="INPUT", + dest="input", + help="Optionally specifies input file when using --import.", + default=False, + const=None, + ) + exporting.add_argument( + "--encrypt", + metavar="FILENAME", + dest="encrypt", + help="Encrypts your existing journal with a new password", + nargs="?", + default=False, + const=None, + ) + exporting.add_argument( + "--decrypt", + metavar="FILENAME", + dest="decrypt", + help="Decrypts your journal and stores it in plain text", + nargs="?", + default=False, + const=None, + ) + exporting.add_argument( + "--edit", + dest="edit", + help="Opens your editor to edit the selected entries.", + action="store_true", + ) return parser.parse_args(args) @@ -61,13 +181,29 @@ def guess_mode(args, config): compose = False export = False import_ = True - elif args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.edit)): + elif ( + args.decrypt is not False + or args.encrypt is not False + or args.export is not False + or any((args.short, args.tags, args.edit)) + ): compose = False export = True - elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred)): + 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 " ".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 @@ -78,36 +214,44 @@ def encrypt(journal, filename=None): """ Encrypt into new file. If filename is not set, we encrypt the journal file itself. """ from . import EncryptedJournal - journal.config['password'] = util.create_password() - journal.config['encrypt'] = True + journal.config["password"] = util.create_password() + journal.config["encrypt"] = True new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config) new_journal.entries = journal.entries new_journal.write(filename) if util.yesno("Do you want to store the password in your keychain?", default=True): - util.set_keychain(journal.name, journal.config['password']) + util.set_keychain(journal.name, journal.config["password"]) - print("Journal encrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr) + print( + "Journal encrypted to {}.".format(filename or new_journal.config["journal"]), + file=sys.stderr, + ) def decrypt(journal, filename=None): """ Decrypts into new file. If filename is not set, we encrypt the journal file itself. """ - journal.config['encrypt'] = False - journal.config['password'] = "" + journal.config["encrypt"] = False + journal.config["password"] = "" new_journal = Journal.PlainJournal(filename, **journal.config) new_journal.entries = journal.entries new_journal.write(filename) - print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr) + 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 = 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) + 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 + ) return result @@ -115,11 +259,11 @@ def update_config(config, new_config, scope, force_local=False): """Updates a config dict with new values - either global if scope is None or config['journals'][scope] is just a string pointing to a journal file, or within the scope""" - if scope and type(config['journals'][scope]) is dict: # Update to journal specific - config['journals'][scope].update(new_config) + if scope and type(config["journals"][scope]) is dict: # Update to journal specific + config["journals"][scope].update(new_config) elif scope and force_local: # Convert to dict - config['journals'][scope] = {"journal": config['journals'][scope]} - config['journals'][scope].update(new_config) + config["journals"][scope] = {"journal": config["journals"][scope]} + config["journals"][scope].update(new_config) else: config.update(new_config) @@ -127,9 +271,11 @@ def update_config(config, new_config, scope, force_local=False): def configure_logger(debug=False): logging.basicConfig( level=logging.DEBUG if debug else logging.INFO, - format='%(levelname)-8s %(name)-12s %(message)s' + format="%(levelname)-8s %(name)-12s %(message)s", ) - logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging + logging.getLogger("parsedatetime").setLevel( + logging.INFO + ) # disable parsedatetime debug logging def run(manual_args=None): @@ -155,11 +301,15 @@ def run(manual_args=None): # If the first textual argument points to a journal file, # use this! - journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default' + journal_name = ( + args.text[0] + if (args.text and args.text[0] in config["journals"]) + else "default" + ) - if journal_name != 'default': + if journal_name != "default": args.text = args.text[1:] - elif "default" not in config['journals']: + elif "default" not in config["journals"]: print("No default journal configured.", file=sys.stderr) print(list_journals(config), file=sys.stderr) sys.exit(1) @@ -187,18 +337,24 @@ def run(manual_args=None): if not sys.stdin.isatty(): # Piping data into jrnl raw = sys.stdin.read() - elif config['editor']: + elif config["editor"]: template = "" - if config['template']: + if config["template"]: try: - template = open(config['template']).read() + template = open(config["template"]).read() except OSError: - print(f"[Could not read template at '{config['template']}']", file=sys.stderr) + 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: - print("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", file=sys.stderr) + print( + "[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", + file=sys.stderr, + ) raw = sys.stdin.read() except KeyboardInterrupt: print("[Entry NOT saved to journal.]", file=sys.stderr) @@ -231,12 +387,15 @@ def run(manual_args=None): old_entries = journal.entries if args.on_date: args.start_date = args.end_date = args.on_date - journal.filter(tags=args.text, - start_date=args.start_date, end_date=args.end_date, - strict=args.strict, - short=args.short, - starred=args.starred, - exclude=args.excluded) + journal.filter( + tags=args.text, + start_date=args.start_date, + end_date=args.end_date, + strict=args.strict, + short=args.short, + starred=args.starred, + exclude=args.excluded, + ) journal.limit(args.limit) # Reading mode @@ -258,20 +417,28 @@ def run(manual_args=None): encrypt(journal, filename=args.encrypt) # Not encrypting to a separate file: update config! if not args.encrypt: - update_config(original_config, {"encrypt": True}, journal_name, force_local=True) + update_config( + original_config, {"encrypt": True}, journal_name, force_local=True + ) install.save_config(original_config) elif args.decrypt is not False: decrypt(journal, filename=args.decrypt) # Not decrypting to a separate file: update config! if not args.decrypt: - update_config(original_config, {"encrypt": False}, journal_name, force_local=True) + update_config( + original_config, {"encrypt": False}, journal_name, force_local=True + ) install.save_config(original_config) elif args.edit: - if not config['editor']: - 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) + if not config["editor"]: + 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 @@ -282,9 +449,17 @@ def run(manual_args=None): num_edited = len([e for e in journal.entries if e.modified]) prompts = [] if num_deleted: - prompts.append("{} {} 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("{} {} 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: print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr) journal.entries += other_entries diff --git a/jrnl/export.py b/jrnl/export.py index 1ee4e6ffa..e95d4c122 100644 --- a/jrnl/export.py +++ b/jrnl/export.py @@ -8,6 +8,7 @@ class Exporter: """This Exporter can convert entries and journals into text files.""" + def __init__(self, format): with open("jrnl/templates/" + format + ".template") as f: front_matter, body = f.read().strip("-\n").split("---", 2) @@ -18,11 +19,7 @@ def export_entry(self, entry): return str(entry) def _get_vars(self, journal): - return { - 'journal': journal, - 'entries': journal.entries, - 'tags': journal.tags - } + return {"journal": journal, "entries": journal.entries, "tags": journal.tags} def export_journal(self, journal): """Returns a string representation of an entire journal.""" @@ -38,7 +35,9 @@ def write_file(self, journal, path): return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]" def make_filename(self, entry): - return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(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.""" @@ -57,7 +56,7 @@ def export(self, journal, format="text", output=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 + elif output: # single file return self.write_file(journal, output) else: return self.export_journal(journal) diff --git a/jrnl/install.py b/jrnl/install.py index 8eb73c76d..fb8777452 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -13,15 +13,16 @@ import yaml import logging import sys + if "win32" not in sys.platform: # readline is not included in Windows Active Python - import readline + import readline -DEFAULT_CONFIG_NAME = 'jrnl.yaml' -DEFAULT_JOURNAL_NAME = 'journal.txt' -XDG_RESOURCE = 'jrnl' +DEFAULT_CONFIG_NAME = "jrnl.yaml" +DEFAULT_JOURNAL_NAME = "journal.txt" +XDG_RESOURCE = "jrnl" -USER_HOME = os.path.expanduser('~') +USER_HOME = os.path.expanduser("~") CONFIG_PATH = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) or USER_HOME CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, DEFAULT_CONFIG_NAME) @@ -42,21 +43,20 @@ def module_exists(module_name): else: return True + default_config = { - 'version': __version__, - 'journals': { - "default": JOURNAL_FILE_PATH - }, - 'editor': os.getenv('VISUAL') or os.getenv('EDITOR') or "", - 'encrypt': False, - 'template': False, - 'default_hour': 9, - 'default_minute': 0, - 'timeformat': "%Y-%m-%d %H:%M", - 'tagsymbols': '@', - 'highlight': True, - 'linewrap': 79, - 'indent_character': '|', + "version": __version__, + "journals": {"default": JOURNAL_FILE_PATH}, + "editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "", + "encrypt": False, + "template": False, + "default_hour": 9, + "default_minute": 0, + "timeformat": "%Y-%m-%d %H:%M", + "tagsymbols": "@", + "highlight": True, + "linewrap": 79, + "indent_character": "|", } @@ -69,13 +69,18 @@ def upgrade_config(config): for key in missing_keys: config[key] = default_config[key] save_config(config) - print(f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", file=sys.stderr) + print( + f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", + file=sys.stderr, + ) def save_config(config): - config['version'] = __version__ - with open(CONFIG_FILE_PATH, 'w') as f: - yaml.safe_dump(config, f, encoding='utf-8', allow_unicode=True, default_flow_style=False) + config["version"] = __version__ + with open(CONFIG_FILE_PATH, "w") as f: + yaml.safe_dump( + config, f, encoding="utf-8", allow_unicode=True, default_flow_style=False + ) def load_or_install_jrnl(): @@ -83,17 +88,27 @@ def load_or_install_jrnl(): If jrnl is already installed, loads and returns a config object. Else, perform various prompts to install jrnl. """ - config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK + config_path = ( + CONFIG_FILE_PATH + if os.path.exists(CONFIG_FILE_PATH) + else CONFIG_FILE_PATH_FALLBACK + ) if os.path.exists(config_path): - log.debug('Reading configuration from file %s', config_path) + log.debug("Reading configuration from file %s", config_path) config = util.load_config(config_path) try: upgrade.upgrade_jrnl_if_necessary(config_path) except upgrade.UpgradeValidationException: 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( + "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) @@ -101,7 +116,7 @@ def load_or_install_jrnl(): return config else: - log.debug('Configuration file not found, installing jrnl...') + log.debug("Configuration file not found, installing jrnl...") try: config = install() except KeyboardInterrupt: @@ -111,42 +126,51 @@ def load_or_install_jrnl(): def install(): if "win32" not in sys.platform: - readline.set_completer_delims(' \t\n;') + readline.set_completer_delims(" \t\n;") readline.parse_and_bind("tab: complete") readline.set_completer(autocomplete) # Where to create the journal? - path_query = f'Path to your journal file (leave blank for {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)) + 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 + path = os.path.split(default_config["journals"]["default"])[ + 0 + ] # If the folder doesn't exist, create it try: os.makedirs(path) except OSError: pass # Encrypt it? - password = getpass.getpass("Enter password for journal (leave blank for no encryption): ") + password = getpass.getpass( + "Enter password for journal (leave blank for no encryption): " + ) if password: - default_config['encrypt'] = True - if util.yesno("Do you want to store the password in your keychain?", default=True): + default_config["encrypt"] = True + if util.yesno( + "Do you want to store the password in your keychain?", default=True + ): util.set_keychain("default", password) else: util.set_keychain("default", None) - EncryptedJournal._create(default_config['journals']['default'], password) + EncryptedJournal._create(default_config["journals"]["default"], password) print("Journal will be encrypted.", file=sys.stderr) else: - PlainJournal._create(default_config['journals']['default']) + PlainJournal._create(default_config["journals"]["default"]) config = default_config save_config(config) if password: - config['password'] = password + config["password"] = password return config + def autocomplete(text, state): - expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + '*') + expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + "*") expansions = [e + "/" if os.path.isdir(e) else e for e in expansions] expansions.append(None) return expansions[state] diff --git a/jrnl/plugins/__init__.py b/jrnl/plugins/__init__.py index 53b595e34..00ee2498a 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -11,8 +11,16 @@ from .template_exporter import __all__ as template_exporters from .fancy_exporter import FancyExporter -__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter, FancyExporter] + template_exporters -__importers =[JRNLImporter] +__exporters = [ + JSONExporter, + MarkdownExporter, + TagExporter, + TextExporter, + XMLExporter, + YAMLExporter, + FancyExporter, +] + template_exporters +__importers = [JRNLImporter] __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} @@ -20,6 +28,7 @@ EXPORT_FORMATS = sorted(__exporter_types.keys()) IMPORT_FORMATS = sorted(__importer_types.keys()) + def get_exporter(format): for exporter in __exporters: if hasattr(exporter, "names") and format in exporter.names: diff --git a/jrnl/plugins/fancy_exporter.py b/jrnl/plugins/fancy_exporter.py index 74d4555f8..f7ff491d2 100644 --- a/jrnl/plugins/fancy_exporter.py +++ b/jrnl/plugins/fancy_exporter.py @@ -8,46 +8,64 @@ class FancyExporter(TextExporter): """This Exporter can convert entries and journals into text with unicode box drawing characters.""" + names = ["fancy", "boxed"] extension = "txt" - border_a="┎" - border_b="─" - border_c="╮" - border_d="╘" - border_e="═" - border_f="╕" - border_g="┃" - border_h="│" - border_i="┠" - border_j="╌" - border_k="┤" - border_l="┖" - border_m="┘" + border_a = "┎" + border_b = "─" + border_c = "╮" + border_d = "╘" + border_e = "═" + border_f = "╕" + border_g = "┃" + border_h = "│" + border_i = "┠" + border_j = "╌" + border_k = "┤" + border_l = "┖" + border_m = "┘" @classmethod def export_entry(cls, entry): """Returns a fancy unicode representation of a single entry.""" - date_str = entry.date.strftime(entry.journal.config['timeformat']) - linewrap = entry.journal.config['linewrap'] or 78 + date_str = entry.date.strftime(entry.journal.config["timeformat"]) + linewrap = entry.journal.config["linewrap"] or 78 initial_linewrap = linewrap - len(date_str) - 2 body_linewrap = linewrap - 2 - card = [cls.border_a + cls.border_b*(initial_linewrap) + cls.border_c + date_str] - w = TextWrapper(width=initial_linewrap, initial_indent=cls.border_g+' ', subsequent_indent=cls.border_g+' ') + card = [ + cls.border_a + cls.border_b * (initial_linewrap) + cls.border_c + date_str + ] + w = TextWrapper( + width=initial_linewrap, + initial_indent=cls.border_g + " ", + subsequent_indent=cls.border_g + " ", + ) title_lines = w.wrap(entry.title) - card.append(title_lines[0].ljust(initial_linewrap+1) + cls.border_d + cls.border_e*(len(date_str)-1) + cls.border_f) + card.append( + title_lines[0].ljust(initial_linewrap + 1) + + cls.border_d + + cls.border_e * (len(date_str) - 1) + + cls.border_f + ) w.width = body_linewrap if len(title_lines) > 1: - for line in w.wrap(' '.join([title_line[len(w.subsequent_indent):] - for title_line in title_lines[1:]])): - card.append(line.ljust(body_linewrap+1) + cls.border_h) + for line in w.wrap( + " ".join( + [ + title_line[len(w.subsequent_indent) :] + for title_line in title_lines[1:] + ] + ) + ): + card.append(line.ljust(body_linewrap + 1) + cls.border_h) if entry.body: - card.append(cls.border_i + cls.border_j*body_linewrap + cls.border_k) + card.append(cls.border_i + cls.border_j * body_linewrap + cls.border_k) for line in entry.body.splitlines(): body_lines = w.wrap(line) or [cls.border_g] for body_line in body_lines: - card.append(body_line.ljust(body_linewrap+1) + cls.border_h) - card.append(cls.border_l + cls.border_b*body_linewrap + cls.border_m) + card.append(body_line.ljust(body_linewrap + 1) + cls.border_h) + card.append(cls.border_l + cls.border_b * body_linewrap + cls.border_m) return "\n".join(card) @classmethod diff --git a/jrnl/plugins/jrnl_importer.py b/jrnl/plugins/jrnl_importer.py index 83341cd94..972114d46 100644 --- a/jrnl/plugins/jrnl_importer.py +++ b/jrnl/plugins/jrnl_importer.py @@ -4,8 +4,10 @@ import sys from .. import util + class JRNLImporter: """This plugin imports entries from other jrnl files.""" + names = ["jrnl"] @staticmethod @@ -25,5 +27,8 @@ def import_(journal, input=None): sys.exit(0) journal.import_(other_journal_txt) new_cnt = len(journal.entries) - print("[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr) + 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 e368a300b..90a2059f0 100644 --- a/jrnl/plugins/json_exporter.py +++ b/jrnl/plugins/json_exporter.py @@ -8,20 +8,21 @@ class JSONExporter(TextExporter): """This Exporter can convert entries and journals into json.""" + names = ["json"] extension = "json" @classmethod def entry_to_dict(cls, entry): entry_dict = { - 'title': entry.title, - 'body': entry.body, - 'date': entry.date.strftime("%Y-%m-%d"), - 'time': entry.date.strftime("%H:%M"), - 'starred': entry.starred + "title": entry.title, + "body": entry.body, + "date": entry.date.strftime("%Y-%m-%d"), + "time": entry.date.strftime("%H:%M"), + "starred": entry.starred, } if hasattr(entry, "uuid"): - entry_dict['uuid'] = entry.uuid + entry_dict["uuid"] = entry.uuid return entry_dict @classmethod @@ -35,6 +36,6 @@ def export_journal(cls, journal): tags = get_tags_count(journal) result = { "tags": {tag: count for count, tag in tags}, - "entries": [cls.entry_to_dict(e) for e in journal.entries] + "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 da2b57482..14060ce97 100644 --- a/jrnl/plugins/markdown_exporter.py +++ b/jrnl/plugins/markdown_exporter.py @@ -10,24 +10,25 @@ class MarkdownExporter(TextExporter): """This Exporter can convert entries and journals into Markdown.""" + names = ["md", "markdown"] extension = "md" @classmethod def export_entry(cls, entry, to_multifile=True): """Returns a markdown representation of a single entry.""" - date_str = entry.date.strftime(entry.journal.config['timeformat']) + date_str = entry.date.strftime(entry.journal.config["timeformat"]) body_wrapper = "\n" if entry.body else "" body = body_wrapper + entry.body if to_multifile is True: - heading = '#' + heading = "#" else: - heading = '###' + heading = "###" - '''Increase heading levels in body text''' - newbody = '' - previous_line = '' + """Increase heading levels in body text""" + newbody = "" + previous_line = "" warn_on_heading_level = False for line in body.splitlines(True): if re.match(r"^#+ ", line): @@ -35,24 +36,30 @@ def export_entry(cls, entry, to_multifile=True): newbody = newbody + previous_line + heading + line if re.match(r"^#######+ ", heading + line): warn_on_heading_level = True - line = '' - elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()): + line = "" + elif re.match(r"^=+$", line.rstrip()) and not re.match( + r"^$", previous_line.strip() + ): """Setext style H1""" newbody = newbody + heading + "# " + previous_line - line = '' - elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()): + line = "" + elif re.match(r"^-+$", line.rstrip()) and not re.match( + r"^$", previous_line.strip() + ): """Setext style H2""" newbody = newbody + heading + "## " + previous_line - line = '' + line = "" else: newbody = newbody + previous_line previous_line = line - newbody = newbody + previous_line # add very last line + newbody = newbody + previous_line # add very last line if warn_on_heading_level is True: - print(f"{WARNING_COLOR}WARNING{RESET_COLOR}: " - f"Headings increased past H6 on export - {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 f"{heading} {date_str} {entry.title}\n{newbody} " diff --git a/jrnl/plugins/tag_exporter.py b/jrnl/plugins/tag_exporter.py index f5453ced8..89d54a1aa 100644 --- a/jrnl/plugins/tag_exporter.py +++ b/jrnl/plugins/tag_exporter.py @@ -7,6 +7,7 @@ class TagExporter(TextExporter): """This Exporter can lists the tags for entries and journals, exported as a plain text file.""" + names = ["tags"] extension = "tags" @@ -21,9 +22,11 @@ def export_journal(cls, journal): tag_counts = get_tags_count(journal) result = "" if not tag_counts: - return '[No tags found in journal.]' + return "[No tags found in 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("{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) + result += "[Removed tags that appear only once.]\n" + 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 7f72e2f80..6370c373b 100644 --- a/jrnl/plugins/template.py +++ b/jrnl/plugins/template.py @@ -7,7 +7,9 @@ PRINT_RE = r"{{ *(.+?) *}}" START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}" END_BLOCK_RE = r"{% *end(for|if) *%}" -FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE, expression=EXPRESSION_RE) +FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format( + varname=VAR_RE, expression=EXPRESSION_RE +) IF_RE = r"{% *if +(.+?) *%}" BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}" INCLUDE_RE = r"{% *include +(.+?) *%}" @@ -41,7 +43,7 @@ def render_block(self, block, **vars): def _eval_context(self, vars): e = asteval.Interpreter(use_numpy=False, writer=None) e.symtable.update(vars) - e.symtable['__last_iteration'] = vars.get("__last_iteration", False) + e.symtable["__last_iteration"] = vars.get("__last_iteration", False) return e def _get_blocks(self): @@ -49,12 +51,19 @@ def s(match): name, contents = match.groups() self.blocks[name] = self._strip_single_nl(contents) return "" + self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE) def _expand(self, template, **vars): stack = sorted( - [(m.start(), 1, m.groups()[0]) for m in re.finditer(START_BLOCK_RE, template)] + - [(m.end(), -1, m.groups()[0]) for m in re.finditer(END_BLOCK_RE, template)] + [ + (m.start(), 1, m.groups()[0]) + for m in re.finditer(START_BLOCK_RE, template) + ] + + [ + (m.end(), -1, m.groups()[0]) + for m in re.finditer(END_BLOCK_RE, template) + ] ) last_nesting, nesting = 0, 0 @@ -80,19 +89,23 @@ def _expand(self, template, **vars): start = pos last_nesting = nesting - result += self._expand_vars(template[stack[-1][0]:], **vars) + result += self._expand_vars(template[stack[-1][0] :], **vars) return result def _expand_vars(self, template, **vars): safe_eval = self._eval_context(vars) - expanded = re.sub(INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template) + expanded = re.sub( + INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template + ) return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded) def _expand_cond(self, template, **vars): start_block = re.search(IF_RE, template, re.M) end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1] expression = start_block.groups()[0] - sub_template = self._strip_single_nl(template[start_block.end():end_block.start()]) + sub_template = self._strip_single_nl( + template[start_block.end() : end_block.start()] + ) safe_eval = self._eval_context(vars) if safe_eval(expression): @@ -110,15 +123,17 @@ def _expand_loops(self, template, **vars): start_block = re.search(FOR_RE, template, re.M) end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1] var_name, iterator = start_block.groups() - sub_template = self._strip_single_nl(template[start_block.end():end_block.start()], strip_r=False) + sub_template = self._strip_single_nl( + template[start_block.end() : end_block.start()], strip_r=False + ) safe_eval = self._eval_context(vars) - result = '' + result = "" items = safe_eval(iterator) for idx, var in enumerate(items): vars[var_name] = var - vars['__last_iteration'] = idx == len(items) - 1 + vars["__last_iteration"] = idx == len(items) - 1 result += self._expand(sub_template, **vars) del vars[var_name] return self._strip_single_nl(result) diff --git a/jrnl/plugins/template_exporter.py b/jrnl/plugins/template_exporter.py index f15328f23..eb360f945 100644 --- a/jrnl/plugins/template_exporter.py +++ b/jrnl/plugins/template_exporter.py @@ -13,20 +13,13 @@ class GenericTemplateExporter(TextExporter): @classmethod def export_entry(cls, entry): """Returns a string representation of a single entry.""" - vars = { - 'entry': entry, - 'tags': entry.tags - } + vars = {"entry": entry, "tags": entry.tags} return cls.template.render_block("entry", **vars) @classmethod def export_journal(cls, journal): """Returns a string representation of an entire journal.""" - vars = { - 'journal': journal, - 'entries': journal.entries, - 'tags': journal.tags - } + vars = {"journal": journal, "entries": journal.entries, "tags": journal.tags} return cls.template.render_block("journal", **vars) @@ -34,11 +27,12 @@ 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(f"{name.title()}Exporter"), (GenericTemplateExporter, ), { - "names": [name], - "extension": template.extension, - "template": template - }) + return type( + str(f"{name.title()}Exporter"), + (GenericTemplateExporter,), + {"names": [name], "extension": template.extension, "template": template}, + ) + __all__ = [] diff --git a/jrnl/plugins/text_exporter.py b/jrnl/plugins/text_exporter.py index ce2e71de0..6f2a4531b 100644 --- a/jrnl/plugins/text_exporter.py +++ b/jrnl/plugins/text_exporter.py @@ -8,6 +8,7 @@ class TextExporter: """This Exporter can convert entries and journals into text files.""" + names = ["text", "txt"] extension = "txt" @@ -33,7 +34,9 @@ def write_file(cls, journal, path): @classmethod def make_filename(cls, entry): - return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(str(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): @@ -44,7 +47,9 @@ def write_files(cls, journal, path): 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 "[{2}ERROR{3}: {0} {1}]".format( + e.filename, e.strerror, ERROR_COLOR, RESET_COLOR + ) return "[Journal exported to {}]".format(path) @classmethod @@ -54,7 +59,7 @@ def export(cls, journal, output=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 + elif output: # single file return cls.write_file(journal, output) else: return cls.export_journal(journal) diff --git a/jrnl/plugins/util.py b/jrnl/plugins/util.py index a056b19ab..a030f8d38 100644 --- a/jrnl/plugins/util.py +++ b/jrnl/plugins/util.py @@ -6,9 +6,7 @@ def get_tags_count(journal): """Returns a set of tuples (count, tag) for all tags present in the journal.""" # Astute reader: should the following line leave you as puzzled as me the first time # I came across this construction, worry not and embrace the ensuing moment of enlightment. - tags = [tag - for entry in journal.entries - for tag in set(entry.tags)] + tags = [tag 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 = {(tags.count(tag), tag) for tag in tags} return tag_counts @@ -24,4 +22,4 @@ def oxford_list(lst): elif len(lst) == 2: return lst[0] + " or " + lst[1] else: - return ', '.join(lst[:-1]) + ", or " + lst[-1] + return ", ".join(lst[:-1]) + ", or " + lst[-1] diff --git a/jrnl/plugins/xml_exporter.py b/jrnl/plugins/xml_exporter.py index 2783663b5..07506cf26 100644 --- a/jrnl/plugins/xml_exporter.py +++ b/jrnl/plugins/xml_exporter.py @@ -8,6 +8,7 @@ class XMLExporter(JSONExporter): """This Exporter can convert entries and journals into XML.""" + names = ["xml"] extension = "xml" @@ -15,7 +16,7 @@ class XMLExporter(JSONExporter): def export_entry(cls, entry, doc=None): """Returns an XML representation of a single entry.""" doc_el = doc or minidom.Document() - entry_el = doc_el.createElement('entry') + 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(value)) @@ -28,11 +29,11 @@ def export_entry(cls, entry, doc=None): @classmethod def entry_to_xml(cls, entry, doc): - entry_el = doc.createElement('entry') - entry_el.setAttribute('date', entry.date.isoformat()) + entry_el = doc.createElement("entry") + entry_el.setAttribute("date", entry.date.isoformat()) if hasattr(entry, "uuid"): - entry_el.setAttribute('uuid', entry.uuid) - entry_el.setAttribute('starred', entry.starred) + entry_el.setAttribute("uuid", entry.uuid) + entry_el.setAttribute("starred", entry.starred) entry_el.appendChild(doc.createTextNode(entry.fulltext)) return entry_el @@ -41,12 +42,12 @@ def export_journal(cls, journal): """Returns an XML representation of an entire journal.""" tags = get_tags_count(journal) doc = minidom.Document() - xml = doc.createElement('journal') - tags_el = doc.createElement('tags') - entries_el = doc.createElement('entries') + xml = doc.createElement("journal") + tags_el = doc.createElement("tags") + entries_el = doc.createElement("entries") for count, tag in tags: - tag_el = doc.createElement('tag') - tag_el.setAttribute('name', tag) + tag_el = doc.createElement("tag") + tag_el.setAttribute("name", tag) count_node = doc.createTextNode(str(count)) tag_el.appendChild(count_node) tags_el.appendChild(tag_el) diff --git a/jrnl/plugins/yaml_exporter.py b/jrnl/plugins/yaml_exporter.py index 4a75667f7..b0177b39b 100644 --- a/jrnl/plugins/yaml_exporter.py +++ b/jrnl/plugins/yaml_exporter.py @@ -10,6 +10,7 @@ class YAMLExporter(TextExporter): """This Exporter can convert entries and journals into Markdown formatted text with YAML front matter.""" + names = ["yaml"] extension = "md" @@ -17,22 +18,29 @@ 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']) + date_str = entry.date.strftime(entry.journal.config["timeformat"]) body_wrapper = "\n" if entry.body else "" body = body_wrapper + entry.body - tagsymbols = entry.journal.config['tagsymbols'] + 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)) + multi_tag_regex = re.compile( + r"(?u)^\s*([{tags}][-+*#/\w]+\s*)+$".format(tags=tagsymbols) + ) - '''Increase heading levels in body text''' - newbody = '' - heading = '#' - previous_line = '' + """Increase heading levels in body text""" + newbody = "" + heading = "#" + previous_line = "" warn_on_heading_level = False for line in entry.body.splitlines(True): if re.match(r"^#+ ", line): @@ -40,45 +48,59 @@ def export_entry(cls, entry, to_multifile=True): newbody = newbody + previous_line + heading + line if re.match(r"^#######+ ", heading + line): warn_on_heading_level = True - line = '' - elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()): + line = "" + elif re.match(r"^=+$", line.rstrip()) and not re.match( + r"^$", previous_line.strip() + ): """Setext style H1""" newbody = newbody + heading + "# " + previous_line - line = '' - elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()): + line = "" + elif re.match(r"^-+$", line.rstrip()) and not re.match( + r"^$", previous_line.strip() + ): """Setext style H2""" newbody = newbody + heading + "## " + previous_line - line = '' + line = "" elif multi_tag_regex.match(line): """Tag only lines""" - line = '' + line = "" else: newbody = newbody + previous_line previous_line = line - newbody = newbody + previous_line # add very last line + 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( + "{}WARNING{}: Headings increased past H6 on export - {} {}".format( + WARNING_COLOR, RESET_COLOR, date_str, entry.title + ), + file=sys.stderr, + ) - dayone_attributes = '' + dayone_attributes = "" if hasattr(entry, "uuid"): - dayone_attributes += 'uuid: ' + entry.uuid + '\n' + dayone_attributes += "uuid: " + entry.uuid + "\n" # TODO: copy over pictures, if present # source directory is entry.journal.config['journal'] # output directory is...? return "title: {title}\ndate: {date}\nstared: {stared}\ntags: {tags}\n{dayone} {body} {space}".format( - date = date_str, - title = entry.title, - stared = entry.starred, - tags = ', '.join([tag[1:] for tag in entry.tags]), - dayone = dayone_attributes, - body = newbody, - space="" + date=date_str, + title=entry.title, + stared=entry.starred, + tags=", ".join([tag[1:] for tag in entry.tags]), + dayone=dayone_attributes, + body=newbody, + space="", ) @classmethod def export_journal(cls, journal): """Returns an error, as YAML export requires a directory as a target.""" - print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(ERROR_COLOR, RESET_COLOR), file=sys.stderr) + print( + "{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format( + ERROR_COLOR, RESET_COLOR + ), + file=sys.stderr, + ) return diff --git a/jrnl/time.py b/jrnl/time.py index 66d3f4f80..5e91cd1bc 100644 --- a/jrnl/time.py +++ b/jrnl/time.py @@ -1,7 +1,10 @@ from datetime import datetime from dateutil.parser import parse as dateparse -try: import parsedatetime.parsedatetime_consts as pdt -except ImportError: import parsedatetime as pdt + +try: + import parsedatetime.parsedatetime_consts as pdt +except ImportError: + import parsedatetime as pdt FAKE_YEAR = 9999 DEFAULT_FUTURE = datetime(FAKE_YEAR, 12, 31, 23, 59, 59) @@ -12,14 +15,16 @@ CALENDAR = pdt.Calendar(consts) -def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False): +def parse( + date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False +): """Parses a string containing a fuzzy date and returns a datetime.datetime object""" if not date_str: return None elif isinstance(date_str, datetime): return date_str - # Don't try to parse anything with 6 or less characters and was parsed from the existing journal. + # Don't try to parse anything with 6 or less characters and was parsed from the existing journal. # It's probably a markdown footnote if len(date_str) <= 6 and bracketed: return None @@ -37,7 +42,7 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bra flag = 1 if date.hour == date.minute == 0 else 2 date = date.timetuple() except Exception as e: - if e.args[0] == 'day is out of range for month': + if e.args[0] == "day is out of range for month": y, m, d, H, M, S = default_date.timetuple()[:6] default_date = datetime(y, m, d - 1, H, M, S) else: @@ -53,10 +58,12 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None, bra return None if flag is 1: # Date found, but no time. Use the default time. - 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) + 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/upgrade.py b/jrnl/upgrade.py index 8f67aa34a..ac29240b6 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -11,9 +11,9 @@ def backup(filename, binary=False): 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: + with open(filename, "rb" if binary else "r") as original: contents = original.read() - with open(filename + ".backup", 'wb' if binary else 'w') as backup: + with open(filename + ".backup", "wb" if binary else "w") as backup: backup.write(contents) @@ -25,7 +25,8 @@ def upgrade_jrnl_if_necessary(config_path): config = util.load_config(config_path) - print("""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 @@ -39,18 +40,21 @@ def upgrade_jrnl_if_necessary(config_path): Please note that jrnl 1.x is NOT forward compatible with this version of jrnl. If you choose to proceed, you will not be able to use your journals with older versions of jrnl anymore. -""".format(__version__)) +""".format( + __version__ + ) + ) encrypted_journals = {} plain_journals = {} other_journals = {} all_journals = [] - for journal_name, journal_conf in config['journals'].items(): + for journal_name, journal_conf in config["journals"].items(): if isinstance(journal_conf, dict): path = journal_conf.get("journal") encrypt = journal_conf.get("encrypt") else: - encrypt = config.get('encrypt') + encrypt = config.get("encrypt") path = journal_conf path = os.path.expanduser(path) @@ -62,21 +66,36 @@ def upgrade_jrnl_if_necessary(config_path): else: plain_journals[journal_name] = path - longest_journal_name = max([len(journal) for journal in config['journals']]) + longest_journal_name = max([len(journal) for journal in config["journals"]]) if encrypted_journals: - print(f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", file=sys.stderr) + print( + f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", + file=sys.stderr, + ) for journal, path in encrypted_journals.items(): - print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) + print( + " {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), + file=sys.stderr, + ) if plain_journals: - print(f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", file=sys.stderr) + print( + f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", + file=sys.stderr, + ) for journal, path in plain_journals.items(): - print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) + print( + " {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), + file=sys.stderr, + ) if other_journals: print("\nFollowing journals will be not be touched:", file=sys.stderr) for journal, path in other_journals.items(): - print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) + print( + " {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), + file=sys.stderr, + ) try: cont = util.yesno("\nContinue upgrading jrnl?", default=False) @@ -86,24 +105,37 @@ def upgrade_jrnl_if_necessary(config_path): raise UserAbort("jrnl NOT upgraded, exiting.") for journal_name, path in encrypted_journals.items(): - print(f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...", file=sys.stderr) + 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) + 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(): - print(f"\nUpgrading plain text '{journal_name}' journal stored in {path}...", file=sys.stderr) + 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) + old_journal = Journal.open_journal( + journal_name, util.scope_config(config, journal_name), legacy=True + ) all_journals.append(Journal.PlainJournal.from_journal(old_journal)) # loop through lists to validate failed_journals = [j for j in all_journals if not j.validate_parsing()] if len(failed_journals) > 0: - 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 + 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 @@ -120,4 +152,5 @@ def upgrade_jrnl_if_necessary(config_path): class UpgradeValidationException(Exception): """Raised when the contents of an upgraded journal do not match the old journal""" + pass diff --git a/jrnl/util.py b/jrnl/util.py index f70b2df97..6ae00c388 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,8 +4,10 @@ import os import getpass as gp import yaml + if "win32" in sys.platform: import colorama + colorama.init() import re import tempfile @@ -22,7 +24,8 @@ # Based on Segtok by Florian Leitner # https://github.com/fnl/segtok -SENTENCE_SPLITTER = re.compile(r""" +SENTENCE_SPLITTER = re.compile( + r""" ( # A sentence ends at one of two sequences: [.!?\u203C\u203D\u2047\u2048\u2049\u3002\uFE52\uFE57\uFF01\uFF0E\uFF1F\uFF61] # Either, a sequence starting with a sentence terminal, [\'\u2019\"\u201D]? # an optional right quote, @@ -30,7 +33,9 @@ \s+ # a sequence of required spaces. | # Otherwise, \n # a sentence also terminates newlines. -)""", re.VERBOSE) +)""", + re.VERBOSE, +) class UserAbort(Exception): @@ -68,21 +73,23 @@ def get_password(validator, keychain=None, max_attempts=3): def get_keychain(journal_name): import keyring + try: - return keyring.get_password('jrnl', journal_name) + return keyring.get_password("jrnl", journal_name) except RuntimeError: return "" def set_keychain(journal_name, password): import keyring + if password is None: try: - keyring.delete_password('jrnl', journal_name) + keyring.delete_password("jrnl", journal_name) except RuntimeError: pass else: - keyring.set_password('jrnl', journal_name, password) + keyring.set_password("jrnl", journal_name, password) def yesno(prompt, default=True): @@ -99,34 +106,40 @@ def load_config(config_path): def scope_config(config, journal_name): - if journal_name not in config['journals']: + if journal_name not in config["journals"]: return config config = config.copy() - 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 journal overrides %s', journal_conf) + 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 journal 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 - config.pop('journals') + config["journal"] = journal_conf + config.pop("journals") return config def get_text_from_editor(config, template=""): filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt") - with open(tmpfile, 'w', encoding="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]) + subprocess.call( + shlex.split(config["editor"], posix="win" not in sys.platform) + [tmpfile] + ) except AttributeError: - subprocess.call(config['editor'] + [tmpfile]) + subprocess.call(config["editor"] + [tmpfile]) with open(tmpfile, "r", encoding="utf-8") as f: raw = f.read() os.close(filehandle) os.remove(tmpfile) if not raw: - print('[Nothing saved to file]', file=sys.stderr) + print("[Nothing saved to file]", file=sys.stderr) return raw @@ -139,9 +152,9 @@ def slugify(string): """Slugifies a string. Based on public domain code from https://github.com/zacharyvoase/slugify """ - 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) + 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 slug @@ -150,4 +163,4 @@ def split_title(text): punkt = SENTENCE_SPLITTER.search(text) if not punkt: return text, "" - return text[:punkt.end()].strip(), text[punkt.end():].strip() + return text[: punkt.end()].strip(), text[punkt.end() :].strip() From c1defc7db128df23a05bdd5d85e16ae958590331 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Sat, 7 Dec 2019 12:45:28 -0700 Subject: [PATCH 2/4] [Travis] add a linting stage (via `black`) --- .travis.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 17251ba94..6454b1abf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,8 +53,17 @@ jobs: - os: windows include: + - stage: Lint + name: Lint, via Black + python: 3.8 + install: + - pip install black + script: + - black . + # Python 3.6 Tests - - name: Python 3.6 on Linux + - stage: Tests + name: Python 3.6 on Linux python: 3.6 - <<: *test_mac name: Python 3.6 on MacOS From 9d183229dfeeb6be36048fb9b28b9c875a9f3383 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 12 Dec 2019 10:41:08 -0700 Subject: [PATCH 3/4] [Travis] update as per code review Remove "Lint" as separate stage; have `black` check the output rather than run the re-formmater --- .travis.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6454b1abf..635e61013 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,17 +53,15 @@ jobs: - os: windows include: - - stage: Lint + - stage: Lint & Tests name: Lint, via Black python: 3.8 - install: - - pip install black + # black is automatically installed by peotry script: - - black . + - black --check . --verbose # Python 3.6 Tests - - stage: Tests - name: Python 3.6 on Linux + - name: Python 3.6 on Linux python: 3.6 - <<: *test_mac name: Python 3.6 on MacOS From c8172efdcc487684d25a7099ec3161604da0e261 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Sat, 14 Dec 2019 14:00:28 -0700 Subject: [PATCH 4/4] [Travis] black -- show a diff on things that need to be reformatted --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 80a86be9d..a4858707a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,7 +58,7 @@ jobs: python: 3.8 # black is automatically installed by peotry script: - - black --check . --verbose + - black --check . --verbose --diff # Python 3.6 Tests - name: Python 3.6 on Linux