From 7e35bd26ae1871b19a7a544af1542ebc82f77446 Mon Sep 17 00:00:00 2001 From: Axlor Date: Thu, 17 Oct 2024 19:56:33 +0800 Subject: [PATCH] Added Line methods and 'length: attribute ## Credits **Geometry Project:** For code, docs and tests: @novialriptide @Emc2356 @itzpr3d4t0r @ScriptLineStudios @avaxar @Matiiss @newpaxonian @maqa41 @blankRiot96 Also thanks to @Starbuck5 for kickstarting the idea and the occasional help! **Functionality added in this PR** - `line.c` https://github.com/pygame-community/pygame-geometry/blame/main/src_c/line.c Credits to @Emc2356 @itzpr3d4t0r @novialriptide @ScriptLineStudios @avaxar - `geometry.pyi` https://github.com/pygame-community/pygame-geometry/blame/main/geometry.pyi Credits to @Emc2356 @itzpr3d4t0r @novialriptide @ScriptLineStudios @avaxar - `geometry_test.py` https://github.com/pygame-community/pygame-geometry/blame/main/test/test_line.py Credits to @Emc2356 @itzpr3d4t0r @novialriptide @ScriptLineStudios @avaxar - `geometry.rst` https://github.com/pygame-community/pygame-geometry/blame/main/docs/line.rst Credits to @itzpr3d4t0r @avaxar @ScriptLineStudios - `geometry.h` https://github.com/pygame-community/pygame-geometry/blame/main/src_c/include/geometry.h Credits to @Emc2356 @itzpr3d4t0r @maqa41 --- buildconfig/stubs/pygame/geometry.pyi | 36 +++++ docs/reST/ref/geometry.rst | 131 +++++++++++++++ src_c/doc/geometry_doc.h | 8 + src_c/line.c | 205 ++++++++++++++++++++++++ test/geometry_test.py | 220 ++++++++++++++++++++++++++ 5 files changed, 600 insertions(+) diff --git a/buildconfig/stubs/pygame/geometry.pyi b/buildconfig/stubs/pygame/geometry.pyi index f7c1edf517..3af1a2420d 100644 --- a/buildconfig/stubs/pygame/geometry.pyi +++ b/buildconfig/stubs/pygame/geometry.pyi @@ -27,6 +27,16 @@ class _HasLineAttribute(Protocol): @property def line(self) -> Union[_LineLike, Callable[[], _LineLike]]: ... +_CanBeLine = Union[ + Rect, + Line, + Tuple[float, float, float, float], + Tuple[Point, Point], + Sequence[float], + Sequence[Point], +] +LineValue = Union[_CanBeLine, _HasLineAttribute] + _LineLike = Union[Line, SequenceLike[float], SequenceLike[Point], _HasLineAttribute] _CanBeCollided = Union[Circle, Rect, FRect, Point] @@ -168,6 +178,7 @@ class Line: def b(self) -> Tuple[float, float]: ... @b.setter def b(self, value: Point) -> None: ... + length: float @overload def __init__(self, ax: float, ay: float, bx: float, by: float) -> None: ... @overload @@ -176,3 +187,28 @@ class Line: def __init__(self, line: _LineLike) -> None: ... def __copy__(self) -> Line: ... def copy(self) -> Line: ... + + @overload + def update(self, xa: float, ya: float, xb: float, yb: float) -> None: ... + @overload + def update(self, a: Point, b: Point) -> None: ... + @overload + def update(self, single_arg: LineValue) -> None: ... + @overload + def move(self, x: float, y: float) -> Line: ... + @overload + def move(self, move_by: Point) -> Line: ... + @overload + def move_ip(self, x: float, y: float) -> None: ... + @overload + def move_ip(self, move_by: Point) -> None: ... + @overload + def scale(self, factor: float, origin: float) -> Line: ... + @overload + def scale_ip(self, factor: float, origin: float) -> None: ... + @overload + def scale(self, factor_and_origin: Sequence[float, float]) -> Line: ... + @overload + def scale_ip(self, factor_and_origin: Sequence[float, float]) -> None: ... + def flip_ab(self) -> Line: ... + def flip_ab_ip(self) -> None: ... diff --git a/docs/reST/ref/geometry.rst b/docs/reST/ref/geometry.rst index c6b3b99021..7480679d93 100644 --- a/docs/reST/ref/geometry.rst +++ b/docs/reST/ref/geometry.rst @@ -597,6 +597,18 @@ .. ## Line.b ## + .. attribute:: length + | :sl:`the length of the line` + | :sg:`length -> float` + + The length of the line. Calculated using the `sqrt((xb-xa)**2 + (yb-ya)**2)` formula. + This attribute is read-only, it cannot be reassigned. To change the line's length + use the `scale` method or change its `a` or `b` attributes. + + .. versionadded:: 2.5.3 + + .. ## Line.length ## + **Line Methods** ---- @@ -611,3 +623,122 @@ .. versionadded:: 2.5.2 .. ## Line.copy ## + + .. method:: move + + | :sl:`moves the line by a given amount` + | :sg:`move((x, y)) -> Line` + | :sg:`move(x, y) -> Line` + + Returns a new `Line` that is moved by the given offset. The original `Line` is + not modified. + + .. note:: + This method is equivalent(behaviour wise) to the following code: + + .. code-block:: python + + Line(line.ax + x, line.ay + y, line.bx + x, line.by + y) + + .. versionadded:: 2.5.3 + + .. ## Line.move ## + + .. method:: move_ip + + | :sl:`moves the line by a given amount` + | :sg:`move_ip((x, y)) -> None` + | :sg:`move_ip(x, y) -> None` + + Moves the `Line` by the given offset. The original `Line` is modified. Always returns + `None`. + + .. note:: + This method is equivalent(behaviour wise) to the following code: + + .. code-block:: python + + line.ax += x + line.ay += y + line.bx += x + line.by += y + + .. versionadded:: 2.5.3 + + .. ## Line.move_ip ## + + .. method:: update + + | :sl:`updates the line's attributes` + | :sg:`update((xa, ya), (xb, yb)) -> None` + | :sg:`update(xa, ya, xb, yb) -> None` + | :sg:`update(Line) -> None` + + Updates the `Line`'s attributes. The original `Line` is modified. Always returns `None`. + + .. note:: + This method is equivalent(behaviour wise) to the following code: + + .. code-block:: python + + line.ax = ax + line.ay = ay + line.bx = bx + line.by = by + + .. versionadded:: 2.5.3 + + .. ## Line.update ## + + .. method:: scale + + | :sl:`scales the line by the given factor from the given origin` + | :sg:`scale(factor, origin) -> Line` + | :sg:`scale(factor_and_origin) -> Line` + + Returns a new `Line` which is scaled by the given factor from the specified origin with 0.0 being + the startpoint, 0.5 being the center and 1.0 being the end point. + The original `Line` is not modified. + + .. versionadded:: 2.5.3 + + .. ## Line.scale ## + + .. method:: scale_ip + + | :sl:`scales the line by the given factor from the given origin in place` + | :sg:`scale_ip(factor, origin) -> None` + | :sg:`scale_ip(factor_and_origin) -> None` + + Scales the `Line` by the given factor from the specified origin with 0.0 being + the startpoint, 0.5 being the center and 1.0 being the end point. + The original `Line` is modified. + Always returns `None`. + + .. versionadded:: 2.5.3 + + .. ## Line.scale_ip ## + + .. method:: flip_ab + + | :sl:`flips the line a and b points` + | :sg:`flip_ab() -> Line` + + Returns a new `Line` that has the `a` and `b` points flipped. + The original `Line` is not modified. + + .. versionadded:: 2.5.3 + + .. ## Line.flip_ab ## + + .. method:: flip_ab_ip + + | :sl:`flips the line a and b points, in place` + | :sg:`flip_ab_ip() -> None` + + Flips the `Line`'s `b` and `b` points. The original `Line` is modified. + Always returns `None`. + + .. versionadded:: 2.5.3 + + .. ## Line.flip_ab_ip ## diff --git a/src_c/doc/geometry_doc.h b/src_c/doc/geometry_doc.h index dc2b2a0f1a..125dcf2e83 100644 --- a/src_c/doc/geometry_doc.h +++ b/src_c/doc/geometry_doc.h @@ -36,4 +36,12 @@ #define DOC_LINE_BY "by -> float\ny coordinate of the end point of the line" #define DOC_LINE_A "a -> (float, float)\nthe first point of the line" #define DOC_LINE_B "b -> (float, float)\nthe second point of the line" +#define DOC_LINE_LENGTH "length -> float\nthe length of the line" #define DOC_LINE_COPY "copy() -> Line\ncopies the line" +#define DOC_LINE_MOVE "move((x, y)) -> Line\nmove(x, y) -> Line\nmoves the line by a given amount" +#define DOC_LINE_MOVEIP "move_ip((x, y)) -> None\nmove_ip(x, y) -> None\nmoves the line by a given amount" +#define DOC_LINE_UPDATE "update((xa, ya), (xb, yb)) -> None\nupdate(xa, ya, xb, yb) -> None\nupdate(Line) -> None\nupdates the line's attributes" +#define DOC_LINE_SCALE "scale(factor, origin) -> Line\nscale(factor_and_origin) -> Line\nscales the line by the given factor from the given origin" +#define DOC_LINE_SCALEIP "scale_ip(factor, origin) -> None\nscale_ip(factor_and_origin) -> None\nscales the line by the given factor from the given origin in place" +#define DOC_LINE_FLIPAB "flip_ab() -> Line\nflips the line a and b points" +#define DOC_LINE_FLIPABIP "flip_ab_ip() -> None\nflips the line a and b points, in place" diff --git a/src_c/line.c b/src_c/line.c index 93987e946d..9bb5a8704b 100644 --- a/src_c/line.c +++ b/src_c/line.c @@ -1,6 +1,16 @@ #include "doc/geometry_doc.h" #include "geometry_common.h" +#define IS_LINE_VALID(line) (line->ax != line->bx || line->ay != line->by) + +static double +pgLine_Length(pgLineBase *line) +{ + double dx = line->bx - line->ax; + double dy = line->by - line->ay; + return sqrt(dx * dx + dy * dy); +} + static PyObject * _pg_line_subtype_new4(PyTypeObject *type, double ax, double ay, double bx, double by) @@ -49,6 +59,38 @@ pg_line_init(pgLineObject *self, PyObject *args, PyObject *kwds) return 0; } +static int +pgLine_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs, + pgLineBase *out) +{ + if (nargs == 1) { + return pgLine_FromObject(args[0], out); + } + else if (nargs == 2) { + if (!pg_TwoDoublesFromObj(args[0], &(out->ax), &(out->ay)) || + !pg_TwoDoublesFromObj(args[1], &(out->bx), &(out->by))) { + return 0; + } + return IS_LINE_VALID(out); + } + else if (nargs == 4) { + if (!pg_DoubleFromObj(args[0], &(out->ax)) || + !pg_DoubleFromObj(args[1], &(out->ay)) || + !pg_DoubleFromObj(args[2], &(out->bx)) || + !pg_DoubleFromObj(args[3], &(out->by))) { + return 0; + } + return IS_LINE_VALID(out); + } + return 0; +} + +static PyObject * +pgLine_New(pgLineBase *l) +{ + return _pg_line_subtype_new4(&pgLine_Type, l->ax, l->ay, l->bx, l->by); +} + static PyObject * pg_line_copy(pgLineObject *self, PyObject *_null) { @@ -56,9 +98,165 @@ pg_line_copy(pgLineObject *self, PyObject *_null) self->line.bx, self->line.by); } +static PyObject * +pg_line_update(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (!pgLine_FromObjectFastcall(args, nargs, &(self->line))) { + return RAISE(PyExc_TypeError, + "Line.update requires a line or LineLike object"); + } + Py_RETURN_NONE; +} + +static PyObject * +pg_line_move(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + double Dx, Dy; + + if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &Dx, &Dy)) { + return RAISE(PyExc_TypeError, "move requires a pair of numbers"); + } + + return _pg_line_subtype_new4(Py_TYPE(self), self->line.ax + Dx, + self->line.ay + Dy, self->line.bx + Dx, + self->line.by + Dy); +} + +static PyObject * +pg_line_move_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + double Dx, Dy; + + if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &Dx, &Dy)) { + return RAISE(PyExc_TypeError, "move_ip requires a pair of numbers"); + } + + self->line.ax += Dx; + self->line.ay += Dy; + self->line.bx += Dx; + self->line.by += Dy; + + Py_RETURN_NONE; +} + +static PyObject * +pg_line_flip(pgLineObject *self, PyObject *_null) +{ + return _pg_line_subtype_new4(Py_TYPE(self), self->line.bx, self->line.by, + self->line.ax, self->line.ay); +} + +static PyObject * +pg_line_flip_ab_ip(pgLineObject *self, PyObject *_null) +{ + double tx = self->line.bx; + double ty = self->line.by; + + self->line.bx = self->line.ax; + self->line.by = self->line.ay; + + self->line.ax = tx; + self->line.ay = ty; + + Py_RETURN_NONE; +} + +static PG_FORCEINLINE double +_lerp_helper(double start, double end, double amount) +{ + return start + (end - start) * amount; +} + +static int +_line_scale_helper(pgLineBase *line, double factor, double origin) +{ + if (factor == 1.0) { + return 1; + } + else if (factor <= 0.0) { + PyErr_SetString(PyExc_ValueError, + "Can only scale by a positive non zero number"); + return 0; + } + + if (origin < 0.0 || origin > 1.0) { + PyErr_SetString(PyExc_ValueError, "Origin must be between 0 and 1"); + return 0; + } + + double ax = line->ax; + double ay = line->ay; + double bx = line->bx; + double by = line->by; + + double x1_factor = ax * factor; + double y1_factor = ay * factor; + double x2_factor = bx * factor; + double y2_factor = by * factor; + + double fac_m_one = factor - 1; + double dx = _lerp_helper(fac_m_one * ax, fac_m_one * bx, origin); + double dy = _lerp_helper(fac_m_one * ay, fac_m_one * by, origin); + + line->ax = x1_factor - dx; + line->ay = y1_factor - dy; + line->bx = x2_factor - dx; + line->by = y2_factor - dy; + + return 1; +} + +static PyObject * +pg_line_scale(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + double factor, origin; + + if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &factor, &origin)) { + return RAISE(PyExc_TypeError, + "scale requires a sequence of two numbers"); + } + + PyObject *line; + if (!(line = pgLine_New(&self->line))) { + return NULL; + } + + if (!_line_scale_helper(&pgLine_AsLine(line), factor, origin)) { + return NULL; + } + + return line; +} + +static PyObject * +pg_line_scale_ip(pgLineObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + double factor, origin; + + if (!pg_TwoDoublesFromFastcallArgs(args, nargs, &factor, &origin)) { + return RAISE(PyExc_TypeError, + "scale_ip requires a sequence of two numbers"); + } + + if (!_line_scale_helper(&pgLine_AsLine(self), factor, origin)) { + return NULL; + } + + Py_RETURN_NONE; +} + static struct PyMethodDef pg_line_methods[] = { {"__copy__", (PyCFunction)pg_line_copy, METH_NOARGS, DOC_LINE_COPY}, {"copy", (PyCFunction)pg_line_copy, METH_NOARGS, DOC_LINE_COPY}, + {"update", (PyCFunction)pg_line_update, METH_FASTCALL, DOC_LINE_UPDATE}, + {"move", (PyCFunction)pg_line_move, METH_FASTCALL, DOC_LINE_MOVE}, + {"move_ip", (PyCFunction)pg_line_move_ip, METH_FASTCALL, DOC_LINE_MOVEIP}, + {"flip_ab", (PyCFunction)pg_line_flip, METH_NOARGS, DOC_LINE_FLIPAB}, + {"flip_ab_ip", (PyCFunction)pg_line_flip_ab_ip, METH_NOARGS, + DOC_LINE_FLIPABIP}, + {"scale", (PyCFunction)pg_line_scale, METH_FASTCALL, DOC_LINE_SCALE}, + {"scale_ip", (PyCFunction)pg_line_scale_ip, METH_FASTCALL, + DOC_LINE_SCALEIP}, {NULL, NULL, 0, NULL}}; static PyObject * @@ -170,6 +368,12 @@ pg_line_setb(pgLineObject *self, PyObject *value, void *closure) return -1; } +static PyObject * +pg_line_getlength(pgLineObject *self, void *closure) +{ + return PyFloat_FromDouble(pgLine_Length(&self->line)); +} + static PyGetSetDef pg_line_getsets[] = { {"ax", (getter)pg_line_getax, (setter)pg_line_setax, DOC_LINE_AX, NULL}, {"ay", (getter)pg_line_getay, (setter)pg_line_setay, DOC_LINE_AY, NULL}, @@ -177,6 +381,7 @@ static PyGetSetDef pg_line_getsets[] = { {"by", (getter)pg_line_getby, (setter)pg_line_setby, DOC_LINE_BY, NULL}, {"a", (getter)pg_line_geta, (setter)pg_line_seta, DOC_LINE_A, NULL}, {"b", (getter)pg_line_getb, (setter)pg_line_setb, DOC_LINE_B, NULL}, + {"length", (getter)pg_line_getlength, NULL, DOC_LINE_LENGTH, NULL}, {NULL, 0, NULL, NULL, NULL}}; static PyTypeObject pgLine_Type = { diff --git a/test/geometry_test.py b/test/geometry_test.py index d2b5fceb50..5077332589 100644 --- a/test/geometry_test.py +++ b/test/geometry_test.py @@ -1967,6 +1967,56 @@ def test_attrib_b(self): with self.assertRaises(AttributeError): del line.b + def test_attrib_length(self): + """a full test for the length attribute""" + expected_length = 3.0 + line = Line(1, 4, 4, 4) + self.assertEqual(line.length, expected_length) + + line.ax = 2 + expected_length = 2.0 + self.assertEqual(line.length, expected_length) + + line.ax = 2.7 + expected_length = 1.2999999999999998 + self.assertEqual(line.length, expected_length) + + line.ay = 2 + expected_length = 2.3853720883753127 + self.assertEqual(line.length, expected_length) + + line.ay = 2.7 + expected_length = 1.8384776310850233 + self.assertEqual(line.length, expected_length) + + line.bx = 2 + expected_length = 1.4764823060233399 + self.assertEqual(line.length, expected_length) + + line.bx = 2.7 + expected_length = 1.2999999999999998 + self.assertEqual(line.length, expected_length) + + line.by = 2 + expected_length = 0.7000000000000002 + self.assertEqual(line.length, expected_length) + + line.by = 2.7 + expected_length = 0.0 + self.assertEqual(line.length, expected_length) + + line1 = Line(7, 3, 2, 3) + line2 = Line(9, 5, 4, 5) + self.assertEqual(line1.length, line2.length) + + line = Line(7.6, 3.2, 2.1, 3.8) + expected_length = 5.532630477449222 + self.assertEqual(line.length, expected_length) + + line = Line(-9.8, -5.2, -4.4, -5.6) + expected_length = 5.414794548272353 + self.assertEqual(line.length, expected_length) + def test_meth_copy(self): line = Line(1, 2, 3, 4) # check 1 arg passed @@ -1981,6 +2031,176 @@ def test_meth_copy(self): self.assertIsNot(line, line_2) + def test_meth_move(self): + line = Line(1.1, 2.2, 3.3, 4.4) + + ret = line.move(1, 2) + + self.assertEqual(ret.ax, 2.1) + self.assertEqual(ret.ay, 4.2) + self.assertEqual(ret.bx, 4.3) + self.assertEqual(ret.by, 6.4) + + with self.assertRaises(TypeError): + line.move() + + with self.assertRaises(TypeError): + line.move(1) + + with self.assertRaises(TypeError): + line.move(1, 2, 3) + + with self.assertRaises(TypeError): + line.move("1", "2") + + def test_meth_move_ip(self): + line = Line(1.1, 2.2, 3.3, 4.4) + + line.move_ip(1, 2) + + self.assertEqual(line.ax, 2.1) + self.assertEqual(line.ay, 4.2) + self.assertEqual(line.bx, 4.3) + self.assertEqual(line.by, 6.4) + + with self.assertRaises(TypeError): + line.move_ip() + + with self.assertRaises(TypeError): + line.move_ip(1) + + with self.assertRaises(TypeError): + line.move_ip(1, 2, 3) + + with self.assertRaises(TypeError): + line.move_ip("1", "2") + + def test_meth_scale(self): + line = Line(0, 0, 10, 0).scale(2, 0) + self.assertEqual(line.length, 20) + line = Line(0, 0, 20, 0).scale(2.1, 0) + self.assertEqual(line.length, 42) + line = Line(0, 0, 10, 0).scale(4, 0) + self.assertEqual(line.length, 40) + line = Line(0, 0, 10, 0).scale(3, 0) + self.assertEqual(line.length, 30) + line = Line(10, 10, 20, 20).scale(2, 0) + self.assertEqual(line.length, 28.284271247461902) + line = Line(10, 10, 20, 20).scale(2, 0.5) + self.assertEqual(line.length, 28.284271247461902) + line = Line(10, 10, 20, 20).scale(2, 1) + self.assertEqual(line.length, 28.284271247461902) + + with self.assertRaises(ValueError): + line = line.scale(0, 0.5) + + with self.assertRaises(ValueError): + line = line.scale(2, -0.1) + + with self.assertRaises(ValueError): + line = line.scale(-2, -0.5) + + with self.assertRaises(ValueError): + line = line.scale(17, 1.1) + + with self.assertRaises(ValueError): + line = line.scale(17, 10.0) + + def test_meth_scale_ip(self): + line = Line(0, 0, 10, 0) + line.scale_ip(2, 0) + self.assertEqual(line.length, 20) + line = Line(0, 0, 20, 0) + line.scale_ip(2.1, 0) + self.assertEqual(line.length, 42) + line = Line(0, 0, 10, 0) + line.scale_ip(4, 0) + self.assertEqual(line.length, 40) + line = Line(0, 0, 10, 0) + line.scale_ip(3, 0) + self.assertEqual(line.length, 30) + line = Line(10, 10, 20, 20) + line.scale_ip(2, 0) + self.assertEqual(line.length, 28.284271247461902) + line = Line(10, 10, 20, 20) + line.scale_ip(2, 0.5) + self.assertEqual(line.length, 28.284271247461902) + line = Line(10, 10, 20, 20) + line.scale_ip(2, 1.0) + self.assertEqual(line.length, 28.284271247461902) + + with self.assertRaises(ValueError): + line.scale_ip(0, 0.5) + + with self.assertRaises(ValueError): + line.scale_ip(2, -0.1) + + with self.assertRaises(ValueError): + line.scale_ip(-2, -0.5) + + with self.assertRaises(ValueError): + line.scale_ip(17, 1.1) + + with self.assertRaises(ValueError): + line.scale_ip(17, 10.0) + + def test_meth_flip(self): + line = Line(1.1, 2.2, 3.3, 4.4) + + ret = line.flip_ab() + + self.assertIsInstance(ret, Line) + self.assertEqual(ret.ax, 3.3) + self.assertEqual(ret.ay, 4.4) + self.assertEqual(ret.bx, 1.1) + self.assertEqual(ret.by, 2.2) + + with self.assertRaises(TypeError): + line.flip_ab(1) + + def test_meth_flip_ab_ip(self): + line = Line(1.1, 2.2, 3.3, 4.4) + + line.flip_ab_ip() + + self.assertEqual(line.ax, 3.3) + self.assertEqual(line.ay, 4.4) + self.assertEqual(line.bx, 1.1) + self.assertEqual(line.by, 2.2) + + with self.assertRaises(TypeError): + line.flip_ab_ip(1) + + def test_meth_update(self): + line = Line(0, 0, 1, 1) + + line.update(1, 2, 3, 4) + self.assertEqual(line.ax, 1) + self.assertEqual(line.ay, 2) + self.assertEqual(line.bx, 3) + self.assertEqual(line.by, 4) + + line.update((5, 6), (7, 8)) + self.assertEqual(line.ax, 5) + self.assertEqual(line.ay, 6) + self.assertEqual(line.bx, 7) + self.assertEqual(line.by, 8) + + line.update((9, 10, 11, 12)) + self.assertEqual(line.ax, 9) + self.assertEqual(line.ay, 10) + self.assertEqual(line.bx, 11) + self.assertEqual(line.by, 12) + + with self.assertRaises(TypeError): + line.update() + + with self.assertRaises(TypeError): + line.update(1, 2, 3, 4, 5) + + with self.assertRaises(TypeError): + line.update(1, 2, 3) + def test__str__(self): """Checks whether the __str__ method works correctly.""" l_str = ""