Best practices in software engineering

rot13.py
import string

_lower_cipher = string.ascii_lowercase[13:] + string.ascii_lowercase[:13]
_upper_cipher = string.ascii_uppercase[13:] + string.ascii_uppercase[:13]

def encode(message):
    """
    Encode a message from English to ROT13
    
    Args:
        message (str): the English message to encode
    
    Returns:
        str: The encoded message
    
    Examples:
        >>> encode("Secretmessage")
        'Frpergzrffntr'
    """
    output = []
    for letter in message:
        if letter in string.ascii_lowercase:
            i = string.ascii_lowercase.find(letter)
            output.append(_lower_cipher[i])
        elif letter in string.ascii_uppercase:
            i = string.ascii_uppercase.find(letter)
            output.append(_upper_cipher[i])
        else:  # Add this else statement
            raise ValueError(f"Cannot encode \"{message}\". Character \"{letter}\" not valid")
    
    return "".join(output)


def decode(message):
    """
    Encode a message from ROT13 to English
    
    Args:
        message (str): the ROT13 message to encode
    
    Returns:
        str: The decoded message
    
    Examples:
        >>> encode("Frpergzrffntr")
        'Secretmessage'
    """
    output = []
    for letter in message:
        if letter in _lower_cipher:
            i = _lower_cipher.find(letter)
            output.append(string.ascii_lowercase[i])  # ascii_uppercase → ascii_lowercase
        elif letter in _upper_cipher:
            i = _upper_cipher.find(letter)
            output.append(string.ascii_uppercase[i])
        else:  # Add this else statement
            raise ValueError(f"Cannot decode \"{message}\". Character \"{letter}\" not valid")
    
    return "".join(output)

    # An alternate "clever" solution is to exploit the fact that rot13 is its own inverse
    # and simply call the encode function again. The entirety of this function would then
    # just become:
    #
    # return encode(message)
test_rot13.py
import pytest

from rot13 import encode, decode

@pytest.mark.parametrize("message, expected", [
    ("SECRET", "FRPERG"),
    ("secret", "frperg"),
])
def test_encode(message, expected):
    assert encode(message) == expected

@pytest.mark.parametrize("message, expected", [
    ("FRPERG", "SECRET"),
    ("frperg", "secret"),
])
def test_decode(message, expected):
    assert decode(message) == expected

def test_encode_spaces_error():
    with pytest.raises(ValueError):
        encode("Secret message for you")
$
pytest -v --doctest-modules morse.py rot13.py test_morse.py test_rot13.py
=================== test session starts ====================
platform linux -- Python 3.8.5, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/matt/projects/courses/software_engineering_best_practices
plugins: requests-mock-1.8.0
collected 21 items                                         

morse.py::morse.decode PASSED                        [  4%]
morse.py::morse.encode PASSED                        [  9%]
rot13.py::rot13.decode PASSED                        [ 14%]
rot13.py::rot13.encode PASSED                        [ 19%]
test_morse.py::test_encode[SOS-... --- ...] PASSED   [ 23%]
test_morse.py::test_encode[help-.... . .-.. .--.] PASSED [ 28%]
test_morse.py::test_encode[-] PASSED                 [ 33%]
test_morse.py::test_encode[ -/] PASSED               [ 38%]
test_morse.py::test_decode[... --- ...-sos] PASSED   [ 42%]
test_morse.py::test_decode[.... . .-.. .--.-help] PASSED [ 47%]
test_morse.py::test_decode[/- ] PASSED               [ 52%]
test_morse.py::test_error PASSED                     [ 57%]
test_morse.py::test_errors[It's sinking] PASSED      [ 61%]
test_morse.py::test_errors[Titanic & Olympic] PASSED [ 66%]
test_morse.py::test_errors[This boat is expensive \xa3\xa3\xa3] PASSED [ 71%]
test_morse.py::test_errors[Help!] PASSED             [ 76%]
test_rot13.py::test_encode[SECRET-FRPERG] PASSED     [ 80%]
test_rot13.py::test_encode[secret-frperg] PASSED     [ 85%]
test_rot13.py::test_decode[FRPERG-SECRET] PASSED     [ 90%]
test_rot13.py::test_decode[frperg-secret] PASSED     [ 95%]
test_rot13.py::test_encode_spaces_error PASSED       [100%]

==================== 21 passed in 0.06s ====================