{"action":"created","comment":{"url":"https://api.github.com/repos/slaclab/pydm/pulls/comments/182621202","pull_request_review_id":113451992,"id":182621202,"diff_hunk":"@@ -0,0 +1,566 @@\n+# coding: utf-8\n+# Unit Tests for the PyDMPushButton Widget\n+\n+\n+import os\n+import pytest\n+import hashlib\n+import numpy as np\n+\n+import logging\n+\n+from ...PyQt.QtCore import QSize\n+from ...PyQt.QtGui import QColor, QInputDialog, QMessageBox\n+from ...widgets.base import PyDMWidget\n+from ...widgets.pushbutton import PyDMPushButton\n+from ...utilities.iconfont import IconFont\n+\n+\n+current_dir = os.path.abspath(os.path.dirname(__file__))\n+\n+\n+# --------------------\n+# POSITIVE TEST CASES\n+# --------------------\n+\n+@pytest.mark.parametrize(\"label, press_value, relative, init_channel, icon_font_name, icon_color\", [\n+    # Testing different types of press value\n+    (\"Test Button\", \"Test Button PressValue\", True, \"CA://MTEST\", \"cogs\", QColor(255, 255, 255)),\n+    (\"Test Button\", 42, True, \"CA://MTEST\", \"cogs\", QColor(255, 255, 255)),\n+    (\"Test Button\", 42.42, True, \"CA://MTEST\", \"cogs\", QColor(255, 255, 255)),\n+\n+    # Testing combinations of parameters\n+    (\"Test Button\", \"Test Button PressValue\", True, \"CA://MTEST\", None, None),\n+    (\"Test Button\", \"Test Button PressValue\", True, \"CA://MTEST\", \"\", None),\n+    (\"Test Button\", \"Test Button PressValue\", True, \"CA://MTEST\", None, \"\"),\n+    (\"Test Button\", \"Test Button PressValue\", True, \"CA://MTEST\", \"cog\", QColor(255, 0, 0)),\n+    (\"Test Button\", \"Test Button PressValue\", True, \"CA://MTEST\", \"cogs\", QColor(255, 255, 255)),\n+\n+    (\"\", \"Test Button PressValue\", True, \"\", \"fast-forward\", QColor(0, 0, 0)),\n+    (\"Test Button\", \"\", True, None, \"fast-forward\", QColor(0, 0, 255)),\n+    (\"\", \"\", True, \"CA://MTEST\", \"fast-forward\", QColor(0, 255, 0)),\n+    (\"\", None, True, \"CA://MTEST\", \"fast-forward\", QColor(255, 0, 0)),\n+\n+    (\"Test Button\", \"Test Button PressValue\", False, \"CA://MTEST\", None, None),\n+    (\"Test Button\", \"Test Button PressValue\", False, \"CA://MTEST\", \"check\", QColor(10, 20, 30)),\n+    (\"\", \"Test Button PressValue\", False, \"\", \"check\", QColor(10, 20, 30)),\n+    (\"Test Button\", \"\", False, None, \"check\", QColor(10, 20, 30)),\n+    (\"\", \"\", False, \"CA://MTEST\", \"check\", QColor(10, 20, 30)),\n+    (\"\", None, False, \"CA://MTEST\", \"check\", QColor(10, 20, 30)),\n+\n+    # Testing variations of empty parameters\n+    (None, \"\", True, \"\", None, None),\n+    (None, None, True, None, None, None),\n+    (None, \"\", False, \"\", None, None),\n+    (None, None, False, None, None, None),\n+])\n+def test_construct(qtbot, label, press_value, relative, init_channel, icon_font_name, icon_color):\n+    \"\"\"\n+    Test the basic instantiation of the widget.\n+\n+    Expectations:\n+    1. The widget can be created successfully with no arguments, some arguments, or all of them\n+    2. The settings will be retained with the created widget instance.\n+\n+    Parameters\n+    ----------\n+    qtbot : fixture\n+        Window for widget testing\n+    label : str\n+        The name of the button widget\n+    press_value : int, float, str\n+        The value to be sent when the button is pressed\n+    relative : bool\n+        Send a relative value (added with the existing value) if True, or just the new value otherwise\n+    init_channel : str\n+        The Channel ID to send the value to\n+    icon_font_name : str\n+        The name of a character from a font set to use as an icon (currently using \"fontawesome.ttf\")\n+    icon_color : QColor\n+        The RGB color value for the icon\n+    \"\"\"\n+    icon = None\n+    if icon_font_name:\n+        icon = IconFont().icon(icon_font_name, icon_color)\n+\n+    pydm_pushbutton = PyDMPushButton(label=label, pressValue=press_value, relative=relative,\n+                                     init_channel=init_channel, icon=icon)\n+    pydm_pushbutton.icon()\n+    qtbot.addWidget(pydm_pushbutton)\n+\n+    assert pydm_pushbutton.text() == label if label else pydm_pushbutton.text() == \"\"\n+    assert pydm_pushbutton.relativeChange == relative\n+\n+    if press_value:\n+        assert pydm_pushbutton.pressValue == str(press_value)\n+    else:\n+        assert pydm_pushbutton.pressValue == \"None\" if press_value is None else pydm_pushbutton.pressValue == \"\"\n+\n+    if init_channel:\n+        assert pydm_pushbutton.channel == init_channel\n+    else:\n+        assert pydm_pushbutton.channel == \"None\" if init_channel is None else pydm_pushbutton.channel == \"\"\n+\n+    if icon:\n+        size = QSize(30, 30)\n+        icon_pixmap = icon.pixmap(size)\n+        button_icon_pixmap = pydm_pushbutton.icon().pixmap(size)\n+        assert icon_pixmap.toImage() == button_icon_pixmap.toImage()\n+\n+    assert pydm_pushbutton.showConfirmDialog == False\n+    assert pydm_pushbutton.confirmMessage == PyDMPushButton.DEFAULT_CONFIRM_MESSAGE\n+    assert pydm_pushbutton.passwordProtected == False\n+    assert pydm_pushbutton.password == \"\"\n+    assert pydm_pushbutton.protectedPassword == \"\"\n+\n+\n+@pytest.mark.parametrize(\"password_is_protected\", [\n+    (True),\n+    (False),\n+])\n+def test_password_protected(qtbot, password_is_protected):\n+    \"\"\"\n+    Test that the password protected property is properly set. The password protected is flag that, if set to True,\n+    will prompt the user to enter a password before the button can be pushed to send the data\n+\n+    Expectation:\n+    The protect password property flag retains the same Boolean value as provided.\n+\n+    Parameters\n+    ----------\n+    qtbot : fixture\n+        Window for widget testing\n+\n+    password_is_protected : bool\n+        The flag to set the password to protected (True), or not (otherwise)\n+    \"\"\"\n+    pydm_pushbutton = PyDMPushButton()\n+    qtbot.addWidget(pydm_pushbutton)\n+\n+    pydm_pushbutton.passwordProtected = password_is_protected\n+    assert pydm_pushbutton._password_protected == password_is_protected\n+\n+\n+@pytest.mark.parametrize(\"relative_choice\", [\n+    (True),\n+    (False),\n+    (False),\n+    (True),\n+])\n+def test_relative_change(qtbot, relative_choice):\n+    \"\"\"\n+    Test that the relative attribute of the button.\n+    Expectations:\n+    1. The button retains the relative attribute setting.\n+    2. The button retains the relative attribute setting correctly even with successions of the same relative value\n+\n+    Parameters\n+    ----------\n+    qtbot : fixture\n+        Window for widget testing\n+    relative_choice : bool\n+        Send a relative value (added with the existing value) if True, or just the new value otherwise\n+    \"\"\"\n+    pydm_pushbutton = PyDMPushButton()\n+    qtbot.addWidget(pydm_pushbutton)\n+\n+    pydm_pushbutton.relativeChange = relative_choice\n+    assert pydm_pushbutton.relativeChange == relative_choice\n+\n+\n+@pytest.mark.parametrize(\"password_protected, plain_text_password\", [\n+    (True, \"$L4C_p4$$wd\"),\n+    (True, \"\"),\n+    (True, None),\n+    (False, \"$L4C_p4$$wd\"),\n+    (False, \"\"),\n+    (False, None)\n+])\n+def test_set_password(qtbot, password_protected, plain_text_password):\n+    \"\"\"\n+    Test the widget's password encryption mechanism.\n+    Expectations:\n+    1. The widget will retain the attribute specifying whether the button requires a password\n+    2. The widget's encrypted password must be the same as the expected encrypted password, i.e. if the encryption\n+        method changes, this test will have to be changed, and the new encryption method will also be reviewed\n+    3. Currently, unless the provided (plain text) password is empty, it will always be encrypted, no matter whether\n+        the passwordProtected attribute is True or False\n+\n+    Parameters\n+    ----------\n+    qtbot : fixture\n+        Window for widget testing\n+    password_protected : bool\n+        True if the Push Button requires a user-provided password before sending the data to the channel; False\n+        otherwise\n+    plain_text_password : str\n+        The password to be required by the Push Button before sending the data to the channel\n+    \"\"\"\n+    pydm_pushbutton = PyDMPushButton()\n+    qtbot.addWidget(pydm_pushbutton)\n+\n+    pydm_pushbutton.passwordProtected = password_protected\n+\n+    # Produce the expected encrypted password\n+    expected_encrypted_password = \"\"\n+    if plain_text_password:\n+        sha = hashlib.sha256()\n+        sha.update(plain_text_password.encode())\n+        expected_encrypted_password = sha.hexdigest()\n+\n+    # Provide the plain text password to the Push Button, and expect to have the encrypted password via\n+    # the passwordProtected attribute\n+    # This takes a plain-text password, encrypts it, and stores it to the widget\n+    pydm_pushbutton.password = plain_text_password\n+\n+    encrypted_password = pydm_pushbutton.protectedPassword\n+\n+    assert pydm_pushbutton._password_protected == password_protected\n+    assert encrypted_password == expected_encrypted_password\n+\n+\n+@pytest.mark.parametrize(\"is_widget_protected_with_password, plain_text_password, input_dialog_status,\"\n+                         \"expected_validation_status\", [\n+    (True, \"$L4C_p4$$wd\", True, True),\n+    (False, \"$L4C_p4$$wd\", True, True),\n+\n+    (True, \"\", True, False),\n+    (False, \"\", True, True),\n+\n+    (True, \"$L4C_p4$$wd\", False, False),\n+    (False, \"$L4C_p4$$wd\", False, True),\n+\n+    (True, \"Wrong_Password\", True, False),\n+    (False, \"Wrong_Password\", True, False),\n+\n+    (True, \"Wrong_Password\", False, False),\n+    (False, \"Wrong_Password\", False, False),\n+])\n+def test_validate_password(qtbot, monkeypatch, is_widget_protected_with_password, plain_text_password,\n+                           input_dialog_status, expected_validation_status):\n+    \"\"\"\n+    Test password validation.\n+    Expectations:\n+    1. The user-provided password to the QInputDialog produces the same message disgest like that of the existing\n+        password's message digest.\n+    2. If the user-provided password to the QInputDialog is empty, or if the QInputDialog returns False, the validation\n+        must fail\n+    3. For the two expectations above to hold, the passwordProtected attribute must be True. If not, the validation\n+        will pass even if the user-provided password is incorrect.\n+    Parameters\n+    ----------\n+    qtbot : fixture\n+        Window for widget testing\n+    monkeypatch : fixture\n+        To override dialog behaviors\n+    is_widget_protected_with_password : bool\n+        True if the widget requires a password, False otherwise\n+    plain_text_password : str\n+        The user-input password, in plain text, i.e. not encrypted\n+    input_dialog_status : bool\n+        The True/False status returned by the QInputDialog. True means the dialog has successfully received the\n+        user-input password, and the user has clicked on its OK button; False otherwise\n+    expected_validation_status : bool\n+        The expected validation result. True means the user-input is validated; False otherwise\n+    \"\"\"\n+    pydm_pushbutton = PyDMPushButton()\n+    qtbot.addWidget(pydm_pushbutton)\n+\n+    pydm_pushbutton.passwordProtected = is_widget_protected_with_password\n+    if is_widget_protected_with_password:\n+        # We set the protected password first, and mock the QInputDialog accepting the user-entered password.\n+        # Then, we mock different scenarios with the input dialog's returning False (the user clicking on Cancel),\n+        # or the user-entered password is not matching\n+        pydm_pushbutton.password = \"$L4C_p4$$wd\"\n+        monkeypatch.setattr(QInputDialog, 'getText', lambda *args: (plain_text_password, input_dialog_status))\n+        if not expected_validation_status:\n+            # Turn off the \"Invalid password\" dialog so that it won't interfere with the test\n+            monkeypatch.setattr(QMessageBox, 'exec_', lambda *args: (False,))\n+\n+        validation_status = pydm_pushbutton.validate_password()\n+        assert validation_status == expected_validation_status\n+\n+\n+@pytest.mark.parametrize(\"press_value, updated_value, is_password_protected, show_confirm_dialog,\"\n+                         \"confirm_message, confirm_dialog_response, is_password_validated, is_value_relative,\"\n+                         \"channel_type\", [\n+    (\"123\", 345, True, True, \"Êtes-vous sur de vouloir continuer?\", QMessageBox.Yes, True, True, int),\n+    (\"123\", 345, True, True, \"\", QMessageBox.Yes, True, True, int),\n+    (\"123.345\", 345.678, True, True, \"\", QMessageBox.Yes, True, True, float),\n+    (\"123.345\", \"345.678\", True, True, \"\", QMessageBox.Yes, True, True, str),\n+\n+    (\"123\", 345, False, True, \"\", QMessageBox.Yes, True, True, int),\n+    (\"123\", 345, True, False, \"\", QMessageBox.Yes, True, True, int),\n+    (\"123\", 345, False, False, \"\", QMessageBox.Yes, True, True, int),\n+\n+    (\"123\", 345, True, True, \"\", QMessageBox.No, True, True, int),\n+    (\"123\", 345, False, True, \"\", QMessageBox.No, True, True, int),\n+\n+    (\"123\", 345, True, True, \"Êtes-vous sur de vouloir continuer?\", QMessageBox.Yes, True, False, int),\n+    (\"123\", 345, True, True, \"\", QMessageBox.Yes, True, False, int),\n+    (\"123.345\", 345.678, True, True, \"\", QMessageBox.Yes, True, False, float),\n+    (\"123.345\", \"345.678\", True, True, \"\", QMessageBox.Yes, True, False, str),\n+\n+    (\"123\", 345, True, True, \"Êtes-vous sur de vouloir continuer?\", QMessageBox.Yes, False, True, int),\n+    (\"123\", 345, True, True, \"\", QMessageBox.Yes, False, False, int),\n+    (None, 345, True, True, \"Êtes-vous sur de vouloir continuer?\", QMessageBox.Yes, True, True, int),\n+    (\"123\", None, True, True, \"Êtes-vous sur de vouloir continuer?\", QMessageBox.Yes, True, True, int),\n+])\n+def test_send_value(qtbot, monkeypatch, signals, press_value, updated_value, is_password_protected, show_confirm_dialog,\n+                    confirm_message, confirm_dialog_response, is_password_validated, is_value_relative, channel_type):\n+    \"\"\"\n+    Test sending a new value to the channel.\n+    Expectations:\n+    1. The new value will be sent to the channel and converted to the current channel type\n+    2. If the relativeChange attribute is set to True, the channel's value is the total of the current value and the\n+        new value. If the same attribute is set to False, or the channel type is str, the channel's value is just the\n+        updated value\n+    3. Scenarios involved whether the confirm dialog's \"Yes\" or \"No\" button will be tested. For the the dialog's\n+        question, we'll also test an empty question or a user-supplied question being provided\n+    4. Scenarios involved whether password validation passes or fails will also be tested\n+\n+    Parameters\n+    ----------\n+    qtbot : fixture\n+        Window for widget testing\n+    monkeypatch : fixture\n+        To override dialog behaviors\n+    signals : fixture\n+        The signals fixture, which provides access signals to be bound to the appropriate slots\n+    press_value : int, float, str\n+        The current value the button is holding\n+    updated_value : int, float, str\n+        The new value to send to the channel\n+    is_password_protected : bool\n+        True if password validation is required; False otherwise\n+    show_confirm_dialog : bool\n+        True if the user will have to confirm the password by clicking on the \"Yes\" button of a dialog, or \"No\" to\n+        cancel; False if no such dialog will be needed\n+    confirm_message : str\n+        The customized question to be displayed in the confirmation dialog\n+    confirm_dialog_response : str\n+        The simulated response (Yes/No) for the confirmation dialog\n+    is_value_relative : bool\n+        If True, the new value will be added to the existing pressValue; if False, the widget, when pressed, will send\n+        only the new value\n+    channel_type : type\n+        The type of the value to be sent to the channel\n+    \"\"\"\n+\n+    pydm_pushbutton = PyDMPushButton()\n+    qtbot.addWidget(pydm_pushbutton)\n+\n+    pydm_pushbutton.showConfirmDialog = show_confirm_dialog\n+    pydm_pushbutton.confirmMessage = confirm_message\n+    pydm_pushbutton.relativeChange = is_value_relative\n+    pydm_pushbutton.channeltype = channel_type\n+\n+    if press_value:\n+        signals.new_value_signal[type(press_value)].connect(pydm_pushbutton.updatePressValue)\n+        signals.new_value_signal[type(press_value)].emit(press_value)\n+\n+        if channel_type == str:\n+            assert pydm_pushbutton.pressValue == str(press_value)\n+\n+    if updated_value:\n+        signals.new_value_signal[type(updated_value)].connect(pydm_pushbutton.channelValueChanged)\n+        signals.new_value_signal[type(updated_value)].emit(updated_value)\n+\n+        if channel_type == str:\n+            assert pydm_pushbutton.pressValue == str(updated_value)\n+        else:\n+            assert pydm_pushbutton.channeltype == type(updated_value)\n+\n+    assert pydm_pushbutton.relativeChange == is_value_relative\n+\n+    if show_confirm_dialog:\n+        monkeypatch.setattr(QMessageBox, 'exec_', lambda *args: confirm_dialog_response)\n+\n+    pydm_pushbutton.passwordProtected = is_password_protected\n+    if is_password_protected:\n+        # We assume the QInputDialog returns success. Further testing scenarios are performed at test_validate_password\n+        plain_text_password = \"$L4C_p4$$wd\"\n+        pydm_pushbutton.password = plain_text_password\n+        monkeypatch.setattr(QInputDialog, 'getText', lambda *args: (plain_text_password, is_password_validated))\n+\n+    # Set up the conftest fixture to receive the value sent out to the channel\n+    pydm_pushbutton.send_value_signal[channel_type].connect(signals.receiveValue)\n+\n+    # Send the value out to the channel\n+    if updated_value:\n+        signals.new_value_signal[type(updated_value)].connect(pydm_pushbutton.sendValue)\n+        signals.new_value_signal[type(updated_value)].emit(updated_value)\n+\n+    if confirm_dialog_response == QMessageBox.No or not is_password_validated or None in (press_value, updated_value):\n+        assert not signals.value\n+    else:\n+        if pydm_pushbutton.showConfirmDialog:\n+            if confirm_message and len(confirm_message):\n+                assert pydm_pushbutton.confirmMessage == confirm_message\n+            else:\n+                assert pydm_pushbutton.confirmMessage == PyDMPushButton.DEFAULT_CONFIRM_MESSAGE\n+\n+        if not is_value_relative or channel_type == str:\n+            # Limit the decimal places since Python 2.7. provided more floating point\n+            assert signals.value == pydm_pushbutton.channeltype(pydm_pushbutton.pressValue)\n+        else:\n+            assert signals.value == updated_value + pydm_pushbutton.channeltype(pydm_pushbutton.pressValue)","path":"pydm/tests/widgets/test_pushbutton.py","position":407,"original_position":407,"commit_id":"021ee2de984cc9815183a16275bfee33b0fc7953","original_commit_id":"021ee2de984cc9815183a16275bfee33b0fc7953","user":{"login":"teddyrendahl","id":25753048,"avatar_url":"https://avatars3.githubusercontent.com/u/25753048?v=4","gravatar_id":"","url":"https://api.github.com/users/teddyrendahl","html_url":"https://github.com/teddyrendahl","followers_url":"https://api.github.com/users/teddyrendahl/followers","following_url":"https://api.github.com/users/teddyrendahl/following{/other_user}","gists_url":"https://api.github.com/users/teddyrendahl/gists{/gist_id}","starred_url":"https://api.github.com/users/teddyrendahl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/teddyrendahl/subscriptions","organizations_url":"https://api.github.com/users/teddyrendahl/orgs","repos_url":"https://api.github.com/users/teddyrendahl/repos","events_url":"https://api.github.com/users/teddyrendahl/events{/privacy}","received_events_url":"https://api.github.com/users/teddyrendahl/received_events","type":"User","site_admin":false},"body":"This is a pretty complicated test. I think it is fine how it is for now, but in the future I would break this up into separate tests. Following the trail of nested `if` statements is difficult and ideally the testing logic simpler than what you are testing 😄 ","created_at":"2018-04-19T02:44:47Z","updated_at":"2018-04-19T02:50:50Z","html_url":"https://github.com/slaclab/pydm/pull/294#discussion_r182621202","pull_request_url":"https://api.github.com/repos/slaclab/pydm/pulls/294","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/slaclab/pydm/pulls/comments/182621202"},"html":{"href":"https://github.com/slaclab/pydm/pull/294#discussion_r182621202"},"pull_request":{"href":"https://api.github.com/repos/slaclab/pydm/pulls/294"}}},"pull_request":{"url":"https://api.github.com/repos/slaclab/pydm/pulls/294","id":182625456,"html_url":"https://github.com/slaclab/pydm/pull/294","diff_url":"https://github.com/slaclab/pydm/pull/294.diff","patch_url":"https://github.com/slaclab/pydm/pull/294.patch","issue_url":"https://api.github.com/repos/slaclab/pydm/issues/294","number":294,"state":"open","locked":false,"title":"TST: Test for PyDM Push Button","user":{"login":"hmbui","id":36712473,"avatar_url":"https://avatars3.githubusercontent.com/u/36712473?v=4","gravatar_id":"","url":"https://api.github.com/users/hmbui","html_url":"https://github.com/hmbui","followers_url":"https://api.github.com/users/hmbui/followers","following_url":"https://api.github.com/users/hmbui/following{/other_user}","gists_url":"https://api.github.com/users/hmbui/gists{/gist_id}","starred_url":"https://api.github.com/users/hmbui/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hmbui/subscriptions","organizations_url":"https://api.github.com/users/hmbui/orgs","repos_url":"https://api.github.com/users/hmbui/repos","events_url":"https://api.github.com/users/hmbui/events{/privacy}","received_events_url":"https://api.github.com/users/hmbui/received_events","type":"User","site_admin":false},"body":"close #258 ","created_at":"2018-04-18T23:01:57Z","updated_at":"2018-04-19T02:50:50Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4fe7cace1b6a7eb6bb72519be2a4bf6777dd25d0","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":892919671,"url":"https://api.github.com/repos/slaclab/pydm/labels/ci","name":"ci","color":"39cec2","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/slaclab/pydm/pulls/294/commits","review_comments_url":"https://api.github.com/repos/slaclab/pydm/pulls/294/comments","review_comment_url":"https://api.github.com/repos/slaclab/pydm/pulls/comments{/number}","comments_url":"https://api.github.com/repos/slaclab/pydm/issues/294/comments","statuses_url":"https://api.github.com/repos/slaclab/pydm/statuses/021ee2de984cc9815183a16275bfee33b0fc7953","head":{"label":"hmbui:test_pushbutton","ref":"test_pushbutton","sha":"021ee2de984cc9815183a16275bfee33b0fc7953","user":{"login":"hmbui","id":36712473,"avatar_url":"https://avatars3.githubusercontent.com/u/36712473?v=4","gravatar_id":"","url":"https://api.github.com/users/hmbui","html_url":"https://github.com/hmbui","followers_url":"https://api.github.com/users/hmbui/followers","following_url":"https://api.github.com/users/hmbui/following{/other_user}","gists_url":"https://api.github.com/users/hmbui/gists{/gist_id}","starred_url":"https://api.github.com/users/hmbui/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hmbui/subscriptions","organizations_url":"https://api.github.com/users/hmbui/orgs","repos_url":"https://api.github.com/users/hmbui/repos","events_url":"https://api.github.com/users/hmbui/events{/privacy}","received_events_url":"https://api.github.com/users/hmbui/received_events","type":"User","site_admin":false},"repo":{"id":122418877,"name":"pydm","full_name":"hmbui/pydm","owner":{"login":"hmbui","id":36712473,"avatar_url":"https://avatars3.githubusercontent.com/u/36712473?v=4","gravatar_id":"","url":"https://api.github.com/users/hmbui","html_url":"https://github.com/hmbui","followers_url":"https://api.github.com/users/hmbui/followers","following_url":"https://api.github.com/users/hmbui/following{/other_user}","gists_url":"https://api.github.com/users/hmbui/gists{/gist_id}","starred_url":"https://api.github.com/users/hmbui/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hmbui/subscriptions","organizations_url":"https://api.github.com/users/hmbui/orgs","repos_url":"https://api.github.com/users/hmbui/repos","events_url":"https://api.github.com/users/hmbui/events{/privacy}","received_events_url":"https://api.github.com/users/hmbui/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/hmbui/pydm","description":"Python Display Manager","fork":true,"url":"https://api.github.com/repos/hmbui/pydm","forks_url":"https://api.github.com/repos/hmbui/pydm/forks","keys_url":"https://api.github.com/repos/hmbui/pydm/keys{/key_id}","collaborators_url":"https://api.github.com/repos/hmbui/pydm/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/hmbui/pydm/teams","hooks_url":"https://api.github.com/repos/hmbui/pydm/hooks","issue_events_url":"https://api.github.com/repos/hmbui/pydm/issues/events{/number}","events_url":"https://api.github.com/repos/hmbui/pydm/events","assignees_url":"https://api.github.com/repos/hmbui/pydm/assignees{/user}","branches_url":"https://api.github.com/repos/hmbui/pydm/branches{/branch}","tags_url":"https://api.github.com/repos/hmbui/pydm/tags","blobs_url":"https://api.github.com/repos/hmbui/pydm/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/hmbui/pydm/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/hmbui/pydm/git/refs{/sha}","trees_url":"https://api.github.com/repos/hmbui/pydm/git/trees{/sha}","statuses_url":"https://api.github.com/repos/hmbui/pydm/statuses/{sha}","languages_url":"https://api.github.com/repos/hmbui/pydm/languages","stargazers_url":"https://api.github.com/repos/hmbui/pydm/stargazers","contributors_url":"https://api.github.com/repos/hmbui/pydm/contributors","subscribers_url":"https://api.github.com/repos/hmbui/pydm/subscribers","subscription_url":"https://api.github.com/repos/hmbui/pydm/subscription","commits_url":"https://api.github.com/repos/hmbui/pydm/commits{/sha}","git_commits_url":"https://api.github.com/repos/hmbui/pydm/git/commits{/sha}","comments_url":"https://api.github.com/repos/hmbui/pydm/comments{/number}","issue_comment_url":"https://api.github.com/repos/hmbui/pydm/issues/comments{/number}","contents_url":"https://api.github.com/repos/hmbui/pydm/contents/{+path}","compare_url":"https://api.github.com/repos/hmbui/pydm/compare/{base}...{head}","merges_url":"https://api.github.com/repos/hmbui/pydm/merges","archive_url":"https://api.github.com/repos/hmbui/pydm/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/hmbui/pydm/downloads","issues_url":"https://api.github.com/repos/hmbui/pydm/issues{/number}","pulls_url":"https://api.github.com/repos/hmbui/pydm/pulls{/number}","milestones_url":"https://api.github.com/repos/hmbui/pydm/milestones{/number}","notifications_url":"https://api.github.com/repos/hmbui/pydm/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/hmbui/pydm/labels{/name}","releases_url":"https://api.github.com/repos/hmbui/pydm/releases{/id}","deployments_url":"https://api.github.com/repos/hmbui/pydm/deployments","created_at":"2018-02-22T02:06:21Z","updated_at":"2018-04-18T22:55:15Z","pushed_at":"2018-04-18T22:58:56Z","git_url":"git://github.com/hmbui/pydm.git","ssh_url":"git@github.com:hmbui/pydm.git","clone_url":"https://github.com/hmbui/pydm.git","svn_url":"https://github.com/hmbui/pydm","homepage":"http://slaclab.github.io/pydm/","size":5588,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"slaclab:master","ref":"master","sha":"3ca73b0416b4fd526111b18ab1eee3f50403f8f0","user":{"login":"slaclab","id":16196854,"avatar_url":"https://avatars2.githubusercontent.com/u/16196854?v=4","gravatar_id":"","url":"https://api.github.com/users/slaclab","html_url":"https://github.com/slaclab","followers_url":"https://api.github.com/users/slaclab/followers","following_url":"https://api.github.com/users/slaclab/following{/other_user}","gists_url":"https://api.github.com/users/slaclab/gists{/gist_id}","starred_url":"https://api.github.com/users/slaclab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/slaclab/subscriptions","organizations_url":"https://api.github.com/users/slaclab/orgs","repos_url":"https://api.github.com/users/slaclab/repos","events_url":"https://api.github.com/users/slaclab/events{/privacy}","received_events_url":"https://api.github.com/users/slaclab/received_events","type":"Organization","site_admin":false},"repo":{"id":59700161,"name":"pydm","full_name":"slaclab/pydm","owner":{"login":"slaclab","id":16196854,"avatar_url":"https://avatars2.githubusercontent.com/u/16196854?v=4","gravatar_id":"","url":"https://api.github.com/users/slaclab","html_url":"https://github.com/slaclab","followers_url":"https://api.github.com/users/slaclab/followers","following_url":"https://api.github.com/users/slaclab/following{/other_user}","gists_url":"https://api.github.com/users/slaclab/gists{/gist_id}","starred_url":"https://api.github.com/users/slaclab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/slaclab/subscriptions","organizations_url":"https://api.github.com/users/slaclab/orgs","repos_url":"https://api.github.com/users/slaclab/repos","events_url":"https://api.github.com/users/slaclab/events{/privacy}","received_events_url":"https://api.github.com/users/slaclab/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/slaclab/pydm","description":"Python Display Manager","fork":false,"url":"https://api.github.com/repos/slaclab/pydm","forks_url":"https://api.github.com/repos/slaclab/pydm/forks","keys_url":"https://api.github.com/repos/slaclab/pydm/keys{/key_id}","collaborators_url":"https://api.github.com/repos/slaclab/pydm/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/slaclab/pydm/teams","hooks_url":"https://api.github.com/repos/slaclab/pydm/hooks","issue_events_url":"https://api.github.com/repos/slaclab/pydm/issues/events{/number}","events_url":"https://api.github.com/repos/slaclab/pydm/events","assignees_url":"https://api.github.com/repos/slaclab/pydm/assignees{/user}","branches_url":"https://api.github.com/repos/slaclab/pydm/branches{/branch}","tags_url":"https://api.github.com/repos/slaclab/pydm/tags","blobs_url":"https://api.github.com/repos/slaclab/pydm/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/slaclab/pydm/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/slaclab/pydm/git/refs{/sha}","trees_url":"https://api.github.com/repos/slaclab/pydm/git/trees{/sha}","statuses_url":"https://api.github.com/repos/slaclab/pydm/statuses/{sha}","languages_url":"https://api.github.com/repos/slaclab/pydm/languages","stargazers_url":"https://api.github.com/repos/slaclab/pydm/stargazers","contributors_url":"https://api.github.com/repos/slaclab/pydm/contributors","subscribers_url":"https://api.github.com/repos/slaclab/pydm/subscribers","subscription_url":"https://api.github.com/repos/slaclab/pydm/subscription","commits_url":"https://api.github.com/repos/slaclab/pydm/commits{/sha}","git_commits_url":"https://api.github.com/repos/slaclab/pydm/git/commits{/sha}","comments_url":"https://api.github.com/repos/slaclab/pydm/comments{/number}","issue_comment_url":"https://api.github.com/repos/slaclab/pydm/issues/comments{/number}","contents_url":"https://api.github.com/repos/slaclab/pydm/contents/{+path}","compare_url":"https://api.github.com/repos/slaclab/pydm/compare/{base}...{head}","merges_url":"https://api.github.com/repos/slaclab/pydm/merges","archive_url":"https://api.github.com/repos/slaclab/pydm/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/slaclab/pydm/downloads","issues_url":"https://api.github.com/repos/slaclab/pydm/issues{/number}","pulls_url":"https://api.github.com/repos/slaclab/pydm/pulls{/number}","milestones_url":"https://api.github.com/repos/slaclab/pydm/milestones{/number}","notifications_url":"https://api.github.com/repos/slaclab/pydm/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/slaclab/pydm/labels{/name}","releases_url":"https://api.github.com/repos/slaclab/pydm/releases{/id}","deployments_url":"https://api.github.com/repos/slaclab/pydm/deployments","created_at":"2016-05-25T21:50:56Z","updated_at":"2018-04-19T02:44:20Z","pushed_at":"2018-04-19T02:44:18Z","git_url":"git://github.com/slaclab/pydm.git","ssh_url":"git@github.com:slaclab/pydm.git","clone_url":"https://github.com/slaclab/pydm.git","svn_url":"https://github.com/slaclab/pydm","homepage":"http://slaclab.github.io/pydm/","size":7204,"stargazers_count":30,"watchers_count":30,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":23,"mirror_url":null,"archived":false,"open_issues_count":54,"license":{"key":"other","name":"Other","spdx_id":null,"url":null},"forks":23,"open_issues":54,"watchers":30,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/slaclab/pydm/pulls/294"},"html":{"href":"https://github.com/slaclab/pydm/pull/294"},"issue":{"href":"https://api.github.com/repos/slaclab/pydm/issues/294"},"comments":{"href":"https://api.github.com/repos/slaclab/pydm/issues/294/comments"},"review_comments":{"href":"https://api.github.com/repos/slaclab/pydm/pulls/294/comments"},"review_comment":{"href":"https://api.github.com/repos/slaclab/pydm/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/slaclab/pydm/pulls/294/commits"},"statuses":{"href":"https://api.github.com/repos/slaclab/pydm/statuses/021ee2de984cc9815183a16275bfee33b0fc7953"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"teddyrendahl"}}