Merge branch 'develop' into mode-actions-1639

Conflicts:
  CHANGELOG.md
  docs/privacy-and-security.md
  jrnl/controller.py
  tests/bdd/features/change_time.feature
This commit is contained in:
Jonathan Wren 2023-03-04 14:07:46 -08:00
commit 9bca32b438
No known key found for this signature in database
28 changed files with 2109 additions and 1626 deletions

View file

@ -6,7 +6,11 @@
**Implemented enhancements:** **Implemented enhancements:**
- Display locations of config file and documentation after initial install [\#1694](https://github.com/jrnl-org/jrnl/issues/1694)
- 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))
- Prompt to include colors in config when first running jrnl [\#1687](https://github.com/jrnl-org/jrnl/pull/1687) ([micahellison](https://github.com/micahellison))
- 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))
- Add `rich` handler to debug logging [\#1627](https://github.com/jrnl-org/jrnl/pull/1627) ([wren](https://github.com/wren)) - Add `rich` handler to debug logging [\#1627](https://github.com/jrnl-org/jrnl/pull/1627) ([wren](https://github.com/wren))
@ -16,6 +20,8 @@
- 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)
- 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)
- 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))
- 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))
@ -26,6 +32,7 @@
**Build:** **Build:**
- Support pytest-bdd 6 [\#1534](https://github.com/jrnl-org/jrnl/issues/1534)
- Update copyright notices for 2023 [\#1660](https://github.com/jrnl-org/jrnl/pull/1660) ([wren](https://github.com/wren)) - Update copyright notices for 2023 [\#1660](https://github.com/jrnl-org/jrnl/pull/1660) ([wren](https://github.com/wren))
- Fix bug where changelog is always slightly out of date on release tags [\#1631](https://github.com/jrnl-org/jrnl/pull/1631) ([wren](https://github.com/wren)) - Fix bug where changelog is always slightly out of date on release tags [\#1631](https://github.com/jrnl-org/jrnl/pull/1631) ([wren](https://github.com/wren))
- Add `simplify` plugin to linting checks [\#1630](https://github.com/jrnl-org/jrnl/pull/1630) ([wren](https://github.com/wren)) - Add `simplify` plugin to linting checks [\#1630](https://github.com/jrnl-org/jrnl/pull/1630) ([wren](https://github.com/wren))
@ -33,7 +40,16 @@
**Documentation:** **Documentation:**
- Document template extension behavior [\#1677](https://github.com/jrnl-org/jrnl/issues/1677)
- Visual Studio Code may store unencrypted temporary files [\#1675](https://github.com/jrnl-org/jrnl/issues/1675)
- Document `-tagged`, `-not -tagged`, and `-not -starred` [\#1668](https://github.com/jrnl-org/jrnl/issues/1668)
- Documentation Change [\#1651](https://github.com/jrnl-org/jrnl/issues/1651) - Documentation Change [\#1651](https://github.com/jrnl-org/jrnl/issues/1651)
- Update console examples on jrnl.sh front page [\#1622](https://github.com/jrnl-org/jrnl/issues/1622)
- Update documentation front page text [\#1698](https://github.com/jrnl-org/jrnl/pull/1698) ([micahellison](https://github.com/micahellison))
- Support mkdocs 1.4.2 and fix its missing breadcrumb [\#1691](https://github.com/jrnl-org/jrnl/pull/1691) ([micahellison](https://github.com/micahellison))
- Document temporary file extension behavior when using template [\#1686](https://github.com/jrnl-org/jrnl/pull/1686) ([micahellison](https://github.com/micahellison))
- Document `-tagged`, `-not -tagged`, and `-not -starred` [\#1684](https://github.com/jrnl-org/jrnl/pull/1684) ([micahellison](https://github.com/micahellison))
- Update documentation about privacy and security in VSCode [\#1680](https://github.com/jrnl-org/jrnl/pull/1680) ([giuseppedandrea](https://github.com/giuseppedandrea))
- Update documentation on temporary files naming [\#1673](https://github.com/jrnl-org/jrnl/pull/1673) ([giuseppedandrea](https://github.com/giuseppedandrea)) - Update documentation on temporary files naming [\#1673](https://github.com/jrnl-org/jrnl/pull/1673) ([giuseppedandrea](https://github.com/giuseppedandrea))
- Update docs to include time and title in arguments with `--edit` [\#1657](https://github.com/jrnl-org/jrnl/pull/1657) ([pconrad-fb](https://github.com/pconrad-fb)) - Update docs to include time and title in arguments with `--edit` [\#1657](https://github.com/jrnl-org/jrnl/pull/1657) ([pconrad-fb](https://github.com/pconrad-fb))
- Fix markup in "Advanced Usage" doc [\#1655](https://github.com/jrnl-org/jrnl/pull/1655) ([multani](https://github.com/multani)) - Fix markup in "Advanced Usage" doc [\#1655](https://github.com/jrnl-org/jrnl/pull/1655) ([multani](https://github.com/multani))

View file

@ -67,12 +67,57 @@ Windows doesn't log history to disk, but it does keep it in your command prompt
session. Close the command prompt or press `Alt`+`F7` to clear your history session. Close the command prompt or press `Alt`+`F7` to clear your history
after journaling. after journaling.
## Files in transit from editor to jrnl
When creating or editing an entry, `jrnl` uses a unencrypted temporary file on
disk in order to give your editor access to your journal. After you close your
editor, `jrnl` then deletes this temporary file.
So, if you have saved a journal entry but haven't closed your editor yet, the
unencrypted temporary remains on your disk. If your computer were to shut off
during this time, or the `jrnl` process were killed unexpectedly, then the
unencrypted temporary file will remain on your disk. You can mitigate this
issue by only saving with your editor right before closing it. You can also
manually delete these files from your temporary folder. By default, they
are named `jrnl*.jrnl`, but if you use a
[template](reference-config-file.md#template), they will have the same
extension as the template.
## Editor history ## Editor history
Some editors keep usage history stored on disk for future use. This can be a Some editors keep usage history stored on disk for future use. This can be a
security risk in the sense that sensitive information can leak via recent security risk in the sense that sensitive information can leak via recent
search patterns or editor commands. search patterns or editor commands.
### Visual Studio Code
Visual Studio Code stores the contents of saved files to allow you to restore or
review the contents later. You can disable this feature for all files by unchecking
the `workbench.localHistory.enabled` setting in the
[Settings editor](https://code.visualstudio.com/docs/getstarted/settings#_settings-editor).
Alternatively, you can disable this feature for specific files by configuring a
[pattern](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)
in the `workbench.localHistory.exclude` setting. To exclude unencrypted temporary files generated
by `jrnl`, you can set the `**/jrnl*.jrnl` (unless you are using a
[template](reference-config-file.md#template)) pattern for the `workbench.localHistory.exclude` setting
in the [Settings editor](https://code.visualstudio.com/docs/getstarted/settings#_settings-editor).
!!! note
On Windows, the history location is typically found at
`%APPDATA%\Code\User\History`.
Visual Studio Code also creates a copy of all unsaved files that are open.
It stores these copies in a backup location that's automatically cleaned when
you save the file. However, if your computer shuts off before you save the file,
or the Visual Studio Code process stops unexpectedly, then an unencrypted
temporary file may remain on your disk. You can manually delete these files
from the backup location.
!!! note
On Windows, the backup location is typically found at
`%APPDATA%\Code\Backups`.
### Vim ### Vim
Vim stores progress data in a so called Viminfo file located at `~/.viminfo` Vim stores progress data in a so called Viminfo file located at `~/.viminfo`
@ -101,7 +146,11 @@ autocommand can be used. Place this in your `~/.vimrc`:
autocmd BufNewFile,BufReadPre *.jrnl setlocal viminfo= noswapfile noundofile nobackup nowritebackup noshelltemp history=0 nomodeline secure autocmd BufNewFile,BufReadPre *.jrnl setlocal viminfo= noswapfile noundofile nobackup nowritebackup noshelltemp history=0 nomodeline secure
``` ```
Please see `:h <option>` in Vim for more information about the options mentioned. !!! note
If you're using a [template](reference-config-file.md#template), you will
have to use the template's file extension instead of `.jrnl`.
See `:h <option>` in Vim for more information about the options mentioned.
### Neovim ### Neovim
@ -143,22 +192,12 @@ vim.api.nvim_create_autocmd( {"BufNewFile","BufReadPre" }, {
}) })
``` ```
!!! note
If you're using a [template](reference-config-file.md#template), you will
have to use the template's file extension instead of `.jrnl`.
Please see `:h <option>` in Neovim for more information about the options mentioned. Please see `:h <option>` in Neovim for more information about the options mentioned.
## Files in transit from editor to jrnl
When creating or editing an entry, `jrnl` uses a unencrypted temporary file on
disk in order to give your editor access to your journal. After you close your
editor, `jrnl` then deletes this temporary file.
So, if you have saved a journal entry but haven't closed your editor yet, the
unencrypted temporary remains on your disk. If your computer were to shut off
during this time, or the `jrnl` process were killed unexpectedly, then the
unencrypted temporary file will remain on your disk. You can mitigate this
issue by only saving with your editor right before closing it. You can also
manually delete these files (i.e. files named `jrnl*.jrnl`) from your temporary
folder.
## Plausible deniability ## Plausible deniability
You may be able to hide the contents of your journal behind a layer of encryption, You may be able to hide the contents of your journal behind a layer of encryption,
@ -178,7 +217,6 @@ In Windows, the keychain is the Windows Credential Manager (WCM), which can't be
and can be accessed by any other application running under your username. If this is and can be accessed by any other application running under your username. If this is
a concern for you, you may not want to store your password. a concern for you, you may not want to store your password.
## Notice any other risks? ## Notice any other risks?
Please let the maintainers know by [filing an issue on GitHub](https://github.com/jrnl-org/jrnl/issues). Please let the maintainers know by [filing an issue on GitHub](https://github.com/jrnl-org/jrnl/issues).

View file

@ -76,8 +76,11 @@ entries, such as `yesterday`, `today`, `Tuesday`, or `2021-08-01`.
| -contains TEXT | Show entries containing specific text (put quotes around text with spaces) | | -contains TEXT | Show entries containing specific text (put quotes around text with spaces) |
| -and | Show only entries that match all conditions, like saying "x AND y" (default: OR) | | -and | Show only entries that match all conditions, like saying "x AND y" (default: OR) |
| -starred | Show only starred entries (marked with *) | | -starred | Show only starred entries (marked with *) |
| -tagged | Show only tagged entries (marked with the [configured tagsymbols](reference-config-file.md#tagsymbols)) |
| -n [NUMBER] | Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same effect) | | -n [NUMBER] | Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same effect) |
| -not [TAG] | Exclude entries with this tag | | -not [TAG] | Exclude entries with this tag |
| -not -starred | Exclude entries that are starred |
| -not -tagged | Exclude entries that are tagged |
## Searching Options ## Searching Options
These help you do various tasks with the selected entries from your search. These help you do various tasks with the selected entries from your search.

View file

@ -59,7 +59,9 @@ value for journals that already have data in them.
### template ### template
The path to a text file to use as a template for new entries. Only works when you The path to a text file to use as a template for new entries. Only works when you
have the `editor` field configured. have the `editor` field configured. If you use a template, the editor's
[temporary files](privacy-and-security.md#files-in-transit-from-editor-to-jrnl)
will have the same extension as the template.
### tagsymbols ### tagsymbols
Symbols to be interpreted as tags. Symbols to be interpreted as tags.

View file

@ -118,7 +118,7 @@ div.rst-content {
background-size: 100%; background-size: 100%;
} }
a.icon-home:before { .wy-side-nav-search a.icon-home:before {
display: block; display: block;
width: 84px; width: 84px;
height: 70px; height: 70px;

View file

@ -0,0 +1,49 @@
<!--
Copied from https://github.com/mkdocs/mkdocs/blob/master/mkdocs/themes/readthedocs/breadcrumbs.html
Then lightly modified for accessibility
-->
<div role="navigation" aria-label="breadcrumbs navigation">
<ul class="wy-breadcrumbs">
<li><a href="{{ nav.homepage.url|url }}" class="icon icon-home" aria-label="{% trans %}Docs{% endtrans %}"></a> &raquo;</li>
{%- if page %}
{%- for doc in page.ancestors[::-1] %}
{%- if doc.link %}
<li><a href="{{ doc.link|e }}">{{ doc.title }}</a> &raquo;</li>
{%- else %}
<li>{{ doc.title }} &raquo;</li>
{%- endif %}
{%- endfor %}
<li>{{ page.title }}</li>
{%- endif %}
<li class="wy-breadcrumbs-aside">
{%- block repo %}
{%- if page and page.edit_url %}
{%- if config.repo_name|lower == 'github' %}
<a href="{{ page.edit_url }}" class="icon icon-github"> {% trans repo_name=config.repo_name %}Edit on {{ repo_name }}{% endtrans %}</a>
{%- elif config.repo_name|lower == 'bitbucket' %}
<a href="{{ page.edit_url }}" class="icon icon-bitbucket"> {% trans repo_name=config.repo_name %}Edit on {{ repo_name }}{% endtrans %}</a>
{%- elif config.repo_name|lower == 'gitlab' %}
<a href="{{ page.edit_url }}" class="icon icon-gitlab"> {% trans repo_name=config.repo_name %}Edit on {{ repo_name }}{% endtrans %}</a>
{%- elif config.repo_name %}
<a href="{{ page.edit_url }}">{% trans repo_name=config.repo_name %}Edit on {{ repo_name }}{% endtrans %}</a>
{%- else %}
<a href="{{ page.edit_url }}">{% trans %}Edit{% endtrans %}</a>
{%- endif %}
{%- endif %}
{%- endblock %}
</li>
</ul>
{%- if config.theme.prev_next_buttons_location|lower in ['top', 'both']
and page and (page.next_page or page.previous_page) %}
<div class="rst-breadcrumbs-buttons" role="navigation" aria-label="{% trans %}Breadcrumb Navigation{% endtrans %}">
{%- if page.previous_page %}
<a href="{{ page.previous_page.url|url }}" class="btn btn-neutral float-left" title="{{ page.previous_page.title }}"><span class="icon icon-circle-arrow-left" aria-hidden="true"></span> {% trans %}Previous{% endtrans %}</a>
{%- endif %}
{%- if page.next_page %}
<a href="{{ page.next_page.url|url }}" class="btn btn-neutral float-right" title="{{ page.next_page.title }}">{% trans %}Next{% endtrans %} <span class="icon icon-circle-arrow-right" aria-hidden="true"></span></a>
{%- endif %}
</div>
{%- endif %}
<hr/>
</div>

View file

@ -76,22 +76,22 @@ License: https://www.gnu.org/licenses/gpl-3.0.html
<section> <section>
<i class="icon future"></i> <i class="icon future"></i>
<h3>Future-proof.</h3> <h3>Future-proof.</h3>
<p>Your journals are stored in plain-text files that will still be readable in 50 years when all your fancy iPad apps will have gone the way of the Dodo.</p> <p>Your journals are stored in plain-text files that will still be readable in 50 years when your fancy proprietary apps will have gone the way of the dodo.</p>
</section> </section>
<section> <section>
<i class="icon secure"></i> <i class="icon secure"></i>
<h3>Secure.</h3> <h3>Secure.</h3>
<p>Encrypt your journals with industry-strength AES encryption. The NSA won't be able to read your dirty secrets.</p> <p>Encrypt your journals with industry-strength AES encryption. Nobody will be able to read your dirty secrets&mdash;not even you, if you lose your password!</p>
</section> </section>
<section> <section>
<i class="icon sync"></i> <i class="icon sync"></i>
<h3>Accessible anywhere.</h3> <h3>Accessible anywhere.</h3>
<p>Sync your journals with Dropbox and capture your thoughts where ever you are.</p> <p>Sync your journal files with other tools like Dropbox to capture your thoughts wherever you are.</p>
</section> </section>
<section> <section>
<i class="icon github"></i> <i class="icon github"></i>
<h3>Free &amp; Open Source.</h3> <h3>Free &amp; Open Source.</h3>
<p>jrnl is made by a bunch of really friendly and remarkably attractive people. Maybe even <a href="https://www.github.com/jrnl-org/jrnl" title="Fork jrnl on GitHub">you</a>?</p> <p>jrnl is made by a bunch of really friendly and remarkably amazing people. Maybe even <a href="https://www.github.com/jrnl-org/jrnl" title="Fork jrnl on GitHub">you</a>?</p>
</section> </section>
<section> <section>
<i class="icon folders"></i> <i class="icon folders"></i>
@ -107,17 +107,17 @@ License: https://www.gnu.org/licenses/gpl-3.0.html
<script> <script>
new Typed("#typed", { new Typed("#typed", {
strings: [ strings: [
"jrnl today: Started writing my memoirs. On the command line. Like a boss.", "jrnl Started writing my memoirs on the command line. 🎉🔥💻🔥🎉",
"jrnl yesterday 2pm: used jrnl to keep track of accomplished tasks. The done.txt for my todo.txt", "jrnl yesterday 2pm: used jrnl to keep track of accomplished tasks. The done.txt for my todo.txt",
"jrnl <b>-from</b> 2009 <b>-until</b> may<br /><i>`(Displays all entries from January 2009 to last may)`</i>", "jrnl <b>-from</b> 2019 <b>-until</b> may<br /><i>`(displays all entries from January 2019 to last May)`</i>",
"jrnl A day on the beach with @beth and @frank. Taggidy-tag-tag.", "jrnl A day on the beach with @beth and @frank. Tagging them so I can easily look this up later.",
"jrnl <b>--tags</b><br /><i>`@idea 7<br />@beth 5</i>`", "jrnl <b>--tags</b><br /><i>`@frank 7<br />@beth 5</i>`",
"jrnl <b>--format</b> json<br /><i>`(Outputs your entire journal as json)</i>`", "jrnl <b>--format</b> json<br /><i>`(Outputs your entire journal as json)</i>`",
"jrnl <b>--encrypt</b><br /><i>`(AES encryption. Crack this, NSA)</i>`" "jrnl <b>--encrypt</b><br /><i>`(AES encryption. Don't lose your password!)</i>`"
], ],
typeSpeed: 35, typeSpeed: 20, // less is faster
backSpeed: 15, backSpeed: 10,
backDelay: 2000, backDelay: 2500,
loop: true, loop: true,
showCursor: false showCursor: false
}); });

View file

@ -1,2 +1,2 @@
mkdocs==1.2.4 mkdocs>=1.4
jinja2==3.1.2 jinja2==3.1.2

View file

@ -28,6 +28,43 @@ class WrappingFormatter(argparse.RawTextHelpFormatter):
return text return text
class IgnoreNoneAppendAction(argparse._AppendAction):
"""
Pass -not without a following string and avoid appending
a None value to the excluded list
"""
def __call__(self, parser, namespace, values, option_string=None):
if values is not None:
super().__call__(parser, namespace, values, option_string)
def parse_not_arg(
args: list[str], parsed_args: argparse.Namespace, parser: argparse.ArgumentParser
) -> argparse.Namespace:
"""
It's possible to use -not as a precursor to -starred and -tagged
to reverse their behaviour, however this requires some extra logic
to parse, and to ensure we still do not allow passing an empty -not
"""
parsed_args.exclude_starred = False
parsed_args.exclude_tagged = False
if "-not-starred" in "".join(args):
parsed_args.starred = False
parsed_args.exclude_starred = True
if "-not-tagged" in "".join(args):
parsed_args.tagged = False
parsed_args.exclude_tagged = True
if "-not" in args and not any(
[parsed_args.exclude_starred, parsed_args.exclude_tagged, parsed_args.excluded]
):
parser.error("argument -not: expected 1 argument")
return parsed_args
def parse_args(args: list[str] = []) -> argparse.Namespace: def parse_args(args: list[str] = []) -> argparse.Namespace:
""" """
Argument parsing that is doable before the config is available. Argument parsing that is doable before the config is available.
@ -237,6 +274,12 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
action="store_true", action="store_true",
help="Show only starred entries (marked with *)", help="Show only starred entries (marked with *)",
) )
reading.add_argument(
"-tagged",
dest="tagged",
action="store_true",
help="Show only entries that have at least one tag",
)
reading.add_argument( reading.add_argument(
"-n", "-n",
dest="limit", dest="limit",
@ -249,11 +292,15 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
reading.add_argument( reading.add_argument(
"-not", "-not",
dest="excluded", dest="excluded",
nargs=1, nargs="?",
default=[], default=[],
metavar="TAG", metavar="TAG/FLAG",
action="extend", action=IgnoreNoneAppendAction,
help="Exclude entries with this tag", help=(
"If passed a string, will exclude entries with that tag. "
"Can be also used before -starred or -tagged flags, to exclude "
"starred or tagged entries respectively."
),
) )
search_options_msg = """ These help you do various tasks with the selected entries from your search. search_options_msg = """ These help you do various tasks with the selected entries from your search.
@ -388,5 +435,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
# Handle '-123' as a shortcut for '-n 123' # Handle '-123' as a shortcut for '-n 123'
num = re.compile(r"^-(\d+)$") num = re.compile(r"^-(\d+)$")
args = [num.sub(r"-n \1", arg) for arg in args] args = [num.sub(r"-n \1", arg) for arg in args]
parsed_args = parser.parse_intermixed_args(args)
parsed_args = parse_not_arg(args, parsed_args, parser)
return parser.parse_intermixed_args(args) return parsed_args

View file

@ -107,14 +107,23 @@ def get_default_config() -> dict[str, Any]:
"linewrap": 79, "linewrap": 79,
"indent_character": "|", "indent_character": "|",
"colors": { "colors": {
"date": "none",
"title": "none",
"body": "none", "body": "none",
"date": "none",
"tags": "none", "tags": "none",
"title": "none",
}, },
} }
def get_default_colors() -> dict[str, Any]:
return {
"body": "none",
"date": "black",
"tags": "yellow",
"title": "cyan",
}
def get_default_journal_path() -> str: def get_default_journal_path() -> str:
journal_data_path = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or home_dir() journal_data_path = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or home_dir()
return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME) return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME)

View file

@ -250,7 +250,10 @@ def _filter_journal_entries(args: "Namespace", journal: "Journal", **kwargs) ->
end_date=args.end_date, end_date=args.end_date,
strict=args.strict, strict=args.strict,
starred=args.starred, starred=args.starred,
tagged=args.tagged,
exclude=args.excluded, exclude=args.excluded,
exclude_starred=args.exclude_starred,
exclude_tagged=args.exclude_tagged,
contains=args.contains, contains=args.contains,
) )
journal.limit(args.limit) journal.limit(args.limit)
@ -419,7 +422,10 @@ def _has_search_args(args: "Namespace") -> bool:
return any( return any(
( (
args.contains, args.contains,
args.excluded, # -not args.tagged,
args.excluded,
args.exclude_starred,
args.exclude_tagged,
args.end_date, args.end_date,
args.today_in_history, args.today_in_history,
args.month, args.month,

View file

@ -12,6 +12,7 @@ from rich.pretty import pretty_repr
from jrnl import __version__ from jrnl import __version__
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_default_colors
from jrnl.config import get_default_config from jrnl.config import get_default_config
from jrnl.config import get_default_journal_path from jrnl.config import get_default_journal_path
from jrnl.config import load_config from jrnl.config import load_config
@ -147,7 +148,21 @@ def install() -> dict:
default_config["encrypt"] = True default_config["encrypt"] = True
print_msg(Message(MsgText.JournalEncrypted, MsgStyle.NORMAL)) print_msg(Message(MsgText.JournalEncrypted, MsgStyle.NORMAL))
# Use colors?
use_colors = yesno(Message(MsgText.UseColorsQuestion), default=True)
if use_colors:
default_config["colors"] = get_default_colors()
save_config(default_config) save_config(default_config)
print_msg(
Message(
MsgText.InstallComplete,
MsgStyle.NORMAL,
params={"config_path": get_config_path()},
)
)
return default_config return default_config

View file

@ -127,6 +127,7 @@ class Journal:
}, },
) )
) )
self.write()
text = self._load(filename) text = self._load(filename)
text = self._decrypt(text) text = self._decrypt(text)
@ -233,16 +234,19 @@ class Journal:
def filter( def filter(
self, self,
tags: list = [], tags=[],
month: str | int | None = None, month=None,
day: str | int | None = None, day=None,
year: str | None = None, year=None,
start_date: str | None = None, start_date=None,
end_date: str | None = None, end_date=None,
starred: bool = False, starred=False,
strict: bool = False, tagged=False,
contains: bool = None, exclude_starred=False,
exclude: list = [], exclude_tagged=False,
strict=False,
contains=None,
exclude=[],
): ):
"""Removes all entries from the journal that don't match the filter. """Removes all entries from the journal that don't match the filter.
@ -263,7 +267,9 @@ class Journal:
start_date = time.parse(start_date) start_date = time.parse(start_date)
# If strict mode is on, all tags have to be present in entry # If strict mode is on, all tags have to be present in entry
tagged = self.search_tags.issubset if strict else self.search_tags.intersection has_tags = (
self.search_tags.issubset if strict else self.search_tags.intersection
)
def excluded(tags): def excluded(tags):
return 0 < len([tag for tag in tags if tag in excluded_tags]) return 0 < len([tag for tag in tags if tag in excluded_tags])
@ -279,8 +285,9 @@ class Journal:
result = [ result = [
entry entry
for entry in self.entries for entry in self.entries
if (not tags or tagged(entry.tags)) if (not tags or has_tags(entry.tags))
and (not starred or entry.starred) and (not (starred or exclude_starred) or entry.starred == starred)
and (not (tagged or exclude_tagged) or bool(entry.tags) == tagged)
and (not month or entry.date.month == compare_d.month) and (not month or entry.date.month == compare_d.month)
and (not day or entry.date.day == compare_d.day) and (not day or entry.date.day == compare_d.day)
and (not year or entry.date.year == compare_d.year) and (not year or entry.date.year == compare_d.year)

View file

@ -28,6 +28,11 @@ class MsgText(Enum):
AllDoneUpgrade = "We're all done here and you can start enjoying jrnl 2" AllDoneUpgrade = "We're all done here and you can start enjoying jrnl 2"
InstallComplete = """
jrnl configuration created at {config_path}
For advanced features, read the docs at https://jrnl.sh
"""
# --- Prompts --- # # --- Prompts --- #
InstallJournalPathQuestion = """ InstallJournalPathQuestion = """
Path to your journal file (leave blank for {default_journal_path}): Path to your journal file (leave blank for {default_journal_path}):
@ -37,6 +42,9 @@ class MsgText(Enum):
EncryptJournalQuestion = """ EncryptJournalQuestion = """
Do you want to encrypt your journal? (You can always change this later) Do you want to encrypt your journal? (You can always change this later)
""" """
UseColorsQuestion = """
Do you want jrnl to use colors when displaying entries? (You can always change this later)
"""
YesOrNoPromptDefaultYes = "[Y/n]" YesOrNoPromptDefaultYes = "[Y/n]"
YesOrNoPromptDefaultNo = "[y/N]" YesOrNoPromptDefaultNo = "[y/N]"
ContinueUpgrade = "Continue upgrading jrnl?" ContinueUpgrade = "Continue upgrading jrnl?"

View file

@ -5,6 +5,9 @@ theme:
custom_dir: docs_theme custom_dir: docs_theme
static_templates: static_templates:
- index.html - index.html
watch:
- docs
- docs_theme
extra_css: extra_css:
- https://fonts.googleapis.com/css?family=Open+Sans:300,600 - https://fonts.googleapis.com/css?family=Open+Sans:300,600
- assets/colors.css - assets/colors.css

3117
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -51,10 +51,11 @@ flake8-type-checking = ">=2.2.0"
flake8-simplify = ">=0.19" flake8-simplify = ">=0.19"
ipdb = "*" ipdb = "*"
isort = ">=5.10" isort = ">=5.10"
mkdocs = ">=1.0,<1.3" mkdocs = ">=1.4"
parse-type = ">=0.6.0"
poethepoet = "*" poethepoet = "*"
pytest = ">=6.2" pytest = ">=6.2"
pytest-bdd = ">=4.0.1,<6.0" pytest-bdd = ">=6.0"
pytest-clarity = "*" pytest-clarity = "*"
pytest-xdist = ">=2.5.0" pytest-xdist = ">=2.5.0"
requests = "*" requests = "*"
@ -174,8 +175,9 @@ isolated_build = True
[testenv] [testenv]
deps = deps =
pytest >= 6.2 pytest >= 6.2
pytest-bdd >=4.0.1,<6.0 pytest-bdd >=6.0
pytest-xdist >=2.5.0 pytest-xdist >=2.5.0
parse-type >=0.6.0
toml >=0.10 toml >=0.10
commands = pytest {posargs} commands = pytest {posargs}

View file

@ -27,15 +27,18 @@ Feature: Change entry times in journal
Scenario Outline: Change flag changes prompted entries Scenario Outline: Change flag changes prompted entries
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 -1" When we run "jrnl --short"
Then the output should contain "2020-09-24 09:14 The third entry finally" 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.
When we run "jrnl --change-time '2022-04-23 10:30'" and enter When we run "jrnl --change-time '2022-04-23 10:30'" and enter
Y Y
N N
Y Y
Then the error output should contain "3 entries found" Then the error output should contain "3 entries found"
And the error output should contain "2 entries modified" And the error output should contain "2 entries modified"
When we run "jrnl -99 --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.
2022-04-23 10:30 Entry the first. 2022-04-23 10:30 Entry the first.

View file

@ -57,6 +57,7 @@ Feature: Journals iteracting with the file system in a way that users can see
When we run "jrnl hello world" and enter When we run "jrnl hello world" and enter
test.txt test.txt
n n
\n
Then the output should contain "Journal 'default' created" Then the output should contain "Journal 'default' created"
When we change directory to "subfolder" When we change directory to "subfolder"
And we run "jrnl -n 1" And we run "jrnl -n 1"

View file

@ -5,7 +5,10 @@ Feature: Installing jrnl
When we run "jrnl hello world" and enter When we run "jrnl hello world" and enter
\n \n
\n \n
Then the output should contain "Journal 'default' created" \n
Then the output should contain "jrnl configuration created at"
And the output should contain "For advanced features, read the docs at https://jrnl.sh"
And the output should contain "Journal 'default' created"
And the default journal "journal.txt" should be in the "." directory And the default journal "journal.txt" should be in the "." directory
And the config should contain "encrypt: false" And the config should contain "encrypt: false"
And the version in the config file should be up-to-date And the version in the config file should be up-to-date
@ -15,6 +18,7 @@ Feature: Installing jrnl
When we run "jrnl hello world" and enter When we run "jrnl hello world" and enter
default/custom.txt default/custom.txt
n n
\n
Then the output should contain "Journal 'default' created" Then the output should contain "Journal 'default' created"
And the default journal "custom.txt" should be in the "default" directory And the default journal "custom.txt" should be in the "default" directory
And the config should contain "encrypt: false" And the config should contain "encrypt: false"
@ -26,6 +30,7 @@ Feature: Installing jrnl
When we run "jrnl hello world" and enter When we run "jrnl hello world" and enter
~/custom.txt ~/custom.txt
n n
\n
Then the output should contain "Journal 'default' created" Then the output should contain "Journal 'default' created"
And the default journal "custom.txt" should be in the "home" directory And the default journal "custom.txt" should be in the "home" directory
And the config should contain "encrypt: false" And the config should contain "encrypt: false"
@ -36,10 +41,57 @@ Feature: Installing jrnl
When we run "jrnl hello world" and enter When we run "jrnl hello world" and enter
encrypted.txt encrypted.txt
y y
\n
Then the output should contain "Journal will be encrypted" Then the output should contain "Journal will be encrypted"
And the default journal "encrypted.txt" should be in the "." directory And the default journal "encrypted.txt" should be in the "." directory
And the config should contain "encrypt: true" And the config should contain "encrypt: true"
And the version in the config file should be up-to-date And the version in the config file should be up-to-date
When we run "jrnl" When we run "jrnl"
Then we should be prompted for a password Then we should be prompted for a password
Scenario: Install jrnl with colors by default
Given we use no config
When we run "jrnl hello world" and enter
\n
\n
\n
Then the output should contain "Journal 'default' created"
And the config should contain
colors:
body: none
date: black
tags: yellow
title: cyan
Scenario: Install jrnl without colors
Given we use no config
When we run "jrnl hello world" and enter
\n
\n
N
Then the output should contain "Journal 'default' created"
And the config should contain
colors:
body: none
date: none
tags: none
title: none
Scenario: Install jrnl with encrypted default journal with no entries
Given we use no config
When we run "jrnl -1" and enter
encrypted.txt
y
n
test
test
n
Then the error output should contain "Journal will be encrypted"
And the default journal "encrypted.txt" should be in the "." directory
And the config should contain "encrypt: true"
And the version in the config file should be up-to-date
When we run "jrnl -1" and enter
test
Then we should be prompted for a password
And the error output should contain "no entries found"
And the error output should not contain "Wrong password, try again"

View file

@ -124,6 +124,50 @@ Feature: Searching in a journal
| basic_folder.yaml | | basic_folder.yaml |
| basic_dayone.yaml | | basic_dayone.yaml |
Scenario Outline: Searching for unstarred entries
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl -not -starred"
Then we should get no error
And the output should contain "2 entries found"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: Searching for tagged entries
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl -tagged"
Then we should get no error
And the output should contain "3 entries found"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: Searching for untagged entries
Given we use the config "empty_folder.yaml"
When we run "jrnl Tagged entry. This one has a @tag."
Then we should get no error
When we run "jrnl Untagged entry. This one has no tag."
Then we should get no error
When we run "jrnl -not -tagged"
Then we should get no error
And the output should contain "1 entry found"
And the output should contain "This one has no tag"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: Searching for dates Scenario Outline: Searching for dates
Given we use the config "<config_file>" Given we use the config "<config_file>"
When we run "jrnl -on 2020-08-31 --short" When we run "jrnl -on 2020-08-31 --short"

View file

@ -201,6 +201,16 @@ def input_method():
return "" return ""
@fixture
def all_input():
return ""
@fixture
def command():
return ""
@fixture @fixture
def cache_dir(): def cache_dir():
return {"exists": False, "path": ""} return {"exists": False, "path": ""}
@ -221,13 +231,15 @@ def mock_user_input(request, password_input, stdin_input):
def _mock_user_input(): def _mock_user_input():
# user_input needs to be here because we don't know it until cli_run starts # user_input needs to be here because we don't know it until cli_run starts
user_input = get_fixture(request, "all_input", None) user_input = get_fixture(request, "all_input", None)
if user_input is None: if user_input is None:
user_input = Exception("Unexpected call for user input") user_input = Exception("Unexpected call for user input")
else: else:
user_input = iter(user_input.splitlines()) user_input = iter(user_input.splitlines())
def mock_console_input(**kwargs): def mock_console_input(**kwargs):
if kwargs["password"] and not isinstance(password_input, Exception): pw = kwargs.get("password", False)
if pw and not isinstance(password_input, Exception):
return password_input return password_input
if isinstance(user_input, Iterable): if isinstance(user_input, Iterable):
@ -236,7 +248,7 @@ def mock_user_input(request, password_input, stdin_input):
return "" if input_line == r"\n" else input_line return "" if input_line == r"\n" else input_line
# exceptions # exceptions
return user_input if not kwargs["password"] else password_input return user_input if not pw else password_input
mock_console = Mock(wraps=Console(stderr=True)) mock_console = Mock(wraps=Console(stderr=True))
mock_console.input = Mock(side_effect=mock_console_input) mock_console.input = Mock(side_effect=mock_console_input)

View file

@ -19,7 +19,6 @@ from jrnl.time import __get_pdt_calendar
from tests.lib.fixtures import FailedKeyring from tests.lib.fixtures import FailedKeyring
from tests.lib.fixtures import NoKeyring from tests.lib.fixtures import NoKeyring
from tests.lib.fixtures import TestKeyring from tests.lib.fixtures import TestKeyring
from tests.lib.helpers import get_fixture
@given(parse("we {editor_method} to the editor if opened\n{editor_input}")) @given(parse("we {editor_method} to the editor if opened\n{editor_input}"))
@ -84,16 +83,16 @@ def we_have_type_of_keyring(keyring_type):
return TestKeyring() return TestKeyring()
@given(parse('we use the config "{config_file}"'), target_fixture="config_path")
@given(parse("we use no config"), target_fixture="config_path") @given(parse("we use no config"), target_fixture="config_path")
def we_use_the_config(request, temp_dir, working_dir): def we_use_no_config(temp_dir):
config_file = get_fixture(request, "config_file") os.chdir(temp_dir.name) # @todo move this step to a more universal place
return os.path.join(temp_dir.name, "non_existing_config.yaml")
@given(parse('we use the config "{config_file}"'), target_fixture="config_path")
def we_use_the_config(request, temp_dir, working_dir, config_file):
# Move into temp dir as cwd # Move into temp dir as cwd
os.chdir(temp_dir.name) os.chdir(temp_dir.name) # @todo move this step to a more universal place
if not config_file:
return os.path.join(temp_dir.name, "non_existing_config.yaml")
# Copy the config file over # Copy the config file over
config_source = os.path.join(working_dir, "data", "configs", config_file) config_source = os.path.join(working_dir, "data", "configs", config_file)
@ -133,7 +132,7 @@ def config_exists(config_file, temp_dir, working_dir):
shutil.copy2(config_source, config_dest) shutil.copy2(config_source, config_dest)
@given(parse('we use the password "{password}" if prompted')) @given(parse('we use the password "{password}" if prompted'), target_fixture="password")
def use_password_forever(password): def use_password_forever(password):
return password return password

View file

@ -32,17 +32,6 @@ def does_directory_contain_n_files(directory_path, number):
return int(number) == count return int(number) == count
def parse_should_or_should_not(should_or_should_not):
if should_or_should_not == "should":
return True
elif should_or_should_not == "should not":
return False
else:
raise Exception(
"should_or_should_not valid values are 'should' or 'should not'"
)
def assert_equal_tags_ignoring_order( def assert_equal_tags_ignoring_order(
actual_line, expected_line, actual_content, expected_content actual_line, expected_line, actual_content, expected_content
): ):
@ -81,7 +70,7 @@ def spy_wrapper(wrapped_function):
def get_fixture(request, name, default=None): def get_fixture(request, name, default=None):
result = default try:
if name in request.node.fixturenames: return request.getfixturevalue(name)
result = request.getfixturevalue(name) except LookupError:
return result return default

View file

@ -15,7 +15,9 @@ from tests.lib.helpers import assert_equal_tags_ignoring_order
from tests.lib.helpers import does_directory_contain_files from tests.lib.helpers import does_directory_contain_files
from tests.lib.helpers import does_directory_contain_n_files from tests.lib.helpers import does_directory_contain_n_files
from tests.lib.helpers import get_nested_val from tests.lib.helpers import get_nested_val
from tests.lib.helpers import parse_should_or_should_not from tests.lib.type_builders import should_choice
SHOULD_DICT = {"Should": should_choice}
@then("we should get no error") @then("we should get no error")
@ -31,40 +33,38 @@ def output_should_match(regex, cli_run):
assert matches, f"\nRegex didn't match:\n{regex}\n{str(out)}\n{str(matches)}" assert matches, f"\nRegex didn't match:\n{regex}\n{str(out)}\n{str(matches)}"
@then(parse("the output {should_or_should_not} contain\n{expected_output}")) @then(parse("the output {it_should:Should} contain\n{expected_output}", SHOULD_DICT))
@then(parse('the output {should_or_should_not} contain "{expected_output}"')) @then(parse('the output {it_should:Should} contain "{expected_output}"', SHOULD_DICT))
@then( @then(
parse( parse(
"the {which_output_stream} output {should_or_should_not} contain\n{expected_output}" "the {which_output_stream} output {it_should:Should} contain\n{expected_output}",
SHOULD_DICT,
) )
) )
@then( @then(
parse( parse(
'the {which_output_stream} output {should_or_should_not} contain "{expected_output}"' 'the {which_output_stream} output {it_should:Should} contain "{expected_output}"',
SHOULD_DICT,
) )
) )
def output_should_contain( def output_should_contain(expected_output, which_output_stream, cli_run, it_should):
expected_output, which_output_stream, cli_run, should_or_should_not
):
we_should = parse_should_or_should_not(should_or_should_not)
output_str = f"\nEXPECTED:\n{expected_output}\n\nACTUAL STDOUT:\n{cli_run['stdout']}\n\nACTUAL STDERR:\n{cli_run['stderr']}" output_str = f"\nEXPECTED:\n{expected_output}\n\nACTUAL STDOUT:\n{cli_run['stdout']}\n\nACTUAL STDERR:\n{cli_run['stderr']}"
assert expected_output assert expected_output
if which_output_stream is None: if which_output_stream is None:
assert ((expected_output in cli_run["stdout"]) == we_should) or ( assert ((expected_output in cli_run["stdout"]) == it_should) or (
(expected_output in cli_run["stderr"]) == we_should (expected_output in cli_run["stderr"]) == it_should
), output_str ), output_str
elif which_output_stream == "standard": elif which_output_stream == "standard":
assert (expected_output in cli_run["stdout"]) == we_should, output_str assert (expected_output in cli_run["stdout"]) == it_should, output_str
elif which_output_stream == "error": elif which_output_stream == "error":
assert (expected_output in cli_run["stderr"]) == we_should, output_str assert (expected_output in cli_run["stderr"]) == it_should, output_str
else: else:
assert ( assert (
expected_output in cli_run[which_output_stream] expected_output in cli_run[which_output_stream]
) == we_should, output_str ) == it_should, output_str
@then(parse("the output should not contain\n{expected_output}")) @then(parse("the output should not contain\n{expected_output}"))
@ -78,7 +78,7 @@ def output_should_not_contain(expected_output, cli_run):
def output_should_be(expected_output, cli_run): def output_should_be(expected_output, cli_run):
actual = cli_run["stdout"].strip() actual = cli_run["stdout"].strip()
expected = expected_output.strip() expected = expected_output.strip()
assert expected == actual assert actual == expected
@then("the output should be empty") @then("the output should be empty")
@ -130,19 +130,19 @@ def default_journal_location(journal_file, journal_dir, config_on_disk, temp_dir
@then( @then(
parse( parse(
'the config for journal "{journal_name}" {should_or_should_not} contain "{some_yaml}"' 'the config for journal "{journal_name}" {it_should:Should} contain "{some_yaml}"',
SHOULD_DICT,
) )
) )
@then( @then(
parse( parse(
'the config for journal "{journal_name}" {should_or_should_not} contain\n{some_yaml}' 'the config for journal "{journal_name}" {it_should:Should} contain\n{some_yaml}',
SHOULD_DICT,
) )
) )
@then(parse('the config {should_or_should_not} contain "{some_yaml}"')) @then(parse('the config {it_should:Should} contain "{some_yaml}"', SHOULD_DICT))
@then(parse("the config {should_or_should_not} contain\n{some_yaml}")) @then(parse("the config {it_should:Should} contain\n{some_yaml}", SHOULD_DICT))
def config_var_on_disk(config_on_disk, journal_name, should_or_should_not, some_yaml): def config_var_on_disk(config_on_disk, journal_name, it_should, some_yaml):
we_should = parse_should_or_should_not(should_or_should_not)
actual = config_on_disk actual = config_on_disk
if journal_name: if journal_name:
actual = actual["journals"][journal_name] actual = actual["journals"][journal_name]
@ -154,26 +154,28 @@ def config_var_on_disk(config_on_disk, journal_name, should_or_should_not, some_
# `expected` objects formatted in yaml only compare one level deep # `expected` objects formatted in yaml only compare one level deep
actual_slice = {key: actual.get(key, None) for key in expected.keys()} actual_slice = {key: actual.get(key, None) for key in expected.keys()}
assert (expected == actual_slice) == we_should assert (expected == actual_slice) == it_should
@then( @then(
parse( parse(
'the config in memory for journal "{journal_name}" {should_or_should_not} contain "{some_yaml}"' 'the config in memory for journal "{journal_name}" {it_should:Should} contain "{some_yaml}"',
SHOULD_DICT,
) )
) )
@then( @then(
parse( parse(
'the config in memory for journal "{journal_name}" {should_or_should_not} contain\n{some_yaml}' 'the config in memory for journal "{journal_name}" {it_should:Should} contain\n{some_yaml}',
SHOULD_DICT,
) )
) )
@then(parse('the config in memory {should_or_should_not} contain "{some_yaml}"')) @then(
@then(parse("the config in memory {should_or_should_not} contain\n{some_yaml}")) parse('the config in memory {it_should:Should} contain "{some_yaml}"', SHOULD_DICT)
def config_var_in_memory( )
config_in_memory, journal_name, should_or_should_not, some_yaml @then(
): parse("the config in memory {it_should:Should} contain\n{some_yaml}", SHOULD_DICT)
we_should = parse_should_or_should_not(should_or_should_not) )
def config_var_in_memory(config_in_memory, journal_name, it_should, some_yaml):
actual = config_in_memory["overrides"] actual = config_in_memory["overrides"]
if journal_name: if journal_name:
actual = actual["journals"][journal_name] actual = actual["journals"][journal_name]
@ -185,7 +187,7 @@ def config_var_in_memory(
# `expected` objects formatted in yaml only compare one level deep # `expected` objects formatted in yaml only compare one level deep
actual_slice = {key: get_nested_val(actual, key) for key in expected.keys()} actual_slice = {key: get_nested_val(actual, key) for key in expected.keys()}
assert (expected == actual_slice) == we_should assert (expected == actual_slice) == it_should
@then("we should be prompted for a password") @then("we should be prompted for a password")
@ -224,31 +226,27 @@ def journal_directory_should_not_exist(config_on_disk, journal_name):
), f'Journal "{journal_name}" does exist' ), f'Journal "{journal_name}" does exist'
@then(parse("the journal {should_or_should_not} exist")) @then(parse("the journal {it_should:Should} exist", SHOULD_DICT))
def journal_should_not_exist(config_on_disk, should_or_should_not): def journal_should_not_exist(config_on_disk, it_should):
scoped_config = scope_config(config_on_disk, "default") scoped_config = scope_config(config_on_disk, "default")
expected_path = scoped_config["journal"] expected_path = scoped_config["journal"]
contains_files = does_directory_contain_files(expected_path, ".") contains_files = does_directory_contain_files(expected_path, ".")
if should_or_should_not == "should": assert contains_files == it_should
assert contains_files
elif should_or_should_not == "should not":
assert not contains_files
else:
raise Exception(
"should_or_should_not valid values are 'should' or 'should not'"
)
@then(parse('the journal "{journal_name}" directory {should_or_should_not} exist')) @then(
def directory_should_not_exist(config_on_disk, should_or_should_not, journal_name): parse(
'the journal "{journal_name}" directory {it_should:Should} exist', SHOULD_DICT
)
)
def directory_should_not_exist(config_on_disk, it_should, journal_name):
scoped_config = scope_config(config_on_disk, journal_name) scoped_config = scope_config(config_on_disk, journal_name)
expected_path = scoped_config["journal"] expected_path = scoped_config["journal"]
we_should = parse_should_or_should_not(should_or_should_not)
dir_exists = os.path.isdir(expected_path) dir_exists = os.path.isdir(expected_path)
assert dir_exists == we_should assert dir_exists == it_should
@then(parse('the content of file "{file_path}" in the cache should be\n{file_content}')) @then(parse('the content of file "{file_path}" in the cache should be\n{file_content}'))
@ -383,26 +381,23 @@ def count_elements(number, item, cli_run):
assert len(xml_tree.findall(".//" + item)) == number assert len(xml_tree.findall(".//" + item)) == number
@then(parse("the editor {should_or_should_not} have been called")) @then(parse("the editor {it_should:Should} have been called", SHOULD_DICT))
@then( @then(
parse( parse(
"the editor {should_or_should_not} have been called with {num_args} arguments" "the editor {it_should:Should} have been called with {num_args} arguments",
SHOULD_DICT,
) )
) )
def count_editor_args(num_args, cli_run, editor_state, should_or_should_not): def count_editor_args(num_args, cli_run, editor_state, it_should):
we_should = parse_should_or_should_not(should_or_should_not) assert cli_run["mocks"]["editor"].called == it_should
assert cli_run["mocks"]["editor"].called == we_should
if isinstance(num_args, int): if isinstance(num_args, int):
assert len(editor_state["command"]) == int(num_args) assert len(editor_state["command"]) == int(num_args)
@then(parse("the stdin prompt {should_or_should_not} have been called")) @then(parse("the stdin prompt {it_should:Should} have been called", SHOULD_DICT))
def stdin_prompt_called(cli_run, should_or_should_not): def stdin_prompt_called(cli_run, it_should):
we_should = parse_should_or_should_not(should_or_should_not) assert cli_run["mocks"]["stdin_input"].called == it_should
assert cli_run["mocks"]["stdin_input"].called == we_should
@then(parse('the editor filename should end with "{suffix}"')) @then(parse('the editor filename should end with "{suffix}"'))

View file

@ -0,0 +1,11 @@
# Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from parse_type import TypeBuilder
should_choice = TypeBuilder.make_enum(
{
"should": True,
"should not": False,
}
)

View file

@ -7,6 +7,7 @@ from contextlib import ExitStack
from pytest_bdd import when from pytest_bdd import when
from pytest_bdd.parsers import parse from pytest_bdd.parsers import parse
from pytest_bdd.parsers import re from pytest_bdd.parsers import re
from pytest_bdd.steps import inject_fixture
from jrnl.main import run from jrnl.main import run
@ -29,13 +30,20 @@ all_input = '("(?P<all_input>[^"]*)")'
@when(parse('we run "jrnl {command}" and {input_method}\n{all_input}')) @when(parse('we run "jrnl {command}" and {input_method}\n{all_input}'))
@when(re(f'we run "jrnl ?{command}" and {input_method} {all_input}')) @when(re(f'we run "jrnl ?{command}" and {input_method} {all_input}'))
@when(parse('we run "jrnl {command}"')) @when(re(f'we run "jrnl {command}"(?! and)'))
@when('we run "jrnl"') @when('we run "jrnl"')
def we_run_jrnl(cli_run, capsys, keyring): def we_run_jrnl(capsys, keyring, request, command, input_method, all_input):
from keyring import set_keyring from keyring import set_keyring
set_keyring(keyring) set_keyring(keyring)
# fixture injection (pytest-bdd >=6.0)
inject_fixture(request, "command", command)
inject_fixture(request, "input_method", input_method)
inject_fixture(request, "all_input", all_input)
cli_run = request.getfixturevalue("cli_run")
with ExitStack() as stack: with ExitStack() as stack:
mocks = cli_run["mocks"] mocks = cli_run["mocks"]
factories = cli_run["mock_factories"] factories = cli_run["mock_factories"]

View file

@ -23,6 +23,8 @@ def expected_args(**kwargs):
"change_time": None, "change_time": None,
"edit": False, "edit": False,
"end_date": None, "end_date": None,
"exclude_starred": False,
"exclude_tagged": False,
"today_in_history": False, "today_in_history": False,
"month": None, "month": None,
"day": None, "day": None,
@ -38,6 +40,7 @@ def expected_args(**kwargs):
"starred": False, "starred": False,
"start_date": None, "start_date": None,
"strict": False, "strict": False,
"tagged": False,
"tags": False, "tags": False,
"text": [], "text": [],
"config_override": [], "config_override": [],