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

[type checking] add mypy to CI, resolve all current mypy errors #312

Merged
merged 5 commits into from
Apr 27, 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: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
DEFAULT_GOAL: help
SHELL := /bin/bash

.PHONY: mypy
mypy: ## Run static type checker
@mypy --ignore-missing-imports securedrop_client

.PHONY: clean
clean: ## Clean the workspace of generated resources
redshiftzero marked this conversation as resolved.
Show resolved Hide resolved
@rm -rf build dist *.egg-info .coverage .eggs docs/_build .pytest_cache lib htmlcov .cache && \
@rm -rf .mypy_cache build dist *.egg-info .coverage .eggs docs/_build .pytest_cache lib htmlcov .cache && \
find . \( -name '*.py[co]' -o -name dropin.cache \) -delete && \
find . \( -name '*.bak' -o -name dropin.cache \) -delete && \
find . \( -name '*.tgz' -o -name dropin.cache \) -delete && \
Expand All @@ -23,7 +27,7 @@ lint: ## Run the linters
@flake8 .

.PHONY: check
check: clean lint test ## Run the full CI test suite
check: clean lint mypy test ## Run the full CI test suite

# Explaination of the below shell command should it ever break.
# 1. Set the field separator to ": ##" and any make targets that might appear between : and ##
Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ pytest = "*"
pytest-cov = "*"
pytest-mock = "*"
pytest-random-order = "*"
mypy = "*"
53 changes: 52 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 0 additions & 21 deletions securedrop_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1 @@
import gettext
import locale
import os


# Configure locale and language.
# Define where the translation assets are to be found.
localedir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'locale'))
try:
# Use the operating system's locale.
current_locale, encoding = locale.getdefaultlocale()
# Get the language code.
language_code = current_locale[:2]
except (TypeError, ValueError): # pragma: no cover
language_code = 'en' # pragma: no cover
# DEBUG/TRANSLATE: override the language code here (e.g. to Chinese).
# language_code = 'zh'
gettext.translation('securedrop_client', localedir=localedir,
languages=[language_code], fallback=True).install()


__version__ = '0.0.6'
27 changes: 26 additions & 1 deletion securedrop_client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"""
import logging
import os
import gettext
import locale
import platform
import signal
import sys
Expand Down Expand Up @@ -54,6 +56,27 @@ def excepthook(*exc_args):
sys.exit(1)


def configure_locale_and_language() -> str:
# Configure locale and language.
# Define where the translation assets are to be found.
localedir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'locale'))
try:
# Use the operating system's locale.
current_locale, encoding = locale.getdefaultlocale()
# Get the language code.
if current_locale is None:
language_code = 'en'
else:
language_code = current_locale[:2]
except ValueError: # pragma: no cover
language_code = 'en' # pragma: no cover
# DEBUG/TRANSLATE: override the language code here (e.g. to Chinese).
# language_code = 'zh'
gettext.translation('securedrop_client', localedir=localedir,
languages=[language_code], fallback=True).install()
return language_code


def configure_logging(sdc_home: str) -> None:
"""
All logging related settings are set up by this function.
Expand All @@ -68,7 +91,7 @@ def configure_logging(sdc_home: str) -> None:

# define log handlers such as for rotating log files
handler = TimedRotatingFileHandler(log_file, when='midnight',
backupCount=5, delay=0,
backupCount=5, delay=False,
encoding=ENCODING)
handler.setFormatter(formatter)
handler.setLevel(logging.DEBUG)
Expand Down Expand Up @@ -140,6 +163,7 @@ def start_app(args, qt_args) -> None:
Create all the top-level assets for the application, set things up and
run the application. Specific tasks include:

- set up locale and language.
- set up logging.
- create an application object.
- create a window for the app.
Expand All @@ -148,6 +172,7 @@ def start_app(args, qt_args) -> None:
- configure the client (logic) object.
- ensure the application is setup in the default safe starting state.
"""
configure_locale_and_language()
init(args.sdc_home)
configure_logging(args.sdc_home)
logging.info('Starting SecureDrop Client {}'.format(__version__))
Expand Down
4 changes: 3 additions & 1 deletion securedrop_client/db.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os

from typing import Any # noqa: F401

from sqlalchemy import Boolean, Column, create_engine, DateTime, ForeignKey, Integer, String, \
Text, MetaData, CheckConstraint, text, UniqueConstraint
from sqlalchemy.ext.declarative import declarative_base
Expand All @@ -16,7 +18,7 @@

metadata = MetaData(naming_convention=convention)

Base = declarative_base(metadata=metadata)
Base = declarative_base(metadata=metadata) # type: Any


def make_engine(home: str):
Expand Down
6 changes: 4 additions & 2 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
from typing import List
from gettext import gettext as _
from typing import Dict, List, Optional # noqa: F401

from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, \
QApplication

from securedrop_client import __version__
from securedrop_client.db import Source
from securedrop_client.storage import source_exists
from securedrop_client.logic import Controller # noqa: F401
from securedrop_client.gui.widgets import TopPane, LeftPane, MainView, LoginDialog, \
SourceConversationWrapper
from securedrop_client.resources import load_icon
Expand Down Expand Up @@ -57,7 +59,7 @@ def __init__(self, sdc_home: str):
self.sdc_home = sdc_home
# Cache a dict of source.uuid -> SourceConversationWrapper
# We do this to not create/destroy widgets constantly (because it causes UI "flicker")
self.conversations = {}
self.conversations = {} # type: Dict

self.setWindowTitle(_("SecureDrop Controller {}").format(__version__))
self.setWindowIcon(load_icon(self.icon))
Expand Down
17 changes: 8 additions & 9 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""
import logging
import arrow
from gettext import gettext as _
import html
import sys
from typing import List
Expand Down Expand Up @@ -727,15 +728,13 @@ def _construct_message(self, source: Source) -> str:
elif isinstance(submission, File):
files += 1

message = (
"<big>Deleting the Source account for",
"<b>{}</b> will also".format(source.journalist_designation,),
"delete {} files, {} replies, and {} messages.</big>".format(files, replies, messages),
"<br>",
"<small>This Source will no longer be able to correspond",
"through the log-in tied to this account.</small>",
)
message = ' '.join(message)
message = ("<big>Deleting the Source account for "
"<b>{}</b> will also "
"delete {} files, {} replies, and {} messages.</big>"
" <br> "
"<small>This Source will no longer be able to correspond "
"through the log-in tied to this account.</small>").format(
source.journalist_designation, files, replies, messages)
return message


Expand Down
9 changes: 5 additions & 4 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import uuid

from PyQt5.QtCore import QObject, QThread, pyqtSignal, QTimer, QProcess
from typing import Dict, Tuple # noqa: F401

from securedrop_client import storage
from securedrop_client import db
Expand Down Expand Up @@ -143,9 +144,9 @@ def __init__(self, hostname, gui, session,
self.gui = gui

# Reference to the API for secure drop proxy.
self.api = None
self.api = None # type: sdclientapi.API
# Contains active threads calling the API.
self.api_threads = {}
self.api_threads = {} # type: Dict[str, Dict]

# Reference to the SqlAlchemy session.
self.session = session
Expand Down Expand Up @@ -719,7 +720,7 @@ def send_reply(self, source_uuid: str, msg_uuid: str, message: str) -> None:
logger.error('not logged in - not implemented!') # pragma: no cover
self.reply_failed.emit(msg_uuid) # pragma: no cover

def _on_reply_complete(self, result, current_object: (str, str)) -> None:
def _on_reply_complete(self, result, current_object: Tuple[str, str]) -> None:
source_uuid, reply_uuid = current_object
source = self.session.query(db.Source).filter_by(uuid=source_uuid).one()
if isinstance(result, sdclientapi.Reply):
Expand All @@ -735,6 +736,6 @@ def _on_reply_complete(self, result, current_object: (str, str)) -> None:
else:
self.reply_failed.emit(reply_uuid)

def _on_reply_timeout(self, current_object: (str, str)) -> None:
def _on_reply_timeout(self, current_object: Tuple[str, str]) -> None:
_, reply_uuid = current_object
self.reply_failed.emit(reply_uuid)
11 changes: 10 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from configparser import ConfigParser
from datetime import datetime
from securedrop_client.config import Config
from securedrop_client.app import configure_locale_and_language
from securedrop_client.db import Base, make_engine, Source
from sqlalchemy.orm import sessionmaker
from uuid import uuid4
Expand All @@ -16,7 +17,15 @@


@pytest.fixture(scope='function')
def homedir():
def i18n():
'''
Set up locale/language/gettext functions. This enables the use of _().
'''
configure_locale_and_language()


@pytest.fixture(scope='function')
def homedir(i18n):
'''
Create a "homedir" for a client.

Expand Down
4 changes: 2 additions & 2 deletions tests/gui/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ def test_StarToggleButton_on_toggle_offline_when_checked(mocker):
set_icon_fn.assert_called_with(on='star_on.svg', off='star_on.svg')


def test_LoginDialog_setup(mocker):
def test_LoginDialog_setup(mocker, i18n):
"""
The LoginView is correctly initialised.
"""
Expand Down Expand Up @@ -728,7 +728,7 @@ def test_LoginDialog_reset(mocker):
ld.error_label.setText.assert_called_once_with('')


def test_LoginDialog_error(mocker):
def test_LoginDialog_error(mocker, i18n):
"""
Any error message passed in is assigned as the text for the error label.
"""
Expand Down
8 changes: 7 additions & 1 deletion tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
from PyQt5.QtWidgets import QApplication
from securedrop_client.app import ENCODING, excepthook, configure_logging, \
start_app, arg_parser, DEFAULT_SDC_HOME, run, configure_signal_handlers, \
prevent_second_instance
prevent_second_instance, configure_locale_and_language

app = QApplication([])


def test_application_sets_en_as_default_language_code(mocker):
mocker.patch('locale.getdefaultlocale', return_value=(None, None))
language_code = configure_locale_and_language()
assert language_code == 'en'


def test_excepthook(mocker):
"""
Ensure the custom excepthook logs the error and calls sys.exit.
Expand Down