From de520814818fb6b2472de9376be63869198fe099 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 7 Nov 2019 16:48:47 -0800 Subject: [PATCH 001/112] Explicitly write Version to file --- .gitignore | 1 + .travis.yml | 1 + jrnl/__init__.py | 11 +++++++---- pyproject.toml | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2382cabe..b14464d0 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ exp/ _extras/ *.sublime-* site/ +jrnl/VERSION.txt diff --git a/.travis.yml b/.travis.yml index fea48f50..3eda0c9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ script: before_deploy: - pip install poetry - poetry config http-basic.pypi $PYPI_USER $PYPI_PASS + - echo $TRAVIS_TAG > jrnl/VERSION.txt - poetry version $TRAVIS_TAG - poetry build deploy: diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 57664dbb..f1cdc3c4 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -1,9 +1,12 @@ #!/usr/bin/env python # encoding: utf-8 -import pkg_resources +import os -dist = pkg_resources.get_distribution('jrnl') -__title__ = dist.project_name -__version__ = dist.version +__title__ = "jrnl" +__version__ = "source" +version_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION.txt") +if os.path.exists(version_path): + with open(version_path) as version_file: + __version__ = version_file.read() diff --git a/pyproject.toml b/pyproject.toml index 1b84acde..df18f9da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ license = "MIT" readme = "README.md" homepage = "https://jrnl.sh" repository = "https://github.com/jrnl-org/jrnl" +include = ["VERSION.txt"] [tool.poetry.dependencies] python = "^3.7" From 80b376b4fb03b023d28e41aa43be8d8c83077d99 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 7 Nov 2019 16:49:01 -0800 Subject: [PATCH 002/112] Delay import of asteval --- jrnl/plugins/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/plugins/template.py b/jrnl/plugins/template.py index 21fb2896..bdb1ebe4 100644 --- a/jrnl/plugins/template.py +++ b/jrnl/plugins/template.py @@ -1,5 +1,4 @@ import re -import asteval import yaml VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*" @@ -39,6 +38,7 @@ class Template(object): return self._expand(self.blocks[block], **vars) def _eval_context(self, vars): + import asteval e = asteval.Interpreter(use_numpy=False, writer=None) e.symtable.update(vars) e.symtable['__last_iteration'] = vars.get("__last_iteration", False) From e682d2799415d581e4728ff0d992303382d8fdc0 Mon Sep 17 00:00:00 2001 From: Jims Date: Tue, 12 Nov 2019 11:53:12 -0500 Subject: [PATCH 003/112] added case-insensitive searching of entry title and body using a -S argument --- jrnl/Journal.py | 4 +++- jrnl/cli.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index f6813222..ab52cd00 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -187,7 +187,7 @@ class Journal: 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, search_plain=None, 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 @@ -209,6 +209,7 @@ class Journal: # If strict mode is on, all tags have to be present in entry 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 if (not tags or tagged(entry.tags)) @@ -216,6 +217,7 @@ class Journal: and (not start_date or entry.date >= start_date) and (not end_date or entry.date <= end_date) and (not exclude or not excluded(entry.tags)) + and (not search_plain or (search_plain.lower() in entry.title.lower() or search_plain.lower() in entry.body.lower())) ] self.entries = result diff --git a/jrnl/cli.py b/jrnl/cli.py index 3cf67030..ea8de891 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -32,6 +32,7 @@ def parse_args(args=None): 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('-S', '-search', dest='search_plain', help='View entries containing a specific string') 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') @@ -64,7 +65,7 @@ def guess_mode(args, config): 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, args.search_plain)): # Any sign of displaying stuff? compose = False elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()): @@ -236,7 +237,8 @@ def run(manual_args=None): strict=args.strict, short=args.short, starred=args.starred, - exclude=args.excluded) + exclude=args.excluded, + search_plain=args.search_plain) journal.limit(args.limit) # Reading mode From fd5a08a4b27b83de3fa1c0067e9cf79a782d605d Mon Sep 17 00:00:00 2001 From: Jims Date: Thu, 14 Nov 2019 18:39:39 -0500 Subject: [PATCH 004/112] added search test --- features/searching.feature | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 features/searching.feature diff --git a/features/searching.feature b/features/searching.feature new file mode 100644 index 00000000..52e1ba54 --- /dev/null +++ b/features/searching.feature @@ -0,0 +1,11 @@ +Feature: Searching + + Scenario: Searching for a string + Given we use the config "basic.yaml" + When we run "jrnl -S life" + Then we should get no error + and the output should be + """ + 2013-06-10 15:40 Life is good. + | But I'm better. + """ From e925fe22f9f7c5c4051eb73b8be708b8c93841d7 Mon Sep 17 00:00:00 2001 From: Peter Schmidbauer Date: Thu, 31 Oct 2019 21:22:50 +0100 Subject: [PATCH 005/112] Add password confirmation dialog --- features/encryption.feature | 42 ++++++++++++++++++++++++------ features/multiple_journals.feature | 7 ++++- features/steps/core.py | 28 +++++++++++--------- jrnl/EncryptedJournal.py | 2 +- jrnl/cli.py | 2 +- jrnl/util.py | 10 +++++-- 6 files changed, 66 insertions(+), 25 deletions(-) diff --git a/features/encryption.feature b/features/encryption.feature index d30c48b3..787fa850 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -3,29 +3,55 @@ Given we use the config "encrypted.yaml" When we run "jrnl -n 1" and enter "bad doggie no biscuit" Then the output should contain "Password" - and the output should contain "2013-06-10 15:40 Life is good" + And the output should contain "2013-06-10 15:40 Life is good" Scenario: Decrypting a journal Given we use the config "encrypted.yaml" When we run "jrnl --decrypt" and enter "bad doggie no biscuit" Then the config for journal "default" should have "encrypt" set to "bool:False" Then we should see the message "Journal decrypted" - and the journal should have 2 entries + And the journal should have 2 entries Scenario: Encrypting a journal Given we use the config "basic.yaml" - When we run "jrnl --encrypt" and enter "swordfish" and "n" + When we run "jrnl --encrypt" and enter + """ + swordfish + swordfish + n + """ Then we should see the message "Journal encrypted" - and the config for journal "default" should have "encrypt" set to "bool:True" + And the config for journal "default" should have "encrypt" set to "bool:True" When we run "jrnl -n 1" and enter "swordfish" Then the output should contain "Password" - and the output should contain "2013-06-10 15:40 Life is good" + And the output should contain "2013-06-10 15:40 Life is good" + + Scenario: Mistyping your password + Given we use the config "basic.yaml" + When we run "jrnl --encrypt" and enter + """ + swordfish + sordfish + swordfish + swordfish + n + """ + Then we should see the message "Passwords did not match" + And 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 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" and "y" + When we run "jrnl simple --encrypt" and enter + """ + sabertooth + sabertooth + 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" - Then we should not see the message "Password" - and the output should contain "2013-06-10 15:40 Life is good" + Then the output should contain "2013-06-10 15:40 Life is good" diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature index 28587f96..56c4b3a7 100644 --- a/features/multiple_journals.feature +++ b/features/multiple_journals.feature @@ -42,5 +42,10 @@ 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" and "y" + When we run "jrnl new_encrypted Adding first entry" and enter + """ + these three eyes + these three eyes + n + """ Then we should see the message "Journal 'new_encrypted' created" diff --git a/features/steps/core.py b/features/steps/core.py index af4d19a3..375c1f74 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -26,7 +26,7 @@ class TestKeyring(keyring.backend.KeyringBackend): def get_password(self, servicename, username): return self.keys[servicename].get(username) - def delete_password(self, servicename, username, password): + def delete_password(self, servicename, username): self.keys[servicename][username] = None @@ -102,29 +102,33 @@ def _mock_input(inputs): @when('we run "{command}" and enter') @when('we run "{command}" and enter ""') -@when('we run "{command}" and enter "{inputs1}"') -@when('we run "{command}" and enter "{inputs1}" and "{inputs2}"') -def run_with_input(context, command, inputs1="", inputs2=""): +@when('we run "{command}" and enter "{inputs}"') +def run_with_input(context, command, inputs=""): # 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()' - if inputs1: - text = iter((inputs1, inputs2)) - elif context.text: + if context.text: text = iter(context.text.split("\n")) else: - text = iter(("", "")) + text = iter([inputs]) + args = ushlex(command)[1:] - 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: + 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 - # assert at least one of the mocked input methods got called + # 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 diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index 1cce66b8..f3ff63c6 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -38,7 +38,7 @@ class EncryptedJournal(Journal.Journal): filename = filename or self.config['journal'] if not os.path.exists(filename): - password = util.getpass("Enter password for new journal: ") + password = util.create_password() if password: if util.yesno("Do you want to store the password in your keychain?", default=True): util.set_keychain(self.name, password) diff --git a/jrnl/cli.py b/jrnl/cli.py index 738087e4..945a728f 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -78,7 +78,7 @@ 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.getpass("Enter new password: ") + journal.config['password'] = util.create_password() journal.config['encrypt'] = True new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config) diff --git a/jrnl/util.py b/jrnl/util.py index fc917ae9..f70b2df9 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -37,12 +37,18 @@ class UserAbort(Exception): pass -getpass = gp.getpass +def create_password(): + while True: + pw = gp.getpass("Enter password for new journal: ") + if pw == gp.getpass("Enter password again: "): + return pw + + print("Passwords did not match, please try again", file=sys.stderr) def get_password(validator, keychain=None, max_attempts=3): pwd_from_keychain = keychain and get_keychain(keychain) - password = pwd_from_keychain or getpass() + password = pwd_from_keychain or gp.getpass() result = validator(password) # Password is bad: if result is None and pwd_from_keychain: From cab23ddc97c0adb083ce2d66808cad581ed63005 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 28 Nov 2019 07:43:05 -0700 Subject: [PATCH 006/112] [Travis] add testing on Windows and Mac See #739 (Python 3.8), #619 (Windows) --- .travis.yml | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e1f35e61..7efc4fd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,33 @@ dist: xenial # required for Python >= 3.7 language: python -python: - - 3.6 - - 3.7 +jobs: + include: + - name: "Python 3.6 on Linux" + python: 3.6 + - name: "Python 3.7 on Linux" + python: 3.7 + - name: "Python 3.8 on Linux" + python: 3.8 + - name: "Python, developmental version, on Linux" + python: nightly + - name: "Python 3.7.4 on MacOS" + os: osx + osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 + language: shell # 'language: python' is an error on Travis CI macOS + - name: "Python 3.8.0 on Windows" + os: windows + langage: shell # 'language: python' is an error on Travis CI Windows + before_install: + - choco install python --version 3.8.0 + - python -m pip install --upgrade pip + allow_failures: + - python: 3.8 + - python: nightly git: depth: false -before_install: - - pip install poetry~=0.12.17 +cache: pip install: + - pip install poetry~=0.12.17 # we run `poetry version` here to appease poetry about '0.0.0-source' - poetry version - poetry install From b0f07eeb5981253cbdaefba7eb55fbb2d9d3e232 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 28 Nov 2019 07:49:33 -0700 Subject: [PATCH 007/112] Appease the Travis gods --- .travis.yml | 9 ++++++--- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7efc4fd3..eac6f3f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ dist: xenial # required for Python >= 3.7 -language: python jobs: include: - name: "Python 3.6 on Linux" @@ -8,26 +7,30 @@ jobs: python: 3.7 - name: "Python 3.8 on Linux" python: 3.8 - - name: "Python, developmental version, on Linux" + - name: "Python dev on Linux" python: nightly - name: "Python 3.7.4 on MacOS" os: osx osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 language: shell # 'language: python' is an error on Travis CI macOS + before_install: + - pip3 install poetry~=0.12.17 - name: "Python 3.8.0 on Windows" os: windows langage: shell # 'language: python' is an error on Travis CI Windows before_install: - choco install python --version 3.8.0 - python -m pip install --upgrade pip + - pip install poetry~=0.12.17 allow_failures: - python: 3.8 - python: nightly git: depth: false cache: pip -install: +before_install: - pip install poetry~=0.12.17 +install: # we run `poetry version` here to appease poetry about '0.0.0-source' - poetry version - poetry install diff --git a/pyproject.toml b/pyproject.toml index 70a44402..136aeb97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ homepage = "https://jrnl.sh" repository = "https://github.com/jrnl-org/jrnl" [tool.poetry.dependencies] -python = ">=3.6.0, <3.8.0" +python = ">=3.6.0, <3.9.0" pyxdg = "^0.26.0" cryptography = "^2.7" passlib = "^1.7" From 3c2ced098915ad31a2acd2af3315017b9575fb12 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 28 Nov 2019 07:59:06 -0700 Subject: [PATCH 008/112] [Travis] no auto-ruby --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index eac6f3f2..22241a4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,16 @@ dist: xenial # required for Python >= 3.7 jobs: include: - name: "Python 3.6 on Linux" + language: python python: 3.6 - name: "Python 3.7 on Linux" + language: python python: 3.7 - name: "Python 3.8 on Linux" + language: python python: 3.8 - name: "Python dev on Linux" + language: python python: nightly - name: "Python 3.7.4 on MacOS" os: osx From cdad3230f023ed6df6c29f21c7b5d92d2a8cb9f1 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 28 Nov 2019 08:21:01 -0700 Subject: [PATCH 009/112] [Travis] include a test in not UTC see #742 --- .travis.yml | 17 ++++++++++------- features/steps/core.py | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 22241a4d..19fa4084 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,31 @@ dist: xenial # required for Python >= 3.7 +language: python jobs: include: - name: "Python 3.6 on Linux" - language: python python: 3.6 - name: "Python 3.7 on Linux" - language: python python: 3.7 + - name: "Python 3.7 on Linux, not UTC" + python: 3.7 + env: + - TZ=America/Edmonton - name: "Python 3.8 on Linux" - language: python python: 3.8 - name: "Python dev on Linux" - language: python python: nightly - name: "Python 3.7.4 on MacOS" os: osx osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 language: shell # 'language: python' is an error on Travis CI macOS before_install: - - pip3 install poetry~=0.12.17 + - pip3 install poetry~=0.12.17 # 'pip' points to Python 2 on MacOS - name: "Python 3.8.0 on Windows" os: windows - langage: shell # 'language: python' is an error on Travis CI Windows + language: shell # 'language: python' is an error on Travis CI Windows before_install: - choco install python --version 3.8.0 + - refreshenv # add Python to PATH - python -m pip install --upgrade pip - pip install poetry~=0.12.17 allow_failures: @@ -33,13 +35,14 @@ git: depth: false cache: pip before_install: + - date - pip install poetry~=0.12.17 install: # we run `poetry version` here to appease poetry about '0.0.0-source' - poetry version - poetry install -script: - poetry run python --version +script: - poetry run behave before_deploy: - poetry config http-basic.pypi $PYPI_USER $PYPI_PASS diff --git a/features/steps/core.py b/features/steps/core.py index 314a5167..5e8e3ea3 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -164,7 +164,7 @@ def has_error(context): @then('we should get no error') def no_error(context): - assert context.exit_status is 0, context.exit_status + assert context.exit_status == 0, context.exit_status @then('the output should be parsable as json') From 9cad7ddb5110b3debbd7f237335664ca0d2ebcee Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 28 Nov 2019 08:39:22 -0700 Subject: [PATCH 010/112] [Travis] Add Python to PATH on Windows --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 19fa4084..e8278da2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,9 +25,10 @@ jobs: language: shell # 'language: python' is an error on Travis CI Windows before_install: - choco install python --version 3.8.0 - - refreshenv # add Python to PATH - python -m pip install --upgrade pip - pip install poetry~=0.12.17 + env: + - PATH=/c/Python38:/c/Python38/Scripts:$PATH allow_failures: - python: 3.8 - python: nightly From 34f7d9830f3aff457336b686f174e2685585fc94 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 28 Nov 2019 08:50:06 -0700 Subject: [PATCH 011/112] [Travis] Windows: try Python 3.7 for cryptography wheels? --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e8278da2..5f020fc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,15 +20,16 @@ jobs: language: shell # 'language: python' is an error on Travis CI macOS before_install: - pip3 install poetry~=0.12.17 # 'pip' points to Python 2 on MacOS - - name: "Python 3.8.0 on Windows" + - name: "Python 3.7.5 on Windows" os: windows language: shell # 'language: python' is an error on Travis CI Windows before_install: - - choco install python --version 3.8.0 + - choco install python --version 3.7.5 - python -m pip install --upgrade pip + - pip --version - pip install poetry~=0.12.17 env: - - PATH=/c/Python38:/c/Python38/Scripts:$PATH + - PATH=/c/Python37:/c/Python37/Scripts:$PATH allow_failures: - python: 3.8 - python: nightly From ae505dcbbda63dc07891d116d447230265451248 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Thu, 28 Nov 2019 09:14:50 -0700 Subject: [PATCH 012/112] [Travis] Windows tests time out --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5f020fc1..297ce340 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,7 @@ jobs: allow_failures: - python: 3.8 - python: nightly + - os: windows git: depth: false cache: pip From 444d6de9e43ad3f4eb608fee6bb986e7f4043f01 Mon Sep 17 00:00:00 2001 From: Peter Schmidbauer Date: Sat, 2 Nov 2019 02:06:05 +0100 Subject: [PATCH 013/112] Move all password handling to EncryptedJournal This commit should greatly simplify all password handling logic. No passwords are stored in the config dict anymore. Only the Encrypted Journals have any password related logic. I also had to remove some password fields from the test files, which shows how dangerous the previous approach was. A slight code change could've leaked passwords to the config file. However, I had to change the install progress a little bit to make this work. It will now not ask you for a password right away but rather if you want to encrypt or not. Only if you reply 'y' will it ask you for the password later on. --- features/data/configs/bug153.yaml | 1 - features/data/configs/dayone.yaml | 1 - features/data/configs/empty_folder.yaml | 1 - features/data/configs/encrypted.yaml | 1 - .../data/configs/markdown-headings-335.yaml | 1 - features/data/configs/multiple.yaml | 1 - features/data/configs/tags-216.yaml | 1 - features/data/configs/tags-237.yaml | 1 - features/data/configs/tags.yaml | 1 - features/multiple_journals.feature | 2 +- jrnl/EncryptedJournal.py | 68 +++++++++---------- jrnl/Journal.py | 19 +++--- jrnl/cli.py | 25 +++---- jrnl/install.py | 26 +++---- jrnl/util.py | 27 ++++++-- 15 files changed, 79 insertions(+), 97 deletions(-) diff --git a/features/data/configs/bug153.yaml b/features/data/configs/bug153.yaml index 765185c3..5bbcbf4a 100644 --- a/features/data/configs/bug153.yaml +++ b/features/data/configs/bug153.yaml @@ -6,7 +6,6 @@ highlight: true journals: default: features/journals/bug153.dayone linewrap: 80 -password: '' tagsymbols: '@' template: false timeformat: '%Y-%m-%d %H:%M' diff --git a/features/data/configs/dayone.yaml b/features/data/configs/dayone.yaml index e7331733..8807e66b 100644 --- a/features/data/configs/dayone.yaml +++ b/features/data/configs/dayone.yaml @@ -7,7 +7,6 @@ highlight: true journals: default: features/journals/dayone.dayone linewrap: 80 -password: '' tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' indent_character: "|" diff --git a/features/data/configs/empty_folder.yaml b/features/data/configs/empty_folder.yaml index ee401ada..52a21854 100644 --- a/features/data/configs/empty_folder.yaml +++ b/features/data/configs/empty_folder.yaml @@ -7,7 +7,6 @@ highlight: true journals: default: features/journals/empty_folder linewrap: 80 -password: '' tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' indent_character: "|" diff --git a/features/data/configs/encrypted.yaml b/features/data/configs/encrypted.yaml index 254b81d9..4d50b607 100644 --- a/features/data/configs/encrypted.yaml +++ b/features/data/configs/encrypted.yaml @@ -7,7 +7,6 @@ highlight: true journals: default: features/journals/encrypted.journal linewrap: 80 -password: '' tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' indent_character: "|" diff --git a/features/data/configs/markdown-headings-335.yaml b/features/data/configs/markdown-headings-335.yaml index dafed64b..5c33c53e 100644 --- a/features/data/configs/markdown-headings-335.yaml +++ b/features/data/configs/markdown-headings-335.yaml @@ -7,7 +7,6 @@ template: false journals: default: features/journals/markdown-headings-335.journal linewrap: 80 -password: '' tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' indent_character: "|" diff --git a/features/data/configs/multiple.yaml b/features/data/configs/multiple.yaml index 2e282232..65f2c256 100644 --- a/features/data/configs/multiple.yaml +++ b/features/data/configs/multiple.yaml @@ -13,7 +13,6 @@ journals: encrypt: true journal: features/journals/new_encrypted.journal linewrap: 80 -password: '' tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' indent_character: "|" diff --git a/features/data/configs/tags-216.yaml b/features/data/configs/tags-216.yaml index 73523c50..b71abea5 100644 --- a/features/data/configs/tags-216.yaml +++ b/features/data/configs/tags-216.yaml @@ -7,7 +7,6 @@ template: false journals: default: features/journals/tags-216.journal linewrap: 80 -password: '' tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' indent_character: "|" diff --git a/features/data/configs/tags-237.yaml b/features/data/configs/tags-237.yaml index 2b360f6f..a5a70d99 100644 --- a/features/data/configs/tags-237.yaml +++ b/features/data/configs/tags-237.yaml @@ -7,7 +7,6 @@ template: false journals: default: features/journals/tags-237.journal linewrap: 80 -password: '' tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' indent_character: "|" diff --git a/features/data/configs/tags.yaml b/features/data/configs/tags.yaml index 51a2b8b2..28ef5f69 100644 --- a/features/data/configs/tags.yaml +++ b/features/data/configs/tags.yaml @@ -7,7 +7,6 @@ template: false journals: default: features/journals/tags.journal linewrap: 80 -password: '' tagsymbols: '@' timeformat: '%Y-%m-%d %H:%M' indent_character: "|" diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature index 56c4b3a7..0f41b0d9 100644 --- a/features/multiple_journals.feature +++ b/features/multiple_journals.feature @@ -48,4 +48,4 @@ Feature: Multiple journals these three eyes n """ - Then we should see the message "Journal 'new_encrypted' created" + Then we should see the message "Encrypted journal 'new_encrypted' created" diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index f3ff63c6..c7a66838 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -1,4 +1,5 @@ -from . import Journal, util +from . import util +from .Journal import Journal, LegacyJournal from cryptography.fernet import Fernet, InvalidToken from cryptography.hazmat.primitives import hashes, padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -9,6 +10,8 @@ import sys import os import base64 import logging +from typing import Optional + log = logging.getLogger() @@ -27,10 +30,11 @@ def make_key(password): return base64.urlsafe_b64encode(key) -class EncryptedJournal(Journal.Journal): +class EncryptedJournal(Journal): def __init__(self, name='default', **kwargs): super().__init__(name, **kwargs) self.config['encrypt'] = True + self.password = None def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. @@ -38,27 +42,17 @@ class EncryptedJournal(Journal.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): - util.set_keychain(self.name, password) - else: - util.set_keychain(self.name, None) - self.config['password'] = password - text = "" - self._store(filename, text) - print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) - else: - print("No password supplied for encrypted journal", file=sys.stderr) - sys.exit(1) - else: - text = self._load(filename) + self.create_file(filename) + self.password = util.create_password(self.name) + print(f"Encrypted journal '{self.name}' created at {filename}", file=sys.stderr) + + text = self._load(filename) self.entries = self._parse(text) self.sort() log.debug("opened %s with %d entries", self.__class__.__name__, len(self)) return self - def _load(self, filename, password=None): + def _load(self, filename): """Loads an encrypted journal from a file and tries to decrypt it. If password is not provided, will look for password in the keychain and otherwise ask the user to enter a password up to three times. @@ -67,50 +61,52 @@ class EncryptedJournal(Journal.Journal): with open(filename, 'rb') as f: journal_encrypted = f.read() - def validate_password(password): + def decrypt_journal(password): key = make_key(password) try: plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8') - self.config['password'] = password + self.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) + + if self.password: + return decrypt_journal(self.password) + + return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal) def _store(self, filename, text): - key = make_key(self.config['password']) + key = make_key(self.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: - f.write(dummy) + def from_journal(cls, other: Journal): + new_journal = super().from_journal(other) + new_journal.password = other.password if hasattr(other, "password") else util.create_password(other.name) + return new_journal -class LegacyEncryptedJournal(Journal.LegacyJournal): +class LegacyEncryptedJournal(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().__init__(name, **kwargs) self.config['encrypt'] = True + self.password = None - def _load(self, filename, password=None): + def _load(self, filename): with open(filename, 'rb') as f: journal_encrypted = f.read() iv, cipher = journal_encrypted[:16], journal_encrypted[16:] - def validate_password(password): + def decrypt_journal(password): 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.password = password if plain_padded[-1] in (" ", 32): # Ancient versions of jrnl. Do not judge me. return plain_padded.decode('utf-8').rstrip(" ") @@ -120,6 +116,6 @@ class LegacyEncryptedJournal(Journal.LegacyJournal): 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) + if self.password: + return decrypt_journal(self.password) + return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 0fd41c22..8d6022a7 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -41,6 +41,7 @@ class Journal: # Set up date parser self.search_tags = None # Store tags we're highlighting self.name = name + self.entries = [] def __len__(self): """Returns the number of entries""" @@ -69,9 +70,9 @@ class Journal: filename = filename or self.config['journal'] if not os.path.exists(filename): + self.create_file(filename) print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) - self._create(filename) - + text = self._load(filename) self.entries = self._parse(text) self.sort() @@ -92,6 +93,11 @@ class Journal: return False return True + @staticmethod + def create_file(filename): + with open(filename, "w"): + pass + def _to_text(self): return "\n".join([str(e) for e in self.entries]) @@ -101,10 +107,6 @@ class Journal: def _store(self, filename, text): raise NotImplementedError - @classmethod - def _create(cls, filename): - raise NotImplementedError - def _parse(self, journal_txt): """Parses a journal that's stored in a string and returns a list of entries""" @@ -270,11 +272,6 @@ class Journal: class PlainJournal(Journal): - @classmethod - def _create(cls, filename): - with open(filename, "a", encoding="utf-8"): - pass - def _load(self, filename): with open(filename, "r", encoding="utf-8") as f: return f.read() diff --git a/jrnl/cli.py b/jrnl/cli.py index 945a728f..6c677fed 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -6,7 +6,8 @@ license: MIT, see LICENSE for more details. """ -from . import Journal +from .Journal import PlainJournal, open_journal +from .EncryptedJournal import EncryptedJournal from . import util from . import install from . import plugins @@ -76,28 +77,19 @@ def guess_mode(args, config): 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 - new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config) - new_journal.entries = journal.entries + new_journal = EncryptedJournal.from_journal(journal) 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']) - 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'] = "" - new_journal = Journal.PlainJournal(filename, **journal.config) - new_journal.entries = journal.entries + new_journal = PlainJournal.from_journal(journal) new_journal.write(filename) print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr) @@ -155,11 +147,12 @@ 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' - if journal_name != 'default': + journal_name = install.DEFAULT_JOURNAL_KEY + if args.text and args.text[0] in config['journals']: + journal_name = args.text[0] args.text = args.text[1:] - elif "default" not in config['journals']: + elif install.DEFAULT_JOURNAL_KEY not in config['journals']: print("No default journal configured.", file=sys.stderr) print(list_journals(config), file=sys.stderr) sys.exit(1) @@ -210,7 +203,7 @@ def run(manual_args=None): # This is where we finally open the journal! try: - journal = Journal.open_journal(journal_name, config) + journal = open_journal(journal_name, config) except KeyboardInterrupt: print(f"[Interrupted while opening journal]", file=sys.stderr) sys.exit(1) diff --git a/jrnl/install.py b/jrnl/install.py index 8eb73c76..f989e0a0 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -19,6 +19,7 @@ if "win32" not in sys.platform: DEFAULT_CONFIG_NAME = 'jrnl.yaml' DEFAULT_JOURNAL_NAME = 'journal.txt' +DEFAULT_JOURNAL_KEY = 'default' XDG_RESOURCE = 'jrnl' USER_HOME = os.path.expanduser('~') @@ -45,7 +46,7 @@ def module_exists(module_name): default_config = { 'version': __version__, 'journals': { - "default": JOURNAL_FILE_PATH + DEFAULT_JOURNAL_KEY: JOURNAL_FILE_PATH }, 'editor': os.getenv('VISUAL') or os.getenv('EDITOR') or "", 'encrypt': False, @@ -118,32 +119,23 @@ def install(): # Where to create the journal? 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_JOURNAL_KEY] = 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_JOURNAL_KEY])[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): ") - if password: + encrypt = util.yesno("Do you want to encrypt your journal? You can always change this later", default=False) + if encrypt: 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) print("Journal will be encrypted.", file=sys.stderr) - else: - PlainJournal._create(default_config['journals']['default']) - config = default_config - save_config(config) - if password: - config['password'] = password - return config + save_config(default_config) + return default_config + def autocomplete(text, state): expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + '*') diff --git a/jrnl/util.py b/jrnl/util.py index f70b2df9..07140ff5 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,8 +4,10 @@ import sys import os import getpass as gp import yaml + if "win32" in sys.platform: import colorama + colorama.init() import re import tempfile @@ -13,6 +15,7 @@ import subprocess import unicodedata import shlex import logging +from typing import Optional, Callable log = logging.getLogger(__name__) @@ -37,19 +40,29 @@ class UserAbort(Exception): pass -def create_password(): +def create_password(journal_name: str, prompt: str = "Enter password for new journal: ") -> str: while True: - pw = gp.getpass("Enter password for new journal: ") - if pw == gp.getpass("Enter password again: "): - return pw + pw = gp.getpass(prompt) + if not pw: + print("Password can't be an empty string!", file=sys.stderr) + continue + elif pw == gp.getpass("Enter password again: "): + break print("Passwords did not match, please try again", file=sys.stderr) + if yesno("Do you want to store the password in your keychain?", default=True): + set_keychain(journal_name, pw) + else: + set_keychain(journal_name, None) -def get_password(validator, keychain=None, max_attempts=3): + return pw + + +def decrypt_content(decrypt_func: Callable[[str], Optional[str]], keychain: str = None, max_attempts: int = 3) -> str: pwd_from_keychain = keychain and get_keychain(keychain) password = pwd_from_keychain or gp.getpass() - result = validator(password) + result = decrypt_func(password) # Password is bad: if result is None and pwd_from_keychain: set_keychain(keychain, None) @@ -57,7 +70,7 @@ def get_password(validator, keychain=None, max_attempts=3): while result is None and attempt < max_attempts: print("Wrong password, try again.", file=sys.stderr) password = gp.getpass() - result = validator(password) + result = decrypt_func(password) attempt += 1 if result is not None: return result From 19fb2207dd15242fe1a6dcb0451c00bec32d94fc Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 11:35:30 -0800 Subject: [PATCH 014/112] [#757] Move deploy to it's own stage on CI so it doesn't run multiple times --- .travis.yml | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 297ce340..b10cb9b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,25 @@ jobs: - pip install poetry~=0.12.17 env: - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - stage: "Deploy" + before_deploy: + - poetry config http-basic.pypi $PYPI_USER $PYPI_PASS + - poetry version $TRAVIS_TAG + - poetry build + deploy: + - provider: script + script: poetry publish + skip_cleanup: true + on: + branch: master + tags: true + after_deploy: + - git config --global user.email "jrnl.bot@gmail.com" + - git config --global user.name "Jrnl Bot" + - git checkout master + - git add pyproject.toml + - git commit -m "Incrementing version to ${TRAVIS_TAG}" + - git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master allow_failures: - python: 3.8 - python: nightly @@ -47,21 +66,4 @@ install: - poetry run python --version script: - poetry run behave -before_deploy: - - poetry config http-basic.pypi $PYPI_USER $PYPI_PASS - - poetry version $TRAVIS_TAG - - poetry build -deploy: - - provider: script - script: poetry publish - skip_cleanup: true - on: - branch: master - tags: true -after_deploy: - - git config --global user.email "jrnl.bot@gmail.com" - - git config --global user.name "Jrnl Bot" - - git checkout master - - git add pyproject.toml - - git commit -m "Incrementing version to ${TRAVIS_TAG}" - - git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master + From 44531bb47f7c53608809e6d12eb0726d4a649366 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 11:36:31 -0800 Subject: [PATCH 015/112] [#757] Take out old code (no longer needed) --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b10cb9b2..ad07de69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,8 +60,6 @@ before_install: - date - pip install poetry~=0.12.17 install: - # we run `poetry version` here to appease poetry about '0.0.0-source' - - poetry version - poetry install - poetry run python --version script: From 7fe1281c38e7b5706b8e9cc5768a6d23ca5c1d42 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 11:37:01 -0800 Subject: [PATCH 016/112] [#757] Add quotes around environment variables to be a little safer --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ad07de69..a948fbac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,8 +32,8 @@ jobs: - PATH=/c/Python37:/c/Python37/Scripts:$PATH - stage: "Deploy" before_deploy: - - poetry config http-basic.pypi $PYPI_USER $PYPI_PASS - - poetry version $TRAVIS_TAG + - poetry config http-basic.pypi "$PYPI_USER" "$PYPI_PASS" + - poetry version "$TRAVIS_TAG" - poetry build deploy: - provider: script From f64f7428394c7be7994fd54bd21ac94015dde8fb Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 11:37:51 -0800 Subject: [PATCH 017/112] [#757] add stage name to test stage to be explicit --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a948fbac..ececef6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,8 @@ dist: xenial # required for Python >= 3.7 language: python jobs: include: - - name: "Python 3.6 on Linux" + - stage: "Test" + name: "Python 3.6 on Linux" python: 3.6 - name: "Python 3.7 on Linux" python: 3.7 From 7f83a1ef263816a8f5ca1a40499a674c198def43 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 11:38:14 -0800 Subject: [PATCH 018/112] [#757] add explicit os at root per travis config linter --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ececef6e..c65ac960 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ dist: xenial # required for Python >= 3.7 +os: linux language: python jobs: include: From 427f1321b7bea1143f241b38dc68fdf3d2fda53b Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 12:11:17 -0800 Subject: [PATCH 019/112] [#757] update CI logic to new format --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c65ac960..2872d30c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,7 @@ jobs: env: - PATH=/c/Python37:/c/Python37/Scripts:$PATH - stage: "Deploy" + if: branch = master AND tag IS present before_deploy: - poetry config http-basic.pypi "$PYPI_USER" "$PYPI_PASS" - poetry version "$TRAVIS_TAG" @@ -40,10 +41,6 @@ jobs: deploy: - provider: script script: poetry publish - skip_cleanup: true - on: - branch: master - tags: true after_deploy: - git config --global user.email "jrnl.bot@gmail.com" - git config --global user.name "Jrnl Bot" From b1307674d879ca5a3314dfda754fe4e4eff67ff5 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 29 Nov 2019 12:51:25 -0800 Subject: [PATCH 020/112] Display header in mobile docs --- docs/assets/theme.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/assets/theme.css b/docs/assets/theme.css index a3de87a9..687b3347 100644 --- a/docs/assets/theme.css +++ b/docs/assets/theme.css @@ -45,9 +45,17 @@ p { /* No-one likes lines that are 400 characters long. */ div.rst-content {max-width: 54em;} -.wy-side-nav-search, .wy-nav-top, .wy-menu-vertical li.current { +.wy-side-nav-search, .wy-menu-vertical li.current { background-color: transparent; } +.wy-nav-top { + background-image: linear-gradient(-211deg, #95699C 0%, #604385 100%); +} + +.wy-nav-top .fa-bars { + line-height: 50px; +} + .wy-side-nav-search a.icon-home { width: 100%; max-width: 250px; From c6eab97f64e00bed82e0189f0c7049a68e3932cf Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 15:05:15 -0800 Subject: [PATCH 021/112] [#757] Take out unneeded quotes around strings in yaml --- .travis.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2872d30c..501cbd5c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,26 +3,26 @@ os: linux language: python jobs: include: - - stage: "Test" - name: "Python 3.6 on Linux" + - stage: Test + - name: Python 3.6 on Linux python: 3.6 - - name: "Python 3.7 on Linux" + - name: Python 3.7 on Linux python: 3.7 - - name: "Python 3.7 on Linux, not UTC" + - name: Python 3.7 on Linux, not UTC python: 3.7 env: - TZ=America/Edmonton - - name: "Python 3.8 on Linux" + - name: Python 3.8 on Linux python: 3.8 - - name: "Python dev on Linux" + - name: Python nightly on Linux python: nightly - - name: "Python 3.7.4 on MacOS" + - name: Python 3.7.4 on MacOS os: osx osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 language: shell # 'language: python' is an error on Travis CI macOS before_install: - pip3 install poetry~=0.12.17 # 'pip' points to Python 2 on MacOS - - name: "Python 3.7.5 on Windows" + - name: Python 3.7 on Windows os: windows language: shell # 'language: python' is an error on Travis CI Windows before_install: @@ -32,7 +32,7 @@ jobs: - pip install poetry~=0.12.17 env: - PATH=/c/Python37:/c/Python37/Scripts:$PATH - - stage: "Deploy" + - stage: Deploy if: branch = master AND tag IS present before_deploy: - poetry config http-basic.pypi "$PYPI_USER" "$PYPI_PASS" From a01fb67c87e11f942adacff036be73d39ecca587 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 15:06:13 -0800 Subject: [PATCH 022/112] [#757] Clean up travis yaml file for readability --- .travis.yml | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 501cbd5c..22b69409 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,28 @@ dist: xenial # required for Python >= 3.7 os: linux language: python +cache: pip + +git: + depth: false + +before_install: + - date + - pip install poetry~=0.12.17 + +install: + - poetry install + - poetry run python --version + +script: + - poetry run behave + jobs: + allow_failures: + - python: 3.8 + - python: nightly + - os: windows + include: - stage: Test - name: Python 3.6 on Linux @@ -32,6 +53,7 @@ jobs: - pip install poetry~=0.12.17 env: - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - stage: Deploy if: branch = master AND tag IS present before_deploy: @@ -48,19 +70,4 @@ jobs: - git add pyproject.toml - git commit -m "Incrementing version to ${TRAVIS_TAG}" - git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master - allow_failures: - - python: 3.8 - - python: nightly - - os: windows -git: - depth: false -cache: pip -before_install: - - date - - pip install poetry~=0.12.17 -install: - - poetry install - - poetry run python --version -script: - - poetry run behave From 7b27e6d0c08ab4f575329ded79cb2d2799a83509 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 15:07:44 -0800 Subject: [PATCH 023/112] [#757] Add more testing for Windows across multiple Python versions --- .travis.yml | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 22b69409..67b3cf2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,15 +8,24 @@ git: before_install: - date - - pip install poetry~=0.12.17 install: + - pip install poetry~=0.12.17 - poetry install - poetry run python --version script: - poetry run behave +aliases: + test_windows: &test_windows + os: windows + language: shell + before_install: + - choco install python --version $JRNL_PYTHON_VERSION + - python -m pip install --upgrade pip + - pip --version + jobs: allow_failures: - python: 3.8 @@ -41,18 +50,21 @@ jobs: os: osx osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 language: shell # 'language: python' is an error on Travis CI macOS - before_install: - - pip3 install poetry~=0.12.17 # 'pip' points to Python 2 on MacOS - - name: Python 3.7 on Windows - os: windows - language: shell # 'language: python' is an error on Travis CI Windows - before_install: - - choco install python --version 3.7.5 - - python -m pip install --upgrade pip - - pip --version - - pip install poetry~=0.12.17 + - <<: *test_windows + name: Python 3.6 on Windows env: + - JRNL_PYTHON_VERSION=3.6.8 + - PATH=/c/Python36:/c/Python36/Scripts:$PATH + - <<: *test_windows + name: Python 3.7 on Windows + env: + - JRNL_PYTHON_VERSION=3.7.5 - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - <<: *test_windows + name: Python 3.8 on Windows + env: + - JRNL_PYTHON_VERSION=3.8.0 + - PATH=/c/Python38:/c/Python38/Scripts:$PATH - stage: Deploy if: branch = master AND tag IS present From cbd0014a52bd72cb4811fb05462049495fc68188 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 15:38:32 -0800 Subject: [PATCH 024/112] [#757] Add more Mac tests for each Python version --- .travis.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 67b3cf2e..76214d83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,16 @@ script: - poetry run behave aliases: + test_mac: &test_mac + os: osx + language: shell + osx_image: xcode11.2 + before_install: + - eval "$(pyenv init -)" + - pyenv install -s $JRNL_PYTHON_VERSION + - pyenv global $JRNL_PYTHON_VERSION + - pip install --upgrade pip + - pip --version test_windows: &test_windows os: windows language: shell @@ -46,10 +56,20 @@ jobs: python: 3.8 - name: Python nightly on Linux python: nightly - - name: Python 3.7.4 on MacOS - os: osx - osx_image: xcode11.2 # Python 3.7.4 running on macOS 10.14.4 - language: shell # 'language: python' is an error on Travis CI macOS + + - <<: *test_mac + name: Python 3.6 on MacOS + env: + - JRNL_PYTHON_VERSION=3.6.8 + - <<: *test_mac + name: Python 3.7 on MacOS + env: + - JRNL_PYTHON_VERSION=3.7.4 + - <<: *test_mac + name: Python 3.8 on MacOS + env: + - JRNL_PYTHON_VERSION=3.8.0 + - <<: *test_windows name: Python 3.6 on Windows env: From f16e7860e043fff5b73036b1c16cf9e1b254e7ed Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 16:21:03 -0800 Subject: [PATCH 025/112] [#757] Group tests by python version --- .travis.yml | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 76214d83..c80e74f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,49 +43,55 @@ jobs: - os: windows include: - - stage: Test + # Python 3.6 Tests - name: Python 3.6 on Linux python: 3.6 - - name: Python 3.7 on Linux - python: 3.7 - - name: Python 3.7 on Linux, not UTC - python: 3.7 - env: - - TZ=America/Edmonton - - name: Python 3.8 on Linux - python: 3.8 - - name: Python nightly on Linux - python: nightly - - <<: *test_mac name: Python 3.6 on MacOS env: - JRNL_PYTHON_VERSION=3.6.8 - - <<: *test_mac - name: Python 3.7 on MacOS - env: - - JRNL_PYTHON_VERSION=3.7.4 - - <<: *test_mac - name: Python 3.8 on MacOS - env: - - JRNL_PYTHON_VERSION=3.8.0 - - <<: *test_windows name: Python 3.6 on Windows env: - JRNL_PYTHON_VERSION=3.6.8 - PATH=/c/Python36:/c/Python36/Scripts:$PATH + + # Python 3.7 Tests + - name: Python 3.7 on Linux + python: 3.7 + - <<: *test_mac + name: Python 3.7 on MacOS + env: + - JRNL_PYTHON_VERSION=3.7.4 - <<: *test_windows name: Python 3.7 on Windows env: - JRNL_PYTHON_VERSION=3.7.5 - PATH=/c/Python37:/c/Python37/Scripts:$PATH + + # Python 3.8 Tests + - name: Python 3.8 on Linux + python: 3.8 + - <<: *test_mac + name: Python 3.8 on MacOS + env: + - JRNL_PYTHON_VERSION=3.8.0 - <<: *test_windows name: Python 3.8 on Windows env: - JRNL_PYTHON_VERSION=3.8.0 - PATH=/c/Python38:/c/Python38/Scripts:$PATH + # ... and beyond! + - name: Python nightly on Linux + python: nightly + + # Specialty tests + - name: Python 3.7 on Linux, not UTC + python: 3.7 + env: + - TZ=America/Edmonton + - stage: Deploy if: branch = master AND tag IS present before_deploy: From 174632f8e38ba98b54ffc0c080b580d9976c094a Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 16:23:52 -0800 Subject: [PATCH 026/112] [#757] Be explicit about python versio so travis doesn't get confused --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index c80e74f2..9b010ffc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,10 +48,12 @@ jobs: python: 3.6 - <<: *test_mac name: Python 3.6 on MacOS + python: 3.6 env: - JRNL_PYTHON_VERSION=3.6.8 - <<: *test_windows name: Python 3.6 on Windows + python: 3.6 env: - JRNL_PYTHON_VERSION=3.6.8 - PATH=/c/Python36:/c/Python36/Scripts:$PATH @@ -61,10 +63,12 @@ jobs: python: 3.7 - <<: *test_mac name: Python 3.7 on MacOS + python: 3.7 env: - JRNL_PYTHON_VERSION=3.7.4 - <<: *test_windows name: Python 3.7 on Windows + python: 3.7 env: - JRNL_PYTHON_VERSION=3.7.5 - PATH=/c/Python37:/c/Python37/Scripts:$PATH @@ -74,10 +78,12 @@ jobs: python: 3.8 - <<: *test_mac name: Python 3.8 on MacOS + python: 3.8 env: - JRNL_PYTHON_VERSION=3.8.0 - <<: *test_windows name: Python 3.8 on Windows + python: 3.8 env: - JRNL_PYTHON_VERSION=3.8.0 - PATH=/c/Python38:/c/Python38/Scripts:$PATH From eb6c5200dedcc4a2fe9c1e6a5396b4bd4ff471a4 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 22:02:57 -0800 Subject: [PATCH 027/112] [#757] update caching for ci --- .travis.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9b010ffc..7b4786fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ dist: xenial # required for Python >= 3.7 os: linux language: python -cache: pip + +cache: + - pip git: depth: false @@ -22,6 +24,9 @@ aliases: os: osx language: shell osx_image: xcode11.2 + cache: + directories: + - $HOME/.pyenv/versions before_install: - eval "$(pyenv init -)" - pyenv install -s $JRNL_PYTHON_VERSION @@ -31,6 +36,11 @@ aliases: test_windows: &test_windows os: windows language: shell + cache: + directories: + - /c/Python36 + - /c/Python37 + - /c/Python38 before_install: - choco install python --version $JRNL_PYTHON_VERSION - python -m pip install --upgrade pip From e424e32a37f89cd72327a73bd4173a50a14450c4 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 22:03:40 -0800 Subject: [PATCH 028/112] [#757] Add fast finish option to travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7b4786fb..949391bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,6 +47,7 @@ aliases: - pip --version jobs: + fast_finish: true allow_failures: - python: 3.8 - python: nightly From a5b5f4732a0c967ed4a7ec5ef664a293700cd181 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Fri, 29 Nov 2019 22:04:02 -0800 Subject: [PATCH 029/112] [#757] update mac python version test to match others --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 949391bd..140349c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -76,7 +76,7 @@ jobs: name: Python 3.7 on MacOS python: 3.7 env: - - JRNL_PYTHON_VERSION=3.7.4 + - JRNL_PYTHON_VERSION=3.7.5 - <<: *test_windows name: Python 3.7 on Windows python: 3.7 From 7200b1fa778e0f83ebc584e13edd336a3e61b4a3 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 30 Nov 2019 13:35:59 -0800 Subject: [PATCH 030/112] [#739][#747] update lock file --- poetry.lock | 132 +++++++++++++++++++++++++++------------------------- 1 file changed, 68 insertions(+), 64 deletions(-) diff --git a/poetry.lock b/poetry.lock index ea35de41..8a698d84 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,25 +6,13 @@ optional = false python-versions = "*" version = "1.4.3" -[[package]] -category = "main" -description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" -name = "asn1crypto" -optional = false -python-versions = "*" -version = "0.24.0" - [[package]] category = "main" description = "Safe, minimalistic evaluator of python expression using ast module" name = "asteval" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.9.14" - -[package.dependencies] -numpy = "*" -six = "*" +version = "0.9.17" [[package]] category = "dev" @@ -32,7 +20,7 @@ description = "Classes Without Boilerplate" name = "attrs" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.1.0" +version = "19.3.0" [[package]] category = "dev" @@ -67,7 +55,7 @@ description = "Foreign Function Interface for Python calling C code." name = "cffi" optional = false python-versions = "*" -version = "1.12.3" +version = "1.13.2" [package.dependencies] pycparser = "*" @@ -95,15 +83,14 @@ description = "cryptography is a package which provides cryptographic recipes an name = "cryptography" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "2.7" +version = "2.8" [package.dependencies] -asn1crypto = ">=0.21.0" cffi = ">=1.8,<1.11.3 || >1.11.3" six = ">=1.4.1" [[package]] -category = "main" +category = "dev" description = "Discover and load entry points from installed packages." name = "entrypoints" optional = false @@ -116,7 +103,7 @@ description = "the modular source code checker: pep8, pyflakes and co" name = "flake8" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.7.8" +version = "3.7.9" [package.dependencies] entrypoints = ">=0.3.0,<0.4.0" @@ -126,11 +113,15 @@ pyflakes = ">=2.1.0,<2.2.0" [[package]] category = "main" -description = "Clean single-source support for Python 3 and 2" -name = "future" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.17.1" +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" +version = "1.0.0" + +[package.dependencies] +zipp = ">=0.5" [[package]] category = "main" @@ -139,15 +130,15 @@ marker = "sys_platform == \"linux\"" name = "jeepney" optional = false python-versions = ">=3.5" -version = "0.4" +version = "0.4.1" [[package]] category = "dev" -description = "A small but fast and easy to use stand-alone template engine written in pure python." +description = "A very fast and expressive template engine." name = "jinja2" optional = false python-versions = "*" -version = "2.10.1" +version = "2.10.3" [package.dependencies] MarkupSafe = ">=0.23" @@ -158,13 +149,16 @@ description = "Store and access your passwords safely." name = "keyring" optional = false python-versions = ">=3.5" -version = "19.0.2" +version = "19.3.0" [package.dependencies] -entrypoints = "*" pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1" secretstorage = "*" +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + [[package]] category = "dev" description = "Python LiveReload is an awesome tool for web developers" @@ -222,11 +216,12 @@ tornado = ">=5.0" [[package]] category = "main" -description = "NumPy is the fundamental package for array computing with Python." -name = "numpy" +description = "More routines for operating on iterables, beyond itertools" +marker = "python_version < \"3.8\"" +name = "more-itertools" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.16.4" +python-versions = ">=3.5" +version = "8.0.0" [[package]] category = "dev" @@ -234,7 +229,7 @@ description = "parse() is the opposite of format()" name = "parse" optional = false python-versions = "*" -version = "1.12.0" +version = "1.12.1" [[package]] category = "dev" @@ -242,10 +237,10 @@ description = "Simplifies to build parse types based on the parse module" name = "parse-type" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "0.4.2" +version = "0.5.2" [package.dependencies] -parse = ">=1.8" +parse = ">=1.8.4" six = ">=1.11" [[package]] @@ -254,10 +249,7 @@ description = "Parse human-readable date/time text." name = "parsedatetime" optional = false python-versions = "*" -version = "2.4" - -[package.dependencies] -future = "*" +version = "2.5" [[package]] category = "main" @@ -265,7 +257,7 @@ description = "comprehensive password hashing framework supporting over 30 schem name = "passlib" optional = false python-versions = "*" -version = "1.7.1" +version = "1.7.2" [[package]] category = "dev" @@ -296,8 +288,8 @@ category = "main" description = "Extensions to the standard Python datetime module" name = "python-dateutil" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.8.0" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" [package.dependencies] six = ">=1.5" @@ -308,7 +300,7 @@ description = "World timezone definitions, modern and historical" name = "pytz" optional = false python-versions = "*" -version = "2019.1" +version = "2019.3" [[package]] category = "main" @@ -354,7 +346,7 @@ description = "Python 2 and 3 compatibility utilities" name = "six" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.12.0" +version = "1.13.0" [[package]] category = "dev" @@ -383,47 +375,59 @@ version = "1.5.1" [package.dependencies] pytz = "*" +[[package]] +category = "main" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "0.6.0" + +[package.dependencies] +more-itertools = "*" + [metadata] -content-hash = "9896cf59c7552b6ad95219ee5555c7445a3fab39c2e4f4c6f3d991a36635e44b" -python-versions = ">=3.6.0, <3.8.0" +content-hash = "f84f27e8b11b6f6a749ae558b693d013c09c086f64a122a1f239eb206370219b" +python-versions = ">=3.6.0, <3.9.0" [metadata.hashes] appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] -asn1crypto = ["2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", "9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"] -asteval = ["7c81fee6707a7a28e8beae891b858535a7e61f9ce275a0a4cf5f428fbc934cb8"] -attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] +asteval = ["3e291b2cb71284bc55816fe7ff65ee15e47fa66df98490cdd95f5531fc37b81e"] +attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] behave = ["b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", "ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"] black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"] -cffi = ["041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", "046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", "066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", "066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", "2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", "300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", "34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", "46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", "4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", "4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", "4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", "50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", "55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", "5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", "59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", "73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", "a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", "a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", "a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", "a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", "ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", "b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", "d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", "d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", "dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", "e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", "e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", "ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"] +cffi = ["0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", "0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", "135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", "19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", "2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", "291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", "2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", "2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", "32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", "3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", "415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", "42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", "4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", "4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", "599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", "5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", "5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", "62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", "6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", "6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", "71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", "74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", "7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", "7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", "7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", "8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", "aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", "ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", "d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", "d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", "dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", "e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", "fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"] click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] -cryptography = ["24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", "25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", "3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", "41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", "5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", "5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", "72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", "7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", "961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", "96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", "ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", "b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", "cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", "e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", "f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", "f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d"] +cryptography = ["02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", "1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", "369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", "3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", "44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", "4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", "58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", "6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", "7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", "73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", "7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", "90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", "971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", "a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", "b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", "b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", "d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", "de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", "df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", "ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", "fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"] entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] -flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] -future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"] -jeepney = ["6089412a5de162c04747f0220f6b2223b8ba660acd041e52a76426ca550e3c70", "f6f8b1428403b4afad04b6b82f9ab9fc426c253d7504c9031c41712a2c01dc74"] -jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"] -keyring = ["1b74595f7439e4581a11d4f9a12790ac34addce64ca389c86272ff465f5e0b90", "afbfe7bc9bdba69d25c551b0c738adde533d87e0b51ad6bbe332cbea19ad8476"] +flake8 = ["45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", "49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"] +importlib-metadata = ["a82ca8c109e194d7d6aee3f7531b0470dd4dd6b36ec14fd55087142a96bd55a7", "f4a7ba72e93bc97ff491b66d69063819ae2b75238bb653cd4c95e3f2847ce76e"] +jeepney = ["13806f91a96e9b2623fd2a81b950d763ee471454aafd9eb6d75dbe7afce428fb", "f6a3f93464a0cf052f4e87da3c8b3ed1e27696758fb9739c63d3a74d9a1b6774"] +jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"] +keyring = ["9b80469783d3f6106bce1d389c6b8b20c8d4d739943b1b8cd0ddc2a45d065f9d", "ee3d35b7f1ac3cb69e9a1e4323534649d3ab2fea402738a77e4250c152970fed"] livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"] markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", "56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"] markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"] -numpy = ["0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", "141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", "14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", "27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", "2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", "3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", "52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", "6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", "7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", "7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", "94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", "a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", "ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", "b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", "b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", "cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", "d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", "dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", "dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", "e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", "ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", "f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", "f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7"] -parse = ["1b68657434d371e5156048ca4a0c5aea5afc6ca59a2fea4dd1a575354f617142"] -parse-type = ["6e906a66f340252e4c324914a60d417d33a4bea01292ea9bbf68b4fc123be8c9", "f596bdc75d3dd93036fbfe3d04127da9f6df0c26c36e01e76da85adef4336b3c"] -parsedatetime = ["3d817c58fb9570d1eec1dd46fa9448cd644eeed4fb612684b02dfda3a79cb84b", "9ee3529454bf35c40a77115f5a596771e59e1aee8c53306f346c461b8e913094"] -passlib = ["3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0", "43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"] +more-itertools = ["53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", "a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"] +parse = ["a5fca7000c6588d77bc65c28f3f21bfce03b5e44daa8f9f07c17fe364990d717"] +parse-type = ["089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e", "7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"] +parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"] +passlib = ["68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177", "8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"] pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] -python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] -pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"] +python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"] +pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"] pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"] pyxdg = ["1948ff8e2db02156c0cccd2529b43c0cff56ebaa71f5f021bbd755bc1419190e", "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06"] pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"] secretstorage = ["20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92", "7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a"] -six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] +six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"] toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"] tzlocal = ["4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"] +zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"] From 3717c26559c56a904df94f2793a4462a930b4885 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 30 Nov 2019 13:36:50 -0800 Subject: [PATCH 031/112] [#739] Update tests so that 3.8 is not an allowed failure --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 140349c3..17251ba9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,6 @@ aliases: jobs: fast_finish: true allow_failures: - - python: 3.8 - python: nightly - os: windows From 45384f3c8f9e5522622383d305c347c8681d6e90 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 1 Dec 2019 19:41:16 -0800 Subject: [PATCH 032/112] Use __version__.py instead of VERSION.txt --- .gitignore | 2 +- .travis.yml | 2 +- jrnl/__init__.py | 11 ++++------- pyproject.toml | 1 - 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index b14464d0..a06808df 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,4 @@ exp/ _extras/ *.sublime-* site/ -jrnl/VERSION.txt +jrnl/__version__.py diff --git a/.travis.yml b/.travis.yml index 3eda0c9a..215ef339 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ script: before_deploy: - pip install poetry - poetry config http-basic.pypi $PYPI_USER $PYPI_PASS - - echo $TRAVIS_TAG > jrnl/VERSION.txt + - echo __version__ = \"$TRAVIS_TAG\" > jrnl/__version__.py - poetry version $TRAVIS_TAG - poetry build deploy: diff --git a/jrnl/__init__.py b/jrnl/__init__.py index f1cdc3c4..f0f0af6d 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -2,11 +2,8 @@ # encoding: utf-8 import os - +try: + from .__version__ import __version__ +except ImportError: + __version__ = "source" __title__ = "jrnl" -__version__ = "source" - -version_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION.txt") -if os.path.exists(version_path): - with open(version_path) as version_file: - __version__ = version_file.read() diff --git a/pyproject.toml b/pyproject.toml index df18f9da..1b84acde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ license = "MIT" readme = "README.md" homepage = "https://jrnl.sh" repository = "https://github.com/jrnl-org/jrnl" -include = ["VERSION.txt"] [tool.poetry.dependencies] python = "^3.7" From 9b6b788af459fa826b70a66bf750f2228784e32b Mon Sep 17 00:00:00 2001 From: Jims Date: Sun, 1 Dec 2019 23:03:38 -0500 Subject: [PATCH 033/112] Renamed searching to contains. Made changes as per pull-request: https://github.com/jrnl-org/jrnl/pull/740 --- features/contains.feature | 29 +++++++++++++++++++++++++++++ features/searching.feature | 11 ----------- jrnl/Journal.py | 6 ++++-- jrnl/cli.py | 6 +++--- 4 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 features/contains.feature delete mode 100644 features/searching.feature diff --git a/features/contains.feature b/features/contains.feature new file mode 100644 index 00000000..5813be20 --- /dev/null +++ b/features/contains.feature @@ -0,0 +1,29 @@ +Feature: Contains + + Scenario: Searching for a string + Given we use the config "basic.yaml" + When we run "jrnl -contains life" + Then we should get no error + and the output should be + """ + 2013-06-10 15:40 Life is good. + | But I'm better. + """ + + Scenario: Searching for a string within tag results + Given we use the config "tags.yaml" + When we run "jrnl @idea -contains software" + Then we should get no error + and the output should contain "software" + + Scenario: Searching for a string within AND tag results + Given we use the config "tags.yaml" + When we run "jrnl -and @journal @idea -contains software" + Then we should get no error + and the output should contain "software" + + Scenario: Searching for a string within NOT tag results + Given we use the config "tags.yaml" + When we run "jrnl -not @dan -contains software" + Then we should get no error + and the output should contain "software" diff --git a/features/searching.feature b/features/searching.feature deleted file mode 100644 index 52e1ba54..00000000 --- a/features/searching.feature +++ /dev/null @@ -1,11 +0,0 @@ -Feature: Searching - - Scenario: Searching for a string - Given we use the config "basic.yaml" - When we run "jrnl -S life" - Then we should get no error - and the output should be - """ - 2013-06-10 15:40 Life is good. - | But I'm better. - """ diff --git a/jrnl/Journal.py b/jrnl/Journal.py index ab52cd00..7360b385 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -187,7 +187,7 @@ class Journal: 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, search_plain=None, exclude=[]): + def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, contains=None, 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 @@ -209,6 +209,8 @@ class Journal: # If strict mode is on, all tags have to be present in entry 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 + if contains: + contains_lower = contains.casefold() result = [ entry for entry in self.entries @@ -217,7 +219,7 @@ class Journal: and (not start_date or entry.date >= start_date) and (not end_date or entry.date <= end_date) and (not exclude or not excluded(entry.tags)) - and (not search_plain or (search_plain.lower() in entry.title.lower() or search_plain.lower() in entry.body.lower())) + and (not contains or (contains_lower in entry.title.casefold() or contains_lower in entry.body.casefold())) ] self.entries = result diff --git a/jrnl/cli.py b/jrnl/cli.py index ea8de891..b1454679 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -32,7 +32,7 @@ def parse_args(args=None): 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('-S', '-search', dest='search_plain', help='View entries containing a specific string') + reading.add_argument('-contains', dest='contains', help='View entries containing a specific string') 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') @@ -65,7 +65,7 @@ def guess_mode(args, config): 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, args.search_plain)): + elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred, args.contains)): # Any sign of displaying stuff? compose = False elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()): @@ -238,7 +238,7 @@ def run(manual_args=None): short=args.short, starred=args.starred, exclude=args.excluded, - search_plain=args.search_plain) + contains=args.contains) journal.limit(args.limit) # Reading mode From 8407ec5fb007117884f9bd3fb9653634d65defd0 Mon Sep 17 00:00:00 2001 From: Jesper Kjeldgaard Date: Tue, 3 Dec 2019 11:59:05 +0100 Subject: [PATCH 034/112] Fix merge conflict left-over --- docs/recipes.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 04604e6d..08e51711 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -130,8 +130,6 @@ Similar to Sublime Text, MacVim must be started with a flag that tells the the process to wait until the file is closed before passing control back to journal. In the case of MacVim, this is `-f`: -<<<<<<< HEAD - ```yaml editor: "mvim -f" ``` From d8f422151d8d4fb068b6fcf365182b16da93fefc Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Sat, 7 Dec 2019 12:32:55 -0700 Subject: [PATCH 035/112] Apply black formatter to code --- features/steps/core.py | 99 +++++----- jrnl/DayOneJournal.py | 64 +++++-- jrnl/EncryptedJournal.py | 51 ++--- jrnl/Entry.py | 70 ++++--- jrnl/Journal.py | 106 ++++++----- jrnl/__init__.py | 3 +- jrnl/cli.py | 301 +++++++++++++++++++++++------- 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, 839 insertions(+), 435 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 57b958f8..641a272e 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -5,8 +5,11 @@ from jrnl import cli, install, Journal, util, plugins 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 shlex 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 @@ keyring.set_keyring(TestKeyring()) 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 open_editor_and_enter(context, text=""): 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 _mock_input(inputs): 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 59314c4b..83eb6788 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 @@ class DayOne(Journal.Journal): 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 @@ class DayOne(Journal.Journal): """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 @@ class DayOne(Journal.Journal): 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 f3ff63c6..73d94269 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 @@ class EncryptedJournal(Journal.Journal): 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 ca80b231..80754e05 100755 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -49,72 +49,84 @@ class Entry: @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 0fd41c22..01a7bf4e 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -25,17 +25,17 @@ class Tag: 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 @@ class Journal: 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 @@ class Journal: 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 @@ class Journal: 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 @@ class Journal: """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 @@ class 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 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 @@ class Journal: 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 @@ class Journal: """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 @@ class Journal: 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 @@ class PlainJournal(Journal): 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 @@ class LegacyJournal(Journal): """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 @@ class LegacyJournal(Journal): 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 1905b195..5a7b8568 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 945a728f..1946cbf5 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -22,32 +22,152 @@ logging.getLogger("keyring.backend").setLevel(logging.ERROR) 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') + 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="*") + 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") + 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") + 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 1ee4e6ff..e95d4c12 100644 --- a/jrnl/export.py +++ b/jrnl/export.py @@ -8,6 +8,7 @@ import os 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 @@ class Exporter: 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 @@ class Exporter: 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 @@ class Exporter: 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 8eb73c76..fb877745 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -13,15 +13,16 @@ from .util import UserAbort 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 53b595e3..00ee2498 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -11,8 +11,16 @@ from .yaml_exporter import YAMLExporter 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 @@ __importer_types = {name: plugin for plugin in __importers for name in plugin.na 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 74d4555f..f7ff491d 100644 --- a/jrnl/plugins/fancy_exporter.py +++ b/jrnl/plugins/fancy_exporter.py @@ -8,46 +8,64 @@ from textwrap import TextWrapper 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 83341cd9..972114d4 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 @@ class JRNLImporter: 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 e368a300..90a2059f 100644 --- a/jrnl/plugins/json_exporter.py +++ b/jrnl/plugins/json_exporter.py @@ -8,20 +8,21 @@ from .util import get_tags_count 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 @@ class JSONExporter(TextExporter): 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 da2b5748..14060ce9 100644 --- a/jrnl/plugins/markdown_exporter.py +++ b/jrnl/plugins/markdown_exporter.py @@ -10,24 +10,25 @@ from ..util import WARNING_COLOR, RESET_COLOR 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 @@ class MarkdownExporter(TextExporter): 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 f5453ced..89d54a1a 100644 --- a/jrnl/plugins/tag_exporter.py +++ b/jrnl/plugins/tag_exporter.py @@ -7,6 +7,7 @@ from .util import get_tags_count 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 @@ class TagExporter(TextExporter): 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 7f72e2f8..6370c373 100644 --- a/jrnl/plugins/template.py +++ b/jrnl/plugins/template.py @@ -7,7 +7,9 @@ EXPRESSION_RE = r"[\[\]():.a-zA-Z0-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 @@ class Template: 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 @@ class Template: 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 @@ class Template: 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 @@ class Template: 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 f15328f2..eb360f94 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 ce2e71de..6f2a4531 100644 --- a/jrnl/plugins/text_exporter.py +++ b/jrnl/plugins/text_exporter.py @@ -8,6 +8,7 @@ from ..util import ERROR_COLOR, RESET_COLOR class TextExporter: """This Exporter can convert entries and journals into text files.""" + names = ["text", "txt"] extension = "txt" @@ -33,7 +34,9 @@ class TextExporter: @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 @@ class TextExporter: 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 @@ class TextExporter: 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 a056b19a..a030f8d3 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 2783663b..07506cf2 100644 --- a/jrnl/plugins/xml_exporter.py +++ b/jrnl/plugins/xml_exporter.py @@ -8,6 +8,7 @@ from xml.dom import minidom 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 @@ class XMLExporter(JSONExporter): @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 @@ class XMLExporter(JSONExporter): """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 4a75667f..b0177b39 100644 --- a/jrnl/plugins/yaml_exporter.py +++ b/jrnl/plugins/yaml_exporter.py @@ -10,6 +10,7 @@ from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR 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 @@ class YAMLExporter(TextExporter): 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 66d3f4f8..5e91cd1b 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 @@ consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday 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 8f67aa34..ac29240b 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -11,9 +11,9 @@ import os 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 @@ you can enjoy all of the great new features that come with jrnl 2: 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 @@ older versions of jrnl anymore. 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 @@ older versions of jrnl anymore. 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 @@ older versions of jrnl anymore. 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 f70b2df9..6ae00c38 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,8 +4,10 @@ import sys 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 @@ RESET_COLOR = "\033[0m" # 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 @@ SENTENCE_SPLITTER = re.compile(r""" \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 036/112] [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 17251ba9..6454b1ab 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 037/112] [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 6454b1ab..635e6101 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 038/112] [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 80a86be9..a4858707 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 From 411a790f2969ce5b93abc15b8c3dd7c3e45b5f08 Mon Sep 17 00:00:00 2001 From: Eshan Date: Sun, 15 Dec 2019 00:04:42 -0500 Subject: [PATCH 039/112] Fix usage notes Notes were correctly indented. --- docs/usage.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 269bdce9..18b35d68 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -35,12 +35,12 @@ jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny. ``` !!! note -Most shell contains a certain number of reserved characters, such as `#` -and `*`. Unbalanced quotes, parenthesis, and so on will also get into -the way of your editing. -For writing longer entries, just enter `jrnl` -and hit `return`. Only then enter the text of your journal entry. -Alternatively, `use an external editor `). + Most shell contains a certain number of reserved characters, such as `#` + and `*`. Unbalanced quotes, parenthesis, and so on will also get into + the way of your editing. + For writing longer entries, just enter `jrnl` + and hit `return`. Only then enter the text of your journal entry. + Alternatively, `use an external editor `). You can also import an entry directly from a file @@ -76,9 +76,9 @@ The following options are equivalent: - `jrnl Best day of my life.*` !!! note -Just make sure that the asterisk sign is **not** surrounded by -whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the -reason being that the `*` sign has a special meaning on most shells). + Just make sure that the asterisk sign is **not** surrounded by + whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the + reason being that the `*` sign has a special meaning on most shells). ## Viewing @@ -127,9 +127,9 @@ You can change which symbols you'd like to use for tagging in the configuration. !!! note -`jrnl @pinkie @WorldDomination` will switch to viewing mode because -although **no** command line arguments are given, all the input strings -look like tags - _jrnl_ will assume you want to filter by tag. + `jrnl @pinkie @WorldDomination` will switch to viewing mode because + although **no** command line arguments are given, all the input strings + look like tags - _jrnl_ will assume you want to filter by tag. ## Editing older entries From 07a633ae51c42fe50543204bc4acbae3313842c0 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 21 Dec 2019 11:47:02 -0800 Subject: [PATCH 040/112] (#770) run black formatter on codebase for standardization --- features/steps/core.py | 103 ++++++----- jrnl/DayOneJournal.py | 64 +++++-- jrnl/EncryptedJournal.py | 47 +++-- jrnl/Entry.py | 66 ++++--- jrnl/Journal.py | 119 +++++++----- jrnl/__init__.py | 1 + jrnl/cli.py | 296 ++++++++++++++++++++++++------ jrnl/export.py | 13 +- jrnl/install.py | 94 ++++++---- 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 | 36 +++- 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 | 59 +++--- 24 files changed, 850 insertions(+), 427 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 57b958f8..77831086 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -5,8 +5,11 @@ from jrnl import cli, install, Journal, util, plugins 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 shlex 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 @@ keyring.set_keyring(TestKeyring()) 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 open_editor_and_enter(context, text=""): 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 _mock_input(inputs): val = next(inputs) print(prompt, val) return val + return prompt_return @@ -119,24 +127,28 @@ def run_with_input(context, command, inputs=""): text = iter([inputs]) args = ushlex(command)[1:] - with patch("builtins.input", side_effect=_mock_input(text)) as mock_input,\ - patch("getpass.getpass", side_effect=_mock_getpass(text)) as mock_getpass,\ + + # fmt: off + # see: https://github.com/psf/black/issues/557 + 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 +170,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 +222,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 +230,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 +250,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 +291,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 +299,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 +307,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 +316,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 59314c4b..83eb6788 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 @@ class DayOne(Journal.Journal): 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 @@ class DayOne(Journal.Journal): """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 @@ class DayOne(Journal.Journal): 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 c7a66838..d6681a47 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -22,29 +22,32 @@ 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): - def __init__(self, name='default', **kwargs): + def __init__(self, name="default", **kwargs): super().__init__(name, **kwargs) - self.config['encrypt'] = True + self.config["encrypt"] = True self.password = None 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): self.create_file(filename) self.password = util.create_password(self.name) - print(f"Encrypted journal '{self.name}' created at {filename}", file=sys.stderr) + print( + f"Encrypted journal '{self.name}' created at {filename}", + file=sys.stderr, + ) text = self._load(filename) self.entries = self._parse(text) @@ -58,13 +61,13 @@ class EncryptedJournal(Journal): 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 decrypt_journal(password): key = make_key(password) try: - plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8') + plain = Fernet(key).decrypt(journal_encrypted).decode("utf-8") self.password = password return plain except (InvalidToken, IndexError): @@ -77,45 +80,53 @@ class EncryptedJournal(Journal): def _store(self, filename, text): key = make_key(self.password) - journal = Fernet(key).encrypt(text.encode('utf-8')) - with open(filename, 'wb') as f: + journal = Fernet(key).encrypt(text.encode("utf-8")) + with open(filename, "wb") as f: f.write(journal) @classmethod def from_journal(cls, other: Journal): new_journal = super().from_journal(other) - new_journal.password = other.password if hasattr(other, "password") else util.create_password(other.name) + new_journal.password = ( + other.password + if hasattr(other, "password") + else util.create_password(other.name) + ) return new_journal class LegacyEncryptedJournal(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 self.password = None def _load(self, filename): - 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 decrypt_journal(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.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 self.password: return decrypt_journal(self.password) return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal) diff --git a/jrnl/Entry.py b/jrnl/Entry.py index ca80b231..09e3fd57 100755 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -49,50 +49,60 @@ class Entry: @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 @@ -104,17 +114,21 @@ class Entry: ) 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 808084b4..e5bf4ecc 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -25,17 +25,17 @@ class Tag: 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 @@ -57,22 +57,29 @@ class Journal: 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): self.create_file(filename) print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) - + text = self._load(filename) self.entries = self._parse(text) self.sort() @@ -81,7 +88,7 @@ class Journal: 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) @@ -129,7 +136,7 @@ class Journal: 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)) @@ -148,18 +155,16 @@ class Journal: """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 @@ -183,14 +188,22 @@ class 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 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, contains=None, exclude=[]): + def filter( + self, + tags=[], + start_date=None, + end_date=None, + starred=False, + strict=False, + short=False, + contains=None, + 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 @@ -213,16 +226,23 @@ class Journal: 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 if contains: - contains_lower = contains.casefold() + contains_lower = contains.casefold() 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) and (not end_date or entry.date <= end_date) and (not exclude or not excluded(entry.tags)) - and (not contains or (contains_lower in entry.title.casefold() or contains_lower in entry.body.casefold())) + and ( + not contains + or ( + contains_lower in entry.title.casefold() + or contains_lower in entry.body.casefold() + ) + ) ] self.entries = result @@ -231,11 +251,11 @@ class Journal: """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: @@ -243,12 +263,12 @@ class Journal: 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") @@ -281,7 +301,7 @@ class PlainJournal(Journal): 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) @@ -289,6 +309,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() @@ -297,17 +318,19 @@ class LegacyJournal(Journal): """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: @@ -319,12 +342,14 @@ class LegacyJournal(Journal): 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" @@ -343,26 +368,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 9c4b051e..eee0d268 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import os + try: from .__version__ import __version__ except ImportError: diff --git a/jrnl/cli.py b/jrnl/cli.py index 2aa45567..1c53dc29 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -23,33 +23,155 @@ logging.getLogger("keyring.backend").setLevel(logging.ERROR) 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') + 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="*") + 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('-contains', dest='contains', help='View entries containing a specific string') - 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") + 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( + "-contains", dest="contains", help="View entries containing a specific string" + ) + 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") + 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) @@ -63,13 +185,30 @@ 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, args.contains)): + elif any( + ( + args.start_date, + args.end_date, + args.on_date, + args.limit, + args.strict, + args.starred, + args.contains, + ) + ): # 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,29 +217,37 @@ def guess_mode(args, config): def encrypt(journal, filename=None): """ Encrypt into new file. If filename is not set, we encrypt the journal file itself. """ - journal.config['encrypt'] = True + journal.config["encrypt"] = True new_journal = EncryptedJournal.from_journal(journal) new_journal.write(filename) - 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["encrypt"] = False new_journal = PlainJournal.from_journal(journal) 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 @@ -108,11 +255,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) @@ -120,9 +267,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): @@ -150,10 +299,10 @@ def run(manual_args=None): # use this! journal_name = install.DEFAULT_JOURNAL_KEY - if args.text and args.text[0] in config['journals']: + if args.text and args.text[0] in config["journals"]: journal_name = args.text[0] args.text = args.text[1:] - elif install.DEFAULT_JOURNAL_KEY not in config['journals']: + elif install.DEFAULT_JOURNAL_KEY not in config["journals"]: print("No default journal configured.", file=sys.stderr) print(list_journals(config), file=sys.stderr) sys.exit(1) @@ -181,18 +330,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) @@ -225,13 +380,16 @@ 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, - contains=args.contains) + 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, + contains=args.contains, + ) journal.limit(args.limit) # Reading mode @@ -253,20 +411,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 @@ -277,9 +443,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 1ee4e6ff..e95d4c12 100644 --- a/jrnl/export.py +++ b/jrnl/export.py @@ -8,6 +8,7 @@ import os 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 @@ class Exporter: 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 @@ class Exporter: 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 @@ class Exporter: 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 f989e0a0..be79724a 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -13,16 +13,17 @@ from .util import UserAbort 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' -DEFAULT_JOURNAL_KEY = 'default' -XDG_RESOURCE = 'jrnl' +DEFAULT_CONFIG_NAME = "jrnl.yaml" +DEFAULT_JOURNAL_NAME = "journal.txt" +DEFAULT_JOURNAL_KEY = "default" +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) @@ -43,21 +44,20 @@ def module_exists(module_name): else: return True + default_config = { - 'version': __version__, - 'journals': { - DEFAULT_JOURNAL_KEY: 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_KEY: 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": "|", } @@ -70,13 +70,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(): @@ -84,17 +89,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) @@ -102,7 +117,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: @@ -112,25 +127,32 @@ 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_JOURNAL_KEY] = os.path.expanduser(os.path.expandvars(journal_path)) + default_config["journals"][DEFAULT_JOURNAL_KEY] = os.path.expanduser( + os.path.expandvars(journal_path) + ) - path = os.path.split(default_config['journals'][DEFAULT_JOURNAL_KEY])[0] # If the folder doesn't exist, create it + path = os.path.split(default_config["journals"][DEFAULT_JOURNAL_KEY])[ + 0 + ] # If the folder doesn't exist, create it try: os.makedirs(path) except OSError: pass # Encrypt it? - encrypt = util.yesno("Do you want to encrypt your journal? You can always change this later", default=False) + encrypt = util.yesno( + "Do you want to encrypt your journal? You can always change this later", + default=False, + ) if encrypt: - default_config['encrypt'] = True + default_config["encrypt"] = True print("Journal will be encrypted.", file=sys.stderr) save_config(default_config) @@ -138,7 +160,7 @@ def install(): 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 53b595e3..00ee2498 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -11,8 +11,16 @@ from .yaml_exporter import YAMLExporter 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 @@ __importer_types = {name: plugin for plugin in __importers for name in plugin.na 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 74d4555f..f7ff491d 100644 --- a/jrnl/plugins/fancy_exporter.py +++ b/jrnl/plugins/fancy_exporter.py @@ -8,46 +8,64 @@ from textwrap import TextWrapper 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 83341cd9..972114d4 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 @@ class JRNLImporter: 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 e368a300..90a2059f 100644 --- a/jrnl/plugins/json_exporter.py +++ b/jrnl/plugins/json_exporter.py @@ -8,20 +8,21 @@ from .util import get_tags_count 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 @@ class JSONExporter(TextExporter): 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 da2b5748..14060ce9 100644 --- a/jrnl/plugins/markdown_exporter.py +++ b/jrnl/plugins/markdown_exporter.py @@ -10,24 +10,25 @@ from ..util import WARNING_COLOR, RESET_COLOR 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 @@ class MarkdownExporter(TextExporter): 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 f5453ced..89d54a1a 100644 --- a/jrnl/plugins/tag_exporter.py +++ b/jrnl/plugins/tag_exporter.py @@ -7,6 +7,7 @@ from .util import get_tags_count 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 @@ class TagExporter(TextExporter): 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 a74784e2..ccb02442 100644 --- a/jrnl/plugins/template.py +++ b/jrnl/plugins/template.py @@ -6,7 +6,9 @@ EXPRESSION_RE = r"[\[\]():.a-zA-Z0-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 +(.+?) *%}" @@ -39,9 +41,10 @@ class Template: def _eval_context(self, vars): import asteval + 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 +52,19 @@ class Template: 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 +90,23 @@ class Template: 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 +124,17 @@ class Template: 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 f15328f2..eb360f94 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 ce2e71de..6f2a4531 100644 --- a/jrnl/plugins/text_exporter.py +++ b/jrnl/plugins/text_exporter.py @@ -8,6 +8,7 @@ from ..util import ERROR_COLOR, RESET_COLOR class TextExporter: """This Exporter can convert entries and journals into text files.""" + names = ["text", "txt"] extension = "txt" @@ -33,7 +34,9 @@ class TextExporter: @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 @@ class TextExporter: 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 @@ class TextExporter: 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 a056b19a..a030f8d3 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 2783663b..07506cf2 100644 --- a/jrnl/plugins/xml_exporter.py +++ b/jrnl/plugins/xml_exporter.py @@ -8,6 +8,7 @@ from xml.dom import minidom 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 @@ class XMLExporter(JSONExporter): @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 @@ class XMLExporter(JSONExporter): """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 4a75667f..b0177b39 100644 --- a/jrnl/plugins/yaml_exporter.py +++ b/jrnl/plugins/yaml_exporter.py @@ -10,6 +10,7 @@ from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR 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 @@ class YAMLExporter(TextExporter): 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 66d3f4f8..5e91cd1b 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 @@ consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday 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 8f67aa34..ac29240b 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -11,9 +11,9 @@ import os 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 @@ you can enjoy all of the great new features that come with jrnl 2: 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 @@ older versions of jrnl anymore. 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 @@ older versions of jrnl anymore. 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 @@ older versions of jrnl anymore. 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 07140ff5..5918b14b 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -25,7 +25,8 @@ RESET_COLOR = "\033[0m" # 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, @@ -33,14 +34,18 @@ SENTENCE_SPLITTER = re.compile(r""" \s+ # a sequence of required spaces. | # Otherwise, \n # a sentence also terminates newlines. -)""", re.VERBOSE) +)""", + re.VERBOSE, +) class UserAbort(Exception): pass -def create_password(journal_name: str, prompt: str = "Enter password for new journal: ") -> str: +def create_password( + journal_name: str, prompt: str = "Enter password for new journal: " +) -> str: while True: pw = gp.getpass(prompt) if not pw: @@ -59,7 +64,11 @@ def create_password(journal_name: str, prompt: str = "Enter password for new jou return pw -def decrypt_content(decrypt_func: Callable[[str], Optional[str]], keychain: str = None, max_attempts: int = 3) -> str: +def decrypt_content( + decrypt_func: Callable[[str], Optional[str]], + keychain: str = None, + max_attempts: int = 3, +) -> str: pwd_from_keychain = keychain and get_keychain(keychain) password = pwd_from_keychain or gp.getpass() result = decrypt_func(password) @@ -81,21 +90,23 @@ def decrypt_content(decrypt_func: Callable[[str], Optional[str]], keychain: str 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): @@ -112,34 +123,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 @@ -152,9 +169,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 @@ -163,4 +180,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 6d8b5a59b3e3d96af60f0833d3d3910cd9af0b19 Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Sat, 4 Jan 2020 11:58:49 -0500 Subject: [PATCH 041/112] Remove merge marker in recipes.md Remove <<<<<<< HEAD marker in recipes.md --- docs/recipes.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 04604e6d..08e51711 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -130,8 +130,6 @@ Similar to Sublime Text, MacVim must be started with a flag that tells the the process to wait until the file is closed before passing control back to journal. In the case of MacVim, this is `-f`: -<<<<<<< HEAD - ```yaml editor: "mvim -f" ``` From 31138c04639b40bcd1c41b06dced47a15b446aea Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 12:01:21 -0800 Subject: [PATCH 042/112] update black version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5a118444..b5c2a6b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ pyyaml = "^5.1" behave = "^1.2" mkdocs = "^1.0" flake8 = "^3.7" -black = {version = "^18.3-alpha.0",allows-prereleases = true} +black = {version = "^19.10b0",allows-prereleases = true} [tool.poetry.scripts] jrnl = 'jrnl.cli:run' From 8384b799a135c80daebfcac5266db8b0f94409d8 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 12:01:51 -0800 Subject: [PATCH 043/112] update lock file --- poetry.lock | 72 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8a698d84..16be3709 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,8 +11,8 @@ category = "main" description = "Safe, minimalistic evaluator of python expression using ast module" name = "asteval" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.9.17" +python-versions = ">=3.5" +version = "0.9.18" [[package]] category = "dev" @@ -41,13 +41,16 @@ description = "The uncompromising code formatter." name = "black" optional = false python-versions = ">=3.6" -version = "18.9b0" +version = "19.10b0" [package.dependencies] appdirs = "*" -attrs = ">=17.4.0" +attrs = ">=18.1.0" click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" toml = ">=0.9.4" +typed-ast = ">=1.4.0" [[package]] category = "main" @@ -74,8 +77,8 @@ description = "Cross-platform colored terminal text." marker = "sys_platform == \"win32\"" name = "colorama" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.4.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" [[package]] category = "main" @@ -117,8 +120,8 @@ description = "Read metadata from Python packages" marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false -python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" -version = "1.0.0" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.3.0" [package.dependencies] zipp = ">=0.5" @@ -130,7 +133,7 @@ marker = "sys_platform == \"linux\"" name = "jeepney" optional = false python-versions = ">=3.5" -version = "0.4.1" +version = "0.4.2" [[package]] category = "dev" @@ -221,7 +224,7 @@ marker = "python_version < \"3.8\"" name = "more-itertools" optional = false python-versions = ">=3.5" -version = "8.0.0" +version = "8.0.2" [[package]] category = "dev" @@ -229,7 +232,7 @@ description = "parse() is the opposite of format()" name = "parse" optional = false python-versions = "*" -version = "1.12.1" +version = "1.14.0" [[package]] category = "dev" @@ -259,6 +262,14 @@ optional = false python-versions = "*" version = "1.7.2" +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.7.0" + [[package]] category = "dev" description = "Python style guide checker" @@ -325,7 +336,15 @@ description = "YAML parser and emitter for Python" name = "pyyaml" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "5.1.2" +version = "5.2" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2019.12.20" [[package]] category = "main" @@ -364,6 +383,14 @@ optional = false python-versions = ">= 3.5" version = "6.0.3" +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.0" + [[package]] category = "main" description = "tzinfo object for the local timezone" @@ -388,23 +415,23 @@ version = "0.6.0" more-itertools = "*" [metadata] -content-hash = "f84f27e8b11b6f6a749ae558b693d013c09c086f64a122a1f239eb206370219b" +content-hash = "98e23837423d5d8621f14cbe592d209ef98e1926b7a3f94e0f88bb6be908aae8" python-versions = ">=3.6.0, <3.9.0" [metadata.hashes] appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] -asteval = ["3e291b2cb71284bc55816fe7ff65ee15e47fa66df98490cdd95f5531fc37b81e"] +asteval = ["5d64e18b8a72c2c7ae8f9b70d1f80b68bbcaa98c1c0d7047c35489d03209bc86"] attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] behave = ["b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", "ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"] -black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"] +black = ["1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", "c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"] cffi = ["0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", "0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", "135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", "19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", "2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", "291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", "2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", "2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", "32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", "3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", "415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", "42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", "4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", "4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", "599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", "5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", "5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", "62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", "6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", "6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", "71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", "74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", "7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", "7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", "7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", "8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", "aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", "ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", "d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", "d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", "dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", "e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", "fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"] click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] -colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] +colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"] cryptography = ["02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", "1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", "369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", "3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", "44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", "4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", "58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", "6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", "7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", "73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", "7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", "90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", "971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", "a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", "b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", "b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", "d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", "de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", "df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", "ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", "fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"] entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] flake8 = ["45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", "49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"] -importlib-metadata = ["a82ca8c109e194d7d6aee3f7531b0470dd4dd6b36ec14fd55087142a96bd55a7", "f4a7ba72e93bc97ff491b66d69063819ae2b75238bb653cd4c95e3f2847ce76e"] -jeepney = ["13806f91a96e9b2623fd2a81b950d763ee471454aafd9eb6d75dbe7afce428fb", "f6a3f93464a0cf052f4e87da3c8b3ed1e27696758fb9739c63d3a74d9a1b6774"] +importlib-metadata = ["073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", "d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"] +jeepney = ["0ba6d8c597e9bef1ebd18aaec595f942a264e25c1a48f164d46120eacaa2e9bb", "6f45dce1125cf6c58a1c88123d3831f36a789f9204fbad3172eac15f8ccd08d0"] jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"] keyring = ["9b80469783d3f6106bce1d389c6b8b20c8d4d739943b1b8cd0ddc2a45d065f9d", "ee3d35b7f1ac3cb69e9a1e4323534649d3ab2fea402738a77e4250c152970fed"] livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"] @@ -412,11 +439,12 @@ markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"] -more-itertools = ["53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", "a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"] -parse = ["a5fca7000c6588d77bc65c28f3f21bfce03b5e44daa8f9f07c17fe364990d717"] +more-itertools = ["b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", "c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"] +parse = ["95a4f4469e37c57b5e924629ac99926f28bee7da59515dc5b8078c4c3e779249"] parse-type = ["089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e", "7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"] parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"] passlib = ["68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177", "8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"] +pathspec = ["163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", "562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"] pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] @@ -424,10 +452,12 @@ python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1 pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"] pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"] pyxdg = ["1948ff8e2db02156c0cccd2529b43c0cff56ebaa71f5f021bbd755bc1419190e", "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06"] -pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"] +pyyaml = ["0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", "2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", "35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", "38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", "483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", "4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", "7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", "8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", "e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", "ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"] +regex = ["032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d", "0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8", "106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e", "1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588", "27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9", "29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b", "4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae", "57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540", "724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63", "77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885", "78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea", "7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8", "8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e", "a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716", "adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1", "c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b", "cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd", "d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f", "d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3", "ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147", "faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656"] secretstorage = ["20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92", "7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a"] six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"] toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"] +typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] tzlocal = ["4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"] zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"] From e2ce40424ee22004f4a600c6c3fad9fdd153d53a Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 12:08:38 -0800 Subject: [PATCH 044/112] clarify build steps --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a4858707..0ff7dc97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,8 +56,8 @@ jobs: - stage: Lint & Tests name: Lint, via Black python: 3.8 - # black is automatically installed by peotry script: + - black --version - black --check . --verbose --diff # Python 3.6 Tests From 06970f3911978c8d62b0b0c1cd9262a3895f9502 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 12:12:00 -0800 Subject: [PATCH 045/112] run black to get codebase into good base state --- features/steps/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 73ed75a3..af0a657e 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -61,9 +61,8 @@ 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 + # 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 From 676cac7e808a129eef273e22daaef917567359b4 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 12:29:31 -0800 Subject: [PATCH 046/112] Change stage so travis doesn't get confused about allowed failures --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0ff7dc97..e493c534 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,8 +53,7 @@ jobs: - os: windows include: - - stage: Lint & Tests - name: Lint, via Black + - name: Lint, via Black python: 3.8 script: - black --version From 686ccaae3b7d58198b4522a5d0b8faaba6d73961 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 10 Dec 2019 22:05:38 -0800 Subject: [PATCH 047/112] [#766] Skip the broken test on windows for now --- features/core.feature | 1 + features/environment.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/features/core.feature b/features/core.feature index 36601fde..34539efb 100644 --- a/features/core.feature +++ b/features/core.feature @@ -20,6 +20,7 @@ Feature: Basic reading and writing to a journal When we run "jrnl -n 1" Then the output should contain "2013-07-23 09:00 A cold and stormy day." + @skip_win Scenario: Writing an empty entry from the editor Given we use the config "editor.yaml" When we open the editor and enter "" diff --git a/features/environment.py b/features/environment.py index 8ba781ac..ebda16b6 100644 --- a/features/environment.py +++ b/features/environment.py @@ -1,5 +1,6 @@ import shutil import os +import sys def before_feature(context, feature): @@ -9,6 +10,9 @@ def before_feature(context, feature): feature.skip("Marked with @skip") return + if "skip_win" in feature.tags and "win32" in sys.platform: + feature.skip("Skipping on Windows") + return def before_scenario(context, scenario): """Before each scenario, backup all config and journal test data.""" @@ -36,6 +40,9 @@ def before_scenario(context, scenario): scenario.skip("Marked with @skip") return + if "skip_win" in scenario.effective_tags and "win32" in sys.platform: + scenario.skip("Skipping on Windows") + return def after_scenario(context, scenario): """After each scenario, restore all test data and remove working_dirs.""" From 5434d476debe37ea15d9db3f689d4b9d98861635 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 10 Dec 2019 22:51:56 -0800 Subject: [PATCH 048/112] [#766] Skip another broken windows test --- features/core.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/core.feature b/features/core.feature index 34539efb..d061d094 100644 --- a/features/core.feature +++ b/features/core.feature @@ -41,6 +41,7 @@ Feature: Basic reading and writing to a journal When we run "jrnl -on 'june 6 2013' --short" Then the output should be "2013-06-10 15:40 Life is good." + @skip_win Scenario: Emoji support Given we use the config "basic.yaml" When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘" From 2d92201157841ac010b6da583e8afb6e02c8b991 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 13:02:15 -0800 Subject: [PATCH 049/112] remove broken test (windows can't handle emoji) --- features/core.feature | 9 --------- 1 file changed, 9 deletions(-) diff --git a/features/core.feature b/features/core.feature index d061d094..c023cd4c 100644 --- a/features/core.feature +++ b/features/core.feature @@ -41,15 +41,6 @@ Feature: Basic reading and writing to a journal When we run "jrnl -on 'june 6 2013' --short" Then the output should be "2013-06-10 15:40 Life is good." - @skip_win - Scenario: Emoji support - Given we use the config "basic.yaml" - When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘" - Then we should see the message "Entry added" - When we run "jrnl -n 1" - Then the output should contain "🌞" - and the output should contain "🐘" - Scenario: Writing an entry at the prompt Given we use the config "basic.yaml" When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive." From efa21831ed5bd4fdceb64ab56a31fae2b1b9d0e1 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 13:23:57 -0800 Subject: [PATCH 050/112] skip another windows test --- features/dayone_regressions.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/dayone_regressions.feature b/features/dayone_regressions.feature index c3b700b9..cbbc4086 100644 --- a/features/dayone_regressions.feature +++ b/features/dayone_regressions.feature @@ -24,6 +24,7 @@ Feature: Zapped Dayone bugs stay dead! | I'm feeling sore because I forgot to stretch. """ + @skip_win Scenario: Opening an folder that's not a DayOne folder gives a nice error message Given we use the config "empty_folder.yaml" When we run "jrnl Herro" From 3f8133cda5a5aabb51e8ee0e6d04bcfcc7d0be8c Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 13:24:07 -0800 Subject: [PATCH 051/112] whitespace changes --- features/dayone_regressions.feature | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/features/dayone_regressions.feature b/features/dayone_regressions.feature index cbbc4086..f6098ba6 100644 --- a/features/dayone_regressions.feature +++ b/features/dayone_regressions.feature @@ -13,16 +13,16 @@ Feature: Zapped Dayone bugs stay dead! # fails when system time is UTC (as on Travis-CI) @skip - Scenario: Title with an embedded period on DayOne journal - Given we use the config "dayone.yaml" - When we run "jrnl 04-24-2014: "Ran 6.2 miles today in 1:02:03. I'm feeling sore because I forgot to stretch."" - Then we should see the message "Entry added" - When we run "jrnl -1" - Then the output should be - """ - 2014-04-24 09:00 Ran 6.2 miles today in 1:02:03. - | I'm feeling sore because I forgot to stretch. - """ + Scenario: Title with an embedded period on DayOne journal + Given we use the config "dayone.yaml" + When we run "jrnl 04-24-2014: "Ran 6.2 miles today in 1:02:03. I'm feeling sore because I forgot to stretch."" + Then we should see the message "Entry added" + When we run "jrnl -1" + Then the output should be + """ + 2014-04-24 09:00 Ran 6.2 miles today in 1:02:03. + | I'm feeling sore because I forgot to stretch. + """ @skip_win Scenario: Opening an folder that's not a DayOne folder gives a nice error message From 549d7719565bc7dde4e986dc8f66d2549e568f1d Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 13:37:50 -0800 Subject: [PATCH 052/112] skip another windows test --- features/encryption.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/encryption.feature b/features/encryption.feature index 787fa850..e5bcd8e6 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -43,6 +43,7 @@ Then the output should contain "Password" And the output should contain "2013-06-10 15:40 Life is good" + @skip_win Scenario: Storing a password in Keychain Given we use the config "multiple.yaml" When we run "jrnl simple --encrypt" and enter From a3e24bcc9fd7eaeea047becc9ef5e0d363971a1a Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 13:42:25 -0800 Subject: [PATCH 053/112] run black --- features/environment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/environment.py b/features/environment.py index ebda16b6..02f2747e 100644 --- a/features/environment.py +++ b/features/environment.py @@ -14,6 +14,7 @@ def before_feature(context, feature): feature.skip("Skipping on Windows") return + def before_scenario(context, scenario): """Before each scenario, backup all config and journal test data.""" # Clean up in case something went wrong @@ -44,6 +45,7 @@ def before_scenario(context, scenario): scenario.skip("Skipping on Windows") return + def after_scenario(context, scenario): """After each scenario, restore all test data and remove working_dirs.""" for folder in ("configs", "journals"): From 6fc1313b47458b3ef989ce69b475f96045dfc7d6 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 4 Jan 2020 13:49:05 -0800 Subject: [PATCH 054/112] skip more windows tests --- features/encryption.feature | 2 ++ features/upgrade.feature | 3 +++ 2 files changed, 5 insertions(+) diff --git a/features/encryption.feature b/features/encryption.feature index e5bcd8e6..081a208f 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -12,6 +12,7 @@ Then we should see the message "Journal decrypted" And the journal should have 2 entries + @skip_win Scenario: Encrypting a journal Given we use the config "basic.yaml" When we run "jrnl --encrypt" and enter @@ -26,6 +27,7 @@ Then the output should contain "Password" And the output should contain "2013-06-10 15:40 Life is good" + @skip_win Scenario: Mistyping your password Given we use the config "basic.yaml" When we run "jrnl --encrypt" and enter diff --git a/features/upgrade.feature b/features/upgrade.feature index ef597d4f..b2c569c7 100644 --- a/features/upgrade.feature +++ b/features/upgrade.feature @@ -1,5 +1,6 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x + @skip_win Scenario: Upgrade and parse journals with square brackets Given we use the config "upgrade_from_195.json" When we run "jrnl -9" and enter "Y" @@ -11,6 +12,7 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x """ Then the journal should have 2 entries + @skip_win 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 @@ -22,6 +24,7 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x Then the output should contain "Password" and the output should contain "2013-06-10 15:40 Life is good" + @skip_win Scenario: Upgrade and parse journals with little endian date format Given we use the config "upgrade_from_195_little_endian_dates.json" When we run "jrnl -9" and enter "Y" From ef18cb4144c11c32dc9716a0d8318b377be88fff Mon Sep 17 00:00:00 2001 From: dbxnr Date: Thu, 9 Jan 2020 18:20:48 +0000 Subject: [PATCH 055/112] Fix for unhandled exception --- jrnl/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/util.py b/jrnl/util.py index 5918b14b..dfe0d13e 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -103,7 +103,7 @@ def set_keychain(journal_name, password): if password is None: try: keyring.delete_password("jrnl", journal_name) - except RuntimeError: + except keyring.errors.PasswordDeleteError: pass else: keyring.set_password("jrnl", journal_name, password) From 41c4c241cc9f3cb6a00094dd3dd234c602ca0142 Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Thu, 9 Jan 2020 20:34:38 -0800 Subject: [PATCH 056/112] #790 - closing temp file before passing it to editor to prevent file locking issues in Windows --- jrnl/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jrnl/util.py b/jrnl/util.py index 5918b14b..7af8d6c4 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -142,21 +142,26 @@ def scope_config(config, journal_name): def get_text_from_editor(config, template=""): filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt") + os.close(filehandle) + 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 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) + return raw From 59d865134afb414361136184b2fb3129ed4302ea Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 11 Jan 2020 21:20:14 -0800 Subject: [PATCH 057/112] Update changelog in prep for auto changelog generator (#724) Getting ready to implement the auto-changelog generator is easier if we are caught up with the old changes. This was generated by running the generator locally. Co-authored-by: Micah Jerome Ellison --- CHANGELOG.md | 156 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 135 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c93758..214d5efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,127 @@ -Changelog -========= +# Changelog -## 2.0 +## Unreleased -* Cryptographic backend changed from PyCrypto to cryptography.io -* Config now respects XDG conventions and may move accordingly -* Config now saved as YAML -* Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path` +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1...HEAD) -### 1.9 (July 21, 2014) +**Implemented enhancements:** + +- Full text search \(case insensitive\) with "-contains" [\#740](https://github.com/jrnl-org/jrnl/pull/740) ([empireshades](https://github.com/empireshades)) +- Reduce startup time by 55% [\#719](https://github.com/jrnl-org/jrnl/pull/719) ([maebert](https://github.com/maebert)) +- Refactor password logic to prevent accidental password leakage [\#708](https://github.com/jrnl-org/jrnl/pull/708) ([pspeter](https://github.com/pspeter)) +- Password confirmation [\#706](https://github.com/jrnl-org/jrnl/pull/706) ([pspeter](https://github.com/pspeter)) + +**Build:** + +- Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren)) +- Run black formatter on codebase for standardization [\#778](https://github.com/jrnl-org/jrnl/pull/778) ([wren](https://github.com/wren)) +- Skip Broken Windows Tests [\#772](https://github.com/jrnl-org/jrnl/pull/772) ([wren](https://github.com/wren)) +- Black Formatter [\#769](https://github.com/jrnl-org/jrnl/pull/769) ([MinchinWeb](https://github.com/MinchinWeb)) +- Update lock file and testing suite for Python 3.8 [\#765](https://github.com/jrnl-org/jrnl/pull/765) ([wren](https://github.com/wren)) +- Fix CI config to only deploy once [\#761](https://github.com/jrnl-org/jrnl/pull/761) ([wren](https://github.com/wren)) +- More Travis-CI Testing [\#759](https://github.com/jrnl-org/jrnl/pull/759) ([MinchinWeb](https://github.com/MinchinWeb)) + +**Updated documentation:** + +- Remove merge marker in recipes.md [\#782](https://github.com/jrnl-org/jrnl/pull/782) ([markphelps](https://github.com/markphelps)) +- Fix merge conflict left-over [\#767](https://github.com/jrnl-org/jrnl/pull/767) ([thejspr](https://github.com/thejspr)) +- Display header in docs on mobile devices [\#763](https://github.com/jrnl-org/jrnl/pull/763) ([maebert](https://github.com/maebert)) + +## [v2.1.1](https://pypi.org/project/jrnl/v2.1.1/) (2019-11-26) + +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1-beta...v2.1.1) + +**Implemented enhancements:** + +- Support Python 3.6+ [\#710](https://github.com/jrnl-org/jrnl/pull/710) ([pspeter](https://github.com/pspeter)) +- Drop Python 2 support, add mocks in tests [\#705](https://github.com/jrnl-org/jrnl/pull/705) ([pspeter](https://github.com/pspeter)) + +**Fixed bugs:** + +- Prevent readline usage on Windows, which was causing Active Python crashes on install [\#751](https://github.com/jrnl-org/jrnl/pull/751) ([micahellison](https://github.com/micahellison)) +- Exit jrnl if no text entered into editor [\#744](https://github.com/jrnl-org/jrnl/pull/744) ([alichtman](https://github.com/alichtman)) +- Fix crash when no keyring backend available [\#699](https://github.com/jrnl-org/jrnl/pull/699) ([pspeter](https://github.com/pspeter)) +- Fix parsing Journals using a little-endian date format [\#694](https://github.com/jrnl-org/jrnl/pull/694) ([pspeter](https://github.com/pspeter)) + +**Updated documentation:** + +- Update developer documentation [\#752](https://github.com/jrnl-org/jrnl/pull/752) ([micahellison](https://github.com/micahellison)) +- Create templates for issues and pull requests [\#679](https://github.com/jrnl-org/jrnl/pull/679) ([C0DK](https://github.com/C0DK)) +- Smaller doc fixes [\#649](https://github.com/jrnl-org/jrnl/pull/649) ([maebert](https://github.com/maebert)) +- Move to mkdocs [\#611](https://github.com/jrnl-org/jrnl/pull/611) ([maebert](https://github.com/maebert)) + +## [v2.1.post2](https://pypi.org/project/jrnl/v2.1.post2/) (2019-11-11) + +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1-beta6...v2.1.post2) + +**Fixed bugs:** + +- Expand paths that use ~ to full path [\#704](https://github.com/jrnl-org/jrnl/pull/704) ([MinchinWeb](https://github.com/MinchinWeb)) + +**Build:** + +- Separate local dev from pipeline releases [\#684](https://github.com/jrnl-org/jrnl/pull/684) ([wren](https://github.com/wren)) +- Update version handling in source and travis deployments [\#683](https://github.com/jrnl-org/jrnl/pull/683) ([wren](https://github.com/wren)) +- Use Poetry for dependency management and deployments [\#612](https://github.com/jrnl-org/jrnl/pull/612) ([maebert](https://github.com/maebert)) + +**Updated documentation:** + +- Fix typos, spelling [\#734](https://github.com/jrnl-org/jrnl/pull/734) ([MinchinWeb](https://github.com/MinchinWeb)) + +## [v2.0.1](https://pypi.org/project/jrnl/v2.0.1/) (2019-09-26) + +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0.1-beta...v2.0.1) + +**Implemented enhancements:** + +- Switch to hashmark Markdown headers on export \(Mk II\) [\#639](https://github.com/jrnl-org/jrnl/pull/639) ([MinchinWeb](https://github.com/MinchinWeb)) +- Add '-not' flag for excluding tags from filter [\#637](https://github.com/jrnl-org/jrnl/pull/637) ([jprof](https://github.com/jprof)) +- Handle KeyboardInterrupt when installing journal [\#550](https://github.com/jrnl-org/jrnl/pull/550) ([silenc3r](https://github.com/silenc3r)) + +**Fixed bugs:** + +- Change pyYAML required version [\#660](https://github.com/jrnl-org/jrnl/pull/660) ([etnnth](https://github.com/etnnth)) + +**Updated documentation:** + +- Fix references to Sphinx in CONTRIBUTING.md [\#655](https://github.com/jrnl-org/jrnl/pull/655) ([maebert](https://github.com/maebert)) + +## [v2.0.0](https://pypi.org/project/jrnl/v2.0.0/) (2019-08-24) + +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0-rc4...v2.0.0) + +**Implemented enhancements:** +- Change cryptographic backend from PyCrypto to cryptography.io +- Config now respects XDG conventions and may move accordingly +- Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path` + +**Fixed bugs:** + +- Confirm that each journal can be parsed during upgrade, and abort upgrade if not [\#650](https://github.com/jrnl-org/jrnl/pull/650) ([micahellison](https://github.com/micahellison)) +- Escape dates in square brackets [\#644](https://github.com/jrnl-org/jrnl/pull/644) ([wren](https://github.com/wren)) +- Create encrypted journal [\#641](https://github.com/jrnl-org/jrnl/pull/641) ([gregorybodnar](https://github.com/gregorybodnar)) +- Resolve issues around unreadable dates to allow markdown footnotes and prevent accidental deletion [\#623](https://github.com/jrnl-org/jrnl/pull/623) ([micahellison](https://github.com/micahellison)) +- Update crypto module \#610 [\#621](https://github.com/jrnl-org/jrnl/pull/621) ([wren](https://github.com/wren)) +- Fix issue \#584 YAMLLoadWarning [\#585](https://github.com/jrnl-org/jrnl/pull/585) ([wren](https://github.com/wren)) + +**Deprecated:** + +- Deprecate Python 2 [\#624](https://github.com/jrnl-org/jrnl/pull/624) ([micahellison](https://github.com/micahellison)) +- Config now saved as YAML (no more JSON) + +**Build:** + +- change pinned label to a super cool emoji ⭐️ [\#646](https://github.com/jrnl-org/jrnl/pull/646) ([wren](https://github.com/wren)) +- Update Travis build badge and restore pypi badges [\#603](https://github.com/jrnl-org/jrnl/pull/603) ([micahellison](https://github.com/micahellison)) + +**Updated documentation:** + +- Mention lack of Day One support and relevant history in readme [\#608](https://github.com/jrnl-org/jrnl/pull/608) ([micahellison](https://github.com/micahellison)) +- Add a code of conduct file \(rather than adding to contributing\) [\#604](https://github.com/jrnl-org/jrnl/pull/604) ([wren](https://github.com/wren)) +- Update docs to reflect merging jrnl-plus fork back upstream [\#601](https://github.com/jrnl-org/jrnl/pull/601) ([micahellison](https://github.com/micahellison)) +- Add instructions for VS Code [\#544](https://github.com/jrnl-org/jrnl/pull/544) ([emceeaich](https://github.com/emceeaich)) + +## v1.9 (2014-07-21) * __1.9.5__ Multi-word tags for DayOne Journals * __1.9.4__ Fixed: Order of journal entries in file correct after --edit'ing @@ -17,7 +130,7 @@ Changelog * __1.9.1__ Fixed: Dates in the future can be parsed as well. * __1.9.0__ Improved: Greatly improved date parsing. Also added an `-on` option for filtering -### 1.8 (May 22, 2014) +## v1.8 (2014-05-22) * __1.8.7__ Fixed: -from and -to filters are inclusive (thanks to @grplyler) * __1.8.6__ Improved: Tags like @C++ and @OS/2 work, too (thanks to @chaitan94) @@ -28,7 +141,7 @@ Changelog * __1.8.1__ Minor bug fixes * __1.8.0__ Official support for python 3.4 -### 1.7 (December 22, 2013) +## v1.7 (2013-12-22) * __1.7.22__ Fixed an issue with writing files when exporting entries containing non-ascii characters. * __1.7.21__ jrnl now uses PKCS#7 padding. @@ -54,7 +167,7 @@ Changelog * __1.7.0__ Edit encrypted or DayOne journals with `jrnl --edit`. -### 1.6 (November 5, 2013) +## v1.6 (2013-11-05) * __1.6.6__ -v prints the current version, also better strings for windows users. Furthermore, jrnl/jrnl.py moved to jrnl/cli.py * __1.6.5__ Allows composing multi-line entries on the command line or importing files @@ -64,7 +177,7 @@ Changelog * __1.6.1__ Attempts to fix broken config files automatically * __1.6.0__ Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated. -### 1.5 (August 6, 2013) +## v1.5 (2013-08-06) * __1.5.7__ The `~` in journal config paths will now expand properly to e.g. `/Users/maebert` * __1.5.6__ Fixed: Fixed a bug where on OS X, the timezone could only be accessed on administrator accounts. @@ -75,23 +188,23 @@ Changelog * __1.5.1__ Fixed: Fixed a bug introduced in 1.5.0 that caused the entire journal to be printed after composing an entry * __1.5.0__ Exporting, encrypting and displaying tags now takes your filter options into account. So you could export everything before May 2012: `jrnl -to 'may 2012' --export json`. Or encrypt all entries tagged with `@work` into a new journal: `jrnl @work --encrypt work_journal.txt`. Or display all tags of posts where Bob is also tagged: `jrnl @bob --tags` -### 1.4 (July 22, 2013) +## v1.4 (2013-07-22) * __1.4.2__ Fixed: Tagging works again * __1.4.0__ Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`). -### 1.3 (July 17, 2013) +## v1.3 (2013-07-17) * __1.3.2__ Everything that is not direct output of jrnl will be written stderr to improve integration * __1.3.0__ Export to multiple files * __1.3.0__ Feature to export to given output file -### 1.2 (July 15, 2013) +## v1.2 (2013-07-15) * __1.2.0__ Fixed: Timezone support for DayOne -### 1.1 (June 9, 2013) +## v1.1 (2013-06-09) * __1.1.1__ Fixed: Unicode and Python3 issues resolved. * __1.1.0__ @@ -99,7 +212,7 @@ Changelog * Nicer error message when there is a syntactical error in your config file. * Unicode support -### 1.0 (March 4, 2013) +## v1.0 (2013-03-04) * __1.0.5__ Backwards compatibility with `parsedatetime` 0.8.7 * __1.0.4__ @@ -122,7 +235,7 @@ Changelog * Fixed: A bug where jrnl would not add entries without timestamp * Fixed: Support for parsedatetime 1.x -### 0.3 (May 24, 2012) +## v0.3 (2012-05-24) * __0.3.2__ Converts `\n` to new lines (if using directly on a command line, make sure to wrap your entry with quotes). * __0.3.1__ @@ -135,7 +248,7 @@ Changelog * Fixed: Bug where composed entry is lost when the journal file fails to load * Changed directory structure and install scripts (removing the necessity to make an alias from `jrnl` to `jrnl.py`) -### 0.2 (April 16, 2012) +## v0.2 (2012-04-16) * __0.2.4__ * Fixed: Parsing of new lines in journal files and entries @@ -153,7 +266,7 @@ Changelog * Encrypts using CBC * Fixed: `key` has been renamed to `password` in config to avoid confusion. (The key use to encrypt and decrypt a journal is the SHA256-hash of the password.) -### 0.1 (April 13, 2012) +## v0.1 (2012-04-13) * __0.1.1__ @@ -166,6 +279,7 @@ Changelog * Filtering by tags and dates * Fixed: Now using dedicated classes for Journals and entries -### 0.0 (March 29, 2012) +## v0.0 (2012-03-29) * __0.0.1__ Composing entries works. That's pretty much it. + From 334c9b3e7a5cd6eb68a0545b3c1cd208226334d0 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Mon, 13 Jan 2020 20:09:28 -0800 Subject: [PATCH 058/112] add config file for changelog generator --- .github_changelog_generator | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github_changelog_generator diff --git a/.github_changelog_generator b/.github_changelog_generator new file mode 100644 index 00000000..38349f58 --- /dev/null +++ b/.github_changelog_generator @@ -0,0 +1,9 @@ +project=jrnl +user=jrnl-org +base=CHANGELOG.md +issues=false +issues-wo-labels=false +include-labels=bug,enhancement,documentation,build,deprecated +release-url='https://pypi.org/project/jrnl/%s/' +add-sections={ "build": { "prefix": "**Build:**", "labels": ["build"]}, "docs": { "prefix": "**Updated documentation:**", "labels": ["documentation"]}} +exclude-tags-regex=(alpha|beta|rc) From f564c8ca347de88d273373213b38b677e427e671 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Mon, 13 Jan 2020 20:45:56 -0800 Subject: [PATCH 059/112] use api token instead of username and pw for pypi --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e493c534..b3903997 100644 --- a/.travis.yml +++ b/.travis.yml @@ -117,7 +117,7 @@ jobs: - stage: Deploy if: branch = master AND tag IS present before_deploy: - - poetry config http-basic.pypi "$PYPI_USER" "$PYPI_PASS" + - poetry config pypi-token.pypi "$PYPI_TOKEN" - poetry version "$TRAVIS_TAG" - echo __version__ = \"$TRAVIS_TAG\" > jrnl/__version__.py - poetry build From 8ecc2baa9c55ceb03ebe41d37fc81595a56cc79c Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Mon, 13 Jan 2020 20:47:55 -0800 Subject: [PATCH 060/112] update changelog generator settings to be less verbose, fix links --- .github_changelog_generator | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github_changelog_generator b/.github_changelog_generator index 38349f58..983e4119 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -4,6 +4,8 @@ base=CHANGELOG.md issues=false issues-wo-labels=false include-labels=bug,enhancement,documentation,build,deprecated -release-url='https://pypi.org/project/jrnl/%s/' +release-url=https://pypi.org/project/jrnl/%s/ add-sections={ "build": { "prefix": "**Build:**", "labels": ["build"]}, "docs": { "prefix": "**Updated documentation:**", "labels": ["documentation"]}} exclude-tags-regex=(alpha|beta|rc) +verbose=false + From 7fba6c77a454af767f4a80475001e26b4785610e Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 14 Jan 2020 19:44:26 -0800 Subject: [PATCH 061/112] fix conditional for deploy step --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b3903997..ba6ead9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -115,7 +115,6 @@ jobs: - TZ=America/Edmonton - stage: Deploy - if: branch = master AND tag IS present before_deploy: - poetry config pypi-token.pypi "$PYPI_TOKEN" - poetry version "$TRAVIS_TAG" @@ -124,6 +123,10 @@ jobs: deploy: - provider: script script: poetry publish + skip_cleanup: true + on: + branch: master + tags: true after_deploy: - git config --global user.email "jrnl.bot@gmail.com" - git config --global user.name "Jrnl Bot" From 778a37cd5ef56cb67d8985e2dbeec49f6d681cd1 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 14 Jan 2020 20:06:14 -0800 Subject: [PATCH 062/112] add script to generate changelog --- .build/generate_changelog.sh | 40 ++++++++++++++++++++++++++++++++++++ .travis.yml | 8 ++++++++ 2 files changed, 48 insertions(+) create mode 100755 .build/generate_changelog.sh diff --git a/.build/generate_changelog.sh b/.build/generate_changelog.sh new file mode 100755 index 00000000..b0267422 --- /dev/null +++ b/.build/generate_changelog.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +FILENAME='CHANGELOG.md' + +# get the latest git tags +gittags="$(git tag --sort=-creatordate | grep -Ev '(alpha|beta|rc)')" +gittag_latest=$(printf '%s' "$gittags" | awk 'NR==1') +gittag_secondlatest=$(printf '%s' "$gittags" | awk 'NR==2') + +echo "gittag_latest: ${gittag_latest}" +echo "gittag_secondlatest: ${gittag_secondlatest}" + +# delete generated line (or it will be added multiple times) +sed -i '/This Changelog was automatically generated by/d' "$FILENAME" + +# delete trailing empty lines +sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "$FILENAME" + +# determine correct tag to go back to +# @TODO +if [[ ! -z $TRAVIS_TAG ]]; then + echo "release build" + gittag=${gittag_secondlatest} +else + echo "merge into master or develop" + gittag=${gittag_latest} +fi +echo "gittag: ${gittag}" + +# find the line the tag starts on, and subtract 1 +tagline=$(grep -n "^## \[\?$gittag\]\?" CHANGELOG.md | awk '{print $1}' FS=':' | head -1) +echo "tagline: ${tagline}" +[[ ! -z $tagline ]] && sed -i "1,$(expr $tagline - 1)d" "$FILENAME" + +# generate the changelog +docker run -it --rm -v "$(pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator -t $CHANGELOG_GITHUB_TOKEN --since-tag $gittag + +# @TODO commit changes, etc +git diff + diff --git a/.travis.yml b/.travis.yml index ba6ead9a..a2d71c48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -114,6 +114,14 @@ jobs: env: - TZ=America/Edmonton + # Changelog for Unreleased changes + - stage: Update Changelog + if: (tag IS present) OR (branch = develop AND type NOT IN (pull_request)) + install: + - echo 'Skipping install' + script: + - ./.build/generate_changelog.sh + - stage: Deploy before_deploy: - poetry config pypi-token.pypi "$PYPI_TOKEN" From 4d70c46b6380c472fdf2351e9bbe0e4bcb23d955 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 14 Jan 2020 21:31:04 -0800 Subject: [PATCH 063/112] update changelog script to handle different scenarios (#724) --- .build/generate_changelog.sh | 40 ++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/.build/generate_changelog.sh b/.build/generate_changelog.sh index b0267422..58867c20 100755 --- a/.build/generate_changelog.sh +++ b/.build/generate_changelog.sh @@ -3,12 +3,12 @@ FILENAME='CHANGELOG.md' # get the latest git tags -gittags="$(git tag --sort=-creatordate | grep -Ev '(alpha|beta|rc)')" -gittag_latest=$(printf '%s' "$gittags" | awk 'NR==1') -gittag_secondlatest=$(printf '%s' "$gittags" | awk 'NR==2') +releases="$(git tag --sort=-creatordate | grep -Ev '(alpha|beta|rc)')" +release_latest=$(printf '%s' "$releases" | awk 'NR==1') +release_secondlatest=$(printf '%s' "$releases" | awk 'NR==2') -echo "gittag_latest: ${gittag_latest}" -echo "gittag_secondlatest: ${gittag_secondlatest}" +echo "release_latest: ${release_latest}" +echo "release_secondlatest: ${release_secondlatest}" # delete generated line (or it will be added multiple times) sed -i '/This Changelog was automatically generated by/d' "$FILENAME" @@ -17,24 +17,38 @@ sed -i '/This Changelog was automatically generated by/d' "$FILENAME" sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "$FILENAME" # determine correct tag to go back to -# @TODO -if [[ ! -z $TRAVIS_TAG ]]; then +if [[ $TRAVIS_TAG == $release_latest ]]; then echo "release build" - gittag=${gittag_secondlatest} + gittag=${release_secondlatest} +elif [[ ! -z $TRAVIS_TAG ]]; then + echo "beta elease" + gittag=${release_latest} else echo "merge into master or develop" - gittag=${gittag_latest} + gittag=${release_latest} fi echo "gittag: ${gittag}" # find the line the tag starts on, and subtract 1 -tagline=$(grep -n "^## \[\?$gittag\]\?" CHANGELOG.md | awk '{print $1}' FS=':' | head -1) +tagline=$(grep -n "^## \[\?$gittag\]\?" "$FILENAME" | awk '{print $1}' FS=':' | head -1) echo "tagline: ${tagline}" [[ ! -z $tagline ]] && sed -i "1,$(expr $tagline - 1)d" "$FILENAME" # generate the changelog -docker run -it --rm -v "$(pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator -t $CHANGELOG_GITHUB_TOKEN --since-tag $gittag +docker run -it --rm -v "$(pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator -t $GITHUB_TOKEN --since-tag $gittag -# @TODO commit changes, etc -git diff +# Put back our link (instead of the broken one) +sed -i 's!https://pypi.org/project/jrnl/HEAD/!https://github.com/jrnl-org/jrnl/!' "$FILENAME" + +BRANCH=$TRAVIS_BRANCH +if [[ $TRAVIS_BRANCH == $TRAVIS_TAG ]]; then + BRANCH='master' +fi + +git config --global user.email "jrnl.bot@gmail.com" +git config --global user.name "Jrnl Bot" +git checkout $BRANCH +git add "$FILENAME" +git commit -m "Updating changelog [ci skip]" +git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git $BRANCH From 4977924977451aa3beeeb8fcdea6f0a06efaf5a9 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 14 Jan 2020 21:36:58 -0800 Subject: [PATCH 064/112] update deploy stage condition --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a2d71c48..0fd39b6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -123,6 +123,7 @@ jobs: - ./.build/generate_changelog.sh - stage: Deploy + if: tag IS present before_deploy: - poetry config pypi-token.pypi "$PYPI_TOKEN" - poetry version "$TRAVIS_TAG" From af970cd47f7e370cb4c228120739eb5b22defd4b Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 14 Jan 2020 21:48:42 -0800 Subject: [PATCH 065/112] small fixes for travis pipeline --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0fd39b6e..37dab626 100644 --- a/.travis.yml +++ b/.travis.yml @@ -125,7 +125,6 @@ jobs: - stage: Deploy if: tag IS present before_deploy: - - poetry config pypi-token.pypi "$PYPI_TOKEN" - poetry version "$TRAVIS_TAG" - echo __version__ = \"$TRAVIS_TAG\" > jrnl/__version__.py - poetry build @@ -141,5 +140,5 @@ jobs: - git config --global user.name "Jrnl Bot" - git checkout master - git add pyproject.toml - - git commit -m "Incrementing version to ${TRAVIS_TAG}" + - git commit -m "Incrementing version to ${TRAVIS_TAG} [ci skip]" - git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master From 43ff998ca3fe084d2b13ebbe669a7e1872f579cf Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Wed, 15 Jan 2020 05:57:01 +0000 Subject: [PATCH 066/112] Updating changelog [ci skip] --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 214d5efc..2d5b4a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## [Unreleased](https://github.com/jrnl-org/jrnl/) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1...HEAD) @@ -13,6 +13,7 @@ **Build:** +- Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren)) - Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren)) - Run black formatter on codebase for standardization [\#778](https://github.com/jrnl-org/jrnl/pull/778) ([wren](https://github.com/wren)) - Skip Broken Windows Tests [\#772](https://github.com/jrnl-org/jrnl/pull/772) ([wren](https://github.com/wren)) @@ -283,3 +284,5 @@ * __0.0.1__ Composing entries works. That's pretty much it. + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* From 284db07a05b4c41557c65b88d56c7e820d68c349 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Wed, 15 Jan 2020 06:16:04 +0000 Subject: [PATCH 067/112] Updating changelog [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d5b4a5f..a3be2742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ **Build:** +- Change PyPI auth method in build pipeline [\#807](https://github.com/jrnl-org/jrnl/pull/807) ([wren](https://github.com/wren)) - Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren)) - Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren)) - Run black formatter on codebase for standardization [\#778](https://github.com/jrnl-org/jrnl/pull/778) ([wren](https://github.com/wren)) From b522f761eed1210fc1cc31c84a3b3b2e4c5ce98f Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Wed, 15 Jan 2020 06:33:18 +0000 Subject: [PATCH 068/112] Updating changelog [ci skip] --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d5b4a5f..45cb7a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,11 @@ - Reduce startup time by 55% [\#719](https://github.com/jrnl-org/jrnl/pull/719) ([maebert](https://github.com/maebert)) - Refactor password logic to prevent accidental password leakage [\#708](https://github.com/jrnl-org/jrnl/pull/708) ([pspeter](https://github.com/pspeter)) - Password confirmation [\#706](https://github.com/jrnl-org/jrnl/pull/706) ([pspeter](https://github.com/pspeter)) +- Pretty print journal entries [\#692](https://github.com/jrnl-org/jrnl/pull/692) ([alichtman](https://github.com/alichtman)) **Build:** +- Change PyPI auth method in build pipeline [\#807](https://github.com/jrnl-org/jrnl/pull/807) ([wren](https://github.com/wren)) - Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren)) - Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren)) - Run black formatter on codebase for standardization [\#778](https://github.com/jrnl-org/jrnl/pull/778) ([wren](https://github.com/wren)) From fd591b82a357a1bbbf62646dca738c2f729640bf Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 14 Jan 2020 22:58:36 -0800 Subject: [PATCH 069/112] unpin poetry version in requirments --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 37dab626..50f088c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ before_install: - date install: - - pip install poetry~=0.12.17 + - pip install poetry - poetry install - poetry run python --version From 2433363ca8773ce60580df00403bfc98b6132cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Ooms?= Date: Thu, 16 Jan 2020 14:51:22 +0100 Subject: [PATCH 070/112] Explain how fish can be configured to exclude jrnl commands from history by default --- docs/encryption.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/encryption.md b/docs/encryption.md index 2cb5d547..35a26313 100644 --- a/docs/encryption.md +++ b/docs/encryption.md @@ -56,10 +56,14 @@ setopt HIST_IGNORE_SPACE alias jrnl=" jrnl" ``` -The fish shell does not support automatically preventing logging like -this. To prevent `jrnl` commands being logged by fish, you must make -sure to type a space before every `jrnl` command you enter. To delete -existing `jrnl` commands from fish’s history, run +If you are using `fish` instead of `bash` or `zsh`, you can get the same behaviour by +adding this to your `fish` configuration: + +``` sh +abbr jrnl=" jrnl" +``` + +To delete existing `jrnl` commands from `fish`’s history, run `history delete --prefix 'jrnl '`. ## Manual decryption From b4be6fa161ff1b95309a74d06e376df355be341c Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Fri, 17 Jan 2020 03:03:35 +0000 Subject: [PATCH 071/112] Updating changelog [ci skip] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3be2742..d2f3a7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - Refactor password logic to prevent accidental password leakage [\#708](https://github.com/jrnl-org/jrnl/pull/708) ([pspeter](https://github.com/pspeter)) - Password confirmation [\#706](https://github.com/jrnl-org/jrnl/pull/706) ([pspeter](https://github.com/pspeter)) +**Fixed bugs:** + +- Close temp file before passing it to editor to prevent file locking issues in Windows [\#792](https://github.com/jrnl-org/jrnl/pull/792) ([micahellison](https://github.com/micahellison)) + **Build:** - Change PyPI auth method in build pipeline [\#807](https://github.com/jrnl-org/jrnl/pull/807) ([wren](https://github.com/wren)) From 306e048f19ded86a96c55283a075b3fa2bdbe66b Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Fri, 17 Jan 2020 03:21:23 +0000 Subject: [PATCH 072/112] Updating changelog [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f3a7a4..de50e8c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ **Fixed bugs:** - Close temp file before passing it to editor to prevent file locking issues in Windows [\#792](https://github.com/jrnl-org/jrnl/pull/792) ([micahellison](https://github.com/micahellison)) +- Fix crash while encrypting a journal on first run without saving password [\#789](https://github.com/jrnl-org/jrnl/pull/789) ([dbxnr](https://github.com/dbxnr)) **Build:** From a753f53f5427931752577fbdc4af89860197a6f6 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 25 Jan 2020 11:47:52 -0800 Subject: [PATCH 073/112] add version file to repo --- .gitignore | 1 - jrnl/__version__.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 jrnl/__version__.py diff --git a/.gitignore b/.gitignore index a06808df..2382cabe 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,3 @@ exp/ _extras/ *.sublime-* site/ -jrnl/__version__.py diff --git a/jrnl/__version__.py b/jrnl/__version__.py new file mode 100644 index 00000000..4bf5d5ff --- /dev/null +++ b/jrnl/__version__.py @@ -0,0 +1 @@ +__version__ = "v2.2-beta" From c88457f9ff58946bf9f4dc9e7dedf14860f13956 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 25 Jan 2020 11:52:47 -0800 Subject: [PATCH 074/112] fix poetry warning --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b5c2a6b1..284da2ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ pyyaml = "^5.1" behave = "^1.2" mkdocs = "^1.0" flake8 = "^3.7" -black = {version = "^19.10b0",allows-prereleases = true} +black = {version = "^19.10b0",allow-prereleases = true} [tool.poetry.scripts] jrnl = 'jrnl.cli:run' From f10cb4d4f89ac6f4ff4e42e8258c9926542e2764 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 25 Jan 2020 12:13:54 -0800 Subject: [PATCH 075/112] add maintainer field to config --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 284da2ba..c53a175c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,9 @@ authors = [ "Jonathan Wren ", "Micah Ellison " ] +maintainers = [ + "Jonathan Wren and Micah Ellison ", +] license = "MIT" readme = "README.md" homepage = "https://jrnl.sh" From 879733a59f5b91c37ddf96c1be88dbdeb10e7cdb Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 25 Jan 2020 12:14:09 -0800 Subject: [PATCH 076/112] update poetry version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c53a175c..bc557beb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jrnl" -version = "v2.1.1" +version = "v2.2-beta" description = "Collect your thoughts and notes without leaving the command line." authors = [ "Manuel Ebert ", From 932a5b6cc33db9834b365e615ceb97b39b1924ec Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 25 Jan 2020 22:12:27 +0000 Subject: [PATCH 077/112] Updating changelog [ci skip] --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93055ede..de50e8c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ - Reduce startup time by 55% [\#719](https://github.com/jrnl-org/jrnl/pull/719) ([maebert](https://github.com/maebert)) - Refactor password logic to prevent accidental password leakage [\#708](https://github.com/jrnl-org/jrnl/pull/708) ([pspeter](https://github.com/pspeter)) - Password confirmation [\#706](https://github.com/jrnl-org/jrnl/pull/706) ([pspeter](https://github.com/pspeter)) -- Pretty print journal entries [\#692](https://github.com/jrnl-org/jrnl/pull/692) ([alichtman](https://github.com/alichtman)) **Fixed bugs:** From 9f615191b4374eb8c850dca8dee1de6c4f903db2 Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 25 Jan 2020 15:39:05 -0800 Subject: [PATCH 078/112] Prevent Travis from turning LF to CRLF in Windows --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 37dab626..8219d49a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ cache: git: depth: false + autocrlf: false before_install: - date From 25a7577f0b5d46acc57a32b2234ddab1ac733118 Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 25 Jan 2020 15:40:05 -0800 Subject: [PATCH 079/112] Remove all skip_win directives now that Travis is not mangling newlines on Windows --- features/core.feature | 1 - features/dayone_regressions.feature | 1 - features/encryption.feature | 3 --- features/upgrade.feature | 3 --- 4 files changed, 8 deletions(-) diff --git a/features/core.feature b/features/core.feature index c023cd4c..94da701a 100644 --- a/features/core.feature +++ b/features/core.feature @@ -20,7 +20,6 @@ Feature: Basic reading and writing to a journal When we run "jrnl -n 1" Then the output should contain "2013-07-23 09:00 A cold and stormy day." - @skip_win Scenario: Writing an empty entry from the editor Given we use the config "editor.yaml" When we open the editor and enter "" diff --git a/features/dayone_regressions.feature b/features/dayone_regressions.feature index f6098ba6..62c8cc24 100644 --- a/features/dayone_regressions.feature +++ b/features/dayone_regressions.feature @@ -24,7 +24,6 @@ Feature: Zapped Dayone bugs stay dead! | I'm feeling sore because I forgot to stretch. """ - @skip_win Scenario: Opening an folder that's not a DayOne folder gives a nice error message Given we use the config "empty_folder.yaml" When we run "jrnl Herro" diff --git a/features/encryption.feature b/features/encryption.feature index 081a208f..787fa850 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -12,7 +12,6 @@ Then we should see the message "Journal decrypted" And the journal should have 2 entries - @skip_win Scenario: Encrypting a journal Given we use the config "basic.yaml" When we run "jrnl --encrypt" and enter @@ -27,7 +26,6 @@ Then the output should contain "Password" And the output should contain "2013-06-10 15:40 Life is good" - @skip_win Scenario: Mistyping your password Given we use the config "basic.yaml" When we run "jrnl --encrypt" and enter @@ -45,7 +43,6 @@ Then the output should contain "Password" And the output should contain "2013-06-10 15:40 Life is good" - @skip_win Scenario: Storing a password in Keychain Given we use the config "multiple.yaml" When we run "jrnl simple --encrypt" and enter diff --git a/features/upgrade.feature b/features/upgrade.feature index b2c569c7..ef597d4f 100644 --- a/features/upgrade.feature +++ b/features/upgrade.feature @@ -1,6 +1,5 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x - @skip_win Scenario: Upgrade and parse journals with square brackets Given we use the config "upgrade_from_195.json" When we run "jrnl -9" and enter "Y" @@ -12,7 +11,6 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x """ Then the journal should have 2 entries - @skip_win 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 @@ -24,7 +22,6 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x Then the output should contain "Password" and the output should contain "2013-06-10 15:40 Life is good" - @skip_win Scenario: Upgrade and parse journals with little endian date format Given we use the config "upgrade_from_195_little_endian_dates.json" When we run "jrnl -9" and enter "Y" From 9cc8539ff94ea18b9d6249b554aedf6de695bdae Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 25 Jan 2020 15:57:58 -0800 Subject: [PATCH 080/112] Keep skip_win directive for empty entry test that still stalls on Travis --- features/core.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/core.feature b/features/core.feature index 94da701a..c023cd4c 100644 --- a/features/core.feature +++ b/features/core.feature @@ -20,6 +20,7 @@ Feature: Basic reading and writing to a journal When we run "jrnl -n 1" Then the output should contain "2013-07-23 09:00 A cold and stormy day." + @skip_win Scenario: Writing an empty entry from the editor Given we use the config "editor.yaml" When we open the editor and enter "" From e54da410008d2c8786b8e4006be300b66fb49ab0 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Sat, 25 Jan 2020 18:01:55 -0700 Subject: [PATCH 081/112] [DayOne] YAML export improvements (#773) * [YAML Export] code style improvements * [Dayone] Brings back extended Dayone attributes to YAML export c.f. 7d3afd811ba2c32463391b709331019e3cbfc40f reverse fd46ffea23eb1cb98121bde3a3993a4fa56e1afb * [YAML Exporter] switch to f-strings * [Black] apply black formatter to YAML Exporter * Code fixes as per review --- jrnl/plugins/yaml_exporter.py | 44 +++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/jrnl/plugins/yaml_exporter.py b/jrnl/plugins/yaml_exporter.py index b0177b39..430b68f7 100644 --- a/jrnl/plugins/yaml_exporter.py +++ b/jrnl/plugins/yaml_exporter.py @@ -19,11 +19,10 @@ class YAMLExporter(TextExporter): """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, + "{}ERROR{}: YAML export must be to individual files. Please \ + specify a directory to export to.".format( + ERROR_COLOR, RESET_COLOR, file=sys.stderr + ) ) return @@ -33,16 +32,14 @@ class YAMLExporter(TextExporter): 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(fr"(?u)^\s*([{tagsymbols}][-+*#/\w]+\s*)+$") """Increase heading levels in body text""" newbody = "" heading = "#" previous_line = "" warn_on_heading_level = False - for line in entry.body.splitlines(True): + for line in body.splitlines(True): if re.match(r"^#+ ", line): """ATX style headings""" newbody = newbody + previous_line + heading + line @@ -80,9 +77,32 @@ class YAMLExporter(TextExporter): dayone_attributes = "" if hasattr(entry, "uuid"): dayone_attributes += "uuid: " + entry.uuid + "\n" - # TODO: copy over pictures, if present - # source directory is entry.journal.config['journal'] - # output directory is...? + if ( + hasattr(entry, "creator_device_agent") + or hasattr(entry, "creator_generation_date") + or hasattr(entry, "creator_host_name") + or hasattr(entry, "creator_os_agent") + or hasattr(entry, "creator_software_agent") + ): + dayone_attributes += "creator:\n" + if hasattr(entry, "creator_device_agent"): + dayone_attributes += f" device agent: {entry.creator_device_agent}\n" + if hasattr(entry, "creator_generation_date"): + dayone_attributes += " generation date: {}\n".format( + str(entry.creator_generation_date) + ) + if hasattr(entry, "creator_host_name"): + dayone_attributes += f" host name: {entry.creator_host_name}\n" + if hasattr(entry, "creator_os_agent"): + dayone_attributes += f" os agent: {entry.creator_os_agent}\n" + if hasattr(entry, "creator_software_agent"): + dayone_attributes += ( + f" software agent: {entry.creator_software_agent}\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, From ace4f576b2db471d718044e25f5c60ce6274051e Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sun, 26 Jan 2020 01:13:04 +0000 Subject: [PATCH 082/112] Updating changelog [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de50e8c7..3969df3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ **Implemented enhancements:** +- Update YAML exporter to handle Dayone format [\#773](https://github.com/jrnl-org/jrnl/pull/773) ([MinchinWeb](https://github.com/MinchinWeb)) - Full text search \(case insensitive\) with "-contains" [\#740](https://github.com/jrnl-org/jrnl/pull/740) ([empireshades](https://github.com/empireshades)) - Reduce startup time by 55% [\#719](https://github.com/jrnl-org/jrnl/pull/719) ([maebert](https://github.com/maebert)) - Refactor password logic to prevent accidental password leakage [\#708](https://github.com/jrnl-org/jrnl/pull/708) ([pspeter](https://github.com/pspeter)) From 5033bb3e30eeba896de22e1fd6ce6a67db75e389 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sun, 26 Jan 2020 01:35:23 +0000 Subject: [PATCH 083/112] Updating changelog [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3969df3d..e4dbdba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ **Build:** +- Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) - Change PyPI auth method in build pipeline [\#807](https://github.com/jrnl-org/jrnl/pull/807) ([wren](https://github.com/wren)) - Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren)) - Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren)) From 9383c6cda3c6a31d196ab2634d141d98c8e2ca21 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 25 Jan 2020 22:13:51 +0000 Subject: [PATCH 084/112] Incrementing version to v2.2-beta2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bc557beb..c4a7e41c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jrnl" -version = "v2.2-beta" +version = "v2.2-beta2" description = "Collect your thoughts and notes without leaving the command line." authors = [ "Manuel Ebert ", From ba3f688e8fd56bc1e0d1f3c73a2d16f926204b04 Mon Sep 17 00:00:00 2001 From: heymajor <13547412+heymajor@users.noreply.github.com> Date: Sat, 1 Feb 2020 16:12:30 -0500 Subject: [PATCH 085/112] Updating/expanding template explanation --- docs/recipes.md | 74 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 08e51711..b9335419 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -13,7 +13,7 @@ jrnl @alberto --tags | grep @melo And will get something like `@melo: 9`, meaning there are 9 entries where both `@alberto` and `@melo` are tagged. How does this work? First, -`jrnl @alberto` will filter the journal to only entries containing the +`jrnl @alberto` will filter the journal to only entries containing **the** tag `@alberto`, and then the `--tags` option will print out how often each tag occurred in this filtered journal. Finally, we pipe this to `grep` which will only display the line containing `@melo`. @@ -70,17 +70,61 @@ jrnlimport () { ### Using templates -Say you always want to use the same template for creating new entries. -If you have an [external editor](../advanced) set up, you can use this: +!!! note + Templates require an [external editor](../advanced) be configured. + +A template is a code snippet that makes it easier to enter use repeated text +each time a new journal entry is started. There are two ways you can utilize +templates in your entries. + +#### 1. Command line arguments + +If you had a `template.txt` file with the following contents: ```sh -jrnl < my_template.txt -jrnl -1 --edit +My Personal Journal +Title: + +Body: ``` -Another nice solution that allows you to define individual prompts comes -from [Jacobo de -Vera](https://github.com/maebert/jrnl/issues/194#issuecomment-47402869): +The `template.txt` file could be used to create a new entry with these +command line arguements: + +```sh +jrnl < template.txt # Imports template.txt as the most recent entry +jrnl -1 --edit # Opens the most recent entry in the editor +``` + +#### 2. Include the template file in `jrnl.yaml` + +A more efficient way to work with a template file is to declare the file +in your config file by changing the `template` setting from `false` to the +template file's path in double quotes: + +```sh +... +tagsymbols: "@" +template: "/path/to/template.txt" +timeformat: '%Y-%m-%d %H:%M' +version: "2.1.1" +``` + +Changes can be saved as you continue writing the journal entry and will be +logged as a new entry in the journal you specified in the original argument. + +!!! tip +To read your journal entry or to verify the entry saved, you can use this +command: `jrnl -n 1' (Check out [Import and Export](../export/#export-to-files) for more export options). + +```sh +jrnl -n 1 +``` + +### Prompts on shell reload + +If you'd like to be prompted each time you refresh your shell, you can include +this in your `.bash_profile`: ```sh function log_question() @@ -93,6 +137,11 @@ log_question 'What did I achieve today?' log_question 'What did I make progress with?' ``` +Whenever your shell is reloaded, you will be prompted to answer each of the +questions in the example above. Each answer will be logged as a separate +journal entry at the `default_hour` and `default_minute` listed in your +`jrnl.yaml` [config file](../advanced/#configuration-file). + ### Display random entry You can use this to select one title at random and then display the whole @@ -107,10 +156,11 @@ jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)" ## External editors -To use external editors for writing and editing journal entries, set -them up in your `jrnl.yaml` (see `advanced usage ` for -details). Generally, after writing an entry, you will have to save and -close the file to save the changes to jrnl. +Configure your preferred external editor by updating the `editor` option +in your `jrnl.yaml` file. (See [advanced usage](../advanced) for details). + +!!! note + To save and log any entry edits, save and close the file. ### Sublime Text From 0efd78cf3c6af1f8bc844cbc95095609ae05255d Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 1 Feb 2020 21:22:57 +0000 Subject: [PATCH 086/112] Updating changelog [ci skip] --- CHANGELOG.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de50e8c7..f86d2325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,19 @@ ## [Unreleased](https://github.com/jrnl-org/jrnl/) -[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1...HEAD) +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.2...HEAD) + +**Implemented enhancements:** + +- Update YAML exporter to handle Dayone format [\#773](https://github.com/jrnl-org/jrnl/pull/773) ([MinchinWeb](https://github.com/MinchinWeb)) + +**Build:** + +- Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) + +## [v2.2](https://pypi.org/project/jrnl/v2.2/) (2020-02-01) + +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.2-beta2...v2.2) **Implemented enhancements:** @@ -18,6 +30,8 @@ **Build:** +- Fix issue where jrnl would always out 'source' for version, fix Poetry config to build and publish properly [\#820](https://github.com/jrnl-org/jrnl/pull/820) ([wren](https://github.com/wren)) +- Unpin poetry [\#808](https://github.com/jrnl-org/jrnl/pull/808) ([wren](https://github.com/wren)) - Change PyPI auth method in build pipeline [\#807](https://github.com/jrnl-org/jrnl/pull/807) ([wren](https://github.com/wren)) - Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren)) - Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren)) @@ -30,6 +44,7 @@ **Updated documentation:** +- Explain how fish can be configured to exclude jrnl commands from history by default [\#809](https://github.com/jrnl-org/jrnl/pull/809) ([aureooms](https://github.com/aureooms)) - Remove merge marker in recipes.md [\#782](https://github.com/jrnl-org/jrnl/pull/782) ([markphelps](https://github.com/markphelps)) - Fix merge conflict left-over [\#767](https://github.com/jrnl-org/jrnl/pull/767) ([thejspr](https://github.com/thejspr)) - Display header in docs on mobile devices [\#763](https://github.com/jrnl-org/jrnl/pull/763) ([maebert](https://github.com/maebert)) From c4f7d481bd84b021b7f5b1d1164ddea284a27f47 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 1 Feb 2020 21:24:21 +0000 Subject: [PATCH 087/112] Incrementing version to v2.2 [ci skip] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c4a7e41c..beb98479 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jrnl" -version = "v2.2-beta2" +version = "v2.2" description = "Collect your thoughts and notes without leaving the command line." authors = [ "Manuel Ebert ", From 57e39b592f5ca56763b555130a3d6ef5d9342e0d Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 1 Feb 2020 14:51:19 -0800 Subject: [PATCH 088/112] Take out build system from pyproject config #781 We don't use it, and it breaks the homebrew install. --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index beb98479..e1dfcfbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,3 @@ black = {version = "^19.10b0",allow-prereleases = true} [tool.poetry.scripts] jrnl = 'jrnl.cli:run' -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" - From 436370b53818b7a6a3746ed681b0e28f451a7ad2 Mon Sep 17 00:00:00 2001 From: heymajor <13547412+heymajor@users.noreply.github.com> Date: Sat, 1 Feb 2020 17:52:33 -0500 Subject: [PATCH 089/112] Smoothing out formatting issues --- docs/recipes.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index b9335419..d688b429 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -13,7 +13,7 @@ jrnl @alberto --tags | grep @melo And will get something like `@melo: 9`, meaning there are 9 entries where both `@alberto` and `@melo` are tagged. How does this work? First, -`jrnl @alberto` will filter the journal to only entries containing **the** +`jrnl @alberto` will filter the journal to only entries containing the tag `@alberto`, and then the `--tags` option will print out how often each tag occurred in this filtered journal. Finally, we pipe this to `grep` which will only display the line containing `@melo`. @@ -104,18 +104,16 @@ template file's path in double quotes: ```sh ... -tagsymbols: "@" template: "/path/to/template.txt" -timeformat: '%Y-%m-%d %H:%M' -version: "2.1.1" +... ``` Changes can be saved as you continue writing the journal entry and will be logged as a new entry in the journal you specified in the original argument. !!! tip -To read your journal entry or to verify the entry saved, you can use this -command: `jrnl -n 1' (Check out [Import and Export](../export/#export-to-files) for more export options). + To read your journal entry or to verify the entry saved, you can use this + command: `jrnl -n 1` (Check out [Import and Export](../export/#export-to-files) for more export options). ```sh jrnl -n 1 From 6bf0ccfb2bbdafe3b3709d9976e42af2f18b53ff Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Wed, 5 Feb 2020 20:34:10 +0000 Subject: [PATCH 090/112] Updating changelog [ci skip] --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4dbdba8..1c635848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ ## [Unreleased](https://github.com/jrnl-org/jrnl/) +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.2...HEAD) + +**Implemented enhancements:** + +- Update YAML exporter to handle Dayone format [\#773](https://github.com/jrnl-org/jrnl/pull/773) ([MinchinWeb](https://github.com/MinchinWeb)) + +**Build:** + +- Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) + +**Updated documentation:** + +- Updating/clarifying template explanation [\#829](https://github.com/jrnl-org/jrnl/pull/829) ([heymajor](https://github.com/heymajor)) + +# Changelog + +## [Unreleased](https://github.com/jrnl-org/jrnl/) + [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1...HEAD) **Implemented enhancements:** From f40e0de4b9efbfeaf876beb0535df75f4c088dc7 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Thu, 6 Feb 2020 06:27:18 +0000 Subject: [PATCH 091/112] Updating changelog [ci skip] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f86d2325..28f147b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) +**Updated documentation:** + +- Updating/clarifying template explanation [\#829](https://github.com/jrnl-org/jrnl/pull/829) ([heymajor](https://github.com/heymajor)) + ## [v2.2](https://pypi.org/project/jrnl/v2.2/) (2020-02-01) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.2-beta2...v2.2) From b747de42f64de7ed40c9f72e4f3f7db37eb98b98 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Thu, 6 Feb 2020 06:28:47 +0000 Subject: [PATCH 092/112] Incrementing version to v2.2.1-beta2 [ci skip] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e1dfcfbd..9dc76df4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jrnl" -version = "v2.2" +version = "v2.2.1-beta2" description = "Collect your thoughts and notes without leaving the command line." authors = [ "Manuel Ebert ", From 2e7da235284b68ec6435c09fe5856328f04e27c6 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 8 Feb 2020 20:12:49 +0000 Subject: [PATCH 093/112] Updating changelog [ci skip] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec865924..99ee37ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,13 @@ **Build:** +- Remove poetry from build system in pyproject config to fix `brew install` [\#830](https://github.com/jrnl-org/jrnl/pull/830) ([wren](https://github.com/wren)) - Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) **Updated documentation:** - Updating/clarifying template explanation [\#829](https://github.com/jrnl-org/jrnl/pull/829) ([heymajor](https://github.com/heymajor)) - ## [v2.2](https://pypi.org/project/jrnl/v2.2/) (2020-02-01) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.2-beta2...v2.2) From 539eafd0bb71c2a9542b838d3ea6ede2a5a7119d Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 8 Feb 2020 12:13:53 -0800 Subject: [PATCH 094/112] Small fixes to changelog --- CHANGELOG.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ee37ec..7c0493e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ ## [v2.2](https://pypi.org/project/jrnl/v2.2/) (2020-02-01) -[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.2-beta2...v2.2) +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1...v2.2) **Implemented enhancements:** @@ -59,7 +59,7 @@ ## [v2.1.1](https://pypi.org/project/jrnl/v2.1.1/) (2019-11-26) -[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1-beta...v2.1.1) +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.post2...v2.1.1) **Implemented enhancements:** @@ -82,7 +82,7 @@ ## [v2.1.post2](https://pypi.org/project/jrnl/v2.1.post2/) (2019-11-11) -[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1-beta6...v2.1.post2) +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0.1...v2.1.post2) **Fixed bugs:** @@ -100,7 +100,7 @@ ## [v2.0.1](https://pypi.org/project/jrnl/v2.0.1/) (2019-09-26) -[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0.1-beta...v2.0.1) +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0.0...v2.0.1) **Implemented enhancements:** @@ -118,7 +118,9 @@ ## [v2.0.0](https://pypi.org/project/jrnl/v2.0.0/) (2019-08-24) -[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0-rc4...v2.0.0) +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/1.9.8...v2.0.0) + +🚨 **BREAKING CHANGES** 🚨 **Implemented enhancements:** - Change cryptographic backend from PyCrypto to cryptography.io From 630a8fcaec3d6f3c8c79fae27f462980d11dc5da Mon Sep 17 00:00:00 2001 From: Stav Shamir Date: Sat, 8 Feb 2020 22:18:01 +0200 Subject: [PATCH 095/112] Add test scenarios for the export feature (#824) * Fix behave keyword "and" to correct case "And" * Extract export_steps module * Add scenario for XML export * Add scenario for tags export * Add scenario for fancy export * Add scenario for yaml export * Remove unused module export.py * Run `make format` * Fix `create_directory` step --- features/exporting.feature | 73 ++++++++++++++++--- features/steps/core.py | 48 ------------- features/steps/export_steps.py | 124 +++++++++++++++++++++++++++++++++ jrnl/export.py | 62 ----------------- 4 files changed, 187 insertions(+), 120 deletions(-) create mode 100644 features/steps/export_steps.py delete mode 100644 jrnl/export.py diff --git a/features/exporting.feature b/features/exporting.feature index db2ef5b3..5705fda1 100644 --- a/features/exporting.feature +++ b/features/exporting.feature @@ -4,21 +4,20 @@ Feature: Exporting a Journal Given we use the config "tags.yaml" When we run "jrnl --export json" Then we should get no error - and the output should be parsable as json - and "entries" in the json output should have 2 elements - and "tags" in the json output should contain "@idea" - and "tags" in the json output should contain "@journal" - and "tags" in the json output should contain "@dan" + And the output should be parsable as json + And "entries" in the json output should have 2 elements + And "tags" in the json output should contain "@idea" + And "tags" in the json output should contain "@journal" + And "tags" in the json output should contain "@dan" Scenario: Exporting using filters should only export parts of the journal Given we use the config "tags.yaml" When we run "jrnl -until 'may 2013' --export json" - # Then we should get no error Then the output should be parsable as json - and "entries" in the json output should have 1 element - and "tags" in the json output should contain "@idea" - and "tags" in the json output should contain "@journal" - and "tags" in the json output should not contain "@dan" + And "entries" in the json output should have 1 element + And "tags" in the json output should contain "@idea" + And "tags" in the json output should contain "@journal" + And "tags" in the json output should not contain "@dan" Scenario: Exporting using custom templates Given we use the config "basic.yaml" @@ -83,3 +82,57 @@ Feature: Exporting a Journal More stuff more stuff again """ + + Scenario: Exporting to XML + Given we use the config "tags.yaml" + When we run "jrnl --export xml" + Then the output should be a valid XML string + And "entries" node in the xml output should have 2 elements + And "tags" in the xml output should contain ["@idea", "@journal", "@dan"] + + Scenario: Exporting tags + Given we use the config "tags.yaml" + When we run "jrnl --export tags" + Then the output should be + """ + @idea : 2 + @journal : 1 + @dan : 1 + """ + + Scenario: Exporting fancy + Given we use the config "tags.yaml" + When we run "jrnl --export fancy" + Then the output should be + """ + ┎──────────────────────────────────────────────────────────────╮2013-04-09 15:39 + ┃ I have an @idea: ╘═══════════════╕ + ┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + ┃ (1) write a command line @journal software │ + ┃ (2) ??? │ + ┃ (3) PROFIT! │ + ┖──────────────────────────────────────────────────────────────────────────────┘ + ┎──────────────────────────────────────────────────────────────╮2013-06-10 15:40 + ┃ I met with @dan. ╘═══════════════╕ + ┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + ┃ As alway's he shared his latest @idea on how to rule the world with me. │ + ┃ inst │ + ┖──────────────────────────────────────────────────────────────────────────────┘ + """ + + Scenario: Export to yaml + Given we use the config "tags.yaml" + And we created a directory named "exported_journal" + When we run "jrnl --export yaml -o exported_journal" + Then "exported_journal" should contain the files ["2013-04-09_i-have-an-idea.md", "2013-06-10_i-met-with-dan.md"] + And the content of exported yaml "exported_journal/2013-04-09_i-have-an-idea.md" should be + """ + title: I have an @idea: + date: 2013-04-09 15:39 + stared: False + tags: idea, journal + + (1) write a command line @journal software + (2) ??? + (3) PROFIT! + """ diff --git a/features/steps/core.py b/features/steps/core.py index af0a657e..0a3841c4 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -3,7 +3,6 @@ from unittest.mock import patch from behave import given, when, then from jrnl import cli, install, Journal, util, plugins from jrnl import __version__ -from dateutil import parser as date_parser from collections import defaultdict try: @@ -185,53 +184,6 @@ def no_error(context): assert context.exit_status == 0, context.exit_status -@then("the output should be parsable as json") -def check_output_json(context): - out = context.stdout_capture.getvalue() - assert json.loads(out), out - - -@then('"{field}" in the json output should have {number:d} elements') -@then('"{field}" in the json output should have 1 element') -def check_output_field(context, field, number=1): - out = context.stdout_capture.getvalue() - out_json = json.loads(out) - assert field in out_json, [field, out_json] - assert len(out_json[field]) == number, len(out_json[field]) - - -@then('"{field}" in the json output should not contain "{key}"') -def check_output_field_not_key(context, field, key): - out = context.stdout_capture.getvalue() - out_json = json.loads(out) - assert field in out_json - assert key not in out_json[field] - - -@then('"{field}" in the json output should contain "{key}"') -def check_output_field_key(context, field, key): - out = context.stdout_capture.getvalue() - out_json = json.loads(out) - assert field in out_json - assert key in out_json[field] - - -@then('the json output should contain {path} = "{value}"') -def check_json_output_path(context, path, value): - """ E.g. - the json output should contain entries.0.title = "hello" - """ - out = context.stdout_capture.getvalue() - struct = json.loads(out) - - for node in path.split("."): - try: - struct = struct[int(node)] - except ValueError: - struct = struct[node] - assert struct == value, struct - - @then("the output should be") @then('the output should be "{text}"') def check_output(context, text=None): diff --git a/features/steps/export_steps.py b/features/steps/export_steps.py new file mode 100644 index 00000000..b7965ab8 --- /dev/null +++ b/features/steps/export_steps.py @@ -0,0 +1,124 @@ +import json +import os +import shutil +from xml.etree import ElementTree + +from behave import then, given + + +@then("the output should be parsable as json") +def check_output_json(context): + out = context.stdout_capture.getvalue() + assert json.loads(out), out + + +@then('"{field}" in the json output should have {number:d} elements') +@then('"{field}" in the json output should have 1 element') +def check_output_field(context, field, number=1): + out = context.stdout_capture.getvalue() + out_json = json.loads(out) + assert field in out_json, [field, out_json] + assert len(out_json[field]) == number, len(out_json[field]) + + +@then('"{field}" in the json output should not contain "{key}"') +def check_output_field_not_key(context, field, key): + out = context.stdout_capture.getvalue() + out_json = json.loads(out) + assert field in out_json + assert key not in out_json[field] + + +@then('"{field}" in the json output should contain "{key}"') +def check_output_field_key(context, field, key): + out = context.stdout_capture.getvalue() + out_json = json.loads(out) + assert field in out_json + assert key in out_json[field] + + +@then('the json output should contain {path} = "{value}"') +def check_json_output_path(context, path, value): + """ E.g. + the json output should contain entries.0.title = "hello" + """ + out = context.stdout_capture.getvalue() + struct = json.loads(out) + + for node in path.split("."): + try: + struct = struct[int(node)] + except ValueError: + struct = struct[node] + assert struct == value, struct + + +@then("the output should be a valid XML string") +def assert_valid_xml_string(context): + output = context.stdout_capture.getvalue() + xml_tree = ElementTree.fromstring(output) + assert xml_tree, output + + +@then('"entries" node in the xml output should have {number:d} elements') +def assert_xml_output_entries_count(context, number): + output = context.stdout_capture.getvalue() + xml_tree = ElementTree.fromstring(output) + + xml_tags = (node.tag for node in xml_tree) + assert "entries" in xml_tags, str(list(xml_tags)) + + actual_entry_count = len(xml_tree.find("entries")) + assert actual_entry_count == number, actual_entry_count + + +@then('"tags" in the xml output should contain {expected_tags_json_list}') +def assert_xml_output_tags(context, expected_tags_json_list): + output = context.stdout_capture.getvalue() + xml_tree = ElementTree.fromstring(output) + + xml_tags = (node.tag for node in xml_tree) + assert "tags" in xml_tags, str(list(xml_tags)) + + expected_tags = json.loads(expected_tags_json_list) + actual_tags = set(t.attrib["name"] for t in xml_tree.find("tags")) + assert actual_tags == set(expected_tags), [actual_tags, set(expected_tags)] + + +@given('we created a directory named "{dir_name}"') +def create_directory(context, dir_name): + if os.path.exists(dir_name): + shutil.rmtree(dir_name) + os.mkdir(dir_name) + + +@then('"{dir_name}" should contain the files {expected_files_json_list}') +def assert_dir_contains_files(context, dir_name, expected_files_json_list): + actual_files = os.listdir(dir_name) + expected_files = json.loads(expected_files_json_list) + assert actual_files == expected_files, [actual_files, expected_files] + + +@then('the content of exported yaml "{file_path}" should be') +def assert_exported_yaml_file_content(context, file_path): + expected_content = context.text.strip().splitlines() + + with open(file_path, "r") as f: + actual_content = f.read().strip().splitlines() + + for actual_line, expected_line in zip(actual_content, expected_content): + if actual_line.startswith("tags: ") and expected_line.startswith("tags: "): + assert_equal_tags_ignoring_order(actual_line, expected_line) + else: + assert actual_line.strip() == expected_line.strip(), [ + actual_line.strip(), + expected_line.strip(), + ] + + +def assert_equal_tags_ignoring_order(actual_line, expected_line): + actual_tags = set(tag.strip() for tag in actual_line[len("tags: ") :].split(",")) + expected_tags = set( + tag.strip() for tag in expected_line[len("tags: ") :].split(",") + ) + assert actual_tags == expected_tags, [actual_tags, expected_tags] diff --git a/jrnl/export.py b/jrnl/export.py deleted file mode 100644 index e95d4c12..00000000 --- a/jrnl/export.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python - -from .util import ERROR_COLOR, RESET_COLOR -from .util import slugify -from .plugins.template import Template -import os - - -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) - self.template = Template(body) - - def export_entry(self, entry): - """Returns a string representation of a single entry.""" - return str(entry) - - def _get_vars(self, journal): - return {"journal": journal, "entries": journal.entries, "tags": journal.tags} - - def export_journal(self, 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 open(path, "w", encoding="utf-8") as f: - f.write(self.export_journal(journal)) - 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_{}.{}".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 open(full_path, "w", encoding="utf-8") as f: - f.write(self.export_entry(entry)) - 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 string if output is None.""" - if output and os.path.isdir(output): # multiple files - return self.write_files(journal, output) - elif output: # single file - return self.write_file(journal, output) - else: - return self.export_journal(journal) From 550347eab1101e3eef38d03dcc0b8531ec5585b5 Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Sat, 8 Feb 2020 13:34:52 -0700 Subject: [PATCH 096/112] Add UTC support for failing DayOne tests (#785) * [Dayone] don't break if the system timezone is UTC * [DayOne] re-enable tests that were failing on Travis * [DayOne] change as per code review to avoid `except: pass` --- features/dayone.feature | 13 +------------ jrnl/DayOneJournal.py | 6 +++++- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/features/dayone.feature b/features/dayone.feature index 51aa2033..8e50b42b 100644 --- a/features/dayone.feature +++ b/features/dayone.feature @@ -1,7 +1,5 @@ Feature: Dayone specific implementation details. - # fails when system time is UTC (as on Travis-CI) - @skip Scenario: Loading a DayOne Journal Given we use the config "dayone.yaml" When we run "jrnl -from 'feb 2013'" @@ -15,7 +13,7 @@ Feature: Dayone specific implementation details. 2013-07-17 11:38 This entry is starred! """ - # fails when system time is UTC (as on Travis-CI) + # broken still @skip Scenario: Entries without timezone information will be interpreted as in the current timezone Given we use the config "dayone.yaml" @@ -23,7 +21,6 @@ Feature: Dayone specific implementation details. Then we should get no error and the output should contain "2013-01-17T18:37Z" in the local time - @skip Scenario: Writing into Dayone Given we use the config "dayone.yaml" When we run "jrnl 01 may 1979: Being born hurts." @@ -33,8 +30,6 @@ Feature: Dayone specific implementation details. 1979-05-01 09:00 Being born hurts. """ - # fails when system time is UTC (as on Travis-CI) - @skip Scenario: Loading tags from a DayOne Journal Given we use the config "dayone.yaml" When we run "jrnl --tags" @@ -44,8 +39,6 @@ Feature: Dayone specific implementation details. @play : 1 """ - # fails when system time is UTC (as on Travis-CI) - @skip Scenario: Saving tags from a DayOne Journal Given we use the config "dayone.yaml" When we run "jrnl A hard day at @work" @@ -56,8 +49,6 @@ Feature: Dayone specific implementation details. @play : 1 """ - # fails when system time is UTC (as on Travis-CI) - @skip Scenario: Filtering by tags from a DayOne Journal Given we use the config "dayone.yaml" When we run "jrnl @work" @@ -66,8 +57,6 @@ Feature: Dayone specific implementation details. 2013-05-17 11:39 This entry has tags! """ - # fails when system time is UTC (as on Travis-CI) - @skip Scenario: Exporting dayone to json Given we use the config "dayone.yaml" When we run "jrnl --export json" diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py index 83eb6788..8e8b2cd0 100644 --- a/jrnl/DayOneJournal.py +++ b/jrnl/DayOneJournal.py @@ -52,7 +52,11 @@ class DayOne(Journal.Journal): except (KeyError, pytz.exceptions.UnknownTimeZoneError): timezone = tzlocal.get_localzone() date = dict_entry["Creation Date"] - date = date + timezone.utcoffset(date, is_dst=False) + # convert the date to UTC rather than keep messing with + # timezones + if timezone.zone != "UTC": + date = date + timezone.utcoffset(date, is_dst=False) + entry = Entry.Entry( self, date, From 0cb6f5ec928bfc897b1dd88cc16289f279cf985e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Ooms?= Date: Sat, 8 Feb 2020 16:53:01 -0400 Subject: [PATCH 097/112] Fix typo in encryption docs (#812) * Explain how fish can be configured to exclude jrnl commands from history by default * Fix typo in encryption docs --- docs/encryption.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/encryption.md b/docs/encryption.md index 35a26313..dc3216d1 100644 --- a/docs/encryption.md +++ b/docs/encryption.md @@ -60,7 +60,7 @@ If you are using `fish` instead of `bash` or `zsh`, you can get the same behavio adding this to your `fish` configuration: ``` sh -abbr jrnl=" jrnl" +abbr jrnl " jrnl" ``` To delete existing `jrnl` commands from `fish`’s history, run From 7ac3e3160d7e7afaf1025cdd9309d048b1f1d7ac Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 8 Feb 2020 21:06:40 +0000 Subject: [PATCH 098/112] Updating changelog [ci skip] --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0493e6..c25fd5fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,12 @@ - Update YAML exporter to handle Dayone format [\#773](https://github.com/jrnl-org/jrnl/pull/773) ([MinchinWeb](https://github.com/MinchinWeb)) +**Fixed bugs:** + +- Add UTC support for failing DayOne tests [\#785](https://github.com/jrnl-org/jrnl/pull/785) ([MinchinWeb](https://github.com/MinchinWeb)) + **Build:** -- Remove poetry from build system in pyproject config to fix `brew install` [\#830](https://github.com/jrnl-org/jrnl/pull/830) ([wren](https://github.com/wren)) - Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) **Updated documentation:** From e5d7845f180d7b6f44d9e0bbd4d7aa2bfa7d425f Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Sat, 8 Feb 2020 14:14:33 -0700 Subject: [PATCH 099/112] Listing all entries in DayOne Classic journal throws IndexError (#786) * Reproduce bug in #780 :musical_note: I have no body, no body to love me... :musical_note: The bug is cause by a DayOne entry that has to entry body. * Deal with empty bodies Close #780. * [Travis-CI] add "tree" command to debug missing files * Fix file location I have no idea why, but it ran locally fine without issue. Travis is more particular... --- .travis.yml | 6 ++++ features/data/configs/bug780.yaml | 12 +++++++ .../48A25033B34047C591160A4480197D8B.doentry | 33 +++++++++++++++++++ features/regression.feature | 20 +++++++---- jrnl/Entry.py | 2 +- 5 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 features/data/configs/bug780.yaml create mode 100644 features/data/journals/bug780.dayone/entries/48A25033B34047C591160A4480197D8B.doentry diff --git a/.travis.yml b/.travis.yml index af825d4f..5f977e76 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,14 @@ git: depth: false autocrlf: false +addons: + apt: + packages: + - tree + before_install: - date + - tree install: - pip install poetry diff --git a/features/data/configs/bug780.yaml b/features/data/configs/bug780.yaml new file mode 100644 index 00000000..e1d830c2 --- /dev/null +++ b/features/data/configs/bug780.yaml @@ -0,0 +1,12 @@ +default_hour: 9 +default_minute: 0 +editor: '' +encrypt: false +highlight: true +journals: + default: features/journals/bug780.dayone +linewrap: 80 +tagsymbols: '@' +template: false +timeformat: '%Y-%m-%d %H:%M' +indent_character: "|" diff --git a/features/data/journals/bug780.dayone/entries/48A25033B34047C591160A4480197D8B.doentry b/features/data/journals/bug780.dayone/entries/48A25033B34047C591160A4480197D8B.doentry new file mode 100644 index 00000000..426f1ea8 --- /dev/null +++ b/features/data/journals/bug780.dayone/entries/48A25033B34047C591160A4480197D8B.doentry @@ -0,0 +1,33 @@ + + + + + Activity + Stationary + Creation Date + 2019-12-30T21:28:54Z + Entry Text + + Starred + + UUID + 48A25033B34047C591160A4480197D8B + Creator + + Device Agent + PC + Generation Date + 2019-12-30T21:28:54Z + Host Name + LE-TREPORT + OS Agent + Microsoft Windows/10 Home + Software Agent + Journaley/2.1 + + Tags + + i_have_no_body + + + diff --git a/features/regression.feature b/features/regression.feature index bedc1295..a8c1e237 100644 --- a/features/regression.feature +++ b/features/regression.feature @@ -15,13 +15,6 @@ Feature: Zapped bugs should stay dead. Then we should see the message "Entry added" and the journal should contain "[2013-11-30 15:42] Project Started." - Scenario: Date in the future should be parsed correctly - # https://github.com/maebert/jrnl/issues/185 - Given we use the config "basic.yaml" - When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019." - Then we should see the message "Entry added" - and the journal should contain "[2019-06-26 09:00] Planet?" - Scenario: Loading entry with ambiguous time stamp #https://github.com/maebert/jrnl/issues/153 Given we use the config "bug153.yaml" @@ -32,6 +25,19 @@ Feature: Zapped bugs should stay dead. 2013-10-27 03:27 Some text. """ + Scenario: Date in the future should be parsed correctly + # https://github.com/maebert/jrnl/issues/185 + Given we use the config "basic.yaml" + When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019." + Then we should see the message "Entry added" + and the journal should contain "[2019-06-26 09:00] Planet?" + + Scenario: Empty DayOne entry bodies should not error + # https://github.com/jrnl-org/jrnl/issues/780 + Given we use the config "bug780.yaml" + When we run "jrnl --short" + Then we should get no error + Scenario: Title with an embedded period. Given we use the config "basic.yaml" When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic." diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 80754e05..01f56d5a 100755 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -22,7 +22,7 @@ class Entry: def _parse_text(self): raw_text = self.text lines = raw_text.splitlines() - if lines[0].strip().endswith("*"): + if lines and lines[0].strip().endswith("*"): self.starred = True raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:]) self._title, self._body = split_title(raw_text) From 4ab9fa573c2387fe5d0219391a3178fed768197e Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 8 Feb 2020 21:26:08 +0000 Subject: [PATCH 100/112] Updating changelog [ci skip] --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c25fd5fb..e0af57a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,14 @@ **Fixed bugs:** +- Listing all entries in DayOne Classic journal throws IndexError [\#786](https://github.com/jrnl-org/jrnl/pull/786) ([MinchinWeb](https://github.com/MinchinWeb)) - Add UTC support for failing DayOne tests [\#785](https://github.com/jrnl-org/jrnl/pull/785) ([MinchinWeb](https://github.com/MinchinWeb)) **Build:** +- Remove poetry from build system in pyproject config to fix `brew install` [\#830](https://github.com/jrnl-org/jrnl/pull/830) ([wren](https://github.com/wren)) - Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) +- Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren)) **Updated documentation:** From 89e0bbc3fb46b4f17b619692dc87ecbd6e573e9e Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 8 Feb 2020 13:26:44 -0800 Subject: [PATCH 101/112] Fix crashing unicode Travis tests on Windows and fail build if Windows tests fail --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index af825d4f..0ddf4c61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,7 +51,6 @@ jobs: fast_finish: true allow_failures: - python: nightly - - os: windows include: - name: Lint, via Black @@ -74,6 +73,7 @@ jobs: env: - JRNL_PYTHON_VERSION=3.6.8 - PATH=/c/Python36:/c/Python36/Scripts:$PATH + - PYTHONIOENCODING=UTF-8 # Python 3.7 Tests - name: Python 3.7 on Linux @@ -89,6 +89,7 @@ jobs: env: - JRNL_PYTHON_VERSION=3.7.5 - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - PYTHONIOENCODING=UTF-8 # Python 3.8 Tests - name: Python 3.8 on Linux @@ -104,6 +105,7 @@ jobs: env: - JRNL_PYTHON_VERSION=3.8.0 - PATH=/c/Python38:/c/Python38/Scripts:$PATH + - PYTHONIOENCODING=UTF-8 # ... and beyond! - name: Python nightly on Linux From 40b330e5ee26861b4df0aceab70cc6b74e8ac86b Mon Sep 17 00:00:00 2001 From: micahellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 8 Feb 2020 13:51:14 -0800 Subject: [PATCH 102/112] Fix crashing unicode Travis tests on Windows and fail build if Windows tests fail (#836) --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5f977e76..4753a731 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,7 +57,6 @@ jobs: fast_finish: true allow_failures: - python: nightly - - os: windows include: - name: Lint, via Black @@ -80,6 +79,7 @@ jobs: env: - JRNL_PYTHON_VERSION=3.6.8 - PATH=/c/Python36:/c/Python36/Scripts:$PATH + - PYTHONIOENCODING=UTF-8 # Python 3.7 Tests - name: Python 3.7 on Linux @@ -95,6 +95,7 @@ jobs: env: - JRNL_PYTHON_VERSION=3.7.5 - PATH=/c/Python37:/c/Python37/Scripts:$PATH + - PYTHONIOENCODING=UTF-8 # Python 3.8 Tests - name: Python 3.8 on Linux @@ -110,6 +111,7 @@ jobs: env: - JRNL_PYTHON_VERSION=3.8.0 - PATH=/c/Python38:/c/Python38/Scripts:$PATH + - PYTHONIOENCODING=UTF-8 # ... and beyond! - name: Python nightly on Linux From aaa2efddec9e0da2e8fcbabb0e2f7689eace2251 Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 8 Feb 2020 13:54:39 -0800 Subject: [PATCH 103/112] Restore emoji test now that it works in Windows Travis builds --- features/core.feature | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/features/core.feature b/features/core.feature index c023cd4c..34539efb 100644 --- a/features/core.feature +++ b/features/core.feature @@ -41,6 +41,14 @@ Feature: Basic reading and writing to a journal When we run "jrnl -on 'june 6 2013' --short" Then the output should be "2013-06-10 15:40 Life is good." + Scenario: Emoji support + Given we use the config "basic.yaml" + When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘" + Then we should see the message "Entry added" + When we run "jrnl -n 1" + Then the output should contain "🌞" + and the output should contain "🐘" + Scenario: Writing an entry at the prompt Given we use the config "basic.yaml" When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive." From d495f6feaa5bccfcac7e4a69c90133517816888f Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 8 Feb 2020 22:03:03 +0000 Subject: [PATCH 104/112] Updating changelog [ci skip] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0af57a4..12344d7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,10 @@ **Build:** +- Fix crashing unicode Travis tests on Windows and fail build if Windows tests fail [\#836](https://github.com/jrnl-org/jrnl/pull/836) ([micahellison](https://github.com/micahellison)) - Remove poetry from build system in pyproject config to fix `brew install` [\#830](https://github.com/jrnl-org/jrnl/pull/830) ([wren](https://github.com/wren)) - Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) -- Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren)) +- Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren)) **Updated documentation:** From 686b111e82e126542b27e3eff1ec54783d7304bb Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 8 Feb 2020 17:33:47 -0800 Subject: [PATCH 105/112] Stop multipe changelog generators from crashing into each other --- .build/generate_changelog.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.build/generate_changelog.sh b/.build/generate_changelog.sh index 58867c20..ea586f4c 100755 --- a/.build/generate_changelog.sh +++ b/.build/generate_changelog.sh @@ -1,5 +1,20 @@ #!/usr/bin/env bash +BRANCH=$TRAVIS_BRANCH +if [[ $TRAVIS_BRANCH == $TRAVIS_TAG ]]; then + BRANCH='master' +fi + +# Check if branch has been updated since this build started +# This tends to happen if multiple things have been merged in at the same time. +if [[ -z $TRAVIS_TAG ]]; then + git fetch origin + if [[ $(git rev-parse "origin/${BRANCH}") != $TRAVIS_COMMIT ]]; then + echo "${BRANCH} has been updated since build started. Aborting changelog." + exit 0 + fi +fi + FILENAME='CHANGELOG.md' # get the latest git tags @@ -40,11 +55,6 @@ docker run -it --rm -v "$(pwd)":/usr/local/src/your-app ferrarimarco/github-chan # Put back our link (instead of the broken one) sed -i 's!https://pypi.org/project/jrnl/HEAD/!https://github.com/jrnl-org/jrnl/!' "$FILENAME" -BRANCH=$TRAVIS_BRANCH -if [[ $TRAVIS_BRANCH == $TRAVIS_TAG ]]; then - BRANCH='master' -fi - git config --global user.email "jrnl.bot@gmail.com" git config --global user.name "Jrnl Bot" git checkout $BRANCH From 28c0d848455b99f9ad74e04e3a386f05d14dc6f5 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Sat, 8 Feb 2020 18:20:06 -0800 Subject: [PATCH 106/112] get rid of debug code --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4753a731..0ddf4c61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,14 +9,8 @@ git: depth: false autocrlf: false -addons: - apt: - packages: - - tree - before_install: - date - - tree install: - pip install poetry From b76cd3dc4960545ea048254899e34a53562cbfbb Mon Sep 17 00:00:00 2001 From: Jonathan Wren <9453067+wren@users.noreply.github.com> Date: Sat, 8 Feb 2020 18:31:47 -0800 Subject: [PATCH 107/112] Update old links (#844) Since we moved the core repo to jrnl-org, the old links no longer work. --- docs/theme/index.html | 6 +++--- features/regression.feature | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/theme/index.html b/docs/theme/index.html index a0f620fb..2221e76f 100755 --- a/docs/theme/index.html +++ b/docs/theme/index.html @@ -34,7 +34,7 @@ "operatingSystem": ["macOS", "Windows", "Linux"], "thumbnailUrl": "https://jrnl.sh/img/banner_og.png", "installUrl": "https://jrnl.sh/installation", - "softwareVersion": "2.0.0rc2" + "softwareVersion": "2.2" } @@ -42,7 +42,7 @@
@@ -58,7 +58,7 @@
diff --git a/features/regression.feature b/features/regression.feature index a8c1e237..5aa1db06 100644 --- a/features/regression.feature +++ b/features/regression.feature @@ -1,7 +1,7 @@ Feature: Zapped bugs should stay dead. Scenario: Writing an entry does not print the entire journal - # https://github.com/maebert/jrnl/issues/87 + # https://github.com/jrnl-org/jrnl/issues/87 Given we use the config "basic.yaml" When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa." Then we should see the message "Entry added" @@ -9,14 +9,14 @@ Feature: Zapped bugs should stay dead. Then the output should not contain "Life is good" Scenario: Date with time should be parsed correctly - # https://github.com/maebert/jrnl/issues/117 + # https://github.com/jrnl-org/jrnl/issues/117 Given we use the config "basic.yaml" When we run "jrnl 2013-11-30 15:42: Project Started." Then we should see the message "Entry added" and the journal should contain "[2013-11-30 15:42] Project Started." Scenario: Loading entry with ambiguous time stamp - #https://github.com/maebert/jrnl/issues/153 + #https://github.com/jrnl-org/jrnl/issues/153 Given we use the config "bug153.yaml" When we run "jrnl -1" Then we should get no error @@ -26,7 +26,7 @@ Feature: Zapped bugs should stay dead. """ Scenario: Date in the future should be parsed correctly - # https://github.com/maebert/jrnl/issues/185 + # https://github.com/jrnl-org/jrnl/issues/185 Given we use the config "basic.yaml" When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019." Then we should see the message "Entry added" From 2be0436ef17aaf61cdeb23fbf677c2f44f454aa1 Mon Sep 17 00:00:00 2001 From: Jonathan Wren <9453067+wren@users.noreply.github.com> Date: Sat, 8 Feb 2020 18:32:30 -0800 Subject: [PATCH 108/112] Update site description (#841) Don't know why this was what it was. Maybe an old joke I don't get? Either way, new description makes more sense. --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index ed1795d1..4c56567b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,7 +12,7 @@ markdown_extensions: - admonition repo_url: https://github.com/jrnl-org/jrnl/ site_author: Manuel Ebert -site_description: Never Worry about Money Again. +site_description: Collect your thoughts and notes without leaving the command line. nav: - Overview: overview.md - Quickstart: installation.md From b2eab4bad38c551b7cb4c0a2e7b80b01068ae7d1 Mon Sep 17 00:00:00 2001 From: Jonathan Wren <9453067+wren@users.noreply.github.com> Date: Sat, 8 Feb 2020 18:32:58 -0800 Subject: [PATCH 109/112] Get rid of dumb sex joke (#840) These kinds of jokes make us look bad, and have gotten this project bad PR on social media. It's best to get rid of them. We can still be super glib and funny without crass jokes. --- docs/usage.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 18b35d68..deb612be 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -119,10 +119,10 @@ Will print all entries in which either `@pinkie` or `@WorldDomination` occurred. ```sh -jrnl -n 5 -and @pineapple @lubricant +jrnl -n 5 -and @pinkie @WorldDomination ``` -the last five entries containing both `@pineapple` **and** `@lubricant`. +the last five entries containing both `@pinkie` **and** `@worldDomination`. You can change which symbols you'd like to use for tagging in the configuration. @@ -154,7 +154,7 @@ encrypt) your edited journal after you save and exit the editor. You can also use this feature for deleting entries from your journal ```sh -jrnl @girlfriend -until 'june 2012' --edit +jrnl @texas -until 'june 2012' --edit ``` Just select all text, press delete, and everything is gone... From 7f1f878a08901e790ce7a58c54128dcf8f9f3226 Mon Sep 17 00:00:00 2001 From: Jonathan Wren <9453067+wren@users.noreply.github.com> Date: Sat, 8 Feb 2020 18:33:28 -0800 Subject: [PATCH 110/112] Don't re-run tests on deployment (#839) By the time we get to the deployment step, we've already run these tests dozens of times. We don't need to run them yet again at deploy time. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0ddf4c61..a602f51b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -131,6 +131,8 @@ jobs: - poetry version "$TRAVIS_TAG" - echo __version__ = \"$TRAVIS_TAG\" > jrnl/__version__.py - poetry build + script: + - echo "Deployment starting..." deploy: - provider: script script: poetry publish From 74a11cebdd996593162d2f663af10f2510d83cdc Mon Sep 17 00:00:00 2001 From: Jonathan Wren <9453067+wren@users.noreply.github.com> Date: Sat, 8 Feb 2020 18:33:54 -0800 Subject: [PATCH 111/112] Put back build lines in poetry config (#838) Taking out these lines earlier fixed the homebrew release, but broke other things. So, I'm putting them back for now until we can find a better solution. --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9dc76df4..c8c9984e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,3 +38,7 @@ black = {version = "^19.10b0",allow-prereleases = true} [tool.poetry.scripts] jrnl = 'jrnl.cli:run' +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + From 95aa3bc1aec9da88678981741f859de5e54cc52a Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sun, 9 Feb 2020 03:31:51 +0000 Subject: [PATCH 112/112] Updating changelog [ci skip] --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12344d7c..026c1718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,13 +15,18 @@ **Build:** +- Stop multipe changelog generators from crashing into each other [\#845](https://github.com/jrnl-org/jrnl/pull/845) ([wren](https://github.com/wren)) +- Don't re-run tests on deployment [\#839](https://github.com/jrnl-org/jrnl/pull/839) ([wren](https://github.com/wren)) +- Put back build lines in Poetry config [\#838](https://github.com/jrnl-org/jrnl/pull/838) ([wren](https://github.com/wren)) +- Restore emoji test [\#837](https://github.com/jrnl-org/jrnl/pull/837) ([micahellison](https://github.com/micahellison)) - Fix crashing unicode Travis tests on Windows and fail build if Windows tests fail [\#836](https://github.com/jrnl-org/jrnl/pull/836) ([micahellison](https://github.com/micahellison)) - Remove poetry from build system in pyproject config to fix `brew install` [\#830](https://github.com/jrnl-org/jrnl/pull/830) ([wren](https://github.com/wren)) - Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison)) -- Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren)) **Updated documentation:** +- Update site description [\#841](https://github.com/jrnl-org/jrnl/pull/841) ([wren](https://github.com/wren)) +- Get rid of dumb sex joke [\#840](https://github.com/jrnl-org/jrnl/pull/840) ([wren](https://github.com/wren)) - Updating/clarifying template explanation [\#829](https://github.com/jrnl-org/jrnl/pull/829) ([heymajor](https://github.com/heymajor)) ## [v2.2](https://pypi.org/project/jrnl/v2.2/) (2020-02-01)