arrayToQPath can handle empty paths (#1920)

* Fixes #1888

* Improve test coverage of arrayToQPath

* Use early exit to solve empty path instead of slice manipulation

* address codeql qualms: Unused import, uneven tuple
This commit is contained in:
ntjess 2021-08-02 13:18:25 -04:00 committed by GitHub
parent d0961cc320
commit e18d1fb1f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 81 additions and 42 deletions

View File

@ -1925,6 +1925,8 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True):
path = QtGui.QPainterPath() path = QtGui.QPainterPath()
n = x.shape[0] n = x.shape[0]
if n == 0:
return path
connect_array = None connect_array = None
if isinstance(connect, np.ndarray): if isinstance(connect, np.ndarray):

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from collections import OrderedDict from collections import OrderedDict
from copy import deepcopy from copy import deepcopy
from contextlib import suppress
from pyqtgraph.functions import arrayToQPath, eq from pyqtgraph.functions import arrayToQPath, eq
import numpy as np import numpy as np
@ -264,20 +263,44 @@ def test_CIELab_reconversion():
MoveToElement = pg.QtGui.QPainterPath.ElementType.MoveToElement MoveToElement = pg.QtGui.QPainterPath.ElementType.MoveToElement
LineToElement = pg.QtGui.QPainterPath.ElementType.LineToElement LineToElement = pg.QtGui.QPainterPath.ElementType.LineToElement
_dtypes = []
for bits in 32, 64:
for base in 'int', 'float', 'uint':
_dtypes.append(f'{base}{bits}')
_dtypes.extend(['uint8', 'uint16'])
def _handle_underflow(dtype, *elements):
"""Wrapper around path description which converts underflow into proper points"""
out = []
for el in elements:
newElement = [el[0]]
for ii in range(1, 3):
coord = el[ii]
if coord < 0:
coord = np.array(coord, dtype=dtype)
newElement.append(float(coord))
out.append(tuple(newElement))
return out
@pytest.mark.parametrize( @pytest.mark.parametrize(
"xs, ys, connect, expected", [ "xs, ys, connect, expected", [
*(
( (
np.arange(6), np.arange(0, -6, step=-1), 'all', ( np.arange(6, dtype=dtype), np.arange(0, -6, step=-1, dtype=dtype), 'all',
_handle_underflow(dtype,
(MoveToElement, 0.0, 0.0), (MoveToElement, 0.0, 0.0),
(LineToElement, 1.0, -1.0), (LineToElement, 1.0, -1.0),
(LineToElement, 2.0, -2.0), (LineToElement, 2.0, -2.0),
(LineToElement, 3.0, -3.0), (LineToElement, 3.0, -3.0),
(LineToElement, 4.0, -4.0), (LineToElement, 4.0, -4.0),
(LineToElement, 5.0, -5.0), (LineToElement, 5.0, -5.0)
) )
) for dtype in _dtypes
), ),
*(
( (
np.arange(6), np.arange(0, -6, step=-1), 'pairs', ( np.arange(6, dtype=dtype), np.arange(0, -6, step=-1, dtype=dtype), 'pairs',
_handle_underflow(dtype,
(MoveToElement, 0.0, 0.0), (MoveToElement, 0.0, 0.0),
(LineToElement, 1.0, -1.0), (LineToElement, 1.0, -1.0),
(MoveToElement, 2.0, -2.0), (MoveToElement, 2.0, -2.0),
@ -285,16 +308,21 @@ LineToElement = pg.QtGui.QPainterPath.ElementType.LineToElement
(MoveToElement, 4.0, -4.0), (MoveToElement, 4.0, -4.0),
(LineToElement, 5.0, -5.0), (LineToElement, 5.0, -5.0),
) )
) for dtype in _dtypes
), ),
*(
( (
np.arange(5), np.arange(0, -5, step=-1), 'pairs', ( np.arange(5, dtype=dtype), np.arange(0, -5, step=-1, dtype=dtype), 'pairs',
_handle_underflow(dtype,
(MoveToElement, 0.0, 0.0), (MoveToElement, 0.0, 0.0),
(LineToElement, 1.0, -1.0), (LineToElement, 1.0, -1.0),
(MoveToElement, 2.0, -2.0), (MoveToElement, 2.0, -2.0),
(LineToElement, 3.0, -3.0), (LineToElement, 3.0, -3.0),
(MoveToElement, 4.0, -4.0) (MoveToElement, 4.0, -4.0)
) )
) for dtype in _dtypes
), ),
# NaN types don't coerce to integers, don't test for all types since that doesn't make sense
( (
np.arange(5), np.array([0, -1, np.NaN, -3, -4]), 'finite', ( np.arange(5), np.array([0, -1, np.NaN, -3, -4]), 'finite', (
(MoveToElement, 0.0, 0.0), (MoveToElement, 0.0, 0.0),
@ -313,24 +341,33 @@ LineToElement = pg.QtGui.QPainterPath.ElementType.LineToElement
(LineToElement, 4.0, -4.0) (LineToElement, 4.0, -4.0)
) )
), ),
*(
( (
np.arange(5), np.arange(0, -5, step=-1), np.array([0, 1, 0, 1, 0]), ( np.arange(5, dtype=dtype), np.arange(0, -5, step=-1, dtype=dtype), np.array([0, 1, 0, 1, 0]),
_handle_underflow(dtype,
(MoveToElement, 0.0, 0.0), (MoveToElement, 0.0, 0.0),
(MoveToElement, 1.0, -1.0), (MoveToElement, 1.0, -1.0),
(LineToElement, 2.0, -2.0), (LineToElement, 2.0, -2.0),
(MoveToElement, 3.0, -3.0), (MoveToElement, 3.0, -3.0),
(LineToElement, 4.0, -4.0) (LineToElement, 4.0, -4.0)
) )
) ) for dtype in _dtypes
),
# Empty path with all types of connection
*(
(
np.arange(0), np.arange(0, dtype=dtype), conn, ()
) for conn in ['all', 'pairs', 'finite', np.array([])] for dtype in _dtypes
),
] ]
) )
def test_arrayToQPath(xs, ys, connect, expected): def test_arrayToQPath(xs, ys, connect, expected):
path = arrayToQPath(xs, ys, connect=connect) path = arrayToQPath(xs, ys, connect=connect)
element = None
for i in range(path.elementCount()): for i in range(path.elementCount()):
with suppress(NameError):
# nan elements add two line-segments, for simplicity of test config # nan elements add two line-segments, for simplicity of test config
# we can ignore the second segment # we can ignore the second segment
if (eq(element.x, np.nan) or eq(element.y, np.nan)): if element is not None and (eq(element.x, np.nan) or eq(element.y, np.nan)):
continue continue
element = path.elementAt(i) element = path.elementAt(i)
assert eq(expected[i], (element.type, element.x, element.y)) assert eq(expected[i], (element.type, element.x, element.y))