Sat, 01 Sep 2018 20:18:11 +0200
Started to implement the connection options dialog and methods to specify these connection options connecting to the server.
--- a/MqttMonitor/MqttClient.py Fri Aug 31 19:28:28 2018 +0200 +++ b/MqttMonitor/MqttClient.py Sat Sep 01 20:18:11 2018 +0200 @@ -79,6 +79,77 @@ self.__mqttClient.on_unsubscribe = \ lambda client, userdata, mid: self.onUnsubscribe.emit(mid) + def reinitialise(self, clientId="", cleanSession=True, userdata=None): + """ + Public method to reinitialize the client with given data. + + @param clientId ID to be used for the client + @type str + @param cleanSession flag indicating to start a clean session + @type bool + @param userdata user data + @type any + """ + self.__mqttClient.reinitialise( + client_id=clientId, clean_session=cleanSession, userdata=userdata) + + def setMaxInflightMessages(self, inflight=20): + """ + Public method to set the maximum number of messages with QoS > 0 that + can be part way through their network flow at once. + + @param inflight maximum number of messages in flight + @type int + """ + self.__mqttClient.max_inflight_messages_set(inflight) + + def setMaxQueuedMessages(self, queueSize=0): + """ + Public method to set the maximum number of messages with QoS > 0 that + can be pending in the outgoing message queue. + + @param queueSize maximum number of queued messages (0 = unlimited) + @type int + """ + self.__mqttClient.max_queued_messages_set(queueSize) + + def setUserCredentials(self, username, password=None): + """ + Public method to set the user name and optionally the password. + + @param username user name to be set + @type str + @param password optional password + @type str + """ + self.__mqttClient.username_pw_set(username, password=password) + + def setUserData(self, userdata): + """ + Public method to set the user data. + + @param userdata user data + @type any + """ + self.__mqttClient.user_data_set(userdata) + + def setLastWill(self, topic, payload=None, qos=0, retain=False): + """ + Public method to set the last will of the client. + + @param topic topic the will message should be published on + @type str + @param payload message to send as a will + @type str, bytes, int or float + @param qos quality of service level to use for the will + @type int, one of 0, 1 or 2 + @param retain flag indicating to set as the "last known good"/retained + message for the will topic + @type bool + """ + self.__mqttClient.will_set(topic, payload=payload, qos=qos, + retain=retain) + def startLoop(self): """ Public method to start the MQTT client loop. @@ -115,6 +186,85 @@ if not self.__loopStarted: self.startLoop() + def connectToServerWithOptions(self, host, port=1883, bindAddress="", + options=None): + """ + Public method to connect to a remote MQTT broker. + + @param host host name or IP address of the remote broker + @type str + @param port network port of the server host to connect to (default: + 1883, using TLS: 8883) + @type int + @param bindAddress IP address of a local network interface to bind + this client to + @type str + @param options dictionary containing the connection options. This + dictionary should contain the keys "ClientId", "Keepalive", + "CleanSession", "Username", "Password", "WillTopic", "WillMessage", + "WillQos", "WillRetain" + @type dict + """ + if options: + parametersDict = self.defaultConnectionOptions() + parametersDict.update(options) + + # step 1: reinitialize to set the client ID and clean session flag + self.reinitialise( + clientId=parametersDict["ClientId"], + cleanSession=parametersDict["CleanSession"] + ) + + # step 2: set username and password + if parametersDict["Username"]: + if parametersDict["Password"]: + self.setUserCredentials(parametersDict["Username"], + parametersDict["Password"]) + else: + self.setUserCredentials(parametersDict["Username"]) + + # step 3: set last will data + if parametersDict["WillTopic"]: + if parametersDict["WillMessage"]: + willMessage = parametersDict["WillMessage"] + else: + # empty message to clear the will + willMessage = None + self.setLastWill(parametersDict["WillTopic"], + willMessage, + parametersDict["WillQos"], + parametersDict["WillRetain"]) + + # step 4: connect to server + self.connectToServer(host, port=port, + keepalive=parametersDict["Keepalive"]) + else: + keepalive = self.defaultConnectionOptions["Keepalive"] + self.connectToServer(host, port=port, keepalive=keepalive, + bindAddress=bindAddress) + + def defaultConnectionOptions(self): + """ + Public method to get a connection options dictionary with default + values. + + @return dictionary containing the default connection options. It has + the keys "ClientId", "Keepalive", "CleanSession", "Username", + "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain" + @rtype dict + """ + return { + "ClientId": "ERIC6_MQTT_MONITOR_CLIENT", + "Keepalive": 60, + "CleanSession": True, + "Username": "", + "Password": "", + "WillTopic": "", + "WillMessage": "", + "WillQos": 0, + "WillRetain": False, + } + def reconnectToServer(self): """ Public method to reconnect the client with the same parameters.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MqttMonitor/MqttConnectionOptionsDialog.py Sat Sep 01 20:18:11 2018 +0200 @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +""" +Module implementing a dialog to enter MQTT connection options. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton + +from .Ui_MqttConnectionOptionsDialog import Ui_MqttConnectionOptionsDialog + + +class MqttConnectionOptionsDialog(QDialog, Ui_MqttConnectionOptionsDialog): + """ + Class implementing a dialog to enter MQTT connection options. + """ + def __init__(self, client, options=None, parent=None): + """ + Constructor + + @param client reference to the MQTT client object + @type MqttClient + @param options dictionary containing the connection options to + populate the dialog with + @@type dict + @param parent reference to the parent widget + @type QWidget + """ + super(MqttConnectionOptionsDialog, self).__init__(parent) + self.setupUi(self) + + self.__client = client + + self.__populateDefaults(options=options) + + + @pyqtSlot(QAbstractButton) + def on_buttonBox_clicked(self, button): + """ + Private slot to handle the press of a button box button. + + @param button button that has been pressed + @type QAbstractButton + """ + if button == self.buttonBox.button(QDialogButtonBox.RestoreDefaults): + self.__populateDefaults(options=None) + + def __populateDefaults(self, options=None): + """ + Private method to populate the dialog. + + If no options dictionary is given, the dialog will be populated with + default values. + """ + if options is None: + options = self.__client.defaultConnectionOptions() + + # general + self.clientIdEdit.setText(options["ClientId"]) + self.keepaliveSpinBox.setValue(options["Keepalive"]) + self.cleanSessionCheckBox.setChecked(options["CleanSession"]) + + # user credentials + self.usernameEdit.setText(options["Username"]) + self.passwordEdit.setText(options["Password"]) + + # last will and testament + self.willQosSpinBox.setValue(options["WillQos"]) + self.willRetainCheckBox.setChecked(options["WillRetain"]) + self.willTopicEdit.setText(options["WillTopic"]) + self.willMessageEdit.setPlainText(options["WillMessage"]) + + def getConnectionOptions(self): + """ + Public method get the entered connection options. + + @return dictionary containing the connection options. It has the keys + "ClientId", "Keepalive", "CleanSession", "Username", "Password", + "WillTopic", "WillMessage", "WillQos", "WillRetain" + @rtype tuple of (int, dict) + """ + return { + "ClientId": self.clientIdEdit.text(), + "Keepalive": self.keepaliveSpinBox.value(), + "CleanSession": self.cleanSessionCheckBox.isChecked(), + "Username": self.usernameEdit.text(), + "Password": self.passwordEdit.text(), + "WillTopic": self.willTopicEdit.text(), + "WillMessage": self.willMessageEdit.toPlainText(), + "WillQos": self.willQosSpinBox.value(), + "WillRetain": self.willRetainCheckBox.isChecked(), + }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MqttMonitor/MqttConnectionOptionsDialog.ui Sat Sep 01 20:18:11 2018 +0200 @@ -0,0 +1,274 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MqttConnectionOptionsDialog</class> + <widget class="QDialog" name="MqttConnectionOptionsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>MQTT Connection Options</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>General</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Client ID:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="E5ClearableLineEdit" name="clientIdEdit"> + <property name="toolTip"> + <string>Enter the ID string for this client</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Keep Alive Interval:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QSpinBox" name="keepaliveSpinBox"> + <property name="toolTip"> + <string>Enter the keep alive interval in seconds</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="maximum"> + <number>300</number> + </property> + <property name="singleStep"> + <number>5</number> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>148</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QCheckBox" name="cleanSessionCheckBox"> + <property name="text"> + <string>Clean Session</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>User Credentials</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>User Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="E5ClearableLineEdit" name="usernameEdit"> + <property name="toolTip"> + <string>Enter the user name</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Password:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="E5ClearableLineEdit" name="passwordEdit"> + <property name="toolTip"> + <string>Enter the password</string> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Last Will and Testament</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>QoS:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="willQosSpinBox"> + <property name="toolTip"> + <string>Enter the desired QoS value</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="maximum"> + <number>2</number> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="willRetainCheckBox"> + <property name="toolTip"> + <string>Select to retain the last will message</string> + </property> + <property name="text"> + <string>Retain</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLineEdit" name="willTopicEdit"> + <property name="toolTip"> + <string>Enter the topic of the last will</string> + </property> + </widget> + </item> + <item> + <widget class="QPlainTextEdit" name="willMessageEdit"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>300</height> + </size> + </property> + <property name="toolTip"> + <string>Enter the last will message to be sent</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>clientIdEdit</tabstop> + <tabstop>keepaliveSpinBox</tabstop> + <tabstop>cleanSessionCheckBox</tabstop> + <tabstop>usernameEdit</tabstop> + <tabstop>passwordEdit</tabstop> + <tabstop>willQosSpinBox</tabstop> + <tabstop>willRetainCheckBox</tabstop> + <tabstop>willTopicEdit</tabstop> + <tabstop>willMessageEdit</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>MqttConnectionOptionsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>MqttConnectionOptionsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- a/MqttMonitor/MqttMonitorWidget.py Fri Aug 31 19:28:28 2018 +0200 +++ b/MqttMonitor/MqttMonitorWidget.py Sat Sep 01 20:18:11 2018 +0200 @@ -19,7 +19,7 @@ from PyQt5.QtCore import pyqtSlot, QTimer from PyQt5.QtGui import QTextCursor -from PyQt5.QtWidgets import QWidget +from PyQt5.QtWidgets import QWidget, QDialog from E5Gui import E5MessageBox @@ -62,8 +62,9 @@ self.brokerWidget.setCurrentIndex(0) self.connectButton.setIcon(UI.PixmapCache.getIcon("ircConnect.png")) - self.brokerComboBox.addItems( - self.__plugin.getPreferences("RecentBrokers")) + self.brokerConnectionOptionsButton.setIcon(UI.PixmapCache.getIcon( + os.path.join("MqttMonitor", "icons", "connectionOptions.png"))) + self.__populateBrokerComboBoxes() self.brokerStatusLabel.hide() self.subscribeButton.setIcon(UI.PixmapCache.getIcon("plus.png")) @@ -78,6 +79,8 @@ self.__updatePublishTopicComboBox() self.publishButton.setEnabled(False) + self.__connectionOptions = None + prefix = MqttMonitorWidget.BrokerStatusTopicPrefix self.__statusLabelMapping = { # broker @@ -148,6 +151,7 @@ """ if rc == 0: self.__connectedToBroker = True + self.__connectionOptions = None msg = mqttConnackMessage(rc) self.__flashBrokerStatusLabel(msg) @@ -291,6 +295,17 @@ self.connectButton.setEnabled(True) @pyqtSlot() + def on_brokerConnectionOptionsButton_clicked(self): + """ + Private slot to show a dialog to modify connection options. + """ + from .MqttConnectionOptionsDialog import MqttConnectionOptionsDialog + dlg = MqttConnectionOptionsDialog( + self.__client, self.__connectionOptions, parent=self) + if dlg.exec_() == QDialog.Accepted: + self.__connectionOptions = dlg.getConnectionOptions() + + @pyqtSlot() def on_connectButton_clicked(self): """ Private slot to handle a connect or disconnect request. @@ -299,10 +314,15 @@ self.__client.disconnectFromServer() else: host = self.brokerComboBox.currentText() + port = self.brokerPortComboBox.currentText().strip() + try: + port = int(port) + except ValueError: + # use standard port at 1883 + port = 1883 if host: - self.__addBrokerToRecent(host) - self.__client.connectToServer(host) - # use standard port at 1883 + self.__addBrokerToRecent(host, port) + self.__client.connectToServer(host, port=port) @pyqtSlot(str) def on_subscribeTopicEdit_textChanged(self, topic): @@ -371,6 +391,10 @@ qos = self.publishQosSpinBox.value() retain = self.publishRetainCheckBox.isChecked() payloadStr = self.publishPayloadEdit.toPlainText() + if not payloadStr: + # use empty string together with the retain flag to clean + # a retained message by sending None instead + payloadStr = None msgInfo = self.__client.publish(topic, payloadStr, qos, retain) if msgInfo.rc == 0: @@ -408,22 +432,50 @@ ## Utility methods ####################################################################### - def __addBrokerToRecent(self, host): + def __addBrokerToRecent(self, host, port): """ Private method to add a host name to the list of recently connected brokers. @param host host name of broker @type str + @param port port number of the connection + @type int """ - brokerList = self.__plugin.getPreferences("RecentBrokers") - if host in brokerList: - brokerList.remove(host) - brokerList.insert(0, host) - self.__plugin.setPreferences("RecentBrokers", brokerList) + brokerList = self.__plugin.getPreferences("RecentBrokersWithPort") + hostAndPort = [host, port] + if hostAndPort in brokerList: + brokerList.remove(hostAndPort) + brokerList.insert(0, hostAndPort) + self.__plugin.setPreferences("RecentBrokersWithPort", brokerList) + + self.__populateBrokerComboBoxes() + + def __populateBrokerComboBoxes(self): + """ + Private method to populate the broker name and port combo boxes. + """ + brokerList = self.__plugin.getPreferences("RecentBrokersWithPort") + # step 1: clear combo boxes self.brokerComboBox.clear() - self.brokerComboBox.addItems(brokerList) + self.brokerPortComboBox.clear() + + # step 2a: populate the broker name list + self.brokerComboBox.addItems([b[0].strip() for b in brokerList]) + + # step 2b: populate the broker ports list + if brokerList: + currentPort = brokerList[0][1] + else: + currentPort = 1883 + currentPortStr = "{0:5}".format(currentPort) + portsSet = set([b[1] for b in brokerList]) + portsSet.update([1883, 8883]) + self.brokerPortComboBox.addItems( + sorted(["{0:5}".format(p) for p in portsSet])) + index = self.brokerPortComboBox.findText(currentPortStr) + self.brokerPortComboBox.setCurrentIndex(index) def __updateUnsubscribeTopicComboBox(self): """
--- a/MqttMonitor/MqttMonitorWidget.ui Fri Aug 31 19:28:28 2018 +0200 +++ b/MqttMonitor/MqttMonitorWidget.ui Sat Sep 01 20:18:11 2018 +0200 @@ -41,7 +41,7 @@ </property> <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0"> - <widget class="QComboBox" name="brokerComboBox"> + <widget class="E5ClearableComboBox" name="brokerComboBox"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> @@ -56,7 +56,7 @@ </property> </widget> </item> - <item row="0" column="1"> + <item row="0" column="3"> <widget class="QToolButton" name="connectButton"> <property name="toolTip"> <string>Press to connect to/disconnect from the broker</string> @@ -66,13 +66,33 @@ </property> </widget> </item> - <item row="1" column="0" colspan="2"> + <item row="1" column="0" colspan="4"> <widget class="QLabel" name="brokerStatusLabel"> <property name="wordWrap"> <bool>true</bool> </property> </widget> </item> + <item row="0" column="1"> + <widget class="QComboBox" name="brokerPortComboBox"> + <property name="toolTip"> + <string>Enter the broker port to connect to</string> + </property> + <property name="editable"> + <bool>true</bool> + </property> + <property name="sizeAdjustPolicy"> + <enum>QComboBox::AdjustToContents</enum> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QToolButton" name="brokerConnectionOptionsButton"> + <property name="toolTip"> + <string>Press to open a dialog to enter connection options</string> + </property> + </widget> + </item> </layout> </widget> </item> @@ -1127,6 +1147,8 @@ </customwidgets> <tabstops> <tabstop>brokerComboBox</tabstop> + <tabstop>brokerPortComboBox</tabstop> + <tabstop>brokerConnectionOptionsButton</tabstop> <tabstop>connectButton</tabstop> <tabstop>brokerWidget</tabstop> <tabstop>subscribeTopicEdit</tabstop> @@ -1134,6 +1156,15 @@ <tabstop>subscribeButton</tabstop> <tabstop>unsubscribeTopicComboBox</tabstop> <tabstop>unsubscribeButton</tabstop> + <tabstop>publishTopicComboBox</tabstop> + <tabstop>publishQosSpinBox</tabstop> + <tabstop>publishRetainCheckBox</tabstop> + <tabstop>publishPayloadEdit</tabstop> + <tabstop>publishButton</tabstop> + <tabstop>messagesEdit</tabstop> + <tabstop>pushButton</tabstop> + <tabstop>brokerStatusButton</tabstop> + <tabstop>scrollArea</tabstop> </tabstops> <resources/> <connections>
--- a/PluginMqttMonitor.e4p Fri Aug 31 19:28:28 2018 +0200 +++ b/PluginMqttMonitor.e4p Sat Sep 01 20:18:11 2018 +0200 @@ -17,12 +17,14 @@ <Eol index="1"/> <Sources> <Source>MqttMonitor/MqttClient.py</Source> + <Source>MqttMonitor/MqttConnectionOptionsDialog.py</Source> <Source>MqttMonitor/MqttMonitorWidget.py</Source> <Source>MqttMonitor/__init__.py</Source> <Source>PluginMqttMonitor.py</Source> <Source>__init__.py</Source> </Sources> <Forms> + <Form>MqttMonitor/MqttConnectionOptionsDialog.ui</Form> <Form>MqttMonitor/MqttMonitorWidget.ui</Form> </Forms> <Others>
--- a/PluginMqttMonitor.py Fri Aug 31 19:28:28 2018 +0200 +++ b/PluginMqttMonitor.py Sat Sep 01 20:18:11 2018 +0200 @@ -10,6 +10,7 @@ from __future__ import unicode_literals import os +import json from PyQt5.QtCore import Qt, QObject, QTranslator, QCoreApplication from PyQt5.QtGui import QKeySequence @@ -92,7 +93,7 @@ self.__initialize() self.__defaults = { - "RecentBrokers": [], + "RecentBrokersWithPort": "[]", # JSON formatted empty list } self.__translator = None @@ -208,8 +209,8 @@ @param key the key of the value to get @return the requested setting """ - if key in ["RecentBrokers"]: - return Preferences.toList(Preferences.Prefs.settings.value( + if key in ["RecentBrokersWithPort"]: + return json.loads(Preferences.Prefs.settings.value( self.PreferencesKey + "/" + key, self.__defaults[key])) else: return Preferences.Prefs.settings.value( @@ -222,8 +223,12 @@ @param key the key of the setting to be set (string) @param value the value to be set """ - Preferences.Prefs.settings.setValue( - self.PreferencesKey + "/" + key, value) + if key in ["RecentBrokersWithPort"]: + Preferences.Prefs.settings.setValue( + self.PreferencesKey + "/" + key, json.dumps(value)) + else: + Preferences.Prefs.settings.setValue( + self.PreferencesKey + "/" + key, value) # # eflag: noqa = M801