mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Merge branch 'develop' into develop
This commit is contained in:
commit
ec724109c0
34 changed files with 912 additions and 1079 deletions
1
.github/actions/run_tests/action.yaml
vendored
1
.github/actions/run_tests/action.yaml
vendored
|
@ -14,6 +14,7 @@ runs:
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
allow-prereleases: true
|
||||||
|
|
||||||
- name: Capture full Python version in env
|
- name: Capture full Python version in env
|
||||||
run: echo "PYTHON_FULL_VERSION=$(python --version)" >> $GITHUB_ENV
|
run: echo "PYTHON_FULL_VERSION=$(python --version)" >> $GITHUB_ENV
|
||||||
|
|
2
.github/workflows/changelog.yaml
vendored
2
.github/workflows/changelog.yaml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JRNL_BOT_TOKEN }}
|
token: ${{ secrets.JRNL_BOT_TOKEN }}
|
||||||
|
|
||||||
|
|
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
|
@ -36,7 +36,7 @@ jobs:
|
||||||
os: [ ubuntu-latest ]
|
os: [ ubuntu-latest ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
|
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
|
@ -71,7 +71,7 @@ jobs:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JRNL_BOT_TOKEN }}
|
token: ${{ secrets.JRNL_BOT_TOKEN }}
|
||||||
|
|
||||||
|
|
2
.github/workflows/testing_pipelines.yaml
vendored
2
.github/workflows/testing_pipelines.yaml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
os: [ ubuntu-latest ]
|
os: [ ubuntu-latest ]
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false
|
- run: git config --global core.autocrlf false
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Check workflow files
|
- name: Check workflow files
|
||||||
uses: docker://rhysd/actionlint:latest
|
uses: docker://rhysd/actionlint:latest
|
||||||
with:
|
with:
|
||||||
|
|
4
.github/workflows/testing_prs.yaml
vendored
4
.github/workflows/testing_prs.yaml
vendored
|
@ -37,11 +37,11 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ '3.10', '3.11' ]
|
python-version: [ '3.10', '3.11', '3.12' ]
|
||||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false
|
- run: git config --global core.autocrlf false
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: ./.github/actions/run_tests
|
uses: ./.github/actions/run_tests
|
||||||
with:
|
with:
|
||||||
|
|
4
.github/workflows/testing_schedule.yaml
vendored
4
.github/workflows/testing_schedule.yaml
vendored
|
@ -17,11 +17,11 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ '3.10', '3.11' ]
|
python-version: [ '3.10', '3.11', '3.12' ]
|
||||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||||
steps:
|
steps:
|
||||||
- run: git config --global core.autocrlf false
|
- run: git config --global core.autocrlf false
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: ./.github/actions/run_tests
|
uses: ./.github/actions/run_tests
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -5,6 +5,13 @@
|
||||||
# Required
|
# Required
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
|
# Set the OS
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3"
|
||||||
|
|
||||||
|
|
||||||
# Build documentation in the docs/ directory
|
# Build documentation in the docs/ directory
|
||||||
mkdocs:
|
mkdocs:
|
||||||
configuration: mkdocs.yml
|
configuration: mkdocs.yml
|
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -1,5 +1,49 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased](https://github.com/jrnl-org/jrnl/)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.1...HEAD)
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
|
||||||
|
- Document security risks of using a computer that someone else has admin access to [\#1793](https://github.com/jrnl-org/jrnl/issues/1793)
|
||||||
|
|
||||||
|
## [v4.1](https://pypi.org/project/jrnl/v4.1/) (2023-11-04)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.1-beta2...v4.1)
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
|
||||||
|
- Add Python 3.12 support [\#1761](https://github.com/jrnl-org/jrnl/pull/1761) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Set new required build fields in the ReadTheDocs config file [\#1803](https://github.com/jrnl-org/jrnl/pull/1803) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Replace flake8 and isort with ruff linter and add `black --check` to linting step [\#1763](https://github.com/jrnl-org/jrnl/pull/1763) ([micahellison](https://github.com/micahellison))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
|
||||||
|
- Add note about messages going to `stderr` and the implication for piping [\#1768](https://github.com/jrnl-org/jrnl/pull/1768) ([micahellison](https://github.com/micahellison))
|
||||||
|
|
||||||
|
**Packaging:**
|
||||||
|
|
||||||
|
- Drop/replace ansiwrap dependency [\#1191](https://github.com/jrnl-org/jrnl/issues/1191)
|
||||||
|
- Use rich instead of ansiwrap to wrap text [\#1693](https://github.com/jrnl-org/jrnl/pull/1693) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Update actions/checkout action to v4 [\#1788](https://github.com/jrnl-org/jrnl/pull/1788) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency black to v23.10.1 [\#1811](https://github.com/jrnl-org/jrnl/pull/1811) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency cryptography to v41.0.5 [\#1815](https://github.com/jrnl-org/jrnl/pull/1815) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency keyring to v24.2.0 [\#1760](https://github.com/jrnl-org/jrnl/pull/1760) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency mkdocs to v1.5.3 [\#1795](https://github.com/jrnl-org/jrnl/pull/1795) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency parse-type to v0.6.2 [\#1762](https://github.com/jrnl-org/jrnl/pull/1762) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency poethepoet to v0.24.1 [\#1806](https://github.com/jrnl-org/jrnl/pull/1806) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency pytest to v7.4.3 [\#1816](https://github.com/jrnl-org/jrnl/pull/1816) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency pytest-bdd to v7 [\#1807](https://github.com/jrnl-org/jrnl/pull/1807) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency rich to v13.6.0 [\#1794](https://github.com/jrnl-org/jrnl/pull/1794) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency ruamel.yaml to v0.18.3 [\#1813](https://github.com/jrnl-org/jrnl/pull/1813) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency ruff to v0.1.3 [\#1810](https://github.com/jrnl-org/jrnl/pull/1810) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency tox to v4.11.3 [\#1782](https://github.com/jrnl-org/jrnl/pull/1782) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
- Update dependency tzlocal to v5.2 [\#1814](https://github.com/jrnl-org/jrnl/pull/1814) ([renovate[bot]](https://github.com/apps/renovate))
|
||||||
|
|
||||||
|
**Special thanks:**
|
||||||
|
- jrnl uses UTC instead of local time for entries in WSL/Ubuntu [\#1607](https://github.com/jrnl-org/jrnl/issues/1607) investigated and reported upstream by [giuseppedandrea](https://github.com/giuseppedandrea)
|
||||||
|
|
||||||
## [v4.0.1](https://pypi.org/project/jrnl/v4.0.1/) (2023-06-20)
|
## [v4.0.1](https://pypi.org/project/jrnl/v4.0.1/) (2023-06-20)
|
||||||
|
|
||||||
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0.1-beta...v4.0.1)
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0.1-beta...v4.0.1)
|
||||||
|
|
|
@ -117,6 +117,11 @@ These formats are mainly intended for piping or exporting your journal to other
|
||||||
programs. Even so, they can still be used in the same way as any other format (like
|
programs. Even so, they can still be used in the same way as any other format (like
|
||||||
written to a file, or displayed in your terminal, if you want).
|
written to a file, or displayed in your terminal, if you want).
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
You may see boxed messages like "2 entries found" when using these formats, but
|
||||||
|
those messages are written to `stderr` instead of `stdout`, and won't be piped when
|
||||||
|
using the `|` operator.
|
||||||
|
|
||||||
### JSON
|
### JSON
|
||||||
|
|
||||||
``` sh
|
``` sh
|
||||||
|
|
|
@ -14,6 +14,35 @@ program there are some limitations to be aware of.
|
||||||
passwords can be easily circumvented by someone with basic security skills
|
passwords can be easily circumvented by someone with basic security skills
|
||||||
to access to your encrypted `jrnl` file.
|
to access to your encrypted `jrnl` file.
|
||||||
|
|
||||||
|
## Plausible deniability
|
||||||
|
|
||||||
|
You may be able to hide the contents of your journal behind a layer of encryption,
|
||||||
|
but if someone has access to your configuration file, then they can figure out that
|
||||||
|
you have a journal, where that journal file is, and when you last edited it.
|
||||||
|
With a sufficient power imbalance, someone may be able to force you to unencrypt
|
||||||
|
it through non-technical means.
|
||||||
|
|
||||||
|
## Spying
|
||||||
|
|
||||||
|
While `jrnl` can protect against unauthorized access to your journal entries while
|
||||||
|
it isn't open, it cannot protect you against an unsafe computer/location.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
- Someone installs a keylogger, tracking what you type into your journal.
|
||||||
|
- Someone watches your screen while you write your entry.
|
||||||
|
- Someone installs a backdoor into `jrnl` or poisons your journal into revealing your entries.
|
||||||
|
|
||||||
|
## Saved Passwords
|
||||||
|
|
||||||
|
When creating an encrypted journal, you'll be prompted as to whether or not you
|
||||||
|
want to "store the password in your keychain." This keychain is accessed using
|
||||||
|
the [Python keyring library](https://pypi.org/project/keyring/), which has different
|
||||||
|
behavior depending on your operating system.
|
||||||
|
|
||||||
|
In Windows, the keychain is the Windows Credential Manager (WCM), which can't be locked
|
||||||
|
and can be accessed by any other application running under your username. If this is
|
||||||
|
a concern for you, you may not want to store your password.
|
||||||
|
|
||||||
## Shell history
|
## Shell history
|
||||||
|
|
||||||
Since you can enter entries from the command line, any tool that logs command
|
Since you can enter entries from the command line, any tool that logs command
|
||||||
|
@ -198,25 +227,6 @@ vim.api.nvim_create_autocmd( {"BufNewFile","BufReadPre" }, {
|
||||||
|
|
||||||
Please see `:h <option>` in Neovim for more information about the options mentioned.
|
Please see `:h <option>` in Neovim for more information about the options mentioned.
|
||||||
|
|
||||||
## Plausible deniability
|
|
||||||
|
|
||||||
You may be able to hide the contents of your journal behind a layer of encryption,
|
|
||||||
but if someone has access to your configuration file, then they can figure out that
|
|
||||||
you have a journal, where that journal file is, and when you last edited it.
|
|
||||||
With a sufficient power imbalance, someone may be able to force you to unencrypt
|
|
||||||
it through non-technical means.
|
|
||||||
|
|
||||||
## Saved Passwords
|
|
||||||
|
|
||||||
When creating an encrypted journal, you'll be prompted as to whether or not you
|
|
||||||
want to "store the password in your keychain." This keychain is accessed using
|
|
||||||
the [Python keyring library](https://pypi.org/project/keyring/), which has different
|
|
||||||
behavior depending on your operating system.
|
|
||||||
|
|
||||||
In Windows, the keychain is the Windows Credential Manager (WCM), which can't be locked
|
|
||||||
and can be accessed by any other application running under your username. If this is
|
|
||||||
a concern for you, you may not want to store your password.
|
|
||||||
|
|
||||||
## Notice any other risks?
|
## Notice any other risks?
|
||||||
|
|
||||||
Please let the maintainers know by [filing an issue on GitHub](https://github.com/jrnl-org/jrnl/issues).
|
Please let the maintainers know by [filing an issue on GitHub](https://github.com/jrnl-org/jrnl/issues).
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = "v4.0.1"
|
__version__ = "v4.1"
|
||||||
|
|
27
jrnl/args.py
27
jrnl/args.py
|
@ -78,7 +78,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||||
"""
|
"""
|
||||||
We gratefully thank all contributors!
|
We gratefully thank all contributors!
|
||||||
Come see the whole list of code and financial contributors at https://github.com/jrnl-org/jrnl
|
Come see the whole list of code and financial contributors at https://github.com/jrnl-org/jrnl
|
||||||
And special thanks to Bad Lip Reading for the Yoda joke in the Writing section above :)"""
|
And special thanks to Bad Lip Reading for the Yoda joke in the Writing section above :)""" # noqa: E501
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -214,7 +214,8 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||||
composing.add_argument(
|
composing.add_argument(
|
||||||
"--template",
|
"--template",
|
||||||
dest="template",
|
dest="template",
|
||||||
help="Path to template file. Can be a local path, absolute path, or a path relative to $XDG_DATA_HOME/jrnl/templates/",
|
help="Path to template file. Can be a local path, absolute path, or a path "
|
||||||
|
"relative to $XDG_DATA_HOME/jrnl/templates/",
|
||||||
)
|
)
|
||||||
|
|
||||||
read_msg = (
|
read_msg = (
|
||||||
|
@ -265,13 +266,15 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||||
"-contains",
|
"-contains",
|
||||||
dest="contains",
|
dest="contains",
|
||||||
metavar="TEXT",
|
metavar="TEXT",
|
||||||
help="Show entries containing specific text (put quotes around text with spaces)",
|
help="Show entries containing specific text (put quotes around text with "
|
||||||
|
"spaces)",
|
||||||
)
|
)
|
||||||
reading.add_argument(
|
reading.add_argument(
|
||||||
"-and",
|
"-and",
|
||||||
dest="strict",
|
dest="strict",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help='Show only entries that match all conditions, like saying "x AND y" (default: OR)',
|
help='Show only entries that match all conditions, like saying "x AND y" '
|
||||||
|
"(default: OR)",
|
||||||
)
|
)
|
||||||
reading.add_argument(
|
reading.add_argument(
|
||||||
"-starred",
|
"-starred",
|
||||||
|
@ -290,7 +293,8 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||||
dest="limit",
|
dest="limit",
|
||||||
default=None,
|
default=None,
|
||||||
metavar="NUMBER",
|
metavar="NUMBER",
|
||||||
help="Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same effect)",
|
help="Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same "
|
||||||
|
"effect)",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
type=int,
|
type=int,
|
||||||
)
|
)
|
||||||
|
@ -308,8 +312,12 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
search_options_msg = """ These help you do various tasks with the selected entries from your search.
|
search_options_msg = (
|
||||||
|
" " # Preserves indentation
|
||||||
|
"""
|
||||||
|
These help you do various tasks with the selected entries from your search.
|
||||||
If used on their own (with no search), they will act on your entire journal"""
|
If used on their own (with no search), they will act on your entire journal"""
|
||||||
|
)
|
||||||
exporting = parser.add_argument_group(
|
exporting = parser.add_argument_group(
|
||||||
"Searching Options", textwrap.dedent(search_options_msg)
|
"Searching Options", textwrap.dedent(search_options_msg)
|
||||||
)
|
)
|
||||||
|
@ -360,7 +368,8 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||||
"--tags",
|
"--tags",
|
||||||
dest="tags",
|
dest="tags",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Alias for '--format tags'. Returns a list of all tags and number of occurrences",
|
help="Alias for '--format tags'. Returns a list of all tags and number of "
|
||||||
|
"occurrences",
|
||||||
)
|
)
|
||||||
exporting.add_argument(
|
exporting.add_argument(
|
||||||
"--short",
|
"--short",
|
||||||
|
@ -400,7 +409,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||||
\t jrnl --config-override editor "nano" \n
|
\t jrnl --config-override editor "nano" \n
|
||||||
\t - Override color selections\n
|
\t - Override color selections\n
|
||||||
\t jrnl --config-override colors.body blue --config-override colors.title green
|
\t jrnl --config-override colors.body blue --config-override colors.title green
|
||||||
""",
|
""", # noqa: E501
|
||||||
)
|
)
|
||||||
config_overrides.add_argument(
|
config_overrides.add_argument(
|
||||||
"--co",
|
"--co",
|
||||||
|
@ -430,7 +439,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
|
||||||
\t jrnl --config-file /home/user1/work_config.yaml
|
\t jrnl --config-file /home/user1/work_config.yaml
|
||||||
\t - Use a personal config file stored on a thumb drive: \n
|
\t - Use a personal config file stored on a thumb drive: \n
|
||||||
\t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml
|
\t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml
|
||||||
""",
|
""", # noqa: E501
|
||||||
)
|
)
|
||||||
|
|
||||||
alternate_config.add_argument(
|
alternate_config.add_argument(
|
||||||
|
|
|
@ -142,7 +142,7 @@ def postconfig_encrypt(
|
||||||
def postconfig_decrypt(
|
def postconfig_decrypt(
|
||||||
args: argparse.Namespace, config: dict, original_config: dict
|
args: argparse.Namespace, config: dict, original_config: dict
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Decrypts into new file. If filename is not set, we encrypt the journal file itself."""
|
"""Decrypts to file. If filename is not set, we encrypt the journal file itself."""
|
||||||
from jrnl.config import update_config
|
from jrnl.config import update_config
|
||||||
from jrnl.install import save_config
|
from jrnl.install import save_config
|
||||||
from jrnl.journals import open_journal
|
from jrnl.journals import open_journal
|
||||||
|
|
|
@ -37,9 +37,10 @@ def make_yaml_valid_dict(input: list) -> dict:
|
||||||
The dict is created through the yaml loader, with the assumption that
|
The dict is created through the yaml loader, with the assumption that
|
||||||
"input[0]: input[1]" is valid yaml.
|
"input[0]: input[1]" is valid yaml.
|
||||||
|
|
||||||
:param input: list of configuration keys in dot-notation and their respective values.
|
:param input: list of configuration keys in dot-notation and their respective values
|
||||||
:type input: list
|
:type input: list
|
||||||
:return: A single level dict of the configuration keys in dot-notation and their respective desired values
|
:return: A single level dict of the configuration keys in dot-notation and their
|
||||||
|
respective desired values
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -105,7 +106,7 @@ def scope_config(config: dict, journal_name: str) -> dict:
|
||||||
return config
|
return config
|
||||||
config = config.copy()
|
config = config.copy()
|
||||||
journal_conf = config["journals"].get(journal_name)
|
journal_conf = config["journals"].get(journal_name)
|
||||||
if type(journal_conf) is dict:
|
if isinstance(journal_conf, dict):
|
||||||
# We can override the default config on a by-journal basis
|
# We can override the default config on a by-journal basis
|
||||||
logging.debug(
|
logging.debug(
|
||||||
"Updating configuration with specific journal overrides:\n%s",
|
"Updating configuration with specific journal overrides:\n%s",
|
||||||
|
@ -180,7 +181,7 @@ def update_config(
|
||||||
"""Updates a config dict with new values - either global if scope is 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 config['journals'][scope] is just a string pointing to a journal file,
|
||||||
or within the scope"""
|
or within the scope"""
|
||||||
if scope and type(config["journals"][scope]) is dict: # Update to journal specific
|
if scope and isinstance(config["journals"][scope], dict):
|
||||||
config["journals"][scope].update(new_config)
|
config["journals"][scope].update(new_config)
|
||||||
elif scope and force_local: # Convert to dict
|
elif scope and force_local: # Convert to dict
|
||||||
config["journals"][scope] = {"journal": config["journals"][scope]}
|
config["journals"][scope] = {"journal": config["journals"][scope]}
|
||||||
|
|
|
@ -34,9 +34,9 @@ if TYPE_CHECKING:
|
||||||
def run(args: "Namespace"):
|
def run(args: "Namespace"):
|
||||||
"""
|
"""
|
||||||
Flow:
|
Flow:
|
||||||
1. Run standalone command if it doesn't require config (help, version, etc), then exit
|
1. Run standalone command if it doesn't need config (help, version, etc), then exit
|
||||||
2. Load config
|
2. Load config
|
||||||
3. Run standalone command if it does require config (encrypt, decrypt, etc), then exit
|
3. Run standalone command if it does need config (encrypt, decrypt, etc), then exit
|
||||||
4. Load specified journal
|
4. Load specified journal
|
||||||
5. Start append mode, or search mode
|
5. Start append mode, or search mode
|
||||||
6. Perform actions with results from search mode (if needed)
|
6. Perform actions with results from search mode (if needed)
|
||||||
|
@ -181,7 +181,9 @@ def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -
|
||||||
def _get_template(args, config) -> str:
|
def _get_template(args, config) -> str:
|
||||||
# Read template file and pass as raw text into the composer
|
# Read template file and pass as raw text into the composer
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"Get template:\n--template: {args.template}\nfrom config: {config.get('template')}"
|
"Get template:\n"
|
||||||
|
f"--template: {args.template}\n"
|
||||||
|
f"from config: {config.get('template')}"
|
||||||
)
|
)
|
||||||
template_path = args.template or config.get("template")
|
template_path = args.template or config.get("template")
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,8 @@ def get_template_path(template_path: str, jrnl_template_dir: str) -> str:
|
||||||
actual_template_path = os.path.join(jrnl_template_dir, template_path)
|
actual_template_path = os.path.join(jrnl_template_dir, template_path)
|
||||||
if not os.path.exists(actual_template_path):
|
if not os.path.exists(actual_template_path):
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"Couldn't open {actual_template_path}. Treating template path like a local / abs path."
|
f"Couldn't open {actual_template_path}. "
|
||||||
|
"Treating template path like a local / abs path."
|
||||||
)
|
)
|
||||||
actual_template_path = absolute_path(template_path)
|
actual_template_path = absolute_path(template_path)
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,11 @@ from jrnl.upgrade import is_old_version
|
||||||
|
|
||||||
|
|
||||||
def upgrade_config(config_data: dict, alt_config_path: str | None = None) -> 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.
|
"""Checks if there are keys missing in a given config dict, and if so, updates the
|
||||||
This essentially automatically ports jrnl installations if new config parameters are introduced in later
|
config file accordingly. This essentially automatically ports jrnl installations
|
||||||
versions.
|
if new config parameters are introduced in later versions. Also checks for
|
||||||
Also checks for existence of and difference in version number between config dict and current jrnl version,
|
existence of and difference in version number between config dict
|
||||||
and if so, update the config file accordingly.
|
and current jrnl version, and if so, update the config file accordingly.
|
||||||
Supply alt_config_path if using an alternate config through --config-file."""
|
Supply alt_config_path if using an alternate config through --config-file."""
|
||||||
default_config = get_default_config()
|
default_config = get_default_config()
|
||||||
missing_keys = set(default_config).difference(config_data)
|
missing_keys = set(default_config).difference(config_data)
|
||||||
|
@ -167,7 +167,7 @@ def install() -> dict:
|
||||||
|
|
||||||
|
|
||||||
def _initialize_autocomplete() -> None:
|
def _initialize_autocomplete() -> None:
|
||||||
# readline is not included in Windows Active Python and perhaps some other distributions
|
# readline is not included in Windows Active Python and perhaps some other distss
|
||||||
if sys.modules.get("readline"):
|
if sys.modules.get("readline"):
|
||||||
import readline
|
import readline
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,9 @@ import os
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import ansiwrap
|
|
||||||
|
|
||||||
from jrnl.color import colorize
|
from jrnl.color import colorize
|
||||||
from jrnl.color import highlight_tags_with_background_color
|
from jrnl.color import highlight_tags_with_background_color
|
||||||
|
from jrnl.output import wrap_with_ansi_colors
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .Journal import Journal
|
from .Journal import Journal
|
||||||
|
@ -89,7 +88,7 @@ class Entry:
|
||||||
}
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Returns a string representation of the entry to be written into a journal file."""
|
"""Returns string representation of the entry to be written to 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 "))
|
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
||||||
if self.starred:
|
if self.starred:
|
||||||
|
@ -129,7 +128,7 @@ class Entry:
|
||||||
columns = 79
|
columns = 79
|
||||||
|
|
||||||
# Color date / title and bold title
|
# Color date / title and bold title
|
||||||
title = ansiwrap.fill(
|
title = wrap_with_ansi_colors(
|
||||||
date_str
|
date_str
|
||||||
+ " "
|
+ " "
|
||||||
+ highlight_tags_with_background_color(
|
+ highlight_tags_with_background_color(
|
||||||
|
@ -143,35 +142,17 @@ class Entry:
|
||||||
body = highlight_tags_with_background_color(
|
body = highlight_tags_with_background_color(
|
||||||
self, self.body.rstrip(" \n"), self.journal.config["colors"]["body"]
|
self, self.body.rstrip(" \n"), self.journal.config["colors"]["body"]
|
||||||
)
|
)
|
||||||
body_text = [
|
|
||||||
colorize(
|
|
||||||
ansiwrap.fill(
|
|
||||||
line,
|
|
||||||
columns,
|
|
||||||
initial_indent=indent,
|
|
||||||
subsequent_indent=indent,
|
|
||||||
drop_whitespace=True,
|
|
||||||
),
|
|
||||||
self.journal.config["colors"]["body"],
|
|
||||||
)
|
|
||||||
or indent
|
|
||||||
for line in body.rstrip(" \n").splitlines()
|
|
||||||
]
|
|
||||||
|
|
||||||
# ansiwrap doesn't handle lines with only the "\n" character and some
|
body = wrap_with_ansi_colors(body, columns - len(indent))
|
||||||
# ANSI escapes properly, so we have this hack here to make sure the
|
if indent:
|
||||||
# beginning of each line has the indent character and it's colored
|
# Without explicitly colorizing the indent character, it will lose its
|
||||||
# properly. textwrap doesn't have this issue, however, it doesn't wrap
|
# color after a tag appears.
|
||||||
# the strings properly as it counts ANSI escapes as literal characters.
|
|
||||||
# TL;DR: I'm sorry.
|
|
||||||
body = "\n".join(
|
body = "\n".join(
|
||||||
[
|
|
||||||
colorize(indent, self.journal.config["colors"]["body"]) + line
|
colorize(indent, self.journal.config["colors"]["body"]) + line
|
||||||
if not ansiwrap.strip_color(line).startswith(indent)
|
for line in body.splitlines()
|
||||||
else line
|
|
||||||
for line in body_text
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
body = colorize(body, self.journal.config["colors"]["body"])
|
||||||
else:
|
else:
|
||||||
title = (
|
title = (
|
||||||
date_str
|
date_str
|
||||||
|
@ -233,7 +214,7 @@ SENTENCE_SPLITTER = re.compile(
|
||||||
\s+ # AND a sequence of required spaces.
|
\s+ # AND a sequence of required spaces.
|
||||||
)
|
)
|
||||||
|[\uFF01\uFF0E\uFF1F\uFF61\u3002] # CJK full/half width terminals usually do not have following spaces.
|
|[\uFF01\uFF0E\uFF1F\uFF61\u3002] # CJK full/half width terminals usually do not have following spaces.
|
||||||
""",
|
""", # noqa: E501
|
||||||
re.VERBOSE,
|
re.VERBOSE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,8 @@ class Folder(Journal):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_files(journal_path: str) -> list[str]:
|
def _get_files(journal_path: str) -> list[str]:
|
||||||
"""Searches through sub directories starting with journal_path and find all text files that look like entries"""
|
"""Searches through sub directories starting with journal_path and find all text
|
||||||
|
files that look like entries"""
|
||||||
for year_folder in Folder._get_year_folders(pathlib.Path(journal_path)):
|
for year_folder in Folder._get_year_folders(pathlib.Path(journal_path)):
|
||||||
for month_folder in Folder._get_month_folders(year_folder):
|
for month_folder in Folder._get_month_folders(year_folder):
|
||||||
yield from Folder._get_day_files(month_folder)
|
yield from Folder._get_day_files(month_folder)
|
||||||
|
|
|
@ -102,7 +102,7 @@ class Journal:
|
||||||
return self.encryption_method.encrypt(text)
|
return self.encryption_method.encrypt(text)
|
||||||
|
|
||||||
def open(self, filename: str | None = None) -> "Journal":
|
def open(self, filename: str | None = None) -> "Journal":
|
||||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
"""Opens the journal file and parses it into a list of Entries
|
||||||
Entries have the form (date, title, body)."""
|
Entries have the form (date, title, body)."""
|
||||||
filename = filename or self.config["journal"]
|
filename = filename or self.config["journal"]
|
||||||
dirname = os.path.dirname(filename)
|
dirname = os.path.dirname(filename)
|
||||||
|
@ -144,7 +144,7 @@ class Journal:
|
||||||
self._store(filename, text)
|
self._store(filename, text)
|
||||||
|
|
||||||
def validate_parsing(self) -> bool:
|
def validate_parsing(self) -> bool:
|
||||||
"""Confirms that the jrnl is still parsed correctly after being dumped to text."""
|
"""Confirms that the jrnl is still parsed correctly after conversion to text."""
|
||||||
new_entries = self._parse(self._to_text())
|
new_entries = self._parse(self._to_text())
|
||||||
return all(entry == new_entries[i] for i, entry in enumerate(self.entries))
|
return all(entry == new_entries[i] for i, entry in enumerate(self.entries))
|
||||||
|
|
||||||
|
@ -225,8 +225,9 @@ class Journal:
|
||||||
@property
|
@property
|
||||||
def tags(self) -> list[Tag]:
|
def tags(self) -> list[Tag]:
|
||||||
"""Returns a set of tuples (count, tag) for all tags present in the 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
|
# Astute reader: should the following line leave you as puzzled as me the first
|
||||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
# 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]
|
# 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}
|
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||||
|
@ -343,7 +344,8 @@ class Journal:
|
||||||
|
|
||||||
def new_entry(self, raw: str, date=None, sort: bool = True) -> Entry:
|
def new_entry(self, raw: str, date=None, sort: bool = True) -> Entry:
|
||||||
"""Constructs a new entry from some raw text input.
|
"""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.
|
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")
|
||||||
|
|
|
@ -43,8 +43,8 @@ class MsgText(Enum):
|
||||||
Do you want to encrypt your journal? (You can always change this later)
|
Do you want to encrypt your journal? (You can always change this later)
|
||||||
"""
|
"""
|
||||||
UseColorsQuestion = """
|
UseColorsQuestion = """
|
||||||
Do you want jrnl to use colors when displaying entries? (You can always change this later)
|
Do you want jrnl to use colors to display entries? (You can always change this later)
|
||||||
"""
|
""" # noqa: E501 - the line is still under 88 when dedented
|
||||||
YesOrNoPromptDefaultYes = "[Y/n]"
|
YesOrNoPromptDefaultYes = "[Y/n]"
|
||||||
YesOrNoPromptDefaultNo = "[y/N]"
|
YesOrNoPromptDefaultNo = "[y/N]"
|
||||||
ContinueUpgrade = "Continue upgrading jrnl?"
|
ContinueUpgrade = "Continue upgrading jrnl?"
|
||||||
|
|
|
@ -131,3 +131,12 @@ def format_msg_text(msg: Message) -> Text:
|
||||||
text = textwrap.dedent(text)
|
text = textwrap.dedent(text)
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
return Text(text)
|
return Text(text)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_with_ansi_colors(text: str, width: int) -> str:
|
||||||
|
richtext = Text.from_ansi(text, no_wrap=False, tab_size=None)
|
||||||
|
|
||||||
|
console = Console(width=width, force_terminal=True)
|
||||||
|
with console.capture() as capture:
|
||||||
|
console.print(richtext, sep="", end="")
|
||||||
|
return capture.get()
|
||||||
|
|
|
@ -56,7 +56,8 @@ def _recursively_apply(tree: dict, nodes: list, override_value) -> dict:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config (dict): Configuration to modify
|
config (dict): Configuration to modify
|
||||||
nodes (list): Vector of override keys; the length of the vector indicates tree depth
|
nodes (list): Vector of override keys; the length of the vector indicates tree
|
||||||
|
depth
|
||||||
override_value (str): Runtime override passed from the command-line
|
override_value (str): Runtime override passed from the command-line
|
||||||
"""
|
"""
|
||||||
key = nodes[0]
|
key = nodes[0]
|
||||||
|
|
|
@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class FancyExporter(TextExporter):
|
class FancyExporter(TextExporter):
|
||||||
"""This Exporter can convert entries and journals into text with unicode box drawing characters."""
|
"""This Exporter converts entries and journals into text with unicode boxes."""
|
||||||
|
|
||||||
names = ["fancy", "boxed"]
|
names = ["fancy", "boxed"]
|
||||||
extension = "txt"
|
extension = "txt"
|
||||||
|
|
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class TagExporter(TextExporter):
|
class TagExporter(TextExporter):
|
||||||
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
|
"""This Exporter lists the tags for entries and journals."""
|
||||||
|
|
||||||
names = ["tags"]
|
names = ["tags"]
|
||||||
extension = "tags"
|
extension = "tags"
|
||||||
|
|
|
@ -13,7 +13,8 @@ if TYPE_CHECKING:
|
||||||
def get_tags_count(journal: "Journal") -> set[tuple[int, str]]:
|
def get_tags_count(journal: "Journal") -> set[tuple[int, str]]:
|
||||||
"""Returns a set of tuples (count, tag) for all tags present in the 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
|
# 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.
|
# 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]
|
# 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}
|
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||||
|
|
|
@ -18,14 +18,15 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class YAMLExporter(TextExporter):
|
class YAMLExporter(TextExporter):
|
||||||
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
"""This Exporter converts entries and journals into Markdown formatted text with
|
||||||
|
YAML front matter."""
|
||||||
|
|
||||||
names = ["yaml"]
|
names = ["yaml"]
|
||||||
extension = "md"
|
extension = "md"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str:
|
def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str:
|
||||||
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
"""Returns a markdown representation of an entry, with YAML front matter."""
|
||||||
if to_multifile is False:
|
if to_multifile is False:
|
||||||
raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR))
|
raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR))
|
||||||
|
|
||||||
|
@ -117,7 +118,14 @@ class YAMLExporter(TextExporter):
|
||||||
# source directory is entry.journal.config['journal']
|
# source directory is entry.journal.config['journal']
|
||||||
# output directory is...?
|
# output directory is...?
|
||||||
|
|
||||||
return "{start}\ntitle: {title}\ndate: {date}\nstarred: {starred}\ntags: {tags}\n{dayone}body: |{body}{end}".format(
|
return (
|
||||||
|
"{start}\n"
|
||||||
|
"title: {title}\n"
|
||||||
|
"date: {date}\n"
|
||||||
|
"starred: {starred}\n"
|
||||||
|
"tags: {tags}\n"
|
||||||
|
"{dayone}body: |{body}{end}"
|
||||||
|
).format(
|
||||||
start="---",
|
start="---",
|
||||||
date=date_str,
|
date=date_str,
|
||||||
title=entry.title,
|
title=entry.title,
|
||||||
|
|
32
jrnl/time.py
32
jrnl/time.py
|
@ -9,14 +9,11 @@ DEFAULT_PAST = datetime.datetime(FAKE_YEAR, 1, 1, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def __get_pdt_calendar():
|
def __get_pdt_calendar():
|
||||||
try:
|
|
||||||
import parsedatetime.parsedatetime_consts as pdt
|
|
||||||
except ImportError:
|
|
||||||
import parsedatetime as pdt
|
import parsedatetime as pdt
|
||||||
|
|
||||||
consts = pdt.Constants(usePyICU=False)
|
consts = pdt.Constants(usePyICU=False)
|
||||||
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
||||||
calendar = pdt.Calendar(consts)
|
calendar = pdt.Calendar(consts, version=pdt.VERSION_CONTEXT_STYLE)
|
||||||
|
|
||||||
return calendar
|
return calendar
|
||||||
|
|
||||||
|
@ -34,14 +31,18 @@ def parse(
|
||||||
elif isinstance(date_str, datetime.datetime):
|
elif isinstance(date_str, datetime.datetime):
|
||||||
return date_str
|
return date_str
|
||||||
|
|
||||||
# Don't try to parse anything with 6 or fewer characters and was parsed from the existing journal.
|
# Don't try to parse anything with 6 or fewer characters and was parsed from the
|
||||||
# It's probably a markdown footnote
|
# existing journal. It's probably a markdown footnote
|
||||||
if len(date_str) <= 6 and bracketed:
|
if len(date_str) <= 6 and bracketed:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST
|
default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST
|
||||||
date = None
|
date = None
|
||||||
year_present = False
|
year_present = False
|
||||||
|
|
||||||
|
hasTime = False
|
||||||
|
hasDate = False
|
||||||
|
|
||||||
while not date:
|
while not date:
|
||||||
try:
|
try:
|
||||||
from dateutil.parser import parse as dateparse
|
from dateutil.parser import parse as dateparse
|
||||||
|
@ -53,7 +54,8 @@ def parse(
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
year_present = True
|
year_present = True
|
||||||
flag = 1 if date.hour == date.minute == 0 else 2
|
hasTime = not (date.hour == date.minute == 0)
|
||||||
|
hasDate = True
|
||||||
date = date.timetuple()
|
date = date.timetuple()
|
||||||
except Exception as e:
|
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":
|
||||||
|
@ -61,9 +63,11 @@ def parse(
|
||||||
default_date = datetime.datetime(y, m, d - 1, H, M, S)
|
default_date = datetime.datetime(y, m, d - 1, H, M, S)
|
||||||
else:
|
else:
|
||||||
calendar = __get_pdt_calendar()
|
calendar = __get_pdt_calendar()
|
||||||
date, flag = calendar.parse(date_str)
|
date, parse_context = calendar.parse(date_str)
|
||||||
|
hasTime = parse_context.hasTime
|
||||||
|
hasDate = parse_context.hasDate
|
||||||
|
|
||||||
if not flag: # Oops, unparsable.
|
if not hasDate and not hasTime:
|
||||||
try: # Try and parse this as a single year
|
try: # Try and parse this as a single year
|
||||||
year = int(date_str)
|
year = int(date_str)
|
||||||
return datetime.datetime(year, 1, 1)
|
return datetime.datetime(year, 1, 1)
|
||||||
|
@ -72,8 +76,8 @@ def parse(
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if flag == 1: # Date found, but no time. Use the default time.
|
if hasDate and not hasTime:
|
||||||
date = datetime.datetime(
|
date = datetime.datetime( # Use the default time
|
||||||
*date[:3],
|
*date[:3],
|
||||||
hour=23 if inclusive else default_hour or 0,
|
hour=23 if inclusive else default_hour or 0,
|
||||||
minute=59 if inclusive else default_minute or 0,
|
minute=59 if inclusive else default_minute or 0,
|
||||||
|
@ -82,9 +86,9 @@ def parse(
|
||||||
else:
|
else:
|
||||||
date = datetime.datetime(*date[:6])
|
date = datetime.datetime(*date[:6])
|
||||||
|
|
||||||
# Ugly heuristic: if the date is more than 4 weeks in the future, we got the year wrong.
|
# Ugly heuristic: if the date is more than 4 weeks in the future, we got the year
|
||||||
# Rather than this, we would like to see parsedatetime patched so we can tell it to prefer
|
# wrong. Rather than this, we would like to see parsedatetime patched so we can
|
||||||
# past dates
|
# tell it to prefer past dates
|
||||||
dt = datetime.datetime.now() - date
|
dt = datetime.datetime.now() - date
|
||||||
if dt.days < -28 and not year_present:
|
if dt.days < -28 and not year_present:
|
||||||
date = date.replace(date.year - 1)
|
date = date.replace(date.year - 1)
|
||||||
|
|
1541
poetry.lock
generated
1541
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "jrnl"
|
name = "jrnl"
|
||||||
version = "v4.0.1"
|
version = "v4.1"
|
||||||
description = "Collect your thoughts and notes without leaving the command line."
|
description = "Collect your thoughts and notes without leaving the command line."
|
||||||
authors = [
|
authors = [
|
||||||
"jrnl contributors <maintainers@jrnl.sh>",
|
"jrnl contributors <maintainers@jrnl.sh>",
|
||||||
|
@ -29,7 +29,6 @@ classifiers = [
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.10.0, <3.13"
|
python = ">=3.10.0, <3.13"
|
||||||
|
|
||||||
ansiwrap = "^0.8.4"
|
|
||||||
colorama = ">=0.4" # https://github.com/tartley/colorama/blob/master/CHANGELOG.rst
|
colorama = ">=0.4" # https://github.com/tartley/colorama/blob/master/CHANGELOG.rst
|
||||||
cryptography = ">=3.0" # https://cryptography.io/en/latest/api-stability.html
|
cryptography = ">=3.0" # https://cryptography.io/en/latest/api-stability.html
|
||||||
keyring = ">=21.0" # https://github.com/jaraco/keyring#integration
|
keyring = ">=21.0" # https://github.com/jaraco/keyring#integration
|
||||||
|
@ -44,13 +43,7 @@ tzlocal = ">=4.0" # https://github.com/regebro/tzlocal/blob/master/CHANGES.txt
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
black = { version = ">=21.5b2", allow-prereleases = true }
|
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"
|
|
||||||
flake8-simplify = ">=0.19"
|
|
||||||
ipdb = "*"
|
ipdb = "*"
|
||||||
isort = ">=5.10"
|
|
||||||
mkdocs = ">=1.4"
|
mkdocs = ">=1.4"
|
||||||
parse-type = ">=0.6.0"
|
parse-type = ">=0.6.0"
|
||||||
poethepoet = "*"
|
poethepoet = "*"
|
||||||
|
@ -59,6 +52,7 @@ pytest-bdd = ">=6.0"
|
||||||
pytest-clarity = "*"
|
pytest-clarity = "*"
|
||||||
pytest-xdist = ">=2.5.0"
|
pytest-xdist = ">=2.5.0"
|
||||||
requests = "*"
|
requests = "*"
|
||||||
|
ruff = ">=0.0.276"
|
||||||
toml = ">=0.10"
|
toml = ">=0.10"
|
||||||
tox = "*"
|
tox = "*"
|
||||||
xmltodict = "*"
|
xmltodict = "*"
|
||||||
|
@ -88,18 +82,18 @@ test-run = [
|
||||||
# Groups of tasks
|
# Groups of tasks
|
||||||
format.default_item_type = "cmd"
|
format.default_item_type = "cmd"
|
||||||
format.sequence = [
|
format.sequence = [
|
||||||
"isort .",
|
"ruff check . --select I --fix", # equivalent to "isort ."
|
||||||
"black .",
|
"black .",
|
||||||
]
|
]
|
||||||
|
|
||||||
lint.env = { FLAKEHEAVEN_CACHE_TIMEOUT = "0" }
|
|
||||||
lint.default_item_type = "cmd"
|
lint.default_item_type = "cmd"
|
||||||
lint.sequence = [
|
lint.sequence = [
|
||||||
"poetry --version",
|
"poetry --version",
|
||||||
"poetry check",
|
"poetry check",
|
||||||
"flakeheaven --version",
|
"ruff --version",
|
||||||
"flakeheaven plugins",
|
"ruff .",
|
||||||
"flakeheaven lint",
|
"black --version",
|
||||||
|
"black --check ."
|
||||||
]
|
]
|
||||||
|
|
||||||
test = [
|
test = [
|
||||||
|
@ -107,11 +101,6 @@ test = [
|
||||||
"test-run",
|
"test-run",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
force_single_line = true
|
|
||||||
known_first_party = ["jrnl", "tests"]
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
minversion = "6.0"
|
minversion = "6.0"
|
||||||
required_plugins = [
|
required_plugins = [
|
||||||
|
@ -132,34 +121,40 @@ addopts = [
|
||||||
|
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
"ignore::DeprecationWarning",
|
"ignore::DeprecationWarning",
|
||||||
"ignore:Flag style will be deprecated in.*",
|
|
||||||
"ignore:[WinError 32].*",
|
"ignore:[WinError 32].*",
|
||||||
"ignore:[WinError 5].*"
|
"ignore:[WinError 5].*"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.flakeheaven]
|
[tool.ruff]
|
||||||
max_line_length = 88
|
line-length = 88
|
||||||
|
target-version = "py310"
|
||||||
|
|
||||||
|
# https://beta.ruff.rs/docs/rules/
|
||||||
|
select = [
|
||||||
|
'F', # Pyflakes
|
||||||
|
'E', # pycodestyle errors
|
||||||
|
'W', # pycodestyle warnings
|
||||||
|
'I', # isort
|
||||||
|
'ASYNC', # flake8-async
|
||||||
|
'S110', # try-except-pass
|
||||||
|
'S112', # try-except-continue
|
||||||
|
'EM', # flake8-errmsg
|
||||||
|
'ISC', # flake8-implicit-str-concat
|
||||||
|
'Q', # flake8-quotes
|
||||||
|
'RSE', # flake8-raise
|
||||||
|
'TID', # flake8-tidy-imports
|
||||||
|
'TCH', # flake8-type-checking
|
||||||
|
'T100', # debugger, don't allow break points
|
||||||
|
'ICN' # flake8-import-conventions
|
||||||
|
]
|
||||||
exclude = [".git", ".tox", ".venv", "node_modules"]
|
exclude = [".git", ".tox", ".venv", "node_modules"]
|
||||||
|
|
||||||
[tool.flakeheaven.plugins]
|
[tool.ruff.isort]
|
||||||
"py*" = ["+*"]
|
force-single-line = true
|
||||||
pycodestyle = [
|
known-first-party = ["jrnl", "tests"]
|
||||||
"-E101",
|
|
||||||
"-E111", "-E114", "-E115", "-E116", "-E117",
|
|
||||||
"-E12*",
|
|
||||||
"-E13*",
|
|
||||||
"-E2*",
|
|
||||||
"-E3*",
|
|
||||||
"-E401",
|
|
||||||
"-E5*",
|
|
||||||
"-E70",
|
|
||||||
"-W1*", "-W2*", "-W3*", "-W5*",
|
|
||||||
]
|
|
||||||
"flake8-*" = ["+*"]
|
|
||||||
flake8-black = ["-BLK901"]
|
|
||||||
|
|
||||||
[tool.flakeheaven.exceptions."*/__init__.py"]
|
[tool.ruff.per-file-ignores]
|
||||||
pyflakes = ["-F401"]
|
"__init__.py" = ["F401"] # unused imports
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|
|
@ -38,38 +38,41 @@ def output_should_match(regex, cli_run):
|
||||||
assert matches, f"\nRegex didn't match:\n{regex}\n{str(out)}\n{str(matches)}"
|
assert matches, f"\nRegex didn't match:\n{regex}\n{str(out)}\n{str(matches)}"
|
||||||
|
|
||||||
|
|
||||||
@then(parse("the output {it_should:Should} contain\n{expected_output}", SHOULD_DICT))
|
@then(parse("the output {it_should:Should} contain\n{expected}", SHOULD_DICT))
|
||||||
@then(parse('the output {it_should:Should} contain "{expected_output}"', SHOULD_DICT))
|
@then(parse('the output {it_should:Should} contain "{expected}"', SHOULD_DICT))
|
||||||
@then(
|
@then(
|
||||||
parse(
|
parse(
|
||||||
"the {which_output_stream} output {it_should:Should} contain\n{expected_output}",
|
"the {which_output_stream} output {it_should:Should} contain\n{expected}",
|
||||||
SHOULD_DICT,
|
SHOULD_DICT,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@then(
|
@then(
|
||||||
parse(
|
parse(
|
||||||
'the {which_output_stream} output {it_should:Should} contain "{expected_output}"',
|
'the {which_output_stream} output {it_should:Should} contain "{expected}"',
|
||||||
SHOULD_DICT,
|
SHOULD_DICT,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def output_should_contain(expected_output, which_output_stream, cli_run, it_should):
|
def output_should_contain(expected, which_output_stream, cli_run, it_should):
|
||||||
output_str = f"\nEXPECTED:\n{expected_output}\n\nACTUAL STDOUT:\n{cli_run['stdout']}\n\nACTUAL STDERR:\n{cli_run['stderr']}"
|
output_str = (
|
||||||
assert expected_output
|
f"\nEXPECTED:\n{expected}\n\n"
|
||||||
|
f"ACTUAL STDOUT:\n{cli_run['stdout']}\n\n"
|
||||||
|
f"ACTUAL STDERR:\n{cli_run['stderr']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert expected
|
||||||
if which_output_stream is None:
|
if which_output_stream is None:
|
||||||
assert ((expected_output in cli_run["stdout"]) == it_should) or (
|
assert ((expected in cli_run["stdout"]) == it_should) or (
|
||||||
(expected_output in cli_run["stderr"]) == it_should
|
(expected in cli_run["stderr"]) == it_should
|
||||||
), output_str
|
), output_str
|
||||||
|
|
||||||
elif which_output_stream == "standard":
|
elif which_output_stream == "standard":
|
||||||
assert (expected_output in cli_run["stdout"]) == it_should, output_str
|
assert (expected in cli_run["stdout"]) == it_should, output_str
|
||||||
|
|
||||||
elif which_output_stream == "error":
|
elif which_output_stream == "error":
|
||||||
assert (expected_output in cli_run["stderr"]) == it_should, output_str
|
assert (expected in cli_run["stderr"]) == it_should, output_str
|
||||||
|
|
||||||
else:
|
else:
|
||||||
assert (
|
assert (expected in cli_run[which_output_stream]) == it_should, output_str
|
||||||
expected_output in cli_run[which_output_stream]
|
|
||||||
) == it_should, output_str
|
|
||||||
|
|
||||||
|
|
||||||
@then(parse("the output should not contain\n{expected_output}"))
|
@then(parse("the output should not contain\n{expected_output}"))
|
||||||
|
@ -119,7 +122,8 @@ def output_should_be_columns_wide(cli_run, width):
|
||||||
|
|
||||||
@then(
|
@then(
|
||||||
parse(
|
parse(
|
||||||
'the default journal "{journal_file}" should be in the "{journal_dir}" directory'
|
'the default journal "{journal_file}" '
|
||||||
|
'should be in the "{journal_dir}" directory'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def default_journal_location(journal_file, journal_dir, config_on_disk, temp_dir):
|
def default_journal_location(journal_file, journal_dir, config_on_disk, temp_dir):
|
||||||
|
@ -135,13 +139,15 @@ def default_journal_location(journal_file, journal_dir, config_on_disk, temp_dir
|
||||||
|
|
||||||
@then(
|
@then(
|
||||||
parse(
|
parse(
|
||||||
'the config for journal "{journal_name}" {it_should:Should} contain "{some_yaml}"',
|
'the config for journal "{journal_name}" '
|
||||||
|
'{it_should:Should} contain "{some_yaml}"',
|
||||||
SHOULD_DICT,
|
SHOULD_DICT,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@then(
|
@then(
|
||||||
parse(
|
parse(
|
||||||
'the config for journal "{journal_name}" {it_should:Should} contain\n{some_yaml}',
|
'the config for journal "{journal_name}" '
|
||||||
|
"{it_should:Should} contain\n{some_yaml}",
|
||||||
SHOULD_DICT,
|
SHOULD_DICT,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -155,7 +161,7 @@ def config_var_on_disk(config_on_disk, journal_name, it_should, some_yaml):
|
||||||
expected = YAML(typ="safe").load(some_yaml)
|
expected = YAML(typ="safe").load(some_yaml)
|
||||||
|
|
||||||
actual_slice = actual
|
actual_slice = actual
|
||||||
if type(actual) is dict:
|
if isinstance(actual, dict):
|
||||||
# `expected` objects formatted in yaml only compare one level deep
|
# `expected` objects formatted in yaml only compare one level deep
|
||||||
actual_slice = {key: actual.get(key) for key in expected.keys()}
|
actual_slice = {key: actual.get(key) for key in expected.keys()}
|
||||||
|
|
||||||
|
@ -164,13 +170,15 @@ def config_var_on_disk(config_on_disk, journal_name, it_should, some_yaml):
|
||||||
|
|
||||||
@then(
|
@then(
|
||||||
parse(
|
parse(
|
||||||
'the config in memory for journal "{journal_name}" {it_should:Should} contain "{some_yaml}"',
|
'the config in memory for journal "{journal_name}" '
|
||||||
|
'{it_should:Should} contain "{some_yaml}"',
|
||||||
SHOULD_DICT,
|
SHOULD_DICT,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@then(
|
@then(
|
||||||
parse(
|
parse(
|
||||||
'the config in memory for journal "{journal_name}" {it_should:Should} contain\n{some_yaml}',
|
'the config in memory for journal "{journal_name}" '
|
||||||
|
"{it_should:Should} contain\n{some_yaml}",
|
||||||
SHOULD_DICT,
|
SHOULD_DICT,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -188,7 +196,7 @@ def config_var_in_memory(config_in_memory, journal_name, it_should, some_yaml):
|
||||||
expected = YAML(typ="safe").load(some_yaml)
|
expected = YAML(typ="safe").load(some_yaml)
|
||||||
|
|
||||||
actual_slice = actual
|
actual_slice = actual
|
||||||
if type(actual) is dict:
|
if isinstance(actual, dict):
|
||||||
# `expected` objects formatted in yaml only compare one level deep
|
# `expected` objects formatted in yaml only compare one level deep
|
||||||
actual_slice = {key: get_nested_val(actual, key) for key in expected.keys()}
|
actual_slice = {key: get_nested_val(actual, key) for key in expected.keys()}
|
||||||
|
|
||||||
|
@ -360,7 +368,7 @@ def assert_output_field_content(field_name, comparison, expected_keys, parsed_ou
|
||||||
my_obj = my_obj[node]
|
my_obj = my_obj[node]
|
||||||
|
|
||||||
if comparison == "be":
|
if comparison == "be":
|
||||||
if type(my_obj) is str:
|
if isinstance(my_obj, str):
|
||||||
assert expected_keys == my_obj, [my_obj, expected_keys]
|
assert expected_keys == my_obj, [my_obj, expected_keys]
|
||||||
else:
|
else:
|
||||||
assert set(expected_keys) == set(my_obj), [
|
assert set(expected_keys) == set(my_obj), [
|
||||||
|
@ -368,7 +376,7 @@ def assert_output_field_content(field_name, comparison, expected_keys, parsed_ou
|
||||||
set(expected_keys),
|
set(expected_keys),
|
||||||
]
|
]
|
||||||
elif comparison == "contain":
|
elif comparison == "contain":
|
||||||
if type(my_obj) is str:
|
if isinstance(my_obj, str):
|
||||||
assert expected_keys in my_obj, [my_obj, expected_keys]
|
assert expected_keys in my_obj, [my_obj, expected_keys]
|
||||||
else:
|
else:
|
||||||
assert all(elem in my_obj for elem in expected_keys), [
|
assert all(elem in my_obj for elem in expected_keys), [
|
||||||
|
|
|
@ -242,7 +242,9 @@ def test_color_override():
|
||||||
|
|
||||||
def test_multiple_overrides():
|
def test_multiple_overrides():
|
||||||
parsed_args = cli_as_dict(
|
parsed_args = cli_as_dict(
|
||||||
'--config-override colors.title green --config-override editor "nano" --config-override journal.scratchpad "/tmp/scratchpad"'
|
"--config-override colors.title green "
|
||||||
|
'--config-override editor "nano" '
|
||||||
|
'--config-override journal.scratchpad "/tmp/scratchpad"'
|
||||||
)
|
)
|
||||||
assert parsed_args == expected_args(
|
assert parsed_args == expected_args(
|
||||||
config_override=[
|
config_override=[
|
||||||
|
|
Loading…
Add table
Reference in a new issue