Repository: engineerjoe440/ElectricPy

Issue:
Improve `release` GitHub-Action to Only run on `master` Branch (Skip Others W/O Failing)
A common annoyance is that the `release` action runs on every branch, but should only run on master branches, and shouldn't FAIL if there's no new version to release, or if the branch is not one we should run on.

Discussion:

Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4c9c9f32..a8e73acb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,8 +1,11 @@
 name: Release
+
 on:
   push:
     branches:
       - master
+    paths:
+      - 'electricpy/version.py'
 
 jobs:
   release:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9ac1424a..a3fbb821 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -15,6 +15,11 @@
         "-m",
         "test"
     ],
+    "files.exclude": {
+        ".venv":true,
+        ".pytest_cache": true,
+        "**/__pycache__/**":true
+    },
     "python.testing.unittestEnabled": false,
     "python.testing.pytestEnabled": true,
     "python.testing.cwd": "${workspaceRoot}",
diff --git a/docsource/conf.py b/docsource/conf.py
index 92bbf768..e0a9498a 100644
--- a/docsource/conf.py
+++ b/docsource/conf.py
@@ -8,7 +8,7 @@ import sys
 
 print("Build with:", sys.version)
 parent_dir = os.path.dirname(os.getcwd())
-initfile = os.path.join(parent_dir, 'electricpy', '__init__.py')
+initfile = os.path.join(parent_dir, 'electricpy', 'version.py')
 sys.path.insert(0, parent_dir)
 sys.path.insert(1, os.path.dirname(os.path.abspath(__file__)))
 print(parent_dir)
@@ -20,8 +20,8 @@ render_images()
 # Gather Version Information from Python File
 with open(initfile) as fh:
     file_str = fh.read()
-    name = re.search('_name_ = \"(.*)\"', file_str).group(1)
-    ver = re.search('_version_ = \"(.*)\"', file_str).group(1)
+    name = re.search('NAME = \"(.*)\"', file_str).group(1)
+    ver = re.search('VERSION = \"(.*)\"', file_str).group(1)
     # Version Breakdown:
     # MAJOR CHANGE . MINOR CHANGE . MICRO CHANGE
     print("Sphinx HTML Build For:", name,"   Version:", ver)
diff --git a/electricpy/__init__.py b/electricpy/__init__.py
index 3246fd9b..5b8b8339 100644
--- a/electricpy/__init__.py
+++ b/electricpy/__init__.py
@@ -12,6 +12,7 @@ to aid in scientific calculations.
 """
 ################################################################################
 
+
 import cmath as _c
 from typing import Union as _Union
 from inspect import getframeinfo as _getframeinfo
@@ -29,16 +30,6 @@ from .constants import *
 from .phasor import compose, parallelz
 from .phasor import phasorz as impedance
 
-# Define Module Specific Variables
-_name_ = "electricpy"
-_version_ = "0.2.2"
-__version__ = _version_  # Alias Version for User Ease
-
-
-# Version Breakdown:
-# MAJOR CHANGE . MINOR CHANGE . MICRO CHANGE
-
-
 # Define Cycle Time Function
 def tcycle(ncycles = 1, freq = 60):
     r"""
@@ -4672,7 +4663,7 @@ def vipf(V=None, I=None, PF=1, find=''):
 
 
 # Define Synchronous Speed Calculator
-def syncspeed(Npol, freq=60, Hz=False):
+def syncspeed(Npol, freq=60, Hz=False, rpm=False):
     # noqa: D401   "Synchronous" is an intentional descriptor
     r"""
     Synchronous Speed Calculator Function.
@@ -4692,16 +4683,25 @@ def syncspeed(Npol, freq=60, Hz=False):
                 Frequency of electrical system in Hertz, default=60
     Hz:         bool, optional
                 Boolean control to enable return in Hertz. default=False
+    rpm:        bool, optional
+                Boolean control to enable return in rpm. default=False
+
 
     Returns
     -------
     wsyn:       float
                 Synchronous Speed of Induction Machine, defaults to units of
-                rad/sec, but may be set to Hertz if `Hz` set to True.
+                rad/sec, but may be set to Hertz or RPM if `Hz` or `rpm` set to True.
     """
-    wsyn = 2 * _np.pi * freq / (Npol / 2)
+    try:
+        wsyn = 2 * _np.pi * freq / (Npol / 2)
+    except ZeroDivisionError:
+        raise ZeroDivisionError("Poles of an electrical machine \
+        can not be zero")
     if Hz:
-        return wsyn / (2 * _np.pi)
+        return (2*freq / (Npol))
+    if rpm:
+        return (120 * freq)/(Npol)
     return wsyn
 
 
@@ -5245,4 +5245,12 @@ def lm317(r1, r2, v_out):
     else:
         raise ValueError("Invalid arguments")
 
+    return R1,R2
+
+# Define Module Specific Variables
+from .version import NAME, VERSION
+_name_ = NAME
+_version_ = VERSION
+__version__ = _version_  # Alias Version for User Ease
+
 # END OF FILE
diff --git a/electricpy/geometry/__init__.py b/electricpy/geometry/__init__.py
index ebcd2084..d6450a85 100644
--- a/electricpy/geometry/__init__.py
+++ b/electricpy/geometry/__init__.py
@@ -4,8 +4,8 @@
 
 >>> import electricpy.geometry as geometry
 
-This Package help to handle coordinate geometry calculatios which are required 
-for plotting various graphs in electrical engineering. 
+This Package help to handle coordinate geometry calculatios which are required
+for plotting various graphs in electrical engineering.
 
 Built to support operations similar to Numpy and Scipy, this package is designed
 to aid in scientific calculations.
@@ -18,7 +18,7 @@ from typing import Tuple
 
 class Point:
     """A point in 2D space.
-    
+
     Parameters
     ----------
     x : float
@@ -65,7 +65,7 @@ class Line:
     """A line in 2D space in the form .
 
     math:: ax + by + c = 0
-    
+
     Parameters
     ----------
     a : float
@@ -189,6 +189,16 @@ class Line:
         """Return the intersection of two lines."""
         return line_intersection(self, l1)
 
+def angle_btw_lines(l1: Line, l2: Line) -> float:
+    """Return angle (in radians) between two lines l1 and l2."""
+    a1, b1 = l1.a, l1.b
+    a2, b2 = l2.a, l2.b
+
+    if a1*a2 + b1*b2 == 0:
+        return cmath.pi/2
+
+    ans = abs((b1*a2 - a1*b2)/(a1*a2 + b1*b2))
+    return cmath.atan(ans)
 
 def line_intersection(l1: Line, l2: Line) -> Point:
     """Calculate the intersection point of two lines."""
diff --git a/electricpy/version.py b/electricpy/version.py
new file mode 100644
index 00000000..8c36b3d5
--- /dev/null
+++ b/electricpy/version.py
@@ -0,0 +1,11 @@
+################################################################################
+"""
+`electricpy` Package - `version` Module.
+
+>>> from electricpy import version
+
+This file is to keep track of Name, Current Version of electricpy for CI and CD
+"""
+################################################################################
+NAME = "electricpy"
+VERSION = "0.2.2"
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 96c45231..2c178ead 100644
--- a/setup.py
+++ b/setup.py
@@ -7,16 +7,16 @@ def read_dependencies():
     with open("requirements.txt", "r") as fh:
         dependencies = fh.readlines()
     return dependencies
-        
+
 # Load Description Document
 with open("README.md", "r") as fh:
     long_description = fh.read()
 
 # Gather Version Information from Python File
-with open("electricpy/__init__.py", encoding="utf-8") as fh:
+with open("electricpy/version.py", encoding="utf-8") as fh:
     file_str = fh.read()
-    name = re.search('_name_ = \"(.*)\"', file_str).group(1)
-    ver = re.search('_version_ = \"(.*)\"', file_str).group(1)
+    name = re.search('NAME = \"(.*)\"', file_str).group(1)
+    ver = re.search('VERSION = \"(.*)\"', file_str).group(1)
     # Version Breakdown:
     # MAJOR CHANGE . MINOR CHANGE . MICRO CHANGE
     print("Setup for:",name,"   Version:",ver)


Test patch:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index c843e23e..8ba3e4eb 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -1,6 +1,12 @@
 name: pytest
 
-on: [push, pull_request]
+on:
+  push:
+    path:
+    - '**.py'
+  pull_request:
+    path:
+    - '**.py'
 
 jobs:
   build:
diff --git a/test/test_circle.py b/test/test_circle.py
index 3d568189..8ea2a6e9 100644
--- a/test/test_circle.py
+++ b/test/test_circle.py
@@ -1,10 +1,10 @@
+import cmath
 from electricpy.geometry.circle import Circle
 from electricpy.geometry import Line, Point
-import cmath
 
-def test_area():
 
-    def test_0():
+class TestArea:
+    def test_0(self):
 
         c = Circle((0, 0), 1)
         assert c.area() == cmath.pi
@@ -12,7 +12,7 @@ def test_area():
         c = Circle((0, 0), 2)
         assert c.area() == cmath.pi * 4
 
-    def test_1():
+    def test_1(self):
 
         c = Circle((0, 0), 1.1)
         assert c.area() == cmath.pi * 1.1**2
@@ -20,12 +20,9 @@ def test_area():
         c = Circle((0, 0), 2.2)
         assert c.area() == cmath.pi * 2.2**2
 
-    for i in range(2):
-        exec("test_{}()".format(i))
 
-def test_circumference():
-
-    def test_0():
+class TestCircumference:
+    def test_0(self):
 
         c = Circle((0, 0), 1)
         assert c.circumference() == cmath.pi * 2
@@ -33,7 +30,7 @@ def test_circumference():
         c = Circle((0, 0), 2)
         assert c.circumference() == cmath.pi * 4
 
-    def test_1():
+    def test_1(self):
 
         c = Circle((0, 0), 1.1)
         assert c.circumference() == cmath.pi * 2.2
@@ -41,12 +38,9 @@ def test_circumference():
         c = Circle((0, 0), 2.2)
         assert c.circumference() == cmath.pi * 4.4
 
-    for i in range(2):
-        exec("test_{}()".format(i))
-
-def test_tangent():
 
-    def test_0():
+class TestTangent:
+    def test_0(self):
         c = Circle((0, 0), 1)
 
         assert c.tangent(Point(0, 1)) == Line(0, 1, -1)
@@ -54,25 +48,21 @@ def test_tangent():
         assert c.tangent(Point(1, 0)) == Line(1, 0, -1)
         assert c.tangent(Point(-1, 0)) == Line(-1, 0, -1)
 
-
-    def test_1():
+    def test_1(self):
 
         from test import compare_lines
 
         c = Circle((0, 0), 1)
 
-        p = Point(cmath.cos(cmath.pi/4), cmath.sin(cmath.pi/4))
+        p = Point(cmath.cos(cmath.pi / 4), cmath.sin(cmath.pi / 4))
         p1 = Point(cmath.sqrt(2), 0)
         p2 = Point(0, cmath.sqrt(2))
 
         assert compare_lines(c.tangent(p), Line.construct(p1, p2))
 
-    for i in range(2):
-        exec("test_{}()".format(i))
-
-def test_normal():
 
-    def test_0():
+class TestNormal:
+    def test_0(self):
         c = Circle((0, 0), 1)
 
         assert c.normal(Point(0, 1)) == Line(1, 0, 0)
@@ -80,16 +70,13 @@ def test_normal():
         assert c.normal(Point(1, 0)) == Line(0, 1, 0)
         assert c.normal(Point(-1, 0)) == Line(0, 1, 0)
 
-    def test_1():
+    def test_1(self):
 
         from test import compare_lines
 
         c = Circle((0, 0), 1)
-        p0 = Point(cmath.cos(cmath.pi/4), cmath.sin(cmath.pi/4))
-        p1 = Point(-cmath.cos(cmath.pi/4), cmath.sin(cmath.pi/4))
+        p0 = Point(cmath.cos(cmath.pi / 4), cmath.sin(cmath.pi / 4))
+        p1 = Point(-cmath.cos(cmath.pi / 4), cmath.sin(cmath.pi / 4))
 
         assert compare_lines(c.normal(p0), Line(1, -1, 0))
         assert compare_lines(c.normal(p1), Line(1, 1, 0))
-
-    for i in range(2):
-        exec("test_{}()".format(i))
diff --git a/test/test_electricpy.py b/test/test_electricpy.py
index 312138d5..34e23807 100644
--- a/test/test_electricpy.py
+++ b/test/test_electricpy.py
@@ -4,6 +4,12 @@ import cmath
 import math
 from numpy.testing import assert_almost_equal
 
+# Electricpy Imports
+from electricpy import powerflow
+from electricpy.phasor import phasor
+from electricpy.phasor import phs
+from electricpy.phasor import vectarray
+
 def test_phasor():
     from electricpy.phasor import phasor
     magnitude = 10
@@ -112,25 +118,6 @@ def test_powerset():
     assert_almost_equal(S, 10)
     assert_almost_equal(PF, 0.6)
 
-    #Test case 2 Failed
-
-    # P = 0
-    # Q = 0
-
-    # _,_,S,PF = powerset(P = P, Q = Q)
-
-    # assert_almost_equal(S,0)
-    # assert_almost_equal(PF,0)
-
-    #Test case 3 input validation is required
-    # P = 10
-    # Q = 10
-    # PF = 0.6
-
-    # _,_,S,_ = powerset(P = P, Q = Q, PF = PF)
-    # assert_almost_equal(S,10*np.sqrt(2))
-    # assert_almost_equal(PF,1/(np.sqrt(2)))
-
 def test_voltdiv():
     from electricpy import voltdiv
     from electricpy.phasor import phasor
@@ -219,30 +206,6 @@ def test_ic_555_astable():
     assert_almost_equal(result['t_low'], 6.931*10**-6, decimal = 3)
     assert_almost_equal(result['t_high'], 1.386*10**-5, decimal = 3)
 
-    #test the other way around
-def test_powerflow():
-    from electricpy import powerflow
-    from electricpy.phasor import phasor
-
-    def test_0():
-        Vsend = phasor(1.01, 30)
-        Vrecv = phasor(1, 0)
-        Xline = 0.2
-
-        ans = powerflow(Vsend, Vrecv, Xline)
-        assert_almost_equal(ans, 2.525)
-
-    def test_1():
-        Vsend = 1.01
-        Vrecv = 1
-        Xline = 0.2
-
-        ans = powerflow(Vsend, Vrecv, Xline)
-        assert ans == 0
-
-    for i in range(2):
-        exec(f"test_{i}()")
-
 def test_slew_rate():
 
     from electricpy import slew_rate
@@ -255,31 +218,6 @@ def test_slew_rate():
     assert_almost_equal(1/(np.pi*2), V)
     assert_almost_equal(1/(np.pi*2), freq)
 
-def test_phs():
-    from electricpy.phasor import phs
-    def test_0():
-
-
-        inputs = [0, 90, 180, 270, 360]
-
-        outputs = [phs(x) for x in inputs]
-        actual_outputs = [1, 1j, -1, -1j, 1]
-
-        for x,y in zip(outputs, actual_outputs):
-            assert_almost_equal(x, y)
-
-    def test_1():
-        inputs = [30, 45, 60, 135]
-
-        outputs = [phs(x) for x in inputs]
-        actual_outputs = [0.866025+0.5j, 0.707106+0.707106j, 0.5+0.866025j, -0.707106+0.707106j]
-
-        for x,y in zip(outputs, actual_outputs):
-            assert_almost_equal(x, y, decimal = 3)
-
-    test_0()
-    test_1()
-
 def test_induction_motor_circle():
     from electricpy.visu import InductionMotorCircle
 
@@ -398,36 +336,6 @@ def test_seq_to_abc():
 
     test_0()
 
-def test_vectarray():
-
-    from electricpy.phasor import vectarray
-
-    def test_0():
-
-        A = [2+3j, 4+5j, 6+7j, 8+9j]
-        B = vectarray(A)
-
-        B_test = [[np.abs(x), np.degrees(np.angle(x))] for x in A]
-
-        np.testing.assert_array_almost_equal(B, B_test)
-
-    def test_1():
-
-        A = np.random.random(size = 16)
-        B = vectarray(A)
-
-        B_test = [[np.abs(x), 0] for x in A]
-        np.testing.assert_array_almost_equal(B, B_test)
-
-        A = np.random.random(size = 16)*1j
-        B = vectarray(A)
-
-        B_test = [[np.abs(x), 90] for x in A]
-        np.testing.assert_array_almost_equal(B, B_test)
-
-    test_0()
-    test_1()
-
 def test_parallel_plate_capacitance():
     from electricpy import parallel_plate_capacitance
 
@@ -494,6 +402,77 @@ def test_solenoid_inductance():
     # Test length given inductance, area, number of turns and permeability
     assert_almost_equal(solenoid_inductance(L=L2, A=A2, N=N2, u=u2), l2)
 
+def test_syncspeed():
+    from electricpy import syncspeed
+    assert syncspeed(4, freq = 60, rpm = True) == 1800
+    assert syncspeed(4, freq = 60, Hz = True) == 30
+
+    with pytest.raises(ZeroDivisionError, match = "Poles of an electrical machine \
+        can not be zero"):
+        syncspeed(0)
+
+class TestVectarray():
+
+    def test_0(self):
+
+        A = [2+3j, 4+5j, 6+7j, 8+9j]
+        B = vectarray(A)
+
+        B_test = [[np.abs(x), np.degrees(np.angle(x))] for x in A]
+
+        np.testing.assert_array_almost_equal(B, B_test)
+
+    def test_1(self):
+
+        A = np.random.random(size = 16)
+        B = vectarray(A)
+
+        B_test = [[np.abs(x), 0] for x in A]
+        np.testing.assert_array_almost_equal(B, B_test)
+
+        A = np.random.random(size = 16)*1j
+        B = vectarray(A)
+
+        B_test = [[np.abs(x), 90] for x in A]
+        np.testing.assert_array_almost_equal(B, B_test)
+
+class TestPhs():
+    def test_0(self):
+        inputs = [0, 90, 180, 270, 360]
+
+        outputs = [phs(x) for x in inputs]
+        actual_outputs = [1, 1j, -1, -1j, 1]
+
+        for x,y in zip(outputs, actual_outputs):
+            assert_almost_equal(x, y)
+
+    def test_1(self):
+        inputs = [30, 45, 60, 135]
+
+        outputs = [phs(x) for x in inputs]
+        actual_outputs = [0.866025+0.5j, 0.707106+0.707106j, 0.5+0.866025j, -0.707106+0.707106j]
+
+        for x,y in zip(outputs, actual_outputs):
+            assert_almost_equal(x, y, decimal = 3)
+
+class TestPowerflow():
+
+    def test_0(self):
+        Vsend = phasor(1.01, 30)
+        Vrecv = phasor(1, 0)
+        Xline = 0.2
+
+        ans = powerflow(Vsend, Vrecv, Xline)
+        assert_almost_equal(ans, 2.525)
+
+    def test_1(self):
+        Vsend = 1.01
+        Vrecv = 1
+        Xline = 0.2
+
+        ans = powerflow(Vsend, Vrecv, Xline)
+        assert ans == 0
+        
 def test_tcycle():
     from electricpy import tcycle
 
diff --git a/test/test_geometry.py b/test/test_geometry.py
index 8ddb7499..d0a8a871 100644
--- a/test/test_geometry.py
+++ b/test/test_geometry.py
@@ -1,152 +1,138 @@
-from electricpy import geometry
+import cmath
+from electricpy import geometry as Geometry
 from electricpy.geometry import Point
 from electricpy.geometry import Line
 from numpy.testing import assert_array_almost_equal
 
-def test_distance():
-    
-    def test_0():
+class TestDistance():
+
+    def test_0(self):
         p1 = Point(1, 2)
         p2 = Point(3, 4)
-        assert geometry.distance(p1, p2) == 2*(2**0.5)
+        assert Geometry.distance(p1, p2) == 2*(2**0.5)
 
         p1 = Point(4, -6)
         p2 = Point(-2, -5)
-        assert geometry.distance(p2, p1) ==  (37**0.5)
+        assert Geometry.distance(p2, p1) ==  (37**0.5)
 
         p1 = Point(1.3, 2.3)
         p2 = Point(1.4, 2.4)
 
-        d_output = geometry.distance(p1, p2)
+        d_output = Geometry.distance(p1, p2)
         d_actual = 0.1*(2**0.5)
 
         assert_array_almost_equal(d_output, d_actual, decimal=6)
 
-    def test_1():
+    def test_1(self):
         p1 = Point(1, 2)
         p2 = Point(1, 3)
-        assert geometry.distance(p1, p2) == 1
+        assert Geometry.distance(p1, p2) == 1
 
         p1 = Point(2.0, 1)
         p2 = Point(3.0, 1)
-        assert geometry.distance(p1, p2) == 1
-
-    for i in range(2):
-        exec("test_{}()".format(i))
+        assert Geometry.distance(p1, p2) == 1
 
-def test_slope():
+class Testslope():
 
-    def test_0():
+    def test_0(self):
         p1 = Point(1, 2)
         p2 = Point(3, 4)
-        assert geometry.slope(p1, p2) == 1
+        assert Geometry.slope(p1, p2) == 1
 
         p1 = Point(4, -6)
         p2 = Point(-2, -5)
-        assert geometry.slope(p2, p1) == -1/6
+        assert Geometry.slope(p2, p1) == -1/6
 
-    def test_1():
+    def test_1(self):
 
         p1 = Point(1, 2)
         p2 = Point(2, 2)
 
-        assert geometry.slope(p1, p2) == 0
+        assert Geometry.slope(p1, p2) == 0
 
         p1 = Point(1, 2)
         p2 = Point(1, 3)
         try:
-            geometry.slope(p1, p2)
+            Geometry.slope(p1, p2)
         except ZeroDivisionError:
             assert True
 
-    for i in range(2):
-        exec("test_{}()".format(i))
+class Testsection():
 
-def test_section():
-        
-    def test_0():
+    def test_0(self):
         p1 = Point(1, 2)
         p2 = Point(3, 4)
 
-        p = geometry.section(p1, p2, 0.5)
+        p = Geometry.section(p1, p2, 0.5)
         assert p == Point(2, 3)
 
-    def test_1():
+    def test_1(self):
         p1 = Point(-1, 3)
         p2 = Point(1, -3)
 
-        p_computed = geometry.section(p1, p2, (2, 3))
+        p_computed = Geometry.section(p1, p2, (2, 3))
         p_actual = Point(-1/5, 3/5)
 
         assert_array_almost_equal(p_computed(), p_actual(), decimal=6)
-    
-    for i in range(2):
-        exec("test_{}()".format(i))
 
-def test_line_equaltion():
-    
-    def test_0():
+class Testline_equaltion():
+
+    def test_0(self):
         p1 = Point(1, 2)
         p2 = Point(3, 4)
-        assert geometry.line_equation(p1, p2) == Line(1, -1, 1)
+        assert Geometry.line_equation(p1, p2) == Line(1, -1, 1)
 
         p1 = Point(4, -6)
         p2 = Point(-2, -5)
-        assert geometry.line_equation(p1, p2) == Line(1, 6, 32)
+        assert Geometry.line_equation(p1, p2) == Line(1, 6, 32)
 
-    def test_1():
+    def test_1(self):
         p1 = Point(1, 2)
         p2 = Point(1, 3)
-        assert geometry.line_equation(p1, p2) == Line(1, 0, -1)
+        assert Geometry.line_equation(p1, p2) == Line(1, 0, -1)
 
         p1 = Point(1, 2)
         p2 = Point(2, 2)
-        assert geometry.line_equation(p1, p2) == Line(0, 1, -2)
+        assert Geometry.line_equation(p1, p2) == Line(0, 1, -2)
 
-    def test_2():
+    def test_2(self):
         assert Line(1, 2, 3) == Line(2, 4, 6)
         assert Line(1, -1, 0) == Line(3, -3, 0)
         assert Line(1, 0, -1) == Line(3, 0, -3)
 
-    for i in range(3):
-        exec("test_{}()".format(i))
-
-def test_line_distance():
+class Testline_distance():
 
-    def test_0():
+    def test_0(self):
         p1 = Point(1, 2)
         p2 = Point(3, 4)
         l = Line.construct(p1, p2)
-        assert geometry.line_distance(p1, l) == 0
-        assert geometry.line_distance(p2, l) ==  0
+        assert Geometry.line_distance(p1, l) == 0
+        assert Geometry.line_distance(p2, l) ==  0
 
-    def test_1():
+    def test_1(self):
         p1 = Point(2, 0)
         p2 = Point(2, 4)
         p = Point(0, 0)
         l = Line.construct(p1, p2)
-        assert geometry.line_distance(p, l) == 2
+        assert Geometry.line_distance(p, l) == 2
         assert l.distance(p) == 2
 
         l = Line(0, 1, -3)
         assert l.distance(p) == 3
 
-    for i in range(2):
-        exec("test_{}()".format(i))
+class Testfoot_perpendicular():
 
-def test_foot_perpendicular():
-    
-    def test_0():
+    def test_0(self):
         p1 = Point(1, 2)
         p2 = Point(3, 4)
         l = Line.construct(p1, p2)
         p = Point(2, 2)
-        assert geometry.foot_perpendicular(p, l) == Point(1.5, 2.5)
+        assert Geometry.foot_perpendicular(p, l) == Point(1.5, 2.5)
 
         p = Point(2, 3)
-        assert geometry.foot_perpendicular(p, l) == Point(2, 3)
+        assert Geometry.foot_perpendicular(p, l) == Point(2, 3)
 
-    def test_1():
+    def test_1(self):
         p = Point(-1, 3)
         l = Line(3, -4, -16)
 
@@ -156,63 +142,69 @@ def test_foot_perpendicular():
         p_desired = Point(68/25, -49/25)
 
         assert_array_almost_equal(p_actual(), p_desired(), decimal=6)
-        assert geometry.midpoint(p, p_image) == p_actual 
+        assert Geometry.midpoint(p, p_image) == p_actual
 
-    for i in range(2):
-        exec("test_{}()".format(i))
+class TestPerpendicularBisector():
 
-def test_perpendicular_bisector():
-
-    from electricpy.geometry import perpendicular_bisector
-    
-    def test_0():
+    def test_0(self):
         p1 = Point(3, 0)
         p2 = Point(0, 3)
-        l = perpendicular_bisector(p1, p2)
+        l = Geometry.perpendicular_bisector(p1, p2)
         assert l == Line(1, -1, 0)
 
-    def test_1():
+    def test_1(self):
         p1 = Point(-3, 0)
         p2 = Point(0, 3)
-        l = perpendicular_bisector(p1, p2)
+        l = Geometry.perpendicular_bisector(p1, p2)
         assert l == Line(1, 1, 0)
 
-    def test_2():
+    def test_2(self):
         p1 = Point(3, 0)
         p2 = Point(5, 0)
-        l = perpendicular_bisector(p1, p2)
+        l = Geometry.perpendicular_bisector(p1, p2)
         assert l == Line(1, 0, -4)
 
-    def test_3():
+    def test_3(self):
         p1 = Point(0, 3)
         p2 = Point(0, 5)
-        l = perpendicular_bisector(p1, p2)
+        l = Geometry.perpendicular_bisector(p1, p2)
         assert l == Line(0, 1, -4)
 
+class Testcolinear():
 
-    for i in range(4):
-        exec("test_{}()".format(i))
-
-def test_colinear():
-
-    def test_0():
+    def test_0(self):
         p1 = Point(1, 2)
         p2 = Point(3, 4)
         p3 = Point(5, 6)
-        assert geometry.colinear(p1, p2, p3)
+        assert Geometry.colinear(p1, p2, p3)
 
-    def test_1():
+    def test_1(self):
         p1 = Point(1, 2)
         p2 = Point(3, 4)
         p3 = Point(5, 7)
-        assert not geometry.colinear(p1, p2, p3)
-    
-    def test_2():
+        assert not Geometry.colinear(p1, p2, p3)
+
+    def test_2(self):
         p1 = Point(1, 0)
         p2 = Point(2, 0)
         p3 = Point(3, 0)
-        assert geometry.colinear(p1, p2, p3)
+        assert Geometry.colinear(p1, p2, p3)
+
+class TestAngleBtwLines():
+
+    def test_0(self):
+        l1 = Line(3, 4, 7)
+        l2 = Line(4, -3, 5)
+
+        assert cmath.pi/2 == Geometry.angle_btw_lines(l1, l2)
+
+    def test_1(self):
+        l1 = Line(3, 0, 0)
+        l2 = Line(4, 0, 0)
+
+        assert 0 == Geometry.angle_btw_lines(l1, l2)
 
-    for i in range(3):
-        exec("test_{}()".format(i))
-    
\ No newline at end of file
+    def test_3(self):
+        l1 = Line(0, 4, 0)
+        l2 = Line(0, 3, 0)
+        assert 0 == Geometry.angle_btw_lines(l1, l2)
\ No newline at end of file
diff --git a/test/test_triangle.py b/test/test_triangle.py
index 0b3dceac..83540443 100644
--- a/test/test_triangle.py
+++ b/test/test_triangle.py
@@ -1,139 +1,69 @@
+import cmath
 from electricpy.geometry import triangle
 from electricpy.geometry import Point
 from test import compare_points
-import cmath
-from numpy.testing import assert_almost_equal
 
-def test_centroid():
+class TestCentroid():
 
-    def test_0():
+    def test_0(self):
         p1 = Point(0, 1)
         p2 = Point(1, 0)
         p3 = Point(0, 0)
         t = triangle.Triangle(p1, p2, p3)
         assert t.centroid() == Point(1/3, 1/3)
 
-    def test_1():
+    def test_1(self):
         p1 = Point(1.1, 2.2)
         p2 = Point(3.1, 4.2)
         p3 = Point(5.1, 6.7)
         t = triangle.Triangle(p1, p2, p3)
         assert compare_points(t.centroid(), Point(3.1, 131/30))
 
-    for i in range(2):
-        exec("test_{}()".format(i))
-
 
-def test_in_center():
+class TestInCenter():
 
-    def test_0():
+    def test_0(self):
         p1 = Point(0, 1)
         p2 = Point(1, 0)
         p3 = Point(0, 0)
         t = triangle.Triangle(p1, p2, p3)
         assert compare_points(t.in_center(), Point(1/(2 + cmath.sqrt(2)), 1/(2 + cmath.sqrt(2))))
 
-    def test_1():
+    def test_1(self):
         p1 = Point(0, 0)
         p2 = Point(1, 0)
         p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3))
         t = triangle.Triangle(p1, p2, p3)
         assert compare_points(t.in_center(), Point(0.5, cmath.sqrt(3)/6))
 
-    for i in range(2):
-        exec("test_{}()".format(i))
+class TestOrthoCenter():
 
-def test_ortho_center():
-    
-    def test_0():
+    def test_0(self):
         p1 = Point(0, 1)
         p2 = Point(1, 0)
         p3 = Point(0, 0)
         t = triangle.Triangle(p1, p2, p3)
         assert compare_points(t.ortho_center(), Point(0, 0))
 
-    def test_1():
+    def test_1(self):
         p1 = Point(0, 0)
         p2 = Point(1, 0)
         p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3))
         t = triangle.Triangle(p1, p2, p3)
         assert compare_points(t.ortho_center(), Point(0.5, cmath.sqrt(3)/6))
 
-    for i in range(2):
-        exec("test_{}()".format(i))
 
-def test_circum_center(): 
-    def test_0():
+class TestCircumCenter():
+    def test_0(self):
         p1 = Point(0, 1)
         p2 = Point(1, 0)
         p3 = Point(0, 0)
         t = triangle.Triangle(p1, p2, p3)
         assert compare_points(t.circum_center(), Point(0.5, 0.5))
 
-    def test_1():
-        p1 = Point(0, 0)
-        p2 = Point(1, 0)
-        p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3))
-        t = triangle.Triangle(p1, p2, p3)
-        assert compare_points(t.circum_center(), Point(0.5, cmath.sqrt(3)/6))
-
-    for i in range(2):
-        exec("test_{}()".format(i))
-
-def test_area():
-    def test_0():
-        p1 = Point(0, 1)
-        p2 = Point(1, 0)
-        p3 = Point(0, 0)
-        t = triangle.Triangle(p1, p2, p3)
-        assert_almost_equal(t.area(), 0.5, decimal=3)
-
-    def test_1():
-        p1 = Point(0, 0)
-        p2 = Point(1, 0)
-        p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3))
-        t = triangle.Triangle(p1, p2, p3)
-        assert_almost_equal(t.area(), cmath.sqrt(3)/4)
-
-    for i in range(2):
-        exec("test_{}()".format(i))
-
-def test_in_radius():
-    def test_0():
+    def test_1(self):
         p1 = Point(0, 0)
         p2 = Point(1, 0)
         p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3))
         t = triangle.Triangle(p1, p2, p3)
-
-        assert_almost_equal(t.in_radius(), 0.5/cmath.sqrt(3))
-
-    def test_1():
-        p1 = Point(0, 0)
-        p2 = Point(1, 0)
-        p3 = Point(0, 1)
-        t = triangle.Triangle(p1, p2, p3)
-
-        assert_almost_equal(t.in_radius(), 1/(2 + cmath.sqrt(2)))
-
-    for i in range(2):
-        exec("test_{}()".format(i))
-
-def test_circum_radius():
-    def test_0():
-        p1 = Point(0, 0)
-        p2 = Point(1, 0)
-        p3 = Point(0, 1)
-        t = triangle.Triangle(p1, p2, p3)
-
-        assert_almost_equal(t.circum_radius(), 1/(2 ** 0.5))
-
-    def test_1():
-        p1 = Point(0, 0)
-        p2 = Point(1, 0)
-        p3 = Point(1*cmath.cos(cmath.pi/3), 1*cmath.sin(cmath.pi/3))
-        t = triangle.Triangle(p1, p2, p3)
-
-        assert_almost_equal(t.circum_radius(), 1/cmath.sqrt(3))
-
-    for i in range(2):
-        exec("test_{}()".format(i))
+        assert compare_points(t.circum_center(), Point(0.5, cmath.sqrt(3)/6))
\ No newline at end of file