├── .coveragerc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.rst ├── LICENSE ├── README.rst ├── changelog.rst ├── conftest.py ├── docs ├── Makefile ├── _static │ ├── qcrash_github_login.png │ ├── qcrash_report.png │ └── qcrash_review.png ├── api_ref.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── examples.rst ├── index.rst ├── intro.rst └── make.bat ├── examples ├── example_pyqt4.py ├── example_pyqt5.py └── example_pyside.py ├── forms ├── dlg_github_login.ui ├── dlg_report_bug.ui ├── dlg_review.ui ├── qcrash.qrc └── rc │ ├── GitHub-Mark-Light.png │ └── GitHub-Mark.png ├── pytest.ini ├── pyuic.json ├── qcrash ├── __init__.py ├── _dialogs │ ├── __init__.py │ ├── gh_login.py │ ├── report.py │ └── review.py ├── _extlibs │ ├── __init__.py │ └── github.py ├── _forms │ ├── __init__.py │ ├── dlg_github_login_ui.py │ ├── dlg_report_bug_ui.py │ ├── dlg_review_ui.py │ └── qcrash_rc.py ├── _hooks.py ├── api.py ├── backends │ ├── __init__.py │ ├── base.py │ ├── email.py │ └── github.py ├── formatters │ ├── __init__.py │ ├── base.py │ ├── email.py │ └── markdown.py └── qt.py ├── requirements.txt ├── scripts └── install-qt.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_api.py ├── test_backends ├── __init__.py ├── test_base.py ├── test_email.py └── test_github.py ├── test_dialogs ├── __init__.py ├── test_dlg_report.py ├── test_gh_login.py └── test_review.py └── test_formatters ├── __init__.py ├── test_base.py ├── test_email.py └── test_markdown.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = qcrash 3 | omit = 4 | # qt design ui files 5 | *_ui.py 6 | *_rc.py 7 | qcrash/_extlibs/* 8 | # tested implicitely, should not be covered 9 | qcrash/qt.py 10 | qcrash/_hooks.py 11 | tests/* 12 | 13 | [report] 14 | # Regexes for lines to exclude from consideration 15 | exclude_lines = 16 | # Don't complain if non-runnable code isn't run: 17 | if __name__ == .__main__.: 18 | exec()_ 19 | QtWidgets.QMessageBox 20 | def main\(.*: 21 | 22 | # Don't complain if non importable code is not run 23 | except ImportError 24 | except ClassNotFound 25 | 26 | pragma: no cover 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | .hackedit 65 | .idea 66 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: required 3 | dist: trusty 4 | 5 | python: 6 | - "2.7" 7 | - "3.4" 8 | 9 | virtualenv: 10 | system_site_packages: true 11 | 12 | matrix: 13 | exclude: 14 | - env: QT_API=pyqt5 15 | python: "2.7" 16 | 17 | env: 18 | - QT_API=pyqt4 19 | - QT_API=pyqt5 20 | 21 | before_install: 22 | - "export DISPLAY=:99.0" 23 | - "sh -e /etc/init.d/xvfb start" 24 | 25 | install: 26 | - sudo apt-get update 27 | 28 | # Qt 29 | - python scripts/install-qt.py 30 | 31 | # pytest 32 | - pip install --quiet pytest pytest-qt pytest-cov pytest-flake8 33 | 34 | # coveralls 35 | - pip install --quiet coveralls --use-wheel 36 | 37 | script: 38 | - pip install -e . --upgrade 39 | - catchsegv py.test --cov qcrash --flake8 40 | 41 | after_success: 42 | - coveralls 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Report bugs or ask a question 2 | ----------------------------- 3 | 4 | You can report bugs or ask question on our `issue tracker`_. 5 | 6 | Submitting pull requests: 7 | ------------------------- 8 | 9 | Pull Requests are great! 10 | 11 | 1) Fork the Repo on github. 12 | 2) Create a feature/bug fix branch based on the master branch. 13 | 3) If you are adding functionality or fixing a bug, please add a test! 14 | 4) Push to your fork and submit a pull request to **the master branch**. 15 | 16 | Please use **PEP8** to style your code:: 17 | 18 | python setup.py test -a "--flake8 -m flake8" 19 | 20 | .. _issue tracker: https://github.com/ColinDuquesnoy/QCrash/issues 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Colin Duquesnoy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | About 2 | ----- 3 | 4 | .. image:: https://img.shields.io/pypi/v/qcrash.svg 5 | :target: https://pypi.python.org/pypi/qcrash/ 6 | :alt: Latest PyPI version 7 | 8 | .. image:: https://img.shields.io/pypi/dm/qcrash.svg 9 | :target: https://pypi.python.org/pypi/qcrash/ 10 | :alt: Number of PyPI downloads 11 | 12 | .. image:: https://img.shields.io/pypi/l/qcrash.svg 13 | 14 | .. image:: https://coveralls.io/repos/github/ColinDuquesnoy/QCrash/badge.svg?branch=master 15 | :target: https://coveralls.io/github/ColinDuquesnoy/QCrash?branch=master 16 | :alt: API Coverage 17 | 18 | 19 | .. image:: https://travis-ci.org/ColinDuquesnoy/QCrash.svg?branch=master 20 | :target: https://travis-ci.org/ColinDuquesnoy/QCrash 21 | :alt: Travis-CI Build Status 22 | 23 | 24 | A PyQt/PySide framework for reporting application crash (unhandled exception) 25 | and/or let the user report an issue/feature request. 26 | 27 | 28 | Features 29 | -------- 30 | 31 | - multiple builtin backends for reporting bugs: 32 | 33 | - github_backend: let you create issues on github 34 | - email_backend: let you send an email with the crash report. 35 | 36 | - highly configurable, you can create your own backend, set your own formatter,... 37 | - a thread safe exception hook mechanism with a way to setup your own function 38 | 39 | Screenshots 40 | ----------- 41 | 42 | *Screenshots taken on KDE Plasma 5* 43 | 44 | - Report dialog 45 | 46 | .. image:: https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/master/docs/_static/qcrash_report.png 47 | 48 | - Review report before submitting 49 | 50 | .. image:: https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/master/docs/_static/qcrash_review.png 51 | 52 | - Github integration 53 | 54 | .. image:: https://github.com/ColinDuquesnoy/QCrash/blob/master/docs/_static/qcrash_github_login.png 55 | 56 | 57 | LICENSE 58 | ------- 59 | 60 | QCrash is licensed under the MIT license. 61 | 62 | Installation 63 | ------------ 64 | 65 | ``pip install qcrash`` 66 | 67 | Usage 68 | ----- 69 | 70 | Basic usage: 71 | 72 | .. code-block:: python 73 | 74 | import qcrash.api as qcrash 75 | 76 | # setup our own function to collect system info and application log 77 | qcrash.get_application_log = my_app.get_application_log 78 | qcrash.get_system_information = my_app.get_system_info 79 | 80 | # configure backends 81 | github = qcrash.backends.GithubBackend('ColinDuquesnoy', 'QCrash') 82 | email = qcrash.backends.EmailBackend('colin.duquesnoy@gmail.com') 83 | qcrash.install_backend([github, email]) 84 | 85 | # install exception hook 86 | qcrash.install_except_hook() 87 | 88 | # or show the report dialog manually 89 | qcrash.show_report_dialog() 90 | 91 | Some more detailed `examples`_ are available. Also have a look at the 92 | `API documentation`_. 93 | 94 | Dependencies 95 | ------------ 96 | 97 | - `keyring`_ 98 | - `githubpy`_ (embedded into the package) 99 | 100 | 101 | .. _keyring: https://pypi.python.org/pypi/keyring 102 | .. _githubpy: https://github.com/michaelliao/githubpy 103 | .. _examples: https://github.com/ColinDuquesnoy/QCrash/tree/master/examples 104 | .. _API documentation: http://qcrash.readthedocs.org/en/latest/index.html 105 | 106 | 107 | Testing 108 | ------- 109 | 110 | To run the tests, just run the following command:: 111 | 112 | python setup.py test 113 | -------------------------------------------------------------------------------- /changelog.rst: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | ----- 3 | 4 | **Improvements** 5 | 6 | - [All] don't limit size of application log, github backend will upload it as an anonymous gist 7 | - [All] split review dialog into two tabs: "General" and "Log" 8 | - [GitHub] add option to save user name only 9 | - [GUI] allow to use TAB to change focus of input text area (do not insert a \t anymore) 10 | - [GUI] allow the user to press Ctrl+Return to accept a dialog 11 | 12 | **Fixed bugs** 13 | 14 | - fix segfault with PyQt 5.6 on Plasma 5.6.1 15 | - fix a few minor GUI issues (unaligned labels in gh login dialog, fix missing dialog icons, remove unused help buttons from dialogs) 16 | 17 | 18 | 0.1.0 19 | ----- 20 | 21 | First public release. 22 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from qcrash.qt import QtWidgets 5 | 6 | APP = QtWidgets.QApplication(sys.argv) 7 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/qcrash.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/qcrash.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/qcrash" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/qcrash" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/_static/qcrash_github_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/docs/_static/qcrash_github_login.png -------------------------------------------------------------------------------- /docs/_static/qcrash_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/docs/_static/qcrash_report.png -------------------------------------------------------------------------------- /docs/_static/qcrash_review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/docs/_static/qcrash_review.png -------------------------------------------------------------------------------- /docs/api_ref.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | qcrash.api 5 | ---------- 6 | 7 | .. automodule:: qcrash.api 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | qcrash.backends 13 | --------------- 14 | 15 | BaseBackend 16 | +++++++++++ 17 | 18 | .. autoclass:: qcrash.backends.BaseBackend 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | EmailBackend 24 | ++++++++++++ 25 | 26 | .. autoclass:: qcrash.backends.EmailBackend 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | GithubBackend 32 | +++++++++++++ 33 | 34 | .. autoclass:: qcrash.backends.GithubBackend 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | qcrash.formatters 41 | ----------------- 42 | 43 | BaseFormatter 44 | +++++++++++++ 45 | 46 | .. autoclass:: qcrash.formatters.base.BaseFormatter 47 | :members: 48 | :undoc-members: 49 | :show-inheritance: 50 | 51 | EmailFormatter 52 | ++++++++++++++ 53 | 54 | .. autoclass:: qcrash.formatters.email.EmailFormatter 55 | :members: 56 | :undoc-members: 57 | :show-inheritance: 58 | 59 | MardownFormatter 60 | ++++++++++++++++ 61 | 62 | .. autoclass:: qcrash.formatters.markdown.MardownFormatter 63 | :members: 64 | :undoc-members: 65 | :show-inheritance: 66 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. include:: ../changelog.rst 5 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # qcrash documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Feb 7 20:46:37 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | from qcrash import __version__ 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.viewcode', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = u'qcrash' 55 | copyright = u'2016, Colin Duquesnoy' 56 | author = u'Colin Duquesnoy' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = __version__ 64 | # The full version, including alpha/beta/rc tags. 65 | release = __version__ 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = 'en' 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = ['_build'] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | #keep_warnings = False 107 | 108 | # If true, `todo` and `todoList` produce output, else they produce nothing. 109 | todo_include_todos = True 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | # html_theme = 'alabaster' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | #html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | #html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | #html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | #html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | #html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | #html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | html_static_path = ['_static'] 146 | 147 | # Add any extra paths that contain custom files (such as robots.txt or 148 | # .htaccess) here, relative to this directory. These files are copied 149 | # directly to the root of the documentation. 150 | #html_extra_path = [] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 153 | # using the given strftime format. 154 | #html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | #html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | #html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | #html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | #html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | #html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | #html_file_suffix = None 192 | 193 | # Language to be used for generating the HTML full-text search index. 194 | # Sphinx supports the following languages: 195 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 196 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 197 | #html_search_language = 'en' 198 | 199 | # A dictionary with options for the search language support, empty by default. 200 | # Now only 'ja' uses this config value 201 | #html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | #html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'qcrashdoc' 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | #'preamble': '', 221 | 222 | # Latex figure (float) alignment 223 | #'figure_align': 'htbp', 224 | } 225 | 226 | # Grouping the document tree into LaTeX files. List of tuples 227 | # (source start file, target name, title, 228 | # author, documentclass [howto, manual, or own class]). 229 | latex_documents = [ 230 | (master_doc, 'qcrash.tex', u'qcrash Documentation', 231 | u'Author', 'manual'), 232 | ] 233 | 234 | # The name of an image file (relative to this directory) to place at the top of 235 | # the title page. 236 | #latex_logo = None 237 | 238 | # For "manual" documents, if this is true, then toplevel headings are parts, 239 | # not chapters. 240 | #latex_use_parts = False 241 | 242 | # If true, show page references after internal links. 243 | #latex_show_pagerefs = False 244 | 245 | # If true, show URL addresses after external links. 246 | #latex_show_urls = False 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #latex_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #latex_domain_indices = True 253 | 254 | 255 | # -- Options for manual page output --------------------------------------- 256 | 257 | # One entry per manual page. List of tuples 258 | # (source start file, name, description, authors, manual section). 259 | man_pages = [ 260 | (master_doc, 'qcrash', u'qcrash Documentation', 261 | [author], 1) 262 | ] 263 | 264 | # If true, show URL addresses after external links. 265 | #man_show_urls = False 266 | 267 | 268 | # -- Options for Texinfo output ------------------------------------------- 269 | 270 | # Grouping the document tree into Texinfo files. List of tuples 271 | # (source start file, target name, title, author, 272 | # dir menu entry, description, category) 273 | texinfo_documents = [ 274 | (master_doc, 'qcrash', u'qcrash Documentation', 275 | author, 'qcrash', 'One line description of project.', 276 | 'Miscellaneous'), 277 | ] 278 | 279 | # Documents to append as an appendix to all manuals. 280 | #texinfo_appendices = [] 281 | 282 | # If false, no module index is generated. 283 | #texinfo_domain_indices = True 284 | 285 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 286 | #texinfo_show_urls = 'footnote' 287 | 288 | # If true, do not generate a @detailmenu in the "Top" node's menu. 289 | #texinfo_no_detailmenu = False 290 | 291 | 292 | # -- Options for Epub output ---------------------------------------------- 293 | 294 | # Bibliographic Dublin Core info. 295 | epub_title = project 296 | epub_author = author 297 | epub_publisher = author 298 | epub_copyright = copyright 299 | 300 | # The basename for the epub file. It defaults to the project name. 301 | #epub_basename = project 302 | 303 | # The HTML theme for the epub output. Since the default themes are not 304 | # optimized for small screen space, using the same theme for HTML and epub 305 | # output is usually not wise. This defaults to 'epub', a theme designed to save 306 | # visual space. 307 | #epub_theme = 'epub' 308 | 309 | # The language of the text. It defaults to the language option 310 | # or 'en' if the language is not set. 311 | #epub_language = '' 312 | 313 | # The scheme of the identifier. Typical schemes are ISBN or URL. 314 | #epub_scheme = '' 315 | 316 | # The unique identifier of the text. This can be a ISBN number 317 | # or the project homepage. 318 | #epub_identifier = '' 319 | 320 | # A unique identification for the text. 321 | #epub_uid = '' 322 | 323 | # A tuple containing the cover image and cover page html template filenames. 324 | #epub_cover = () 325 | 326 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 327 | #epub_guide = () 328 | 329 | # HTML files that should be inserted before the pages created by sphinx. 330 | # The format is a list of tuples containing the path and title. 331 | #epub_pre_files = [] 332 | 333 | # HTML files that should be inserted after the pages created by sphinx. 334 | # The format is a list of tuples containing the path and title. 335 | #epub_post_files = [] 336 | 337 | # A list of files that should not be packed into the epub file. 338 | epub_exclude_files = ['search.html'] 339 | 340 | # The depth of the table of contents in toc.ncx. 341 | #epub_tocdepth = 3 342 | 343 | # Allow duplicate toc entries. 344 | #epub_tocdup = True 345 | 346 | # Choose between 'default' and 'includehidden'. 347 | #epub_tocscope = 'default' 348 | 349 | # Fix unsupported image types using the Pillow. 350 | #epub_fix_images = False 351 | 352 | # Scale large images. 353 | #epub_max_image_width = 0 354 | 355 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 356 | #epub_show_urls = 'inline' 357 | 358 | # If false, no index is generated. 359 | #epub_use_index = True 360 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | .. include:: ../CONTRIBUTING.rst 5 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | PyQt5 5 | ----- 6 | 7 | .. literalinclude:: ../examples/example_pyqt5.py 8 | :linenos: 9 | 10 | PyQt4 11 | ----- 12 | 13 | .. literalinclude:: ../examples/example_pyqt4.py 14 | :linenos: 15 | 16 | PySide 17 | ------ 18 | 19 | .. literalinclude:: ../examples/example_pyside.py 20 | :linenos: 21 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. qcrash documentation master file, created by 2 | sphinx-quickstart on Sun Feb 7 20:46:37 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to qcrash's documentation! 7 | ================================== 8 | 9 | A PyQt/PySide framework for reporting application crash (unhandled exception) 10 | and/or let the user report an issue/feature request. 11 | 12 | Contents: 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | 17 | intro 18 | changelog 19 | examples 20 | api_ref 21 | contributing 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | .. include:: ../README.rst 5 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\qcrash.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\qcrash.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /examples/example_pyqt4.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from PyQt4 import QtCore, QtGui 4 | import qcrash.api as qcrash 5 | 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | GITHUB_OWNER = 'ColinDuquesnoy' 9 | GITHUB_REPO = 'QCrash-Test' 10 | EMAIL = 'your.email@provider.com' 11 | 12 | 13 | def get_system_info(): 14 | return 'OS: %s\nPython: %r' % (sys.platform, sys.version_info) 15 | 16 | 17 | def get_application_log(): 18 | return "Blabla" 19 | 20 | 21 | app = QtGui.QApplication(sys.argv) 22 | my_settings = QtCore.QSettings() 23 | 24 | 25 | # use own qsettings to remember username,... (password stored via keyring) 26 | qcrash.set_qsettings(my_settings) 27 | 28 | 29 | # configure backends 30 | qcrash.install_backend(qcrash.backends.GithubBackend( 31 | GITHUB_OWNER, GITHUB_REPO)) 32 | qcrash.install_backend(qcrash.backends.EmailBackend(EMAIL, 'TestApp')) 33 | 34 | 35 | # setup our own function to collect system info and application log 36 | qcrash.get_application_log = get_application_log 37 | qcrash.get_system_information = get_system_info 38 | 39 | 40 | # show report dialog manually 41 | qcrash.show_report_dialog() 42 | 43 | 44 | # create a window 45 | win = QtGui.QMainWindow() 46 | label = QtGui.QLabel() 47 | label.setText('Wait a few seconds for an unhandled exception to occur...') 48 | label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) 49 | win.setCentralWidget(label) 50 | win.showMaximized() 51 | 52 | 53 | # install our own except hook. 54 | def except_hook(exc, tb): 55 | res = QtGui.QMessageBox.question( 56 | win, "Unhandled exception", "An unhandled exception has occured. Do " 57 | "you want to report") 58 | if res == QtGui.QMessageBox.Ok: 59 | qcrash.show_report_dialog( 60 | window_title='Report unhandled exception', 61 | issue_title=str(exc), traceback=tb) 62 | 63 | qcrash.install_except_hook(except_hook=except_hook) 64 | 65 | 66 | # raise an unhandled exception in a few seconds 67 | def raise_unhandled_exception(): 68 | raise Exception('this is an unhandled exception') 69 | QtCore.QTimer.singleShot(2000, raise_unhandled_exception) 70 | 71 | # run qt app 72 | app.exec_() 73 | -------------------------------------------------------------------------------- /examples/example_pyqt5.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from PyQt5 import QtCore, QtWidgets 5 | 6 | import qcrash.api as qcrash 7 | 8 | 9 | logging.basicConfig() 10 | 11 | GITHUB_OWNER = 'ColinDuquesnoy' 12 | GITHUB_REPO = 'QCrash-Test' 13 | EMAIL = 'your.email@provider.com' 14 | 15 | 16 | def get_system_info(): 17 | return 'OS: %s\nPython: %r' % (sys.platform, sys.version_info) 18 | 19 | 20 | def get_application_log(): 21 | return "Blabla" 22 | 23 | 24 | app = QtWidgets.QApplication(sys.argv) 25 | my_settings = QtCore.QSettings() 26 | 27 | 28 | # use own qsettings to remember username,... (password stored via keyring) 29 | qcrash.set_qsettings(my_settings) 30 | 31 | 32 | # configure backends 33 | qcrash.install_backend(qcrash.backends.GithubBackend( 34 | GITHUB_OWNER, GITHUB_REPO)) 35 | qcrash.install_backend(qcrash.backends.EmailBackend(EMAIL, 'TestApp')) 36 | 37 | 38 | # setup our own function to collect system info and application log 39 | qcrash.get_application_log = get_application_log 40 | qcrash.get_system_information = get_system_info 41 | 42 | 43 | # show report dialog manually 44 | qcrash.show_report_dialog() 45 | 46 | 47 | # create a window 48 | win = QtWidgets.QMainWindow() 49 | label = QtWidgets.QLabel() 50 | label.setText('Wait a few seconds for an unhandled exception to occur...') 51 | label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) 52 | win.setCentralWidget(label) 53 | win.showMaximized() 54 | 55 | 56 | # install our own except hook. 57 | def except_hook(exc, tb): 58 | res = QtWidgets.QMessageBox.question( 59 | win, "Unhandled exception", "An unhandled exception has occured. Do " 60 | "you want to report") 61 | if res == QtWidgets.QMessageBox.Yes: 62 | qcrash.show_report_dialog( 63 | window_title='Report unhandled exception', 64 | issue_title=str(exc), traceback=tb) 65 | 66 | qcrash.install_except_hook(except_hook=except_hook) 67 | 68 | 69 | # raise an unhandled exception in a few seconds 70 | def raise_unhandled_exception(): 71 | raise Exception('this is an unhandled exception') 72 | QtCore.QTimer.singleShot(2000, raise_unhandled_exception) 73 | 74 | # run qt app 75 | app.exec_() 76 | -------------------------------------------------------------------------------- /examples/example_pyside.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from PySide import QtCore, QtGui 5 | 6 | import qcrash.api as qcrash 7 | 8 | 9 | logging.basicConfig() 10 | 11 | GITHUB_OWNER = 'ColinDuquesnoy' 12 | GITHUB_REPO = 'QCrash-Test' 13 | EMAIL = 'your.email@provider.com' 14 | 15 | 16 | def get_system_info(): 17 | return 'OS: %s\nPython: %r' % (sys.platform, sys.version_info) 18 | 19 | 20 | def get_application_log(): 21 | return "Blabla" 22 | 23 | 24 | app = QtGui.QApplication(sys.argv) 25 | my_settings = QtCore.QSettings() 26 | 27 | 28 | # use own qsettings to remember username,... (password stored via keyring) 29 | qcrash.set_qsettings(my_settings) 30 | 31 | 32 | # configure backends 33 | qcrash.install_backend(qcrash.backends.GithubBackend( 34 | GITHUB_OWNER, GITHUB_REPO)) 35 | qcrash.install_backend(qcrash.backends.EmailBackend(EMAIL, 'TestApp')) 36 | 37 | 38 | # setup our own function to collect system info and application log 39 | qcrash.get_application_log = get_application_log 40 | qcrash.get_system_information = get_system_info 41 | 42 | 43 | # show report dialog manually 44 | qcrash.show_report_dialog() 45 | 46 | 47 | # create a window 48 | win = QtGui.QMainWindow() 49 | label = QtGui.QLabel() 50 | label.setText('Wait a few seconds for an unhandled exception to occur...') 51 | label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) 52 | win.setCentralWidget(label) 53 | win.showMaximized() 54 | 55 | 56 | # install our own except hook. 57 | def except_hook(exc, tb): 58 | res = QtGui.QMessageBox.question( 59 | win, "Unhandled exception", "An unhandled exception has occured. Do " 60 | "you want to report") 61 | if res == QtGui.QMessageBox.Ok: 62 | qcrash.show_report_dialog( 63 | window_title='Report unhandled exception', 64 | issue_title=str(exc), traceback=tb) 65 | 66 | qcrash.install_except_hook(except_hook=except_hook) 67 | 68 | 69 | # raise an unhandled exception in a few seconds 70 | def raise_unhandled_exception(): 71 | raise Exception('this is an unhandled exception') 72 | QtCore.QTimer.singleShot(2000, raise_unhandled_exception) 73 | 74 | # run qt app 75 | app.exec_() 76 | -------------------------------------------------------------------------------- /forms/dlg_github_login.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 366 10 | 248 11 | 12 | 13 | 14 | 15 | 350 16 | 0 17 | 18 | 19 | 20 | Sign in to github 21 | 22 | 23 | 24 | 25 | 26 | <html><head/><body><p align="center"><img src=":/rc/GitHub-Mark.png"/></p><p align="center">Sign in to GitHub</p></body></html> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | Username: 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Password: 49 | 50 | 51 | 52 | 53 | 54 | 55 | QLineEdit::Password 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Remember me 65 | 66 | 67 | 68 | 69 | 70 | 71 | Remember password 72 | 73 | 74 | 75 | 76 | 77 | 78 | Sign in 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | cb_remember 90 | toggled(bool) 91 | cb_remember_password 92 | setEnabled(bool) 93 | 94 | 95 | 199 96 | 136 97 | 98 | 99 | 199 100 | 164 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /forms/dlg_report_bug.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 544 10 | 272 11 | 12 | 13 | 14 | Report an issue... 15 | 16 | 17 | 18 | 19 | 20 | QFormLayout::ExpandingFieldsGrow 21 | 22 | 23 | 24 | 25 | Title: 26 | 27 | 28 | 29 | 30 | 31 | 32 | Title of the issue 33 | 34 | 35 | 36 | 37 | 38 | 39 | Description: 40 | 41 | 42 | 43 | 44 | 45 | 46 | Description of the issue (mandatory) 47 | 48 | 49 | true 50 | 51 | 52 | 53 | 54 | 55 | 56 | Include system information 57 | 58 | 59 | Include system &information 60 | 61 | 62 | 63 | 64 | 65 | 66 | Include application log 67 | 68 | 69 | Include application &log 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 0 79 | 80 | 81 | 82 | 83 | Qt::Horizontal 84 | 85 | 86 | 87 | 40 88 | 20 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /forms/dlg_review.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 622 10 | 495 11 | 12 | 13 | 14 | Review 15 | 16 | 17 | 18 | .. 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | background-color:palette(highlight) ; 28 | color: palette(highlighted-text); 29 | padding: 10px; 30 | border-radius:3px; 31 | 32 | 33 | <html><head/><body><p align="center">Review the final report</p></body></html> 34 | 35 | 36 | 37 | 38 | 39 | 40 | 1 41 | 42 | 43 | 44 | General 45 | 46 | 47 | 48 | 49 | 50 | true 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Log 59 | 60 | 61 | 62 | 63 | 64 | true 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Qt::Horizontal 76 | 77 | 78 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | buttonBox 88 | accepted() 89 | Dialog 90 | accept() 91 | 92 | 93 | 248 94 | 254 95 | 96 | 97 | 157 98 | 274 99 | 100 | 101 | 102 | 103 | buttonBox 104 | rejected() 105 | Dialog 106 | reject() 107 | 108 | 109 | 316 110 | 260 111 | 112 | 113 | 286 114 | 274 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /forms/qcrash.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | rc/GitHub-Mark-Light.png 4 | rc/GitHub-Mark.png 5 | 6 | 7 | -------------------------------------------------------------------------------- /forms/rc/GitHub-Mark-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/forms/rc/GitHub-Mark-Light.png -------------------------------------------------------------------------------- /forms/rc/GitHub-Mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/forms/rc/GitHub-Mark.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = .* doc build _extlibs scripts 3 | ;addopts=--capture=no 4 | 5 | pep8ignore= 6 | runtests.py ALL 7 | conftest.py ALL 8 | docs/conf.py ALL 9 | *_rc.py ALL 10 | *_ui.py ALL 11 | 12 | flake8-ignore = 13 | runtests.py ALL 14 | conftest.py ALL 15 | docs/conf.py ALL 16 | *_rc.py ALL 17 | *_ui.py ALL 18 | tests/test_api/test_special_icons.py F403 19 | 20 | flake8-max-line-length = 119 21 | -------------------------------------------------------------------------------- /pyuic.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | [ 4 | "forms/*.ui", 5 | "qcrash/_forms/" 6 | ], 7 | [ 8 | "forms/*.qrc", 9 | "qcrash/_forms/" 10 | ] 11 | ], 12 | "pyrcc": "pyrcc5", 13 | "pyrcc_options": "", 14 | "pyuic": "pyuic5", 15 | "pyuic_options": "--from-imports", 16 | "hooks": ["fix_qt_imports"] 17 | } 18 | -------------------------------------------------------------------------------- /qcrash/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A qt dialog for reporting an issue on github or via email 3 | """ 4 | 5 | # qcrash version 6 | __version__ = "0.3.0" 7 | -------------------------------------------------------------------------------- /qcrash/_dialogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/qcrash/_dialogs/__init__.py -------------------------------------------------------------------------------- /qcrash/_dialogs/gh_login.py: -------------------------------------------------------------------------------- 1 | from qcrash.qt import QtCore, QtWidgets 2 | 3 | from qcrash._forms import dlg_github_login_ui 4 | 5 | 6 | GH_MARK_NORMAL = ':/rc/GitHub-Mark.png' 7 | GH_MARK_LIGHT = ':/rc/GitHub-Mark-Light.png' 8 | 9 | 10 | class DlgGitHubLogin(QtWidgets.QDialog): 11 | HTML = '

' \ 12 | '

Sign in to GitHub

' 13 | 14 | def __init__(self, parent, username, remember, remember_password): 15 | super(DlgGitHubLogin, self).__init__(parent) 16 | self.ui = dlg_github_login_ui.Ui_Dialog() 17 | self.ui.setupUi(self) 18 | self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) 19 | self.ui.cb_remember.toggled.connect( 20 | self.ui.cb_remember_password.setEnabled) 21 | 22 | mark = GH_MARK_NORMAL 23 | if self.palette().base().color().lightness() < 128: 24 | mark = GH_MARK_LIGHT 25 | html = self.HTML % mark 26 | self.ui.lbl_html.setText(html) 27 | self.ui.bt_sign_in.clicked.connect(self.accept) 28 | self.ui.le_username.textChanged.connect(self.update_btn_state) 29 | self.ui.le_password.textChanged.connect(self.update_btn_state) 30 | self.ui.bt_sign_in.setDisabled(True) 31 | self.ui.le_username.setText(username) 32 | self.ui.cb_remember.setChecked(remember) 33 | self.ui.cb_remember_password.setChecked(remember_password) 34 | self.ui.cb_remember_password.setEnabled(remember) 35 | if username: 36 | self.ui.le_password.setFocus() 37 | else: 38 | self.ui.le_username.setFocus() 39 | self.adjustSize() 40 | self.setFixedSize(self.width(), self.height()) 41 | self.ui.le_password.installEventFilter(self) 42 | self.ui.le_username.installEventFilter(self) 43 | 44 | def eventFilter(self, obj, event): 45 | interesting_objects = [self.ui.le_password, self.ui.le_username] 46 | if obj in interesting_objects and event.type() == QtCore.QEvent.KeyPress: 47 | if event.key() == QtCore.Qt.Key_Return and event.modifiers() & QtCore.Qt.ControlModifier and \ 48 | self.ui.bt_sign_in.isEnabled(): 49 | self.accept() 50 | return True 51 | return False 52 | 53 | def update_btn_state(self): 54 | enable = str(self.ui.le_username.text()).strip() != '' 55 | enable &= str(self.ui.le_password.text()).strip() != '' 56 | self.ui.bt_sign_in.setEnabled(enable) 57 | 58 | @classmethod 59 | def login(cls, parent, username, remember, remember_pswd): # pragma: no cover 60 | dlg = DlgGitHubLogin(parent, username, remember, remember_pswd) 61 | if dlg.exec_() == dlg.Accepted: 62 | return dlg.ui.le_username.text(), dlg.ui.le_password.text(), \ 63 | dlg.ui.cb_remember.isChecked(), \ 64 | dlg.ui.cb_remember_password.isEnabled() and dlg.ui.cb_remember_password.isChecked() 65 | return None, None, None, None 66 | -------------------------------------------------------------------------------- /qcrash/_dialogs/report.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from qcrash._forms import dlg_report_bug_ui 4 | from qcrash.qt import QtCore, QtGui, QtWidgets 5 | from qcrash._dialogs.review import DlgReview 6 | 7 | 8 | _logger = logging.getLogger(__name__) 9 | 10 | 11 | LOG_LIMIT = 100 # keep only the last 100 lines 12 | 13 | 14 | class DlgReport(QtWidgets.QDialog): 15 | """ 16 | A qt dialog for reporting an issue on github or via email. 17 | 18 | The dialog let the user choose whether he wants to report an issue or 19 | propose an enhancement. Depending on its choice the issue title will be 20 | prefixed by "[Bug]" or "Enhancement". The user must write a description 21 | of the issue. User can choose to send the report via email or on github 22 | (using the user's credentials). 23 | 24 | The client code can specify some additional information to be included in 25 | the report (but hidden in thr dialog). 26 | 27 | - sys_infos: information about the system 28 | - traceback: the traceback of an unhandled exception 29 | - log: the application log (will be truncated). 30 | 31 | """ 32 | 33 | def __init__(self, backends, window_title='Report an issue...', 34 | window_icon=None, traceback=None, issue_title='', 35 | issue_description='', include_log=True, include_sys_info=True, 36 | **kwargs): 37 | super(DlgReport, self).__init__(**kwargs) 38 | self._traceback = traceback 39 | self.window_icon = window_icon 40 | self.ui = dlg_report_bug_ui.Ui_Dialog() 41 | self.ui.setupUi(self) 42 | self.ui.cb_include_sys_info.setChecked(include_sys_info) 43 | self.ui.cb_include_application_log.setChecked(include_log) 44 | self.setWindowTitle(window_title) 45 | self.setWindowIcon(QtGui.QIcon.fromTheme('tools-report-bug') 46 | if window_icon is None else window_icon) 47 | self.ui.lineEditTitle.setText(issue_title) 48 | self.ui.plainTextEditDesc.setPlainText(issue_description) 49 | self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) 50 | 51 | self.ui.lineEditTitle.textChanged.connect(self._enable_buttons) 52 | self.ui.plainTextEditDesc.textChanged.connect(self._enable_buttons) 53 | 54 | self.buttons = [] 55 | for backend in backends: 56 | bt = QtWidgets.QPushButton() 57 | bt.setText(backend.button_text) 58 | if backend.button_icon: 59 | bt.setIcon(backend.button_icon) 60 | bt.backend = backend 61 | bt.clicked.connect(self._on_button_clicked) 62 | self.ui.layout_buttons.addWidget(bt) 63 | self.buttons.append(bt) 64 | self._enable_buttons() 65 | 66 | def _enable_buttons(self, *_): 67 | title = str(self.ui.lineEditTitle.text()).strip() 68 | desc = str(self.ui.plainTextEditDesc.toPlainText()).strip() 69 | enable = title != '' and desc != '' 70 | for bt in self.buttons: 71 | bt.setEnabled(enable) 72 | 73 | def _on_button_clicked(self): 74 | from qcrash import api 75 | bt = self.sender() 76 | description = self.ui.plainTextEditDesc.toPlainText() 77 | backend = bt.backend 78 | backend.parent_widget = self 79 | title = backend.formatter.format_title( 80 | str(self.ui.lineEditTitle.text())) 81 | 82 | sys_info = None 83 | if self.ui.cb_include_sys_info.isChecked(): 84 | sys_info = api.get_system_information() 85 | 86 | log = None 87 | if self.ui.cb_include_application_log.isChecked(): 88 | log = api.get_application_log() 89 | 90 | body = backend.formatter.format_body( 91 | str(description), sys_info, self._traceback) 92 | 93 | if backend.need_review: # pragma: no cover 94 | body, log = DlgReview.review(body, log, self, self.window_icon) 95 | if body is None and log is None: 96 | return # user cancelled the review dialog 97 | 98 | try: 99 | if backend.send_report(title, body, log): 100 | self.accept() 101 | except Exception as e: 102 | QtWidgets.QMessageBox.warning(self, "Failed to send report", "Failed to send report.\n\n%r" % e) 103 | -------------------------------------------------------------------------------- /qcrash/_dialogs/review.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the review dialog. 3 | """ 4 | from qcrash.qt import QtCore, QtGui, QtWidgets 5 | from qcrash._forms import dlg_review_ui 6 | 7 | 8 | class DlgReview(QtWidgets.QDialog): 9 | """ 10 | Dialog for reviewing the final report. 11 | """ 12 | def __init__(self, content, log, parent, window_icon): 13 | """ 14 | :param content: content of the final report, before review 15 | :param parent: parent widget 16 | """ 17 | super(DlgReview, self).__init__(parent) 18 | self.ui = dlg_review_ui.Ui_Dialog() 19 | self.ui.setupUi(self) 20 | self.ui.tabWidget.setCurrentIndex(0) 21 | self.ui.edit_main.setPlainText(content) 22 | self.ui.edit_main.installEventFilter(self) 23 | self.ui.edit_log.installEventFilter(self) 24 | self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) 25 | self.setWindowIcon( 26 | QtGui.QIcon.fromTheme('document-edit') 27 | if window_icon is None else window_icon) 28 | if log: 29 | self.ui.edit_log.setPlainText(log) 30 | else: 31 | self.ui.tabWidget.tabBar().hide() 32 | self.ui.edit_main.setFocus() 33 | 34 | def eventFilter(self, obj, event): 35 | interesting_objects = [self.ui.edit_log, self.ui.edit_main] 36 | if obj in interesting_objects and event.type() == QtCore.QEvent.KeyPress: 37 | if event.key() == QtCore.Qt.Key_Return and \ 38 | event.modifiers() & QtCore.Qt.ControlModifier: 39 | self.accept() 40 | return True 41 | return False 42 | 43 | @classmethod 44 | def review(cls, content, log, parent, window_icon): # pragma: no cover 45 | """ 46 | Reviews the final bug report. 47 | 48 | :param content: content of the final report, before review 49 | :param parent: parent widget 50 | 51 | :returns: the reviewed report content or None if the review was 52 | canceled. 53 | """ 54 | dlg = DlgReview(content, log, parent, window_icon) 55 | if dlg.exec_(): 56 | return dlg.ui.edit_main.toPlainText(), \ 57 | dlg.ui.edit_log.toPlainText() 58 | return None, None 59 | -------------------------------------------------------------------------------- /qcrash/_extlibs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains some 3rd party modules: 3 | 4 | - githubpy: https://github.com/michaelliao/githubpy 5 | 6 | """ 7 | -------------------------------------------------------------------------------- /qcrash/_extlibs/github.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-coding: utf8 -*- 3 | 4 | ''' 5 | GitHub API Python SDK. (Python >= 2.6) 6 | 7 | Apache License 8 | 9 | Michael Liao (askxuefeng@gmail.com) 10 | 11 | Usage: 12 | 13 | >>> gh = GitHub(username='githubpy', password='test-githubpy-1234') 14 | >>> L = gh.users('githubpy').followers.get() 15 | >>> L[0].id 16 | 470058 17 | >>> L[0].login == u'michaelliao' 18 | True 19 | >>> x_ratelimit_remaining = gh.x_ratelimit_remaining 20 | >>> x_ratelimit_limit = gh.x_ratelimit_limit 21 | >>> x_ratelimit_reset = gh.x_ratelimit_reset 22 | >>> L = gh.users('githubpy').following.get() 23 | >>> L[0].url == u'https://api.github.com/users/michaelliao' 24 | True 25 | >>> L = gh.repos('githubpy')('testgithubpy').issues.get(state='closed', sort='created') 26 | >>> L[0].title == u'sample issue for test' 27 | True 28 | >>> L[0].number 29 | 1 30 | >>> I = gh.repos('githubpy')('testgithubpy').issues(1).get() 31 | >>> I.url == u'https://api.github.com/repos/githubpy/testgithubpy/issues/1' 32 | True 33 | >>> gh = GitHub(username='githubpy', password='test-githubpy-1234') 34 | >>> r = gh.repos('githubpy')('testgithubpy').issues.post(title='test create issue', body='just a test') 35 | >>> r.title == u'test create issue' 36 | True 37 | >>> r.state == u'open' 38 | True 39 | >>> gh.repos.thisisabadurl.get() 40 | Traceback (most recent call last): 41 | ... 42 | ApiNotFoundError: https://api.github.com/repos/thisisabadurl 43 | >>> gh.users('github-not-exist-user').followers.get() 44 | Traceback (most recent call last): 45 | ... 46 | ApiNotFoundError: https://api.github.com/users/github-not-exist-user/followers 47 | ''' 48 | 49 | __version__ = '1.1.1' 50 | 51 | try: 52 | # Python 2 53 | from urllib2 import build_opener, HTTPSHandler, Request, HTTPError 54 | from urllib import quote as urlquote 55 | from StringIO import StringIO 56 | def bytes(string, encoding=None): 57 | return str(string) 58 | except: 59 | # Python 3 60 | from urllib.request import build_opener, HTTPSHandler, HTTPError, Request 61 | from urllib.parse import quote as urlquote 62 | from io import StringIO 63 | 64 | import re, os, time, hmac, base64, hashlib, urllib, mimetypes, json 65 | from collections import Iterable 66 | from datetime import datetime, timedelta, tzinfo 67 | 68 | TIMEOUT=60 69 | 70 | _URL = 'https://api.github.com' 71 | _METHOD_MAP = dict( 72 | GET=lambda: 'GET', 73 | PUT=lambda: 'PUT', 74 | POST=lambda: 'POST', 75 | PATCH=lambda: 'PATCH', 76 | DELETE=lambda: 'DELETE') 77 | 78 | DEFAULT_SCOPE = None 79 | RW_SCOPE = 'user,public_repo,repo,repo:status,gist' 80 | 81 | def _encode_params(kw): 82 | ''' 83 | Encode parameters. 84 | ''' 85 | args = [] 86 | for k, v in kw.items(): 87 | try: 88 | # Python 2 89 | qv = v.encode('utf-8') if isinstance(v, unicode) else str(v) 90 | except: 91 | qv = v 92 | args.append('%s=%s' % (k, urlquote(qv))) 93 | return '&'.join(args) 94 | 95 | def _encode_json(obj): 96 | ''' 97 | Encode object as json str. 98 | ''' 99 | def _dump_obj(obj): 100 | if isinstance(obj, dict): 101 | return obj 102 | d = dict() 103 | for k in dir(obj): 104 | if not k.startswith('_'): 105 | d[k] = getattr(obj, k) 106 | return d 107 | return json.dumps(obj, default=_dump_obj) 108 | 109 | def _parse_json(jsonstr): 110 | def _obj_hook(pairs): 111 | o = JsonObject() 112 | for k, v in pairs.items(): 113 | o[str(k)] = v 114 | return o 115 | return json.loads(jsonstr, object_hook=_obj_hook) 116 | 117 | class _Executable(object): 118 | 119 | def __init__(self, _gh, _method, _path): 120 | self._gh = _gh 121 | self._method = _method 122 | self._path = _path 123 | 124 | def __call__(self, **kw): 125 | return self._gh._http(self._method, self._path, **kw) 126 | 127 | def __str__(self): 128 | return '_Executable (%s %s)' % (self._method, self._path) 129 | 130 | __repr__ = __str__ 131 | 132 | class _Callable(object): 133 | 134 | def __init__(self, _gh, _name): 135 | self._gh = _gh 136 | self._name = _name 137 | 138 | def __call__(self, *args): 139 | if len(args)==0: 140 | return self 141 | name = '%s/%s' % (self._name, '/'.join([str(arg) for arg in args])) 142 | return _Callable(self._gh, name) 143 | 144 | def __getattr__(self, attr): 145 | if attr=='get': 146 | return _Executable(self._gh, 'GET', self._name) 147 | if attr=='put': 148 | return _Executable(self._gh, 'PUT', self._name) 149 | if attr=='post': 150 | return _Executable(self._gh, 'POST', self._name) 151 | if attr=='patch': 152 | return _Executable(self._gh, 'PATCH', self._name) 153 | if attr=='delete': 154 | return _Executable(self._gh, 'DELETE', self._name) 155 | name = '%s/%s' % (self._name, attr) 156 | return _Callable(self._gh, name) 157 | 158 | def __str__(self): 159 | return '_Callable (%s)' % self._name 160 | 161 | __repr__ = __str__ 162 | 163 | class GitHub(object): 164 | 165 | ''' 166 | GitHub client. 167 | ''' 168 | 169 | def __init__(self, username=None, password=None, access_token=None, client_id=None, client_secret=None, redirect_uri=None, scope=None): 170 | self.x_ratelimit_remaining = (-1) 171 | self.x_ratelimit_limit = (-1) 172 | self.x_ratelimit_reset = (-1) 173 | self._authorization = None 174 | if username and password: 175 | # roundabout hack for Python 3 176 | userandpass = base64.b64encode(bytes('%s:%s' % (username, password), 'utf-8')) 177 | userandpass = userandpass.decode('ascii') 178 | self._authorization = 'Basic %s' % userandpass 179 | elif access_token: 180 | self._authorization = 'token %s' % access_token 181 | self._client_id = client_id 182 | self._client_secret = client_secret 183 | self._redirect_uri = redirect_uri 184 | self._scope = scope 185 | 186 | def authorize_url(self, state=None): 187 | ''' 188 | Generate authorize_url. 189 | 190 | >>> GitHub(client_id='3ebf94c5776d565bcf75').authorize_url() 191 | 'https://github.com/login/oauth/authorize?client_id=3ebf94c5776d565bcf75' 192 | ''' 193 | if not self._client_id: 194 | raise ApiAuthError('No client id.') 195 | kw = dict(client_id=self._client_id) 196 | if self._redirect_uri: 197 | kw['redirect_uri'] = self._redirect_uri 198 | if self._scope: 199 | kw['scope'] = self._scope 200 | if state: 201 | kw['state'] = state 202 | return 'https://github.com/login/oauth/authorize?%s' % _encode_params(kw) 203 | 204 | def get_access_token(self, code, state=None): 205 | ''' 206 | In callback url: http://host/callback?code=123&state=xyz 207 | 208 | use code and state to get an access token. 209 | ''' 210 | kw = dict(client_id=self._client_id, client_secret=self._client_secret, code=code) 211 | if self._redirect_uri: 212 | kw['redirect_uri'] = self._redirect_uri 213 | if state: 214 | kw['state'] = state 215 | opener = build_opener(HTTPSHandler) 216 | request = Request('https://github.com/login/oauth/access_token', data=_encode_params(kw)) 217 | request.get_method = _METHOD_MAP['POST'] 218 | request.add_header('Accept', 'application/json') 219 | try: 220 | response = opener.open(request, timeout=TIMEOUT) 221 | r = _parse_json(response.read()) 222 | if 'error' in r: 223 | raise ApiAuthError(str(r.error)) 224 | return str(r.access_token) 225 | except HTTPError as e: 226 | raise ApiAuthError('HTTPError when get access token') 227 | 228 | def __getattr__(self, attr): 229 | return _Callable(self, '/%s' % attr) 230 | 231 | def _http(self, _method, _path, **kw): 232 | data = None 233 | params = None 234 | if _method=='GET' and kw: 235 | _path = '%s?%s' % (_path, _encode_params(kw)) 236 | if _method in ['POST', 'PATCH', 'PUT']: 237 | data = bytes(_encode_json(kw), 'utf-8') 238 | url = '%s%s' % (_URL, _path) 239 | opener = build_opener(HTTPSHandler) 240 | request = Request(url, data=data) 241 | request.get_method = _METHOD_MAP[_method] 242 | if self._authorization: 243 | request.add_header('Authorization', self._authorization) 244 | if _method in ['POST', 'PATCH', 'PUT']: 245 | request.add_header('Content-Type', 'application/x-www-form-urlencoded') 246 | try: 247 | response = opener.open(request, timeout=TIMEOUT) 248 | is_json = self._process_resp(response.headers) 249 | if is_json: 250 | return _parse_json(response.read().decode('utf-8')) 251 | except HTTPError as e: 252 | is_json = self._process_resp(e.headers) 253 | if is_json: 254 | json = _parse_json(e.read().decode('utf-8')) 255 | else: 256 | json = e.read().decode('utf-8') 257 | req = JsonObject(method=_method, url=url) 258 | resp = JsonObject(code=e.code, json=json) 259 | if resp.code==404: 260 | raise ApiNotFoundError(url, req, resp) 261 | raise ApiError(url, req, resp) 262 | 263 | def _process_resp(self, headers): 264 | is_json = False 265 | if headers: 266 | for k in headers: 267 | h = k.lower() 268 | if h=='x-ratelimit-remaining': 269 | self.x_ratelimit_remaining = int(headers[k]) 270 | elif h=='x-ratelimit-limit': 271 | self.x_ratelimit_limit = int(headers[k]) 272 | elif h=='x-ratelimit-reset': 273 | self.x_ratelimit_reset = int(headers[k]) 274 | elif h=='content-type': 275 | is_json = headers[k].startswith('application/json') 276 | return is_json 277 | 278 | class JsonObject(dict): 279 | ''' 280 | general json object that can bind any fields but also act as a dict. 281 | ''' 282 | def __getattr__(self, key): 283 | try: 284 | return self[key] 285 | except KeyError: 286 | raise AttributeError(r"'Dict' object has no attribute '%s'" % key) 287 | 288 | def __setattr__(self, attr, value): 289 | self[attr] = value 290 | 291 | class ApiError(Exception): 292 | 293 | def __init__(self, url, request, response): 294 | super(ApiError, self).__init__(url) 295 | self.request = request 296 | self.response = response 297 | 298 | class ApiAuthError(ApiError): 299 | 300 | def __init__(self, msg): 301 | super(ApiAuthError, self).__init__(msg, None, None) 302 | 303 | class ApiNotFoundError(ApiError): 304 | pass 305 | 306 | if __name__ == '__main__': 307 | import doctest 308 | doctest.testmod() 309 | -------------------------------------------------------------------------------- /qcrash/_forms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/qcrash/_forms/__init__.py -------------------------------------------------------------------------------- /qcrash/_forms/dlg_github_login_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/colin/dev/QCrash/forms/dlg_github_login.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.5.1 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from qcrash.qt import QtCore, QtGui, QtWidgets 10 | 11 | class Ui_Dialog(object): 12 | def setupUi(self, Dialog): 13 | Dialog.setObjectName("Dialog") 14 | Dialog.resize(366, 248) 15 | Dialog.setMinimumSize(QtCore.QSize(350, 0)) 16 | self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) 17 | self.verticalLayout.setObjectName("verticalLayout") 18 | self.lbl_html = QtWidgets.QLabel(Dialog) 19 | self.lbl_html.setObjectName("lbl_html") 20 | self.verticalLayout.addWidget(self.lbl_html) 21 | self.formLayout = QtWidgets.QFormLayout() 22 | self.formLayout.setContentsMargins(-1, 0, -1, -1) 23 | self.formLayout.setObjectName("formLayout") 24 | self.label_2 = QtWidgets.QLabel(Dialog) 25 | self.label_2.setObjectName("label_2") 26 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_2) 27 | self.le_username = QtWidgets.QLineEdit(Dialog) 28 | self.le_username.setObjectName("le_username") 29 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.le_username) 30 | self.label_3 = QtWidgets.QLabel(Dialog) 31 | self.label_3.setObjectName("label_3") 32 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_3) 33 | self.le_password = QtWidgets.QLineEdit(Dialog) 34 | self.le_password.setEchoMode(QtWidgets.QLineEdit.Password) 35 | self.le_password.setObjectName("le_password") 36 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.le_password) 37 | self.verticalLayout.addLayout(self.formLayout) 38 | self.cb_remember = QtWidgets.QCheckBox(Dialog) 39 | self.cb_remember.setObjectName("cb_remember") 40 | self.verticalLayout.addWidget(self.cb_remember) 41 | self.cb_remember_password = QtWidgets.QCheckBox(Dialog) 42 | self.cb_remember_password.setObjectName("cb_remember_password") 43 | self.verticalLayout.addWidget(self.cb_remember_password) 44 | self.bt_sign_in = QtWidgets.QPushButton(Dialog) 45 | self.bt_sign_in.setObjectName("bt_sign_in") 46 | self.verticalLayout.addWidget(self.bt_sign_in) 47 | 48 | self.retranslateUi(Dialog) 49 | self.cb_remember.toggled['bool'].connect(self.cb_remember_password.setEnabled) 50 | QtCore.QMetaObject.connectSlotsByName(Dialog) 51 | 52 | def retranslateUi(self, Dialog): 53 | _translate = QtCore.QCoreApplication.translate 54 | Dialog.setWindowTitle(_translate("Dialog", "Sign in to github")) 55 | self.lbl_html.setText(_translate("Dialog", "

Sign in to GitHub

")) 56 | self.label_2.setText(_translate("Dialog", "Username:")) 57 | self.label_3.setText(_translate("Dialog", "Password: ")) 58 | self.cb_remember.setText(_translate("Dialog", "Remember me")) 59 | self.cb_remember_password.setText(_translate("Dialog", "Remember password")) 60 | self.bt_sign_in.setText(_translate("Dialog", "Sign in")) 61 | 62 | from . import qcrash_rc -------------------------------------------------------------------------------- /qcrash/_forms/dlg_report_bug_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/colin/dev/QCrash/forms/dlg_report_bug.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.5.1 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from qcrash.qt import QtCore, QtGui, QtWidgets 10 | 11 | class Ui_Dialog(object): 12 | def setupUi(self, Dialog): 13 | Dialog.setObjectName("Dialog") 14 | Dialog.resize(544, 272) 15 | self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) 16 | self.verticalLayout.setObjectName("verticalLayout") 17 | self.formLayout = QtWidgets.QFormLayout() 18 | self.formLayout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow) 19 | self.formLayout.setObjectName("formLayout") 20 | self.label = QtWidgets.QLabel(Dialog) 21 | self.label.setObjectName("label") 22 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label) 23 | self.lineEditTitle = QtWidgets.QLineEdit(Dialog) 24 | self.lineEditTitle.setObjectName("lineEditTitle") 25 | self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.lineEditTitle) 26 | self.label_2 = QtWidgets.QLabel(Dialog) 27 | self.label_2.setObjectName("label_2") 28 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_2) 29 | self.plainTextEditDesc = QtWidgets.QPlainTextEdit(Dialog) 30 | self.plainTextEditDesc.setTabChangesFocus(True) 31 | self.plainTextEditDesc.setObjectName("plainTextEditDesc") 32 | self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.plainTextEditDesc) 33 | self.cb_include_sys_info = QtWidgets.QCheckBox(Dialog) 34 | self.cb_include_sys_info.setObjectName("cb_include_sys_info") 35 | self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.cb_include_sys_info) 36 | self.cb_include_application_log = QtWidgets.QCheckBox(Dialog) 37 | self.cb_include_application_log.setObjectName("cb_include_application_log") 38 | self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.cb_include_application_log) 39 | self.verticalLayout.addLayout(self.formLayout) 40 | self.layout_buttons = QtWidgets.QHBoxLayout() 41 | self.layout_buttons.setContentsMargins(-1, 0, -1, -1) 42 | self.layout_buttons.setObjectName("layout_buttons") 43 | spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 44 | self.layout_buttons.addItem(spacerItem) 45 | self.verticalLayout.addLayout(self.layout_buttons) 46 | 47 | self.retranslateUi(Dialog) 48 | QtCore.QMetaObject.connectSlotsByName(Dialog) 49 | 50 | def retranslateUi(self, Dialog): 51 | _translate = QtCore.QCoreApplication.translate 52 | Dialog.setWindowTitle(_translate("Dialog", "Report an issue...")) 53 | self.label.setText(_translate("Dialog", "Title:")) 54 | self.lineEditTitle.setToolTip(_translate("Dialog", "Title of the issue")) 55 | self.label_2.setText(_translate("Dialog", "Description:")) 56 | self.plainTextEditDesc.setToolTip(_translate("Dialog", "Description of the issue (mandatory)")) 57 | self.cb_include_sys_info.setToolTip(_translate("Dialog", "Include system information")) 58 | self.cb_include_sys_info.setText(_translate("Dialog", "Include system &information")) 59 | self.cb_include_application_log.setToolTip(_translate("Dialog", "Include application log")) 60 | self.cb_include_application_log.setText(_translate("Dialog", "Include application &log")) 61 | 62 | from . import qcrash_rc -------------------------------------------------------------------------------- /qcrash/_forms/dlg_review_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/colin/dev/QCrash/forms/dlg_review.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.5.1 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from qcrash.qt import QtCore, QtGui, QtWidgets 10 | 11 | class Ui_Dialog(object): 12 | def setupUi(self, Dialog): 13 | Dialog.setObjectName("Dialog") 14 | Dialog.resize(622, 495) 15 | icon = QtGui.QIcon.fromTheme("document-edit") 16 | Dialog.setWindowIcon(icon) 17 | Dialog.setToolTip("") 18 | self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) 19 | self.verticalLayout.setObjectName("verticalLayout") 20 | self.label = QtWidgets.QLabel(Dialog) 21 | self.label.setStyleSheet("background-color:palette(highlight) ;\n" 22 | "color: palette(highlighted-text);\n" 23 | "padding: 10px;\n" 24 | "border-radius:3px;") 25 | self.label.setObjectName("label") 26 | self.verticalLayout.addWidget(self.label) 27 | self.tabWidget = QtWidgets.QTabWidget(Dialog) 28 | self.tabWidget.setObjectName("tabWidget") 29 | self.tab = QtWidgets.QWidget() 30 | self.tab.setObjectName("tab") 31 | self.gridLayout = QtWidgets.QGridLayout(self.tab) 32 | self.gridLayout.setObjectName("gridLayout") 33 | self.edit_main = QtWidgets.QPlainTextEdit(self.tab) 34 | self.edit_main.setTabChangesFocus(True) 35 | self.edit_main.setObjectName("edit_main") 36 | self.gridLayout.addWidget(self.edit_main, 0, 0, 1, 1) 37 | self.tabWidget.addTab(self.tab, "") 38 | self.tab_2 = QtWidgets.QWidget() 39 | self.tab_2.setObjectName("tab_2") 40 | self.gridLayout_2 = QtWidgets.QGridLayout(self.tab_2) 41 | self.gridLayout_2.setObjectName("gridLayout_2") 42 | self.edit_log = QtWidgets.QPlainTextEdit(self.tab_2) 43 | self.edit_log.setTabChangesFocus(True) 44 | self.edit_log.setObjectName("edit_log") 45 | self.gridLayout_2.addWidget(self.edit_log, 0, 0, 1, 1) 46 | self.tabWidget.addTab(self.tab_2, "") 47 | self.verticalLayout.addWidget(self.tabWidget) 48 | self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) 49 | self.buttonBox.setOrientation(QtCore.Qt.Horizontal) 50 | self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) 51 | self.buttonBox.setObjectName("buttonBox") 52 | self.verticalLayout.addWidget(self.buttonBox) 53 | 54 | self.retranslateUi(Dialog) 55 | self.tabWidget.setCurrentIndex(1) 56 | self.buttonBox.accepted.connect(Dialog.accept) 57 | self.buttonBox.rejected.connect(Dialog.reject) 58 | QtCore.QMetaObject.connectSlotsByName(Dialog) 59 | 60 | def retranslateUi(self, Dialog): 61 | _translate = QtCore.QCoreApplication.translate 62 | Dialog.setWindowTitle(_translate("Dialog", "Review")) 63 | self.label.setText(_translate("Dialog", "

Review the final report

")) 64 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("Dialog", "General")) 65 | self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("Dialog", "Log")) 66 | -------------------------------------------------------------------------------- /qcrash/_forms/qcrash_rc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.6.0) 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from qcrash.qt import QtCore 10 | 11 | qt_resource_data = b"\ 12 | \x00\x00\x06\xb2\ 13 | \x89\ 14 | \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ 15 | \x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ 16 | \x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\ 17 | \x00\x41\x64\x6f\x62\x65\x20\x49\x6d\x61\x67\x65\x52\x65\x61\x64\ 18 | \x79\x71\xc9\x65\x3c\x00\x00\x03\x24\x69\x54\x58\x74\x58\x4d\x4c\ 19 | \x3a\x63\x6f\x6d\x2e\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\ 20 | \x00\x00\x00\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\ 21 | \x69\x6e\x3d\x22\xef\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\ 22 | \x30\x4d\x70\x43\x65\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\ 23 | \x7a\x6b\x63\x39\x64\x22\x3f\x3e\x20\x3c\x78\x3a\x78\x6d\x70\x6d\ 24 | \x65\x74\x61\x20\x78\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\ 25 | \x62\x65\x3a\x6e\x73\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\ 26 | \x6d\x70\x74\x6b\x3d\x22\x41\x64\x6f\x62\x65\x20\x58\x4d\x50\x20\ 27 | \x43\x6f\x72\x65\x20\x35\x2e\x33\x2d\x63\x30\x31\x31\x20\x36\x36\ 28 | \x2e\x31\x34\x35\x36\x36\x31\x2c\x20\x32\x30\x31\x32\x2f\x30\x32\ 29 | \x2f\x30\x36\x2d\x31\x34\x3a\x35\x36\x3a\x32\x37\x20\x20\x20\x20\ 30 | \x20\x20\x20\x20\x22\x3e\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\ 31 | \x78\x6d\x6c\x6e\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\ 32 | \x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\ 33 | \x39\x2f\x30\x32\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\ 34 | \x61\x78\x2d\x6e\x73\x23\x22\x3e\x20\x3c\x72\x64\x66\x3a\x44\x65\ 35 | \x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\ 36 | \x6f\x75\x74\x3d\x22\x22\x20\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\ 37 | \x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\ 38 | \x65\x2e\x63\x6f\x6d\x2f\x78\x61\x70\x2f\x31\x2e\x30\x2f\x22\x20\ 39 | \x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\ 40 | \x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\ 41 | \x2f\x78\x61\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x20\x78\x6d\ 42 | \x6c\x6e\x73\x3a\x73\x74\x52\x65\x66\x3d\x22\x68\x74\x74\x70\x3a\ 43 | \x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\ 44 | \x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\x73\ 45 | \x6f\x75\x72\x63\x65\x52\x65\x66\x23\x22\x20\x78\x6d\x70\x3a\x43\ 46 | \x72\x65\x61\x74\x6f\x72\x54\x6f\x6f\x6c\x3d\x22\x41\x64\x6f\x62\ 47 | \x65\x20\x50\x68\x6f\x74\x6f\x73\x68\x6f\x70\x20\x43\x53\x36\x20\ 48 | \x28\x4d\x61\x63\x69\x6e\x74\x6f\x73\x68\x29\x22\x20\x78\x6d\x70\ 49 | \x4d\x4d\x3a\x49\x6e\x73\x74\x61\x6e\x63\x65\x49\x44\x3d\x22\x78\ 50 | \x6d\x70\x2e\x69\x69\x64\x3a\x45\x35\x31\x37\x38\x41\x32\x41\x39\ 51 | \x39\x41\x30\x31\x31\x45\x32\x39\x41\x31\x35\x42\x43\x31\x30\x34\ 52 | \x36\x41\x38\x39\x30\x34\x44\x22\x20\x78\x6d\x70\x4d\x4d\x3a\x44\ 53 | \x6f\x63\x75\x6d\x65\x6e\x74\x49\x44\x3d\x22\x78\x6d\x70\x2e\x64\ 54 | \x69\x64\x3a\x45\x35\x31\x37\x38\x41\x32\x42\x39\x39\x41\x30\x31\ 55 | \x31\x45\x32\x39\x41\x31\x35\x42\x43\x31\x30\x34\x36\x41\x38\x39\ 56 | \x30\x34\x44\x22\x3e\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x44\x65\x72\ 57 | \x69\x76\x65\x64\x46\x72\x6f\x6d\x20\x73\x74\x52\x65\x66\x3a\x69\ 58 | \x6e\x73\x74\x61\x6e\x63\x65\x49\x44\x3d\x22\x78\x6d\x70\x2e\x69\ 59 | \x69\x64\x3a\x45\x35\x31\x37\x38\x41\x32\x38\x39\x39\x41\x30\x31\ 60 | \x31\x45\x32\x39\x41\x31\x35\x42\x43\x31\x30\x34\x36\x41\x38\x39\ 61 | \x30\x34\x44\x22\x20\x73\x74\x52\x65\x66\x3a\x64\x6f\x63\x75\x6d\ 62 | \x65\x6e\x74\x49\x44\x3d\x22\x78\x6d\x70\x2e\x64\x69\x64\x3a\x45\ 63 | \x35\x31\x37\x38\x41\x32\x39\x39\x39\x41\x30\x31\x31\x45\x32\x39\ 64 | \x41\x31\x35\x42\x43\x31\x30\x34\x36\x41\x38\x39\x30\x34\x44\x22\ 65 | \x2f\x3e\x20\x3c\x2f\x72\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\ 66 | \x74\x69\x6f\x6e\x3e\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\ 67 | \x20\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x3e\x20\x3c\x3f\ 68 | \x78\x70\x61\x63\x6b\x65\x74\x20\x65\x6e\x64\x3d\x22\x72\x22\x3f\ 69 | \x3e\x9b\x84\x06\xb9\x00\x00\x03\x24\x49\x44\x41\x54\x78\xda\xc4\ 70 | \x97\x6d\x68\x8d\x61\x18\xc7\xcf\x79\x4c\x33\xdb\x30\xd9\x41\x2b\ 71 | \x8e\x88\x52\x23\x66\x3e\xf1\xc1\x49\x48\xda\xa9\xcd\x07\xf2\x91\ 72 | \x79\xf9\xa0\xd4\x28\xf1\x01\xa9\x4d\x8d\x85\x14\x29\x5f\x94\x6f\ 73 | \xe6\x35\xa5\x0c\x85\xa5\x30\x2b\xfb\x30\xc4\x96\xb1\x6c\x26\x6f\ 74 | \x7b\x69\x5e\xe6\xf8\x5d\x75\x9d\x3c\x7b\xdc\xf7\xf1\x9c\xe3\x59\ 75 | \xee\xfa\x77\x9d\x73\xbf\xfc\xaf\xff\x7d\x3d\xf7\xcb\x75\x87\x43\ 76 | \x3e\xcb\xe4\xc2\xc8\x24\x4c\x1c\x2c\x03\xf3\xc0\x0c\x90\xa7\xcd\ 77 | \x7d\xa0\x1d\x3c\x01\xb7\xc1\xe5\xee\x9e\x77\xef\xfd\xf0\x86\x7d\ 78 | \x38\x5e\x80\xd9\x0d\xca\x41\x96\x4f\xbd\x3f\xc0\x05\x70\x08\x21\ 79 | \xcd\x19\x09\xc0\xf1\x78\xcc\x61\xb0\xd1\x8f\x50\x4b\x49\x80\x33\ 80 | \x60\x27\x42\x3e\xfb\x16\xa0\xb3\xae\xd7\x30\x07\x51\xe4\xf3\x54\ 81 | \x98\xa2\x31\xca\xe0\x3c\x86\xb9\x0e\xa6\x84\x82\x2b\x05\x60\x7d\ 82 | \x5e\x6e\xee\x83\xfe\x81\xfe\x76\x6b\x04\x70\x5e\x82\xb9\x05\xc6\ 83 | \x85\x46\xa6\x7c\x01\x31\x22\xd1\xf4\x47\x04\x70\x2e\x2a\x6f\xca\ 84 | \x4f\xd7\x80\x56\x70\x17\x14\x81\xec\x0c\x9c\x5d\xd3\xdf\x85\x6a\ 85 | \x85\x63\x05\x91\x38\x4b\x24\x06\xa5\xc2\x71\x0d\x38\x02\xa2\x1e\ 86 | \x92\x6a\xd4\x56\x60\x23\x60\x13\xe8\x76\x6d\xbb\x16\x70\x47\xd1\ 87 | \xa2\x75\x21\xed\x23\x7d\x23\x3a\xb6\xda\xc3\x19\x55\x5f\xbf\x3f\ 88 | \x01\xb3\x5f\x84\x79\x68\x98\x45\x09\x24\x8f\x3d\x3b\x63\x2a\x78\ 89 | \x4e\xfd\x4f\xcf\xe7\x93\xc9\xcc\x06\x6f\xdd\x2b\x9e\xfa\x85\x98\ 90 | \x26\x03\x77\x29\xfd\x1e\x25\xf7\xf5\x1e\x4b\x18\x87\x2d\x52\x25\ 91 | \x36\x6e\x27\x15\xf4\xf4\x6f\x1c\xae\x22\x3e\xcb\x1d\x14\xca\x6a\ 92 | \x2f\xb3\x74\x9a\x1b\xc0\xc2\xb3\x71\x94\x89\x6f\x47\x8f\x57\x93\ 93 | \xca\x37\xe0\x6a\x00\x02\x84\xe3\xb5\x25\x32\x71\x11\x10\xb3\x0c\ 94 | \xdc\x47\x58\x3f\xfc\xab\x77\xe5\xd8\x6f\x69\x8e\x89\x80\x62\x43\ 95 | \xc3\x90\x9e\x84\x41\x95\x7a\xe5\xf4\x96\x62\xc7\xb0\xf5\xa4\x74\ 96 | \xda\xce\xee\x0c\xa3\x20\x5c\x9d\x86\xa6\xa8\x08\xc8\x31\x34\x7c\ 97 | \x1b\x81\x53\xf0\xbb\xa1\x2e\xc7\xb1\x5d\x86\x23\x20\x20\x62\xaa\ 98 | \x14\x01\xbd\x86\xfa\x7c\xb6\x48\x24\x28\xcf\xca\x95\x6f\x68\xea\ 99 | \x15\x01\x6d\x96\x71\xab\x02\x9c\xbd\x8d\xab\xcd\xb1\x1c\x93\x52\ 100 | \xaa\x50\x9e\x15\xc0\xec\x85\xa3\xca\xd2\xdc\x24\x02\x1a\x2c\x8d\ 101 | \x92\xf7\xd5\x06\x30\xfb\x5a\xe5\x32\x95\x86\x30\x0a\xe5\xdb\x74\ 102 | \x81\xb1\x96\x4e\xe7\xc1\x76\xb6\x52\x57\x9a\x33\x97\x4b\xeb\x38\ 103 | \x58\x6b\xe9\x32\x20\x49\x4f\xf2\x36\x3c\x89\xd9\xea\x52\x2c\xf7\ 104 | \xf8\x41\xb0\xd4\xb5\x85\x2e\x81\x1b\x40\xd2\xaa\x66\x04\x0d\x19\ 105 | \x42\x5d\x0a\xe6\xcb\x9d\x0f\xd6\x80\xd1\x29\x34\x9e\x82\x63\x5b\ 106 | \x52\xc0\x34\xcc\x33\x30\x46\x13\xc9\x5d\xe0\x84\xde\xf5\x8b\x3d\ 107 | \x03\x2f\x6a\x7e\x97\xf0\x08\x08\x6b\x5b\xdc\x47\x80\x24\x19\x99\ 108 | \x03\x47\x87\xa3\x27\x55\x07\xa6\xc6\x95\x23\x48\x36\x2c\x89\xe9\ 109 | \x3a\xc3\x45\x52\xe7\x75\xae\x1c\x52\x57\xe7\xf3\x0b\xd5\xa8\xcf\ 110 | \x61\x19\x91\x64\x2e\x8d\xae\xff\x07\xe8\xd4\xae\x0b\x48\x32\x9c\ 111 | \xbd\x60\x25\xb8\x9f\x82\xb8\xd5\x87\xf3\x46\x77\x96\xe4\x4d\x4a\ 112 | \x25\x37\xb8\x07\x66\x6a\xd5\x0e\x44\x1c\x4b\x63\xe1\x4d\xc0\x7c\ 113 | \x4c\xd1\xe5\x25\x58\xe2\x5e\xd0\x61\x03\xc9\x74\xdd\x9a\xb3\xb4\ 114 | \x4a\x04\x5d\x01\x3d\x9a\xaa\x1f\x85\x60\x30\x03\x01\x2f\xc0\x72\ 115 | \xc6\xbe\xf2\xf3\x30\x99\x88\x39\x67\x39\xc1\x0a\x20\xf9\x94\xa6\ 116 | \x00\x79\x67\x6c\x30\xe5\x17\x4e\x8a\x24\x62\x35\xa8\xd4\x99\x67\ 117 | \x5a\xe4\x81\xba\x59\xb8\x6c\xc9\x8d\x9f\xc7\xa9\xbc\x80\xb7\x28\ 118 | \x24\x32\x45\x90\x7d\xb5\xf4\xcd\xd6\x7b\x5f\x9c\x9d\xd6\xbd\xde\ 119 | \x97\xf2\x71\x9a\x48\x24\x42\xff\xb3\xfc\x12\x60\x00\xe6\x38\x0c\ 120 | \x00\x42\x07\x69\x04\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ 121 | \x82\ 122 | \x00\x00\x02\x93\ 123 | \x89\ 124 | \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ 125 | \x00\x00\x20\x00\x00\x00\x20\x08\x04\x00\x00\x00\xd9\x73\xb2\x7f\ 126 | \x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x26\x00\x00\x80\x84\ 127 | \x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00\x75\x30\x00\x00\xea\x60\ 128 | \x00\x00\x3a\x98\x00\x00\x17\x70\x9c\xba\x51\x3c\x00\x00\x00\x02\ 129 | \x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x02\x20\x49\x44\ 130 | \x41\x54\x48\xc7\x95\x95\xbd\x4b\x5b\x61\x14\xc6\x4f\x6e\x2f\xa4\ 131 | \x42\x42\x62\xe8\x47\xba\x28\x6e\x4e\x85\x68\x84\x16\x8a\xa5\x0e\ 132 | \xa5\x5b\x67\x41\xb0\x98\x8a\xdd\x3a\xa4\x85\xd2\xc1\x3f\xa0\x8b\ 133 | \xa3\x83\xe0\x1e\xc5\x50\x27\xe9\x54\x6c\x5d\x3a\xb8\x64\xc8\xa0\ 134 | \x92\x60\xbb\x48\x9b\x25\xa4\x2a\x24\x21\xbf\x0e\x49\xde\x7b\xee\ 135 | \xf5\xe6\xde\xdb\xf3\x2e\xe7\x9c\xf7\x79\x9e\xfb\x7e\x9d\x73\x63\ 136 | \x88\x8f\xdd\x91\x97\xf2\x4c\x1e\xca\x94\x24\x44\xe4\xaf\xd4\xa5\ 137 | \x22\x5f\x65\x5f\x1a\x3e\x58\xbc\x23\x47\x89\x0e\x7e\xd6\xa1\x44\ 138 | \xce\x8b\x77\x87\x29\xb6\xe8\x11\x64\x3d\xb6\x48\x8d\x12\xc8\x51\ 139 | \x23\x8a\xd5\xf4\x3a\x1c\xfa\x02\xcd\x48\x74\x80\x26\x0b\x5e\x81\ 140 | \xd9\xff\xa0\xf7\x25\x66\xb5\xc0\x38\xf5\xc1\x44\x95\xbd\x00\xa9\ 141 | \x26\x7b\x54\x07\x7e\x9d\x71\x47\x60\xdb\x40\x96\x10\xe2\x14\xb8\ 142 | \x00\x5a\x54\x38\xe4\x90\x0a\x2d\xe0\x82\x02\x71\x84\x25\x83\xdd\ 143 | \x1e\x0a\xe4\xd5\x37\x66\xcc\x7d\x4c\x63\x99\xf3\xb1\x98\x36\x67\ 144 | \x3f\xa3\xd0\xf9\xbe\x40\x59\xa5\xe6\x90\x90\x31\xa7\xd0\x65\x44\ 145 | \xc8\xd2\x55\xa9\xe5\x50\x81\x65\x85\xee\x92\x15\xd6\x54\xe2\x17\ 146 | \x99\x50\x81\x0c\x3f\x15\x63\x4d\x28\xa9\x70\x25\x94\x2e\x08\x2b\ 147 | \x8a\x51\x12\x73\x2d\xd0\x75\x3f\xd2\x91\x23\xa5\x36\x5d\x15\xae\ 148 | \x4c\x70\x1e\x89\x2e\x08\xe7\x86\x73\x65\xc9\x98\x29\xcc\xb6\x44\ 149 | \xb5\x8e\xf1\xc6\x2c\x95\xbe\x1f\x59\xe0\x9e\xe3\x5a\xd2\x32\x7e\ 150 | \x52\x4f\x04\xd2\x93\xc6\x6f\x59\x52\x53\x53\x2f\x22\x09\x68\x54\ 151 | \xcd\x92\x63\x15\x16\xc5\x0e\xa5\xdb\x52\x54\xd1\xb1\xb0\xe8\xaa\ 152 | \xb7\x8d\xd0\x1b\xd8\x70\xe1\x17\x85\x24\x97\xae\xd4\x2e\xd9\x91\ 153 | \xe4\x07\xec\xba\xb0\x97\x24\x05\x61\x13\x80\x4f\x3c\xe5\x1b\x00\ 154 | \x6d\x76\x58\x25\xcf\x2d\x43\xb4\x79\xcc\x1b\xca\xb4\x3d\xfd\x61\ 155 | \xb3\x5f\x8d\x13\x5c\x03\x3d\x8a\xc4\xf9\xa1\x2a\x2d\x66\x04\x62\ 156 | \x7c\xf6\x69\x2f\xd7\x4c\x0c\x1b\xca\xfa\x20\xf5\x88\x29\x53\x2a\ 157 | \x4f\x5c\x8b\x9f\xf7\x11\x58\x77\x3a\x92\xcd\x11\x00\x5f\x10\xd2\ 158 | \x14\xf8\xc8\x73\xb5\x01\x41\xb8\x7b\x83\x7e\x84\xad\x9b\x6a\x96\ 159 | \x33\x00\xde\x8e\x38\xbe\xb4\x87\x7e\x36\x3c\x6a\x07\x32\xc9\x29\ 160 | \x00\xdf\x79\xcf\x2b\x3e\x70\x3b\x40\xe0\x94\x49\xbf\x1f\x4b\x86\ 161 | \x03\x05\x4a\x8f\x14\x38\xd0\x6d\xc7\xbd\xd0\x18\xaf\xf9\x1d\x28\ 162 | \xf0\x87\x55\x75\x3b\x37\x04\x04\x21\x41\x91\x13\x1a\xc4\x5d\xd9\ 163 | \x38\x0d\x4e\x78\x47\xc2\x8b\x8f\x11\xfa\xf8\x83\xed\x1f\xda\x5c\ 164 | \xa2\xd6\x42\x8d\x25\x2c\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ 165 | \x60\x82\ 166 | " 167 | 168 | qt_resource_name = b"\ 169 | \x00\x02\ 170 | \x00\x00\x07\x83\ 171 | \x00\x72\ 172 | \x00\x63\ 173 | \x00\x0f\ 174 | \x09\x7d\xcb\x07\ 175 | \x00\x47\ 176 | \x00\x69\x00\x74\x00\x48\x00\x75\x00\x62\x00\x2d\x00\x4d\x00\x61\x00\x72\x00\x6b\x00\x2e\x00\x70\x00\x6e\x00\x67\ 177 | \x00\x15\ 178 | \x0f\x0c\x2a\x07\ 179 | \x00\x47\ 180 | \x00\x69\x00\x74\x00\x48\x00\x75\x00\x62\x00\x2d\x00\x4d\x00\x61\x00\x72\x00\x6b\x00\x2d\x00\x4c\x00\x69\x00\x67\x00\x68\x00\x74\ 181 | \x00\x2e\x00\x70\x00\x6e\x00\x67\ 182 | " 183 | 184 | qt_resource_struct = b"\ 185 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 186 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ 187 | \x00\x00\x00\x0a\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 188 | \x00\x00\x00\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x06\xb6\ 189 | " 190 | 191 | def qInitResources(): 192 | QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) 193 | 194 | def qCleanupResources(): 195 | QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) 196 | 197 | qInitResources() -------------------------------------------------------------------------------- /qcrash/_hooks.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package two hooks functions: 3 | 4 | - a hook for pyqt distutis to replace PyQt5 imports by our own (qcrash.qt). 5 | - a sys.excepthook is installed by :meth:`qcrash.api.install_except_hook` 6 | """ 7 | import logging 8 | import sys 9 | import traceback 10 | 11 | from .qt import QtCore, QtWidgets 12 | 13 | 14 | def _logger(): 15 | return logging.getLogger(__name__) 16 | 17 | 18 | try: 19 | Signal = QtCore.pyqtSignal 20 | except AttributeError: 21 | Signal = QtCore.Signal 22 | 23 | 24 | def fix_qt_imports(path): 25 | with open(path, 'r') as f_script: 26 | lines = f_script.read().splitlines() 27 | new_lines = [] 28 | for l in lines: 29 | if l.startswith("import "): 30 | l = "from . " + l 31 | if "from PyQt5 import" in l: 32 | l = l.replace("from PyQt5 import", "from qcrash.qt import") 33 | new_lines.append(l) 34 | with open(path, 'w') as f_script: 35 | f_script.write("\n".join(new_lines)) 36 | 37 | 38 | def except_hook(exc, tb): 39 | from qcrash.api import show_report_dialog 40 | title = '[Unhandled exception] %s: %s' % ( 41 | exc.__class__.__name__, str(exc)) 42 | msg_box = QtWidgets.QMessageBox() 43 | msg_box.setWindowTitle('Unhandled exception') 44 | msg_box.setText('An unhandled exception has occured...') 45 | msg_box.setInformativeText( 46 | 'Would you like to report the bug to the developers?') 47 | msg_box.setIcon(msg_box.Critical) 48 | msg_box.setDetailedText(tb) 49 | msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok | 50 | QtWidgets.QMessageBox.Cancel) 51 | msg_box.button(msg_box.Ok).setText('Report') 52 | msg_box.button(msg_box.Cancel).setText('Close') 53 | if msg_box.exec_() == msg_box.Ok: 54 | show_report_dialog(window_title='Report unhandled exception', 55 | issue_title=title, traceback=tb) 56 | 57 | 58 | class QtExceptHook(QtCore.QObject): 59 | _report_exception_requested = Signal(object, object) 60 | 61 | def __init__(self, except_hook, *args, **kwargs): 62 | super(QtExceptHook, self).__init__(*args, **kwargs) 63 | sys.excepthook = self._except_hook 64 | self._report_exception_requested.connect(except_hook) 65 | 66 | def _except_hook(self, exc_type, exc_val, tb): 67 | tb = '\n'.join([''.join(traceback.format_tb(tb)), 68 | '{0}: {1}'.format(exc_type.__name__, exc_val)]) 69 | _logger().critical('unhandled exception:\n%s', tb) 70 | # exception might come from another thread, use a signal 71 | # so that we can be sure we will show the bug report dialog from 72 | # the main gui thread. 73 | self._report_exception_requested.emit(exc_val, tb) 74 | -------------------------------------------------------------------------------- /qcrash/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the top level API functions. 3 | """ 4 | from . import _hooks 5 | from .qt import QtCore 6 | 7 | from . import backends 8 | 9 | 10 | def install_backend(*args): 11 | """ 12 | Install one or more backends. 13 | 14 | Usage:: 15 | 16 | qcrash.install_backend(backend1) 17 | qcrash.install_backend(backend2, backend3) 18 | 19 | :param args: the backends to install. Each backend must be a subclass 20 | of :class:`qcrash.backends.BaseBackend` (e.g.:: 21 | :class:`qcrash.backends.EmailBackend` or 22 | :class:`qcrash.backends.GithubBackend`) 23 | """ 24 | global _backends 25 | for b in args: 26 | _backends.append(b) 27 | 28 | 29 | def get_backends(): 30 | """ 31 | Gets the list of installed backends. 32 | """ 33 | return _backends 34 | 35 | 36 | def install_except_hook(except_hook=_hooks.except_hook): 37 | """ 38 | Install an except hook that will show the crash report dialog when an 39 | unhandled exception has occured. 40 | 41 | :param except_hook: except_hook function that will be called on the main 42 | thread whenever an unhandled exception occured. The function takes 43 | two parameters: the exception object and the traceback string. 44 | """ 45 | if not _backends: 46 | raise ValueError('no backends found, you must at least install one ' 47 | 'backend before calling this function') 48 | global _except_hook 49 | _except_hook = _hooks.QtExceptHook(except_hook) 50 | 51 | 52 | def set_qsettings(qsettings): 53 | """ 54 | Sets the qsettings used by the backends to cache some information such 55 | as the user credentials. If no custom qsettings is defined, qcrash will 56 | use its own settings (QSettings('QCrash')) 57 | 58 | :param qsettings: QtCore.QSettings instance 59 | """ 60 | global _qsettings 61 | _qsettings = qsettings 62 | 63 | 64 | def show_report_dialog(window_title='Report an issue...', 65 | window_icon=None, traceback=None, issue_title='', 66 | issue_description='', parent=None, 67 | modal=None, include_log=True, include_sys_info=True): 68 | """ 69 | Show the issue report dialog manually. 70 | 71 | :param window_title: Title of dialog window 72 | :param window_icon: the icon to use for the dialog window 73 | :param traceback: optional traceback string to include in the report. 74 | :param issue_title: optional issue title 75 | :param issue_description: optional issue description 76 | :param parent: parent widget 77 | :param include_log: Initial state of the include log check box 78 | :param include_sys_info: Initial state of the include system info check box 79 | """ 80 | if not _backends: 81 | raise ValueError('no backends found, you must at least install one ' 82 | 'backend before calling this function') 83 | from ._dialogs.report import DlgReport 84 | dlg = DlgReport(_backends, window_title=window_title, 85 | window_icon=window_icon, traceback=traceback, 86 | issue_title=issue_title, 87 | issue_description=issue_description, parent=parent, 88 | include_log=include_log, include_sys_info=include_sys_info) 89 | if modal: 90 | dlg.show() 91 | return dlg 92 | else: 93 | dlg.exec_() 94 | 95 | 96 | def _return_empty_string(): 97 | return '' 98 | 99 | 100 | #: Reference to the function to use to collect system information. Client code 101 | #: should redefine it. 102 | get_system_information = _return_empty_string 103 | 104 | #: Reference to the function to use to collect the application's log. 105 | #: Client code should redefine it. 106 | get_application_log = _return_empty_string 107 | 108 | 109 | _backends = [] 110 | _qsettings = QtCore.QSettings('QCrash') 111 | 112 | 113 | __all__ = [ 114 | 'backends', 115 | 'install_backend', 116 | 'get_backends', 117 | 'install_except_hook', 118 | 'set_qsettings', 119 | 'show_report_dialog', 120 | 'get_application_log', 121 | 'get_system_information', 122 | ] 123 | -------------------------------------------------------------------------------- /qcrash/backends/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains the backends used to actually send the crash report. 3 | 4 | QCrash provides 2 backends: 5 | 6 | - email: let the user report the crash/issue via an email 7 | - github: let the user report the crash/issue on you github issue tracker 8 | 9 | To use a backend, use :meth:`qcrash.api.install_backend`. 10 | 11 | To add your own, just subclass :class:`qcrash.backends.BaseBackend` and 12 | implement `send_report`. 13 | """ 14 | from .base import BaseBackend 15 | from .email import EmailBackend 16 | from .github import GithubBackend 17 | 18 | 19 | __all__ = [ 20 | 'BaseBackend', 21 | 'EmailBackend', 22 | 'GithubBackend' 23 | ] 24 | -------------------------------------------------------------------------------- /qcrash/backends/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the base class for implementing a report backend. 3 | """ 4 | 5 | 6 | class BaseBackend(object): 7 | """ 8 | Base class for implementing a backend. 9 | 10 | Subclass must define ``button_text``, ``button_tooltip``and ``button_icon`` 11 | and implement ``send_report(title, description)``. 12 | 13 | The report's title and body will be formatted automatically by the 14 | associated :attr:`formatter`. 15 | """ 16 | def __init__(self, formatter, button_text, button_tooltip, 17 | button_icon=None, need_review=True): 18 | """ 19 | :param formatter: the associated formatter (see :meth:`set_formatter`) 20 | :param button_text: Text of the associated button in the report dialog 21 | :param button_icon: Icon of the associated button in the report dialog 22 | :param button_tooltip: Tooltip of the associated button in the report 23 | dialog 24 | :param need_review: True to show the review dialog before submitting. 25 | Some backends (such as the email backend) do not need a review 26 | dialog as the user can already review it before sending the final 27 | report 28 | """ 29 | self.formatter = formatter 30 | self.button_text = button_text 31 | self.button_tooltip = button_tooltip 32 | self.button_icon = button_icon 33 | self.need_review = need_review 34 | self.parent_widget = None 35 | 36 | def qsettings(self): 37 | """ 38 | Gets the qsettings instance that you can use to store various settings 39 | such as the user credentials (you should use the `keyring` module if 40 | you want to store user's password). 41 | """ 42 | from qcrash.api import _qsettings 43 | return _qsettings 44 | 45 | def set_formatter(self, formatter): 46 | """ 47 | Sets the formatter associated with the backend. 48 | 49 | The formatter will automatically get called to format the report title 50 | and body before ``send_report`` is being called. 51 | """ 52 | self.formatter = formatter 53 | 54 | def send_report(self, title, body, application_log=None): 55 | """ 56 | Sends the actual bug report. 57 | 58 | :param title: title of the report, already formatted. 59 | :param body: body of the reporit, already formtatted. 60 | :param application_log: Content of the application log. Default is None. 61 | 62 | :returns: Whether the dialog should be closed. 63 | """ 64 | raise NotImplementedError 65 | -------------------------------------------------------------------------------- /qcrash/backends/email.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module containes the email backend. 3 | """ 4 | from .base import BaseBackend 5 | from ..formatters.email import EmailFormatter 6 | from ..qt import QtCore, QtGui 7 | 8 | 9 | class EmailBackend(BaseBackend): 10 | """ 11 | This backend sends the crash report via email (using mailto). 12 | 13 | Usage:: 14 | 15 | email_backend = qcrash.backends.EmailBackend( 16 | 'your_email@provider.com', 'YourAppName') 17 | qcrash.install_backend(email_backend) 18 | 19 | """ 20 | def __init__(self, email, app_name, formatter=EmailFormatter()): 21 | """ 22 | :param email: email address to send the bug report to 23 | :param app_name: application name, will appear in the object of the 24 | mail 25 | :param formatter: the formatter to use to create the final report. 26 | """ 27 | super(EmailBackend, self).__init__( 28 | formatter, "Send email", "Send the report via email", 29 | QtGui.QIcon.fromTheme('mail-send'), need_review=False) 30 | self.formatter.app_name = app_name 31 | self.email = email 32 | 33 | def send_report(self, title, body, application_log=None): 34 | base_url = "mailto:%s?subject=%s" % (self.email, title) 35 | if application_log: 36 | body += "\nApplication log\n----------------\n\n%s" % application_log 37 | base_url += '&body=%s' % body 38 | url = QtCore.QUrl(base_url) 39 | QtGui.QDesktopServices.openUrl(url) 40 | return False 41 | -------------------------------------------------------------------------------- /qcrash/backends/github.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the github backend. 3 | """ 4 | import logging 5 | import webbrowser 6 | 7 | import keyring 8 | 9 | from .base import BaseBackend 10 | from ..formatters.markdown import MardownFormatter 11 | from ..qt import QtGui, QtCore, QtWidgets 12 | from .._dialogs.gh_login import DlgGitHubLogin 13 | from .._extlibs import github 14 | 15 | 16 | GH_MARK_NORMAL = ':/rc/GitHub-Mark.png' 17 | GH_MARK_LIGHT = ':/rc/GitHub-Mark-Light.png' 18 | 19 | 20 | def _logger(): 21 | return logging.getLogger(__name__) 22 | 23 | 24 | class GithubBackend(BaseBackend): 25 | """ 26 | This backend sends the crash report on a github issue tracker:: 27 | 28 | https://github.com/gh_owner/gh_repo 29 | 30 | Usage:: 31 | 32 | github_backend = qcrash.backends.GithubBackend( 33 | 'ColinDuquesnoy', 'QCrash') 34 | qcrash.install_backend(github_backend) 35 | """ 36 | def __init__(self, gh_owner, gh_repo, formatter=MardownFormatter()): 37 | """ 38 | :param gh_owner: Name of the owner of the github repository. 39 | :param gh_repo: Name of the repository on github. 40 | """ 41 | super(GithubBackend, self).__init__( 42 | formatter, "Submit on github", 43 | "Submit the issue on our issue tracker on github", None) 44 | icon = GH_MARK_NORMAL 45 | if QtWidgets.qApp.palette().base().color().lightness() < 128: 46 | icon = GH_MARK_LIGHT 47 | self.button_icon = QtGui.QIcon(icon) 48 | self.gh_owner = gh_owner 49 | self.gh_repo = gh_repo 50 | self._show_msgbox = True # False when running the test suite 51 | 52 | def send_report(self, title, body, application_log=None): 53 | _logger().debug('sending bug report on github\ntitle=%s\nbody=%s', 54 | title, body) 55 | username, password, remember, remember_pswd = self.get_user_credentials() 56 | if not username or not password: 57 | return False 58 | _logger().debug('got user credentials') 59 | 60 | # upload log file as a gist 61 | if application_log: 62 | url = self.upload_log_file(application_log) 63 | body += '\nApplication log: %s' % url 64 | try: 65 | gh = github.GitHub(username=username, password=password) 66 | repo = gh.repos(self.gh_owner)(self.gh_repo) 67 | ret = repo.issues.post(title=title, body=body) 68 | except github.ApiError as e: 69 | _logger().warn('failed to send bug report on github. response=%r' % 70 | e.response) 71 | # invalid credentials 72 | if e.response.code == 401: 73 | self.qsettings().setValue('github/remember_credentials', 0) 74 | if self._show_msgbox: 75 | QtWidgets.QMessageBox.warning( 76 | self.parent_widget, 'Invalid credentials', 77 | 'Failed to create github issue, invalid credentials...') 78 | else: 79 | # other issue 80 | if self._show_msgbox: 81 | QtWidgets.QMessageBox.warning( 82 | self.parent_widget, 83 | 'Failed to create issue', 84 | 'Failed to create github issue. Error %d' % 85 | e.response.code) 86 | return False 87 | else: 88 | issue_nbr = ret['number'] 89 | if self._show_msgbox: 90 | ret = QtWidgets.QMessageBox.question( 91 | self.parent_widget, 'Issue created on github', 92 | 'Issue successfully created. Would you like to open the ' 93 | 'ticket in your web browser?') 94 | if ret in [QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.Ok]: 95 | webbrowser.open( 96 | 'https://github.com/%s/%s/issues/%d' % ( 97 | self.gh_owner, self.gh_repo, issue_nbr)) 98 | return True 99 | 100 | def _get_credentials_from_qsettings(self): 101 | remember = self.qsettings().value('github/remember_credentials', "0") 102 | remember_password = self.qsettings().value('github/remember_password', "0") 103 | username = self.qsettings().value('github/username', "") 104 | try: 105 | # PyQt5 or PyQt4 api v2 106 | remember = bool(int(remember)) 107 | remember_password = bool(int(remember_password)) 108 | except TypeError: # pragma: no cover 109 | # pyside returns QVariants 110 | remember, _ok = remember.toInt() 111 | remember_password, _ok = remember_password.toInt() 112 | username = username.toString() 113 | if not remember: 114 | username = '' 115 | return username, bool(remember), bool(remember_password) 116 | 117 | def _store_credentials(self, username, password, remember, remember_pswd): 118 | if remember: 119 | self.qsettings().setValue('github/username', username) 120 | if remember_pswd: 121 | try: 122 | keyring.set_password('github', username, password) 123 | except RuntimeError: # pragma: no cover 124 | _logger().warn('failed to save password in keyring, you ' 125 | 'will be prompted for your credentials ' 126 | 'next time you want to report an issue') 127 | remember_pswd = False 128 | self.qsettings().setValue( 129 | 'github/remember_credentials', int(remember)) 130 | self.qsettings().setValue( 131 | 'github/remember_password', int(remember_pswd)) 132 | 133 | def get_user_credentials(self): # pragma: no cover 134 | # reason: hard to test methods that shows modal dialogs 135 | username, remember, remember_pswd = self._get_credentials_from_qsettings() 136 | 137 | if remember_pswd and username: 138 | # get password from keyring 139 | try: 140 | password = keyring.get_password('github', username) 141 | except RuntimeError: 142 | # no safe keyring backend 143 | _logger().warn('failed to retrieve password from keyring...') 144 | else: 145 | return username, password, remember, remember_pswd 146 | 147 | # ask for credentials 148 | username, password, remember, remember_pswd = DlgGitHubLogin.login( 149 | self.parent_widget, username, remember, remember_pswd) 150 | 151 | if remember: 152 | self._store_credentials(username, password, remember, remember_pswd) 153 | 154 | return username, password, remember, remember_pswd 155 | 156 | def upload_log_file(self, log_content): 157 | gh = github.GitHub() 158 | try: 159 | QtWidgets.qApp.setOverrideCursor(QtCore.Qt.WaitCursor) 160 | ret = gh.gists.post( 161 | description="OpenCobolIDE log", public=True, 162 | files={'OpenCobolIDE.log': {"content": log_content}}) 163 | QtWidgets.qApp.restoreOverrideCursor() 164 | except github.ApiError: 165 | _logger().warn('failed to upload log report as a gist') 166 | return '"failed to upload log file as a gist"' 167 | else: 168 | return ret['html_url'] 169 | -------------------------------------------------------------------------------- /qcrash/formatters/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains the formatters used by the different backends: 3 | 4 | - html_formatter: create a bug report using HTML 5 | - md_formatter: create a bug report using Markdown 6 | 7 | Implement :class:`qcrash.formatters.BaseFormatter` to add your own formatter 8 | and set it the installed backends using 9 | :meth:`qcrash.backends.BaseBackend.set_formatter`. 10 | 11 | """ 12 | -------------------------------------------------------------------------------- /qcrash/formatters/base.py: -------------------------------------------------------------------------------- 1 | class BaseFormatter(object): 2 | """ 3 | Base class for implementing a custom formatter. 4 | 5 | Just implement :meth:`format_body` and :meth:`format_title` functions and 6 | set your formatter on the backends you created. 7 | """ 8 | def format_title(self, title): 9 | """ 10 | Formats the issue title. By default this method does nothing. 11 | 12 | An email formatter might want to append the application to name to 13 | the object field... 14 | """ 15 | return title 16 | 17 | def format_body(self, description, sys_info=None, traceback=None): 18 | """ 19 | Not implemented. 20 | 21 | :param description: Description of the issue, written by the user. 22 | :param sys_info: Optional system information string 23 | :param traceback: Optional traceback. 24 | """ 25 | raise NotImplementedError 26 | -------------------------------------------------------------------------------- /qcrash/formatters/email.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the Html formatter used by the email backend. 3 | """ 4 | from .base import BaseFormatter 5 | 6 | 7 | BODY_ITEM_TEMPLATE = '''%(name)s 8 | %(delim)s 9 | 10 | %(value)s 11 | 12 | 13 | ''' 14 | 15 | NB_LINES_MAX = 50 16 | 17 | 18 | class EmailFormatter(BaseFormatter): 19 | """ 20 | Formats the crash report for use in an email (text/plain) 21 | """ 22 | def __init__(self, app_name=None): 23 | """ 24 | :param app_name: Name of the application. If set the email subject will 25 | starts with [app_name] 26 | """ 27 | self.app_name = app_name 28 | 29 | def format_title(self, title): 30 | """ 31 | Formats title (add ``[app_name]`` if app_name is not None). 32 | """ 33 | if self.app_name: 34 | return '[%s] %s' % (self.app_name, title) 35 | return title 36 | 37 | def format_body(self, description, sys_info=None, traceback=None): 38 | """ 39 | Formats the body in plain text. (add a series of '-' under each section 40 | title). 41 | 42 | :param description: Description of the issue, written by the user. 43 | :param sys_info: Optional system information string 44 | :param log: Optional application log 45 | :param traceback: Optional traceback. 46 | """ 47 | name = 'Description' 48 | delim = '-' * 40 49 | body = BODY_ITEM_TEMPLATE % { 50 | 'name': name, 'value': description, 'delim': delim 51 | } 52 | if traceback: 53 | name = 'Traceback' 54 | traceback = '\n'.join(traceback.splitlines()[-NB_LINES_MAX:]) 55 | body += BODY_ITEM_TEMPLATE % { 56 | 'name': name, 'value': traceback, 'delim': delim 57 | } 58 | if sys_info: 59 | name = 'System information' 60 | body += BODY_ITEM_TEMPLATE % { 61 | 'name': name, 'value': sys_info, 'delim': delim 62 | } 63 | return body 64 | -------------------------------------------------------------------------------- /qcrash/formatters/markdown.py: -------------------------------------------------------------------------------- 1 | """ 2 | A PyQt/PySide framework for reporting application crash (unhandled exception) 3 | and let the user report an issue/feature request. 4 | """ 5 | from .base import BaseFormatter 6 | 7 | 8 | BODY_ITEM_TEMPLATE = '''### %(name)s 9 | 10 | %(value)s 11 | 12 | ''' 13 | 14 | NB_LINES_MAX = 50 15 | 16 | 17 | class MardownFormatter(BaseFormatter): 18 | """ 19 | Formats the issue report using Markdown. 20 | """ 21 | def format_body(self, description, sys_info=None, traceback=None): 22 | """ 23 | Formats the body using markdown. 24 | 25 | :param description: Description of the issue, written by the user. 26 | :param sys_info: Optional system information string 27 | :param log: Optional application log 28 | :param traceback: Optional traceback. 29 | """ 30 | body = BODY_ITEM_TEMPLATE % { 31 | 'name': 'Description', 'value': description 32 | } 33 | if traceback: 34 | traceback = '\n'.join(traceback.splitlines()[-NB_LINES_MAX:]) 35 | body += BODY_ITEM_TEMPLATE % { 36 | 'name': 'Traceback', 'value': '```\n%s\n```' % traceback 37 | } 38 | if sys_info: 39 | sys_info = '- %s' % '\n- '.join(sys_info.splitlines()) 40 | body += BODY_ITEM_TEMPLATE % { 41 | 'name': 'System information', 'value': sys_info 42 | } 43 | return body 44 | -------------------------------------------------------------------------------- /qcrash/qt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | _logger = logging.getLogger(__name__) 5 | 6 | try: 7 | from PyQt5 import QtWidgets, QtGui, QtCore 8 | except (ImportError, RuntimeError): 9 | _logger.warning('failed to import PyQt5, going to try PyQt4') 10 | try: 11 | from PyQt4 import QtGui, QtGui as QtWidgets, QtCore 12 | except (ImportError, RuntimeError): 13 | _logger.warning('failed to import PyQt4, going to try PySide') 14 | try: 15 | from PySide import QtGui, QtCore, QtGui as QtWidgets 16 | except (ImportError, RuntimeError): 17 | _logger.warning('failed to import PySide') 18 | _logger.critical('No Qt bindings found, aborting...') 19 | try: 20 | from unittest.mock import MagicMock 21 | QtCore = MagicMock() 22 | QtGui = MagicMock() 23 | QtWidgets = MagicMock() 24 | except ImportError: 25 | sys.exit(1) 26 | 27 | __all__ = ['QtCore', 'QtGui', 'QtWidgets'] 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keyring 2 | -------------------------------------------------------------------------------- /scripts/install-qt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple script to install PyQt or PySide in CI (Travis and AppVeyor). 3 | """ 4 | from __future__ import print_function 5 | import os 6 | import sys 7 | import subprocess 8 | 9 | 10 | def apt_get_install(packages): 11 | print('Installing %s...' % ', '.join(packages)) 12 | subprocess.check_call(['sudo', 'apt-get', 'install', '-y', '-qq'] + 13 | packages + ['--fix-missing']) 14 | 15 | py3k = sys.version_info[0] == 3 16 | pyqt_version = {'pyqt4': 4, 'pyqt5': 5} 17 | pyqt_ver = pyqt_version[os.environ['QT_API']] 18 | if py3k: 19 | pkg = 'python3-pyqt%s' % pyqt_ver 20 | else: 21 | pkg = 'python-qt%s' % pyqt_ver 22 | apt_get_install([pkg]) 23 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Setup script for QCrash 4 | """ 5 | import sys 6 | from setuptools import setup, find_packages 7 | from setuptools.command.test import test as TestCommand 8 | from qcrash import __version__ 9 | 10 | try: 11 | from pyqt_distutils.build_ui import build_ui 12 | cmdclass = {'build_ui': build_ui} 13 | except ImportError: 14 | build_ui = None 15 | cmdclass = {} 16 | 17 | 18 | class PyTest(TestCommand, object): 19 | user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] 20 | 21 | def initialize_options(self): 22 | super(PyTest, self).initialize_options() 23 | self.pytest_args = [] 24 | 25 | def finalize_options(self): 26 | super(PyTest, self).finalize_options() 27 | 28 | def run_tests(self): 29 | # import here, cause outside the eggs aren't loaded 30 | import pytest 31 | errno = pytest.main(self.pytest_args) 32 | sys.exit(errno) 33 | 34 | 35 | cmdclass['test'] = PyTest 36 | 37 | 38 | # Get long description 39 | with open('README.rst', 'r') as readme: 40 | long_desc = readme.read() 41 | 42 | setup( 43 | name='qcrash', 44 | version=__version__, 45 | keywords=['Github', 'PyQt4', 'PyQt5', 'PySide', 'Issue', 'Report', 'Crash', 46 | 'Tool'], 47 | url='https://github.com/ColinDuquesnoy/qcrash', 48 | license='MIT', 49 | author='Colin Duquesnoy', 50 | author_email='colin.duquesnoy@gmail.com', 51 | description='A crash report framework for PyQt/PySide applications', 52 | long_description=long_desc, 53 | packages=find_packages(), 54 | cmdclass=cmdclass, 55 | install_requires=['keyring'], 56 | tests_require=['pytest', 'pytest-qt', 'pytest-cov', 'pytest-flake8'], 57 | entry_points={ 58 | 'pyqt_distutils_hooks': [ 59 | 'fix_qt_imports = qcrash._hooks:fix_qt_imports'] 60 | }, 61 | classifiers=[ 62 | 'Development Status :: 5 - Production/Stable', 63 | 'Environment :: X11 Applications :: Qt', 64 | 'Environment :: Win32 (MS Windows)', 65 | 'Intended Audience :: Developers', 66 | 'License :: OSI Approved :: GNU General Public License v3 or later ' 67 | '(GPLv3+)', 68 | 'Operating System :: Microsoft :: Windows', 69 | 'Operating System :: POSIX :: Linux', 70 | 'Programming Language :: Python :: 2.7', 71 | 'Programming Language :: Python :: 3.2', 72 | 'Programming Language :: Python :: 3.3', 73 | 'Programming Language :: Python :: 3.4', 74 | 'Programming Language :: Python :: 3.5', 75 | 'Topic :: Software Development :: Bug Tracking', 76 | 'Topic :: Software Development :: User Interfaces', 77 | 'Topic :: Software Development :: Widget Sets'] 78 | ) 79 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | 4 | from qcrash import api 5 | from qcrash.qt import QtCore 6 | from qcrash._hooks import QtExceptHook 7 | 8 | 9 | def test_set_qsettings(): 10 | assert api._qsettings.organizationName() == 'QCrash' 11 | api.set_qsettings(QtCore.QSettings('TestQCrash')) 12 | assert api._qsettings.organizationName() == 'TestQCrash' 13 | 14 | 15 | def test_value_errors(): 16 | api._backends[:] = [] 17 | with pytest.raises(ValueError): 18 | api.install_except_hook() 19 | with pytest.raises(ValueError): 20 | api.show_report_dialog() 21 | 22 | 23 | def test_install_backend(): 24 | api._backends[:] = [] 25 | assert len(api.get_backends()) == 0 26 | b1 = api.backends.GithubBackend('ColinDuquesnoy', 'TestQCrash') 27 | b2 = api.backends.EmailBackend('colin.duquesnoy@gmail.com', 'TestQCrash') 28 | api.install_backend(b1, b2) 29 | assert len(api.get_backends()) == 2 30 | b1 = api.backends.GithubBackend('ColinDuquesnoy', 'TestQCrash') 31 | b2 = api.backends.EmailBackend('colin.duquesnoy@gmail.com', 'TestQCrash') 32 | api.install_backend(b1) 33 | assert len(api.get_backends()) == 3 34 | api.install_backend(b2) 35 | assert len(api.get_backends()) == 4 36 | 37 | 38 | def test_install_except_hook(): 39 | api.install_except_hook() 40 | assert isinstance(api._except_hook, QtExceptHook) 41 | assert sys.excepthook == api._except_hook._except_hook 42 | 43 | 44 | def test_show_report_dialog(qtbot): 45 | api._backends[:] = [] 46 | assert len(api.get_backends()) == 0 47 | b1 = api.backends.GithubBackend('ColinDuquesnoy', 'TestQCrash') 48 | b2 = api.backends.EmailBackend('colin.duquesnoy@gmail.com', 'TestQCrash') 49 | api.install_backend(b1, b2) 50 | assert len(api.get_backends()) == 2 51 | 52 | dlg = api.show_report_dialog(modal=True) 53 | 54 | assert len(dlg.buttons) == 2 55 | 56 | # force email backend to return True to close the dialog 57 | b2.old_send_report = b2.send_report 58 | 59 | qtbot.wait(1000) 60 | 61 | def send_report(*args, **kwargs): 62 | b2.old_send_report(*args, **kwargs) 63 | return True 64 | 65 | b2.send_report = send_report 66 | dlg.buttons[1].clicked.emit(True) 67 | 68 | dlg.reject() 69 | 70 | 71 | def test_return_empty_string(): 72 | assert api._return_empty_string() == '' 73 | -------------------------------------------------------------------------------- /tests/test_backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/tests/test_backends/__init__.py -------------------------------------------------------------------------------- /tests/test_backends/test_base.py: -------------------------------------------------------------------------------- 1 | from qcrash.backends.base import BaseBackend 2 | from qcrash.formatters.email import EmailFormatter 3 | 4 | import pytest 5 | 6 | 7 | def test_qsettings(): 8 | b = BaseBackend(None, '', '', None) 9 | assert b.qsettings() is not None 10 | 11 | 12 | def test_set_formatter(): 13 | b = BaseBackend(None, '', '', None) 14 | assert b.formatter is None 15 | b.set_formatter(EmailFormatter("test")) 16 | assert isinstance(b.formatter, EmailFormatter) 17 | 18 | 19 | def test_send_report(): 20 | b = BaseBackend(None, '', '', None) 21 | with pytest.raises(NotImplementedError): 22 | b.send_report('', '') 23 | -------------------------------------------------------------------------------- /tests/test_backends/test_email.py: -------------------------------------------------------------------------------- 1 | from qcrash.backends.email import EmailBackend 2 | 3 | 4 | EMAIL = 'your.email@provider.com' 5 | 6 | 7 | def get_backend(): 8 | return EmailBackend(EMAIL, 'TestQCrash') 9 | 10 | 11 | def test_send_report(): 12 | b = get_backend() 13 | ret = b.send_report('A title', 'A body') 14 | assert ret is False # means report dialog must not be closed 15 | -------------------------------------------------------------------------------- /tests/test_backends/test_github.py: -------------------------------------------------------------------------------- 1 | from qcrash import api, qt 2 | 3 | USERNAME = 'QCrash-Tester' 4 | PASSWORD = 'TestQCrash1234' 5 | GH_OWNER = 'ColinDuquesnoy' 6 | GH_REPO = 'QCrash-Test' 7 | 8 | 9 | def get_backend(): 10 | b = api.backends.GithubBackend(GH_OWNER, GH_REPO) 11 | b._show_msgbox = False 12 | return b 13 | 14 | 15 | def get_backend_bad_repo(): 16 | b = api.backends.GithubBackend(GH_OWNER, GH_REPO + '1234') 17 | b._show_msgbox = False 18 | return b 19 | 20 | 21 | def get_wrong_user_credentials(): 22 | """ 23 | Monkeypatch GithubBackend.get_user_credentials to force the case where 24 | invalid credentias were provided 25 | """ 26 | return 'invalid', 'invalid', False, False 27 | 28 | 29 | def get_empty_user_credentials(): 30 | """ 31 | Monkeypatch GithubBackend.get_user_credentials to force the case where 32 | invalid credentias were provided 33 | """ 34 | return '', '', False, False 35 | 36 | 37 | def get_fake_user_credentials(): 38 | """ 39 | Monkeypatch GithubBackend.get_user_credentials to force the case where 40 | invalid credentias were provided 41 | """ 42 | return USERNAME, PASSWORD, False, False 43 | 44 | 45 | def test_invalid_credentials(): 46 | b = get_backend() 47 | b.get_user_credentials = get_wrong_user_credentials 48 | ret = b.send_report('Wrong credentials', 'Wrong credentials') 49 | assert ret is False 50 | 51 | 52 | def test_empty_credentials(): 53 | b = get_backend() 54 | b.get_user_credentials = get_empty_user_credentials 55 | ret = b.send_report('Empty credentials', 'Wrong credentials') 56 | assert ret is False 57 | 58 | 59 | def test_fake_credentials(): 60 | b = get_backend() 61 | b.get_user_credentials = get_fake_user_credentials 62 | ret = b.send_report('Test suite', 'Test fake credentials') 63 | assert ret is True 64 | 65 | 66 | def test_fake_credentials_bad_repo(): 67 | b = get_backend_bad_repo() 68 | b.get_user_credentials = get_fake_user_credentials 69 | ret = b.send_report('Test suite', 'Test fake credentials') 70 | assert ret is False 71 | 72 | 73 | def test_get_credentials_from_qsettings(): 74 | qsettings = qt.QtCore.QSettings('TestCrashCredentials') 75 | qsettings.clear() 76 | api.set_qsettings(qsettings) 77 | b = get_backend() 78 | username, remember, remember_password = b._get_credentials_from_qsettings() 79 | assert username == '' 80 | assert remember is False 81 | assert remember_password is False 82 | 83 | qsettings.setValue('github/username', 'toto') 84 | qsettings.setValue('github/remember_credentials', '1') 85 | qsettings.setValue('github/remember_password', '1') 86 | 87 | username, remember, remember_password = b._get_credentials_from_qsettings() 88 | assert username == 'toto' 89 | assert remember is True 90 | assert remember_password is True 91 | 92 | 93 | def test_store_user_credentials(): 94 | qsettings = qt.QtCore.QSettings('TestCrashCredentials') 95 | qsettings.clear() 96 | api.set_qsettings(qsettings) 97 | b = get_backend() 98 | b._store_credentials('user', 'toto', True, False) 99 | username, remember, remember_pasword = b._get_credentials_from_qsettings() 100 | assert username == 'user' 101 | assert remember is True 102 | assert remember_pasword is False 103 | -------------------------------------------------------------------------------- /tests/test_dialogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/tests/test_dialogs/__init__.py -------------------------------------------------------------------------------- /tests/test_dialogs/test_dlg_report.py: -------------------------------------------------------------------------------- 1 | from qcrash._dialogs.report import DlgReport 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize('include_log', [True, False]) 7 | def test_include_log_param(include_log): 8 | dlg = DlgReport([], include_log=include_log) 9 | assert dlg.ui.cb_include_application_log.isChecked() == include_log 10 | 11 | 12 | @pytest.mark.parametrize('include_sys_info', [True, False]) 13 | def test_include_sys_info_param(include_sys_info): 14 | dlg = DlgReport([], include_sys_info=include_sys_info) 15 | assert dlg.ui.cb_include_sys_info.isChecked() == include_sys_info 16 | -------------------------------------------------------------------------------- /tests/test_dialogs/test_gh_login.py: -------------------------------------------------------------------------------- 1 | from qcrash._dialogs.gh_login import DlgGitHubLogin 2 | 3 | 4 | def test_github_login_dialog(): 5 | dlg = DlgGitHubLogin(None, '', False, False) 6 | assert not dlg.ui.bt_sign_in.isEnabled() 7 | dlg.ui.le_username.setText('user') 8 | dlg.ui.le_password.setText('password') 9 | assert dlg.ui.cb_remember.isChecked() is False 10 | assert dlg.ui.cb_remember_password.isChecked() is False 11 | assert dlg.ui.cb_remember_password.isEnabled() is False 12 | assert dlg.ui.bt_sign_in.isEnabled() 13 | 14 | 15 | def test_focus_username(qtbot): 16 | dlg = DlgGitHubLogin(None, '', False, False) 17 | dlg.show() 18 | assert dlg.ui.le_username.text() == '' 19 | assert dlg.ui.le_password.text() == '' 20 | qtbot.waitForWindowShown(dlg) 21 | dlg.close() 22 | 23 | 24 | def test_focus_password(qtbot): 25 | dlg = DlgGitHubLogin(None, 'user', False, False) 26 | dlg.show() 27 | qtbot.waitForWindowShown(dlg) 28 | assert dlg.ui.le_username.text() == 'user' 29 | assert dlg.ui.le_password.text() == '' 30 | dlg.close() 31 | -------------------------------------------------------------------------------- /tests/test_dialogs/test_review.py: -------------------------------------------------------------------------------- 1 | from qcrash._dialogs.review import DlgReview 2 | 3 | 4 | def test_review(qtbot): 5 | dlg = DlgReview('some content', 'log content', None, None) 6 | assert dlg.ui.edit_main.toPlainText() == 'some content' 7 | assert dlg.ui.edit_log.toPlainText() == 'log content' 8 | qtbot.keyPress(dlg.ui.edit_main, 'A') 9 | assert dlg.ui.edit_main.toPlainText() == 'Asome content' 10 | qtbot.keyPress(dlg.ui.edit_log, 'A') 11 | assert dlg.ui.edit_log.toPlainText() == 'Alog content' 12 | -------------------------------------------------------------------------------- /tests/test_formatters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinDuquesnoy/QCrash/775e1b15764e2041a8f9a08bea938e4d6ce817c7/tests/test_formatters/__init__.py -------------------------------------------------------------------------------- /tests/test_formatters/test_base.py: -------------------------------------------------------------------------------- 1 | from qcrash.formatters.base import BaseFormatter 2 | import pytest 3 | 4 | 5 | def test_format_title(): 6 | title = 'test' 7 | assert BaseFormatter().format_title(title) == title 8 | 9 | 10 | def test_format_body(): 11 | with pytest.raises(NotImplementedError): 12 | assert BaseFormatter().format_body('') 13 | -------------------------------------------------------------------------------- /tests/test_formatters/test_email.py: -------------------------------------------------------------------------------- 1 | from qcrash.formatters.email import EmailFormatter 2 | 3 | 4 | def test_format_title(): 5 | title = 'test' 6 | appname = 'TestQCrash' 7 | expected = '[%s] %s' % (appname, title) 8 | assert EmailFormatter(app_name=appname).format_title(title) == expected 9 | assert EmailFormatter().format_title(title) == title 10 | 11 | 12 | def test_format_body(): 13 | appname = 'TestQCrash' 14 | description = 'A description' 15 | traceback = 'A traceback' 16 | sys_info = '''OS: Linux 17 | Python: 3.4.1 18 | Qt: 5.5.1''' 19 | expected = '''Description 20 | ---------------------------------------- 21 | 22 | %s 23 | 24 | 25 | Traceback 26 | ---------------------------------------- 27 | 28 | %s 29 | 30 | 31 | System information 32 | ---------------------------------------- 33 | 34 | %s 35 | 36 | 37 | ''' % (description, traceback, sys_info) 38 | 39 | assert EmailFormatter(app_name=appname).format_body( 40 | description, sys_info, traceback) == expected 41 | 42 | 43 | def test_format_body_no_log_no_sys_info(): 44 | appname = 'TestQCrash' 45 | description = 'A description' 46 | traceback = 'A traceback' 47 | sys_info = None 48 | expected = '''Description 49 | ---------------------------------------- 50 | 51 | %s 52 | 53 | 54 | Traceback 55 | ---------------------------------------- 56 | 57 | %s 58 | 59 | 60 | ''' % (description, traceback) 61 | 62 | assert EmailFormatter(app_name=appname).format_body( 63 | description, sys_info, traceback) == expected 64 | -------------------------------------------------------------------------------- /tests/test_formatters/test_markdown.py: -------------------------------------------------------------------------------- 1 | from qcrash.formatters.markdown import MardownFormatter 2 | 3 | 4 | def test_format_body(): 5 | description = 'A description' 6 | traceback = 'A traceback' 7 | sys_info = '''OS: Linux 8 | Python: 3.4.1 9 | Qt: 5.5.1''' 10 | expected = '''### Description 11 | 12 | %s 13 | 14 | ### Traceback 15 | 16 | ``` 17 | %s 18 | ``` 19 | 20 | ### System information 21 | 22 | - OS: Linux 23 | - Python: 3.4.1 24 | - Qt: 5.5.1 25 | 26 | ''' % (description, traceback) 27 | 28 | assert MardownFormatter().format_body( 29 | description, sys_info, traceback) == expected 30 | 31 | 32 | def test_format_body_no_log_no_sys_info(): 33 | description = 'A description' 34 | traceback = 'A traceback' 35 | sys_info = None 36 | expected = '''### Description 37 | 38 | %s 39 | 40 | ### Traceback 41 | 42 | ``` 43 | %s 44 | ``` 45 | 46 | ''' % (description, traceback) 47 | 48 | assert MardownFormatter().format_body( 49 | description, sys_info, traceback) == expected 50 | --------------------------------------------------------------------------------