diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..0019514d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +## Bug report + +Hello! Thank you for reporting an issue! +If you would fill out the below points, that would make our process a whole lot easier! + +* **Please tell us about your environment:** + - Jrnl version: (run `jrnl -v`) + - How you installed Jrnl + - Operating system [MacOS, Linux, Windows?] + +* **What is the current behavior?** + +* **Please provide the steps to reproduce and if possible a minimal demo of the problem** + +* **What is the expected behavior?** + +* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3ba13e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..a2970a2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for jrnl +title: '' +labels: enhancement +assignees: '' + +--- + +## Feature Request + +Hello! Thank you for reporting an issue! +If you would fill out the below points, that would make our process a whole lot easier! + +* **What is the motivation / use case for changing the behavior?** + +* **Please provide examples of the usage** + +* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc) diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 00000000..d3ce69b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,27 @@ +--- +name: Support Request +about: Get help with jrnl +title: '' +labels: support +assignees: '' + +--- + +## Support request + +Hello! Thank you for reporting an issue! +If you would fill out the below points, that would make our process a whole lot easier! + +* **Please tell us about your environment:** + + - Jrnl version: (run `jrnl -v`) + - How you installed Jrnl + + - Operating system [MacOS, Linux, Windows?] + +* **What are you trying to do?** + +* **What have you tried?** + +* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc) + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..986f10ee --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ + +# **TEMPLATE PLEASE EDIT** +*Thank you for wanting to contribute! Please fill out this description as well as look at the checklist!* + +*Short block of text containing: +- Relevant changes in text form +- related issues +- Motivation (if applicable) +- Example of usage (if applicable) +- Example of changes to config files (if applicable) +* +### Checklist +- [ ] The code change is tested and works locally. +- [ ] Tests pass. Your PR cannot be merged unless tests pass +- [ ] There is no commented out code in this PR. +- [ ] Have you followed the guidelines in our Contributing document? +- [ ] Have you checked to ensure there aren't other open [Pull Requests](../pulls) for the same update/change? +- [ ] Have you added an explanation of what your changes do and why you'd like us to include them? +- [ ] Have you written new tests for your core changes, as applicable? diff --git a/.travis.yml b/.travis.yml index fea48f50..11e6586c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ dist: xenial # required for Python >= 3.7 language: python python: "3.7" +git: + depth: false before_install: - - pip install poetry + - pip install poetry~=0.12.17 install: # we run `poetry version` here to appease poetry about '0.0.0-source' - poetry version @@ -11,7 +13,6 @@ script: - poetry run python --version - poetry run behave before_deploy: - - pip install poetry - poetry config http-basic.pypi $PYPI_USER $PYPI_PASS - poetry version $TRAVIS_TAG - poetry build @@ -22,3 +23,10 @@ deploy: on: branch: master tags: true +after_deploy: + - git config --global user.email "jrnl.bot@gmail.com" + - git config --global user.name "Jrnl Bot" + - git checkout master + - git add pyproject.toml + - git commit -m "Incrementing version to ${TRAVIS_TAG}" + - git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c400b8c..81c93758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changelog ## 2.0 -* Cryptographical backend changed from PyCrypto to cryptography.io +* Cryptographic backend changed from PyCrypto to cryptography.io * Config now respects XDG conventions and may move accordingly * Config now saved as YAML * Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 989ccf7e..0de5982e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,13 +18,13 @@ Unfortunately, bugs happen. If you found one, please [open a new issue](https:// Feature requests and ideas -------------------------- -So, you have an idea for a great feature? Awesome! We'd love to hear from you! Please [open a new issue](https://github.com/jrnl-org/jrnl/issues) and describe the goal of the feature, and any relvant use cases. We'll discuss the issue with you, and decide if it's a good fit for the project. +So, you have an idea for a great feature? Awesome! We'd love to hear from you! Please [open a new issue](https://github.com/jrnl-org/jrnl/issues) and describe the goal of the feature, and any relevant use cases. We'll discuss the issue with you, and decide if it's a good fit for the project. When discussing new features, please keep in mind our design goals. jrnl strives to do one thing well. To us, that means: * be _slim_ * have a simple interface -* avoid dupicating functionality +* avoid duplicating functionality A short note for new programmers and programmers new to python diff --git a/LICENSE b/LICENSE index dc4571c0..4419cd43 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Manuel Ebert +Copyright (c) 2014-2019 Manuel Ebert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/advanced.md b/docs/advanced.md index 2a6fd6b4..0375da10 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -3,15 +3,19 @@ ## Configuration File You can configure the way jrnl behaves in a configuration file. By -default, this is `~/.jrnl_config`. If you have the `XDG_CONFIG_HOME` +default, this is `~/.config/jrnl/jrnl.yaml`. If you have the `XDG_CONFIG_HOME` variable set, the configuration file will be saved as -`$XDG_CONFIG_HOME/jrnl/.jrnl_config`. +`$XDG_CONFIG_HOME/jrnl/jrnl.yaml`. !!! note - On Windows, The configuration file is typically found at `C:\Users\[Your Username]\.jrnl_config`. + On Windows, the configuration file is typically found at `%USERPROFILE%\.config\jrnl\jrnl.yaml`. -The configuration file is a simple JSON file with the following options -and can be edited with any plain text editor. +The configuration file is a YAML file with the following options +and can be edited with a plain text editor. + +!!! note + Backup your config file before editing. Changes to the config file + have destructive effects on your journal! - `journals` paths to your journal files @@ -51,46 +55,16 @@ and can be edited with any plain text editor. Or use the built-in prompt or an external editor to compose your entries. -## DayOne Integration - -Using your DayOne journal instead of a flat text file is dead simple -- -instead of pointing to a text file, change your `.jrnl_config` to point -to your DayOne journal. This is a folder named something like -`Journal_dayone` or `Journal.dayone`, and it's located at - - - `~/Library/Application Support/Day One/` by default - - `~/Dropbox/Apps/Day One/` if you're syncing with Dropbox and - - `~/Library/Mobile - Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/` if you're - syncing with iCloud. - -Instead of all entries being in a single file, each entry will live in a -separate `plist` file. So your `.jrnl_config` should look like this: - -``` javascript -{ - ... - "journals": { - "default": "~/journal.txt", - "dayone": "~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/Journal_dayone" - } -} -``` - ## Multiple journal files You can configure `jrnl`to use with multiple journals (eg. -`private` and `work`) by defining more journals in your `.jrnl_config`, +`private` and `work`) by defining more journals in your `jrnl.yaml`, for example: -``` javascript -{ -... - "journals": { - "default": "~/journal.txt", - "work": "~/work.txt" - } -} +``` yaml +journals: + default: ~\journal.txt + work: ~\work.txt ``` The `default` journal gets created the first time you start `jrnl` @@ -106,26 +80,22 @@ will both use `~/work.txt`, while `jrnl -n 3` will display the last three entries from `~/journal.txt` (and so does `jrnl default -n 3`). You can also override the default options for each individual journal. -If you `.jrnl_config` looks like this: +If your `jrnl.yaml` looks like this: -``` javascript -{ - ... - "encrypt": false - "journals": { - "default": "~/journal.txt", - "work": { - "journal": "~/work.txt", - "encrypt": true - }, - "food": "~/my_recipes.txt", -} +``` yaml +encrypt: false +journals: +default: ~/journal.txt +work: + journal: ~/work.txt + encrypt: true +food: ~/my_recipes.txt ``` Your `default` and your `food` journals won't be encrypted, however your `work` journal will! You can override all options that are present at -the top level of `.jrnl_config`, just make sure that at the very least -you specify a `"journal": ...` key that points to the journal file of +the top level of `jrnl.yaml`, just make sure that at the very least +you specify a `journal: ...` key that points to the journal file of that journal. !!! note diff --git a/docs/encryption.md b/docs/encryption.md index 19bb178b..2cb5d547 100644 --- a/docs/encryption.md +++ b/docs/encryption.md @@ -2,9 +2,9 @@ ## Encrypting and decrypting -If you don't choose to encrypt your file when you run +If you don’t choose to encrypt your file when you run `jrnl` for the first time, you can encrypt -your existing journal file or change its password using +your existing journal file or change its password using this: ``` sh jrnl --encrypt @@ -18,44 +18,50 @@ replaced by the encrypted file. Conversely, jrnl --decrypt ``` -will replace your encrypted journal file by a Journal in plain text. You -can also specify a filename, ie. `jrnl --decrypt plain_text_copy.txt`, +will replace your encrypted journal file with a journal in plain text. You +can also specify a filename, i.e. `jrnl --decrypt plain_text_copy.txt`, to leave your original file untouched. ## Storing passwords in your keychain Whenever you encrypt your journal, you are asked whether you want to store the encryption password in your keychain. If you do this, you -won't have to enter your password every time you want to write or read +won’t have to enter your password every time you want to write or read your journal. -If you don't initially store the password in the keychain but decide to -do so at a later point -- or maybe want to store it on one computer but -not on another -- you can simply run `jrnl --encrypt` on an encrypted +If you don’t initially store the password in the keychain but decide to +do so at a later point – or maybe want to store it on one computer but +not on another – you can simply run `jrnl --encrypt` on an encrypted journal and use the same password again. ## A note on security While jrnl follows best practises, true security is an illusion. Specifically, jrnl will leave traces in your memory and your shell -history -- it's meant to keep journals secure in transit, for example +history – it’s meant to keep journals secure in transit, for example when storing it on an [untrusted](http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/) -services such as Dropbox. If you're concerned about security, disable -history logging for journal in your `.bashrc` +services such as Dropbox. If you’re concerned about security, disable +history logging for journal in your `.bashrc`: ``` sh HISTIGNORE="$HISTIGNORE:jrnl *" ``` -If you are using zsh instead of bash, you can get the same behaviour -adding this to your `zshrc` +If you are using zsh instead of bash, you can get the same behaviour by +adding this to your `zshrc`: ``` sh setopt HIST_IGNORE_SPACE alias jrnl=" jrnl" ``` +The fish shell does not support automatically preventing logging like +this. To prevent `jrnl` commands being logged by fish, you must make +sure to type a space before every `jrnl` command you enter. To delete +existing `jrnl` commands from fish’s history, run +`history delete --prefix 'jrnl '`. + ## Manual decryption Should you ever want to decrypt your journal manually, you can do so @@ -63,8 +69,8 @@ with any program that supports the AES algorithm in CBC. The key used for encryption is the SHA-256-hash of your password, the IV (initialisation vector) is stored in the first 16 bytes of the encrypted file. The plain text is encoded in UTF-8 and padded according to PKCS\#7 -before being encrypted. Here's a Python script that you can use to -decrypt your journal +before being encrypted. Here’s a Python script that you can use to +decrypt your journal: ``` python #!/usr/bin/env python3 diff --git a/docs/installation.md b/docs/installation.md index 36b694ce..8bf29bb0 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -15,27 +15,12 @@ On other platforms, install *jrnl* using pip pip install jrnl ``` -Or, if you want the option to encrypt your journal, - -``` sh -pip install jrnl[encrypted] -``` - -to install the dependencies for encrypting journals as well. - - -!!! note - Installing the encryption library, `pycrypto`, requires a `gcc` compiler. For this reason, jrnl will - not install `pycrypto` unless explicitly told so like this. You can [install PyCrypto manually](https://www.dlitz.net/software/pycrypto/) - first or install it with `pip install pycrypto` if you have a `gcc` compiler. - Also note that when using zsh, the correct syntax is `pip install "jrnl[encrypted]"` (note the quotes). - The first time you run `jrnl` you will be asked where your journal file should be created and whether you wish to encrypt it. ## Quickstart -to make a new entry, just type +To make a new entry, just type ``` sh jrnl yesterday: Called in sick. Used the time to clean the house and spent 4h on writing my book. diff --git a/docs/overview.md b/docs/overview.md index 8a0ec10f..86211814 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -8,10 +8,6 @@ files - you can put them into a Dropbox folder for instant syncing and you can be assured that your journal will still be readable in 2050, when all your fancy iPad journal applications will long be forgotten. -`jrnl` also plays nice with the fabulous -[DayOne](http://dayoneapp.com) and can read and write directly from and -to DayOne Journals. - Optionally, your journal can be encrypted using the [256-bit AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard). diff --git a/docs/recipes.md b/docs/recipes.md index 7af5b246..04604e6d 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -7,7 +7,7 @@ If I want to find out how often I mentioned my flatmates Alberto and Melo in the same entry, I run -``` sh +```sh jrnl @alberto --tags | grep @melo ``` @@ -22,7 +22,7 @@ each tag occurred in this filtered journal. Finally, we pipe this to You can do things like -``` sh +```sh jrnl @fixed -starred -n 10 -until "jan 2013" --short ``` @@ -33,14 +33,14 @@ January 1, 2013 that are tagged with `@fixed`. How much did I write last year? -``` sh +```sh jrnl -from "jan 1 2013" -until "dec 31 2013" | wc -w ``` Will give you the number of words you wrote in 2013. How long is my average entry? -``` sh +```sh expr $(jrnl --export text | wc -w) / $(jrnl --short | wc -l) ``` @@ -50,11 +50,10 @@ print exactly one line per entry). ### Importing older files -If you want to import a file as an entry to jrnl, you can just do `jrnl -< entry.ext`. But what if you want the modification date of the file to +If you want to import a file as an entry to jrnl, you can just do `jrnl < entry.ext`. But what if you want the modification date of the file to be the date of the entry in jrnl? Try this -``` sh +```sh echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' entry.txt` `cat entry.txt` | jrnl ``` @@ -63,7 +62,7 @@ then combine it with the contents of the file before piping it to jrnl. If you do that often, consider creating a function in your `.bashrc` or `.bash_profile` -``` sh +```sh jrnlimport () { echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl } @@ -83,7 +82,7 @@ Another nice solution that allows you to define individual prompts comes from [Jacobo de Vera](https://github.com/maebert/jrnl/issues/194#issuecomment-47402869): -``` sh +```sh function log_question() { echo $1 @@ -94,22 +93,32 @@ log_question 'What did I achieve today?' log_question 'What did I make progress with?' ``` +### Display random entry + +You can use this to select one title at random and then display the whole +entry. The invocation of `cut` needs to match the format of the timestamp. +For timestamps that have a space between data and time components, select +fields 1 and 2 as shown. For timestamps that have no whitespace, select +only field 1. + +```sh +jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)" +``` + ## External editors To use external editors for writing and editing journal entries, set -them up in your `.jrnl_config` (see `advanced usage ` for +them up in your `jrnl.yaml` (see `advanced usage ` for details). Generally, after writing an entry, you will have to save and close the file to save the changes to jrnl. ### Sublime Text To use Sublime Text, install the command line tools for Sublime Text and -configure your `.jrnl_config` like this: +configure your `jrnl.yaml` like this: -``` json -{ - "editor": "subl -w" -} +```yaml +editor: "subl -w" ``` Note the `-w` flag to make sure jrnl waits for Sublime Text to close the @@ -121,22 +130,20 @@ Similar to Sublime Text, MacVim must be started with a flag that tells the the process to wait until the file is closed before passing control back to journal. In the case of MacVim, this is `-f`: -``` json -{ - "editor": "mvim -f" -} +<<<<<<< HEAD + +```yaml +editor: "mvim -f" ``` ### iA Writer On OS X, you can use the fabulous [iA Writer](http://www.iawriter.com/mac) to write entries. Configure your -`.jrnl_config` like this: +`jrnl.yaml` like this: -``` json -{ - "editor": "open -b pro.writer.mac -Wn" -} +```yaml +editor: "open -b pro.writer.mac -Wn" ``` What does this do? `open -b ...` opens a file using the application @@ -148,19 +155,17 @@ If the `pro.writer.mac` bundle identifier is not found on your system, you can find the right string to use by inspecting iA Writer's `Info.plist` file in your shell: -``` sh +```sh grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist ``` ### Notepad++ on Windows To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit -the jrnl config file (`.jrnl_config`) like this: +the jrnl config file (`jrnl.yaml`) like this: -``` json -{ - "editor": "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession", -} +```yaml +editor: "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession" ``` The double backslashes are needed so jrnl can read the file path @@ -169,12 +174,10 @@ its own Notepad++ window. ### Visual Studio Code -To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `.jrnl_config` like this: +To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `jrnl.yaml` like this: -```json -{ - "editor": "/usr/bin/code --wait", -} +```yaml +editor: "/usr/bin/code --wait" ``` The `--wait` argument tells VS Code to wait for files to be written out before handing back control to jrnl. @@ -184,14 +187,13 @@ On MacOS you will need to add VS Code to your PATH. You can do that by adding: ```sh export PATH="\$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin" ``` + to your `.bash_profile`, or by running the **Install 'code' command in PATH** command from the command pallet in VS Code. Then you can add: -```javascript -{ - "editor": "code --wait", -} +```yaml +editor: "code --wait" ``` -to ``.jrnl_config``. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac) +to `jrnl.yaml`. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac) diff --git a/docs/theme/index.html b/docs/theme/index.html index 0c179210..a0f620fb 100755 --- a/docs/theme/index.html +++ b/docs/theme/index.html @@ -82,11 +82,6 @@

Accessible anywhere.

Sync your journals with Dropbox and capture your thoughts where ever you are

-
- -

DayOne compatible.

-

Read, write and search your DayOne journal from the command line.

-

Free & Open Source.

diff --git a/docs/usage.md b/docs/usage.md index fa5050a0..269bdce9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,15 +1,15 @@ # Basic Usage `jrnl` has two modes: **composing** and **viewing**. Basically, whenever -you *don't* supply any arguments that start +you _don't_ supply any arguments that start with a dash or double-dash, you're in composing mode, meaning you can write your entry on the command line or an editor of your choice. We intentionally break a convention on command line arguments: all -arguments starting with a *single dash* -will *filter* your journal before viewing +arguments starting with a _single dash_ +will _filter_ your journal before viewing it, and can be combined arbitrarily. Arguments with a -*double dash* will control how your journal +_double dash_ will control how your journal is displayed or exported and are mutually exclusive (ie. you can only specify one way to display or export your journal at a time). @@ -17,7 +17,7 @@ specify one way to display or export your journal at a time). You can list the journals accessible by jrnl -``` sh +```sh jrnl -ls ``` @@ -30,21 +30,21 @@ Composing mode is entered by either starting `jrnl` without any arguments -- which will prompt you to write an entry or launch your editor -- or by just writing an entry on the prompt, such as -``` sh +```sh jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny. ``` !!! note - Most shell contains a certain number of reserved characters, such as `#` - and `*`. Unbalanced quotes, parenthesis, and so on will also get into - the way of your editing. - For writing longer entries, just enter `jrnl` - and hit `return`. Only then enter the text of your journal entry. - Alternatively, `use an external editor `). +Most shell contains a certain number of reserved characters, such as `#` +and `*`. Unbalanced quotes, parenthesis, and so on will also get into +the way of your editing. +For writing longer entries, just enter `jrnl` +and hit `return`. Only then enter the text of your journal entry. +Alternatively, `use an external editor `). You can also import an entry directly from a file -``` sh +```sh jrnl < my_entry.txt ``` @@ -52,51 +52,51 @@ jrnl < my_entry.txt Timestamps that work: - - at 6am - - yesterday - - last monday - - sunday at noon - - 2 march 2012 - - 7 apr - - 5/20/1998 at 23:42 +- at 6am +- yesterday +- last monday +- sunday at noon +- 2 march 2012 +- 7 apr +- 5/20/1998 at 23:42 ### Starring entries To mark an entry as a favourite, simply "star" it -``` sh +```sh jrnl last sunday *: Best day of my life. ``` If you don't want to add a date (ie. your entry will be dated as now), The following options are equivalent: - - `jrnl *: Best day of my life.` - - `jrnl *Best day of my life.` - - `jrnl Best day of my life.*` +- `jrnl *: Best day of my life.` +- `jrnl *Best day of my life.` +- `jrnl Best day of my life.*` !!! note - Just make sure that the asterisk sign is **not** surrounded by - whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the - reason being that the `*` sign has a special meaning on most shells). +Just make sure that the asterisk sign is **not** surrounded by +whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the +reason being that the `*` sign has a special meaning on most shells). ## Viewing -``` sh +```sh jrnl -n 10 ``` will list you the ten latest entries (if you're lazy, `jrnl -10` will do the same), -``` sh +```sh jrnl -from "last year" -until march ``` everything that happened from the start of last year to the start of last march. To only see your favourite entries, use -``` sh +```sh jrnl -starred ``` @@ -105,20 +105,20 @@ jrnl -starred Keep track of people, projects or locations, by tagging them with an `@` in your entries -``` sh +```sh jrnl Had a wonderful day on the @beach with @Tom and @Anna. ``` You can filter your journal entries just like this: -``` sh +```sh jrnl @pinkie @WorldDomination ``` Will print all entries in which either `@pinkie` or `@WorldDomination` occurred. -``` sh +```sh jrnl -n 5 -and @pineapple @lubricant ``` @@ -127,18 +127,18 @@ You can change which symbols you'd like to use for tagging in the configuration. !!! note - `jrnl @pinkie @WorldDomination` will switch to viewing mode because - although **no** command line arguments are given, all the input strings - look like tags - *jrnl* will assume you want to filter by tag. +`jrnl @pinkie @WorldDomination` will switch to viewing mode because +although **no** command line arguments are given, all the input strings +look like tags - _jrnl_ will assume you want to filter by tag. ## Editing older entries You can edit selected entries after you wrote them. This is particularly -useful when your journal file is encrypted or if you're using a DayOne -journal. To use this feature, you need to have an editor configured in -your journal configuration file (see `advanced usage `) +useful when your journal file is encrypted. To use this feature, you need +to have an editor configured in your journal configuration file (see +`advanced usage `) -``` sh +```sh jrnl -until 1950 @texas -and @history --edit ``` @@ -153,30 +153,8 @@ encrypt) your edited journal after you save and exit the editor. You can also use this feature for deleting entries from your journal -``` sh +```sh jrnl @girlfriend -until 'june 2012' --edit ``` Just select all text, press delete, and everything is gone... - -### Editing DayOne Journals - -DayOne journals can be edited exactly the same way, however the output -looks a little bit different because of the way DayOne stores its -entries: - -```md -# af8dbd0d43fb55458f11aad586ea2abf -2013-05-02 15:30 I told everyone I built my @robot wife for sex. -But late at night when we're alone we mostly play Battleship. - -# 2391048fe24111e1983ed49a20be6f9e -2013-08-10 03:22 I had all kinds of plans in case of a @zombie attack. -I just figured I'd be on the other side. -``` - -The long strings starting with hash symbol are the so-called UUIDs, -unique identifiers for each entry. Don't touch them. If you do, then the -old entry would get deleted and a new one written, which means that you -could lose DayOne data that jrnl can't handle (such as as the entry's -geolocation). diff --git a/features/encryption.feature b/features/encryption.feature index 82d971eb..d30c48b3 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -2,7 +2,7 @@ Scenario: Loading an encrypted journal Given we use the config "encrypted.yaml" When we run "jrnl -n 1" and enter "bad doggie no biscuit" - Then we should see the message "Password" + Then the output should contain "Password" and the output should contain "2013-06-10 15:40 Life is good" Scenario: Decrypting a journal @@ -14,16 +14,16 @@ Scenario: Encrypting a journal Given we use the config "basic.yaml" - When we run "jrnl --encrypt" and enter "swordfish" + When we run "jrnl --encrypt" and enter "swordfish" and "n" Then we should see the message "Journal encrypted" and the config for journal "default" should have "encrypt" set to "bool:True" When we run "jrnl -n 1" and enter "swordfish" - Then we should see the message "Password" + Then the output should contain "Password" and the output should contain "2013-06-10 15:40 Life is good" Scenario: Storing a password in Keychain Given we use the config "multiple.yaml" - When we run "jrnl simple --encrypt" and enter "sabertooth" + When we run "jrnl simple --encrypt" and enter "sabertooth" and "y" When we set the keychain password of "simple" to "sabertooth" Then the config for journal "simple" should have "encrypt" set to "bool:True" When we run "jrnl simple -n 1" diff --git a/features/environment.py b/features/environment.py index 6f9ac5df..7a918feb 100644 --- a/features/environment.py +++ b/features/environment.py @@ -1,25 +1,15 @@ -from behave import * import shutil import os -import jrnl -try: - from io import StringIO -except ImportError: - from cStringIO import StringIO + def before_scenario(context, scenario): """Before each scenario, backup all config and journal test data.""" - context.messages = StringIO() - jrnl.util.STDERR = context.messages - jrnl.util.TEST = True - # Clean up in case something went wrong for folder in ("configs", "journals"): working_dir = os.path.join("features", folder) if os.path.exists(working_dir): shutil.rmtree(working_dir) - for folder in ("configs", "journals"): original = os.path.join("features", "data", folder) working_dir = os.path.join("features", folder) @@ -32,10 +22,9 @@ def before_scenario(context, scenario): else: shutil.copy2(source, working_dir) + def after_scenario(context, scenario): """After each scenario, restore all test data and remove working_dirs.""" - context.messages.close() - context.messages = None for folder in ("configs", "journals"): working_dir = os.path.join("features", folder) if os.path.exists(working_dir): diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature index 1d4943ee..28587f96 100644 --- a/features/multiple_journals.feature +++ b/features/multiple_journals.feature @@ -42,5 +42,5 @@ Feature: Multiple journals Scenario: Don't crash if no file exists for a configured encrypted journal Given we use the config "multiple.yaml" - When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes" + When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes" and "y" Then we should see the message "Journal 'new_encrypted' created" diff --git a/features/steps/core.py b/features/steps/core.py index 83981d13..9217f77f 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -1,5 +1,4 @@ -from __future__ import unicode_literals -from __future__ import absolute_import +from unittest.mock import patch from behave import given, when, then from jrnl import cli, install, Journal, util, plugins @@ -10,10 +9,13 @@ import os import json import yaml import keyring +import tzlocal +import shlex +import sys class TestKeyring(keyring.backend.KeyringBackend): - """A test keyring that just stores its valies in a hash""" + """A test keyring that just stores its values in a hash""" priority = 1 keys = defaultdict(dict) @@ -31,15 +33,6 @@ class TestKeyring(keyring.backend.KeyringBackend): keyring.set_keyring(TestKeyring()) -try: - from io import StringIO -except ImportError: - from cStringIO import StringIO -import tzlocal -import shlex -import sys - - def ushlex(command): if sys.version_info[0] == 3: return shlex.split(command) @@ -73,18 +66,41 @@ def set_config(context, config_file): cf.write("version: {}".format(__version__)) +def _mock_getpass(inputs): + def prompt_return(prompt="Password: "): + print(prompt) + return next(inputs) + return prompt_return + + +def _mock_input(inputs): + def prompt_return(prompt=""): + val = next(inputs) + print(prompt, val) + return val + return prompt_return + + @when('we run "{command}" and enter') -@when('we run "{command}" and enter "{inputs}"') -def run_with_input(context, command, inputs=None): - text = inputs or context.text +@when('we run "{command}" and enter "{inputs1}"') +@when('we run "{command}" and enter "{inputs1}" and "{inputs2}"') +def run_with_input(context, command, inputs1="", inputs2=""): + # create an iterator through all inputs. These inputs will be fed one by one + # to the mocked calls for 'input()', 'util.getpass()' and 'sys.stdin.read()' + text = iter((inputs1, inputs2)) if inputs1 else iter(context.text.split("\n")) args = ushlex(command)[1:] - buffer = StringIO(text.strip()) - util.STDIN = buffer - try: - cli.run(args or []) - context.exit_status = 0 - except SystemExit as e: - context.exit_status = e.code + with patch("builtins.input", side_effect=_mock_input(text)) as mock_input: + with patch("jrnl.util.getpass", side_effect=_mock_getpass(text)) as mock_getpass: + with patch("sys.stdin.read", side_effect=text) as mock_read: + try: + cli.run(args or []) + context.exit_status = 0 + except SystemExit as e: + context.exit_status = e.code + + # assert at least one of the mocked input methods got called + assert mock_input.called or mock_getpass.called or mock_read.called + @when('we run "{command}"') @@ -190,28 +206,24 @@ def check_output_time_inline(context, text): def check_output_inline(context, text=None): text = text or context.text out = context.stdout_capture.getvalue() - if isinstance(out, bytes): - out = out.decode('utf-8') assert text in out, text @then('the output should not contain "{text}"') def check_output_not_inline(context, text): out = context.stdout_capture.getvalue() - if isinstance(out, bytes): - out = out.decode('utf-8') assert text not in out @then('we should see the message "{text}"') def check_message(context, text): - out = context.messages.getvalue() + out = context.stderr_capture.getvalue() assert text in out, [text, out] @then('we should not see the message "{text}"') def check_not_message(context, text): - out = context.messages.getvalue() + out = context.stderr_capture.getvalue() assert text not in out, [text, out] diff --git a/features/upgrade.feature b/features/upgrade.feature index fd8c1bd4..ef597d4f 100644 --- a/features/upgrade.feature +++ b/features/upgrade.feature @@ -13,14 +13,14 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x Scenario: Upgrading a journal encrypted with jrnl 1.x Given we use the config "encrypted_old.json" - When we run "jrnl -n 1" and enter + When we run "jrnl -n 1" and enter """ Y bad doggie no biscuit bad doggie no biscuit """ - Then we should see the message "Password" - And the output should contain "2013-06-10 15:40 Life is good" + Then the output should contain "Password" + and the output should contain "2013-06-10 15:40 Life is good" Scenario: Upgrade and parse journals with little endian date format Given we use the config "upgrade_from_195_little_endian_dates.json" diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py index 9e988f78..59314c4b 100644 --- a/jrnl/DayOneJournal.py +++ b/jrnl/DayOneJournal.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import, unicode_literals from . import Entry from . import Journal from . import time as jrnl_time @@ -26,7 +24,7 @@ class DayOne(Journal.Journal): def __init__(self, **kwargs): self.entries = [] self._deleted_entries = [] - super(DayOne, self).__init__(**kwargs) + super().__init__(**kwargs) def open(self): filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))] @@ -83,7 +81,7 @@ class DayOne(Journal.Journal): def editable_str(self): """Turns the journal into a string of entries that can be edited manually and later be parsed with eslf.parse_editable_str.""" - return "\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries]) + return "\n".join([f"# {e.uuid}\n{str(e)}" for e in self.entries]) def parse_editable_str(self, edited): """Parses the output of self.editable_str and updates its entries.""" @@ -107,7 +105,7 @@ class DayOne(Journal.Journal): current_entry.modified = False current_entry.uuid = m.group(1).lower() else: - date_blob_re = re.compile("^\[[^\\]]+\] ") + date_blob_re = re.compile("^\\[[^\\]]+\\] ") date_blob = date_blob_re.findall(line) if date_blob: date_blob = date_blob[0] diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index a83651e4..1cce66b8 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -8,14 +8,13 @@ from cryptography.hazmat.backends import default_backend import sys import os import base64 -import getpass import logging log = logging.getLogger() def make_key(password): - password = util.bytes(password) + password = password.encode("utf-8") kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, @@ -30,7 +29,7 @@ def make_key(password): class EncryptedJournal(Journal.Journal): def __init__(self, name='default', **kwargs): - super(EncryptedJournal, self).__init__(name, **kwargs) + super().__init__(name, **kwargs) self.config['encrypt'] = True def open(self, filename=None): @@ -48,9 +47,9 @@ class EncryptedJournal(Journal.Journal): self.config['password'] = password text = "" self._store(filename, text) - util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename)) + print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) else: - util.prompt("No password supplied for encrypted journal") + print("No password supplied for encrypted journal", file=sys.stderr) sys.exit(1) else: text = self._load(filename) @@ -59,7 +58,6 @@ class EncryptedJournal(Journal.Journal): log.debug("opened %s with %d entries", self.__class__.__name__, len(self)) return self - def _load(self, filename, password=None): """Loads an encrypted journal from a file and tries to decrypt it. If password is not provided, will look for password in the keychain @@ -99,7 +97,7 @@ class LegacyEncryptedJournal(Journal.LegacyJournal): """Legacy class to support opening journals encrypted with the jrnl 1.x standard. You'll not be able to save these journals anymore.""" def __init__(self, name='default', **kwargs): - super(LegacyEncryptedJournal, self).__init__(name, **kwargs) + super().__init__(name, **kwargs) self.config['encrypt'] = True def _load(self, filename, password=None): diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 1306cef5..ca80b231 100755 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import unicode_literals import re import textwrap from datetime import datetime @@ -51,14 +49,14 @@ class Entry: @staticmethod def tag_regex(tagsymbols): - pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols) - return re.compile(pattern, re.UNICODE) + pattern = fr'(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)' + return re.compile(pattern) def _parse_tags(self): tagsymbols = self.journal.config['tagsymbols'] - return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)) + return {tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)} - def __unicode__(self): + def __str__(self): """Returns a string representation of the entry to be written into a journal file.""" date_str = self.date.strftime(self.journal.config['timeformat']) title = "[{}] {}".format(date_str, self.title.rstrip("\n ")) @@ -106,7 +104,7 @@ class Entry: ) def __repr__(self): - return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) + return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) def __hash__(self): return hash(self.__repr__()) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 83f3b1a2..f6813222 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -1,13 +1,10 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import, unicode_literals from . import Entry from . import util from . import time import os import sys -import codecs import re from datetime import datetime import logging @@ -15,7 +12,7 @@ import logging log = logging.getLogger(__name__) -class Tag(object): +class Tag: def __init__(self, name, count=0): self.name = name self.count = count @@ -24,10 +21,10 @@ class Tag(object): return self.name def __repr__(self): - return "".format(self.name) + return f"" -class Journal(object): +class Journal: def __init__(self, name='default', **kwargs): self.config = { 'journal': "journal.txt", @@ -72,7 +69,7 @@ class Journal(object): filename = filename or self.config['journal'] if not os.path.exists(filename): - util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename)) + print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr) self._create(filename) text = self._load(filename) @@ -96,7 +93,7 @@ class Journal(object): return True def _to_text(self): - return "\n".join([e.__unicode__() for e in self.entries]) + return "\n".join([str(e) for e in self.entries]) def _load(self, filename): raise NotImplementedError @@ -118,7 +115,7 @@ class Journal(object): # Initialise our current entry entries = [] - date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ") + date_blob_re = re.compile("(?:^|\n)\\[([^\\]]+)\\] ") last_entry_pos = 0 for match in date_blob_re.finditer(journal_txt): date_blob = match.groups()[0] @@ -144,9 +141,6 @@ class Journal(object): entry._parse_text() return entries - def __unicode__(self): - return self.pprint() - def pprint(self, short=False): """Prettyprints the journal's entries""" sep = "\n" @@ -157,7 +151,7 @@ class Journal(object): tagre = re.compile(re.escape(tag), re.IGNORECASE) pp = re.sub(tagre, lambda match: util.colorize(match.group(0)), - pp, re.UNICODE) + pp) else: pp = re.sub( Entry.Entry.tag_regex(self.config['tagsymbols']), @@ -166,8 +160,11 @@ class Journal(object): ) return pp + def __str__(self): + return self.pprint() + def __repr__(self): - return "".format(len(self.entries)) + return f"" def sort(self): """Sorts the Journal's entries by date""" @@ -187,7 +184,7 @@ class Journal(object): 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] - tag_counts = set([(tags.count(tag), tag) for tag in tags]) + tag_counts = {(tags.count(tag), tag) for tag in tags} return [Tag(tag, count=count) for count, tag in sorted(tag_counts)] def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, exclude=[]): @@ -204,8 +201,8 @@ class Journal(object): exclude is a list of the tags which should not appear in the results. entry is kept if any tag is present, unless they appear in exclude.""" - self.search_tags = set([tag.lower() for tag in tags]) - excluded_tags = set([tag.lower() for tag in exclude]) + self.search_tags = {tag.lower() for tag in tags} + excluded_tags = {tag.lower() for tag in exclude} end_date = time.parse(end_date, inclusive=True) start_date = time.parse(start_date) @@ -230,7 +227,7 @@ class Journal(object): raw = raw.replace('\\n ', '\n').replace('\\n', '\n') starred = False # Split raw text into title and body - sep = re.search("\n|[\?!.]+ +\n?", raw) + sep = re.search(r"\n|[?!.]+ +\n?", raw) first_line = raw[:sep.end()].strip() if sep else raw starred = False @@ -258,7 +255,7 @@ class Journal(object): def editable_str(self): """Turns the journal into a string of entries that can be edited manually and later be parsed with eslf.parse_editable_str.""" - return "\n".join([e.__unicode__() for e in self.entries]) + return "\n".join([str(e) for e in self.entries]) def parse_editable_str(self, edited): """Parses the output of self.editable_str and updates it's entries.""" @@ -274,15 +271,15 @@ class Journal(object): class PlainJournal(Journal): @classmethod def _create(cls, filename): - with codecs.open(filename, "a", "utf-8"): + with open(filename, "a", encoding="utf-8"): pass def _load(self, filename): - with codecs.open(filename, "r", "utf-8") as f: + with open(filename, "r", encoding="utf-8") as f: return f.read() def _store(self, filename, text): - with codecs.open(filename, 'w', "utf-8") as f: + with open(filename, 'w', encoding="utf-8") as f: f.write(text) @@ -291,7 +288,7 @@ class LegacyJournal(Journal): standard. Main difference here is that in 1.x, timestamps were not cuddled by square brackets. You'll not be able to save these journals anymore.""" def _load(self, filename): - with codecs.open(filename, "r", "utf-8") as f: + with open(filename, "r", encoding="utf-8") as f: return f.read() def _parse(self, journal_txt): @@ -327,7 +324,7 @@ class LegacyJournal(Journal): # escaping for the new format). line = new_date_format_regex.sub(r' \1', line) if current_entry: - current_entry.text += line + u"\n" + current_entry.text += line + "\n" # Append last entry if current_entry: @@ -351,8 +348,9 @@ def open_journal(name, config, legacy=False): from . import DayOneJournal return DayOneJournal.DayOne(**config).open() else: - util.prompt( - u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal']) + print( + f"[Error: {config['journal']} is a directory, but doesn't seem to be a DayOne journal either.", + file=sys.stderr ) sys.exit(1) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 57664dbb..1905b195 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# encoding: utf-8 import pkg_resources diff --git a/jrnl/__main__.py b/jrnl/__main__.py index 73e08b33..3ce86730 100644 --- a/jrnl/__main__.py +++ b/jrnl/__main__.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import, unicode_literals from . import cli diff --git a/jrnl/cli.py b/jrnl/cli.py index 65a53516..3cf67030 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# encoding: utf-8 """ jrnl @@ -7,8 +6,6 @@ license: MIT, see LICENSE for more details. """ -from __future__ import unicode_literals -from __future__ import absolute_import from . import Journal from . import util from . import install @@ -91,7 +88,7 @@ def encrypt(journal, filename=None): if util.yesno("Do you want to store the password in your keychain?", default=True): util.set_keychain(journal.name, journal.config['password']) - util.prompt("Journal encrypted to {0}.".format(filename or new_journal.config['journal'])) + print("Journal encrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr) def decrypt(journal, filename=None): @@ -102,12 +99,12 @@ def decrypt(journal, filename=None): new_journal = Journal.PlainJournal(filename, **journal.config) new_journal.entries = journal.entries new_journal.write(filename) - util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal'])) + print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr) def list_journals(config): """List the journals specified in the configuration file""" - result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH) + result = f"Journals defined in {install.CONFIG_FILE_PATH}\n" ml = min(max(len(k) for k in config['journals']), 20) for journal, cfg in config['journals'].items(): result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg) @@ -138,20 +135,19 @@ def configure_logger(debug=False): def run(manual_args=None): args = parse_args(manual_args) configure_logger(args.debug) - args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text] if args.version: - version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__) - print(util.py2encode(version_str)) + version_str = f"{jrnl.__title__} version {jrnl.__version__}" + print(version_str) sys.exit(0) try: config = install.load_or_install_jrnl() except UserAbort as err: - util.prompt("\n{}".format(err)) + print(f"\n{err}", file=sys.stderr) sys.exit(1) if args.ls: - util.prnt(list_journals(config)) + print(list_journals(config)) sys.exit(0) log.debug('Using configuration "%s"', config) @@ -161,11 +157,11 @@ def run(manual_args=None): # use this! journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default' - if journal_name is not 'default': + if journal_name != 'default': args.text = args.text[1:] elif "default" not in config['journals']: - util.prompt("No default journal configured.") - util.prompt(list_journals(config)) + print("No default journal configured.", file=sys.stderr) + print(list_journals(config), file=sys.stderr) sys.exit(1) config = util.scope_config(config, journal_name) @@ -175,12 +171,12 @@ def run(manual_args=None): try: args.limit = int(args.text[0].lstrip("-")) args.text = args.text[1:] - except: + except ValueError: pass log.debug('Using journal "%s"', journal_name) mode_compose, mode_export, mode_import = guess_mode(args, config) - + # How to quit writing? if "win32" in sys.platform: _exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter" @@ -190,21 +186,22 @@ def run(manual_args=None): if mode_compose and not args.text: if not sys.stdin.isatty(): # Piping data into jrnl - raw = util.py23_read() + raw = sys.stdin.read() elif config['editor']: template = "" if config['template']: try: template = open(config['template']).read() - except: - util.prompt("[Could not read template at '']".format(config['template'])) + except OSError: + print(f"[Could not read template at '{config['template']}']", file=sys.stderr) sys.exit(1) raw = util.get_text_from_editor(config, template) else: try: - raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n") + print("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", file=sys.stderr) + raw = sys.stdin.read() except KeyboardInterrupt: - util.prompt("[Entry NOT saved to journal.]") + print("[Entry NOT saved to journal.]", file=sys.stderr) sys.exit(0) if raw: args.text = [raw] @@ -215,7 +212,7 @@ def run(manual_args=None): try: journal = Journal.open_journal(journal_name, config) except KeyboardInterrupt: - util.prompt("[Interrupted while opening journal]".format(journal_name)) + print(f"[Interrupted while opening journal]", file=sys.stderr) sys.exit(1) # Import mode @@ -225,11 +222,9 @@ def run(manual_args=None): # Writing mode elif mode_compose: raw = " ".join(args.text).strip() - if util.PY2 and type(raw) is not unicode: - raw = raw.decode(sys.getfilesystemencoding()) log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name) journal.new_entry(raw) - util.prompt("[Entry added to {0} journal]".format(journal_name)) + print(f"[Entry added to {journal_name} journal]", file=sys.stderr) journal.write() if not mode_compose: @@ -246,14 +241,14 @@ def run(manual_args=None): # Reading mode if not mode_compose and not mode_export and not mode_import: - print(util.py2encode(journal.pprint())) + print(journal.pprint()) # Various export modes elif args.short: - print(util.py2encode(journal.pprint(short=True))) + print(journal.pprint(short=True)) elif args.tags: - print(util.py2encode(plugins.get_exporter("tags").export(journal))) + print(plugins.get_exporter("tags").export(journal)) elif args.export is not False: exporter = plugins.get_exporter(args.export) @@ -275,7 +270,8 @@ def run(manual_args=None): elif args.edit: if not config['editor']: - util.prompt("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR)) + print("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]" + .format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR), file=sys.stderr) sys.exit(1) other_entries = [e for e in old_entries if e not in journal.entries] # Edit @@ -286,11 +282,11 @@ def run(manual_args=None): num_edited = len([e for e in journal.entries if e.modified]) prompts = [] if num_deleted: - prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries")) + prompts.append("{} {} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries")) if num_edited: - prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries")) + prompts.append("{} {} modified".format(num_edited, "entry" if num_deleted == 1 else "entries")) if prompts: - util.prompt("[{0}]".format(", ".join(prompts).capitalize())) + print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr) journal.entries += other_entries journal.sort() journal.write() diff --git a/jrnl/export.py b/jrnl/export.py index d4873314..1ee4e6ff 100644 --- a/jrnl/export.py +++ b/jrnl/export.py @@ -1,15 +1,12 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import, unicode_literals from .util import ERROR_COLOR, RESET_COLOR -from .util import slugify, u -from .template import Template +from .util import slugify +from .plugins.template import Template import os -import codecs -class Exporter(object): +class Exporter: """This Exporter can convert entries and journals into text files.""" def __init__(self, format): with open("jrnl/templates/" + format + ".template") as f: @@ -17,8 +14,8 @@ class Exporter(object): self.template = Template(body) def export_entry(self, entry): - """Returns a unicode representation of a single entry.""" - return entry.__unicode__() + """Returns a string representation of a single entry.""" + return str(entry) def _get_vars(self, journal): return { @@ -28,36 +25,36 @@ class Exporter(object): } def export_journal(self, journal): - """Returns a unicode representation of an entire journal.""" + """Returns a string representation of an entire journal.""" return self.template.render_block("journal", **self._get_vars(journal)) def write_file(self, journal, path): """Exports a journal into a single file.""" try: - with codecs.open(path, "w", "utf-8") as f: + with open(path, "w", encoding="utf-8") as f: f.write(self.export_journal(journal)) - return "[Journal exported to {0}]".format(path) - except IOError as e: - return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) + return f"[Journal exported to {path}]" + except OSError as e: + return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]" def make_filename(self, entry): - return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), self.extension)) + return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(entry.title), self.extension)) def write_files(self, journal, path): """Exports a journal into individual files for each entry.""" for entry in journal.entries: try: full_path = os.path.join(path, self.make_filename(entry)) - with codecs.open(full_path, "w", "utf-8") as f: + with open(full_path, "w", encoding="utf-8") as f: f.write(self.export_entry(entry)) - except IOError as e: - return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) - return "[Journal exported to {0}]".format(path) + except OSError as e: + return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]" + return f"[Journal exported to {path}]" def export(self, journal, format="text", output=None): """Exports to individual files if output is an existing path, or into a single file if output is a file name, or returns the exporter's - representation as unicode if output is None.""" + representation as string if output is None.""" if output and os.path.isdir(output): # multiple files return self.write_files(journal, output) elif output: # single file diff --git a/jrnl/install.py b/jrnl/install.py index 5a80562f..c104b46b 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# encoding: utf-8 -from __future__ import absolute_import import readline import glob import getpass @@ -69,7 +67,7 @@ def upgrade_config(config): for key in missing_keys: config[key] = default_config[key] save_config(config) - print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH)) + print(f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", file=sys.stderr) def save_config(config): @@ -91,10 +89,10 @@ def load_or_install_jrnl(): try: upgrade.upgrade_jrnl_if_necessary(config_path) except upgrade.UpgradeValidationException: - util.prompt("Aborting upgrade.") - util.prompt("Please tell us about this problem at the following URL:") - util.prompt("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException") - util.prompt("Exiting.") + print("Aborting upgrade.", file=sys.stderr) + print("Please tell us about this problem at the following URL:", file=sys.stderr) + print("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException", file=sys.stderr) + print("Exiting.", file=sys.stderr) sys.exit(1) upgrade_config(config) @@ -120,8 +118,8 @@ def install(): readline.set_completer(autocomplete) # Where to create the journal? - path_query = 'Path to your journal file (leave blank for {}): '.format(JOURNAL_FILE_PATH) - journal_path = util.py23_input(path_query).strip() or JOURNAL_FILE_PATH + path_query = f'Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): ' + journal_path = input(path_query).strip() or JOURNAL_FILE_PATH default_config['journals']['default'] = os.path.expanduser(os.path.expandvars(journal_path)) path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it @@ -139,7 +137,7 @@ def install(): else: util.set_keychain("default", None) EncryptedJournal._create(default_config['journals']['default'], password) - print("Journal will be encrypted.") + print("Journal will be encrypted.", file=sys.stderr) else: PlainJournal._create(default_config['journals']['default']) diff --git a/jrnl/plugins/__init__.py b/jrnl/plugins/__init__.py index 64d7b3ba..8d59b556 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -1,8 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals - from .text_exporter import TextExporter from .jrnl_importer import JRNLImporter from .json_exporter import JSONExporter @@ -15,8 +13,8 @@ from .template_exporter import __all__ as template_exporters __exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters __importers =[JRNLImporter] -__exporter_types = dict([(name, plugin) for plugin in __exporters for name in plugin.names]) -__importer_types = dict([(name, plugin) for plugin in __importers for name in plugin.names]) +__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names} +__importer_types = {name: plugin for plugin in __importers for name in plugin.names} EXPORT_FORMATS = sorted(__exporter_types.keys()) IMPORT_FORMATS = sorted(__importer_types.keys()) diff --git a/jrnl/plugins/jrnl_importer.py b/jrnl/plugins/jrnl_importer.py index 85615e75..83341cd9 100644 --- a/jrnl/plugins/jrnl_importer.py +++ b/jrnl/plugins/jrnl_importer.py @@ -1,12 +1,10 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals -import codecs import sys from .. import util -class JRNLImporter(object): +class JRNLImporter: """This plugin imports entries from other jrnl files.""" names = ["jrnl"] @@ -17,15 +15,15 @@ class JRNLImporter(object): old_cnt = len(journal.entries) old_entries = journal.entries if input: - with codecs.open(input, "r", "utf-8") as f: + with open(input, "r", encoding="utf-8") as f: other_journal_txt = f.read() else: try: - other_journal_txt = util.py23_read() + other_journal_txt = sys.stdin.read() except KeyboardInterrupt: - util.prompt("[Entries NOT imported into journal.]") + print("[Entries NOT imported into journal.]", file=sys.stderr) sys.exit(0) journal.import_(other_journal_txt) new_cnt = len(journal.entries) - util.prompt("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name)) + print("[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr) journal.write() diff --git a/jrnl/plugins/json_exporter.py b/jrnl/plugins/json_exporter.py index 5abaf916..e368a300 100644 --- a/jrnl/plugins/json_exporter.py +++ b/jrnl/plugins/json_exporter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals from .text_exporter import TextExporter import json from .util import get_tags_count @@ -35,7 +34,7 @@ class JSONExporter(TextExporter): """Returns a json representation of an entire journal.""" tags = get_tags_count(journal) result = { - "tags": dict((tag, count) for count, tag in tags), + "tags": {tag: count for count, tag in tags}, "entries": [cls.entry_to_dict(e) for e in journal.entries] } return json.dumps(result, indent=2) diff --git a/jrnl/plugins/markdown_exporter.py b/jrnl/plugins/markdown_exporter.py index 19b5404d..da2b5748 100644 --- a/jrnl/plugins/markdown_exporter.py +++ b/jrnl/plugins/markdown_exporter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals, print_function from .text_exporter import TextExporter import os import re @@ -51,15 +50,11 @@ class MarkdownExporter(TextExporter): newbody = newbody + previous_line # add very last line if warn_on_heading_level is True: - print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr) + print(f"{WARNING_COLOR}WARNING{RESET_COLOR}: " + f"Headings increased past H6 on export - {date_str} {entry.title}", + file=sys.stderr) - return "{md} {date} {title}\n{body} {space}".format( - md=heading, - date=date_str, - title=entry.title, - body=newbody, - space="" - ) + return f"{heading} {date_str} {entry.title}\n{newbody} " @classmethod def export_journal(cls, journal): diff --git a/jrnl/plugins/tag_exporter.py b/jrnl/plugins/tag_exporter.py index 439bac7c..f5453ced 100644 --- a/jrnl/plugins/tag_exporter.py +++ b/jrnl/plugins/tag_exporter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals from .text_exporter import TextExporter from .util import get_tags_count @@ -26,5 +25,5 @@ class TagExporter(TextExporter): elif min(tag_counts)[0] == 0: tag_counts = filter(lambda x: x[0] > 1, tag_counts) result += '[Removed tags that appear only once.]\n' - result += "\n".join("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) + result += "\n".join("{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) return result diff --git a/jrnl/plugins/template.py b/jrnl/plugins/template.py index 21fb2896..7f72e2f8 100644 --- a/jrnl/plugins/template.py +++ b/jrnl/plugins/template.py @@ -13,7 +13,7 @@ BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}" INCLUDE_RE = r"{% *include +(.+?) *%}" -class Template(object): +class Template: def __init__(self, template): self.template = template self.clean_template = None diff --git a/jrnl/plugins/template_exporter.py b/jrnl/plugins/template_exporter.py index 85aa2236..f15328f2 100644 --- a/jrnl/plugins/template_exporter.py +++ b/jrnl/plugins/template_exporter.py @@ -1,8 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals - from .text_exporter import TextExporter from .template import Template import os @@ -14,7 +12,7 @@ class GenericTemplateExporter(TextExporter): @classmethod def export_entry(cls, entry): - """Returns a unicode representation of a single entry.""" + """Returns a string representation of a single entry.""" vars = { 'entry': entry, 'tags': entry.tags @@ -23,7 +21,7 @@ class GenericTemplateExporter(TextExporter): @classmethod def export_journal(cls, journal): - """Returns a unicode representation of an entire journal.""" + """Returns a string representation of an entire journal.""" vars = { 'journal': journal, 'entries': journal.entries, @@ -36,7 +34,7 @@ def __exporter_from_file(template_file): """Create a template class from a file""" name = os.path.basename(template_file).replace(".template", "") template = Template.from_file(template_file) - return type(str("{}Exporter".format(name.title())), (GenericTemplateExporter, ), { + return type(str(f"{name.title()}Exporter"), (GenericTemplateExporter, ), { "names": [name], "extension": template.extension, "template": template diff --git a/jrnl/plugins/text_exporter.py b/jrnl/plugins/text_exporter.py index dbb54d04..ce2e71de 100644 --- a/jrnl/plugins/text_exporter.py +++ b/jrnl/plugins/text_exporter.py @@ -1,41 +1,39 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals -import codecs -from ..util import u, slugify +from ..util import slugify import os from ..util import ERROR_COLOR, RESET_COLOR -class TextExporter(object): +class TextExporter: """This Exporter can convert entries and journals into text files.""" names = ["text", "txt"] extension = "txt" @classmethod def export_entry(cls, entry): - """Returns a unicode representation of a single entry.""" - return entry.__unicode__() + """Returns a string representation of a single entry.""" + return str(entry) @classmethod def export_journal(cls, journal): - """Returns a unicode representation of an entire journal.""" + """Returns a string representation of an entire journal.""" return "\n".join(cls.export_entry(entry) for entry in journal) @classmethod def write_file(cls, journal, path): """Exports a journal into a single file.""" try: - with codecs.open(path, "w", "utf-8") as f: + with open(path, "w", encoding="utf-8") as f: f.write(cls.export_journal(journal)) - return "[Journal exported to {0}]".format(path) + return f"[Journal exported to {path}]" except IOError as e: - return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) + return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]" @classmethod def make_filename(cls, entry): - return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension)) + return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension)) @classmethod def write_files(cls, journal, path): @@ -43,17 +41,17 @@ class TextExporter(object): for entry in journal.entries: try: full_path = os.path.join(path, cls.make_filename(entry)) - with codecs.open(full_path, "w", "utf-8") as f: + with open(full_path, "w", encoding="utf-8") as f: f.write(cls.export_entry(entry)) except IOError as e: return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR) - return "[Journal exported to {0}]".format(path) + return "[Journal exported to {}]".format(path) @classmethod def export(cls, journal, output=None): """Exports to individual files if output is an existing path, or into a single file if output is a file name, or returns the exporter's - representation as unicode if output is None.""" + representation as string if output is None.""" if output and os.path.isdir(output): # multiple files return cls.write_files(journal, output) elif output: # single file diff --git a/jrnl/plugins/util.py b/jrnl/plugins/util.py index 0a642cb2..a056b19a 100644 --- a/jrnl/plugins/util.py +++ b/jrnl/plugins/util.py @@ -10,7 +10,7 @@ def get_tags_count(journal): 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] - tag_counts = set([(tags.count(tag), tag) for tag in tags]) + tag_counts = {(tags.count(tag), tag) for tag in tags} return tag_counts diff --git a/jrnl/plugins/xml_exporter.py b/jrnl/plugins/xml_exporter.py index 0af2ed47..2783663b 100644 --- a/jrnl/plugins/xml_exporter.py +++ b/jrnl/plugins/xml_exporter.py @@ -1,10 +1,8 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals from .json_exporter import JSONExporter from .util import get_tags_count -from ..util import u from xml.dom import minidom @@ -20,7 +18,7 @@ class XMLExporter(JSONExporter): entry_el = doc_el.createElement('entry') for key, value in cls.entry_to_dict(entry).items(): elem = doc_el.createElement(key) - elem.appendChild(doc_el.createTextNode(u(value))) + elem.appendChild(doc_el.createTextNode(value)) entry_el.appendChild(elem) if not doc: doc_el.appendChild(entry_el) @@ -33,8 +31,8 @@ class XMLExporter(JSONExporter): entry_el = doc.createElement('entry') entry_el.setAttribute('date', entry.date.isoformat()) if hasattr(entry, "uuid"): - entry_el.setAttribute('uuid', u(entry.uuid)) - entry_el.setAttribute('starred', u(entry.starred)) + entry_el.setAttribute('uuid', entry.uuid) + entry_el.setAttribute('starred', entry.starred) entry_el.appendChild(doc.createTextNode(entry.fulltext)) return entry_el @@ -49,7 +47,7 @@ class XMLExporter(JSONExporter): for count, tag in tags: tag_el = doc.createElement('tag') tag_el.setAttribute('name', tag) - count_node = doc.createTextNode(u(count)) + count_node = doc.createTextNode(str(count)) tag_el.appendChild(count_node) tags_el.appendChild(tag_el) for entry in journal.entries: diff --git a/jrnl/plugins/yaml_exporter.py b/jrnl/plugins/yaml_exporter.py index c0735811..4a75667f 100644 --- a/jrnl/plugins/yaml_exporter.py +++ b/jrnl/plugins/yaml_exporter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals, print_function from .text_exporter import TextExporter import os import re @@ -18,7 +17,8 @@ class YAMLExporter(TextExporter): def export_entry(cls, entry, to_multifile=True): """Returns a markdown representation of a single entry, with YAML front matter.""" if to_multifile is False: - print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format("\033[31m", "\033[0m", file=sys.stderr)) + print("{}ERROR{}: YAML export must be to individual files. " + "Please specify a directory to export to.".format("\033[31m", "\033[0m"), file=sys.stderr) return date_str = entry.date.strftime(entry.journal.config['timeformat']) @@ -27,7 +27,7 @@ class YAMLExporter(TextExporter): tagsymbols = entry.journal.config['tagsymbols'] # see also Entry.Entry.rag_regex - multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols), re.UNICODE) + multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols)) '''Increase heading levels in body text''' newbody = '' diff --git a/jrnl/time.py b/jrnl/time.py index 9ff125aa..eee3fc20 100644 --- a/jrnl/time.py +++ b/jrnl/time.py @@ -51,7 +51,7 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None): except TypeError: return None - if flag is 1: # Date found, but no time. Use the default time. + if flag == 1: # Date found, but no time. Use the default time. date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0) else: date = datetime(*date[:6]) diff --git a/jrnl/upgrade.py b/jrnl/upgrade.py index f2b80af3..8f67aa34 100644 --- a/jrnl/upgrade.py +++ b/jrnl/upgrade.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +import sys from . import __version__ from . import Journal @@ -6,11 +6,10 @@ from . import util from .EncryptedJournal import EncryptedJournal from .util import UserAbort import os -import codecs def backup(filename, binary=False): - util.prompt(" Created a backup at {}.backup".format(filename)) + print(f" Created a backup at {filename}.backup", file=sys.stderr) filename = os.path.expanduser(os.path.expandvars(filename)) with open(filename, 'rb' if binary else 'r') as original: contents = original.read() @@ -19,14 +18,14 @@ def backup(filename, binary=False): def upgrade_jrnl_if_necessary(config_path): - with codecs.open(config_path, "r", "utf-8") as f: + with open(config_path, "r", encoding="utf-8") as f: config_file = f.read() if not config_file.strip().startswith("{"): return config = util.load_config(config_path) - util.prompt("""Welcome to jrnl {}. + print("""Welcome to jrnl {}. It looks like you've been using an older version of jrnl until now. That's okay - jrnl will now upgrade your configuration and journal files. Afterwards @@ -54,6 +53,8 @@ older versions of jrnl anymore. encrypt = config.get('encrypt') path = journal_conf + path = os.path.expanduser(path) + if encrypt: encrypted_journals[journal_name] = path elif os.path.isdir(path): @@ -63,19 +64,19 @@ older versions of jrnl anymore. longest_journal_name = max([len(journal) for journal in config['journals']]) if encrypted_journals: - util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__)) + print(f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", file=sys.stderr) for journal, path in encrypted_journals.items(): - util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) + print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) if plain_journals: - util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__)) + print(f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", file=sys.stderr) for journal, path in plain_journals.items(): - util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) + print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) if other_journals: - util.prompt("\nFollowing journals will be not be touched:") + print("\nFollowing journals will be not be touched:", file=sys.stderr) for journal, path in other_journals.items(): - util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name)) + print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr) try: cont = util.yesno("\nContinue upgrading jrnl?", default=False) @@ -85,13 +86,13 @@ older versions of jrnl anymore. raise UserAbort("jrnl NOT upgraded, exiting.") for journal_name, path in encrypted_journals.items(): - util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path)) + print(f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...", file=sys.stderr) backup(path, binary=True) old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True) all_journals.append(EncryptedJournal.from_journal(old_journal)) for journal_name, path in plain_journals.items(): - util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path)) + print(f"\nUpgrading plain text '{journal_name}' journal stored in {path}...", file=sys.stderr) backup(path) old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True) all_journals.append(Journal.PlainJournal.from_journal(old_journal)) @@ -100,8 +101,9 @@ older versions of jrnl anymore. failed_journals = [j for j in all_journals if not j.validate_parsing()] if len(failed_journals) > 0: - util.prompt("\nThe following journal{} failed to upgrade:\n{}".format( - 's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)) + print("\nThe following journal{} failed to upgrade:\n{}".format( + 's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)), + file=sys.stderr ) raise UpgradeValidationException @@ -110,10 +112,11 @@ older versions of jrnl anymore. for j in all_journals: j.write() - util.prompt("\nUpgrading config...") + print("\nUpgrading config...", file=sys.stderr) backup(config_path) - util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path)) + print("\nWe're all done here and you can start enjoying jrnl 2.", file=sys.stderr) + class UpgradeValidationException(Exception): """Raised when the contents of an upgraded journal do not match the old journal""" diff --git a/jrnl/util.py b/jrnl/util.py index bc36ba9b..959d63a1 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -1,8 +1,4 @@ #!/usr/bin/env python -# encoding: utf-8 - -from __future__ import unicode_literals -from __future__ import absolute_import import sys import os @@ -14,22 +10,12 @@ if "win32" in sys.platform: import re import tempfile import subprocess -import codecs import unicodedata import shlex import logging log = logging.getLogger(__name__) - -PY3 = sys.version_info[0] == 3 -PY2 = sys.version_info[0] == 2 -STDIN = sys.stdin -STDERR = sys.stderr -STDOUT = sys.stdout -TEST = False -__cached_tz = None - WARNING_COLOR = "\033[33m" ERROR_COLOR = "\033[31m" RESET_COLOR = "\033[0m" @@ -44,18 +30,14 @@ SENTENCE_SPLITTER = re.compile(r""" \s+ # a sequence of required spaces. | # Otherwise, \n # a sentence also terminates newlines. -)""", re.UNICODE | re.VERBOSE) +)""", re.VERBOSE) class UserAbort(Exception): pass -def getpass(prompt="Password: "): - if not TEST: - return gp.getpass(bytes(prompt)) - else: - return py23_input(prompt) +getpass = gp.getpass def get_password(validator, keychain=None, max_attempts=3): @@ -67,14 +49,14 @@ def get_password(validator, keychain=None, max_attempts=3): set_keychain(keychain, None) attempt = 1 while result is None and attempt < max_attempts: - prompt("Wrong password, try again.") - password = getpass() + print("Wrong password, try again.", file=sys.stderr) + password = gp.getpass() result = validator(password) attempt += 1 if result is not None: return result else: - prompt("Extremely wrong password.") + print("Extremely wrong password.", file=sys.stderr) sys.exit(1) @@ -88,57 +70,16 @@ def set_keychain(journal_name, password): if password is None: try: keyring.delete_password('jrnl', journal_name) - except: + except RuntimeError: pass - elif not TEST: + else: keyring.set_password('jrnl', journal_name, password) -def u(s): - """Mock unicode function for python 2 and 3 compatibility.""" - if not isinstance(s, str): - s = str(s) - return s if PY3 or type(s) is unicode else s.decode("utf-8") - - -def py2encode(s): - """Encodes to UTF-8 in Python 2 but not in Python 3.""" - return s.encode("utf-8") if PY2 and type(s) is unicode else s - - -def bytes(s): - """Returns bytes, no matter what.""" - if PY3: - return s.encode("utf-8") if type(s) is not bytes else s - return s.encode("utf-8") if type(s) is unicode else s - - -def prnt(s): - """Encode and print a string""" - STDOUT.write(u(s + "\n")) - - -def prompt(msg): - """Prints a message to the std err stream defined in util.""" - if not msg.endswith("\n"): - msg += "\n" - STDERR.write(u(msg)) - - -def py23_input(msg=""): - prompt(msg) - return STDIN.readline().strip() - - -def py23_read(msg=""): - print(msg) - return STDIN.read() - - def yesno(prompt, default=True): - prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]") - raw = py23_input(prompt) - return {'y': True, 'n': False}.get(raw.lower(), default) + prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} " + response = input(prompt) + return {"y": True, "n": False}.get(response.lower(), default) def load_config(config_path): @@ -164,51 +105,35 @@ def scope_config(config, journal_name): def get_text_from_editor(config, template=""): filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt") - with codecs.open(tmpfile, 'w', "utf-8") as f: + with open(tmpfile, 'w', encoding="utf-8") as f: if template: f.write(template) try: subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile]) except AttributeError: subprocess.call(config['editor'] + [tmpfile]) - with codecs.open(tmpfile, "r", "utf-8") as f: + with open(tmpfile, "r", encoding="utf-8") as f: raw = f.read() os.close(filehandle) os.remove(tmpfile) if not raw: - prompt('[Nothing saved to file]') + print('[Nothing saved to file]', file=sys.stderr) return raw def colorize(string): """Returns the string wrapped in cyan ANSI escape""" - return u"\033[36m{}\033[39m".format(string) + return f"\033[36m{string}\033[39m" def slugify(string): """Slugifies a string. Based on public domain code from https://github.com/zacharyvoase/slugify - and ported to deal with all kinds of python 2 and 3 strings """ - string = u(string) - ascii_string = str(unicodedata.normalize('NFKD', string).encode('ascii', 'ignore')) - if PY3: - ascii_string = ascii_string[1:] # removed the leading 'b' - no_punctuation = re.sub(r'[^\w\s-]', '', ascii_string).strip().lower() + normalized_string = str(unicodedata.normalize('NFKD', string)) + no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower() slug = re.sub(r'[-\s]+', '-', no_punctuation) - return u(slug) - - -def int2byte(i): - """Converts an integer to a byte. - This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3.""" - return chr(i) if PY2 else bytes((i,)) - - -def byte2int(b): - """Converts a byte to an integer. - This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3.""" - return ord(b)if PY2 else b + return slug def split_title(text): diff --git a/pyproject.toml b/pyproject.toml index 1b84acde..7a284da8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jrnl" -version = "0.0.0-source" +version = "v2.1.post2" description = "Collect your thoughts and notes without leaving the command line." authors = [ "Manuel Ebert ",