Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop Python 2 support, add mocks in tests #705

Merged
merged 9 commits into from
Nov 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions features/encryption.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
15 changes: 2 additions & 13 deletions features/environment.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion features/multiple_journals.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
68 changes: 40 additions & 28 deletions features/steps/core.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,10 +9,13 @@
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)
Expand All @@ -31,15 +33,6 @@ def delete_password(self, servicename, username, password):
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)
Expand Down Expand Up @@ -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}"')
Expand Down Expand Up @@ -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]


Expand Down
4 changes: 2 additions & 2 deletions features/upgrade.feature
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥇

When we run "jrnl -n 1" and enter
"""
Y
bad doggie no biscuit
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"
8 changes: 3 additions & 5 deletions jrnl/DayOneJournal.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"))]
Expand Down Expand Up @@ -83,7 +81,7 @@ def write(self):
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."""
Expand All @@ -107,7 +105,7 @@ def parse_editable_str(self, edited):
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]
Expand Down
12 changes: 5 additions & 7 deletions jrnl/EncryptedJournal.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
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,
Expand All @@ -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):
Expand All @@ -48,9 +47,9 @@ def open(self, filename=None):
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)
Expand All @@ -59,7 +58,6 @@ def open(self, filename=None):
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
Expand Down Expand Up @@ -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):
Expand Down
12 changes: 5 additions & 7 deletions jrnl/Entry.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/usr/bin/env python
# encoding: utf-8

from __future__ import unicode_literals
import re
import textwrap
from datetime import datetime
Expand Down Expand Up @@ -51,14 +49,14 @@ def tags(self):

@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 "))
Expand Down Expand Up @@ -106,7 +104,7 @@ def pprint(self, short=False):
)

def __repr__(self):
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
return "<Entry '{}' on {}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))

def __hash__(self):
return hash(self.__repr__())
Expand Down
Loading