├── .coveragerc ├── .gitignore ├── AUTHORS.txt ├── CHANGES.txt ├── LICENSE.txt ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── modules.rst ├── pyrect.rst └── roadmap.rst ├── pyrect └── __init__.py ├── setup.py ├── tests └── test_pyrect.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | 3 | 4 | 5 | # To run coverage tests, run `coverage run tests\test_pyrect.py` then `coverage html`, report will be in htmlconv\index.html 6 | 7 | 8 | 9 | [run] 10 | 11 | 12 | 13 | [report] 14 | # Regexes for lines to exclude from consideration 15 | exclude_lines = 16 | # Have to re-enable the standard pragma 17 | pragma: no cover 18 | 19 | # Don't complain if tests don't hit defensive assertion code: 20 | raise AssertionError 21 | raise NotImplementedError 22 | 23 | # Don't complain if non-runnable code isn't run: 24 | if 0: 25 | if __name__ == .__main__.: 26 | 27 | ignore_errors = True 28 | 29 | 30 | [html] 31 | directory = htmlcov -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .tox/ 3 | 4 | *.pyc 5 | __pycache__/ 6 | 7 | instance/ 8 | 9 | .pytest_cache/ 10 | .coverage 11 | htmlcov/ 12 | 13 | dist/ 14 | build/ 15 | *.egg-info/ -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- 2 | people who have submitted patches, reported bugs, added translations, helped 3 | answer newbie questions, and generally made PyRect that much better: 4 | 5 | Al Sweigart https://github.com/asweigart/ 6 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.0.4, 2019/01/03 -- Added named tuples. 2 | v0.0.3, 2018/08/28 -- Corrections to readme and pypi page. 3 | v0.0.2, 2018/08/27 -- Documentation and unit test updates. 4 | v0.0.1, 2018/XX/XX -- Initial release. 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Al Sweigart 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the PyBresenham nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | PyRect 3 | ====== 4 | PyRect is a simple module with a Rect class for Pygame-like rectangular areas. 5 | 6 | This module is like a stand-alone version of Pygame's Rect class. It is similar to the Rect module by Simon Wittber, but compatible with both Python 2 and 3. 7 | 8 | Currently under development, though the basic features work. 9 | 10 | Installation 11 | ============ 12 | 13 | ``pip install pyrect`` 14 | 15 | Quickstart Guide 16 | ================ 17 | 18 | First, create a Rect object by providing the XY coordinates of its top-left corner, and then the width and height: 19 | 20 | >>> import pyrect 21 | >>> r = pyrect.Rect(0, 0, 10, 20) 22 | 23 | There are several attributes that are automatically calculated (they have the same names as Pygame's Rect objects): 24 | 25 | >>> r.width, r.height, r.size 26 | (10, 20, (10, 20)) 27 | >>> r. left 28 | 0 29 | >>> r.right 30 | 10 31 | >>> r.top 32 | 0 33 | >>> r.bottom 34 | 20 35 | >>> r.center 36 | (5, 10) 37 | >>> r.topleft 38 | (0, 0) 39 | >>> r.topright 40 | (10, 0) 41 | >>> r.midleft 42 | (0, 10) 43 | 44 | Changing these attributes re-calculates the others. The top-left corner is anchored for any growing or shrinking that takes place. 45 | 46 | >>> r.topleft 47 | (0, 0) 48 | >>> r.left = 100 49 | >>> r.topleft 50 | (100, 0) 51 | >>> r.topright 52 | (110, 0) 53 | >>> r.width = 30 54 | >>> r.topright 55 | (130, 0) 56 | 57 | Rect objects are locked to integers, unless you set `enableFloat` to `True`: 58 | 59 | >>> r = pyrect.Rect(0, 0, 10, 20) 60 | >>> r.width = 10.5 61 | >>> r.width 62 | 10 63 | >>> r.enableFloat = True 64 | >>> r.width = 10.5 65 | >>> r.width 66 | 10.5 67 | >>> r2 = pyrect.Rect(0, 0, 10.5, 20.5, enableFloat=True) 68 | >>> r2.size 69 | (10.5, 20.5) 70 | 71 | Rect Attributes 72 | =============== 73 | 74 | Rect objects have several attributes that can be read or modified. They are identical to Pygame's Rect objects: 75 | 76 | ``x, y`` 77 | 78 | ``top, left, bottom, right`` 79 | 80 | ``topleft, bottomleft, topright, bottomright`` 81 | 82 | ``midtop, midleft, midbottom, midright`` 83 | 84 | ``center, centerx, centery`` 85 | 86 | ``size, width, height`` 87 | 88 | ``w, h`` 89 | 90 | There are a couple other attributes as well: 91 | 92 | ``box (a tuple (left, top, width, height))`` 93 | 94 | ``area (read-only)`` 95 | 96 | ``perimeter (read-only)`` 97 | 98 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = PyRect 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'PyRect' 23 | copyright = '2018, Al Sweigart' 24 | author = 'Al Sweigart' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc' 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = '.rst' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | # 60 | # This is also used if you do content translation via gettext catalogs. 61 | # Usually you set "language" from the command line for these cases. 62 | language = None 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | # This pattern also affects html_static_path and html_extra_path . 67 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 68 | 69 | # The name of the Pygments (syntax highlighting) style to use. 70 | pygments_style = 'sphinx' 71 | 72 | 73 | # -- Options for HTML output ------------------------------------------------- 74 | 75 | # The theme to use for HTML and HTML Help pages. See the documentation for 76 | # a list of builtin themes. 77 | # 78 | html_theme = 'alabaster' 79 | 80 | # Theme options are theme-specific and customize the look and feel of a theme 81 | # further. For a list of options available for each theme, see the 82 | # documentation. 83 | # 84 | # html_theme_options = {} 85 | 86 | # Add any paths that contain custom static files (such as style sheets) here, 87 | # relative to this directory. They are copied after the builtin static files, 88 | # so a file named "default.css" will overwrite the builtin "default.css". 89 | html_static_path = ['_static'] 90 | 91 | # Custom sidebar templates, must be a dictionary that maps document names 92 | # to template names. 93 | # 94 | # The default sidebars (for documents that don't match any pattern) are 95 | # defined by theme itself. Builtin themes are using these templates by 96 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 97 | # 'searchbox.html']``. 98 | # 99 | # html_sidebars = {} 100 | 101 | 102 | # -- Options for HTMLHelp output --------------------------------------------- 103 | 104 | # Output file base name for HTML help builder. 105 | htmlhelp_basename = 'PyRectdoc' 106 | 107 | 108 | # -- Options for LaTeX output ------------------------------------------------ 109 | 110 | latex_elements = { 111 | # The paper size ('letterpaper' or 'a4paper'). 112 | # 113 | # 'papersize': 'letterpaper', 114 | 115 | # The font size ('10pt', '11pt' or '12pt'). 116 | # 117 | # 'pointsize': '10pt', 118 | 119 | # Additional stuff for the LaTeX preamble. 120 | # 121 | # 'preamble': '', 122 | 123 | # Latex figure (float) alignment 124 | # 125 | # 'figure_align': 'htbp', 126 | } 127 | 128 | # Grouping the document tree into LaTeX files. List of tuples 129 | # (source start file, target name, title, 130 | # author, documentclass [howto, manual, or own class]). 131 | latex_documents = [ 132 | (master_doc, 'PyRect.tex', 'PyRect Documentation', 133 | 'Al Sweigart', 'manual'), 134 | ] 135 | 136 | 137 | # -- Options for manual page output ------------------------------------------ 138 | 139 | # One entry per manual page. List of tuples 140 | # (source start file, name, description, authors, manual section). 141 | man_pages = [ 142 | (master_doc, 'pyrect', 'PyRect Documentation', 143 | [author], 1) 144 | ] 145 | 146 | 147 | # -- Options for Texinfo output ---------------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | (master_doc, 'PyRect', 'PyRect Documentation', 154 | author, 'PyRect', 'One line description of project.', 155 | 'Miscellaneous'), 156 | ] -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. PyRect documentation master file, created by 2 | sphinx-quickstart on Fri Aug 24 15:06:58 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ================================== 7 | Welcome to PyRect's documentation! 8 | ================================== 9 | 10 | Rect provides a simple Rect class for Pygame-like rectangular areas. 11 | 12 | This module is like a stand-alone version of Pygame's `Rect` class. The `PyRect` module uses a new codebase, but is similar to the original Rect module by Simon Wittber. `PyRect` is compatible with both Python 2 and 3. 13 | 14 | Currently under development, though the basic features work. 15 | 16 | Installation 17 | ============ 18 | 19 | You can install PyRect with pip: 20 | 21 | pip install pyrect 22 | 23 | Example Usage 24 | ============= 25 | 26 | First, create a `Rect` object by providing the XY coordinates of its top-left corner, and then the width and height: 27 | 28 | >>> import pyrect 29 | >>> r = pyrect.Rect(0, 0, 10, 20) 30 | 31 | There are several attributes that are automatically calculated (they have the same names as Pygame's `Rect` objects): 32 | 33 | >>> r.width, r.height, r.size 34 | (10, 20, (10, 20)) 35 | >>> r. left 36 | 0 37 | >>> r.right 38 | 10 39 | >>> r.top 40 | 0 41 | >>> r.bottom 42 | 20 43 | >>> r.center 44 | (5, 10) 45 | >>> r.topleft 46 | (0, 0) 47 | >>> r.topright 48 | (10, 0) 49 | >>> r.midleft 50 | (0, 10) 51 | 52 | Changing these attributes re-calculates the others. The top-left corner is anchored for any growing or shrinking that takes place. 53 | 54 | >>> r.topleft 55 | (0, 0) 56 | >>> r.left = 100 57 | >>> r.topleft 58 | (100, 0) 59 | >>> r.topright 60 | (110, 0) 61 | >>> r.width = 30 62 | >>> r.topright 63 | (130, 0) 64 | 65 | Rect objects are locked to integers, unless you set `enableFloat` to `True`: 66 | 67 | >>> r = pyrect.Rect(0, 0, 10, 20) 68 | >>> r.width = 10.5 69 | >>> r.width 70 | 10 71 | >>> r.enableFloat = True 72 | >>> r.width = 10.5 73 | >>> r.width 74 | 10.5 75 | >>> r2 = pyrect.Rect(0, 0, 10.5, 20.5, enableFloat=True) 76 | >>> r2.size 77 | (10.5, 20.5) 78 | 79 | Rect Attributes 80 | =============== 81 | 82 | Rect objects have several attributes that can be read or modified. They are identical to Pygame's `Rect` objects: 83 | 84 | :: 85 | 86 | x, y 87 | top, left, bottom, right 88 | topleft, bottomleft, topright, bottomright 89 | midtop, midleft, midbottom, midright 90 | center, centerx, centery 91 | size, width, height 92 | w,h 93 | 94 | There are a couple other attributes as well: 95 | 96 | :: 97 | 98 | box (a tuple (left, top, width, height)) 99 | area (read-only) 100 | 101 | Note that a `Rect` object 102 | 103 | 104 | .. toctree:: 105 | :maxdepth: 2 106 | :caption: Contents: 107 | 108 | pyrect.rst 109 | modules.rst 110 | roadmap.rst 111 | 112 | 113 | 114 | Indices and tables 115 | ================== 116 | 117 | * :ref:`genindex` 118 | * :ref:`modindex` 119 | * :ref:`search` 120 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=PyRect 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | pyrect 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pyrect 8 | -------------------------------------------------------------------------------- /docs/pyrect.rst: -------------------------------------------------------------------------------- 1 | pyrect package 2 | ============== 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: pyrect 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | -------------------------------------------------------------------------------- /docs/roadmap.rst: -------------------------------------------------------------------------------- 1 | Road Map 2 | ======== 3 | 4 | The following functions are still unimplemented as of version 0.0.2: 5 | 6 | * `collideAny(self, rectsOrPoints)` 7 | * `collideAll(self, rectsOrPoints)` 8 | -------------------------------------------------------------------------------- /pyrect/__init__.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import collections 3 | 4 | # TODO - finish doc tests 5 | 6 | # TODO - unit tests needed for get/set and Box named tuple 7 | 8 | __version__ = "0.2.0" 9 | 10 | 11 | # Constants for rectangle attributes: 12 | TOP = "top" 13 | BOTTOM = "bottom" 14 | LEFT = "left" 15 | RIGHT = "right" 16 | TOPLEFT = "topleft" 17 | TOPRIGHT = "topright" 18 | BOTTOMLEFT = "bottomleft" 19 | BOTTOMRIGHT = "bottomright" 20 | MIDTOP = "midtop" 21 | MIDRIGHT = "midright" 22 | MIDLEFT = "midleft" 23 | MIDBOTTOM = "midbottom" 24 | CENTER = "center" 25 | CENTERX = "centerx" 26 | CENTERY = "centery" 27 | WIDTH = "width" 28 | HEIGHT = "height" 29 | SIZE = "size" 30 | BOX = "box" 31 | AREA = "area" 32 | PERIMETER = "perimeter" 33 | 34 | Box = collections.namedtuple("Box", "left top width height") 35 | Point = collections.namedtuple("Point", "x y") 36 | Size = collections.namedtuple("Size", "width height") 37 | 38 | 39 | class PyRectException(Exception): 40 | """ 41 | This class exists for PyRect exceptions. If the PyRect module raises any 42 | non-PyRectException exceptions, this indicates there's a bug in PyRect. 43 | """ 44 | 45 | pass 46 | 47 | 48 | def _checkForIntOrFloat(arg): 49 | """Raises an exception if arg is not an int or float. Always returns None.""" 50 | if not isinstance(arg, (int, float)): 51 | raise PyRectException( 52 | "argument must be int or float, not %s" % (arg.__class__.__name__) 53 | ) 54 | 55 | 56 | def _checkForInt(arg): 57 | """Raises an exception if arg is not an int. Always returns None.""" 58 | if not isinstance(arg, int): 59 | raise PyRectException( 60 | "argument must be int or float, not %s" % (arg.__class__.__name__) 61 | ) 62 | 63 | 64 | def _checkForTwoIntOrFloatTuple(arg): 65 | try: 66 | if not isinstance(arg[0], (int, float)) or not isinstance(arg[1], (int, float)): 67 | raise PyRectException( 68 | "argument must be a two-item tuple containing int or float values" 69 | ) 70 | except: 71 | raise PyRectException( 72 | "argument must be a two-item tuple containing int or float values" 73 | ) 74 | 75 | 76 | def _checkForFourIntOrFloatTuple(arg): 77 | try: 78 | if ( 79 | not isinstance(arg[0], (int, float)) 80 | or not isinstance(arg[1], (int, float)) 81 | or not isinstance(arg[2], (int, float)) 82 | or not isinstance(arg[3], (int, float)) 83 | ): 84 | raise PyRectException( 85 | "argument must be a four-item tuple containing int or float values" 86 | ) 87 | except: 88 | raise PyRectException( 89 | "argument must be a four-item tuple containing int or float values" 90 | ) 91 | 92 | 93 | def _collides(rectOrPoint1, rectOrPoint2): 94 | """Returns True if rectOrPoint1 and rectOrPoint2 collide with each other.""" 95 | 96 | 97 | def _getRectsAndPoints(rectsOrPoints): 98 | points = [] 99 | rects = [] 100 | for rectOrPoint in rectsOrPoints: 101 | try: 102 | _checkForTwoIntOrFloatTuple(rectOrPoint) 103 | points.append(rectOrPoint) 104 | except PyRectException: 105 | try: 106 | _checkForFourIntOrFloatTuple(rectOrPoint) 107 | except: 108 | raise PyRectException("argument is not a point or a rect tuple") 109 | rects.append(rectOrPoint) 110 | return (rects, points) 111 | 112 | 113 | ''' 114 | def collideAnyBetween(rectsOrPoints): 115 | """Returns True if any of the (x, y) or (left, top, width, height) tuples 116 | in rectsOrPoints collides with any other point or box tuple in rectsOrPoints. 117 | 118 | >>> p1 = (50, 50) 119 | >>> p2 = (100, 100) 120 | >>> p3 = (50, 200) 121 | >>> r1 = (-50, -50, 20, 20) 122 | >>> r2 = (25, 25, 50, 50) 123 | >>> collideAnyBetween([p1, p2, p3, r1, r2]) # p1 and r2 collide 124 | True 125 | >>> collideAnyBetween([p1, p2, p3, r1]) 126 | False 127 | """ 128 | # TODO - needs to be complete 129 | 130 | # split up 131 | rects, points = _getRectsAndPoints(rectsOrPoints) 132 | 133 | # compare points with each other 134 | if len(points) > 1: 135 | for point in points: 136 | if point != points[0]: 137 | return False 138 | 139 | # TODO finish 140 | ''' 141 | 142 | 143 | ''' 144 | def collideAllBetween(rectsOrPoints): 145 | """Returns True if any of the (x, y) or (left, top, width, height) tuples 146 | in rectsOrPoints collides with any other point or box tuple in rectsOrPoints. 147 | 148 | >>> p1 = (50, 50) 149 | >>> p2 = (100, 100) 150 | >>> p3 = (50, 200) 151 | >>> r1 = (-50, -50, 20, 20) 152 | >>> r2 = (25, 25, 50, 50) 153 | >>> collideAllBetween([p1, p2, p3, r1, r2]) 154 | False 155 | >>> collideAllBetween([p1, p2, p3, r1]) 156 | False 157 | >>> collideAllBetween([p1, r2]) # Everything in the list collides with each other. 158 | True 159 | """ 160 | 161 | # Check for valid arguments 162 | try: 163 | for rectOrPoint in rectsOrPoints: 164 | if len(rectOrPoint) == 2: 165 | _checkForTwoIntOrFloatTuple(rectOrPoint) 166 | elif len(rectOrPoint) == 4: 167 | _checkForFourIntOrFloatTuple(rectOrPoint) 168 | else: 169 | raise PyRectException() 170 | except: 171 | raise PyRectException('Arguments in rectsOrPoints must be 2- or 4-integer/float tuples.') 172 | 173 | raise NotImplementedError # return a list of all rects or points that collide with any other in the argument 174 | ''' 175 | 176 | 177 | class Rect(object): 178 | def __init__( 179 | self, 180 | left=0, 181 | top=0, 182 | width=0, 183 | height=0, 184 | enableFloat=False, 185 | readOnly=False, 186 | onChange=None, 187 | onRead=None, 188 | ): 189 | _checkForIntOrFloat(width) 190 | _checkForIntOrFloat(height) 191 | _checkForIntOrFloat(left) 192 | _checkForIntOrFloat(top) 193 | 194 | self._enableFloat = bool(enableFloat) 195 | self._readOnly = bool(readOnly) 196 | 197 | if onChange is not None and not callable(onChange): 198 | raise PyRectException( 199 | "onChange argument must be None or callable (function, method, etc.)" 200 | ) 201 | self.onChange = onChange 202 | 203 | if onRead is not None and not callable(onRead): 204 | raise PyRectException( 205 | "onRead argument must be None or callable (function, method, etc.)" 206 | ) 207 | self.onRead = onRead 208 | 209 | if enableFloat: 210 | self._width = float(width) 211 | self._height = float(height) 212 | self._left = float(left) 213 | self._top = float(top) 214 | else: 215 | self._width = int(width) 216 | self._height = int(height) 217 | self._left = int(left) 218 | self._top = int(top) 219 | 220 | # OPERATOR OVERLOADING / DUNDER METHODS 221 | def __repr__(self): 222 | """Return a string of the constructor function call to create this Rect object.""" 223 | return "%s(left=%s, top=%s, width=%s, height=%s)" % ( 224 | self.__class__.__name__, 225 | self._left, 226 | self._top, 227 | self._width, 228 | self._height, 229 | ) 230 | 231 | def __str__(self): 232 | """Return a string representation of this Rect object.""" 233 | return "(x=%s, y=%s, w=%s, h=%s)" % ( 234 | self._left, 235 | self._top, 236 | self._width, 237 | self._height, 238 | ) 239 | 240 | def callOnChange(self, oldLeft, oldTop, oldWidth, oldHeight): 241 | # Note: callOnChange() should be called *after* the attribute has been changed. 242 | # Note: This isn't thread safe; the attributes can change between the calling of this function and the code in the function running. 243 | if self.onChange is not None: 244 | self.onChange( 245 | Box(oldLeft, oldTop, oldWidth, oldHeight), 246 | Box(self._left, self._top, self._width, self._height), 247 | ) 248 | 249 | @property 250 | def enableFloat(self): 251 | """ 252 | A Boolean attribute that determines if this rectangle uses floating point 253 | numbers for its position and size. False, by default. 254 | 255 | >>> r = Rect(0, 0, 10, 20) 256 | >>> r.enableFloat 257 | False 258 | >>> r.enableFloat = True 259 | >>> r.top = 3.14 260 | >>> r 261 | Rect(left=0.0, top=3.14, width=10.0, height=20.0) 262 | """ 263 | return self._enableFloat 264 | 265 | @enableFloat.setter 266 | def enableFloat(self, value): 267 | if not isinstance(value, bool): 268 | raise PyRectException("enableFloat must be set to a bool value") 269 | self._enableFloat = value 270 | 271 | if self._enableFloat: 272 | self._left = float(self._left) 273 | self._top = float(self._top) 274 | self._width = float(self._width) 275 | self._height = float(self._height) 276 | else: 277 | self._left = int(self._left) 278 | self._top = int(self._top) 279 | self._width = int(self._width) 280 | self._height = int(self._height) 281 | 282 | # LEFT SIDE PROPERTY 283 | @property 284 | def left(self): 285 | """ 286 | The x coordinate for the left edge of the rectangle. `x` is an alias for `left`. 287 | 288 | >>> r = Rect(0, 0, 10, 20) 289 | >>> r.left 290 | 0 291 | >>> r.left = 50 292 | >>> r 293 | Rect(left=50, top=0, width=10, height=20) 294 | """ 295 | if self.onRead is not None: 296 | self.onRead(LEFT) 297 | return self._left 298 | 299 | @left.setter 300 | def left(self, newLeft): 301 | if self._readOnly: 302 | raise PyRectException("Rect object is read-only") 303 | 304 | _checkForIntOrFloat(newLeft) 305 | if ( 306 | newLeft != self._left 307 | ): # Only run this code if the size/position has changed. 308 | originalLeft = self._left 309 | if self._enableFloat: 310 | self._left = newLeft 311 | else: 312 | self._left = int(newLeft) 313 | self.callOnChange(originalLeft, self._top, self._width, self._height) 314 | 315 | x = left # x is an alias for left 316 | 317 | # TOP SIDE PROPERTY 318 | @property 319 | def top(self): 320 | """ 321 | The y coordinate for the top edge of the rectangle. `y` is an alias for `top`. 322 | 323 | >>> r = Rect(0, 0, 10, 20) 324 | >>> r.top 325 | 0 326 | >>> r.top = 50 327 | >>> r 328 | Rect(left=0, top=50, width=10, height=20) 329 | """ 330 | if self.onRead is not None: 331 | self.onRead(TOP) 332 | return self._top 333 | 334 | @top.setter 335 | def top(self, newTop): 336 | if self._readOnly: 337 | raise PyRectException("Rect object is read-only") 338 | 339 | _checkForIntOrFloat(newTop) 340 | if newTop != self._top: # Only run this code if the size/position has changed. 341 | originalTop = self._top 342 | if self._enableFloat: 343 | self._top = newTop 344 | else: 345 | self._top = int(newTop) 346 | self.callOnChange(self._left, originalTop, self._width, self._height) 347 | 348 | y = top # y is an alias for top 349 | 350 | # RIGHT SIDE PROPERTY 351 | @property 352 | def right(self): 353 | """ 354 | The x coordinate for the right edge of the rectangle. 355 | 356 | >>> r = Rect(0, 0, 10, 20) 357 | >>> r.right 358 | 10 359 | >>> r.right = 50 360 | >>> r 361 | Rect(left=40, top=0, width=10, height=20) 362 | """ 363 | if self.onRead is not None: 364 | self.onRead(RIGHT) 365 | return self._left + self._width 366 | 367 | @right.setter 368 | def right(self, newRight): 369 | if self._readOnly: 370 | raise PyRectException("Rect object is read-only") 371 | 372 | _checkForIntOrFloat(newRight) 373 | if ( 374 | newRight != self._left + self._width 375 | ): # Only run this code if the size/position has changed. 376 | originalLeft = self._left 377 | if self._enableFloat: 378 | self._left = newRight - self._width 379 | else: 380 | self._left = int(newRight) - self._width 381 | self.callOnChange(originalLeft, self._top, self._width, self._height) 382 | 383 | # BOTTOM SIDE PROPERTY 384 | @property 385 | def bottom(self): 386 | """The y coordinate for the bottom edge of the rectangle. 387 | 388 | >>> r = Rect(0, 0, 10, 20) 389 | >>> r.bottom 390 | 20 391 | >>> r.bottom = 30 392 | >>> r 393 | Rect(left=0, top=10, width=10, height=20) 394 | """ 395 | if self.onRead is not None: 396 | self.onRead(BOTTOM) 397 | return self._top + self._height 398 | 399 | @bottom.setter 400 | def bottom(self, newBottom): 401 | if self._readOnly: 402 | raise PyRectException("Rect object is read-only") 403 | 404 | _checkForIntOrFloat(newBottom) 405 | if ( 406 | newBottom != self._top + self._height 407 | ): # Only run this code if the size/position has changed. 408 | originalTop = self._top 409 | if self._enableFloat: 410 | self._top = newBottom - self._height 411 | else: 412 | self._top = int(newBottom) - self._height 413 | self.callOnChange(self._left, originalTop, self._width, self._height) 414 | 415 | # TOP LEFT CORNER PROPERTY 416 | @property 417 | def topleft(self): 418 | """ 419 | The x and y coordinates for the top right corner of the rectangle, as a tuple. 420 | 421 | >>> r = Rect(0, 0, 10, 20) 422 | >>> r.topleft 423 | (0, 0) 424 | >>> r.topleft = (30, 30) 425 | >>> r 426 | Rect(left=30, top=30, width=10, height=20) 427 | """ 428 | if self.onRead is not None: 429 | self.onRead(TOPLEFT) 430 | return Point(x=self._left, y=self._top) 431 | 432 | @topleft.setter 433 | def topleft(self, value): 434 | if self._readOnly: 435 | raise PyRectException("Rect object is read-only") 436 | 437 | _checkForTwoIntOrFloatTuple(value) 438 | newLeft, newTop = value 439 | if (newLeft != self._left) or ( 440 | newTop != self._top 441 | ): # Only run this code if the size/position has changed. 442 | originalLeft = self._left 443 | originalTop = self._top 444 | if self._enableFloat: 445 | self._left = newLeft 446 | self._top = newTop 447 | else: 448 | self._left = int(newLeft) 449 | self._top = int(newTop) 450 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 451 | 452 | # BOTTOM LEFT CORNER PROPERTY 453 | @property 454 | def bottomleft(self): 455 | """ 456 | The x and y coordinates for the bottom right corner of the rectangle, as a tuple. 457 | 458 | >>> r = Rect(0, 0, 10, 20) 459 | >>> r.bottomleft 460 | (0, 20) 461 | >>> r.bottomleft = (30, 30) 462 | >>> r 463 | Rect(left=30, top=10, width=10, height=20) 464 | """ 465 | if self.onRead is not None: 466 | self.onRead(BOTTOMLEFT) 467 | return Point(x=self._left, y=self._top + self._height) 468 | 469 | @bottomleft.setter 470 | def bottomleft(self, value): 471 | if self._readOnly: 472 | raise PyRectException("Rect object is read-only") 473 | 474 | _checkForTwoIntOrFloatTuple(value) 475 | newLeft, newBottom = value 476 | if (newLeft != self._left) or ( 477 | newBottom != self._top + self._height 478 | ): # Only run this code if the size/position has changed. 479 | originalLeft = self._left 480 | originalTop = self._top 481 | if self._enableFloat: 482 | self._left = newLeft 483 | self._top = newBottom - self._height 484 | else: 485 | self._left = int(newLeft) 486 | self._top = int(newBottom) - self._height 487 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 488 | 489 | # TOP RIGHT CORNER PROPERTY 490 | @property 491 | def topright(self): 492 | """ 493 | The x and y coordinates for the top right corner of the rectangle, as a tuple. 494 | 495 | >>> r = Rect(0, 0, 10, 20) 496 | >>> r.topright 497 | (10, 0) 498 | >>> r.topright = (30, 30) 499 | >>> r 500 | Rect(left=20, top=30, width=10, height=20) 501 | """ 502 | if self.onRead is not None: 503 | self.onRead(TOPRIGHT) 504 | return Point(x=self._left + self._width, y=self._top) 505 | 506 | @topright.setter 507 | def topright(self, value): 508 | if self._readOnly: 509 | raise PyRectException("Rect object is read-only") 510 | 511 | _checkForTwoIntOrFloatTuple(value) 512 | newRight, newTop = value 513 | if (newRight != self._left + self._width) or ( 514 | newTop != self._top 515 | ): # Only run this code if the size/position has changed. 516 | originalLeft = self._left 517 | originalTop = self._top 518 | if self._enableFloat: 519 | self._left = newRight - self._width 520 | self._top = newTop 521 | else: 522 | self._left = int(newRight) - self._width 523 | self._top = int(newTop) 524 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 525 | 526 | # BOTTOM RIGHT CORNER PROPERTY 527 | @property 528 | def bottomright(self): 529 | """ 530 | The x and y coordinates for the bottom right corner of the rectangle, as a tuple. 531 | 532 | >>> r = Rect(0, 0, 10, 20) 533 | >>> r.bottomright 534 | (10, 20) 535 | >>> r.bottomright = (30, 30) 536 | >>> r 537 | Rect(left=20, top=10, width=10, height=20) 538 | """ 539 | if self.onRead is not None: 540 | self.onRead(BOTTOMRIGHT) 541 | return Point(x=self._left + self._width, y=self._top + self._height) 542 | 543 | @bottomright.setter 544 | def bottomright(self, value): 545 | if self._readOnly: 546 | raise PyRectException("Rect object is read-only") 547 | 548 | _checkForTwoIntOrFloatTuple(value) 549 | newRight, newBottom = value 550 | if (newBottom != self._top + self._height) or ( 551 | newRight != self._left + self._width 552 | ): # Only run this code if the size/position has changed. 553 | originalLeft = self._left 554 | originalTop = self._top 555 | if self._enableFloat: 556 | self._left = newRight - self._width 557 | self._top = newBottom - self._height 558 | else: 559 | self._left = int(newRight) - self._width 560 | self._top = int(newBottom) - self._height 561 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 562 | 563 | # MIDDLE OF TOP SIDE PROPERTY 564 | @property 565 | def midtop(self): 566 | """ 567 | The x and y coordinates for the midpoint of the top edge of the rectangle, as a tuple. 568 | 569 | >>> r = Rect(0, 0, 10, 20) 570 | >>> r.midtop 571 | (5, 0) 572 | >>> r.midtop = (40, 50) 573 | >>> r 574 | Rect(left=35, top=50, width=10, height=20) 575 | """ 576 | if self.onRead is not None: 577 | self.onRead(MIDTOP) 578 | if self._enableFloat: 579 | return Point(x=self._left + (self._width / 2.0), y=self._top) 580 | else: 581 | return Point(x=self._left + (self._width // 2), y=self._top) 582 | 583 | @midtop.setter 584 | def midtop(self, value): 585 | if self._readOnly: 586 | raise PyRectException("Rect object is read-only") 587 | 588 | _checkForTwoIntOrFloatTuple(value) 589 | newMidTop, newTop = value 590 | originalLeft = self._left 591 | originalTop = self._top 592 | if self._enableFloat: 593 | if (newMidTop != self._left + self._width / 2.0) or ( 594 | newTop != self._top 595 | ): # Only run this code if the size/position has changed. 596 | self._left = newMidTop - (self._width / 2.0) 597 | self._top = newTop 598 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 599 | else: 600 | if (newMidTop != self._left + self._width // 2) or ( 601 | newTop != self._top 602 | ): # Only run this code if the size/position has changed. 603 | self._left = int(newMidTop) - (self._width // 2) 604 | self._top = int(newTop) 605 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 606 | 607 | # MIDDLE OF BOTTOM SIDE PROPERTY 608 | @property 609 | def midbottom(self): 610 | """ 611 | The x and y coordinates for the midpoint of the bottom edge of the rectangle, as a tuple. 612 | 613 | >>> r = Rect(0, 0, 10, 20) 614 | >>> r.midbottom 615 | (5, 20) 616 | >>> r.midbottom = (40, 50) 617 | >>> r 618 | Rect(left=35, top=30, width=10, height=20) 619 | """ 620 | if self.onRead is not None: 621 | self.onRead(MIDBOTTOM) 622 | if self._enableFloat: 623 | return Point(x=self._left + (self._width / 2.0), y=self._top + self._height) 624 | else: 625 | return Point(x=self._left + (self._width // 2), y=self._top + self._height) 626 | 627 | @midbottom.setter 628 | def midbottom(self, value): 629 | if self._readOnly: 630 | raise PyRectException("Rect object is read-only") 631 | 632 | _checkForTwoIntOrFloatTuple(value) 633 | newMidBottom, newBottom = value 634 | originalLeft = self._left 635 | originalTop = self._top 636 | if self._enableFloat: 637 | if (newMidBottom != self._left + self._width / 2.0) or ( 638 | newBottom != self._top + self._height 639 | ): # Only run this code if the size/position has changed. 640 | self._left = newMidBottom - (self._width / 2.0) 641 | self._top = newBottom - self._height 642 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 643 | else: 644 | if (newMidBottom != self._left + self._width // 2) or ( 645 | newBottom != self._top + self._height 646 | ): # Only run this code if the size/position has changed. 647 | self._left = int(newMidBottom) - (self._width // 2) 648 | self._top = int(newBottom) - self._height 649 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 650 | 651 | # MIDDLE OF LEFT SIDE PROPERTY 652 | @property 653 | def midleft(self): 654 | """ 655 | The x and y coordinates for the midpoint of the left edge of the rectangle, as a tuple. 656 | 657 | >>> r = Rect(0, 0, 10, 20) 658 | >>> r.midleft 659 | (0, 10) 660 | >>> r.midleft = (40, 50) 661 | >>> r 662 | Rect(left=40, top=40, width=10, height=20) 663 | """ 664 | if self.onRead is not None: 665 | self.onRead(MIDLEFT) 666 | if self._enableFloat: 667 | return Point(x=self._left, y=self._top + (self._height / 2.0)) 668 | else: 669 | return Point(x=self._left, y=self._top + (self._height // 2)) 670 | 671 | @midleft.setter 672 | def midleft(self, value): 673 | if self._readOnly: 674 | raise PyRectException("Rect object is read-only") 675 | 676 | _checkForTwoIntOrFloatTuple(value) 677 | newLeft, newMidLeft = value 678 | originalLeft = self._left 679 | originalTop = self._top 680 | if self._enableFloat: 681 | if (newLeft != self._left) or ( 682 | newMidLeft != self._top + (self._height / 2.0) 683 | ): # Only run this code if the size/position has changed. 684 | self._left = newLeft 685 | self._top = newMidLeft - (self._height / 2.0) 686 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 687 | else: 688 | if (newLeft != self._left) or ( 689 | newMidLeft != self._top + (self._height // 2) 690 | ): # Only run this code if the size/position has changed. 691 | self._left = int(newLeft) 692 | self._top = int(newMidLeft) - (self._height // 2) 693 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 694 | 695 | # MIDDLE OF RIGHT SIDE PROPERTY 696 | @property 697 | def midright(self): 698 | """ 699 | The x and y coordinates for the midpoint of the right edge of the rectangle, as a tuple. 700 | 701 | >>> r = Rect(0, 0, 10, 20) 702 | >>> r.midright 703 | (10, 10) 704 | >>> r.midright = (40, 50) 705 | >>> r 706 | Rect(left=30, top=40, width=10, height=20) 707 | """ 708 | if self.onRead is not None: 709 | self.onRead(MIDRIGHT) 710 | if self._enableFloat: 711 | return Point(x=self._left + self._width, y=self._top + (self._height / 2.0)) 712 | else: 713 | return Point(x=self._left + self._width, y=self._top + (self._height // 2)) 714 | 715 | @midright.setter 716 | def midright(self, value): 717 | if self._readOnly: 718 | raise PyRectException("Rect object is read-only") 719 | 720 | _checkForTwoIntOrFloatTuple(value) 721 | newRight, newMidRight = value 722 | originalLeft = self._left 723 | originalTop = self._top 724 | if self._enableFloat: 725 | if (newRight != self._left + self._width) or ( 726 | newMidRight != self._top + self._height / 2.0 727 | ): # Only run this code if the size/position has changed. 728 | self._left = newRight - self._width 729 | self._top = newMidRight - (self._height / 2.0) 730 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 731 | else: 732 | if (newRight != self._left + self._width) or ( 733 | newMidRight != self._top + self._height // 2 734 | ): # Only run this code if the size/position has changed. 735 | self._left = int(newRight) - self._width 736 | self._top = int(newMidRight) - (self._height // 2) 737 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 738 | 739 | # CENTER POINT PROPERTY 740 | @property 741 | def center(self): 742 | """ 743 | The x and y coordinates for the center of the rectangle, as a tuple. 744 | 745 | >>> r = Rect(0, 0, 10, 20) 746 | >>> r.center 747 | (5, 10) 748 | >>> r.center = (40, 50) 749 | >>> r 750 | Rect(left=35, top=40, width=10, height=20) 751 | """ 752 | if self.onRead is not None: 753 | self.onRead(CENTER) 754 | if self._enableFloat: 755 | return Point( 756 | x=self._left + (self._width / 2.0), y=self._top + (self._height / 2.0) 757 | ) 758 | else: 759 | return Point( 760 | x=self._left + (self._width // 2), y=self._top + (self._height // 2) 761 | ) 762 | 763 | @center.setter 764 | def center(self, value): 765 | if self._readOnly: 766 | raise PyRectException("Rect object is read-only") 767 | 768 | _checkForTwoIntOrFloatTuple(value) 769 | newCenterx, newCentery = value 770 | originalLeft = self._left 771 | originalTop = self._top 772 | if self._enableFloat: 773 | if (newCenterx != self._left + self._width / 2.0) or ( 774 | newCentery != self._top + self._height / 2.0 775 | ): # Only run this code if the size/position has changed. 776 | self._left = newCenterx - (self._width / 2.0) 777 | self._top = newCentery - (self._height / 2.0) 778 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 779 | else: 780 | if (newCenterx != self._left + self._width // 2) or ( 781 | newCentery != self._top + self._height // 2 782 | ): # Only run this code if the size/position has changed. 783 | self._left = int(newCenterx) - (self._width // 2) 784 | self._top = int(newCentery) - (self._height // 2) 785 | self.callOnChange(originalLeft, originalTop, self._width, self._height) 786 | 787 | # X COORDINATE OF CENTER POINT PROPERTY 788 | @property 789 | def centerx(self): 790 | """ 791 | The x coordinate for the center of the rectangle, as a tuple. 792 | 793 | >>> r = Rect(0, 0, 10, 20) 794 | >>> r.centerx 795 | 5 796 | >>> r.centerx = 50 797 | >>> r 798 | Rect(left=45, top=0, width=10, height=20) 799 | """ 800 | if self.onRead is not None: 801 | self.onRead(CENTERX) 802 | if self._enableFloat: 803 | return self._left + (self._width / 2.0) 804 | else: 805 | return self._left + (self._width // 2) 806 | 807 | @centerx.setter 808 | def centerx(self, newCenterx): 809 | if self._readOnly: 810 | raise PyRectException("Rect object is read-only") 811 | 812 | _checkForIntOrFloat(newCenterx) 813 | originalLeft = self._left 814 | if self._enableFloat: 815 | if ( 816 | newCenterx != self._left + self._width / 2.0 817 | ): # Only run this code if the size/position has changed. 818 | self._left = newCenterx - (self._width / 2.0) 819 | self.callOnChange(originalLeft, self._top, self._width, self._height) 820 | else: 821 | if ( 822 | newCenterx != self._left + self._width // 2 823 | ): # Only run this code if the size/position has changed. 824 | self._left = int(newCenterx) - (self._width // 2) 825 | self.callOnChange(originalLeft, self._top, self._width, self._height) 826 | 827 | # Y COORDINATE OF CENTER POINT PROPERTY 828 | @property 829 | def centery(self): 830 | """ 831 | The y coordinate for the center of the rectangle, as a tuple. 832 | 833 | >>> r = Rect(0, 0, 10, 20) 834 | >>> r.centery 835 | 10 836 | >>> r.centery = 50 837 | >>> r 838 | Rect(left=0, top=40, width=10, height=20) 839 | """ 840 | if self.onRead is not None: 841 | self.onRead(CENTERY) 842 | if self._enableFloat: 843 | return self._top + (self._height / 2.0) 844 | else: 845 | return self._top + (self._height // 2) 846 | 847 | @centery.setter 848 | def centery(self, newCentery): 849 | if self._readOnly: 850 | raise PyRectException("Rect object is read-only") 851 | 852 | _checkForIntOrFloat(newCentery) 853 | originalTop = self._top 854 | if self._enableFloat: 855 | if ( 856 | newCentery != self._top + self._height / 2.0 857 | ): # Only run this code if the size/position has changed. 858 | self._top = newCentery - (self._height / 2.0) 859 | self.callOnChange(self._left, originalTop, self._width, self._height) 860 | else: 861 | if ( 862 | newCentery != self._top + self._height // 2 863 | ): # Only run this code if the size/position has changed. 864 | self._top = int(newCentery) - (self._height // 2) 865 | self.callOnChange(self._left, originalTop, self._width, self._height) 866 | 867 | # SIZE PROPERTY (i.e. (width, height)) 868 | @property 869 | def size(self): 870 | """ 871 | The width and height of the rectangle, as a tuple. 872 | 873 | >>> r = Rect(0, 0, 10, 20) 874 | >>> r.size 875 | (10, 20) 876 | >>> r.size = (40, 50) 877 | >>> r 878 | Rect(left=0, top=0, width=40, height=50) 879 | """ 880 | if self.onRead is not None: 881 | self.onRead(SIZE) 882 | return Size(width=self._width, height=self._height) 883 | 884 | @size.setter 885 | def size(self, value): 886 | if self._readOnly: 887 | raise PyRectException("Rect object is read-only") 888 | 889 | _checkForTwoIntOrFloatTuple(value) 890 | newWidth, newHeight = value 891 | if newWidth != self._width or newHeight != self._height: 892 | originalWidth = self._width 893 | originalHeight = self._height 894 | if self._enableFloat: 895 | self._width = newWidth 896 | self._height = newHeight 897 | else: 898 | self._width = int(newWidth) 899 | self._height = int(newHeight) 900 | self.callOnChange(self._left, self._top, originalWidth, originalHeight) 901 | 902 | # WIDTH PROPERTY 903 | @property 904 | def width(self): 905 | """ 906 | The width of the rectangle. `w` is an alias for `width`. 907 | 908 | >>> r = Rect(0, 0, 10, 20) 909 | >>> r.width 910 | 10 911 | >>> r.width = 50 912 | >>> r 913 | Rect(left=0, top=0, width=50, height=20) 914 | """ 915 | if self.onRead is not None: 916 | self.onRead(WIDTH) 917 | return self._width 918 | 919 | @width.setter 920 | def width(self, newWidth): 921 | if self._readOnly: 922 | raise PyRectException("Rect object is read-only") 923 | 924 | _checkForIntOrFloat(newWidth) 925 | if ( 926 | newWidth != self._width 927 | ): # Only run this code if the size/position has changed. 928 | originalWidth = self._width 929 | if self._enableFloat: 930 | self._width = newWidth 931 | else: 932 | self._width = int(newWidth) 933 | self.callOnChange(self._left, self._top, originalWidth, self._height) 934 | 935 | w = width 936 | 937 | # HEIGHT PROPERTY 938 | @property 939 | def height(self): 940 | """ 941 | The height of the rectangle. `h` is an alias for `height` 942 | 943 | >>> r = Rect(0, 0, 10, 20) 944 | >>> r.height 945 | 20 946 | >>> r.height = 50 947 | >>> r 948 | Rect(left=0, top=0, width=10, height=50) 949 | """ 950 | if self.onRead is not None: 951 | self.onRead(HEIGHT) 952 | return self._height 953 | 954 | @height.setter 955 | def height(self, newHeight): 956 | if self._readOnly: 957 | raise PyRectException("Rect object is read-only") 958 | 959 | _checkForIntOrFloat(newHeight) 960 | if ( 961 | newHeight != self._height 962 | ): # Only run this code if the size/position has changed. 963 | originalHeight = self._height 964 | if self._enableFloat: 965 | self._height = newHeight 966 | else: 967 | self._height = int(newHeight) 968 | self.callOnChange(self._left, self._top, self._width, originalHeight) 969 | 970 | h = height 971 | 972 | # AREA PROPERTY 973 | @property 974 | def area(self): 975 | """The area of the `Rect`, which is simply the width times the height. 976 | This is a read-only attribute. 977 | 978 | >>> r = Rect(0, 0, 10, 20) 979 | >>> r.area 980 | 200 981 | """ 982 | if self.onRead is not None: 983 | self.onRead(AREA) 984 | return self._width * self._height 985 | 986 | 987 | # PERIMETER PROPERTY 988 | @property 989 | def perimeter(self): 990 | """The perimeter of the `Rect`, which is simply the (width + height) * 2. 991 | This is a read-only attribute. 992 | 993 | >>> r = Rect(0, 0, 10, 20) 994 | >>> r.area 995 | 200 996 | """ 997 | if self.onRead is not None: 998 | self.onRead(AREA) 999 | return (self._width + self._height) * 2 1000 | 1001 | 1002 | # BOX PROPERTY 1003 | @property 1004 | def box(self): 1005 | """A tuple of four integers: (left, top, width, height). 1006 | 1007 | >>> r = Rect(0, 0, 10, 20) 1008 | >>> r.box 1009 | (0, 0, 10, 20) 1010 | >>> r.box = (5, 15, 100, 200) 1011 | >>> r.box 1012 | (5, 15, 100, 200)""" 1013 | if self.onRead is not None: 1014 | self.onRead(BOX) 1015 | return Box( 1016 | left=self._left, top=self._top, width=self._width, height=self._height 1017 | ) 1018 | 1019 | @box.setter 1020 | def box(self, value): 1021 | if self._readOnly: 1022 | raise PyRectException("Rect object is read-only") 1023 | 1024 | _checkForFourIntOrFloatTuple(value) 1025 | newLeft, newTop, newWidth, newHeight = value 1026 | if ( 1027 | (newLeft != self._left) 1028 | or (newTop != self._top) 1029 | or (newWidth != self._width) 1030 | or (newHeight != self._height) 1031 | ): 1032 | originalLeft = self._left 1033 | originalTop = self._top 1034 | originalWidth = self._width 1035 | originalHeight = self._height 1036 | if self._enableFloat: 1037 | self._left = float(newLeft) 1038 | self._top = float(newTop) 1039 | self._width = float(newWidth) 1040 | self._height = float(newHeight) 1041 | else: 1042 | self._left = int(newLeft) 1043 | self._top = int(newTop) 1044 | self._width = int(newWidth) 1045 | self._height = int(newHeight) 1046 | self.callOnChange(originalLeft, originalTop, originalWidth, originalHeight) 1047 | 1048 | def get(self, rectAttrName): 1049 | # Access via the properties so that it triggers onRead(). 1050 | if rectAttrName == TOP: 1051 | return self.top 1052 | elif rectAttrName == BOTTOM: 1053 | return self.bottom 1054 | elif rectAttrName == LEFT: 1055 | return self.left 1056 | elif rectAttrName == RIGHT: 1057 | return self.right 1058 | elif rectAttrName == TOPLEFT: 1059 | return self.topleft 1060 | elif rectAttrName == TOPRIGHT: 1061 | return self.topright 1062 | elif rectAttrName == BOTTOMLEFT: 1063 | return self.bottomleft 1064 | elif rectAttrName == BOTTOMRIGHT: 1065 | return self.bottomright 1066 | elif rectAttrName == MIDTOP: 1067 | return self.midtop 1068 | elif rectAttrName == MIDBOTTOM: 1069 | return self.midbottom 1070 | elif rectAttrName == MIDLEFT: 1071 | return self.midleft 1072 | elif rectAttrName == MIDRIGHT: 1073 | return self.midright 1074 | elif rectAttrName == CENTER: 1075 | return self.center 1076 | elif rectAttrName == CENTERX: 1077 | return self.centerx 1078 | elif rectAttrName == CENTERY: 1079 | return self.centery 1080 | elif rectAttrName == WIDTH: 1081 | return self.width 1082 | elif rectAttrName == HEIGHT: 1083 | return self.height 1084 | elif rectAttrName == SIZE: 1085 | return self.size 1086 | elif rectAttrName == AREA: 1087 | return self.area 1088 | elif rectAttrName == BOX: 1089 | return self.box 1090 | else: 1091 | raise PyRectException("'%s' is not a valid attribute name" % (rectAttrName)) 1092 | 1093 | def set(self, rectAttrName, value): 1094 | # Set via the properties so that it triggers onChange(). 1095 | if rectAttrName == TOP: 1096 | self.top = value 1097 | elif rectAttrName == BOTTOM: 1098 | self.bottom = value 1099 | elif rectAttrName == LEFT: 1100 | self.left = value 1101 | elif rectAttrName == RIGHT: 1102 | self.right = value 1103 | elif rectAttrName == TOPLEFT: 1104 | self.topleft = value 1105 | elif rectAttrName == TOPRIGHT: 1106 | self.topright = value 1107 | elif rectAttrName == BOTTOMLEFT: 1108 | self.bottomleft = value 1109 | elif rectAttrName == BOTTOMRIGHT: 1110 | self.bottomright = value 1111 | elif rectAttrName == MIDTOP: 1112 | self.midtop = value 1113 | elif rectAttrName == MIDBOTTOM: 1114 | self.midbottom = value 1115 | elif rectAttrName == MIDLEFT: 1116 | self.midleft = value 1117 | elif rectAttrName == MIDRIGHT: 1118 | self.midright = value 1119 | elif rectAttrName == CENTER: 1120 | self.center = value 1121 | elif rectAttrName == CENTERX: 1122 | self.centerx = value 1123 | elif rectAttrName == CENTERY: 1124 | self.centery = value 1125 | elif rectAttrName == WIDTH: 1126 | self.width = value 1127 | elif rectAttrName == HEIGHT: 1128 | self.height = value 1129 | elif rectAttrName == SIZE: 1130 | self.size = value 1131 | elif rectAttrName == AREA: 1132 | raise PyRectException("area is a read-only attribute") 1133 | elif rectAttrName == BOX: 1134 | self.box = value 1135 | else: 1136 | raise PyRectException("'%s' is not a valid attribute name" % (rectAttrName)) 1137 | 1138 | def move(self, xOffset, yOffset): 1139 | """Moves this Rect object by the given offsets. The xOffset and yOffset 1140 | arguments can be any integer value, positive or negative. 1141 | >>> r = Rect(0, 0, 100, 100) 1142 | >>> r.move(10, 20) 1143 | >>> r 1144 | Rect(left=10, top=20, width=100, height=100) 1145 | """ 1146 | if self._readOnly: 1147 | raise PyRectException("Rect object is read-only") 1148 | 1149 | _checkForIntOrFloat(xOffset) 1150 | _checkForIntOrFloat(yOffset) 1151 | if self._enableFloat: 1152 | self._left += xOffset 1153 | self._top += yOffset 1154 | else: 1155 | self._left += int(xOffset) 1156 | self._top += int(yOffset) 1157 | 1158 | def copy(self): 1159 | """Return a copied `Rect` object with the same position and size as this 1160 | `Rect` object. 1161 | 1162 | >>> r1 = Rect(0, 0, 100, 150) 1163 | >>> r2 = r1.copy() 1164 | >>> r1 == r2 1165 | True 1166 | >>> r2 1167 | Rect(left=0, top=0, width=100, height=150) 1168 | """ 1169 | return Rect( 1170 | self._left, 1171 | self._top, 1172 | self._width, 1173 | self._height, 1174 | self._enableFloat, 1175 | self._readOnly, 1176 | ) 1177 | 1178 | def inflate(self, widthChange=0, heightChange=0): 1179 | """Increases the size of this Rect object by the given offsets. The 1180 | rectangle's center doesn't move. Negative values will shrink the 1181 | rectangle. 1182 | 1183 | >>> r = Rect(0, 0, 100, 150) 1184 | >>> r.inflate(20, 40) 1185 | >>> r 1186 | Rect(left=-10, top=-20, width=120, height=190) 1187 | """ 1188 | if self._readOnly: 1189 | raise PyRectException("Rect object is read-only") 1190 | 1191 | originalCenter = self.center 1192 | self.width += widthChange 1193 | self.height += heightChange 1194 | self.center = originalCenter 1195 | 1196 | def clamp(self, otherRect): 1197 | """Centers this Rect object at the center of otherRect. 1198 | 1199 | >>> r1 =Rect(0, 0, 100, 100) 1200 | >>> r2 = Rect(-20, -90, 50, 50) 1201 | >>> r2.clamp(r1) 1202 | >>> r2 1203 | Rect(left=25, top=25, width=50, height=50) 1204 | >>> r1.center == r2.center 1205 | True 1206 | """ 1207 | if self._readOnly: 1208 | raise PyRectException("Rect object is read-only") 1209 | 1210 | self.center = otherRect.center 1211 | 1212 | ''' 1213 | def intersection(self, otherRect): 1214 | """Returns a new Rect object of the overlapping area between this 1215 | Rect object and otherRect. 1216 | 1217 | `clip()` is an alias for `intersection()`. 1218 | """ 1219 | pass 1220 | 1221 | clip = intersection 1222 | ''' 1223 | 1224 | def union(self, otherRect): 1225 | """Adjusts the width and height to also cover the area of `otherRect`. 1226 | 1227 | >>> r1 = Rect(0, 0, 100, 100) 1228 | >>> r2 = Rect(-10, -10, 100, 100) 1229 | >>> r1.union(r2) 1230 | >>> r1 1231 | Rect(left=-10, top=-10, width=110, height=110) 1232 | """ 1233 | 1234 | # TODO - Change otherRect so that it could be a point as well. 1235 | 1236 | unionLeft = min(self._left, otherRect._left) 1237 | unionTop = min(self._top, otherRect._top) 1238 | unionRight = max(self.right, otherRect.right) 1239 | unionBottom = max(self.bottom, otherRect.bottom) 1240 | 1241 | self._left = unionLeft 1242 | self._top = unionTop 1243 | self._width = unionRight - unionLeft 1244 | self._height = unionBottom - unionTop 1245 | 1246 | def unionAll(self, otherRects): 1247 | """Adjusts the width and height to also cover all the `Rect` objects in 1248 | the `otherRects` sequence. 1249 | 1250 | >>> r = Rect(0, 0, 100, 100) 1251 | >>> r1 = Rect(0, 0, 150, 100) 1252 | >>> r2 = Rect(-10, -10, 100, 100) 1253 | >>> r.unionAll([r1, r2]) 1254 | >>> r 1255 | Rect(left=-10, top=-10, width=160, height=110) 1256 | """ 1257 | 1258 | # TODO - Change otherRect so that it could be a point as well. 1259 | 1260 | otherRects = list(otherRects) 1261 | otherRects.append(self) 1262 | 1263 | unionLeft = min([r._left for r in otherRects]) 1264 | unionTop = min([r._top for r in otherRects]) 1265 | unionRight = max([r.right for r in otherRects]) 1266 | unionBottom = max([r.bottom for r in otherRects]) 1267 | 1268 | self._left = unionLeft 1269 | self._top = unionTop 1270 | self._width = unionRight - unionLeft 1271 | self._height = unionBottom - unionTop 1272 | 1273 | """ 1274 | def fit(self, other): 1275 | pass # TODO - needs to be complete 1276 | """ 1277 | 1278 | def normalize(self): 1279 | """Rect objects with a negative width or height cover a region where the 1280 | right/bottom edge is to the left/above of the left/top edge, respectively. 1281 | The `normalize()` method sets the `width` and `height` to positive if they 1282 | were negative. 1283 | 1284 | The Rect stays in the same place, though with the `top` and `left` 1285 | attributes representing the true top and left side. 1286 | 1287 | >>> r = Rect(0, 0, -10, -20) 1288 | >>> r.normalize() 1289 | >>> r 1290 | Rect(left=-10, top=-20, width=10, height=20) 1291 | """ 1292 | if self._readOnly: 1293 | raise PyRectException("Rect object is read-only") 1294 | 1295 | if self._width < 0: 1296 | self._width = -self._width 1297 | self._left -= self._width 1298 | if self._height < 0: 1299 | self._height = -self._height 1300 | self._top -= self._height 1301 | # Note: No need to intify here, since the four attributes should already be ints and no multiplication was done. 1302 | 1303 | def __contains__( 1304 | self, value 1305 | ): # for either points or other Rect objects. For Rects, the *entire* Rect must be in this Rect. 1306 | if isinstance(value, Rect): 1307 | return ( 1308 | value.topleft in self 1309 | and value.topright in self 1310 | and value.bottomleft in self 1311 | and value.bottomright in self 1312 | ) 1313 | 1314 | # Check if value is an (x, y) sequence or a (left, top, width, height) sequence. 1315 | try: 1316 | len(value) 1317 | except: 1318 | raise PyRectException( 1319 | "in requires an (x, y) tuple, a (left, top, width, height) tuple, or a Rect object as left operand, not %s" 1320 | % (value.__class__.__name__) 1321 | ) 1322 | 1323 | if len(value) == 2: 1324 | # Assume that value is an (x, y) sequence. 1325 | _checkForTwoIntOrFloatTuple(value) 1326 | x, y = value 1327 | return ( 1328 | self._left < x < self._left + self._width 1329 | and self._top < y < self._top + self._height 1330 | ) 1331 | 1332 | elif len(value) == 4: 1333 | # Assume that value is an (x, y) sequence. 1334 | _checkForFourIntOrFloatTuple(value) 1335 | left, top, width, height = value 1336 | return ( 1337 | (left, top) in self 1338 | and (left + width, top) in self 1339 | and (left, top + height) in self 1340 | and (left + width, top + height) in self 1341 | ) 1342 | else: 1343 | raise PyRectException( 1344 | "in requires an (x, y) tuple, a (left, top, width, height) tuple, or a Rect object as left operand, not %s" 1345 | % (value.__class__.__name__) 1346 | ) 1347 | 1348 | def collide(self, value): 1349 | """Returns `True` if value collides with this `Rect` object, where value can 1350 | be an (x, y) tuple, a (left, top, width, height) box tuple, or another `Rect` 1351 | object. If value represents a rectangular area, any part of that area 1352 | can collide with this `Rect` object to make `collide()` return `True`. 1353 | Otherwise, returns `False`.""" 1354 | 1355 | # Note: This code is similar to __contains__(), with some minor changes 1356 | # because __contains__() requires the rectangular are to be COMPELTELY 1357 | # within the Rect object. 1358 | if isinstance(value, Rect): 1359 | return ( 1360 | value.topleft in self 1361 | or value.topright in self 1362 | or value.bottomleft in self 1363 | or value.bottomright in self 1364 | ) 1365 | 1366 | # Check if value is an (x, y) sequence or a (left, top, width, height) sequence. 1367 | try: 1368 | len(value) 1369 | except: 1370 | raise PyRectException( 1371 | "in requires an (x, y) tuple, a (left, top, width, height) tuple, or a Rect object as left operand, not %s" 1372 | % (value.__class__.__name__) 1373 | ) 1374 | 1375 | if len(value) == 2: 1376 | # Assume that value is an (x, y) sequence. 1377 | _checkForTwoIntOrFloatTuple(value) 1378 | x, y = value 1379 | return ( 1380 | self._left < x < self._left + self._width 1381 | and self._top < y < self._top + self._height 1382 | ) 1383 | 1384 | elif len(value) == 4: 1385 | # Assume that value is an (x, y) sequence. 1386 | left, top, width, height = value 1387 | return ( 1388 | (left, top) in self 1389 | or (left + width, top) in self 1390 | or (left, top + height) in self 1391 | or (left + width, top + height) in self 1392 | ) 1393 | else: 1394 | raise PyRectException( 1395 | "in requires an (x, y) tuple, a (left, top, width, height) tuple, or a Rect object as left operand, not %s" 1396 | % (value.__class__.__name__) 1397 | ) 1398 | 1399 | ''' 1400 | def collideAny(self, rectsOrPoints): 1401 | """Returns True if any of the (x, y) or (left, top, width, height) 1402 | tuples in rectsOrPoints is inside this Rect object. 1403 | 1404 | >> r = Rect(0, 0, 100, 100) 1405 | >> p1 = (150, 80) 1406 | >> p2 = (100, 100) # This point collides. 1407 | >> r.collideAny([p1, p2]) 1408 | True 1409 | >> r1 = Rect(50, 50, 10, 20) # This Rect collides. 1410 | >> r.collideAny([r1]) 1411 | True 1412 | >> r.collideAny([p1, p2, r1]) 1413 | True 1414 | """ 1415 | # TODO - needs to be complete 1416 | pass # returns True or False 1417 | raise NotImplementedError 1418 | ''' 1419 | 1420 | ''' 1421 | def collideAll(self, rectsOrPoints): 1422 | """Returns True if all of the (x, y) or (left, top, width, height) 1423 | tuples in rectsOrPoints is inside this Rect object. 1424 | """ 1425 | 1426 | pass # return a list of all rects or points that collide with any other in the argument 1427 | raise NotImplementedError 1428 | ''' 1429 | 1430 | # TODO - Add overloaded operators for + - * / and others once we can determine actual use cases for them. 1431 | 1432 | """NOTE: All of the comparison magic methods compare the box tuple of Rect 1433 | objects. This is the behavior of the pygame Rect objects. Originally, 1434 | I thought about having the <, <=, >, and >= operators compare the area 1435 | of Rect objects. But at the same time, I wanted to have == and != compare 1436 | not just area, but all four left, top, width, and height attributes. 1437 | But that's weird to have different comparison operators comparing different 1438 | features of a rectangular area. So I just defaulted to what Pygame does 1439 | and compares the box tuple. This means that the == and != operators are 1440 | the only really useful comparison operators, so I decided to ditch the 1441 | other operators altogether and just have Rect only support == and !=. 1442 | """ 1443 | 1444 | def __eq__(self, other): 1445 | if isinstance(other, Rect): 1446 | return other.box == self.box 1447 | else: 1448 | raise PyRectException( 1449 | "Rect objects can only be compared with other Rect objects" 1450 | ) 1451 | 1452 | def __ne__(self, other): 1453 | if isinstance(other, Rect): 1454 | return other.box != self.box 1455 | else: 1456 | raise PyRectException( 1457 | "Rect objects can only be compared with other Rect objects" 1458 | ) 1459 | 1460 | 1461 | if __name__ == "__main__": 1462 | print(doctest.testmod()) 1463 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | import re 4 | 5 | # Load version from module (without loading the whole module) 6 | with open('pyrect/__init__.py', 'r') as fd: 7 | version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', 8 | fd.read(), re.MULTILINE).group(1) 9 | 10 | here = os.path.abspath(os.path.dirname(__file__)) 11 | 12 | # Get the long description from the README file 13 | with open(os.path.join(here, 'README.rst')) as f: 14 | long_description = f.read() 15 | 16 | 17 | setup( 18 | name='PyRect', 19 | version=version, 20 | url='https://github.com/asweigart/pyrect', 21 | author='Al Sweigart', 22 | author_email='al@inventwithpython.com', 23 | description=('PyRect is a simple module with a Rect class for Pygame-like rectangular areas.'), 24 | license='BSD', 25 | long_description=long_description, 26 | packages=['pyrect'], 27 | test_suite='tests', 28 | install_requires=[], 29 | keywords="pygame rect rectangular rectangle area", 30 | classifiers=[ 31 | 'Development Status :: 4 - Beta', 32 | 'Environment :: Win32 (MS Windows)', 33 | 'Environment :: MacOS X', 34 | 'Intended Audience :: Developers', 35 | 'License :: OSI Approved :: BSD License', 36 | 'Operating System :: OS Independent', 37 | 'Programming Language :: Python', 38 | 'Programming Language :: Python :: 2', 39 | 'Programming Language :: Python :: 2.5', 40 | 'Programming Language :: Python :: 2.6', 41 | 'Programming Language :: Python :: 2.7', 42 | 'Programming Language :: Python :: 3', 43 | 'Programming Language :: Python :: 3.1', 44 | 'Programming Language :: Python :: 3.2', 45 | 'Programming Language :: Python :: 3.3', 46 | 'Programming Language :: Python :: 3.4', 47 | 'Programming Language :: Python :: 3.5', 48 | 'Programming Language :: Python :: 3.6', 49 | 'Programming Language :: Python :: 3.7', 50 | 'Programming Language :: Python :: 3.8', 51 | 'Programming Language :: Python :: 3.9', 52 | 'Programming Language :: Python :: 3.10', 53 | 'Programming Language :: Python :: 3.11', 54 | ], 55 | ) -------------------------------------------------------------------------------- /tests/test_pyrect.py: -------------------------------------------------------------------------------- 1 | # TODO - We need tests for when enableFloat is True. 2 | 3 | import pytest 4 | import sys 5 | 6 | USING_PY_2 = sys.version_info[0] < 3 7 | 8 | if USING_PY_2: 9 | ModuleNotFoundError = None 10 | 11 | try: 12 | import pygame # Used for comparisons to Pygame's Rect class. 13 | except (ModuleNotFoundError, ImportError): 14 | sys.exit('Pygame is required to run these tests so we can compare PygRect\'s rectangles to Pygame\'s rectangles.') 15 | 16 | import pyrect 17 | 18 | 19 | def _compareToPygameRect(rect, pygameRectleft, pygameRectTop, pygameRectWidth, pygameRectHeight): 20 | rect = pyrect.Rect(pygameRectleft, pygameRectTop, pygameRectWidth, pygameRectHeight) 21 | pygameRect = pygame.Rect(pygameRectleft, pygameRectTop, pygameRectWidth, pygameRectHeight) 22 | assert rect.left == pygameRect.left 23 | assert rect.right == pygameRect.right 24 | assert rect.top == pygameRect.top 25 | assert rect.bottom == pygameRect.bottom 26 | 27 | assert rect.width == pygameRect.width 28 | assert rect.height == pygameRect.height 29 | 30 | assert rect.topleft == pygameRect.topleft 31 | assert rect.topright == pygameRect.topright 32 | assert rect.bottomleft == pygameRect.bottomleft 33 | assert rect.bottomright == pygameRect.bottomright 34 | 35 | assert rect.midtop == pygameRect.midtop 36 | assert rect.midbottom == pygameRect.midbottom 37 | assert rect.midleft == pygameRect.midleft 38 | assert rect.midleft == pygameRect.midleft 39 | 40 | assert rect.center == pygameRect.center 41 | assert rect.centerx == pygameRect.centerx 42 | assert rect.centery == pygameRect.centery 43 | 44 | 45 | def test_enableFloat(): 46 | rect = pyrect.Rect(1, 2, 10, 20) 47 | assert rect.enableFloat == False 48 | assert rect.topleft == (1, 2) 49 | rect.enableFloat = True 50 | assert rect.topleft == (1.0, 2.0) 51 | rect.enableFloat = False 52 | assert rect.topleft == (1, 2) 53 | 54 | with pytest.raises(pyrect.PyRectException): 55 | rect.enableFloat = 'invalid' 56 | 57 | with pytest.raises(AttributeError): 58 | del rect.enableFloat 59 | 60 | 61 | def test_ctor(): 62 | # Test basic positional and keyword arguments. 63 | r = pyrect.Rect(0, 1, 100, 200) 64 | _compareToPygameRect(r, 0, 1, 100, 200) 65 | assert r.left == 0 66 | assert r.top == 1 67 | assert r.width == 100 68 | assert r.height == 200 69 | 70 | r = pyrect.Rect(left=0, top=1, width=100, height=200) 71 | _compareToPygameRect(r, 0, 1, 100, 200) 72 | assert r.left == 0 73 | assert r.top == 1 74 | assert r.width == 100 75 | assert r.height == 200 76 | 77 | # Test float arguments with enableFloat 78 | r = pyrect.Rect(0.9, 1.9, 100.9, 200.9, enableFloat=True) 79 | assert r.left == 0.9 80 | assert r.top == 1.9 81 | assert r.width == 100.9 82 | assert r.height == 200.9 83 | 84 | # Test float arguments without enableFloat 85 | r = pyrect.Rect(0.9, 1.9, 100.9, 200.9, enableFloat=False) 86 | assert r.left == 0 87 | assert r.top == 1 88 | assert r.width == 100 89 | assert r.height == 200 90 | 91 | r = pyrect.Rect(0.9, 1.9, 100.9, 200.9) # enableFloat should be False by default 92 | assert r.left == 0 93 | assert r.top == 1 94 | assert r.width == 100 95 | assert r.height == 200 96 | 97 | 98 | # Test invalid settings 99 | with pytest.raises(pyrect.PyRectException): 100 | pyrect.Rect('invalid', 1, 100, 200) 101 | with pytest.raises(pyrect.PyRectException): 102 | pyrect.Rect(0, 'invalid', 100, 200) 103 | with pytest.raises(pyrect.PyRectException): 104 | pyrect.Rect(0, 1, 'invalid', 200) 105 | with pytest.raises(pyrect.PyRectException): 106 | pyrect.Rect(0, 1, 100, 'invalid') 107 | 108 | 109 | def test_top(): 110 | r = pyrect.Rect(0, 99, 100, 200) 111 | r.top = 0 112 | assert r.left == 0 113 | assert r.top == 0 114 | assert r.right == 100 115 | assert r.bottom == 200 116 | assert r.width == 100 117 | assert r.height == 200 118 | 119 | with pytest.raises(pyrect.PyRectException): 120 | r.top = 'invalid' 121 | 122 | with pytest.raises(AttributeError): 123 | del r.top 124 | 125 | r.enableFloat = True 126 | r.top = 99.1 127 | assert r.top == 99.1 128 | 129 | 130 | def test_bottom(): 131 | r = pyrect.Rect(0, 99, 100, 200) 132 | r.bottom = 200 133 | assert r.left == 0 134 | assert r.top == 0 135 | assert r.right == 100 136 | assert r.bottom == 200 137 | assert r.width == 100 138 | assert r.height == 200 139 | 140 | with pytest.raises(pyrect.PyRectException): 141 | r.bottom = 'invalid' 142 | 143 | with pytest.raises(AttributeError): 144 | del r.bottom 145 | 146 | r.enableFloat = True 147 | r.bottom = 99.1 148 | assert r.bottom == 99.1 149 | 150 | 151 | def test_left(): 152 | r = pyrect.Rect(99, 0, 100, 200) 153 | r.left = 0 154 | assert r.left == 0 155 | assert r.top == 0 156 | assert r.right == 100 157 | assert r.bottom == 200 158 | assert r.width == 100 159 | assert r.height == 200 160 | 161 | with pytest.raises(pyrect.PyRectException): 162 | r.left = 'invalid' 163 | 164 | with pytest.raises(AttributeError): 165 | del r.left 166 | 167 | r.enableFloat = True 168 | r.left = 99.1 169 | assert r.left == 99.1 170 | 171 | 172 | def test_right(): 173 | r = pyrect.Rect(99, 0, 100, 200) 174 | r.right = 100 175 | assert r.left == 0 176 | assert r.top == 0 177 | assert r.right == 100 178 | assert r.bottom == 200 179 | assert r.width == 100 180 | assert r.height == 200 181 | 182 | with pytest.raises(pyrect.PyRectException): 183 | r.right = 'invalid' 184 | 185 | with pytest.raises(AttributeError): 186 | del r.right 187 | 188 | r.enableFloat = True 189 | r.right = 99.1 190 | assert r.right == 99.1 191 | 192 | 193 | def test_width(): 194 | r = pyrect.Rect(0, 0, 101, 200) 195 | r.width = 100 196 | assert r.left == 0 197 | assert r.top == 0 198 | assert r.right == 100 199 | assert r.bottom == 200 200 | assert r.width == 100 201 | assert r.height == 200 202 | 203 | with pytest.raises(pyrect.PyRectException): 204 | r.width = 'invalid' 205 | 206 | with pytest.raises(AttributeError): 207 | del r.width 208 | 209 | r.enableFloat = True 210 | r.width = 99.1 211 | assert r.width == 99.1 212 | 213 | 214 | def test_height(): 215 | r = pyrect.Rect(0, 0, 100, 201) 216 | r.height = 200 217 | assert r.left == 0 218 | assert r.top == 0 219 | assert r.right == 100 220 | assert r.bottom == 200 221 | assert r.width == 100 222 | assert r.height == 200 223 | 224 | with pytest.raises(pyrect.PyRectException): 225 | r.height = 'invalid' 226 | 227 | with pytest.raises(AttributeError): 228 | del r.height 229 | 230 | r.enableFloat = True 231 | r.height = 99.1 232 | assert r.height == 99.1 233 | 234 | 235 | def test_topleft(): 236 | r = pyrect.Rect(0, 99, 100, 200) 237 | r.topleft = (100, 150) 238 | assert r.topleft == (100, 150) 239 | assert r.left == 100 240 | assert r.top == 150 241 | assert r.right == 200 242 | assert r.bottom == 350 243 | assert r.width == 100 244 | assert r.height == 200 245 | 246 | with pytest.raises(pyrect.PyRectException): 247 | r.topleft = 'invalid' 248 | with pytest.raises(pyrect.PyRectException): 249 | r.topleft = 42 250 | 251 | with pytest.raises(AttributeError): 252 | del r.topleft 253 | 254 | r.enableFloat = True 255 | r.topleft = (99.1, 99.2) 256 | assert r.topleft == (99.1, 99.2) 257 | 258 | 259 | def test_topright(): 260 | r = pyrect.Rect(0, 99, 100, 200) 261 | r.topright = (100, 150) 262 | assert r.topright == (100, 150) 263 | assert r.left == 0 264 | assert r.top == 150 265 | assert r.right == 100 266 | assert r.bottom == 350 267 | assert r.width == 100 268 | assert r.height == 200 269 | 270 | with pytest.raises(pyrect.PyRectException): 271 | r.topright = 'invalid' 272 | with pytest.raises(pyrect.PyRectException): 273 | r.topright = 42 274 | 275 | with pytest.raises(AttributeError): 276 | del r.topright 277 | 278 | r.enableFloat = True 279 | r.topright = (99.1, 99.2) 280 | assert r.topright == (99.1, 99.2) 281 | 282 | 283 | def test_bottomleft(): 284 | r = pyrect.Rect(0, 99, 100, 200) 285 | r.bottomleft = (100, 150) 286 | assert r.bottomleft == (100, 150) 287 | assert r.left == 100 288 | assert r.top == -50 289 | assert r.right == 200 290 | assert r.bottom == 150 291 | assert r.width == 100 292 | assert r.height == 200 293 | 294 | with pytest.raises(pyrect.PyRectException): 295 | r.bottomleft = 'invalid' 296 | with pytest.raises(pyrect.PyRectException): 297 | r.bottomleft = 42 298 | 299 | with pytest.raises(AttributeError): 300 | del r.bottomleft 301 | 302 | r.enableFloat = True 303 | r.bottomleft = (99.1, 99.2) 304 | assert r.bottomleft == (99.1, 99.2) 305 | 306 | 307 | def test_bottomright(): 308 | r = pyrect.Rect(0, 99, 100, 200) 309 | r.bottomright = (100, 150) 310 | assert r.bottomright == (100, 150) 311 | assert r.left == 0 312 | assert r.top == -50 313 | assert r.right == 100 314 | assert r.bottom == 150 315 | assert r.width == 100 316 | assert r.height == 200 317 | 318 | with pytest.raises(pyrect.PyRectException): 319 | r.bottomright = 'invalid' 320 | with pytest.raises(pyrect.PyRectException): 321 | r.bottomright = 42 322 | 323 | with pytest.raises(AttributeError): 324 | del r.bottomright 325 | 326 | r.enableFloat = True 327 | r.bottomright = (99.1, 99.2) 328 | assert r.bottomright == (99.1, 99.2) 329 | 330 | 331 | def test_midleft(): 332 | r = pyrect.Rect(0, 99, 100, 200) 333 | r.midleft = (100, 150) 334 | assert r.midleft == (100, 150) 335 | assert r.left == 100 336 | assert r.top == 50 337 | assert r.right == 200 338 | assert r.bottom == 250 339 | assert r.width == 100 340 | assert r.height == 200 341 | 342 | with pytest.raises(pyrect.PyRectException): 343 | r.midleft = 'invalid' 344 | with pytest.raises(pyrect.PyRectException): 345 | r.midleft = 42 346 | 347 | with pytest.raises(AttributeError): 348 | del r.midleft 349 | 350 | r.enableFloat = True 351 | r.midleft = (99.1, 99.2) 352 | assert r.midleft == (99.1, 99.2) 353 | 354 | 355 | def test_midright(): 356 | r = pyrect.Rect(0, 99, 100, 200) 357 | r.midright = (100, 150) 358 | assert r.midright == (100, 150) 359 | assert r.left == 0 360 | assert r.top == 50 361 | assert r.right == 100 362 | assert r.bottom == 250 363 | assert r.width == 100 364 | assert r.height == 200 365 | 366 | with pytest.raises(pyrect.PyRectException): 367 | r.midright = 'invalid' 368 | with pytest.raises(pyrect.PyRectException): 369 | r.midright = 42 370 | 371 | with pytest.raises(AttributeError): 372 | del r.midright 373 | 374 | r.enableFloat = True 375 | r.midright = (99.1, 99.2) 376 | assert r.midright == (99.1, 99.2) 377 | 378 | 379 | def test_midtop(): 380 | r = pyrect.Rect(0, 99, 100, 200) 381 | r.midtop = (100, 150) 382 | assert r.midtop == (100, 150) 383 | assert r.left == 50 384 | assert r.top == 150 385 | assert r.right == 150 386 | assert r.bottom == 350 387 | assert r.width == 100 388 | assert r.height == 200 389 | 390 | with pytest.raises(pyrect.PyRectException): 391 | r.midtop = 'invalid' 392 | with pytest.raises(pyrect.PyRectException): 393 | r.midtop = 42 394 | 395 | with pytest.raises(AttributeError): 396 | del r.midtop 397 | 398 | r.enableFloat = True 399 | r.midtop = (99.1, 99.2) 400 | assert r.midtop == (99.1, 99.2) 401 | 402 | 403 | def test_midbottom(): 404 | r = pyrect.Rect(0, 99, 100, 200) 405 | r.midbottom = (100, 150) 406 | assert r.midbottom == (100, 150) 407 | assert r.left == 50 408 | assert r.top == -50 409 | assert r.right == 150 410 | assert r.bottom == 150 411 | assert r.width == 100 412 | assert r.height == 200 413 | 414 | with pytest.raises(pyrect.PyRectException): 415 | r.midbottom = 'invalid' 416 | with pytest.raises(pyrect.PyRectException): 417 | r.midbottom = 42 418 | 419 | with pytest.raises(AttributeError): 420 | del r.midbottom 421 | 422 | r.enableFloat = True 423 | r.midbottom = (99.1, 99.2) 424 | assert r.midbottom == (99.1, 99.2) 425 | 426 | 427 | def test_center(): 428 | r = pyrect.Rect(0, 99, 100, 200) 429 | r.center = (100, 150) 430 | assert r.center == (100, 150) 431 | assert r.left == 50 432 | assert r.top == 50 433 | assert r.right == 150 434 | assert r.bottom == 250 435 | assert r.width == 100 436 | assert r.height == 200 437 | 438 | with pytest.raises(pyrect.PyRectException): 439 | r.center = 'invalid' 440 | with pytest.raises(pyrect.PyRectException): 441 | r.center = 42 442 | 443 | with pytest.raises(AttributeError): 444 | del r.center 445 | 446 | r.enableFloat = True 447 | r.center = (99.1, 99.2) 448 | assert r.center == (99.1, 99.2) 449 | 450 | 451 | def test_centerx(): 452 | r = pyrect.Rect(0, 150, 100, 200) 453 | r.centerx = 100 454 | assert r.centerx == 100 455 | assert r.left == 50 456 | assert r.top == 150 457 | assert r.right == 150 458 | assert r.bottom == 350 459 | assert r.width == 100 460 | assert r.height == 200 461 | 462 | with pytest.raises(pyrect.PyRectException): 463 | r.centerx = 'invalid' 464 | 465 | with pytest.raises(AttributeError): 466 | del r.centerx 467 | 468 | r.enableFloat = True 469 | r.centerx = 99.1 470 | assert r.centerx == 99.1 471 | 472 | 473 | def test_centery(): 474 | r = pyrect.Rect(0, 99, 100, 200) 475 | r.centery = 100 476 | assert r.centery == 100 477 | assert r.left == 0 478 | assert r.top == 0 479 | assert r.right == 100 480 | assert r.bottom == 200 481 | assert r.width == 100 482 | assert r.height == 200 483 | 484 | with pytest.raises(pyrect.PyRectException): 485 | r.centery = 'invalid' 486 | 487 | with pytest.raises(AttributeError): 488 | del r.centery 489 | 490 | r.enableFloat = True 491 | r.centery = 99.1 492 | assert r.centery == 99.1 493 | 494 | 495 | def test_size(): 496 | r = pyrect.Rect(0, 0, 100, 200) 497 | r.size = (22, 33) 498 | assert r.size == (22, 33) 499 | assert r.left == 0 500 | assert r.top == 0 501 | assert r.right == 22 502 | assert r.bottom == 33 503 | assert r.width == 22 504 | assert r.height == 33 505 | 506 | with pytest.raises(pyrect.PyRectException): 507 | r.size = 'invalid' 508 | 509 | with pytest.raises(AttributeError): 510 | del r.size 511 | 512 | r.enableFloat = True 513 | r.size = (99.1, 99.2) 514 | assert r.size == (99.1, 99.2) 515 | 516 | 517 | def test__checkForIntOrFloat(): 518 | pyrect._checkForIntOrFloat(42) 519 | pyrect._checkForIntOrFloat(3.14) 520 | with pytest.raises(pyrect.PyRectException): 521 | pyrect._checkForIntOrFloat('invalid') 522 | 523 | 524 | def test__checkForInt(): 525 | pyrect._checkForInt(42) 526 | with pytest.raises(pyrect.PyRectException): 527 | pyrect._checkForInt(3.14) 528 | 529 | 530 | def test__checkForTwoIntOrFloatTuple(): 531 | pyrect._checkForTwoIntOrFloatTuple((0, 0)) 532 | pyrect._checkForTwoIntOrFloatTuple((0.5, 0)) 533 | pyrect._checkForTwoIntOrFloatTuple((0, 0.5)) 534 | pyrect._checkForTwoIntOrFloatTuple((0.5, 0.5)) 535 | 536 | # Test invalid values 537 | with pytest.raises(pyrect.PyRectException): 538 | pyrect._checkForTwoIntOrFloatTuple('invalid') 539 | with pytest.raises(pyrect.PyRectException): 540 | pyrect._checkForTwoIntOrFloatTuple(('invalid', 0)) 541 | with pytest.raises(pyrect.PyRectException): 542 | pyrect._checkForTwoIntOrFloatTuple((0, 'invalid')) 543 | with pytest.raises(pyrect.PyRectException): 544 | pyrect._checkForTwoIntOrFloatTuple(('invalid', 'invalid')) 545 | 546 | 547 | def test_str(): 548 | r = pyrect.Rect(20, 30, 100, 150) 549 | assert str(r) == '(x=20, y=30, w=100, h=150)' 550 | 551 | 552 | def test_repr(): 553 | r = pyrect.Rect(20, 30, 100, 150) 554 | assert repr(r) == 'Rect(left=20, top=30, width=100, height=150)' 555 | 556 | 557 | def test_box(): 558 | # Test invalid settings 559 | r = pyrect.Rect(0, 0, 100, 100) 560 | 561 | assert r.box == (0, 0, 100, 100) 562 | r.box = (1, 2, 3, 4) 563 | assert r.left == 1 564 | assert r.top == 2 565 | assert r.width == 3 566 | assert r.height == 4 567 | 568 | with pytest.raises(pyrect.PyRectException): 569 | r.box = ('invalid', 1, 100, 200) 570 | with pytest.raises(pyrect.PyRectException): 571 | r.box = (0, 'invalid', 100, 200) 572 | with pytest.raises(pyrect.PyRectException): 573 | r.box = (0, 1, 'invalid', 200) 574 | with pytest.raises(pyrect.PyRectException): 575 | r.box = (0, 1, 100, 'invalid') 576 | 577 | with pytest.raises(AttributeError): 578 | del r.box 579 | 580 | r.enableFloat = True 581 | assert r.box == (1.0, 2.0, 3.0, 4.0) 582 | 583 | 584 | def test_operators(): 585 | r1 = pyrect.Rect(0, 0, 100, 100) 586 | r2 = pyrect.Rect(0, 0, 100, 100) 587 | 588 | assert r1 == r2 589 | 590 | 591 | def test_copy(): 592 | r = pyrect.Rect(0, 0, 100, 100) 593 | c = r.copy() 594 | 595 | assert r == c 596 | 597 | 598 | def test_eq_ne(): 599 | r1 = pyrect.Rect(0, 0, 100, 100) 600 | r2 = pyrect.Rect(0, 0, 100, 100) 601 | 602 | assert r1 == r2 603 | assert not r1 != r2 604 | 605 | 606 | def test_onChange_intRects(): 607 | # TODO - using a global variable means this test can't be executed in parallel 608 | global spam 609 | def callbackFn(oldBox, newBox): 610 | global spam 611 | spam = 'changed' 612 | 613 | # testing side changes 614 | spam = 'unchanged' 615 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 616 | r.left = 0 617 | assert spam == 'unchanged' 618 | r.left = 1000 # changing left 619 | assert spam == 'changed' 620 | 621 | spam = 'unchanged' 622 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 623 | r.top = 10 624 | assert spam == 'unchanged' 625 | r.top = 1000 # changing top 626 | assert spam == 'changed' 627 | 628 | spam = 'unchanged' 629 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 630 | r.right = 100 631 | assert spam == 'unchanged' 632 | r.right = 1000 # changing right 633 | assert spam == 'changed' 634 | 635 | spam = 'unchanged' 636 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 637 | r.bottom = 210 638 | assert spam == 'unchanged' 639 | r.bottom = 1000 # changing bottom 640 | assert spam == 'changed' 641 | 642 | # testing size changes 643 | spam = 'unchanged' 644 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 645 | r.width = 100 646 | assert spam == 'unchanged' 647 | r.width = 1000 # changing width 648 | assert spam == 'changed' 649 | 650 | spam = 'unchanged' 651 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 652 | r.height = 200 653 | assert spam == 'unchanged' 654 | r.height = 1000 # changing height 655 | assert spam == 'changed' 656 | 657 | 658 | spam = 'unchanged' 659 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 660 | r.size = (100, 200) 661 | assert spam == 'unchanged' 662 | r.size = (1000, 200) # changing width 663 | assert spam == 'changed' 664 | 665 | spam = 'unchanged' 666 | r.size = (1000, 1000) # changing height 667 | assert spam == 'changed' 668 | 669 | spam = 'unchanged' 670 | r.size = (2000, 2000) # changing width and height 671 | assert spam == 'changed' 672 | 673 | 674 | # testing corner changes 675 | spam = 'unchanged' 676 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 677 | r.topleft = (0, 10) 678 | assert spam == 'unchanged' 679 | r.topleft = (1000, 10) # changing left 680 | assert spam == 'changed' 681 | 682 | spam = 'unchanged' 683 | r.topleft = (1000, 1000) # changing top 684 | assert spam == 'changed' 685 | 686 | spam = 'unchanged' 687 | r.topleft = (2000, 2000) # changing top and left 688 | assert spam == 'changed' 689 | 690 | 691 | spam = 'unchanged' 692 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 693 | r.topright = (100, 10) 694 | assert spam == 'unchanged' 695 | r.topright = (1000, 10) # changing right 696 | assert spam == 'changed' 697 | 698 | spam = 'unchanged' 699 | r.topright = (1000, 1000) # changing top 700 | assert spam == 'changed' 701 | 702 | spam = 'unchanged' 703 | r.topright = (2000, 2000) # changing top and left 704 | assert spam == 'changed' 705 | 706 | 707 | spam = 'unchanged' 708 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 709 | r.bottomleft = (0, 210) 710 | assert spam == 'unchanged' 711 | r.bottomleft = (1000, 10) # changing left 712 | assert spam == 'changed' 713 | 714 | spam = 'unchanged' 715 | r.bottomleft = (1000, 1000) # changing bottom 716 | assert spam == 'changed' 717 | 718 | spam = 'unchanged' 719 | r.bottomleft = (2000, 2000) # changing bottom and left 720 | assert spam == 'changed' 721 | 722 | 723 | spam = 'unchanged' 724 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 725 | r.bottomright = (100, 210) 726 | assert spam == 'unchanged' 727 | r.bottomright = (1000, 210) # changing right 728 | assert spam == 'changed' 729 | 730 | spam = 'unchanged' 731 | r.bottomright = (1000, 1000) # changing bottom 732 | assert spam == 'changed' 733 | 734 | spam = 'unchanged' 735 | r.bottomright = (2000, 2000) # changing bottom and right 736 | assert spam == 'changed' 737 | 738 | 739 | # test midpoints 740 | spam = 'unchanged' 741 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 742 | r.midleft = (0, 110) 743 | assert spam == 'unchanged' 744 | r.midleft = (1000, 110) # changing right 745 | assert spam == 'changed' 746 | 747 | spam = 'unchanged' 748 | r.midleft = (1000, 1000) # changing mid left 749 | assert spam == 'changed' 750 | 751 | spam = 'unchanged' 752 | r.midleft = (2000, 2000) # changing mid left and right 753 | assert spam == 'changed' 754 | 755 | 756 | spam = 'unchanged' 757 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 758 | r.midright = (100, 110) 759 | assert spam == 'unchanged' 760 | r.midright = (1000, 110) # changing right 761 | assert spam == 'changed' 762 | 763 | spam = 'unchanged' 764 | r.midright = (1000, 1000) # changing mid right 765 | assert spam == 'changed' 766 | 767 | spam = 'unchanged' 768 | r.midright = (2000, 2000) # changing mid right and right 769 | assert spam == 'changed' 770 | 771 | 772 | spam = 'unchanged' 773 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 774 | r.midtop = (50, 10) 775 | assert spam == 'unchanged' 776 | r.midtop = (1000, 10) # changing mid top 777 | assert spam == 'changed' 778 | 779 | spam = 'unchanged' 780 | r.midtop = (1000, 1000) # changing top 781 | assert spam == 'changed' 782 | 783 | spam = 'unchanged' 784 | r.midtop = (2000, 2000) # changing mid top and top 785 | assert spam == 'changed' 786 | 787 | 788 | spam = 'unchanged' 789 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 790 | r.midbottom = (50, 210) 791 | assert spam == 'unchanged' 792 | r.midbottom = (1000, 210) # changing mid bottom 793 | assert spam == 'changed' 794 | 795 | spam = 'unchanged' 796 | r.midbottom = (1000, 1000) # changing bottom 797 | assert spam == 'changed' 798 | 799 | spam = 'unchanged' 800 | r.midbottom = (2000, 2000) # changing bottom and mid bottom 801 | assert spam == 'changed' 802 | 803 | # testing center 804 | spam = 'unchanged' 805 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 806 | r.center = (50, 110) 807 | assert spam == 'unchanged' 808 | r.center = (1000, 110) # changing centerx 809 | assert spam == 'changed' 810 | 811 | spam = 'unchanged' 812 | r.center = (1000, 1000) # changing centery 813 | assert spam == 'changed' 814 | 815 | spam = 'unchanged' 816 | r.center = (2000, 2000) # changing centerx and centery 817 | assert spam == 'changed' 818 | 819 | 820 | spam = 'unchanged' 821 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 822 | r.centerx = 50 823 | assert spam == 'unchanged' 824 | r.centerx = 1000 # changing centerx 825 | assert spam == 'changed' 826 | 827 | 828 | spam = 'unchanged' 829 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 830 | r.centery = 110 831 | assert spam == 'unchanged' 832 | r.centery = 1000 # changing mid bottom 833 | assert spam == 'changed' 834 | 835 | # testing box 836 | spam = 'unchanged' 837 | r = pyrect.Rect(0, 10, 100, 200, onChange=callbackFn) 838 | r.box = (0, 10, 100, 200) 839 | assert spam == 'unchanged' 840 | r.box = (1000, 10, 100, 200) # changing left 841 | assert spam == 'changed' 842 | 843 | spam = 'unchanged' 844 | r.box = (1000, 2000, 100, 200) # changing top 845 | assert spam == 'changed' 846 | 847 | spam = 'unchanged' 848 | r.box = (1000, 2000, 3000, 200) # changing width 849 | assert spam == 'changed' 850 | 851 | spam = 'unchanged' 852 | r.box = (1000, 2000, 3000, 4000) # changing height 853 | assert spam == 'changed' 854 | 855 | 856 | def test_readonly(): 857 | r = pyrect.Rect(0, 10, 100, 200, readOnly=True) 858 | with pytest.raises(pyrect.PyRectException): 859 | r.left = 1000 860 | with pytest.raises(pyrect.PyRectException): 861 | r.right = 1000 862 | with pytest.raises(pyrect.PyRectException): 863 | r.top = 1000 864 | with pytest.raises(pyrect.PyRectException): 865 | r.bottom = 1000 866 | with pytest.raises(pyrect.PyRectException): 867 | r.centerx = 1000 868 | with pytest.raises(pyrect.PyRectException): 869 | r.centery = 1000 870 | with pytest.raises(pyrect.PyRectException): 871 | r.width = 1000 872 | with pytest.raises(pyrect.PyRectException): 873 | r.height = 1000 874 | with pytest.raises(pyrect.PyRectException): 875 | r.topleft = (1000, 2000) 876 | with pytest.raises(pyrect.PyRectException): 877 | r.topright = (1000, 2000) 878 | with pytest.raises(pyrect.PyRectException): 879 | r.bottomleft = (1000, 2000) 880 | with pytest.raises(pyrect.PyRectException): 881 | r.bottomright = (1000, 2000) 882 | with pytest.raises(pyrect.PyRectException): 883 | r.size = (1000, 2000) 884 | with pytest.raises(pyrect.PyRectException): 885 | r.midleft = (1000, 2000) 886 | with pytest.raises(pyrect.PyRectException): 887 | r.midright = (1000, 2000) 888 | with pytest.raises(pyrect.PyRectException): 889 | r.midtop = (1000, 2000) 890 | with pytest.raises(pyrect.PyRectException): 891 | r.midbottom = (1000, 2000) 892 | with pytest.raises(pyrect.PyRectException): 893 | r.box = (1000, 2000, 3000, 4000) 894 | 895 | 896 | if __name__ == '__main__': 897 | pytest.main() 898 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py25, py26, py27, py31, py32, py33, py34, py35, py36 8 | 9 | [testenv] 10 | deps = 11 | pygame 12 | pytest 13 | commands = 14 | pytest 15 | --------------------------------------------------------------------------------