Repository: robbievanleeuwen/section-properties (language: python)

Issue:
Switch to GitHub Actions
### Discussed in https://github.com/robbievanleeuwen/section-properties/discussions/60

<div type='discussions-op-text'>

<sup>Originally posted by **Spectre5** February 12, 2021</sup>
@robbievanleeuwen what are you thoughts on switching from travis CI to the native github actions?  After we add some lint/code quality checks to the repository, I'll follow up with another PR to add those checks to the CI pipeline.  Travis is fine, I'm just more familiar with github actions.  I'm mostly curious if there is a specific reason you're using travis instead?</div>


Discussion / hints:
After a steep learning curve, I've implemented a full CI/CD workflow in GHA here: https://github.com/MonashSmartStructures/ospgrillage Some/most/all could be adopted here to get up and running quickly.

Fix patch:
diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml
new file mode 100644
index 00000000..ddb30a72
--- /dev/null
+++ b/.github/workflows/black.yml
@@ -0,0 +1,13 @@
+name: Lint with Black
+
+on: [push, pull_request]
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+
+      - uses: psf/black@stable
+        with:
+          options: "--check --verbose" 
diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml
new file mode 100644
index 00000000..161ed36b
--- /dev/null
+++ b/.github/workflows/code_coverage.yml
@@ -0,0 +1,38 @@
+name: Code Coverage
+
+on:
+  push:
+    branches: [ master, v2 ]
+  pull_request:
+    branches: [ master, v2 ]
+  workflow_dispatch:
+
+jobs:
+  tests:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [ '3.8' ]
+
+    name: Code coverage with python ${{ matrix.python-version }}
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: Setup python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Update pip
+      run: pip install --upgrade pip
+
+    - name: Install package
+      run: python -m pip install -e .
+
+    - name: Generate report
+      run: |
+        pip install pytest-cov
+        pytest --cov=./ --cov-report=xml
+
+    - name: Upload coverage to Codecov
+      uses: codecov/codecov-action@v2.1.0
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e9fc34d3..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-language: python
-
-before_install:
-  - pip install pybind11
-
-python:
-  - "3.8"
-
-install:
-  - pip install --upgrade pip
-  - python setup.py install
-
-script:
-  - python ./sectionproperties/tests/test_rectangle.py
-  - python ./sectionproperties/tests/test_validation.py
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 18e06834..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,1 +0,0 @@
-include CHANGELOG.rst
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..bfa8c68c
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,17 @@
+codecov:
+  require_ci_to_pass: yes
+
+coverage:
+  precision: 2
+  round: down
+  range: "70...100"
+
+parsers:
+  gcov:
+    branch_detection:
+      conditional: yes
+      loop: yes
+      method: no
+      macro: no
+
+comment: false
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 063deec2..a0747cd5 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -14,20 +14,22 @@
 
 import os
 import sys
-sys.path.insert(0, os.path.abspath('../../'))
+
+sys.path.insert(0, os.path.abspath("../../"))
 from sectionproperties import __version__ as ver
-autodoc_mock_imports = ['rhino_shapely_interop']
+
+autodoc_mock_imports = ["rhino_shapely_interop", "cad_to_shapely"]
 
 # -- Project information -----------------------------------------------------
 
-project = 'sectionproperties'
-copyright = '2021, Robbie van Leeuwen'
-author = 'Robbie van Leeuwen'
+project = "sectionproperties"
+copyright = "2021, Robbie van Leeuwen"
+author = "Robbie van Leeuwen"
 
 # The short Major.Minor.Build version
-_v = ver.split('.')
-_build = ''.join([c for c in _v[2] if c.isdigit()])
-version = _v[0] + '.' + _v[1] + '.' + _build
+_v = ver.split(".")
+_build = "".join([c for c in _v[2] if c.isdigit()])
+version = _v[0] + "." + _v[1] + "." + _build
 # The full version, including alpha/beta/rc tags
 release = ver
 
@@ -42,29 +44,29 @@
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx.ext.autodoc',
-    'sphinx.ext.mathjax',
-    'sphinx.ext.viewcode',
+    "sphinx.ext.autodoc",
+    "sphinx.ext.mathjax",
+    "sphinx.ext.viewcode",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
 #
 # source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 #
 # This is also used if you do content translation via gettext catalogs.
 # Usually you set "language" from the command line for these cases.
-language = 'en'
+language = "en"
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
@@ -72,7 +74,7 @@
 exclude_patterns = []
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 
 # -- Options for HTML output -------------------------------------------------
@@ -80,7 +82,7 @@
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = 'sphinx_rtd_theme'
+html_theme = "sphinx_rtd_theme"
 
 # 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
@@ -107,7 +109,7 @@
 # -- Options for HTMLHelp output ---------------------------------------------
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'sectionpropertiesdoc'
+htmlhelp_basename = "sectionpropertiesdoc"
 
 
 # -- Options for LaTeX output ------------------------------------------------
@@ -116,15 +118,12 @@
     # The paper size ('letterpaper' or 'a4paper').
     #
     # 'papersize': 'letterpaper',
-
     # The font size ('10pt', '11pt' or '12pt').
     #
     # 'pointsize': '10pt',
-
     # Additional stuff for the LaTeX preamble.
     #
     # 'preamble': '',
-
     # Latex figure (float) alignment
     #
     # 'figure_align': 'htbp',
@@ -134,8 +133,13 @@
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, 'sectionproperties.tex', 'sectionproperties Documentation',
-     'Robbie van Leeuwen', 'manual'),
+    (
+        master_doc,
+        "sectionproperties.tex",
+        "sectionproperties Documentation",
+        "Robbie van Leeuwen",
+        "manual",
+    ),
 ]
 
 
@@ -144,8 +148,7 @@
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    (master_doc, 'sectionproperties', 'sectionproperties Documentation',
-     [author], 1)
+    (master_doc, "sectionproperties", "sectionproperties Documentation", [author], 1)
 ]
 
 
@@ -155,9 +158,15 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (master_doc, 'sectionproperties', 'sectionproperties Documentation',
-     author, 'sectionproperties', 'One line description of project.',
-     'Miscellaneous'),
+    (
+        master_doc,
+        "sectionproperties",
+        "sectionproperties Documentation",
+        author,
+        "sectionproperties",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 
diff --git a/sectionproperties/analysis/cross_section.py b/sectionproperties/analysis/cross_section.py
index 2ab58bb9..d3854f81 100644
--- a/sectionproperties/analysis/cross_section.py
+++ b/sectionproperties/analysis/cross_section.py
@@ -235,7 +235,17 @@ def calculate_geom():
 
             # calculate global geometric properties
             for el in self.elements:
-                (area, qx, qy, ixx_g, iyy_g, ixy_g, e, g, rho) = el.geometric_properties()
+                (
+                    area,
+                    qx,
+                    qy,
+                    ixx_g,
+                    iyy_g,
+                    ixy_g,
+                    e,
+                    g,
+                    rho,
+                ) = el.geometric_properties()
                 self.section_props.area += area
                 self.section_props.mass += area * rho
                 self.section_props.ea += area * e
@@ -855,12 +865,16 @@ def calculate_stress(self, N=0, Vx=0, Vy=0, Mxx=0, Myy=0, M11=0, M22=0, Mzz=0):
         """
 
         # check that a geometric and warping analysis has been performed
-        if None in [
-            self.section_props.area,
-            self.section_props.ixx_c,
-            self.section_props.cx,
-            self.section_props.j,
-        ] and self.section_props.omega is None:
+        if (
+            None
+            in [
+                self.section_props.area,
+                self.section_props.ixx_c,
+                self.section_props.cx,
+                self.section_props.j,
+            ]
+            and self.section_props.omega is None
+        ):
             err = "Perform a geometric and warping analysis before carrying out a stress analysis."
             raise RuntimeError(err)
 
@@ -1056,7 +1070,16 @@ def assemble_torsion(self, lg=True):
 
         return (csc_matrix(k), csc_matrix(k_lg), f_torsion)
 
-    def plot_mesh(self, ax=None, pause=True, alpha=0.5, materials=True, mask=None, size=500, dpi=96):
+    def plot_mesh(
+        self,
+        ax=None,
+        pause=True,
+        alpha=0.5,
+        materials=True,
+        mask=None,
+        size=500,
+        dpi=96,
+    ):
         """Plots the finite element mesh. If no axes object is supplied a new figure and axis is
         created.
 
@@ -1142,9 +1165,7 @@ def plot_mesh(self, ax=None, pause=True, alpha=0.5, materials=True, mask=None, s
                 if i == 0 or material not in self.materials[0:i]:
                     # add the material colour and name to the legend list
                     patch = mpatches.Patch(color=material.color, label=material.name)
-                    legend_labels.append(
-                        patch
-                    )
+                    legend_labels.append(patch)
 
             cmap = ListedColormap(color_array)  # custom colormap
 
@@ -2325,7 +2346,9 @@ def __init__(self, cross_section):
         # can be saved to a new material group
         self.material_groups = copy.deepcopy(cross_section.material_groups)
 
-    def plot_stress_contour(self, sigs, title, pause, cmap, normalize=True, size=500, dpi=96):
+    def plot_stress_contour(
+        self, sigs, title, pause, cmap, normalize=True, size=500, dpi=96
+    ):
         """Plots filled stress contours over the finite element mesh.
 
         :param sigs: List of nodal stress values for each material
@@ -2400,7 +2423,9 @@ def plot_stress_contour(self, sigs, title, pause, cmap, normalize=True, size=500
 
         return (fig, ax)
 
-    def plot_stress_vector(self, sigxs, sigys, title, pause, cmap, normalize=False, size=500, dpi=96):
+    def plot_stress_vector(
+        self, sigxs, sigys, title, pause, cmap, normalize=False, size=500, dpi=96
+    ):
         """Plots stress vectors over the finite element mesh.
 
         :param sigxs: List of x-components of the nodal stress values for each material
@@ -2438,7 +2463,6 @@ def plot_stress_vector(self, sigxs, sigys, title, pause, cmap, normalize=False,
         if normalize:
             norm = CenteredNorm()
 
-
         # plot the vectors
         for (i, sigx) in enumerate(sigxs):
             sigy = sigys[i]
@@ -2634,7 +2658,9 @@ def get_stress(self):
 
         return stress
 
-    def plot_stress_n_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_n_zz(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the normal stress :math:`\sigma_{zz,N}` resulting from the
         axial load :math:`N`.
 
@@ -2678,9 +2704,13 @@ def plot_stress_n_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zz_n)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_mxx_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_mxx_zz(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the normal stress :math:`\sigma_{zz,Mxx}` resulting from the
         bending moment :math:`M_{xx}`.
 
@@ -2724,9 +2754,13 @@ def plot_stress_mxx_zz(self, pause=True, cmap="coolwarm", normalize=True, size=5
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zz_mxx)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_myy_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_myy_zz(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the normal stress :math:`\sigma_{zz,Myy}` resulting from the
         bending moment :math:`M_{yy}`.
 
@@ -2770,9 +2804,13 @@ def plot_stress_myy_zz(self, pause=True, cmap="coolwarm", normalize=True, size=5
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zz_myy)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_m11_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_m11_zz(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the normal stress :math:`\sigma_{zz,M11}` resulting from the
         bending moment :math:`M_{11}`.
 
@@ -2816,9 +2854,13 @@ def plot_stress_m11_zz(self, pause=True, cmap="coolwarm", normalize=True, size=5
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zz_m11)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_m22_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_m22_zz(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the normal stress :math:`\sigma_{zz,M22}` resulting from the
         bending moment :math:`M_{22}`.
 
@@ -2862,9 +2904,13 @@ def plot_stress_m22_zz(self, pause=True, cmap="coolwarm", normalize=True, size=5
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zz_m22)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_m_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_m_zz(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the normal stress :math:`\sigma_{zz,\Sigma M}` resulting from
         all bending moments :math:`M_{xx} + M_{yy} + M_{11} + M_{22}`.
 
@@ -2909,9 +2955,13 @@ def plot_stress_m_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zz_m)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_mzz_zx(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_mzz_zx(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *x*-component of the shear stress :math:`\sigma_{zx,Mzz}`
         resulting from the torsion moment :math:`M_{zz}`.
 
@@ -2955,9 +3005,13 @@ def plot_stress_mzz_zx(self, pause=True, cmap="coolwarm", normalize=True, size=5
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zx_mzz)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_mzz_zy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_mzz_zy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *y*-component of the shear stress :math:`\sigma_{zy,Mzz}`
         resulting from the torsion moment :math:`M_{zz}`.
 
@@ -3001,9 +3055,13 @@ def plot_stress_mzz_zy(self, pause=True, cmap="coolwarm", normalize=True, size=5
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zy_mzz)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_mzz_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_mzz_zxy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the resultant shear stress :math:`\sigma_{zxy,Mzz}` resulting
         from the torsion moment :math:`M_{zz}`.
 
@@ -3047,9 +3105,13 @@ def plot_stress_mzz_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zxy_mzz)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_vector_mzz_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96):
+    def plot_vector_mzz_zxy(
+        self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96
+    ):
         """Produces a vector plot of the resultant shear stress :math:`\sigma_{zxy,Mzz}` resulting
         from the torsion moment :math:`M_{zz}`.
 
@@ -3095,9 +3157,13 @@ def plot_vector_mzz_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=5
             sigxs.append(group.stress_result.sig_zx_mzz)
             sigys.append(group.stress_result.sig_zy_mzz)
 
-        return self.plot_stress_vector(sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_vector(
+            sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_vx_zx(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_vx_zx(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *x*-component of the shear stress :math:`\sigma_{zx,Vx}`
         resulting from the shear force :math:`V_{x}`.
 
@@ -3141,9 +3207,13 @@ def plot_stress_vx_zx(self, pause=True, cmap="coolwarm", normalize=True, size=50
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zx_vx)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_vx_zy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_vx_zy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *y*-component of the shear stress :math:`\sigma_{zy,Vx}`
         resulting from the shear force :math:`V_{x}`.
 
@@ -3187,9 +3257,13 @@ def plot_stress_vx_zy(self, pause=True, cmap="coolwarm", normalize=True, size=50
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zy_vx)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_vx_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_vx_zxy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the resultant shear stress :math:`\sigma_{zxy,Vx}` resulting
         from the shear force :math:`V_{x}`.
 
@@ -3233,9 +3307,13 @@ def plot_stress_vx_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=5
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zxy_vx)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_vector_vx_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96):
+    def plot_vector_vx_zxy(
+        self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96
+    ):
         """Produces a vector plot of the resultant shear stress :math:`\sigma_{zxy,Vx}` resulting
         from the shear force :math:`V_{x}`.
 
@@ -3281,9 +3359,13 @@ def plot_vector_vx_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=50
             sigxs.append(group.stress_result.sig_zx_vx)
             sigys.append(group.stress_result.sig_zy_vx)
 
-        return self.plot_stress_vector(sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_vector(
+            sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_vy_zx(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_vy_zx(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *x*-component of the shear stress :math:`\sigma_{zx,Vy}`
         resulting from the shear force :math:`V_{y}`.
 
@@ -3327,9 +3409,13 @@ def plot_stress_vy_zx(self, pause=True, cmap="coolwarm", normalize=True, size=50
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zx_vy)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_vy_zy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_vy_zy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *y*-component of the shear stress :math:`\sigma_{zy,Vy}`
         resulting from the shear force :math:`V_{y}`.
 
@@ -3373,9 +3459,13 @@ def plot_stress_vy_zy(self, pause=True, cmap="coolwarm", normalize=True, size=50
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zy_vy)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_vy_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_vy_zxy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the resultant shear stress :math:`\sigma_{zxy,Vy}` resulting
         from the shear force :math:`V_{y}`.
 
@@ -3419,9 +3509,13 @@ def plot_stress_vy_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=5
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zxy_vy)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_vector_vy_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96):
+    def plot_vector_vy_zxy(
+        self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96
+    ):
         """Produces a vector plot of the resultant shear stress :math:`\sigma_{zxy,Vy}` resulting
         from the shear force :math:`V_{y}`.
 
@@ -3467,9 +3561,13 @@ def plot_vector_vy_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=50
             sigxs.append(group.stress_result.sig_zx_vy)
             sigys.append(group.stress_result.sig_zy_vy)
 
-        return self.plot_stress_vector(sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_vector(
+            sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_v_zx(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_v_zx(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *x*-component of the shear stress
         :math:`\sigma_{zx,\Sigma V}` resulting from the sum of the applied shear forces
         :math:`V_{x} + V_{y}`.
@@ -3515,9 +3613,13 @@ def plot_stress_v_zx(self, pause=True, cmap="coolwarm", normalize=True, size=500
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zx_v)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_v_zy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_v_zy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *y*-component of the shear stress
         :math:`\sigma_{zy,\Sigma V}` resulting from the sum of the applied shear forces
         :math:`V_{x} + V_{y}`.
@@ -3563,9 +3665,13 @@ def plot_stress_v_zy(self, pause=True, cmap="coolwarm", normalize=True, size=500
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zy_v)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_v_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_v_zxy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the resultant shear stress
         :math:`\sigma_{zxy,\Sigma V}` resulting from the sum of the applied shear forces
         :math:`V_{x} + V_{y}`.
@@ -3611,9 +3717,13 @@ def plot_stress_v_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=50
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zxy_v)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_vector_v_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96):
+    def plot_vector_v_zxy(
+        self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96
+    ):
         """Produces a vector plot of the resultant shear stress
         :math:`\sigma_{zxy,\Sigma V}` resulting from the sum of the  applied shear forces
         :math:`V_{x} + V_{y}`.
@@ -3661,9 +3771,13 @@ def plot_vector_v_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=500
             sigxs.append(group.stress_result.sig_zx_v)
             sigys.append(group.stress_result.sig_zy_v)
 
-        return self.plot_stress_vector(sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_vector(
+            sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_zz(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the combined normal stress :math:`\sigma_{zz}` resulting from
         all actions.
 
@@ -3708,9 +3822,13 @@ def plot_stress_zz(self, pause=True, cmap="coolwarm", normalize=True, size=500,
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zz)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_zx(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_zx(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *x*-component of the shear stress :math:`\sigma_{zx}`
         resulting from all actions.
 
@@ -3755,9 +3873,13 @@ def plot_stress_zx(self, pause=True, cmap="coolwarm", normalize=True, size=500,
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zx)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_zy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_zy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the *y*-component of the shear stress :math:`\sigma_{zy}`
         resulting from all actions.
 
@@ -3802,9 +3924,13 @@ def plot_stress_zy(self, pause=True, cmap="coolwarm", normalize=True, size=500,
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zy)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_zxy(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the resultant shear stress :math:`\sigma_{zxy}` resulting
         from all actions.
 
@@ -3849,9 +3975,13 @@ def plot_stress_zxy(self, pause=True, cmap="coolwarm", normalize=True, size=500,
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_zxy)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_vector_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96):
+    def plot_vector_zxy(
+        self, pause=True, cmap="YlOrBr", normalize=False, size=500, dpi=96
+    ):
         """Produces a vector plot of the resultant shear stress :math:`\sigma_{zxy}` resulting
         from all actions.
 
@@ -3898,9 +4028,13 @@ def plot_vector_zxy(self, pause=True, cmap="YlOrBr", normalize=False, size=500,
             sigxs.append(group.stress_result.sig_zx)
             sigys.append(group.stress_result.sig_zy)
 
-        return self.plot_stress_vector(sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_vector(
+            sigxs, sigys, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_1(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_1(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the major principal stress :math:`\sigma_{1}` resulting from all
         actions.
 
@@ -3944,15 +4078,19 @@ def plot_stress_1(self, pause=True, cmap="coolwarm", normalize=True, size=500, d
             Contour plot of the major principal stress.
         """
 
-        title = 'Stress Contour Plot - $\sigma_{1}$'
+        title = "Stress Contour Plot - $\sigma_{1}$"
         sigs = []
 
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_1)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_3(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_3(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the Minor principal stress :math:`\sigma_{3}` resulting from all
         actions.
 
@@ -3996,15 +4134,19 @@ def plot_stress_3(self, pause=True, cmap="coolwarm", normalize=True, size=500, d
             Contour plot of the minor principal stress.
         """
 
-        title = 'Stress Contour Plot - $\sigma_{3}$'
+        title = "Stress Contour Plot - $\sigma_{3}$"
         sigs = []
 
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_3)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
-    def plot_stress_vm(self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96):
+    def plot_stress_vm(
+        self, pause=True, cmap="coolwarm", normalize=True, size=500, dpi=96
+    ):
         """Produces a contour plot of the von Mises stress :math:`\sigma_{vM}` resulting from all
         actions.
 
@@ -4059,8 +4201,9 @@ def plot_stress_vm(self, pause=True, cmap="coolwarm", normalize=True, size=500,
         for group in self.material_groups:
             sigs.append(group.stress_result.sig_vm)
 
-        return self.plot_stress_contour(sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi)
-
+        return self.plot_stress_contour(
+            sigs, title, pause, cmap, normalize=normalize, size=size, dpi=dpi
+        )
 
     def plot_mohrs_circles(self, x, y, pause=True, cmap="coolwarm", size=500, dpi=96):
         """Plots Mohr's Circles of the 3D stress state at position x,y
@@ -4110,7 +4253,7 @@ def plot_mohrs_circles(self, x, y, pause=True, cmap="coolwarm", size=500, dpi=96
 
         """
 
-        pt = (x,y)
+        pt = (x, y)
         nodes = self.cross_section.mesh_nodes
         ele = self.cross_section.mesh_elements
         triang = tri.Triangulation(nodes[:, 0], nodes[:, 1], ele[:, 0:3])
@@ -4148,50 +4291,76 @@ def plot_mohrs_circles(self, x, y, pause=True, cmap="coolwarm", size=500, dpi=96
         sigma_xx = 0
         sigma_yy = 0
         tau_xy = 0
-        sigma = np.array([[sigma_xx, tau_xy, tau_xz],
-                          [tau_xy, sigma_yy, tau_yz],
-                          [tau_xz, tau_yz, sigma_zz]])
+        sigma = np.array(
+            [
+                [sigma_xx, tau_xy, tau_xz],
+                [tau_xy, sigma_yy, tau_yz],
+                [tau_xz, tau_yz, sigma_zz],
+            ]
+        )
 
         # Solve for the principal stresses using the general approach
         s, n = np.linalg.eig(sigma)
         sigma_3, sigma_2, sigma_1 = np.sort(s)
         s_l = s.tolist()
-        sigma_1i, sigma_2i, sigma_3i = s_l.index(sigma_1), s_l.index(sigma_2), s_l.index(sigma_3)
-        n1, n2, n3 = n[:,sigma_1i], n[:,sigma_2i], n[:,sigma_3i]
+        sigma_1i, sigma_2i, sigma_3i = (
+            s_l.index(sigma_1),
+            s_l.index(sigma_2),
+            s_l.index(sigma_3),
+        )
+        n1, n2, n3 = n[:, sigma_1i], n[:, sigma_2i], n[:, sigma_3i]
 
         # The tractions on each plane in cartesian coords wrt principal axes
         n_inv = np.linalg.inv(n)
         tractions = []
         for col in range(3):
-            ss = n_inv[:,col].T @ np.diag(s) @ n_inv[:,col]
-            ts = np.sqrt(np.linalg.norm(np.diag(s) @ n_inv[:,col])**2 - ss**2)
-            tractions.append((ss,ts))
+            ss = n_inv[:, col].T @ np.diag(s) @ n_inv[:, col]
+            ts = np.sqrt(np.linalg.norm(np.diag(s) @ n_inv[:, col]) ** 2 - ss ** 2)
+            tractions.append((ss, ts))
 
-        def plot_circle(ax,c,R,col,label=None,fill=None):
+        def plot_circle(ax, c, R, col, label=None, fill=None):
             circ = plt.Circle(c, R, fill=fill, ec=col, label=label)
             ax.add_patch(circ)
             ax.set_aspect(1)
             ax.autoscale_view()
 
         fig, ax = plt.subplots()
-        plot_circle(ax, (0.5*(sigma_2+sigma_3),0), 0.5*(sigma_2-sigma_3),'r', r"C1: ($\sigma_2$, $\sigma_3$)")
-        plot_circle(ax, (0.5*(sigma_1+sigma_3),0), 0.5*(sigma_1-sigma_3),'b', r"C2: ($\sigma_1$, $\sigma_3$)")
-        plot_circle(ax, (0.5*(sigma_1+sigma_2),0), 0.5*(sigma_1-sigma_2),'k', r"C3: ($\sigma_1$, $\sigma_2$)")
-        for i,plane,col in zip(range(3),['X','Y','Z'],['r','b','k']):
-            ax.plot(*tractions[i],f'{col}.', label=rf"{plane}-face")
+        plot_circle(
+            ax,
+            (0.5 * (sigma_2 + sigma_3), 0),
+            0.5 * (sigma_2 - sigma_3),
+            "r",
+            r"C1: ($\sigma_2$, $\sigma_3$)",
+        )
+        plot_circle(
+            ax,
+            (0.5 * (sigma_1 + sigma_3), 0),
+            0.5 * (sigma_1 - sigma_3),
+            "b",
+            r"C2: ($\sigma_1$, $\sigma_3$)",
+        )
+        plot_circle(
+            ax,
+            (0.5 * (sigma_1 + sigma_2), 0),
+            0.5 * (sigma_1 - sigma_2),
+            "k",
+            r"C3: ($\sigma_1$, $\sigma_2$)",
+        )
+        for i, plane, col in zip(range(3), ["X", "Y", "Z"], ["r", "b", "k"]):
+            ax.plot(*tractions[i], f"{col}.", label=rf"{plane}-face")
 
         ax.set_axisbelow(True)
-        ax.grid(which='both')
-        ax.legend(loc='upper left', bbox_to_anchor=(1.0, 1.0))
+        ax.grid(which="both")
+        ax.legend(loc="upper left", bbox_to_anchor=(1.0, 1.0))
 
-        ax.spines['top'].set_visible(False)
-        ax.spines['right'].set_visible(False)
+        ax.spines["top"].set_visible(False)
+        ax.spines["right"].set_visible(False)
 
-        ax.set_ylabel(r'Shear stress $\tau$ (MPa)')
-        ax.set_xlabel(r'Direct stress $\sigma$ (MPa)')
+        ax.set_ylabel(r"Shear stress $\tau$ (MPa)")
+        ax.set_xlabel(r"Direct stress $\sigma$ (MPa)")
 
-        ax.xaxis.set_tick_params(bottom=True, top=False, direction='inout')
-        ax.yaxis.set_tick_params(left=True, right=False, direction='inout')
+        ax.xaxis.set_tick_params(bottom=True, top=False, direction="inout")
+        ax.yaxis.set_tick_params(left=True, right=False, direction="inout")
 
         #
         # The following is just to get the labels positioned outside the axes
@@ -4200,14 +4369,20 @@ def plot_circle(ax,c,R,col,label=None,fill=None):
         x_lbl_pos = ax.xaxis.label.get_position()
         y_lbl_pos = ax.yaxis.label.get_position()
         # Make spines pass through zero of the other axis
-        ax.spines['bottom'].set_position('zero')
-        ax.spines['left'].set_position('zero')
+        ax.spines["bottom"].set_position("zero")
+        ax.spines["left"].set_position("zero")
         # Now set the coords
         ax.xaxis.set_label_coords(*x_lbl_pos)
         ax.yaxis.set_label_coords(*y_lbl_pos)
 
         fig.subplots_adjust(right=0.8)
-        post.finish_plot(ax, pause, f"Mohr's Circles for 3D Stress State at {(*pt,)}", size=size, dpi=dpi)
+        post.finish_plot(
+            ax,
+            pause,
+            f"Mohr's Circles for 3D Stress State at {(*pt,)}",
+            size=size,
+            dpi=dpi,
+        )
         return (fig, ax)
 
 
@@ -4375,8 +4550,12 @@ def calculate_combined_stresses(self):
         self.sig_zx = self.sig_zx_mzz + self.sig_zx_v
         self.sig_zy = self.sig_zy_mzz + self.sig_zy_v
         self.sig_zxy = (self.sig_zx ** 2 + self.sig_zy ** 2) ** 0.5
-        self.sig_1 = self.sig_zz / 2 + np.sqrt( (self.sig_zz / 2) ** 2 + self.sig_zxy ** 2)
-        self.sig_3 = self.sig_zz / 2 - np.sqrt( (self.sig_zz / 2) ** 2 + self.sig_zxy ** 2)
+        self.sig_1 = self.sig_zz / 2 + np.sqrt(
+            (self.sig_zz / 2) ** 2 + self.sig_zxy ** 2
+        )
+        self.sig_3 = self.sig_zz / 2 - np.sqrt(
+            (self.sig_zz / 2) ** 2 + self.sig_zxy ** 2
+        )
         self.sig_vm = (self.sig_zz ** 2 + 3 * self.sig_zxy ** 2) ** 0.5
 
 
diff --git a/sectionproperties/analysis/fea.py b/sectionproperties/analysis/fea.py
index 9dd75bf5..b02baf9d 100644
--- a/sectionproperties/analysis/fea.py
+++ b/sectionproperties/analysis/fea.py
@@ -88,7 +88,7 @@ def geometric_properties(self):
             ixy,
             self.material.elastic_modulus,
             self.material.shear_modulus,
-            self.material.density
+            self.material.density,
         )
 
     def torsion_properties(self):
diff --git a/sectionproperties/post/post.py b/sectionproperties/post/post.py
index 774af04f..56fd2fdb 100644
--- a/sectionproperties/post/post.py
+++ b/sectionproperties/post/post.py
@@ -28,8 +28,8 @@ def finish_plot(ax, pause, title="", size=500, dpi=96):
     :param str title: Plot title
     """
     ax.set_aspect("equal", anchor="C")
-    ax.set_title(title, fontdict={"fontsize": 4*size/dpi})
-    ax.tick_params(labelsize=2.5*size/dpi)
+    ax.set_title(title, fontdict={"fontsize": 4 * size / dpi})
+    ax.tick_params(labelsize=2.5 * size / dpi)
     plt.tight_layout()
 
     fig = plt.gcf()
diff --git a/sectionproperties/pre/bisect_section.py b/sectionproperties/pre/bisect_section.py
index b63b0de1..a221230e 100644
--- a/sectionproperties/pre/bisect_section.py
+++ b/sectionproperties/pre/bisect_section.py
@@ -35,7 +35,8 @@ def create_line_segment(
 
 
 def group_top_and_bottom_polys(
-    polys: shapely.geometry.GeometryCollection, line: shapely.geometry.LineString,
+    polys: shapely.geometry.GeometryCollection,
+    line: shapely.geometry.LineString,
 ) -> Tuple[list, list]:
     """
     Returns tuple of two lists representing the list of Polygons in 'polys' on the "top" side of 'line' and the
@@ -66,7 +67,9 @@ def group_top_and_bottom_polys(
     return top_acc, bot_acc
 
 
-def line_mx_plus_b(line: shapely.geometry.LineString,) -> Tuple[float, float]:
+def line_mx_plus_b(
+    line: shapely.geometry.LineString,
+) -> Tuple[float, float]:
     """
     Returns a tuple representing the values of "m" and "b" from the definition of 'line' as "y = mx + b".
     """
@@ -84,7 +87,8 @@ def line_mx_plus_b(line: shapely.geometry.LineString,) -> Tuple[float, float]:
 
 
 def perp_mx_plus_b(
-    m_slope: float, point_on_line: Tuple[float, float],
+    m_slope: float,
+    point_on_line: Tuple[float, float],
 ) -> Tuple[float, float]:
     """
     Returns a tuple representing the values of "m" and "b" from
@@ -98,7 +102,10 @@ def perp_mx_plus_b(
 
 
 def line_intersection(
-    m_1: float, b_1: float, m_2: float, b_2: float,
+    m_1: float,
+    b_1: float,
+    m_2: float,
+    b_2: float,
 ) -> Optional[float]:
     """
     Returns a float representing the x-ordinate of the intersection
@@ -113,7 +120,9 @@ def line_intersection(
     return x
 
 
-def sum_poly_areas(lop: List[shapely.geometry.Polygon],) -> float:
+def sum_poly_areas(
+    lop: List[shapely.geometry.Polygon],
+) -> float:
     """
     Returns a float representing the total area of all polygons
     in 'lop', the list of polygons.
diff --git a/sectionproperties/pre/dxf.py b/sectionproperties/pre/dxf.py
index 9b7a4502..50df83a1 100644
--- a/sectionproperties/pre/dxf.py
+++ b/sectionproperties/pre/dxf.py
@@ -6,9 +6,9 @@
 
 def load_dxf(dxf_filepath: pathlib.Path):
     """
-            Import any-old-shape in dxf format for analysis.
-            Code by aegis1980 and connorferster
-        """
+    Import any-old-shape in dxf format for analysis.
+    Code by aegis1980 and connorferster
+    """
     if not dxf_filepath.exists():
         raise ValueError(f"The filepath does not exist: {dxf_filepath}")
 
diff --git a/sectionproperties/pre/nastran_sections.py b/sectionproperties/pre/nastran_sections.py
index d618aa5a..023e362a 100644
--- a/sectionproperties/pre/nastran_sections.py
+++ b/sectionproperties/pre/nastran_sections.py
@@ -5,7 +5,9 @@
 from sectionproperties.pre.sections import draw_radius
 
 
-def nastran_bar(DIM1: float, DIM2: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_bar(
+    DIM1: float, DIM2: float, material: pre.Material = pre.DEFAULT_MATERIAL
+) -> Geometry:
     """Constructs a BAR section with the center at the origin *(0, 0)*, with two parameters
     defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ [5]_ for definition of
     parameters. Added by JohnDN90.
@@ -49,8 +51,14 @@ def nastran_bar(DIM1: float, DIM2: float, material: pre.Material = pre.DEFAULT_M
     return geometry
 
 
-def nastran_box(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
-    """ Constructs a BOX section with the center at the origin *(0, 0)*, with four parameters
+def nastran_box(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
+    """Constructs a BOX section with the center at the origin *(0, 0)*, with four parameters
     defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ [5]_ for definition of
     parameters. Added by JohnDN90.
 
@@ -104,7 +112,13 @@ def nastran_box(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pr
 
 
 def nastran_box1(
-    DIM1: float, DIM2: float, DIM3: float, DIM4: float, DIM5: float, DIM6: float, material: pre.Material = pre.DEFAULT_MATERIAL
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    DIM5: float,
+    DIM6: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
 ) -> Geometry:
     """Constructs a BOX1 section with the center at the origin *(0, 0)*, with six parameters
     defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more details. Added by
@@ -166,8 +180,14 @@ def nastran_box1(
     return geometry
 
 
-def nastran_chan(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
-    """ Constructs a CHAN (C-Channel) section with the web's middle center at the origin *(0, 0)*,
+def nastran_chan(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
+    """Constructs a CHAN (C-Channel) section with the web's middle center at the origin *(0, 0)*,
     with four parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for
     more details. Added by JohnDN90.
 
@@ -224,8 +244,14 @@ def nastran_chan(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: p
     return geometry
 
 
-def nastran_chan1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
-    """ Constructs a CHAN1 (C-Channel) section with the web's middle center at the origin *(0, 0)*,
+def nastran_chan1(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
+    """Constructs a CHAN1 (C-Channel) section with the web's middle center at the origin *(0, 0)*,
     with four parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for
     more details. Added by JohnDN90.
 
@@ -280,8 +306,14 @@ def nastran_chan1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material:
     return geometry
 
 
-def nastran_chan2(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
-    """ Constructs a CHAN2 (C-Channel) section with the bottom web's middle center at the origin
+def nastran_chan2(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
+    """Constructs a CHAN2 (C-Channel) section with the bottom web's middle center at the origin
     *(0, 0)*, with four parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_
     [4]_ for more details. Added by JohnDN90.
 
@@ -336,8 +368,14 @@ def nastran_chan2(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material:
     return geometry
 
 
-def nastran_cross(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
-    """ Constructs Nastran's cruciform/cross section with the intersection's middle center at the
+def nastran_cross(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
+    """Constructs Nastran's cruciform/cross section with the intersection's middle center at the
     origin *(0, 0)*, with four parameters defining dimensions. See Nastran documentation [1]_ [2]_
     [3]_ [4]_ for more details. Added by JohnDN90.
 
@@ -405,9 +443,9 @@ def nastran_fcross(
     DIM6: float,
     DIM7: float,
     DIM8: float,
-    material: pre.Material = pre.DEFAULT_MATERIAL
+    material: pre.Material = pre.DEFAULT_MATERIAL,
 ) -> Geometry:
-    """ Constructs a flanged cruciform/cross section with the intersection's middle center at the
+    """Constructs a flanged cruciform/cross section with the intersection's middle center at the
     origin *(0, 0)*, with eight parameters defining dimensions. Added by JohnDN90.
 
     :param float DIM1: Depth (y) of flanged cruciform
@@ -502,9 +540,9 @@ def nastran_dbox(
     DIM8: float,
     DIM9: float,
     DIM10: float,
-    material: pre.Material = pre.DEFAULT_MATERIAL
+    material: pre.Material = pre.DEFAULT_MATERIAL,
 ) -> Geometry:
-    """ Constructs a DBOX section with the center at the origin *(0, 0)*, with ten parameters
+    """Constructs a DBOX section with the center at the origin *(0, 0)*, with ten parameters
     defining dimensions. See MSC Nastran documentation [1]_ for more details. Added by JohnDN90.
 
     :param float DIM1: Width (x) of the DBOX-section
@@ -581,9 +619,15 @@ def nastran_dbox(
 
 
 def nastran_gbox(
-    DIM1: float, DIM2: float, DIM3: float, DIM4: float, DIM5: float, DIM6: float, material: pre.Material = pre.DEFAULT_MATERIAL
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    DIM5: float,
+    DIM6: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
 ) -> Geometry:
-    """ Constructs a GBOX section with the center at the origin *(0, 0)*, with six parameters
+    """Constructs a GBOX section with the center at the origin *(0, 0)*, with six parameters
     defining dimensions. See ASTROS documentation [5]_ for more details. Added by JohnDN90.
 
     :param float DIM1: Width (x) of the GBOX-section
@@ -653,7 +697,13 @@ def nastran_gbox(
     return geometry
 
 
-def nastran_h(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_h(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
     """Constructs a H section with the middle web's middle center at the origin *(0, 0)*, with four
     parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more details.
     Added by JohnDN90.
@@ -714,7 +764,13 @@ def nastran_h(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.
     return geometry
 
 
-def nastran_hat(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_hat(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
     """Constructs a Hat section with the top most section's middle center at the origin *(0, 0)*,
     with four parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for
     more details. Note that HAT in ASTROS is actually HAT1 in this code. Added by JohnDN90.
@@ -774,8 +830,15 @@ def nastran_hat(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pr
     return geometry
 
 
-def nastran_hat1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, DIM5: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
-    """ Constructs a HAT1 section with the bottom plate's bottom center at the origin *(0, 0)*,
+def nastran_hat1(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    DIM5: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
+    """Constructs a HAT1 section with the bottom plate's bottom center at the origin *(0, 0)*,
     with five parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [5]_ for
     definition of parameters. Note that in ASTROS, HAT1 is called HAT. Added by JohnDN90.
 
@@ -821,7 +884,9 @@ def nastran_hat1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, DIM5: float
 
     hat = nastran_hat(DIM1=d1, DIM2=d2, DIM3=d3, DIM4=d4)
     # Merge the two sections into one geometry
-    geometry = hat.align_center(bottom_plate).align_to(bottom_plate, on="top") + bottom_plate
+    geometry = (
+        hat.align_center(bottom_plate).align_to(bottom_plate, on="top") + bottom_plate
+    )
 
     C = (-0.5 * DIM1, 0)
     D = (0.5 * DIM1, 0)
@@ -834,8 +899,10 @@ def nastran_hat1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, DIM5: float
     return geometry
 
 
-def nastran_hexa(DIM1: float, DIM2: float, DIM3: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
-    """ Constructs a HEXA (hexagon) section with the center at the origin *(0, 0)*, with three
+def nastran_hexa(
+    DIM1: float, DIM2: float, DIM3: float, material: pre.Material = pre.DEFAULT_MATERIAL
+) -> Geometry:
+    """Constructs a HEXA (hexagon) section with the center at the origin *(0, 0)*, with three
     parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more details.
     Added by JohnDN90.
 
@@ -888,7 +955,13 @@ def nastran_hexa(DIM1: float, DIM2: float, DIM3: float, material: pre.Material =
 
 
 def nastran_i(
-    DIM1: float, DIM2: float, DIM3: float, DIM4: float, DIM5: float, DIM6: float, material: pre.Material = pre.DEFAULT_MATERIAL
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    DIM5: float,
+    DIM6: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
 ) -> Geometry:
     """Constructs Nastran's I section with the bottom flange's middle center at the origin
     *(0, 0)*, with six parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_
@@ -957,7 +1030,13 @@ def nastran_i(
     return geometry
 
 
-def nastran_i1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_i1(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
     """Constructs a I1 section with the web's middle center at the origin *(0, 0)*, with four
     parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more details.
     Added by JohnDN90.
@@ -1019,7 +1098,13 @@ def nastran_i1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre
     return geometry
 
 
-def nastran_l(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_l(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
     """Constructs a L section with the intersection's center at the origin *(0, 0)*, with four
     parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ for more details.
     Added by JohnDN90.
@@ -1068,7 +1153,9 @@ def nastran_l(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.
     return geometry
 
 
-def nastran_rod(DIM1: float, n: int, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_rod(
+    DIM1: float, n: int, material: pre.Material = pre.DEFAULT_MATERIAL
+) -> Geometry:
     """Constructs a circular rod section with the center at the origin *(0, 0)*, with one parameter
     defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more details. Added by
     JohnDN90.
@@ -1123,7 +1210,13 @@ def nastran_rod(DIM1: float, n: int, material: pre.Material = pre.DEFAULT_MATERI
     return geometry
 
 
-def nastran_tee(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_tee(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
     """Constructs a T section with the top flange's middle center at the origin *(0, 0)*, with four
     parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ [5]_ for more
     details. Added by JohnDN90.
@@ -1196,7 +1289,13 @@ def nastran_tee(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pr
     return geometry
 
 
-def nastran_tee1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_tee1(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
     """Constructs a T1 section with the right flange's middle center at the origin *(0, 0)*, with
     four parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more
     details. Added by JohnDN90.
@@ -1254,7 +1353,13 @@ def nastran_tee1(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: p
     return geometry
 
 
-def nastran_tee2(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_tee2(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
     """Constructs a T2 section with the bottom flange's middle center at the origin *(0, 0)*, with
     four parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more
     details. Added by JohnDN90.
@@ -1312,7 +1417,9 @@ def nastran_tee2(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: p
     return geometry
 
 
-def nastran_tube(DIM1: float, DIM2: float, n: int, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_tube(
+    DIM1: float, DIM2: float, n: int, material: pre.Material = pre.DEFAULT_MATERIAL
+) -> Geometry:
     """Constructs a circular tube section with the center at the origin *(0, 0)*, with two
     parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more
     details. Added by JohnDN90.
@@ -1378,7 +1485,9 @@ def nastran_tube(DIM1: float, DIM2: float, n: int, material: pre.Material = pre.
     return geometry
 
 
-def nastran_tube2(DIM1: float, DIM2: float, n: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_tube2(
+    DIM1: float, DIM2: float, n: float, material: pre.Material = pre.DEFAULT_MATERIAL
+) -> Geometry:
     """Constructs a circular TUBE2 section with the center at the origin *(0, 0)*, with two
     parameters defining dimensions. See MSC Nastran documentation [1]_ for more details. Added by
     JohnDN90.
@@ -1445,7 +1554,13 @@ def nastran_tube2(DIM1: float, DIM2: float, n: float, material: pre.Material = p
         return geometry
 
 
-def nastran_zed(DIM1: float, DIM2: float, DIM3: float, DIM4: float, material: pre.Material = pre.DEFAULT_MATERIAL) -> Geometry:
+def nastran_zed(
+    DIM1: float,
+    DIM2: float,
+    DIM3: float,
+    DIM4: float,
+    material: pre.Material = pre.DEFAULT_MATERIAL,
+) -> Geometry:
     """Constructs a Z section with the web's middle center at the origin *(0, 0)*, with four
     parameters defining dimensions. See Nastran documentation [1]_ [2]_ [3]_ [4]_ for more details.
     Added by JohnDN90.
diff --git a/sectionproperties/pre/pre.py b/sectionproperties/pre/pre.py
index 740a6092..aca63a7a 100644
--- a/sectionproperties/pre/pre.py
+++ b/sectionproperties/pre/pre.py
@@ -114,6 +114,6 @@ def create_mesh(
     tri["regions"] = regions  # set regions
 
     # generate mesh
-    mesh = triangle.triangulate(tri, 'pq30Aao2')
+    mesh = triangle.triangulate(tri, "pq30Aao2")
 
     return mesh
diff --git a/sectionproperties/pre/rhino.py b/sectionproperties/pre/rhino.py
index 5c472e4d..a1043d83 100644
--- a/sectionproperties/pre/rhino.py
+++ b/sectionproperties/pre/rhino.py
@@ -3,19 +3,20 @@
 from shapely.geometry.polygon import Polygon
 from rhino_shapely_interop.importers import RhImporter
 
+
 def load_3dm(r3dm_filepath: Union[pathlib.Path, str], **kwargs) -> List[Polygon]:
     """Load a Rhino `.3dm` file and import the single surface planer breps.
 
-    :param r3dm_filepath: 
+    :param r3dm_filepath:
         File path to the rhino `.3dm` file.
     :type r3dm_filepath: pathlib.Path or string
     :param \**kwargs:
         See below.
-    :raises RuntimeError: 
-        A RuntimeError is raised if no polygons are found in the file. 
-        This is dependent on the keyword arguments. 
-        Try adjusting the keyword arguments if this error is raised. 
-    :return: 
+    :raises RuntimeError:
+        A RuntimeError is raised if no polygons are found in the file.
+        This is dependent on the keyword arguments.
+        Try adjusting the keyword arguments if this error is raised.
+    :return:
         List of Polygons found in the file.
     :rtype: List[shapely.geometry.Polygon]
 
@@ -23,7 +24,7 @@ def load_3dm(r3dm_filepath: Union[pathlib.Path, str], **kwargs) -> List[Polygon]
         * *refine_num* (``int, optional``) --
             Bézier curve interpolation number. In Rhino a surface's edges are nurb based curves.
             Shapely does not support nurbs, so the individual Bézier curves are interpolated using straight lines.
-            This parameter sets the number of straight lines used in the interpolation. 
+            This parameter sets the number of straight lines used in the interpolation.
             Default is 1.
         * *vec1* (``numpy.ndarray, optional``) --
             A 3d vector in the Shapely plane. Rhino is a 3D geometry environment.
@@ -48,27 +49,28 @@ def load_3dm(r3dm_filepath: Union[pathlib.Path, str], **kwargs) -> List[Polygon]
     """
     rhi = RhImporter.from_file(str(r3dm_filepath))
     list_polygons = list(rhi.get_planer_brep(**kwargs))
-    if len(list_polygons)==0:
+    if len(list_polygons) == 0:
         raise RuntimeError(
             f"No shapely.Polygon objects found. "
             f"Consider adjusting the keyword arguments. "
             f"File name: {r3dm_filepath}. "
-            )
+        )
     return list_polygons
 
+
 def load_brep_encoding(brep: str, **kwargs) -> Polygon:
     """Load an encoded single surface planer brep.
 
-    :param brep: 
+    :param brep:
         Rhino3dm.Brep encoded as a string.
     :type brep: str
     :param \**kwargs:
         See below.
-    :raises RuntimeError: 
-        A RuntimeError is raised if no polygons are found in the encoding. 
-        This is dependent on the keyword arguments. 
-        Try adjusting the keyword arguments if this error is raised. 
-    :return: 
+    :raises RuntimeError:
+        A RuntimeError is raised if no polygons are found in the encoding.
+        This is dependent on the keyword arguments.
+        Try adjusting the keyword arguments if this error is raised.
+    :return:
         The Polygons found in the encoding string.
     :rtype: shapely.geometry.Polygon
 
@@ -76,7 +78,7 @@ def load_brep_encoding(brep: str, **kwargs) -> Polygon:
         * *refine_num* (``int, optional``) --
             Bézier curve interpolation number. In Rhino a surface's edges are nurb based curves.
             Shapely does not support nurbs, so the individual Bézier curves are interpolated using straight lines.
-            This parameter sets the number of straight lines used in the interpolation. 
+            This parameter sets the number of straight lines used in the interpolation.
             Default is 1.
         * *vec1* (``numpy.ndarray, optional``) --
             A 3d vector in the Shapely plane. Rhino is a 3D geometry environment.
@@ -98,9 +100,9 @@ def load_brep_encoding(brep: str, **kwargs) -> Polygon:
             Controls if only the rhino surfaces that have the same normal as the Shapely plane are yielded.
             If true, all non parallel surfaces are filtered out.
             Default is False.
-    """    
+    """
     rhi = RhImporter.from_serialzed_brep(brep)
     geom = list(rhi.get_planer_brep(**kwargs))
-    if len(geom)==0:
+    if len(geom) == 0:
         raise RuntimeError(f"No shapely.Polygon objects found for encoded object")
-    return geom
\ No newline at end of file
+    return geom
diff --git a/sectionproperties/pre/sections.py b/sectionproperties/pre/sections.py
index 904f55bd..197ceffa 100644
--- a/sectionproperties/pre/sections.py
+++ b/sectionproperties/pre/sections.py
@@ -51,8 +51,7 @@ def __init__(
         material: pre.Material = pre.DEFAULT_MATERIAL,
         control_points: Optional[List[float, float]] = None,
     ):
-        """Inits the Geometry class.
-        """
+        """Inits the Geometry class."""
         if isinstance(geom, MultiPolygon):
             raise ValueError(f"Use CompoundGeometry(...) for a MultiPolygon object.")
         if not isinstance(geom, Polygon):
@@ -373,7 +372,10 @@ def create_mesh(self, mesh_sizes: Union[float, List[float]]):
         return self
 
     def align_to(
-        self, other: Union[Geometry, Tuple[float, float]], on: str, inner: bool = False,
+        self,
+        other: Union[Geometry, Tuple[float, float]],
+        on: str,
+        inner: bool = False,
     ) -> Geometry:
         """
         Returns a new Geometry object, representing 'self' translated so that is aligned
@@ -479,7 +481,9 @@ def align_center(self, align_to: Optional[Geometry] = None):
         return new_geom
 
     def shift_section(
-        self, x_offset=0.0, y_offset=0.0,
+        self,
+        x_offset=0.0,
+        y_offset=0.0,
     ):
         """
         Returns a new Geometry object translated by 'x_offset' and 'y_offset'.
@@ -634,7 +638,12 @@ def split_section(
             right_geom, left_geom = geometry.split_section((0, 0), (0, 1))
         """
         if point_j:
-            vector = np.array([point_j[0] - point_i[0], point_j[1] - point_i[1],])
+            vector = np.array(
+                [
+                    point_j[0] - point_i[0],
+                    point_j[1] - point_i[1],
+                ]
+            )
         elif vector is not None:
             vector = np.array(vector)
         elif not point_j and not vector:
@@ -807,13 +816,21 @@ def shift_points(
             current_points[point_idx] = (new_x, new_y)
 
         new_geom = Geometry.from_points(
-            current_points, current_facets, current_control_points, current_holes, material=current_material
+            current_points,
+            current_facets,
+            current_control_points,
+            current_holes,
+            material=current_material,
         )
         if self.assigned_control_point and new_geom.geom.contains(
             self.assigned_control_point
         ):
             new_geom = Geometry.from_points(
-                current_points, current_facets, current_control_points[0], current_holes, current_material
+                current_points,
+                current_facets,
+                current_control_points[0],
+                current_holes,
+                current_material,
             )
         return new_geom
 
diff --git a/setup.py b/setup.py
index 3eebc805..8f4bf7da 100644
--- a/setup.py
+++ b/setup.py
@@ -4,48 +4,61 @@
 
 
 def description_text():
-    description = 'A python package for the analysis of arbitrary cross-sections using the'
-    description += ' finite element method.'
+    description = (
+        "A python package for the analysis of arbitrary cross-sections using the"
+    )
+    description += " finite element method."
 
     return description
 
 
 def readme():
-    with open('README.md') as f:
+    with open("README.md") as f:
         return f.read()
 
 
-if sys.version_info[0] < 3 or sys.version_info[0] == 3 and sys.version_info[1] < 8:
-    sys.exit('Sorry, Python < 3.8 is not supported')
+if sys.version_info[0] < 3 or sys.version_info[0] == 3 and sys.version_info[1] < 7:
+    sys.exit("Sorry, Python < 3.7 is not supported")
 
 install_requires = [
-    'numpy', 'scipy', 'pytest_check', 'more_itertools', 'matplotlib', 'shapely', 'triangle',
-    'rhino-shapley-interop'
+    "numpy",
+    "scipy",
+    "pytest_check",
+    "more_itertools",
+    "matplotlib",
+    "shapely",
+    "triangle",
+    "rhino-shapley-interop",
+    "cad_to_shapely",
 ]
 
 setup(
-    name='sectionproperties',
+    name="sectionproperties",
     version=version,
     description=description_text(),
     long_description=readme(),
-    long_description_content_type='text/markdown',
+    long_description_content_type="text/markdown",
     classifiers=[
-        'Development Status :: 5 - Production/Stable',
-        'Environment :: Console',
-        'Intended Audience :: Science/Research',
-        'License :: OSI Approved :: MIT License',
-        'Programming Language :: Python :: 3 :: Only',
-        'Topic :: Scientific/Engineering',
+        "Development Status :: 5 - Production/Stable",
+        "Environment :: Console",
+        "Intended Audience :: Science/Research",
+        "License :: OSI Approved :: MIT License",
+        "Programming Language :: Python :: 3 :: Only",
+        "Topic :: Scientific/Engineering",
     ],
-    url='https://github.com/robbievanleeuwen/section-properties',
-    author='Robbie van Leeuwen',
-    author_email='robbie.vanleeuwen@gmail.com',
-    license='MIT',
+    url="https://github.com/robbievanleeuwen/section-properties",
+    author="Robbie van Leeuwen",
+    author_email="robbie.vanleeuwen@gmail.com",
+    license="MIT",
     packages=[
-        'sectionproperties', 'sectionproperties.analysis', 'sectionproperties.post',
-        'sectionproperties.pre', 'sectionproperties.examples', 'sectionproperties.tests'
+        "sectionproperties",
+        "sectionproperties.analysis",
+        "sectionproperties.post",
+        "sectionproperties.pre",
+        "sectionproperties.examples",
+        "sectionproperties.tests",
     ],
     install_requires=install_requires,
     include_package_data=True,
-    zip_safe=False
+    zip_safe=False,
 )
diff --git a/todo.md b/todo.md
deleted file mode 100755
index 7d2a9128..00000000
--- a/todo.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# TODO LIST:
-- [ ] add lots of tests!
-- [ ] concrete reinforcement - create circle with slightly larger diameter but correct area??


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..fd23a827
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,30 @@
+name: Run Tests
+
+on: [workflow_dispatch, push, pull_request]
+
+jobs:
+  tests:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: ['3.7', '3.8', '3.9']
+
+    name: Runs tests with python ${{ matrix.python-version }} using ${{ matrix.os }}
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: Setup python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Update pip
+      run: pip install --upgrade pip
+
+    - name: Install package
+      run: python -m pip install -e .
+
+    - name: Run package tests
+      run: python -m pytest
diff --git a/sectionproperties/tests/test_offset.py b/sectionproperties/tests/test_offset.py
index 85908a2f..b4211a41 100644
--- a/sectionproperties/tests/test_offset.py
+++ b/sectionproperties/tests/test_offset.py
@@ -151,7 +151,7 @@ def test_compound_rectangular_isection_offset_corrode():
 
 def test_compound_stiffened_isection():
     """
-    Tests that plates 1 and 2 can be eroded to nothing and a valid Section can 
+    Tests that plates 1 and 2 can be eroded to nothing and a valid Section can
     still be generated without errors.
     """
     uc = sections.i_section(d=400, b=400, t_f=25, t_w=25, r=30, n_r=8)
diff --git a/sectionproperties/tests/test_perimeter.py b/sectionproperties/tests/test_perimeter.py
index f6999a46..8426240d 100644
--- a/sectionproperties/tests/test_perimeter.py
+++ b/sectionproperties/tests/test_perimeter.py
@@ -6,6 +6,7 @@
 
 r_tol = 1e-3
 
+
 def test_rectangular_perimeter():
     rect = sections.rectangular_section(d=500, b=300)
     rect.create_mesh([200])
@@ -13,41 +14,60 @@ def test_rectangular_perimeter():
     section.calculate_geometric_properties()
     assert section.get_perimeter() == 2 * (500 + 300)
 
+
 def test_i_section():
     i_section = sections.i_section(d=308, b=305, t_f=15.4, t_w=9.9, r=16.5, n_r=16)
     i_section.create_mesh([100])
     section = Section(i_section)
     section.calculate_geometric_properties()
-    perim = (2 * 305) + (4 * 15.4) + 2 * (305 - 9.9 - 2 * 16.5) + (2 * np.pi * 16.5) + 2 * (308 - 2 * 15.4 - 2 * 16.5)
+    perim = (
+        (2 * 305)
+        + (4 * 15.4)
+        + 2 * (305 - 9.9 - 2 * 16.5)
+        + (2 * np.pi * 16.5)
+        + 2 * (308 - 2 * 15.4 - 2 * 16.5)
+    )
     check.almost_equal(section.get_perimeter(), perim, rel=r_tol)
 
+
 def test_box_girder_perimeter():
-    box_girder = sections.box_girder_section(d=400, b_t=700, b_b=100, t_ft=20, t_fb=20, t_w=12)
+    box_girder = sections.box_girder_section(
+        d=400, b_t=700, b_b=100, t_ft=20, t_fb=20, t_w=12
+    )
     box_girder.create_mesh([100])
     section = Section(box_girder)
     section.calculate_geometric_properties()
     assert section.get_perimeter() == 700 + 100 + 2 * 500
 
+
 def test_custom_geometry_perimeter():
-    points = [[0,0], [5,0], [11,8], [3,2], [0, 2]]
-    facets = [[0,1], [1,2], [2,3], [3,4], [4,0]]
-    control_points = [[5,5]]
+    points = [[0, 0], [5, 0], [11, 8], [3, 2], [0, 2]]
+    facets = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]]
+    control_points = [[5, 5]]
     custom = sections.Geometry.from_points(points, facets, control_points, holes=None)
     custom.create_mesh([100])
     section = Section(custom)
     section.calculate_geometric_properties()
     assert section.get_perimeter() == 5 + 2 + 10 + 10 + 3
 
+
 def test_compound_rectangular_perimeter():
     rect1 = sections.rectangular_section(d=100, b=100)
     rect2 = sections.rectangular_section(d=50, b=50).align_to(rect1, on="top")
     rect3 = sections.rectangular_section(d=50, b=100).align_to(rect1, on="right")
-    rect4 = sections.rectangular_section(d=50, b=50).align_to(rect3, on="bottom").align_to(rect3, on="right", inner=True)
+    rect4 = (
+        sections.rectangular_section(d=50, b=50)
+        .align_to(rect3, on="bottom")
+        .align_to(rect3, on="right", inner=True)
+    )
     geom = rect1 + rect2 + rect3 + rect4
     geom.create_mesh([100])
     section = Section(geom)
     section.calculate_geometric_properties()
-    assert section.get_perimeter() == (150 + 50 + 50 + 100 + 100 + 50 + 50 + 50 + 50 + 150)
+    assert section.get_perimeter() == (
+        150 + 50 + 50 + 100 + 100 + 50 + 50 + 50 + 50 + 150
+    )
+
 
 def test_compound_rectangular_isection_perimeter1():
     d = 300
@@ -58,24 +78,47 @@ def test_compound_rectangular_isection_perimeter1():
     b_p = 250
     t_p = 16
     ub = sections.i_section(d=d, b=b, t_f=tf, t_w=tw, r=r, n_r=16)
-    plate = sections.rectangular_section(b=b_p, d=t_p).align_center(ub).align_to(ub, on="top")
+    plate = (
+        sections.rectangular_section(b=b_p, d=t_p)
+        .align_center(ub)
+        .align_to(ub, on="top")
+    )
     geom = ub + plate
     geom.create_mesh([100])
     section = Section(geom)
     section.calculate_geometric_properties()
-    perim = b + (4 * tf) + 2 * (b - tw - 2 * r) + (2 * np.pi * r) + 2 * (d - 2 * tf - 2 * r) + (b_p - b) + (2 * t_p) + b_p
+    perim = (
+        b
+        + (4 * tf)
+        + 2 * (b - tw - 2 * r)
+        + (2 * np.pi * r)
+        + 2 * (d - 2 * tf - 2 * r)
+        + (b_p - b)
+        + (2 * t_p)
+        + b_p
+    )
     check.almost_equal(section.get_perimeter(), perim, rel=r_tol)
 
-def  test_compound_rectangular_isection_perimeter2():
+
+def test_compound_rectangular_isection_perimeter2():
     i_section = sections.i_section(d=308, b=305, t_f=15.4, t_w=9.9, r=16.5, n_r=16)
-    rect1 = sections.rectangular_section(d=330, b=16).align_center(i_section).align_to(i_section, "left")
-    rect2 = sections.rectangular_section(d=330, b=16).align_center(i_section).align_to(i_section, "right")
+    rect1 = (
+        sections.rectangular_section(d=330, b=16)
+        .align_center(i_section)
+        .align_to(i_section, "left")
+    )
+    rect2 = (
+        sections.rectangular_section(d=330, b=16)
+        .align_center(i_section)
+        .align_to(i_section, "right")
+    )
     geom = i_section + rect1 + rect2
     geom.create_mesh([100])
     section = Section(geom)
     section.calculate_geometric_properties()
     assert section.get_perimeter() == 2 * 330 + 4 * 16 + 2 * 305 + 2 * (330 - 308)
 
+
 def test_compound_rhs_isection_perimeter():
     d = 200
     b = 150
@@ -84,11 +127,25 @@ def test_compound_rhs_isection_perimeter():
     b_p = 250
     t_p = 16
     rhs = sections.rectangular_hollow_section(d=d, b=b, t=t, r_out=r, n_r=16)
-    plate1 = sections.rectangular_section(b=b_p, d=t_p).align_center(rhs).align_to(rhs, on="top")
-    plate2 = sections.rectangular_section(b=b_p, d=t_p).align_center(rhs).align_to(rhs, on="bottom")
+    plate1 = (
+        sections.rectangular_section(b=b_p, d=t_p)
+        .align_center(rhs)
+        .align_to(rhs, on="top")
+    )
+    plate2 = (
+        sections.rectangular_section(b=b_p, d=t_p)
+        .align_center(rhs)
+        .align_to(rhs, on="bottom")
+    )
     geom = rhs + plate1 + plate2
     geom.create_mesh([100])
     section = Section(geom)
     section.calculate_geometric_properties()
-    perim = (2 * b_p) + (4 * t_p) + 2 * (b_p - b + 2 * r) + (2 * np.pi * r) + 2 * (d - 2 * r)
+    perim = (
+        (2 * b_p)
+        + (4 * t_p)
+        + 2 * (b_p - b + 2 * r)
+        + (2 * np.pi * r)
+        + 2 * (d - 2 * r)
+    )
     check.almost_equal(section.get_perimeter(), perim, rel=r_tol)
diff --git a/sectionproperties/tests/test_sections.py b/sectionproperties/tests/test_sections.py
index 1b48429b..4bc2c3c4 100644
--- a/sectionproperties/tests/test_sections.py
+++ b/sectionproperties/tests/test_sections.py
@@ -6,7 +6,13 @@
 from sectionproperties.analysis.cross_section import Section
 from sectionproperties.pre.pre import Material
 from sectionproperties.pre.rhino import load_3dm, load_brep_encoding
-from shapely.geometry import Polygon, MultiPolygon, LineString, Point, GeometryCollection
+from shapely.geometry import (
+    Polygon,
+    MultiPolygon,
+    LineString,
+    Point,
+    GeometryCollection,
+)
 from shapely import wkt
 import json
 
@@ -78,12 +84,15 @@ def test_geometry_from_points():
     ]
     control_points = [[0, 0]]
     holes = [[0, 6], [0, -6]]
-    new_geom = Geometry.from_points(points=points, facets=facets, control_points=control_points, holes=holes)
+    new_geom = Geometry.from_points(
+        points=points, facets=facets, control_points=control_points, holes=holes
+    )
     wkt_test_geom = shapely.wkt.loads(
         "POLYGON ((6 10, 6 -10, -6 -10, -6 10, 6 10), (-4 4, 4 4, 4 8, -4 8, -4 4), (4 -8, 4 -4, -4 -4, -4 -8, 4 -8))"
     )
     assert (new_geom.geom - wkt_test_geom) == Polygon()
 
+
 def test_compound_geometry_from_points():
     # CompoundGeometry.from_points() tests a shape with an arbitrary
     # number of exteriors and an arbitrary number of interiors being
@@ -208,10 +217,22 @@ def test_plastic_centroid():
     # correct "center" of the original section which is affected by EA of each
     # of the constituent geometries.
 
-    steel = Material(name='Steel', elastic_modulus=200e3, poissons_ratio=0.3, density=7.85e-6,
-                    yield_strength=500, color='grey')
-    timber = Material(name='Timber', elastic_modulus=5e3, poissons_ratio=0.35, density=6.5e-7,
-                    yield_strength=20, color='burlywood')
+    steel = Material(
+        name="Steel",
+        elastic_modulus=200e3,
+        poissons_ratio=0.3,
+        density=7.85e-6,
+        yield_strength=500,
+        color="grey",
+    )
+    timber = Material(
+        name="Timber",
+        elastic_modulus=5e3,
+        poissons_ratio=0.35,
+        density=6.5e-7,
+        yield_strength=20,
+        color="burlywood",
+    )
 
     # create 310UB40.4
     ub = i_section(d=304, b=165, t_f=10.2, t_w=6.1, r=11.4, n_r=8, material=steel)
@@ -279,22 +300,37 @@ def test_geometry_from_3dm_encode():
         brep_encoded = json.load(file)
     exp = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
     test = Geometry.from_rhino_encoding(brep_encoded)
-    assert (test.geom-exp).is_empty
+    assert (test.geom - exp).is_empty
 
 
 def test_shift_points():
     assymetrical_chan = nastran_chan(75, 200, 8, 16).shift_points(1, dy=-10)
-    assert assymetrical_chan.geom.wkt == 'POLYGON ((0 0, 75 -10, 75 16, 8 16, 8 184, 75 184, 75 200, 0 200, 0 0))'
+    assert (
+        assymetrical_chan.geom.wkt
+        == "POLYGON ((0 0, 75 -10, 75 16, 8 16, 8 184, 75 184, 75 200, 0 200, 0 0))"
+    )
 
 
 def test_mirror_section():
     assymetrical_chan = nastran_chan(75, 200, 8, 16).shift_points(1, dy=-10)
-    assert assymetrical_chan.mirror_section(axis="x").geom.wkt == 'POLYGON ((0 190, 75 200, 75 174, 8 174, 8 6, 75 6, 75 -10, 0 -10, 0 190))'
-    assert assymetrical_chan.mirror_section(axis="y").geom.wkt == 'POLYGON ((75 0, 0 -10, 0 16, 67 16, 67 184, 0 184, 0 200, 75 200, 75 0))'
-    assert assymetrical_chan.mirror_section(axis='y', mirror_point=[50, 50]).geom.wkt == 'POLYGON ((100 0, 25 -10, 25 16, 92 16, 92 184, 25 184, 25 200, 100 200, 100 0))'
-    assert assymetrical_chan.mirror_section(axis='x', mirror_point=[50, 50]).geom.wkt == 'POLYGON ((0 100, 75 110, 75 84, 8 84, 8 -84, 75 -84, 75 -100, 0 -100, 0 100))'
+    assert (
+        assymetrical_chan.mirror_section(axis="x").geom.wkt
+        == "POLYGON ((0 190, 75 200, 75 174, 8 174, 8 6, 75 6, 75 -10, 0 -10, 0 190))"
+    )
+    assert (
+        assymetrical_chan.mirror_section(axis="y").geom.wkt
+        == "POLYGON ((75 0, 0 -10, 0 16, 67 16, 67 184, 0 184, 0 200, 75 200, 75 0))"
+    )
+    assert (
+        assymetrical_chan.mirror_section(axis="y", mirror_point=[50, 50]).geom.wkt
+        == "POLYGON ((100 0, 25 -10, 25 16, 92 16, 92 184, 25 184, 25 200, 100 200, 100 0))"
+    )
+    assert (
+        assymetrical_chan.mirror_section(axis="x", mirror_point=[50, 50]).geom.wkt
+        == "POLYGON ((0 100, 75 110, 75 84, 8 84, 8 -84, 75 -84, 75 -100, 0 -100, 0 100))"
+    )
+
 
-    
 def test_filter_non_polygons():
     point1 = Point([0, 0])
     point2 = Point([1, 1])