Skip to content

Commit

Permalink
get package working with python 2.7, 3.4-3.6
Browse files Browse the repository at this point in the history
  • Loading branch information
jborbely committed Aug 28, 2017
1 parent c54f76d commit e1ebf95
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 63 deletions.
10 changes: 5 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ Alternatively, using the `MSL Package Manager`_ run::

Dependencies
------------
* Python versions 3.5, 3.6
* Python 2.7, 3.4-3.6
* PyQt5_
* `Python for .NET`_ -- if installing on Windows
* `Python for .NET`_ -- only required if you want to load icons from DLL/EXE files on Windows

.. |docs| image:: https://readthedocs.org/projects/msl-qt/badge/?version=latest
:target: http://msl-qt.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
:scale: 100%

.. _Qt: https://wiki.python.org/moin/PyQt
.. _Qt: https://www.qt.io/
.. _MSL Package Manager: http://msl-package-manager.readthedocs.io/en/latest/?badge=latest
.. _PyQt5: https://pypi.python.org/pypi/PyQt5
.. _Python for .NET: https://pypi.python.org/pypi/pythonnet/
.. _PyQt5: https://www.riverbankcomputing.com/software/pyqt/download5
.. _Python for .NET: https://pythonnet.github.io/
3 changes: 1 addition & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ MSL-Qt

This package provides custom Qt_ components that can be used for the user interface.

.. _Qt: https://wiki.python.org/moin/PyQt
.. _Qt: https://www.qt.io/

Contents
========
Expand All @@ -22,4 +22,3 @@ Indices and tables

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
8 changes: 4 additions & 4 deletions docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ Alternatively, using the `MSL Package Manager`_ run::

Dependencies
------------
* Python versions 3.5, 3.6
* Python 2.7, 3.4-3.6
* PyQt5_
* `Python for .NET`_ -- if installing on Windows
* `Python for .NET`_ -- only required if you want to load icons from DLL/EXE files on Windows

.. _MSL Package Manager: http://msl-package-manager.readthedocs.io/en/latest/?badge=latest
.. _PyQt5: https://pypi.python.org/pypi/PyQt5
.. _Python for .NET: https://pypi.python.org/pypi/pythonnet/
.. _PyQt5: https://www.riverbankcomputing.com/software/pyqt/download5
.. _Python for .NET: https://pythonnet.github.io/
2 changes: 1 addition & 1 deletion msl/examples/qt/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def show():

logger.resize(800, 400)
logger.show()
app.exec()
app.exec_()

if __name__ == '__main__':
show()
2 changes: 1 addition & 1 deletion msl/examples/qt/show_standard_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __init__(self):
self.update_message('Loaded {} icons.'.format(self.num_icons))
self.progress_bar.hide()

app.exec()
app.exec_()

def add_qt_tab(self, label, icons):
"""Add the Qt icons."""
Expand Down
6 changes: 3 additions & 3 deletions msl/qt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
Custom `Qt <https://wiki.python.org/moin/PyQt>`_ components for the user interface.
Custom `Qt <https://www.qt.io/>`_ components for the user interface.
"""
import sys
from collections import namedtuple

from PyQt5 import QtWidgets
from PyQt5 import QtWidgets, QtCore

__author__ = 'Joseph Borbely'
__copyright__ = '\xa9 2017, ' + __author__
Expand Down Expand Up @@ -49,7 +49,7 @@ def application(args=None):
'ns7XZd0pDR49A8qrM3Eht5lgF/H9lKAuFC5KM1me5V48ZAVgBwJTNgwLD3/f3Q2sm57LA/SbCSuMiUggKpK' \
'QbQN4KwgXy9AA4r13gAhPhxfL6T8y/FrQlyIjl7wYAAAAASUVORK5CYII='
from msl.qt.io import get_icon
app.setWindowIcon(get_icon(logo.encode()))
app.setWindowIcon(get_icon(QtCore.QByteArray(logo.encode())))

return app

Expand Down
40 changes: 20 additions & 20 deletions msl/qt/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""
import os
import sys
import base64
import fnmatch

from PyQt5 import QtWidgets, QtGui, QtCore
Expand All @@ -29,8 +28,8 @@ def get_icon(obj):
get_icon(PyQt5.QtWidgets.QStyle.SP_TitleBarMenuButton)
* :obj:`bytes`: A `Base64 <https://en.wikipedia.org/wiki/Base64>`_ representation
of an encoded image.
* :class:`~QtCore.QByteArray`: A `Base64 <https://en.wikipedia.org/wiki/Base64>`_
representation of an encoded image.
See :func:`image_to_base64`.
Expand Down Expand Up @@ -78,7 +77,7 @@ def get_icon(obj):
Raises
------
:obj:`FileNotFoundError`
:obj:`IOError`
If `obj` is of type :obj:`str` and the file cannot be found.
:obj:`TypeError`
If the data type of `obj` is not supported.
Expand All @@ -103,27 +102,27 @@ def get_icon(obj):
full_path = os.path.join(path, obj)
if os.path.isfile(full_path):
return QtGui.QIcon(full_path)
raise FileNotFoundError("Cannot find image file '{}'".format(obj))
raise IOError("Cannot find image file '{}'".format(obj))
elif isinstance(obj, QtWidgets.QStyle.StandardPixmap):
return QtGui.QIcon(application().style().standardIcon(obj))
elif isinstance(obj, QtGui.QPixmap):
return QtGui.QIcon(obj)
elif isinstance(obj, bytes):
elif isinstance(obj, (bytes, bytearray, QtCore.QByteArray)):
img = QtGui.QImage()
img.loadFromData(QtCore.QByteArray.fromBase64(obj))
return QtGui.QIcon(QtGui.QPixmap.fromImage(img))
else:
raise TypeError("Argument has unexpected type '{}'".format(type(obj).__name__))
raise TypeError('Argument has unexpected type {}'.format(type(obj)))


def image_to_base64(image=None, size=None, mode=QtCore.Qt.KeepAspectRatio, fmt='PNG'):
"""Encode the image using Base64_ and return the encoded :obj:`bytes`.
"""The image as a :class:`~QtCore.QByteArray` encoded as Base64_.
This function is useful if you want to save images in a database or if you
want to use images in your GUI and rather than loading images from a
file on the hard disk you define your images in a Python module as a Base64_
:obj:`bytes` variable. Loading the images from the hard disk means that you
must also distribute the images with your Python code if you share your code.
variable. Loading the images from the hard disk means that you must also distribute
the images with your Python code if you share your code.
.. _Base64: https://en.wikipedia.org/wiki/Base64
Expand All @@ -146,12 +145,12 @@ def image_to_base64(image=None, size=None, mode=QtCore.Qt.KeepAspectRatio, fmt='
Returns
-------
:obj:`bytes`
The Base64_ representation of the image encoded as :obj:`bytes`.
:class:`~QtCore.QByteArray`
The Base64_ representation of the image.
Raises
------
:obj:`FileNotFoundError`
:obj:`IOError`
If the image file cannot be found.
:obj:`ValueError`
If the image format, `fmt`, to use for converting is not supported.
Expand Down Expand Up @@ -186,14 +185,14 @@ def image_to_base64(image=None, size=None, mode=QtCore.Qt.KeepAspectRatio, fmt='
if not os.path.isfile(path):
err_msg = "Cannot find DLL/EXE file '{}'".format(s[0])
if os.path.split(path)[0]: # then it wasn't just the filename that was specified
raise FileNotFoundError(err_msg)
raise IOError(err_msg)

filename = os.path.splitext(os.path.basename(path))[0]
path = 'C:/Windows/System32/{}.dll'.format(filename)
if not os.path.isfile(path):
path = 'C:/Windows/{}.exe'.format(filename)
if not os.path.isfile(path):
raise FileNotFoundError(err_msg)
raise IOError(err_msg)

# extract the handle to the "large" icon
path_ptr = ctypes.c_char_p(path.encode())
Expand All @@ -211,10 +210,10 @@ def image_to_base64(image=None, size=None, mode=QtCore.Qt.KeepAspectRatio, fmt='
# get the icon bitmap and convert it to base64
handle = clr.System.Int32(handle_large.value)
handle_ptr = clr.System.IntPtr.op_Explicit(handle)
bmp = clr.System.Drawing.Bitmap.FromHicon(handle_ptr)
stream = clr.System.IO.MemoryStream()
bmp = clr.System.Drawing.Icon.FromHandle(handle_ptr).ToBitmap()
bmp.Save(stream, img_fmts[fmt])
base = base64.b64encode(bytes(stream.GetBuffer()))
base = QtCore.QByteArray(clr.System.Convert.ToBase64String(stream.GetBuffer()).encode())

# clean up
ctypes.windll.user32.DestroyIcon(handle_large)
Expand All @@ -229,14 +228,15 @@ def image_to_base64(image=None, size=None, mode=QtCore.Qt.KeepAspectRatio, fmt='
filters = {'Images': ('bmp', 'jpg', 'jpeg', 'png'), 'All files': '*'}
image = prompt.filename(title=title, filters=filters)
if image is None:
return b''
return QtCore.QByteArray()
image = str(image)

icon = get_icon(image)
try:
default_size = icon.availableSizes()[-1] # use the largest size as the default size
except IndexError:
prompt.critical('Invalid image file.')
return b''
return QtCore.QByteArray()
pixmap = icon.pixmap(default_size)

if size is None:
Expand All @@ -261,7 +261,7 @@ def image_to_base64(image=None, size=None, mode=QtCore.Qt.KeepAspectRatio, fmt='
buffer.open(QtCore.QIODevice.WriteOnly)
pixmap.save(buffer, fmt)
buffer.close()
return bytes(array.toBase64())
return array.toBase64()


def get_drag_enter_paths(event, pattern=None):
Expand Down
38 changes: 30 additions & 8 deletions msl/qt/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,31 @@

class Logger(logging.Handler, QtWidgets.QWidget):

def __init__(self, **kwargs):
def __init__(self,
level=logging.INFO,
fmt='%(asctime)s [%(levelname)s] -- %(name)s -- %(message)s',
datefmt=None,
):
"""A :class:`QWidget` to display :mod:`logging` messages.
Parameters
----------
**kwargs
The keyword arguments are passed to :obj:`logging.basicConfig`.
level : :obj:`int`, optional
The default `logging level`_ to use to display the :obj:`~logging.LogRecord`
(e.g., ``logging.INFO``).
.. _logging level: https://docs.python.org/3/library/logging.html#logging-levels
fmt : :obj:`str`, optional
The `string format`_ to use to display the :obj:`~logging.LogRecord`.
.. _string format: https://docs.python.org/3/library/logging.html#logrecord-attributes
datefmt : :obj:`str` or :obj:`None`, optional
The `date format`_ to use for the time stamp. If :obj:`None` then the ``ISO8601``
date format is used.
.. _date format: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior
Example
-------
Expand Down Expand Up @@ -56,11 +74,15 @@ def __init__(self, **kwargs):
self.color_map[logging.DEMO] = QtGui.QColor(93, 170, 78)
self._level_names['DEMO'] = logging.DEMO

#
# configure logging
self._current_level = kwargs.pop('level', logging.INFO)
handlers = (self,) + kwargs.pop('handlers', ())
format = kwargs.pop('format', '%(asctime)s [%(levelname)s] -- %(name)s -- %(message)s')
logging.basicConfig(level=logging.NOTSET, handlers=handlers, format=format, **kwargs)
#

root = logging.getLogger()
self._current_level = level # set the initial logging level
root.setLevel(logging.NOTSET) # however, the root logger must have access to all logging levels
self.setFormatter(logging.Formatter(fmt, datefmt))
logging.getLogger().addHandler(self)

#
# create the widgets
Expand Down Expand Up @@ -108,7 +130,7 @@ def __init__(self, **kwargs):

@property
def records(self):
""":obj:`list` of :obj:`logging.LogRecord`: The history of all the log records."""
""":obj:`list` of :obj:`~logging.LogRecord`: The history of all the log records."""
return self._records

def emit(self, record):
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def fetch_init(key):
'Development Status :: 3 - Alpha',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Scientific/Engineering :: Physics',
Expand Down
2 changes: 1 addition & 1 deletion tests/iterate_through_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import msl.examples.qt as ex

examples = [
ex.LoopExample,
ex.logger.show,
ex.toggle_switch.show,
ex.LoopExample,
ex.ShowStandardIcons,
]

Expand Down
33 changes: 17 additions & 16 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,27 @@ def test_get_icon():
assert isinstance(io.get_icon(QtGui.QPixmap()), QtGui.QIcon)
assert isinstance(io.get_icon(QtWidgets.QStyle.SP_TitleBarMenuButton), QtGui.QIcon)

base64 = b'iVBORw0KGgoAAAANSUhEUgAAACgAAABCCAYAAAAlkZRRAAAACXBIWXMAAAsTAAALEwEAmpwYAAABpElEQVR' \
b'oge2ZT07CQBSHf29kLXKOsnApBEx7DAl3sT2BZzAx6C1ahYBLFzRxyQ1Q12SeiwoSQonjzMiLmW9Fh+nMlz' \
b'ft/HkFhEOuGnofRLz+3RyVztpVrhryhXjBhu8OlsN2DADQ/NYalS+m93sXVJpzACBGASAxvt+1kGuCoC3On' \
b'kGXc9824iMYBG0JgrZ4X0lMWe+KtKKkdTcvQgT3sRy2Y6URr6+bo3laV/cogpUcX28VpbV1vdtY4iyCYcsv' \
b'lSBoi7j94G474iMoXjDMg7YEQVvEC4rPDxq/xctBdH7CuAGA0/vSOBlkivk0o+iMNcfuVWq6+6uOfot4wdo' \
b'h/riKcqbqYOMrMfQTxEdQvKC4/eAu4iMYBG0RL9gAthd6yg4lco6B+AiKFzSfB1erBVQj8+CyF2PB1sPrAg' \
b'fyea4RP8TiBb+GmDIA0ArF5h/CLUCPx5AKuISAsJJYIV4w8O8hAOj2O9VbTJRNn6YpAHT6nZxQnYun49nmQ' \
b'NTrXcSaKN8t37RRU85AMRvPEgDoXnZT8Pe3un31FXMymTzL/xwrXlA8n2MHdwPYAbB5AAAAAElFTkSuQmCC'
assert isinstance(io.get_icon(base64), QtGui.QIcon)
base64 = 'iVBORw0KGgoAAAANSUhEUgAAACgAAABCCAYAAAAlkZRRAAAACXBIWXMAAAsTAAALEwEAmpwYAAABpElEQVR' \
'oge2ZT07CQBSHf29kLXKOsnApBEx7DAl3sT2BZzAx6C1ahYBLFzRxyQ1Q12SeiwoSQonjzMiLmW9Fh+nMlz' \
'ft/HkFhEOuGnofRLz+3RyVztpVrhryhXjBhu8OlsN2DADQ/NYalS+m93sXVJpzACBGASAxvt+1kGuCoC3On' \
'kGXc9824iMYBG0JgrZ4X0lMWe+KtKKkdTcvQgT3sRy2Y6URr6+bo3laV/cogpUcX28VpbV1vdtY4iyCYcsv' \
'lSBoi7j94G474iMoXjDMg7YEQVvEC4rPDxq/xctBdH7CuAGA0/vSOBlkivk0o+iMNcfuVWq6+6uOfot4wdo' \
'h/riKcqbqYOMrMfQTxEdQvKC4/eAu4iMYBG0RL9gAthd6yg4lco6B+AiKFzSfB1erBVQj8+CyF2PB1sPrAg' \
'fyea4RP8TiBb+GmDIA0ArF5h/CLUCPx5AKuISAsJJYIV4w8O8hAOj2O9VbTJRNn6YpAHT6nZxQnYun49nmQ' \
'NTrXcSaKN8t37RRU85AMRvPEgDoXnZT8Pe3un31FXMymTzL/xwrXlA8n2MHdwPYAbB5AAAAAElFTkSuQmCC'

assert isinstance(io.get_icon(QtCore.QByteArray(base64.encode())), QtGui.QIcon)
assert isinstance(io.get_icon(bytearray(base64.encode())), QtGui.QIcon)

default_size = io.get_icon(QtWidgets.QStyle.SP_TitleBarMenuButton).availableSizes()[-1]
assert default_size.width() == 64 # the default size is chosen to be the largest QSize
assert default_size.height() == 64
assert isinstance(default_size, QtCore.QSize)

with pytest.raises(TypeError):
io.get_icon(None)

with pytest.raises(TypeError):
io.get_icon(99999)

with pytest.raises(TypeError):
io.get_icon(bytearray(base64))

with pytest.raises(IOError):
io.get_icon('this is not an image')

Expand All @@ -62,7 +60,7 @@ def test_get_icon():
assert isinstance(io.get_icon('shell32.dll|0'), QtGui.QIcon)
assert isinstance(io.get_icon('shell32|0'), QtGui.QIcon)
with pytest.raises(IOError):
io.get_icon('/shell32|0') # it appears as though a full path is being specified
io.get_icon('/shell32|0') # fails because it appears as though a full path is being specified
assert isinstance(io.get_icon('C:/Windows/explorer.exe|0'), QtGui.QIcon)
assert isinstance(io.get_icon('explorer.exe|0'), QtGui.QIcon)
assert isinstance(io.get_icon('explorer|0'), QtGui.QIcon)
Expand All @@ -73,12 +71,15 @@ def test_get_icon():


def test_image_to_base64():
assert isinstance(io.image_to_base64('explorer|0'), QtCore.QByteArray)
assert isinstance(io.image_to_base64(QtWidgets.QStyle.SP_TitleBarMenuButton), QtCore.QByteArray)

image = io.get_icon('gamma.png')
default_size = QtCore.QSize(191, 291)
assert default_size.width() == 191
assert default_size.height() == 291

assert isinstance(io.image_to_base64(image), bytes)
assert isinstance(io.image_to_base64(image), QtCore.QByteArray)

new_size = io.get_icon(io.image_to_base64(image)).availableSizes()[0]
assert new_size.width() == default_size.width()
Expand Down

0 comments on commit e1ebf95

Please sign in to comment.