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)