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

poetry.lock retrieved from develop then re-locked with poetry lock --no-update
This commit is contained in:
Micah Jerome Ellison 2023-07-17 11:50:21 -07:00
commit d777d41796
33 changed files with 533 additions and 712 deletions

View file

@ -59,7 +59,7 @@ jobs:
if [[ "$(git rev-parse "origin/$BRANCH")" != "$GITHUB_SHA" ]]; then if [[ "$(git rev-parse "origin/$BRANCH")" != "$GITHUB_SHA" ]]; then
# Normal build on a branch (no tag) # Normal build on a branch (no tag)
echo "::debug::BRANCH: $BRANCH $(git rev-parse origin/$BRANCH)" echo "::debug::BRANCH: $BRANCH $(git rev-parse "origin/$BRANCH")"
echo "::debug::GITHUB_SHA: $GITHUB_SHA" echo "::debug::GITHUB_SHA: $GITHUB_SHA"
echo "::error::$BRANCH has been updated since build started. Aborting changelog." echo "::error::$BRANCH has been updated since build started. Aborting changelog."
exit 1 exit 1

View file

@ -200,7 +200,7 @@ jobs:
--force --force
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v5
with: with:
path: ${{ env.BREW_TAP_DIRECTORY }} path: ${{ env.BREW_TAP_DIRECTORY }}
token: ${{ secrets.JRNL_BOT_TOKEN }} token: ${{ secrets.JRNL_BOT_TOKEN }}

View file

@ -14,6 +14,8 @@ on:
paths: paths:
- '.github/workflows/**' - '.github/workflows/**'
- '.github/actions/**' - '.github/actions/**'
schedule:
- cron: '0 0 * * SAT'
jobs: jobs:
test: test:

3
.gitignore vendored
View file

@ -19,6 +19,7 @@ var/
node_modules/ node_modules/
__pycache__/ __pycache__/
.pytest_cache/ .pytest_cache/
.flakeheaven_cache/
# Versioning # Versioning
.python-version .python-version
@ -39,7 +40,7 @@ exp/
objects.inv objects.inv
searchindex.js searchindex.js
# virtaulenv # virtualenv
.venv*/ .venv*/
env/ env/
env*/ env*/

View file

@ -1,45 +1,71 @@
# Changelog # Changelog
## [v4.0-beta3](https://pypi.org/project/jrnl/v4.0-beta3/) (2023-04-29) ## [Unreleased](https://github.com/jrnl-org/jrnl/)
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0-beta2...v4.0-beta3) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0.1...HEAD)
**Fixed bugs:** **Fixed bugs:**
- jrnl reads extraneous text files when reading folder journal [\#1692](https://github.com/jrnl-org/jrnl/issues/1692) - Linting rules aren't enforced the same as format rules [\#1742](https://github.com/jrnl-org/jrnl/issues/1742)
- 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)) **Build:**
- Replace flake8 and isort with ruff linter and add `black --check` to linting step [\#1763](https://github.com/jrnl-org/jrnl/pull/1763) ([micahellison](https://github.com/micahellison))
**Documentation:** **Documentation:**
- Update contributing.md links in documentation [\#1726](https://github.com/jrnl-org/jrnl/pull/1726) ([ahosking](https://github.com/ahosking)) - Add note about messages going to `stderr` and the implication for piping [\#1768](https://github.com/jrnl-org/jrnl/pull/1768) ([micahellison](https://github.com/micahellison))
## [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:** **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 keyring to v24.2.0 [\#1760](https://github.com/jrnl-org/jrnl/pull/1760) ([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 keyring to v24 [\#1758](https://github.com/jrnl-org/jrnl/pull/1758) ([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 pytest to v7.4.0 [\#1757](https://github.com/jrnl-org/jrnl/pull/1757) ([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 rich to v13.4.2 [\#1754](https://github.com/jrnl-org/jrnl/pull/1754) ([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 cryptography to v41 [\#1753](https://github.com/jrnl-org/jrnl/pull/1753) ([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 ruamel.yaml to v0.17.32 [\#1752](https://github.com/jrnl-org/jrnl/pull/1752) ([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)) - Update dependency tox to v4.6.3 [\#1751](https://github.com/jrnl-org/jrnl/pull/1751) ([renovate[bot]](https://github.com/apps/renovate))
## [v4.0-beta](https://pypi.org/project/jrnl/v4.0-beta/) (2023-03-25) ## [v4.0.1](https://pypi.org/project/jrnl/v4.0.1/) (2023-06-20)
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v3.3...v4.0-beta) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0.1-beta...v4.0.1)
**Fixed bugs:**
- jrnl crashes when running `jrnl --list --format json` and `jrnl --list --format yaml` [\#1737](https://github.com/jrnl-org/jrnl/issues/1737)
- Refactor --template code [\#1711](https://github.com/jrnl-org/jrnl/pull/1711) ([micahellison](https://github.com/micahellison))
**Build:**
- Fix linting issue in CI pipeline [\#1743](https://github.com/jrnl-org/jrnl/pull/1743) ([wren](https://github.com/wren))
**Packaging:**
- Update dependency ruamel.yaml to v0.17.28 [\#1749](https://github.com/jrnl-org/jrnl/pull/1749) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency requests to v2.31.0 [\#1748](https://github.com/jrnl-org/jrnl/pull/1748) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency ruamel.yaml to v0.17.26 [\#1746](https://github.com/jrnl-org/jrnl/pull/1746) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency tzlocal to v5 [\#1741](https://github.com/jrnl-org/jrnl/pull/1741) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency pytest-xdist to v3.3.1 [\#1740](https://github.com/jrnl-org/jrnl/pull/1740) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency poethepoet to v0.20.0 [\#1735](https://github.com/jrnl-org/jrnl/pull/1735) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency mkdocs to v1.4.3 [\#1733](https://github.com/jrnl-org/jrnl/pull/1733) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency rich to v13.3.5 [\#1729](https://github.com/jrnl-org/jrnl/pull/1729) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency requests to v2.30.0 [\#1728](https://github.com/jrnl-org/jrnl/pull/1728) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency tox to v4.5.1 [\#1727](https://github.com/jrnl-org/jrnl/pull/1727) ([renovate[bot]](https://github.com/apps/renovate))
- Update peter-evans/create-pull-request action to v5 [\#1719](https://github.com/jrnl-org/jrnl/pull/1719) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency flake8-simplify to v0.20.0 [\#1716](https://github.com/jrnl-org/jrnl/pull/1716) ([renovate[bot]](https://github.com/apps/renovate))
## [v4.0](https://pypi.org/project/jrnl/v4.0/) (2023-05-20)
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v4.0-beta3...v4.0)
🚨 **BREAKING CHANGES** 🚨
**Deprecated:**
- Drop Python 3.9 and use Python 3.11 official release [\#1611](https://github.com/jrnl-org/jrnl/pull/1611) ([micahellison](https://github.com/micahellison))
**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)
- 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)) - Add ability to use template with `--template` [\#1667](https://github.com/jrnl-org/jrnl/pull/1667) ([alichtman](https://github.com/alichtman))
@ -51,25 +77,15 @@
**Fixed bugs:** **Fixed bugs:**
- Combinations of `--change-time`, `--delete`, and `--edit` don't work consistently [\#1696](https://github.com/jrnl-org/jrnl/issues/1696) - 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))
- 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)
- 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 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)) - 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))
**Deprecated:**
- Drop Python 3.9 and use Python 3.11 official release [\#1611](https://github.com/jrnl-org/jrnl/pull/1611) ([micahellison](https://github.com/micahellison))
**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))
@ -77,11 +93,8 @@
**Documentation:** **Documentation:**
- Document template extension behavior [\#1677](https://github.com/jrnl-org/jrnl/issues/1677) - Update contributing.md links in documentation [\#1726](https://github.com/jrnl-org/jrnl/pull/1726) ([ahosking](https://github.com/ahosking))
- Visual Studio Code may store unencrypted temporary files [\#1675](https://github.com/jrnl-org/jrnl/issues/1675) - Fix various typos [\#1718](https://github.com/jrnl-org/jrnl/pull/1718) ([hezhizhen](https://github.com/hezhizhen))
- 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)
- 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)) - 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)) - 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 temporary file extension behavior when using template [\#1686](https://github.com/jrnl-org/jrnl/pull/1686) ([micahellison](https://github.com/micahellison))
@ -94,17 +107,18 @@
**Packaging:** **Packaging:**
- Update dependency cryptography to v40 [\#1710](https://github.com/jrnl-org/jrnl/pull/1710) ([renovate[bot]](https://github.com/apps/renovate)) - Lock ruamel.yaml version to v0.17.21 until bug is fixed [\#1738](https://github.com/jrnl-org/jrnl/pull/1738) ([wren](https://github.com/wren))
- 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 black to v23.3.0 [\#1715](https://github.com/jrnl-org/jrnl/pull/1715) ([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 cryptography to v40.0.2 [\#1723](https://github.com/jrnl-org/jrnl/pull/1723) ([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 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.2 [\#1706](https://github.com/jrnl-org/jrnl/pull/1706) ([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-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 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 poethepoet to v0.19.0 [\#1709](https://github.com/jrnl-org/jrnl/pull/1709) ([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 pytest to v7.3.1 [\#1720](https://github.com/jrnl-org/jrnl/pull/1720) ([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 pytest-xdist to v3.2.1 [\#1705](https://github.com/jrnl-org/jrnl/pull/1705) ([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.7 [\#1707](https://github.com/jrnl-org/jrnl/pull/1707) ([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))
## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29) ## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29)
@ -203,6 +217,10 @@
🚨 **BREAKING CHANGES** 🚨 🚨 **BREAKING CHANGES** 🚨
**Deprecated:**
- Drop support for Python 3.7 and 3.8 [\#1412](https://github.com/jrnl-org/jrnl/pull/1412) ([micahellison](https://github.com/micahellison))
**Implemented enhancements:** **Implemented enhancements:**
- Show name of journal when creating a password/encrypting [\#1478](https://github.com/jrnl-org/jrnl/pull/1478) ([jonakeys](https://github.com/jonakeys)) - Show name of journal when creating a password/encrypting [\#1478](https://github.com/jrnl-org/jrnl/pull/1478) ([jonakeys](https://github.com/jonakeys))
@ -225,10 +243,6 @@
- Display "No entry to save, because no text was received" after empty entry on cmdline [\#1459](https://github.com/jrnl-org/jrnl/pull/1459) ([apainintheneck](https://github.com/apainintheneck)) - Display "No entry to save, because no text was received" after empty entry on cmdline [\#1459](https://github.com/jrnl-org/jrnl/pull/1459) ([apainintheneck](https://github.com/apainintheneck))
- Yaml export errors now don't show stack trace [\#1449](https://github.com/jrnl-org/jrnl/pull/1449) ([apainintheneck](https://github.com/apainintheneck)) - Yaml export errors now don't show stack trace [\#1449](https://github.com/jrnl-org/jrnl/pull/1449) ([apainintheneck](https://github.com/apainintheneck))
**Deprecated:**
- Drop support for Python 3.7 and 3.8 [\#1412](https://github.com/jrnl-org/jrnl/pull/1412) ([micahellison](https://github.com/micahellison))
**Build:** **Build:**
- Pin `pytest-bdd` to \<6.0 to temporarily avoid breaking changes [\#1536](https://github.com/jrnl-org/jrnl/pull/1536) ([wren](https://github.com/wren)) - Pin `pytest-bdd` to \<6.0 to temporarily avoid breaking changes [\#1536](https://github.com/jrnl-org/jrnl/pull/1536) ([wren](https://github.com/wren))

View file

@ -117,6 +117,11 @@ These formats are mainly intended for piping or exporting your journal to other
programs. Even so, they can still be used in the same way as any other format (like programs. Even so, they can still be used in the same way as any other format (like
written to a file, or displayed in your terminal, if you want). written to a file, or displayed in your terminal, if you want).
!!! note
You may see boxed messages like "2 entries found" when using these formats, but
those messages are written to `stderr` instead of `stdout`, and won't be piped when
using the `|` operator.
### JSON ### JSON
``` sh ``` sh

View file

@ -1 +1 @@
__version__ = "v4.0-beta3" __version__ = "v4.0.1"

View file

@ -78,7 +78,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
""" """
We gratefully thank all contributors! We gratefully thank all contributors!
Come see the whole list of code and financial contributors at https://github.com/jrnl-org/jrnl Come see the whole list of code and financial contributors at https://github.com/jrnl-org/jrnl
And special thanks to Bad Lip Reading for the Yoda joke in the Writing section above :)""" And special thanks to Bad Lip Reading for the Yoda joke in the Writing section above :)""" # noqa: E501
), ),
) )
@ -214,7 +214,8 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
composing.add_argument( composing.add_argument(
"--template", "--template",
dest="template", dest="template",
help="Path to template file. Can be a local path, absolute path, or a path relative to $XDG_DATA_HOME/jrnl/templates/", help="Path to template file. Can be a local path, absolute path, or a path "
"relative to $XDG_DATA_HOME/jrnl/templates/",
) )
read_msg = ( read_msg = (
@ -265,13 +266,15 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
"-contains", "-contains",
dest="contains", dest="contains",
metavar="TEXT", metavar="TEXT",
help="Show entries containing specific text (put quotes around text with spaces)", help="Show entries containing specific text (put quotes around text with "
"spaces)",
) )
reading.add_argument( reading.add_argument(
"-and", "-and",
dest="strict", dest="strict",
action="store_true", action="store_true",
help='Show only entries that match all conditions, like saying "x AND y" (default: OR)', help='Show only entries that match all conditions, like saying "x AND y" '
"(default: OR)",
) )
reading.add_argument( reading.add_argument(
"-starred", "-starred",
@ -290,7 +293,8 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
dest="limit", dest="limit",
default=None, default=None,
metavar="NUMBER", metavar="NUMBER",
help="Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same effect)", help="Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same "
"effect)",
nargs="?", nargs="?",
type=int, type=int,
) )
@ -308,8 +312,12 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
), ),
) )
search_options_msg = """ These help you do various tasks with the selected entries from your search. search_options_msg = (
" " # Preserves indentation
"""
These help you do various tasks with the selected entries from your search.
If used on their own (with no search), they will act on your entire journal""" If used on their own (with no search), they will act on your entire journal"""
)
exporting = parser.add_argument_group( exporting = parser.add_argument_group(
"Searching Options", textwrap.dedent(search_options_msg) "Searching Options", textwrap.dedent(search_options_msg)
) )
@ -360,7 +368,8 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
"--tags", "--tags",
dest="tags", dest="tags",
action="store_true", action="store_true",
help="Alias for '--format tags'. Returns a list of all tags and number of occurrences", help="Alias for '--format tags'. Returns a list of all tags and number of "
"occurrences",
) )
exporting.add_argument( exporting.add_argument(
"--short", "--short",
@ -400,7 +409,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
\t jrnl --config-override editor "nano" \n \t jrnl --config-override editor "nano" \n
\t - Override color selections\n \t - Override color selections\n
\t jrnl --config-override colors.body blue --config-override colors.title green \t jrnl --config-override colors.body blue --config-override colors.title green
""", """, # noqa: E501
) )
config_overrides.add_argument( config_overrides.add_argument(
"--co", "--co",
@ -430,7 +439,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
\t jrnl --config-file /home/user1/work_config.yaml \t jrnl --config-file /home/user1/work_config.yaml
\t - Use a personal config file stored on a thumb drive: \n \t - Use a personal config file stored on a thumb drive: \n
\t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml \t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml
""", """, # noqa: E501
) )
alternate_config.add_argument( alternate_config.add_argument(

View file

@ -142,7 +142,7 @@ def postconfig_encrypt(
def postconfig_decrypt( def postconfig_decrypt(
args: argparse.Namespace, config: dict, original_config: dict args: argparse.Namespace, config: dict, original_config: dict
) -> int: ) -> int:
"""Decrypts into new file. If filename is not set, we encrypt the journal file itself.""" """Decrypts to file. If filename is not set, we encrypt the journal file itself."""
from jrnl.config import update_config from jrnl.config import update_config
from jrnl.install import save_config from jrnl.install import save_config
from jrnl.journals import open_journal from jrnl.journals import open_journal

View file

@ -4,12 +4,10 @@
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
import colorama import colorama
import xdg.BaseDirectory
from rich.pretty import pretty_repr from rich.pretty import pretty_repr
from ruamel.yaml import YAML from ruamel.yaml import YAML
from ruamel.yaml import constructor from ruamel.yaml import constructor
@ -21,13 +19,10 @@ from jrnl.messages import MsgStyle
from jrnl.messages import MsgText from jrnl.messages import MsgText
from jrnl.output import list_journals from jrnl.output import list_journals
from jrnl.output import print_msg from jrnl.output import print_msg
from jrnl.path import home_dir from jrnl.path import get_config_path
from jrnl.path import get_default_journal_path
# Constants # Constants
DEFAULT_CONFIG_NAME = "jrnl.yaml"
XDG_RESOURCE = "jrnl"
DEFAULT_JOURNAL_NAME = "journal.txt"
DEFAULT_JOURNAL_KEY = "default" DEFAULT_JOURNAL_KEY = "default"
YAML_SEPARATOR = ": " YAML_SEPARATOR = ": "
@ -42,9 +37,10 @@ def make_yaml_valid_dict(input: list) -> dict:
The dict is created through the yaml loader, with the assumption that The dict is created through the yaml loader, with the assumption that
"input[0]: input[1]" is valid yaml. "input[0]: input[1]" is valid yaml.
:param input: list of configuration keys in dot-notation and their respective values. :param input: list of configuration keys in dot-notation and their respective values
:type input: list :type input: list
:return: A single level dict of the configuration keys in dot-notation and their respective desired values :return: A single level dict of the configuration keys in dot-notation and their
respective desired values
:rtype: dict :rtype: dict
""" """
@ -73,31 +69,6 @@ def save_config(config: dict, alt_config_path: str | None = None) -> None:
yaml.dump(config, f) yaml.dump(config, f)
def get_config_directory() -> str:
try:
return xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
except FileExistsError:
raise JrnlException(
Message(
MsgText.ConfigDirectoryIsFile,
MsgStyle.ERROR,
{
"config_directory_path": os.path.join(
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
)
},
),
)
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]:
return { return {
"version": __version__, "version": __version__,
@ -130,20 +101,6 @@ def get_default_colors() -> dict[str, Any]:
} }
def get_default_journal_path() -> str:
journal_data_path = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or home_dir()
return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME)
def 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,10 +11,10 @@ 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.editor import read_template_file
from jrnl.exception import JrnlException from jrnl.exception import JrnlException
from jrnl.journals import open_journal from jrnl.journals import open_journal
from jrnl.messages import Message from jrnl.messages import Message
@ -23,7 +23,6 @@ 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 absolute_path
if TYPE_CHECKING: if TYPE_CHECKING:
from argparse import Namespace from argparse import Namespace
@ -35,9 +34,9 @@ if TYPE_CHECKING:
def run(args: "Namespace"): def run(args: "Namespace"):
""" """
Flow: Flow:
1. Run standalone command if it doesn't require config (help, version, etc), then exit 1. Run standalone command if it doesn't need config (help, version, etc), then exit
2. Load config 2. Load config
3. Run standalone command if it does require config (encrypt, decrypt, etc), then exit 3. Run standalone command if it does need config (encrypt, decrypt, etc), then exit
4. Load specified journal 4. Load specified journal
5. Start append mode, or search mode 5. Start append mode, or search mode
6. Perform actions with results from search mode (if needed) 6. Perform actions with results from search mode (if needed)
@ -130,74 +129,6 @@ def _is_append_mode(args: "Namespace", config: dict, **kwargs) -> bool:
return append_mode return append_mode
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: 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
@ -210,26 +141,22 @@ def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -
""" """
logging.debug("Append mode: starting") logging.debug("Append mode: starting")
if args.template or config["template"]: template_text = _get_template(args, config)
logging.debug(f"Append mode: template CLI arg detected: {args.template}")
# Read template file and pass as raw text into the composer if args.text:
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}") 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("Append 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, template_text)
if template_text is not None and raw == template_text:
logging.error("Append mode: raw text was the same as the template")
raise JrnlException(Message(MsgText.NoChangesToTemplate, MsgStyle.NORMAL))
if not raw or raw.isspace(): if not raw or raw.isspace():
logging.error("Append mode: couldn't get raw text or entry was empty") logging.error("Append mode: couldn't get raw text or entry was empty")
@ -251,6 +178,23 @@ def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -
logging.debug("Append mode: completed journal.write()") logging.debug("Append mode: completed journal.write()")
def _get_template(args, config) -> str:
# Read template file and pass as raw text into the composer
logging.debug(
"Get template:\n"
f"--template: {args.template}\n"
f"from config: {config.get('template')}"
)
template_path = args.template or config.get("template")
template_text = None
if template_path:
template_text = read_template_file(template_path)
return template_text
def search_mode(args: "Namespace", journal: "Journal", **kwargs) -> None: def search_mode(args: "Namespace", journal: "Journal", **kwargs) -> None:
""" """
Search for entries in a journal, and return the Search for entries in a journal, and return the

View file

@ -15,6 +15,8 @@ from jrnl.messages import MsgText
from jrnl.os_compat import on_windows from jrnl.os_compat import on_windows
from jrnl.os_compat import split_args from jrnl.os_compat import split_args
from jrnl.output import print_msg from jrnl.output import print_msg
from jrnl.path import absolute_path
from jrnl.path import get_templates_path
def get_text_from_editor(config: dict, template: str = "") -> str: def get_text_from_editor(config: dict, template: str = "") -> str:
@ -73,3 +75,47 @@ def get_text_from_stdin() -> str:
) )
return raw return raw
def get_template_path(template_path: str, jrnl_template_dir: str) -> str:
actual_template_path = os.path.join(jrnl_template_dir, template_path)
if not os.path.exists(actual_template_path):
logging.debug(
f"Couldn't open {actual_template_path}. "
"Treating template path like a local / abs path."
)
actual_template_path = absolute_path(template_path)
return actual_template_path
def read_template_file(template_path: str) -> str:
"""
Reads the template file given a template path in this order:
* Check $XDG_DATA_HOME/jrnl/templates/template_path.
* 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.
"""
jrnl_template_dir = get_templates_path()
actual_template_path = get_template_path(template_path, jrnl_template_dir)
try:
with open(actual_template_path, encoding="utf-8") as f:
template_data = f.read()
return template_data
except FileNotFoundError:
raise JrnlException(
Message(
MsgText.CantReadTemplate,
MsgStyle.ERROR,
{
"template_path": template_path,
"actual_template_path": actual_template_path,
"jrnl_template_dir": str(jrnl_template_dir) + os.sep,
},
)
)

View file

@ -31,11 +31,11 @@ from jrnl.upgrade import is_old_version
def upgrade_config(config_data: dict, alt_config_path: str | None = None) -> None: def upgrade_config(config_data: dict, alt_config_path: str | None = None) -> None:
"""Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly. """Checks if there are keys missing in a given config dict, and if so, updates the
This essentially automatically ports jrnl installations if new config parameters are introduced in later config file accordingly. This essentially automatically ports jrnl installations
versions. if new config parameters are introduced in later versions. Also checks for
Also checks for existence of and difference in version number between config dict and current jrnl version, existence of and difference in version number between config dict
and if so, update the config file accordingly. and current jrnl version, and if so, update the config file accordingly.
Supply alt_config_path if using an alternate config through --config-file.""" Supply alt_config_path if using an alternate config through --config-file."""
default_config = get_default_config() default_config = get_default_config()
missing_keys = set(default_config).difference(config_data) missing_keys = set(default_config).difference(config_data)
@ -167,7 +167,7 @@ def install() -> dict:
def _initialize_autocomplete() -> None: def _initialize_autocomplete() -> None:
# readline is not included in Windows Active Python and perhaps some other distributions # readline is not included in Windows Active Python and perhaps some other distss
if sys.modules.get("readline"): if sys.modules.get("readline"):
import readline import readline

View file

@ -88,7 +88,7 @@ class Entry:
} }
def __str__(self): def __str__(self):
"""Returns a string representation of the entry to be written into a journal file.""" """Returns string representation of the entry to be written to journal file."""
date_str = self.date.strftime(self.journal.config["timeformat"]) date_str = self.date.strftime(self.journal.config["timeformat"])
title = "[{}] {}".format(date_str, self.title.rstrip("\n ")) title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
if self.starred: if self.starred:
@ -214,7 +214,7 @@ SENTENCE_SPLITTER = re.compile(
\s+ # AND a sequence of required spaces. \s+ # AND a sequence of required spaces.
) )
|[\uFF01\uFF0E\uFF1F\uFF61\u3002] # CJK full/half width terminals usually do not have following spaces. |[\uFF01\uFF0E\uFF1F\uFF61\u3002] # CJK full/half width terminals usually do not have following spaces.
""", """, # noqa: E501
re.VERBOSE, re.VERBOSE,
) )

View file

@ -122,7 +122,8 @@ class Folder(Journal):
@staticmethod @staticmethod
def _get_files(journal_path: str) -> list[str]: def _get_files(journal_path: str) -> list[str]:
"""Searches through sub directories starting with journal_path and find all text files that look like entries""" """Searches through sub directories starting with journal_path and find all text
files that look like entries"""
for year_folder in Folder._get_year_folders(pathlib.Path(journal_path)): for year_folder in Folder._get_year_folders(pathlib.Path(journal_path)):
for month_folder in Folder._get_month_folders(year_folder): for month_folder in Folder._get_month_folders(year_folder):
yield from Folder._get_day_files(month_folder) yield from Folder._get_day_files(month_folder)

View file

@ -102,7 +102,7 @@ class Journal:
return self.encryption_method.encrypt(text) return self.encryption_method.encrypt(text)
def open(self, filename: str | None = None) -> "Journal": def open(self, filename: str | None = None) -> "Journal":
"""Opens the journal file defined in the config and parses it into a list of Entries. """Opens the journal file and parses it into a list of Entries
Entries have the form (date, title, body).""" Entries have the form (date, title, body)."""
filename = filename or self.config["journal"] filename = filename or self.config["journal"]
dirname = os.path.dirname(filename) dirname = os.path.dirname(filename)
@ -144,7 +144,7 @@ class Journal:
self._store(filename, text) self._store(filename, text)
def validate_parsing(self) -> bool: def validate_parsing(self) -> bool:
"""Confirms that the jrnl is still parsed correctly after being dumped to text.""" """Confirms that the jrnl is still parsed correctly after conversion to text."""
new_entries = self._parse(self._to_text()) new_entries = self._parse(self._to_text())
return all(entry == new_entries[i] for i, entry in enumerate(self.entries)) return all(entry == new_entries[i] for i, entry in enumerate(self.entries))
@ -225,8 +225,9 @@ class Journal:
@property @property
def tags(self) -> list[Tag]: def tags(self) -> list[Tag]:
"""Returns a set of tuples (count, tag) for all tags present in the journal.""" """Returns a set of tuples (count, tag) for all tags present in the journal."""
# Astute reader: should the following line leave you as puzzled as me the first time # Astute reader: should the following line leave you as puzzled as me the first
# I came across this construction, worry not and embrace the ensuing moment of enlightment. # time I came across this construction, worry not and embrace the ensuing moment
# of enlightment.
tags = [tag for entry in self.entries for tag in set(entry.tags)] tags = [tag for entry in self.entries for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag] # To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = {(tags.count(tag), tag) for tag in tags} tag_counts = {(tags.count(tag), tag) for tag in tags}
@ -343,7 +344,8 @@ class Journal:
def new_entry(self, raw: str, date=None, sort: bool = True) -> Entry: def new_entry(self, raw: str, date=None, sort: bool = True) -> Entry:
"""Constructs a new entry from some raw text input. """Constructs a new entry from some raw text input.
If a date is given, it will parse and use this, otherwise scan for a date in the input first. If a date is given, it will parse and use this, otherwise scan for a date in
the input first.
""" """
raw = raw.replace("\\n ", "\n").replace("\\n", "\n") raw = raw.replace("\\n ", "\n").replace("\\n", "\n")

View file

@ -90,4 +90,4 @@ class MsgStyle(Enum):
@property @property
def box_title(self) -> MsgText: def box_title(self) -> MsgText:
return self.value.get("box_title", None) return self.value.get("box_title")

View file

@ -43,8 +43,8 @@ class MsgText(Enum):
Do you want to encrypt your journal? (You can always change this later) Do you want to encrypt your journal? (You can always change this later)
""" """
UseColorsQuestion = """ UseColorsQuestion = """
Do you want jrnl to use colors when displaying entries? (You can always change this later) Do you want jrnl to use colors to display entries? (You can always change this later)
""" """ # noqa: E501 - the line is still under 88 when dedented
YesOrNoPromptDefaultYes = "[Y/n]" YesOrNoPromptDefaultYes = "[Y/n]"
YesOrNoPromptDefaultNo = "[y/N]" YesOrNoPromptDefaultNo = "[y/N]"
ContinueUpgrade = "Continue upgrading jrnl?" ContinueUpgrade = "Continue upgrading jrnl?"
@ -105,16 +105,12 @@ class MsgText(Enum):
KeyboardInterruptMsg = "Aborted by user" KeyboardInterruptMsg = "Aborted by user"
CantReadTemplateGlobalConfig = """ CantReadTemplate = """
Could not read template file defined in config: Unable to find a template file {template_path}.
{global_template_path}
"""
CantReadTemplateCLIArg = """ The following paths were checked:
Unable to find a template file based on the passed arg, and no global template was detected. * {jrnl_template_dir}{template_path}
The following filepaths were checked: * {actual_template_path}
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}"

View file

@ -39,7 +39,10 @@ def journal_list_to_yaml(journal_list: dict) -> str:
from ruamel.yaml import YAML from ruamel.yaml import YAML
output = StringIO() output = StringIO()
YAML().dump(journal_list, output) dumper = YAML()
dumper.width = 1000
dumper.dump(journal_list, output)
return output.getvalue() return output.getvalue()

View file

@ -21,7 +21,7 @@ def apply_overrides(args: "Namespace", base_config: dict) -> dict:
:return: Configuration to be used during runtime with the overrides applied :return: Configuration to be used during runtime with the overrides applied
:rtype: dict :rtype: dict
""" """
overrides = vars(args).get("config_override", None) overrides = vars(args).get("config_override")
if not overrides: if not overrides:
return base_config return base_config
@ -56,7 +56,8 @@ def _recursively_apply(tree: dict, nodes: list, override_value) -> dict:
Args: Args:
config (dict): Configuration to modify config (dict): Configuration to modify
nodes (list): Vector of override keys; the length of the vector indicates tree depth nodes (list): Vector of override keys; the length of the vector indicates tree
depth
override_value (str): Runtime override passed from the command-line override_value (str): Runtime override passed from the command-line
""" """
key = nodes[0] key = nodes[0]

View file

@ -2,6 +2,19 @@
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
import os.path import os.path
from pathlib import Path
import xdg.BaseDirectory
from jrnl.exception import JrnlException
from jrnl.messages import Message
from jrnl.messages import MsgStyle
from jrnl.messages import MsgText
# Constants
XDG_RESOURCE = "jrnl"
DEFAULT_CONFIG_NAME = "jrnl.yaml"
DEFAULT_JOURNAL_NAME = "journal.txt"
def home_dir() -> str: def home_dir() -> str:
@ -14,3 +27,46 @@ def expand_path(path: str) -> str:
def absolute_path(path: str) -> str: def absolute_path(path: str) -> str:
return os.path.abspath(expand_path(path)) return os.path.abspath(expand_path(path))
def get_default_journal_path() -> str:
journal_data_path = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or home_dir()
return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME)
def get_templates_path() -> str:
"""
Get the path to the XDG templates directory. Creates the directory if it
doesn't exist.
"""
# 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 str(jrnl_templates_path)
def get_config_directory() -> str:
try:
return xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
except FileExistsError:
raise JrnlException(
Message(
MsgText.ConfigDirectoryIsFile,
MsgStyle.ERROR,
{
"config_directory_path": os.path.join(
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
)
},
),
)
def get_config_path() -> str:
try:
config_directory_path = get_config_directory()
except JrnlException:
return os.path.join(home_dir(), DEFAULT_CONFIG_NAME)
return os.path.join(config_directory_path, DEFAULT_CONFIG_NAME)

View file

@ -18,7 +18,7 @@ if TYPE_CHECKING:
class FancyExporter(TextExporter): class FancyExporter(TextExporter):
"""This Exporter can convert entries and journals into text with unicode box drawing characters.""" """This Exporter converts entries and journals into text with unicode boxes."""
names = ["fancy", "boxed"] names = ["fancy", "boxed"]
extension = "txt" extension = "txt"

View file

@ -12,7 +12,7 @@ if TYPE_CHECKING:
class TagExporter(TextExporter): class TagExporter(TextExporter):
"""This Exporter can lists the tags for entries and journals, exported as a plain text file.""" """This Exporter lists the tags for entries and journals."""
names = ["tags"] names = ["tags"]
extension = "tags" extension = "tags"

View file

@ -10,7 +10,8 @@ if TYPE_CHECKING:
def get_tags_count(journal: "Journal") -> set[tuple[int, str]]: def get_tags_count(journal: "Journal") -> set[tuple[int, str]]:
"""Returns a set of tuples (count, tag) for all tags present in the journal.""" """Returns a set of tuples (count, tag) for all tags present in the journal."""
# Astute reader: should the following line leave you as puzzled as me the first time # Astute reader: should the following line leave you as puzzled as me the first time
# I came across this construction, worry not and embrace the ensuing moment of enlightment. # I came across this construction, worry not and embrace the ensuing moment of
# enlightment.
tags = [tag for entry in journal.entries for tag in set(entry.tags)] tags = [tag for entry in journal.entries for tag in set(entry.tags)]
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag] # To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
tag_counts = {(tags.count(tag), tag) for tag in tags} tag_counts = {(tags.count(tag), tag) for tag in tags}

View file

@ -18,14 +18,15 @@ if TYPE_CHECKING:
class YAMLExporter(TextExporter): class YAMLExporter(TextExporter):
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter.""" """This Exporter converts entries and journals into Markdown formatted text with
YAML front matter."""
names = ["yaml"] names = ["yaml"]
extension = "md" extension = "md"
@classmethod @classmethod
def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str: def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str:
"""Returns a markdown representation of a single entry, with YAML front matter.""" """Returns a markdown representation of an entry, with YAML front matter."""
if to_multifile is False: if to_multifile is False:
raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR)) raise JrnlException(Message(MsgText.YamlMustBeDirectory, MsgStyle.ERROR))
@ -117,7 +118,14 @@ class YAMLExporter(TextExporter):
# source directory is entry.journal.config['journal'] # source directory is entry.journal.config['journal']
# output directory is...? # output directory is...?
return "{start}\ntitle: {title}\ndate: {date}\nstarred: {starred}\ntags: {tags}\n{dayone}body: |{body}{end}".format( return (
"{start}\n"
"title: {title}\n"
"date: {date}\n"
"starred: {starred}\n"
"tags: {tags}\n"
"{dayone}body: |{body}{end}"
).format(
start="---", start="---",
date=date_str, date=date_str,
title=entry.title, title=entry.title,

View file

@ -34,8 +34,8 @@ def parse(
elif isinstance(date_str, datetime.datetime): elif isinstance(date_str, datetime.datetime):
return date_str return date_str
# Don't try to parse anything with 6 or fewer characters and was parsed from the existing journal. # Don't try to parse anything with 6 or fewer characters and was parsed from the
# It's probably a markdown footnote # existing journal. It's probably a markdown footnote
if len(date_str) <= 6 and bracketed: if len(date_str) <= 6 and bracketed:
return None return None
@ -82,9 +82,9 @@ def parse(
else: else:
date = datetime.datetime(*date[:6]) date = datetime.datetime(*date[:6])
# Ugly heuristic: if the date is more than 4 weeks in the future, we got the year wrong. # Ugly heuristic: if the date is more than 4 weeks in the future, we got the year
# Rather than this, we would like to see parsedatetime patched so we can tell it to prefer # wrong. Rather than this, we would like to see parsedatetime patched so we can
# past dates # tell it to prefer past dates
dt = datetime.datetime.now() - date dt = datetime.datetime.now() - date
if dt.days < -28 and not year_present: if dt.days < -28 and not year_present:
date = date.replace(date.year - 1) date = date.replace(date.year - 1)

540
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "jrnl" name = "jrnl"
version = "v4.0-beta3" version = "v4.0.1"
description = "Collect your thoughts and notes without leaving the command line." description = "Collect your thoughts and notes without leaving the command line."
authors = [ authors = [
"jrnl contributors <maintainers@jrnl.sh>", "jrnl contributors <maintainers@jrnl.sh>",
@ -35,7 +35,7 @@ keyring = ">=21.0" # https://github.com/jaraco/keyring#integration
parsedatetime = ">=2.6" parsedatetime = ">=2.6"
python-dateutil = "^2.8" # https://github.com/dateutil/dateutil/blob/master/RELEASING python-dateutil = "^2.8" # https://github.com/dateutil/dateutil/blob/master/RELEASING
pyxdg = ">=0.27.0" pyxdg = ">=0.27.0"
"ruamel.yaml" = "^0.17.21" "ruamel.yaml" = ">=0.17.22"
rich = ">=12.2.0, <14.0.0" rich = ">=12.2.0, <14.0.0"
# dayone-only deps # dayone-only deps
@ -43,13 +43,7 @@ tzlocal = ">=4.0" # https://github.com/regebro/tzlocal/blob/master/CHANGES.txt
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = { version = ">=21.5b2", allow-prereleases = true } black = { version = ">=21.5b2", allow-prereleases = true }
flakeheaven = ">=3.0"
flake8-black = ">=0.3.3"
flake8-isort = ">=5.0.0"
flake8-type-checking = ">=2.2.0"
flake8-simplify = ">=0.19"
ipdb = "*" ipdb = "*"
isort = ">=5.10"
mkdocs = ">=1.4" mkdocs = ">=1.4"
parse-type = ">=0.6.0" parse-type = ">=0.6.0"
poethepoet = "*" poethepoet = "*"
@ -58,6 +52,7 @@ pytest-bdd = ">=6.0"
pytest-clarity = "*" pytest-clarity = "*"
pytest-xdist = ">=2.5.0" pytest-xdist = ">=2.5.0"
requests = "*" requests = "*"
ruff = ">=0.0.276"
toml = ">=0.10" toml = ">=0.10"
tox = "*" tox = "*"
xmltodict = "*" xmltodict = "*"
@ -87,18 +82,18 @@ test-run = [
# Groups of tasks # Groups of tasks
format.default_item_type = "cmd" format.default_item_type = "cmd"
format.sequence = [ format.sequence = [
"isort .", "ruff check . --select I --fix", # equivalent to "isort ."
"black .", "black .",
] ]
lint.env = { FLAKEHEAVEN_CACHE_TIMEOUT = "0" }
lint.default_item_type = "cmd" lint.default_item_type = "cmd"
lint.sequence = [ lint.sequence = [
"poetry --version", "poetry --version",
"poetry check", "poetry check",
"flakeheaven --version", "ruff --version",
"flakeheaven plugins", "ruff .",
"flakeheaven lint", "black --version",
"black --check ."
] ]
test = [ test = [
@ -106,11 +101,6 @@ test = [
"test-run", "test-run",
] ]
[tool.isort]
profile = "black"
force_single_line = true
known_first_party = ["jrnl", "tests"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
minversion = "6.0" minversion = "6.0"
required_plugins = [ required_plugins = [
@ -136,29 +126,35 @@ filterwarnings = [
"ignore:[WinError 5].*" "ignore:[WinError 5].*"
] ]
[tool.flakeheaven] [tool.ruff]
max_line_length = 88 line-length = 88
# https://beta.ruff.rs/docs/rules/
select = [
'F', # Pyflakes
'E', # pycodestyle errors
'W', # pycodestyle warnings
'I', # isort
'ASYNC', # flake8-async
'S110', # try-except-pass
'S112', # try-except-continue
'EM', # flake8-errmsg
'ISC', # flake8-implicit-str-concat
'Q', # flake8-quotes
'RSE', # flake8-raise
'TID', # flake8-tidy-imports
'TCH', # flake8-type-checking
'T100', # debugger, don't allow break points
'ICN' # flake8-import-conventions
]
exclude = [".git", ".tox", ".venv", "node_modules"] exclude = [".git", ".tox", ".venv", "node_modules"]
[tool.flakeheaven.plugins] [tool.ruff.isort]
"py*" = ["+*"] force-single-line = true
pycodestyle = [ known-first-party = ["jrnl", "tests"]
"-E101",
"-E111", "-E114", "-E115", "-E116", "-E117",
"-E12*",
"-E13*",
"-E2*",
"-E3*",
"-E401",
"-E5*",
"-E70",
"-W1*", "-W2*", "-W3*", "-W5*",
]
"flake8-*" = ["+*"]
flake8-black = ["-BLK901"]
[tool.flakeheaven.exceptions."jrnl/journals/__init__.py"] [tool.ruff.per-file-ignores]
pyflakes = ["-F401"] "__init__.py" = ["F401"] # unused imports
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View file

@ -38,7 +38,7 @@ Feature: Using templates
And we use the password "test" if prompted And we use the password "test" if prompted
When we run "jrnl --template this_template_does_not_exist.template" When we run "jrnl --template this_template_does_not_exist.template"
Then we should get an error Then we should get an error
Then the error output should contain "Unable to find a template file based on the passed arg" Then the error output should contain "Unable to find a template file"
Examples: configs Examples: configs
| config_file | | config_file |

View file

@ -183,10 +183,10 @@ def mock_default_journal_path(temp_dir):
@fixture @fixture
def mock_default_templates_path(temp_dir): def mock_default_templates_path(temp_dir):
templates_path = Path(temp_dir.name, "templates") templates_path = os.path.join(temp_dir.name, "templates")
return { return {
"get_templates_path": lambda: patch( "get_templates_path": lambda: patch(
"jrnl.controller.get_templates_path", return_value=templates_path "jrnl.editor.get_templates_path", return_value=templates_path
), ),
} }

View file

@ -38,38 +38,41 @@ def output_should_match(regex, cli_run):
assert matches, f"\nRegex didn't match:\n{regex}\n{str(out)}\n{str(matches)}" assert matches, f"\nRegex didn't match:\n{regex}\n{str(out)}\n{str(matches)}"
@then(parse("the output {it_should:Should} contain\n{expected_output}", SHOULD_DICT)) @then(parse("the output {it_should:Should} contain\n{expected}", SHOULD_DICT))
@then(parse('the output {it_should:Should} contain "{expected_output}"', SHOULD_DICT)) @then(parse('the output {it_should:Should} contain "{expected}"', SHOULD_DICT))
@then( @then(
parse( parse(
"the {which_output_stream} output {it_should:Should} contain\n{expected_output}", "the {which_output_stream} output {it_should:Should} contain\n{expected}",
SHOULD_DICT, SHOULD_DICT,
) )
) )
@then( @then(
parse( parse(
'the {which_output_stream} output {it_should:Should} contain "{expected_output}"', 'the {which_output_stream} output {it_should:Should} contain "{expected}"',
SHOULD_DICT, SHOULD_DICT,
) )
) )
def output_should_contain(expected_output, which_output_stream, cli_run, it_should): def output_should_contain(expected, which_output_stream, cli_run, it_should):
output_str = f"\nEXPECTED:\n{expected_output}\n\nACTUAL STDOUT:\n{cli_run['stdout']}\n\nACTUAL STDERR:\n{cli_run['stderr']}" output_str = (
assert expected_output f"\nEXPECTED:\n{expected}\n\n"
f"ACTUAL STDOUT:\n{cli_run['stdout']}\n\n"
f"ACTUAL STDERR:\n{cli_run['stderr']}"
)
assert expected
if which_output_stream is None: if which_output_stream is None:
assert ((expected_output in cli_run["stdout"]) == it_should) or ( assert ((expected in cli_run["stdout"]) == it_should) or (
(expected_output in cli_run["stderr"]) == it_should (expected in cli_run["stderr"]) == it_should
), output_str ), output_str
elif which_output_stream == "standard": elif which_output_stream == "standard":
assert (expected_output in cli_run["stdout"]) == it_should, output_str assert (expected in cli_run["stdout"]) == it_should, output_str
elif which_output_stream == "error": elif which_output_stream == "error":
assert (expected_output in cli_run["stderr"]) == it_should, output_str assert (expected in cli_run["stderr"]) == it_should, output_str
else: else:
assert ( assert (expected in cli_run[which_output_stream]) == it_should, output_str
expected_output in cli_run[which_output_stream]
) == it_should, output_str
@then(parse("the output should not contain\n{expected_output}")) @then(parse("the output should not contain\n{expected_output}"))
@ -119,7 +122,8 @@ def output_should_be_columns_wide(cli_run, width):
@then( @then(
parse( parse(
'the default journal "{journal_file}" should be in the "{journal_dir}" directory' 'the default journal "{journal_file}" '
'should be in the "{journal_dir}" directory'
) )
) )
def default_journal_location(journal_file, journal_dir, config_on_disk, temp_dir): def default_journal_location(journal_file, journal_dir, config_on_disk, temp_dir):
@ -135,13 +139,15 @@ def default_journal_location(journal_file, journal_dir, config_on_disk, temp_dir
@then( @then(
parse( parse(
'the config for journal "{journal_name}" {it_should:Should} contain "{some_yaml}"', 'the config for journal "{journal_name}" '
'{it_should:Should} contain "{some_yaml}"',
SHOULD_DICT, SHOULD_DICT,
) )
) )
@then( @then(
parse( parse(
'the config for journal "{journal_name}" {it_should:Should} contain\n{some_yaml}', 'the config for journal "{journal_name}" '
"{it_should:Should} contain\n{some_yaml}",
SHOULD_DICT, SHOULD_DICT,
) )
) )
@ -157,20 +163,22 @@ def config_var_on_disk(config_on_disk, journal_name, it_should, some_yaml):
actual_slice = actual actual_slice = actual
if type(actual) is dict: if type(actual) is dict:
# `expected` objects formatted in yaml only compare one level deep # `expected` objects formatted in yaml only compare one level deep
actual_slice = {key: actual.get(key, None) for key in expected.keys()} actual_slice = {key: actual.get(key) for key in expected.keys()}
assert (expected == actual_slice) == it_should assert (expected == actual_slice) == it_should
@then( @then(
parse( parse(
'the config in memory for journal "{journal_name}" {it_should:Should} contain "{some_yaml}"', 'the config in memory for journal "{journal_name}" '
'{it_should:Should} contain "{some_yaml}"',
SHOULD_DICT, SHOULD_DICT,
) )
) )
@then( @then(
parse( parse(
'the config in memory for journal "{journal_name}" {it_should:Should} contain\n{some_yaml}', 'the config in memory for journal "{journal_name}" '
"{it_should:Should} contain\n{some_yaml}",
SHOULD_DICT, SHOULD_DICT,
) )
) )

45
tests/unit/test_editor.py Normal file
View file

@ -0,0 +1,45 @@
# Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
import os
from unittest.mock import mock_open
from unittest.mock import patch
import pytest
from jrnl.editor import get_template_path
from jrnl.editor import read_template_file
from jrnl.exception import JrnlException
@patch(
"os.getcwd", side_effect="/"
) # prevent failures in CI if current directory has been deleted
@patch("builtins.open", side_effect=FileNotFoundError())
def test_read_template_file_with_no_file_raises_exception(mock_open, mock_getcwd):
with pytest.raises(JrnlException) as ex:
read_template_file("invalid_file.txt")
assert isinstance(ex.value, JrnlException)
@patch(
"os.getcwd", side_effect="/"
) # prevent failures in CI if current directory has been deleted
@patch("builtins.open", new_callable=mock_open, read_data="template text")
def test_read_template_file_with_valid_file_returns_text(mock_file, mock_getcwd):
assert read_template_file("valid_file.txt") == "template text"
def test_get_template_path_when_exists_returns_correct_path():
with patch("os.path.exists", return_value=True):
output = get_template_path("template", "templatepath")
assert output == os.path.join("templatepath", "template")
@patch("jrnl.editor.absolute_path")
def test_get_template_path_when_doesnt_exist_returns_correct_path(mock_absolute_paths):
with patch("os.path.exists", return_value=False):
output = get_template_path("template", "templatepath")
assert output == mock_absolute_paths.return_value

View file

@ -242,7 +242,9 @@ def test_color_override():
def test_multiple_overrides(): def test_multiple_overrides():
parsed_args = cli_as_dict( parsed_args = cli_as_dict(
'--config-override colors.title green --config-override editor "nano" --config-override journal.scratchpad "/tmp/scratchpad"' "--config-override colors.title green "
'--config-override editor "nano" '
'--config-override journal.scratchpad "/tmp/scratchpad"'
) )
assert parsed_args == expected_args( assert parsed_args == expected_args(
config_override=[ config_override=[