diff --git a/buildconfig/stubs/pygame/geometry.pyi b/buildconfig/stubs/pygame/geometry.pyi index 4814310b6e..a23e731c87 100644 --- a/buildconfig/stubs/pygame/geometry.pyi +++ b/buildconfig/stubs/pygame/geometry.pyi @@ -14,6 +14,11 @@ from .rect import Rect, FRect from .math import Vector2 _CanBeCircle = Union[Circle, Tuple[Point, float], SequenceLike[float]] +_CanBeLine = Union[ + Line, + SequenceLike[float], + SequenceLike[Point], +] class _HasCirclettribute(Protocol): # An object that has a circle attribute that is either a circle, or a function @@ -21,6 +26,15 @@ class _HasCirclettribute(Protocol): circle: Union[_CanBeCircle, Callable[[], _CanBeCircle]] _CircleValue = Union[_CanBeCircle, _HasCirclettribute] + +class _HasLineAttribute(Protocol): + # An object that has a line attribute that is either a line, or a function + # that returns a line + line: Union[_CanBeLine, Callable[[], _CanBeLine]] + + +_LineValue = Union[_CanBeLine, _HasLineAttribute] + _CanBeCollided = Union[Circle, Rect, FRect, Point, Vector2] _CanBeIntersected = Union[Circle] @@ -134,3 +148,37 @@ class Circle: def as_frect(self) -> FRect: ... def copy(self) -> Circle: ... def __copy__(self) -> Circle: ... + +class Line: + @property + def ax(self) -> float: ... + @ax.setter + def ax(self, value: float) -> None: ... + @property + def ay(self) -> float: ... + @ay.setter + def ay(self, value: float) -> None: ... + @property + def bx(self) -> float: ... + @bx.setter + def bx(self, value: float) -> None: ... + @property + def by(self) -> float: ... + @by.setter + def by(self, value: float) -> None: ... + @property + def a(self) -> Tuple[float, float]: ... + @a.setter + def a(self, value: Point) -> None: ... + @property + def b(self) -> Tuple[float, float]: ... + @b.setter + def b(self, value: Point) -> None: ... + @overload + def __init__(self, ax: float, ay: float, bx: float, by: float) -> None: ... + @overload + def __init__(self, a: Point, b: Point) -> None: ... + @overload + def __init__(self, line: _LineValue) -> None: ... + def __copy__(self) -> Line: ... + def copy(self) -> Line: ... diff --git a/docs/reST/ref/geometry.rst b/docs/reST/ref/geometry.rst index 3bcd81f785..c6b3b99021 100644 --- a/docs/reST/ref/geometry.rst +++ b/docs/reST/ref/geometry.rst @@ -487,3 +487,127 @@ .. ## Circle.copy ## .. ## pygame.Circle ## + + +.. class:: Line + + | :sl:`pygame object for representing a line` + | :sg:`Line((ax, ay), (bx, by)) -> Line` + | :sg:`Line(ax, ay, bx, by) -> Line` + + .. versionadded:: 2.5.2 + + The `Line` class provides many useful methods for collision testing, transformation and intersection. + A `Line` can be created from a combination of two pairs of coordinates that represent the start and end points. + Lines can also be created from python objects that are already a `Line` (effectively copying the line) or have an attribute named "line". + + Specifically, to construct a `Line` you can pass the ax, ay, bx, and by values as separate + arguments or inside a sequence(list or tuple). + + As a special case you can also pass in `pygame.Rect` / `pygame.FRect`, in which case the + line will be created with (x, y, width, height) as the start and end points. + + You can create lines with the same start and end points, but beware that some methods may + not work as expected or error out. + + Functions that require a `Line` argument may also accept these values as Lines: + + :: + + ((ax, ay), (bx, by)) + (ax, ay, bx, by) + (vector2, vector2) + + The `Line` class only stores the ax, ay, bx, and by attributes, everything else is calculated + on the fly based on them. + + **Line Attributes** + + ---- + + .. attribute:: ax + + | :sl:`x coordinate of the start point of the line` + | :sg:`ax -> float` + + The horizontal coordinate of the start point of the line. Reassigning it moves the line. + + .. versionadded:: 2.5.2 + + .. ## Line.ax ## + + .. attribute:: ay + + | :sl:`y coordinate of the start point of the line` + | :sg:`ay -> float` + + The vertical coordinate of the start point of the line. Reassigning it moves the line. + + .. versionadded:: 2.5.2 + + .. ## Line.ay ## + + .. attribute:: bx + + | :sl:`x coordinate of the end point of the line` + | :sg:`bx -> float` + + The horizontal coordinate of the end point of the line. Reassigning it moves the line. + + .. versionadded:: 2.5.2 + + .. ## Line.bx ## + + .. attribute:: by + + | :sl:`y coordinate of the end point of the line` + | :sg:`by -> float` + + The vertical coordinate of the end point of the line. Reassigning it moves the line. + + .. versionadded:: 2.5.2 + + .. ## Line.by ## + + .. attribute:: a + + | :sl:`the first point of the line` + | :sg:`a -> (float, float)` + + It's a tuple containing the `ax` and `ay` attributes representing the line's first point. + It can be reassigned to move the `Line`. If reassigned the `ax` and `ay` attributes + will be changed to produce a `Line` with matching first point position. + The `bx` and `by` attributes will not be affected. + + .. versionadded:: 2.5.2 + + .. ## Line.a ## + + .. attribute:: b + + | :sl:`the second point of the line` + | :sg:`b -> (float, float)` + + It's a tuple containing `bx` and `by` attributes representing the line's second point. + It can be reassigned to move the `Line`. If reassigned the `bx` and `by` attributes + will be changed to produce a `Line` with matching second point position. + The `ax` and `ay` attributes will not be affected. + + .. versionadded:: 2.5.2 + + .. ## Line.b ## + + **Line Methods** + + ---- + + .. method:: copy + + | :sl:`copies the line` + | :sg:`copy() -> Line` + + Returns a copy of this `Line`. + + .. versionadded:: 2.5.2 + + .. ## Line.copy ## diff --git a/src_c/_pygame.h b/src_c/_pygame.h index e87986d776..26a212fe74 100644 --- a/src_c/_pygame.h +++ b/src_c/_pygame.h @@ -536,6 +536,6 @@ typedef enum { #define PYGAMEAPI_BASE_NUMSLOTS 29 #define PYGAMEAPI_EVENT_NUMSLOTS 10 #define PYGAMEAPI_WINDOW_NUMSLOTS 1 -#define PYGAMEAPI_GEOMETRY_NUMSLOTS 1 +#define PYGAMEAPI_GEOMETRY_NUMSLOTS 2 #endif /* _PYGAME_INTERNAL_H */ diff --git a/src_c/doc/geometry_doc.h b/src_c/doc/geometry_doc.h index fbcfefaf9f..dc2b2a0f1a 100644 --- a/src_c/doc/geometry_doc.h +++ b/src_c/doc/geometry_doc.h @@ -29,3 +29,11 @@ #define DOC_CIRCLE_ASRECT "as_rect() -> Rect\nreturns the smallest Rect containing the circle" #define DOC_CIRCLE_ASFRECT "as_frect() -> FRect\nreturns the smallest FRect containing the circle" #define DOC_CIRCLE_COPY "copy() -> Circle\ncopies the circle" +#define DOC_LINE "Line((ax, ay), (bx, by)) -> Line\nLine(ax, ay, bx, by) -> Line\npygame object for representing a line" +#define DOC_LINE_AX "ax -> float\nx coordinate of the start point of the line" +#define DOC_LINE_AY "ay -> float\ny coordinate of the start point of the line" +#define DOC_LINE_BX "bx -> float\nx coordinate of the end point of the line" +#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_COPY "copy() -> Line\ncopies the line" diff --git a/src_c/geometry.c b/src_c/geometry.c index 4ccb0ad22d..d55eeb7504 100644 --- a/src_c/geometry.c +++ b/src_c/geometry.c @@ -1,4 +1,5 @@ #include "circle.c" +#include "line.c" #include "geometry_common.c" static PyMethodDef geometry_methods[] = {{NULL, NULL, 0, NULL}}; @@ -30,6 +31,10 @@ MODINIT_DEFINE(geometry) return NULL; } + if (PyType_Ready(&pgLine_Type) < 0) { + return NULL; + } + module = PyModule_Create(&_module); if (!module) { return NULL; @@ -42,7 +47,15 @@ MODINIT_DEFINE(geometry) return NULL; } + Py_INCREF(&pgLine_Type); + if (PyModule_AddObject(module, "Line", (PyObject *)&pgLine_Type)) { + Py_DECREF(&pgLine_Type); + Py_DECREF(module); + return NULL; + } + c_api[0] = &pgCircle_Type; + c_api[1] = &pgLine_Type; apiobj = encapsulate_api(c_api, "geometry"); if (PyModule_AddObject(module, PYGAMEAPI_LOCAL_ENTRY, apiobj)) { Py_XDECREF(apiobj); diff --git a/src_c/geometry.h b/src_c/geometry.h index aa9cae6ca1..6750409ced 100644 --- a/src_c/geometry.h +++ b/src_c/geometry.h @@ -15,12 +15,25 @@ typedef struct { #define pgCircle_CAST(o) ((pgCircleObject *)(o)) #define pgCircle_AsCircle(o) (pgCircle_CAST(o)->circle) -#define pgCircle_GETX(self) (pgCircle_CAST(self)->circle.x) -#define pgCircle_GETY(self) (pgCircle_CAST(self)->circle.y) -#define pgCircle_GETR(self) (pgCircle_CAST(self)->circle.r) #define pgCircle_Check(o) ((o)->ob_type == &pgCircle_Type) +typedef struct { + double ax, ay; + double bx, by; +} pgLineBase; + +typedef struct { + PyObject_HEAD pgLineBase line; + PyObject *weakreflist; +} pgLineObject; + +#define pgLine_CAST(o) ((pgLineObject *)(o)) +#define pgLine_AsLine(o) (pgLine_CAST(o)->line) +#define pgLine_Check(o) ((o)->ob_type == &pgLine_Type) + static PyTypeObject pgCircle_Type; +static PyTypeObject pgLine_Type; + /* Constants */ /* PI */ diff --git a/src_c/geometry_common.c b/src_c/geometry_common.c index 07d84f0843..4e25c32d9e 100644 --- a/src_c/geometry_common.c +++ b/src_c/geometry_common.c @@ -147,6 +147,135 @@ pgCircle_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs, } } +int +pgLine_FromObject(PyObject *obj, pgLineBase *out) +{ + Py_ssize_t length; + + if (pgLine_Check(obj)) { + *out = pgLine_AsLine(obj); + return 1; + } + + /* Paths for sequences */ + if (pgSequenceFast_Check(obj)) { + length = PySequence_Fast_GET_SIZE(obj); + PyObject **farray = PySequence_Fast_ITEMS(obj); + + if (length == 4) { + if (!pg_DoubleFromObj(farray[0], &out->ax) || + !pg_DoubleFromObj(farray[1], &out->ay) || + !pg_DoubleFromObj(farray[2], &out->bx) || + !pg_DoubleFromObj(farray[3], &out->by)) { + return 0; + } + return 1; + } + else if (length == 2) { + if (!pg_TwoDoublesFromObj(farray[0], &out->ax, &out->ay) || + !pg_TwoDoublesFromObj(farray[1], &out->bx, &out->by)) { + PyErr_Clear(); + return 0; + } + return 1; + } + else if (length == 1) /*looks like an arg?*/ { + if (PyUnicode_Check(farray[0]) || + !pgLine_FromObject(farray[0], out)) { + return 0; + } + return 1; + } + } + else if (PySequence_Check(obj)) { + length = PySequence_Length(obj); + if (length == 4) { + PyObject *tmp; + tmp = PySequence_ITEM(obj, 0); + if (!pg_DoubleFromObj(tmp, &out->ax)) { + Py_DECREF(tmp); + return 0; + } + Py_DECREF(tmp); + tmp = PySequence_ITEM(obj, 1); + if (!pg_DoubleFromObj(tmp, &out->ay)) { + Py_DECREF(tmp); + return 0; + } + Py_DECREF(tmp); + tmp = PySequence_ITEM(obj, 2); + if (!pg_DoubleFromObj(tmp, &out->bx)) { + Py_DECREF(tmp); + return 0; + } + Py_DECREF(tmp); + tmp = PySequence_ITEM(obj, 3); + if (!pg_DoubleFromObj(tmp, &out->by)) { + Py_DECREF(tmp); + return 0; + } + Py_DECREF(tmp); + return 1; + } + else if (length == 2) { + PyObject *tmp; + tmp = PySequence_ITEM(obj, 0); + if (!pg_TwoDoublesFromObj(tmp, &out->ax, &out->ay)) { + Py_DECREF(tmp); + return 0; + } + Py_DECREF(tmp); + tmp = PySequence_ITEM(obj, 1); + if (!pg_TwoDoublesFromObj(tmp, &out->bx, &out->by)) { + Py_DECREF(tmp); + return 0; + } + Py_DECREF(tmp); + return 1; + } + else if (PyTuple_Check(obj) && length == 1) /*looks like an arg?*/ { + PyObject *sub = PySequence_ITEM(obj, 0); + if (PyUnicode_Check(sub) || !pgLine_FromObject(sub, out)) { + Py_DECREF(sub); + return 0; + } + Py_DECREF(sub); + return 1; + } + else { + return 0; + } + } + + /* Path for objects that have a line attribute */ + PyObject *lineattr; + if (!(lineattr = PyObject_GetAttrString(obj, "line"))) { + PyErr_Clear(); + return 0; + } + + if (PyCallable_Check(lineattr)) /*call if it's a method*/ + { + PyObject *lineresult = PyObject_CallNoArgs(lineattr); + Py_DECREF(lineattr); + if (!lineresult) { + PyErr_Clear(); + return 0; + } + lineattr = lineresult; + } + + if (!pgLine_FromObject(lineattr, out)) { + PyErr_Clear(); + Py_DECREF(lineattr); + return 0; + } + + Py_DECREF(lineattr); + + return 1; +} + static inline int double_compare(double a, double b) { diff --git a/src_c/geometry_common.h b/src_c/geometry_common.h index 115f248768..8ecc9b259a 100644 --- a/src_c/geometry_common.h +++ b/src_c/geometry_common.h @@ -13,6 +13,9 @@ int pgCircle_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs, pgCircleBase *out); +int +pgLine_FromObject(PyObject *obj, pgLineBase *out); + static inline int double_compare(double a, double b); diff --git a/src_c/line.c b/src_c/line.c new file mode 100644 index 0000000000..93987e946d --- /dev/null +++ b/src_c/line.c @@ -0,0 +1,195 @@ +#include "doc/geometry_doc.h" +#include "geometry_common.h" + +static PyObject * +_pg_line_subtype_new4(PyTypeObject *type, double ax, double ay, double bx, + double by) +{ + pgLineObject *line = (pgLineObject *)pgLine_Type.tp_new(type, NULL, NULL); + + if (line) { + line->line.ax = ax; + line->line.ay = ay; + line->line.bx = bx; + line->line.by = by; + } + return (PyObject *)line; +} + +static PyObject * +pg_line_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + pgLineObject *self = (pgLineObject *)type->tp_alloc(type, 0); + + if (self != NULL) + memset(&self->line, 0, sizeof(pgLineBase)); + + return (PyObject *)self; +} + +static void +pg_line_dealloc(pgLineObject *self) +{ + if (self->weakreflist != NULL) { + PyObject_ClearWeakRefs((PyObject *)self); + } + + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static int +pg_line_init(pgLineObject *self, PyObject *args, PyObject *kwds) +{ + if (!pgLine_FromObject(args, &(self->line))) { + PyErr_SetString(PyExc_TypeError, + "Invalid line end points, expected 4 " + "numbers or 2 sequences of 2 numbers"); + return -1; + } + return 0; +} + +static PyObject * +pg_line_copy(pgLineObject *self, PyObject *_null) +{ + return _pg_line_subtype_new4(Py_TYPE(self), self->line.ax, self->line.ay, + self->line.bx, self->line.by); +} + +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}, + {NULL, NULL, 0, NULL}}; + +static PyObject * +pg_line_repr(pgLineObject *self) +{ + PyObject *result, *ax, *ay, *bx, *by; + + ax = PyFloat_FromDouble(self->line.ax); + if (!ax) { + return NULL; + } + ay = PyFloat_FromDouble(self->line.ay); + if (!ay) { + Py_DECREF(ax); + return NULL; + } + bx = PyFloat_FromDouble(self->line.bx); + if (!bx) { + Py_DECREF(ax); + Py_DECREF(ay); + return NULL; + } + by = PyFloat_FromDouble(self->line.by); + if (!by) { + Py_DECREF(ax); + Py_DECREF(ay); + Py_DECREF(bx); + return NULL; + } + + result = + PyUnicode_FromFormat("", ax, ay, bx, by); + + Py_DECREF(ax); + Py_DECREF(ay); + Py_DECREF(bx); + Py_DECREF(by); + + return result; +} + +static PyObject * +pg_line_str(pgLineObject *self) +{ + return pg_line_repr(self); +} + +#define __LINE_GETSET_NAME(name) \ + static PyObject *pg_line_get##name(pgLineObject *self, void *closure) \ + { \ + return PyFloat_FromDouble(self->line.name); \ + } \ + static int pg_line_set##name(pgLineObject *self, PyObject *value, \ + void *closure) \ + { \ + double val; \ + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); \ + if (pg_DoubleFromObj(value, &val)) { \ + self->line.name = val; \ + return 0; \ + } \ + PyErr_SetString(PyExc_TypeError, "Expected a number"); \ + return -1; \ + } + +__LINE_GETSET_NAME(ax) +__LINE_GETSET_NAME(ay) +__LINE_GETSET_NAME(bx) +__LINE_GETSET_NAME(by) +#undef __LINE_GETSET_NAME + +static PyObject * +pg_line_geta(pgLineObject *self, void *closure) +{ + return pg_tuple_couple_from_values_double(self->line.ax, self->line.ay); +} + +static int +pg_line_seta(pgLineObject *self, PyObject *value, void *closure) +{ + double x, y; + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); + if (pg_TwoDoublesFromObj(value, &x, &y)) { + self->line.ax = x; + self->line.ay = y; + return 0; + } + PyErr_SetString(PyExc_TypeError, "Expected a sequence of 2 numbers"); + return -1; +} + +static PyObject * +pg_line_getb(pgLineObject *self, void *closure) +{ + return pg_tuple_couple_from_values_double(self->line.bx, self->line.by); +} + +static int +pg_line_setb(pgLineObject *self, PyObject *value, void *closure) +{ + double x, y; + DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value); + if (pg_TwoDoublesFromObj(value, &x, &y)) { + self->line.bx = x; + self->line.by = y; + return 0; + } + PyErr_SetString(PyExc_TypeError, "Expected a sequence of 2 numbers"); + return -1; +} + +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}, + {"bx", (getter)pg_line_getbx, (setter)pg_line_setbx, DOC_LINE_BX, NULL}, + {"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}, + {NULL, 0, NULL, NULL, NULL}}; + +static PyTypeObject pgLine_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "pygame.geometry.Line", + .tp_basicsize = sizeof(pgLineObject), + .tp_dealloc = (destructor)pg_line_dealloc, + .tp_repr = (reprfunc)pg_line_repr, + .tp_str = (reprfunc)pg_line_str, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = DOC_LINE, + .tp_weaklistoffset = offsetof(pgLineObject, weakreflist), + .tp_methods = pg_line_methods, + .tp_getset = pg_line_getsets, + .tp_init = (initproc)pg_line_init, + .tp_new = pg_line_new, +}; diff --git a/test/geometry_test.py b/test/geometry_test.py index c951bb3fac..d2b5fceb50 100644 --- a/test/geometry_test.py +++ b/test/geometry_test.py @@ -3,7 +3,7 @@ from math import sqrt from pygame import Vector2, Vector3, Rect, FRect -from pygame.geometry import Circle +from pygame.geometry import Circle, Line def float_range(a, b, step): @@ -1650,5 +1650,351 @@ def test_intersect(self): self.assertEqual([(14.0, 10.0)], c.intersect(c5)) +class LineTypeTest(unittest.TestCase): + class ClassWithLineAttrib: + def __init__(self, line): + self.line = line + + class ClassWithLineProperty: + def __init__(self, line): + self._line = line + + @property + def line(self): + return self._line + + class ClassWithLineFunction: + def __init__(self, line): + self._line = line + + def line(self): + return self._line + + def testConstruction_invalid_type(self): + """Checks whether passing wrong types to the constructor + raises the appropriate errors + """ + invalid_types = (None, [], "1", (1,), [1, 2, 3], Vector2(1, 1)) + + # Test ax + for value in invalid_types: + with self.assertRaises(TypeError): + Line(value, 0, 1, 2) + # Test ay + for value in invalid_types: + with self.assertRaises(TypeError): + Line(0, value, 1, 2) + # Test bx + for value in invalid_types: + with self.assertRaises(TypeError): + Line(0, 0, value, 2) + # Test by + for value in invalid_types: + with self.assertRaises(TypeError): + Line(0, 1, 2, value) + + # Test ax + for value in invalid_types: + with self.assertRaises(TypeError): + Line((value, 0), (1, 2)) + # Test ay + for value in invalid_types: + with self.assertRaises(TypeError): + Line((0, value), (1, 2)) + # Test bx + for value in invalid_types: + with self.assertRaises(TypeError): + Line((0, 0), (value, 2)) + # Test by + for value in invalid_types: + with self.assertRaises(TypeError): + Line((0, 1), (2, value)) + + # Test ax + for value in invalid_types: + with self.assertRaises(TypeError): + Line(((value, 0), (1, 2))) + # Test ay + for value in invalid_types: + with self.assertRaises(TypeError): + Line(((0, value), (1, 2))) + # Test bx + for value in invalid_types: + with self.assertRaises(TypeError): + Line(((0, 0), (value, 2))) + # Test by + for value in invalid_types: + with self.assertRaises(TypeError): + Line(((0, 1), (2, value))) + + def testConstruction_invalid_arguments_number(self): + """Checks whether passing the wrong number of arguments to the constructor + raises the appropriate errors + """ + arguments = ( + (1,), # one non vec3 non circle arg + (1, 1), # two args + (1, 1, 1), # three args + (1, 1, 1, 1, 1), # five args + ) + + for arg_seq in arguments: + with self.assertRaises(TypeError): + Line(*arg_seq) + + def testConstructionaxabyxby_float(self): + """Tests the construction of a line with 4 float arguments""" + line = Line(1.0, 2.0, 3.0, 4.0) + + self.assertEqual(line.ax, 1.0) + self.assertEqual(line.ay, 2.0) + self.assertEqual(line.bx, 3.0) + self.assertEqual(line.by, 4.0) + + def testConstructionTUP_axabyxby_float(self): + """Tests the construction of a line with a tuple of 4 float arguments""" + line = Line((1.0, 2.0, 3.0, 4.0)) + + self.assertEqual(line.ax, 1.0) + self.assertEqual(line.ay, 2.0) + self.assertEqual(line.bx, 3.0) + self.assertEqual(line.by, 4.0) + + def testConstructionaxabyxby_int(self): + """Tests the construction of a line with 4 int arguments""" + line = Line(1, 2, 3, 4) + + self.assertEqual(line.ax, 1.0) + self.assertEqual(line.ay, 2.0) + self.assertEqual(line.bx, 3.0) + self.assertEqual(line.by, 4.0) + + def testConstructionTUP_axabyxby_int(self): + """Tests the construction of a line with a tuple of 4 int arguments""" + line = Line((1, 2, 3, 4)) + + self.assertEqual(line.ax, 1.0) + self.assertEqual(line.ay, 2.0) + self.assertEqual(line.bx, 3.0) + self.assertEqual(line.by, 4.0) + + def testConstruction_class_with_line_attrib(self): + """Tests the construction of a line with a class that has a line attribute""" + class_ = self.ClassWithLineAttrib(Line(1.1, 2.2, 3.3, 4.4)) + + line = Line(class_) + + self.assertEqual(line.ax, 1.1) + self.assertEqual(line.ay, 2.2) + self.assertEqual(line.bx, 3.3) + self.assertEqual(line.by, 4.4) + + def testConstruction_class_with_line_property(self): + """Tests the construction of a line with a class that has a line property""" + class_ = self.ClassWithLineProperty(Line(1.1, 2.2, 3.3, 4.4)) + + line = Line(class_) + + self.assertEqual(line.ax, 1.1) + self.assertEqual(line.ay, 2.2) + self.assertEqual(line.bx, 3.3) + self.assertEqual(line.by, 4.4) + + def testConstruction_class_with_line_function(self): + """Tests the construction of a line with a class that has a line function""" + class_ = self.ClassWithLineFunction(Line(1.1, 2.2, 3.3, 4.4)) + + line = Line(class_) + + self.assertEqual(line.ax, 1.1) + self.assertEqual(line.ay, 2.2) + self.assertEqual(line.bx, 3.3) + self.assertEqual(line.by, 4.4) + + def test_attrib_x1(self): + """a full test for the ax attribute""" + expected_x1 = 10.0 + expected_y1 = 2.0 + expected_x2 = 5.0 + expected_y2 = 6.0 + line = Line(1, expected_y1, expected_x2, expected_y2) + + line.ax = expected_x1 + + self.assertEqual(line.ax, expected_x1) + self.assertEqual(line.ay, expected_y1) + self.assertEqual(line.bx, expected_x2) + self.assertEqual(line.by, expected_y2) + + line = Line(0, 0, 1, 0) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + line.ax = value + + line = Line(0, 0, 1, 0) + + with self.assertRaises(AttributeError): + del line.ax + + def test_attrib_y1(self): + """a full test for the ay attribute""" + expected_x1 = 10.0 + expected_y1 = 2.0 + expected_x2 = 5.0 + expected_y2 = 6.0 + line = Line(expected_x1, 1, expected_x2, expected_y2) + + line.ay = expected_y1 + + self.assertEqual(line.ax, expected_x1) + self.assertEqual(line.ay, expected_y1) + self.assertEqual(line.bx, expected_x2) + self.assertEqual(line.by, expected_y2) + + line = Line(0, 0, 1, 0) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + line.ay = value + + line = Line(0, 0, 1, 0) + + with self.assertRaises(AttributeError): + del line.ay + + def test_attrib_x2(self): + """a full test for the ay attribute""" + expected_x1 = 10.0 + expected_y1 = 2.0 + expected_x2 = 5.0 + expected_y2 = 6.0 + line = Line(expected_x1, expected_y1, 1, expected_y2) + + line.bx = expected_x2 + + self.assertEqual(line.ax, expected_x1) + self.assertEqual(line.ay, expected_y1) + self.assertEqual(line.bx, expected_x2) + self.assertEqual(line.by, expected_y2) + + line = Line(0, 0, 1, 0) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + line.bx = value + + line = Line(0, 0, 1, 0) + + with self.assertRaises(AttributeError): + del line.bx + + def test_attrib_y2(self): + """a full test for the ay attribute""" + expected_x1 = 10.0 + expected_y1 = 2.0 + expected_x2 = 5.0 + expected_y2 = 6.0 + line = Line(expected_x1, expected_y1, expected_x2, 1) + + line.by = expected_y2 + + self.assertEqual(line.ax, expected_x1) + self.assertEqual(line.ay, expected_y1) + self.assertEqual(line.bx, expected_x2) + self.assertEqual(line.by, expected_y2) + + line = Line(0, 0, 1, 0) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + line.by = value + + line = Line(0, 0, 1, 0) + + with self.assertRaises(AttributeError): + del line.by + + def test_attrib_a(self): + """a full test for the ay attribute""" + expected_x1 = 10.0 + expected_y1 = 2.0 + expected_x2 = 5.0 + expected_y2 = 6.0 + expected_a = expected_x1, expected_y1 + expected_b = expected_x2, expected_y2 + line = Line((0, 1), expected_b) + + line.a = expected_a + + self.assertEqual(line.a, expected_a) + self.assertEqual(line.b, expected_b) + + line = Line(0, 0, 1, 0) + + for value in (None, [], "1", (1,), [1, 2, 3], 1): + with self.assertRaises(TypeError): + line.a = value + + line = Line(0, 0, 1, 0) + + with self.assertRaises(AttributeError): + del line.a + + def test_attrib_b(self): + """a full test for the ay attribute""" + expected_x1 = 10.0 + expected_y1 = 2.0 + expected_x2 = 5.0 + expected_y2 = 6.0 + expected_a = expected_x1, expected_y1 + expected_b = expected_x2, expected_y2 + line = Line(expected_a, (0, 1)) + + line.b = expected_b + + self.assertEqual(line.a, expected_a) + self.assertEqual(line.b, expected_b) + + line = Line(0, 0, 1, 0) + + for value in (None, [], "1", (1,), [1, 2, 3], 1): + with self.assertRaises(TypeError): + line.b = value + + line = Line(0, 0, 1, 0) + + with self.assertRaises(AttributeError): + del line.b + + def test_meth_copy(self): + line = Line(1, 2, 3, 4) + # check 1 arg passed + with self.assertRaises(TypeError): + line.copy(10) + + line_2 = line.copy() + self.assertEqual(line.ax, line_2.ax) + self.assertEqual(line.by, line_2.by) + self.assertEqual(line.bx, line_2.bx) + self.assertEqual(line.by, line_2.by) + + self.assertIsNot(line, line_2) + + def test__str__(self): + """Checks whether the __str__ method works correctly.""" + l_str = "" + line = Line(10.1, 10.2, 4.3, 56.4) + self.assertEqual(str(line), l_str) + self.assertEqual(line.__str__(), l_str) + + def test__repr__(self): + """Checks whether the __repr__ method works correctly.""" + l_repr = "" + line = Line(10.1, 10.2, 4.3, 56.4) + self.assertEqual(repr(line), l_repr) + self.assertEqual(line.__repr__(), l_repr) + + if __name__ == "__main__": unittest.main()