Пример #1
0
    def startEdit(self, tabIndex: int) -> None:
        self.editingTabIndex = tabIndex
        rect: QRect = self.tabRect(tabIndex)

        topMargin = 3
        leftMargin = 6

        lineEdit = QLineEdit(self)
        lineEdit.setAlignment(Qt.AlignCenter)
        lineEdit.move(rect.left() + leftMargin, rect.top() + topMargin)
        lineEdit.resize(rect.width() - 2 * leftMargin,
                        rect.height() - 2 * topMargin)
        lineEdit.setText(self.tabText(tabIndex))
        lineEdit.selectAll()
        lineEdit.setFocus()
        lineEdit.show()
        lineEdit.editingFinished.connect(self.finishEdit)
        self.lineEdit = lineEdit
Пример #2
0
class PlotNameWidget(QWidget):
    """A widget to display the plot name, and edit and close buttons

    This widget is added to the table widget to support the renaming
    and close buttons, as well as the direct renaming functionality.
    """
    def __init__(self, presenter, plot_number, parent=None):
        super(PlotNameWidget, self).__init__(parent)

        self.presenter = presenter
        self.plot_number = plot_number

        self.mutex = QMutex()

        self.line_edit = QLineEdit(
            self.presenter.get_plot_name_from_number(plot_number))
        self.line_edit.setReadOnly(True)
        self.line_edit.setFrame(False)
        # changes the line edit to look like normal even when
        self.line_edit.setStyleSheet(
            """* { background-color: rgba(0, 0, 0, 0); }
        QLineEdit:disabled { color: black; }""")
        self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        self.line_edit.editingFinished.connect(self.rename_plot)
        # Disabling the line edit prevents it from temporarily
        # grabbing focus when changing code editors - this triggered
        # the editingFinished signal, which was causing #26305
        self.line_edit.setDisabled(True)

        shown_icon = get_icon('mdi.eye')
        self.hide_button = QPushButton(shown_icon, "")
        self.hide_button.setToolTip('Hide')
        self.hide_button.setFlat(True)
        self.hide_button.setMaximumWidth(self.hide_button.iconSize().width() *
                                         5 / 3)
        self.hide_button.clicked.connect(self.toggle_visibility)

        rename_icon = get_icon('mdi.square-edit-outline')
        self.rename_button = QPushButton(rename_icon, "")
        self.rename_button.setToolTip('Rename')
        self.rename_button.setFlat(True)
        self.rename_button.setMaximumWidth(
            self.rename_button.iconSize().width() * 5 / 3)
        self.rename_button.setCheckable(True)
        self.rename_button.toggled.connect(self.rename_button_toggled)

        close_icon = get_icon('mdi.close')
        self.close_button = QPushButton(close_icon, "")
        self.close_button.setToolTip('Delete')
        self.close_button.setFlat(True)
        self.close_button.setMaximumWidth(
            self.close_button.iconSize().width() * 5 / 3)
        self.close_button.clicked.connect(
            lambda: self.close_pressed(self.plot_number))

        self.layout = QHBoxLayout()

        # Get rid of the top and bottom margins - the button provides
        # some natural margin anyway. Get rid of right margin and
        # reduce spacing to get buttons closer together.
        self.layout.setContentsMargins(5, 0, 0, 0)
        self.layout.setSpacing(0)

        self.layout.addWidget(self.line_edit)
        self.layout.addWidget(self.hide_button)
        self.layout.addWidget(self.rename_button)
        self.layout.addWidget(self.close_button)

        self.layout.sizeHint()
        self.setLayout(self.layout)

    def set_plot_name(self, new_name):
        """
        Sets the internally stored and displayed plot name
        :param new_name: The name to set
        """
        self.line_edit.setText(new_name)

    def close_pressed(self, plot_number):
        """
        Close the plot with the given name
        :param plot_number: The unique number in GlobalFigureManager
        """
        self.presenter.close_single_plot(plot_number)

    def rename_button_toggled(self, checked):
        """
        If the rename button is pressed from being unchecked then
        make the line edit item editable
        :param checked: True if the rename toggle is now pressed
        """
        if checked:
            self.toggle_plot_name_editable(True, toggle_rename_button=False)

    def toggle_plot_name_editable(self, editable, toggle_rename_button=True):
        """
        Set the line edit item to be editable or not editable. If
        editable move the cursor focus to the editable name and
        highlight it all.
        :param editable: If true make the plot name editable, else
                         make it read only
        :param toggle_rename_button: If true also toggle the rename
                                     button state
        """
        self.line_edit.setReadOnly(not editable)
        self.line_edit.setDisabled(not editable)
        self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents,
                                    not editable)

        # This is a sneaky way to avoid the issue of two calls to
        # this toggle method, by effectively disabling the button
        # press in edit mode.
        self.rename_button.setAttribute(Qt.WA_TransparentForMouseEvents,
                                        editable)

        if toggle_rename_button:
            self.rename_button.setChecked(editable)

        if editable:
            self.line_edit.setFocus()
            self.line_edit.selectAll()
        else:
            self.line_edit.setSelection(0, 0)

    def toggle_visibility(self):
        """
        Calls the presenter to hide the selected plot
        """
        self.presenter.toggle_plot_visibility(self.plot_number)

    def set_visibility_icon(self, is_shown):
        """
        Change the widget icon between shown and hidden
        :param is_shown: True if plot is shown, false if hidden
        """
        if is_shown:
            self.hide_button.setIcon(get_icon('mdi.eye'))
            self.hide_button.setToolTip('Hide')
        else:
            self.hide_button.setIcon(get_icon('mdi.eye', 'lightgrey'))
            self.hide_button.setToolTip('Show')

    def rename_plot(self):
        """
        Called when the editing is finished, gets the presenter to
        do the real renaming of the plot
        """
        self.presenter.rename_figure(self.plot_number, self.line_edit.text())

        self.toggle_plot_name_editable(False)
Пример #3
0
class AuthenticationDialog(DialogBase):
    FORGOT_USERNAME_URL = 'https://anaconda.org/account/forgot_username'
    FORGOT_PASWORD_URL = 'https://anaconda.org/account/forgot_password'
    REGISTER_URL = 'https://anaconda.org'

    def __init__(self, api, parent=None):
        super(AuthenticationDialog, self).__init__(parent)

        self.api = api
        self._parent = parent
        self.token = None
        self.error = None
        self.tracker = GATracker()

        # Widgets
        self.label_username = QLabel('Username:'******'Password:'******'You can register ')
        self.label_signin_text = QLabel('<hr><br><b>Already a member? '
                                        'Sign in!</b><br>')
        # For styling purposes the label next to a ButtonLink is also a button
        # so they align adequately
        self.button_register_text = ButtonLabel('You can register by '
                                                'visiting the')
        self.button_register = ButtonLink('Anaconda Cloud')
        self.button_register_after_text = ButtonLabel('website.')
        self.label_information = QLabel('''
            <strong>Anaconda Cloud</strong> is where packages, notebooks,
            and <br> environments are shared. It provides powerful <br>
            collaboration and package management for open <br>
            source and private projects.<br>
            ''')
        self.label_message = QLabel('')
        self.button_forgot_username = ButtonLink('I forgot my username')
        self.button_forgot_password = ButtonLink('I forgot my password')
        self.button_login = QPushButton('Login')
        self.button_cancel = ButtonCancel('Cancel')
        self.bbox = QDialogButtonBox(Qt.Horizontal)

        # Widgets setup
        self.bbox.addButton(self.button_cancel, QDialogButtonBox.RejectRole)
        self.bbox.addButton(self.button_login, QDialogButtonBox.AcceptRole)
        self.text_username.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.text_password.setAttribute(Qt.WA_MacShowFocusRect, False)

        self.setMinimumWidth(260)
        self.setWindowTitle('Sign in')

        # This allows to completely style the dialog with css using the frame
        self.text_password.setEchoMode(QLineEdit.Password)
        self.label_message.setVisible(False)

        # Layout
        grid_layout = QGridLayout()
        grid_layout.addWidget(self.label_username, 0, 0)
        grid_layout.addWidget(self.text_username, 0, 1)
        grid_layout.addWidget(self.label_password, 1, 0)
        grid_layout.addWidget(self.text_password, 1, 1)

        main_layout = QVBoxLayout()
        main_layout.addWidget(self.label_information)

        register_layout = QHBoxLayout()
        register_layout.addWidget(self.button_register_text, 0)
        register_layout.addWidget(self.button_register, 0, Qt.AlignLeft)
        register_layout.addWidget(self.button_register_after_text, 0,
                                  Qt.AlignLeft)
        register_layout.addStretch()
        register_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.addLayout(register_layout)
        main_layout.addWidget(self.label_signin_text)
        main_layout.addLayout(grid_layout)
        main_layout.addSpacing(5)
        main_layout.addWidget(self.label_message)
        main_layout.addWidget(self.button_forgot_username, 0, Qt.AlignRight)
        main_layout.addWidget(self.button_forgot_password, 0, Qt.AlignRight)

        main_layout.addSpacing(15)
        main_layout.addWidget(self.bbox)

        self.setLayout(main_layout)

        # Signals
        self.button_forgot_username.clicked.connect(
            lambda: self.open_url(self.FORGOT_USERNAME_URL))
        self.button_forgot_password.clicked.connect(
            lambda: self.open_url(self.FORGOT_PASWORD_URL))
        self.button_register.clicked.connect(
            lambda: self.open_url(self.REGISTER_URL))
        self.text_username.textEdited.connect(self.check_text)
        self.text_password.textEdited.connect(self.check_text)
        self.button_login.clicked.connect(self.login)
        self.button_cancel.clicked.connect(self.reject)

        # Setup
        self.check_text()
        self.update_style_sheet()
        self.text_username.setFocus()

    @property
    def username(self):
        return self.text_username.text()

    def update_style_sheet(self, style_sheet=None):
        if style_sheet is None:
            style_sheet = load_style_sheet()
        self.setStyleSheet(style_sheet)

    def check_text(self):
        """
        Check that `username` and `password` are not empty and disabel/enable
        buttons accordingly.
        """
        username = self.text_username.text()
        password = self.text_password.text()

        if len(username) == 0 or len(password) == 0:
            self.button_login.setDisabled(True)
        else:
            self.button_login.setDisabled(False)

    def login(self):
        self.button_login.setEnabled(False)
        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.label_message.setText('')
        worker = self.api.client_login(self.text_username.text(),
                                       self.text_password.text(),
                                       'Anaconda Navigator',
                                       '')
        worker.sig_finished.connect(self._finished)

    def _finished(self, worker, output, error):
        """
        Method called when the anaconda-client Api has finished a process
        that runs in a separate worker thread.
        """
        token = output

        if token:
            self.token = token
            self.accept()
        elif error:
            username = self.text_username.text()
            bold_username = '******'.format(username)

            # The error might come in (error_message, http_error) format
            try:
                error_message = eval(str(error))[0]
            except Exception:
                error_message = str(error)

            error_message = error_message.lower().capitalize()
            error_message = error_message.split(', ')[0]
            error_text = '<i>{0}</i>'.format(error_message)
            error_text = error_text.replace(username, bold_username)
            self.label_message.setText(error_text)
            self.label_message.setVisible(True)

            if error_message:
                domain = self.api.client_domain()
                label = '{0}/{1}: {2}'.format(domain, username,
                                              error_message.lower())
                self.tracker.track_event('authenticate', 'login failed',
                                         label=label)
                self.text_password.setFocus()
                self.text_password.selectAll()

        self.button_login.setDisabled(False)
        self.check_text()
        QApplication.restoreOverrideCursor()

    def open_url(self, url):
        self.tracker.track_event('content', 'click', url)
        QDesktopServices.openUrl(QUrl(url))
Пример #4
0
class AuthenticationDialog(DialogBase):
    """Login dialog."""

    # See https://github.com/Anaconda-Platform/anaconda-server settings
    USER_RE = QRegExp('^[A-Za-z0-9_][A-Za-z0-9_-]+$')
    FORGOT_USERNAME_URL = 'account/forgot_username'
    FORGOT_PASSWORD_URL = 'account/forgot_password'

    sig_authentication_succeeded = Signal()
    sig_authentication_failed = Signal()
    sig_url_clicked = Signal(object)

    def __init__(self, api, parent=None):
        """Login dialog."""
        super(AuthenticationDialog, self).__init__(parent)

        self._parent = parent
        self.config = CONF
        self.api = api
        self.token = None
        self.error = None
        self.tracker = GATracker()
        self.forgot_username_url = None
        self.forgot_password_url = None

        # Widgets
        self.label_username = QLabel('Username:'******'Password:'******'<hr><br><b>Already a member? '
                                        'Sign in!</b><br>')
        # For styling purposes the label next to a ButtonLink is also a button
        # so they align adequately
        self.button_register_text = ButtonLabel('You can register by '
                                                'visiting the ')
        self.button_register = ButtonLink('Anaconda Cloud')
        self.button_register_after_text = ButtonLabel('website.')
        self.label_information = QLabel('''
            <strong>Anaconda Cloud</strong> is where packages, notebooks,
            and <br> environments are shared. It provides powerful <br>
            collaboration and package management for open <br>
            source and private projects.<br>
            ''')
        self.label_message = QLabel('')
        self.button_forgot_username = ButtonLink('I forgot my username')
        self.button_forgot_password = ButtonLink('I forgot my password')
        self.button_login = ButtonPrimary('Login')
        self.button_cancel = ButtonNormal('Cancel')

        # Widgets setup
        self.button_login.setDefault(True)
        username_validator = QRegExpValidator(self.USER_RE)
        self.text_username.setValidator(username_validator)

        self.setMinimumWidth(260)
        self.setWindowTitle('Sign in')

        # This allows to completely style the dialog with css using the frame
        self.text_password.setEchoMode(QLineEdit.Password)
        self.label_message.setVisible(False)

        # Layout
        grid_layout = QVBoxLayout()
        grid_layout.addWidget(self.label_username)
        # grid_layout.addWidget(SpacerVertical())
        grid_layout.addWidget(self.text_username)
        grid_layout.addWidget(SpacerVertical())
        grid_layout.addWidget(self.label_password)
        # grid_layout.addWidget(SpacerVertical())
        grid_layout.addWidget(self.text_password)

        main_layout = QVBoxLayout()
        main_layout.addWidget(self.label_information)

        register_layout = QHBoxLayout()
        register_layout.addWidget(self.button_register_text)
        register_layout.addWidget(self.button_register)
        register_layout.addWidget(self.button_register_after_text)
        register_layout.addStretch()
        main_layout.addLayout(register_layout)
        main_layout.addWidget(self.label_signin_text)
        main_layout.addLayout(grid_layout)
        main_layout.addWidget(SpacerVertical())
        main_layout.addWidget(self.label_message)
        main_layout.addWidget(self.button_forgot_username, 0, Qt.AlignRight)
        main_layout.addWidget(self.button_forgot_password, 0, Qt.AlignRight)

        layout_buttons = QHBoxLayout()
        layout_buttons.addStretch()
        layout_buttons.addWidget(self.button_cancel)
        layout_buttons.addWidget(SpacerHorizontal())
        layout_buttons.addWidget(self.button_login)

        main_layout.addWidget(SpacerVertical())
        main_layout.addWidget(SpacerVertical())
        main_layout.addLayout(layout_buttons)

        self.setLayout(main_layout)

        # Signals
        self.text_username.textEdited.connect(self.check_text)
        self.text_password.textEdited.connect(self.check_text)
        self.button_login.clicked.connect(self.login)
        self.button_cancel.clicked.connect(self.reject)

        # Setup
        self.check_text()
        self.update_style_sheet()
        self.text_username.setFocus()
        self.setup()

    def setup(self):
        """Setup login dialog."""
        self.update_links()

    def update_links(self):
        """Update links."""
        for button in [
                self.button_forgot_username,
                self.button_forgot_password,
                self.button_register,
        ]:
            try:
                button.disconnect()
            except TypeError:  # pragma: no cover
                pass

        # TODO, get this from anaconda client directly?
        # from binstar_client.utils import get_config, set_config
        # config = get_config()
        anaconda_api_url = self.config.get('main', 'anaconda_api_url', None)
        if anaconda_api_url:
            # Remove api if using a subdomain
            base_url = anaconda_api_url.lower().replace('//api.', '//')
            self.base_url = base_url

            # Remove api if not using a subdomain
            parts = base_url.lower().split('/')
            if parts[-1] == 'api':
                base_url = '/'.join(parts[:-1])

            self.forgot_username_url = (base_url + '/' +
                                        self.FORGOT_USERNAME_URL)
            self.forgot_password_url = (base_url + '/' +
                                        self.FORGOT_PASSWORD_URL)

            self.button_register.clicked.connect(
                lambda: self.open_url(base_url))
            self.button_forgot_username.clicked.connect(
                lambda: self.open_url(self.forgot_username_url))
            self.button_forgot_password.clicked.connect(
                lambda: self.open_url(self.forgot_password_url))

    @property
    def username(self):
        """Return the logged username."""
        return self.text_username.text().lower()

    def update_style_sheet(self, style_sheet=None):
        """Update custom css style sheet."""
        if style_sheet is None:
            style_sheet = load_style_sheet()
        self.setStyleSheet(style_sheet)

    def check_text(self):
        """Check that `username` and `password` are valid.

        If not empty and disable/enable buttons accordingly.
        """
        username = self.text_username.text()
        password = self.text_password.text()

        if len(username) == 0 or len(password) == 0:
            self.button_login.setDisabled(True)
        else:
            self.button_login.setDisabled(False)

    def login(self):
        """Try to log the user in the specified anaconda api endpoint."""
        self.button_login.setEnabled(False)
        self.text_username.setText(self.text_username.text().lower())
        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.label_message.setText('')
        worker = self.api.login(self.text_username.text().lower(),
                                self.text_password.text())
        worker.sig_finished.connect(self._finished)

    def _finished(self, worker, output, error):
        """Callback for the login procedure after worker has finished."""
        token = output
        if token:
            self.token = token
            self.sig_authentication_succeeded.emit()
            self.accept()
        elif error:
            username = self.text_username.text().lower()
            bold_username = '******'.format(username)

            # The error might come in (error_message, http_error) format
            try:
                error_message = ast.literal_eval(str(error))[0]
            except Exception:  # pragma: no cover
                error_message = str(error)

            error_message = error_message.lower().capitalize()
            error_message = error_message.split(', ')[0]
            error_text = '<i>{0}</i>'.format(error_message)
            error_text = error_text.replace(username, bold_username)
            self.label_message.setText(error_text)
            self.label_message.setVisible(True)

            if error_message:
                domain = self.api.client_domain()
                label = '{0}/{1}: {2}'.format(domain, username,
                                              error_message.lower())
                self.tracker.track_event(
                    'authenticate',
                    'login failed',
                    label=label,
                )
                self.text_password.setFocus()
                self.text_password.selectAll()
            self.sig_authentication_failed.emit()

        self.button_login.setDisabled(False)
        self.check_text()
        QApplication.restoreOverrideCursor()

    def open_url(self, url):
        """Open given url in the default browser and log the action."""
        self.tracker.track_event('content', 'click', url)
        self.sig_url_clicked.emit(url)
        QDesktopServices.openUrl(QUrl(url))
Пример #5
0
class PlotNameWidget(QWidget):
    """A widget to display the plot name, and edit and close buttons

    This widget is added to the table widget to support the renaming
    and close buttons, as well as the direct renaming functionality.
    """

    def __init__(self, presenter, plot_number, parent=None):
        super(PlotNameWidget, self).__init__(parent)

        self.presenter = presenter
        self.plot_number = plot_number

        self.mutex = QMutex()

        self.line_edit = QLineEdit(self.presenter.get_plot_name_from_number(plot_number))
        self.line_edit.setReadOnly(True)
        self.line_edit.setFrame(False)
        self.line_edit.setStyleSheet("* { background-color: rgba(0, 0, 0, 0); }")
        self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, True)
        self.line_edit.editingFinished.connect(self.rename_plot)

        shown_icon = get_icon('mdi.eye')
        self.hide_button = QPushButton(shown_icon, "")
        self.hide_button.setToolTip('Hide')
        self.hide_button.setFlat(True)
        self.hide_button.setMaximumWidth(self.hide_button.iconSize().width() * 5 / 3)
        self.hide_button.clicked.connect(self.toggle_visibility)

        rename_icon = get_icon('mdi.square-edit-outline')
        self.rename_button = QPushButton(rename_icon, "")
        self.rename_button.setToolTip('Rename')
        self.rename_button.setFlat(True)
        self.rename_button.setMaximumWidth(self.rename_button.iconSize().width() * 5 / 3)
        self.rename_button.setCheckable(True)
        self.rename_button.toggled.connect(self.rename_button_toggled)

        close_icon = get_icon('mdi.close')
        self.close_button = QPushButton(close_icon, "")
        self.close_button.setToolTip('Delete')
        self.close_button.setFlat(True)
        self.close_button.setMaximumWidth(self.close_button.iconSize().width() * 5 / 3)
        self.close_button.clicked.connect(lambda: self.close_pressed(self.plot_number))

        self.layout = QHBoxLayout()

        # Get rid of the top and bottom margins - the button provides
        # some natural margin anyway. Get rid of right margin and
        # reduce spacing to get buttons closer together.
        self.layout.setContentsMargins(5, 0, 0, 0)
        self.layout.setSpacing(0)

        self.layout.addWidget(self.line_edit)
        self.layout.addWidget(self.hide_button)
        self.layout.addWidget(self.rename_button)
        self.layout.addWidget(self.close_button)

        self.layout.sizeHint()
        self.setLayout(self.layout)

    def set_plot_name(self, new_name):
        """
        Sets the internally stored and displayed plot name
        :param new_name: The name to set
        """
        self.line_edit.setText(new_name)

    def close_pressed(self, plot_number):
        """
        Close the plot with the given name
        :param plot_number: The unique number in GlobalFigureManager
        """
        self.presenter.close_single_plot(plot_number)

    def rename_button_toggled(self, checked):
        """
        If the rename button is pressed from being unchecked then
        make the line edit item editable
        :param checked: True if the rename toggle is now pressed
        """
        if checked:
            self.toggle_plot_name_editable(True, toggle_rename_button=False)

    def toggle_plot_name_editable(self, editable, toggle_rename_button=True):
        """
        Set the line edit item to be editable or not editable. If
        editable move the cursor focus to the editable name and
        highlight it all.
        :param editable: If true make the plot name editable, else
                         make it read only
        :param toggle_rename_button: If true also toggle the rename
                                     button state
        """
        self.line_edit.setReadOnly(not editable)
        self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, not editable)

        # This is a sneaky way to avoid the issue of two calls to
        # this toggle method, by effectively disabling the button
        # press in edit mode.
        self.rename_button.setAttribute(Qt.WA_TransparentForMouseEvents, editable)

        if toggle_rename_button:
            self.rename_button.setChecked(editable)

        if editable:
            self.line_edit.setFocus()
            self.line_edit.selectAll()
        else:
            self.line_edit.setSelection(0, 0)

    def toggle_visibility(self):
        """
        Calls the presenter to hide the selected plot
        """
        self.presenter.toggle_plot_visibility(self.plot_number)

    def set_visibility_icon(self, is_shown):
        """
        Change the widget icon between shown and hidden
        :param is_shown: True if plot is shown, false if hidden
        """
        if is_shown:
            self.hide_button.setIcon(get_icon('mdi.eye'))
            self.hide_button.setToolTip('Hide')
        else:
            self.hide_button.setIcon(get_icon('mdi.eye', 'lightgrey'))
            self.hide_button.setToolTip('Show')

    def rename_plot(self):
        """
        Called when the editing is finished, gets the presenter to
        do the real renaming of the plot
        """
        self.presenter.rename_figure(self.plot_number, self.line_edit.text())
        self.toggle_plot_name_editable(False)
Пример #6
0
 def createEditor(self, parent: QWidget, option: QStyleOptionViewItem,
                  index: QModelIndex) -> QWidget:
     editor = QLineEdit(parent=parent)
     editor.setAlignment(Qt.AlignCenter)
     editor.selectAll()
     return editor
class LineEditDoubleValidatorTest(unittest.TestCase):

    def setUp(self):
        self.initial_value = "0.0"

        self.line_edit = QLineEdit(self.initial_value)
        self.line_edit.show()

    def tearDown(self):
        self.assertTrue(self.line_edit.close())

    def test_initialization_of_the_validator_does_not_raise(self):
        try:
            _ = self._create_validator()
        except Exception as ex:
            self.fail(f"An exception was thrown when attempting to create the validator {ex}.")

    def test_that_editing_the_line_edit_with_a_valid_double_will_work(self):
        valid_entry = "1.0"

        self._validator = self._create_validator()
        self._edit_line_edit(valid_entry)

        self.assertEqual(self.line_edit.text(), valid_entry)

    def test_that_editing_the_line_edit_with_a_valid_double_in_e_notation_will_work(self):
        valid_entry = "3e5"

        self._validator = self._create_validator()
        self._edit_line_edit(valid_entry)

        self.assertEqual(self.line_edit.text(), valid_entry)

    def test_that_editing_the_line_edit_with_an_empty_string_will_reset_to_the_last_valid_value(self):
        invalid_entry = ""

        self._validator = self._create_validator()
        self._edit_line_edit(invalid_entry)

        self.assertEqual(self.line_edit.text(), self.initial_value)

    def test_that_editing_the_line_edit_with_broken_e_notation_will_reset_to_the_last_valid_value(self):
        invalid_entry = "e"

        self._validator = self._create_validator()
        self._edit_line_edit(invalid_entry)

        self.assertEqual(self.line_edit.text(), self.initial_value)

    def _create_validator(self):
        validator = LineEditDoubleValidator(self.line_edit, self.initial_value)
        self.line_edit.setValidator(validator)
        return validator

    def _edit_line_edit(self, key_entry):
        QTest.mouseClick(self.line_edit, Qt.LeftButton)
        self.line_edit.selectAll()
        QTest.keyClick(self.line_edit, Qt.Key_Backspace)
        QTest.keyClicks(self.line_edit, key_entry)
        QTest.keyClick(self.line_edit, Qt.Key_Enter)
        QApplication.sendPostedEvents()