From 7d3688e59ba26548a32a6da00ca16c2b6ca96b27 Mon Sep 17 00:00:00 2001 From: Andrew Lock <69500916+andrewjlock@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:18:30 +1000 Subject: [PATCH 1/6] Update base.py Add True argument to check() in get_mass_properties_with_density() --- stl/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stl/base.py b/stl/base.py index f3953ce..5215874 100644 --- a/stl/base.py +++ b/stl/base.py @@ -655,7 +655,7 @@ def __repr__(self): def get_mass_properties_with_density(self, density): # add density for mesh,density unit kg/m3 when mesh is unit is m - self.check() + self.check(True) def subexpression(x): w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2] From 483fd01ce97964527e80209864e27155243751f7 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 25 Nov 2024 00:55:10 +0100 Subject: [PATCH 2/6] moderniszed setup a little --- .github/workflows/main.yml | 2 +- docs/_theme/flask_theme_support.py | 149 ++++++++++----------- docs/conf.py | 202 +++++++++++++++-------------- docs/requirements.txt | 3 +- pytest.ini | 4 - setup.py | 76 ++++++++--- tox.ini | 8 +- 7 files changed, 242 insertions(+), 202 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5ab1b6..7c04ba9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 diff --git a/docs/_theme/flask_theme_support.py b/docs/_theme/flask_theme_support.py index 555c116..288a708 100644 --- a/docs/_theme/flask_theme_support.py +++ b/docs/_theme/flask_theme_support.py @@ -1,86 +1,89 @@ # flasky extensions. flasky pygments style based on tango style from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal +from pygments.token import ( + Keyword, + Name, + Comment, + String, + Error, + Number, + Operator, + Generic, + Whitespace, + Punctuation, + Other, + Literal, +) class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" + background_color = '#f8f8f8' + default_style = '' styles = { # No corresponding class for the following: # Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - + Whitespace: 'underline #f8f8f8', # class: 'w' + Error: '#a40000 border:#ef2929', # class: 'err' + Other: '#000000', # class 'x' + Comment: 'italic #8f5902', # class: 'c' + Comment.Preproc: 'noitalic', # class: 'cp' + Keyword: 'bold #004461', # class: 'k' + Keyword.Constant: 'bold #004461', # class: 'kc' + Keyword.Declaration: 'bold #004461', # class: 'kd' + Keyword.Namespace: 'bold #004461', # class: 'kn' + Keyword.Pseudo: 'bold #004461', # class: 'kp' + Keyword.Reserved: 'bold #004461', # class: 'kr' + Keyword.Type: 'bold #004461', # class: 'kt' + Operator: '#582800', # class: 'o' + Operator.Word: 'bold #004461', # class: 'ow' - like keywords + Punctuation: 'bold #000000', # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' + Name: '#000000', # class: 'n' + Name.Attribute: '#c4a000', # class: 'na' - to be revised + Name.Builtin: '#004461', # class: 'nb' + Name.Builtin.Pseudo: '#3465a4', # class: 'bp' + Name.Class: '#000000', # class: 'nc' - to be revised + Name.Constant: '#000000', # class: 'no' - to be revised + Name.Decorator: '#888', # class: 'nd' - to be revised + Name.Entity: '#ce5c00', # class: 'ni' + Name.Exception: 'bold #cc0000', # class: 'ne' + Name.Function: '#000000', # class: 'nf' + Name.Property: '#000000', # class: 'py' + Name.Label: '#f57900', # class: 'nl' + Name.Namespace: '#000000', # class: 'nn' - to be revised + Name.Other: '#000000', # class: 'nx' + Name.Tag: 'bold #004461', # class: 'nt' - like a keyword + Name.Variable: '#000000', # class: 'nv' - to be revised + Name.Variable.Class: '#000000', # class: 'vc' - to be revised + Name.Variable.Global: '#000000', # class: 'vg' - to be revised + Name.Variable.Instance: '#000000', # class: 'vi' - to be revised + Number: '#990000', # class: 'm' + Literal: '#000000', # class: 'l' + Literal.Date: '#000000', # class: 'ld' + String: '#4e9a06', # class: 's' + String.Backtick: '#4e9a06', # class: 'sb' + String.Char: '#4e9a06', # class: 'sc' + String.Doc: 'italic #8f5902', # class: 'sd' - like a comment + String.Double: '#4e9a06', # class: 's2' + String.Escape: '#4e9a06', # class: 'se' + String.Heredoc: '#4e9a06', # class: 'sh' + String.Interpol: '#4e9a06', # class: 'si' + String.Other: '#4e9a06', # class: 'sx' + String.Regex: '#4e9a06', # class: 'sr' + String.Single: '#4e9a06', # class: 's1' + String.Symbol: '#4e9a06', # class: 'ss' + Generic: '#000000', # class: 'g' + Generic.Deleted: '#a40000', # class: 'gd' + Generic.Emph: 'italic #000000', # class: 'ge' + Generic.Error: '#ef2929', # class: 'gr' + Generic.Heading: 'bold #000080', # class: 'gh' + Generic.Inserted: '#00A000', # class: 'gi' + Generic.Output: '#888', # class: 'go' + Generic.Prompt: '#745334', # class: 'gp' + Generic.Strong: 'bold #000000', # class: 'gs' + Generic.Subheading: 'bold #800080', # class: 'gu' + Generic.Traceback: 'bold #a40000', # class: 'gt' } diff --git a/docs/conf.py b/docs/conf.py index 2339c01..00d2a5b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,11 +18,12 @@ try: import numpy + assert numpy except ImportError: # From the readthedocs manual # http://read-the-docs.readthedocs.org/en/latest/faq.html?highlight=numpy - print >>sys.stderr, 'Unable to import numpy, falling back to mock' + print('Unable to import numpy, falling back to mock', file=sys.stderr) import mock MOCK_MODULES = ['pygtk', 'gtk', 'gobject', 'argparse', 'numpy', 'pandas'] @@ -39,7 +40,7 @@ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -62,14 +63,14 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = metadata.__package_name__.replace('-', ' ').capitalize() -copyright = u'%s, %s' % ( +copyright = '%s, %s' % ( datetime.date.today().year, metadata.__author__, ) @@ -85,13 +86,13 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -99,27 +100,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# If true, keep warnings as 'system message' paragraphs in the built documents. +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- @@ -131,77 +132,77 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_theme'] # The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None +# ' v documentation'. +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". +# so a file named 'default.css' will overwrite the builtin 'default.css'. # html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# If true, 'Created using Sphinx' is shown in the HTML footer. Default is True. +# html_show_sphinx = True -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# If true, '(C) Copyright ...' is shown in the HTML footer. Default is True. +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# This is the file name suffix for HTML files (e.g. '.xhtml'). +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = metadata.__package_name__ + '-doc' @@ -210,61 +211,65 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # 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__, - u'%s Documentation' % metadata.__package_name__.replace('-', ' ').capitalize(), - metadata.__author__, - 'manual', -)] +latex_documents = [ + ( + 'index', + '%s.tex' % metadata.__package_name__, + '%s Documentation' + % metadata.__package_name__.replace('-', ' ').capitalize(), + metadata.__author__, + 'manual', + ) +] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None -# For "manual" documents, if this is true, then toplevel headings are parts, +# For 'manual' documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [( - 'index', - metadata.__package_name__, - u'%s Documentation' % metadata.__package_name__.replace('-', ' ').capitalize(), - [metadata.__author__], - 1, -)] +man_pages = [ + ( + 'index', + metadata.__package_name__, + '%s Documentation' + % metadata.__package_name__.replace('-', ' ').capitalize(), + [metadata.__author__], + 1, + ) +] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -272,27 +277,30 @@ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) -texinfo_documents = [( - 'index', - metadata.__package_name__, - u'%s Documentation' % metadata.__package_name__.replace('-', ' ').capitalize(), - metadata.__author__, - metadata.__package_name__, - metadata.__description__, - 'Miscellaneous', -)] +texinfo_documents = [ + ( + 'index', + metadata.__package_name__, + '%s Documentation' + % metadata.__package_name__.replace('-', ' ').capitalize(), + metadata.__author__, + metadata.__package_name__, + metadata.__description__, + 'Miscellaneous', + ) +] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# If true, do not generate a @detailmenu in the 'Top' node's menu. +# texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- @@ -307,59 +315,59 @@ # 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' +# epub_theme = 'epub' # The language of the text. It defaults to the language option # or en if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # A tuple containing the cover image and cover page html template filenames. -#epub_cover = () +# epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () +# epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 # Allow duplicate toc entries. -#epub_tocdup = True +# epub_tocdup = True # Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' +# epub_tocscope = 'default' # Fix unsupported image types using the PIL. -#epub_fix_images = False +# epub_fix_images = False # Scale large images. -#epub_max_image_width = 0 +# epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' +# epub_show_urls = 'inline' # If false, no index is generated. -#epub_use_index = True +# epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. diff --git a/docs/requirements.txt b/docs/requirements.txt index 007ab87..142b6ca 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1 @@ -mock -python-utils +-e .[docs] diff --git a/pytest.ini b/pytest.ini index 9a40476..ce91b88 100644 --- a/pytest.ini +++ b/pytest.ini @@ -14,7 +14,3 @@ addopts = --no-cov-on-fail --ignore=build --basetemp=tmp - -flake8-ignore = - *.py W391 W504 - docs/*.py ALL diff --git a/setup.py b/setup.py index 6b44b05..f0db90e 100644 --- a/setup.py +++ b/setup.py @@ -14,10 +14,13 @@ def error(*lines): try: from stl import stl + if not hasattr(stl, 'BaseStl'): - error('ERROR', - 'You have an incompatible stl package installed' - 'Please run "pip uninstall -y stl" first') + error( + 'ERROR', + 'You have an incompatible stl package installed' + 'Please run "pip uninstall -y stl" first', + ) sys.exit(1) except ImportError: pass @@ -27,17 +30,21 @@ def error(*lines): import numpy from Cython import Build - setup_kwargs['ext_modules'] = Build.cythonize([ - extension.Extension( - 'stl._speedups', - ['stl/_speedups.pyx'], - include_dirs=[numpy.get_include()], - ), - ]) + setup_kwargs['ext_modules'] = Build.cythonize( + [ + extension.Extension( + 'stl._speedups', + ['stl/_speedups.pyx'], + include_dirs=[numpy.get_include()], + ), + ] + ) except ImportError: - error('WARNING', - 'Cython and Numpy is required for building extension.', - 'Falling back to pure Python implementation.') + error( + 'WARNING', + 'Cython and Numpy is required for building extension.', + 'Falling back to pure Python implementation.', + ) # To prevent importing about and thereby breaking the coverage info we use this # exec hack @@ -50,8 +57,9 @@ 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/%s/' % (about['__package_name__']) + ) install_requires = [ 'numpy', @@ -63,21 +71,23 @@ def error(*lines): class BuildExt(build_ext): - def run(self): try: build_ext.run(self) except Exception as e: - warnings.warn(''' + warnings.warn( + """ 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 + ) if __name__ == '__main__': setup( - python_requires='>3.6.0', + python_requires='>3.9.0', name=about['__package_name__'], version=about['__version__'], author=about['__author__'], @@ -115,6 +125,30 @@ def run(self): cmdclass=dict( build_ext=BuildExt, ), - **setup_kwargs + extras_require={ + 'docs': [ + 'mock', + 'sphinx', + 'python-utils', + ], + 'tests': [ + 'cov-core', + 'coverage', + 'docutils', + 'execnet', + 'numpy', + 'cython', + 'pep8', + 'py', + 'pyflakes', + 'pytest', + 'pytest-cache', + 'pytest-cov', + 'python-utils', + 'Sphinx', + 'flake8', + 'wheel', + ], + }, + **setup_kwargs, ) - diff --git a/tox.ini b/tox.ini index a5eb822..118c5a3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36, py37, py38, py39, py310, pypy3, flake8, docs +envlist = ruff, black, pypy3, py39, py310, py311, py312, py313, docs, mypy, pyright skip_missing_interpreters = True [testenv] @@ -9,11 +9,11 @@ commands = python setup.py build_ext --inplace python -m pytest -vvv {posargs} basepython = - py36: python3.6 - py37: python3.7 - py38: python3.8 py39: python3.9 py310: python3.10 + py311: python3.11 + py312: python3.12 + py313: python3.13 pypy3: pypy3 [gh-actions] From bf125a3881156458054b265d01f2f8a9ac3cf887 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 25 Nov 2024 00:55:33 +0100 Subject: [PATCH 3/6] ruff format... it really likes double quotes for some reason --- stl/__about__.py | 7 +- stl/base.py | 219 ++++++++++++++++++++--------------- stl/main.py | 71 ++++++++---- stl/mesh.py | 1 - stl/stl.py | 141 ++++++++++++---------- tests/stl_corruption.py | 20 ++-- tests/test_ascii.py | 68 ++++++----- tests/test_binary.py | 9 +- tests/test_convert.py | 10 +- tests/test_mesh.py | 207 +++++++++++++++------------------ tests/test_meshProperties.py | 71 ++++++++---- tests/test_multiple.py | 16 +-- tests/test_rotate.py | 149 +++++++++++------------- 13 files changed, 520 insertions(+), 469 deletions(-) diff --git a/stl/__about__.py b/stl/__about__.py index 6467f12..9e5588d 100644 --- a/stl/__about__.py +++ b/stl/__about__.py @@ -3,9 +3,10 @@ __version__ = '3.1.2' __author__ = 'Rick van Hattem' __author_email__ = 'Wolph@Wol.ph' -__description__ = ' '.join(''' +__description__ = ' '.join( + """ Library to make reading, writing and modifying both binary and ascii STL files easy. -'''.split()) +""".split() +) __url__ = 'https://github.com/WoLpH/numpy-stl/' - diff --git a/stl/base.py b/stl/base.py index f3953ce..8255165 100644 --- a/stl/base.py +++ b/stl/base.py @@ -36,10 +36,11 @@ class Dimension(enum.IntEnum): class RemoveDuplicates(enum.Enum): - ''' + """ Choose whether to remove no duplicates, leave only a single of the duplicates or remove all duplicates (leaving holes). - ''' + """ + NONE = 0 SINGLE = 1 ALL = 2 @@ -77,7 +78,7 @@ def logged(class_): @logged class BaseMesh(logger.Logged, abc.Mapping): - ''' + """ Mesh object with easy access to the vectors through v0, v1 and v2. The normals, areas, min, max and units are calculated automatically. @@ -108,32 +109,34 @@ class BaseMesh(logger.Logged, abc.Mapping): >>> # Check item 0 (contains v0, v1 and v2) >>> assert numpy.array_equal( - ... mesh[0], - ... numpy.array([1., 1., 1., 2., 2., 2., 0., 0., 0.])) - >>> assert numpy.array_equal( - ... mesh.vectors[0], - ... numpy.array([[1., 1., 1.], - ... [2., 2., 2.], - ... [0., 0., 0.]])) + ... mesh[0], numpy.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., 1., 1.])) + ... mesh.vectors[0], + ... numpy.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( ... mesh.points[0], - ... numpy.array([1., 1., 1., 2., 2., 2., 0., 0., 0.])) + ... numpy.array([1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0]), + ... ) >>> assert numpy.array_equal( ... mesh.data[0], - ... numpy.array(( - ... [0., 0., 0.], - ... [[1., 1., 1.], [2., 2., 2.], [0., 0., 0.]], - ... [0]), - ... dtype=BaseMesh.dtype)) - >>> assert numpy.array_equal(mesh.x[0], numpy.array([1., 2., 0.])) + ... numpy.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]], + ... [0], + ... ), + ... dtype=BaseMesh.dtype, + ... ), + ... ) + >>> assert numpy.array_equal(mesh.x[0], numpy.array([1.0, 2.0, 0.0])) >>> mesh[0] = 3 >>> assert numpy.array_equal( - ... mesh[0], - ... numpy.array([3., 3., 3., 3., 3., 3., 3., 3., 3.])) + ... mesh[0], numpy.array([3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0]) + ... ) >>> len(mesh) == len(list(mesh)) True @@ -164,29 +167,39 @@ class BaseMesh(logger.Logged, abc.Mapping): >>> mesh.points = 4 >>> (mesh.points == 4).all() True - ''' + """ + #: - normals: :func:`numpy.float32`, `(3, )` #: - vectors: :func:`numpy.float32`, `(3, 3)` #: - attr: :func:`numpy.uint16`, `(1, )` - dtype = numpy.dtype([ - ('normals', numpy.float32, (3,)), - ('vectors', numpy.float32, (3, 3)), - ('attr', numpy.uint16, (1,)), - ]) + dtype = numpy.dtype( + [ + ('normals', numpy.float32, (3,)), + ('vectors', numpy.float32, (3, 3)), + ('attr', numpy.uint16, (1,)), + ] + ) dtype = dtype.newbyteorder('<') # Even on big endian arches, use little e. - def __init__(self, data, calculate_normals=True, - remove_empty_areas=False, - remove_duplicate_polygons=RemoveDuplicates.NONE, - name='', speedups=True, **kwargs): + def __init__( + self, + data, + calculate_normals=True, + remove_empty_areas=False, + remove_duplicate_polygons=RemoveDuplicates.NONE, + name='', + speedups=True, + **kwargs, + ): super(BaseMesh, self).__init__(**kwargs) self.speedups = speedups if remove_empty_areas: data = self.remove_empty_areas(data) if RemoveDuplicates.map(remove_duplicate_polygons).value: - data = self.remove_duplicate_polygons(data, - remove_duplicate_polygons) + data = self.remove_duplicate_polygons( + data, remove_duplicate_polygons + ) self.name = name self.data = data @@ -252,27 +265,27 @@ def v2(self, value): @property def x(self): - return self.points[:, Dimension.X::3] + return self.points[:, Dimension.X :: 3] @x.setter def x(self, value): - self.points[:, Dimension.X::3] = value + self.points[:, Dimension.X :: 3] = value @property def y(self): - return self.points[:, Dimension.Y::3] + return self.points[:, Dimension.Y :: 3] @y.setter def y(self, value): - self.points[:, Dimension.Y::3] = value + self.points[:, Dimension.Y :: 3] = value @property def z(self): - return self.points[:, Dimension.Z::3] + return self.points[:, Dimension.Z :: 3] @z.setter def z(self, value): - self.points[:, Dimension.Z::3] = value + self.points[:, Dimension.Z :: 3] = value @classmethod def remove_duplicate_polygons(cls, data, value=RemoveDuplicates.SINGLE): @@ -309,11 +322,11 @@ def remove_empty_areas(cls, data): v1 = vectors[:, 1] v2 = vectors[:, 2] normals = numpy.cross(v1 - v0, v2 - v0) - squared_areas = (normals ** 2).sum(axis=1) - return data[squared_areas > AREA_SIZE_THRESHOLD ** 2] + 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''' + """Update the normals, areas, and centroids for all points""" normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0) if update_areas: @@ -342,30 +355,30 @@ def update_areas(self, normals=None): if normals is None: normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0) - areas = .5 * numpy.sqrt((normals ** 2).sum(axis=1)) + areas = 0.5 * numpy.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) def check(self, exact=False): - '''Check the mesh is valid or not + """Check the mesh is valid or not :param bool exact: Perform exact checks. - ''' + """ return self.is_closed(exact=exact) def is_closed(self, exact=False): # pragma: no cover - '''Check the mesh is closed or not + """Check the mesh is closed or not :param bool exact: Perform a exact check on edges. - ''' + """ if exact: reversed_triangles = ( - numpy.cross(self.v1 - self.v0, - self.v2 - self.v0) * self.normals - ).sum(axis=1) < 0 + numpy.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()) for rev, edge in zip( @@ -378,39 +391,40 @@ def is_closed(self, exact=False): # pragma: no cover ) } if len(directed_edges) == 3 * self.data.size: - undirected_edges = {frozenset((edge[:3], edge[3:])) for edge in - directed_edges} + undirected_edges = { + frozenset((edge[:3], edge[3:])) for edge in directed_edges + } if len(directed_edges) == 2 * len(undirected_edges): return True else: - self.warning(''' + self.warning( + """ Use of not exact is_closed check. This check can lead to misleading results. You could try to use `exact=True`. See: - false positive: https://github.com/wolph/numpy-stl/issues/198 - false negative: https://github.com/wolph/numpy-stl/pull/213 - '''.strip() - ) + """.strip() + ) normals = numpy.asarray(self.normals, dtype=numpy.float64) allowed_max_errors = ( - numpy.abs(normals).sum(axis=0) * numpy.finfo( - numpy.float32).eps + numpy.abs(normals).sum(axis=0) * numpy.finfo(numpy.float32).eps ) if (numpy.abs(normals.sum(axis=0)) <= allowed_max_errors).all(): return True self.warning( - ''' + """ Your mesh is not closed, the mass methods will not function correctly on this mesh. For more info: https://github.com/WoLpH/numpy-stl/issues/69 - '''.strip() + """.strip() ) return False def get_mass_properties(self): - ''' + """ Evaluate and return a tuple with the following elements: - the volume - the position of the center of gravity (COG) @@ -418,7 +432,7 @@ def get_mass_properties(self): Documentation can be found here: http://www.geometrictools.com/Documentation/PolyhedralMassProperties.pdf - ''' + """ self.check(True) def subexpression(x): @@ -455,7 +469,7 @@ def subexpression(x): intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120]) volume = intg[0] cog = intg[1:4] / volume - cogsq = cog ** 2 + cogsq = cog**2 inertia = numpy.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]) @@ -471,8 +485,10 @@ def update_units(self): areas = self.areas if non_zero_areas.shape[0] != areas.shape[0]: # pragma: no cover - self.warning('Zero sized areas found, ' - 'units calculation will be partially incorrect') + self.warning( + 'Zero sized areas found, ' + 'units calculation will be partially incorrect' + ) if non_zero_areas.any(): non_zero_areas.shape = non_zero_areas.shape[0] @@ -483,7 +499,7 @@ def update_units(self): @classmethod def rotation_matrix(cls, axis, theta): - ''' + """ Generate a rotation matrix to Rotate the matrix over the given axis by the given theta (angle) @@ -494,7 +510,7 @@ def rotation_matrix(cls, axis, theta): :param numpy.array axis: Axis to rotate over (x, y, z) :param float theta: Rotation angle in radians, use `math.radians` to convert degrees to radians if needed. - ''' + """ axis = numpy.asarray(axis) # No need to rotate if there is no actual rotation if not axis.any(): @@ -505,7 +521,7 @@ def rotation_matrix(cls, axis, theta): axis = axis / numpy.linalg.norm(axis) a = math.cos(theta) - b, c, d = - axis * math.sin(theta) + b, c, d = -axis * math.sin(theta) angles = a, b, c, d powers = [x * y for x in angles for y in angles] aa, ab, ac, ad = powers[0:4] @@ -513,12 +529,16 @@ def rotation_matrix(cls, axis, theta): ca, cb, cc, cd = powers[8:12] da, db, dc, dd = powers[12:16] - return numpy.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)], - [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)], - [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]]) + return numpy.array( + [ + [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)], + [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)], + [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc], + ] + ) def rotate(self, axis, theta=0, point=None): - ''' + """ Rotate the matrix over the given axis by the given theta (angle) Uses the :py:func:`rotation_matrix` in the background. @@ -533,7 +553,7 @@ def rotate(self, axis, theta=0, point=None): convert degrees to radians if needed. :param numpy.array point: Rotation point so manual translation is not required - ''' + """ # No need to rotate if there is no actual rotation if not theta: return @@ -541,14 +561,14 @@ def rotate(self, axis, theta=0, point=None): self.rotate_using_matrix(self.rotation_matrix(axis, theta), point) def rotate_using_matrix(self, rotation_matrix, point=None): - ''' + """ Rotate using a given rotation matrix and optional rotation point Note that this rotation produces clockwise rotations for positive angles which is arguably incorrect but will remain for legacy reasons. For more details, read here: https://github.com/WoLpH/numpy-stl/issues/166 - ''' + """ identity = numpy.identity(rotation_matrix.shape[0]) # No need to rotate if there is no actual rotation @@ -580,18 +600,18 @@ def _rotate(matrix): self.vectors[:, i] = _rotate(self.vectors[:, i]) def translate(self, translation): - ''' + """ Translate the mesh in the three directions :param numpy.array translation: Translation vector (x, y, z) - ''' + """ assert len(translation) == 3, 'Translation vector must be of length 3' self.x += translation[0] self.y += translation[1] self.z += translation[2] def transform(self, matrix): - ''' + """ Transform the mesh with a rotation and a translation stored in a single 4x4 matrix @@ -600,7 +620,7 @@ def transform(self, matrix): part of the transformation matrix[0:3, 3] represents the translation part of the transformation - ''' + """ 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] @@ -626,16 +646,19 @@ def _set(self, value): return _set - min_ = property(_get_or_update('min'), _set('min'), - doc='Mesh minimum value') - max_ = property(_get_or_update('max'), _set('max'), - doc='Mesh maximum value') - areas = property(_get_or_update('areas'), _set('areas'), - doc='Mesh areas') - centroids = property(_get_or_update('centroids'), _set('centroids'), - doc='Mesh centroids') - units = property(_get_or_update('units'), _set('units'), - doc='Mesh unit vectors') + min_ = property( + _get_or_update('min'), _set('min'), doc='Mesh minimum value' + ) + max_ = property( + _get_or_update('max'), _set('max'), doc='Mesh maximum value' + ) + areas = property(_get_or_update('areas'), _set('areas'), doc='Mesh areas') + centroids = property( + _get_or_update('centroids'), _set('centroids'), doc='Mesh centroids' + ) + units = property( + _get_or_update('units'), _set('units'), doc='Mesh unit vectors' + ) def __getitem__(self, k): return self.points[k] @@ -691,21 +714,27 @@ def subexpression(x): intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120]) volume = intg[0] cog = intg[1:4] / volume - cogsq = cog ** 2 + cogsq = cog**2 vmass = volume * density inertia = numpy.zeros((3, 3)) inertia[0, 0] = (intg[5] + intg[6]) * density - vmass * ( - cogsq[1] + cogsq[2]) + cogsq[1] + cogsq[2] + ) inertia[1, 1] = (intg[4] + intg[6]) * density - vmass * ( - cogsq[2] + cogsq[0]) + cogsq[2] + cogsq[0] + ) inertia[2, 2] = (intg[4] + intg[5]) * density - vmass * ( - cogsq[0] + cogsq[1]) + cogsq[0] + cogsq[1] + ) inertia[0, 1] = inertia[1, 0] = -( - intg[7] * density - vmass * cog[0] * cog[1]) + intg[7] * density - vmass * cog[0] * cog[1] + ) inertia[1, 2] = inertia[2, 1] = -( - intg[8] * density - vmass * cog[1] * cog[2]) + intg[8] * density - vmass * cog[1] * cog[2] + ) inertia[0, 2] = inertia[2, 0] = -( - intg[9] * density - vmass * cog[2] * cog[0]) + intg[9] * density - vmass * cog[2] * cog[0] + ) return volume, vmass, cog, inertia diff --git a/stl/main.py b/stl/main.py index a3ee185..a9e108d 100644 --- a/stl/main.py +++ b/stl/main.py @@ -8,26 +8,38 @@ def _get_parser(description): parser = argparse.ArgumentParser(description=description) parser.add_argument( - 'infile', nargs='?', type=argparse.FileType('rb'), - default=sys.stdin, help='STL file to read' + 'infile', + nargs='?', + type=argparse.FileType('rb'), + default=sys.stdin, + help='STL file to read', ) parser.add_argument( - 'outfile', nargs='?', type=argparse.FileType('wb'), - default=sys.stdout, help='STL file to write' + 'outfile', + nargs='?', + type=argparse.FileType('wb'), + default=sys.stdout, + help='STL file to write', ) parser.add_argument('--name', nargs='?', help='Name of the mesh') parser.add_argument( - '-n', '--use-file-normals', action='store_true', - help='Read the normals from the file instead of recalculating them' + '-n', + '--use-file-normals', + action='store_true', + help='Read the normals from the file instead of recalculating them', ) parser.add_argument( - '-r', '--remove-empty-areas', action='store_true', + '-r', + '--remove-empty-areas', + action='store_true', help='Remove areas with 0 surface areas to prevent errors during ' - 'normal calculation' + 'normal calculation', ) parser.add_argument( - '-s', '--disable-speedups', action='store_true', - help='Disable Cython speedups' + '-s', + '--disable-speedups', + action='store_true', + help='Disable Cython speedups', ) return parser @@ -55,12 +67,16 @@ def _get_name(args): def main(): parser = _get_parser('Convert STL files from ascii to binary and back') parser.add_argument( - '-a', '--ascii', action='store_true', - help='Write ASCII file (default is binary)' + '-a', + '--ascii', + action='store_true', + help='Write ASCII file (default is binary)', ) parser.add_argument( - '-b', '--binary', action='store_true', - help='Force binary file (for TTYs)' + '-b', + '--binary', + action='store_true', + help='Force binary file (for TTYs)', ) args = parser.parse_args() @@ -70,7 +86,7 @@ def main(): fh=args.infile, calculate_normals=False, remove_empty_areas=args.remove_empty_areas, - speedups=not args.disable_speedups + speedups=not args.disable_speedups, ) if args.binary: @@ -81,8 +97,7 @@ def main(): mode = stl.AUTOMATIC stl_file.save( - name, args.outfile, mode=mode, - update_normals=not args.use_file_normals + name, args.outfile, mode=mode, update_normals=not args.use_file_normals ) @@ -91,14 +106,17 @@ def to_ascii(): args = parser.parse_args() name = _get_name(args) stl_file = stl.StlMesh( - filename=name, fh=args.infile, + filename=name, + fh=args.infile, calculate_normals=False, remove_empty_areas=args.remove_empty_areas, - speedups=not args.disable_speedups + speedups=not args.disable_speedups, ) stl_file.save( - name, args.outfile, mode=stl.ASCII, - update_normals=not args.use_file_normals + name, + args.outfile, + mode=stl.ASCII, + update_normals=not args.use_file_normals, ) @@ -107,12 +125,15 @@ def to_binary(): args = parser.parse_args() name = _get_name(args) stl_file = stl.StlMesh( - filename=name, fh=args.infile, + filename=name, + fh=args.infile, calculate_normals=False, remove_empty_areas=args.remove_empty_areas, - speedups=not args.disable_speedups + speedups=not args.disable_speedups, ) stl_file.save( - name, args.outfile, mode=stl.BINARY, - update_normals=not args.use_file_normals + name, + args.outfile, + mode=stl.BINARY, + update_normals=not args.use_file_normals, ) diff --git a/stl/mesh.py b/stl/mesh.py index b2af7d6..b713202 100644 --- a/stl/mesh.py +++ b/stl/mesh.py @@ -3,4 +3,3 @@ class Mesh(stl.BaseStl): pass - diff --git a/stl/stl.py b/stl/stl.py index b6bcd2d..f5b0490 100644 --- a/stl/stl.py +++ b/stl/stl.py @@ -45,16 +45,15 @@ class Mode(enum.IntEnum): class BaseStl(base.BaseMesh): - @classmethod def load(cls, fh, mode=AUTOMATIC, speedups=True): - '''Load Mesh from STL file + """Load Mesh from STL file Automatically detects binary versus ascii STL files. :param file fh: The file handle to open :param int mode: Automatically detect the filetype or force binary - ''' + """ header = fh.read(HEADER_SIZE) if not header: return @@ -65,9 +64,7 @@ def load(cls, fh, mode=AUTOMATIC, speedups=True): if mode is AUTOMATIC: if header.lstrip().lower().startswith(b'solid'): try: - name, data = cls._load_ascii( - fh, header, speedups=speedups - ) + name, data = cls._load_ascii(fh, header, speedups=speedups) except RuntimeError as exception: print('exception', exception) (recoverable, e) = exception.args @@ -75,8 +72,7 @@ def load(cls, fh, mode=AUTOMATIC, speedups=True): # readable through the binary reader if recoverable: name, data = cls._load_binary( - fh, header, - check_size=False + fh, header, check_size=False ) else: # Apparently we've read beyond the header. Let's try @@ -89,8 +85,7 @@ def load(cls, fh, mode=AUTOMATIC, speedups=True): # not 100% certain it's binary, check the size while # reading name, data = cls._load_binary( - fh, header, - check_size=True + fh, header, check_size=True ) else: name, data = cls._load_binary(fh, header) @@ -108,11 +103,12 @@ def _load_binary(cls, fh, header, check_size=False): if len(count_data) != COUNT_SIZE: count = 0 else: - count, = struct.unpack(' 0: position = fh.tell() @@ -184,15 +181,14 @@ def get(prefix=''): else: raise RuntimeError( recoverable[0], - '%r should start with %r' % (line, prefix) + '%r should start with %r' % (line, prefix), ) 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], 'Incorrect value %r' % line ) else: return b(raw_line) @@ -200,8 +196,7 @@ def get(prefix=''): line = get() if not lines: raise RuntimeError( - recoverable[0], - 'No lines found, impossible to read' + recoverable[0], 'No lines found, impossible to read' ) # Yield the name @@ -246,7 +241,7 @@ def _load_ascii(cls, fh, header, speedups=True): return name, numpy.fromiter(iterator, dtype=cls.dtype) def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True): - '''Save the STL to a (binary) file + """Save the STL to a (binary) file If mode is :py:data:`AUTOMATIC` an :py:data:`ASCII` file will be written if the output is a TTY and a :py:data:`BINARY` file otherwise. @@ -255,7 +250,7 @@ def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True): :param file fh: The file handle to open :param int mode: The mode to write, default is :py:data:`AUTOMATIC`. :param bool update_normals: Whether to update the normals - ''' + """ assert filename, 'Filename is required for the STL headers' if update_normals: self.update_normals() @@ -285,8 +280,8 @@ def save(self, filename, fh=None, mode=AUTOMATIC, update_normals=True): # Provide a more helpful error if the user mistakenly # assumes ASCII files should be text files. raise TypeError( - "File handles should be in binary mode - even when" - " writing an ASCII STL." + 'File handles should be in binary mode - even when' + ' writing an ASCII STL.' ) name = self.name @@ -312,6 +307,7 @@ def _write_ascii(self, fh, name): if _speedups and speedups: # pragma: no cover _speedups.ascii_write(fh, b(name), self.data) else: + def p(s, file): file.write(b('%s\n' % s)) @@ -319,7 +315,10 @@ def p(s, file): for row in self.data: vectors = row['vectors'] - p('facet normal %r %r %r' % tuple(row['normals'].tolist()), file=fh) + p( + 'facet normal %r %r %r' % tuple(row['normals'].tolist()), + file=fh, + ) p(' outer loop', file=fh) p(' vertex %r %r %r' % tuple(vectors[0].tolist()), file=fh) p(' vertex %r %r %r' % tuple(vectors[1].tolist()), file=fh) @@ -366,42 +365,48 @@ def _write_binary(self, fh, name): if self.data.size: # pragma: no cover assert fh.tell() > 84, ( 'numpy silently refused to write our file. Note that writing ' - 'to `StringIO` objects is not supported by `numpy`') + 'to `StringIO` objects is not supported by `numpy`' + ) @classmethod def from_file( - cls, filename, calculate_normals=True, fh=None, - mode=Mode.AUTOMATIC, speedups=True, **kwargs + cls, + filename, + calculate_normals=True, + fh=None, + mode=Mode.AUTOMATIC, + speedups=True, + **kwargs, ): - '''Load a mesh from a STL file + """Load a mesh from a STL file :param str filename: The file to load :param bool calculate_normals: Whether to update the normals :param file fh: The file handle to open :param dict kwargs: The same as for :py:class:`stl.mesh.Mesh` - ''' + """ if fh: - name, data = cls.load( - fh, mode=mode, speedups=speedups - ) + name, data = cls.load(fh, mode=mode, speedups=speedups) else: with open(filename, 'rb') as fh: - name, data = cls.load( - fh, mode=mode, speedups=speedups - ) + name, data = cls.load(fh, mode=mode, speedups=speedups) return cls( - data, calculate_normals, name=name, - speedups=speedups, **kwargs + data, calculate_normals, name=name, speedups=speedups, **kwargs ) @classmethod def from_multi_file( - cls, filename, calculate_normals=True, fh=None, - mode=Mode.AUTOMATIC, speedups=True, **kwargs + cls, + filename, + calculate_normals=True, + fh=None, + mode=Mode.AUTOMATIC, + speedups=True, + **kwargs, ): - '''Load multiple meshes from a STL file + """Load multiple meshes from a STL file Note: mode is hardcoded to ascii since binary stl files do not support the multi format @@ -410,7 +415,7 @@ def from_multi_file( :param bool calculate_normals: Whether to update the normals :param file fh: The file handle to open :param dict kwargs: The same as for :py:class:`stl.mesh.Mesh` - ''' + """ if fh: close = False else: @@ -422,13 +427,13 @@ def from_multi_file( while raw_data: name, data = raw_data yield cls( - data, calculate_normals, name=name, - speedups=speedups, **kwargs - ) - raw_data = cls.load( - fh, mode=ASCII, - speedups=speedups + data, + calculate_normals, + name=name, + speedups=speedups, + **kwargs, ) + raw_data = cls.load(fh, mode=ASCII, speedups=speedups) finally: if close: @@ -436,10 +441,14 @@ def from_multi_file( @classmethod def from_files( - cls, filenames, calculate_normals=True, mode=Mode.AUTOMATIC, - speedups=True, **kwargs + cls, + filenames, + calculate_normals=True, + mode=Mode.AUTOMATIC, + speedups=True, + **kwargs, ): - '''Load multiple meshes from STL files into a single mesh + """Load multiple meshes from STL files into a single mesh Note: mode is hardcoded to ascii since binary stl files do not support the multi format @@ -448,7 +457,7 @@ def from_files( :param bool calculate_normals: Whether to update the normals :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( @@ -457,7 +466,7 @@ def from_files( calculate_normals=calculate_normals, mode=mode, speedups=speedups, - **kwargs + **kwargs, ) ) @@ -490,15 +499,19 @@ def from_3mf_file(cls, filename, calculate_normals=True, **kwargs): if tag.endswith('vertices'): # Collect all the vertices for vertice in element: - a = {k: float(v) for k, v in - vertice.attrib.items()} + a = { + k: float(v) + for k, v in vertice.attrib.items() + } vertices.append([a['x'], a['y'], a['z']]) elif tag.endswith('triangles'): # pragma: no branch # Map the triangles to the vertices and collect for triangle in element: - a = {k: int(v) for k, v in - triangle.attrib.items()} + a = { + k: int(v) + for k, v in triangle.attrib.items() + } triangles.append( [ vertices[a['v1']], diff --git a/tests/stl_corruption.py b/tests/stl_corruption.py index cac14f9..b155ad6 100644 --- a/tests/stl_corruption.py +++ b/tests/stl_corruption.py @@ -6,7 +6,7 @@ from stl import mesh -_STL_FILE = ''' +_STL_FILE = """ solid test.stl facet normal -0.014565 0.073223 -0.002897 outer loop @@ -16,7 +16,7 @@ endloop endfacet endsolid test.stl -'''.lstrip() +""".lstrip() def test_valid_ascii(tmpdir, speedups): @@ -51,7 +51,7 @@ def test_ascii_with_missing_name(tmpdir, speedups): def test_ascii_with_blank_lines(tmpdir, speedups): - _stl_file = ''' + _stl_file = """ solid test.stl @@ -69,7 +69,7 @@ def test_ascii_with_blank_lines(tmpdir, speedups): endfacet endsolid test.stl - '''.lstrip() + """.lstrip() tmp_file = tmpdir.join('tmp.stl') with tmp_file.open('w+') as fh: @@ -141,14 +141,8 @@ 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.]]) - data['vectors'][0] = numpy.array([[0, 0, 0], - [2, 0, 0], - [0, 2, 1.]]) - data['vectors'][0] = numpy.array([[0, 0, 0], - [3, 0, 0], - [0, 3, 1.]]) + 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]]) assert not mesh.Mesh(data, remove_empty_areas=False).check() diff --git a/tests/test_ascii.py b/tests/test_ascii.py index 229b90d..c5a695c 100644 --- a/tests/test_ascii.py +++ b/tests/test_ascii.py @@ -22,7 +22,8 @@ def test_ascii_file(speedups): def test_chinese_name(tmpdir, speedups): name = 'Test Chinese name 月球' - _stl_file = (''' + _stl_file = ( + """ solid %s facet normal -0.014565 0.073223 -0.002897 outer loop @@ -32,14 +33,17 @@ def test_chinese_name(tmpdir, speedups): endloop endfacet endsolid - ''' % name).lstrip() + """ + % name + ).lstrip() tmp_file = tmpdir.join('tmp.stl') with tmp_file.open('wb+') as fh: fh.write(b(_stl_file)) fh.seek(0) - test_mesh = mesh.Mesh.from_file(str(tmp_file), fh=fh, - speedups=speedups) + test_mesh = mesh.Mesh.from_file( + str(tmp_file), fh=fh, speedups=speedups + ) if speedups: assert test_mesh.name.lower() == b(name).lower() else: @@ -49,7 +53,8 @@ def test_chinese_name(tmpdir, speedups): def test_long_name(tmpdir, speedups): name = 'Just Some Very Long Name which will not fit within the standard' name += name - _stl_file = (''' + _stl_file = ( + """ solid %s facet normal -0.014565 0.073223 -0.002897 outer loop @@ -59,14 +64,17 @@ def test_long_name(tmpdir, speedups): endloop endfacet endsolid - ''' % name).lstrip() + """ + % name + ).lstrip() tmp_file = tmpdir.join('tmp.stl') with tmp_file.open('wb+') as fh: fh.write(b(_stl_file)) fh.seek(0) - test_mesh = mesh.Mesh.from_file(str(tmp_file), fh=fh, - speedups=speedups) + test_mesh = mesh.Mesh.from_file( + str(tmp_file), fh=fh, speedups=speedups + ) if speedups: assert test_mesh.name.lower() == b(name).lower() @@ -77,7 +85,8 @@ def test_long_name(tmpdir, speedups): def test_scientific_notation(tmpdir, speedups): name = 'just some very long name which will not fit within the standard' name += name - _stl_file = (''' + _stl_file = ( + """ solid %s facet normal 1.014565e-10 7.3223e-5 -10 outer loop @@ -87,19 +96,23 @@ def test_scientific_notation(tmpdir, speedups): endloop endfacet endsolid - ''' % name).lstrip() + """ + % name + ).lstrip() tmp_file = tmpdir.join('tmp.stl') with tmp_file.open('wb+') as fh: fh.write(b(_stl_file)) fh.seek(0) - test_mesh = mesh.Mesh.from_file(str(tmp_file), fh=fh, - speedups=speedups) + test_mesh = mesh.Mesh.from_file( + str(tmp_file), fh=fh, speedups=speedups + ) assert test_mesh.name == b(name) -@pytest.mark.skipif(sys.platform.startswith('win'), - reason='Only makes sense on Unix') +@pytest.mark.skipif( + sys.platform.startswith('win'), reason='Only makes sense on Unix' +) def test_locale_restore(speedups): if not speedups: pytest.skip('Only makes sense with speedups') @@ -113,8 +126,9 @@ def test_locale_restore(speedups): assert old_locale == new_locale -@pytest.mark.skipif(sys.platform.startswith('win'), - reason='Only makes sense on Unix') +@pytest.mark.skipif( + sys.platform.startswith('win'), reason='Only makes sense on Unix' +) def test_use_with_qt_with_custom_locale_decimal_delimeter(speedups): if not speedups: pytest.skip('Only makes sense with speedups') @@ -146,11 +160,13 @@ def test_use_with_qt_with_custom_locale_decimal_delimeter(speedups): if sys.platform.startswith('linux'): prefix = ('xvfb-run', '-a') - p = subprocess.Popen(prefix + (sys.executable, script_path), - env=env, - universal_newlines=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + p = subprocess.Popen( + prefix + (sys.executable, script_path), + env=env, + universal_newlines=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) out, err = p.communicate() # Unable to read the file with speedups, retrying @@ -170,16 +186,16 @@ def test_ascii_io(): # Check that unhelpful 'expected str but got bytes' error is caught and # replaced. - with pytest.raises(TypeError, match="handles should be in binary mode"): - mesh_.save("nameless", fh=io.StringIO(), mode=Mode.ASCII) + with pytest.raises(TypeError, match='handles should be in binary mode'): + mesh_.save('nameless', fh=io.StringIO(), mode=Mode.ASCII) # Write to an io.BytesIO(). fh = io.BytesIO() - mesh_.save("nameless", fh=fh, mode=Mode.ASCII) + mesh_.save('nameless', fh=fh, mode=Mode.ASCII) # Assert binary file is still only ascii characters. - fh.getvalue().decode("ascii") + fh.getvalue().decode('ascii') # Read the mesh back in. - read = mesh.Mesh.from_file("anonymous.stl", fh=io.BytesIO(fh.getvalue())) + 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) diff --git a/tests/test_binary.py b/tests/test_binary.py index 6740d5d..63f6449 100644 --- a/tests/test_binary.py +++ b/tests/test_binary.py @@ -26,17 +26,14 @@ def _test(tmpdir, speedups, mode, use_filehandle=True): filename = TESTS_PATH / 'stl_binary' / 'rear_case.stl' if use_filehandle: with open(filename, 'rb') as fh: - mesh.Mesh.from_file(filename, fh=fh, speedups=speedups, - mode=mode) + mesh.Mesh.from_file(filename, fh=fh, speedups=speedups, mode=mode) with open(filename, 'rb') as fh: # Test with BytesIO fh = io.BytesIO(fh.read()) - mesh.Mesh.from_file(filename, fh=fh, speedups=speedups, - mode=mode) + mesh.Mesh.from_file(filename, fh=fh, speedups=speedups, mode=mode) else: - mesh.Mesh.from_file(filename, - speedups=speedups, mode=mode) + mesh.Mesh.from_file(filename, speedups=speedups, mode=mode) @pytest.mark.parametrize('mode', [Mode.BINARY, Mode.AUTOMATIC]) diff --git a/tests/test_convert.py b/tests/test_convert.py index 2d0d1ad..38390bd 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -36,13 +36,15 @@ def _test_conversion(from_, to, mode, speedups): def test_ascii_to_binary(ascii_path, binary_path, speedups): - _test_conversion(ascii_path, binary_path, mode=stl.BINARY, - speedups=speedups) + _test_conversion( + ascii_path, binary_path, mode=stl.BINARY, speedups=speedups + ) def test_binary_to_ascii(ascii_path, binary_path, speedups): - _test_conversion(binary_path, ascii_path, mode=stl.ASCII, - speedups=speedups) + _test_conversion( + binary_path, ascii_path, mode=stl.ASCII, speedups=speedups + ) def test_stl_mesh(ascii_file, tmpdir, speedups): diff --git a/tests/test_mesh.py b/tests/test_mesh.py index c5c93cb..94fd9a2 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -9,9 +9,7 @@ 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['vectors'][0] = numpy.array([[0, 0, 0], [1, 0, 0], [2, 0, 0]]) mesh = Mesh(data, remove_empty_areas=False) mesh.update_units() @@ -25,67 +23,48 @@ def test_units_1d(): 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['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]]) 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.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(mesh.get_unit_normals(), [ - [0.0, 0.0, 1.0], - [0.0, 0.0, -1.0]]) + assert numpy.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.]]) + data['vectors'][0] = numpy.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 ** .5) < 0.0001 + 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 numpy.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['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]]) mesh = Mesh(data) assert mesh.data.size == 6 @@ -108,47 +87,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 numpy.allclose(mesh.vectors[1], numpy.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 numpy.allclose( + mesh.vectors[0], numpy.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 numpy.allclose( + mesh.vectors[2], numpy.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 numpy.allclose(mesh.vectors[1], numpy.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 numpy.allclose( + mesh.vectors[0], numpy.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 numpy.allclose( + mesh.vectors[2], numpy.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['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]]) mesh = Mesh(data, remove_duplicate_polygons=False) assert mesh.data.size == 5 @@ -157,28 +126,22 @@ def test_remove_all_duplicate_polygons(): mesh = Mesh(data, remove_duplicate_polygons=RemoveDuplicates.ALL) assert mesh.data.size == 3 - assert (mesh.vectors[0] == numpy.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]])).all() - assert (mesh.vectors[2] == numpy.array([[2, 0, 0], - [0, 0, 0], - [0, 0, 0]])).all() + assert ( + mesh.vectors[0] == numpy.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]]) + ).all() + assert ( + mesh.vectors[2] == numpy.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['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]]) mesh = Mesh(data, calculate_normals=False, remove_empty_areas=False) assert mesh.data.size == 3 @@ -190,21 +153,22 @@ def test_empty_areas(): mesh.centroids[1] = [1, 2, 3] mesh.centroids[2] = [4, 5, 6] - assert numpy.allclose(mesh.centroids, [[1 / 3, 1 / 3, 0], - [1, 2, 3], - [4, 5, 6]]) + assert numpy.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(mesh.centroids, [[1 / 3, 1 / 3, 0], - [1, 2, 3], - [4, 5, 6]]) + assert numpy.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(mesh.centroids, [[1 / 3, 1 / 3, 0], - [2 / 3, 1 / 3, 0], - [2 / 3, 1 / 3, 0]]) + assert numpy.allclose( + mesh.centroids, + [[1 / 3, 1 / 3, 0], [2 / 3, 1 / 3, 0], [2 / 3, 1 / 3, 0]], + ) mesh = Mesh(data, remove_empty_areas=True) assert mesh.data.size == 1 @@ -218,23 +182,38 @@ def test_base_mesh(): mesh.v1[0] += 2 # Check item 0 (contains v0, v1 and v2) - assert (mesh[0] == numpy.array( - [1., 1., 1., 2., 2., 2., 0., 0., 0.], dtype=numpy.float32) + 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 + ) ).all() - assert (mesh.vectors[0] == numpy.array([ - [1., 1., 1.], - [2., 2., 2.], - [0., 0., 0.]], dtype=numpy.float32)).all() - assert (mesh.v0[0] == numpy.array([1., 1., 1.], dtype=numpy.float32)).all() - assert (mesh.points[0] == numpy.array( - [1., 1., 1., 2., 2., 2., 0., 0., 0.], dtype=numpy.float32) + assert ( + mesh.vectors[0] + == numpy.array( + [[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [0.0, 0.0, 0.0]], + dtype=numpy.float32, + ) + ).all() + assert ( + mesh.v0[0] == numpy.array([1.0, 1.0, 1.0], dtype=numpy.float32) ).all() assert ( - mesh.x[0] == numpy.array([1., 2., 0.], dtype=numpy.float32)).all() + 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 + ) + ).all() + assert ( + mesh.x[0] == numpy.array([1.0, 2.0, 0.0], dtype=numpy.float32) + ).all() mesh[0] = 3 - assert (mesh[0] == numpy.array( - [3., 3., 3., 3., 3., 3., 3., 3., 3.], dtype=numpy.float32) + 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 + ) ).all() assert len(mesh) == len(list(mesh)) diff --git a/tests/test_meshProperties.py b/tests/test_meshProperties.py index b2b4729..c95707e 100644 --- a/tests/test_meshProperties.py +++ b/tests/test_meshProperties.py @@ -12,69 +12,85 @@ def close(a, b): def test_mass_properties_for_half_donut(binary_ascii_path, speedups): - ''' + """ Checks the results of method get_mass_properties() on STL ASCII and binary files HalfDonut.stl One checks the results obtained with stl with the ones obtained with meshlab - ''' - filename = binary_ascii_path/'HalfDonut.stl' + """ + filename = binary_ascii_path / 'HalfDonut.stl' mesh = stl.StlMesh(str(filename), speedups=speedups) volume, cog, inertia = mesh.get_mass_properties() assert close([volume], [2.343149]) assert close(cog, [1.500001, 0.209472, 1.500001]) - assert close(inertia, [[+1.390429, +0.000000, +0.000000], - [+0.000000, +2.701025, +0.000000], - [+0.000000, +0.000000, +1.390429]]) + assert close( + inertia, + [ + [+1.390429, +0.000000, +0.000000], + [+0.000000, +2.701025, +0.000000], + [+0.000000, +0.000000, +1.390429], + ], + ) def test_mass_properties_for_moon(binary_ascii_path, speedups): - ''' + """ Checks the results of method get_mass_properties() on STL ASCII and binary files Moon.stl One checks the results obtained with stl with the ones obtained with meshlab - ''' - filename = binary_ascii_path/'Moon.stl' + """ + filename = binary_ascii_path / 'Moon.stl' mesh = stl.StlMesh(str(filename), speedups=speedups) volume, cog, inertia = mesh.get_mass_properties() assert close([volume], [0.888723]) assert close(cog, [0.906913, 0.170731, 1.500001]) - assert close(inertia, [[+0.562097, -0.000457, +0.000000], - [-0.000457, +0.656851, +0.000000], - [+0.000000, +0.000000, +0.112465]]) + assert close( + inertia, + [ + [+0.562097, -0.000457, +0.000000], + [-0.000457, +0.656851, +0.000000], + [+0.000000, +0.000000, +0.112465], + ], + ) @pytest.mark.parametrize('filename', ('Star.stl', 'StarWithEmptyHeader.stl')) def test_mass_properties_for_star(binary_ascii_path, filename, speedups): - ''' + """ Checks the results of method get_mass_properties() on STL ASCII and binary files Star.stl and STL binary file StarWithEmptyHeader.stl (with no header) One checks the results obtained with stl with the ones obtained with meshlab - ''' - filename = binary_ascii_path/filename + """ + filename = binary_ascii_path / filename if not filename.exists(): pytest.skip('STL file does not exist') mesh = stl.StlMesh(str(filename), speedups=speedups) volume, cog, inertia = mesh.get_mass_properties() assert close([volume], [1.416599]) assert close(cog, [1.299040, 0.170197, 1.499999]) - assert close(inertia, [[+0.509549, +0.000000, -0.000000], - [+0.000000, +0.991236, +0.000000], - [-0.000000, +0.000000, +0.509550]]) + assert close( + inertia, + [ + [+0.509549, +0.000000, -0.000000], + [+0.000000, +0.991236, +0.000000], + [-0.000000, +0.000000, +0.509550], + ], + ) def test_mass_properties_for_half_donut_with_density( - binary_ascii_path, speedups): - ''' + binary_ascii_path, speedups +): + """ Checks the results of method get_mass_properties_with_density() on STL ASCII and binary files HalfDonut.stl One checks the results obtained with stl with the ones obtained with meshlab - ''' - filename = binary_ascii_path/'HalfDonut.stl' + """ + filename = binary_ascii_path / 'HalfDonut.stl' mesh = stl.StlMesh(str(filename), speedups=speedups) volume, mass, cog, inertia = mesh.get_mass_properties_with_density(1.23) @@ -84,6 +100,11 @@ def test_mass_properties_for_half_donut_with_density( print('inertia') numpy.set_printoptions(suppress=True) print(inertia) - assert close(inertia, [[+1.71022851, +0.00000001, -0.00000011], - [+0.00000001, +3.32226227, +0.00000002], - [-0.00000011, +0.00000002, +1.71022859]]) + assert close( + inertia, + [ + [+1.71022851, +0.00000001, -0.00000011], + [+0.00000001, +3.32226227, +0.00000002], + [-0.00000011, +0.00000002, +1.71022859], + ], + ) diff --git a/tests/test_multiple.py b/tests/test_multiple.py index 6991544..611b501 100644 --- a/tests/test_multiple.py +++ b/tests/test_multiple.py @@ -4,7 +4,7 @@ from stl import mesh -_STL_FILE = b''' +_STL_FILE = b""" solid test.stl facet normal -0.014565 0.073223 -0.002897 outer loop @@ -14,7 +14,7 @@ endloop endfacet endsolid test.stl -''' +""" def test_single_stl(tmpdir, speedups): @@ -35,9 +35,7 @@ def test_multiple_stl(tmpdir, speedups): fh.write(_STL_FILE) fh.seek(0) for i, m in enumerate( - mesh.Mesh.from_multi_file( - str(tmp_file), fh=fh, speedups=speedups - ) + mesh.Mesh.from_multi_file(str(tmp_file), fh=fh, speedups=speedups) ): assert m.name == b'test.stl' @@ -49,9 +47,7 @@ def test_single_stl_file(tmpdir, speedups): with tmp_file.open('wb+') as fh: fh.write(_STL_FILE) fh.seek(0) - for m in mesh.Mesh.from_multi_file( - str(tmp_file), speedups=speedups - ): + for m in mesh.Mesh.from_multi_file(str(tmp_file), speedups=speedups): pass @@ -63,9 +59,7 @@ def test_multiple_stl_file(tmpdir, speedups): fh.seek(0) for i, m in enumerate( - mesh.Mesh.from_multi_file( - str(tmp_file), speedups=speedups - ) + mesh.Mesh.from_multi_file(str(tmp_file), speedups=speedups) ): assert m.name == b'test.stl' diff --git a/tests/test_rotate.py b/tests/test_rotate.py index 5275fad..56ca3a7 100644 --- a/tests/test_rotate.py +++ b/tests/test_rotate.py @@ -12,32 +12,20 @@ def test_rotation(): data = numpy.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] = 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]]) # 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] = 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]]) # 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] = 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]]) mesh = Mesh(data, remove_empty_areas=False) # Since the cube faces are from 0 to 1 we can move it to the middle by # substracting .5 - data['vectors'] -= .5 + data['vectors'] -= 0.5 # Rotate 90 degrees over the X axis followed by the Y axis followed by the # X axis @@ -47,50 +35,50 @@ def test_rotation(): # Since the cube faces are from 0 to 1 we can move it to the middle by # substracting .5 - data['vectors'] += .5 + data['vectors'] += 0.5 # We use a slightly higher absolute tolerance here, for ppc64le # https://github.com/WoLpH/numpy-stl/issues/78 - assert numpy.allclose(mesh.vectors, numpy.array([ - [[1, 0, 0], [0, 1, 0], [0, 0, 0]], - [[0, 1, 0], [1, 0, 0], [1, 1, 0]], - [[0, 1, 1], [0, 1, 0], [1, 1, 1]], - [[1, 1, 0], [0, 1, 0], [1, 1, 1]], - [[0, 0, 1], [0, 1, 1], [0, 1, 0]], - [[0, 0, 1], [0, 0, 0], [0, 1, 0]], - ]), atol=1e-07) + assert numpy.allclose( + mesh.vectors, + numpy.array( + [ + [[1, 0, 0], [0, 1, 0], [0, 0, 0]], + [[0, 1, 0], [1, 0, 0], [1, 1, 0]], + [[0, 1, 1], [0, 1, 0], [1, 1, 1]], + [[1, 1, 0], [0, 1, 0], [1, 1, 1]], + [[0, 0, 1], [0, 1, 1], [0, 1, 0]], + [[0, 0, 1], [0, 0, 0], [0, 1, 0]], + ] + ), + atol=1e-07, + ) def test_rotation_over_point(): # Create a single face data = numpy.zeros(1, dtype=Mesh.dtype) - data['vectors'][0] = numpy.array([[1, 0, 0], - [0, 1, 0], - [0, 0, 1]]) + data['vectors'][0] = numpy.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., 4., 6.], - [0., 3., 6.], - [0., 4., 5.]]])) + numpy.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, numpy.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., -1., -0.], - [0., 0., -1.]]])) + numpy.array([[[1.0, 0.0, -0.0], [0.0, -1.0, -0.0], [0.0, 0.0, -1.0]]]), + ) with pytest.raises(TypeError): mesh.rotate([1, 0, 0], math.radians(180), point='x') @@ -100,9 +88,7 @@ def test_double_rotation(): # Create a single face data = numpy.zeros(1, dtype=Mesh.dtype) - data['vectors'][0] = numpy.array([[1, 0, 0], - [0, 1, 0], - [0, 0, 1]]) + data['vectors'][0] = numpy.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) mesh = Mesh(data, remove_empty_areas=False) @@ -112,100 +98,99 @@ def test_double_rotation(): mesh.rotate_using_matrix(combined_rotation_matrix) utils.array_equals( mesh.vectors, - numpy.array([[[1., 0., 0.], - [0., 1., 0.], - [0., 0., 1.]]])) + numpy.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['vectors'][0] = numpy.array([[0, 1, 1], - [1, 0, 1], - [0, 0, 1]]) + data['vectors'][0] = numpy.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 numpy.allclose( + mesh.vectors, numpy.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 numpy.allclose( + mesh.vectors, numpy.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['vectors'][0] = numpy.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 numpy.allclose( + mesh.vectors, numpy.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 numpy.allclose( + mesh.vectors, numpy.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['vectors'][0] = numpy.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 numpy.allclose( + mesh.vectors, numpy.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 numpy.allclose( + mesh.vectors, numpy.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['vectors'][0] = numpy.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 numpy.allclose( + mesh.vectors, numpy.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]]])) + assert numpy.allclose( + mesh.vectors, numpy.array([[[0, 1, 1], [1, 0, 1], [0, 0, 1]]]) + ) assert numpy.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['vectors'][0] = numpy.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 numpy.allclose( + mesh.vectors, numpy.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[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 numpy.allclose( + mesh.vectors, numpy.array([[[0, 2, 4], [1, 3, 4], [1, 2, 4]]]) + ) assert numpy.allclose(mesh.areas, 0.5) From e132950a64296c26e08f4af0569fbc47273e930f Mon Sep 17 00:00:00 2001 From: Jeff Olander <58099798+JeffreyOI@users.noreply.github.com> Date: Mon, 25 Nov 2024 07:27:59 -0500 Subject: [PATCH 4/6] Second pull request attempt (#227) * Update README.rst Added simple mesh (one triangle) to illustrate the structure of the mesh class. Some of this may belong in the src, but since everyone looks at the readme, I thought it helpful to do a little explanation here. * Update appveyor.yml Missing setuptools installation. Causing AV check to fail. * Update base.py * Fixing doctest * Further doctest fixes * Final doctest fixes (hopefully) * ascii test failure, attempting a fix * Indentation error * possible fp arithmetic errors, added tolerance to inertia check * my first edit failed, saw that tolerance was already set, increased 1e-6 -> 1e-5 --------- Co-authored-by: Rick van Hattem --- README.rst | 33 +++++++++++++++++++++++++++++++++ appveyor.yml | 2 +- stl/base.py | 14 +++++++------- tests/test_ascii.py | 9 +++++++++ tests/test_meshProperties.py | 2 +- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index c1573bc..a2fc7ac 100644 --- a/README.rst +++ b/README.rst @@ -300,6 +300,39 @@ Extending Mesh objects # Show the plot to the screen pyplot.show() +Creating a single triangle +---------------------------------- + +.. code-block:: python + + import numpy + from stl import mesh + + # A unit triangle + tri_vectors = [[0,0,0],[0,1,0],[0,0,1]] + + # Create the vector data. It’s a numpy structured array with N entries, where N is the number of triangles (here N=1), and each entry is in the format ('normals','vectors','attr') + data = numpy.array([( + 0, # Set 'normals' to zero, and the mesh class will automatically calculate them at initialization + tri_vectors, # 'vectors' + 0 # 'attr' + )], dtype = mesh.Mesh.dtype) # The structure defined by the mesh class (N x ('normals','vectors','attr')) + + # Create the mesh object from the structured array + tri_mesh = mesh.Mesh(data) + + # Optionally make a plot for fun + # Load the plot tools + from matplotlib import pyplot + from mpl_toolkits import mplot3d + + # Create a new plot + figure = pyplot.figure() + axes = figure.add_subplot(projection='3d') + + # Add mesh to plot + axes.add_collection3d(mplot3d.art3d.Poly3DCollection(tri_mesh.vectors)) # Just need the 'vectors' attribute for display + Creating Mesh objects from a list of vertices and faces ------------------------------------------------------------------------------ diff --git a/appveyor.yml b/appveyor.yml index 6827176..6586d95 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -28,7 +28,7 @@ install: build: false # Not a C# project, build stuff at the test step instead. before_test: - - py -m pip install tox numpy cython wheel + - py -m pip install tox numpy cython wheel setuptools test_script: - "py -m tox -e %TOXENV%" diff --git a/stl/base.py b/stl/base.py index f42da1d..33b792d 100644 --- a/stl/base.py +++ b/stl/base.py @@ -140,32 +140,32 @@ class BaseMesh(logger.Logged, abc.Mapping): >>> len(mesh) == len(list(mesh)) True - >>> (mesh.min_ < mesh.max_).all() + >>> bool((mesh.min_ < mesh.max_).all()) True >>> mesh.update_normals() - >>> mesh.units.sum() + >>> float(mesh.units.sum()) 0.0 >>> mesh.v0[:] = mesh.v1[:] = mesh.v2[:] = 0 - >>> mesh.points.sum() + >>> float(mesh.points.sum()) 0.0 >>> mesh.v0 = mesh.v1 = mesh.v2 = 0 >>> mesh.x = mesh.y = mesh.z = 0 >>> mesh.attr = 1 - >>> (mesh.attr == 1).all() + >>> bool((mesh.attr == 1).all()) True >>> mesh.normals = 2 - >>> (mesh.normals == 2).all() + >>> bool((mesh.normals == 2).all()) True >>> mesh.vectors = 3 - >>> (mesh.vectors == 3).all() + >>> bool((mesh.vectors == 3).all()) True >>> mesh.points = 4 - >>> (mesh.points == 4).all() + >>> bool((mesh.points == 4).all()) True """ diff --git a/tests/test_ascii.py b/tests/test_ascii.py index c5a695c..af1015a 100644 --- a/tests/test_ascii.py +++ b/tests/test_ascii.py @@ -195,6 +195,15 @@ def test_ascii_io(): # Assert binary file is still only ascii characters. fh.getvalue().decode('ascii') + import tempfile + + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + # Save the mesh to the temporary file + mesh_.save(temp_file.name, mode=Mode.ASCII) + + # Read the mesh back from the temporary file + read = mesh.Mesh.from_file(temp_file.name) + # 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. diff --git a/tests/test_meshProperties.py b/tests/test_meshProperties.py index c95707e..e5e9ba7 100644 --- a/tests/test_meshProperties.py +++ b/tests/test_meshProperties.py @@ -4,7 +4,7 @@ from stl import stl -tolerance = 1e-6 +tolerance = 1e-5 def close(a, b): From 4d244d6f2adade55d20e0bd023a94c70b9a5b9fd Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 25 Nov 2024 13:35:18 +0100 Subject: [PATCH 5/6] Testing both numpy 1 and 2. --- tox.ini | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 118c5a3..e841a32 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,13 @@ [tox] -envlist = ruff, black, pypy3, py39, py310, py311, py312, py313, docs, mypy, pyright +env_list = ruff, black, pypy3, py3{9,10,11,12,13}-numpy{1,2}, docs, mypy, pyright skip_missing_interpreters = True [testenv] -deps = -rtests/requirements.txt -commands = +deps = + numpy1: numpy==1.* + numpy2: numpy==2.* + -rtests/requirements.txt +commands = python -m pip install -U pip wheel setuptools python setup.py build_ext --inplace python -m pytest -vvv {posargs} @@ -18,10 +21,13 @@ basepython = [gh-actions] python = - 3.7: py37 - 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + pypy3: pypy3 + [testenv:flake8] basepython=python From c1ce8b5ada7d3980032909abe17d331b446fc39d Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 25 Nov 2024 15:01:41 +0100 Subject: [PATCH 6/6] Testing numpy versions separately --- .github/workflows/main.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7c04ba9..6f6be0d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,6 +12,7 @@ jobs: strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12'] + numpy-version: ['1', '2'] steps: - uses: actions/checkout@v3 @@ -23,5 +24,14 @@ jobs: run: | python -m pip install --upgrade pip pip install tox tox-gh-actions + - name: python version + env: + TOXENV: "py${{ matrix.python }}-numpy${{ matrix.numpy-version }}" + run: | + TOXENV=${{ env.TOXENV }} + TOXENV=${TOXENV//.} # replace all dots + echo TOXENV=${TOXENV} >> $GITHUB_ENV # update GitHub ENV vars + - name: print env + run: echo ${{ env.TOXENV }} - name: Test with tox run: tox