diff --git a/docs/_theme/flask_theme_support.py b/docs/_theme/flask_theme_support.py
index 288a708..6915638 100644
--- a/docs/_theme/flask_theme_support.py
+++ b/docs/_theme/flask_theme_support.py
@@ -1,18 +1,18 @@
# flasky extensions. flasky pygments style based on tango style
from pygments.style import Style
from pygments.token import (
- Keyword,
- Name,
Comment,
- String,
Error,
+ Generic,
+ Keyword,
+ Literal,
+ Name,
Number,
Operator,
- Generic,
- Whitespace,
- Punctuation,
Other,
- Literal,
+ Punctuation,
+ String,
+ Whitespace,
)
diff --git a/docs/conf.py b/docs/conf.py
index 00d2a5b..ab0af5f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
#
# Documentation build configuration file, created by
# sphinx-quickstart on Thu Feb 27 20:00:23 2014.
@@ -12,19 +11,18 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
+import datetime
import os
import sys
-import datetime
try:
- import numpy
+ import numpy as np
- assert numpy
+ assert np
except ImportError:
# From the readthedocs manual
# http://read-the-docs.readthedocs.org/en/latest/faq.html?highlight=numpy
- print('Unable to import numpy, falling back to mock', file=sys.stderr)
- import mock
+ from unittest import mock
MOCK_MODULES = ['pygtk', 'gtk', 'gobject', 'argparse', 'numpy', 'pandas']
for mod_name in MOCK_MODULES:
@@ -36,7 +34,6 @@
sys.path.insert(0, os.path.abspath('..'))
from stl import __about__ as metadata
-
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@@ -70,10 +67,7 @@
# General information about the project.
project = metadata.__package_name__.replace('-', ' ').capitalize()
-copyright = '%s, %s' % (
- datetime.date.today().year,
- metadata.__author__,
-)
+copyright = f'{datetime.date.today().year}, {metadata.__author__}'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -219,14 +213,15 @@
#'preamble': '',
}
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
+# Grouping the document tree into LaTeX files. List of tuples (source start
+# file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
(
'index',
- '%s.tex' % metadata.__package_name__,
- '%s Documentation'
- % metadata.__package_name__.replace('-', ' ').capitalize(),
+ f'{metadata.__package_name__}.tex',
+ '{} Documentation'.format(
+ metadata.__package_name__.replace('-', ' ').capitalize()
+ ),
metadata.__author__,
'manual',
)
@@ -261,8 +256,9 @@
(
'index',
metadata.__package_name__,
- '%s Documentation'
- % metadata.__package_name__.replace('-', ' ').capitalize(),
+ '{} Documentation'.format(
+ metadata.__package_name__.replace('-', ' ').capitalize()
+ ),
[metadata.__author__],
1,
)
@@ -281,8 +277,9 @@
(
'index',
metadata.__package_name__,
- '%s Documentation'
- % metadata.__package_name__.replace('-', ' ').capitalize(),
+ '{} Documentation'.format(
+ metadata.__package_name__.replace('-', ' ').capitalize()
+ ),
metadata.__author__,
metadata.__package_name__,
metadata.__description__,
@@ -311,11 +308,10 @@
epub_publisher = metadata.__author__
epub_copyright = copyright
-# The HTML theme for the epub output. Since the default themes are not optimized
-# for small screen space, using the same theme for HTML and epub output is
-# usually not wise. This defaults to 'epub', a theme designed to save visual
-# space.
-# epub_theme = 'epub'
+# The HTML theme for the epub output. Since the default themes are not
+# optimized for small screen space, using the same theme for HTML and epub
+# output is usually not wise. This defaults to 'epub', a theme designed to
+# save visual space. epub_theme = 'epub'
# The language of the text. It defaults to the language option
# or en if the language is not set.
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..d4a2734
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,111 @@
+# We keep the ruff configuration separate so it can easily be shared across
+# all projects
+
+target-version = 'py39'
+
+src = ['stl']
+exclude = [
+ '.tox',
+ # Ignore local test files/directories/old-stuff
+ 'test.py',
+ '*_old.py',
+]
+
+line-length = 79
+
+[lint]
+ignore = [
+ 'A001', # Variable {name} is shadowing a Python builtin
+ 'A002', # Argument {name} is shadowing a Python builtin
+ 'A003', # Class attribute {name} is shadowing a Python builtin
+ 'B023', # function-uses-loop-variable
+ 'B024', # `FormatWidgetMixin` is an abstract base class, but it has no abstract methods
+ 'D205', # blank-line-after-summary
+ 'D212', # multi-line-summary-first-line
+ 'RET505', # Unnecessary `else` after `return` statement
+ 'TRY003', # Avoid specifying long messages outside the exception class
+ 'RET507', # Unnecessary `elif` after `continue` statement
+ 'C405', # Unnecessary {obj_type} literal (rewrite as a set literal)
+ 'C406', # Unnecessary {obj_type} literal (rewrite as a dict literal)
+ 'C408', # Unnecessary {obj_type} call (rewrite as a literal)
+ 'SIM114', # Combine `if` branches using logical `or` operator
+ 'RET506', # Unnecessary `else` after `raise` statement
+ 'Q001', # Remove bad quotes
+ 'Q002', # Remove bad quotes
+ 'FA100', # Missing `from __future__ import annotations`, but uses `typing.Optional`
+ 'COM812', # Missing trailing comma in a list
+ 'ISC001', # String concatenation with implicit str conversion
+ 'SIM108', # Ternary operators are not always more readable
+ 'RUF100', # Unused noqa directives. Due to multiple Python versions, we need to keep them
+]
+
+select = [
+ 'A', # flake8-builtins
+ 'ASYNC', # flake8 async checker
+ 'B', # flake8-bugbear
+ 'C4', # flake8-comprehensions
+ 'C90', # mccabe
+ 'COM', # flake8-commas
+
+ ## Require docstrings for all public methods, would be good to enable at some point
+ # 'D', # pydocstyle
+
+ 'E', # pycodestyle error ('W' for warning)
+ 'F', # pyflakes
+ 'FA', # flake8-future-annotations
+ 'I', # isort
+ 'ICN', # flake8-import-conventions
+ 'INP', # flake8-no-pep420
+ 'ISC', # flake8-implicit-str-concat
+ 'N', # pep8-naming
+ 'NPY', # NumPy-specific rules
+ 'PERF', # perflint,
+ 'PIE', # flake8-pie
+ 'Q', # flake8-quotes
+
+ 'RET', # flake8-return
+ 'RUF', # Ruff-specific rules
+ 'SIM', # flake8-simplify
+ 'T20', # flake8-print
+ 'TD', # flake8-todos
+ 'TRY', # tryceratops
+ 'UP', # pyupgrade
+]
+
+[lint.per-file-ignores]
+'tests/*' = ['SIM115', 'SIM117', 'T201', 'B007']
+'docs/*' = ['INP001', 'RUF012']
+
+[lint.pydocstyle]
+convention = 'google'
+ignore-decorators = [
+ 'typing.overload',
+ 'typing.override',
+]
+
+[lint.isort]
+case-sensitive = true
+combine-as-imports = true
+force-wrap-aliases = true
+
+[lint.flake8-quotes]
+docstring-quotes = 'single'
+inline-quotes = 'single'
+multiline-quotes = 'single'
+
+[format]
+line-ending = 'lf'
+indent-style = 'space'
+quote-style = 'single'
+docstring-code-format = true
+skip-magic-trailing-comma = false
+exclude = [
+ '__init__.py',
+]
+
+[lint.pycodestyle]
+max-line-length = 79
+
+[lint.flake8-pytest-style]
+mark-parentheses = true
+
diff --git a/setup.py b/setup.py
index f0db90e..7778889 100644
--- a/setup.py
+++ b/setup.py
@@ -1,15 +1,16 @@
import os
import sys
import warnings
-from setuptools import setup, extension
+
+from setuptools import extension, setup
from setuptools.command.build_ext import build_ext
setup_kwargs = {}
def error(*lines):
- for line in lines:
- print(line, file=sys.stderr)
+ for _line in lines:
+ pass
try:
@@ -27,7 +28,7 @@ def error(*lines):
try:
- import numpy
+ import numpy as np
from Cython import Build
setup_kwargs['ext_modules'] = Build.cythonize(
@@ -35,7 +36,7 @@ def error(*lines):
extension.Extension(
'stl._speedups',
['stl/_speedups.pyx'],
- include_dirs=[numpy.get_include()],
+ include_dirs=[np.get_include()],
),
]
)
@@ -57,8 +58,8 @@ def error(*lines):
with open('README.rst') as fh:
long_description = fh.read()
else:
- long_description = (
- 'See http://pypi.python.org/pypi/%s/' % (about['__package_name__'])
+ long_description = 'See http://pypi.python.org/pypi/{}/'.format(
+ about['__package_name__']
)
install_requires = [
@@ -76,12 +77,12 @@ def run(self):
build_ext.run(self)
except Exception as e:
warnings.warn(
- """
+ f"""
Unable to build speedups module, defaulting to pure Python. Note
that the pure Python version is more than fast enough in most cases
- %r
- """
- % e
+ {e!r}
+ """,
+ stacklevel=2,
)
@@ -101,9 +102,11 @@ def run(self):
tests_require=tests_require,
entry_points={
'console_scripts': [
- 'stl = %s.main:main' % about['__import_name__'],
- 'stl2ascii = %s.main:to_ascii' % about['__import_name__'],
- 'stl2bin = %s.main:to_binary' % about['__import_name__'],
+ 'stl = {}.main:main'.format(about['__import_name__']),
+ 'stl2ascii = {}.main:to_ascii'.format(
+ about['__import_name__']
+ ),
+ 'stl2bin = {}.main:to_binary'.format(about['__import_name__']),
],
},
classifiers=[
diff --git a/stl/__init__.py b/stl/__init__.py
index ac98687..4ca0e60 100644
--- a/stl/__init__.py
+++ b/stl/__init__.py
@@ -1,12 +1,6 @@
-from .stl import BUFFER_SIZE
-from .stl import HEADER_SIZE
-from .stl import COUNT_SIZE
-from .stl import MAX_COUNT
-
-from .stl import Mode
-from .base import Dimension
-from .base import RemoveDuplicates
+from .base import Dimension, RemoveDuplicates
from .mesh import Mesh
+from .stl import BUFFER_SIZE, COUNT_SIZE, HEADER_SIZE, MAX_COUNT, Mode
__all__ = [
'BUFFER_SIZE',
diff --git a/stl/base.py b/stl/base.py
index 33b792d..b226f25 100644
--- a/stl/base.py
+++ b/stl/base.py
@@ -3,7 +3,7 @@
import logging
import math
-import numpy
+import numpy as np
try: # pragma: no cover
from collections import abc
@@ -76,6 +76,23 @@ def logged(class_):
return class_
+def _get_or_update(key):
+ def _get(self):
+ attr = f'_{key}'
+ if not hasattr(self, attr):
+ getattr(self, f'update_{key}')()
+ return getattr(self, attr)
+
+ return _get
+
+
+def _set(key):
+ def __set(self, value):
+ setattr(self, f'_{key}', value)
+
+ return __set
+
+
@logged
class BaseMesh(logger.Logged, abc.Mapping):
"""
@@ -101,28 +118,28 @@ class BaseMesh(logger.Logged, abc.Mapping):
:ivar numpy.array v1: Points in vector 1 (Nx3)
:ivar numpy.array v2: Points in vector 2 (Nx3)
- >>> data = numpy.zeros(10, dtype=BaseMesh.dtype)
+ >>> data = np.zeros(10, dtype=BaseMesh.dtype)
>>> mesh = BaseMesh(data, remove_empty_areas=False)
>>> # Increment vector 0 item 0
>>> mesh.v0[0] += 1
>>> mesh.v1[0] += 2
>>> # Check item 0 (contains v0, v1 and v2)
- >>> assert numpy.array_equal(
- ... mesh[0], numpy.array([1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0])
+ >>> assert np.array_equal(
+ ... mesh[0], np.array([1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0])
... )
- >>> assert numpy.array_equal(
+ >>> assert np.array_equal(
... mesh.vectors[0],
- ... numpy.array([[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [0.0, 0.0, 0.0]]),
+ ... np.array([[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [0.0, 0.0, 0.0]]),
... )
- >>> assert numpy.array_equal(mesh.v0[0], numpy.array([1.0, 1.0, 1.0]))
- >>> assert numpy.array_equal(
+ >>> assert np.array_equal(mesh.v0[0], np.array([1.0, 1.0, 1.0]))
+ >>> assert np.array_equal(
... mesh.points[0],
- ... numpy.array([1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0]),
+ ... np.array([1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0]),
... )
- >>> assert numpy.array_equal(
+ >>> assert np.array_equal(
... mesh.data[0],
- ... numpy.array(
+ ... np.array(
... (
... [0.0, 0.0, 0.0],
... [[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [0.0, 0.0, 0.0]],
@@ -131,11 +148,11 @@ class BaseMesh(logger.Logged, abc.Mapping):
... dtype=BaseMesh.dtype,
... ),
... )
- >>> assert numpy.array_equal(mesh.x[0], numpy.array([1.0, 2.0, 0.0]))
+ >>> assert np.array_equal(mesh.x[0], np.array([1.0, 2.0, 0.0]))
>>> mesh[0] = 3
- >>> assert numpy.array_equal(
- ... mesh[0], numpy.array([3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0])
+ >>> assert np.array_equal(
+ ... mesh[0], np.array([3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0])
... )
>>> len(mesh) == len(list(mesh))
@@ -172,11 +189,11 @@ class BaseMesh(logger.Logged, abc.Mapping):
#: - normals: :func:`numpy.float32`, `(3, )`
#: - vectors: :func:`numpy.float32`, `(3, 3)`
#: - attr: :func:`numpy.uint16`, `(1, )`
- dtype = numpy.dtype(
+ dtype = np.dtype(
[
- ('normals', numpy.float32, (3,)),
- ('vectors', numpy.float32, (3, 3)),
- ('attr', numpy.uint16, (1,)),
+ ('normals', np.float32, (3,)),
+ ('vectors', np.float32, (3, 3)),
+ ('attr', np.uint16, (1,)),
]
)
dtype = dtype.newbyteorder('<') # Even on big endian arches, use little e.
@@ -191,7 +208,7 @@ def __init__(
speedups=True,
**kwargs,
):
- super(BaseMesh, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.speedups = speedups
if remove_empty_areas:
data = self.remove_empty_areas(data)
@@ -292,26 +309,26 @@ def remove_duplicate_polygons(cls, data, value=RemoveDuplicates.SINGLE):
value = RemoveDuplicates.map(value)
polygons = data['vectors'].sum(axis=1)
# Get a sorted list of indices
- idx = numpy.lexsort(polygons.T)
+ idx = np.lexsort(polygons.T)
# Get the indices of all different indices
- diff = numpy.any(polygons[idx[1:]] != polygons[idx[:-1]], axis=1)
+ diff = np.any(polygons[idx[1:]] != polygons[idx[:-1]], axis=1)
if value is RemoveDuplicates.SINGLE:
# Only return the unique data, the True is so we always get at
# least the originals
- return data[numpy.sort(idx[numpy.concatenate(([True], diff))])]
+ return data[np.sort(idx[np.concatenate(([True], diff))])]
elif value is RemoveDuplicates.ALL:
# We need to return both items of the shifted diff
- diff_a = numpy.concatenate(([True], diff))
- diff_b = numpy.concatenate((diff, [True]))
- diff = numpy.concatenate((diff, [False]))
+ diff_a = np.concatenate(([True], diff))
+ diff_b = np.concatenate((diff, [True]))
+ diff = np.concatenate((diff, [False]))
# Combine both unique lists
- filtered_data = data[numpy.sort(idx[diff_a & diff_b])]
+ filtered_data = data[np.sort(idx[diff_a & diff_b])]
if len(filtered_data) <= len(data) / 2:
- return data[numpy.sort(idx[diff_a])]
+ return data[np.sort(idx[diff_a])]
else:
- return data[numpy.sort(idx[diff])]
+ return data[np.sort(idx[diff])]
else:
return data
@@ -321,13 +338,13 @@ def remove_empty_areas(cls, data):
v0 = vectors[:, 0]
v1 = vectors[:, 1]
v2 = vectors[:, 2]
- normals = numpy.cross(v1 - v0, v2 - v0)
+ normals = np.cross(v1 - v0, v2 - v0)
squared_areas = (normals**2).sum(axis=1)
return data[squared_areas > AREA_SIZE_THRESHOLD**2]
def update_normals(self, update_areas=True, update_centroids=True):
"""Update the normals, areas, and centroids for all points"""
- normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0)
+ normals = np.cross(self.v1 - self.v0, self.v2 - self.v0)
if update_areas:
self.update_areas(normals)
@@ -339,7 +356,7 @@ def update_normals(self, update_areas=True, update_centroids=True):
def get_unit_normals(self):
normals = self.normals.copy()
- normal = numpy.linalg.norm(normals, axis=1)
+ normal = np.linalg.norm(normals, axis=1)
non_zero = normal > 0
if non_zero.any():
normals[non_zero] /= normal[non_zero][:, None]
@@ -353,13 +370,13 @@ def update_max(self):
def update_areas(self, normals=None):
if normals is None:
- normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0)
+ normals = np.cross(self.v1 - self.v0, self.v2 - self.v0)
- areas = 0.5 * numpy.sqrt((normals**2).sum(axis=1))
+ areas = 0.5 * np.sqrt((normals**2).sum(axis=1))
self.areas = areas.reshape((areas.size, 1))
def update_centroids(self):
- self.centroids = numpy.mean([self.v0, self.v1, self.v2], axis=0)
+ self.centroids = np.mean([self.v0, self.v1, self.v2], axis=0)
def check(self, exact=False):
"""Check the mesh is valid or not
@@ -376,8 +393,7 @@ def is_closed(self, exact=False): # pragma: no cover
if exact:
reversed_triangles = (
- numpy.cross(self.v1 - self.v0, self.v2 - self.v0)
- * self.normals
+ np.cross(self.v1 - self.v0, self.v2 - self.v0) * self.normals
).sum(axis=1) < 0
directed_edges = {
tuple(edge.ravel() if not rev else edge[::-1, :].ravel())
@@ -407,11 +423,11 @@ def is_closed(self, exact=False): # pragma: no cover
- false negative: https://github.com/wolph/numpy-stl/pull/213
""".strip()
)
- normals = numpy.asarray(self.normals, dtype=numpy.float64)
+ normals = np.asarray(self.normals, dtype=np.float64)
allowed_max_errors = (
- numpy.abs(normals).sum(axis=0) * numpy.finfo(numpy.float32).eps
+ np.abs(normals).sum(axis=0) * np.finfo(np.float32).eps
)
- if (numpy.abs(normals.sum(axis=0)) <= allowed_max_errors).all():
+ if (np.abs(normals.sum(axis=0)) <= allowed_max_errors).all():
return True
self.warning(
@@ -459,18 +475,18 @@ def subexpression(x):
f1y, f2y, f3y, g0y, g1y, g2y = subexpression(self.y)
f1z, f2z, f3z, g0z, g1z, g2z = subexpression(self.z)
- intg = numpy.zeros((10))
+ intg = np.zeros(10)
intg[0] = sum(d0 * f1x)
intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z)
intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z)
intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x))
intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y))
intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z))
- intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120])
+ intg /= np.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120])
volume = intg[0]
cog = intg[1:4] / volume
cogsq = cog**2
- inertia = numpy.zeros((3, 3))
+ inertia = np.zeros((3, 3))
inertia[0, 0] = intg[5] + intg[6] - volume * (cogsq[1] + cogsq[2])
inertia[1, 1] = intg[4] + intg[6] - volume * (cogsq[2] + cogsq[0])
inertia[2, 2] = intg[4] + intg[5] - volume * (cogsq[0] + cogsq[1])
@@ -492,7 +508,7 @@ def update_units(self):
if non_zero_areas.any():
non_zero_areas.shape = non_zero_areas.shape[0]
- areas = numpy.hstack((2 * areas[non_zero_areas],) * DIMENSIONS)
+ areas = np.hstack((2 * areas[non_zero_areas],) * DIMENSIONS)
units[non_zero_areas] /= areas
self.units = units
@@ -511,14 +527,14 @@ def rotation_matrix(cls, axis, theta):
:param float theta: Rotation angle in radians, use `math.radians` to
convert degrees to radians if needed.
"""
- axis = numpy.asarray(axis)
+ axis = np.asarray(axis)
# No need to rotate if there is no actual rotation
if not axis.any():
- return numpy.identity(3)
+ return np.identity(3)
- theta = 0.5 * numpy.asarray(theta)
+ theta = 0.5 * np.asarray(theta)
- axis = axis / numpy.linalg.norm(axis)
+ axis = axis / np.linalg.norm(axis)
a = math.cos(theta)
b, c, d = -axis * math.sin(theta)
@@ -529,7 +545,7 @@ def rotation_matrix(cls, axis, theta):
ca, cb, cc, cd = powers[8:12]
da, db, dc, dd = powers[12:16]
- return numpy.array(
+ return np.array(
[
[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
[2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
@@ -570,17 +586,17 @@ def rotate_using_matrix(self, rotation_matrix, point=None):
https://github.com/WoLpH/numpy-stl/issues/166
"""
- identity = numpy.identity(rotation_matrix.shape[0])
+ identity = np.identity(rotation_matrix.shape[0])
# No need to rotate if there is no actual rotation
if not rotation_matrix.any() or (identity == rotation_matrix).all():
return
- if isinstance(point, (numpy.ndarray, list, tuple)) and len(point) == 3:
- point = numpy.asarray(point)
+ if isinstance(point, (np.ndarray, list, tuple)) and len(point) == 3:
+ point = np.asarray(point)
elif point is None:
- point = numpy.array([0, 0, 0])
+ point = np.array([0, 0, 0])
elif isinstance(point, (int, float)):
- point = numpy.asarray([point] * 3)
+ point = np.asarray([point] * 3)
else:
raise TypeError('Incorrect type for point', point)
@@ -624,28 +640,14 @@ def transform(self, matrix):
is_a_4x4_matrix = matrix.shape == (4, 4)
assert is_a_4x4_matrix, 'Transformation matrix must be of shape (4, 4)'
rotation = matrix[0:3, 0:3]
- unit_det_rotation = numpy.allclose(numpy.linalg.det(rotation), 1.0)
+ unit_det_rotation = np.allclose(np.linalg.det(rotation), 1.0)
assert unit_det_rotation, 'Rotation matrix has not a unit determinant'
for i in range(3):
- self.vectors[:, i] = numpy.dot(rotation, self.vectors[:, i].T).T
+ self.vectors[:, i] = np.dot(rotation, self.vectors[:, i].T).T
self.x += matrix[0, 3]
self.y += matrix[1, 3]
self.z += matrix[2, 3]
- def _get_or_update(key):
- def _get(self):
- if not hasattr(self, '_%s' % key):
- getattr(self, 'update_%s' % key)()
- return getattr(self, '_%s' % key)
-
- return _get
-
- def _set(key):
- def _set(self, value):
- setattr(self, '_%s' % key, value)
-
- return _set
-
min_ = property(
_get_or_update('min'), _set('min'), doc='Mesh minimum value'
)
@@ -670,8 +672,7 @@ def __len__(self):
return self.points.shape[0]
def __iter__(self):
- for point in self.points:
- yield point
+ yield from self.points
def __repr__(self):
return f''
@@ -704,19 +705,19 @@ def subexpression(x):
f1y, f2y, f3y, g0y, g1y, g2y = subexpression(self.y)
f1z, f2z, f3z, g0z, g1z, g2z = subexpression(self.z)
- intg = numpy.zeros((10))
+ intg = np.zeros(10)
intg[0] = sum(d0 * f1x)
intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z)
intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z)
intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x))
intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y))
intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z))
- intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120])
+ intg /= np.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120])
volume = intg[0]
cog = intg[1:4] / volume
cogsq = cog**2
vmass = volume * density
- inertia = numpy.zeros((3, 3))
+ inertia = np.zeros((3, 3))
inertia[0, 0] = (intg[5] + intg[6]) * density - vmass * (
cogsq[1] + cogsq[2]
diff --git a/stl/main.py b/stl/main.py
index a9e108d..73e9798 100644
--- a/stl/main.py
+++ b/stl/main.py
@@ -62,6 +62,7 @@ def _get_name(args):
continue
else:
return name
+ return None # pragma: no cover
def main():
diff --git a/stl/stl.py b/stl/stl.py
index 67e91e8..68d5b71 100644
--- a/stl/stl.py
+++ b/stl/stl.py
@@ -4,11 +4,14 @@
import os
import struct
import zipfile
-from xml.etree import ElementTree
+from xml.etree import ElementTree as ET
-import numpy
+import numpy as np
-from . import __about__ as metadata, base
+from . import (
+ __about__ as metadata,
+ base,
+)
from .utils import b
try:
@@ -56,7 +59,7 @@ def load(cls, fh, mode=AUTOMATIC, speedups=True):
"""
header = fh.read(HEADER_SIZE)
if not header:
- return
+ return None
if isinstance(header, str): # pragma: no branch
header = b(header)
@@ -66,7 +69,6 @@ def load(cls, fh, mode=AUTOMATIC, speedups=True):
try:
name, data = cls._load_ascii(fh, header, speedups=speedups)
except RuntimeError as exception:
- print('exception', exception)
(recoverable, e) = exception.args
# If we didn't read beyond the header the stream is still
# readable through the binary reader
@@ -120,21 +122,21 @@ def _load_binary(cls, fh, header, check_size=False):
'Expected %d vectors but ' 'header indicates %d'
) % (expected_count, count)
fh.seek(HEADER_SIZE + COUNT_SIZE)
- except IOError: # pragma: no cover
+ except OSError: # pragma: no cover
pass
name = header.strip()
# Read the rest of the binary data
try:
- return name, numpy.fromfile(fh, dtype=cls.dtype, count=count)
+ return name, np.fromfile(fh, dtype=cls.dtype, count=count)
except io.UnsupportedOperation:
- data = numpy.frombuffer(fh.read(), dtype=cls.dtype, count=count)
+ data = np.frombuffer(fh.read(), dtype=cls.dtype, count=count)
# Copy to make the buffer writable
return name, data.copy()
@classmethod
- def _ascii_reader(cls, fh, header):
+ def _ascii_reader(cls, fh, header): # noqa: C901
if b'\n' in header:
recoverable = [True]
else:
@@ -166,9 +168,7 @@ def get(prefix=''):
if prefix:
if line.startswith(prefix):
values = line.replace(prefix, b(''), 1).strip().split()
- elif line.startswith(b('endsolid')) or line.startswith(
- b('end solid')
- ):
+ elif line.startswith((b('endsolid'), b('end solid'))):
# go back to the beginning of new solid part
size_unprocessedlines = (
sum(len(line) + 1 for line in lines) - 1
@@ -181,14 +181,14 @@ def get(prefix=''):
else:
raise RuntimeError(
recoverable[0],
- '%r should start with %r' % (line, prefix),
+ f'{line!r} should start with {prefix!r}',
)
if len(values) == 3:
return [float(v) for v in values]
else: # pragma: no cover
raise RuntimeError(
- recoverable[0], 'Incorrect value %r' % line
+ recoverable[0], f'Incorrect value {line!r}'
)
else:
return b(raw_line)
@@ -219,8 +219,8 @@ def get(prefix=''):
assert get().lower() == b('endfacet')
attrs = 0
yield (normals, (v0, v1, v2), attrs)
- except AssertionError as e: # pragma: no cover
- raise RuntimeError(recoverable[0], e)
+ except AssertionError as e: # pragma: no cover # noqa: PERF203
+ raise RuntimeError(recoverable[0], e) from e
except StopIteration:
return
@@ -238,9 +238,9 @@ def _load_ascii(cls, fh, header, speedups=True):
else:
iterator = cls._ascii_reader(fh, header)
name = next(iterator)
- return name, numpy.fromiter(iterator, dtype=cls.dtype)
+ return name, np.fromiter(iterator, dtype=cls.dtype)
- def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True):
+ def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True): # noqa: C901
"""Save the STL to a (binary) file
If mode is :py:data:`AUTOMATIC` an :py:data:`ASCII` file will be
@@ -263,7 +263,7 @@ def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True):
write = self._write_ascii
else:
write = self._write_binary
- except IOError:
+ except OSError:
# If TTY checking fails then it's an io.BytesIO() (or one
# of its siblings from io). Assume binary.
write = self._write_binary
@@ -274,7 +274,7 @@ def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True):
elif mode is ASCII:
write = self._write_ascii
else:
- raise ValueError('Mode %r is invalid' % mode)
+ raise ValueError(f'Mode {mode!r} is invalid')
if isinstance(fh, io.TextIOBase):
# Provide a more helpful error if the user mistakenly
@@ -294,7 +294,7 @@ def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True):
else:
with open(filename, 'wb') as fh:
write(fh, name)
- except IOError: # pragma: no cover
+ except OSError: # pragma: no cover
pass
def _write_ascii(self, fh, name):
@@ -309,23 +309,39 @@ def _write_ascii(self, fh, name):
else:
def p(s, file):
- file.write(b('%s\n' % s))
+ file.write(b(f'{s}\n'))
- p('solid %s' % name, file=fh)
+ p(f'solid {name}', file=fh)
for row in self.data:
- # Explicitly convert each component to standard float for normals and vertices
+ # Explicitly convert each component to standard float for
+ # normals and vertices to be compatible with numpy 2.x
normals = tuple(float(n) for n in row['normals'])
vectors = row['vectors']
- p('facet normal %f %f %f' % normals, file=fh)
+ p('facet normal {:f} {:f} {:f}'.format(*normals), file=fh)
p(' outer loop', file=fh)
- p(' vertex %f %f %f' % tuple(float(v) for v in vectors[0]), file=fh)
- p(' vertex %f %f %f' % tuple(float(v) for v in vectors[1]), file=fh)
- p(' vertex %f %f %f' % tuple(float(v) for v in vectors[2]), file=fh)
+ p(
+ ' vertex {:f} {:f} {:f}'.format(
+ *tuple(float(v) for v in vectors[0])
+ ),
+ file=fh,
+ )
+ p(
+ ' vertex {:f} {:f} {:f}'.format(
+ *tuple(float(v) for v in vectors[1])
+ ),
+ file=fh,
+ )
+ p(
+ ' vertex {:f} {:f} {:f}'.format(
+ *tuple(float(v) for v in vectors[2])
+ ),
+ file=fh,
+ )
p(' endloop', file=fh)
p('endfacet', file=fh)
- p('endsolid %s' % name, file=fh)
+ p(f'endsolid {name}', file=fh)
def get_header(self, name):
# Format the header
@@ -418,7 +434,7 @@ def from_multi_file(
if fh:
close = False
else:
- fh = open(filename, 'rb')
+ fh = open(filename, 'rb') # noqa: SIM115
close = True
try:
@@ -457,19 +473,18 @@ def from_files(
:param file fh: The file handle to open
:param dict kwargs: The same as for :py:class:`stl.mesh.Mesh`
"""
- meshes = []
- for filename in filenames:
- meshes.append(
- cls.from_file(
- filename,
- calculate_normals=calculate_normals,
- mode=mode,
- speedups=speedups,
- **kwargs,
- )
+ meshes = [
+ cls.from_file(
+ filename,
+ calculate_normals=calculate_normals,
+ mode=mode,
+ speedups=speedups,
+ **kwargs,
)
+ for filename in filenames
+ ]
- data = numpy.concatenate([mesh.data for mesh in meshes])
+ data = np.concatenate([mesh.data for mesh in meshes])
return cls(data, calculate_normals=calculate_normals, **kwargs)
@classmethod
@@ -477,16 +492,16 @@ def from_3mf_file(cls, filename, calculate_normals=True, **kwargs):
with zipfile.ZipFile(filename) as zip:
with zip.open('_rels/.rels') as rels_fh:
model = None
- root = ElementTree.parse(rels_fh).getroot()
+ root = ET.parse(rels_fh).getroot()
for child in root: # pragma: no branch
type_ = child.attrib.get('Type', '')
if type_.endswith('3dmodel'): # pragma: no branch
model = child.attrib.get('Target', '')
break
- assert model, 'No 3D model found in %s' % filename
+ assert model, f'No 3D model found in {filename}'
with zip.open(model.lstrip('/')) as fh:
- root = ElementTree.parse(fh).getroot()
+ root = ET.parse(fh).getroot()
elements = root.findall('./{*}resources/{*}object/{*}mesh')
for mesh_element in elements: # pragma: no branch
@@ -519,8 +534,8 @@ def from_3mf_file(cls, filename, calculate_normals=True, **kwargs):
]
)
- mesh = cls(numpy.zeros(len(triangles), dtype=cls.dtype))
- mesh.vectors[:] = numpy.array(triangles)
+ mesh = cls(np.zeros(len(triangles), dtype=cls.dtype))
+ mesh.vectors[:] = np.array(triangles)
yield mesh
diff --git a/tests/stl_corruption.py b/tests/stl_corruption.py
index b155ad6..cb27cd4 100644
--- a/tests/stl_corruption.py
+++ b/tests/stl_corruption.py
@@ -1,8 +1,8 @@
-from __future__ import print_function
+import struct
import sys
-import numpy
+
+import numpy as np
import pytest
-import struct
from stl import mesh
@@ -140,9 +140,9 @@ def test_corrupt_binary_file(tmpdir, speedups):
def test_duplicate_polygons():
- data = numpy.zeros(3, dtype=mesh.Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 0, 0], [1, 0, 0], [0, 1, 1.0]])
- data['vectors'][0] = numpy.array([[0, 0, 0], [2, 0, 0], [0, 2, 1.0]])
- data['vectors'][0] = numpy.array([[0, 0, 0], [3, 0, 0], [0, 3, 1.0]])
+ data = np.zeros(3, dtype=mesh.Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 1.0]])
+ data['vectors'][0] = np.array([[0, 0, 0], [2, 0, 0], [0, 2, 1.0]])
+ data['vectors'][0] = np.array([[0, 0, 0], [3, 0, 0], [0, 3, 1.0]])
assert not mesh.Mesh(data, remove_empty_areas=False).check()
diff --git a/tests/test_ascii.py b/tests/test_ascii.py
index af1015a..dfad269 100644
--- a/tests/test_ascii.py
+++ b/tests/test_ascii.py
@@ -1,16 +1,16 @@
-import os
-import sys
+import io
import locale
-import pytest
+import os
import pathlib
-import warnings
import subprocess
-import io
-import numpy
+import sys
+import warnings
-from stl.utils import b
-from stl import mesh, Mode
+import numpy as np
+import pytest
+from stl import Mode, mesh
+from stl.utils import b
FILES_PATH = pathlib.Path(__file__).parent / 'stl_tests'
@@ -23,8 +23,8 @@ def test_ascii_file(speedups):
def test_chinese_name(tmpdir, speedups):
name = 'Test Chinese name 月球'
_stl_file = (
- """
- solid %s
+ f"""
+ solid {name}
facet normal -0.014565 0.073223 -0.002897
outer loop
vertex 0.399344 0.461940 1.044090
@@ -34,7 +34,6 @@ def test_chinese_name(tmpdir, speedups):
endfacet
endsolid
"""
- % name
).lstrip()
tmp_file = tmpdir.join('tmp.stl')
@@ -54,8 +53,8 @@ def test_long_name(tmpdir, speedups):
name = 'Just Some Very Long Name which will not fit within the standard'
name += name
_stl_file = (
- """
- solid %s
+ f"""
+ solid {name}
facet normal -0.014565 0.073223 -0.002897
outer loop
vertex 0.399344 0.461940 1.044090
@@ -65,7 +64,6 @@ def test_long_name(tmpdir, speedups):
endfacet
endsolid
"""
- % name
).lstrip()
tmp_file = tmpdir.join('tmp.stl')
@@ -86,8 +84,8 @@ def test_scientific_notation(tmpdir, speedups):
name = 'just some very long name which will not fit within the standard'
name += name
_stl_file = (
- """
- solid %s
+ f"""
+ solid {name}
facet normal 1.014565e-10 7.3223e-5 -10
outer loop
vertex 0.399344 0.461940 1.044090e-5
@@ -97,7 +95,6 @@ def test_scientific_notation(tmpdir, speedups):
endfacet
endsolid
"""
- % name
).lstrip()
tmp_file = tmpdir.join('tmp.stl')
@@ -134,7 +131,7 @@ def test_use_with_qt_with_custom_locale_decimal_delimeter(speedups):
pytest.skip('Only makes sense with speedups')
venv = os.environ.get('VIRTUAL_ENV', '')
- if (3, 6) == sys.version_info[:2] and venv.startswith('/home/travis/'):
+ if sys.version_info[:2] == (3, 6) and venv.startswith('/home/travis/'):
pytest.skip('PySide2/PyQt5 tests are broken on Travis Python 3.6')
try:
@@ -146,6 +143,7 @@ def test_use_with_qt_with_custom_locale_decimal_delimeter(speedups):
warnings.warn(
'Unable to import PySide2/PyQt5, skipping locale tests',
ImportWarning,
+ stacklevel=1,
)
pytest.skip('PySide2/PyQt5 missing')
assert QtWidgets
@@ -161,7 +159,7 @@ def test_use_with_qt_with_custom_locale_decimal_delimeter(speedups):
prefix = ('xvfb-run', '-a')
p = subprocess.Popen(
- prefix + (sys.executable, script_path),
+ (*prefix, sys.executable, script_path),
env=env,
universal_newlines=True,
stdout=subprocess.PIPE,
@@ -181,8 +179,8 @@ def test_use_with_qt_with_custom_locale_decimal_delimeter(speedups):
def test_ascii_io():
# Create a vanilla mesh.
- mesh_ = mesh.Mesh(numpy.empty(3, mesh.Mesh.dtype))
- mesh_.vectors = numpy.arange(27).reshape((3, 3, 3))
+ mesh_ = mesh.Mesh(np.empty(3, mesh.Mesh.dtype))
+ mesh_.vectors = np.arange(27).reshape((3, 3, 3))
# Check that unhelpful 'expected str but got bytes' error is caught and
# replaced.
@@ -207,4 +205,4 @@ def test_ascii_io():
# Read the mesh back in.
read = mesh.Mesh.from_file('anonymous.stl', fh=io.BytesIO(fh.getvalue()))
# Check what comes out is the same as what went in.
- assert numpy.allclose(mesh_.vectors, read.vectors)
+ assert np.allclose(mesh_.vectors, read.vectors)
diff --git a/tests/test_binary.py b/tests/test_binary.py
index 63f6449..43b5170 100644
--- a/tests/test_binary.py
+++ b/tests/test_binary.py
@@ -1,9 +1,10 @@
import io
-import numpy
-import pytest
import pathlib
-from stl import mesh, Mode
+import numpy as np
+import pytest
+
+from stl import Mode, mesh
TESTS_PATH = pathlib.Path(__file__).parent
@@ -48,7 +49,7 @@ def test_write_bytes_io(binary_file, mode):
assert fh.getvalue()[84:] == mesh_.data.tobytes()
read = mesh.Mesh.from_file('nameless', fh=io.BytesIO(fh.getvalue()))
- assert numpy.allclose(read.vectors, mesh_.vectors)
+ assert np.allclose(read.vectors, mesh_.vectors)
def test_binary_file():
diff --git a/tests/test_commandline.py b/tests/test_commandline.py
index 42a2f97..3d7fcc9 100644
--- a/tests/test_commandline.py
+++ b/tests/test_commandline.py
@@ -1,3 +1,4 @@
+import contextlib
import sys
from stl import main
@@ -12,13 +13,13 @@ def test_main(ascii_file, binary_file, tmpdir, speedups):
args_pre.append('-s')
try:
- sys.argv[:] = args_pre + [ascii_file] + args_post
+ sys.argv[:] = [*args_pre, ascii_file, *args_post]
main.main()
- sys.argv[:] = args_pre + ['-r', ascii_file] + args_post
+ sys.argv[:] = [*args_pre, '-r', ascii_file, *args_post]
main.main()
- sys.argv[:] = args_pre + ['-a', binary_file] + args_post
+ sys.argv[:] = [*args_pre, '-a', binary_file, *args_post]
main.main()
- sys.argv[:] = args_pre + ['-b', ascii_file] + args_post
+ sys.argv[:] = [*args_pre, '-b', ascii_file, *args_post]
main.main()
finally:
sys.argv[:] = original_argv
@@ -45,10 +46,8 @@ def test_ascii(binary_file, tmpdir, speedups):
binary_file,
str(tmpdir.join('ascii.stl')),
]
- try:
+ with contextlib.suppress(SystemExit):
main.to_ascii()
- except SystemExit:
- pass
finally:
sys.argv[:] = original_argv
@@ -62,9 +61,7 @@ def test_binary(ascii_file, tmpdir, speedups):
ascii_file,
str(tmpdir.join('binary.stl')),
]
- try:
+ with contextlib.suppress(SystemExit):
main.to_binary()
- except SystemExit:
- pass
finally:
sys.argv[:] = original_argv
diff --git a/tests/test_convert.py b/tests/test_convert.py
index 38390bd..de14643 100644
--- a/tests/test_convert.py
+++ b/tests/test_convert.py
@@ -1,6 +1,7 @@
+import tempfile
+
import py.path
import pytest
-import tempfile
from stl import stl
diff --git a/tests/test_line_endings.py b/tests/test_line_endings.py
index 8d3c4ec..020bc49 100644
--- a/tests/test_line_endings.py
+++ b/tests/test_line_endings.py
@@ -1,12 +1,13 @@
import pathlib
+
import pytest
-from stl import mesh
+from stl import mesh
FILES_PATH = pathlib.Path(__file__).parent / 'stl_tests'
@pytest.mark.parametrize('line_ending', ['dos', 'unix'])
def test_line_endings(line_ending, speedups):
- filename = FILES_PATH / ('%s.stl' % line_ending)
+ filename = FILES_PATH / (f'{line_ending}.stl')
mesh.Mesh.from_file(filename, speedups=speedups)
diff --git a/tests/test_mesh.py b/tests/test_mesh.py
index 94fd9a2..d7132ea 100644
--- a/tests/test_mesh.py
+++ b/tests/test_mesh.py
@@ -1,70 +1,65 @@
-import numpy
+import numpy as np
+from stl.base import BaseMesh, RemoveDuplicates
from stl.mesh import Mesh
-from stl.base import BaseMesh
-from stl.base import RemoveDuplicates
from . import utils
def test_units_1d():
- data = numpy.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 0, 0], [1, 0, 0], [2, 0, 0]])
+ data = np.zeros(1, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [2, 0, 0]])
mesh = Mesh(data, remove_empty_areas=False)
mesh.update_units()
assert mesh.areas == 0
- assert numpy.allclose(mesh.centroids, [[1, 0, 0]])
+ assert np.allclose(mesh.centroids, [[1, 0, 0]])
utils.array_equals(mesh.normals, [0, 0, 0])
utils.array_equals(mesh.units, [0, 0, 0])
utils.array_equals(mesh.get_unit_normals(), [0, 0, 0])
def test_units_2d():
- data = numpy.zeros(2, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
- data['vectors'][1] = numpy.array([[1, 0, 0], [0, 1, 0], [1, 1, 0]])
+ data = np.zeros(2, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
+ data['vectors'][1] = np.array([[1, 0, 0], [0, 1, 0], [1, 1, 0]])
mesh = Mesh(data, remove_empty_areas=False)
mesh.update_units()
- assert numpy.allclose(mesh.areas, [0.5, 0.5])
- assert numpy.allclose(
- mesh.centroids, [[1 / 3, 1 / 3, 0], [2 / 3, 2 / 3, 0]]
- )
- assert numpy.allclose(mesh.normals, [[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]])
- assert numpy.allclose(mesh.units, [[0, 0, 1], [0, 0, -1]])
- assert numpy.allclose(
+ assert np.allclose(mesh.areas, [0.5, 0.5])
+ assert np.allclose(mesh.centroids, [[1 / 3, 1 / 3, 0], [2 / 3, 2 / 3, 0]])
+ assert np.allclose(mesh.normals, [[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]])
+ assert np.allclose(mesh.units, [[0, 0, 1], [0, 0, -1]])
+ assert np.allclose(
mesh.get_unit_normals(), [[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]]
)
def test_units_3d():
- data = numpy.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 0, 0], [1, 0, 0], [0, 1, 1.0]])
+ data = np.zeros(1, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 1.0]])
mesh = Mesh(data, remove_empty_areas=False)
mesh.update_units()
assert (mesh.areas - 2**0.5) < 0.0001
- assert numpy.allclose(mesh.centroids, [1 / 3, 1 / 3, 1 / 3])
- assert numpy.allclose(mesh.normals, [0.0, -1.0, 1.0])
- assert numpy.allclose(mesh.units[0], [0.0, -0.70710677, 0.70710677])
- assert numpy.allclose(numpy.linalg.norm(mesh.units, axis=-1), 1)
- assert numpy.allclose(
- mesh.get_unit_normals(), [0.0, -0.70710677, 0.70710677]
- )
+ assert np.allclose(mesh.centroids, [1 / 3, 1 / 3, 1 / 3])
+ assert np.allclose(mesh.normals, [0.0, -1.0, 1.0])
+ assert np.allclose(mesh.units[0], [0.0, -0.70710677, 0.70710677])
+ assert np.allclose(np.linalg.norm(mesh.units, axis=-1), 1)
+ assert np.allclose(mesh.get_unit_normals(), [0.0, -0.70710677, 0.70710677])
def test_duplicate_polygons():
- data = numpy.zeros(6, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][1] = numpy.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][2] = numpy.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][3] = numpy.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][4] = numpy.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][5] = numpy.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data = np.zeros(6, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][1] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][2] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][3] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][4] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][5] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
mesh = Mesh(data)
assert mesh.data.size == 6
@@ -87,37 +82,37 @@ def test_duplicate_polygons():
mesh = Mesh(data, remove_duplicate_polygons=True)
assert mesh.data.size == 3
- assert numpy.allclose(
- mesh.vectors[0], numpy.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
+ assert np.allclose(
+ mesh.vectors[0], np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
)
- assert numpy.allclose(
- mesh.vectors[1], numpy.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
+ assert np.allclose(
+ mesh.vectors[1], np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
)
- assert numpy.allclose(
- mesh.vectors[2], numpy.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+ assert np.allclose(
+ mesh.vectors[2], np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
)
mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.ALL)
assert mesh.data.size == 3
- assert numpy.allclose(
- mesh.vectors[0], numpy.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
+ assert np.allclose(
+ mesh.vectors[0], np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
)
- assert numpy.allclose(
- mesh.vectors[1], numpy.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
+ assert np.allclose(
+ mesh.vectors[1], np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
)
- assert numpy.allclose(
- mesh.vectors[2], numpy.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+ assert np.allclose(
+ mesh.vectors[2], np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
)
def test_remove_all_duplicate_polygons():
- data = numpy.zeros(5, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][1] = numpy.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][2] = numpy.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][3] = numpy.array([[3, 0, 0], [0, 0, 0], [0, 0, 0]])
- data['vectors'][4] = numpy.array([[3, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data = np.zeros(5, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][1] = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][2] = np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][3] = np.array([[3, 0, 0], [0, 0, 0], [0, 0, 0]])
+ data['vectors'][4] = np.array([[3, 0, 0], [0, 0, 0], [0, 0, 0]])
mesh = Mesh(data, remove_duplicate_polygons=False)
assert mesh.data.size == 5
@@ -127,21 +122,21 @@ def test_remove_all_duplicate_polygons():
assert mesh.data.size == 3
assert (
- mesh.vectors[0] == numpy.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
+ mesh.vectors[0] == np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
).all()
assert (
- mesh.vectors[1] == numpy.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
+ mesh.vectors[1] == np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]])
).all()
assert (
- mesh.vectors[2] == numpy.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
+ mesh.vectors[2] == np.array([[2, 0, 0], [0, 0, 0], [0, 0, 0]])
).all()
def test_empty_areas():
- data = numpy.zeros(3, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
- data['vectors'][1] = numpy.array([[1, 0, 0], [0, 1, 0], [1, 0, 0]])
- data['vectors'][2] = numpy.array([[1, 0, 0], [0, 1, 0], [1, 0, 0]])
+ data = np.zeros(3, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
+ data['vectors'][1] = np.array([[1, 0, 0], [0, 1, 0], [1, 0, 0]])
+ data['vectors'][2] = np.array([[1, 0, 0], [0, 1, 0], [1, 0, 0]])
mesh = Mesh(data, calculate_normals=False, remove_empty_areas=False)
assert mesh.data.size == 3
@@ -149,23 +144,23 @@ def test_empty_areas():
# Test the normals recalculation which also calculates the areas by default
mesh.areas[1] = 1
mesh.areas[2] = 2
- assert numpy.allclose(mesh.areas, [[0.5], [1.0], [2.0]])
+ assert np.allclose(mesh.areas, [[0.5], [1.0], [2.0]])
mesh.centroids[1] = [1, 2, 3]
mesh.centroids[2] = [4, 5, 6]
- assert numpy.allclose(
+ assert np.allclose(
mesh.centroids, [[1 / 3, 1 / 3, 0], [1, 2, 3], [4, 5, 6]]
)
mesh.update_normals(update_areas=False, update_centroids=False)
- assert numpy.allclose(mesh.areas, [[0.5], [1.0], [2.0]])
- assert numpy.allclose(
+ assert np.allclose(mesh.areas, [[0.5], [1.0], [2.0]])
+ assert np.allclose(
mesh.centroids, [[1 / 3, 1 / 3, 0], [1, 2, 3], [4, 5, 6]]
)
mesh.update_normals(update_areas=True, update_centroids=True)
- assert numpy.allclose(mesh.areas, [[0.5], [0.0], [0.0]])
- assert numpy.allclose(
+ assert np.allclose(mesh.areas, [[0.5], [0.0], [0.0]])
+ assert np.allclose(
mesh.centroids,
[[1 / 3, 1 / 3, 0], [2 / 3, 1 / 3, 0], [2 / 3, 1 / 3, 0]],
)
@@ -175,7 +170,7 @@ def test_empty_areas():
def test_base_mesh():
- data = numpy.zeros(10, dtype=BaseMesh.dtype)
+ data = np.zeros(10, dtype=BaseMesh.dtype)
mesh = BaseMesh(data, remove_empty_areas=False)
# Increment vector 0 item 0
mesh.v0[0] += 1
@@ -184,35 +179,31 @@ def test_base_mesh():
# Check item 0 (contains v0, v1 and v2)
assert (
mesh[0]
- == numpy.array(
- [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0], dtype=numpy.float32
+ == np.array(
+ [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0], dtype=np.float32
)
).all()
assert (
mesh.vectors[0]
- == numpy.array(
+ == np.array(
[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [0.0, 0.0, 0.0]],
- dtype=numpy.float32,
+ dtype=np.float32,
)
).all()
- assert (
- mesh.v0[0] == numpy.array([1.0, 1.0, 1.0], dtype=numpy.float32)
- ).all()
+ assert (mesh.v0[0] == np.array([1.0, 1.0, 1.0], dtype=np.float32)).all()
assert (
mesh.points[0]
- == numpy.array(
- [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0], dtype=numpy.float32
+ == np.array(
+ [1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0], dtype=np.float32
)
).all()
- assert (
- mesh.x[0] == numpy.array([1.0, 2.0, 0.0], dtype=numpy.float32)
- ).all()
+ assert (mesh.x[0] == np.array([1.0, 2.0, 0.0], dtype=np.float32)).all()
mesh[0] = 3
assert (
mesh[0]
- == numpy.array(
- [3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0], dtype=numpy.float32
+ == np.array(
+ [3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0], dtype=np.float32
)
).all()
diff --git a/tests/test_meshProperties.py b/tests/test_mesh_properties.py
similarity index 97%
rename from tests/test_meshProperties.py
rename to tests/test_mesh_properties.py
index e5e9ba7..b0ff31f 100644
--- a/tests/test_meshProperties.py
+++ b/tests/test_mesh_properties.py
@@ -1,14 +1,13 @@
-import numpy
+import numpy as np
import pytest
from stl import stl
-
tolerance = 1e-5
def close(a, b):
- return numpy.allclose(a, b, atol=tolerance)
+ return np.allclose(a, b, atol=tolerance)
def test_mass_properties_for_half_donut(binary_ascii_path, speedups):
@@ -98,7 +97,7 @@ def test_mass_properties_for_half_donut_with_density(
assert close([volume], [2.343149026234945])
assert close(cog, [1.500001, 0.209472, 1.500001])
print('inertia')
- numpy.set_printoptions(suppress=True)
+ np.set_printoptions(suppress=True)
print(inertia)
assert close(
inertia,
diff --git a/tests/test_rotate.py b/tests/test_rotate.py
index 56ca3a7..53b5946 100644
--- a/tests/test_rotate.py
+++ b/tests/test_rotate.py
@@ -1,5 +1,6 @@
import math
-import numpy
+
+import numpy as np
import pytest
from stl.mesh import Mesh
@@ -9,17 +10,17 @@
def test_rotation():
# Create 6 faces of a cube
- data = numpy.zeros(6, dtype=Mesh.dtype)
+ data = np.zeros(6, dtype=Mesh.dtype)
# Top of the cube
- data['vectors'][0] = numpy.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
- data['vectors'][1] = numpy.array([[1, 0, 1], [0, 1, 1], [1, 1, 1]])
+ data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
+ data['vectors'][1] = np.array([[1, 0, 1], [0, 1, 1], [1, 1, 1]])
# Right face
- data['vectors'][2] = numpy.array([[1, 0, 0], [1, 0, 1], [1, 1, 0]])
- data['vectors'][3] = numpy.array([[1, 1, 1], [1, 0, 1], [1, 1, 0]])
+ data['vectors'][2] = np.array([[1, 0, 0], [1, 0, 1], [1, 1, 0]])
+ data['vectors'][3] = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 0]])
# Left face
- data['vectors'][4] = numpy.array([[0, 0, 0], [1, 0, 0], [1, 0, 1]])
- data['vectors'][5] = numpy.array([[0, 0, 0], [0, 0, 1], [1, 0, 1]])
+ data['vectors'][4] = np.array([[0, 0, 0], [1, 0, 0], [1, 0, 1]])
+ data['vectors'][5] = np.array([[0, 0, 0], [0, 0, 1], [1, 0, 1]])
mesh = Mesh(data, remove_empty_areas=False)
@@ -39,9 +40,9 @@ def test_rotation():
# We use a slightly higher absolute tolerance here, for ppc64le
# https://github.com/WoLpH/numpy-stl/issues/78
- assert numpy.allclose(
+ assert np.allclose(
mesh.vectors,
- numpy.array(
+ np.array(
[
[[1, 0, 0], [0, 1, 0], [0, 0, 0]],
[[0, 1, 0], [1, 0, 0], [1, 1, 0]],
@@ -57,27 +58,27 @@ def test_rotation():
def test_rotation_over_point():
# Create a single face
- data = numpy.zeros(1, dtype=Mesh.dtype)
+ data = np.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+ data['vectors'][0] = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
mesh = Mesh(data, remove_empty_areas=False)
mesh.rotate([1, 0, 0], math.radians(180), point=[1, 2, 3])
utils.array_equals(
mesh.vectors,
- numpy.array([[[1.0, 4.0, 6.0], [0.0, 3.0, 6.0], [0.0, 4.0, 5.0]]]),
+ np.array([[[1.0, 4.0, 6.0], [0.0, 3.0, 6.0], [0.0, 4.0, 5.0]]]),
)
mesh.rotate([1, 0, 0], math.radians(-180), point=[1, 2, 3])
utils.array_equals(
- mesh.vectors, numpy.array([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]])
+ mesh.vectors, np.array([[[1, 0, 0], [0, 1, 0], [0, 0, 1]]])
)
mesh.rotate([1, 0, 0], math.radians(180), point=0.0)
utils.array_equals(
mesh.vectors,
- numpy.array([[[1.0, 0.0, -0.0], [0.0, -1.0, -0.0], [0.0, 0.0, -1.0]]]),
+ np.array([[[1.0, 0.0, -0.0], [0.0, -1.0, -0.0], [0.0, 0.0, -1.0]]]),
)
with pytest.raises(TypeError):
@@ -86,111 +87,111 @@ def test_rotation_over_point():
def test_double_rotation():
# Create a single face
- data = numpy.zeros(1, dtype=Mesh.dtype)
+ data = np.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
+ data['vectors'][0] = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
mesh = Mesh(data, remove_empty_areas=False)
rotation_matrix = mesh.rotation_matrix([1, 0, 0], math.radians(180))
- combined_rotation_matrix = numpy.dot(rotation_matrix, rotation_matrix)
+ combined_rotation_matrix = np.dot(rotation_matrix, rotation_matrix)
mesh.rotate_using_matrix(combined_rotation_matrix)
utils.array_equals(
mesh.vectors,
- numpy.array([[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]]),
+ np.array([[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]]),
)
def test_no_rotation():
# Create a single face
- data = numpy.zeros(1, dtype=Mesh.dtype)
+ data = np.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
+ data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
mesh = Mesh(data, remove_empty_areas=False)
# Rotate by 0 degrees
mesh.rotate([0.5, 0.0, 0.0], math.radians(0))
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
)
# Use a zero rotation matrix
mesh.rotate([0.0, 0.0, 0.0], math.radians(90))
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
)
def test_no_translation():
# Create a single face
- data = numpy.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
+ data = np.zeros(1, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
mesh = Mesh(data, remove_empty_areas=False)
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
)
# Translate mesh with a zero vector
mesh.translate([0.0, 0.0, 0.0])
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
)
def test_translation():
# Create a single face
- data = numpy.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
+ data = np.zeros(1, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
mesh = Mesh(data, remove_empty_areas=False)
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
)
# Translate mesh with vector [1, 2, 3]
mesh.translate([1.0, 2.0, 3.0])
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[1, 3, 4], [2, 2, 4], [1, 2, 4]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[1, 3, 4], [2, 2, 4], [1, 2, 4]]])
)
def test_no_transformation():
# Create a single face
- data = numpy.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
+ data = np.zeros(1, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
mesh = Mesh(data, remove_empty_areas=False)
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
)
# Transform mesh with identity matrix
- mesh.transform(numpy.eye(4))
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
+ mesh.transform(np.eye(4))
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
)
- assert numpy.allclose(mesh.areas, 0.5)
+ assert np.allclose(mesh.areas, 0.5)
def test_transformation():
# Create a single face
- data = numpy.zeros(1, dtype=Mesh.dtype)
- data['vectors'][0] = numpy.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
+ data = np.zeros(1, dtype=Mesh.dtype)
+ data['vectors'][0] = np.array([[0, 1, 1], [1, 0, 1], [0, 0, 1]])
mesh = Mesh(data, remove_empty_areas=False)
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]])
)
# Transform mesh with identity matrix
- tr = numpy.zeros((4, 4))
- tr[0:3, 0:3] = Mesh.rotation_matrix([0, 0, 1], 0.5 * numpy.pi)
+ tr = np.zeros((4, 4))
+ tr[0:3, 0:3] = Mesh.rotation_matrix([0, 0, 1], 0.5 * np.pi)
tr[0:3, 3] = [1, 2, 3]
mesh.transform(tr)
- assert numpy.allclose(
- mesh.vectors, numpy.array([[[0, 2, 4], [1, 3, 4], [1, 2, 4]]])
+ assert np.allclose(
+ mesh.vectors, np.array([[[0, 2, 4], [1, 3, 4], [1, 2, 4]]])
)
- assert numpy.allclose(mesh.areas, 0.5)
+ assert np.allclose(mesh.areas, 0.5)
diff --git a/tests/tmp/test_args_False_0/binary.stl b/tests/tmp/test_args_False_0/binary.stl
new file mode 100644
index 0000000..e69de29
diff --git a/tests/tmp/test_args_False_current b/tests/tmp/test_args_False_current
new file mode 120000
index 0000000..eedf3da
--- /dev/null
+++ b/tests/tmp/test_args_False_current
@@ -0,0 +1 @@
+/Volumes/workspace/numpy-stl/tests/tmp/test_args_False_0
\ No newline at end of file
diff --git a/tests/tmp/test_args_True_0/binary.stl b/tests/tmp/test_args_True_0/binary.stl
new file mode 100644
index 0000000..e69de29
diff --git a/tests/tmp/test_args_True_current b/tests/tmp/test_args_True_current
new file mode 120000
index 0000000..ecb22d9
--- /dev/null
+++ b/tests/tmp/test_args_True_current
@@ -0,0 +1 @@
+/Volumes/workspace/numpy-stl/tests/tmp/test_args_True_0
\ No newline at end of file
diff --git a/tests/utils.py b/tests/utils.py
index 1f7b919..4f62d13 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,11 +1,11 @@
-import numpy
+import numpy as np
def to_array(array, round):
__tracebackhide__ = True
- if not isinstance(array, numpy.ndarray):
- array = numpy.array(array)
+ if not isinstance(array, np.ndarray):
+ array = np.array(array)
if round:
array = array.round(round)
@@ -18,7 +18,7 @@ def array_equals(left, right, round=6):
left = to_array(left, round)
right = to_array(right, round)
- message = 'Arrays are unequal:\n%s\n%s' % (left, right)
+ message = f'Arrays are unequal:\n{left}\n{right}'
if left.size == right.size:
message += '\nDifference:\n%s' % (left - right)