From 5cf93115020ea9a94912fb923f820cc637a954a4 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 29 Oct 2022 21:39:27 +0000 Subject: [PATCH 01/12] Update changelog [ci skip] --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ec50c1b..2275b0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [Unreleased](https://github.com/jrnl-org/jrnl/) + +[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3...HEAD) + +**Deprecated:** + +- Drop Python 3.9 and use Python 3.11 official release [\#1611](https://github.com/jrnl-org/jrnl/pull/1611) ([micahellison](https://github.com/micahellison)) + ## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3-beta2...v3.3) From 44e2ace833955fba93447acda3a0aed803c66347 Mon Sep 17 00:00:00 2001 From: Micah Jerome Ellison Date: Sun, 30 Oct 2022 13:06:54 -0700 Subject: [PATCH 02/12] Add double encryption test (#1626) --- tests/bdd/features/encrypt.feature | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/bdd/features/encrypt.feature b/tests/bdd/features/encrypt.feature index 5c201361..b5ef1126 100644 --- a/tests/bdd/features/encrypt.feature +++ b/tests/bdd/features/encrypt.feature @@ -46,6 +46,22 @@ Feature: Encrypting and decrypting journals Then we should be prompted for a password And the output should contain "2013-06-10 15:40 Life is good" + Scenario: Encrypt journal twice and get prompted each time + Given we use the config "simple.yaml" + When we run "jrnl --encrypt" and enter + swordfish + swordfish + y + Then we should get no error + And the output should contain "Journal encrypted" + When we run "jrnl --encrypt" and enter + swordfish + swordfish + y + Then we should get no error + And the output should contain "Journal default is already encrypted. Create a new password." + And we should be prompted for a password + And the config for journal "default" should contain "encrypt: true" Scenario Outline: Running jrnl with encrypt: true on unencryptable journals Given we use the config "" From cd47070894d455f097769d7e82368e36f51c7d73 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Mon, 31 Oct 2022 08:55:23 -0700 Subject: [PATCH 03/12] Add rich handler for better-formatted debug logging (#1627) --- jrnl/cli.py | 8 ++++++-- jrnl/config.py | 6 +++++- jrnl/install.py | 4 +++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/jrnl/cli.py b/jrnl/cli.py index 7c692c4f..d8009df6 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -5,6 +5,8 @@ import logging import sys import traceback +from rich.logging import RichHandler + from jrnl.args import parse_args from jrnl.exception import JrnlException from jrnl.jrnl import run @@ -21,7 +23,9 @@ def configure_logger(debug=False): logging.basicConfig( level=logging.DEBUG, - format="%(levelname)-8s %(name)-12s %(message)s", + datefmt="[%X]", + format="%(message)s", + handlers=[RichHandler()], ) logging.getLogger("parsedatetime").setLevel(logging.INFO) logging.getLogger("keyring.backend").setLevel(logging.ERROR) @@ -34,7 +38,7 @@ def cli(manual_args=None): args = parse_args(manual_args) configure_logger(args.debug) - logging.debug("Parsed args: %s", args) + logging.debug("Parsed args:\n%s", args) status_code = run(args) diff --git a/jrnl/config.py b/jrnl/config.py index 8e5c5a14..e3290d8b 100644 --- a/jrnl/config.py +++ b/jrnl/config.py @@ -8,6 +8,7 @@ from typing import Callable import colorama import xdg.BaseDirectory +from rich.pretty import pretty_repr from ruamel.yaml import YAML from ruamel.yaml import constructor @@ -126,12 +127,15 @@ def scope_config(config, journal_name): if type(journal_conf) is dict: # We can override the default config on a by-journal basis logging.debug( - "Updating configuration with specific journal overrides %s", journal_conf + "Updating configuration with specific journal overrides:\n%s", + pretty_repr(journal_conf), ) config.update(journal_conf) else: # But also just give them a string to point to the journal file config["journal"] = journal_conf + + logging.debug("Scoped config:\n%s", pretty_repr(config)) return config diff --git a/jrnl/install.py b/jrnl/install.py index b20685f0..be0c7bc3 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -6,6 +6,8 @@ import logging import os import sys +from rich.pretty import pretty_repr + from jrnl.config import DEFAULT_JOURNAL_KEY from jrnl.config import get_config_path from jrnl.config import get_default_config @@ -101,7 +103,7 @@ def load_or_install_jrnl(alt_config_path): logging.debug("Configuration file not found, installing jrnl...") config = install() - logging.debug('Using configuration "%s"', config) + logging.debug('Using configuration:\n"%s"', pretty_repr(config)) return config From 9150f0798411e822ac3ddc1b260d70b98503c639 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Mon, 31 Oct 2022 15:58:15 +0000 Subject: [PATCH 04/12] Update changelog [ci skip] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2275b0b7..a8dc3517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3...HEAD) +**Implemented enhancements:** + +- Add `rich` handler to debug logging [\#1627](https://github.com/jrnl-org/jrnl/pull/1627) ([wren](https://github.com/wren)) + **Deprecated:** - Drop Python 3.9 and use Python 3.11 official release [\#1611](https://github.com/jrnl-org/jrnl/pull/1611) ([micahellison](https://github.com/micahellison)) From 51e9ce563888da6e1180486949c535146a728aea Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Mon, 31 Oct 2022 09:31:17 -0700 Subject: [PATCH 05/12] Move existing linting into `flakeheaven` (#1628) * add black and isort to flakeheaven * update lock file * clean up poe config * run formta on python blocks in markdown file * disable code for black being confused about markdown * add cache timeout for flakeheaven See: https://github.com/flakeheaven/flakeheaven/issues/71 --- docs/encryption.md | 13 ++++++------ poetry.lock | 38 +++++++++++++++++++++++++++++++++- pyproject.toml | 51 ++++++++++++++++------------------------------ 3 files changed, 62 insertions(+), 40 deletions(-) diff --git a/docs/encryption.md b/docs/encryption.md index 6dae9a48..b019e4a6 100644 --- a/docs/encryption.md +++ b/docs/encryption.md @@ -100,16 +100,16 @@ something like `pip3 install crytography`) import base64 import getpass from pathlib import Path + from cryptography.fernet import Fernet from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - filepath = input("journal file path: ") password = getpass.getpass("Password: ") -with open(Path(filepath),"rb") as f: +with open(Path(filepath), "rb") as f: ciphertext = f.read() password = password.encode("utf-8") @@ -123,7 +123,7 @@ kdf = PBKDF2HMAC( key = base64.urlsafe_b64encode(kdf.derive(password)) -print(Fernet(key).decrypt(ciphertext).decode('utf-8')) +print(Fernet(key).decrypt(ciphertext).decode("utf-8")) ``` **Example for jrnl v1 files**: @@ -137,18 +137,19 @@ like `pip3 install pycrypto`) """ import argparse -from Crypto.Cipher import AES import getpass import hashlib +from Crypto.Cipher import AES + parser = argparse.ArgumentParser() parser.add_argument("filepath", help="journal file to decrypt") args = parser.parse_args() pwd = getpass.getpass() -key = hashlib.sha256(pwd.encode('utf-8')).digest() +key = hashlib.sha256(pwd.encode("utf-8")).digest() -with open(args.filepath, 'rb') as f: +with open(args.filepath, "rb") as f: ciphertext = f.read() crypto = AES.new(key, AES.MODE_CBC, ciphertext[:16]) diff --git a/poetry.lock b/poetry.lock index 2dcfe584..36475675 100644 --- a/poetry.lock +++ b/poetry.lock @@ -229,6 +229,34 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" +[[package]] +name = "flake8-black" +version = "0.3.3" +description = "flake8 plugin to call black as a code style validator" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +black = ">=22.1.0" +flake8 = ">=3.0.0" +tomli = "*" + +[[package]] +name = "flake8-isort" +version = "5.0.0" +description = "flake8 plugin that integrates isort ." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = "*" +isort = ">=4.3.5,<6" + +[package.extras] +test = ["pytest"] + [[package]] name = "flakeheaven" version = "3.2.0" @@ -1130,7 +1158,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.10.0, <3.13" -content-hash = "e2a31438b3c6fbf90093531b3f877818d8dbf85e2c4f95e879888a3aa66a4ee3" +content-hash = "13e2102b7ddeb9ac4f1f2fddcfa6275d565c3eec9fa8da1b4657a02e20f900c9" [metadata.files] ansiwrap = [ @@ -1319,6 +1347,14 @@ flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] +flake8-black = [ + {file = "flake8-black-0.3.3.tar.gz", hash = "sha256:8211f5e20e954cb57c709acccf2f3281ce27016d4c4b989c3e51f878bb7ce12a"}, + {file = "flake8_black-0.3.3-py3-none-any.whl", hash = "sha256:7d667d0059fd1aa468de1669d77cc934b7f1feeac258d57bdae69a8e73c4cd90"}, +] +flake8-isort = [ + {file = "flake8-isort-5.0.0.tar.gz", hash = "sha256:e336f928c7edc509684930ab124414194b7f4e237c712af8fcbdf49d8747b10c"}, + {file = "flake8_isort-5.0.0-py3-none-any.whl", hash = "sha256:c73f9cbd1bf209887f602a27b827164ccfeba1676801b2aa23cb49051a1be79c"}, +] flakeheaven = [ {file = "flakeheaven-3.2.0-py3-none-any.whl", hash = "sha256:ec5a508c3db64d73128b65cb2a5a2c0a2d9f2e4b435e9fa2bcc03bf0df86da79"}, {file = "flakeheaven-3.2.0.tar.gz", hash = "sha256:225333d7bf309079f19a2c5f02d427fc7558a0d0c065944de88041ca94f5525c"}, diff --git a/pyproject.toml b/pyproject.toml index be70a1e4..695a1ea7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,8 @@ tzlocal = ">=4.0" # https://github.com/regebro/tzlocal/blob/master/CHANGES.txt [tool.poetry.dev-dependencies] black = { version = ">=21.5b2", allow-prereleases = true } flakeheaven = ">=3.0" +flake8-black = ">=0.3.3" +flake8-isort = ">=5.0.0" ipdb = "*" isort = ">=5.10" mkdocs = ">=1.0,<1.3" @@ -62,26 +64,6 @@ xmltodict = "*" jrnl = 'jrnl.cli:cli' [tool.poe.tasks] -format-run = [ - {cmd = "black ."}, -] -format-check = [ - {cmd = "black --version"}, - {cmd = "black --check --diff ."}, -] -style-check = [ - {cmd = "flakeheaven --version"}, - {cmd = "flakeheaven plugins"}, - {cmd = "flakeheaven lint"}, -] -sort-run = [ - {cmd = "isort ."}, -] -sort-check = [ - {cmd = "isort --version"}, - {cmd = "isort --check ."}, -] - docs-check.default_item_type = "script" docs-check.sequence = [ "tasks:delete_files(['sitemap.xml', 'config.json'])", @@ -100,22 +82,23 @@ test-run = [ {cmd = "tox -q -e py --"}, ] -installer-check = [ - {cmd = "poetry --version"}, - {cmd = "poetry check"}, +# Groups of tasks +format.default_item_type = "cmd" +format.sequence = [ + "isort .", + "black .", ] -# Groups of tasks -format = [ - "format-run", - "sort-run", -] -lint = [ - "installer-check", - "style-check", - "sort-check", - "format-check", +lint.env = { FLAKEHEAVEN_CACHE_TIMEOUT = "0" } +lint.default_item_type = "cmd" +lint.sequence = [ + "poetry --version", + "poetry check", + "flakeheaven --version", + "flakeheaven plugins", + "flakeheaven lint", ] + test = [ "lint", "test-run", @@ -169,6 +152,8 @@ pycodestyle = [ "-E70", "-W1*", "-W2*", "-W3*", "-W5*", ] +"flake8-*" = ["+*"] +flake8-black = ["-BLK901"] [build-system] From 8ad9e2bdd6b40476dcf81d9da4aff93a9342ccdb Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Tue, 1 Nov 2022 16:50:25 -0700 Subject: [PATCH 06/12] Fix bug where changelog is always slightly out of date on release tags (#1631) * fix issue where changelog is always slightly out of date on release tags * fix tag step running all the time per CR * update tag name to use more clear variable name --- .github/workflows/changelog.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml index c3f8037f..f8b4eca1 100644 --- a/.github/workflows/changelog.yaml +++ b/.github/workflows/changelog.yaml @@ -152,6 +152,14 @@ jobs: git commit -m "Update changelog [ci skip]" git push origin "$BRANCH" + - name: Update tag to include changelog + if: startsWith(env.GITHUB_REF, 'refs/tags/') + run: | + # This is a tag build (releases and prereleases) + # update the tag to include the changelog + git tag -fam "$GITHUB_REF_NAME" "$GITHUB_REF_NAME" + git push --tags --force + - name: Merge to Release branch if: env.FULL_RELEASE == 'true' run: | From 8e482321f2c8a61f428cab2ac8db2b9e90f21f72 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Tue, 1 Nov 2022 23:52:21 +0000 Subject: [PATCH 07/12] Update changelog [ci skip] --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8dc3517..f1de227b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ - Drop Python 3.9 and use Python 3.11 official release [\#1611](https://github.com/jrnl-org/jrnl/pull/1611) ([micahellison](https://github.com/micahellison)) +**Build:** + +- Fix bug where changelog is always slightly out of date on release tags [\#1631](https://github.com/jrnl-org/jrnl/pull/1631) ([wren](https://github.com/wren)) + +**Documentation:** + +- Document that editors must be blocking processes [\#1624](https://github.com/jrnl-org/jrnl/pull/1624) ([micahellison](https://github.com/micahellison)) + ## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3-beta2...v3.3) From e6130dbf4a7be094c09504ae723602c99c1ab7b8 Mon Sep 17 00:00:00 2001 From: Micah Jerome Ellison Date: Wed, 2 Nov 2022 19:53:32 -0700 Subject: [PATCH 08/12] Resolve failing GitHub Actions linting by replacing deprecated set-output command (#1632) * Replace deprecated set-output command * Resolve SC2086 - double quote to prevent globbing and word splitting * fix quotes in output command Co-authored-by: Jonathan Wren --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index eff2d991..4b55f253 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -111,7 +111,7 @@ jobs: id: pypi-version-getter run: | pypi_version="$(find dist/jrnl-*.tar.gz | sed -r 's!dist/jrnl-(.*)\.tar\.gz!\1!')" - echo "::set-output name=pypi_version::$pypi_version" + echo "pypi_version=$pypi_version" >> "$GITHUB_OUTPUT" release_homebrew: if: ${{ github.event.inputs.include_brew == 'true' }} From cb7e0ed2892db5e0e22adacb4c5bcfde743c437c Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Thu, 3 Nov 2022 02:55:34 +0000 Subject: [PATCH 09/12] Update changelog [ci skip] --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1de227b..d4d86379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,6 @@ - Fix bug where changelog is always slightly out of date on release tags [\#1631](https://github.com/jrnl-org/jrnl/pull/1631) ([wren](https://github.com/wren)) -**Documentation:** - -- Document that editors must be blocking processes [\#1624](https://github.com/jrnl-org/jrnl/pull/1624) ([micahellison](https://github.com/micahellison)) - ## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3-beta2...v3.3) From c1eb0c54a35049b46e0f718cea6cda823852fb93 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Thu, 3 Nov 2022 07:18:35 -0700 Subject: [PATCH 10/12] Add `type-checking` plugin to linting checks (#1629) * add type-checking plugin for flakeheaven * update lock file * fix type-checking issues in current codebase * run linters --- jrnl/exception.py | 8 ++++++-- jrnl/messages/Message.py | 7 +++++-- jrnl/override.py | 7 ++++--- poetry.lock | 30 +++++++++++++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/jrnl/exception.py b/jrnl/exception.py index 86140cd9..b4ec2be8 100644 --- a/jrnl/exception.py +++ b/jrnl/exception.py @@ -1,14 +1,18 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html -from jrnl.messages import Message +from typing import TYPE_CHECKING + from jrnl.output import print_msg +if TYPE_CHECKING: + from jrnl.messages import Message + class JrnlException(Exception): """Common exceptions raised by jrnl.""" - def __init__(self, *messages: Message): + def __init__(self, *messages: "Message"): self.messages = messages def print(self) -> None: diff --git a/jrnl/messages/Message.py b/jrnl/messages/Message.py index e4d6176a..bb193f5f 100644 --- a/jrnl/messages/Message.py +++ b/jrnl/messages/Message.py @@ -1,14 +1,17 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html +from typing import TYPE_CHECKING from typing import Mapping from typing import NamedTuple from jrnl.messages.MsgStyle import MsgStyle -from jrnl.messages.MsgText import MsgText + +if TYPE_CHECKING: + from jrnl.messages.MsgText import MsgText class Message(NamedTuple): - text: MsgText + text: "MsgText" style: MsgStyle = MsgStyle.NORMAL params: Mapping = {} diff --git a/jrnl/override.py b/jrnl/override.py index 4b6506ee..75a400b9 100644 --- a/jrnl/override.py +++ b/jrnl/override.py @@ -1,14 +1,15 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html - -from argparse import Namespace +from typing import TYPE_CHECKING from jrnl.config import make_yaml_valid_dict from jrnl.config import update_config +if TYPE_CHECKING: + from argparse import Namespace # import logging -def apply_overrides(args: Namespace, base_config: dict) -> dict: +def apply_overrides(args: "Namespace", base_config: dict) -> dict: """Unpack CLI provided overrides into the configuration tree. :param overrides: List of configuration key-value pairs collected from the CLI diff --git a/poetry.lock b/poetry.lock index 36475675..9b4d4c65 100644 --- a/poetry.lock +++ b/poetry.lock @@ -112,6 +112,14 @@ python-versions = ">=3.6.0" [package.extras] unicode-backport = ["unicodedata2"] +[[package]] +name = "classify-imports" +version = "4.2.0" +description = "Utilities for refactoring imports in python-like syntax." +category = "dev" +optional = false +python-versions = ">=3.7" + [[package]] name = "click" version = "8.1.3" @@ -257,6 +265,18 @@ isort = ">=4.3.5,<6" [package.extras] test = ["pytest"] +[[package]] +name = "flake8-type-checking" +version = "2.2.0" +description = "A flake8 plugin for managing type-checking imports & forward references" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +classify-imports = "*" +flake8 = "*" + [[package]] name = "flakeheaven" version = "3.2.0" @@ -1158,7 +1178,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.10.0, <3.13" -content-hash = "13e2102b7ddeb9ac4f1f2fddcfa6275d565c3eec9fa8da1b4657a02e20f900c9" +content-hash = "63f39baa62c8641eb6329472de340a9f06d9ffea3096a4095e90f98ce2986f91" [metadata.files] ansiwrap = [ @@ -1283,6 +1303,10 @@ charset-normalizer = [ {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, ] +classify-imports = [ + {file = "classify_imports-4.2.0-py2.py3-none-any.whl", hash = "sha256:dbbc264b70a470ed8c6c95976a11dfb8b7f63df44ed1af87328bbed2663f5161"}, + {file = "classify_imports-4.2.0.tar.gz", hash = "sha256:7abfb7ea92149b29d046bd34573d247ba6e68cc28100c801eba4af17964fc40e"}, +] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -1355,6 +1379,10 @@ flake8-isort = [ {file = "flake8-isort-5.0.0.tar.gz", hash = "sha256:e336f928c7edc509684930ab124414194b7f4e237c712af8fcbdf49d8747b10c"}, {file = "flake8_isort-5.0.0-py3-none-any.whl", hash = "sha256:c73f9cbd1bf209887f602a27b827164ccfeba1676801b2aa23cb49051a1be79c"}, ] +flake8-type-checking = [ + {file = "flake8_type_checking-2.2.0-py3-none-any.whl", hash = "sha256:c7d9d7adc6cd635a5a1a7859e5e0140f4f8f1705982a22db45872dd9acd49753"}, + {file = "flake8_type_checking-2.2.0.tar.gz", hash = "sha256:f7972fc9102f3f632ace1f4b1c5c20b900b8b7b529f04bb6c1fe0a11801e9658"}, +] flakeheaven = [ {file = "flakeheaven-3.2.0-py3-none-any.whl", hash = "sha256:ec5a508c3db64d73128b65cb2a5a2c0a2d9f2e4b435e9fa2bcc03bf0df86da79"}, {file = "flakeheaven-3.2.0.tar.gz", hash = "sha256:225333d7bf309079f19a2c5f02d427fc7558a0d0c065944de88041ca94f5525c"}, diff --git a/pyproject.toml b/pyproject.toml index 695a1ea7..e3c5592f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ black = { version = ">=21.5b2", allow-prereleases = true } flakeheaven = ">=3.0" flake8-black = ">=0.3.3" flake8-isort = ">=5.0.0" +flake8-type-checking = ">=2.2.0" ipdb = "*" isort = ">=5.10" mkdocs = ">=1.0,<1.3" From 30b41fdb88784cc2de2b203005869c55e4457a26 Mon Sep 17 00:00:00 2001 From: outa Date: Sat, 5 Nov 2022 23:29:50 +0100 Subject: [PATCH 11/12] Add type hints (#1614) * Add type hints * Fix linters * Add remaining type hints * Fix type-checking linter * Update jrnl/DayOneJournal.py Co-authored-by: Jonathan Wren --- jrnl/DayOneJournal.py | 12 +++---- jrnl/Entry.py | 24 ++++++++++---- jrnl/FolderJournal.py | 18 +++++++---- jrnl/args.py | 4 +-- jrnl/cli.py | 4 +-- jrnl/color.py | 14 +++++--- jrnl/config.py | 23 +++++++------ jrnl/editor.py | 4 +-- jrnl/install.py | 14 ++++---- jrnl/jrnl.py | 54 ++++++++++++++++++++----------- jrnl/os_compat.py | 6 ++-- jrnl/output.py | 20 ++++++------ jrnl/override.py | 4 +-- jrnl/path.py | 6 ++-- jrnl/plugins/__init__.py | 6 ++-- jrnl/plugins/dates_exporter.py | 9 ++++-- jrnl/plugins/fancy_exporter.py | 11 +++++-- jrnl/plugins/jrnl_importer.py | 6 +++- jrnl/plugins/json_exporter.py | 11 +++++-- jrnl/plugins/markdown_exporter.py | 9 ++++-- jrnl/plugins/tag_exporter.py | 10 ++++-- jrnl/plugins/text_exporter.py | 19 +++++++---- jrnl/plugins/util.py | 9 ++++-- jrnl/plugins/xml_exporter.py | 13 ++++++-- jrnl/plugins/yaml_exporter.py | 9 ++++-- jrnl/prompt.py | 2 +- jrnl/time.py | 8 +++-- jrnl/upgrade.py | 10 +++--- 28 files changed, 219 insertions(+), 120 deletions(-) diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py index d7f06249..4806bdbf 100644 --- a/jrnl/DayOneJournal.py +++ b/jrnl/DayOneJournal.py @@ -38,7 +38,7 @@ class DayOne(Journal.Journal): self.can_be_encrypted = False super().__init__(**kwargs) - def open(self): + def open(self) -> "DayOne": filenames = [] for root, dirnames, f in os.walk(self.config["journal"]): for filename in fnmatch.filter(f, "*.doentry"): @@ -113,7 +113,7 @@ class DayOne(Journal.Journal): self.sort() return self - def write(self): + def write(self) -> None: """Writes only the entries that have been modified into plist files.""" for entry in self.entries: if entry.modified: @@ -177,12 +177,12 @@ class DayOne(Journal.Journal): ) os.remove(filename) - def editable_str(self): + def editable_str(self) -> str: """Turns the journal into a string of entries that can be edited manually and later be parsed with eslf.parse_editable_str.""" return "\n".join([f"{str(e)}\n# {e.uuid}\n" for e in self.entries]) - def _update_old_entry(self, entry, new_entry): + def _update_old_entry(self, entry: Entry, new_entry: Entry) -> None: for attr in ("title", "body", "date"): old_attr = getattr(entry, attr) new_attr = getattr(new_entry, attr) @@ -190,7 +190,7 @@ class DayOne(Journal.Journal): entry.modified = True setattr(entry, attr, new_attr) - def _get_and_remove_uuid_from_entry(self, entry): + def _get_and_remove_uuid_from_entry(self, entry: Entry) -> Entry: uuid_regex = "^ *?# ([a-zA-Z0-9]+) *?$" m = re.search(uuid_regex, entry.body, re.MULTILINE) entry.uuid = m.group(1) if m else None @@ -201,7 +201,7 @@ class DayOne(Journal.Journal): return entry - def parse_editable_str(self, edited): + def parse_editable_str(self, edited: str) -> None: """Parses the output of self.editable_str and updates its entries.""" # Method: create a new list of entries from the edited text, then match # UUIDs of the new entries against self.entries, updating the entries diff --git a/jrnl/Entry.py b/jrnl/Entry.py index b8c3b640..9dfdf00f 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -5,15 +5,25 @@ import datetime import logging import os import re +from typing import TYPE_CHECKING import ansiwrap from .color import colorize from .color import highlight_tags_with_background_color +if TYPE_CHECKING: + from .Journal import Journal + class Entry: - def __init__(self, journal, date=None, text="", starred=False): + def __init__( + self, + journal: "Journal", + date: datetime.datetime | None = None, + text: str = "", + starred: bool = False, + ): self.journal = journal # Reference to journal mainly to access its config self.date = date or datetime.datetime.now() self.text = text @@ -24,7 +34,7 @@ class Entry: self.modified = False @property - def fulltext(self): + def fulltext(self) -> str: return self.title + " " + self.body def _parse_text(self): @@ -68,11 +78,11 @@ class Entry: self._tags = x @staticmethod - def tag_regex(tagsymbols): + def tag_regex(tagsymbols: str) -> re.Pattern: pattern = rf"(? set[str]: tagsymbols = self.journal.config["tagsymbols"] return { tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text) @@ -90,7 +100,7 @@ class Entry: body=self.body.rstrip("\n "), ) - def pprint(self, short=False): + def pprint(self, short: bool = False) -> str: """Returns a pretty-printed version of the entry. If short is true, only print the title.""" # Handle indentation @@ -197,7 +207,7 @@ class Entry: def __hash__(self): return hash(self.__repr__()) - def __eq__(self, other): + def __eq__(self, other: "Entry"): if ( not isinstance(other, Entry) or self.title.strip() != other.title.strip() @@ -230,7 +240,7 @@ SENTENCE_SPLITTER = re.compile( SENTENCE_SPLITTER_ONLY_NEWLINE = re.compile("\n") -def split_title(text): +def split_title(text: str) -> tuple[str, str]: """Splits the first sentence off from a text.""" sep = SENTENCE_SPLITTER_ONLY_NEWLINE.search(text.lstrip()) if not sep: diff --git a/jrnl/FolderJournal.py b/jrnl/FolderJournal.py index 25a927e1..ab9547ed 100644 --- a/jrnl/FolderJournal.py +++ b/jrnl/FolderJournal.py @@ -4,12 +4,16 @@ import codecs import fnmatch import os +from typing import TYPE_CHECKING from jrnl import Journal from jrnl import time +if TYPE_CHECKING: + from jrnl.Entry import Entry -def get_files(journal_config): + +def get_files(journal_config: str) -> list[str]: """Searches through sub directories starting with journal_config and find all text files""" filenames = [] for root, dirnames, f in os.walk(journal_config): @@ -21,13 +25,13 @@ def get_files(journal_config): class Folder(Journal.Journal): """A Journal handling multiple files in a folder""" - def __init__(self, name="default", **kwargs): + def __init__(self, name: str = "default", **kwargs): self.entries = [] self._diff_entry_dates = [] self.can_be_encrypted = False super().__init__(name, **kwargs) - def open(self): + def open(self) -> "Folder": filenames = [] self.entries = [] filenames = get_files(self.config["journal"]) @@ -38,7 +42,7 @@ class Folder(Journal.Journal): self.sort() return self - def write(self): + def write(self) -> None: """Writes only the entries that have been modified into proper files.""" # Create a list of dates of modified entries. Start with diff_entry_dates modified_dates = self._diff_entry_dates @@ -81,13 +85,13 @@ class Folder(Journal.Journal): if os.stat(filename).st_size <= 0: os.remove(filename) - def delete_entries(self, entries_to_delete): + def delete_entries(self, entries_to_delete: list["Entry"]) -> None: """Deletes specific entries from a journal.""" for entry in entries_to_delete: self.entries.remove(entry) self._diff_entry_dates.append(entry.date) - def change_date_entries(self, date): + def change_date_entries(self, date: str) -> None: """Changes entry dates to given date.""" date = time.parse(date) @@ -98,7 +102,7 @@ class Folder(Journal.Journal): self._diff_entry_dates.append(entry.date) entry.date = date - def parse_editable_str(self, edited): + def parse_editable_str(self, edited: str) -> None: """Parses the output of self.editable_str and updates its entries.""" mod_entries = self._parse(edited) diff_entries = set(self.entries) - set(mod_entries) diff --git a/jrnl/args.py b/jrnl/args.py index c6b0f1a0..f7f0eb3a 100644 --- a/jrnl/args.py +++ b/jrnl/args.py @@ -20,7 +20,7 @@ from jrnl.plugins import util class WrappingFormatter(argparse.RawTextHelpFormatter): """Used in help screen""" - def _split_lines(self, text, width): + def _split_lines(self, text: str, width: int) -> list[str]: text = text.split("\n\n") text = map(lambda t: self._whitespace_matcher.sub(" ", t).strip(), text) text = map(lambda t: textwrap.wrap(t, width=56), text) @@ -28,7 +28,7 @@ class WrappingFormatter(argparse.RawTextHelpFormatter): return text -def parse_args(args=[]): +def parse_args(args: list[str] = []) -> argparse.Namespace: """ Argument parsing that is doable before the config is available. Everything else goes into "text" for later parsing. diff --git a/jrnl/cli.py b/jrnl/cli.py index d8009df6..609a8aab 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -16,7 +16,7 @@ from jrnl.messages import MsgText from jrnl.output import print_msg -def configure_logger(debug=False): +def configure_logger(debug: bool = False) -> None: if not debug: logging.disable() return @@ -31,7 +31,7 @@ def configure_logger(debug=False): logging.getLogger("keyring.backend").setLevel(logging.ERROR) -def cli(manual_args=None): +def cli(manual_args: list[str] | None = None) -> int: try: if manual_args is None: manual_args = sys.argv[1:] diff --git a/jrnl/color.py b/jrnl/color.py index f5634bbd..91d56254 100644 --- a/jrnl/color.py +++ b/jrnl/color.py @@ -4,16 +4,20 @@ import re from string import punctuation from string import whitespace +from typing import TYPE_CHECKING import colorama from jrnl.os_compat import on_windows +if TYPE_CHECKING: + from jrnl.Entry import Entry + if on_windows(): colorama.init() -def colorize(string, color, bold=False): +def colorize(string: str, color: str, bold: bool = False) -> str: """Returns the string colored with colorama.Fore.color. If the color set by the user is "NONE" or the color doesn't exist in the colorama.Fore attributes, it returns the string without any modification.""" @@ -26,7 +30,9 @@ def colorize(string, color, bold=False): return colorama.Style.BRIGHT + color_escape + string + colorama.Style.RESET_ALL -def highlight_tags_with_background_color(entry, text, color, is_title=False): +def highlight_tags_with_background_color( + entry: "Entry", text: str, color: str, is_title: bool = False +) -> str: """ Takes a string and colorizes the tags in it based upon the config value for color.tags, while colorizing the rest of the text based on `color`. @@ -45,9 +51,9 @@ def highlight_tags_with_background_color(entry, text, color, is_title=False): :returns [(colorized_str, original_str)]""" for part in fragments: if part and part[0] not in config["tagsymbols"]: - yield (colorize(part, color, bold=is_title), part) + yield colorize(part, color, bold=is_title), part elif part: - yield (colorize(part, config["colors"]["tags"], bold=True), part) + yield colorize(part, config["colors"]["tags"], bold=True), part config = entry.journal.config if config["highlight"]: # highlight tags diff --git a/jrnl/config.py b/jrnl/config.py index e3290d8b..bcd67e2c 100644 --- a/jrnl/config.py +++ b/jrnl/config.py @@ -4,6 +4,7 @@ import argparse import logging import os +from typing import Any from typing import Callable import colorama @@ -57,7 +58,7 @@ def make_yaml_valid_dict(input: list) -> dict: return runtime_modifications -def save_config(config, alt_config_path=None): +def save_config(config: dict, alt_config_path: str | None = None) -> None: """Supply alt_config_path if using an alternate config through --config-file.""" config["version"] = __version__ @@ -72,7 +73,7 @@ def save_config(config, alt_config_path=None): yaml.dump(config, f) -def get_config_path(): +def get_config_path() -> str: try: config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) except FileExistsError: @@ -91,7 +92,7 @@ def get_config_path(): return os.path.join(config_directory_path or home_dir(), DEFAULT_CONFIG_NAME) -def get_default_config(): +def get_default_config() -> dict[str, Any]: return { "version": __version__, "journals": {"default": {"journal": get_default_journal_path()}}, @@ -114,12 +115,12 @@ def get_default_config(): } -def get_default_journal_path(): +def get_default_journal_path() -> str: journal_data_path = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or home_dir() return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME) -def scope_config(config, journal_name): +def scope_config(config: dict, journal_name: str) -> dict: if journal_name not in config["journals"]: return config config = config.copy() @@ -139,7 +140,7 @@ def scope_config(config, journal_name): return config -def verify_config_colors(config): +def verify_config_colors(config: dict) -> bool: """ Ensures the keys set for colors are valid colorama.Fore attributes, or "None" :return: True if all keys are set correctly, False otherwise @@ -164,7 +165,7 @@ def verify_config_colors(config): return all_valid_colors -def load_config(config_path): +def load_config(config_path: str) -> dict: """Tries to load a config file from YAML.""" try: with open(config_path, encoding=YAML_FILE_ENCODING) as f: @@ -187,13 +188,15 @@ def load_config(config_path): return yaml.load(f) -def is_config_json(config_path): +def is_config_json(config_path: str) -> bool: with open(config_path, "r", encoding="utf-8") as f: config_file = f.read() return config_file.strip().startswith("{") -def update_config(config, new_config, scope, force_local=False): +def update_config( + config: dict, new_config: dict, scope: str | None, force_local: bool = False +) -> None: """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""" @@ -206,7 +209,7 @@ def update_config(config, new_config, scope, force_local=False): config.update(new_config) -def get_journal_name(args, config): +def get_journal_name(args: argparse.Namespace, config: dict) -> argparse.Namespace: args.journal_name = DEFAULT_JOURNAL_KEY # The first arg might be a journal name diff --git a/jrnl/editor.py b/jrnl/editor.py index fd063f44..d578e2d8 100644 --- a/jrnl/editor.py +++ b/jrnl/editor.py @@ -17,7 +17,7 @@ from jrnl.os_compat import split_args from jrnl.output import print_msg -def get_text_from_editor(config, template=""): +def get_text_from_editor(config: dict, template: str = "") -> str: suffix = ".jrnl" if config["template"]: template_filename = Path(config["template"]).name @@ -50,7 +50,7 @@ def get_text_from_editor(config, template=""): return raw -def get_text_from_stdin(): +def get_text_from_stdin() -> str: print_msg( Message( MsgText.WritingEntryStart, diff --git a/jrnl/install.py b/jrnl/install.py index be0c7bc3..a37e51f9 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -27,7 +27,7 @@ from jrnl.prompt import yesno from jrnl.upgrade import is_old_version -def upgrade_config(config_data, alt_config_path=None): +def upgrade_config(config_data: dict, alt_config_path: str | None = None) -> None: """Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly. This essentially automatically ports jrnl installations if new config parameters are introduced in later versions. @@ -46,7 +46,7 @@ def upgrade_config(config_data, alt_config_path=None): ) -def find_default_config(): +def find_default_config() -> str: config_path = ( get_config_path() if os.path.exists(get_config_path()) @@ -55,7 +55,7 @@ def find_default_config(): return config_path -def find_alt_config(alt_config): +def find_alt_config(alt_config: str) -> str: if not os.path.exists(alt_config): raise JrnlException( Message( @@ -66,7 +66,7 @@ def find_alt_config(alt_config): return alt_config -def load_or_install_jrnl(alt_config_path): +def load_or_install_jrnl(alt_config_path: str) -> dict: """ If jrnl is already installed, loads and returns a default config object. If alternate config is specified via --config-file flag, it will be used. @@ -107,7 +107,7 @@ def load_or_install_jrnl(alt_config_path): return config -def install(): +def install() -> dict: _initialize_autocomplete() # Where to create the journal? @@ -143,7 +143,7 @@ def install(): return default_config -def _initialize_autocomplete(): +def _initialize_autocomplete() -> None: # readline is not included in Windows Active Python and perhaps some other distributions if sys.modules.get("readline"): import readline @@ -153,7 +153,7 @@ def _initialize_autocomplete(): readline.set_completer(_autocomplete_path) -def _autocomplete_path(text, state): +def _autocomplete_path(text: str, state: int) -> list[str | None]: expansions = glob.glob(expand_path(text) + "*") expansions = [e + "/" if os.path.isdir(e) else e for e in expansions] expansions.append(None) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 5c9ae9ac..39c25b3f 100644 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -1,8 +1,8 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html - import logging import sys +from typing import TYPE_CHECKING from jrnl import install from jrnl import plugins @@ -14,6 +14,7 @@ from jrnl.config import scope_config from jrnl.editor import get_text_from_editor from jrnl.editor import get_text_from_stdin from jrnl.exception import JrnlException +from jrnl.Journal import Journal from jrnl.Journal import open_journal from jrnl.messages import Message from jrnl.messages import MsgStyle @@ -23,8 +24,13 @@ from jrnl.output import print_msgs from jrnl.override import apply_overrides from jrnl.path import expand_path +if TYPE_CHECKING: + from argparse import Namespace -def run(args): + from jrnl.Entry import Entry + + +def run(args: "Namespace"): """ Flow: 1. Run standalone command if it doesn't require config (help, version, etc), then exit @@ -72,10 +78,8 @@ def run(args): search_mode(**kwargs) -def _is_write_mode(args, config, **kwargs): +def _is_write_mode(args: "Namespace", config: dict, **kwargs) -> bool: """Determines if we are in write mode (as opposed to search mode)""" - write_mode = True - # Are any search filters present? If so, then search mode. write_mode = not any( ( @@ -115,7 +119,7 @@ def _is_write_mode(args, config, **kwargs): return write_mode -def write_mode(args, config, journal, **kwargs): +def write_mode(args: "Namespace", config: dict, journal: Journal, **kwargs) -> None: """ Gets input from the user to write to the journal 1. Check for input from cli @@ -159,7 +163,7 @@ def write_mode(args, config, journal, **kwargs): logging.debug("Write mode: completed journal.write()") -def search_mode(args, journal, **kwargs): +def search_mode(args: "Namespace", journal: Journal, **kwargs) -> None: """ Search for entries in a journal, then either: 1. Send them to configured editor for user manipulation (and also @@ -213,7 +217,7 @@ def search_mode(args, journal, **kwargs): _display_search_results(**kwargs) -def _write_in_editor(config, template=None): +def _write_in_editor(config: dict, template: str | None = None) -> str: if config["editor"]: logging.debug("Write mode: opening editor") if not template: @@ -226,7 +230,7 @@ def _write_in_editor(config, template=None): return raw -def _get_editor_template(config, **kwargs): +def _get_editor_template(config: dict, **kwargs) -> str: logging.debug("Write mode: loading template for entry") if not config["template"]: @@ -251,7 +255,7 @@ def _get_editor_template(config, **kwargs): return template -def _has_search_args(args): +def _has_search_args(args: "Namespace") -> bool: return any( ( args.on_date, @@ -271,7 +275,7 @@ def _has_search_args(args): ) -def _filter_journal_entries(args, journal, **kwargs): +def _filter_journal_entries(args: "Namespace", journal: Journal, **kwargs) -> None: """Filter journal entries in-place based upon search args""" if args.on_date: args.start_date = args.end_date = args.on_date @@ -296,7 +300,7 @@ def _filter_journal_entries(args, journal, **kwargs): journal.limit(args.limit) -def _print_entries_found_count(count, args): +def _print_entries_found_count(count: int, args: "Namespace") -> None: if count == 0: if args.edit or args.change_time: print_msg(Message(MsgText.NothingToModify, MsgStyle.WARNING)) @@ -317,12 +321,14 @@ def _print_entries_found_count(count, args): print_msg(Message(my_msg, MsgStyle.NORMAL, {"num": count})) -def _other_entries(journal, entries): +def _other_entries(journal: Journal, entries: list["Entry"]) -> list["Entry"]: """Find entries that are not in journal""" return [e for e in entries if e not in journal.entries] -def _edit_search_results(config, journal, old_entries, **kwargs): +def _edit_search_results( + config: dict, journal: Journal, old_entries: list["Entry"], **kwargs +) -> None: """ 1. Send the given journal entries to the user-configured editor 2. Print out stats on any modifications to journal @@ -356,7 +362,9 @@ def _edit_search_results(config, journal, old_entries, **kwargs): journal.write() -def _print_edited_summary(journal, old_stats, **kwargs): +def _print_edited_summary( + journal: Journal, old_stats: dict[str, int], **kwargs +) -> None: stats = { "added": len(journal) - old_stats["count"], "deleted": old_stats["count"] - len(journal), @@ -395,11 +403,13 @@ def _print_edited_summary(journal, old_stats, **kwargs): print_msgs(msgs) -def _get_predit_stats(journal): +def _get_predit_stats(journal: Journal) -> dict[str, int]: return {"count": len(journal)} -def _delete_search_results(journal, old_entries, **kwargs): +def _delete_search_results( + journal: Journal, old_entries: list["Entry"], **kwargs +) -> None: entries_to_delete = journal.prompt_action_entries(MsgText.DeleteEntryQuestion) if entries_to_delete: @@ -409,7 +419,13 @@ def _delete_search_results(journal, old_entries, **kwargs): journal.write() -def _change_time_search_results(args, journal, old_entries, no_prompt=False, **kwargs): +def _change_time_search_results( + args: "Namespace", + journal: Journal, + old_entries: list["Entry"], + no_prompt: bool = False, + **kwargs +) -> None: # separate entries we are not editing other_entries = _other_entries(journal, old_entries) @@ -432,7 +448,7 @@ def _change_time_search_results(args, journal, old_entries, no_prompt=False, **k journal.write() -def _display_search_results(args, journal, **kwargs): +def _display_search_results(args: "Namespace", journal: Journal, **kwargs) -> None: # Get export format from config file if not provided at the command line args.export = args.export or kwargs["config"].get("display_format") diff --git a/jrnl/os_compat.py b/jrnl/os_compat.py index 2bb46bc5..ef6d878a 100644 --- a/jrnl/os_compat.py +++ b/jrnl/os_compat.py @@ -5,14 +5,14 @@ import shlex from sys import platform -def on_windows(): +def on_windows() -> bool: return "win32" in platform -def on_posix(): +def on_posix() -> bool: return not on_windows() -def split_args(args): +def split_args(args: str) -> list[str]: """Split arguments and add escape characters as appropriate for the OS""" return shlex.split(args, posix=on_posix()) diff --git a/jrnl/output.py b/jrnl/output.py index 2db0362e..0781263c 100644 --- a/jrnl/output.py +++ b/jrnl/output.py @@ -2,7 +2,7 @@ # License: https://www.gnu.org/licenses/gpl-3.0.html import textwrap -from typing import Union +from typing import Callable from rich.console import Console from rich.text import Text @@ -12,7 +12,9 @@ from jrnl.messages import MsgStyle from jrnl.messages import MsgText -def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs): +def deprecated_cmd( + old_cmd: str, new_cmd: str, callback: Callable | None = None, **kwargs +) -> None: print_msg( Message( MsgText.DeprecatedCommand, @@ -25,13 +27,13 @@ def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs): callback(**kwargs) -def journal_list_to_json(journal_list): +def journal_list_to_json(journal_list: dict) -> str: import json return json.dumps(journal_list) -def journal_list_to_yaml(journal_list): +def journal_list_to_yaml(journal_list: dict) -> str: from io import StringIO from ruamel.yaml import YAML @@ -41,7 +43,7 @@ def journal_list_to_yaml(journal_list): return output.getvalue() -def journal_list_to_stdout(journal_list): +def journal_list_to_stdout(journal_list: dict) -> str: result = f"Journals defined in config ({journal_list['config_path']})\n" ml = min(max(len(k) for k in journal_list["journals"]), 20) for journal, cfg in journal_list["journals"].items(): @@ -51,7 +53,7 @@ def journal_list_to_stdout(journal_list): return result -def list_journals(configuration, format=None): +def list_journals(configuration: dict, format: str | None = None) -> str: from jrnl import config """List the journals specified in the configuration file""" @@ -69,7 +71,7 @@ def list_journals(configuration, format=None): return journal_list_to_stdout(journal_list) -def print_msg(msg: Message, **kwargs) -> Union[None, str]: +def print_msg(msg: Message, **kwargs) -> str | None: """Helper function to print a single message""" kwargs["style"] = msg.style return print_msgs([msg], **kwargs) @@ -81,7 +83,7 @@ def print_msgs( style: MsgStyle = MsgStyle.NORMAL, get_input: bool = False, hide_input: bool = False, -) -> Union[None, str]: +) -> str | None: # Same as print_msg, but for a list text = Text("", end="") kwargs = style.decoration.args @@ -113,7 +115,7 @@ def _get_console(stderr: bool = True) -> Console: return Console(stderr=stderr) -def _add_extra_style_args_if_needed(args, msg): +def _add_extra_style_args_if_needed(args: dict, msg: Message): args["border_style"] = msg.style.color args["title"] = msg.style.box_title return args diff --git a/jrnl/override.py b/jrnl/override.py index 75a400b9..932add85 100644 --- a/jrnl/override.py +++ b/jrnl/override.py @@ -37,12 +37,12 @@ def apply_overrides(args: "Namespace", base_config: dict) -> dict: return base_config -def _get_key_and_value_from_pair(pairs): +def _get_key_and_value_from_pair(pairs: dict) -> tuple: key_as_dots, override_value = list(pairs.items())[0] return key_as_dots, override_value -def _convert_dots_to_list(key_as_dots): +def _convert_dots_to_list(key_as_dots: str) -> list[str]: keys = key_as_dots.split(".") keys = [k for k in keys if k != ""] # remove empty elements return keys diff --git a/jrnl/path.py b/jrnl/path.py index 0ec3492f..97e04ffe 100644 --- a/jrnl/path.py +++ b/jrnl/path.py @@ -4,13 +4,13 @@ import os.path -def home_dir(): +def home_dir() -> str: return os.path.expanduser("~") -def expand_path(path): +def expand_path(path: str) -> str: return os.path.expanduser(os.path.expandvars(path)) -def absolute_path(path): +def absolute_path(path: str) -> str: return os.path.abspath(expand_path(path)) diff --git a/jrnl/plugins/__init__.py b/jrnl/plugins/__init__.py index ebfe5d3f..f67cebaf 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -1,6 +1,8 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html +from typing import Type + from jrnl.plugins.dates_exporter import DatesExporter from jrnl.plugins.fancy_exporter import FancyExporter from jrnl.plugins.jrnl_importer import JRNLImporter @@ -32,14 +34,14 @@ EXPORT_FORMATS = sorted(__exporter_types.keys()) IMPORT_FORMATS = sorted(__importer_types.keys()) -def get_exporter(format): +def get_exporter(format: str) -> Type[TextExporter] | None: for exporter in __exporters: if hasattr(exporter, "names") and format in exporter.names: return exporter return None -def get_importer(format): +def get_importer(format: str) -> Type[JRNLImporter] | None: for importer in __importers: if hasattr(importer, "names") and format in importer.names: return importer diff --git a/jrnl/plugins/dates_exporter.py b/jrnl/plugins/dates_exporter.py index a5032f49..920c6ff9 100644 --- a/jrnl/plugins/dates_exporter.py +++ b/jrnl/plugins/dates_exporter.py @@ -2,9 +2,14 @@ # License: https://www.gnu.org/licenses/gpl-3.0.html from collections import Counter +from typing import TYPE_CHECKING from jrnl.plugins.text_exporter import TextExporter +if TYPE_CHECKING: + from jrnl.Entry import Entry + from jrnl.Journal import Journal + class DatesExporter(TextExporter): """This Exporter lists dates and their respective counts, for heatingmapping etc.""" @@ -13,11 +18,11 @@ class DatesExporter(TextExporter): extension = "dates" @classmethod - def export_entry(cls, entry): + def export_entry(cls, entry: "Entry"): raise NotImplementedError @classmethod - def export_journal(cls, journal): + def export_journal(cls, journal: "Journal") -> str: """Returns dates and their frequencies for an entire journal.""" date_counts = Counter() for entry in journal.entries: diff --git a/jrnl/plugins/fancy_exporter.py b/jrnl/plugins/fancy_exporter.py index 1eaf74c0..50189ab3 100644 --- a/jrnl/plugins/fancy_exporter.py +++ b/jrnl/plugins/fancy_exporter.py @@ -4,6 +4,7 @@ import logging import os from textwrap import TextWrapper +from typing import TYPE_CHECKING from jrnl.exception import JrnlException from jrnl.messages import Message @@ -11,6 +12,10 @@ from jrnl.messages import MsgStyle from jrnl.messages import MsgText from jrnl.plugins.text_exporter import TextExporter +if TYPE_CHECKING: + from jrnl.Entry import Entry + from jrnl.Journal import Journal + class FancyExporter(TextExporter): """This Exporter can convert entries and journals into text with unicode box drawing characters.""" @@ -35,7 +40,7 @@ class FancyExporter(TextExporter): border_m = "┘" @classmethod - def export_entry(cls, entry): + def export_entry(cls, entry: "Entry") -> str: """Returns a fancy unicode representation of a single entry.""" date_str = entry.date.strftime(entry.journal.config["timeformat"]) @@ -95,12 +100,12 @@ class FancyExporter(TextExporter): return "\n".join(card) @classmethod - def export_journal(cls, journal): + def export_journal(cls, journal) -> str: """Returns a unicode representation of an entire journal.""" return "\n".join(cls.export_entry(entry) for entry in journal) -def check_provided_linewrap_viability(linewrap, card, journal): +def check_provided_linewrap_viability(linewrap: int, card: list[str], journal: "Journal"): if len(card[0]) > linewrap: width_violation = len(card[0]) - linewrap raise JrnlException( diff --git a/jrnl/plugins/jrnl_importer.py b/jrnl/plugins/jrnl_importer.py index c69d4673..e93792de 100644 --- a/jrnl/plugins/jrnl_importer.py +++ b/jrnl/plugins/jrnl_importer.py @@ -2,6 +2,7 @@ # License: https://www.gnu.org/licenses/gpl-3.0.html import sys +from typing import TYPE_CHECKING from jrnl.exception import JrnlException from jrnl.messages import Message @@ -9,6 +10,9 @@ from jrnl.messages import MsgStyle from jrnl.messages import MsgText from jrnl.output import print_msg +if TYPE_CHECKING: + from jrnl.Journal import Journal + class JRNLImporter: """This plugin imports entries from other jrnl files.""" @@ -16,7 +20,7 @@ class JRNLImporter: names = ["jrnl"] @staticmethod - def import_(journal, input=None): + def import_(journal: "Journal", input: str | None = None) -> None: """Imports from an existing file if input is specified, and standard input otherwise.""" old_cnt = len(journal.entries) diff --git a/jrnl/plugins/json_exporter.py b/jrnl/plugins/json_exporter.py index ae153b70..1a3a5670 100644 --- a/jrnl/plugins/json_exporter.py +++ b/jrnl/plugins/json_exporter.py @@ -2,10 +2,15 @@ # License: https://www.gnu.org/licenses/gpl-3.0.html import json +from typing import TYPE_CHECKING from jrnl.plugins.text_exporter import TextExporter from jrnl.plugins.util import get_tags_count +if TYPE_CHECKING: + from jrnl.Entry import Entry + from jrnl.Journal import Journal + class JSONExporter(TextExporter): """This Exporter can convert entries and journals into json.""" @@ -14,7 +19,7 @@ class JSONExporter(TextExporter): extension = "json" @classmethod - def entry_to_dict(cls, entry): + def entry_to_dict(cls, entry: "Entry") -> dict: entry_dict = { "title": entry.title, "body": entry.body, @@ -49,12 +54,12 @@ class JSONExporter(TextExporter): return entry_dict @classmethod - def export_entry(cls, entry): + def export_entry(cls, entry: "Entry") -> str: """Returns a json representation of a single entry.""" return json.dumps(cls.entry_to_dict(entry), indent=2) + "\n" @classmethod - def export_journal(cls, journal): + def export_journal(cls, journal: "Journal") -> str: """Returns a json representation of an entire journal.""" tags = get_tags_count(journal) result = { diff --git a/jrnl/plugins/markdown_exporter.py b/jrnl/plugins/markdown_exporter.py index c3476d51..8f0d07b4 100644 --- a/jrnl/plugins/markdown_exporter.py +++ b/jrnl/plugins/markdown_exporter.py @@ -3,6 +3,7 @@ import os import re +from typing import TYPE_CHECKING from jrnl.messages import Message from jrnl.messages import MsgStyle @@ -10,6 +11,10 @@ from jrnl.messages import MsgText from jrnl.output import print_msg from jrnl.plugins.text_exporter import TextExporter +if TYPE_CHECKING: + from jrnl.Entry import Entry + from jrnl.Journal import Journal + class MarkdownExporter(TextExporter): """This Exporter can convert entries and journals into Markdown.""" @@ -18,7 +23,7 @@ class MarkdownExporter(TextExporter): extension = "md" @classmethod - def export_entry(cls, entry, to_multifile=True): + def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str: """Returns a markdown representation of a single entry.""" date_str = entry.date.strftime(entry.journal.config["timeformat"]) body_wrapper = "\n" if entry.body else "" @@ -73,7 +78,7 @@ class MarkdownExporter(TextExporter): return f"{heading} {date_str} {entry.title}\n{newbody} " @classmethod - def export_journal(cls, journal): + def export_journal(cls, journal: "Journal") -> str: """Returns a Markdown representation of an entire journal.""" out = [] year, month = -1, -1 diff --git a/jrnl/plugins/tag_exporter.py b/jrnl/plugins/tag_exporter.py index b9ab45b3..250f05c7 100644 --- a/jrnl/plugins/tag_exporter.py +++ b/jrnl/plugins/tag_exporter.py @@ -1,9 +1,15 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html +from typing import TYPE_CHECKING + from jrnl.plugins.text_exporter import TextExporter from jrnl.plugins.util import get_tags_count +if TYPE_CHECKING: + from jrnl.Entry import Entry + from jrnl.Journal import Journal + class TagExporter(TextExporter): """This Exporter can lists the tags for entries and journals, exported as a plain text file.""" @@ -12,12 +18,12 @@ class TagExporter(TextExporter): extension = "tags" @classmethod - def export_entry(cls, entry): + def export_entry(cls, entry: "Entry") -> str: """Returns a list of tags for a single entry.""" return ", ".join(entry.tags) @classmethod - def export_journal(cls, journal): + def export_journal(cls, journal: "Journal") -> str: """Returns a list of tags and their frequency for an entire journal.""" tag_counts = get_tags_count(journal) result = "" diff --git a/jrnl/plugins/text_exporter.py b/jrnl/plugins/text_exporter.py index 4451cfef..7f2321e7 100644 --- a/jrnl/plugins/text_exporter.py +++ b/jrnl/plugins/text_exporter.py @@ -5,12 +5,17 @@ import errno import os import re import unicodedata +from typing import TYPE_CHECKING from jrnl.messages import Message from jrnl.messages import MsgStyle from jrnl.messages import MsgText from jrnl.output import print_msg +if TYPE_CHECKING: + from jrnl.Entry import Entry + from jrnl.Journal import Journal + class TextExporter: """This Exporter can convert entries and journals into text files.""" @@ -19,17 +24,17 @@ class TextExporter: extension = "txt" @classmethod - def export_entry(cls, entry): + def export_entry(cls, entry: "Entry") -> str: """Returns a string representation of a single entry.""" return str(entry) @classmethod - def export_journal(cls, journal): + def export_journal(cls, journal: "Journal") -> str: """Returns a string representation of an entire journal.""" return "\n".join(cls.export_entry(entry) for entry in journal) @classmethod - def write_file(cls, journal, path): + def write_file(cls, journal: "Journal", path: str) -> str: """Exports a journal into a single file.""" export_str = cls.export_journal(journal) with open(path, "w", encoding="utf-8") as f: @@ -46,13 +51,13 @@ class TextExporter: return "" @classmethod - def make_filename(cls, entry): + def make_filename(cls, entry: "Entry") -> str: return entry.date.strftime("%Y-%m-%d") + "_{}.{}".format( cls._slugify(str(entry.title)), cls.extension ) @classmethod - def write_files(cls, journal, path): + def write_files(cls, journal: "Journal", path: str) -> str: """Exports a journal into individual files for each entry.""" for entry in journal.entries: entry_is_written = False @@ -82,7 +87,7 @@ class TextExporter: ) return "" - def _slugify(string): + def _slugify(string: str) -> str: """Slugifies a string. Based on public domain code from https://github.com/zacharyvoase/slugify """ @@ -92,7 +97,7 @@ class TextExporter: return slug @classmethod - def export(cls, journal, output=None): + def export(cls, journal: "Journal", output: str | None = None) -> str: """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.""" diff --git a/jrnl/plugins/util.py b/jrnl/plugins/util.py index 55ae73c5..009651e9 100644 --- a/jrnl/plugins/util.py +++ b/jrnl/plugins/util.py @@ -1,8 +1,13 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html +from typing import TYPE_CHECKING -def get_tags_count(journal): +if TYPE_CHECKING: + from jrnl.Journal import Journal + + +def get_tags_count(journal: "Journal") -> set[tuple[int, str]]: """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. @@ -12,7 +17,7 @@ def get_tags_count(journal): return tag_counts -def oxford_list(lst): +def oxford_list(lst: list) -> str: """Return Human-readable list of things obeying the object comma)""" lst = sorted(lst) if not lst: diff --git a/jrnl/plugins/xml_exporter.py b/jrnl/plugins/xml_exporter.py index a6c2fe79..61b6b297 100644 --- a/jrnl/plugins/xml_exporter.py +++ b/jrnl/plugins/xml_exporter.py @@ -1,11 +1,16 @@ # Copyright © 2012-2022 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html +from typing import TYPE_CHECKING from xml.dom import minidom from jrnl.plugins.json_exporter import JSONExporter from jrnl.plugins.util import get_tags_count +if TYPE_CHECKING: + from jrnl.Entry import Entry + from jrnl.Journal import Journal + class XMLExporter(JSONExporter): """This Exporter can convert entries and journals into XML.""" @@ -14,7 +19,9 @@ class XMLExporter(JSONExporter): extension = "xml" @classmethod - def export_entry(cls, entry, doc=None): + def export_entry( + cls, entry: "Entry", doc: minidom.Document | None = None + ) -> minidom.Element | str: """Returns an XML representation of a single entry.""" doc_el = doc or minidom.Document() entry_el = doc_el.createElement("entry") @@ -29,7 +36,7 @@ class XMLExporter(JSONExporter): return entry_el @classmethod - def entry_to_xml(cls, entry, doc): + def entry_to_xml(cls, entry: "Entry", doc: minidom.Document) -> minidom.Element: entry_el = doc.createElement("entry") entry_el.setAttribute("date", entry.date.isoformat()) if hasattr(entry, "uuid"): @@ -44,7 +51,7 @@ class XMLExporter(JSONExporter): return entry_el @classmethod - def export_journal(cls, journal): + def export_journal(cls, journal: "Journal") -> str: """Returns an XML representation of an entire journal.""" tags = get_tags_count(journal) doc = minidom.Document() diff --git a/jrnl/plugins/yaml_exporter.py b/jrnl/plugins/yaml_exporter.py index c97692ad..5f5d5b21 100644 --- a/jrnl/plugins/yaml_exporter.py +++ b/jrnl/plugins/yaml_exporter.py @@ -3,6 +3,7 @@ import os import re +from typing import TYPE_CHECKING from jrnl.exception import JrnlException from jrnl.messages import Message @@ -11,6 +12,10 @@ from jrnl.messages import MsgText from jrnl.output import print_msg from jrnl.plugins.text_exporter import TextExporter +if TYPE_CHECKING: + from jrnl.Entry import Entry + from jrnl.Journal import Journal + class YAMLExporter(TextExporter): """This Exporter can convert entries and journals into Markdown formatted text with YAML front matter.""" @@ -19,7 +24,7 @@ class YAMLExporter(TextExporter): extension = "md" @classmethod - def export_entry(cls, entry, to_multifile=True): + def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str: """Returns a markdown representation of a single entry, with YAML front matter.""" if to_multifile is False: raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR)) @@ -124,6 +129,6 @@ class YAMLExporter(TextExporter): ) @classmethod - def export_journal(cls, journal): + def export_journal(cls, journal: "Journal"): """Returns an error, as YAML export requires a directory as a target.""" raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR)) diff --git a/jrnl/prompt.py b/jrnl/prompt.py index f4d62ae4..8f7e36c9 100644 --- a/jrnl/prompt.py +++ b/jrnl/prompt.py @@ -42,7 +42,7 @@ def create_password(journal_name: str) -> str: return pw -def yesno(prompt: Message, default: bool = True) -> bool: +def yesno(prompt: Message | str, default: bool = True) -> bool: response = print_msgs( [ prompt, diff --git a/jrnl/time.py b/jrnl/time.py index fd17ca0a..ca1668b3 100644 --- a/jrnl/time.py +++ b/jrnl/time.py @@ -22,8 +22,12 @@ def __get_pdt_calendar(): def parse( - date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False -): + date_str: str | datetime.datetime, + inclusive: bool = False, + default_hour: int | None = None, + default_minute: int | None = None, + bracketed: bool = False, +) -> datetime.datetime | None: """Parses a string containing a fuzzy date and returns a datetime.datetime object""" if not date_str: return None diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index 206b9405..e620537d 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -19,7 +19,7 @@ from jrnl.path import expand_path from jrnl.prompt import yesno -def backup(filename, binary=False): +def backup(filename: str, binary: bool = False): filename = expand_path(filename) try: @@ -42,14 +42,14 @@ def backup(filename, binary=False): raise JrnlException(Message(MsgText.UpgradeAborted, MsgStyle.WARNING)) -def check_exists(path): +def check_exists(path: str) -> bool: """ Checks if a given path exists. """ return os.path.exists(path) -def upgrade_jrnl(config_path): +def upgrade_jrnl(config_path: str) -> None: config = load_config(config_path) print_msg(Message(MsgText.WelcomeToJrnl, MsgStyle.NORMAL, {"version": __version__})) @@ -115,7 +115,7 @@ def upgrade_jrnl(config_path): cont = yesno(Message(MsgText.ContinueUpgrade), default=False) if not cont: - raise JrnlException(Message(MsgText.UpgradeAborted), MsgStyle.WARNING) + raise JrnlException(Message(MsgText.UpgradeAborted, MsgStyle.WARNING)) for journal_name, path in encrypted_journals.items(): print_msg( @@ -178,7 +178,7 @@ def upgrade_jrnl(config_path): print_msg(Message(MsgText.AllDoneUpgrade, MsgStyle.NORMAL)) -def is_old_version(config_path): +def is_old_version(config_path: str) -> bool: return is_config_json(config_path) From 1e694957284034818822ba6b330af716b45ac2e8 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 5 Nov 2022 22:31:41 +0000 Subject: [PATCH 12/12] Update changelog [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d86379..93eaf62d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ **Build:** - Fix bug where changelog is always slightly out of date on release tags [\#1631](https://github.com/jrnl-org/jrnl/pull/1631) ([wren](https://github.com/wren)) +- Add type hints [\#1614](https://github.com/jrnl-org/jrnl/pull/1614) ([outa](https://github.com/outa)) ## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29)