Merge branch 'develop' into rich-instead-of-ansiwrap-1191

This commit is contained in:
Micah Jerome Ellison 2023-05-02 18:09:36 -07:00
commit 8cd8a5ba17
36 changed files with 1030 additions and 484 deletions

View file

@ -12,7 +12,7 @@ Here are some key points to include in your description:
### Checklist ### Checklist
- [ ] I have read the [contributing doc](https://github.com/jrnl-org/jrnl/blob/develop/CONTRIBUTING.md). - [ ] I have read the [contributing doc](https://github.com/jrnl-org/jrnl/blob/develop/docs/contributing.md).
- [ ] I have included a link to the relevant issue number. - [ ] I have included a link to the relevant issue number.
- [ ] I have checked to ensure there aren't other open [pull requests](../pulls) - [ ] I have checked to ensure there aren't other open [pull requests](../pulls)
for the same issue. for the same issue.

View file

@ -1,8 +1,40 @@
# Changelog # Changelog
## [Unreleased](https://github.com/jrnl-org/jrnl/) ## [v4.0-beta3](https://pypi.org/project/jrnl/v4.0-beta3/) (2023-04-29)
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3...HEAD) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0-beta2...v4.0-beta3)
**Fixed bugs:**
- jrnl reads extraneous text files when reading folder journal [\#1692](https://github.com/jrnl-org/jrnl/issues/1692)
- jrnl crashes when adding tag argument after `--change-time` [\#1644](https://github.com/jrnl-org/jrnl/issues/1644)
- Only read text files that look like entries when opening folder journal [\#1697](https://github.com/jrnl-org/jrnl/pull/1697) ([micahellison](https://github.com/micahellison))
**Documentation:**
- Update contributing.md links in documentation [\#1726](https://github.com/jrnl-org/jrnl/pull/1726) ([ahosking](https://github.com/ahosking))
## [v4.0-beta2](https://pypi.org/project/jrnl/v4.0-beta2/) (2023-04-22)
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0-beta...v4.0-beta2)
**Documentation:**
- Fix various typos [\#1718](https://github.com/jrnl-org/jrnl/pull/1718) ([hezhizhen](https://github.com/hezhizhen))
**Packaging:**
- Update dependency cryptography to v40.0.2 [\#1723](https://github.com/jrnl-org/jrnl/pull/1723) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency flakeheaven to v3.3.0 [\#1722](https://github.com/jrnl-org/jrnl/pull/1722) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency pytest to v7.3.1 [\#1720](https://github.com/jrnl-org/jrnl/pull/1720) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency black to v23.3.0 [\#1715](https://github.com/jrnl-org/jrnl/pull/1715) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency flake8-type-checking to v2.4.0 [\#1714](https://github.com/jrnl-org/jrnl/pull/1714) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency rich to v13.3.4 [\#1713](https://github.com/jrnl-org/jrnl/pull/1713) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency tox to v4.4.12 [\#1712](https://github.com/jrnl-org/jrnl/pull/1712) ([renovate[bot]](https://github.com/apps/renovate))
## [v4.0-beta](https://pypi.org/project/jrnl/v4.0-beta/) (2023-03-25)
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3...v4.0-beta)
**Implemented enhancements:** **Implemented enhancements:**
@ -10,6 +42,7 @@
- Don't import cryptography package if not needed [\#1521](https://github.com/jrnl-org/jrnl/issues/1521) - Don't import cryptography package if not needed [\#1521](https://github.com/jrnl-org/jrnl/issues/1521)
- Add message with config location and docs location when installation is complete [\#1695](https://github.com/jrnl-org/jrnl/pull/1695) ([micahellison](https://github.com/micahellison)) - Add message with config location and docs location when installation is complete [\#1695](https://github.com/jrnl-org/jrnl/pull/1695) ([micahellison](https://github.com/micahellison))
- Prompt to include colors in config when first running jrnl [\#1687](https://github.com/jrnl-org/jrnl/pull/1687) ([micahellison](https://github.com/micahellison)) - Prompt to include colors in config when first running jrnl [\#1687](https://github.com/jrnl-org/jrnl/pull/1687) ([micahellison](https://github.com/micahellison))
- Add ability to use template with `--template` [\#1667](https://github.com/jrnl-org/jrnl/pull/1667) ([alichtman](https://github.com/alichtman))
- Search for entries with no tags or stars with `-not -starred` and `-not -tagged` [\#1663](https://github.com/jrnl-org/jrnl/pull/1663) ([cjcon90](https://github.com/cjcon90)) - Search for entries with no tags or stars with `-not -starred` and `-not -tagged` [\#1663](https://github.com/jrnl-org/jrnl/pull/1663) ([cjcon90](https://github.com/cjcon90))
- Refactor flow for easier access to some files \(avoid things like `jrnl.Journal.Journal` and `jrnl.jrnl` co-existing\) [\#1662](https://github.com/jrnl-org/jrnl/pull/1662) ([wren](https://github.com/wren)) - Refactor flow for easier access to some files \(avoid things like `jrnl.Journal.Journal` and `jrnl.jrnl` co-existing\) [\#1662](https://github.com/jrnl-org/jrnl/pull/1662) ([wren](https://github.com/wren))
- Add more type hints [\#1642](https://github.com/jrnl-org/jrnl/pull/1642) ([outa](https://github.com/outa)) - Add more type hints [\#1642](https://github.com/jrnl-org/jrnl/pull/1642) ([outa](https://github.com/outa))
@ -18,10 +51,14 @@
**Fixed bugs:** **Fixed bugs:**
- Combinations of `--change-time`, `--delete`, and `--edit` don't work consistently [\#1696](https://github.com/jrnl-org/jrnl/issues/1696)
- jrnl doesn't display count of entries deleted after deleting entries with `--delete` [\#1666](https://github.com/jrnl-org/jrnl/issues/1666)
- Templated entries should not be saved if the raw text is identical to the original template [\#1652](https://github.com/jrnl-org/jrnl/issues/1652) - Templated entries should not be saved if the raw text is identical to the original template [\#1652](https://github.com/jrnl-org/jrnl/issues/1652)
- Adding an entry with a combination of flags causes a journal overwrite [\#1639](https://github.com/jrnl-org/jrnl/issues/1639)
- jrnl does not update version key in config file [\#1638](https://github.com/jrnl-org/jrnl/issues/1638) - jrnl does not update version key in config file [\#1638](https://github.com/jrnl-org/jrnl/issues/1638)
- jrnl should not create 0-length "encrypted" file on startup [\#1493](https://github.com/jrnl-org/jrnl/issues/1493) - jrnl should not create 0-length "encrypted" file on startup [\#1493](https://github.com/jrnl-org/jrnl/issues/1493)
- Save empty journal on install instead of just creating a zero-length file [\#1690](https://github.com/jrnl-org/jrnl/pull/1690) ([micahellison](https://github.com/micahellison)) - Save empty journal on install instead of just creating a zero-length file [\#1690](https://github.com/jrnl-org/jrnl/pull/1690) ([micahellison](https://github.com/micahellison))
- Allow combinations of `--change-time`, `--delete`, and `--edit` while correctly counting the number of entries affected [\#1669](https://github.com/jrnl-org/jrnl/pull/1669) ([wren](https://github.com/wren))
- Don't save templated journal entries if the received raw text is the same as the template itself [\#1653](https://github.com/jrnl-org/jrnl/pull/1653) ([Briscoooe](https://github.com/Briscoooe)) - Don't save templated journal entries if the received raw text is the same as the template itself [\#1653](https://github.com/jrnl-org/jrnl/pull/1653) ([Briscoooe](https://github.com/Briscoooe))
- Add tag to XML file when edited DayOne entry and is searchable afterward [\#1648](https://github.com/jrnl-org/jrnl/pull/1648) ([jonakeys](https://github.com/jonakeys)) - Add tag to XML file when edited DayOne entry and is searchable afterward [\#1648](https://github.com/jrnl-org/jrnl/pull/1648) ([jonakeys](https://github.com/jonakeys))
- Update version key in config file after version changes [\#1646](https://github.com/jrnl-org/jrnl/pull/1646) ([jonakeys](https://github.com/jonakeys)) - Update version key in config file after version changes [\#1646](https://github.com/jrnl-org/jrnl/pull/1646) ([jonakeys](https://github.com/jonakeys))
@ -57,6 +94,16 @@
**Packaging:** **Packaging:**
- Update dependency cryptography to v40 [\#1710](https://github.com/jrnl-org/jrnl/pull/1710) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency poethepoet to v0.19.0 [\#1709](https://github.com/jrnl-org/jrnl/pull/1709) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency tzlocal to v4.3 [\#1708](https://github.com/jrnl-org/jrnl/pull/1708) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency tox to v4.4.7 [\#1707](https://github.com/jrnl-org/jrnl/pull/1707) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency rich to v13.3.2 [\#1706](https://github.com/jrnl-org/jrnl/pull/1706) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency pytest-xdist to v3.2.1 [\#1705](https://github.com/jrnl-org/jrnl/pull/1705) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency pytest to v7.2.2 [\#1704](https://github.com/jrnl-org/jrnl/pull/1704) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency ipdb to v0.13.13 [\#1703](https://github.com/jrnl-org/jrnl/pull/1703) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency flake8-type-checking to v2.3.1 [\#1702](https://github.com/jrnl-org/jrnl/pull/1702) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency cryptography to v39.0.2 [\#1701](https://github.com/jrnl-org/jrnl/pull/1701) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency rich to v13 [\#1654](https://github.com/jrnl-org/jrnl/pull/1654) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency rich to v13 [\#1654](https://github.com/jrnl-org/jrnl/pull/1654) ([renovate[bot]](https://github.com/apps/renovate))
## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29) ## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29)

View file

@ -19,7 +19,7 @@ jrnl
==== ====
_To get help, [submit an issue](https://github.com/jrnl-org/jrnl/issues/new/choose) on _To get help, [submit an issue](https://github.com/jrnl-org/jrnl/issues/new/choose) on
Github._ GitHub._
`jrnl` is a simple journal application for the command line. `jrnl` is a simple journal application for the command line.
@ -70,7 +70,7 @@ src="https://opencollective.com/jrnl/contributors.svg?width=890&button=false"
/></a> /></a>
If you'd also like to help make `jrnl` better, please see our [contributing If you'd also like to help make `jrnl` better, please see our [contributing
documentation](CONTRIBUTING.md). documentation](docs/contributing.md).
### Financial Backers ### Financial Backers

View file

@ -74,39 +74,45 @@ jrnlimport () {
} }
``` ```
## Using templates ## Using Templates
!!! note !!! note
Templates require an [external editor](./advanced.md) be configured. Templates require an [external editor](./advanced.md) be configured.
A template is a code snippet that makes it easier to use repeated text Templates are text files that are used for creating structured journals.
each time a new journal entry is started. There are two ways you can utilize There are three ways you can use templates:
templates in your entries.
### 1. Command line arguments ### 1. Use the `--template` command line argument and the default $XDG_DATA_HOME/jrnl/templates directory
If you had a `template.txt` file with the following contents: `$XDG_DATA_HOME/jrnl/templates` is created by default to store your templates! Create a template (like `default.md`) in this directory and pass `--template FILE_IN_DIR`.
```sh ```sh
jrnl --template default.md
```
### 2. Use the `--template` command line argument with a local / absolute path
You can create a template file with any text. Here is an example:
```sh
# /tmp/template.txt
My Personal Journal My Personal Journal
Title: Title:
Body: Body:
``` ```
The `template.txt` file could be used to create a new entry with these Then, pass the absolute or relative path to the template file as an argument, and your external
command line arguments: editor will open and have your template pre-populated.
```sh ```sh
jrnl < template.txt # Imports template.txt as the most recent entry jrnl --template /tmp/template.md
jrnl -1 --edit # Opens the most recent entry in the editor
``` ```
### 2. Include the template file in `jrnl.yaml` ### 3. Set a default template file in `jrnl.yaml`
A more efficient way to work with a template file is to declare the file If you want a template by default, change the value of `template` in the [config file](./reference-config-file.md)
in your [config file](./reference-config-file.md) by changing the `template` from `false` to the template file's path, wrapped in double quotes:
setting from `false` to the template file's path in double quotes:
```sh ```sh
... ...
@ -114,9 +120,6 @@ template: "/path/to/template.txt"
... ...
``` ```
Changes can be saved as you continue writing the journal entry and will be
logged as a new entry in the journal you specified in the original argument.
!!! tip !!! tip
To read your journal entry or to verify the entry saved, you can use this To read your journal entry or to verify the entry saved, you can use this
command: `jrnl -n 1` (Check out [Formats](./formats.md) for more options). command: `jrnl -n 1` (Check out [Formats](./formats.md) for more options).
@ -219,4 +222,3 @@ To cause vi to jump to the end of the last line of the entry you edit, in your c
```yaml ```yaml
editor: vi + -c "call cursor('.',strwidth(getline('.')))" editor: vi + -c "call cursor('.',strwidth(getline('.')))"
``` ```

View file

@ -1 +1 @@
__version__ = "v3.3" __version__ = "v4.0-beta3"

View file

@ -211,6 +211,11 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
"Writing", textwrap.dedent(compose_msg).strip() "Writing", textwrap.dedent(compose_msg).strip()
) )
composing.add_argument("text", metavar="", nargs="*") composing.add_argument("text", metavar="", nargs="*")
composing.add_argument(
"--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/",
)
read_msg = ( read_msg = (
"To find entries from your journal, use any combination of the below filters." "To find entries from your journal, use any combination of the below filters."
@ -326,7 +331,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
nargs="?", nargs="?",
metavar="DATE", metavar="DATE",
const="now", const="now",
help="Change timestamp for seleted entries (default: now)", help="Change timestamp for selected entries (default: now)",
) )
exporting.add_argument( exporting.add_argument(
"--format", "--format",
@ -355,7 +360,7 @@ 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 occurences", help="Alias for '--format tags'. Returns a list of all tags and number of occurrences",
) )
exporting.add_argument( exporting.add_argument(
"--short", "--short",
@ -419,7 +424,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
default="", default="",
help=""" help="""
Overrides default (created when first installed) config file for this command only. Overrides default (created when first installed) config file for this command only.
Examples: \n Examples: \n
\t - Use a work config file for this jrnl entry, call: \n \t - Use a work config file for this jrnl entry, call: \n
\t jrnl --config-file /home/user1/work_config.yaml \t jrnl --config-file /home/user1/work_config.yaml

View file

@ -4,6 +4,7 @@
import argparse import argparse
import logging import logging
import os import os
from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
@ -34,7 +35,6 @@ YAML_FILE_ENCODING = "utf-8"
def make_yaml_valid_dict(input: list) -> dict: def make_yaml_valid_dict(input: list) -> dict:
""" """
Convert a two-element list of configuration key-value pair into a flat dict. Convert a two-element list of configuration key-value pair into a flat dict.
@ -73,9 +73,9 @@ def save_config(config: dict, alt_config_path: str | None = None) -> None:
yaml.dump(config, f) yaml.dump(config, f)
def get_config_path() -> str: def get_config_directory() -> str:
try: try:
config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) return xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
except FileExistsError: except FileExistsError:
raise JrnlException( raise JrnlException(
Message( Message(
@ -89,7 +89,13 @@ def get_config_path() -> str:
), ),
) )
return os.path.join(config_directory_path or home_dir(), DEFAULT_CONFIG_NAME)
def get_config_path() -> Path:
try:
config_directory_path = get_config_directory()
except JrnlException:
return Path(home_dir(), DEFAULT_CONFIG_NAME)
return Path(config_directory_path, DEFAULT_CONFIG_NAME)
def get_default_config() -> dict[str, Any]: def get_default_config() -> dict[str, Any]:
@ -129,6 +135,15 @@ def get_default_journal_path() -> str:
return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME) return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME)
def get_templates_path() -> Path:
# jrnl_xdg_resource_path is created by save_data_path if it does not exist
jrnl_xdg_resource_path = Path(xdg.BaseDirectory.save_data_path(XDG_RESOURCE))
jrnl_templates_path = jrnl_xdg_resource_path / "templates"
# Create the directory if needed.
jrnl_templates_path.mkdir(exist_ok=True)
return jrnl_templates_path
def scope_config(config: dict, journal_name: str) -> dict: def scope_config(config: dict, journal_name: str) -> dict:
if journal_name not in config["journals"]: if journal_name not in config["journals"]:
return config return config

View file

@ -11,11 +11,11 @@ from jrnl import time
from jrnl.config import DEFAULT_JOURNAL_KEY from jrnl.config import DEFAULT_JOURNAL_KEY
from jrnl.config import get_config_path from jrnl.config import get_config_path
from jrnl.config import get_journal_name from jrnl.config import get_journal_name
from jrnl.config import get_templates_path
from jrnl.config import scope_config from jrnl.config import scope_config
from jrnl.editor import get_text_from_editor from jrnl.editor import get_text_from_editor
from jrnl.editor import get_text_from_stdin from jrnl.editor import get_text_from_stdin
from jrnl.exception import JrnlException from jrnl.exception import JrnlException
from jrnl.journals import Journal
from jrnl.journals import open_journal from jrnl.journals import open_journal
from jrnl.messages import Message from jrnl.messages import Message
from jrnl.messages import MsgStyle from jrnl.messages import MsgStyle
@ -23,12 +23,13 @@ from jrnl.messages import MsgText
from jrnl.output import print_msg from jrnl.output import print_msg
from jrnl.output import print_msgs from jrnl.output import print_msgs
from jrnl.override import apply_overrides from jrnl.override import apply_overrides
from jrnl.path import expand_path from jrnl.path import absolute_path
if TYPE_CHECKING: if TYPE_CHECKING:
from argparse import Namespace from argparse import Namespace
from jrnl.journals import Entry from jrnl.journals import Entry
from jrnl.journals import Journal
def run(args: "Namespace"): def run(args: "Namespace"):
@ -38,8 +39,9 @@ def run(args: "Namespace"):
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 require config (encrypt, decrypt, etc), then exit
4. Load specified journal 4. Load specified journal
5. Start write mode, or search mode 5. Start append mode, or search mode
6. Profit 6. Perform actions with results from search mode (if needed)
7. Profit
""" """
# Run command if possible before config is available # Run command if possible before config is available
@ -71,91 +73,170 @@ def run(args: "Namespace"):
"args": args, "args": args,
"config": config, "config": config,
"journal": journal, "journal": journal,
"old_entries": journal.entries,
} }
if _is_write_mode(**kwargs): if _is_append_mode(**kwargs):
write_mode(**kwargs) append_mode(**kwargs)
return
# If not append mode, then we're in search mode (only 2 modes exist)
search_mode(**kwargs)
entries_found_count = len(journal)
_print_entries_found_count(entries_found_count, args)
# Actions
_perform_actions_on_search_results(**kwargs)
if entries_found_count != 0 and _has_action_args(args):
_print_changed_counts(journal)
else: else:
search_mode(**kwargs) # display only occurs if no other action occurs
_display_search_results(**kwargs)
def _is_write_mode(args: "Namespace", config: dict, **kwargs) -> bool: def _perform_actions_on_search_results(**kwargs):
"""Determines if we are in write mode (as opposed to search mode)""" args = kwargs["args"]
# Perform actions (if needed)
if args.change_time:
_change_time_search_results(**kwargs)
if args.delete:
_delete_search_results(**kwargs)
# open results in editor (if `--edit` was used)
if args.edit:
_edit_search_results(**kwargs)
def _is_append_mode(args: "Namespace", config: dict, **kwargs) -> bool:
"""Determines if we are in append mode (as opposed to search mode)"""
# Are any search filters present? If so, then search mode. # Are any search filters present? If so, then search mode.
write_mode = not any( append_mode = (
( not _has_search_args(args)
args.contains, and not _has_action_args(args)
args.delete, and not _has_display_args(args)
args.edit,
args.change_time,
args.excluded,
args.exclude_starred,
args.exclude_tagged,
args.export,
args.end_date,
args.today_in_history,
args.month,
args.day,
args.year,
args.limit,
args.on_date,
args.short,
args.starred,
args.start_date,
args.strict,
args.tagged,
args.tags,
)
) )
# Might be writing and want to move to editor part of the way through # Might be writing and want to move to editor part of the way through
if args.edit and args.text: if args.edit and args.text:
write_mode = True append_mode = True
# If the text is entirely tags, then we are also searching (not writing) # If the text is entirely tags, then we are also searching (not writing)
if ( if append_mode and args.text and _has_only_tags(config["tagsymbols"], args.text):
write_mode append_mode = False
and args.text
and all(word[0] in config["tagsymbols"] for word in " ".join(args.text).split())
):
write_mode = False
return write_mode return append_mode
def write_mode(args: "Namespace", config: dict, journal: Journal, **kwargs) -> None: def _read_template_file(template_arg: str, template_path_from_config: str) -> str:
"""
This function is called when either a template file is passed with --template, or config.template is set.
The processing logic is:
If --template was not used: Load the global template file.
If --template was used:
* Check $XDG_DATA_HOME/jrnl/templates/template_arg.
* Check template_arg as an absolute / relative path.
If a file is found, its contents are returned as a string.
If not, a JrnlException is raised.
"""
logging.debug(
"Append mode: Either a template arg was passed, or the global config is set."
)
# If filename is unset, we are in this flow due to a global template being configured
if not template_arg:
logging.debug("Append mode: Global template configuration detected.")
global_template_path = absolute_path(template_path_from_config)
try:
with open(global_template_path, encoding="utf-8") as f:
template_data = f.read()
return template_data
except FileNotFoundError:
raise JrnlException(
Message(
MsgText.CantReadTemplateGlobalConfig,
MsgStyle.ERROR,
{
"global_template_path": global_template_path,
},
)
)
else: # A template CLI arg was passed.
logging.debug("Trying to load template from $XDG_DATA_HOME/jrnl/templates/")
jrnl_template_dir = get_templates_path()
logging.debug(f"Append mode: jrnl templates directory: {jrnl_template_dir}")
template_path = jrnl_template_dir / template_arg
try:
with open(template_path, encoding="utf-8") as f:
template_data = f.read()
return template_data
except FileNotFoundError:
logging.debug(
f"Couldn't open {template_path}. Treating --template argument like a local / abs path."
)
pass
normalized_template_arg_filepath = absolute_path(template_arg)
try:
with open(normalized_template_arg_filepath, encoding="utf-8") as f:
template_data = f.read()
return template_data
except FileNotFoundError:
raise JrnlException(
Message(
MsgText.CantReadTemplateCLIArg,
MsgStyle.ERROR,
{
"normalized_template_arg_filepath": normalized_template_arg_filepath,
"jrnl_template_dir": template_path,
},
)
)
def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -> None:
""" """
Gets input from the user to write to the journal Gets input from the user to write to the journal
0. Check for a template passed as an argument, or in the global config
1. Check for input from cli 1. Check for input from cli
2. Check input being piped in 2. Check input being piped in
3. Open editor if configured (prepopulated with template if available) 3. Open editor if configured (prepopulated with template if available)
4. Use stdin.read as last resort 4. Use stdin.read as last resort
6. Write any found text to journal, or exit 6. Write any found text to journal, or exit
""" """
logging.debug("Write mode: starting") logging.debug("Append mode: starting")
if args.text: if args.template or config["template"]:
logging.debug("Write mode: cli text detected: %s", args.text) logging.debug(f"Append mode: template CLI arg detected: {args.template}")
# Read template file and pass as raw text into the composer
template_data = _read_template_file(args.template, config["template"])
raw = _write_in_editor(config, template_data)
if raw == template_data:
logging.error("Append mode: raw text was the same as the template")
raise JrnlException(Message(MsgText.NoChangesToTemplate, MsgStyle.NORMAL))
elif args.text:
logging.debug(f"Append mode: cli text detected: {args.text}")
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()
if args.edit: if args.edit:
raw = _write_in_editor(config, raw) raw = _write_in_editor(config, raw)
elif not sys.stdin.isatty(): elif not sys.stdin.isatty():
logging.debug("Write mode: receiving piped text") logging.debug("Append mode: receiving piped text")
raw = sys.stdin.read() raw = sys.stdin.read()
else: else:
raw = _write_in_editor(config) raw = _write_in_editor(config)
if not raw or raw.isspace(): if not raw or raw.isspace():
logging.error("Write mode: couldn't get raw text or entry was empty") logging.error("Append mode: couldn't get raw text or entry was empty")
raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL)) raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL))
if config["template"] and raw == _get_editor_template(config):
logging.error("Write mode: raw text was the same as the template")
raise JrnlException(Message(MsgText.NoChangesToTemplate, MsgStyle.NORMAL))
logging.debug( logging.debug(
'Write mode: appending raw text to journal "%s": %s', args.journal_name, raw f"Append mode: appending raw text to journal '{args.journal_name}': {raw}"
) )
journal.new_entry(raw) journal.new_entry(raw)
if args.journal_name != DEFAULT_JOURNAL_KEY: if args.journal_name != DEFAULT_JOURNAL_KEY:
@ -167,126 +248,36 @@ def write_mode(args: "Namespace", config: dict, journal: Journal, **kwargs) -> N
) )
) )
journal.write() journal.write()
logging.debug("Write mode: completed journal.write()") logging.debug("Append mode: completed journal.write()")
def search_mode(args: "Namespace", journal: Journal, **kwargs) -> None: def search_mode(args: "Namespace", journal: "Journal", **kwargs) -> None:
""" """
Search for entries in a journal, then either: Search for entries in a journal, and return the
1. Send them to configured editor for user manipulation (and also results. If no search args, then return all results
change their timestamps if requested)
2. Change their timestamps
2. Delete them (with confirmation for each entry)
3. Display them (with formatting options)
""" """
kwargs = { logging.debug("Search mode: starting")
**kwargs,
"args": args,
"journal": journal,
"old_entries": journal.entries,
}
if _has_search_args(args): # If no search args, then return all results (don't filter anything)
_filter_journal_entries(**kwargs) if not _has_search_args(args) and not _has_display_args(args) and not args.text:
_print_entries_found_count(len(journal), args) logging.debug("Search mode: has no search args")
# Where do the search results go?
if args.edit:
# If we want to both edit and change time in one action
if args.change_time:
# Generate a new list instead of assigning so it won't be
# modified by _change_time_search_results
selected_entries = [e for e in journal.entries]
no_change_time_prompt = len(journal.entries) == 1
_change_time_search_results(no_prompt=no_change_time_prompt, **kwargs)
# Re-filter the journal enties (_change_time_search_results
# puts the filtered entries back); use selected_entries
# instead of running _search_journal again, because times
# have changed since the original search
kwargs["old_entries"] = journal.entries
journal.entries = selected_entries
_edit_search_results(**kwargs)
elif not journal:
# Bail out if there are no entries and we're not editing
return return
elif args.change_time: logging.debug("Search mode: has search args")
_change_time_search_results(**kwargs) _filter_journal_entries(args, journal)
elif args.delete:
_delete_search_results(**kwargs)
else:
_display_search_results(**kwargs)
def _write_in_editor(config: dict, template: str | None = None) -> str: def _write_in_editor(config: dict, prepopulated_text: str | None = None) -> str:
if config["editor"]: if config["editor"]:
logging.debug("Write mode: opening editor") logging.debug("Append mode: opening editor")
if not template: raw = get_text_from_editor(config, prepopulated_text)
template = _get_editor_template(config)
raw = get_text_from_editor(config, template)
else: else:
raw = get_text_from_stdin() raw = get_text_from_stdin()
return raw return raw
def _get_editor_template(config: dict, **kwargs) -> str: def _filter_journal_entries(args: "Namespace", journal: "Journal", **kwargs) -> None:
logging.debug("Write mode: loading template for entry")
if not config["template"]:
logging.debug("Write mode: no template configured")
return ""
template_path = expand_path(config["template"])
try:
with open(template_path) as f:
template = f.read()
logging.debug("Write mode: template loaded: %s", template)
except OSError:
logging.error("Write mode: template not loaded")
raise JrnlException(
Message(
MsgText.CantReadTemplate,
MsgStyle.ERROR,
{"template": template_path},
)
)
return template
def _has_search_args(args: "Namespace") -> bool:
return any(
(
args.on_date,
args.today_in_history,
args.text,
args.month,
args.day,
args.year,
args.start_date,
args.end_date,
args.strict,
args.starred,
args.tagged,
args.excluded,
args.exclude_starred,
args.exclude_tagged,
args.contains,
args.limit,
)
)
def _filter_journal_entries(args: "Namespace", journal: Journal, **kwargs) -> None:
"""Filter journal entries in-place based upon search args""" """Filter journal entries in-place based upon search args"""
if args.on_date: if args.on_date:
args.start_date = args.end_date = args.on_date args.start_date = args.end_date = args.on_date
@ -315,6 +306,7 @@ def _filter_journal_entries(args: "Namespace", journal: Journal, **kwargs) -> No
def _print_entries_found_count(count: int, args: "Namespace") -> None: def _print_entries_found_count(count: int, args: "Namespace") -> None:
logging.debug(f"count: {count}")
if count == 0: if count == 0:
if args.edit or args.change_time: if args.edit or args.change_time:
print_msg(Message(MsgText.NothingToModify, MsgStyle.WARNING)) print_msg(Message(MsgText.NothingToModify, MsgStyle.WARNING))
@ -322,26 +314,26 @@ def _print_entries_found_count(count: int, args: "Namespace") -> None:
print_msg(Message(MsgText.NothingToDelete, MsgStyle.WARNING)) print_msg(Message(MsgText.NothingToDelete, MsgStyle.WARNING))
else: else:
print_msg(Message(MsgText.NoEntriesFound, MsgStyle.NORMAL)) print_msg(Message(MsgText.NoEntriesFound, MsgStyle.NORMAL))
elif args.limit:
# Don't show count if the user expects a limited number of results
return return
elif args.edit or not (args.change_time or args.delete): elif args.limit and args.limit == count:
# Don't show count if we are ONLY changing the time or deleting entries # Don't show count if the user expects a limited number of results
my_msg = ( logging.debug("args.limit is true-ish")
MsgText.EntryFoundCountSingular return
if count == 1
else MsgText.EntryFoundCountPlural logging.debug("Printing general summary")
) my_msg = (
print_msg(Message(my_msg, MsgStyle.NORMAL, {"num": count})) MsgText.EntryFoundCountSingular if count == 1 else MsgText.EntryFoundCountPlural
)
print_msg(Message(my_msg, MsgStyle.NORMAL, {"num": count}))
def _other_entries(journal: Journal, entries: list["Entry"]) -> list["Entry"]: def _other_entries(journal: "Journal", entries: list["Entry"]) -> list["Entry"]:
"""Find entries that are not in journal""" """Find entries that are not in journal"""
return [e for e in entries if e not in journal.entries] return [e for e in entries if e not in journal.entries]
def _edit_search_results( def _edit_search_results(
config: dict, journal: Journal, old_entries: list["Entry"], **kwargs config: dict, journal: "Journal", old_entries: list["Entry"], **kwargs
) -> None: ) -> None:
""" """
1. Send the given journal entries to the user-configured editor 1. Send the given journal entries to the user-configured editor
@ -360,15 +352,18 @@ def _edit_search_results(
# separate entries we are not editing # separate entries we are not editing
other_entries = _other_entries(journal, old_entries) other_entries = _other_entries(journal, old_entries)
# Get stats now for summary later
old_stats = _get_predit_stats(journal)
# Send user to the editor # Send user to the editor
edited = get_text_from_editor(config, journal.editable_str()) try:
journal.parse_editable_str(edited) edited = get_text_from_editor(config, journal.editable_str())
except JrnlException as e:
if e.has_message_text(MsgText.NoTextReceived):
raise JrnlException(
Message(MsgText.NoEditsReceivedJournalNotDeleted, MsgStyle.WARNING)
)
else:
raise e
# Print summary if available journal.parse_editable_str(edited)
_print_edited_summary(journal, old_stats)
# Put back entries we separated earlier, sort, and write the journal # Put back entries we separated earlier, sort, and write the journal
journal.entries += other_entries journal.entries += other_entries
@ -376,15 +371,8 @@ def _edit_search_results(
journal.write() journal.write()
def _print_edited_summary( def _print_changed_counts(journal: "Journal", **kwargs) -> None:
journal: Journal, old_stats: dict[str, int], **kwargs stats = journal.get_change_counts()
) -> None:
stats = {
"added": len(journal) - old_stats["count"],
"deleted": old_stats["count"] - len(journal),
"modified": len([e for e in journal.entries if e.modified]),
}
stats["modified"] -= stats["added"]
msgs = [] msgs = []
if stats["added"] > 0: if stats["added"] > 0:
@ -417,17 +405,18 @@ def _print_edited_summary(
print_msgs(msgs) print_msgs(msgs)
def _get_predit_stats(journal: Journal) -> dict[str, int]: def _get_predit_stats(journal: "Journal") -> dict[str, int]:
return {"count": len(journal)} return {"count": len(journal)}
def _delete_search_results( def _delete_search_results(
journal: Journal, old_entries: list["Entry"], **kwargs journal: "Journal", old_entries: list["Entry"], **kwargs
) -> None: ) -> None:
entries_to_delete = journal.prompt_action_entries(MsgText.DeleteEntryQuestion) entries_to_delete = journal.prompt_action_entries(MsgText.DeleteEntryQuestion)
journal.entries = old_entries
if entries_to_delete: if entries_to_delete:
journal.entries = old_entries
journal.delete_entries(entries_to_delete) journal.delete_entries(entries_to_delete)
journal.write() journal.write()
@ -435,34 +424,27 @@ def _delete_search_results(
def _change_time_search_results( def _change_time_search_results(
args: "Namespace", args: "Namespace",
journal: Journal, journal: "Journal",
old_entries: list["Entry"], old_entries: list["Entry"],
no_prompt: bool = False, no_prompt: bool = False,
**kwargs **kwargs,
) -> None: ) -> None:
# separate entries we are not editing # separate entries we are not editing
other_entries = _other_entries(journal, old_entries) # @todo if there's only 1, don't prompt
entries_to_change = journal.prompt_action_entries(MsgText.ChangeTimeEntryQuestion)
if no_prompt:
entries_to_change = journal.entries
else:
entries_to_change = journal.prompt_action_entries(
MsgText.ChangeTimeEntryQuestion
)
if entries_to_change: if entries_to_change:
other_entries += [e for e in journal.entries if e not in entries_to_change]
journal.entries = entries_to_change
date = time.parse(args.change_time) date = time.parse(args.change_time)
journal.change_date_entries(date) journal.entries = old_entries
journal.change_date_entries(date, entries_to_change)
journal.entries += other_entries
journal.sort()
journal.write() journal.write()
def _display_search_results(args: "Namespace", journal: Journal, **kwargs) -> None: def _display_search_results(args: "Namespace", journal: "Journal", **kwargs) -> None:
if len(journal) == 0:
return
# Get export format from config file if not provided at the command line # Get export format from config file if not provided at the command line
args.export = args.export or kwargs["config"].get("display_format") args.export = args.export or kwargs["config"].get("display_format")
@ -480,3 +462,50 @@ def _display_search_results(args: "Namespace", journal: Journal, **kwargs) -> No
print(exporter.export(journal, args.filename)) print(exporter.export(journal, args.filename))
else: else:
print(journal.pprint()) print(journal.pprint())
def _has_search_args(args: "Namespace") -> bool:
"""Looking for arguments that filter a journal"""
return any(
(
args.contains,
args.tagged,
args.excluded,
args.exclude_starred,
args.exclude_tagged,
args.end_date,
args.today_in_history,
args.month,
args.day,
args.year,
args.limit,
args.on_date,
args.starred,
args.start_date,
args.strict, # -and
)
)
def _has_action_args(args: "Namespace") -> bool:
return any(
(
args.change_time,
args.delete,
args.edit,
)
)
def _has_display_args(args: "Namespace") -> bool:
return any(
(
args.tags,
args.short,
args.export, # --format
)
)
def _has_only_tags(tag_symbols: str, args_text: str) -> bool:
return all(word[0] in tag_symbols for word in " ".join(args_text).split())

View file

@ -66,7 +66,7 @@ def get_text_from_stdin() -> str:
try: try:
raw = sys.stdin.read() raw = sys.stdin.read()
except KeyboardInterrupt: except KeyboardInterrupt:
logging.error("Write mode: keyboard interrupt") logging.error("Append mode: keyboard interrupt")
raise JrnlException( raise JrnlException(
Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE), Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE),
Message(MsgText.JournalNotSaved, MsgStyle.WARNING), Message(MsgText.JournalNotSaved, MsgStyle.WARNING),

View file

@ -7,6 +7,7 @@ from jrnl.output import print_msg
if TYPE_CHECKING: if TYPE_CHECKING:
from jrnl.messages import Message from jrnl.messages import Message
from jrnl.messages import MsgText
class JrnlException(Exception): class JrnlException(Exception):
@ -18,3 +19,6 @@ class JrnlException(Exception):
def print(self) -> None: def print(self) -> None:
for msg in self.messages: for msg in self.messages:
print_msg(msg) print_msg(msg)
def has_message_text(self, message_text: "MsgText"):
return any([m.text == message_text for m in self.messages])

View file

@ -2,8 +2,8 @@
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
import codecs import codecs
import fnmatch
import os import os
import pathlib
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from jrnl import time from jrnl import time
@ -13,14 +13,11 @@ from .Journal import Journal
if TYPE_CHECKING: if TYPE_CHECKING:
from jrnl.journals import Entry from jrnl.journals import Entry
# glob search patterns for folder/file structure
def get_files(journal_config: str) -> list[str]: DIGIT_PATTERN = "[0123456789]"
"""Searches through sub directories starting with journal_config and find all text files""" YEAR_PATTERN = DIGIT_PATTERN * 4
filenames = [] MONTH_PATTERN = "[01]" + DIGIT_PATTERN
for root, dirnames, f in os.walk(journal_config): DAY_PATTERN = "[0123]" + DIGIT_PATTERN + ".txt"
for filename in fnmatch.filter(f, "*.txt"):
filenames.append(os.path.join(root, filename))
return filenames
class Folder(Journal): class Folder(Journal):
@ -35,12 +32,15 @@ class Folder(Journal):
def open(self) -> "Folder": def open(self) -> "Folder":
filenames = [] filenames = []
self.entries = [] self.entries = []
filenames = get_files(self.config["journal"])
for filename in filenames: if os.path.exists(self.config["journal"]):
with codecs.open(filename, "r", "utf-8") as f: filenames = Folder._get_files(self.config["journal"])
journal = f.read() for filename in filenames:
self.entries.extend(self._parse(journal)) with codecs.open(filename, "r", "utf-8") as f:
self.sort() journal = f.read()
self.entries.extend(self._parse(journal))
self.sort()
return self return self
def write(self) -> None: def write(self) -> None:
@ -81,7 +81,7 @@ class Folder(Journal):
journal_file.write(journal) journal_file.write(journal)
# look for and delete empty files # look for and delete empty files
filenames = [] filenames = []
filenames = get_files(self.config["journal"]) filenames = Folder._get_files(self.config["journal"])
for filename in filenames: for filename in filenames:
if os.stat(filename).st_size <= 0: if os.stat(filename).st_size <= 0:
os.remove(filename) os.remove(filename)
@ -91,17 +91,19 @@ class Folder(Journal):
for entry in entries_to_delete: for entry in entries_to_delete:
self.entries.remove(entry) self.entries.remove(entry)
self._diff_entry_dates.append(entry.date) self._diff_entry_dates.append(entry.date)
self.deleted_entry_count += 1
def change_date_entries(self, date: str) -> None: def change_date_entries(self, date: str, entries_to_change: list["Entry"]) -> None:
"""Changes entry dates to given date.""" """Changes entry dates to given date."""
date = time.parse(date) date = time.parse(date)
self._diff_entry_dates.append(date) self._diff_entry_dates.append(date)
for entry in self.entries: for entry in entries_to_change:
self._diff_entry_dates.append(entry.date) self._diff_entry_dates.append(entry.date)
entry.date = date entry.date = date
entry.modified = True
def parse_editable_str(self, edited: str) -> None: def parse_editable_str(self, edited: str) -> None:
"""Parses the output of self.editable_str and updates its entries.""" """Parses the output of self.editable_str and updates its entries."""
@ -114,4 +116,42 @@ class Folder(Journal):
# modified and how many got deleted later. # modified and how many got deleted later.
for entry in mod_entries: for entry in mod_entries:
entry.modified = not any(entry == old_entry for old_entry in self.entries) entry.modified = not any(entry == old_entry for old_entry in self.entries)
self.increment_change_counts_by_edit(mod_entries)
self.entries = mod_entries self.entries = mod_entries
@staticmethod
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"""
for year_folder in Folder._get_year_folders(pathlib.Path(journal_path)):
for month_folder in Folder._get_month_folders(year_folder):
yield from Folder._get_day_files(month_folder)
@staticmethod
def _get_year_folders(path: pathlib.Path) -> list[pathlib.Path]:
for child in path.glob(YEAR_PATTERN):
if child.is_dir():
yield child
return
@staticmethod
def _get_month_folders(path: pathlib.Path) -> list[pathlib.Path]:
for child in path.glob(MONTH_PATTERN):
if int(child.name) > 0 and int(child.name) <= 12 and path.is_dir():
yield child
return
@staticmethod
def _get_day_files(path: pathlib.Path) -> list[str]:
for child in path.glob(DAY_PATTERN):
if (
int(child.stem) > 0
and int(child.stem) <= 31
and time.is_valid_date(
year=int(path.parent.name),
month=int(path.name),
day=int(child.stem),
)
and child.is_file()
):
yield str(child)

View file

@ -51,6 +51,10 @@ class Journal:
self.entries = [] self.entries = []
self.encryption_method = None self.encryption_method = None
# Track changes to journal in session. Modified is tracked in Entry
self.added_entry_count = 0
self.deleted_entry_count = 0
def __len__(self): def __len__(self):
"""Returns the number of entries""" """Returns the number of entries"""
return len(self.entries) return len(self.entries)
@ -305,13 +309,17 @@ class Journal:
"""Deletes specific entries from a journal.""" """Deletes specific entries from a journal."""
for entry in entries_to_delete: for entry in entries_to_delete:
self.entries.remove(entry) self.entries.remove(entry)
self.deleted_entry_count += 1
def change_date_entries(self, date: datetime.datetime | None) -> None: def change_date_entries(
self, date: datetime.datetime, entries_to_change: list[Entry]
) -> None:
"""Changes entry dates to given date.""" """Changes entry dates to given date."""
date = time.parse(date) date = time.parse(date)
for entry in self.entries: for entry in entries_to_change:
entry.date = date entry.date = date
entry.modified = True
def prompt_action_entries(self, msg: MsgText) -> list[Entry]: def prompt_action_entries(self, msg: MsgText) -> list[Entry]:
"""Prompts for action for each entry in a journal, using given message. """Prompts for action for each entry in a journal, using given message.
@ -335,7 +343,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")
# Split raw text into title and body # Split raw text into title and body
@ -382,8 +391,24 @@ class Journal:
# modified and how many got deleted later. # modified and how many got deleted later.
for entry in mod_entries: for entry in mod_entries:
entry.modified = not any(entry == old_entry for old_entry in self.entries) entry.modified = not any(entry == old_entry for old_entry in self.entries)
self.increment_change_counts_by_edit(mod_entries)
self.entries = mod_entries self.entries = mod_entries
def increment_change_counts_by_edit(self, mod_entries: Entry) -> None:
if len(mod_entries) > len(self.entries):
self.added_entry_count += len(mod_entries) - len(self.entries)
else:
self.deleted_entry_count += len(self.entries) - len(mod_entries)
def get_change_counts(self) -> dict:
return {
"added": self.added_entry_count,
"deleted": self.deleted_entry_count,
"modified": len([e for e in self.entries if e.modified]),
}
class LegacyJournal(Journal): class LegacyJournal(Journal):
"""Legacy class to support opening journals formatted with the jrnl 1.x """Legacy class to support opening journals formatted with the jrnl 1.x
@ -443,7 +468,7 @@ def open_journal(journal_name: str, config: dict, legacy: bool = False) -> Journ
If legacy is True, it will open Journals with legacy classes build for If legacy is True, it will open Journals with legacy classes build for
backwards compatibility with jrnl 1.x backwards compatibility with jrnl 1.x
""" """
logging.debug("open_journal start") logging.debug(f"open_journal '{journal_name}'")
validate_journal_name(journal_name, config) validate_journal_name(journal_name, config)
config = config.copy() config = config.copy()
config["journal"] = expand_path(config["journal"]) config["journal"] = expand_path(config["journal"])

View file

@ -105,10 +105,16 @@ class MsgText(Enum):
KeyboardInterruptMsg = "Aborted by user" KeyboardInterruptMsg = "Aborted by user"
CantReadTemplate = """ CantReadTemplateGlobalConfig = """
Unreadable template Could not read template file defined in config:
Could not read template file at: {global_template_path}
{template} """
CantReadTemplateCLIArg = """
Unable to find a template file based on the passed arg, and no global template was detected.
The following filepaths were checked:
jrnl XDG Template Directory : {jrnl_template_dir}
Local Filepath : {normalized_template_arg_filepath}
""" """
NoNamedJournal = "No '{journal_name}' journal configured\n{journals}" NoNamedJournal = "No '{journal_name}' journal configured\n{journals}"
@ -159,6 +165,14 @@ class MsgText(Enum):
https://jrnl.sh/en/stable/external-editors/ https://jrnl.sh/en/stable/external-editors/
""" """
NoEditsReceivedJournalNotDeleted = """
No text received from editor. Were you trying to delete all the entries?
This seems a bit drastic, so the operation was cancelled.
To delete all entries, use the --delete option.
"""
NoEditsReceived = "No edits to save, because nothing was changed" NoEditsReceived = "No edits to save, because nothing was changed"
NoTextReceived = """ NoTextReceived = """

View file

@ -9,6 +9,7 @@ from jrnl.config import update_config
if TYPE_CHECKING: if TYPE_CHECKING:
from argparse import Namespace from argparse import Namespace
# import logging # 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. """Unpack CLI provided overrides into the configuration tree.
@ -26,7 +27,6 @@ def apply_overrides(args: "Namespace", base_config: dict) -> dict:
cfg_with_overrides = base_config.copy() cfg_with_overrides = base_config.copy()
for pairs in overrides: for pairs in overrides:
pairs = make_yaml_valid_dict(pairs) pairs = make_yaml_valid_dict(pairs)
key_as_dots, override_value = _get_key_and_value_from_pair(pairs) key_as_dots, override_value = _get_key_and_value_from_pair(pairs)
keys = _convert_dots_to_list(key_as_dots) keys = _convert_dots_to_list(key_as_dots)

View file

@ -34,7 +34,7 @@ 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 less characters and was parsed from the existing journal. # Don't try to parse anything with 6 or fewer characters and was parsed from the existing journal.
# It's probably a markdown footnote # It's probably a markdown footnote
if len(date_str) <= 6 and bracketed: if len(date_str) <= 6 and bracketed:
return None return None
@ -83,9 +83,17 @@ def parse(
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 wrong.
# Rather then this, we would like to see parsedatetime patched so we can tell it to prefer # Rather than this, we would like to see parsedatetime patched so we can tell it to prefer
# past dates # 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)
return date return date
def is_valid_date(year: int, month: int, day: int) -> bool:
try:
datetime.datetime(year, month, day)
return True
except ValueError:
return False

266
poetry.lock generated
View file

@ -1,4 +1,19 @@
# This file is automatically @generated by Poetry and should not be changed by hand. # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "ansiwrap"
version = "0.8.4"
description = "textwrap, but savvy to ANSI colors and styles"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "ansiwrap-0.8.4-py2.py3-none-any.whl", hash = "sha256:7b053567c88e1ad9eed030d3ac41b722125e4c1271c8a99ade797faff1f49fb1"},
{file = "ansiwrap-0.8.4.zip", hash = "sha256:ca0c740734cde59bf919f8ff2c386f74f9a369818cdc60efe94893d01ea8d9b7"},
]
[package.dependencies]
textwrap3 = ">=0.9.2"
[[package]] [[package]]
name = "appnope" name = "appnope"
@ -42,25 +57,6 @@ six = "*"
[package.extras] [package.extras]
test = ["astroid", "pytest"] test = ["astroid", "pytest"]
[[package]]
name = "attrs"
version = "22.2.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
{file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
]
[package.extras]
cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
dev = ["attrs[docs,tests]"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
tests = ["attrs[tests-no-zope]", "zope.interface"]
tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
[[package]] [[package]]
name = "backcall" name = "backcall"
version = "0.2.0" version = "0.2.0"
@ -75,37 +71,37 @@ files = [
[[package]] [[package]]
name = "black" name = "black"
version = "23.1.0" version = "23.3.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
{file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
{file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
{file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
{file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
{file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
{file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
{file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
{file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
{file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
{file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
{file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
{file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
{file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
] ]
[package.dependencies] [package.dependencies]
@ -374,35 +370,31 @@ files = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "39.0.1" version = "40.0.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"}, {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b"},
{file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"}, {file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"}, {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"}, {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"}, {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"}, {file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"}, {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"}, {file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c"},
{file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"}, {file = "cryptography-40.0.2-cp36-abi3-win32.whl", hash = "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9"},
{file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"}, {file = "cryptography-40.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b"},
{file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"}, {file = "cryptography-40.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b"},
{file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"}, {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"}, {file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"}, {file = "cryptography-40.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"}, {file = "cryptography-40.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"}, {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"}, {file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"}, {file = "cryptography-40.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"}, {file = "cryptography-40.0.2.tar.gz", hash = "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"},
{file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"},
] ]
[package.dependencies] [package.dependencies]
@ -411,10 +403,10 @@ cffi = ">=1.12"
[package.extras] [package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"] pep8test = ["black", "check-manifest", "mypy", "ruff"]
sdist = ["setuptools-rust (>=0.11.4)"] sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"] test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"]
test-randomorder = ["pytest-randomly"] test-randomorder = ["pytest-randomly"]
tox = ["tox"] tox = ["tox"]
@ -501,19 +493,19 @@ tests = ["asttokens", "littleutils", "pytest", "rich"]
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.9.0" version = "3.11.0"
description = "A platform independent file lock." description = "A platform independent file lock."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, {file = "filelock-3.11.0-py3-none-any.whl", hash = "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"},
{file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, {file = "filelock-3.11.0.tar.gz", hash = "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
[[package]] [[package]]
name = "flake8" name = "flake8"
@ -589,14 +581,14 @@ flake8 = ">=3.7"
[[package]] [[package]]
name = "flake8-type-checking" name = "flake8-type-checking"
version = "2.3.0" version = "2.4.0"
description = "A flake8 plugin for managing type-checking imports & forward references" description = "A flake8 plugin for managing type-checking imports & forward references"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "flake8_type_checking-2.3.0-py3-none-any.whl", hash = "sha256:7117b8a22d64db02f9d8c724df5d2517e59c6290b034cfa54496c7ae73c07f51"}, {file = "flake8_type_checking-2.4.0-py3-none-any.whl", hash = "sha256:2dee127f300bb95b7f17b7c3fff4f6336f5e4ba92082c15928c6e19b666cfba4"},
{file = "flake8_type_checking-2.3.0.tar.gz", hash = "sha256:f802c9933b2a98b96fc4a0b3b90ef0f8379625f867cb73633c09fc2bf746333b"}, {file = "flake8_type_checking-2.4.0.tar.gz", hash = "sha256:9ea96d01e6557a47835acf04020c48fabb9c3d4664c15f2920915e09e65c1d55"},
] ]
[package.dependencies] [package.dependencies]
@ -605,14 +597,14 @@ flake8 = "*"
[[package]] [[package]]
name = "flakeheaven" name = "flakeheaven"
version = "3.2.1" version = "3.3.0"
description = "FlakeHeaven is a [Flake8](https://gitlab.com/pycqa/flake8) wrapper to make it cool." description = "FlakeHeaven is a [Flake8](https://gitlab.com/pycqa/flake8) wrapper to make it cool."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7,<4.0" python-versions = ">=3.7,<4.0"
files = [ files = [
{file = "flakeheaven-3.2.1-py3-none-any.whl", hash = "sha256:fdae542414a8cd327dbbc969bb18d5972379570f6562af21b4a83f67bdd6b87c"}, {file = "flakeheaven-3.3.0-py3-none-any.whl", hash = "sha256:ae246197a178845b30b63fc03023f7ba925cc84cc96314ec19807dafcd6b39a3"},
{file = "flakeheaven-3.2.1.tar.gz", hash = "sha256:f2d54aedd98b817e94c8c0fcc0da1230b43dbf911ce38aa412d00eb5db6fb71d"}, {file = "flakeheaven-3.3.0.tar.gz", hash = "sha256:eb07860e028ff8dd56cce742c4766624a37a4ce397fd34300254ab623d13047b"},
] ]
[package.dependencies] [package.dependencies]
@ -690,14 +682,14 @@ files = [
[[package]] [[package]]
name = "ipdb" name = "ipdb"
version = "0.13.11" version = "0.13.13"
description = "IPython-enabled pdb" description = "IPython-enabled pdb"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
{file = "ipdb-0.13.11-py3-none-any.whl", hash = "sha256:f74c2f741c18b909eaf89f19fde973f745ac721744aa1465888ce45813b63a9c"}, {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"},
{file = "ipdb-0.13.11.tar.gz", hash = "sha256:c23b6736f01fd4586cc2ecbebdf79a5eb454796853e1cd8f2ed3b7b91d4a3e93"}, {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"},
] ]
[package.dependencies] [package.dependencies]
@ -1206,19 +1198,19 @@ files = [
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "3.0.0" version = "3.2.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
@ -1238,14 +1230,14 @@ testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "poethepoet" name = "poethepoet"
version = "0.18.1" version = "0.19.0"
description = "A task runner that works well with poetry." description = "A task runner that works well with poetry."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "poethepoet-0.18.1-py3-none-any.whl", hash = "sha256:e85727bf6f4a10bf6c1a43026bdeb40df689bea3c4682d03cbe531cabc8f2ba6"}, {file = "poethepoet-0.19.0-py3-none-any.whl", hash = "sha256:87038be589077e4b407050a9da644d9cd9e4076ccfc8abc7f855cf6870d5c6c2"},
{file = "poethepoet-0.18.1.tar.gz", hash = "sha256:5f3566b14c2f5dccdfbc3bb26f0096006b38dc0b9c74bd4f8dd1eba7b0e29f6a"}, {file = "poethepoet-0.19.0.tar.gz", hash = "sha256:897eb85ec15876d79befc7d19d4c80ce7c8b214d1bb0dcfec640abd81616bfed"},
] ]
[package.dependencies] [package.dependencies]
@ -1362,38 +1354,37 @@ plugins = ["importlib-metadata"]
[[package]] [[package]]
name = "pyproject-api" name = "pyproject-api"
version = "1.5.0" version = "1.5.1"
description = "API to interact with the python pyproject.toml based projects" description = "API to interact with the python pyproject.toml based projects"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pyproject_api-1.5.0-py3-none-any.whl", hash = "sha256:4c111277dfb96bcd562c6245428f27250b794bfe3e210b8714c4f893952f2c17"}, {file = "pyproject_api-1.5.1-py3-none-any.whl", hash = "sha256:4698a3777c2e0f6b624f8a4599131e2a25376d90fe8d146d7ac74c67c6f97c43"},
{file = "pyproject_api-1.5.0.tar.gz", hash = "sha256:0962df21f3e633b8ddb9567c011e6c1b3dcdfc31b7860c0ede7e24c5a1200fbe"}, {file = "pyproject_api-1.5.1.tar.gz", hash = "sha256:435f46547a9ff22cf4208ee274fca3e2869aeb062a4834adfc99a4dd64af3cf9"},
] ]
[package.dependencies] [package.dependencies]
packaging = ">=21.3" packaging = ">=23"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[package.extras] [package.extras]
docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
testing = ["covdefaults (>=2.2.2)", "importlib-metadata (>=5.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "virtualenv (>=20.17)", "wheel (>=0.38.4)"] testing = ["covdefaults (>=2.2.2)", "importlib-metadata (>=6)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "virtualenv (>=20.17.1)", "wheel (>=0.38.4)"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.2.1" version = "7.3.1"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"},
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"},
] ]
[package.dependencies] [package.dependencies]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""} colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*" iniconfig = "*"
@ -1402,7 +1393,7 @@ pluggy = ">=0.12,<2.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras] [package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]] [[package]]
name = "pytest-bdd" name = "pytest-bdd"
@ -1441,14 +1432,14 @@ rich = ">=8.0.0"
[[package]] [[package]]
name = "pytest-xdist" name = "pytest-xdist"
version = "3.2.0" version = "3.2.1"
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pytest-xdist-3.2.0.tar.gz", hash = "sha256:fa10f95a2564cd91652f2d132725183c3b590d9fdcdec09d3677386ecf4c1ce9"}, {file = "pytest-xdist-3.2.1.tar.gz", hash = "sha256:1849bd98d8b242b948e472db7478e090bf3361912a8fed87992ed94085f54727"},
{file = "pytest_xdist-3.2.0-py3-none-any.whl", hash = "sha256:336098e3bbd8193276867cc87db8b22903c3927665dff9d1ac8684c02f597b68"}, {file = "pytest_xdist-3.2.1-py3-none-any.whl", hash = "sha256:37290d161638a20b672401deef1cba812d110ac27e35d213f091d15b8beb40c9"},
] ]
[package.dependencies] [package.dependencies]
@ -1603,19 +1594,19 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]] [[package]]
name = "rich" name = "rich"
version = "13.3.1" version = "13.3.4"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7.0" python-versions = ">=3.7.0"
files = [ files = [
{file = "rich-13.3.1-py3-none-any.whl", hash = "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9"}, {file = "rich-13.3.4-py3-none-any.whl", hash = "sha256:22b74cae0278fd5086ff44144d3813be1cedc9115bdfabbfefd86400cb88b20a"},
{file = "rich-13.3.1.tar.gz", hash = "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f"}, {file = "rich-13.3.4.tar.gz", hash = "sha256:b5d573e13605423ec80bdd0cd5f8541f7844a0e71a13f74cf454ccb2f490708b"},
] ]
[package.dependencies] [package.dependencies]
markdown-it-py = ">=2.1.0,<3.0.0" markdown-it-py = ">=2.2.0,<3.0.0"
pygments = ">=2.14.0,<3.0.0" pygments = ">=2.13.0,<3.0.0"
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"] jupyter = ["ipywidgets (>=7.5.1,<9)"]
@ -1656,6 +1647,8 @@ files = [
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"},
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"},
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"},
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"},
{file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"},
{file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"},
{file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"},
{file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"},
@ -1731,6 +1724,18 @@ pure-eval = "*"
[package.extras] [package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "textwrap3"
version = "0.9.2"
description = "textwrap from Python 3.6 backport (plus a few tweaks)"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "textwrap3-0.9.2-py2.py3-none-any.whl", hash = "sha256:bf5f4c40faf2a9ff00a9e0791fed5da7415481054cef45bb4a3cfb1f69044ae0"},
{file = "textwrap3-0.9.2.zip", hash = "sha256:5008eeebdb236f6303dcd68f18b856d355f6197511d952ba74bc75e40e0c3414"},
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.10.2" version = "0.10.2"
@ -1757,31 +1762,31 @@ files = [
[[package]] [[package]]
name = "tox" name = "tox"
version = "4.4.6" version = "4.4.12"
description = "tox is a generic virtualenv management and test command line tool" description = "tox is a generic virtualenv management and test command line tool"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tox-4.4.6-py3-none-any.whl", hash = "sha256:e3d4a65852f029e5ba441a01824d2d839d30bb8fb071635ef9cb53952698e6bf"}, {file = "tox-4.4.12-py3-none-any.whl", hash = "sha256:d4be558809d86fad13f4553976b0500352630a8fbfa39ea4b1ce3bd945ba680b"},
{file = "tox-4.4.6.tar.gz", hash = "sha256:9786671d23b673ace7499c602c5746e2a225d1ecd9d9f624d0461303f40bd93b"}, {file = "tox-4.4.12.tar.gz", hash = "sha256:740f5209d0dec19451b951ee5b1cce4a207acdc7357af84dbc8ec35bcf2c454e"},
] ]
[package.dependencies] [package.dependencies]
cachetools = ">=5.3" cachetools = ">=5.3"
chardet = ">=5.1" chardet = ">=5.1"
colorama = ">=0.4.6" colorama = ">=0.4.6"
filelock = ">=3.9" filelock = ">=3.11"
packaging = ">=23" packaging = ">=23"
platformdirs = ">=2.6.2" platformdirs = ">=3.2"
pluggy = ">=1" pluggy = ">=1"
pyproject-api = ">=1.5" pyproject-api = ">=1.5.1"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
virtualenv = ">=20.17.1" virtualenv = ">=20.21"
[package.extras] [package.extras]
docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-argparse-cli (>=1.11)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx-copybutton (>=0.5.1)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-argparse-cli (>=1.11)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx-copybutton (>=0.5.1)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.2.2)", "devpi-process (>=0.3)", "diff-cover (>=7.4)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.12.2)", "psutil (>=5.9.4)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-xdist (>=3.1)", "re-assert (>=1.1)", "time-machine (>=2.9)", "wheel (>=0.38.4)"] testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "devpi-process (>=0.3)", "diff-cover (>=7.5)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.14)", "psutil (>=5.9.4)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-xdist (>=3.2.1)", "re-assert (>=1.1)", "time-machine (>=2.9)", "wheel (>=0.40)"]
[[package]] [[package]]
name = "traitlets" name = "traitlets"
@ -1825,14 +1830,14 @@ files = [
[[package]] [[package]]
name = "tzlocal" name = "tzlocal"
version = "4.2" version = "4.3"
description = "tzinfo object for the local timezone" description = "tzinfo object for the local timezone"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.7"
files = [ files = [
{file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"}, {file = "tzlocal-4.3-py3-none-any.whl", hash = "sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2"},
{file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"}, {file = "tzlocal-4.3.tar.gz", hash = "sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355"},
] ]
[package.dependencies] [package.dependencies]
@ -1840,8 +1845,7 @@ pytz-deprecation-shim = "*"
tzdata = {version = "*", markers = "platform_system == \"Windows\""} tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras] [package.extras]
devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"] devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
test = ["pytest (>=4.3)", "pytest-mock (>=3.3)"]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
@ -1862,14 +1866,14 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.19.0" version = "20.21.0"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"},
{file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"},
] ]
[package.dependencies] [package.dependencies]
@ -1965,4 +1969,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.10.0, <3.13" python-versions = ">=3.10.0, <3.13"
content-hash = "39227dc2d020370fccdb4c3a0c140dbe1cda73e0ea703abb5d9c6857d2c145b9" content-hash = "dfc32ee61025dae6033987a8ff8290d4c2a34197502b8030cef02db58b86baf1"

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "jrnl" name = "jrnl"
version = "v3.3" version = "v4.0-beta3"
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>",

View file

@ -0,0 +1,108 @@
# Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
Feature: Test combinations of edit, change-time, and delete
Scenario Outline: --change-time with --edit modifies selected entries
Given we use the config "<config_file>"
And we write nothing to the editor if opened
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' --edit" and enter
Y
N
Y
Then the error output should contain "No text received from editor. Were you trying to delete all the entries?"
And the editor should have been called
When we run "jrnl -99 --short"
Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series.
2022-04-23 10:30 Entry the first.
2022-04-23 10:30 The third entry finally after weeks without writing.
Examples: Configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo
Scenario Outline: --delete with --edit deletes selected entries
Given we use the config "<config_file>"
And we append to the editor if opened
[2023-02-21 10:32] Here is a new entry
And we use the password "test" if prompted
When we run "jrnl --delete --edit" and enter
Y
N
Y
Then the editor should have been called
And the error output should contain "3 entries found"
And the error output should contain "2 entries deleted"
And the error output should contain "1 entry added"
When we run "jrnl -99 --short"
Then the error output should contain "2 entries found"
And the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series.
2023-02-21 10:32 Here is a new entry
Examples: Configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo
Scenario Outline: --change-time with --delete affects appropriate entries
Given we use the config "<config_file>"
And we use the password "test" if prompted
# --change-time is asked first, then --delete
When we run "jrnl --change-time '2022-04-23 10:30' --delete" and enter
N
N
Y
Y
N
N
Then the error output should contain "3 entries found"
And the error output should contain "1 entry deleted"
And the error output should contain "1 entry modified"
When we run "jrnl -99 --short"
Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series.
2022-04-23 10:30 The third entry finally after weeks without writing.
Examples: Configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo
Scenario Outline: Combining --change-time and --delete and --edit affects appropriate entries
Given we use the config "<config_file>"
And we append to the editor if opened
[2023-02-21 10:32] Here is a new entry
And we use the password "test" if prompted
# --change-time is asked first, then --delete, then --edit
When we run "jrnl --change-time '2022-04-23 10:30' --delete --edit" and enter
N
Y
Y
Y
Y
N
Then the error output should contain "3 entries found"
And the error output should contain "2 entries deleted"
And the error output should contain "1 entry modified" # only 1, because the other was deleted
And the error output should contain "1 entry added" # by edit
When we run "jrnl -99 --short"
Then the output should be
2022-04-23 10:30 The third entry finally after weeks without writing.
2023-02-21 10:32 Here is a new entry
Examples: Configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo

View file

@ -9,6 +9,8 @@ Feature: Change entry times in journal
Then the output should contain "2020-09-24 09:14 The third entry finally" Then the output should contain "2020-09-24 09:14 The third entry finally"
When we run "jrnl -1 --change-time '2022-04-23 10:30'" and enter When we run "jrnl -1 --change-time '2022-04-23 10:30'" and enter
Y Y
Then the error output should contain "1 entry modified"
And the error output should not contain "deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-29 11:11 Entry the first. 2020-08-29 11:11 Entry the first.
@ -34,6 +36,8 @@ Feature: Change entry times in journal
Y Y
N N
Y Y
Then the error output should contain "3 entries found"
And the error output should contain "2 entries modified"
When we run "jrnl --short" When we run "jrnl --short"
Then the output should be Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series. 2020-08-31 14:32 A second entry in what I hope to be a long series.
@ -48,10 +52,15 @@ Feature: Change entry times in journal
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with nonsense input changes nothing Scenario Outline: Answering "N" to change-time prompt deletes no entries
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --change-time now asdfasdf" And we use the password "test" if prompted
Then the output should contain "No entries to modify" When we run "jrnl -1"
Then the output should contain "2020-09-24 09:14 The third entry finally"
When we run "jrnl -1 --change-time '2023-02-21 10:30'" and enter
N
Then the error output should not contain "modified"
And the error output should not contain "deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-29 11:11 Entry the first. 2020-08-29 11:11 Entry the first.
@ -59,16 +68,40 @@ Feature: Change entry times in journal
2020-09-24 09:14 The third entry finally after weeks without writing. 2020-09-24 09:14 The third entry finally after weeks without writing.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_encrypted.yaml |
| basic_dayone.yaml | | basic_folder.yaml |
# | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with nonsense input changes nothing
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time now asdfasdf"
Then the output should contain "No entries to modify"
And the error output should not contain "entries modified"
And the error output should not contain "entries deleted"
When we run "jrnl -99 --short"
Then the output should be
2020-08-29 11:11 Entry the first.
2020-08-31 14:32 A second entry in what I hope to be a long series.
2020-09-24 09:14 The third entry finally after weeks without writing.
Examples: Configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
| basic_encrypted.yaml |
| basic_dayone.yaml |
Scenario Outline: Change time flag with tag only changes tagged entries Scenario Outline: Change time flag with tag only changes tagged entries
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' @ipsum" and enter When we run "jrnl --change-time '2022-04-23 10:30' @ipsum" and enter
Y Y
Then the error output should contain "1 entry found"
And the error output should contain "1 entry modified"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series. 2020-08-31 14:32 A second entry in what I hope to be a long series.
@ -76,14 +109,16 @@ Feature: Change entry times in journal
2022-04-23 10:30 Entry the first. 2022-04-23 10:30 Entry the first.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with multiple tags changes all entries matching any of the tags Scenario Outline: Change time flag with multiple tags changes all entries matching any of the tags
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' @ipsum @tagthree" and enter When we run "jrnl --change-time '2022-04-23 10:30' @ipsum @tagthree" and enter
Y Y
Y Y
@ -94,14 +129,16 @@ Feature: Change entry times in journal
2022-04-23 10:30 The third entry finally after weeks without writing. 2022-04-23 10:30 The third entry finally after weeks without writing.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with -and changes boolean AND of tagged entries Scenario Outline: Change time flag with -and changes boolean AND of tagged entries
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' -and @tagone @tagtwo" and enter When we run "jrnl --change-time '2022-04-23 10:30' -and @tagone @tagtwo" and enter
Y Y
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
@ -111,14 +148,16 @@ Feature: Change entry times in journal
2022-04-23 10:30 Entry the first. 2022-04-23 10:30 Entry the first.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with -not does not change entries from given tag Scenario Outline: Change time flag with -not does not change entries from given tag
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' @tagone -not @ipsum" and enter When we run "jrnl --change-time '2022-04-23 10:30' @tagone -not @ipsum" and enter
Y Y
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
@ -128,14 +167,16 @@ Feature: Change entry times in journal
2022-04-23 10:30 The third entry finally after weeks without writing. 2022-04-23 10:30 The third entry finally after weeks without writing.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with -from search operator only changes entries since that date Scenario Outline: Change time flag with -from search operator only changes entries since that date
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' -from 2020-09-01" and enter When we run "jrnl --change-time '2022-04-23 10:30' -from 2020-09-01" and enter
Y Y
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
@ -145,14 +186,16 @@ Feature: Change entry times in journal
2022-04-23 10:30 The third entry finally after weeks without writing. 2022-04-23 10:30 The third entry finally after weeks without writing.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with -to only changes entries up to specified date Scenario Outline: Change time flag with -to only changes entries up to specified date
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' -to 2020-08-31" and enter When we run "jrnl --change-time '2022-04-23 10:30' -to 2020-08-31" and enter
Y Y
Y Y
@ -163,14 +206,16 @@ Feature: Change entry times in journal
2022-04-23 10:30 A second entry in what I hope to be a long series. 2022-04-23 10:30 A second entry in what I hope to be a long series.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with -starred only changes starred entries Scenario Outline: Change time flag with -starred only changes starred entries
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' -starred" and enter When we run "jrnl --change-time '2022-04-23 10:30' -starred" and enter
Y Y
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
@ -180,16 +225,19 @@ Feature: Change entry times in journal
2022-04-23 10:30 A second entry in what I hope to be a long series. 2022-04-23 10:30 A second entry in what I hope to be a long series.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with -contains only changes entries containing expression Scenario Outline: Change time flag with -contains only changes entries containing expression
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' -contains dignissim" and enter When we run "jrnl --change-time '2022-04-23 10:30' -contains dignissim" and enter
Y Y
Then the error output should contain "1 entry modified"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series. 2020-08-31 14:32 A second entry in what I hope to be a long series.
@ -197,13 +245,14 @@ Feature: Change entry times in journal
2022-04-23 10:30 Entry the first. 2022-04-23 10:30 Entry the first.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_encrypted.yaml |
# | basic_dayone.yaml | @todo # | basic_dayone.yaml | @todo
Scenario Outline: Change time flag with no enties specified changes nothing Scenario Outline: Change time flag with no entries specified changes nothing
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we use the password "test" if prompted And we use the password "test" if prompted
When we run "jrnl --change-time" and enter When we run "jrnl --change-time" and enter
@ -217,30 +266,8 @@ Feature: Change entry times in journal
2020-09-24 09:14 The third entry finally after weeks without writing. 2020-09-24 09:14 The third entry finally after weeks without writing.
Examples: Configs Examples: Configs
| config_file | | config_file |
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_dayone.yaml | | basic_encrypted.yaml |
| basic_dayone.yaml |
Scenario Outline: --change-time with --edit modifies selected entries
Given we use the config "<config_file>"
And we write nothing to the editor if opened
And we use the password "test" if prompted
When we run "jrnl --change-time '2022-04-23 10:30' --edit" and enter
Y
N
Y
Then the error output should contain "No entry to save"
And the editor should have been called
When we run "jrnl -99 --short"
Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series.
2022-04-23 10:30 Entry the first.
2022-04-23 10:30 The third entry finally after weeks without writing.
Examples: Configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
# | basic_dayone.yaml | @todo

View file

@ -11,6 +11,8 @@ Feature: Delete entries from journal
N N
N N
Y Y
Then the error output should contain "3 entries found"
And the error output should contain "1 entry deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-29 11:11 Entry the first. 2020-08-29 11:11 Entry the first.
@ -28,6 +30,7 @@ Feature: Delete entries from journal
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --delete -n 1" and enter When we run "jrnl --delete -n 1" and enter
N N
Then the error output should not contain "deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-29 11:11 Entry the first. 2020-08-29 11:11 Entry the first.
@ -44,7 +47,7 @@ Feature: Delete entries from journal
Scenario Outline: Delete flag with nonsense input deletes nothing (issue #932) Scenario Outline: Delete flag with nonsense input deletes nothing (issue #932)
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --delete asdfasdf" When we run "jrnl --delete asdfasdf"
Then the output should contain "No entries to delete" Then the error output should contain "No entries to delete"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-29 11:11 Entry the first. 2020-08-29 11:11 Entry the first.
@ -62,6 +65,8 @@ Feature: Delete entries from journal
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --delete @ipsum" and enter When we run "jrnl --delete @ipsum" and enter
Y Y
Then the error output should contain "1 entry found"
Then the error output should contain "1 entry deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series. 2020-08-31 14:32 A second entry in what I hope to be a long series.
@ -79,6 +84,8 @@ Feature: Delete entries from journal
When we run "jrnl --delete @ipsum @tagthree" and enter When we run "jrnl --delete @ipsum @tagthree" and enter
Y Y
Y Y
Then the error output should contain "2 entries found"
And the error output should contain "2 entries deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series. 2020-08-31 14:32 A second entry in what I hope to be a long series.
@ -94,6 +101,8 @@ Feature: Delete entries from journal
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --delete -and @tagone @tagtwo" and enter When we run "jrnl --delete -and @tagone @tagtwo" and enter
Y Y
Then the error output should contain "1 entry found"
And the error output should contain "1 entry deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series. 2020-08-31 14:32 A second entry in what I hope to be a long series.
@ -110,6 +119,8 @@ Feature: Delete entries from journal
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --delete @tagone -not @ipsum" and enter When we run "jrnl --delete @tagone -not @ipsum" and enter
Y Y
Then the error output should contain "1 entry found"
And the error output should contain "1 entry deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-29 11:11 Entry the first. 2020-08-29 11:11 Entry the first.
@ -126,6 +137,8 @@ Feature: Delete entries from journal
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --delete -from 2020-09-01" and enter When we run "jrnl --delete -from 2020-09-01" and enter
Y Y
Then the error output should contain "1 entry found"
And the error output should contain "1 entry deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-29 11:11 Entry the first. 2020-08-29 11:11 Entry the first.
@ -143,6 +156,8 @@ Feature: Delete entries from journal
When we run "jrnl --delete -to 2020-08-31" and enter When we run "jrnl --delete -to 2020-08-31" and enter
Y Y
Y Y
Then the error output should contain "2 entries found"
And the error output should contain "2 entries deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-09-24 09:14 The third entry finally after weeks without writing. 2020-09-24 09:14 The third entry finally after weeks without writing.
@ -158,6 +173,7 @@ Feature: Delete entries from journal
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --delete -starred" and enter When we run "jrnl --delete -starred" and enter
Y Y
Then the error output should contain "1 entry deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-29 11:11 Entry the first. 2020-08-29 11:11 Entry the first.
@ -174,6 +190,8 @@ Feature: Delete entries from journal
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl --delete -contains dignissim" and enter When we run "jrnl --delete -contains dignissim" and enter
Y Y
Then the error output should contain "1 entry found"
And the error output should contain "1 entry deleted"
When we run "jrnl -99 --short" When we run "jrnl -99 --short"
Then the output should be Then the output should be
2020-08-31 14:32 A second entry in what I hope to be a long series. 2020-08-31 14:32 A second entry in what I hope to be a long series.

View file

@ -372,3 +372,29 @@ Feature: Searching in a journal
2013-06-17 20:38 This entry has a location. 2013-06-17 20:38 This entry has a location.
2013-07-17 11:38 This entry is starred! 2013-07-17 11:38 This entry is starred!
Scenario Outline: Searching the most recent entry should not show found count
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl -1"
Then the error output should not contain "1 entry found"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: Searching for more entries than are in the journal should show found count
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl -4"
Then the error output should contain "3 entries found"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |

View file

@ -31,4 +31,47 @@ Feature: Using templates
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_encrypted.yaml | | basic_encrypted.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_dayone.yaml | | basic_dayone.yaml |
Scenario Outline: --template nonexistent_file should throw an error
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --template this_template_does_not_exist.template"
Then we should get an error
Then the error output should contain "Unable to find a template file based on the passed arg"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: --template local_filepath should be used in new entry
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --template features/templates/basic.template"
Then the output should contain "No entry to save, because the template was not changed"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: --template file_in_XDG_templates_dir should be used in new entry
Given we use the config "<config_file>"
And we use the password "test" if prompted
And we copy the template "basic.template" to the default templates folder
When we run "jrnl --template basic.template"
Then the output should contain "No entry to save, because the template was not changed"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
| basic_dayone.yaml |

View file

@ -76,21 +76,21 @@ Feature: Writing new entries.
| basic_dayone.yaml | | basic_dayone.yaml |
| basic_folder.yaml | | basic_folder.yaml |
Scenario Outline: Writing an empty entry from the editor should yield "No entry to save" message Scenario Outline: Clearing the editor's contents should yield "No text received" message
Given we use the config "<config_file>" Given we use the config "<config_file>"
And we write nothing to the editor if opened And we write nothing to the editor if opened
And we use the password "test" if prompted And we use the password "test" if prompted
When we run "jrnl --edit" When we run "jrnl --edit"
Then the error output should contain "No entry to save, because no text was received" Then the error output should contain "No text received from editor. Were you trying to delete all the entries?"
And the editor should have been called And the editor should have been called
Examples: configs Examples: configs
| config_file | | config_file |
| editor.yaml | | editor.yaml |
| editor_empty_folder.yaml | | basic_onefile.yaml |
| dayone.yaml | | basic_encrypted.yaml |
| basic_encrypted.yaml | | basic_dayone.yaml |
| basic_onefile.yaml | | basic_folder.yaml |
Scenario Outline: Writing an empty entry from the command line should yield "No entry to save" message Scenario Outline: Writing an empty entry from the command line should yield "No entry to save" message
Given we use the config "<config_file>" Given we use the config "<config_file>"
@ -236,7 +236,9 @@ Feature: Writing new entries.
And we append to the editor if opened And we append to the editor if opened
[2021-11-13] worked on jrnl tests [2021-11-13] worked on jrnl tests
When we run "jrnl --edit" When we run "jrnl --edit"
Then the output should contain "1 entry added" Then the error output should contain "3 entries found"
And the error output should contain "1 entry added"
Examples: configs Examples: configs
| config_file | | config_file |
@ -254,7 +256,8 @@ Feature: Writing new entries.
[2021-11-12] worked on jrnl tests again [2021-11-12] worked on jrnl tests again
[2021-11-13] worked on jrnl tests a little bit more [2021-11-13] worked on jrnl tests a little bit more
When we run "jrnl --edit" When we run "jrnl --edit"
Then the error output should contain "3 entries added" Then the error output should contain "3 entries found"
And the error output should contain "3 entries added"
Examples: configs Examples: configs
| config_file | | config_file |
@ -271,7 +274,7 @@ Feature: Writing new entries.
[2021-11-13] I am replacing my whole journal with this entry [2021-11-13] I am replacing my whole journal with this entry
When we run "jrnl --edit" When we run "jrnl --edit"
Then the output should contain "2 entries deleted" Then the output should contain "2 entries deleted"
Then the output should contain "3 entries modified" And the output should contain "1 entry modified"
Examples: configs Examples: configs
| config_file | | config_file |
@ -287,8 +290,7 @@ Feature: Writing new entries.
And we write to the editor if opened And we write to the editor if opened
[2021-11-13] I am replacing the last entry with this entry [2021-11-13] I am replacing the last entry with this entry
When we run "jrnl --edit -1" When we run "jrnl --edit -1"
Then the output should contain Then the error output should contain "1 entry modified"
1 entry modified
Examples: configs Examples: configs
| config_file | | config_file |
@ -304,8 +306,8 @@ Feature: Writing new entries.
And we append to the editor if opened And we append to the editor if opened
This is a small addendum to my latest entry. This is a small addendum to my latest entry.
When we run "jrnl --edit" When we run "jrnl --edit"
Then the output should contain Then the error output should contain "3 entries found"
1 entry modified And the error output should contain "1 entry modified"
Examples: configs Examples: configs
| config_file | | config_file |
@ -341,6 +343,7 @@ Feature: Writing new entries.
And we append to the editor if opened And we append to the editor if opened
@newtag @newtag
When we run "jrnl --edit -1" When we run "jrnl --edit -1"
Then the error output should contain "1 entry modified"
When we run "jrnl --tags @newtag" When we run "jrnl --tags @newtag"
Then the output should contain Then the output should contain
1 entry found 1 entry found

View file

@ -3,6 +3,7 @@
from pytest_bdd import scenarios from pytest_bdd import scenarios
scenarios("features/actions.feature")
scenarios("features/build.feature") scenarios("features/build.feature")
scenarios("features/config_file.feature") scenarios("features/config_file.feature")
scenarios("features/core.feature") scenarios("features/core.feature")

View file

@ -0,0 +1,4 @@
[2022-03-02 9:25:00 AM] This file should be ignored (month)
This text file is in a folder journal's month directory ("2020/09"), but it's not in the file name format used by jrnl for folder journal entries, so it should be ignored.
This file should not ever appear in a test.

View file

@ -0,0 +1,4 @@
[2022-03-02 9:25:00 AM] This file should be ignored (year)
This text file is in a folder journal's year directory ("2020"), but it's not in the file name format used by jrnl for folder journal entries, so it should be ignored.
This file should not ever appear in a test.

View file

@ -0,0 +1,4 @@
[2022-03-02 9:25:00 AM] This file should be ignored (root)
This text file is in a folder journal's root directory, but it's not in the file name format used by jrnl for folder journal entries, so it should be ignored.
This file should not ever appear in a test.

View file

@ -89,6 +89,7 @@ def cli_run(
mock_user_input, mock_user_input,
mock_overrides, mock_overrides,
mock_default_journal_path, mock_default_journal_path,
mock_default_templates_path,
): ):
# Check if we need more mocks # Check if we need more mocks
mock_factories.update(mock_args) mock_factories.update(mock_args)
@ -98,6 +99,7 @@ def cli_run(
mock_factories.update(mock_config_path) mock_factories.update(mock_config_path)
mock_factories.update(mock_user_input) mock_factories.update(mock_user_input)
mock_factories.update(mock_default_journal_path) mock_factories.update(mock_default_journal_path)
mock_factories.update(mock_default_templates_path)
return { return {
"status": 0, "status": 0,
@ -179,6 +181,16 @@ def mock_default_journal_path(temp_dir):
} }
@fixture
def mock_default_templates_path(temp_dir):
templates_path = Path(temp_dir.name, "templates")
return {
"get_templates_path": lambda: patch(
"jrnl.controller.get_templates_path", return_value=templates_path
),
}
@fixture @fixture
def temp_dir(): def temp_dir():
return tempfile.TemporaryDirectory() return tempfile.TemporaryDirectory()

View file

@ -125,6 +125,22 @@ def we_use_the_config(request, temp_dir, working_dir, config_file):
return config_dest return config_dest
@given(
parse('we copy the template "{template_file}" to the default templates folder'),
target_fixture="default_templates_path",
)
def we_copy_the_template(request, temp_dir, working_dir, template_file):
# Move into temp dir as cwd
os.chdir(temp_dir.name) # @todo move this step to a more universal place
# Copy template over
template_source = os.path.join(working_dir, "data", "templates", template_file)
template_dest = os.path.join(temp_dir.name, "templates", template_file)
os.makedirs(os.path.dirname(template_dest), exist_ok=True)
shutil.copy2(template_source, template_dest)
return template_dest
@given(parse('the config "{config_file}" exists'), target_fixture="config_path") @given(parse('the config "{config_file}" exists'), target_fixture="config_path")
def config_exists(config_file, temp_dir, working_dir): def config_exists(config_file, temp_dir, working_dir):
config_source = os.path.join(working_dir, "data", "configs", config_file) config_source = os.path.join(working_dir, "data", "configs", config_file)

View file

@ -25,6 +25,11 @@ def should_get_no_error(cli_run):
assert cli_run["status"] == 0, cli_run["status"] assert cli_run["status"] == 0, cli_run["status"]
@then("we should get an error")
def should_get_an_error(cli_run):
assert cli_run["status"] != 0, cli_run["status"]
@then(parse("the output should match\n{regex}")) @then(parse("the output should match\n{regex}"))
@then(parse('the output should match "{regex}"')) @then(parse('the output should match "{regex}"'))
def output_should_match(regex, cli_run): def output_should_match(regex, cli_run):
@ -192,12 +197,12 @@ def config_var_in_memory(config_in_memory, journal_name, it_should, some_yaml):
@then("we should be prompted for a password") @then("we should be prompted for a password")
def password_was_called(cli_run): def password_was_called(cli_run):
assert cli_run["mocks"]["user_input"].called assert cli_run["mocks"]["user_input"].return_value.input.called
@then("we should not be prompted for a password") @then("we should not be prompted for a password")
def password_was_not_called(cli_run): def password_was_not_called(cli_run):
assert not cli_run["mocks"]["user_input"].called assert not cli_run["mocks"]["user_input"].return_value.input.called
@then(parse("the cache directory should contain the files\n{file_list}")) @then(parse("the cache directory should contain the files\n{file_list}"))

View file

@ -18,15 +18,17 @@ def random_string():
@pytest.mark.parametrize("export_format", ["pretty", "short"]) @pytest.mark.parametrize("export_format", ["pretty", "short"])
@mock.patch("builtins.print") def test_display_search_results_pretty_short(export_format):
@mock.patch("jrnl.controller.Journal.pprint")
def test_display_search_results_pretty_short(mock_pprint, mock_print, export_format):
mock_args = parse_args(["--format", export_format]) mock_args = parse_args(["--format", export_format])
test_journal = mock.Mock(wraps=jrnl.journals.Journal)
test_journal = jrnl.journals.Journal()
test_journal.new_entry("asdf")
test_journal.pprint = mock.Mock()
_display_search_results(mock_args, test_journal) _display_search_results(mock_args, test_journal)
mock_print.assert_called_once_with(mock_pprint.return_value) test_journal.pprint.assert_called_once()
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -40,7 +42,9 @@ def test_display_search_results_builtin_plugins(
test_filename = random_string test_filename = random_string
mock_args = parse_args(["--format", export_format, "--file", test_filename]) mock_args = parse_args(["--format", export_format, "--file", test_filename])
test_journal = mock.Mock(wraps=jrnl.journals.Journal) test_journal = jrnl.journals.Journal()
test_journal.new_entry("asdf")
mock_export = mock.Mock() mock_export = mock.Mock()
mock_exporter.return_value.export = mock_export mock_exporter.return_value.export = mock_export

View file

@ -26,7 +26,6 @@ def build_card_header(datestr):
class TestFancy: class TestFancy:
def test_too_small_linewrap(self, datestr): def test_too_small_linewrap(self, datestr):
journal = "test_journal" journal = "test_journal"
content = build_card_header(datestr) content = build_card_header(datestr)

View file

@ -0,0 +1,59 @@
# Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
import pathlib
from unittest import mock
import pytest
from jrnl.journals.FolderJournal import Folder
@pytest.mark.parametrize(
"inputs_and_outputs",
[
[
"/2020/01",
["02.txt", "03.txt", "31.txt"],
["/2020/01/02.txt", "/2020/01/03.txt", "/2020/01/31.txt"],
],
[
"/2020/02", # leap year
["02.txt", "03.txt", "28.txt", "29.txt", "31.txt", "39.txt"],
[
"/2020/02/02.txt",
"/2020/02/03.txt",
"/2020/02/28.txt",
"/2020/02/29.txt",
],
],
[
"/2100/02", # not a leap year
["01.txt", "28.txt", "29.txt", "39.txt"],
["/2100/02/01.txt", "/2100/02/28.txt"],
],
[
"/2023/04",
["29.txt", "30.txt", "31.txt", "39.txt"],
["/2023/04/29.txt", "/2023/04/30.txt"],
],
],
)
def test_get_day_files_expected_filtering(inputs_and_outputs):
year_month_path, glob_filenames, expected_output = inputs_and_outputs
year_month_path = pathlib.Path(year_month_path)
glob_files = map(lambda x: year_month_path / x, glob_filenames)
expected_output = list(map(lambda x: str(pathlib.PurePath(x)), expected_output))
with (
mock.patch("pathlib.Path.glob", return_value=glob_files),
mock.patch.object(pathlib.Path, "is_file", return_value=True),
):
actual_output = list(Folder._get_day_files(year_month_path))
actual_output.sort()
expected_output.sort()
assert actual_output == expected_output

View file

@ -101,7 +101,6 @@ def test_get_kv_from_pair():
class TestDotNotationToList: class TestDotNotationToList:
def test_unpack_dots_to_list(self): def test_unpack_dots_to_list(self):
keys = "a.b.c.d.e.f" keys = "a.b.c.d.e.f"
keys_list = _convert_dots_to_list(keys) keys_list = _convert_dots_to_list(keys)
assert len(keys_list) == 6 assert len(keys_list) == 6

View file

@ -42,6 +42,7 @@ def expected_args(**kwargs):
"strict": False, "strict": False,
"tagged": False, "tagged": False,
"tags": False, "tags": False,
"template": None,
"text": [], "text": [],
"config_override": [], "config_override": [],
"config_file_path": "", "config_file_path": "",
@ -229,7 +230,6 @@ def test_version_alone():
def test_editor_override(): def test_editor_override():
parsed_args = cli_as_dict('--config-override editor "nano"') parsed_args = cli_as_dict('--config-override editor "nano"')
assert parsed_args == expected_args(config_override=[["editor", "nano"]]) assert parsed_args == expected_args(config_override=[["editor", "nano"]])
@ -293,7 +293,6 @@ class TestDeserialization:
], ],
) )
def test_deserialize_multiword_strings(self, input_str): def test_deserialize_multiword_strings(self, input_str):
runtime_config = make_yaml_valid_dict(input_str) runtime_config = make_yaml_valid_dict(input_str)
assert runtime_config.__class__ == dict assert runtime_config.__class__ == dict
assert input_str[0] in runtime_config assert input_str[0] in runtime_config

View file

@ -3,6 +3,8 @@
import datetime import datetime
import pytest
from jrnl import time from jrnl import time
@ -20,3 +22,23 @@ def test_default_minute_is_added():
default_minute=30, default_minute=30,
bracketed=False, bracketed=False,
) == datetime.datetime(2020, 6, 20, 0, 30) ) == datetime.datetime(2020, 6, 20, 0, 30)
@pytest.mark.parametrize(
"inputs",
[
[2000, 2, 29, True],
[2023, 1, 0, False],
[2023, 1, 1, True],
[2023, 4, 31, False],
[2023, 12, 31, True],
[2023, 12, 32, False],
[2023, 13, 1, False],
[2100, 2, 27, True],
[2100, 2, 28, True],
[2100, 2, 29, False],
],
)
def test_is_valid_date(inputs):
year, month, day, expected_result = inputs
assert time.is_valid_date(year, month, day) == expected_result