Skip to content

Commit

Permalink
Stop using pkg_resources
Browse files Browse the repository at this point in the history
pkg_resources is deprecated and we're supposed to use
importlib.resources instead. Unfortunately it's very awkward
to use because it wants to support being able to read from
possibly compressed archives or other Python package distribution
formats.

Since our distribution mechanism is Debian packages, we can avoid
all of this entirely and just read the files off disk like any
other file. A new `load_relative_css` helper finds CSS files next to the
file invoking the function.

Take the opportunity to simplify font loading: instead of hardcoding
each font, just load every TTF in the fonts/ directory by using a
recursive glob.

And then drop some tests that don't test anything useful

Fixes #1672.
Fixes #1836.
  • Loading branch information
legoktm authored and rocodes committed Feb 22, 2024
1 parent 1288bbb commit 90d20a9
Show file tree
Hide file tree
Showing 10 changed files with 48 additions and 69 deletions.
6 changes: 2 additions & 4 deletions client/securedrop_client/gui/auth/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import logging
from gettext import gettext as _

from pkg_resources import resource_string
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QBrush, QPalette
from PyQt5.QtWidgets import (
Expand All @@ -39,7 +38,7 @@
from securedrop_client.gui.base import PasswordEdit
from securedrop_client.gui.base.checkbox import SDCheckBox
from securedrop_client.logic import Controller
from securedrop_client.resources import load_image
from securedrop_client.resources import load_image, load_relative_css

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -82,8 +81,7 @@ def __init__(self, parent: QWidget) -> None:
form = QWidget()

form.setObjectName("LoginDialog_form")
styles = resource_string(__name__, "dialog.css").decode("utf-8")
self.setStyleSheet(styles)
self.setStyleSheet(load_relative_css(__file__, "dialog.css"))

form_layout = QVBoxLayout()
form.setLayout(form_layout)
Expand Down
6 changes: 3 additions & 3 deletions client/securedrop_client/gui/auth/sign_in/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
"""
from gettext import gettext as _

from pkg_resources import resource_string
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QCursor
from PyQt5.QtWidgets import QGraphicsDropShadowEffect, QPushButton

from securedrop_client.resources import load_relative_css


class SignInButton(QPushButton):
"""
Expand All @@ -35,8 +36,7 @@ def __init__(self) -> None:

# Set css id
self.setObjectName("SignInButton")
styles = resource_string(__name__, "button.css").decode("utf-8")
self.setStyleSheet(styles)
self.setStyleSheet(load_relative_css(__file__, "button.css"))

self.setFixedHeight(40)
self.setFixedWidth(140)
Expand Down
6 changes: 3 additions & 3 deletions client/securedrop_client/gui/auth/sign_in/error_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from pkg_resources import resource_string

from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QHBoxLayout, QWidget

from securedrop_client.gui.base import SecureQLabel, SvgLabel
from securedrop_client.resources import load_relative_css


class LoginErrorBar(QWidget):
Expand All @@ -32,8 +33,7 @@ def __init__(self) -> None:
super().__init__()

self.setObjectName("LoginErrorBar")
styles = resource_string(__name__, "error_bar.css").decode("utf-8")
self.setStyleSheet(styles)
self.setStyleSheet(load_relative_css(__file__, "error_bar.css"))

# Set layout
layout = QHBoxLayout(self)
Expand Down
6 changes: 3 additions & 3 deletions client/securedrop_client/gui/base/checkbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
"""
from gettext import gettext as _

from pkg_resources import resource_string
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QCursor, QFont, QMouseEvent
from PyQt5.QtWidgets import QCheckBox, QFrame, QHBoxLayout, QLabel, QWidget

from securedrop_client.resources import load_relative_css


class SDCheckBox(QWidget):
clicked = pyqtSignal()
CHECKBOX_CSS = resource_string(__name__, "checkbox.css").decode("utf-8")
PASSPHRASE_LABEL_SPACING = 1

def __init__(self) -> None:
super().__init__()
self.setObjectName("ShowPassphrase_widget")
self.setStyleSheet(self.CHECKBOX_CSS)
self.setStyleSheet(load_relative_css(__file__, "checkbox.css"))

font = QFont()
font.setLetterSpacing(QFont.AbsoluteSpacing, self.PASSPHRASE_LABEL_SPACING)
Expand Down
9 changes: 4 additions & 5 deletions client/securedrop_client/gui/base/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"""
from gettext import gettext as _

from pkg_resources import resource_string
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QIcon, QKeyEvent, QPixmap
from PyQt5.QtWidgets import (
Expand All @@ -33,13 +32,13 @@
)

from securedrop_client.gui.base.misc import SvgLabel
from securedrop_client.resources import load_movie
from securedrop_client.resources import load_movie, load_relative_css


class ModalDialog(QDialog):
DIALOG_CSS = resource_string(__name__, "dialogs.css").decode("utf-8")
BUTTON_CSS = resource_string(__name__, "dialog_button.css").decode("utf-8")
ERROR_DETAILS_CSS = resource_string(__name__, "dialog_message.css").decode("utf-8")
DIALOG_CSS = load_relative_css(__file__, "dialogs.css")
BUTTON_CSS = load_relative_css(__file__, "dialog_button.css")
ERROR_DETAILS_CSS = load_relative_css(__file__, "dialog_message.css")

MARGIN = 40
NO_MARGIN = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from gettext import gettext as _
from typing import List, Optional

from pkg_resources import resource_string
from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtGui import QIcon, QKeyEvent
from PyQt5.QtWidgets import QAbstractButton # noqa: F401
Expand All @@ -20,7 +19,7 @@
PassphraseWizardPage,
PreflightPage,
)
from securedrop_client.resources import load_movie
from securedrop_client.resources import load_movie, load_relative_css

logger = logging.getLogger(__name__)

Expand All @@ -34,8 +33,6 @@ class ExportWizard(QWizard):
NO_MARGIN = 0
FILENAME_WIDTH_PX = 260
FILE_OPTIONS_FONT_SPACING = 1.6
BUTTON_CSS = resource_string(__name__, "wizard_button.css").decode("utf-8")
WIZARD_CSS = resource_string(__name__, "wizard.css").decode("utf-8")

# If the drive is unlocked, we don't need a passphrase; if we do need one,
# it's populated later.
Expand Down Expand Up @@ -97,6 +94,7 @@ def _style_buttons(self) -> None:
self.button_animation = load_movie("activestate-wide.gif")
self.button_animation.setScaledSize(QSize(32, 32))
self.button_animation.frameChanged.connect(self._animate_activestate)
button_stylesheet = load_relative_css(__file__, "wizard_button.css")

# Buttons
self.next_button = self.button(QWizard.WizardButton.NextButton) # type: QAbstractButton
Expand All @@ -105,27 +103,27 @@ def _style_buttons(self) -> None:
self.finish_button = self.button(QWizard.WizardButton.FinishButton) # type: QAbstractButton

self.next_button.setObjectName("QWizardButton_PrimaryButton")
self.next_button.setStyleSheet(self.BUTTON_CSS)
self.next_button.setStyleSheet(button_stylesheet)
self.next_button.setMinimumSize(QSize(142, 40))
self.next_button.setMaximumHeight(40)
self.next_button.setIconSize(QSize(21, 21))
self.next_button.clicked.connect(self.request_export)
self.next_button.setFixedSize(QSize(142, 40))

self.cancel_button.setObjectName("QWizardButton_GenericButton")
self.cancel_button.setStyleSheet(self.BUTTON_CSS)
self.cancel_button.setStyleSheet(button_stylesheet)
self.cancel_button.setMinimumSize(QSize(142, 40))
self.cancel_button.setMaximumHeight(40)
self.cancel_button.setFixedSize(QSize(142, 40))

self.back_button.setObjectName("QWizardButton_GenericButton")
self.back_button.setStyleSheet(self.BUTTON_CSS)
self.back_button.setStyleSheet(button_stylesheet)
self.back_button.setMinimumSize(QSize(142, 40))
self.back_button.setMaximumHeight(40)
self.back_button.setFixedSize(QSize(142, 40))

self.finish_button.setObjectName("QWizardButton_GenericButton")
self.finish_button.setStyleSheet(self.BUTTON_CSS)
self.finish_button.setStyleSheet(button_stylesheet)
self.finish_button.setMinimumSize(QSize(142, 40))
self.finish_button.setMaximumHeight(40)
self.finish_button.setFixedSize(QSize(142, 40))
Expand All @@ -149,7 +147,7 @@ def _set_layout(self) -> None:
title = ("Export %(summary)s") % {"summary": self.summary_text}
self.setWindowTitle(title)
self.setObjectName("QWizard_export")
self.setStyleSheet(self.WIZARD_CSS)
self.setStyleSheet(load_relative_css(__file__, "wizard.css"))
self.setModal(False)
self.setOptions(
QWizard.NoBackButtonOnLastPage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from gettext import gettext as _
from typing import Optional

from pkg_resources import resource_string
from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtGui import QColor, QFont, QPixmap
from PyQt5.QtWidgets import (
Expand All @@ -21,7 +20,7 @@
from securedrop_client.gui.base.checkbox import SDCheckBox
from securedrop_client.gui.base.misc import SvgLabel
from securedrop_client.gui.conversation.export.export_wizard_constants import STATUS_MESSAGES, Pages
from securedrop_client.resources import load_movie
from securedrop_client.resources import load_movie, load_relative_css

logger = logging.getLogger(__name__)

Expand All @@ -47,9 +46,6 @@ class ExportWizardPage(QWizardPage):
appears on recoverable error to help the user advance to the next stage)
"""

WIZARD_CSS = resource_string(__name__, "wizard.css").decode("utf-8")
ERROR_DETAILS_CSS = resource_string(__name__, "wizard_message.css").decode("utf-8")

MARGIN = 40
PASSPHRASE_LABEL_SPACING = 0.5
NO_MARGIN = 0
Expand Down Expand Up @@ -94,7 +90,8 @@ def _build_layout(self) -> QVBoxLayout:
Create parent layout, draw elements, return parent layout
"""
self.setObjectName("QWizard_export_page")
self.setStyleSheet(self.WIZARD_CSS)
self.setStyleSheet(load_relative_css(__file__, "wizard.css"))

parent_layout = QVBoxLayout(self)
parent_layout.setContentsMargins(self.MARGIN, self.MARGIN, self.MARGIN, self.MARGIN)

Expand Down Expand Up @@ -139,7 +136,7 @@ def _build_layout(self) -> QVBoxLayout:
# Widget for displaying error messages (hidden by default)
self.error_details = QLabel()
self.error_details.setObjectName("QWizard_error_details")
self.error_details.setStyleSheet(self.ERROR_DETAILS_CSS)
self.error_details.setStyleSheet(load_relative_css(__file__, "wizard_message.css"))
self.error_details.setContentsMargins(
self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN, self.NO_MARGIN
)
Expand Down
5 changes: 2 additions & 3 deletions client/securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from securedrop_client.gui.auth import LoginDialog
from securedrop_client.gui.widgets import LeftPane, MainView, TopPane
from securedrop_client.logic import Controller
from securedrop_client.resources import load_css, load_font, load_icon
from securedrop_client.resources import load_all_fonts, load_css, load_icon

logger = logging.getLogger(__name__)

Expand All @@ -59,8 +59,7 @@ def __init__(
place for details / message contents / forms.
"""
super().__init__()
load_font("Montserrat")
load_font("Source_Sans_Pro")
load_all_fonts()
self.setStyleSheet(load_css("sdclient.css"))
self.setWindowTitle(_("SecureDrop Client {}").format(__version__))
self.setWindowIcon(load_icon(self.icon))
Expand Down
32 changes: 20 additions & 12 deletions client/securedrop_client/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,33 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os
from pathlib import Path
from typing import Optional

from pkg_resources import resource_filename, resource_string
from PyQt5.QtCore import QDir
from PyQt5.QtGui import QFontDatabase, QIcon, QMovie, QPixmap
from PyQt5.QtSvg import QSvgWidget

RESOURCES_DIR = Path(__file__).parent

# Add resource directories to the search path.
QDir.addSearchPath("images", resource_filename(__name__, "images"))
QDir.addSearchPath("css", resource_filename(__name__, "css"))
QDir.addSearchPath("images", str(RESOURCES_DIR / "images"))
QDir.addSearchPath("css", str(RESOURCES_DIR / "css"))


def path(name: str, resource_dir: str = "images/") -> str:
def path(name: str) -> str:
"""
Return the filename for the referenced image.
Qt uses unix path conventions.
"""
return resource_filename(__name__, resource_dir + name)
return str(RESOURCES_DIR / "images" / name)


def load_font(font_folder_name: str) -> None:
directory = resource_filename(__name__, "fonts/") + font_folder_name
for filename in os.listdir(directory):
if filename.endswith(".ttf"):
QFontDatabase.addApplicationFont(directory + "/" + filename)
def load_all_fonts() -> None:
"""Load all the fonts in the fonts/ directory"""
for font in (RESOURCES_DIR / "fonts").glob("**/*.ttf"):
QFontDatabase.addApplicationFont(str(font.absolute()))


def load_icon(
Expand Down Expand Up @@ -133,7 +133,15 @@ def load_css(name: str) -> str:
"""
Return the contents of the referenced CSS file in the resources.
"""
return resource_string(__name__, "css/" + name).decode("utf-8")
return (RESOURCES_DIR / "css" / name).read_text()


def load_relative_css(file: str, name: str) -> str:
"""
Load CSS that's in the same directory as the file calling this.
The first argument should be __name__ and the second is the name of the CSS
"""
return (Path(file).parent / name).read_text()


def load_movie(name: str) -> QMovie:
Expand Down
20 changes: 0 additions & 20 deletions client/tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@
from tests.helper import app # noqa: F401


def test_path(mocker):
"""
Ensure the resource_filename function is called with the expected args and
the path function under test returns its result.
"""
r = mocker.patch("securedrop_client.resources.resource_filename", return_value="bar")
assert securedrop_client.resources.path("foo") == "bar"
r.assert_called_once_with(securedrop_client.resources.__name__, "images/foo")


def test_load_icon():
"""
Check the load_icon function returns the expected QIcon object.
Expand Down Expand Up @@ -51,16 +41,6 @@ def test_load_image():
assert isinstance(result, QPixmap)


def test_load_css(mocker):
"""
Ensure the resource_string function is called with the expected args and
the load_css function returns its result.
"""
rs = mocker.patch("securedrop_client.resources.resource_string", return_value=b"foo")
assert "foo" == securedrop_client.resources.load_css("foo")
rs.assert_called_once_with(securedrop_client.resources.__name__, "css/foo")


def test_load_movie():
"""
Check the load_movie function returns the expected QMovie object.
Expand Down

0 comments on commit 90d20a9

Please sign in to comment.