Repository: chris-greening/spyrograph (language: python)

Issue:
Add a descriptive __str__ or __repr__ to the classes
**Is your feature request related to a problem? Please describe.**
Define a useful human readable __str__ and/or __repr__ in [_Trochoid](https://github.com/chris-greening/spyrograph/blob/main/spyrograph/_trochoid.py) that indicates what the shape is (i.e. the name of the class `Hypocycloid`, `Epitrochoid`, etc.) and the input parameters `R`, `r`, `d`, etc.

Example say we have a `Hypotrochoid` instance set as variable `hypotrochoid_obj` with parameters `R=5`, `r=3`, `d=1`, `thetas=[1.1 1.2, 1.3, 1.4, ... 5]`. When a user types `hypotrochoid_obj` in console or `print(hypotrochoid_obj)` they should get some sort of useful output like `Hypotrochoid(R=5, r=3, d=1, thetas = [1, 1.1 ... 5])` as opposed to just the default memory address

NOTE: `thetas` can be many, many values so perhaps just the first two values and the last to show start, stop, and the step between values


Discussion / hints:
Taking this one

Fix patch:
diff --git a/setup.py b/setup.py
index e9df1d7..e5bca35 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@
 
 setuptools.setup(
     name="spyrograph",
-    version="0.18.1",
+    version="0.18.2",
     author="Chris Greening",
     author_email="chris@christophergreening.com",
     description="Library for drawing spirographs in Python",
diff --git a/spyrograph/_trochoid.py b/spyrograph/_trochoid.py
index 8f06fa6..aaec0be 100644
--- a/spyrograph/_trochoid.py
+++ b/spyrograph/_trochoid.py
@@ -514,3 +514,16 @@ def _calculate_x(self, theta: Number) -> float:
     @abstractmethod
     def _calculate_y(self, theta: Number) -> float:
         """Return calculated y-value from parametrized equation"""
+
+    def __repr__(self) -> str:
+        """Return formatted string with useful information about the current object"""
+        # pylint: disable=line-too-long
+        if len(self.thetas) == 1:
+            thetas_str = f"[{self.thetas[0]}]"
+        elif len(self.thetas) == 2:
+            thetas_str = f"[{self.thetas[0]}, {self.thetas[1]}]"
+        elif len(self.thetas) == 3:
+            thetas_str = f"[{self.thetas[0]}, {self.thetas[1]}, {self.thetas[2]}]"
+        else:
+            thetas_str = f"[{self.thetas[0]}, {self.thetas[1]}, ... {self.thetas[-1]}]"
+        return(f"{self.__class__.__name__}(R={self.R}, r={self.r}, d={self.d}, thetas={thetas_str}, origin=({self.origin[0]},{self.origin[1]}))")


Test patch:
diff --git a/tests/_roulette.py b/tests/_roulette.py
index da90d34..33117ba 100644
--- a/tests/_roulette.py
+++ b/tests/_roulette.py
@@ -38,6 +38,16 @@ def test_invalid_arguments_exception(self, R, r, d, thetas):
                 thetas=thetas
             )
 
+    def test_dunder_repr_doesnt_break_on_repr_call(self, instance):
+        repr_val = repr(instance)
+        assert isinstance(repr_val, str)
+
+    def test_dunder_repr_values(self, instance):
+        repr_val = repr(instance)
+        assert f"R={instance.R}" in repr_val
+        assert f"r={instance.r}" in repr_val
+        assert f"d={instance.d}" in repr_val
+
     def test_scale_return_instance_is_same_class(self, instance):
         """Test that the return instance is from the same class"""
         scaled_instance = instance.scale(factor=2)