diff --git a/laplas/abstract_map/objects/camera.py b/laplas/abstract_map/objects/camera.py new file mode 100644 index 0000000..6922b96 --- /dev/null +++ b/laplas/abstract_map/objects/camera.py @@ -0,0 +1,46 @@ +import pygame + +class camera: + def __init__(self, screen, surface, new_x, new_y) -> None: + self.width = 800 + self.height = 600 + self.dragging = False + self.last_mouse_x = 0 + self.last_mouse_y = 0 + + self.x = new_x + self.y = new_y + + self.screen = screen + self.render_surface = surface + self.camera_rect = pygame.Rect(self.x, self.y, self.width, self.height) + + print(f"Camera initialized at ({self.x}, {self.y})") # Отладочный вывод + + def process(self): + for event in pygame.event.get(): + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + self.dragging = True + self.last_mouse_x, self.last_mouse_y = event.pos + print(f"Started dragging at ({self.last_mouse_x}, {self.last_mouse_y})") + + if event.type == pygame.MOUSEBUTTONUP: + if event.button == 1: + self.dragging = False + print("Stopped dragging") + + if event.type == pygame.MOUSEMOTION: + if self.dragging: + mouse_x, mouse_y = event.pos + dx = mouse_x - self.last_mouse_x + dy = mouse_y - self.last_mouse_y + self.x -= dx + self.y -= dy + self.last_mouse_x, self.last_mouse_y = mouse_x, mouse_y + + print(f"Dragging to ({self.x}, {self.y})") + + self.camera_rect.topleft = (self.x, self.y) + self.screen.blit(self.render_surface, (0, 0), self.camera_rect) + print(f"Camera rect top-left at ({self.camera_rect.x}, {self.camera_rect.y})") # Отладочный вывод diff --git a/laplas/tools/abstract_map/objects/object.py b/laplas/abstract_map/objects/object.py similarity index 100% rename from laplas/tools/abstract_map/objects/object.py rename to laplas/abstract_map/objects/object.py diff --git a/laplas/tools/abstract_map/overmap.py b/laplas/abstract_map/overmap.py similarity index 74% rename from laplas/tools/abstract_map/overmap.py rename to laplas/abstract_map/overmap.py index 0eede1e..52384f1 100644 --- a/laplas/tools/abstract_map/overmap.py +++ b/laplas/abstract_map/overmap.py @@ -1,13 +1,11 @@ import pygame from objects import object -import camera +from objects import camera camera_width, camera_height = 800, 600 class overmap: - pygame.init() - def __init__(self, size_x: int, size_y: int, visual: bool) -> None: # A map full size self.size_x = size_x @@ -23,19 +21,22 @@ def __init__(self, size_x: int, size_y: int, visual: bool) -> None: self.screen = pygame.display.set_mode((camera_width, camera_height)) self.create_map() self.create_camera() + + self.clock = pygame.time.Clock() pygame.display.set_caption("Large Map with Coordinate System") + self.camera = camera ## Overmap functions # Actually creates a non physical map, ans setups a cordinates system def create_map(self): self.map_holder = pygame.Surface((self.size_x, self.size_y)) - self.level_surface.fill((25, 25, 25)) + self.map_holder.fill((25, 25, 25)) def create_camera(self): - camera_x, camera_y = 0, 0 - self.camera = camera(self.map_holder, self.screen, camera_x, camera_y) + camera_x, camera_y = 1, 1 +# self.camera = camera(self.map_holder, self.screen, camera_x, camera_y) ## Function for manipulate with objects def create_object(self, obj, args): @@ -48,9 +49,15 @@ def adjust_speed(self): pass def process(self): - self.camera.process() +# self.camera.process(self.camera) + self.clock.tick(60) if(not self.all_object.__len__()): return for obj in self.all_object: obj.process() + + +def process_map(state: bool): + while(state): + overmap.process() diff --git a/laplas/abstract_map/pygame-2.6.0.dist-info/INSTALLER b/laplas/abstract_map/pygame-2.6.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/laplas/abstract_map/pygame-2.6.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/laplas/abstract_map/pygame-2.6.0.dist-info/METADATA b/laplas/abstract_map/pygame-2.6.0.dist-info/METADATA new file mode 100644 index 0000000..8e53e0e --- /dev/null +++ b/laplas/abstract_map/pygame-2.6.0.dist-info/METADATA @@ -0,0 +1,314 @@ +Metadata-Version: 2.1 +Name: pygame +Version: 2.6.0 +Summary: Python Game Development +Home-page: https://www.pygame.org +Author: A community project. +Author-email: pygame@pygame.org +License: LGPL +Project-URL: Documentation, https://pygame.org/docs +Project-URL: Bug Tracker, https://github.com/pygame/pygame/issues +Project-URL: Source, https://github.com/pygame/pygame +Project-URL: Twitter, https://twitter.com/pygame_org +Platform: UNKNOWN +Classifier: Development Status :: 6 - Mature +Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) +Classifier: Programming Language :: Assembly +Classifier: Programming Language :: C +Classifier: Programming Language :: Cython +Classifier: Programming Language :: Objective C +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Games/Entertainment +Classifier: Topic :: Multimedia :: Sound/Audio +Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI +Classifier: Topic :: Multimedia :: Sound/Audio :: Players +Classifier: Topic :: Multimedia :: Graphics +Classifier: Topic :: Multimedia :: Graphics :: Capture :: Digital Camera +Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture +Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion +Classifier: Topic :: Multimedia :: Graphics :: Viewers +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Operating System :: Unix +Classifier: Operating System :: MacOS +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst + +.. image:: https://raw.githubusercontent.com/pygame/pygame/main/docs/reST/_static/pygame_logo.svg + :alt: pygame + :target: https://www.pygame.org/ + + +|AppVeyorBuild| |PyPiVersion| |PyPiLicense| +|Python3| |GithubCommits| |BlackFormatBadge| + +Pygame_ is a free and open-source cross-platform library +for the development of multimedia applications like video games using Python. +It uses the `Simple DirectMedia Layer library`_ and several other +popular libraries to abstract the most common functions, making writing +these programs a more intuitive task. + +`We need your help`_ to make pygame the best it can be! +New contributors are welcome. + + +Installation +------------ + +Before installing pygame, you must check that Python is installed +on your machine. To find out, open a command prompt (if you have +Windows) or a terminal (if you have MacOS or Linux) and type this: +:: + + python --version + + +If a message such as "Python 3.8.10" appears, it means that Python +is correctly installed. If an error message appears, it means that +it is not installed yet. You must then go to the `official website +`_ to download it. + +Once Python is installed, you have to perform a final check: you have +to see if pip is installed. Generally, pip is pre-installed with +Python but we are never sure. Same as for Python, type the following +command: +:: + + pip --version + + +If a message such as "pip 20.0.2 from /usr/lib/python3/dist-packages/pip +(python 3.8)" appears, you are ready to install pygame! To install +it, enter this command: +:: + + pip install pygame + +Once pygame is installed, quickly test your library by entering the following +command, which opens one of the many example games that comes pre-installed: +:: + + python3 -m pygame.examples.aliens + + +If this doesn’t work, the `Getting Started +`_ section of the official +website has more information for platform specific issues, such as adding +python to your machine’s PATH settings + + +Help +---- + +If you are just getting started with pygame, you should be able to +get started fairly quickly. Pygame comes with many tutorials and +introductions. There is also full reference documentation for the +entire library. Browse the documentation on the `docs page`_. You +can also browse the documentation locally by running +``python -m pygame.docs`` in your terminal. If the docs aren't found +locally, it'll launch the online website instead. + +The online documentation stays up to date with the development version +of pygame on GitHub. This may be a bit newer than the version of pygame +you are using. To upgrade to the latest full release, run +``pip install pygame --upgrade`` in your terminal. + +Best of all, the examples directory has many playable small programs +which can get you started playing with the code right away. + + +Features +---------- + +Pygame is a powerful library for game development, offering a wide +range of features to simplify your coding journey. Let's delve into +what pygame has to offer: + +Graphics - With pygame, creating dynamic and engaging graphics has +never been easier. The library provides simple yet effective tools for +2D graphics and animation, including support for images, rectangles, +and polygon shapes. Whether you're a seasoned game developer or just +starting out, pygame has you covered. + +Sound - Pygame also includes support for playing and manipulating sound +and music, making it easy to add sound effects and background music to +your games. With support for WAV, MP3, and OGG file formats, you have +plenty of options to choose from. + +Input - Pygame provides intuitive functions for handling keyboard, mouse, +and joystick input, allowing you to quickly and easily implement player +controls in your games. No more struggling with complex input code, pygame +makes it simple. + +Game Development - Lastly, pygame provides a comprehensive suite of tools +and features specifically designed for game development. From collision +detection to sprite management, pygame has everything you need to create +exciting and engaging games. Whether you're building a platformer, puzzle +game, or anything in between, pygame has you covered. + + +Building From Source +-------------------- + +If you want to use features that are currently in development, +or you want to contribute to pygame, you will need to build pygame +locally from its source code, rather than pip installing it. + +Installing from source is fairly automated. The most work will +involve compiling and installing all the pygame dependencies. Once +that is done, run the ``setup.py`` script which will attempt to +auto-configure, build, and install pygame. + +Much more information about installing and compiling is available +on the `Compilation wiki page`_. + +Contribute +---------- + +* `Documentation Contributions `_ - Guidelines for contributing to the main documentations +* `Writing your first unit test `_ - Step by step guide on how to write your first unit test in Python for Pygame. +* `How to Hack Pygame `_ - Information on hacking, developing, and modifying Pygame +* `Issue Tracker for beginners `_ - A way for beginners to contribute to the project +* `Bugs & Patches `_ - Report bugs +* `Communication tools `_ - More information and ways to get in touch with the Pygame team + + +Credits +------- + +Thanks to everyone who has helped contribute to this library. +Special thanks are also in order. + +* Marcus Von Appen: many changes, and fixes, 1.7.1+ freebsd maintainer +* Lenard Lindstrom: the 1.8+ windows maintainer, many changes, and fixes +* Brian Fisher for svn auto builder, bug tracker and many contributions +* Rene Dudfield: many changes, and fixes, 1.7+ release manager/maintainer +* Phil Hassey for his work on the pygame.org website +* DR0ID for his work on the sprite module +* Richard Goedeken for his smoothscale function +* Ulf Ekström for his pixel perfect collision detection code +* Pete Shinners: original author +* David Clark for filling the right-hand-man position +* Ed Boraas and Francis Irving: Debian packages +* Maxim Sobolev: FreeBSD packaging +* Bob Ippolito: MacOS and OS X porting (much work!) +* Jan Ekhol, Ray Kelm, and Peter Nicolai: putting up with early design ideas +* Nat Pryce for starting our unit tests +* Dan Richter for documentation work +* TheCorruptor for his incredible logos and graphics +* Nicholas Dudfield: many test improvements +* Alex Folkner for pygame-ctypes + +Thanks to those sending in patches and fixes: Niki Spahiev, Gordon +Tyler, Nathaniel Pryce, Dave Wallace, John Popplewell, Michael Urman, +Andrew Straw, Michael Hudson, Ole Martin Bjoerndalen, Herve Cauwelier, +James Mazer, Lalo Martins, Timothy Stranex, Chad Lester, Matthias +Spiller, Bo Jangeborg, Dmitry Borisov, Campbell Barton, Diego Essaya, +Eyal Lotem, Regis Desgroppes, Emmanuel Hainry, Randy Kaelber +Matthew L Daniel, Nirav Patel, Forrest Voight, Charlie Nolan, +Frankie Robertson, John Krukoff, Lorenz Quack, Nick Irvine, +Michael George, Saul Spatz, Thomas Ibbotson, Tom Rothamel, Evan Kroske, +Cambell Barton. + +And our bug hunters above and beyond: Angus, Guillaume Proux, Frank +Raiser, Austin Henry, Kaweh Kazemi, Arturo Aldama, Mike Mulcheck, +Michael Benfield, David Lau + +There's many more folks out there who've submitted helpful ideas, kept +this project going, and basically made our life easier. Thanks! + +Many thank you's for people making documentation comments, and adding to the +pygame.org wiki. + +Also many thanks for people creating games and putting them on the +pygame.org website for others to learn from and enjoy. + +Lots of thanks to James Paige for hosting the pygame bugzilla. + +Also a big thanks to Roger Dingledine and the crew at SEUL.ORG for our +excellent hosting. + +Dependencies +------------ + +Pygame is obviously strongly dependent on SDL and Python. It also +links to and embeds several other smaller libraries. The font +module relies on SDL_ttf, which is dependent on freetype. The mixer +(and mixer.music) modules depend on SDL_mixer. The image module +depends on SDL_image, which also can use libjpeg and libpng. The +transform module has an embedded version of SDL_rotozoom for its +own rotozoom function. The surfarray module requires the Python +NumPy package for its multidimensional numeric arrays. +Dependency versions: + + ++----------+------------------------+ +| CPython | >= 3.6 (Or use PyPy3) | ++----------+------------------------+ +| SDL | >= 2.0.8 | ++----------+------------------------+ +| SDL_mixer| >= 2.0.0 | ++----------+------------------------+ +| SDL_image| >= 2.0.2 | ++----------+------------------------+ +| SDL_ttf | >= 2.0.11 | ++----------+------------------------+ +| SDL_gfx | (Optional, vendored in)| ++----------+------------------------+ +| NumPy | >= 1.6.2 (Optional) | ++----------+------------------------+ + + + +License +------- + +This library is distributed under `GNU LGPL version 2.1`_, which can +be found in the file ``docs/LGPL.txt``. We reserve the right to place +future versions of this library under a different license. + +This basically means you can use pygame in any project you want, +but if you make any changes or additions to pygame itself, those +must be released with a compatible license (preferably submitted +back to the pygame project). Closed source and commercial games are fine. + +The programs in the ``examples`` subdirectory are in the public domain. + +See docs/licenses for licenses of dependencies. + + +.. |AppVeyorBuild| image:: https://ci.appveyor.com/api/projects/status/x4074ybuobsh4myx?svg=true + :target: https://ci.appveyor.com/project/pygame/pygame + +.. |PyPiVersion| image:: https://img.shields.io/pypi/v/pygame.svg?v=1 + :target: https://pypi.python.org/pypi/pygame + +.. |PyPiLicense| image:: https://img.shields.io/pypi/l/pygame.svg?v=1 + :target: https://pypi.python.org/pypi/pygame + +.. |Python3| image:: https://img.shields.io/badge/python-3-blue.svg?v=1 + +.. |GithubCommits| image:: https://img.shields.io/github/commits-since/pygame/pygame/2.1.2.svg + :target: https://github.com/pygame/pygame/compare/2.1.2...main + +.. |BlackFormatBadge| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + +.. _pygame: https://www.pygame.org +.. _Simple DirectMedia Layer library: https://www.libsdl.org +.. _We need your help: https://www.pygame.org/contribute.html +.. _Compilation wiki page: https://www.pygame.org/wiki/Compilation +.. _docs page: https://www.pygame.org/docs/ +.. _GNU LGPL version 2.1: https://www.gnu.org/copyleft/lesser.html + + diff --git a/laplas/abstract_map/pygame-2.6.0.dist-info/RECORD b/laplas/abstract_map/pygame-2.6.0.dist-info/RECORD new file mode 100644 index 0000000..d75b439 --- /dev/null +++ b/laplas/abstract_map/pygame-2.6.0.dist-info/RECORD @@ -0,0 +1,771 @@ +../../include/python/pygame/_blit_info.h,sha256=wRHRXxQ9k7NBMHHPymtStYuI8Prwqbdu0jE0gouqUYc,470 +../../include/python/pygame/_camera.h,sha256=T0VYAfQxm0c4zww_BZaJGz4exa4z0FdEf3RSN_W2E-E,839 +../../include/python/pygame/_pygame.h,sha256=UhRJF2E7W8CVfKAW5cySBOxf8tvzyAX0tDjEhUrJPeM,11726 +../../include/python/pygame/_surface.h,sha256=Bbi9rW0SqwGs6THID0l6eB5d-5h-kxW7517TdqoxEZM,957 +../../include/python/pygame/camera.h,sha256=SDNTLUp5qj5HHZyrfNR8f-t-lpSNHxj3EXO-WqQNPYo,5605 +../../include/python/pygame/font.h,sha256=VHcKhYtIHduegTXEf1hbmbxCwN5IrqsJch2HaxEtB6I,348 +../../include/python/pygame/freetype.h,sha256=LbGY6saj9oakyoeGSlWlirSHySOpeoTKKOF-DO4zLRs,3245 +../../include/python/pygame/include/_pygame.h,sha256=8ftev1IbOAg9AFaFQBzeA4bnYLM78lriFjjtqzyGIFQ,30436 +../../include/python/pygame/include/bitmask.h,sha256=tGzYwZ407sMIHDQG7xeXBQCRmhZZ4wp-yTerhUmIlCU,4952 +../../include/python/pygame/include/pgcompat.h,sha256=l-At6iLGU8EeMqYdsDMATS2zfkviXWe1Rs7lnOwO7oc,1944 +../../include/python/pygame/include/pgimport.h,sha256=3VrUyOZC6kbEdOcXn1QbOZ3nusBAuFSQxYgDadHNTPI,2636 +../../include/python/pygame/include/pgplatform.h,sha256=4y5BDK1oKeK2i5pO3UfZAbZXv_KdNdco5ZBbrtBsrtc,2312 +../../include/python/pygame/include/pygame.h,sha256=OsEc_zNPFlXo4owOhGRqH4cbuYMtNJ_7K9d_Te--OEU,1245 +../../include/python/pygame/include/pygame_bufferproxy.h,sha256=Poh7HsIjugo3NFeKjItZe40_BavF5V7re8bVFjkpmfU,1834 +../../include/python/pygame/include/pygame_font.h,sha256=JKPbDFQdh_BAuz5F9S37iBSZbfjnL2scUlkS1geLd4s,1501 +../../include/python/pygame/include/pygame_freetype.h,sha256=VNyvy7xukNOXymg3IMZne9-iVu3sI5LvKLagfkWpKAk,1346 +../../include/python/pygame/include/pygame_mask.h,sha256=ONXIz3M3MPF4BlPSS2xRquysEYjZZf7AP2JRro8j4I0,1303 +../../include/python/pygame/include/pygame_mixer.h,sha256=HthA7STa9TLomwQQswroyAmAd72pywDu_UCLfIV71is,2021 +../../include/python/pygame/include/sse2neon.h,sha256=DcazZmLfny6MVJFIUlWbsdI39ZWQWGwmCJDuFEVZsw0,237885 +../../include/python/pygame/mask.h,sha256=Y7OqzNUqQQHchUsSlvd-ja5d9IgAfSV2uFlaa8_5Lys,153 +../../include/python/pygame/mixer.h,sha256=HJMd0Ho0DrdGBdPMDo_egSqkVxZnUZPMEQyPJMIw6_M,348 +../../include/python/pygame/palette.h,sha256=dzARYIsQdHAaV8ypCrQbYRWFisXYXABD4ToMlzYKojg,7057 +../../include/python/pygame/pgarrinter.h,sha256=alsw7p6X7ukOB1o3curyrjWOcGHgVCQgCvS1D9FtiRc,1060 +../../include/python/pygame/pgbufferproxy.h,sha256=tqMDkdkH40QoYJ3NtTjiknAnSMh0i1sfNMaow3npvKI,179 +../../include/python/pygame/pgcompat.h,sha256=Ro6kJ6ak2LQSHR4LmwnulolGuQO0KW6UZiu02iP-1_I,737 +../../include/python/pygame/pgopengl.h,sha256=bbIysbLph5paPfeE2nnrQBIrq8iZz4T4pGfz2mYPuRw,606 +../../include/python/pygame/pgplatform.h,sha256=LuZxNMbDYRCgjDk1_ruNTOiQsyO_FcI9qxNtJeAUpXQ,553 +../../include/python/pygame/pygame.h,sha256=AQcZWIoAWGmN9fYBynCXxc81hd9lcwxBwWeq8WZjTKE,1083 +../../include/python/pygame/scrap.h,sha256=d36ZWp5LM7o9RRcHCFiHwAT0aJZN_c17wTNu_LYHuEE,4704 +../../include/python/pygame/simd_blitters.h,sha256=8Bv4j0uHf4ErZMeJeXjnrnVvKBQdSLmKcrEVgbYJlPc,2494 +../../include/python/pygame/surface.h,sha256=6Goq2WvnwB_mYosiq4wZMIzPXn7uoCyA_fTK4uKtf6I,14584 +pygame-2.6.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pygame-2.6.0.dist-info/METADATA,sha256=0E_Q0bagnKiFLtYrVz5ed36rymbEf4fS8oxye5air6s,12898 +pygame-2.6.0.dist-info/RECORD,, +pygame-2.6.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pygame-2.6.0.dist-info/WHEEL,sha256=zukbZftcRdNfJ55Q_RYV9JZ3uVyqWKyq-wDJbmszs_U,101 +pygame-2.6.0.dist-info/entry_points.txt,sha256=ecT3iHcE_RL35uzQ2sXUdmOZbqq1XtPQNmbotP1FOQY,64 +pygame-2.6.0.dist-info/top_level.txt,sha256=ABXdFGIAE2g9m2VOzQPaLa917r6XEu6d96RqIzvAWCs,7 +pygame/SDL2.dll,sha256=Ug0EWbke-jL7zPkCepyh_FquZX5nnOjpDxefnPWv0nk,2499072 +pygame/SDL2_image.dll,sha256=HjZK91_uDINQb739TVsOOGxOnGoz3b3axh3bEx42AZQ,125440 +pygame/SDL2_mixer.dll,sha256=Kg_F6fcsLq7DJAy4K3WUpYzNpglIWYHyVrlNCk3Y1vg,291840 +pygame/SDL2_ttf.dll,sha256=_N-r386GjrM_dRQCX_WcG7bEGPG81qziMAqc1AU-HWM,1552384 +pygame/__init__.py,sha256=cfk8mST6YzXpJH83W86aq5Sj6HzZYCZ8RYJUUR3KRt4,9483 +pygame/__init__.pyi,sha256=javlxYV7i0wDGZ69E_J2x1PLmXoEN-pfcF0MCcLqKbY,20400 +pygame/__pycache__/__init__.cpython-36.pyc,, +pygame/__pycache__/_camera_opencv.cpython-36.pyc,, +pygame/__pycache__/_camera_vidcapture.cpython-36.pyc,, +pygame/__pycache__/camera.cpython-36.pyc,, +pygame/__pycache__/colordict.cpython-36.pyc,, +pygame/__pycache__/cursors.cpython-36.pyc,, +pygame/__pycache__/draw_py.cpython-36.pyc,, +pygame/__pycache__/fastevent.cpython-36.pyc,, +pygame/__pycache__/freetype.cpython-36.pyc,, +pygame/__pycache__/ftfont.cpython-36.pyc,, +pygame/__pycache__/locals.cpython-36.pyc,, +pygame/__pycache__/macosx.cpython-36.pyc,, +pygame/__pycache__/midi.cpython-36.pyc,, +pygame/__pycache__/pkgdata.cpython-36.pyc,, +pygame/__pycache__/sndarray.cpython-36.pyc,, +pygame/__pycache__/sprite.cpython-36.pyc,, +pygame/__pycache__/surfarray.cpython-36.pyc,, +pygame/__pycache__/sysfont.cpython-36.pyc,, +pygame/__pycache__/version.cpython-36.pyc,, +pygame/__pyinstaller/__init__.py,sha256=-c4Zo8nQGKAm8wc_LDscxMtK7zr_YhZwRnC9CMruUBE,72 +pygame/__pyinstaller/__pycache__/__init__.cpython-36.pyc,, +pygame/__pyinstaller/__pycache__/hook-pygame.cpython-36.pyc,, +pygame/__pyinstaller/hook-pygame.py,sha256=F54YZHmQpTIBH265sM0r5_8_YZYWo0IBhG_QVAQybgI,1368 +pygame/_camera.cp36-win_amd64.pyd,sha256=ycgz6kjDlOUoYMYgPigW6ezRwv1U2ritpG9pPSBV_6w,17408 +pygame/_camera_opencv.py,sha256=DiPrxXiIGUxoz4lSONGs04sOo9pEA8RKQ9cnNO7JC2M,5454 +pygame/_camera_vidcapture.py,sha256=mbPFnTU0520qXpj0qt6ei3YQ1_1OFRuDU-5fXFm-yos,3403 +pygame/_common.pyi,sha256=YPuDlQLtP8KzA-qA9LnYkOhS9zzSvA5ENwWHI2WSvoI,1349 +pygame/_freetype.cp36-win_amd64.pyd,sha256=Zh6qKUHscfFSapl6RxJ3Urgc0gvw-Oesths86ZjEFoM,78336 +pygame/_sdl2/__init__.py,sha256=gmSh3cXyxHqEXwbck_mNOmoW--itmGMwonEHQnY49Zo,248 +pygame/_sdl2/__init__.pyi,sha256=fWR2P9epfYw8F_xyyMBoUxDZwJTIroizEnkN_eLf2rw,98 +pygame/_sdl2/__pycache__/__init__.cpython-36.pyc,, +pygame/_sdl2/audio.cp36-win_amd64.pyd,sha256=ij7EWpWSC0yOejk09M92KAevVSX9k4HXgD_0xOW4Tls,165888 +pygame/_sdl2/audio.pyi,sha256=DRRipL7rySE-0TddObb1A-l99rWQPK4YI36hvyHj4dw,1300 +pygame/_sdl2/controller.cp36-win_amd64.pyd,sha256=lSaE7lHNpJbw0MRcK7v4b_EK1JB6_0YRy_piWwuW6xE,104960 +pygame/_sdl2/controller.pyi,sha256=8WZ0Qf5iHIJTElgu29LeAhZO3Htbo00b8oL3aohK5P4,1165 +pygame/_sdl2/mixer.cp36-win_amd64.pyd,sha256=uCZi3jBZ4NiLVy-97IjRDAM1yVDSqoHGrZyvTSln0AQ,144896 +pygame/_sdl2/sdl2.cp36-win_amd64.pyd,sha256=cl_JktCoC3sHwWkXtQniyCnP_MUyF1WpuLRBW_rmvgA,42496 +pygame/_sdl2/sdl2.pyi,sha256=n0Qa04-g3FdR7wiMEC_4ZhmAojvQTXGjTl1z13OWU70,338 +pygame/_sdl2/touch.cp36-win_amd64.pyd,sha256=BuTpW-Sza4SPW6Ja1lteNCZ9UbsOiHzY0DKKvgRCYCg,13824 +pygame/_sdl2/touch.pyi,sha256=-vYGJsSC18E2gitH10HIv_V_DUc6DIeCb55zuQp3jPc,231 +pygame/_sdl2/video.cp36-win_amd64.pyd,sha256=ifatdKVx-KSTwesyo-L8oz1CLAdW356TSi48K1M4-vM,228864 +pygame/_sdl2/video.pyi,sha256=1ziRZwVFLhFTYubfNBwtyIF7hBWlCiscXul7V4dMcS8,4677 +pygame/_sprite.cp36-win_amd64.pyd,sha256=25QlNSngwNaYbctMdM3gBxTLqIB3F4r1sTTSnGjPg-c,318464 +pygame/base.cp36-win_amd64.pyd,sha256=4ffRlz76iLc5r6hxsODjEU42D1XPbpzTNr0En6I-qsw,30720 +pygame/base.pyi,sha256=MPRqq61kkfR-s7IXcoZyahrRP3VzERTED46GCuVacpg,586 +pygame/bufferproxy.cp36-win_amd64.pyd,sha256=DqG-V4LwXemOq4yDX_W14mDJ1Je0XDpdEOzf8HjfHcU,18432 +pygame/bufferproxy.pyi,sha256=znPTlMWUbbJI2iCUz8F3pY0guxHOzVnQhpnUaPhfB2E,458 +pygame/camera.py,sha256=MklsW1Bj7vEm8_33RMWZhelsgDQF7yy5ZrvGyD4CIG8,6083 +pygame/camera.pyi,sha256=yeFKyldBEFDOUwMRXsqPcDdH2hOgeoth9fOuBJ8Dc8Y,1624 +pygame/color.cp36-win_amd64.pyd,sha256=P4t_QGkkvYS1ZPnrWc_jat5UGb_T_ihOORohosvyAjk,35840 +pygame/color.pyi,sha256=rBSvRlXAFlP4kzuVdI_z5n9TkCcZpz6_2XEAvYHG0gk,2050 +pygame/colordict.py,sha256=xS-mwTxatoVa3Howpp53otRNZ3p-b1MzZChlvy0cuic,25773 +pygame/constants.cp36-win_amd64.pyd,sha256=s5QdGgyOAjCxdd_OB08KHlzrJ7mPinQfJWcfWUEOeag,50688 +pygame/constants.pyi,sha256=PqbIVEVcVE9pKTaCFd9RExuYFf4i-1H_u_zjx6KP44U,9902 +pygame/cursors.py,sha256=kwDhVH_LeFzfvakDmxPYnf8FB3ElmAH8aClRYedi4os,18093 +pygame/cursors.pyi,sha256=rvpf35D0IPYU2SBH30K46RRmigQfQH4SyQfb7TwnNiQ,2090 +pygame/display.cp36-win_amd64.pyd,sha256=EMtGCTQF_SQhnlkNeiopIck1eg3f7QFOFIUXG3Dctok,45056 +pygame/display.pyi,sha256=5rC8JySsJTs0GWDaAG29lQan4HhtzIA0ilVvsi3Vvqs,2273 +pygame/docs/__main__.py,sha256=1dEMTojlXYV8qZ_T9h2f_AYdpPwwyCqmHBC8iuJ2KGI,995 +pygame/docs/__pycache__/__main__.cpython-36.pyc,, +pygame/docs/generated/LGPL.txt,sha256=oZDcnIBDdV2Q-LCnX6ZrnkLUr0yYC_XdxjPwEk2zzuc,26430 +pygame/docs/generated/_images/AdvancedInputOutput1.gif,sha256=uSCxW5dFtO7PYQyoJglWJTe_aRYFfOav5DjnPkKBzKg,5649 +pygame/docs/generated/_images/AdvancedInputOutput11.gif,sha256=uSCxW5dFtO7PYQyoJglWJTe_aRYFfOav5DjnPkKBzKg,5649 +pygame/docs/generated/_images/AdvancedInputOutput2.gif,sha256=2UMDweIgRefzHFTJcJgbDsWgy-SlyOYaw2AKD6NKwmM,72233 +pygame/docs/generated/_images/AdvancedInputOutput21.gif,sha256=2UMDweIgRefzHFTJcJgbDsWgy-SlyOYaw2AKD6NKwmM,72233 +pygame/docs/generated/_images/AdvancedInputOutput3.gif,sha256=ez4Y7Yy0LsNH6UPYybfGckLmzutcLh7VIisajgFa4O8,6294 +pygame/docs/generated/_images/AdvancedInputOutput31.gif,sha256=ez4Y7Yy0LsNH6UPYybfGckLmzutcLh7VIisajgFa4O8,6294 +pygame/docs/generated/_images/AdvancedInputOutput4.gif,sha256=2-rK9cGngrgpJLW404oI3lG4NQnoT-UNwzA3Up48YIc,29185 +pygame/docs/generated/_images/AdvancedInputOutput41.gif,sha256=2-rK9cGngrgpJLW404oI3lG4NQnoT-UNwzA3Up48YIc,29185 +pygame/docs/generated/_images/AdvancedInputOutput5.gif,sha256=C6N4d_JD4QNMjLwPdINCOFWufyD4vM7b5LpR2GaHMcY,37349 +pygame/docs/generated/_images/AdvancedInputOutput51.gif,sha256=C6N4d_JD4QNMjLwPdINCOFWufyD4vM7b5LpR2GaHMcY,37349 +pygame/docs/generated/_images/AdvancedOutputAlpha1.gif,sha256=MH3CmK658WpN_u56GL9CV4YMFky9GLaKh0CAcixkMrA,14915 +pygame/docs/generated/_images/AdvancedOutputAlpha11.gif,sha256=MH3CmK658WpN_u56GL9CV4YMFky9GLaKh0CAcixkMrA,14915 +pygame/docs/generated/_images/AdvancedOutputAlpha2.gif,sha256=9E2YAIULWHZsAELfqLf5gdqL7-uE-Ebg4uqKgbSyVYk,71819 +pygame/docs/generated/_images/AdvancedOutputAlpha21.gif,sha256=9E2YAIULWHZsAELfqLf5gdqL7-uE-Ebg4uqKgbSyVYk,71819 +pygame/docs/generated/_images/AdvancedOutputAlpha3.gif,sha256=7gHdbhSKaUpyXgOC7GSusTdN7oJQvjGFY_o8DQNZ2Fc,30380 +pygame/docs/generated/_images/AdvancedOutputAlpha31.gif,sha256=7gHdbhSKaUpyXgOC7GSusTdN7oJQvjGFY_o8DQNZ2Fc,30380 +pygame/docs/generated/_images/AdvancedOutputProcess1.gif,sha256=QihTI3ThxtEnPjQ0qZaYF4rKj5GxSwDfFgfUmFjvcOw,15951 +pygame/docs/generated/_images/AdvancedOutputProcess11.gif,sha256=QihTI3ThxtEnPjQ0qZaYF4rKj5GxSwDfFgfUmFjvcOw,15951 +pygame/docs/generated/_images/AdvancedOutputProcess2.gif,sha256=qP09Je0xWwST1CoAs9BCy7_vq6OB69opna8kejTln50,1868 +pygame/docs/generated/_images/AdvancedOutputProcess21.gif,sha256=qP09Je0xWwST1CoAs9BCy7_vq6OB69opna8kejTln50,1868 +pygame/docs/generated/_images/AdvancedOutputProcess3.gif,sha256=4WhoBbEheizUdZfs_pFEBGGAm9aoetw3tJhgcNgVIyY,1912 +pygame/docs/generated/_images/AdvancedOutputProcess31.gif,sha256=4WhoBbEheizUdZfs_pFEBGGAm9aoetw3tJhgcNgVIyY,1912 +pygame/docs/generated/_images/AdvancedOutputProcess4.gif,sha256=m-gUJNOn6AuXT7FpKF6HRR8A6ytWorY9Y07N2uZaSIQ,14500 +pygame/docs/generated/_images/AdvancedOutputProcess41.gif,sha256=m-gUJNOn6AuXT7FpKF6HRR8A6ytWorY9Y07N2uZaSIQ,14500 +pygame/docs/generated/_images/AdvancedOutputProcess5.gif,sha256=GCi9KGUIhQTFg-HLEnp2GEgrnQl8_2KftS3N_UkuEH8,16896 +pygame/docs/generated/_images/AdvancedOutputProcess51.gif,sha256=GCi9KGUIhQTFg-HLEnp2GEgrnQl8_2KftS3N_UkuEH8,16896 +pygame/docs/generated/_images/AdvancedOutputProcess6.gif,sha256=nzV_M0JoA4aDyGwpe8lWoUFd5c1PK-WxUI-lF8WzfQQ,34058 +pygame/docs/generated/_images/AdvancedOutputProcess61.gif,sha256=nzV_M0JoA4aDyGwpe8lWoUFd5c1PK-WxUI-lF8WzfQQ,34058 +pygame/docs/generated/_images/Bagic-INPUT-resultscreen.png,sha256=RDZbxtVyFMJXdZA8wouSyJJjXf2MQ2WYTBzVotsDH88,5973 +pygame/docs/generated/_images/Bagic-INPUT-resultscreen1.png,sha256=RDZbxtVyFMJXdZA8wouSyJJjXf2MQ2WYTBzVotsDH88,5973 +pygame/docs/generated/_images/Bagic-INPUT-sourcecode.png,sha256=3F2c3AnravGgsRMxaNXlaekyqrhMl2cwYySJxhj7L0A,77061 +pygame/docs/generated/_images/Bagic-INPUT-sourcecode1.png,sha256=3F2c3AnravGgsRMxaNXlaekyqrhMl2cwYySJxhj7L0A,77061 +pygame/docs/generated/_images/Bagic-PROCESS-resultscreen.png,sha256=hQ1m6S1xhXpcaf0g50VRoOagmsiZZpUeOBx7QgG7Lqs,5348 +pygame/docs/generated/_images/Bagic-PROCESS-resultscreen1.png,sha256=hQ1m6S1xhXpcaf0g50VRoOagmsiZZpUeOBx7QgG7Lqs,5348 +pygame/docs/generated/_images/Bagic-PROCESS-sourcecode.png,sha256=vj0D6wrXFNjIHKmRFZrltZH4nH51zG6YSy94ID2fWos,66070 +pygame/docs/generated/_images/Bagic-PROCESS-sourcecode1.png,sha256=vj0D6wrXFNjIHKmRFZrltZH4nH51zG6YSy94ID2fWos,66070 +pygame/docs/generated/_images/Bagic-ouput-result-screen.png,sha256=Ig1vKczM-l0ebtdjYEHdwcJuqt8IoyUiK-RANEC065k,4819 +pygame/docs/generated/_images/Bagic-ouput-result-screen1.png,sha256=Ig1vKczM-l0ebtdjYEHdwcJuqt8IoyUiK-RANEC065k,4819 +pygame/docs/generated/_images/Basic-ouput-sourcecode.png,sha256=B6OVjvOtA2ZwiEwJyYkK1-tsGyN13y8O9kls1OYvzTo,57466 +pygame/docs/generated/_images/Basic-ouput-sourcecode1.png,sha256=B6OVjvOtA2ZwiEwJyYkK1-tsGyN13y8O9kls1OYvzTo,57466 +pygame/docs/generated/_images/angle_to.png,sha256=vP3M5zZVFf-ooagw-hRTlnhBLbGQvX9gxO7nX_gBgbc,25349 +pygame/docs/generated/_images/camera_average.jpg,sha256=dkXZ7NdHmM69rbcYCpu0vKtqmHF3qh9nPUgZX3wlWDc,20881 +pygame/docs/generated/_images/camera_background.jpg,sha256=exoGN5fT9IKQyMJK_3VrEjfKTvr5yMeoSLCQplD0hes,7493 +pygame/docs/generated/_images/camera_green.jpg,sha256=NpIuT5qRzN5I7TFLva8m_kCAo1cwOuR5R5-Du9kaEo0,10219 +pygame/docs/generated/_images/camera_hsv.jpg,sha256=tfL0KJyxSk5A_KjVZR7MdV-qegBhej5HlXXw2CnoZR8,36673 +pygame/docs/generated/_images/camera_mask.jpg,sha256=0u0yMCldZMvSW1vyO2KK32D-fVYuYpXlNzmWwbdZ__s,18779 +pygame/docs/generated/_images/camera_rgb.jpg,sha256=GN_1jI8mnDJm1bRbjNBmpJETDSAKcVAS9BxmycYMMv0,32488 +pygame/docs/generated/_images/camera_thresh.jpg,sha256=WBYm8M-TxnuKCYEBvmu68iO_r9EYucnENZ5Ew8i6tqk,4346 +pygame/docs/generated/_images/camera_thresholded.jpg,sha256=OMh-3zXV2a-aahnMQlE7ihvxwXy1UADb15c3LzIWgh0,23678 +pygame/docs/generated/_images/camera_yuv.jpg,sha256=Gp0omp1py-_j6Qpv95VIo6EmDO2u5VY83fPWA2Rd6Bk,20105 +pygame/docs/generated/_images/chimpshot.gif,sha256=Yc_ufSFTkZ5NA1IogV2juH5Cr4_ykoI7QcXQvfGYBfc,46010 +pygame/docs/generated/_images/draw_module_example.png,sha256=jAhc1HG8RXjjPnjn9AwncGwfi4dKbaO7rQgqcd5OCeQ,6476 +pygame/docs/generated/_images/intro_ball.gif,sha256=vEs0-OG_j55JZJML48IXhHsN4ZMIWrqlS7dfxd9-hxc,5015 +pygame/docs/generated/_images/intro_blade.jpg,sha256=Aj59Tt9z1mdJeDK89HbWaQ7DVTDKbzoUDT-vNcJnYQo,2631 +pygame/docs/generated/_images/intro_freedom.jpg,sha256=RL-jChKVMdqoS7BN5NGV7hjlxSgU4qaKrj4ZXH2zsI8,7050 +pygame/docs/generated/_images/introduction-Battleship.png,sha256=6iHEhqo_HnXRfmQv9yriYMc3dX8Q2TwGbSGDQREFkN4,165586 +pygame/docs/generated/_images/introduction-Battleship1.png,sha256=6iHEhqo_HnXRfmQv9yriYMc3dX8Q2TwGbSGDQREFkN4,165586 +pygame/docs/generated/_images/introduction-PuyoPuyo.png,sha256=OEMjFSzQc8vJLQryUdkp7lJ3DhIw_yEo5-5nm2vBfrs,31388 +pygame/docs/generated/_images/introduction-PuyoPuyo1.png,sha256=OEMjFSzQc8vJLQryUdkp7lJ3DhIw_yEo5-5nm2vBfrs,31388 +pygame/docs/generated/_images/introduction-TPS.png,sha256=M4ioZMyjR2n7pQIp8UhGRV4m2V_rcXJCuo5lU3V7yGw,136031 +pygame/docs/generated/_images/introduction-TPS1.png,sha256=M4ioZMyjR2n7pQIp8UhGRV4m2V_rcXJCuo5lU3V7yGw,136031 +pygame/docs/generated/_images/joystick_calls.png,sha256=oNAQgfZ8GM5_Z17hoNVrLf1tYeRnths60ZrlhJduWhs,30004 +pygame/docs/generated/_images/pygame_lofi.png,sha256=QBECBUalJHRExXhf7nt-QpsVcu9LQ3RSAYVVS04AX3M,134242 +pygame/docs/generated/_images/pygame_logo.png,sha256=Jc1Lz47pY3mHjEFNzQXV9gx-olUUYCGuFYrutncaTXE,132068 +pygame/docs/generated/_images/pygame_powered.png,sha256=LgbswFcg647alSC5SawDPH96xuTMcWmMqVA6Zvs2K3Q,179911 +pygame/docs/generated/_images/pygame_powered_lowres.png,sha256=9go5WMiAE4fTEmB9w-zatEfROGl1IUWJGBpKoLqy-wU,179911 +pygame/docs/generated/_images/pygame_tiny.png,sha256=BXPk3OkSWdSkqjMkCQ1Dt5WjxZfb4zj4c2ir9U9_Y7Q,15310 +pygame/docs/generated/_images/surfarray_allblack.png,sha256=XEUO2hKFfTfZMyaqbPvM0u3zmETfl___AANBkHn6y-w,125 +pygame/docs/generated/_images/surfarray_flipped.png,sha256=UZ1FpljGrdAnB1UCUTuAfeJJxyqI-_0duSCKqy9UN3w,50835 +pygame/docs/generated/_images/surfarray_redimg.png,sha256=6tlO_tZokQTsfgvD3yNW21nHA5uA9hDWoaL7POrr_qE,23443 +pygame/docs/generated/_images/surfarray_rgbarray.png,sha256=8US5r3GcG_jZBncK-47HG2JLB2ZwQjaSBWhb3ynNs9w,50897 +pygame/docs/generated/_images/surfarray_scaledown.png,sha256=Z68XSoPUvV5bYIhJdkmhZYr42JpQKnC8g9hmN18iaWI,15109 +pygame/docs/generated/_images/surfarray_scaleup.png,sha256=sdxQlmVoRhlF_ocD2ecre0q51Q2LLoCxNsJaI8O1NYI,67759 +pygame/docs/generated/_images/surfarray_soften.png,sha256=XNzAZzfLUqn-QIQ25TxB2ujoDZkPEIRI6QSDnjJMIk0,47540 +pygame/docs/generated/_images/surfarray_striped.png,sha256=iH7gLZhBu5aATV-vfrsQmW30KdsMQoYB_LD-efRzl_Y,392 +pygame/docs/generated/_images/surfarray_xfade.png,sha256=uD8g8Ueqc3IMZaOqiD4n4sZmg5I4j798oIzl1pWDM70,41834 +pygame/docs/generated/_images/tom_basic.png,sha256=RzKBFmep1ksfD5QrJVW7JzdHvDN1_4ayUfGyVN-8Wms,5139 +pygame/docs/generated/_images/tom_event-flowchart.png,sha256=sG8YOH8YX2yTtx4-agBUWIcT8-jj1m6uKCF98QxmbKQ,5528 +pygame/docs/generated/_images/tom_formulae.png,sha256=6k8VDsueGVOh01ZrAVLE5miliOWKhQJqIrlPW_JEmXk,6763 +pygame/docs/generated/_images/tom_radians.png,sha256=BkBTx4OoiSXO5d6sMG01MyYBtO7EmDvgJixbTRKMm9Q,17409 +pygame/docs/generated/_sources/c_api.rst.txt,sha256=vSJF6tvDzMDdGTnXyyAi-Zz-iApxeuO-n5jTvceAQNw,473 +pygame/docs/generated/_sources/filepaths.rst.txt,sha256=sou-1N5amW1JXHqwKUU0BqYQmm2c5qDjjFKVIKIppfo,899 +pygame/docs/generated/_sources/index.rst.txt,sha256=UMDM3eMRAtTSwIiQfMXkaCKNWW6dlsnqR6MpxfGAMyI,5965 +pygame/docs/generated/_sources/logos.rst.txt,sha256=oJNZMoKbfJ9EuXTeipAahQGSpPkTaAcyHEIGWJPdWW8,1337 +pygame/docs/generated/_sources/ref/bufferproxy.rst.txt,sha256=V5gSzq__85alqL5Is05MTmpDv5NUHbLuuW4PYH98MzI,4708 +pygame/docs/generated/_sources/ref/camera.rst.txt,sha256=VGSdYbW2ii37WG3kC8HGn2Ao-TPRPYXTLez9VnwI364,9628 +pygame/docs/generated/_sources/ref/cdrom.rst.txt,sha256=0FlYODuxoQOsTYpZ9EiDYmUD9PIQrAeAlsXE8I67RQk,9068 +pygame/docs/generated/_sources/ref/color.rst.txt,sha256=DbdHOccIvmf8D8ERvo-fHKKvqKmZYb3tcNbQkSh_dj8,10798 +pygame/docs/generated/_sources/ref/color_list.rst.txt,sha256=XLIKLmTx_liiWk0gdKlrElBlCNZD0_sIvAg_El6tgfE,96353 +pygame/docs/generated/_sources/ref/cursors.rst.txt,sha256=dRrEevF8Uaho0It82zk-f1ySA9Pa_y3kUhM4mEm-zTQ,9415 +pygame/docs/generated/_sources/ref/display.rst.txt,sha256=Vnh3egkzBw1odhLMoVwFrR_WZLodTbgR2X01K4BhAkk,29190 +pygame/docs/generated/_sources/ref/draw.rst.txt,sha256=O-i4wVe9KULlpBvhdKrKbAYXiszO2_FUDhjzoEArmI0,24657 +pygame/docs/generated/_sources/ref/event.rst.txt,sha256=_0wgRCiHprZux0vJSF4wuyq0nAo1iOuK5T5eF1jzwTM,22211 +pygame/docs/generated/_sources/ref/examples.rst.txt,sha256=044bT9v4pQYRZqmaAwrdAI_GV3HLiwRlWHKYturVbN4,14095 +pygame/docs/generated/_sources/ref/fastevent.rst.txt,sha256=6gyem0cRZ-syKMDcQ6qexC83dJMro1O9-XHY_k2_HNg,3545 +pygame/docs/generated/_sources/ref/font.rst.txt,sha256=AGiup5hzk_KnfOElwadQGmzkzG9XcRex2yq9EvsNB-o,18217 +pygame/docs/generated/_sources/ref/freetype.rst.txt,sha256=R5RnUREdR9b5NPNsPYYTthl_ExWUcmX7vclWFjeNpyM,31386 +pygame/docs/generated/_sources/ref/gfxdraw.rst.txt,sha256=bQYID2bOb6xETZoDKM0mRtaCU_QD48MjQmCV8lvEErE,21842 +pygame/docs/generated/_sources/ref/image.rst.txt,sha256=4NJf_cJ9VVHm7VD1MD4msBUvjytoaCTkB83W6vubfkE,13395 +pygame/docs/generated/_sources/ref/joystick.rst.txt,sha256=sXgDyfTcwBdasNxdzW9r0Xnesxo6iwz23OBYG6SB0ws,19389 +pygame/docs/generated/_sources/ref/key.rst.txt,sha256=vSngmd3fawSny-yD8ord6lUR4sryVa9B6XcIiq8LyVA,16327 +pygame/docs/generated/_sources/ref/locals.rst.txt,sha256=VZi8cE2ZlPei4WdzSsok0fU_BOgPhlOmjfTCTSvl0N8,1022 +pygame/docs/generated/_sources/ref/mask.rst.txt,sha256=RAVpZZNK2i-ltHlCbNZgFa80x2RAXc_4lYkPgAVoyjs,24220 +pygame/docs/generated/_sources/ref/math.rst.txt,sha256=YbCqnfzaJeI1k8AYI45w8rokZ_x9f2NdnkoK2njrv3k,38384 +pygame/docs/generated/_sources/ref/midi.rst.txt,sha256=jIJ5qOZNvlMtBH0TA2icWTSB4_7CcUmU6_y9uVhGiak,14358 +pygame/docs/generated/_sources/ref/mixer.rst.txt,sha256=OHZqbN6F8ARkFGLe0cXHpAnOVf_MeqdC1cp5OeWRlbQ,22459 +pygame/docs/generated/_sources/ref/mouse.rst.txt,sha256=pzAlscsACbo_6p1pbazcbUaJW2vEf1Zx--jwy3ktA3k,8129 +pygame/docs/generated/_sources/ref/music.rst.txt,sha256=lLimQ-C1JXLMGw-gIHIsZ7r0bbav7SPTovbCqiXuO5Q,9531 +pygame/docs/generated/_sources/ref/overlay.rst.txt,sha256=loD8HVw0KQnsaPPisw_Xe8yms59CEAbdsboXwhS7zqk,2659 +pygame/docs/generated/_sources/ref/pixelarray.rst.txt,sha256=p0HY0TLZFoZAayVW9FxW8t0G4-Cjblhfd07uTkXqJK0,10204 +pygame/docs/generated/_sources/ref/pixelcopy.rst.txt,sha256=SMb-VGMZSxBBWgAjsFg2ucaQp80KglnIo56AkS7O-vE,4531 +pygame/docs/generated/_sources/ref/pygame.rst.txt,sha256=mEu8Av5H35_dC0JNXu9q1W6xnwgwvRr8ywaxNUEOcu0,15129 +pygame/docs/generated/_sources/ref/rect.rst.txt,sha256=Qf24A1_9KRJgS1-L5xlHoRWKQbDI0bE4rViR8Zc1aBM,21147 +pygame/docs/generated/_sources/ref/scrap.rst.txt,sha256=eeN2xL1riH4ZKbpNJSAXVlG1Ew2JYn1B-JiSBmdtZ_Q,7988 +pygame/docs/generated/_sources/ref/sdl2_controller.rst.txt,sha256=8wI9cr_XTTZ0Kym7dtICQhivaI40sh_0_vy_C2EK4X4,9401 +pygame/docs/generated/_sources/ref/sdl2_video.rst.txt,sha256=1WSISwTtmvOX6n_wEzAFk5Yj7XgmeG5GdrPIZB8K6RQ,9117 +pygame/docs/generated/_sources/ref/sndarray.rst.txt,sha256=9N1u6w7AxTr943nJiSUoDyNi-8qE_tWJeVU0rVEQYi0,3260 +pygame/docs/generated/_sources/ref/sprite.rst.txt,sha256=2RfcFwVV0xc3duWp_n_sVJSPFa9U_SOZKRb0AkTIGOM,30575 +pygame/docs/generated/_sources/ref/surface.rst.txt,sha256=eNBOZq2pkRHQcVtErIZs8jiG-nhAXSq3CR-Bup7_OQ8,36859 +pygame/docs/generated/_sources/ref/surfarray.rst.txt,sha256=rBeiZncxG9HIetGkZS0IcwxIlHqQgCxpiZxO6bSvY8s,12251 +pygame/docs/generated/_sources/ref/tests.rst.txt,sha256=vzkEPNYlI2274wXW3FKI1c9Qqmo293E7_eZhIIV9Xs0,4636 +pygame/docs/generated/_sources/ref/time.rst.txt,sha256=WJTVcpIu2GT39OD3k5MYQ3gXTqQJwJJaiNjA7GvWMlk,5624 +pygame/docs/generated/_sources/ref/touch.rst.txt,sha256=v7V0P85KlxiQIBLjaVyhTtH1fmDmqif-kDLhV0O6x4s,1957 +pygame/docs/generated/_sources/ref/transform.rst.txt,sha256=iZRvH-iMBYCszGMcoMQhut5OQDrGo07ElfF7jtX_mdM,13055 +pygame/docs/generated/_static/basic.css,sha256=sAj59T-GAN18hejRlkVoHGWW1U4oam_yVWMgFt5P4xc,15597 +pygame/docs/generated/_static/doctools.js,sha256=tcrUIItYleYYKj1roqKMOLpMPtfd_0Y1g5qkMO7llhQ,10766 +pygame/docs/generated/_static/documentation_options.js,sha256=tyP9OL3RCgebL7m7OJFenaW4w7PdYhH0PKEkEEvzE08,435 +pygame/docs/generated/_static/file.png,sha256=XEvJoWrr84xLlQ9ZuOUByjZJUyjLnrYiIYvOkGSjXj4,286 +pygame/docs/generated/_static/jquery-3.5.1.js,sha256=QWo7LDvxbWT2tbbQ97B53yJnYU3WhH_C8ycbRAkjPDc,287630 +pygame/docs/generated/_static/jquery.js,sha256=9_aliU8dGd2tb6OSsuzixeV4y_faTqgFtohetphbbj0,89476 +pygame/docs/generated/_static/language_data.js,sha256=JUzCtS3qbjtQkX7mhfWeiEGT3a8lHfhiLzC_G3YxgnU,11151 +pygame/docs/generated/_static/legacy_logos.zip,sha256=69C68jO62qau7eQx3z6U9pLnDhAMXK72RZ4PatlSMdY,51315 +pygame/docs/generated/_static/minus.png,sha256=R-f8UNs2mfHKQc6aL_ogLADF0dUYDFX2K6hZsb1swAg,90 +pygame/docs/generated/_static/plus.png,sha256=VBFRmblqEwy6AhR8R8DetD3Mm58ItRYruoZCs0mArGM,90 +pygame/docs/generated/_static/pygame.css,sha256=RTUqrXsr09Rdj6xtpM2Y_RZK90S1zXOpOAo2TV-0Gho,12699 +pygame/docs/generated/_static/pygame.ico,sha256=YeIWletq938Rg_G11m_1iGL_zw9T_U1QXZhjnbHjJSU,1078 +pygame/docs/generated/_static/pygame_lofi.png,sha256=QBECBUalJHRExXhf7nt-QpsVcu9LQ3RSAYVVS04AX3M,134242 +pygame/docs/generated/_static/pygame_lofi.svg,sha256=5eTaA6Alehzg0SuUSEFa7uONJe-0WHthEXNKPj75j5o,59281 +pygame/docs/generated/_static/pygame_logo.png,sha256=Jc1Lz47pY3mHjEFNzQXV9gx-olUUYCGuFYrutncaTXE,132068 +pygame/docs/generated/_static/pygame_logo.svg,sha256=3Oxb-IF0TZVcfuYPz-bHbd-OYLNLkTVuxiY1vWAmHUk,59262 +pygame/docs/generated/_static/pygame_powered.png,sha256=LgbswFcg647alSC5SawDPH96xuTMcWmMqVA6Zvs2K3Q,179911 +pygame/docs/generated/_static/pygame_powered.svg,sha256=1_0Cxa_PMRGgGvd2KktrKYSajaxGH_3FKaNWIpxeUbU,102819 +pygame/docs/generated/_static/pygame_powered_lowres.png,sha256=9go5WMiAE4fTEmB9w-zatEfROGl1IUWJGBpKoLqy-wU,179911 +pygame/docs/generated/_static/pygame_tiny.png,sha256=BXPk3OkSWdSkqjMkCQ1Dt5WjxZfb4zj4c2ir9U9_Y7Q,15310 +pygame/docs/generated/_static/pygments.css,sha256=85BWybvZ71cAlI5uqwNERzofjlACIqPlYvQDmkKVM2o,4919 +pygame/docs/generated/_static/reset.css,sha256=wqvSs8L_cB2K6bR903cOJkEtfJi0LpNQFl3nC_kZQr4,1083 +pygame/docs/generated/_static/searchtools.js,sha256=1rXuIe3XtGwCnFERMmcZ3OxcX1I2hwSpOy1khcsiQUw,16634 +pygame/docs/generated/_static/tooltip.css,sha256=UkuHG9X2M7DaTSMJX3-mW-4LV8wBtFxfG5k_pr7xrc4,798 +pygame/docs/generated/_static/underscore-1.13.1.js,sha256=zBD3mc0Pa2X5XEASRFSX5bo8ufUZZKlGiUCye96YtIc,68420 +pygame/docs/generated/_static/underscore.js,sha256=IY-xwfxy6a9rhm9DC-Kmf6N2OStNsvTb8ydyZxtq5Vw,19530 +pygame/docs/generated/c_api.html,sha256=8coHCHwmQDNR5PpRtRo7pfY9t8qwV4gqMRY_KndG0PU,7507 +pygame/docs/generated/c_api/base.html,sha256=PRtqzsz9MVqBTZ_XNySMb7zC4FCvpcHyLkScorf-_EQ,32451 +pygame/docs/generated/c_api/bufferproxy.html,sha256=ssNbOizMQNZpiKNacpE5FrlF9awXEx1axdGvpM2cJ4Q,12006 +pygame/docs/generated/c_api/color.html,sha256=hgXgBXCtMM_5VhlGIoLFy3KuDaykcQU2cnA7V6BCx54,10457 +pygame/docs/generated/c_api/display.html,sha256=oVZ3rja2N0NuNT2qevnUj0hmnn9Tl_7TjkXY82M8Kps,10934 +pygame/docs/generated/c_api/event.html,sha256=Yen7R7QmeR9u3LYtdr5tWdNKaSpH6vw9EoYyhrNAwCk,11914 +pygame/docs/generated/c_api/freetype.html,sha256=f1gqf95VYDFoCJPD_ZlAjSfM9-hER7dOISE5faHgSE8,11380 +pygame/docs/generated/c_api/mixer.html,sha256=RZLtL_Yhxl4iU4jTE8ohMzYl1ImBebnUs3bj8s5-EQM,15384 +pygame/docs/generated/c_api/rect.html,sha256=KF3mGaSyR5kDcm_IJTXR2gWURwI7HIUZ-cWQUDLrTnc,13874 +pygame/docs/generated/c_api/rwobject.html,sha256=2LBEh9ad6FodbAnaAv2praqFgOBxIkqtnznLe4L3ba4,15297 +pygame/docs/generated/c_api/slots.html,sha256=8n9EJ-rT8OupZAP0vR7JZPyUta8t2UgaNLwQH7Q2Go4,6823 +pygame/docs/generated/c_api/surface.html,sha256=O_emn82ZiVmrTmejZYEbMBK9elBNzb1MQQhNWzYOq24,14914 +pygame/docs/generated/c_api/surflock.html,sha256=1ktd_26-TI5V26yMBdgZM0wCr4zHARakTZB-BGPQLQg,16816 +pygame/docs/generated/c_api/version.html,sha256=N22Kce03eSq5vNKxkCyTdHOmwGiXTZ4B_uycQ6ZQJHg,8229 +pygame/docs/generated/filepaths.html,sha256=aUVG_q9n_rI7MOnPbg8QDjyo1-Hj8fX2iVl2bfWCb9M,6490 +pygame/docs/generated/genindex.html,sha256=9rB5dLKkvQtyyf-mHDRqQEHAsbT5gpPqPk_U_gkZ2qU,124089 +pygame/docs/generated/index.html,sha256=qyk-Y3FR6RPf2stn2w0iRU--1SfT7Ipl8hot15RQ4h0,24861 +pygame/docs/generated/logos.html,sha256=1DLRqlTAGMJwbaaej6ZAHnWj2kP7BqqZVqqn6KnqdZ0,7536 +pygame/docs/generated/py-modindex.html,sha256=EOxpsN6KqknHQAR4XStMh3i_vmYZIKdFc4jqEu5jy3k,11506 +pygame/docs/generated/ref/bufferproxy.html,sha256=vEyZAJpzy9dcc67VihI7K696mN-k2EW9VTaUkSJBEDA,17771 +pygame/docs/generated/ref/camera.html,sha256=PubUdbpaJUBTRnaPFjGykO78toSOwqHM5RLaBBT0FaY,27392 +pygame/docs/generated/ref/cdrom.html,sha256=A6PmC3hnjvNoHL7fx712bOg0hRCem4diCeVPExxNGxc,33410 +pygame/docs/generated/ref/color.html,sha256=WVMxogTDP7qqMa8TgTd1HuaRhLgfbbrD-Pxjd9SNrbk,36793 +pygame/docs/generated/ref/color_list.html,sha256=t95LHGAbAsRzisOkkEVzP7TCezxbDrZmqwMkpISYfD4,177095 +pygame/docs/generated/ref/cursors.html,sha256=9HZX58pnQEiX7ajKv0VTvrHdPQ9T0CQrUXF0b9m4l5Q,35588 +pygame/docs/generated/ref/display.html,sha256=I4pwizeY5oj_TW7Cw21NVFNS4aAsVrXOFX1yAiG84f4,77425 +pygame/docs/generated/ref/draw.html,sha256=Os3iS6SR77jn0FBWkeKXCavta7Wz5AAoeF94OH9JCYI,83998 +pygame/docs/generated/ref/event.html,sha256=ZCsHUpEozdLVkouqtutb4OIoGm7uYslagzhD-lGoluE,70340 +pygame/docs/generated/ref/examples.html,sha256=EgdeAVuocgVXmWRFKJL5BuImibKy-k4GXHJmA3SjE6k,48573 +pygame/docs/generated/ref/fastevent.html,sha256=tljyp2huAe1_8fPlsdintv2w71JDoYzf7GXxhd_ycY8,15114 +pygame/docs/generated/ref/font.html,sha256=ElhG9Kt-1ezdktewzUODYO-1NAgfhYe_kWyOnZs9axk,50015 +pygame/docs/generated/ref/freetype.html,sha256=llVWXTZXAiiCzO1UE7KdtPwDImneJMZzWy0NeEQE6NI,100747 +pygame/docs/generated/ref/gfxdraw.html,sha256=ju4C2Te9k3Hhnj2hfVGL6cWI9mKlZB-QtH-kkuAgl5Q,79822 +pygame/docs/generated/ref/image.html,sha256=wHp0ZI2-p1kC2nk_9ErhsJwUsGDltG-c3JKHOjVqYu8,40443 +pygame/docs/generated/ref/joystick.html,sha256=VzZCJPtbPt_pENszwxpbBed3LMLh22A8WzcaWt1_Zfk,96521 +pygame/docs/generated/ref/key.html,sha256=HSNX1SstfvGO9asWK3E4C0XALQlo0J2QbhintQD2uLs,42717 +pygame/docs/generated/ref/locals.html,sha256=9_NSEXvxLoYpwXzeGijFKqjZ24f7wU2QT6AHQx078Y8,8633 +pygame/docs/generated/ref/mask.html,sha256=QFl67JWyH-cgPJBTBtJYrXmOckESkYsq8D-4UhxWyhI,80499 +pygame/docs/generated/ref/math.html,sha256=wp6QzdEE7mHIesBv0Hq7LrE2TJWzQFgk-McEcHJLogE,123868 +pygame/docs/generated/ref/midi.html,sha256=4BrazKm37S0wtlJnuU1u0fjAiQxndraTlAcuu67pV8Y,52611 +pygame/docs/generated/ref/mixer.html,sha256=sS03x53pyYvixwvl89WRLbvbO5bY4Gg3xo0bxMVR7qc,64149 +pygame/docs/generated/ref/mouse.html,sha256=dr3viGiTdUHJIiumIEkCW2rKh8mTbKDMWNmQJt6htJ8,27506 +pygame/docs/generated/ref/music.html,sha256=U27g19ceOeWguNBg7X76e5Ekasc900BGIy9yv1looNg,33651 +pygame/docs/generated/ref/overlay.html,sha256=24evT1lPj5hSqqTzbPKPZzg_-Y5aNn0DZB_R77SuhHo,11027 +pygame/docs/generated/ref/pixelarray.html,sha256=2P6dxyjpqhAwEXEdTHgEm740GYubAuSmP_YbIe9q0zE,31490 +pygame/docs/generated/ref/pixelcopy.html,sha256=91M900OS2JtwmRuDxZ5Ex9_HPCvHubx--gjIgLYtGfE,15134 +pygame/docs/generated/ref/pygame.html,sha256=kTizk86vaGBanQyJ4KEKUSzYFyShLq5NQ7l6N5MXu3Y,50121 +pygame/docs/generated/ref/rect.html,sha256=EZMUwILGNUqEu1k7uachHbcucPxIffwfSmfNFbzLs2s,70063 +pygame/docs/generated/ref/scrap.html,sha256=V55CZ4qaW2wSAsVXFd90rEtUiy-IPUiFcXdK9sJEvGE,30582 +pygame/docs/generated/ref/sdl2_controller.html,sha256=jEDdXLANSmojvYdyzznfFd-DxlfAGlQBK5OTnwRLrAc,36874 +pygame/docs/generated/ref/sdl2_video.html,sha256=_eMPe52-cQCYHEW_dCm3adBGPImXM4kxpjiWbZYh_KA,61994 +pygame/docs/generated/ref/sndarray.html,sha256=J31OVRP0E2etH6sWntnbSRe22UcEbyySp6hww_MTA8k,14735 +pygame/docs/generated/ref/sprite.html,sha256=RXiS-P6IWmhfcmHAOYKPjGXrogoa6MFNrTBKOrvDNV8,96216 +pygame/docs/generated/ref/surface.html,sha256=8PqVHlynD77jzkGGmNo1g3WlI6jIe_6OIwOgKtxpTe0,89988 +pygame/docs/generated/ref/surfarray.html,sha256=4C1jPEnOvBuKK9K6SpySUwQp2Qtf45VRp1c5IQwztNI,38336 +pygame/docs/generated/ref/tests.html,sha256=300EcKiUdeXOHHw1lb1V3drHG6WjklYnyYFB3Sd529s,18592 +pygame/docs/generated/ref/time.html,sha256=TXwW69sXxky47zykj-ROPZc7OMItzOcbp5xfY4ROd-4,20060 +pygame/docs/generated/ref/touch.html,sha256=yFBWrQIxlAMcb6yP1KdBO3vadlWnlHf-2fWrqWEflH8,13013 +pygame/docs/generated/ref/transform.html,sha256=zuFY0gXBSDwlhPccx6zkMhPZbg8T30z3Sy_sqgB4iQI,42958 +pygame/docs/generated/search.html,sha256=hTjD-HvY02xD_aoaGfBwlpbtdCEXsqCCbm5pr03OIwU,3238 +pygame/docs/generated/searchindex.js,sha256=mGsUZXgrXxTsro7ZbK0iydSphgtKR72x4xRUUzemWLU,204820 +pygame/docs/generated/tut/CameraIntro.html,sha256=8U8pj6fCQ4wwZNTXP-VMDdtkC7BtIIPWNfmSj0E5qDI,38465 +pygame/docs/generated/tut/ChimpLineByLine.html,sha256=8adynePcZakKoLjqYCXxZ8mpELflOylzlOv9-w6x_2w,58949 +pygame/docs/generated/tut/DisplayModes.html,sha256=HPolOtYD6aTpYURzi0XgWi5b4fY9FULit4aD18zECbo,23420 +pygame/docs/generated/tut/ImportInit.html,sha256=uznuFJ_RG05GeB2Nzi74EvRICWSkuleooLTactmUb2E,9766 +pygame/docs/generated/tut/MakeGames.html,sha256=i_DqhG02gVAX1EDRaM9gYxO9difPT_4Uqw7jAd5Kwxg,14944 +pygame/docs/generated/tut/MoveIt.html,sha256=5Ac2zrAVLyIHZXTH8DauSWgqDunTuUVuNkXe_gaIq6Q,67457 +pygame/docs/generated/tut/PygameIntro.html,sha256=kYXXDGTrXOtpBeEPXHaQ1PosAaoRzDzitQMGnireJiI,29378 +pygame/docs/generated/tut/SpriteIntro.html,sha256=3FO6mIPTHeFWnbamGh53jdlpt5o4bNHuPWPc1UGL5v4,44630 +pygame/docs/generated/tut/SurfarrayIntro.html,sha256=eyCbnScwymNfEH1k0xRs01OKzq7cyipbfrKjL8B-OZY,51111 +pygame/docs/generated/tut/chimp.py.html,sha256=tU9PD4BwAUg2B8IhBccSRulf8OkGr7Q1rPmR1ybatSI,36492 +pygame/docs/generated/tut/newbieguide.html,sha256=JOfMz2TetldOl7G1yjYAgJ8sIEJOgZVYwR4WwiZG2dw,46008 +pygame/docs/generated/tut/tom_games2.html,sha256=91ZmYNb_dqx-M4vN-7k7X2upupPwqrX9Y999B8zcWWM,17798 +pygame/docs/generated/tut/tom_games3.html,sha256=V6IO3zHcR8j6Msqqm_btnIM4LipHkR7kJr9xTJbcMFc,15410 +pygame/docs/generated/tut/tom_games4.html,sha256=Yq18mTMBwLLObcydZnHOB02eHFBY9TdpifIhnxKpXAg,19305 +pygame/docs/generated/tut/tom_games5.html,sha256=2SLVRx4HtYnGRfhQkbxnI7Mtsn9crqvYCjFxmC9sBx0,21566 +pygame/docs/generated/tut/tom_games6.html,sha256=YU1Zyoo3iJsIQqMSRH87h-PlcfY7IJECEtgIko_fHsM,53335 +pygame/draw.cp36-win_amd64.pyd,sha256=xBbEC08GvNOWwHmcEX_hst-1tv9Yz4jud9QNNsnx7LM,50176 +pygame/draw.pyi,sha256=VtcYPysR7rBEPcB8XZrTmkvgvBZDoLPKI6mwhFIWubM,1694 +pygame/draw_py.py,sha256=ME-6PCJ7TRsIYoExnwgvEwj3iGTFPgjkNepQag8Nh1g,18662 +pygame/event.cp36-win_amd64.pyd,sha256=mUnxEsJyRM-N8P1wETEy46Pcpvimr3XHHLG3T-QZiEM,44032 +pygame/event.pyi,sha256=4v_0k2oj2RLibGr_hg5585mvi9CHaAB1tN7wN286Db0,1488 +pygame/examples/README.rst,sha256=K1xE9Fz9XWB05ZKpu4b96ycF19_DY5a3Y-Plv-oeqww,4174 +pygame/examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pygame/examples/__pycache__/__init__.cpython-36.pyc,, +pygame/examples/__pycache__/aacircle.cpython-36.pyc,, +pygame/examples/__pycache__/aliens.cpython-36.pyc,, +pygame/examples/__pycache__/arraydemo.cpython-36.pyc,, +pygame/examples/__pycache__/audiocapture.cpython-36.pyc,, +pygame/examples/__pycache__/blend_fill.cpython-36.pyc,, +pygame/examples/__pycache__/blit_blends.cpython-36.pyc,, +pygame/examples/__pycache__/camera.cpython-36.pyc,, +pygame/examples/__pycache__/chimp.cpython-36.pyc,, +pygame/examples/__pycache__/cursors.cpython-36.pyc,, +pygame/examples/__pycache__/dropevent.cpython-36.pyc,, +pygame/examples/__pycache__/eventlist.cpython-36.pyc,, +pygame/examples/__pycache__/font_viewer.cpython-36.pyc,, +pygame/examples/__pycache__/fonty.cpython-36.pyc,, +pygame/examples/__pycache__/freetype_misc.cpython-36.pyc,, +pygame/examples/__pycache__/glcube.cpython-36.pyc,, +pygame/examples/__pycache__/go_over_there.cpython-36.pyc,, +pygame/examples/__pycache__/grid.cpython-36.pyc,, +pygame/examples/__pycache__/headless_no_windows_needed.cpython-36.pyc,, +pygame/examples/__pycache__/joystick.cpython-36.pyc,, +pygame/examples/__pycache__/liquid.cpython-36.pyc,, +pygame/examples/__pycache__/mask.cpython-36.pyc,, +pygame/examples/__pycache__/midi.cpython-36.pyc,, +pygame/examples/__pycache__/moveit.cpython-36.pyc,, +pygame/examples/__pycache__/music_drop_fade.cpython-36.pyc,, +pygame/examples/__pycache__/pixelarray.cpython-36.pyc,, +pygame/examples/__pycache__/playmus.cpython-36.pyc,, +pygame/examples/__pycache__/resizing_new.cpython-36.pyc,, +pygame/examples/__pycache__/scaletest.cpython-36.pyc,, +pygame/examples/__pycache__/scrap_clipboard.cpython-36.pyc,, +pygame/examples/__pycache__/scroll.cpython-36.pyc,, +pygame/examples/__pycache__/setmodescale.cpython-36.pyc,, +pygame/examples/__pycache__/sound.cpython-36.pyc,, +pygame/examples/__pycache__/sound_array_demos.cpython-36.pyc,, +pygame/examples/__pycache__/sprite_texture.cpython-36.pyc,, +pygame/examples/__pycache__/stars.cpython-36.pyc,, +pygame/examples/__pycache__/testsprite.cpython-36.pyc,, +pygame/examples/__pycache__/textinput.cpython-36.pyc,, +pygame/examples/__pycache__/vgrade.cpython-36.pyc,, +pygame/examples/__pycache__/video.cpython-36.pyc,, +pygame/examples/aacircle.py,sha256=cKBEPuz4nVNgiyxf9SFI4c4P8jFmvJMHh_rlKDahm9I,1037 +pygame/examples/aliens.py,sha256=acRm3demyftDlt3uTc8_rI1xVlWqM4PZqt1ekxuYIs4,12310 +pygame/examples/arraydemo.py,sha256=G4ZYl8JqPNnbEJl6Ed7UCBfJvrz9QtHauFoFus87WB0,3255 +pygame/examples/audiocapture.py,sha256=hkq7mMKSmSvfHHD5UiIER4928il2l0mqdahkJViHHr8,1561 +pygame/examples/blend_fill.py,sha256=CCQJraeaBLEea-2_lWaUxVlcjM5rxy5UykRIjWCUAu0,3399 +pygame/examples/blit_blends.py,sha256=han3N9jK8dy57EePLJNJ73MEjfl34ypahoY7oGwZwt0,6318 +pygame/examples/camera.py,sha256=5205gt_W9EoTartrtQtXrdD-AkCLx6r8DkBaAvdiFO4,3905 +pygame/examples/chimp.py,sha256=2uBvczKQW8XEG87yJlf4CB46RR4b1a5iPjMi_C--fAY,5912 +pygame/examples/cursors.py,sha256=CtDYC49tEm2xcRft-Wlxa1zSVY3pqeunx0njhcCyV2c,7938 +pygame/examples/data/BGR.png,sha256=DvOrlW5BJdat94nNV8XEETBLRrSWRV7byQsMPsA69uw,244 +pygame/examples/data/alien1.gif,sha256=8Wveo1zpLVaFCtYITm_SoYqjy8L-TDuaZOcNa8Osqsw,3826 +pygame/examples/data/alien1.jpg,sha256=HOjXjmW4Ofsu_en9WNrkuIp_DCwupXcFB0Yt_cqV9rA,3103 +pygame/examples/data/alien1.png,sha256=femzLssV7oGvT3S2tyviyq7qO32QfhBDtMOR3ENBCLs,3522 +pygame/examples/data/alien2.gif,sha256=0MPpVYzvjAECy0pd7YRFKCEzzIYDKEJt70rbjlLbTZM,3834 +pygame/examples/data/alien2.png,sha256=FKGYDI2FBBR1Z56BLn357PNfh3-M38gAJpSQL8BpKYY,3526 +pygame/examples/data/alien3.gif,sha256=bFCRGZOQPaadCKIc-tlqoUjHdsi5IzR0E-2SjpPEvmA,3829 +pygame/examples/data/alien3.png,sha256=a51Tb9E4IvoICGzQChHq51RKVQJLf1GOCEeqA5yYfnk,3518 +pygame/examples/data/arraydemo.bmp,sha256=xM4-n_hRCQFZlfwwdTK6eaBweycUc863TgSFbWp3dbA,76854 +pygame/examples/data/asprite.bmp,sha256=97XMpKq9lLpMuv8UveCf8UJEAxheBhPUjHfMRQBkUx4,578 +pygame/examples/data/background.gif,sha256=-3kZwt99MFUBbBo-kHvPZXVlFrSB34XVNQWWxfHb970,9133 +pygame/examples/data/black.ppm,sha256=Yu8BwDOeFwOnVYjdWTMo7Tl1xcx2a7J38zZP-JllcMQ,6203 +pygame/examples/data/blue.gif,sha256=hqbgDzCeUz0NHjAQHYURIxSOpRbpHf6QeFch8ux_dAE,84 +pygame/examples/data/blue.mpg,sha256=XDj1CRPt1MWxspCfA3oqb822nlZgQ7CyyEuVJwlgmpg,6144 +pygame/examples/data/bomb.gif,sha256=TZ60QP1S2QBN6QPNSqBwS5VyebZA93iu8ZMUXzEg2QA,1170 +pygame/examples/data/boom.wav,sha256=kfoWs0VVDGHv0JSa46nXZBGyw70-jpfPq_B31qNA_F8,12562 +pygame/examples/data/brick.png,sha256=K_mshK0aL81nzOjAorTXyPps6n9mvofLeOWFXFpVjYA,170 +pygame/examples/data/car_door.wav,sha256=TwYWVqme5NqVVID1N4es92RSKEdTYkxbNx6dNamK-_4,3910 +pygame/examples/data/chimp.png,sha256=gFY5lDOflZ5fCMXpL9_HmipP4-3ALn_r6cCB9yTZKBk,826 +pygame/examples/data/city.png,sha256=c0Nu2o7x7QmvGMDmDCaPnhvJ8tPNuguKKpI_Z-NfQ40,143 +pygame/examples/data/crimson.pnm,sha256=o9ziiY4ox_cCmEo07w08SQckCQTRttxtLgKBE0VmZY8,3124 +pygame/examples/data/cursor.png,sha256=3RDqIuKTXH8Bs67n_ZwEbuS09dQtJeKRgSMR6D9gtWQ,2708 +pygame/examples/data/danger.gif,sha256=m0CBKalFbkqlohgOmrwkwVOfqBhRWonb7xm1pzbDy2Q,2761 +pygame/examples/data/explosion1.gif,sha256=WYcdwbZqmYdaaaPYFiR5vka0Anp4F4nnNlpSSx_1xug,6513 +pygame/examples/data/fist.png,sha256=X0VOsy6fP0UGqBjy7baoBX8XAXyp_1_s2tOItbtA7EI,86196 +pygame/examples/data/green.pcx,sha256=si9WT7dyn3nsXoh34UBW0yOCKWbC-Rz0fKkc_7TDRbY,320 +pygame/examples/data/grey.pgm,sha256=uWTtnBH-Fv605OtEJzS9fG5ns9XaeUHq2YeAC_cdkKU,4155 +pygame/examples/data/house_lo.mp3,sha256=R0nZUXymMp_XLPU8S1yvsiVeWT6MKLt5Rjp-WSnVrLQ,116320 +pygame/examples/data/house_lo.ogg,sha256=64FiQ1Zjq-cOj6Bmya_v3ZjEWmBaGZlTl19udKaz6sU,31334 +pygame/examples/data/house_lo.wav,sha256=B1BwfFaPIsSxaash-igVI_YE9SQd1BCXRTnSAKsNunY,78464 +pygame/examples/data/laplacian.png,sha256=uWI8dPstqMEPVuFPGtm-guu48T2-L3kn99rWA3ZhZ-Q,253 +pygame/examples/data/liquid.bmp,sha256=qtzPXhq0dr2ORNCCZ6gY2loT2Tsu0Dx5YvXB548I1Xg,11734 +pygame/examples/data/midikeys.png,sha256=9HCCmMHvlubR6G9a0jMv1C-AKeBzYfb5jjNhol2Mdqw,19666 +pygame/examples/data/player1.gif,sha256=3ZTVWGxnedKqtf3R-X1omPC0Y8jUSPGgHBAzeGhnV4c,3470 +pygame/examples/data/punch.wav,sha256=A0F1xT8aIZ6aNI_5McMqLygb1EfmdIzPi4kWkU4EwQc,4176 +pygame/examples/data/purple.xpm,sha256=3r6_3v6tob2qy-1hrQ3ujYHpuFb9UQ7LuNsHWq9mj5A,1249 +pygame/examples/data/red.jpg,sha256=mgaTBGP_k55FcqJIL7eV4jYll80zaZHPHfFtXAOLnF8,1251 +pygame/examples/data/sans.ttf,sha256=nrZ6FRet4dwlvA7xOReYCP2QwyGebk0iVJaSFbtpOhM,133088 +pygame/examples/data/scarlet.webp,sha256=iLN1RrY8LCSUnDrwYvWC99v_pLGy0iN8winH7VAyVL0,82 +pygame/examples/data/secosmic_lo.wav,sha256=-EIFkzj7k5qEqG04n7mnUGUp1SsyCJ4n08TzPT600DY,18700 +pygame/examples/data/shot.gif,sha256=bF2eY629zQzvDu83AKpveSFhJq5G4QpOE98A0tvbPFI,129 +pygame/examples/data/static.png,sha256=Xe4wN80awt7nTNiLemoSNTEKlAbGFW7djNETP8IleNs,1202 +pygame/examples/data/teal.svg,sha256=nkksR3fo0NPwC9sVXQPrPR_QrvqRiUB1vC4I-K83dho,313 +pygame/examples/data/turquoise.tif,sha256=4OkIy6CDPMv77tRR_wA9ZHA6qZzG3pjZ-1m1mNB7bcI,1186 +pygame/examples/data/whiff.wav,sha256=FMWM3XnYtce6mHFXQCYPgzT-xu-Q4DJybZfpPjG8cpE,5850 +pygame/examples/data/yellow.tga,sha256=EhxUG3SMO6bbHxr4yFggnKrsC1mYZVq-L6znAsR3z8I,3116 +pygame/examples/dropevent.py,sha256=WMvQbhrHNuSsEyv2hcFr4K4Q83MbZCKtsFNpfuH_SDU,2187 +pygame/examples/eventlist.py,sha256=aPTb0B3DAGnuG76Bz6l8aj6-r3VQ314o9LAiJEl2VHM,5912 +pygame/examples/font_viewer.py,sha256=5l9vYH-P4CTCBxM7TkYh9ASLhdSTf2NB7n-vNu5ZY94,9648 +pygame/examples/fonty.py,sha256=qiYuIracT_jwH5HFx9-tLcwc8qlgALwmL93aZjcUQrU,2073 +pygame/examples/freetype_misc.py,sha256=Fd3USUExyXsmQWWO9I8f2TMLXDooCAebYx10Pnnmbxk,3659 +pygame/examples/glcube.py,sha256=UONh_9RvLhCG8qLQUys4sE2xXY8ukf2zs0KXr_IRE5k,16860 +pygame/examples/go_over_there.py,sha256=YPlaHd2rd4Kc7Cm5VYzRjHUiF8eng04rTO-5cEC_hJ8,2150 +pygame/examples/grid.py,sha256=9dEYCiBjkNxjvk5aMukZxyGtaSVUIWXR4MCN56JEsYY,1745 +pygame/examples/headless_no_windows_needed.py,sha256=Lf-FVBNEHON53nSPLFR5DXHVua5S5N_4LX3gd_yRf_o,1299 +pygame/examples/joystick.py,sha256=pIw3_JRd9R0bpu9LPMiMuN53LVZliWehR3MJ5SFcmzo,5252 +pygame/examples/liquid.py,sha256=mGAniBgkpFYhNyM4XMoFRtlwCbQtd5aZaWCI6C23Bak,2544 +pygame/examples/mask.py,sha256=henO1A-xYDZrB6yqYVOnXkVr-8JeZbNXDODhGmczUgU,5725 +pygame/examples/midi.py,sha256=iQh1tPyyi7zejua6VJa--77aPW-i8Bb8TokRtfaHLRo,31267 +pygame/examples/moveit.py,sha256=cLYWPTnqRVud-Lbq9bVd2S1W8KA8eSExjXmFof35wdI,3330 +pygame/examples/music_drop_fade.py,sha256=bzhtDpDkjyod6Jb-rLU6KAh3KAiHQLFP1XVx717fPt0,9110 +pygame/examples/pixelarray.py,sha256=UQzu0tO7g8EOMoQPVLoFRUvMv1ePd9ybkGUg1PKYoXA,3453 +pygame/examples/playmus.py,sha256=xPhC5wCIyEOjiJkz-ZNtRUl9kladd6m0ZU_jl6QSEko,5215 +pygame/examples/resizing_new.py,sha256=8p6Sy8s74A49OXeboxmWnKGQJVO99ATE4nm7A_ADTxY,1046 +pygame/examples/scaletest.py,sha256=Iq8w-K4pf92-2oee7caNcW9KaOIosArD4AL6roR-svY,4826 +pygame/examples/scrap_clipboard.py,sha256=L2tOzkBSxV3f3hva8XiUibMmYW3oxEMlVUmnX97dXlI,3033 +pygame/examples/scroll.py,sha256=2mKc79QAHLWMO8mG50jmuWTXAvZXdtVsqwjKVFA18RY,6642 +pygame/examples/setmodescale.py,sha256=6GCUOLrGp7KITI1qoGj1hJqNxkouUqmdfYgXlD7IYns,1801 +pygame/examples/sound.py,sha256=CwQ3hSKjD_sHmXEBLfimdbt18cQcXwk5QQ7jfnOuSS0,1172 +pygame/examples/sound_array_demos.py,sha256=Pp6ZsH2WRn_T2z5JNSdpEwILbTh2t_60F5tvNL7zt_s,5756 +pygame/examples/sprite_texture.py,sha256=XCbP01L_tVSylh67kvnPfbU7jLVGXfCwLwZ-HnPa1rU,2667 +pygame/examples/stars.py,sha256=fB6OkpKUYh2WtI04nM32_8A5QDrrw8Xlg_ZCTmVfl6w,2714 +pygame/examples/testsprite.py,sha256=2SRjgFolTSWWxQroPm_HztVADxoKvg5JXddawjvjkjY,6862 +pygame/examples/textinput.py,sha256=vf887f8nAInO66p4qwZbJu316I2bWmUkmZHi50hu9TQ,7579 +pygame/examples/vgrade.py,sha256=RZDRrsAi0bTg6SAh8UJqRQx8viMwjmLSyK20dCql7us,3263 +pygame/examples/video.py,sha256=v_81PS92Mxad1KelrHOAOGwLsIjE46GIM7HwUj9oSPo,4357 +pygame/fastevent.py,sha256=NOVGX3eAvQGCSHDOZZJ_VuWSyekqdPO1Wrr2MvCEQeU,1694 +pygame/fastevent.pyi,sha256=3FmWstDsaRS5TOqExgceS8jeZnkkYhSrYIgTEqSKkU0,249 +pygame/font.cp36-win_amd64.pyd,sha256=8E-mqqILfl2J3qzcOhuVjfJgPMG2Klg5R_hVav_icvk,24064 +pygame/font.pyi,sha256=YDTjkLtJUC_xp4Na6q-tolDN1AiFk9WTioGtwNHWFPU,2079 +pygame/freesansbold.ttf,sha256=v5JRJp8R5LNVgqmTdglt7uPQxJc6RZy9l7C-vAH0QK0,98600 +pygame/freetype.dll,sha256=HEhzktbQaXC6PHtScFiB8fsGn2ByQ0mSdsLwwDPH328,654336 +pygame/freetype.py,sha256=OyLcjKlZXyhQEV2EZ0xMY57HypqGA77K14CJGMxw5H8,2224 +pygame/freetype.pyi,sha256=sjguJUovQqRjoKZ9xACq52-ykUY2XRir__Uody8UPVo,3512 +pygame/ftfont.py,sha256=wJeDvzh9XT3Mi3Yur-JUCIBICEsOeNpeXwW--eBoLbg,6153 +pygame/gfxdraw.cp36-win_amd64.pyd,sha256=xSVgXNvjtA4E6vmm38wgfmz3nWwGKkLfo_yW6p-1WmE,58880 +pygame/gfxdraw.pyi,sha256=GbHQUuqOdJAxe6tQfp1EfhL3-P6vBltbayaMBeFjsVY,2500 +pygame/image.cp36-win_amd64.pyd,sha256=hH9vSXnSILrDLdd0T8iOm8MauxyuwQ_bobk9CAPqgr0,29696 +pygame/image.pyi,sha256=5bCTCOTJ5azmeIV82settKwR4aKfOA_kB_1mSiBTqSs,1705 +pygame/imageext.cp36-win_amd64.pyd,sha256=aY80X6Ygt09lpCoxyOMjMvE8q25-L_vlrOxD5C2NVrE,17408 +pygame/joystick.cp36-win_amd64.pyd,sha256=0_QI3Zr3dCTIHhFwwvB--mwMFytPlS5EtUkTMMO5nGk,20480 +pygame/joystick.pyi,sha256=Lr0DiEuJjbuyGEc1fCPkzVMYPu_qPGUky_x-k5KikPQ,1348 +pygame/key.cp36-win_amd64.pyd,sha256=_XrtabSwKu9ez4V_61M4XozngFu3W0DgwckRrJR6I5o,20480 +pygame/key.pyi,sha256=7Ei4SZ3tuWMb9XAtRZm2lw9Sd5BDwjzM3QS1-WRrcBY,562 +pygame/libjpeg-9.dll,sha256=OiJK9UDJZXSAD16az2SyzfuQYOcnkZ7BT70Yeptb_mk,244224 +pygame/libmodplug-1.dll,sha256=DBqQMoEuxMIAA6mXQj5ntx7LXlnWLNwYpb9ZEXapAQ4,265216 +pygame/libogg-0.dll,sha256=V6vE9qmszdCL-aKwIqZmQMxialvU2sbHxPBqXfYe4f4,25600 +pygame/libopus-0.dll,sha256=dxyueUEPf8xPmToQWhjE7Z6Mvd1vgHpCIo2V9XWAiAY,368128 +pygame/libopusfile-0.dll,sha256=zKrKgYEL0tHKtGkrQlOmOfjVUWmW2w4k2IHv0-_cxqQ,46592 +pygame/libpng16-16.dll,sha256=5oi0pNGPS2zMmcbKSYD1EhjLglYQd1GS2bYLLwXv8tU,210944 +pygame/libtiff-5.dll,sha256=6_6XrF7ya5SUWvPbX_0RCkuOktwCVZv4HMsz8NXrzpU,432640 +pygame/libwebp-7.dll,sha256=mlNWO2BY9w8nJQKbfdL-lvhpwg6AkAMc0wPplN_ge1A,447488 +pygame/locals.py,sha256=IO2_D-d3Z5FStP6QDjiyN_W0QPq0i_hHuCQWhxdFRSs,1195 +pygame/locals.pyi,sha256=fLWVStrayTJQMr7r5z45ywHMIOgEYIqLYJkyjIcUsGg,9925 +pygame/macosx.py,sha256=GJYmXKulp9UCu5NgYMtHIzp22ij-QZBhNOLbrJdgtsM,329 +pygame/mask.cp36-win_amd64.pyd,sha256=opBMxmHUzrm5waEPjpXGB1QRUxgyB7293o0snvof0zw,55296 +pygame/mask.pyi,sha256=bBxSevY0teCHn86xPnQIgx10xLd5HX2O3Ohkfrz_m5E,2304 +pygame/math.cp36-win_amd64.pyd,sha256=zSxy02uIizENi3IjAAVtOYbjFOjdITYiQDgeDlDz408,76800 +pygame/math.pyi,sha256=crW5wZF1MKl2sERrePqGW1roEXACWAbyreqbi2n9zZs,11685 +pygame/midi.py,sha256=GEmQFXnCclms235CT5ULsMs6fIbnrIpwo57J-Kbla_c,24341 +pygame/midi.pyi,sha256=LlPPnnXyHdklV5OTipShPxi8WUcc-NyasfLYxz-fcGw,1774 +pygame/mixer.cp36-win_amd64.pyd,sha256=d9hP9oxHmhOIII5Bj5uznL3KFGAWzBrRjZ0VkdR0LUs,37888 +pygame/mixer.pyi,sha256=HuPeysk7q79eWlPe2r54Z-j1Kgx1JKTQZnvXCESibjA,2856 +pygame/mixer_music.cp36-win_amd64.pyd,sha256=uDecDQI4mJBREyvzPNipbW48FJ8HUrgvpy2_9g0CKDA,20992 +pygame/mixer_music.pyi,sha256=gMklc23g-bBIAGa3giyM00yi2jLedrKipx9_8lebvFY,691 +pygame/mouse.cp36-win_amd64.pyd,sha256=qySAtcB6snuc6osGIEJKAAjpocwBFPZksUTx1sKr1-Y,19456 +pygame/mouse.pyi,sha256=a9Y_UtpHhMVQqpE3LnkAaI3EoVjQMdVNJNVeZ-eXTXE,1184 +pygame/newbuffer.cp36-win_amd64.pyd,sha256=2g6pNLh0k8XpjIUfNuEo127nuGyACW01OidL2ZCBWgg,21504 +pygame/pixelarray.cp36-win_amd64.pyd,sha256=vyMbiJx0rm4Dch86EFB7k5nhOrC1-O9MSagsV6NEdsY,47616 +pygame/pixelarray.pyi,sha256=8bDvIdo9QPBfgSSU04kxZaYCkkdv_XyXRQtA28YI6P0,1226 +pygame/pixelcopy.cp36-win_amd64.pyd,sha256=V9APSiCOzPgTuQpPFRxojl-NX7u1NzvE-SGrgNTqRq4,27136 +pygame/pixelcopy.pyi,sha256=9P6c4l_16qAfJYsQ3IkJGPMKyp_weLB1e9iQIvLYDZM,534 +pygame/pkgdata.py,sha256=RTPgP8uEaPK9GdyeOSUzdN4N9LLdrUASHUXMoUersFY,2421 +pygame/portmidi.dll,sha256=yfjZBDrBVwsQ8QTy0ArseR9WJhyE7kB3O-c9Cjgi4BM,41984 +pygame/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pygame/pygame.ico,sha256=PBF9cw0Ca9Rw2pNmDD3iXvcYYQeI9ZzZ9vxtRLQRoJc,145516 +pygame/pygame_icon.bmp,sha256=Twnby8nv4HMhGka49n-47CPseDvwrSLZ0l1o9U2Bb5s,630 +pygame/pygame_icon.icns,sha256=4jwAo9VtMhTK9hq9ibt6MZ4_sd2VsZueWZ3YAMuTPgw,264148 +pygame/pygame_icon_mac.bmp,sha256=QrAs3kEF6v7wVMlIJgktI90aUdTg9RdTmp16d2HZhNg,262282 +pygame/pypm.cp36-win_amd64.pyd,sha256=27oPUBiMNj95yDcMeahvST8pEPexaOMoyyStngbjo7s,103936 +pygame/rect.cp36-win_amd64.pyd,sha256=ydjqM2clNsw8k7321dZYXoA_a78Ra0ROvM_zVHG_YHg,46080 +pygame/rect.pyi,sha256=gOICRGGXcziZKQvJrWVJ34H3wNl6-3Qp9uyWicbLHko,7138 +pygame/rwobject.cp36-win_amd64.pyd,sha256=I_FHmYJk_xSib71k9nXEXrfYs695qgmMYj5FyAgTqtw,19968 +pygame/rwobject.pyi,sha256=7pjMcSV8digIazj9i0h5se7steQNUtBBBcK-f2sY-Yo,544 +pygame/scrap.cp36-win_amd64.pyd,sha256=GVNFke5qe0c7f8isFP65VNddWfQbmZ81NoJM0bxIJMg,18944 +pygame/scrap.pyi,sha256=xEbe7P1WNSmqeFQ-M3XCX3JztjHu1-m_0x8vkL6S4bA,366 +pygame/sndarray.py,sha256=hJMTnQBEbOBYzeBRdCiGHCr3S20NqpHVevX2WFxw51I,4083 +pygame/sndarray.pyi,sha256=-kUmCShhKSazBRhHberq4kDhQau0I-02LZHvfIVYi7A,337 +pygame/sprite.py,sha256=Uaas5bn2a85gfbRXHGJwlQVh6OLA_vrPc8iK4qbTcqE,62899 +pygame/sprite.pyi,sha256=M6V64QuHdfITdWOaX6Xa20v4B9tJoUjYtyCdCcqz0LI,9748 +pygame/surface.cp36-win_amd64.pyd,sha256=DmMbde06AUcbC4PZN5WY9ya1BNtD4AdQ2-i_qDTVG34,239104 +pygame/surface.pyi,sha256=mKyduOgOgekHQ3JObqdjFMkTMI22Aeat2MCAkOTWseE,4726 +pygame/surfarray.py,sha256=xlKZVqyoL8DSOAGOarb0E8VSha0vPn3xYY2fJz7zT8o,14405 +pygame/surfarray.pyi,sha256=g4w8kePGEZGMGhdtKo0ClZ3G-VPitz7DmrtIK6a7FGY,1287 +pygame/surflock.cp36-win_amd64.pyd,sha256=QdWl_9P896mHsx8iCk4QsIWn6ABcAN9EPkYdkfcGsvI,13824 +pygame/surflock.pyi,sha256=p6HFejTvHw2HxSUOD4vW9HolANRfvGHjADdRh63skKQ,122 +pygame/sysfont.py,sha256=8-p9XqoPP15Vkmtm9WcIzi_xmpIvk-WobfExf4MWxsM,15391 +pygame/tests/__init__.py,sha256=wfUhz-LZF-OXZNT81UfGdofNYPCUMJoF3nwgXpzg4sE,1251 +pygame/tests/__main__.py,sha256=xLKWh5Kk0hc-2vz_OrNsMkVproZ3ndWz6ZCMY7Myk5A,3788 +pygame/tests/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/__pycache__/__main__.cpython-36.pyc,, +pygame/tests/__pycache__/base_test.cpython-36.pyc,, +pygame/tests/__pycache__/blit_test.cpython-36.pyc,, +pygame/tests/__pycache__/bufferproxy_test.cpython-36.pyc,, +pygame/tests/__pycache__/camera_test.cpython-36.pyc,, +pygame/tests/__pycache__/color_test.cpython-36.pyc,, +pygame/tests/__pycache__/constants_test.cpython-36.pyc,, +pygame/tests/__pycache__/controller_test.cpython-36.pyc,, +pygame/tests/__pycache__/cursors_test.cpython-36.pyc,, +pygame/tests/__pycache__/display_test.cpython-36.pyc,, +pygame/tests/__pycache__/docs_test.cpython-36.pyc,, +pygame/tests/__pycache__/draw_test.cpython-36.pyc,, +pygame/tests/__pycache__/event_test.cpython-36.pyc,, +pygame/tests/__pycache__/font_test.cpython-36.pyc,, +pygame/tests/__pycache__/freetype_tags.cpython-36.pyc,, +pygame/tests/__pycache__/freetype_test.cpython-36.pyc,, +pygame/tests/__pycache__/ftfont_tags.cpython-36.pyc,, +pygame/tests/__pycache__/ftfont_test.cpython-36.pyc,, +pygame/tests/__pycache__/gfxdraw_test.cpython-36.pyc,, +pygame/tests/__pycache__/image__save_gl_surface_test.cpython-36.pyc,, +pygame/tests/__pycache__/image_tags.cpython-36.pyc,, +pygame/tests/__pycache__/image_test.cpython-36.pyc,, +pygame/tests/__pycache__/imageext_tags.cpython-36.pyc,, +pygame/tests/__pycache__/imageext_test.cpython-36.pyc,, +pygame/tests/__pycache__/joystick_test.cpython-36.pyc,, +pygame/tests/__pycache__/key_test.cpython-36.pyc,, +pygame/tests/__pycache__/locals_test.cpython-36.pyc,, +pygame/tests/__pycache__/mask_test.cpython-36.pyc,, +pygame/tests/__pycache__/math_test.cpython-36.pyc,, +pygame/tests/__pycache__/midi_test.cpython-36.pyc,, +pygame/tests/__pycache__/mixer_music_tags.cpython-36.pyc,, +pygame/tests/__pycache__/mixer_music_test.cpython-36.pyc,, +pygame/tests/__pycache__/mixer_tags.cpython-36.pyc,, +pygame/tests/__pycache__/mixer_test.cpython-36.pyc,, +pygame/tests/__pycache__/mouse_test.cpython-36.pyc,, +pygame/tests/__pycache__/pixelarray_test.cpython-36.pyc,, +pygame/tests/__pycache__/pixelcopy_test.cpython-36.pyc,, +pygame/tests/__pycache__/rect_test.cpython-36.pyc,, +pygame/tests/__pycache__/rwobject_test.cpython-36.pyc,, +pygame/tests/__pycache__/scrap_tags.cpython-36.pyc,, +pygame/tests/__pycache__/scrap_test.cpython-36.pyc,, +pygame/tests/__pycache__/sndarray_tags.cpython-36.pyc,, +pygame/tests/__pycache__/sndarray_test.cpython-36.pyc,, +pygame/tests/__pycache__/sprite_test.cpython-36.pyc,, +pygame/tests/__pycache__/surface_test.cpython-36.pyc,, +pygame/tests/__pycache__/surfarray_tags.cpython-36.pyc,, +pygame/tests/__pycache__/surfarray_test.cpython-36.pyc,, +pygame/tests/__pycache__/surflock_test.cpython-36.pyc,, +pygame/tests/__pycache__/sysfont_test.cpython-36.pyc,, +pygame/tests/__pycache__/threads_test.cpython-36.pyc,, +pygame/tests/__pycache__/time_test.cpython-36.pyc,, +pygame/tests/__pycache__/touch_test.cpython-36.pyc,, +pygame/tests/__pycache__/transform_test.cpython-36.pyc,, +pygame/tests/__pycache__/version_test.cpython-36.pyc,, +pygame/tests/__pycache__/video_test.cpython-36.pyc,, +pygame/tests/base_test.py,sha256=d6POJDqd_uxot9yxTxRBW2thiOD4o0HYuIHBq84OBUg,22449 +pygame/tests/blit_test.py,sha256=IUGAXjy0ZK72jmboE2r9LF9wZ7TXlYJAzm99a4JNfV0,6343 +pygame/tests/bufferproxy_test.py,sha256=bji75h8zDPEHNXSKEnT9Gb4T0_cMcAlDxqvPdlR5WU0,16451 +pygame/tests/camera_test.py,sha256=kMyJ3SYnifZe5Mv7JRl8exEuJ4IOe2SA98_6y-Uour0,801 +pygame/tests/color_test.py,sha256=I0OxIOf8sqYk8nW3Wmy76AqFuFSCwafDGUOF8O6QA58,49781 +pygame/tests/constants_test.py,sha256=kOOqPnXMMka37d4VUDo70j3i_zAN74Y5QuRLnNQsRao,9308 +pygame/tests/controller_test.py,sha256=a6NSWn1k1Rpt4HTYeOAbF-cDx0KfnKsrG6DwA0vk_0k,10834 +pygame/tests/cursors_test.py,sha256=qc-T5sdh25LO-KOK16CMp9FnWCXJSwdUo29YkePeSTs,7700 +pygame/tests/display_test.py,sha256=hSHKBJUqDJWzgnQgNten0L5ejEQgNd1wZRklzBDLzmo,46425 +pygame/tests/docs_test.py,sha256=r2qa_ox8eg2_Y5Pb9-XzZExAhqJrdVMO30guO1o7PPM,1091 +pygame/tests/draw_test.py,sha256=YEt_6zJKnTRBjxHbO4dKBfWwFOmGDVbhwPe0zvdt5zI,237912 +pygame/tests/event_test.py,sha256=XNd_lLqGbZ9PwAJPL4AxoZwblLTlQrsLU_KpoiMyaog,33593 +pygame/tests/fixtures/fonts/A_PyGameMono-8.png,sha256=QmhReADwKrzW5RWnG1KHEtZIqpVtwWzhXmydX1su10c,92 +pygame/tests/fixtures/fonts/PlayfairDisplaySemibold.ttf,sha256=4Q60pnYY-7dwBTVr7PBSz5r-w_kfUzhal8i85ISG7V4,236636 +pygame/tests/fixtures/fonts/PyGameMono-18-100dpi.bdf,sha256=nm3okxnfAFtADlp7s2AY43zS49NYg9jq7GVzG2lPhOQ,1947 +pygame/tests/fixtures/fonts/PyGameMono-18-75dpi.bdf,sha256=4kB0uYeEpa3W-ZAomFMpc0hD-h6FnOh2m5IPi6xzfds,1648 +pygame/tests/fixtures/fonts/PyGameMono-8.bdf,sha256=aK0KV-_osDPTPiA1BUCgZHOmufy6J9Vh5pf1IAi0_yg,1365 +pygame/tests/fixtures/fonts/PyGameMono.otf,sha256=_Af4LyMEgKKGa8jDlfik89axhLc3HoS8aG5JHWN5sZw,3128 +pygame/tests/fixtures/fonts/test_fixed.otf,sha256=FWHmFsQUobgtbm370Y5XJv1lAokTreGR5fo4tuw3Who,58464 +pygame/tests/fixtures/fonts/test_sans.ttf,sha256=nrZ6FRet4dwlvA7xOReYCP2QwyGebk0iVJaSFbtpOhM,133088 +pygame/tests/fixtures/fonts/u13079_PyGameMono-8.png,sha256=x_D28PW8aKed8ZHBK6AISEZ9vlEV76Whi770ItTuFVU,89 +pygame/tests/fixtures/xbm_cursors/white_sizing.xbm,sha256=VLAS1A417T-Vg6GMsmicUCYpOhvGsrgJJYUvdFYYteY,366 +pygame/tests/fixtures/xbm_cursors/white_sizing_mask.xbm,sha256=CKQeiOtlFoJdAts83UmTEeVk-3pxgJ9Wu2QJaCjzAQM,391 +pygame/tests/font_test.py,sha256=lYgOKil5siDjsy-0FV2kHOTO8vdAESmVzYOoT8yONyc,27182 +pygame/tests/freetype_tags.py,sha256=NdjMDSYHfrhopKR0JuTeUfFX-AbcCu4fsXnS1a46iVM,182 +pygame/tests/freetype_test.py,sha256=GFZUMLZF-KjXEyOhq0FqI2Oe_nt1hbkPHTkSW7RN3pk,64896 +pygame/tests/ftfont_tags.py,sha256=IvteBUDEp4rv9q6FwlTpQ9X2px-XUromSMQ921VrhCU,180 +pygame/tests/ftfont_test.py,sha256=YZesw8r_NfqcoUsg78FbAh0kJaeC6iAzixHMsQcH3gY,421 +pygame/tests/gfxdraw_test.py,sha256=XWxHvtvkMWllmlCsGhaUGCdOFBAiOfJgYg_KnUj8Q_E,32354 +pygame/tests/image__save_gl_surface_test.py,sha256=5H8TeGZNRZzu5kJInWPe8AuuKqHv-utunadoBmn--CI,1198 +pygame/tests/image_tags.py,sha256=_WJGXgTOaUn4IG7fIk1sDKfDDZP3W8N6PkrrOpPT-U8,132 +pygame/tests/image_test.py,sha256=LaHCxn5hpcy1s9SdOq712iPau_4p-2Gq0mKjmc9pifc,43024 +pygame/tests/imageext_tags.py,sha256=-vnXr7O5F1NVrEDrOHBEYdaD-JiuBT9NI-lxGps-K1U,135 +pygame/tests/imageext_test.py,sha256=Dam4nzQG1dZtJ8rtAmSw2xdhIvENATklN81mVl5Mh4I,2852 +pygame/tests/joystick_test.py,sha256=XArf2gXSYYupNOuOekpKtGlJ_vaLWWUSl7hpKnBE3FU,6078 +pygame/tests/key_test.py,sha256=rZ9EPqi8q6VQI7kQzdjW8U0Ebqk2QYnU9D7i2TXsdDY,9172 +pygame/tests/locals_test.py,sha256=2g4vCW-wJG0U_wA7VP1kieV-wSsvGInyGkLW6OjvJ4o,417 +pygame/tests/mask_test.py,sha256=95xNu2gy1TVgxKNnDdcYXgo_rKaTdKPAqeeKn1jI_Xg,245880 +pygame/tests/math_test.py,sha256=LmwS-6B6LU4EXRx9_I-s_WyrddmEcu7HDp7xNL6VK30,111540 +pygame/tests/midi_test.py,sha256=utmDFvk5wZCcXrTTGXnl4N6dMjmkXgrIV6nGtgcws6k,16905 +pygame/tests/mixer_music_tags.py,sha256=o0gsQDjuICFYw8j3WOlIluwk9fdA42ledU1U6DIJzNU,138 +pygame/tests/mixer_music_test.py,sha256=rgTeUXFm__7ZR5SDVqB6O5VyRlwFuTZ9LrSP1Q4-R3s,17522 +pygame/tests/mixer_tags.py,sha256=qKcn8AD46H3V87xONG0iXlGH_FveeGBgf2gE1MMh2s0,132 +pygame/tests/mixer_test.py,sha256=OMElH1sGbJGlRK7Psr1qwWZvHG_MciAA5F_6o9S6rsE,54019 +pygame/tests/mouse_test.py,sha256=wL5Jmwj65UhtrON5rSXImomjNE_afKGlFQqlS-JZfT8,13146 +pygame/tests/pixelarray_test.py,sha256=NKVn_4NumiMz0CHUDm7DH-OcEZI0XpAZmjenAXwDJlY,62745 +pygame/tests/pixelcopy_test.py,sha256=j8ywtiFayJMiRBi1v6ywED8xnRkpSVv5vZ579yEvpDA,25543 +pygame/tests/rect_test.py,sha256=qN1IQrf9iFXuJfIF-8sOwqouxbMJkzNPLTD-YvIxHlA,117687 +pygame/tests/run_tests__tests/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/__pycache__/run_tests__test.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/all_ok/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/__pycache__/fake_3_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/__pycache__/fake_4_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/__pycache__/fake_5_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/__pycache__/fake_6_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/__pycache__/no_assertions__ret_code_of_1__test.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/__pycache__/zero_tests_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/all_ok/fake_2_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/all_ok/fake_3_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/all_ok/fake_4_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/all_ok/fake_5_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/all_ok/fake_6_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/all_ok/no_assertions__ret_code_of_1__test.py,sha256=PNrfACCpcPnO964Oxv2-9l4ciuJ-Iqw3x8HDs-kebVg,797 +pygame/tests/run_tests__tests/all_ok/zero_tests_test.py,sha256=XzLaMjkygsvNkFEqnRU9y2Ijm6bfds9n5Z6mg_LOMJQ,545 +pygame/tests/run_tests__tests/everything/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/everything/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/everything/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/everything/__pycache__/incomplete_todo_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/everything/__pycache__/magic_tag_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/everything/__pycache__/sleep_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/everything/fake_2_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/everything/incomplete_todo_test.py,sha256=71myeZtFerYY2rB-j60l5Ltz3FiRCuOR4evFXtJHC34,909 +pygame/tests/run_tests__tests/everything/magic_tag_test.py,sha256=SjIKB_7aLfGdih8cotQ34m1KbSEII_1wGQUBwrWeIyY,859 +pygame/tests/run_tests__tests/everything/sleep_test.py,sha256=AyGwZk5fQAkfeCr9VewdsuD_z5BzlVfkmbZD-XetB50,715 +pygame/tests/run_tests__tests/exclude/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/exclude/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/exclude/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/exclude/__pycache__/invisible_tag_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/exclude/__pycache__/magic_tag_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/exclude/fake_2_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/exclude/invisible_tag_test.py,sha256=AdHFvOK-kCRi2iUs68So6Ngef6C_LEdx3QpMLjhKtmM,925 +pygame/tests/run_tests__tests/exclude/magic_tag_test.py,sha256=SjIKB_7aLfGdih8cotQ34m1KbSEII_1wGQUBwrWeIyY,859 +pygame/tests/run_tests__tests/failures1/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/failures1/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/failures1/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/failures1/__pycache__/fake_3_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/failures1/__pycache__/fake_4_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/failures1/fake_2_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/failures1/fake_3_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/failures1/fake_4_test.py,sha256=xWpIVUpzevSs4bVeze48Q9jkZzss4szdw6eMOrJnZV8,949 +pygame/tests/run_tests__tests/incomplete/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/incomplete/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/incomplete/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/incomplete/__pycache__/fake_3_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/incomplete/fake_2_test.py,sha256=RVUuQZxqYScIUAflNIsXd7UE6Rxm6HHFZSi8cpz5m-k,889 +pygame/tests/run_tests__tests/incomplete/fake_3_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/incomplete_todo/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/incomplete_todo/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/incomplete_todo/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/incomplete_todo/__pycache__/fake_3_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/incomplete_todo/fake_2_test.py,sha256=71myeZtFerYY2rB-j60l5Ltz3FiRCuOR4evFXtJHC34,909 +pygame/tests/run_tests__tests/incomplete_todo/fake_3_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/infinite_loop/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/infinite_loop/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/infinite_loop/__pycache__/fake_1_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/infinite_loop/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/infinite_loop/fake_1_test.py,sha256=rNt-VaNziz7OmfbDXcbXbDIbwC_6ScFJ-MtenMjR68Y,906 +pygame/tests/run_tests__tests/infinite_loop/fake_2_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/print_stderr/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/print_stderr/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/print_stderr/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/print_stderr/__pycache__/fake_3_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/print_stderr/__pycache__/fake_4_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/print_stderr/fake_2_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/print_stderr/fake_3_test.py,sha256=6AGEff135DU_spRhZ09oDGXE4lZC3dlHU_phnfOyWYY,954 +pygame/tests/run_tests__tests/print_stderr/fake_4_test.py,sha256=xWpIVUpzevSs4bVeze48Q9jkZzss4szdw6eMOrJnZV8,949 +pygame/tests/run_tests__tests/print_stdout/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/print_stdout/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/print_stdout/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/print_stdout/__pycache__/fake_3_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/print_stdout/__pycache__/fake_4_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/print_stdout/fake_2_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/print_stdout/fake_3_test.py,sha256=cruYqrh3O3MQ8fczEFloLpsrQrYmMOd6jgxMU6e5H8w,1012 +pygame/tests/run_tests__tests/print_stdout/fake_4_test.py,sha256=xWpIVUpzevSs4bVeze48Q9jkZzss4szdw6eMOrJnZV8,949 +pygame/tests/run_tests__tests/run_tests__test.py,sha256=9mDlobUHX5baqNIJvCo4hN7XGb5qRIRO_DFfNcOno5I,4315 +pygame/tests/run_tests__tests/timeout/__init__.py,sha256=9_8wL9Scv8_Cs8HJyJHGvx1vwXErsuvlsAqNZLcJQR0,8 +pygame/tests/run_tests__tests/timeout/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/run_tests__tests/timeout/__pycache__/fake_2_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/timeout/__pycache__/sleep_test.cpython-36.pyc,, +pygame/tests/run_tests__tests/timeout/fake_2_test.py,sha256=zFUNsDLmH9Pvpo-YEpvfW2raQyA6EL_BW3CuP10YIKU,899 +pygame/tests/run_tests__tests/timeout/sleep_test.py,sha256=5EDW4U6kYN4QIid0IgHBypJ3T3a78pILILF41DPpujk,716 +pygame/tests/rwobject_test.py,sha256=LAJun6obwHADEiONe6F68WNM9qAuL7i4hqbPErjmox4,4323 +pygame/tests/scrap_tags.py,sha256=zHyLWy2JRyfw0DamlH9dz-MZq2R2uOryjH9JRu-RCkw,671 +pygame/tests/scrap_test.py,sha256=qt47IQLTs3jqf8FEP1mBC8gX6SzbeLsjJnIdszMhbYU,9160 +pygame/tests/sndarray_tags.py,sha256=ThDQxqGFaAembuWgdYGsFSWEppVezgXJ2htYRvvDaXE,190 +pygame/tests/sndarray_test.py,sha256=BOv9SLpvUiQraLEykbOHKyE4jkSJuKYPd1IRUdU510Y,6290 +pygame/tests/sprite_test.py,sha256=08ZqxvMi7Bii4gGFVy2NdKJ3yWKz1an6AyEQs8Qhmqs,47210 +pygame/tests/surface_test.py,sha256=1bzv6cwWVL8wxNk0ZlX8iGVrP2Ep8NagqsP_iem8qKI,165585 +pygame/tests/surfarray_tags.py,sha256=AwlglKM7DrjHvvcSMm-yXb-PSxsVhJkS6VE7Z8wOhes,260 +pygame/tests/surfarray_test.py,sha256=mYy2tMofKo6ESr70UOc-qxxU2X22sUKw_zyMHmwHgFc,25812 +pygame/tests/surflock_test.py,sha256=dMZkzND7-R_z-GaxN4ZIcpNtW3PsK1i7kdnizpU21UY,4728 +pygame/tests/sysfont_test.py,sha256=uf7ISqCvoiegyUvYTqOT1-kYQi_POeejL1TqXXeYF9Q,1463 +pygame/tests/test_utils/__init__.py,sha256=_eQvMYQ_-gFuB1fiftJLjL0RYqV3tl5jRYeBUzI8x-4,4429 +pygame/tests/test_utils/__pycache__/__init__.cpython-36.pyc,, +pygame/tests/test_utils/__pycache__/arrinter.cpython-36.pyc,, +pygame/tests/test_utils/__pycache__/async_sub.cpython-36.pyc,, +pygame/tests/test_utils/__pycache__/buftools.cpython-36.pyc,, +pygame/tests/test_utils/__pycache__/endian.cpython-36.pyc,, +pygame/tests/test_utils/__pycache__/png.cpython-36.pyc,, +pygame/tests/test_utils/__pycache__/run_tests.cpython-36.pyc,, +pygame/tests/test_utils/__pycache__/test_machinery.cpython-36.pyc,, +pygame/tests/test_utils/__pycache__/test_runner.cpython-36.pyc,, +pygame/tests/test_utils/arrinter.py,sha256=xmfJDli6Q3SPBy5cLAF0SqTXlRc5QDZRhbDP50p5pjc,14698 +pygame/tests/test_utils/async_sub.py,sha256=2BqxoeKTo6sPlW2bwGCieEb0lwxZsHUHBnGFjSPny68,9128 +pygame/tests/test_utils/buftools.py,sha256=9bae7xJK78yZQk1Pc2juF-1Gp19Zpwsl3ujnCfsh48o,23641 +pygame/tests/test_utils/endian.py,sha256=Rc7rl38YamHgi8EzB92Muu8C4XH6yltH9f5On7qfMpY,495 +pygame/tests/test_utils/png.py,sha256=dxobMiiDvvAEZSNFoy0EkQxYoLNzfc4rHKLwYM58PEQ,152320 +pygame/tests/test_utils/run_tests.py,sha256=KeqrO4gUuPx6ElW9x-AuS_02Nqk7hMLasFFGfO-CjfY,12039 +pygame/tests/test_utils/test_machinery.py,sha256=4vsi3mOw_581298rsp4cmcQYpbB6P5PK3Eg0cTBy138,2429 +pygame/tests/test_utils/test_runner.py,sha256=ktlXMmx0LOD9gmeTXyk72Ozo1UkYlUUJGoYoohdMZfY,9328 +pygame/tests/threads_test.py,sha256=WWTg2G0x3eP0JxWZa3qGCbkRMWfiNCpkX9-_e0aUWZY,7833 +pygame/tests/time_test.py,sha256=lQTubG8CiDnTwHOcV0Esvk1QJX90vjbxZNDdorg1wZU,16065 +pygame/tests/touch_test.py,sha256=9e5LDHeZrtQSaUBrb9dLHT1YDuNuccSlYvcYIWYIm7k,3216 +pygame/tests/transform_test.py,sha256=EryZIaG25yunlIW9wS4pnQhtswVNI-IBp9oAJzTe7zY,53328 +pygame/tests/version_test.py,sha256=dvNIneFf1c4PAKa4xo1YLAlYRDFYL8_ZeUwI1FS55Is,1536 +pygame/tests/video_test.py,sha256=USdLAov8GnN_9vsXl_1pe0MI0ddM3KurURDhVQIV3nY,694 +pygame/threads/__init__.py,sha256=mkOLm--1G55m8Vj5AY30IMUhMfL1lBjZxcgNESooZBw,8063 +pygame/threads/__pycache__/__init__.cpython-36.pyc,, +pygame/time.cp36-win_amd64.pyd,sha256=8XP7eGid-b52ppHXX6zxOrDIHEqbhLqX5cbwHlkpl14,18944 +pygame/time.pyi,sha256=0dUmVQjxwyhXkSRGX0DnqM2-0JzOMIVbHx4psLfrlhI,501 +pygame/transform.cp36-win_amd64.pyd,sha256=_aKj3g_SmRLx36TfqmNdUZXCe94VbexEBJZJF_9A68E,58368 +pygame/transform.pyi,sha256=EsjIBP706hur5c6aKmUBMbCp9cPQIilWcqkSHkCeUk4,1999 +pygame/version.py,sha256=fMY0j8dhul8aqGWskX4naIsnDXXx7l-jxex9FPRf6p0,2526 +pygame/version.pyi,sha256=NvmU4694WwSRWMiZ5WL3APQYA2s_3xY4foOnPsncNuM,600 +pygame/zlib1.dll,sha256=sfWKF_O_1VUj5772haz1sy0cKm8lq9zUQmgSZv0mqwg,108544 diff --git a/laplas/tools/abstract_map/flask/py.typed b/laplas/abstract_map/pygame-2.6.0.dist-info/REQUESTED similarity index 100% rename from laplas/tools/abstract_map/flask/py.typed rename to laplas/abstract_map/pygame-2.6.0.dist-info/REQUESTED diff --git a/laplas/abstract_map/pygame-2.6.0.dist-info/WHEEL b/laplas/abstract_map/pygame-2.6.0.dist-info/WHEEL new file mode 100644 index 0000000..83ad8eb --- /dev/null +++ b/laplas/abstract_map/pygame-2.6.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: false +Tag: cp36-cp36m-win_amd64 + diff --git a/laplas/abstract_map/pygame-2.6.0.dist-info/entry_points.txt b/laplas/abstract_map/pygame-2.6.0.dist-info/entry_points.txt new file mode 100644 index 0000000..e5a477e --- /dev/null +++ b/laplas/abstract_map/pygame-2.6.0.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[pyinstaller40] +hook-dirs = pygame.__pyinstaller:get_hook_dirs + diff --git a/laplas/abstract_map/pygame-2.6.0.dist-info/top_level.txt b/laplas/abstract_map/pygame-2.6.0.dist-info/top_level.txt new file mode 100644 index 0000000..0cb7ff1 --- /dev/null +++ b/laplas/abstract_map/pygame-2.6.0.dist-info/top_level.txt @@ -0,0 +1 @@ +pygame diff --git a/laplas/tools/abstract_map/pygame/SDL2.dll b/laplas/abstract_map/pygame/SDL2.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/SDL2.dll rename to laplas/abstract_map/pygame/SDL2.dll diff --git a/laplas/tools/abstract_map/pygame/SDL2_image.dll b/laplas/abstract_map/pygame/SDL2_image.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/SDL2_image.dll rename to laplas/abstract_map/pygame/SDL2_image.dll diff --git a/laplas/tools/abstract_map/pygame/SDL2_mixer.dll b/laplas/abstract_map/pygame/SDL2_mixer.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/SDL2_mixer.dll rename to laplas/abstract_map/pygame/SDL2_mixer.dll diff --git a/laplas/tools/abstract_map/pygame/SDL2_ttf.dll b/laplas/abstract_map/pygame/SDL2_ttf.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/SDL2_ttf.dll rename to laplas/abstract_map/pygame/SDL2_ttf.dll diff --git a/laplas/tools/abstract_map/pygame/__init__.py b/laplas/abstract_map/pygame/__init__.py similarity index 100% rename from laplas/tools/abstract_map/pygame/__init__.py rename to laplas/abstract_map/pygame/__init__.py diff --git a/laplas/tools/abstract_map/pygame/__init__.pyi b/laplas/abstract_map/pygame/__init__.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/__init__.pyi rename to laplas/abstract_map/pygame/__init__.pyi diff --git a/laplas/tools/abstract_map/pygame/__pyinstaller/__init__.py b/laplas/abstract_map/pygame/__pyinstaller/__init__.py similarity index 100% rename from laplas/tools/abstract_map/pygame/__pyinstaller/__init__.py rename to laplas/abstract_map/pygame/__pyinstaller/__init__.py diff --git a/laplas/tools/abstract_map/pygame/__pyinstaller/hook-pygame.py b/laplas/abstract_map/pygame/__pyinstaller/hook-pygame.py similarity index 100% rename from laplas/tools/abstract_map/pygame/__pyinstaller/hook-pygame.py rename to laplas/abstract_map/pygame/__pyinstaller/hook-pygame.py diff --git a/laplas/tools/abstract_map/pygame/_camera_opencv.py b/laplas/abstract_map/pygame/_camera_opencv.py similarity index 100% rename from laplas/tools/abstract_map/pygame/_camera_opencv.py rename to laplas/abstract_map/pygame/_camera_opencv.py diff --git a/laplas/tools/abstract_map/pygame/_camera_vidcapture.py b/laplas/abstract_map/pygame/_camera_vidcapture.py similarity index 100% rename from laplas/tools/abstract_map/pygame/_camera_vidcapture.py rename to laplas/abstract_map/pygame/_camera_vidcapture.py diff --git a/laplas/tools/abstract_map/pygame/_common.pyi b/laplas/abstract_map/pygame/_common.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/_common.pyi rename to laplas/abstract_map/pygame/_common.pyi diff --git a/laplas/tools/abstract_map/pygame/_sdl2/__init__.py b/laplas/abstract_map/pygame/_sdl2/__init__.py similarity index 100% rename from laplas/tools/abstract_map/pygame/_sdl2/__init__.py rename to laplas/abstract_map/pygame/_sdl2/__init__.py diff --git a/laplas/tools/abstract_map/pygame/_sdl2/__init__.pyi b/laplas/abstract_map/pygame/_sdl2/__init__.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/_sdl2/__init__.pyi rename to laplas/abstract_map/pygame/_sdl2/__init__.pyi diff --git a/laplas/tools/abstract_map/pygame/_sdl2/audio.pyi b/laplas/abstract_map/pygame/_sdl2/audio.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/_sdl2/audio.pyi rename to laplas/abstract_map/pygame/_sdl2/audio.pyi diff --git a/laplas/tools/abstract_map/pygame/_sdl2/controller.pyi b/laplas/abstract_map/pygame/_sdl2/controller.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/_sdl2/controller.pyi rename to laplas/abstract_map/pygame/_sdl2/controller.pyi diff --git a/laplas/tools/abstract_map/pygame/_sdl2/sdl2.pyi b/laplas/abstract_map/pygame/_sdl2/sdl2.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/_sdl2/sdl2.pyi rename to laplas/abstract_map/pygame/_sdl2/sdl2.pyi diff --git a/laplas/tools/abstract_map/pygame/_sdl2/touch.pyi b/laplas/abstract_map/pygame/_sdl2/touch.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/_sdl2/touch.pyi rename to laplas/abstract_map/pygame/_sdl2/touch.pyi diff --git a/laplas/tools/abstract_map/pygame/_sdl2/video.pyi b/laplas/abstract_map/pygame/_sdl2/video.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/_sdl2/video.pyi rename to laplas/abstract_map/pygame/_sdl2/video.pyi diff --git a/laplas/tools/abstract_map/pygame/base.pyi b/laplas/abstract_map/pygame/base.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/base.pyi rename to laplas/abstract_map/pygame/base.pyi diff --git a/laplas/tools/abstract_map/pygame/bufferproxy.pyi b/laplas/abstract_map/pygame/bufferproxy.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/bufferproxy.pyi rename to laplas/abstract_map/pygame/bufferproxy.pyi diff --git a/laplas/tools/abstract_map/pygame/camera.py b/laplas/abstract_map/pygame/camera.py similarity index 100% rename from laplas/tools/abstract_map/pygame/camera.py rename to laplas/abstract_map/pygame/camera.py diff --git a/laplas/tools/abstract_map/pygame/camera.pyi b/laplas/abstract_map/pygame/camera.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/camera.pyi rename to laplas/abstract_map/pygame/camera.pyi diff --git a/laplas/tools/abstract_map/pygame/color.pyi b/laplas/abstract_map/pygame/color.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/color.pyi rename to laplas/abstract_map/pygame/color.pyi diff --git a/laplas/tools/abstract_map/pygame/colordict.py b/laplas/abstract_map/pygame/colordict.py similarity index 100% rename from laplas/tools/abstract_map/pygame/colordict.py rename to laplas/abstract_map/pygame/colordict.py diff --git a/laplas/tools/abstract_map/pygame/constants.pyi b/laplas/abstract_map/pygame/constants.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/constants.pyi rename to laplas/abstract_map/pygame/constants.pyi diff --git a/laplas/tools/abstract_map/pygame/cursors.py b/laplas/abstract_map/pygame/cursors.py similarity index 100% rename from laplas/tools/abstract_map/pygame/cursors.py rename to laplas/abstract_map/pygame/cursors.py diff --git a/laplas/tools/abstract_map/pygame/cursors.pyi b/laplas/abstract_map/pygame/cursors.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/cursors.pyi rename to laplas/abstract_map/pygame/cursors.pyi diff --git a/laplas/tools/abstract_map/pygame/display.pyi b/laplas/abstract_map/pygame/display.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/display.pyi rename to laplas/abstract_map/pygame/display.pyi diff --git a/laplas/tools/abstract_map/pygame/draw.pyi b/laplas/abstract_map/pygame/draw.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/draw.pyi rename to laplas/abstract_map/pygame/draw.pyi diff --git a/laplas/tools/abstract_map/pygame/draw_py.py b/laplas/abstract_map/pygame/draw_py.py similarity index 100% rename from laplas/tools/abstract_map/pygame/draw_py.py rename to laplas/abstract_map/pygame/draw_py.py diff --git a/laplas/tools/abstract_map/pygame/event.pyi b/laplas/abstract_map/pygame/event.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/event.pyi rename to laplas/abstract_map/pygame/event.pyi diff --git a/laplas/tools/abstract_map/pygame/fastevent.py b/laplas/abstract_map/pygame/fastevent.py similarity index 100% rename from laplas/tools/abstract_map/pygame/fastevent.py rename to laplas/abstract_map/pygame/fastevent.py diff --git a/laplas/tools/abstract_map/pygame/fastevent.pyi b/laplas/abstract_map/pygame/fastevent.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/fastevent.pyi rename to laplas/abstract_map/pygame/fastevent.pyi diff --git a/laplas/tools/abstract_map/pygame/font.pyi b/laplas/abstract_map/pygame/font.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/font.pyi rename to laplas/abstract_map/pygame/font.pyi diff --git a/laplas/tools/abstract_map/pygame/freesansbold.ttf b/laplas/abstract_map/pygame/freesansbold.ttf similarity index 100% rename from laplas/tools/abstract_map/pygame/freesansbold.ttf rename to laplas/abstract_map/pygame/freesansbold.ttf diff --git a/laplas/tools/abstract_map/pygame/freetype.dll b/laplas/abstract_map/pygame/freetype.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/freetype.dll rename to laplas/abstract_map/pygame/freetype.dll diff --git a/laplas/tools/abstract_map/pygame/freetype.py b/laplas/abstract_map/pygame/freetype.py similarity index 100% rename from laplas/tools/abstract_map/pygame/freetype.py rename to laplas/abstract_map/pygame/freetype.py diff --git a/laplas/tools/abstract_map/pygame/freetype.pyi b/laplas/abstract_map/pygame/freetype.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/freetype.pyi rename to laplas/abstract_map/pygame/freetype.pyi diff --git a/laplas/tools/abstract_map/pygame/ftfont.py b/laplas/abstract_map/pygame/ftfont.py similarity index 100% rename from laplas/tools/abstract_map/pygame/ftfont.py rename to laplas/abstract_map/pygame/ftfont.py diff --git a/laplas/tools/abstract_map/pygame/gfxdraw.pyi b/laplas/abstract_map/pygame/gfxdraw.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/gfxdraw.pyi rename to laplas/abstract_map/pygame/gfxdraw.pyi diff --git a/laplas/tools/abstract_map/pygame/image.pyi b/laplas/abstract_map/pygame/image.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/image.pyi rename to laplas/abstract_map/pygame/image.pyi diff --git a/laplas/tools/abstract_map/pygame/joystick.pyi b/laplas/abstract_map/pygame/joystick.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/joystick.pyi rename to laplas/abstract_map/pygame/joystick.pyi diff --git a/laplas/tools/abstract_map/pygame/key.pyi b/laplas/abstract_map/pygame/key.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/key.pyi rename to laplas/abstract_map/pygame/key.pyi diff --git a/laplas/tools/abstract_map/pygame/libjpeg-9.dll b/laplas/abstract_map/pygame/libjpeg-9.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/libjpeg-9.dll rename to laplas/abstract_map/pygame/libjpeg-9.dll diff --git a/laplas/tools/abstract_map/pygame/libmodplug-1.dll b/laplas/abstract_map/pygame/libmodplug-1.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/libmodplug-1.dll rename to laplas/abstract_map/pygame/libmodplug-1.dll diff --git a/laplas/tools/abstract_map/pygame/libogg-0.dll b/laplas/abstract_map/pygame/libogg-0.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/libogg-0.dll rename to laplas/abstract_map/pygame/libogg-0.dll diff --git a/laplas/tools/abstract_map/pygame/libopus-0.dll b/laplas/abstract_map/pygame/libopus-0.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/libopus-0.dll rename to laplas/abstract_map/pygame/libopus-0.dll diff --git a/laplas/tools/abstract_map/pygame/libopusfile-0.dll b/laplas/abstract_map/pygame/libopusfile-0.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/libopusfile-0.dll rename to laplas/abstract_map/pygame/libopusfile-0.dll diff --git a/laplas/tools/abstract_map/pygame/libpng16-16.dll b/laplas/abstract_map/pygame/libpng16-16.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/libpng16-16.dll rename to laplas/abstract_map/pygame/libpng16-16.dll diff --git a/laplas/tools/abstract_map/pygame/libtiff-5.dll b/laplas/abstract_map/pygame/libtiff-5.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/libtiff-5.dll rename to laplas/abstract_map/pygame/libtiff-5.dll diff --git a/laplas/tools/abstract_map/pygame/libwebp-7.dll b/laplas/abstract_map/pygame/libwebp-7.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/libwebp-7.dll rename to laplas/abstract_map/pygame/libwebp-7.dll diff --git a/laplas/tools/abstract_map/pygame/locals.py b/laplas/abstract_map/pygame/locals.py similarity index 100% rename from laplas/tools/abstract_map/pygame/locals.py rename to laplas/abstract_map/pygame/locals.py diff --git a/laplas/tools/abstract_map/pygame/locals.pyi b/laplas/abstract_map/pygame/locals.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/locals.pyi rename to laplas/abstract_map/pygame/locals.pyi diff --git a/laplas/tools/abstract_map/pygame/macosx.py b/laplas/abstract_map/pygame/macosx.py similarity index 100% rename from laplas/tools/abstract_map/pygame/macosx.py rename to laplas/abstract_map/pygame/macosx.py diff --git a/laplas/tools/abstract_map/pygame/mask.pyi b/laplas/abstract_map/pygame/mask.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/mask.pyi rename to laplas/abstract_map/pygame/mask.pyi diff --git a/laplas/tools/abstract_map/pygame/math.pyi b/laplas/abstract_map/pygame/math.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/math.pyi rename to laplas/abstract_map/pygame/math.pyi diff --git a/laplas/tools/abstract_map/pygame/midi.py b/laplas/abstract_map/pygame/midi.py similarity index 100% rename from laplas/tools/abstract_map/pygame/midi.py rename to laplas/abstract_map/pygame/midi.py diff --git a/laplas/tools/abstract_map/pygame/midi.pyi b/laplas/abstract_map/pygame/midi.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/midi.pyi rename to laplas/abstract_map/pygame/midi.pyi diff --git a/laplas/tools/abstract_map/pygame/mixer.pyi b/laplas/abstract_map/pygame/mixer.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/mixer.pyi rename to laplas/abstract_map/pygame/mixer.pyi diff --git a/laplas/tools/abstract_map/pygame/mixer_music.pyi b/laplas/abstract_map/pygame/mixer_music.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/mixer_music.pyi rename to laplas/abstract_map/pygame/mixer_music.pyi diff --git a/laplas/tools/abstract_map/pygame/mouse.pyi b/laplas/abstract_map/pygame/mouse.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/mouse.pyi rename to laplas/abstract_map/pygame/mouse.pyi diff --git a/laplas/tools/abstract_map/pygame/pixelarray.pyi b/laplas/abstract_map/pygame/pixelarray.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/pixelarray.pyi rename to laplas/abstract_map/pygame/pixelarray.pyi diff --git a/laplas/tools/abstract_map/pygame/pixelcopy.pyi b/laplas/abstract_map/pygame/pixelcopy.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/pixelcopy.pyi rename to laplas/abstract_map/pygame/pixelcopy.pyi diff --git a/laplas/tools/abstract_map/pygame/pkgdata.py b/laplas/abstract_map/pygame/pkgdata.py similarity index 100% rename from laplas/tools/abstract_map/pygame/pkgdata.py rename to laplas/abstract_map/pygame/pkgdata.py diff --git a/laplas/tools/abstract_map/pygame/portmidi.dll b/laplas/abstract_map/pygame/portmidi.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/portmidi.dll rename to laplas/abstract_map/pygame/portmidi.dll diff --git a/laplas/tools/abstract_map/pygame/py.typed b/laplas/abstract_map/pygame/py.typed similarity index 100% rename from laplas/tools/abstract_map/pygame/py.typed rename to laplas/abstract_map/pygame/py.typed diff --git a/laplas/tools/abstract_map/pygame/pygame.ico b/laplas/abstract_map/pygame/pygame.ico similarity index 100% rename from laplas/tools/abstract_map/pygame/pygame.ico rename to laplas/abstract_map/pygame/pygame.ico diff --git a/laplas/tools/abstract_map/pygame/pygame_icon.bmp b/laplas/abstract_map/pygame/pygame_icon.bmp similarity index 100% rename from laplas/tools/abstract_map/pygame/pygame_icon.bmp rename to laplas/abstract_map/pygame/pygame_icon.bmp diff --git a/laplas/tools/abstract_map/pygame/pygame_icon.icns b/laplas/abstract_map/pygame/pygame_icon.icns similarity index 100% rename from laplas/tools/abstract_map/pygame/pygame_icon.icns rename to laplas/abstract_map/pygame/pygame_icon.icns diff --git a/laplas/tools/abstract_map/pygame/pygame_icon_mac.bmp b/laplas/abstract_map/pygame/pygame_icon_mac.bmp similarity index 100% rename from laplas/tools/abstract_map/pygame/pygame_icon_mac.bmp rename to laplas/abstract_map/pygame/pygame_icon_mac.bmp diff --git a/laplas/tools/abstract_map/pygame/rect.pyi b/laplas/abstract_map/pygame/rect.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/rect.pyi rename to laplas/abstract_map/pygame/rect.pyi diff --git a/laplas/tools/abstract_map/pygame/rwobject.pyi b/laplas/abstract_map/pygame/rwobject.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/rwobject.pyi rename to laplas/abstract_map/pygame/rwobject.pyi diff --git a/laplas/tools/abstract_map/pygame/scrap.pyi b/laplas/abstract_map/pygame/scrap.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/scrap.pyi rename to laplas/abstract_map/pygame/scrap.pyi diff --git a/laplas/tools/abstract_map/pygame/sndarray.py b/laplas/abstract_map/pygame/sndarray.py similarity index 100% rename from laplas/tools/abstract_map/pygame/sndarray.py rename to laplas/abstract_map/pygame/sndarray.py diff --git a/laplas/tools/abstract_map/pygame/sndarray.pyi b/laplas/abstract_map/pygame/sndarray.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/sndarray.pyi rename to laplas/abstract_map/pygame/sndarray.pyi diff --git a/laplas/tools/abstract_map/pygame/sprite.py b/laplas/abstract_map/pygame/sprite.py similarity index 100% rename from laplas/tools/abstract_map/pygame/sprite.py rename to laplas/abstract_map/pygame/sprite.py diff --git a/laplas/tools/abstract_map/pygame/sprite.pyi b/laplas/abstract_map/pygame/sprite.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/sprite.pyi rename to laplas/abstract_map/pygame/sprite.pyi diff --git a/laplas/tools/abstract_map/pygame/surface.pyi b/laplas/abstract_map/pygame/surface.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/surface.pyi rename to laplas/abstract_map/pygame/surface.pyi diff --git a/laplas/tools/abstract_map/pygame/surfarray.py b/laplas/abstract_map/pygame/surfarray.py similarity index 100% rename from laplas/tools/abstract_map/pygame/surfarray.py rename to laplas/abstract_map/pygame/surfarray.py diff --git a/laplas/tools/abstract_map/pygame/surfarray.pyi b/laplas/abstract_map/pygame/surfarray.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/surfarray.pyi rename to laplas/abstract_map/pygame/surfarray.pyi diff --git a/laplas/tools/abstract_map/pygame/surflock.pyi b/laplas/abstract_map/pygame/surflock.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/surflock.pyi rename to laplas/abstract_map/pygame/surflock.pyi diff --git a/laplas/tools/abstract_map/pygame/sysfont.py b/laplas/abstract_map/pygame/sysfont.py similarity index 100% rename from laplas/tools/abstract_map/pygame/sysfont.py rename to laplas/abstract_map/pygame/sysfont.py diff --git a/laplas/abstract_map/pygame/tests/__init__.py b/laplas/abstract_map/pygame/tests/__init__.py new file mode 100644 index 0000000..dd26586 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/__init__.py @@ -0,0 +1,40 @@ +"""Pygame unit test suite package + +Exports function run() + +A quick way to run the test suite package from the command line +is by importing the go submodule: + +python -m "import pygame.tests" [] + +Command line option --help displays a usage message. Available options +correspond to the pygame.tests.run arguments. + +The xxxx_test submodules of the tests package are unit test suites for +individual parts of Pygame. Each can also be run as a main program. This is +useful if the test, such as cdrom_test, is interactive. + +For Pygame development the test suite can be run from a Pygame distribution +root directory using run_tests.py. Alternately, test/__main__.py can be run +directly. + +""" + +if __name__ == "pygame.tests": + from pygame.tests.test_utils.run_tests import run +elif __name__ == "__main__": + import os + import sys + + pkg_dir = os.path.split(os.path.abspath(__file__))[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) + + if is_pygame_pkg: + import pygame.tests.__main__ + else: + import test.__main__ +else: + from test.test_utils.run_tests import run diff --git a/laplas/abstract_map/pygame/tests/__main__.py b/laplas/abstract_map/pygame/tests/__main__.py new file mode 100644 index 0000000..9b4cedd --- /dev/null +++ b/laplas/abstract_map/pygame/tests/__main__.py @@ -0,0 +1,143 @@ +"""Load and run the Pygame test suite + +python -c "import pygame.tests.go" [] + +or + +python test/go.py [] + +Command line option --help displays a command line usage message. + +run_tests.py in the main distribution directory is an alternative to test.go + +""" + +import sys + +if __name__ == "__main__": + import os + + pkg_dir = os.path.split(os.path.abspath(__file__))[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +if is_pygame_pkg: + from pygame.tests.test_utils.run_tests import run_and_exit + from pygame.tests.test_utils.test_runner import opt_parser +else: + from test.test_utils.run_tests import run_and_exit + from test.test_utils.test_runner import opt_parser + +if is_pygame_pkg: + test_pkg_name = "pygame.tests" +else: + test_pkg_name = "test" +program_name = sys.argv[0] +if program_name == "-c": + program_name = f'python -c "import {test_pkg_name}.go"' + +########################################################################### +# Set additional command line options +# +# Defined in test_runner.py as it shares options, added to here + +opt_parser.set_usage( + f""" + +Runs all or some of the {test_pkg_name}.xxxx_test tests. + +$ {program_name} sprite threads -sd + +Runs the sprite and threads module tests isolated in subprocesses, dumping +all failing tests info in the form of a dict. + +""" +) + +opt_parser.add_option( + "-d", "--dump", action="store_true", help="dump results as dict ready to eval" +) + +opt_parser.add_option("-F", "--file", help="dump results to a file") + +opt_parser.add_option( + "-m", + "--multi_thread", + metavar="THREADS", + type="int", + help="run subprocessed tests in x THREADS", +) + +opt_parser.add_option( + "-t", + "--time_out", + metavar="SECONDS", + type="int", + help="kill stalled subprocessed tests after SECONDS", +) + +opt_parser.add_option( + "-f", "--fake", metavar="DIR", help="run fake tests in run_tests__tests/$DIR" +) + +opt_parser.add_option( + "-p", + "--python", + metavar="PYTHON", + help="path to python executable to run subproccesed tests\n" + "default (sys.executable): %s" % sys.executable, +) + +opt_parser.add_option( + "-I", + "--interactive", + action="store_true", + help="include tests requiring user input", +) + +opt_parser.add_option("-S", "--seed", type="int", help="Randomisation seed") + +########################################################################### +# Set run() keyword arguments according to command line arguments. +# args will be the test module list, passed as positional argumemts. + +options, args = opt_parser.parse_args() +kwds = {} +if options.incomplete: + kwds["incomplete"] = True +if options.usesubprocess: + kwds["usesubprocess"] = True +else: + kwds["usesubprocess"] = False +if options.dump: + kwds["dump"] = True +if options.file: + kwds["file"] = options.file +if options.exclude: + kwds["exclude"] = options.exclude +if options.unbuffered: + kwds["unbuffered"] = True +if options.randomize: + kwds["randomize"] = True +if options.seed is not None: + kwds["seed"] = options.seed +if options.multi_thread is not None: + kwds["multi_thread"] = options.multi_thread +if options.time_out is not None: + kwds["time_out"] = options.time_out +if options.fake: + kwds["fake"] = options.fake +if options.python: + kwds["python"] = options.python +if options.interactive: + kwds["interactive"] = True +kwds["verbosity"] = options.verbosity if options.verbosity is not None else 1 + + +########################################################################### +# Run the test suite. +run_and_exit(*args, **kwds) diff --git a/laplas/abstract_map/pygame/tests/base_test.py b/laplas/abstract_map/pygame/tests/base_test.py new file mode 100644 index 0000000..b11d2d6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/base_test.py @@ -0,0 +1,623 @@ +import sys +import unittest + +import platform + +IS_PYPY = "PyPy" == platform.python_implementation() + +try: + from pygame.tests.test_utils import arrinter +except NameError: + pass +import pygame + + +quit_count = 0 + + +def quit_hook(): + global quit_count + quit_count += 1 + + +class BaseModuleTest(unittest.TestCase): + def tearDown(self): + # Clean up after each test method. + pygame.quit() + + def test_get_sdl_byteorder(self): + """Ensure the SDL byte order is valid""" + byte_order = pygame.get_sdl_byteorder() + expected_options = (pygame.LIL_ENDIAN, pygame.BIG_ENDIAN) + + self.assertIn(byte_order, expected_options) + + def test_get_sdl_version(self): + """Ensure the SDL version is valid""" + self.assertEqual(len(pygame.get_sdl_version()), 3) + + class ExporterBase: + def __init__(self, shape, typechar, itemsize): + import ctypes + + ndim = len(shape) + self.ndim = ndim + self.shape = tuple(shape) + array_len = 1 + for d in shape: + array_len *= d + self.size = itemsize * array_len + self.parent = ctypes.create_string_buffer(self.size) + self.itemsize = itemsize + strides = [itemsize] * ndim + for i in range(ndim - 1, 0, -1): + strides[i - 1] = strides[i] * shape[i] + self.strides = tuple(strides) + self.data = ctypes.addressof(self.parent), False + if self.itemsize == 1: + byteorder = "|" + elif sys.byteorder == "big": + byteorder = ">" + else: + byteorder = "<" + self.typestr = byteorder + typechar + str(self.itemsize) + + def assertSame(self, proxy, obj): + self.assertEqual(proxy.length, obj.size) + iface = proxy.__array_interface__ + self.assertEqual(iface["typestr"], obj.typestr) + self.assertEqual(iface["shape"], obj.shape) + self.assertEqual(iface["strides"], obj.strides) + self.assertEqual(iface["data"], obj.data) + + def test_PgObject_GetBuffer_array_interface(self): + from pygame.bufferproxy import BufferProxy + + class Exporter(self.ExporterBase): + def get__array_interface__(self): + return { + "version": 3, + "typestr": self.typestr, + "shape": self.shape, + "strides": self.strides, + "data": self.data, + } + + __array_interface__ = property(get__array_interface__) + # Should be ignored by PgObject_GetBuffer + __array_struct__ = property(lambda self: None) + + _shape = [2, 3, 5, 7, 11] # Some prime numbers + for ndim in range(1, len(_shape)): + o = Exporter(_shape[0:ndim], "i", 2) + v = BufferProxy(o) + self.assertSame(v, o) + ndim = 2 + shape = _shape[0:ndim] + for typechar in ("i", "u"): + for itemsize in (1, 2, 4, 8): + o = Exporter(shape, typechar, itemsize) + v = BufferProxy(o) + self.assertSame(v, o) + for itemsize in (4, 8): + o = Exporter(shape, "f", itemsize) + v = BufferProxy(o) + self.assertSame(v, o) + + # Is the dict received from an exporting object properly released? + # The dict should be freed before PgObject_GetBuffer returns. + # When the BufferProxy v's length property is referenced, v calls + # PgObject_GetBuffer, which in turn references Exporter2 o's + # __array_interface__ property. The Exporter2 instance o returns a + # dict subclass for which it keeps both a regular reference and a + # weak reference. The regular reference should be the only + # remaining reference when PgObject_GetBuffer returns. This is + # verified by first checking the weak reference both before and + # after the regular reference held by o is removed. + + import weakref, gc + + class NoDictError(RuntimeError): + pass + + class WRDict(dict): + """Weak referenceable dict""" + + pass + + class Exporter2(Exporter): + def get__array_interface__2(self): + self.d = WRDict(Exporter.get__array_interface__(self)) + self.dict_ref = weakref.ref(self.d) + return self.d + + __array_interface__ = property(get__array_interface__2) + + def free_dict(self): + self.d = None + + def is_dict_alive(self): + try: + return self.dict_ref() is not None + except AttributeError: + raise NoDictError("__array_interface__ is unread") + + o = Exporter2((2, 4), "u", 4) + v = BufferProxy(o) + self.assertRaises(NoDictError, o.is_dict_alive) + length = v.length + self.assertTrue(o.is_dict_alive()) + o.free_dict() + gc.collect() + self.assertFalse(o.is_dict_alive()) + + def test_GetView_array_struct(self): + from pygame.bufferproxy import BufferProxy + + class Exporter(self.ExporterBase): + def __init__(self, shape, typechar, itemsize): + super().__init__(shape, typechar, itemsize) + self.view = BufferProxy(self.__dict__) + + def get__array_struct__(self): + return self.view.__array_struct__ + + __array_struct__ = property(get__array_struct__) + # Should not cause PgObject_GetBuffer to fail + __array_interface__ = property(lambda self: None) + + _shape = [2, 3, 5, 7, 11] # Some prime numbers + for ndim in range(1, len(_shape)): + o = Exporter(_shape[0:ndim], "i", 2) + v = BufferProxy(o) + self.assertSame(v, o) + ndim = 2 + shape = _shape[0:ndim] + for typechar in ("i", "u"): + for itemsize in (1, 2, 4, 8): + o = Exporter(shape, typechar, itemsize) + v = BufferProxy(o) + self.assertSame(v, o) + for itemsize in (4, 8): + o = Exporter(shape, "f", itemsize) + v = BufferProxy(o) + self.assertSame(v, o) + + # Check returned cobject/capsule reference count + try: + from sys import getrefcount + except ImportError: + # PyPy: no reference counting + pass + else: + o = Exporter(shape, typechar, itemsize) + self.assertEqual(getrefcount(o.__array_struct__), 1) + + if pygame.HAVE_NEWBUF: + from pygame.tests.test_utils import buftools + + def NEWBUF_assertSame(self, proxy, exp): + buftools = self.buftools + Importer = buftools.Importer + self.assertEqual(proxy.length, exp.len) + imp = Importer(proxy, buftools.PyBUF_RECORDS_RO) + self.assertEqual(imp.readonly, exp.readonly) + self.assertEqual(imp.format, exp.format) + self.assertEqual(imp.itemsize, exp.itemsize) + self.assertEqual(imp.ndim, exp.ndim) + self.assertEqual(imp.shape, exp.shape) + self.assertEqual(imp.strides, exp.strides) + self.assertTrue(imp.suboffsets is None) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + @unittest.skipIf(IS_PYPY, "pypy no likey") + def test_newbuf(self): + from pygame.bufferproxy import BufferProxy + + Exporter = self.buftools.Exporter + _shape = [2, 3, 5, 7, 11] # Some prime numbers + for ndim in range(1, len(_shape)): + o = Exporter(_shape[0:ndim], "=h") + v = BufferProxy(o) + self.NEWBUF_assertSame(v, o) + ndim = 2 + shape = _shape[0:ndim] + for format in [ + "b", + "B", + "=h", + "=H", + "=i", + "=I", + "=q", + "=Q", + "f", + "d", + "1h", + "=1h", + "x", + "1x", + "2x", + "3x", + "4x", + "5x", + "6x", + "7x", + "8x", + "9x", + ]: + o = Exporter(shape, format) + v = BufferProxy(o) + self.NEWBUF_assertSame(v, o) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def test_bad_format(self): + from pygame.bufferproxy import BufferProxy + from pygame.newbuffer import BufferMixin + from ctypes import create_string_buffer, addressof + + buftools = self.buftools + Exporter = buftools.Exporter + Importer = buftools.Importer + PyBUF_FORMAT = buftools.PyBUF_FORMAT + + for format in [ + "", + "=", + "1", + " ", + "2h", + "=2h", + "0x", + "11x", + "=!", + "h ", + " h", + "hh", + "?", + ]: + exp = Exporter((1,), format, itemsize=2) + b = BufferProxy(exp) + self.assertRaises(ValueError, Importer, b, PyBUF_FORMAT) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + @unittest.skipIf(IS_PYPY, "fails on pypy") + def test_PgDict_AsBuffer_PyBUF_flags(self): + from pygame.bufferproxy import BufferProxy + + is_lil_endian = pygame.get_sdl_byteorder() == pygame.LIL_ENDIAN + fsys, frev = ("<", ">") if is_lil_endian else (">", "<") + buftools = self.buftools + Importer = buftools.Importer + a = BufferProxy( + {"typestr": "|u4", "shape": (10, 2), "data": (9, False)} + ) # 9? No data accesses. + b = Importer(a, buftools.PyBUF_SIMPLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 4) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, 9) + b = Importer(a, buftools.PyBUF_WRITABLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 4) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, 9) + b = Importer(a, buftools.PyBUF_ND) + self.assertEqual(b.ndim, 2) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 4) + self.assertEqual(b.shape, (10, 2)) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, 9) + a = BufferProxy( + { + "typestr": fsys + "i2", + "shape": (5, 10), + "strides": (24, 2), + "data": (42, False), + } + ) # 42? No data accesses. + b = Importer(a, buftools.PyBUF_STRIDES) + self.assertEqual(b.ndim, 2) + self.assertTrue(b.format is None) + self.assertEqual(b.len, 100) + self.assertEqual(b.itemsize, 2) + self.assertEqual(b.shape, (5, 10)) + self.assertEqual(b.strides, (24, 2)) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, 42) + b = Importer(a, buftools.PyBUF_FULL_RO) + self.assertEqual(b.ndim, 2) + self.assertEqual(b.format, "=h") + self.assertEqual(b.len, 100) + self.assertEqual(b.itemsize, 2) + self.assertEqual(b.shape, (5, 10)) + self.assertEqual(b.strides, (24, 2)) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, 42) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_CONTIG) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_CONTIG) + a = BufferProxy( + { + "typestr": frev + "i2", + "shape": (3, 5, 10), + "strides": (120, 24, 2), + "data": (1000000, True), + } + ) # 1000000? No data accesses. + b = Importer(a, buftools.PyBUF_FULL_RO) + self.assertEqual(b.ndim, 3) + self.assertEqual(b.format, frev + "h") + self.assertEqual(b.len, 300) + self.assertEqual(b.itemsize, 2) + self.assertEqual(b.shape, (3, 5, 10)) + self.assertEqual(b.strides, (120, 24, 2)) + self.assertTrue(b.suboffsets is None) + self.assertTrue(b.readonly) + self.assertEqual(b.buf, 1000000) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_FULL) + + @unittest.skipIf(IS_PYPY or (not pygame.HAVE_NEWBUF), "newbuf with ctypes") + def test_PgObject_AsBuffer_PyBUF_flags(self): + from pygame.bufferproxy import BufferProxy + import ctypes + + is_lil_endian = pygame.get_sdl_byteorder() == pygame.LIL_ENDIAN + fsys, frev = ("<", ">") if is_lil_endian else (">", "<") + buftools = self.buftools + Importer = buftools.Importer + e = arrinter.Exporter( + (10, 2), typekind="f", itemsize=ctypes.sizeof(ctypes.c_double) + ) + a = BufferProxy(e) + b = Importer(a, buftools.PyBUF_SIMPLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, e.len) + self.assertEqual(b.itemsize, e.itemsize) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, e.data) + b = Importer(a, buftools.PyBUF_WRITABLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, e.len) + self.assertEqual(b.itemsize, e.itemsize) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, e.data) + b = Importer(a, buftools.PyBUF_ND) + self.assertEqual(b.ndim, e.nd) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, e.itemsize) + self.assertEqual(b.shape, e.shape) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, e.data) + e = arrinter.Exporter((5, 10), typekind="i", itemsize=2, strides=(24, 2)) + a = BufferProxy(e) + b = Importer(a, buftools.PyBUF_STRIDES) + self.assertEqual(b.ndim, e.nd) + self.assertTrue(b.format is None) + self.assertEqual(b.len, e.len) + self.assertEqual(b.itemsize, e.itemsize) + self.assertEqual(b.shape, e.shape) + self.assertEqual(b.strides, e.strides) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, e.data) + b = Importer(a, buftools.PyBUF_FULL_RO) + self.assertEqual(b.ndim, e.nd) + self.assertEqual(b.format, "=h") + self.assertEqual(b.len, e.len) + self.assertEqual(b.itemsize, e.itemsize) + self.assertEqual(b.shape, e.shape) + self.assertEqual(b.strides, e.strides) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, e.data) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_WRITABLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_WRITABLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_CONTIG) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_CONTIG) + e = arrinter.Exporter( + (3, 5, 10), + typekind="i", + itemsize=2, + strides=(120, 24, 2), + flags=arrinter.PAI_ALIGNED, + ) + a = BufferProxy(e) + b = Importer(a, buftools.PyBUF_FULL_RO) + self.assertEqual(b.ndim, e.nd) + self.assertEqual(b.format, frev + "h") + self.assertEqual(b.len, e.len) + self.assertEqual(b.itemsize, e.itemsize) + self.assertEqual(b.shape, e.shape) + self.assertEqual(b.strides, e.strides) + self.assertTrue(b.suboffsets is None) + self.assertTrue(b.readonly) + self.assertEqual(b.buf, e.data) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_FULL) + + def test_PgObject_GetBuffer_exception(self): + # For consistency with surfarray + from pygame.bufferproxy import BufferProxy + + bp = BufferProxy(1) + self.assertRaises(ValueError, getattr, bp, "length") + + def not_init_assertions(self): + self.assertFalse(pygame.get_init(), "pygame shouldn't be initialized") + self.assertFalse(pygame.display.get_init(), "display shouldn't be initialized") + + if "pygame.mixer" in sys.modules: + self.assertFalse(pygame.mixer.get_init(), "mixer shouldn't be initialized") + + if "pygame.font" in sys.modules: + self.assertFalse(pygame.font.get_init(), "init shouldn't be initialized") + + ## !!! TODO : Remove when scrap works for OS X + import platform + + if platform.system().startswith("Darwin"): + return + + try: + self.assertRaises(pygame.error, pygame.scrap.get) + except NotImplementedError: + # Scrap is optional. + pass + + # pygame.cdrom + # pygame.joystick + + def init_assertions(self): + self.assertTrue(pygame.get_init()) + self.assertTrue(pygame.display.get_init()) + + if "pygame.mixer" in sys.modules: + self.assertTrue(pygame.mixer.get_init()) + + if "pygame.font" in sys.modules: + self.assertTrue(pygame.font.get_init()) + + def test_quit__and_init(self): + # __doc__ (as of 2008-06-25) for pygame.base.quit: + + # pygame.quit(): return None + # uninitialize all pygame modules + + # Make sure everything is not init + self.not_init_assertions() + + # Initiate it + pygame.init() + + # Check + self.init_assertions() + + # Quit + pygame.quit() + + # All modules have quit + self.not_init_assertions() + + def test_register_quit(self): + """Ensure that a registered function is called on quit()""" + self.assertEqual(quit_count, 0) + + pygame.init() + pygame.register_quit(quit_hook) + pygame.quit() + + self.assertEqual(quit_count, 1) + + def test_get_error(self): + # __doc__ (as of 2008-08-02) for pygame.base.get_error: + + # pygame.get_error(): return errorstr + # get the current error message + # + # SDL maintains an internal error message. This message will usually + # be given to you when pygame.error is raised. You will rarely need to + # call this function. + # + + # The first error could be all sorts of nonsense or empty. + e = pygame.get_error() + pygame.set_error("hi") + self.assertEqual(pygame.get_error(), "hi") + pygame.set_error("") + self.assertEqual(pygame.get_error(), "") + + def test_set_error(self): + # The first error could be all sorts of nonsense or empty. + e = pygame.get_error() + pygame.set_error("hi") + self.assertEqual(pygame.get_error(), "hi") + pygame.set_error("") + self.assertEqual(pygame.get_error(), "") + + def test_unicode_error(self): + pygame.set_error("你好") + self.assertEqual("你好", pygame.get_error()) + + def test_init(self): + """Ensures init() works properly.""" + # Make sure nothing initialized. + self.not_init_assertions() + + # display and joystick must init, at minimum + expected_min_passes = 2 + + # All modules should pass. + expected_fails = 0 + + passes, fails = pygame.init() + + self.init_assertions() + self.assertGreaterEqual(passes, expected_min_passes) + self.assertEqual(fails, expected_fails) + + def test_get_init(self): + # Test if get_init() gets the init state. + self.assertFalse(pygame.get_init()) + + def test_get_init__after_init(self): + # Test if get_init() gets the init state after pygame.init() called. + pygame.init() + + self.assertTrue(pygame.get_init()) + + def test_get_init__after_quit(self): + # Test if get_init() gets the init state after pygame.quit() called. + pygame.init() + pygame.quit() + + self.assertFalse(pygame.get_init()) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/blit_test.py b/laplas/abstract_map/pygame/tests/blit_test.py new file mode 100644 index 0000000..dcd7d4c --- /dev/null +++ b/laplas/abstract_map/pygame/tests/blit_test.py @@ -0,0 +1,192 @@ +import unittest + +import pygame +from pygame.locals import * + +from time import time + + +class BlitTest(unittest.TestCase): + def test_SRCALPHA(self): + """SRCALPHA tests.""" + # blend(s, 0, d) = d + s = pygame.Surface((1, 1), SRCALPHA, 32) + s.fill((255, 255, 255, 0)) + + d = pygame.Surface((1, 1), SRCALPHA, 32) + d.fill((0, 0, 255, 255)) + + s.blit(d, (0, 0)) + self.assertEqual(s.get_at((0, 0)), d.get_at((0, 0))) + + # blend(s, 255, d) = s + s = pygame.Surface((1, 1), SRCALPHA, 32) + s.fill((123, 0, 0, 255)) + s1 = pygame.Surface((1, 1), SRCALPHA, 32) + s1.fill((123, 0, 0, 255)) + d = pygame.Surface((1, 1), SRCALPHA, 32) + d.fill((10, 0, 0, 0)) + s.blit(d, (0, 0)) + self.assertEqual(s.get_at((0, 0)), s1.get_at((0, 0))) + + # TODO: these should be true too. + # blend(0, sA, 0) = 0 + # blend(255, sA, 255) = 255 + # blend(s, sA, d) <= 255 + + def test_BLEND(self): + """BLEND_ tests.""" + + # test that it doesn't overflow, and that it is saturated. + s = pygame.Surface((1, 1), SRCALPHA, 32) + s.fill((255, 255, 255, 0)) + + d = pygame.Surface((1, 1), SRCALPHA, 32) + d.fill((0, 0, 255, 255)) + + s.blit(d, (0, 0), None, BLEND_ADD) + + # print("d %s" % (d.get_at((0,0)),)) + # print(s.get_at((0,0))) + # self.assertEqual(s.get_at((0,0))[2], 255 ) + # self.assertEqual(s.get_at((0,0))[3], 0 ) + + s.blit(d, (0, 0), None, BLEND_RGBA_ADD) + # print(s.get_at((0,0))) + self.assertEqual(s.get_at((0, 0))[3], 255) + + # test adding works. + s.fill((20, 255, 255, 0)) + d.fill((10, 0, 255, 255)) + s.blit(d, (0, 0), None, BLEND_ADD) + self.assertEqual(s.get_at((0, 0))[2], 255) + + # test subbing works. + s.fill((20, 255, 255, 0)) + d.fill((10, 0, 255, 255)) + s.blit(d, (0, 0), None, BLEND_SUB) + self.assertEqual(s.get_at((0, 0))[0], 10) + + # no overflow in sub blend. + s.fill((20, 255, 255, 0)) + d.fill((30, 0, 255, 255)) + s.blit(d, (0, 0), None, BLEND_SUB) + self.assertEqual(s.get_at((0, 0))[0], 0) + + +class BlitsTest(unittest.TestCase): + """Tests for pygame.Surface.blits""" + + def setUp(self): + self.NUM_SURFS = 255 + self.PRINT_TIMING = 0 + self.dst = pygame.Surface((self.NUM_SURFS * 10, 10), SRCALPHA, 32) + self.dst.fill((230, 230, 230)) + self.blit_list = self.make_blit_list(self.NUM_SURFS) + + def make_blit_list(self, num_surfs): + """Generate a list of tuples representing surfaces and destinations + for blitting""" + + blit_list = [] + for i in range(num_surfs): + dest = (i * 10, 0) + surf = pygame.Surface((10, 10), SRCALPHA, 32) + color = (i * 1, i * 1, i * 1) + surf.fill(color) + blit_list.append((surf, dest)) + return blit_list + + def custom_blits(self, blit_list): + """Custom blits method that manually iterates over the blit_list and blits + each surface onto the destination.""" + + for surface, dest in blit_list: + self.dst.blit(surface, dest) + + def test_custom_blits_performance(self): + """Checks time performance of the custom blits method""" + + t0 = time() + results = self.custom_blits(self.blit_list) + t1 = time() + if self.PRINT_TIMING: + print(f"python blits: {t1 - t0}") + + def test_blits_performance(self): + """Checks time performance of blits""" + + t0 = time() + results = self.dst.blits(self.blit_list) + t1 = time() + if self.PRINT_TIMING: + print(f"Surface.blits: {t1 - t0}") + + # Measure time performance of blits with doreturn=0 + t0 = time() + results = self.dst.blits(self.blit_list, doreturn=0) + t1 = time() + if self.PRINT_TIMING: + print(f"Surface.blits doreturn=0: {t1 - t0}") + + # Measure time performance of blits using a generator + t0 = time() + results = self.dst.blits(((surf, dest) for surf, dest in self.blit_list)) + t1 = time() + if self.PRINT_TIMING: + print(f"Surface.blits generator: {t1 - t0}") + + def test_blits_correctness(self): + """Checks the correctness of the colors on the destination + after blitting and tests that the length of the results list + matches the number of surfaces blitted.""" + + results = self.dst.blits(self.blit_list) + for i in range(self.NUM_SURFS): + color = (i * 1, i * 1, i * 1) + self.assertEqual(self.dst.get_at((i * 10, 0)), color) + self.assertEqual(self.dst.get_at(((i * 10) + 5, 5)), color) + + self.assertEqual(len(results), self.NUM_SURFS) + + def test_blits_doreturn(self): + """Tests that when doreturn=0, it returns None""" + + results = self.dst.blits(self.blit_list, doreturn=0) + self.assertEqual(results, None) + + def test_blits_not_sequence(self): + """Tests that calling blits with an invalid non-sequence None argument + raises a ValueError.""" + + dst = pygame.Surface((100, 10), SRCALPHA, 32) + with self.assertRaises(ValueError): + dst.blits(None) + + def test_blits_wrong_length(self): + """Tests that calling blits with an invalid sequence containing a single surface + (without a destination) raises a ValueError.""" + + dst = pygame.Surface((100, 10), SRCALPHA, 32) + with self.assertRaises(ValueError): + dst.blits([pygame.Surface((10, 10), SRCALPHA, 32)]) + + def test_blits_bad_surf_args(self): + """Tests that calling blits with a sequence containing an invalid tuple of + None arguments raises a TypeError.""" + + dst = pygame.Surface((100, 10), SRCALPHA, 32) + with self.assertRaises(TypeError): + dst.blits([(None, None)]) + + def test_blits_bad_dest(self): + """Tests that calling blits with a sequence containing an invalid tuple with a + destination of None raises a TypeError.""" + + dst = pygame.Surface((100, 10), SRCALPHA, 32) + with self.assertRaises(TypeError): + dst.blits([(pygame.Surface((10, 10), SRCALPHA, 32), None)]) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/bufferproxy_test.py b/laplas/abstract_map/pygame/tests/bufferproxy_test.py new file mode 100644 index 0000000..1282e35 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/bufferproxy_test.py @@ -0,0 +1,504 @@ +import re +import weakref +import gc +import ctypes +import unittest + +import pygame +from pygame.bufferproxy import BufferProxy + + +try: + BufferError +except NameError: + from pygame import BufferError + + +class BufferProxyTest(unittest.TestCase): + view_keywords = { + "shape": (5, 4, 3), + "typestr": "|u1", + "data": (0, True), + "strides": (4, 20, 1), + } + + def test_module_name(self): + self.assertEqual(pygame.bufferproxy.__name__, "pygame.bufferproxy") + + def test_class_name(self): + self.assertEqual(BufferProxy.__name__, "BufferProxy") + + def test___array_struct___property(self): + kwds = self.view_keywords + v = BufferProxy(kwds) + d = pygame.get_array_interface(v) + self.assertEqual(len(d), 5) + self.assertEqual(d["version"], 3) + self.assertEqual(d["shape"], kwds["shape"]) + self.assertEqual(d["typestr"], kwds["typestr"]) + self.assertEqual(d["data"], kwds["data"]) + self.assertEqual(d["strides"], kwds["strides"]) + + def test___array_interface___property(self): + kwds = self.view_keywords + v = BufferProxy(kwds) + d = v.__array_interface__ + self.assertEqual(len(d), 5) + self.assertEqual(d["version"], 3) + self.assertEqual(d["shape"], kwds["shape"]) + self.assertEqual(d["typestr"], kwds["typestr"]) + self.assertEqual(d["data"], kwds["data"]) + self.assertEqual(d["strides"], kwds["strides"]) + + def test_parent_property(self): + kwds = dict(self.view_keywords) + p = [] + kwds["parent"] = p + v = BufferProxy(kwds) + + self.assertIs(v.parent, p) + + def test_before(self): + def callback(parent): + success.append(parent is p) + + class MyException(Exception): + pass + + def raise_exception(parent): + raise MyException("Just a test.") + + kwds = dict(self.view_keywords) + p = [] + kwds["parent"] = p + + # For array interface + success = [] + kwds["before"] = callback + v = BufferProxy(kwds) + self.assertEqual(len(success), 0) + d = v.__array_interface__ + self.assertEqual(len(success), 1) + self.assertTrue(success[0]) + d = v.__array_interface__ + self.assertEqual(len(success), 1) + d = v = None + gc.collect() + self.assertEqual(len(success), 1) + + # For array struct + success = [] + kwds["before"] = callback + v = BufferProxy(kwds) + self.assertEqual(len(success), 0) + c = v.__array_struct__ + self.assertEqual(len(success), 1) + self.assertTrue(success[0]) + c = v.__array_struct__ + self.assertEqual(len(success), 1) + c = v = None + gc.collect() + self.assertEqual(len(success), 1) + + # Callback raises an exception + kwds["before"] = raise_exception + v = BufferProxy(kwds) + self.assertRaises(MyException, lambda: v.__array_struct__) + + def test_after(self): + def callback(parent): + success.append(parent is p) + + kwds = dict(self.view_keywords) + p = [] + kwds["parent"] = p + + # For array interface + success = [] + kwds["after"] = callback + v = BufferProxy(kwds) + self.assertEqual(len(success), 0) + d = v.__array_interface__ + self.assertEqual(len(success), 0) + d = v.__array_interface__ + self.assertEqual(len(success), 0) + d = v = None + gc.collect() + self.assertEqual(len(success), 1) + self.assertTrue(success[0]) + + # For array struct + success = [] + kwds["after"] = callback + v = BufferProxy(kwds) + self.assertEqual(len(success), 0) + c = v.__array_struct__ + self.assertEqual(len(success), 0) + c = v.__array_struct__ + self.assertEqual(len(success), 0) + c = v = None + gc.collect() + self.assertEqual(len(success), 1) + self.assertTrue(success[0]) + + def test_attribute(self): + v = BufferProxy(self.view_keywords) + self.assertRaises(AttributeError, getattr, v, "undefined") + v.undefined = 12 + self.assertEqual(v.undefined, 12) + del v.undefined + self.assertRaises(AttributeError, getattr, v, "undefined") + + def test_weakref(self): + v = BufferProxy(self.view_keywords) + weak_v = weakref.ref(v) + + self.assertIs(weak_v(), v) + + v = None + gc.collect() + + self.assertIsNone(weak_v()) + + def test_gc(self): + """refcount agnostic check that contained objects are freed""" + + def before_callback(parent): + return r[0] + + def after_callback(parent): + return r[1] + + class Obj: + pass + + p = Obj() + a = Obj() + r = [Obj(), Obj()] + weak_p = weakref.ref(p) + weak_a = weakref.ref(a) + weak_r0 = weakref.ref(r[0]) + weak_r1 = weakref.ref(r[1]) + weak_before = weakref.ref(before_callback) + weak_after = weakref.ref(after_callback) + kwds = dict(self.view_keywords) + kwds["parent"] = p + kwds["before"] = before_callback + kwds["after"] = after_callback + v = BufferProxy(kwds) + v.some_attribute = a + weak_v = weakref.ref(v) + kwds = p = a = before_callback = after_callback = None + gc.collect() + self.assertTrue(weak_p() is not None) + self.assertTrue(weak_a() is not None) + self.assertTrue(weak_before() is not None) + self.assertTrue(weak_after() is not None) + v = None + [gc.collect() for x in range(4)] + self.assertTrue(weak_v() is None) + self.assertTrue(weak_p() is None) + self.assertTrue(weak_a() is None) + self.assertTrue(weak_before() is None) + self.assertTrue(weak_after() is None) + self.assertTrue(weak_r0() is not None) + self.assertTrue(weak_r1() is not None) + r = None + gc.collect() + self.assertTrue(weak_r0() is None) + self.assertTrue(weak_r1() is None) + + # Cycle removal + kwds = dict(self.view_keywords) + kwds["parent"] = [] + v = BufferProxy(kwds) + v.some_attribute = v + tracked = True + for o in gc.get_objects(): + if o is v: + break + else: + tracked = False + self.assertTrue(tracked) + kwds["parent"].append(v) + kwds = None + gc.collect() + n1 = len(gc.garbage) + v = None + gc.collect() + n2 = len(gc.garbage) + self.assertEqual(n2, n1) + + def test_c_api(self): + api = pygame.bufferproxy._PYGAME_C_API + api_type = type(pygame.base._PYGAME_C_API) + + self.assertIsInstance(api, api_type) + + def test_repr(self): + v = BufferProxy(self.view_keywords) + cname = BufferProxy.__name__ + oname, ovalue = re.findall(r"<([^)]+)\(([^)]+)\)>", repr(v))[0] + self.assertEqual(oname, cname) + self.assertEqual(v.length, int(ovalue)) + + def test_subclassing(self): + class MyBufferProxy(BufferProxy): + def __repr__(self): + return f"*{BufferProxy.__repr__(self)}*" + + kwds = dict(self.view_keywords) + kwds["parent"] = 0 + v = MyBufferProxy(kwds) + self.assertEqual(v.parent, 0) + r = repr(v) + self.assertEqual(r[:2], "*<") + self.assertEqual(r[-2:], ">*") + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def NEWBUF_test_newbuf(self): + from ctypes import string_at + + from pygame.tests.test_utils import buftools + + Exporter = buftools.Exporter + Importer = buftools.Importer + exp = Exporter((10,), "B", readonly=True) + b = BufferProxy(exp) + self.assertEqual(b.length, exp.len) + self.assertEqual(b.raw, string_at(exp.buf, exp.len)) + d = b.__array_interface__ + try: + self.assertEqual(d["typestr"], "|u1") + self.assertEqual(d["shape"], exp.shape) + self.assertEqual(d["strides"], exp.strides) + self.assertEqual(d["data"], (exp.buf, True)) + finally: + d = None + exp = Exporter((3,), "=h") + b = BufferProxy(exp) + self.assertEqual(b.length, exp.len) + self.assertEqual(b.raw, string_at(exp.buf, exp.len)) + d = b.__array_interface__ + try: + lil_endian = pygame.get_sdl_byteorder() == pygame.LIL_ENDIAN + f = f"{'<' if lil_endian else '>'}i{exp.itemsize}" + self.assertEqual(d["typestr"], f) + self.assertEqual(d["shape"], exp.shape) + self.assertEqual(d["strides"], exp.strides) + self.assertEqual(d["data"], (exp.buf, False)) + finally: + d = None + + exp = Exporter((10, 2), "=i") + b = BufferProxy(exp) + imp = Importer(b, buftools.PyBUF_RECORDS) + self.assertTrue(imp.obj is b) + self.assertEqual(imp.buf, exp.buf) + self.assertEqual(imp.ndim, exp.ndim) + self.assertEqual(imp.format, exp.format) + self.assertEqual(imp.readonly, exp.readonly) + self.assertEqual(imp.itemsize, exp.itemsize) + self.assertEqual(imp.len, exp.len) + self.assertEqual(imp.shape, exp.shape) + self.assertEqual(imp.strides, exp.strides) + self.assertTrue(imp.suboffsets is None) + + d = { + "typestr": "|u1", + "shape": (10,), + "strides": (1,), + "data": (9, True), + } # 9? Will not reading the data anyway. + b = BufferProxy(d) + imp = Importer(b, buftools.PyBUF_SIMPLE) + self.assertTrue(imp.obj is b) + self.assertEqual(imp.buf, 9) + self.assertEqual(imp.len, 10) + self.assertEqual(imp.format, None) + self.assertEqual(imp.itemsize, 1) + self.assertEqual(imp.ndim, 0) + self.assertTrue(imp.readonly) + self.assertTrue(imp.shape is None) + self.assertTrue(imp.strides is None) + self.assertTrue(imp.suboffsets is None) + + try: + pygame.bufferproxy.get_segcount + except AttributeError: + pass + else: + + def test_oldbuf_arg(self): + self.OLDBUF_test_oldbuf_arg() + + def OLDBUF_test_oldbuf_arg(self): + from pygame.bufferproxy import get_segcount, get_read_buffer, get_write_buffer + + content = b"\x01\x00\x00\x02" * 12 + memory = ctypes.create_string_buffer(content) + memaddr = ctypes.addressof(memory) + + def raise_exception(o): + raise ValueError("An exception") + + bf = BufferProxy( + { + "shape": (len(content),), + "typestr": "|u1", + "data": (memaddr, False), + "strides": (1,), + } + ) + seglen, segaddr = get_read_buffer(bf, 0) + self.assertEqual(segaddr, 0) + self.assertEqual(seglen, 0) + seglen, segaddr = get_write_buffer(bf, 0) + self.assertEqual(segaddr, 0) + self.assertEqual(seglen, 0) + segcount, buflen = get_segcount(bf) + self.assertEqual(segcount, 1) + self.assertEqual(buflen, len(content)) + seglen, segaddr = get_read_buffer(bf, 0) + self.assertEqual(segaddr, memaddr) + self.assertEqual(seglen, len(content)) + seglen, segaddr = get_write_buffer(bf, 0) + self.assertEqual(segaddr, memaddr) + self.assertEqual(seglen, len(content)) + + bf = BufferProxy( + { + "shape": (len(content),), + "typestr": "|u1", + "data": (memaddr, True), + "strides": (1,), + } + ) + segcount, buflen = get_segcount(bf) + self.assertEqual(segcount, 1) + self.assertEqual(buflen, len(content)) + seglen, segaddr = get_read_buffer(bf, 0) + self.assertEqual(segaddr, memaddr) + self.assertEqual(seglen, len(content)) + self.assertRaises(ValueError, get_write_buffer, bf, 0) + + bf = BufferProxy( + { + "shape": (len(content),), + "typestr": "|u1", + "data": (memaddr, True), + "strides": (1,), + "before": raise_exception, + } + ) + segcount, buflen = get_segcount(bf) + self.assertEqual(segcount, 0) + self.assertEqual(buflen, 0) + + bf = BufferProxy( + { + "shape": (3, 4), + "typestr": "|u4", + "data": (memaddr, True), + "strides": (12, 4), + } + ) + segcount, buflen = get_segcount(bf) + self.assertEqual(segcount, 3 * 4) + self.assertEqual(buflen, 3 * 4 * 4) + for i in range(0, 4): + seglen, segaddr = get_read_buffer(bf, i) + self.assertEqual(segaddr, memaddr + i * 4) + self.assertEqual(seglen, 4) + + +class BufferProxyLegacyTest(unittest.TestCase): + content = b"\x01\x00\x00\x02" * 12 + buffer = ctypes.create_string_buffer(content) + data = (ctypes.addressof(buffer), True) + + def test_length(self): + # __doc__ (as of 2008-08-02) for pygame.bufferproxy.BufferProxy.length: + + # The size of the buffer data in bytes. + bf = BufferProxy( + {"shape": (3, 4), "typestr": "|u4", "data": self.data, "strides": (12, 4)} + ) + self.assertEqual(bf.length, len(self.content)) + bf = BufferProxy( + {"shape": (3, 3), "typestr": "|u4", "data": self.data, "strides": (12, 4)} + ) + self.assertEqual(bf.length, 3 * 3 * 4) + + def test_raw(self): + # __doc__ (as of 2008-08-02) for pygame.bufferproxy.BufferProxy.raw: + + # The raw buffer data as string. The string may contain NUL bytes. + + bf = BufferProxy( + {"shape": (len(self.content),), "typestr": "|u1", "data": self.data} + ) + self.assertEqual(bf.raw, self.content) + bf = BufferProxy( + {"shape": (3, 4), "typestr": "|u4", "data": self.data, "strides": (4, 12)} + ) + self.assertEqual(bf.raw, self.content) + bf = BufferProxy( + {"shape": (3, 4), "typestr": "|u1", "data": self.data, "strides": (16, 4)} + ) + self.assertRaises(ValueError, getattr, bf, "raw") + + def test_write(self): + # __doc__ (as of 2008-08-02) for pygame.bufferproxy.BufferProxy.write: + + # B.write (bufferproxy, buffer, offset) -> None + # + # Writes raw data to the bufferproxy. + # + # Writes the raw data from buffer to the BufferProxy object, starting + # at the specified offset within the BufferProxy. + # If the length of the passed buffer exceeds the length of the + # BufferProxy (reduced by the offset), an IndexError will be raised. + from ctypes import c_byte, sizeof, addressof, string_at, memset + + nullbyte = b"\x00" + Buf = c_byte * 10 + data_buf = Buf(*range(1, 3 * sizeof(Buf) + 1, 3)) + data = string_at(data_buf, sizeof(data_buf)) + buf = Buf() + bp = BufferProxy( + {"typestr": "|u1", "shape": (sizeof(buf),), "data": (addressof(buf), False)} + ) + try: + self.assertEqual(bp.raw, nullbyte * sizeof(Buf)) + bp.write(data) + self.assertEqual(bp.raw, data) + memset(buf, 0, sizeof(buf)) + bp.write(data[:3], 2) + raw = bp.raw + self.assertEqual(raw[:2], nullbyte * 2) + self.assertEqual(raw[2:5], data[:3]) + self.assertEqual(raw[5:], nullbyte * (sizeof(Buf) - 5)) + bp.write(data[:3], bp.length - 3) + raw = bp.raw + self.assertEqual(raw[-3:], data[:3]) + self.assertRaises(IndexError, bp.write, data, 1) + self.assertRaises(IndexError, bp.write, data[:5], -1) + self.assertRaises(IndexError, bp.write, data[:5], bp.length) + self.assertRaises(TypeError, bp.write, 12) + bp = BufferProxy( + { + "typestr": "|u1", + "shape": (sizeof(buf),), + "data": (addressof(buf), True), + } + ) + self.assertRaises(pygame.BufferError, bp.write, b"123") + finally: + # Make sure bp is garbage collected before buf + bp = None + gc.collect() + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/camera_test.py b/laplas/abstract_map/pygame/tests/camera_test.py new file mode 100644 index 0000000..c15d4f0 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/camera_test.py @@ -0,0 +1,35 @@ +import unittest +import os +import pygame +import pygame.camera + + +class CameraModuleTest(unittest.TestCase): + def setUp(self): + pygame.init() + + pygame.camera.init() + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") in ["dummy", "android"], + "requires the SDL_VIDEODRIVER to be non dummy", + ) + def test_camera(self): + cameras = pygame.camera.list_cameras() + + if len(cameras) == 0: + self.skipTest("No cameras found") + + cam = pygame.camera.Camera(cameras[0], (640, 480)) + cam.start() + image = cam.get_image() + self.assertIsNotNone(image, "Could not capture image") + cam.stop() + + def tearDown(self): + pygame.camera.quit() + pygame.quit() + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/color_test.py b/laplas/abstract_map/pygame/tests/color_test.py new file mode 100644 index 0000000..eee5c11 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/color_test.py @@ -0,0 +1,1360 @@ +import math +import operator +import platform +import unittest +from collections.abc import Collection, Sequence + +import pygame +from pygame.colordict import THECOLORS + +IS_PYPY = "PyPy" == platform.python_implementation() +################################### CONSTANTS ################################## + +rgba_vals = [0, 1, 62, 63, 126, 127, 255] + +rgba_combinations = [ + (r, g, b, a) + for r in rgba_vals + for g in rgba_vals + for b in rgba_vals + for a in rgba_vals +] + +################################################################################ + + +def rgba_combos_Color_generator(): + for rgba in rgba_combinations: + yield pygame.Color(*rgba) + + +# Python gamma correct +def gamma_correct(rgba_0_255, gamma): + corrected = round(255.0 * math.pow(rgba_0_255 / 255.0, gamma)) + return max(min(int(corrected), 255), 0) + + +################################################################################ + +# TODO: add tests for +# correct_gamma() -- test against statically defined verified correct values +# coerce () -- ?? + + +def _assignr(x, y): + x.r = y + + +def _assigng(x, y): + x.g = y + + +def _assignb(x, y): + x.b = y + + +def _assigna(x, y): + x.a = y + + +def _assign_item(x, p, y): + x[p] = y + + +class ColorTypeTest(unittest.TestCase): + def test_new(self): + c = pygame.Color.__new__(pygame.Color) + self.assertEqual(c, pygame.Color(0, 0, 0, 255)) + self.assertEqual(len(c), 4) + + def test_init(self): + c = pygame.Color(10, 20, 30, 200) + self.assertEqual(c, (10, 20, 30, 200)) + c.set_length(3) + self.assertEqual(len(c), 3) + c.__init__(100, 110, 120, 128) + self.assertEqual(len(c), 4) + self.assertEqual(c, (100, 110, 120, 128)) + + def test_invalid_html_hex_codes(self): + # This was a problem with the way 2 digit hex numbers were + # calculated. The test_hex_digits test is related to the fix. + Color = pygame.color.Color + self.assertRaises(ValueError, lambda: Color("# f000000")) + self.assertRaises(ValueError, lambda: Color("#f 000000")) + self.assertRaises(ValueError, lambda: Color("#-f000000")) + + def test_hex_digits(self): + # This is an implementation specific test. + # Two digit hex numbers are calculated using table lookups + # for the upper and lower digits. + Color = pygame.color.Color + self.assertEqual(Color("#00000000").r, 0x00) + self.assertEqual(Color("#10000000").r, 0x10) + self.assertEqual(Color("#20000000").r, 0x20) + self.assertEqual(Color("#30000000").r, 0x30) + self.assertEqual(Color("#40000000").r, 0x40) + self.assertEqual(Color("#50000000").r, 0x50) + self.assertEqual(Color("#60000000").r, 0x60) + self.assertEqual(Color("#70000000").r, 0x70) + self.assertEqual(Color("#80000000").r, 0x80) + self.assertEqual(Color("#90000000").r, 0x90) + self.assertEqual(Color("#A0000000").r, 0xA0) + self.assertEqual(Color("#B0000000").r, 0xB0) + self.assertEqual(Color("#C0000000").r, 0xC0) + self.assertEqual(Color("#D0000000").r, 0xD0) + self.assertEqual(Color("#E0000000").r, 0xE0) + self.assertEqual(Color("#F0000000").r, 0xF0) + self.assertEqual(Color("#01000000").r, 0x01) + self.assertEqual(Color("#02000000").r, 0x02) + self.assertEqual(Color("#03000000").r, 0x03) + self.assertEqual(Color("#04000000").r, 0x04) + self.assertEqual(Color("#05000000").r, 0x05) + self.assertEqual(Color("#06000000").r, 0x06) + self.assertEqual(Color("#07000000").r, 0x07) + self.assertEqual(Color("#08000000").r, 0x08) + self.assertEqual(Color("#09000000").r, 0x09) + self.assertEqual(Color("#0A000000").r, 0x0A) + self.assertEqual(Color("#0B000000").r, 0x0B) + self.assertEqual(Color("#0C000000").r, 0x0C) + self.assertEqual(Color("#0D000000").r, 0x0D) + self.assertEqual(Color("#0E000000").r, 0x0E) + self.assertEqual(Color("#0F000000").r, 0x0F) + + def test_comparison(self): + Color = pygame.color.Color + + # Check valid comparisons + self.assertTrue(Color(255, 0, 0, 0) == Color(255, 0, 0, 0)) + self.assertTrue(Color(0, 255, 0, 0) == Color(0, 255, 0, 0)) + self.assertTrue(Color(0, 0, 255, 0) == Color(0, 0, 255, 0)) + self.assertTrue(Color(0, 0, 0, 255) == Color(0, 0, 0, 255)) + self.assertFalse(Color(0, 0, 0, 0) == Color(255, 0, 0, 0)) + self.assertFalse(Color(0, 0, 0, 0) == Color(0, 255, 0, 0)) + self.assertFalse(Color(0, 0, 0, 0) == Color(0, 0, 255, 0)) + self.assertFalse(Color(0, 0, 0, 0) == Color(0, 0, 0, 255)) + self.assertTrue(Color(0, 0, 0, 0) != Color(255, 0, 0, 0)) + self.assertTrue(Color(0, 0, 0, 0) != Color(0, 255, 0, 0)) + self.assertTrue(Color(0, 0, 0, 0) != Color(0, 0, 255, 0)) + self.assertTrue(Color(0, 0, 0, 0) != Color(0, 0, 0, 255)) + self.assertFalse(Color(255, 0, 0, 0) != Color(255, 0, 0, 0)) + self.assertFalse(Color(0, 255, 0, 0) != Color(0, 255, 0, 0)) + self.assertFalse(Color(0, 0, 255, 0) != Color(0, 0, 255, 0)) + self.assertFalse(Color(0, 0, 0, 255) != Color(0, 0, 0, 255)) + + self.assertTrue(Color(255, 0, 0, 0) == (255, 0, 0, 0)) + self.assertTrue(Color(0, 255, 0, 0) == (0, 255, 0, 0)) + self.assertTrue(Color(0, 0, 255, 0) == (0, 0, 255, 0)) + self.assertTrue(Color(0, 0, 0, 255) == (0, 0, 0, 255)) + self.assertFalse(Color(0, 0, 0, 0) == (255, 0, 0, 0)) + self.assertFalse(Color(0, 0, 0, 0) == (0, 255, 0, 0)) + self.assertFalse(Color(0, 0, 0, 0) == (0, 0, 255, 0)) + self.assertFalse(Color(0, 0, 0, 0) == (0, 0, 0, 255)) + self.assertTrue(Color(0, 0, 0, 0) != (255, 0, 0, 0)) + self.assertTrue(Color(0, 0, 0, 0) != (0, 255, 0, 0)) + self.assertTrue(Color(0, 0, 0, 0) != (0, 0, 255, 0)) + self.assertTrue(Color(0, 0, 0, 0) != (0, 0, 0, 255)) + self.assertFalse(Color(255, 0, 0, 0) != (255, 0, 0, 0)) + self.assertFalse(Color(0, 255, 0, 0) != (0, 255, 0, 0)) + self.assertFalse(Color(0, 0, 255, 0) != (0, 0, 255, 0)) + self.assertFalse(Color(0, 0, 0, 255) != (0, 0, 0, 255)) + + self.assertTrue((255, 0, 0, 0) == Color(255, 0, 0, 0)) + self.assertTrue((0, 255, 0, 0) == Color(0, 255, 0, 0)) + self.assertTrue((0, 0, 255, 0) == Color(0, 0, 255, 0)) + self.assertTrue((0, 0, 0, 255) == Color(0, 0, 0, 255)) + self.assertFalse((0, 0, 0, 0) == Color(255, 0, 0, 0)) + self.assertFalse((0, 0, 0, 0) == Color(0, 255, 0, 0)) + self.assertFalse((0, 0, 0, 0) == Color(0, 0, 255, 0)) + self.assertFalse((0, 0, 0, 0) == Color(0, 0, 0, 255)) + self.assertTrue((0, 0, 0, 0) != Color(255, 0, 0, 0)) + self.assertTrue((0, 0, 0, 0) != Color(0, 255, 0, 0)) + self.assertTrue((0, 0, 0, 0) != Color(0, 0, 255, 0)) + self.assertTrue((0, 0, 0, 0) != Color(0, 0, 0, 255)) + self.assertFalse((255, 0, 0, 0) != Color(255, 0, 0, 0)) + self.assertFalse((0, 255, 0, 0) != Color(0, 255, 0, 0)) + self.assertFalse((0, 0, 255, 0) != Color(0, 0, 255, 0)) + self.assertFalse((0, 0, 0, 255) != Color(0, 0, 0, 255)) + + class TupleSubclass(tuple): + pass + + self.assertTrue(Color(255, 0, 0, 0) == TupleSubclass((255, 0, 0, 0))) + self.assertTrue(TupleSubclass((255, 0, 0, 0)) == Color(255, 0, 0, 0)) + self.assertFalse(Color(255, 0, 0, 0) != TupleSubclass((255, 0, 0, 0))) + self.assertFalse(TupleSubclass((255, 0, 0, 0)) != Color(255, 0, 0, 0)) + + # These are not supported so will be unequal. + self.assertFalse(Color(255, 0, 0, 0) == "#ff000000") + self.assertTrue(Color(255, 0, 0, 0) != "#ff000000") + + self.assertFalse("#ff000000" == Color(255, 0, 0, 0)) + self.assertTrue("#ff000000" != Color(255, 0, 0, 0)) + + self.assertFalse(Color(255, 0, 0, 0) == 0xFF000000) + self.assertTrue(Color(255, 0, 0, 0) != 0xFF000000) + + self.assertFalse(0xFF000000 == Color(255, 0, 0, 0)) + self.assertTrue(0xFF000000 != Color(255, 0, 0, 0)) + + self.assertFalse(Color(255, 0, 0, 0) == [255, 0, 0, 0]) + self.assertTrue(Color(255, 0, 0, 0) != [255, 0, 0, 0]) + + self.assertFalse([255, 0, 0, 0] == Color(255, 0, 0, 0)) + self.assertTrue([255, 0, 0, 0] != Color(255, 0, 0, 0)) + + # Comparison is not implemented for invalid color values. + class Test: + def __eq__(self, other): + return -1 + + def __ne__(self, other): + return -2 + + class TestTuple(tuple): + def __eq__(self, other): + return -1 + + def __ne__(self, other): + return -2 + + t = Test() + t_tuple = TestTuple(("a", 0, 0, 0)) + black = Color("black") + self.assertEqual(black == t, -1) + self.assertEqual(t == black, -1) + self.assertEqual(black != t, -2) + self.assertEqual(t != black, -2) + self.assertEqual(black == t_tuple, -1) + self.assertEqual(black != t_tuple, -2) + self.assertEqual(t_tuple == black, -1) + self.assertEqual(t_tuple != black, -2) + + def test_ignore_whitespace(self): + self.assertEqual(pygame.color.Color("red"), pygame.color.Color(" r e d ")) + + def test_slice(self): + # """|tags: python3_ignore|""" + + # slicing a color gives you back a tuple. + # do all sorts of slice combinations. + c = pygame.Color(1, 2, 3, 4) + + self.assertEqual((1, 2, 3, 4), c[:]) + self.assertEqual((1, 2, 3), c[:-1]) + + self.assertEqual((), c[:-5]) + + self.assertEqual((1, 2, 3, 4), c[:4]) + self.assertEqual((1, 2, 3, 4), c[:5]) + self.assertEqual((1, 2), c[:2]) + self.assertEqual((1,), c[:1]) + self.assertEqual((), c[:0]) + + self.assertEqual((2,), c[1:-2]) + self.assertEqual((3, 4), c[-2:]) + self.assertEqual((4,), c[-1:]) + + # NOTE: assigning to a slice is currently unsupported. + + def test_unpack(self): + # should be able to unpack to r,g,b,a and r,g,b + c = pygame.Color(1, 2, 3, 4) + r, g, b, a = c + self.assertEqual((1, 2, 3, 4), (r, g, b, a)) + self.assertEqual(c, (r, g, b, a)) + + c.set_length(3) + r, g, b = c + self.assertEqual((1, 2, 3), (r, g, b)) + + # Checking if DeprecationWarning is triggered + # when function is called + for i in range(1, 5): + with self.assertWarns(DeprecationWarning): + c.set_length(i) + + def test_length(self): + # should be able to unpack to r,g,b,a and r,g,b + c = pygame.Color(1, 2, 3, 4) + self.assertEqual(len(c), 4) + + c.set_length(3) + self.assertEqual(len(c), 3) + + # it keeps the old alpha anyway... + self.assertEqual(c.a, 4) + + # however you can't get the alpha in this way: + self.assertRaises(IndexError, lambda x: c[x], 4) + + c.set_length(4) + self.assertEqual(len(c), 4) + self.assertEqual(len(c), 4) + + self.assertRaises(ValueError, c.set_length, 5) + self.assertRaises(ValueError, c.set_length, -1) + self.assertRaises(ValueError, c.set_length, 0) + self.assertRaises(ValueError, c.set_length, pow(2, 33)) + + def test_case_insensitivity_of_string_args(self): + self.assertEqual(pygame.color.Color("red"), pygame.color.Color("Red")) + + def test_color(self): + """Ensures Color objects can be created.""" + color = pygame.Color(0, 0, 0, 0) + + self.assertIsInstance(color, pygame.Color) + + def test_color__rgba_int_args(self): + """Ensures Color objects can be created using ints.""" + color = pygame.Color(10, 20, 30, 40) + + self.assertEqual(color.r, 10) + self.assertEqual(color.g, 20) + self.assertEqual(color.b, 30) + self.assertEqual(color.a, 40) + + def test_color__rgba_int_args_without_alpha(self): + """Ensures Color objects can be created without providing alpha.""" + color = pygame.Color(10, 20, 30) + + self.assertEqual(color.r, 10) + self.assertEqual(color.g, 20) + self.assertEqual(color.b, 30) + self.assertEqual(color.a, 255) + + def test_color__rgba_int_args_invalid_value(self): + """Ensures invalid values are detected when creating Color objects.""" + self.assertRaises(ValueError, pygame.Color, 257, 10, 105, 44) + self.assertRaises(ValueError, pygame.Color, 10, 257, 105, 44) + self.assertRaises(ValueError, pygame.Color, 10, 105, 257, 44) + self.assertRaises(ValueError, pygame.Color, 10, 105, 44, 257) + + def test_color__rgba_int_args_invalid_value_without_alpha(self): + """Ensures invalid values are detected when creating Color objects + without providing an alpha. + """ + self.assertRaises(ValueError, pygame.Color, 256, 10, 105) + self.assertRaises(ValueError, pygame.Color, 10, 256, 105) + self.assertRaises(ValueError, pygame.Color, 10, 105, 256) + + def test_color__color_object_arg(self): + """Ensures Color objects can be created using Color objects.""" + color_args = (10, 20, 30, 40) + color_obj = pygame.Color(*color_args) + + new_color_obj = pygame.Color(color_obj) + + self.assertIsInstance(new_color_obj, pygame.Color) + self.assertEqual(new_color_obj, color_obj) + self.assertEqual(new_color_obj.r, color_args[0]) + self.assertEqual(new_color_obj.g, color_args[1]) + self.assertEqual(new_color_obj.b, color_args[2]) + self.assertEqual(new_color_obj.a, color_args[3]) + + def test_color__name_str_arg(self): + """Ensures Color objects can be created using str names.""" + for name in ("aquamarine3", "AQUAMARINE3", "AqUAmArIne3"): + color = pygame.Color(name) + + self.assertEqual(color.r, 102) + self.assertEqual(color.g, 205) + self.assertEqual(color.b, 170) + self.assertEqual(color.a, 255) + + def test_color__name_str_arg_from_colordict(self): + """Ensures Color objects can be created using str names + from the THECOLORS dict.""" + for name, values in THECOLORS.items(): + color = pygame.Color(name) + + self.assertEqual(color.r, values[0]) + self.assertEqual(color.g, values[1]) + self.assertEqual(color.b, values[2]) + self.assertEqual(color.a, values[3]) + + def test_color__html_str_arg(self): + """Ensures Color objects can be created using html strings.""" + # See test_webstyle() for related tests. + color = pygame.Color("#a1B2c3D4") + + self.assertEqual(color.r, 0xA1) + self.assertEqual(color.g, 0xB2) + self.assertEqual(color.b, 0xC3) + self.assertEqual(color.a, 0xD4) + + def test_color__hex_str_arg(self): + """Ensures Color objects can be created using hex strings.""" + # See test_webstyle() for related tests. + color = pygame.Color("0x1a2B3c4D") + + self.assertEqual(color.r, 0x1A) + self.assertEqual(color.g, 0x2B) + self.assertEqual(color.b, 0x3C) + self.assertEqual(color.a, 0x4D) + + def test_color__int_arg(self): + """Ensures Color objects can be created using one int value.""" + for value in (0x0, 0xFFFFFFFF, 0xAABBCCDD): + color = pygame.Color(value) + + self.assertEqual(color.r, (value >> 24) & 0xFF) + self.assertEqual(color.g, (value >> 16) & 0xFF) + self.assertEqual(color.b, (value >> 8) & 0xFF) + self.assertEqual(color.a, value & 0xFF) + + def test_color__int_arg_invalid(self): + """Ensures invalid int values are detected when creating Color objects.""" + with self.assertRaises(ValueError): + color = pygame.Color(0x1FFFFFFFF) + + def test_color__sequence_arg(self): + """Ensures Color objects can be created using tuples/lists.""" + color_values = (33, 44, 55, 66) + for seq_type in (tuple, list): + color = pygame.Color(seq_type(color_values)) + + self.assertEqual(color.r, color_values[0]) + self.assertEqual(color.g, color_values[1]) + self.assertEqual(color.b, color_values[2]) + self.assertEqual(color.a, color_values[3]) + + def test_color__sequence_arg_without_alpha(self): + """Ensures Color objects can be created using tuples/lists + without providing an alpha value. + """ + color_values = (33, 44, 55) + for seq_type in (tuple, list): + color = pygame.Color(seq_type(color_values)) + + self.assertEqual(color.r, color_values[0]) + self.assertEqual(color.g, color_values[1]) + self.assertEqual(color.b, color_values[2]) + self.assertEqual(color.a, 255) + + def test_color__sequence_arg_invalid_value(self): + """Ensures invalid sequences are detected when creating Color objects.""" + cls = pygame.Color + for seq_type in (tuple, list): + self.assertRaises(ValueError, cls, seq_type((256, 90, 80, 70))) + self.assertRaises(ValueError, cls, seq_type((100, 256, 80, 70))) + self.assertRaises(ValueError, cls, seq_type((100, 90, 256, 70))) + self.assertRaises(ValueError, cls, seq_type((100, 90, 80, 256))) + + def test_color__sequence_arg_invalid_value_without_alpha(self): + """Ensures invalid sequences are detected when creating Color objects + without providing an alpha. + """ + cls = pygame.Color + for seq_type in (tuple, list): + self.assertRaises(ValueError, cls, seq_type((256, 90, 80))) + self.assertRaises(ValueError, cls, seq_type((100, 256, 80))) + self.assertRaises(ValueError, cls, seq_type((100, 90, 256))) + + def test_color__sequence_arg_invalid_format(self): + """Ensures invalid sequences are detected when creating Color objects + with the wrong number of values. + """ + cls = pygame.Color + for seq_type in (tuple, list): + self.assertRaises(ValueError, cls, seq_type((100,))) + self.assertRaises(ValueError, cls, seq_type((100, 90))) + self.assertRaises(ValueError, cls, seq_type((100, 90, 80, 70, 60))) + + def test_rgba(self): + c = pygame.Color(0) + self.assertEqual(c.r, 0) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 0) + self.assertEqual(c.a, 0) + + # Test simple assignments + c.r = 123 + self.assertEqual(c.r, 123) + self.assertRaises(ValueError, _assignr, c, 537) + self.assertEqual(c.r, 123) + self.assertRaises(ValueError, _assignr, c, -3) + self.assertEqual(c.r, 123) + + c.g = 55 + self.assertEqual(c.g, 55) + self.assertRaises(ValueError, _assigng, c, 348) + self.assertEqual(c.g, 55) + self.assertRaises(ValueError, _assigng, c, -44) + self.assertEqual(c.g, 55) + + c.b = 77 + self.assertEqual(c.b, 77) + self.assertRaises(ValueError, _assignb, c, 256) + self.assertEqual(c.b, 77) + self.assertRaises(ValueError, _assignb, c, -12) + self.assertEqual(c.b, 77) + + c.a = 255 + self.assertEqual(c.a, 255) + self.assertRaises(ValueError, _assigna, c, 312) + self.assertEqual(c.a, 255) + self.assertRaises(ValueError, _assigna, c, -10) + self.assertEqual(c.a, 255) + + def test_repr(self): + c = pygame.Color(68, 38, 26, 69) + t = "(68, 38, 26, 69)" + self.assertEqual(repr(c), t) + + def test_add(self): + c1 = pygame.Color(0) + self.assertEqual(c1.r, 0) + self.assertEqual(c1.g, 0) + self.assertEqual(c1.b, 0) + self.assertEqual(c1.a, 0) + + c2 = pygame.Color(20, 33, 82, 193) + self.assertEqual(c2.r, 20) + self.assertEqual(c2.g, 33) + self.assertEqual(c2.b, 82) + self.assertEqual(c2.a, 193) + + c3 = c1 + c2 + self.assertEqual(c3.r, 20) + self.assertEqual(c3.g, 33) + self.assertEqual(c3.b, 82) + self.assertEqual(c3.a, 193) + + c3 = c3 + c2 + self.assertEqual(c3.r, 40) + self.assertEqual(c3.g, 66) + self.assertEqual(c3.b, 164) + self.assertEqual(c3.a, 255) + + # Issue #286: Is type checking done for Python 3.x? + self.assertRaises(TypeError, operator.add, c1, None) + self.assertRaises(TypeError, operator.add, None, c1) + + def test_sub(self): + c1 = pygame.Color(0xFFFFFFFF) + self.assertEqual(c1.r, 255) + self.assertEqual(c1.g, 255) + self.assertEqual(c1.b, 255) + self.assertEqual(c1.a, 255) + + c2 = pygame.Color(20, 33, 82, 193) + self.assertEqual(c2.r, 20) + self.assertEqual(c2.g, 33) + self.assertEqual(c2.b, 82) + self.assertEqual(c2.a, 193) + + c3 = c1 - c2 + self.assertEqual(c3.r, 235) + self.assertEqual(c3.g, 222) + self.assertEqual(c3.b, 173) + self.assertEqual(c3.a, 62) + + c3 = c3 - c2 + self.assertEqual(c3.r, 215) + self.assertEqual(c3.g, 189) + self.assertEqual(c3.b, 91) + self.assertEqual(c3.a, 0) + + # Issue #286: Is type checking done for Python 3.x? + self.assertRaises(TypeError, operator.sub, c1, None) + self.assertRaises(TypeError, operator.sub, None, c1) + + def test_mul(self): + c1 = pygame.Color(0x01010101) + self.assertEqual(c1.r, 1) + self.assertEqual(c1.g, 1) + self.assertEqual(c1.b, 1) + self.assertEqual(c1.a, 1) + + c2 = pygame.Color(2, 5, 3, 22) + self.assertEqual(c2.r, 2) + self.assertEqual(c2.g, 5) + self.assertEqual(c2.b, 3) + self.assertEqual(c2.a, 22) + + c3 = c1 * c2 + self.assertEqual(c3.r, 2) + self.assertEqual(c3.g, 5) + self.assertEqual(c3.b, 3) + self.assertEqual(c3.a, 22) + + c3 = c3 * c2 + self.assertEqual(c3.r, 4) + self.assertEqual(c3.g, 25) + self.assertEqual(c3.b, 9) + self.assertEqual(c3.a, 255) + + # Issue #286: Is type checking done for Python 3.x? + self.assertRaises(TypeError, operator.mul, c1, None) + self.assertRaises(TypeError, operator.mul, None, c1) + + def test_div(self): + c1 = pygame.Color(0x80808080) + self.assertEqual(c1.r, 128) + self.assertEqual(c1.g, 128) + self.assertEqual(c1.b, 128) + self.assertEqual(c1.a, 128) + + c2 = pygame.Color(2, 4, 8, 16) + self.assertEqual(c2.r, 2) + self.assertEqual(c2.g, 4) + self.assertEqual(c2.b, 8) + self.assertEqual(c2.a, 16) + + c3 = c1 // c2 + self.assertEqual(c3.r, 64) + self.assertEqual(c3.g, 32) + self.assertEqual(c3.b, 16) + self.assertEqual(c3.a, 8) + + c3 = c3 // c2 + self.assertEqual(c3.r, 32) + self.assertEqual(c3.g, 8) + self.assertEqual(c3.b, 2) + self.assertEqual(c3.a, 0) + + # Issue #286: Is type checking done for Python 3.x? + self.assertRaises(TypeError, operator.floordiv, c1, None) + self.assertRaises(TypeError, operator.floordiv, None, c1) + + # Division by zero check + dividend = pygame.Color(255, 255, 255, 255) + for i in range(4): + divisor = pygame.Color(64, 64, 64, 64) + divisor[i] = 0 + quotient = pygame.Color(3, 3, 3, 3) + quotient[i] = 0 + self.assertEqual(dividend // divisor, quotient) + + def test_mod(self): + c1 = pygame.Color(0xFFFFFFFF) + self.assertEqual(c1.r, 255) + self.assertEqual(c1.g, 255) + self.assertEqual(c1.b, 255) + self.assertEqual(c1.a, 255) + + c2 = pygame.Color(2, 4, 8, 16) + self.assertEqual(c2.r, 2) + self.assertEqual(c2.g, 4) + self.assertEqual(c2.b, 8) + self.assertEqual(c2.a, 16) + + c3 = c1 % c2 + self.assertEqual(c3.r, 1) + self.assertEqual(c3.g, 3) + self.assertEqual(c3.b, 7) + self.assertEqual(c3.a, 15) + + # Issue #286: Is type checking done for Python 3.x? + self.assertRaises(TypeError, operator.mod, c1, None) + self.assertRaises(TypeError, operator.mod, None, c1) + + # Division by zero check + dividend = pygame.Color(255, 255, 255, 255) + for i in range(4): + divisor = pygame.Color(64, 64, 64, 64) + divisor[i] = 0 + quotient = pygame.Color(63, 63, 63, 63) + quotient[i] = 0 + self.assertEqual(dividend % divisor, quotient) + + def test_float(self): + c = pygame.Color(0xCC00CC00) + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 0) + self.assertEqual(float(c), float(0xCC00CC00)) + + c = pygame.Color(0x33727592) + self.assertEqual(c.r, 51) + self.assertEqual(c.g, 114) + self.assertEqual(c.b, 117) + self.assertEqual(c.a, 146) + self.assertEqual(float(c), float(0x33727592)) + + def test_oct(self): + c = pygame.Color(0xCC00CC00) + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 0) + self.assertEqual(oct(c), oct(0xCC00CC00)) + + c = pygame.Color(0x33727592) + self.assertEqual(c.r, 51) + self.assertEqual(c.g, 114) + self.assertEqual(c.b, 117) + self.assertEqual(c.a, 146) + self.assertEqual(oct(c), oct(0x33727592)) + + def test_hex(self): + c = pygame.Color(0xCC00CC00) + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 0) + self.assertEqual(hex(c), hex(0xCC00CC00)) + + c = pygame.Color(0x33727592) + self.assertEqual(c.r, 51) + self.assertEqual(c.g, 114) + self.assertEqual(c.b, 117) + self.assertEqual(c.a, 146) + self.assertEqual(hex(c), hex(0x33727592)) + + def test_webstyle(self): + c = pygame.Color("#CC00CC11") + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 17) + self.assertEqual(hex(c), hex(0xCC00CC11)) + + c = pygame.Color("#CC00CC") + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 255) + self.assertEqual(hex(c), hex(0xCC00CCFF)) + + c = pygame.Color("0xCC00CC11") + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 17) + self.assertEqual(hex(c), hex(0xCC00CC11)) + + c = pygame.Color("0xCC00CC") + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 255) + self.assertEqual(hex(c), hex(0xCC00CCFF)) + + self.assertRaises(ValueError, pygame.Color, "#cc00qq") + self.assertRaises(ValueError, pygame.Color, "0xcc00qq") + self.assertRaises(ValueError, pygame.Color, "09abcdef") + self.assertRaises(ValueError, pygame.Color, "09abcde") + self.assertRaises(ValueError, pygame.Color, "quarky") + + def test_int(self): + # This will be a long + c = pygame.Color(0xCC00CC00) + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 0) + self.assertEqual(int(c), int(0xCC00CC00)) + + # This will be an int + c = pygame.Color(0x33727592) + self.assertEqual(c.r, 51) + self.assertEqual(c.g, 114) + self.assertEqual(c.b, 117) + self.assertEqual(c.a, 146) + self.assertEqual(int(c), int(0x33727592)) + + def test_long(self): + # This will be a long + c = pygame.Color(0xCC00CC00) + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 204) + self.assertEqual(c.a, 0) + self.assertEqual(int(c), int(0xCC00CC00)) + + # This will be an int + c = pygame.Color(0x33727592) + self.assertEqual(c.r, 51) + self.assertEqual(c.g, 114) + self.assertEqual(c.b, 117) + self.assertEqual(c.a, 146) + self.assertEqual(int(c), int(0x33727592)) + + def test_normalize(self): + c = pygame.Color(204, 38, 194, 55) + self.assertEqual(c.r, 204) + self.assertEqual(c.g, 38) + self.assertEqual(c.b, 194) + self.assertEqual(c.a, 55) + + t = c.normalize() + + self.assertAlmostEqual(t[0], 0.800000, 5) + self.assertAlmostEqual(t[1], 0.149016, 5) + self.assertAlmostEqual(t[2], 0.760784, 5) + self.assertAlmostEqual(t[3], 0.215686, 5) + + def test_len(self): + c = pygame.Color(204, 38, 194, 55) + self.assertEqual(len(c), 4) + + def test_get_item(self): + c = pygame.Color(204, 38, 194, 55) + self.assertEqual(c[0], 204) + self.assertEqual(c[1], 38) + self.assertEqual(c[2], 194) + self.assertEqual(c[3], 55) + + def test_set_item(self): + c = pygame.Color(204, 38, 194, 55) + self.assertEqual(c[0], 204) + self.assertEqual(c[1], 38) + self.assertEqual(c[2], 194) + self.assertEqual(c[3], 55) + + c[0] = 33 + self.assertEqual(c[0], 33) + c[1] = 48 + self.assertEqual(c[1], 48) + c[2] = 173 + self.assertEqual(c[2], 173) + c[3] = 213 + self.assertEqual(c[3], 213) + + # Now try some 'invalid' ones + self.assertRaises(TypeError, _assign_item, c, 0, 95.485) + self.assertEqual(c[0], 33) + self.assertRaises(ValueError, _assign_item, c, 1, -83) + self.assertEqual(c[1], 48) + self.assertRaises(TypeError, _assign_item, c, 2, "Hello") + self.assertEqual(c[2], 173) + + def test_Color_type_works_for_Surface_get_and_set_colorkey(self): + s = pygame.Surface((32, 32)) + + c = pygame.Color(33, 22, 11, 255) + s.set_colorkey(c) + + get_r, get_g, get_b, get_a = s.get_colorkey() + + self.assertTrue(get_r == c.r) + self.assertTrue(get_g == c.g) + self.assertTrue(get_b == c.b) + self.assertTrue(get_a == c.a) + + ########## HSLA, HSVA, CMY, I1I2I3 ALL ELEMENTS WITHIN SPECIFIED RANGE ######### + + def test_hsla__all_elements_within_limits(self): + for c in rgba_combos_Color_generator(): + h, s, l, a = c.hsla + self.assertTrue(0 <= h <= 360) + self.assertTrue(0 <= s <= 100) + self.assertTrue(0 <= l <= 100) + self.assertTrue(0 <= a <= 100) + + def test_hsva__all_elements_within_limits(self): + for c in rgba_combos_Color_generator(): + h, s, v, a = c.hsva + self.assertTrue(0 <= h <= 360) + self.assertTrue(0 <= s <= 100) + self.assertTrue(0 <= v <= 100) + self.assertTrue(0 <= a <= 100) + + def test_cmy__all_elements_within_limits(self): + for c in rgba_combos_Color_generator(): + c, m, y = c.cmy + self.assertTrue(0 <= c <= 1) + self.assertTrue(0 <= m <= 1) + self.assertTrue(0 <= y <= 1) + + def test_i1i2i3__all_elements_within_limits(self): + for c in rgba_combos_Color_generator(): + i1, i2, i3 = c.i1i2i3 + self.assertTrue(0 <= i1 <= 1) + self.assertTrue(-0.5 <= i2 <= 0.5) + self.assertTrue(-0.5 <= i3 <= 0.5) + + def test_issue_269(self): + """PyColor OverflowError on HSVA with hue value of 360 + + >>> c = pygame.Color(0) + >>> c.hsva = (360,0,0,0) + Traceback (most recent call last): + File "", line 1, in + OverflowError: this is not allowed to happen ever + >>> pygame.ver + '1.9.1release' + >>> + + """ + + c = pygame.Color(0) + c.hsva = 360, 0, 0, 0 + self.assertEqual(c.hsva, (0, 0, 0, 0)) + c.hsva = 360, 100, 100, 100 + self.assertEqual(c.hsva, (0, 100, 100, 100)) + self.assertEqual(c, (255, 0, 0, 255)) + + ####################### COLORSPACE PROPERTY SANITY TESTS ####################### + + def colorspaces_converted_should_not_raise(self, prop): + fails = 0 + + x = 0 + for c in rgba_combos_Color_generator(): + x += 1 + + other = pygame.Color(0) + + try: + setattr(other, prop, getattr(c, prop)) + # eg other.hsla = c.hsla + + except ValueError: + fails += 1 + + self.assertTrue(x > 0, "x is combination counter, 0 means no tests!") + self.assertTrue((fails, x) == (0, x)) + + def test_hsla__sanity_testing_converted_should_not_raise(self): + self.colorspaces_converted_should_not_raise("hsla") + + def test_hsva__sanity_testing_converted_should_not_raise(self): + self.colorspaces_converted_should_not_raise("hsva") + + def test_cmy__sanity_testing_converted_should_not_raise(self): + self.colorspaces_converted_should_not_raise("cmy") + + def test_i1i2i3__sanity_testing_converted_should_not_raise(self): + self.colorspaces_converted_should_not_raise("i1i2i3") + + ################################################################################ + + def colorspaces_converted_should_equate_bar_rounding(self, prop): + for c in rgba_combos_Color_generator(): + other = pygame.Color(0) + + try: + setattr(other, prop, getattr(c, prop)) + # eg other.hsla = c.hsla + + self.assertTrue(abs(other.r - c.r) <= 1) + self.assertTrue(abs(other.b - c.b) <= 1) + self.assertTrue(abs(other.g - c.g) <= 1) + # CMY and I1I2I3 do not care about the alpha + if prop not in ("cmy", "i1i2i3"): + self.assertTrue(abs(other.a - c.a) <= 1) + + except ValueError: + pass # other tests will notify, this tests equation + + def test_hsla__sanity_testing_converted_should_equate_bar_rounding(self): + self.colorspaces_converted_should_equate_bar_rounding("hsla") + + def test_hsva__sanity_testing_converted_should_equate_bar_rounding(self): + self.colorspaces_converted_should_equate_bar_rounding("hsva") + + def test_cmy__sanity_testing_converted_should_equate_bar_rounding(self): + self.colorspaces_converted_should_equate_bar_rounding("cmy") + + def test_i1i2i3__sanity_testing_converted_should_equate_bar_rounding(self): + self.colorspaces_converted_should_equate_bar_rounding("i1i2i3") + + ################################################################################ + + def test_correct_gamma__verified_against_python_implementation(self): + "|tags:slow|" + # gamma_correct defined at top of page + + gammas = [i / 10.0 for i in range(1, 31)] # [0.1 ... 3.0] + gammas_len = len(gammas) + + for i, c in enumerate(rgba_combos_Color_generator()): + gamma = gammas[i % gammas_len] + + corrected = pygame.Color(*[gamma_correct(x, gamma) for x in tuple(c)]) + lib_corrected = c.correct_gamma(gamma) + + self.assertTrue(corrected.r == lib_corrected.r) + self.assertTrue(corrected.g == lib_corrected.g) + self.assertTrue(corrected.b == lib_corrected.b) + self.assertTrue(corrected.a == lib_corrected.a) + + # TODO: test against statically defined verified _correct_ values + # assert corrected.r == 125 etc. + + def test_pickle(self): + import pickle + + c1 = pygame.Color(1, 2, 3, 4) + # c2 = pygame.Color(255,254,253,252) + pickle_string = pickle.dumps(c1) + c1_frompickle = pickle.loads(pickle_string) + self.assertEqual(c1, c1_frompickle) + + ################################################################################ + # only available if ctypes module is also available + + @unittest.skipIf(IS_PYPY, "PyPy has no ctypes") + def test_arraystruct(self): + import pygame.tests.test_utils.arrinter as ai + import ctypes as ct + + c_byte_p = ct.POINTER(ct.c_byte) + c = pygame.Color(5, 7, 13, 23) + flags = ai.PAI_CONTIGUOUS | ai.PAI_FORTRAN | ai.PAI_ALIGNED | ai.PAI_NOTSWAPPED + for i in range(1, 5): + c.set_length(i) + inter = ai.ArrayInterface(c) + self.assertEqual(inter.two, 2) + self.assertEqual(inter.nd, 1) + self.assertEqual(inter.typekind, "u") + self.assertEqual(inter.itemsize, 1) + self.assertEqual(inter.flags, flags) + self.assertEqual(inter.shape[0], i) + self.assertEqual(inter.strides[0], 1) + data = ct.cast(inter.data, c_byte_p) + for j in range(i): + self.assertEqual(data[j], c[j]) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def test_newbuf(self): + from pygame.tests.test_utils import buftools + from ctypes import cast, POINTER, c_uint8 + + class ColorImporter(buftools.Importer): + def __init__(self, color, flags): + super().__init__(color, flags) + self.items = cast(self.buf, POINTER(c_uint8)) + + def __getitem__(self, index): + if 0 <= index < 4: + return self.items[index] + raise IndexError(f"valid index values are between 0 and 3: got {index}") + + def __setitem__(self, index, value): + if 0 <= index < 4: + self.items[index] = value + else: + raise IndexError( + f"valid index values are between 0 and 3: got {index}" + ) + + c = pygame.Color(50, 100, 150, 200) + imp = ColorImporter(c, buftools.PyBUF_SIMPLE) + self.assertTrue(imp.obj is c) + self.assertEqual(imp.ndim, 0) + self.assertEqual(imp.itemsize, 1) + self.assertEqual(imp.len, 4) + self.assertTrue(imp.readonly) + self.assertTrue(imp.format is None) + self.assertTrue(imp.shape is None) + self.assertTrue(imp.strides is None) + self.assertTrue(imp.suboffsets is None) + for i in range(4): + self.assertEqual(c[i], imp[i]) + imp[0] = 60 + self.assertEqual(c.r, 60) + imp[1] = 110 + self.assertEqual(c.g, 110) + imp[2] = 160 + self.assertEqual(c.b, 160) + imp[3] = 210 + self.assertEqual(c.a, 210) + imp = ColorImporter(c, buftools.PyBUF_FORMAT) + self.assertEqual(imp.ndim, 0) + self.assertEqual(imp.itemsize, 1) + self.assertEqual(imp.len, 4) + self.assertEqual(imp.format, "B") + self.assertEqual(imp.ndim, 0) + self.assertEqual(imp.itemsize, 1) + self.assertEqual(imp.len, 4) + imp = ColorImporter(c, buftools.PyBUF_ND) + self.assertEqual(imp.ndim, 1) + self.assertEqual(imp.itemsize, 1) + self.assertEqual(imp.len, 4) + self.assertTrue(imp.format is None) + self.assertEqual(imp.shape, (4,)) + self.assertEqual(imp.strides, None) + imp = ColorImporter(c, buftools.PyBUF_STRIDES) + self.assertEqual(imp.ndim, 1) + self.assertTrue(imp.format is None) + self.assertEqual(imp.shape, (4,)) + self.assertEqual(imp.strides, (1,)) + imp = ColorImporter(c, buftools.PyBUF_C_CONTIGUOUS) + self.assertEqual(imp.ndim, 1) + imp = ColorImporter(c, buftools.PyBUF_F_CONTIGUOUS) + self.assertEqual(imp.ndim, 1) + imp = ColorImporter(c, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertEqual(imp.ndim, 1) + for i in range(1, 5): + c.set_length(i) + imp = ColorImporter(c, buftools.PyBUF_ND) + self.assertEqual(imp.ndim, 1) + self.assertEqual(imp.len, i) + self.assertEqual(imp.shape, (i,)) + self.assertRaises(BufferError, ColorImporter, c, buftools.PyBUF_WRITABLE) + + def test_color_iter(self): + c = pygame.Color(50, 100, 150, 200) + + # call __iter__ explicitly to test that it is defined + color_iterator = c.__iter__() + for i, val in enumerate(color_iterator): + self.assertEqual(c[i], val) + + def test_color_contains(self): + c = pygame.Color(50, 60, 70) + + # call __contains__ explicitly to test that it is defined + self.assertTrue(c.__contains__(50)) + self.assertTrue(60 in c) + self.assertTrue(70 in c) + self.assertFalse(100 in c) + self.assertFalse(c.__contains__(10)) + + self.assertRaises(TypeError, lambda: "string" in c) + self.assertRaises(TypeError, lambda: 3.14159 in c) + + def test_grayscale(self): + Color = pygame.color.Color + + color = Color(255, 0, 0, 255) + self.assertEqual(color.grayscale(), Color(76, 76, 76, 255)) + color = Color(3, 5, 7, 255) + self.assertEqual(color.grayscale(), Color(4, 4, 4, 255)) + color = Color(3, 5, 70, 255) + self.assertEqual(color.grayscale(), Color(11, 11, 11, 255)) + color = Color(3, 50, 70, 255) + self.assertEqual(color.grayscale(), Color(38, 38, 38, 255)) + color = Color(30, 50, 70, 255) + self.assertEqual(color.grayscale(), Color(46, 46, 46, 255)) + + color = Color(255, 0, 0, 144) + self.assertEqual(color.grayscale(), Color(76, 76, 76, 144)) + color = Color(3, 5, 7, 144) + self.assertEqual(color.grayscale(), Color(4, 4, 4, 144)) + color = Color(3, 5, 70, 144) + self.assertEqual(color.grayscale(), Color(11, 11, 11, 144)) + color = Color(3, 50, 70, 144) + self.assertEqual(color.grayscale(), Color(38, 38, 38, 144)) + color = Color(30, 50, 70, 144) + self.assertEqual(color.grayscale(), Color(46, 46, 46, 144)) + + def test_lerp(self): + # setup + Color = pygame.color.Color + + color0 = Color(0, 0, 0, 0) + color128 = Color(128, 128, 128, 128) + color255 = Color(255, 255, 255, 255) + color100 = Color(100, 100, 100, 100) + + # type checking + self.assertTrue(isinstance(color0.lerp(color128, 0.5), Color)) + + # common value testing + self.assertEqual(color0.lerp(color128, 0.5), Color(64, 64, 64, 64)) + self.assertEqual(color0.lerp(color128, 0.5), Color(64, 64, 64, 64)) + self.assertEqual(color128.lerp(color255, 0.5), Color(192, 192, 192, 192)) + self.assertEqual(color0.lerp(color255, 0.5), Color(128, 128, 128, 128)) + + # testing extremes + self.assertEqual(color0.lerp(color100, 0), color0) + self.assertEqual(color0.lerp(color100, 0.01), Color(1, 1, 1, 1)) + self.assertEqual(color0.lerp(color100, 0.99), Color(99, 99, 99, 99)) + self.assertEqual(color0.lerp(color100, 1), color100) + + # kwarg testing + self.assertEqual(color0.lerp(color=color100, amount=0.5), Color(50, 50, 50, 50)) + self.assertEqual(color0.lerp(amount=0.5, color=color100), Color(50, 50, 50, 50)) + + # invalid input testing + self.assertRaises(ValueError, lambda: color0.lerp(color128, 2.5)) + self.assertRaises(ValueError, lambda: color0.lerp(color128, -0.5)) + self.assertRaises(ValueError, lambda: color0.lerp((256, 0, 0, 0), 0.5)) + self.assertRaises(ValueError, lambda: color0.lerp((0, 256, 0, 0), 0.5)) + self.assertRaises(ValueError, lambda: color0.lerp((0, 0, 256, 0), 0.5)) + self.assertRaises(ValueError, lambda: color0.lerp((0, 0, 0, 256), 0.5)) + self.assertRaises(TypeError, lambda: color0.lerp(0.2, 0.5)) + + def test_premul_alpha(self): + # setup + Color = pygame.color.Color + + color0 = Color(0, 0, 0, 0) + alpha0 = Color(255, 255, 255, 0) + alpha49 = Color(255, 0, 0, 49) + alpha67 = Color(0, 255, 0, 67) + alpha73 = Color(0, 0, 255, 73) + alpha128 = Color(255, 255, 255, 128) + alpha199 = Color(255, 255, 255, 199) + alpha255 = Color(128, 128, 128, 255) + + # type checking + self.assertTrue(isinstance(color0.premul_alpha(), Color)) + + # hand crafted value testing + self.assertEqual(alpha0.premul_alpha(), Color(0, 0, 0, 0)) + self.assertEqual(alpha49.premul_alpha(), Color(49, 0, 0, 49)) + self.assertEqual(alpha67.premul_alpha(), Color(0, 67, 0, 67)) + self.assertEqual(alpha73.premul_alpha(), Color(0, 0, 73, 73)) + self.assertEqual(alpha128.premul_alpha(), Color(128, 128, 128, 128)) + self.assertEqual(alpha199.premul_alpha(), Color(199, 199, 199, 199)) + self.assertEqual(alpha255.premul_alpha(), Color(128, 128, 128, 255)) + + # full range of alpha auto sub-testing + test_colors = [ + (200, 30, 74), + (76, 83, 24), + (184, 21, 6), + (74, 4, 74), + (76, 83, 24), + (184, 21, 234), + (160, 30, 74), + (96, 147, 204), + (198, 201, 60), + (132, 89, 74), + (245, 9, 224), + (184, 112, 6), + ] + + for r, g, b in test_colors: + for a in range(255): + with self.subTest(r=r, g=g, b=b, a=a): + alpha = a / 255.0 + self.assertEqual( + Color(r, g, b, a).premul_alpha(), + Color( + ((r + 1) * a) >> 8, + ((g + 1) * a) >> 8, + ((b + 1) * a) >> 8, + a, + ), + ) + + def test_update(self): + c = pygame.color.Color(0, 0, 0) + c.update(1, 2, 3, 4) + + self.assertEqual(c.r, 1) + self.assertEqual(c.g, 2) + self.assertEqual(c.b, 3) + self.assertEqual(c.a, 4) + + c = pygame.color.Color(0, 0, 0) + c.update([1, 2, 3, 4]) + + self.assertEqual(c.r, 1) + self.assertEqual(c.g, 2) + self.assertEqual(c.b, 3) + self.assertEqual(c.a, 4) + + c = pygame.color.Color(0, 0, 0) + c2 = pygame.color.Color(1, 2, 3, 4) + c.update(c2) + + self.assertEqual(c.r, 1) + self.assertEqual(c.g, 2) + self.assertEqual(c.b, 3) + self.assertEqual(c.a, 4) + + c = pygame.color.Color(1, 1, 1) + c.update("black") + + self.assertEqual(c.r, 0) + self.assertEqual(c.g, 0) + self.assertEqual(c.b, 0) + self.assertEqual(c.a, 255) + + c = pygame.color.Color(0, 0, 0, 120) + c.set_length(3) + c.update(1, 2, 3) + self.assertEqual(len(c), 3) + c.set_length(4) + self.assertEqual(c[3], 120) + + c.set_length(3) + c.update(1, 2, 3, 4) + self.assertEqual(len(c), 4) + + def test_collection_abc(self): + c = pygame.Color(64, 70, 75, 255) + self.assertTrue(isinstance(c, Collection)) + self.assertFalse(isinstance(c, Sequence)) + + +class SubclassTest(unittest.TestCase): + class MyColor(pygame.Color): + def __init__(self, *args, **kwds): + super(SubclassTest.MyColor, self).__init__(*args, **kwds) + self.an_attribute = True + + def test_add(self): + mc1 = self.MyColor(128, 128, 128, 255) + self.assertTrue(mc1.an_attribute) + c2 = pygame.Color(64, 64, 64, 255) + mc2 = mc1 + c2 + self.assertTrue(isinstance(mc2, self.MyColor)) + self.assertRaises(AttributeError, getattr, mc2, "an_attribute") + c3 = c2 + mc1 + self.assertTrue(type(c3) is pygame.Color) + + def test_sub(self): + mc1 = self.MyColor(128, 128, 128, 255) + self.assertTrue(mc1.an_attribute) + c2 = pygame.Color(64, 64, 64, 255) + mc2 = mc1 - c2 + self.assertTrue(isinstance(mc2, self.MyColor)) + self.assertRaises(AttributeError, getattr, mc2, "an_attribute") + c3 = c2 - mc1 + self.assertTrue(type(c3) is pygame.Color) + + def test_mul(self): + mc1 = self.MyColor(128, 128, 128, 255) + self.assertTrue(mc1.an_attribute) + c2 = pygame.Color(64, 64, 64, 255) + mc2 = mc1 * c2 + self.assertTrue(isinstance(mc2, self.MyColor)) + self.assertRaises(AttributeError, getattr, mc2, "an_attribute") + c3 = c2 * mc1 + self.assertTrue(type(c3) is pygame.Color) + + def test_div(self): + mc1 = self.MyColor(128, 128, 128, 255) + self.assertTrue(mc1.an_attribute) + c2 = pygame.Color(64, 64, 64, 255) + mc2 = mc1 // c2 + self.assertTrue(isinstance(mc2, self.MyColor)) + self.assertRaises(AttributeError, getattr, mc2, "an_attribute") + c3 = c2 // mc1 + self.assertTrue(type(c3) is pygame.Color) + + def test_mod(self): + mc1 = self.MyColor(128, 128, 128, 255) + self.assertTrue(mc1.an_attribute) + c2 = pygame.Color(64, 64, 64, 255) + mc2 = mc1 % c2 + self.assertTrue(isinstance(mc2, self.MyColor)) + self.assertRaises(AttributeError, getattr, mc2, "an_attribute") + c3 = c2 % mc1 + self.assertTrue(type(c3) is pygame.Color) + + def test_inv(self): + mc1 = self.MyColor(64, 64, 64, 64) + self.assertTrue(mc1.an_attribute) + mc2 = ~mc1 + self.assertTrue(isinstance(mc2, self.MyColor)) + self.assertRaises(AttributeError, getattr, mc2, "an_attribute") + + def test_correct_gamma(self): + mc1 = self.MyColor(64, 70, 75, 255) + self.assertTrue(mc1.an_attribute) + mc2 = mc1.correct_gamma(0.03) + self.assertTrue(isinstance(mc2, self.MyColor)) + self.assertRaises(AttributeError, getattr, mc2, "an_attribute") + + def test_collection_abc(self): + mc1 = self.MyColor(64, 70, 75, 255) + self.assertTrue(isinstance(mc1, Collection)) + self.assertFalse(isinstance(mc1, Sequence)) + + +################################################################################ + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/constants_test.py b/laplas/abstract_map/pygame/tests/constants_test.py new file mode 100644 index 0000000..a028f98 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/constants_test.py @@ -0,0 +1,426 @@ +import unittest +import pygame.constants + + +# K_* and KSCAN_* common names. +K_AND_KSCAN_COMMON_NAMES = ( + "UNKNOWN", + "BACKSPACE", + "TAB", + "CLEAR", + "RETURN", + "PAUSE", + "ESCAPE", + "SPACE", + "COMMA", + "MINUS", + "PERIOD", + "SLASH", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "SEMICOLON", + "EQUALS", + "LEFTBRACKET", + "BACKSLASH", + "RIGHTBRACKET", + "DELETE", + "KP0", + "KP1", + "KP2", + "KP3", + "KP4", + "KP5", + "KP6", + "KP7", + "KP8", + "KP9", + "KP_PERIOD", + "KP_DIVIDE", + "KP_MULTIPLY", + "KP_MINUS", + "KP_PLUS", + "KP_ENTER", + "KP_EQUALS", + "UP", + "DOWN", + "RIGHT", + "LEFT", + "INSERT", + "HOME", + "END", + "PAGEUP", + "PAGEDOWN", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + "F13", + "F14", + "F15", + "NUMLOCK", + "CAPSLOCK", + "SCROLLOCK", + "RSHIFT", + "LSHIFT", + "RCTRL", + "LCTRL", + "RALT", + "LALT", + "RMETA", + "LMETA", + "LSUPER", + "RSUPER", + "MODE", + "HELP", + "PRINT", + "SYSREQ", + "BREAK", + "MENU", + "POWER", + "EURO", + "KP_0", + "KP_1", + "KP_2", + "KP_3", + "KP_4", + "KP_5", + "KP_6", + "KP_7", + "KP_8", + "KP_9", + "NUMLOCKCLEAR", + "SCROLLLOCK", + "RGUI", + "LGUI", + "PRINTSCREEN", + "CURRENCYUNIT", + "CURRENCYSUBUNIT", +) + +# Constants that have the same value. +K_AND_KSCAN_COMMON_OVERLAPS = ( + ("KP0", "KP_0"), + ("KP1", "KP_1"), + ("KP2", "KP_2"), + ("KP3", "KP_3"), + ("KP4", "KP_4"), + ("KP5", "KP_5"), + ("KP6", "KP_6"), + ("KP7", "KP_7"), + ("KP8", "KP_8"), + ("KP9", "KP_9"), + ("NUMLOCK", "NUMLOCKCLEAR"), + ("SCROLLOCK", "SCROLLLOCK"), + ("LSUPER", "LMETA", "LGUI"), + ("RSUPER", "RMETA", "RGUI"), + ("PRINT", "PRINTSCREEN"), + ("BREAK", "PAUSE"), + ("EURO", "CURRENCYUNIT"), +) + + +def create_overlap_set(constant_names): + """Helper function to find overlapping constant values/names. + + Returns a set of fronzensets: + set(frozenset(names of overlapping constants), ...) + """ + # Create an overlap dict. + overlap_dict = {} + + for name in constant_names: + value = getattr(pygame.constants, name) + overlap_dict.setdefault(value, set()).add(name) + + # Get all entries with more than 1 value. + overlaps = set() + + for overlap_names in overlap_dict.values(): + if len(overlap_names) > 1: + overlaps.add(frozenset(overlap_names)) + + return overlaps + + +class KConstantsTests(unittest.TestCase): + """Test K_* (key) constants.""" + + # K_* specific names. + K_SPECIFIC_NAMES = ( + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "QUOTE", + "BACKQUOTE", + "EXCLAIM", + "QUOTEDBL", + "HASH", + "DOLLAR", + "AMPERSAND", + "LEFTPAREN", + "RIGHTPAREN", + "ASTERISK", + "PLUS", + "COLON", + "LESS", + "GREATER", + "QUESTION", + "AT", + "CARET", + "UNDERSCORE", + "PERCENT", + ) + + # Create a sequence of all the K_* constant names. + K_NAMES = tuple("K_" + n for n in K_AND_KSCAN_COMMON_NAMES + K_SPECIFIC_NAMES) + + def test_k__existence(self): + """Ensures K constants exist.""" + for name in self.K_NAMES: + self.assertTrue(hasattr(pygame.constants, name), f"missing constant {name}") + + def test_k__type(self): + """Ensures K constants are the correct type.""" + for name in self.K_NAMES: + value = getattr(pygame.constants, name) + + self.assertIs(type(value), int) + + def test_k__value_overlap(self): + """Ensures no unexpected K constant values overlap.""" + EXPECTED_OVERLAPS = { + frozenset("K_" + n for n in item) for item in K_AND_KSCAN_COMMON_OVERLAPS + } + + overlaps = create_overlap_set(self.K_NAMES) + + self.assertSetEqual(overlaps, EXPECTED_OVERLAPS) + + +class KscanConstantsTests(unittest.TestCase): + """Test KSCAN_* (scancode) constants.""" + + # KSCAN_* specific names. + KSCAN_SPECIFIC_NAMES = ( + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "APOSTROPHE", + "GRAVE", + "INTERNATIONAL1", + "INTERNATIONAL2", + "INTERNATIONAL3", + "INTERNATIONAL4", + "INTERNATIONAL5", + "INTERNATIONAL6", + "INTERNATIONAL7", + "INTERNATIONAL8", + "INTERNATIONAL9", + "LANG1", + "LANG2", + "LANG3", + "LANG4", + "LANG5", + "LANG6", + "LANG7", + "LANG8", + "LANG9", + "NONUSBACKSLASH", + "NONUSHASH", + ) + + # Create a sequence of all the KSCAN_* constant names. + KSCAN_NAMES = tuple( + "KSCAN_" + n for n in K_AND_KSCAN_COMMON_NAMES + KSCAN_SPECIFIC_NAMES + ) + + def test_kscan__existence(self): + """Ensures KSCAN constants exist.""" + for name in self.KSCAN_NAMES: + self.assertTrue(hasattr(pygame.constants, name), f"missing constant {name}") + + def test_kscan__type(self): + """Ensures KSCAN constants are the correct type.""" + for name in self.KSCAN_NAMES: + value = getattr(pygame.constants, name) + + self.assertIs(type(value), int) + + def test_kscan__value_overlap(self): + """Ensures no unexpected KSCAN constant values overlap.""" + EXPECTED_OVERLAPS = { + frozenset("KSCAN_" + n for n in item) + for item in K_AND_KSCAN_COMMON_OVERLAPS + } + + overlaps = create_overlap_set(self.KSCAN_NAMES) + + self.assertSetEqual(overlaps, EXPECTED_OVERLAPS) + + +class KmodConstantsTests(unittest.TestCase): + """Test KMOD_* (key modifier) constants.""" + + # KMOD_* constant names. + KMOD_CONSTANTS = ( + "KMOD_NONE", + "KMOD_LSHIFT", + "KMOD_RSHIFT", + "KMOD_SHIFT", + "KMOD_LCTRL", + "KMOD_RCTRL", + "KMOD_CTRL", + "KMOD_LALT", + "KMOD_RALT", + "KMOD_ALT", + "KMOD_LMETA", + "KMOD_RMETA", + "KMOD_META", + "KMOD_NUM", + "KMOD_CAPS", + "KMOD_MODE", + "KMOD_LGUI", + "KMOD_RGUI", + "KMOD_GUI", + ) + + def test_kmod__existence(self): + """Ensures KMOD constants exist.""" + for name in self.KMOD_CONSTANTS: + self.assertTrue(hasattr(pygame.constants, name), f"missing constant {name}") + + def test_kmod__type(self): + """Ensures KMOD constants are the correct type.""" + for name in self.KMOD_CONSTANTS: + value = getattr(pygame.constants, name) + + self.assertIs(type(value), int) + + def test_kmod__value_overlap(self): + """Ensures no unexpected KMOD constant values overlap.""" + # KMODs that have the same values. + EXPECTED_OVERLAPS = { + frozenset(["KMOD_LGUI", "KMOD_LMETA"]), + frozenset(["KMOD_RGUI", "KMOD_RMETA"]), + frozenset(["KMOD_GUI", "KMOD_META"]), + } + + overlaps = create_overlap_set(self.KMOD_CONSTANTS) + + self.assertSetEqual(overlaps, EXPECTED_OVERLAPS) + + def test_kmod__no_bitwise_overlap(self): + """Ensures certain KMOD constants have no overlapping bits.""" + NO_BITWISE_OVERLAP = ( + "KMOD_NONE", + "KMOD_LSHIFT", + "KMOD_RSHIFT", + "KMOD_LCTRL", + "KMOD_RCTRL", + "KMOD_LALT", + "KMOD_RALT", + "KMOD_LMETA", + "KMOD_RMETA", + "KMOD_NUM", + "KMOD_CAPS", + "KMOD_MODE", + ) + + kmods = 0 + + for name in NO_BITWISE_OVERLAP: + value = getattr(pygame.constants, name) + + self.assertFalse(kmods & value) + + kmods |= value + + def test_kmod__bitwise_overlap(self): + """Ensures certain KMOD constants have overlapping bits.""" + # KMODS that are comprised of other KMODs. + KMOD_COMPRISED_DICT = { + "KMOD_SHIFT": ("KMOD_LSHIFT", "KMOD_RSHIFT"), + "KMOD_CTRL": ("KMOD_LCTRL", "KMOD_RCTRL"), + "KMOD_ALT": ("KMOD_LALT", "KMOD_RALT"), + "KMOD_META": ("KMOD_LMETA", "KMOD_RMETA"), + "KMOD_GUI": ("KMOD_LGUI", "KMOD_RGUI"), + } + + for base_name, seq_names in KMOD_COMPRISED_DICT.items(): + expected_value = 0 # Reset. + + for name in seq_names: + expected_value |= getattr(pygame.constants, name) + + value = getattr(pygame.constants, base_name) + + self.assertEqual(value, expected_value) + + +################################################################################ + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/controller_test.py b/laplas/abstract_map/pygame/tests/controller_test.py new file mode 100644 index 0000000..f05c00c --- /dev/null +++ b/laplas/abstract_map/pygame/tests/controller_test.py @@ -0,0 +1,357 @@ +import unittest +import pygame +import pygame._sdl2.controller as controller +from pygame.tests.test_utils import prompt, question + + +class ControllerModuleTest(unittest.TestCase): + def setUp(self): + controller.init() + + def tearDown(self): + controller.quit() + + def test_init(self): + controller.quit() + controller.init() + self.assertTrue(controller.get_init()) + + def test_init__multiple(self): + controller.init() + controller.init() + self.assertTrue(controller.get_init()) + + def test_quit(self): + controller.quit() + self.assertFalse(controller.get_init()) + + def test_quit__multiple(self): + controller.quit() + controller.quit() + self.assertFalse(controller.get_init()) + + def test_get_init(self): + self.assertTrue(controller.get_init()) + + def test_get_eventstate(self): + controller.set_eventstate(True) + self.assertTrue(controller.get_eventstate()) + + controller.set_eventstate(False) + self.assertFalse(controller.get_eventstate()) + + controller.set_eventstate(True) + + def test_get_count(self): + self.assertGreaterEqual(controller.get_count(), 0) + + def test_is_controller(self): + for i in range(controller.get_count()): + if controller.is_controller(i): + c = controller.Controller(i) + self.assertIsInstance(c, controller.Controller) + c.quit() + else: + with self.assertRaises(pygame._sdl2.sdl2.error): + c = controller.Controller(i) + + with self.assertRaises(TypeError): + controller.is_controller("Test") + + def test_name_forindex(self): + self.assertIsNone(controller.name_forindex(-1)) + + +class ControllerTypeTest(unittest.TestCase): + def setUp(self): + controller.init() + + def tearDown(self): + controller.quit() + + def _get_first_controller(self): + for i in range(controller.get_count()): + if controller.is_controller(i): + return controller.Controller(i) + + def test_construction(self): + c = self._get_first_controller() + if c: + self.assertIsInstance(c, controller.Controller) + else: + self.skipTest("No controller connected") + + def test__auto_init(self): + c = self._get_first_controller() + if c: + self.assertTrue(c.get_init()) + else: + self.skipTest("No controller connected") + + def test_get_init(self): + c = self._get_first_controller() + if c: + self.assertTrue(c.get_init()) + c.quit() + self.assertFalse(c.get_init()) + else: + self.skipTest("No controller connected") + + def test_from_joystick(self): + for i in range(controller.get_count()): + if controller.is_controller(i): + joy = pygame.joystick.Joystick(i) + break + else: + self.skipTest("No controller connected") + + c = controller.Controller.from_joystick(joy) + self.assertIsInstance(c, controller.Controller) + + def test_as_joystick(self): + c = self._get_first_controller() + if c: + joy = c.as_joystick() + self.assertIsInstance(joy, type(pygame.joystick.Joystick(0))) + else: + self.skipTest("No controller connected") + + def test_get_mapping(self): + c = self._get_first_controller() + if c: + mapping = c.get_mapping() + self.assertIsInstance(mapping, dict) + self.assertIsNotNone(mapping["a"]) + else: + self.skipTest("No controller connected") + + def test_set_mapping(self): + c = self._get_first_controller() + if c: + mapping = c.get_mapping() + mapping["a"] = "b3" + mapping["y"] = "b0" + c.set_mapping(mapping) + new_mapping = c.get_mapping() + + self.assertEqual(len(mapping), len(new_mapping)) + for i in mapping: + if mapping[i] not in ("a", "y"): + self.assertEqual(mapping[i], new_mapping[i]) + else: + if i == "a": + self.assertEqual(new_mapping[i], mapping["y"]) + else: + self.assertEqual(new_mapping[i], mapping["a"]) + else: + self.skipTest("No controller connected") + + +class ControllerInteractiveTest(unittest.TestCase): + __tags__ = ["interactive"] + + def _get_first_controller(self): + for i in range(controller.get_count()): + if controller.is_controller(i): + return controller.Controller(i) + + def setUp(self): + controller.init() + + def tearDown(self): + controller.quit() + + def test__get_count_interactive(self): + prompt( + "Please connect at least one controller " + "before the test for controller.get_count() starts" + ) + + # Reset the number of joysticks counted + controller.quit() + controller.init() + + joystick_num = controller.get_count() + ans = question( + "get_count() thinks there are {} joysticks " + "connected. Is that correct?".format(joystick_num) + ) + + self.assertTrue(ans) + + def test_set_eventstate_on_interactive(self): + c = self._get_first_controller() + if not c: + self.skipTest("No controller connected") + + pygame.display.init() + pygame.font.init() + + screen = pygame.display.set_mode((400, 400)) + font = pygame.font.Font(None, 20) + running = True + + screen.fill((255, 255, 255)) + screen.blit( + font.render("Press button 'x' (on ps4) or 'a' (on xbox).", True, (0, 0, 0)), + (0, 0), + ) + pygame.display.update() + + controller.set_eventstate(True) + + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + if event.type == pygame.CONTROLLERBUTTONDOWN: + running = False + + pygame.display.quit() + pygame.font.quit() + + def test_set_eventstate_off_interactive(self): + c = self._get_first_controller() + if not c: + self.skipTest("No controller connected") + + pygame.display.init() + pygame.font.init() + + screen = pygame.display.set_mode((400, 400)) + font = pygame.font.Font(None, 20) + running = True + + screen.fill((255, 255, 255)) + screen.blit( + font.render("Press button 'x' (on ps4) or 'a' (on xbox).", True, (0, 0, 0)), + (0, 0), + ) + pygame.display.update() + + controller.set_eventstate(False) + + while running: + for event in pygame.event.get(pygame.QUIT): + if event: + running = False + + if c.get_button(pygame.CONTROLLER_BUTTON_A): + if pygame.event.peek(pygame.CONTROLLERBUTTONDOWN): + pygame.display.quit() + pygame.font.quit() + self.fail() + else: + running = False + + pygame.display.quit() + pygame.font.quit() + + def test_get_button_interactive(self): + c = self._get_first_controller() + if not c: + self.skipTest("No controller connected") + + pygame.display.init() + pygame.font.init() + + screen = pygame.display.set_mode((400, 400)) + font = pygame.font.Font(None, 20) + running = True + + label1 = font.render( + "Press button 'x' (on ps4) or 'a' (on xbox).", True, (0, 0, 0) + ) + + label2 = font.render( + 'The two values should match up. Press "y" or "n" to confirm.', + True, + (0, 0, 0), + ) + + is_pressed = [False, False] # event, get_button() + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + if event.type == pygame.CONTROLLERBUTTONDOWN and event.button == 0: + is_pressed[0] = True + if event.type == pygame.CONTROLLERBUTTONUP and event.button == 0: + is_pressed[0] = False + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_y: + running = False + if event.key == pygame.K_n: + running = False + pygame.display.quit() + pygame.font.quit() + self.fail() + + is_pressed[1] = c.get_button(pygame.CONTROLLER_BUTTON_A) + + screen.fill((255, 255, 255)) + screen.blit(label1, (0, 0)) + screen.blit(label2, (0, 20)) + screen.blit(font.render(str(is_pressed), True, (0, 0, 0)), (0, 40)) + pygame.display.update() + + pygame.display.quit() + pygame.font.quit() + + def test_get_axis_interactive(self): + c = self._get_first_controller() + if not c: + self.skipTest("No controller connected") + + pygame.display.init() + pygame.font.init() + + screen = pygame.display.set_mode((400, 400)) + font = pygame.font.Font(None, 20) + running = True + + label1 = font.render( + "Press down the right trigger. The value on-screen should", True, (0, 0, 0) + ) + + label2 = font.render( + "indicate how far the trigger is pressed down. This value should", + True, + (0, 0, 0), + ) + + label3 = font.render( + 'be in the range of 0-32767. Press "y" or "n" to confirm.', True, (0, 0, 0) + ) + + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_y: + running = False + if event.key == pygame.K_n: + running = False + pygame.display.quit() + pygame.font.quit() + self.fail() + + right_trigger = c.get_axis(pygame.CONTROLLER_AXIS_TRIGGERRIGHT) + + screen.fill((255, 255, 255)) + screen.blit(label1, (0, 0)) + screen.blit(label2, (0, 20)) + screen.blit(label3, (0, 40)) + screen.blit(font.render(str(right_trigger), True, (0, 0, 0)), (0, 60)) + pygame.display.update() + + pygame.display.quit() + pygame.font.quit() + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/cursors_test.py b/laplas/abstract_map/pygame/tests/cursors_test.py new file mode 100644 index 0000000..8132c51 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/cursors_test.py @@ -0,0 +1,290 @@ +import unittest +from pygame.tests.test_utils import fixture_path +import pygame + + +class CursorsModuleTest(unittest.TestCase): + def test_compile(self): + # __doc__ (as of 2008-06-25) for pygame.cursors.compile: + + # pygame.cursors.compile(strings, black, white,xor) -> data, mask + # compile cursor strings into cursor data + # + # This takes a set of strings with equal length and computes + # the binary data for that cursor. The string widths must be + # divisible by 8. + # + # The black and white arguments are single letter strings that + # tells which characters will represent black pixels, and which + # characters represent white pixels. All other characters are + # considered clear. + # + # This returns a tuple containing the cursor data and cursor mask + # data. Both these arguments are used when setting a cursor with + # pygame.mouse.set_cursor(). + + # Various types of input strings + test_cursor1 = ("X.X.XXXX", "XXXXXX..", " XXXX ") + + test_cursor2 = ( + "X.X.XXXX", + "XXXXXX..", + "XXXXXX ", + "XXXXXX..", + "XXXXXX..", + "XXXXXX", + "XXXXXX..", + "XXXXXX..", + ) + test_cursor3 = (".XX.", " ", ".. ", "X.. X") + + # Test such that total number of strings is not divisible by 8 + with self.assertRaises(ValueError): + pygame.cursors.compile(test_cursor1) + + # Test such that size of individual string is not divisible by 8 + with self.assertRaises(ValueError): + pygame.cursors.compile(test_cursor2) + + # Test such that neither size of individual string nor total number of strings is divisible by 8 + with self.assertRaises(ValueError): + pygame.cursors.compile(test_cursor3) + + # Test that checks whether the byte data from compile function is equal to actual byte data + actual_byte_data = ( + 192, + 0, + 0, + 224, + 0, + 0, + 240, + 0, + 0, + 216, + 0, + 0, + 204, + 0, + 0, + 198, + 0, + 0, + 195, + 0, + 0, + 193, + 128, + 0, + 192, + 192, + 0, + 192, + 96, + 0, + 192, + 48, + 0, + 192, + 56, + 0, + 192, + 248, + 0, + 220, + 192, + 0, + 246, + 96, + 0, + 198, + 96, + 0, + 6, + 96, + 0, + 3, + 48, + 0, + 3, + 48, + 0, + 1, + 224, + 0, + 1, + 128, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ), ( + 192, + 0, + 0, + 224, + 0, + 0, + 240, + 0, + 0, + 248, + 0, + 0, + 252, + 0, + 0, + 254, + 0, + 0, + 255, + 0, + 0, + 255, + 128, + 0, + 255, + 192, + 0, + 255, + 224, + 0, + 255, + 240, + 0, + 255, + 248, + 0, + 255, + 248, + 0, + 255, + 192, + 0, + 247, + 224, + 0, + 199, + 224, + 0, + 7, + 224, + 0, + 3, + 240, + 0, + 3, + 240, + 0, + 1, + 224, + 0, + 1, + 128, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ) + + cursor = pygame.cursors.compile(pygame.cursors.thickarrow_strings) + self.assertEqual(cursor, actual_byte_data) + + # Test such that cursor byte data obtained from compile function is valid in pygame.mouse.set_cursor() + pygame.display.init() + try: + pygame.mouse.set_cursor((24, 24), (0, 0), *cursor) + except pygame.error as e: + if "not currently supported" in str(e): + unittest.skip("skipping test as set_cursor() is not supported") + finally: + pygame.display.quit() + + ################################################################################ + + def test_load_xbm(self): + # __doc__ (as of 2008-06-25) for pygame.cursors.load_xbm: + + # pygame.cursors.load_xbm(cursorfile, maskfile) -> cursor_args + # reads a pair of XBM files into set_cursor arguments + # + # Arguments can either be filenames or filelike objects + # with the readlines method. Not largely tested, but + # should work with typical XBM files. + + # Test that load_xbm will take filenames as arguments + cursorfile = fixture_path(r"xbm_cursors/white_sizing.xbm") + maskfile = fixture_path(r"xbm_cursors/white_sizing_mask.xbm") + cursor = pygame.cursors.load_xbm(cursorfile, maskfile) + + # Test that load_xbm will take file objects as arguments + with open(cursorfile) as cursor_f, open(maskfile) as mask_f: + cursor = pygame.cursors.load_xbm(cursor_f, mask_f) + + # Can it load using pathlib.Path? + import pathlib + + cursor = pygame.cursors.load_xbm( + pathlib.Path(cursorfile), pathlib.Path(maskfile) + ) + + # Is it in a format that mouse.set_cursor won't blow up on? + pygame.display.init() + try: + pygame.mouse.set_cursor(*cursor) + except pygame.error as e: + if "not currently supported" in str(e): + unittest.skip("skipping test as set_cursor() is not supported") + finally: + pygame.display.quit() + + def test_Cursor(self): + """Ensure that the cursor object parses information properly""" + + c1 = pygame.cursors.Cursor(pygame.SYSTEM_CURSOR_CROSSHAIR) + + self.assertEqual(c1.data, (pygame.SYSTEM_CURSOR_CROSSHAIR,)) + self.assertEqual(c1.type, "system") + + c2 = pygame.cursors.Cursor(c1) + + self.assertEqual(c1, c2) + + with self.assertRaises(TypeError): + pygame.cursors.Cursor(-34002) + with self.assertRaises(TypeError): + pygame.cursors.Cursor("a", "b", "c", "d") + with self.assertRaises(TypeError): + pygame.cursors.Cursor((2,)) + + c3 = pygame.cursors.Cursor((0, 0), pygame.Surface((20, 20))) + + self.assertEqual(c3.data[0], (0, 0)) + self.assertEqual(c3.data[1].get_size(), (20, 20)) + self.assertEqual(c3.type, "color") + + xormask, andmask = pygame.cursors.compile(pygame.cursors.thickarrow_strings) + c4 = pygame.cursors.Cursor((24, 24), (0, 0), xormask, andmask) + + self.assertEqual(c4.data, ((24, 24), (0, 0), xormask, andmask)) + self.assertEqual(c4.type, "bitmap") + + +################################################################################ + +if __name__ == "__main__": + unittest.main() + +################################################################################ diff --git a/laplas/abstract_map/pygame/tests/display_test.py b/laplas/abstract_map/pygame/tests/display_test.py new file mode 100644 index 0000000..eec521f --- /dev/null +++ b/laplas/abstract_map/pygame/tests/display_test.py @@ -0,0 +1,1199 @@ +import unittest +import os +import sys +import time + +import pygame, pygame.transform + +from pygame.tests.test_utils import question + +from pygame import display + + +class DisplayModuleTest(unittest.TestCase): + default_caption = "pygame window" + + def setUp(self): + display.init() + + def tearDown(self): + display.quit() + + def test_Info(self): + inf = pygame.display.Info() + self.assertNotEqual(inf.current_h, -1) + self.assertNotEqual(inf.current_w, -1) + # probably have an older SDL than 1.2.10 if -1. + + screen = pygame.display.set_mode((128, 128)) + inf = pygame.display.Info() + self.assertEqual(inf.current_h, 128) + self.assertEqual(inf.current_w, 128) + + def test_flip(self): + screen = pygame.display.set_mode((100, 100)) + + # test without a change + self.assertIsNone(pygame.display.flip()) + + # test with a change + pygame.Surface.fill(screen, (66, 66, 53)) + self.assertIsNone(pygame.display.flip()) + + # test without display init + pygame.display.quit() + with self.assertRaises(pygame.error): + (pygame.display.flip()) + + # test without window + del screen + with self.assertRaises(pygame.error): + (pygame.display.flip()) + + def test_get_active(self): + """Test the get_active function""" + + # Initially, the display is not active + pygame.display.quit() + self.assertEqual(pygame.display.get_active(), False) + + # get_active defaults to true after a set_mode + pygame.display.init() + pygame.display.set_mode((640, 480)) + self.assertEqual(pygame.display.get_active(), True) + + # get_active after init/quit should be False + # since no display is visible + pygame.display.quit() + pygame.display.init() + self.assertEqual(pygame.display.get_active(), False) + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", + "requires the SDL_VIDEODRIVER to be a non dummy value", + ) + def test_get_active_iconify(self): + """Test the get_active function after an iconify""" + + # According to the docs, get_active should return + # false if the display is iconified + pygame.display.set_mode((640, 480)) + + pygame.event.clear() + pygame.display.iconify() + + for _ in range(100): + time.sleep(0.01) + pygame.event.pump() + + self.assertEqual(pygame.display.get_active(), False) + + def test_get_caption(self): + screen = display.set_mode((100, 100)) + + self.assertEqual(display.get_caption()[0], self.default_caption) + + def test_set_caption(self): + TEST_CAPTION = "test" + screen = display.set_mode((100, 100)) + + self.assertIsNone(display.set_caption(TEST_CAPTION)) + self.assertEqual(display.get_caption()[0], TEST_CAPTION) + self.assertEqual(display.get_caption()[1], TEST_CAPTION) + + def test_set_caption_kwargs(self): + TEST_CAPTION = "test" + screen = display.set_mode((100, 100)) + + self.assertIsNone(display.set_caption(title=TEST_CAPTION)) + self.assertEqual(display.get_caption()[0], TEST_CAPTION) + self.assertEqual(display.get_caption()[1], TEST_CAPTION) + + def test_caption_unicode(self): + TEST_CAPTION = "台" + display.set_caption(TEST_CAPTION) + self.assertEqual(display.get_caption()[0], TEST_CAPTION) + + def test_get_driver(self): + drivers = [ + "aalib", + "android", + "arm", + "cocoa", + "dga", + "directx", + "directfb", + "dummy", + "emscripten", + "fbcon", + "ggi", + "haiku", + "khronos", + "kmsdrm", + "nacl", + "offscreen", + "pandora", + "psp", + "qnx", + "raspberry", + "svgalib", + "uikit", + "vgl", + "vivante", + "wayland", + "windows", + "windib", + "winrt", + "x11", + ] + driver = display.get_driver() + self.assertIn(driver, drivers) + + display.quit() + with self.assertRaises(pygame.error): + driver = display.get_driver() + + def test_get_init(self): + """Ensures the module's initialization state can be retrieved.""" + # display.init() already called in setUp() + self.assertTrue(display.get_init()) + + # This test can be uncommented when issues #991 and #993 are resolved. + @unittest.skipIf(True, "SDL2 issues") + def test_get_surface(self): + """Ensures get_surface gets the current display surface.""" + lengths = (1, 5, 100) + + for expected_size in ((w, h) for w in lengths for h in lengths): + for expected_depth in (8, 16, 24, 32): + expected_surface = display.set_mode(expected_size, 0, expected_depth) + + surface = pygame.display.get_surface() + + self.assertEqual(surface, expected_surface) + self.assertIsInstance(surface, pygame.Surface) + self.assertEqual(surface.get_size(), expected_size) + self.assertEqual(surface.get_bitsize(), expected_depth) + + def test_get_surface__mode_not_set(self): + """Ensures get_surface handles the display mode not being set.""" + surface = pygame.display.get_surface() + + self.assertIsNone(surface) + + def test_get_wm_info(self): + wm_info = display.get_wm_info() + # Assert function returns a dictionary type + self.assertIsInstance(wm_info, dict) + + wm_info_potential_keys = { + "colorbuffer", + "connection", + "data", + "dfb", + "display", + "framebuffer", + "fswindow", + "hdc", + "hglrc", + "hinstance", + "lock_func", + "resolveFramebuffer", + "shell_surface", + "surface", + "taskHandle", + "unlock_func", + "wimpVersion", + "window", + "wmwindow", + } + + # If any unexpected dict keys are present, they + # will be stored in set wm_info_remaining_keys + wm_info_remaining_keys = set(wm_info.keys()).difference(wm_info_potential_keys) + + # Assert set is empty (& therefore does not + # contain unexpected dict keys) + self.assertFalse(wm_info_remaining_keys) + + @unittest.skipIf( + ( + "skipping for all because some failures on rasppi and maybe other platforms" + or os.environ.get("SDL_VIDEODRIVER") == "dummy" + ), + 'OpenGL requires a non-"dummy" SDL_VIDEODRIVER', + ) + def test_gl_get_attribute(self): + screen = display.set_mode((0, 0), pygame.OPENGL) + + # We create a list where we store the original values of the + # flags before setting them with a different value. + original_values = [] + + original_values.append(pygame.display.gl_get_attribute(pygame.GL_ALPHA_SIZE)) + original_values.append(pygame.display.gl_get_attribute(pygame.GL_DEPTH_SIZE)) + original_values.append(pygame.display.gl_get_attribute(pygame.GL_STENCIL_SIZE)) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_ACCUM_RED_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_ACCUM_GREEN_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_ACCUM_BLUE_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_ACCUM_ALPHA_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLEBUFFERS) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLESAMPLES) + ) + original_values.append(pygame.display.gl_get_attribute(pygame.GL_STEREO)) + + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_ACCELERATED_VISUAL) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_CONTEXT_MAJOR_VERSION) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_CONTEXT_MINOR_VERSION) + ) + original_values.append(pygame.display.gl_get_attribute(pygame.GL_CONTEXT_FLAGS)) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_CONTEXT_PROFILE_MASK) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_SHARE_WITH_CURRENT_CONTEXT) + ) + original_values.append( + pygame.display.gl_get_attribute(pygame.GL_FRAMEBUFFER_SRGB_CAPABLE) + ) + + # Setting the flags with values supposedly different from the original values + + # assign SDL1-supported values with gl_set_attribute + pygame.display.gl_set_attribute(pygame.GL_ALPHA_SIZE, 8) + pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 24) + pygame.display.gl_set_attribute(pygame.GL_STENCIL_SIZE, 8) + pygame.display.gl_set_attribute(pygame.GL_ACCUM_RED_SIZE, 16) + pygame.display.gl_set_attribute(pygame.GL_ACCUM_GREEN_SIZE, 16) + pygame.display.gl_set_attribute(pygame.GL_ACCUM_BLUE_SIZE, 16) + pygame.display.gl_set_attribute(pygame.GL_ACCUM_ALPHA_SIZE, 16) + pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1) + pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, 1) + pygame.display.gl_set_attribute(pygame.GL_STEREO, 0) + pygame.display.gl_set_attribute(pygame.GL_ACCELERATED_VISUAL, 0) + pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MAJOR_VERSION, 1) + pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MINOR_VERSION, 1) + pygame.display.gl_set_attribute(pygame.GL_CONTEXT_FLAGS, 0) + pygame.display.gl_set_attribute(pygame.GL_CONTEXT_PROFILE_MASK, 0) + pygame.display.gl_set_attribute(pygame.GL_SHARE_WITH_CURRENT_CONTEXT, 0) + pygame.display.gl_set_attribute(pygame.GL_FRAMEBUFFER_SRGB_CAPABLE, 0) + + # We create a list where we store the values that we set each flag to + set_values = [8, 24, 8, 16, 16, 16, 16, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0] + + # We create a list where we store the values after getting them + get_values = [] + + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ALPHA_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_DEPTH_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_STENCIL_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCUM_RED_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCUM_GREEN_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCUM_BLUE_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCUM_ALPHA_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLEBUFFERS)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLESAMPLES)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_STEREO)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCELERATED_VISUAL)) + get_values.append( + pygame.display.gl_get_attribute(pygame.GL_CONTEXT_MAJOR_VERSION) + ) + get_values.append( + pygame.display.gl_get_attribute(pygame.GL_CONTEXT_MINOR_VERSION) + ) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_CONTEXT_FLAGS)) + get_values.append( + pygame.display.gl_get_attribute(pygame.GL_CONTEXT_PROFILE_MASK) + ) + get_values.append( + pygame.display.gl_get_attribute(pygame.GL_SHARE_WITH_CURRENT_CONTEXT) + ) + get_values.append( + pygame.display.gl_get_attribute(pygame.GL_FRAMEBUFFER_SRGB_CAPABLE) + ) + + # We check to see if the values that we get correspond to the values that we set + # them to or to the original values. + for i in range(len(original_values)): + self.assertTrue( + (get_values[i] == original_values[i]) + or (get_values[i] == set_values[i]) + ) + + # test using non-flag argument + with self.assertRaises(TypeError): + pygame.display.gl_get_attribute("DUMMY") + + @unittest.skipIf( + ( + "skipping for all because some failures on rasppi and maybe other platforms" + or os.environ.get("SDL_VIDEODRIVER") == "dummy" + ), + 'OpenGL requires a non-"dummy" SDL_VIDEODRIVER', + ) + def test_gl_get_attribute_kwargs(self): + screen = display.set_mode((0, 0), pygame.OPENGL) + + # We create a list where we store the original values of the + # flags before setting them with a different value. + original_values = [] + + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ALPHA_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_DEPTH_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_STENCIL_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_RED_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_GREEN_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_BLUE_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_ALPHA_SIZE) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_MULTISAMPLEBUFFERS) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_MULTISAMPLESAMPLES) + ) + original_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_STEREO)) + + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCELERATED_VISUAL) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_CONTEXT_MAJOR_VERSION) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_CONTEXT_MINOR_VERSION) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_CONTEXT_FLAGS) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_CONTEXT_PROFILE_MASK) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_SHARE_WITH_CURRENT_CONTEXT) + ) + original_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_FRAMEBUFFER_SRGB_CAPABLE) + ) + + # Setting the flags with values supposedly different from the original values + + # assign SDL1-supported values with gl_set_attribute + pygame.display.gl_set_attribute(flag=pygame.GL_ALPHA_SIZE, value=8) + pygame.display.gl_set_attribute(flag=pygame.GL_DEPTH_SIZE, value=24) + pygame.display.gl_set_attribute(flag=pygame.GL_STENCIL_SIZE, value=8) + pygame.display.gl_set_attribute(flag=pygame.GL_ACCUM_RED_SIZE, value=16) + pygame.display.gl_set_attribute(flag=pygame.GL_ACCUM_GREEN_SIZE, value=16) + pygame.display.gl_set_attribute(flag=pygame.GL_ACCUM_BLUE_SIZE, value=16) + pygame.display.gl_set_attribute(flag=pygame.GL_ACCUM_ALPHA_SIZE, value=16) + pygame.display.gl_set_attribute(flag=pygame.GL_MULTISAMPLEBUFFERS, value=1) + pygame.display.gl_set_attribute(flag=pygame.GL_MULTISAMPLESAMPLES, value=1) + pygame.display.gl_set_attribute(flag=pygame.GL_STEREO, value=0) + pygame.display.gl_set_attribute(flag=pygame.GL_ACCELERATED_VISUAL, value=0) + pygame.display.gl_set_attribute(flag=pygame.GL_CONTEXT_MAJOR_VERSION, value=1) + pygame.display.gl_set_attribute(flag=pygame.GL_CONTEXT_MINOR_VERSION, value=1) + pygame.display.gl_set_attribute(flag=pygame.GL_CONTEXT_FLAGS, value=0) + pygame.display.gl_set_attribute(flag=pygame.GL_CONTEXT_PROFILE_MASK, value=0) + pygame.display.gl_set_attribute( + flag=pygame.GL_SHARE_WITH_CURRENT_CONTEXT, value=0 + ) + pygame.display.gl_set_attribute( + flag=pygame.GL_FRAMEBUFFER_SRGB_CAPABLE, value=0 + ) + + # We create a list where we store the values that we set each flag to + set_values = [8, 24, 8, 16, 16, 16, 16, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0] + + # We create a list where we store the values after getting them + get_values = [] + + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_ALPHA_SIZE)) + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_DEPTH_SIZE)) + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_STENCIL_SIZE)) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_RED_SIZE) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_GREEN_SIZE) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_BLUE_SIZE) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_ALPHA_SIZE) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_MULTISAMPLEBUFFERS) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_MULTISAMPLESAMPLES) + ) + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_STEREO)) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCELERATED_VISUAL) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_CONTEXT_MAJOR_VERSION) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_CONTEXT_MINOR_VERSION) + ) + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_CONTEXT_FLAGS)) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_CONTEXT_PROFILE_MASK) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_SHARE_WITH_CURRENT_CONTEXT) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_FRAMEBUFFER_SRGB_CAPABLE) + ) + + # We check to see if the values that we get correspond to the values that we set + # them to or to the original values. + for i in range(len(original_values)): + self.assertTrue( + (get_values[i] == original_values[i]) + or (get_values[i] == set_values[i]) + ) + + # test using non-flag argument + with self.assertRaises(TypeError): + pygame.display.gl_get_attribute("DUMMY") + + @unittest.skipIf( + ( + "skipping for all because some failures on rasppi and maybe other platforms" + or os.environ.get("SDL_VIDEODRIVER") == "dummy" + ), + 'OpenGL requires a non-"dummy" SDL_VIDEODRIVER', + ) + def test_gl_set_attribute(self): + # __doc__ (as of 2008-08-02) for pygame.display.gl_set_attribute: + + # pygame.display.gl_set_attribute(flag, value): return None + # request an opengl display attribute for the display mode + # + # When calling pygame.display.set_mode() with the pygame.OPENGL flag, + # Pygame automatically handles setting the OpenGL attributes like + # color and doublebuffering. OpenGL offers several other attributes + # you may want control over. Pass one of these attributes as the flag, + # and its appropriate value. This must be called before + # pygame.display.set_mode() + # + # The OPENGL flags are; + # GL_ALPHA_SIZE, GL_DEPTH_SIZE, GL_STENCIL_SIZE, GL_ACCUM_RED_SIZE, + # GL_ACCUM_GREEN_SIZE, GL_ACCUM_BLUE_SIZE, GL_ACCUM_ALPHA_SIZE, + # GL_MULTISAMPLEBUFFERS, GL_MULTISAMPLESAMPLES, GL_STEREO + + screen = display.set_mode((0, 0), pygame.OPENGL) + + # We create a list where we store the values that we set each flag to + set_values = [8, 24, 8, 16, 16, 16, 16, 1, 1, 0] + + # Setting the flags with values supposedly different from the original values + + # assign SDL1-supported values with gl_set_attribute + pygame.display.gl_set_attribute(pygame.GL_ALPHA_SIZE, set_values[0]) + pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, set_values[1]) + pygame.display.gl_set_attribute(pygame.GL_STENCIL_SIZE, set_values[2]) + pygame.display.gl_set_attribute(pygame.GL_ACCUM_RED_SIZE, set_values[3]) + pygame.display.gl_set_attribute(pygame.GL_ACCUM_GREEN_SIZE, set_values[4]) + pygame.display.gl_set_attribute(pygame.GL_ACCUM_BLUE_SIZE, set_values[5]) + pygame.display.gl_set_attribute(pygame.GL_ACCUM_ALPHA_SIZE, set_values[6]) + pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, set_values[7]) + pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, set_values[8]) + pygame.display.gl_set_attribute(pygame.GL_STEREO, set_values[9]) + + # We create a list where we store the values after getting them + get_values = [] + + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ALPHA_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_DEPTH_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_STENCIL_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCUM_RED_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCUM_GREEN_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCUM_BLUE_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_ACCUM_ALPHA_SIZE)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLEBUFFERS)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLESAMPLES)) + get_values.append(pygame.display.gl_get_attribute(pygame.GL_STEREO)) + + # We check to see if the values that we get correspond to the values that we set + # them to or to the original values. + for i in range(len(set_values)): + self.assertTrue(get_values[i] == set_values[i]) + + # test using non-flag argument + with self.assertRaises(TypeError): + pygame.display.gl_get_attribute("DUMMY") + + @unittest.skipIf( + ( + "skipping for all because some failures on rasppi and maybe other platforms" + or os.environ.get("SDL_VIDEODRIVER") == "dummy" + ), + 'OpenGL requires a non-"dummy" SDL_VIDEODRIVER', + ) + def test_gl_set_attribute_kwargs(self): + # __doc__ (as of 2008-08-02) for pygame.display.gl_set_attribute: + + # pygame.display.gl_set_attribute(flag, value): return None + # request an opengl display attribute for the display mode + # + # When calling pygame.display.set_mode() with the pygame.OPENGL flag, + # Pygame automatically handles setting the OpenGL attributes like + # color and doublebuffering. OpenGL offers several other attributes + # you may want control over. Pass one of these attributes as the flag, + # and its appropriate value. This must be called before + # pygame.display.set_mode() + # + # The OPENGL flags are; + # GL_ALPHA_SIZE, GL_DEPTH_SIZE, GL_STENCIL_SIZE, GL_ACCUM_RED_SIZE, + # GL_ACCUM_GREEN_SIZE, GL_ACCUM_BLUE_SIZE, GL_ACCUM_ALPHA_SIZE, + # GL_MULTISAMPLEBUFFERS, GL_MULTISAMPLESAMPLES, GL_STEREO + + screen = display.set_mode((0, 0), pygame.OPENGL) + + # We create a list where we store the values that we set each flag to + set_values = [8, 24, 8, 16, 16, 16, 16, 1, 1, 0] + + # Setting the flags with values supposedly different from the original values + + # assign SDL1-supported values with gl_set_attribute + pygame.display.gl_set_attribute(flag=pygame.GL_ALPHA_SIZE, value=set_values[0]) + pygame.display.gl_set_attribute(flag=pygame.GL_DEPTH_SIZE, value=set_values[1]) + pygame.display.gl_set_attribute( + flag=pygame.GL_STENCIL_SIZE, value=set_values[2] + ) + pygame.display.gl_set_attribute( + flag=pygame.GL_ACCUM_RED_SIZE, value=set_values[3] + ) + pygame.display.gl_set_attribute( + flag=pygame.GL_ACCUM_GREEN_SIZE, value=set_values[4] + ) + pygame.display.gl_set_attribute( + flag=pygame.GL_ACCUM_BLUE_SIZE, value=set_values[5] + ) + pygame.display.gl_set_attribute( + flag=pygame.GL_ACCUM_ALPHA_SIZE, value=set_values[6] + ) + pygame.display.gl_set_attribute( + flag=pygame.GL_MULTISAMPLEBUFFERS, value=set_values[7] + ) + pygame.display.gl_set_attribute( + flag=pygame.GL_MULTISAMPLESAMPLES, value=set_values[8] + ) + pygame.display.gl_set_attribute(flag=pygame.GL_STEREO, value=set_values[9]) + + # We create a list where we store the values after getting them + get_values = [] + + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_ALPHA_SIZE)) + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_DEPTH_SIZE)) + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_STENCIL_SIZE)) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_RED_SIZE) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_GREEN_SIZE) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_BLUE_SIZE) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_ACCUM_ALPHA_SIZE) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_MULTISAMPLEBUFFERS) + ) + get_values.append( + pygame.display.gl_get_attribute(flag=pygame.GL_MULTISAMPLESAMPLES) + ) + get_values.append(pygame.display.gl_get_attribute(flag=pygame.GL_STEREO)) + + # We check to see if the values that we get correspond to the values that we set + # them to or to the original values. + for i in range(len(set_values)): + self.assertTrue(get_values[i] == set_values[i]) + + # test using non-flag argument + with self.assertRaises(TypeError): + pygame.display.gl_get_attribute("DUMMY") + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") in ["dummy", "android"], + "iconify is only supported on some video drivers/platforms", + ) + def test_iconify(self): + pygame.display.set_mode((640, 480)) + + self.assertEqual(pygame.display.get_active(), True) + + success = pygame.display.iconify() + + if success: + active_event = window_minimized_event = False + # make sure we cycle the event loop enough to get the display + # hidden. Test that both ACTIVEEVENT and WINDOWMINIMISED event appears + for _ in range(50): + time.sleep(0.01) + for event in pygame.event.get(): + if event.type == pygame.ACTIVEEVENT: + if not event.gain and event.state == pygame.APPACTIVE: + active_event = True + if event.type == pygame.WINDOWMINIMIZED: + window_minimized_event = True + + self.assertTrue(window_minimized_event) + self.assertTrue(active_event) + self.assertFalse(pygame.display.get_active()) + + else: + self.fail("Iconify not supported on this platform, please skip") + + def test_init(self): + """Ensures the module is initialized after init called.""" + # display.init() already called in setUp(), so quit and re-init + display.quit() + display.init() + + self.assertTrue(display.get_init()) + + def test_init__multiple(self): + """Ensures the module is initialized after multiple init calls.""" + display.init() + display.init() + + self.assertTrue(display.get_init()) + + def test_list_modes(self): + modes = pygame.display.list_modes(depth=0, flags=pygame.FULLSCREEN, display=0) + # modes == -1 means any mode is supported. + if modes != -1: + self.assertEqual(len(modes[0]), 2) + self.assertEqual(type(modes[0][0]), int) + + modes = pygame.display.list_modes() + if modes != -1: + self.assertEqual(len(modes[0]), 2) + self.assertEqual(type(modes[0][0]), int) + self.assertEqual(len(modes), len(set(modes))) + + modes = pygame.display.list_modes(depth=0, flags=0, display=0) + if modes != -1: + self.assertEqual(len(modes[0]), 2) + self.assertEqual(type(modes[0][0]), int) + + def test_mode_ok(self): + pygame.display.mode_ok((128, 128)) + modes = pygame.display.list_modes() + if modes != -1: + size = modes[0] + self.assertNotEqual(pygame.display.mode_ok(size), 0) + + pygame.display.mode_ok((128, 128), 0, 32) + pygame.display.mode_ok((128, 128), flags=0, depth=32, display=0) + + def test_mode_ok_fullscreen(self): + modes = pygame.display.list_modes() + if modes != -1: + size = modes[0] + self.assertNotEqual( + pygame.display.mode_ok(size, flags=pygame.FULLSCREEN), 0 + ) + + def test_mode_ok_scaled(self): + modes = pygame.display.list_modes() + if modes != -1: + size = modes[0] + self.assertNotEqual(pygame.display.mode_ok(size, flags=pygame.SCALED), 0) + + def test_get_num_displays(self): + self.assertGreater(pygame.display.get_num_displays(), 0) + + def test_quit(self): + """Ensures the module is not initialized after quit called.""" + display.quit() + + self.assertFalse(display.get_init()) + + def test_quit__multiple(self): + """Ensures the module is not initialized after multiple quit calls.""" + display.quit() + display.quit() + + self.assertFalse(display.get_init()) + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", "Needs a not dummy videodriver" + ) + def test_set_gamma(self): + pygame.display.set_mode((1, 1)) + + gammas = [0.25, 0.5, 0.88, 1.0] + for gamma in gammas: + with self.subTest(gamma=gamma): + with self.assertWarns(DeprecationWarning): + self.assertEqual(pygame.display.set_gamma(gamma), True) + self.assertEqual(pygame.display.set_gamma(gamma), True) + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", "Needs a not dummy videodriver" + ) + def test_set_gamma__tuple(self): + pygame.display.set_mode((1, 1)) + + gammas = [(0.5, 0.5, 0.5), (1.0, 1.0, 1.0), (0.25, 0.33, 0.44)] + for r, g, b in gammas: + with self.subTest(r=r, g=g, b=b): + self.assertEqual(pygame.display.set_gamma(r, g, b), True) + + @unittest.skipIf( + not hasattr(pygame.display, "set_gamma_ramp"), + "Not all systems and hardware support gamma ramps", + ) + def test_set_gamma_ramp(self): + # __doc__ (as of 2008-08-02) for pygame.display.set_gamma_ramp: + + # change the hardware gamma ramps with a custom lookup + # pygame.display.set_gamma_ramp(red, green, blue): return bool + # set_gamma_ramp(red, green, blue): return bool + # + # Set the red, green, and blue gamma ramps with an explicit lookup + # table. Each argument should be sequence of 256 integers. The + # integers should range between 0 and 0xffff. Not all systems and + # hardware support gamma ramps, if the function succeeds it will + # return True. + # + pygame.display.set_mode((5, 5)) + r = list(range(256)) + g = [number + 256 for number in r] + b = [number + 256 for number in g] + with self.assertWarns(DeprecationWarning): + isSupported = pygame.display.set_gamma_ramp(r, g, b) + if isSupported: + self.assertTrue(pygame.display.set_gamma_ramp(r, g, b)) + else: + self.assertFalse(pygame.display.set_gamma_ramp(r, g, b)) + + def test_set_mode_kwargs(self): + pygame.display.set_mode(size=(1, 1), flags=0, depth=0, display=0) + + def test_set_mode_scaled(self): + surf = pygame.display.set_mode( + size=(1, 1), flags=pygame.SCALED, depth=0, display=0 + ) + winsize = pygame.display.get_window_size() + self.assertEqual( + winsize[0] % surf.get_size()[0], + 0, + "window width should be a multiple of the surface width", + ) + self.assertEqual( + winsize[1] % surf.get_size()[1], + 0, + "window height should be a multiple of the surface height", + ) + self.assertEqual( + winsize[0] / surf.get_size()[0], winsize[1] / surf.get_size()[1] + ) + + def test_set_mode_vector2(self): + pygame.display.set_mode(pygame.Vector2(1, 1)) + + def test_set_mode_unscaled(self): + """Ensures a window created with SCALED can become smaller.""" + # see https://github.com/pygame/pygame/issues/2327 + + screen = pygame.display.set_mode((300, 300), pygame.SCALED) + self.assertEqual(screen.get_size(), (300, 300)) + + screen = pygame.display.set_mode((200, 200)) + self.assertEqual(screen.get_size(), (200, 200)) + + def test_screensaver_support(self): + pygame.display.set_allow_screensaver(True) + self.assertTrue(pygame.display.get_allow_screensaver()) + pygame.display.set_allow_screensaver(False) + self.assertFalse(pygame.display.get_allow_screensaver()) + pygame.display.set_allow_screensaver() + self.assertTrue(pygame.display.get_allow_screensaver()) + + # the following test fails always with SDL2 + @unittest.skipIf(True, "set_palette() not supported in SDL2") + def test_set_palette(self): + with self.assertRaises(pygame.error): + palette = [1, 2, 3] + pygame.display.set_palette(palette) + pygame.display.set_mode((1024, 768), 0, 8) + palette = [] + self.assertIsNone(pygame.display.set_palette(palette)) + + with self.assertRaises(ValueError): + palette = 12 + pygame.display.set_palette(palette) + with self.assertRaises(TypeError): + palette = [[1, 2], [1, 2]] + pygame.display.set_palette(palette) + with self.assertRaises(TypeError): + palette = [[0, 0, 0, 0, 0]] + [[x, x, x, x, x] for x in range(1, 255)] + pygame.display.set_palette(palette) + with self.assertRaises(TypeError): + palette = "qwerty" + pygame.display.set_palette(palette) + with self.assertRaises(TypeError): + palette = [[123, 123, 123] * 10000] + pygame.display.set_palette(palette) + with self.assertRaises(TypeError): + palette = [1, 2, 3] + pygame.display.set_palette(palette) + + skip_list = ["dummy", "android"] + + @unittest.skipIf(True, "set_palette() not supported in SDL2") + def test_set_palette_kwargs(self): + with self.assertRaises(pygame.error): + palette = [1, 2, 3] + pygame.display.set_palette(palette=palette) + pygame.display.set_mode((1024, 768), 0, 8) + palette = [] + self.assertIsNone(pygame.display.set_palette(palette=palette)) + + with self.assertRaises(ValueError): + palette = 12 + pygame.display.set_palette(palette=palette) + with self.assertRaises(TypeError): + palette = [[1, 2], [1, 2]] + pygame.display.set_palette(palette=palette) + with self.assertRaises(TypeError): + palette = [[0, 0, 0, 0, 0]] + [[x, x, x, x, x] for x in range(1, 255)] + pygame.display.set_palette(palette=palette) + with self.assertRaises(TypeError): + palette = "qwerty" + pygame.display.set_palette(palette=palette) + with self.assertRaises(TypeError): + palette = [[123, 123, 123] * 10000] + pygame.display.set_palette(palette=palette) + with self.assertRaises(TypeError): + palette = [1, 2, 3] + pygame.display.set_palette(palette=palette) + + skip_list = ["dummy", "android"] + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") in skip_list, + "requires the SDL_VIDEODRIVER to be non dummy", + ) + def test_toggle_fullscreen(self): + """Test for toggle fullscreen""" + + # try to toggle fullscreen with no active display + # this should result in an error + pygame.display.quit() + with self.assertRaises(pygame.error): + pygame.display.toggle_fullscreen() + + pygame.display.init() + width_height = (640, 480) + test_surf = pygame.display.set_mode(width_height) + + # try to toggle fullscreen + try: + pygame.display.toggle_fullscreen() + + except pygame.error: + self.fail() + + else: + # if toggle success, the width/height should be a + # value found in list_modes + if pygame.display.toggle_fullscreen() == 1: + boolean = ( + test_surf.get_width(), + test_surf.get_height(), + ) in pygame.display.list_modes( + depth=0, flags=pygame.FULLSCREEN, display=0 + ) + + self.assertEqual(boolean, True) + + # if not original width/height should be preserved + else: + self.assertEqual( + (test_surf.get_width(), test_surf.get_height()), width_height + ) + + +class DisplayUpdateTest(unittest.TestCase): + def question(self, qstr): + """this is used in the interactive subclass.""" + + def setUp(self): + display.init() + self.screen = pygame.display.set_mode((500, 500)) + self.screen.fill("black") + pygame.display.flip() + pygame.event.pump() # so mac updates + + def tearDown(self): + display.quit() + + def test_update_negative(self): + """takes rects with negative values.""" + self.screen.fill("green") + + r1 = pygame.Rect(0, 0, 100, 100) + pygame.display.update(r1) + + r2 = pygame.Rect(-10, 0, 100, 100) + pygame.display.update(r2) + + r3 = pygame.Rect(-10, 0, -100, -100) + pygame.display.update(r3) + + self.question("Is the screen green in (0, 0, 100, 100)?") + + def test_update_sequence(self): + """only updates the part of the display given by the rects.""" + self.screen.fill("green") + rects = [ + pygame.Rect(0, 0, 100, 100), + pygame.Rect(100, 0, 100, 100), + pygame.Rect(200, 0, 100, 100), + pygame.Rect(300, 300, 100, 100), + ] + pygame.display.update(rects) + pygame.event.pump() # so mac updates + + self.question(f"Is the screen green in {rects}?") + + def test_update_none_skipped(self): + """None is skipped inside sequences.""" + self.screen.fill("green") + rects = ( + None, + pygame.Rect(100, 0, 100, 100), + None, + pygame.Rect(200, 0, 100, 100), + pygame.Rect(300, 300, 100, 100), + ) + pygame.display.update(rects) + pygame.event.pump() # so mac updates + + self.question(f"Is the screen green in {rects}?") + + def test_update_none(self): + """does NOT update the display.""" + self.screen.fill("green") + pygame.display.update(None) + pygame.event.pump() # so mac updates + self.question("Is the screen black and NOT green?") + + def test_update_no_args(self): + """does NOT update the display.""" + self.screen.fill("green") + pygame.display.update() + pygame.event.pump() # so mac updates + self.question("Is the WHOLE screen green?") + + def test_update_args(self): + """updates the display using the args as a rect.""" + self.screen.fill("green") + pygame.display.update(100, 100, 100, 100) + pygame.event.pump() # so mac updates + self.question("Is the screen green in (100, 100, 100, 100)?") + + def test_update_incorrect_args(self): + """raises a ValueError when inputs are wrong.""" + + with self.assertRaises(ValueError): + pygame.display.update(100, "asdf", 100, 100) + + with self.assertRaises(ValueError): + pygame.display.update([100, "asdf", 100, 100]) + + def test_update_no_init(self): + """raises a pygame.error.""" + + pygame.display.quit() + with self.assertRaises(pygame.error): + pygame.display.update() + + +class DisplayUpdateInteractiveTest(DisplayUpdateTest): + """Because we want these tests to run as interactive and not interactive.""" + + __tags__ = ["interactive"] + + def question(self, qstr): + """since this is the interactive subclass we ask a question.""" + question(qstr) + + +class DisplayInteractiveTest(unittest.TestCase): + __tags__ = ["interactive"] + + def test_set_icon_interactive(self): + os.environ["SDL_VIDEO_WINDOW_POS"] = "100,250" + pygame.display.quit() + pygame.display.init() + + test_icon = pygame.Surface((32, 32)) + test_icon.fill((255, 0, 0)) + + pygame.display.set_icon(test_icon) + screen = pygame.display.set_mode((400, 100)) + pygame.display.set_caption("Is the window icon a red square?") + + response = question("Is the display icon red square?") + + self.assertTrue(response) + pygame.display.quit() + + def test_set_gamma_ramp(self): + os.environ["SDL_VIDEO_WINDOW_POS"] = "100,250" + pygame.display.quit() + pygame.display.init() + + screen = pygame.display.set_mode((400, 100)) + screen.fill((100, 100, 100)) + + blue_ramp = [x * 256 for x in range(0, 256)] + blue_ramp[100] = 150 * 256 # Can't tint too far or gamma ramps fail + normal_ramp = [x * 256 for x in range(0, 256)] + # test to see if this platform supports gamma ramps + gamma_success = False + if pygame.display.set_gamma_ramp(normal_ramp, normal_ramp, blue_ramp): + pygame.display.update() + gamma_success = True + + if gamma_success: + response = question("Is the window background tinted blue?") + self.assertTrue(response) + # restore normal ramp + pygame.display.set_gamma_ramp(normal_ramp, normal_ramp, normal_ramp) + + pygame.display.quit() + + +class FullscreenToggleTests(unittest.TestCase): + __tags__ = ["interactive"] + + screen = None + font = None + isfullscreen = False + + WIDTH = 800 + HEIGHT = 600 + + def setUp(self): + pygame.init() + if sys.platform == "win32": + # known issue with windows, must have mode from pygame.display.list_modes() + # or window created with flag pygame.SCALED + self.screen = pygame.display.set_mode( + (self.WIDTH, self.HEIGHT), flags=pygame.SCALED + ) + else: + self.screen = pygame.display.set_mode((self.WIDTH, self.HEIGHT)) + pygame.display.set_caption("Fullscreen Tests") + self.screen.fill((255, 255, 255)) + pygame.display.flip() + self.font = pygame.font.Font(None, 32) + + def tearDown(self): + if self.isfullscreen: + pygame.display.toggle_fullscreen() + pygame.quit() + + def visual_test(self, fullscreen=False): + text = "" + if fullscreen: + if not self.isfullscreen: + pygame.display.toggle_fullscreen() + self.isfullscreen = True + text = "Is this in fullscreen? [y/n]" + else: + if self.isfullscreen: + pygame.display.toggle_fullscreen() + self.isfullscreen = False + text = "Is this not in fullscreen [y/n]" + s = self.font.render(text, False, (0, 0, 0)) + self.screen.blit(s, (self.WIDTH / 2 - self.font.size(text)[0] / 2, 100)) + pygame.display.flip() + + while True: + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + return False + if event.key == pygame.K_y: + return True + if event.key == pygame.K_n: + return False + if event.type == pygame.QUIT: + return False + + def test_fullscreen_true(self): + self.assertTrue(self.visual_test(fullscreen=True)) + + def test_fullscreen_false(self): + self.assertTrue(self.visual_test(fullscreen=False)) + + +@unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", + 'OpenGL requires a non-"dummy" SDL_VIDEODRIVER', +) +class DisplayOpenGLTest(unittest.TestCase): + def test_screen_size_opengl(self): + """returns a surface with the same size requested. + |tags:display,slow,opengl| + """ + pygame.display.init() + screen = pygame.display.set_mode((640, 480), pygame.OPENGL) + self.assertEqual((640, 480), screen.get_size()) + + +class X11CrashTest(unittest.TestCase): + def test_x11_set_mode_crash_gh1654(self): + # Test for https://github.com/pygame/pygame/issues/1654 + # If unfixed, this will trip a segmentation fault + pygame.display.init() + pygame.display.quit() + screen = pygame.display.set_mode((640, 480), 0) + self.assertEqual((640, 480), screen.get_size()) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/docs_test.py b/laplas/abstract_map/pygame/tests/docs_test.py new file mode 100644 index 0000000..de021a8 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/docs_test.py @@ -0,0 +1,35 @@ +import os +import subprocess +import sys +import unittest + + +class DocsIncludedTest(unittest.TestCase): + def test_doc_import_works(self): + from pygame.docs.__main__ import has_local_docs, open_docs + + @unittest.skipIf("CI" not in os.environ, "Docs not required for local builds") + def test_docs_included(self): + from pygame.docs.__main__ import has_local_docs + + self.assertTrue(has_local_docs()) + + @unittest.skipIf("CI" not in os.environ, "Docs not required for local builds") + def test_docs_command(self): + try: + subprocess.run( + [sys.executable, "-m", "pygame.docs"], + timeout=5, + # check ensures an exception is raised when the process fails + check=True, + # pipe stdout/stderr so that they don't clutter main stdout + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except subprocess.TimeoutExpired: + # timeout errors are not an issue + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/draw_test.py b/laplas/abstract_map/pygame/tests/draw_test.py new file mode 100644 index 0000000..a33f864 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/draw_test.py @@ -0,0 +1,6577 @@ +import math +import unittest +import sys +import warnings + +import pygame +from pygame import draw +from pygame import draw_py +from pygame.locals import SRCALPHA +from pygame.tests import test_utils +from pygame.math import Vector2 + + +RED = BG_RED = pygame.Color("red") +GREEN = FG_GREEN = pygame.Color("green") + +# Clockwise from the top left corner and ending with the center point. +RECT_POSITION_ATTRIBUTES = ( + "topleft", + "midtop", + "topright", + "midright", + "bottomright", + "midbottom", + "bottomleft", + "midleft", + "center", +) + + +def get_border_values(surface, width, height): + """Returns a list containing lists with the values of the surface's + borders. + """ + border_top = [surface.get_at((x, 0)) for x in range(width)] + border_left = [surface.get_at((0, y)) for y in range(height)] + border_right = [surface.get_at((width - 1, y)) for y in range(height)] + border_bottom = [surface.get_at((x, height - 1)) for x in range(width)] + + return [border_top, border_left, border_right, border_bottom] + + +def corners(surface): + """Returns a tuple with the corner positions of the given surface. + + Clockwise from the top left corner. + """ + width, height = surface.get_size() + return ((0, 0), (width - 1, 0), (width - 1, height - 1), (0, height - 1)) + + +def rect_corners_mids_and_center(rect): + """Returns a tuple with each corner, mid, and the center for a given rect. + + Clockwise from the top left corner and ending with the center point. + """ + return ( + rect.topleft, + rect.midtop, + rect.topright, + rect.midright, + rect.bottomright, + rect.midbottom, + rect.bottomleft, + rect.midleft, + rect.center, + ) + + +def border_pos_and_color(surface): + """Yields each border position and its color for a given surface. + + Clockwise from the top left corner. + """ + width, height = surface.get_size() + right, bottom = width - 1, height - 1 + + # Top edge. + for x in range(width): + pos = (x, 0) + yield pos, surface.get_at(pos) + + # Right edge. + # Top right done in top edge loop. + for y in range(1, height): + pos = (right, y) + yield pos, surface.get_at(pos) + + # Bottom edge. + # Bottom right done in right edge loop. + for x in range(right - 1, -1, -1): + pos = (x, bottom) + yield pos, surface.get_at(pos) + + # Left edge. + # Bottom left done in bottom edge loop. Top left done in top edge loop. + for y in range(bottom - 1, 0, -1): + pos = (0, y) + yield pos, surface.get_at(pos) + + +def get_color_points(surface, color, bounds_rect=None, match_color=True): + """Get all the points of a given color on the surface within the given + bounds. + + If bounds_rect is None the full surface is checked. + If match_color is True, all points matching the color are returned, + otherwise all points not matching the color are returned. + """ + get_at = surface.get_at # For possible speed up. + + if bounds_rect is None: + x_range = range(surface.get_width()) + y_range = range(surface.get_height()) + else: + x_range = range(bounds_rect.left, bounds_rect.right) + y_range = range(bounds_rect.top, bounds_rect.bottom) + + surface.lock() # For possible speed up. + + if match_color: + pts = [(x, y) for x in x_range for y in y_range if get_at((x, y)) == color] + else: + pts = [(x, y) for x in x_range for y in y_range if get_at((x, y)) != color] + + surface.unlock() + return pts + + +def create_bounding_rect(surface, surf_color, default_pos): + """Create a rect to bound all the pixels that don't match surf_color. + + The default_pos parameter is used to position the bounding rect for the + case where all pixels match the surf_color. + """ + width, height = surface.get_clip().size + xmin, ymin = width, height + xmax, ymax = -1, -1 + get_at = surface.get_at # For possible speed up. + + surface.lock() # For possible speed up. + + for y in range(height): + for x in range(width): + if get_at((x, y)) != surf_color: + xmin = min(x, xmin) + xmax = max(x, xmax) + ymin = min(y, ymin) + ymax = max(y, ymax) + + surface.unlock() + + if -1 == xmax: + # No points means a 0 sized rect positioned at default_pos. + return pygame.Rect(default_pos, (0, 0)) + return pygame.Rect((xmin, ymin), (xmax - xmin + 1, ymax - ymin + 1)) + + +class InvalidBool: + """To help test invalid bool values.""" + + __bool__ = None + + +class DrawTestCase(unittest.TestCase): + """Base class to test draw module functions.""" + + draw_rect = staticmethod(draw.rect) + draw_polygon = staticmethod(draw.polygon) + draw_circle = staticmethod(draw.circle) + draw_ellipse = staticmethod(draw.ellipse) + draw_arc = staticmethod(draw.arc) + draw_line = staticmethod(draw.line) + draw_lines = staticmethod(draw.lines) + draw_aaline = staticmethod(draw.aaline) + draw_aalines = staticmethod(draw.aalines) + + +class PythonDrawTestCase(unittest.TestCase): + """Base class to test draw_py module functions.""" + + # draw_py is currently missing some functions. + # draw_rect = staticmethod(draw_py.draw_rect) + draw_polygon = staticmethod(draw_py.draw_polygon) + # draw_circle = staticmethod(draw_py.draw_circle) + # draw_ellipse = staticmethod(draw_py.draw_ellipse) + # draw_arc = staticmethod(draw_py.draw_arc) + draw_line = staticmethod(draw_py.draw_line) + draw_lines = staticmethod(draw_py.draw_lines) + draw_aaline = staticmethod(draw_py.draw_aaline) + draw_aalines = staticmethod(draw_py.draw_aalines) + + +### Ellipse Testing ########################################################### + + +class DrawEllipseMixin: + """Mixin tests for drawing ellipses. + + This class contains all the general ellipse drawing tests. + """ + + def test_ellipse__args(self): + """Ensures draw ellipse accepts the correct args.""" + bounds_rect = self.draw_ellipse( + pygame.Surface((3, 3)), (0, 10, 0, 50), pygame.Rect((0, 0), (3, 2)), 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__args_without_width(self): + """Ensures draw ellipse accepts the args without a width.""" + bounds_rect = self.draw_ellipse( + pygame.Surface((2, 2)), (1, 1, 1, 99), pygame.Rect((1, 1), (1, 1)) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__args_with_negative_width(self): + """Ensures draw ellipse accepts the args with negative width.""" + bounds_rect = self.draw_ellipse( + pygame.Surface((3, 3)), (0, 10, 0, 50), pygame.Rect((2, 3), (3, 2)), -1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + self.assertEqual(bounds_rect, pygame.Rect(2, 3, 0, 0)) + + def test_ellipse__args_with_width_gt_radius(self): + """Ensures draw ellipse accepts the args with + width > rect.w // 2 and width > rect.h // 2. + """ + rect = pygame.Rect((0, 0), (4, 4)) + bounds_rect = self.draw_ellipse( + pygame.Surface((3, 3)), (0, 10, 0, 50), rect, rect.w // 2 + 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + bounds_rect = self.draw_ellipse( + pygame.Surface((3, 3)), (0, 10, 0, 50), rect, rect.h // 2 + 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__kwargs(self): + """Ensures draw ellipse accepts the correct kwargs + with and without a width arg. + """ + kwargs_list = [ + { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("yellow"), + "rect": pygame.Rect((0, 0), (3, 2)), + "width": 1, + }, + { + "surface": pygame.Surface((2, 1)), + "color": (0, 10, 20), + "rect": (0, 0, 1, 1), + }, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_ellipse(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__kwargs_order_independent(self): + """Ensures draw ellipse's kwargs are not order dependent.""" + bounds_rect = self.draw_ellipse( + color=(1, 2, 3), + surface=pygame.Surface((3, 2)), + width=0, + rect=pygame.Rect((1, 0), (1, 1)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__args_missing(self): + """Ensures draw ellipse detects any missing required args.""" + surface = pygame.Surface((1, 1)) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_ellipse(surface, pygame.Color("red")) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_ellipse(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_ellipse() + + def test_ellipse__kwargs_missing(self): + """Ensures draw ellipse detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((1, 2)), + "color": pygame.Color("red"), + "rect": pygame.Rect((1, 0), (2, 2)), + "width": 2, + } + + for name in ("rect", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_ellipse(**invalid_kwargs) + + def test_ellipse__arg_invalid_types(self): + """Ensures draw ellipse detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + rect = pygame.Rect((1, 1), (1, 1)) + + with self.assertRaises(TypeError): + # Invalid width. + bounds_rect = self.draw_ellipse(surface, color, rect, "1") + + with self.assertRaises(TypeError): + # Invalid rect. + bounds_rect = self.draw_ellipse(surface, color, (1, 2, 3, 4, 5), 1) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_ellipse(surface, 2.3, rect, 0) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_ellipse(rect, color, rect, 2) + + def test_ellipse__kwarg_invalid_types(self): + """Ensures draw ellipse detects invalid kwarg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("green") + rect = pygame.Rect((0, 1), (1, 1)) + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "rect": rect, + "width": 1, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "rect": rect, + "width": 1, + }, + { + "surface": surface, + "color": color, + "rect": (0, 0, 0), # Invalid rect. + "width": 1, + }, + {"surface": surface, "color": color, "rect": rect, "width": 1.1}, + ] # Invalid width. + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_ellipse(**kwargs) + + def test_ellipse__kwarg_invalid_name(self): + """Ensures draw ellipse detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + rect = pygame.Rect((0, 1), (2, 2)) + kwargs_list = [ + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1, + "invalid": 1, + }, + {"surface": surface, "color": color, "rect": rect, "invalid": 1}, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_ellipse(**kwargs) + + def test_ellipse__args_and_kwargs(self): + """Ensures draw ellipse accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 1)) + color = (255, 255, 0, 0) + rect = pygame.Rect((1, 0), (2, 1)) + width = 0 + kwargs = {"surface": surface, "color": color, "rect": rect, "width": width} + + for name in ("surface", "color", "rect", "width"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_ellipse(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_ellipse(surface, color, **kwargs) + elif "rect" == name: + bounds_rect = self.draw_ellipse(surface, color, rect, **kwargs) + else: + bounds_rect = self.draw_ellipse(surface, color, rect, width, **kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__valid_width_values(self): + """Ensures draw ellipse accepts different width values.""" + pos = (1, 1) + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + color = (10, 20, 30, 255) + kwargs = { + "surface": surface, + "color": color, + "rect": pygame.Rect(pos, (3, 2)), + "width": None, + } + + for width in (-1000, -10, -1, 0, 1, 10, 1000): + surface.fill(surface_color) # Clear for each test. + kwargs["width"] = width + expected_color = color if width >= 0 else surface_color + + bounds_rect = self.draw_ellipse(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__valid_rect_formats(self): + """Ensures draw ellipse accepts different rect formats.""" + pos = (1, 1) + expected_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((4, 4)) + kwargs = {"surface": surface, "color": expected_color, "rect": None, "width": 0} + rects = (pygame.Rect(pos, (1, 3)), (pos, (2, 1)), (pos[0], pos[1], 1, 1)) + + for rect in rects: + surface.fill(surface_color) # Clear for each test. + kwargs["rect"] = rect + + bounds_rect = self.draw_ellipse(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__valid_color_formats(self): + """Ensures draw ellipse accepts different color formats.""" + pos = (1, 1) + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": None, + "rect": pygame.Rect(pos, (1, 2)), + "width": 0, + } + reds = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in reds: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_ellipse(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_ellipse__invalid_color_formats(self): + """Ensures draw ellipse handles invalid color formats correctly.""" + pos = (1, 1) + surface = pygame.Surface((4, 3)) + kwargs = { + "surface": surface, + "color": None, + "rect": pygame.Rect(pos, (2, 2)), + "width": 1, + } + + for expected_color in (2.3, surface): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_ellipse(**kwargs) + + def test_ellipse(self): + """Tests ellipses of differing sizes on surfaces of differing sizes. + + Checks if the number of sides touching the border of the surface is + correct. + """ + left_top = [(0, 0), (1, 0), (0, 1), (1, 1)] + sizes = [(4, 4), (5, 4), (4, 5), (5, 5)] + color = (1, 13, 24, 255) + + def same_size(width, height, border_width): + """Test for ellipses with the same size as the surface.""" + surface = pygame.Surface((width, height)) + + self.draw_ellipse(surface, color, (0, 0, width, height), border_width) + + # For each of the four borders check if it contains the color + borders = get_border_values(surface, width, height) + for border in borders: + self.assertTrue(color in border) + + def not_same_size(width, height, border_width, left, top): + """Test for ellipses that aren't the same size as the surface.""" + surface = pygame.Surface((width, height)) + + self.draw_ellipse( + surface, color, (left, top, width - 1, height - 1), border_width + ) + + borders = get_border_values(surface, width, height) + + # Check if two sides of the ellipse are touching the border + sides_touching = [color in border for border in borders].count(True) + self.assertEqual(sides_touching, 2) + + for width, height in sizes: + for border_width in (0, 1): + same_size(width, height, border_width) + for left, top in left_top: + not_same_size(width, height, border_width, left, top) + + def test_ellipse__big_ellipse(self): + """Test for big ellipse that could overflow in algorithm""" + width = 1025 + height = 1025 + border = 1 + x_value_test = int(0.4 * height) + y_value_test = int(0.4 * height) + surface = pygame.Surface((width, height)) + + self.draw_ellipse(surface, (255, 0, 0), (0, 0, width, height), border) + colored_pixels = 0 + for y in range(height): + if surface.get_at((x_value_test, y)) == (255, 0, 0): + colored_pixels += 1 + for x in range(width): + if surface.get_at((x, y_value_test)) == (255, 0, 0): + colored_pixels += 1 + self.assertEqual(colored_pixels, border * 4) + + def test_ellipse__thick_line(self): + """Ensures a thick lined ellipse is drawn correctly.""" + ellipse_color = pygame.Color("yellow") + surface_color = pygame.Color("black") + surface = pygame.Surface((40, 40)) + rect = pygame.Rect((0, 0), (31, 23)) + rect.center = surface.get_rect().center + + # As the lines get thicker the internals of the ellipse are not + # cleanly defined. So only test up to a few thicknesses before the + # maximum thickness. + for thickness in range(1, min(*rect.size) // 2 - 2): + surface.fill(surface_color) # Clear for each test. + + self.draw_ellipse(surface, ellipse_color, rect, thickness) + + surface.lock() # For possible speed up. + + # Check vertical thickness on the ellipse's top. + x = rect.centerx + y_start = rect.top + y_end = rect.top + thickness - 1 + + for y in range(y_start, y_end + 1): + self.assertEqual(surface.get_at((x, y)), ellipse_color, thickness) + + # Check pixels above and below this line. + self.assertEqual(surface.get_at((x, y_start - 1)), surface_color, thickness) + self.assertEqual(surface.get_at((x, y_end + 1)), surface_color, thickness) + + # Check vertical thickness on the ellipse's bottom. + x = rect.centerx + y_start = rect.bottom - thickness + y_end = rect.bottom - 1 + + for y in range(y_start, y_end + 1): + self.assertEqual(surface.get_at((x, y)), ellipse_color, thickness) + + # Check pixels above and below this line. + self.assertEqual(surface.get_at((x, y_start - 1)), surface_color, thickness) + self.assertEqual(surface.get_at((x, y_end + 1)), surface_color, thickness) + + # Check horizontal thickness on the ellipse's left. + x_start = rect.left + x_end = rect.left + thickness - 1 + y = rect.centery + + for x in range(x_start, x_end + 1): + self.assertEqual(surface.get_at((x, y)), ellipse_color, thickness) + + # Check pixels to the left and right of this line. + self.assertEqual(surface.get_at((x_start - 1, y)), surface_color, thickness) + self.assertEqual(surface.get_at((x_end + 1, y)), surface_color, thickness) + + # Check horizontal thickness on the ellipse's right. + x_start = rect.right - thickness + x_end = rect.right - 1 + y = rect.centery + + for x in range(x_start, x_end + 1): + self.assertEqual(surface.get_at((x, y)), ellipse_color, thickness) + + # Check pixels to the left and right of this line. + self.assertEqual(surface.get_at((x_start - 1, y)), surface_color, thickness) + self.assertEqual(surface.get_at((x_end + 1, y)), surface_color, thickness) + + surface.unlock() + + def test_ellipse__no_holes(self): + width = 80 + height = 70 + surface = pygame.Surface((width + 1, height)) + rect = pygame.Rect(0, 0, width, height) + for thickness in range(1, 37, 5): + surface.fill("BLACK") + self.draw_ellipse(surface, "RED", rect, thickness) + for y in range(height): + number_of_changes = 0 + drawn_pixel = False + for x in range(width + 1): + if ( + not drawn_pixel + and surface.get_at((x, y)) == pygame.Color("RED") + or drawn_pixel + and surface.get_at((x, y)) == pygame.Color("BLACK") + ): + drawn_pixel = not drawn_pixel + number_of_changes += 1 + if y < thickness or y > height - thickness - 1: + self.assertEqual(number_of_changes, 2) + else: + self.assertEqual(number_of_changes, 4) + + def test_ellipse__max_width(self): + """Ensures an ellipse with max width (and greater) is drawn correctly.""" + ellipse_color = pygame.Color("yellow") + surface_color = pygame.Color("black") + surface = pygame.Surface((40, 40)) + rect = pygame.Rect((0, 0), (31, 21)) + rect.center = surface.get_rect().center + max_thickness = (min(*rect.size) + 1) // 2 + + for thickness in range(max_thickness, max_thickness + 3): + surface.fill(surface_color) # Clear for each test. + + self.draw_ellipse(surface, ellipse_color, rect, thickness) + + surface.lock() # For possible speed up. + + # Check vertical thickness. + for y in range(rect.top, rect.bottom): + self.assertEqual(surface.get_at((rect.centerx, y)), ellipse_color) + + # Check horizontal thickness. + for x in range(rect.left, rect.right): + self.assertEqual(surface.get_at((x, rect.centery)), ellipse_color) + + # Check pixels above and below ellipse. + self.assertEqual( + surface.get_at((rect.centerx, rect.top - 1)), surface_color + ) + self.assertEqual( + surface.get_at((rect.centerx, rect.bottom + 1)), surface_color + ) + + # Check pixels to the left and right of the ellipse. + self.assertEqual( + surface.get_at((rect.left - 1, rect.centery)), surface_color + ) + self.assertEqual( + surface.get_at((rect.right + 1, rect.centery)), surface_color + ) + + surface.unlock() + + def _check_1_pixel_sized_ellipse( + self, surface, collide_rect, surface_color, ellipse_color + ): + # Helper method to check the surface for 1 pixel wide and/or high + # ellipses. + surf_w, surf_h = surface.get_size() + + surface.lock() # For possible speed up. + + for pos in ((x, y) for y in range(surf_h) for x in range(surf_w)): + # Since the ellipse is just a line we can use a rect to help find + # where it is expected to be drawn. + if collide_rect.collidepoint(pos): + expected_color = ellipse_color + else: + expected_color = surface_color + + self.assertEqual( + surface.get_at(pos), + expected_color, + f"collide_rect={collide_rect}, pos={pos}", + ) + + surface.unlock() + + def test_ellipse__1_pixel_width(self): + """Ensures an ellipse with a width of 1 is drawn correctly. + + An ellipse with a width of 1 pixel is a vertical line. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 10, 20 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (1, 0)) + collide_rect = rect.copy() + + # Calculate some positions. + off_left = -1 + off_right = surf_w + off_bottom = surf_h + center_x = surf_w // 2 + center_y = surf_h // 2 + + # Test some even and odd heights. + for ellipse_h in range(6, 10): + collide_rect.h = ellipse_h + rect.h = ellipse_h + + # Calculate some variable positions. + off_top = -(ellipse_h + 1) + half_off_top = -(ellipse_h // 2) + half_off_bottom = surf_h - (ellipse_h // 2) + + # Draw the ellipse in different positions: fully on-surface, + # partially off-surface, and fully off-surface. + positions = ( + (off_left, off_top), + (off_left, half_off_top), + (off_left, center_y), + (off_left, half_off_bottom), + (off_left, off_bottom), + (center_x, off_top), + (center_x, half_off_top), + (center_x, center_y), + (center_x, half_off_bottom), + (center_x, off_bottom), + (off_right, off_top), + (off_right, half_off_top), + (off_right, center_y), + (off_right, half_off_bottom), + (off_right, off_bottom), + ) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + collide_rect.topleft = rect_pos + + self.draw_ellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_ellipse( + surface, collide_rect, surface_color, ellipse_color + ) + + def test_ellipse__1_pixel_width_spanning_surface(self): + """Ensures an ellipse with a width of 1 is drawn correctly + when spanning the height of the surface. + + An ellipse with a width of 1 pixel is a vertical line. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 10, 20 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (1, surf_h + 2)) # Longer than the surface. + + # Draw the ellipse in different positions: on-surface and off-surface. + positions = ( + (-1, -1), # (off_left, off_top) + (0, -1), # (left_edge, off_top) + (surf_w // 2, -1), # (center_x, off_top) + (surf_w - 1, -1), # (right_edge, off_top) + (surf_w, -1), + ) # (off_right, off_top) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + + self.draw_ellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_ellipse( + surface, rect, surface_color, ellipse_color + ) + + def test_ellipse__1_pixel_height(self): + """Ensures an ellipse with a height of 1 is drawn correctly. + + An ellipse with a height of 1 pixel is a horizontal line. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 20, 10 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (0, 1)) + collide_rect = rect.copy() + + # Calculate some positions. + off_right = surf_w + off_top = -1 + off_bottom = surf_h + center_x = surf_w // 2 + center_y = surf_h // 2 + + # Test some even and odd widths. + for ellipse_w in range(6, 10): + collide_rect.w = ellipse_w + rect.w = ellipse_w + + # Calculate some variable positions. + off_left = -(ellipse_w + 1) + half_off_left = -(ellipse_w // 2) + half_off_right = surf_w - (ellipse_w // 2) + + # Draw the ellipse in different positions: fully on-surface, + # partially off-surface, and fully off-surface. + positions = ( + (off_left, off_top), + (half_off_left, off_top), + (center_x, off_top), + (half_off_right, off_top), + (off_right, off_top), + (off_left, center_y), + (half_off_left, center_y), + (center_x, center_y), + (half_off_right, center_y), + (off_right, center_y), + (off_left, off_bottom), + (half_off_left, off_bottom), + (center_x, off_bottom), + (half_off_right, off_bottom), + (off_right, off_bottom), + ) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + collide_rect.topleft = rect_pos + + self.draw_ellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_ellipse( + surface, collide_rect, surface_color, ellipse_color + ) + + def test_ellipse__1_pixel_height_spanning_surface(self): + """Ensures an ellipse with a height of 1 is drawn correctly + when spanning the width of the surface. + + An ellipse with a height of 1 pixel is a horizontal line. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 20, 10 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (surf_w + 2, 1)) # Wider than the surface. + + # Draw the ellipse in different positions: on-surface and off-surface. + positions = ( + (-1, -1), # (off_left, off_top) + (-1, 0), # (off_left, top_edge) + (-1, surf_h // 2), # (off_left, center_y) + (-1, surf_h - 1), # (off_left, bottom_edge) + (-1, surf_h), + ) # (off_left, off_bottom) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + + self.draw_ellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_ellipse( + surface, rect, surface_color, ellipse_color + ) + + def test_ellipse__1_pixel_width_and_height(self): + """Ensures an ellipse with a width and height of 1 is drawn correctly. + + An ellipse with a width and height of 1 pixel is a single pixel. + """ + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("black") + surf_w, surf_h = 10, 10 + + surface = pygame.Surface((surf_w, surf_h)) + rect = pygame.Rect((0, 0), (1, 1)) + + # Calculate some positions. + off_left = -1 + off_right = surf_w + off_top = -1 + off_bottom = surf_h + left_edge = 0 + right_edge = surf_w - 1 + top_edge = 0 + bottom_edge = surf_h - 1 + center_x = surf_w // 2 + center_y = surf_h // 2 + + # Draw the ellipse in different positions: center surface, + # top/bottom/left/right edges, and off-surface. + positions = ( + (off_left, off_top), + (off_left, top_edge), + (off_left, center_y), + (off_left, bottom_edge), + (off_left, off_bottom), + (left_edge, off_top), + (left_edge, top_edge), + (left_edge, center_y), + (left_edge, bottom_edge), + (left_edge, off_bottom), + (center_x, off_top), + (center_x, top_edge), + (center_x, center_y), + (center_x, bottom_edge), + (center_x, off_bottom), + (right_edge, off_top), + (right_edge, top_edge), + (right_edge, center_y), + (right_edge, bottom_edge), + (right_edge, off_bottom), + (off_right, off_top), + (off_right, top_edge), + (off_right, center_y), + (off_right, bottom_edge), + (off_right, off_bottom), + ) + + for rect_pos in positions: + surface.fill(surface_color) # Clear before each draw. + rect.topleft = rect_pos + + self.draw_ellipse(surface, ellipse_color, rect) + + self._check_1_pixel_sized_ellipse( + surface, rect, surface_color, ellipse_color + ) + + def test_ellipse__bounding_rect(self): + """Ensures draw ellipse returns the correct bounding rect. + + Tests ellipses on and off the surface and a range of width/thickness + values. + """ + ellipse_color = pygame.Color("red") + surf_color = pygame.Color("black") + min_width = min_height = 5 + max_width = max_height = 7 + sizes = ((min_width, min_height), (max_width, max_height)) + surface = pygame.Surface((20, 20), 0, 32) + surf_rect = surface.get_rect() + # Make a rect that is bigger than the surface to help test drawing + # ellipses off and partially off the surface. + big_rect = surf_rect.inflate(min_width * 2 + 1, min_height * 2 + 1) + + for pos in rect_corners_mids_and_center( + surf_rect + ) + rect_corners_mids_and_center(big_rect): + # Each of the ellipse's rect position attributes will be set to + # the pos value. + for attr in RECT_POSITION_ATTRIBUTES: + # Test using different rect sizes and thickness values. + for width, height in sizes: + ellipse_rect = pygame.Rect((0, 0), (width, height)) + setattr(ellipse_rect, attr, pos) + + for thickness in (0, 1, 2, 3, min(width, height)): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_ellipse( + surface, ellipse_color, ellipse_rect, thickness + ) + + # Calculating the expected_rect after the ellipse + # is drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect( + surface, surf_color, ellipse_rect.topleft + ) + + self.assertEqual(bounding_rect, expected_rect) + + def test_ellipse__surface_clip(self): + """Ensures draw ellipse respects a surface's clip area. + + Tests drawing the ellipse filled and unfilled. + """ + surfw = surfh = 30 + ellipse_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (11, 11)) + clip_rect.center = surface.get_rect().center + pos_rect = clip_rect.copy() # Manages the ellipse's pos. + + for width in (0, 1): # Filled and unfilled. + # Test centering the ellipse along the clip rect's edge. + for center in rect_corners_mids_and_center(clip_rect): + # Get the expected points by drawing the ellipse without the + # clip area set. + pos_rect.center = center + surface.set_clip(None) + surface.fill(surface_color) + self.draw_ellipse(surface, ellipse_color, pos_rect, width) + expected_pts = get_color_points(surface, ellipse_color, clip_rect) + + # Clear the surface and set the clip area. Redraw the ellipse + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_ellipse(surface, ellipse_color, pos_rect, width) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure only the expected_pts + # are the ellipse_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + expected_color = ellipse_color + else: + expected_color = surface_color + + self.assertEqual(surface.get_at(pt), expected_color, pt) + + surface.unlock() + + +class DrawEllipseTest(DrawEllipseMixin, DrawTestCase): + """Test draw module function ellipse. + + This class inherits the general tests from DrawEllipseMixin. It is also + the class to add any draw.ellipse specific tests to. + """ + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever properly supports drawing ellipses. +# @unittest.skip('draw_py.draw_ellipse not supported yet') +# class PythonDrawEllipseTest(DrawEllipseMixin, PythonDrawTestCase): +# """Test draw_py module function draw_ellipse. +# +# This class inherits the general tests from DrawEllipseMixin. It is also +# the class to add any draw_py.draw_ellipse specific tests to. +# """ + + +### Line/Lines/AALine/AALines Testing ######################################### + + +class BaseLineMixin: + """Mixin base for drawing various lines. + + This class contains general helper methods and setup for testing the + different types of lines. + """ + + COLORS = ( + (0, 0, 0), + (255, 0, 0), + (0, 255, 0), + (0, 0, 255), + (255, 255, 0), + (255, 0, 255), + (0, 255, 255), + (255, 255, 255), + ) + + @staticmethod + def _create_surfaces(): + # Create some surfaces with different sizes, depths, and flags. + surfaces = [] + for size in ((49, 49), (50, 50)): + for depth in (8, 16, 24, 32): + for flags in (0, SRCALPHA): + surface = pygame.display.set_mode(size, flags, depth) + surfaces.append(surface) + surfaces.append(surface.convert_alpha()) + return surfaces + + @staticmethod + def _rect_lines(rect): + # Yields pairs of end points and their reverse (to test symmetry). + # Uses a rect with the points radiating from its midleft. + for pt in rect_corners_mids_and_center(rect): + if pt in [rect.midleft, rect.center]: + # Don't bother with these points. + continue + yield (rect.midleft, pt) + yield (pt, rect.midleft) + + +### Line Testing ############################################################## + + +class LineMixin(BaseLineMixin): + """Mixin test for drawing a single line. + + This class contains all the general single line drawing tests. + """ + + def test_line__args(self): + """Ensures draw line accepts the correct args.""" + bounds_rect = self.draw_line( + pygame.Surface((3, 3)), (0, 10, 0, 50), (0, 0), (1, 1), 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__args_without_width(self): + """Ensures draw line accepts the args without a width.""" + bounds_rect = self.draw_line( + pygame.Surface((2, 2)), (0, 0, 0, 50), (0, 0), (2, 2) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__kwargs(self): + """Ensures draw line accepts the correct kwargs + with and without a width arg. + """ + surface = pygame.Surface((4, 4)) + color = pygame.Color("yellow") + start_pos = (1, 1) + end_pos = (2, 2) + kwargs_list = [ + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + "width": 1, + }, + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + }, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_line(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__kwargs_order_independent(self): + """Ensures draw line's kwargs are not order dependent.""" + bounds_rect = self.draw_line( + start_pos=(1, 2), + end_pos=(2, 1), + width=2, + color=(10, 20, 30), + surface=pygame.Surface((3, 2)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__args_missing(self): + """Ensures draw line detects any missing required args.""" + surface = pygame.Surface((1, 1)) + color = pygame.Color("blue") + + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(surface, color, (0, 0)) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(surface, color) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_line() + + def test_line__kwargs_missing(self): + """Ensures draw line detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((3, 2)), + "color": pygame.Color("red"), + "start_pos": (2, 1), + "end_pos": (2, 2), + "width": 1, + } + + for name in ("end_pos", "start_pos", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(**invalid_kwargs) + + def test_line__arg_invalid_types(self): + """Ensures draw line detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + start_pos = (0, 1) + end_pos = (1, 2) + + with self.assertRaises(TypeError): + # Invalid width. + bounds_rect = self.draw_line(surface, color, start_pos, end_pos, "1") + + with self.assertRaises(TypeError): + # Invalid end_pos. + bounds_rect = self.draw_line(surface, color, start_pos, (1, 2, 3)) + + with self.assertRaises(TypeError): + # Invalid start_pos. + bounds_rect = self.draw_line(surface, color, (1,), end_pos) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_line(surface, 2.3, start_pos, end_pos) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_line((1, 2, 3, 4), color, start_pos, end_pos) + + def test_line__kwarg_invalid_types(self): + """Ensures draw line detects invalid kwarg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("green") + start_pos = (1, 0) + end_pos = (2, 0) + width = 1 + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + "width": width, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "start_pos": start_pos, + "end_pos": end_pos, + "width": width, + }, + { + "surface": surface, + "color": color, + "start_pos": (0, 0, 0), # Invalid start_pos. + "end_pos": end_pos, + "width": width, + }, + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": (0,), # Invalid end_pos. + "width": width, + }, + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + "width": 1.2, + }, + ] # Invalid width. + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(**kwargs) + + def test_line__kwarg_invalid_name(self): + """Ensures draw line detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + start_pos = (1, 1) + end_pos = (2, 0) + kwargs_list = [ + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + "width": 1, + "invalid": 1, + }, + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + "invalid": 1, + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(**kwargs) + + def test_line__args_and_kwargs(self): + """Ensures draw line accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 2)) + color = (255, 255, 0, 0) + start_pos = (0, 1) + end_pos = (1, 2) + width = 0 + kwargs = { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + "width": width, + } + + for name in ("surface", "color", "start_pos", "end_pos", "width"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_line(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_line(surface, color, **kwargs) + elif "start_pos" == name: + bounds_rect = self.draw_line(surface, color, start_pos, **kwargs) + elif "end_pos" == name: + bounds_rect = self.draw_line( + surface, color, start_pos, end_pos, **kwargs + ) + else: + bounds_rect = self.draw_line( + surface, color, start_pos, end_pos, width, **kwargs + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__valid_width_values(self): + """Ensures draw line accepts different width values.""" + line_color = pygame.Color("yellow") + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + pos = (2, 1) + kwargs = { + "surface": surface, + "color": line_color, + "start_pos": pos, + "end_pos": (2, 2), + "width": None, + } + + for width in (-100, -10, -1, 0, 1, 10, 100): + surface.fill(surface_color) # Clear for each test. + kwargs["width"] = width + expected_color = line_color if width > 0 else surface_color + + bounds_rect = self.draw_line(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__valid_start_pos_formats(self): + """Ensures draw line accepts different start_pos formats.""" + expected_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((4, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "start_pos": None, + "end_pos": (2, 2), + "width": 2, + } + x, y = 2, 1 # start position + + # The point values can be ints or floats. + for start_pos in ((x, y), (x + 0.1, y), (x, y + 0.1), (x + 0.1, y + 0.1)): + # The point type can be a tuple/list/Vector2. + for seq_type in (tuple, list, Vector2): + surface.fill(surface_color) # Clear for each test. + kwargs["start_pos"] = seq_type(start_pos) + + bounds_rect = self.draw_line(**kwargs) + + self.assertEqual(surface.get_at((x, y)), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__valid_end_pos_formats(self): + """Ensures draw line accepts different end_pos formats.""" + expected_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((4, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "start_pos": (2, 1), + "end_pos": None, + "width": 2, + } + x, y = 2, 2 # end position + + # The point values can be ints or floats. + for end_pos in ((x, y), (x + 0.2, y), (x, y + 0.2), (x + 0.2, y + 0.2)): + # The point type can be a tuple/list/Vector2. + for seq_type in (tuple, list, Vector2): + surface.fill(surface_color) # Clear for each test. + kwargs["end_pos"] = seq_type(end_pos) + + bounds_rect = self.draw_line(**kwargs) + + self.assertEqual(surface.get_at((x, y)), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__invalid_start_pos_formats(self): + """Ensures draw line handles invalid start_pos formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "start_pos": None, + "end_pos": (2, 2), + "width": 1, + } + + start_pos_fmts = ( + (2,), # Too few coords. + (2, 1, 0), # Too many coords. + (2, "1"), # Wrong type. + {2, 1}, # Wrong type. + {2: 1}, + ) # Wrong type. + + for start_pos in start_pos_fmts: + kwargs["start_pos"] = start_pos + + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(**kwargs) + + def test_line__invalid_end_pos_formats(self): + """Ensures draw line handles invalid end_pos formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "start_pos": (2, 2), + "end_pos": None, + "width": 1, + } + + end_pos_fmts = ( + (2,), # Too few coords. + (2, 1, 0), # Too many coords. + (2, "1"), # Wrong type. + {2, 1}, # Wrong type. + {2: 1}, + ) # Wrong type. + + for end_pos in end_pos_fmts: + kwargs["end_pos"] = end_pos + + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(**kwargs) + + def test_line__valid_color_formats(self): + """Ensures draw line accepts different color formats.""" + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + pos = (1, 1) + kwargs = { + "surface": surface, + "color": None, + "start_pos": pos, + "end_pos": (2, 1), + "width": 3, + } + greens = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in greens: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_line(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_line__invalid_color_formats(self): + """Ensures draw line handles invalid color formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 3)), + "color": None, + "start_pos": (1, 1), + "end_pos": (2, 1), + "width": 1, + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_line(**kwargs) + + def test_line__color(self): + """Tests if the line drawn is the correct color.""" + pos = (0, 0) + for surface in self._create_surfaces(): + for expected_color in self.COLORS: + self.draw_line(surface, expected_color, pos, (1, 0)) + + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + + def test_line__color_with_thickness(self): + """Ensures a thick line is drawn using the correct color.""" + from_x = 5 + to_x = 10 + y = 5 + for surface in self._create_surfaces(): + for expected_color in self.COLORS: + self.draw_line(surface, expected_color, (from_x, y), (to_x, y), 5) + for pos in ((x, y + i) for i in (-2, 0, 2) for x in (from_x, to_x)): + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + + def test_line__gaps(self): + """Tests if the line drawn contains any gaps.""" + expected_color = (255, 255, 255) + for surface in self._create_surfaces(): + width = surface.get_width() + self.draw_line(surface, expected_color, (0, 0), (width - 1, 0)) + + for x in range(width): + pos = (x, 0) + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + + def test_line__gaps_with_thickness(self): + """Ensures a thick line is drawn without any gaps.""" + expected_color = (255, 255, 255) + thickness = 5 + for surface in self._create_surfaces(): + width = surface.get_width() - 1 + h = width // 5 + w = h * 5 + self.draw_line(surface, expected_color, (0, 5), (w, 5 + h), thickness) + + for x in range(w + 1): + for y in range(3, 8): + pos = (x, y + ((x + 2) // 5)) + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + + def test_line__bounding_rect(self): + """Ensures draw line returns the correct bounding rect. + + Tests lines with endpoints on and off the surface and a range of + width/thickness values. + """ + if isinstance(self, PythonDrawTestCase): + self.skipTest("bounding rects not supported in draw_py.draw_line") + + line_color = pygame.Color("red") + surf_color = pygame.Color("black") + width = height = 30 + # Using a rect to help manage where the lines are drawn. + helper_rect = pygame.Rect((0, 0), (width, height)) + + # Testing surfaces of different sizes. One larger than the helper_rect + # and one smaller (to test lines that span the surface). + for size in ((width + 5, height + 5), (width - 5, height - 5)): + surface = pygame.Surface(size, 0, 32) + surf_rect = surface.get_rect() + + # Move the helper rect to different positions to test line + # endpoints on and off the surface. + for pos in rect_corners_mids_and_center(surf_rect): + helper_rect.center = pos + + # Draw using different thicknesses. + for thickness in range(-1, 5): + for start, end in self._rect_lines(helper_rect): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_line( + surface, line_color, start, end, thickness + ) + + if 0 < thickness: + # Calculating the expected_rect after the line is + # drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect( + surface, surf_color, start + ) + else: + # Nothing drawn. + expected_rect = pygame.Rect(start, (0, 0)) + + self.assertEqual( + bounding_rect, + expected_rect, + "start={}, end={}, size={}, thickness={}".format( + start, end, size, thickness + ), + ) + + def test_line__surface_clip(self): + """Ensures draw line respects a surface's clip area.""" + surfw = surfh = 30 + line_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (11, 11)) + clip_rect.center = surface.get_rect().center + pos_rect = clip_rect.copy() # Manages the line's pos. + + for thickness in (1, 3): # Test different line widths. + # Test centering the line along the clip rect's edge. + for center in rect_corners_mids_and_center(clip_rect): + # Get the expected points by drawing the line without the + # clip area set. + pos_rect.center = center + surface.set_clip(None) + surface.fill(surface_color) + self.draw_line( + surface, line_color, pos_rect.midtop, pos_rect.midbottom, thickness + ) + expected_pts = get_color_points(surface, line_color, clip_rect) + + # Clear the surface and set the clip area. Redraw the line + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_line( + surface, line_color, pos_rect.midtop, pos_rect.midbottom, thickness + ) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure only the expected_pts + # are the line_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + expected_color = line_color + else: + expected_color = surface_color + + self.assertEqual(surface.get_at(pt), expected_color, pt) + + surface.unlock() + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever fully supports drawing single lines. +# @unittest.skip('draw_py.draw_line not fully supported yet') +# class PythonDrawLineTest(LineMixin, PythonDrawTestCase): +# """Test draw_py module function line. +# +# This class inherits the general tests from LineMixin. It is also the class +# to add any draw_py.draw_line specific tests to. +# """ + + +class DrawLineTest(LineMixin, DrawTestCase): + """Test draw module function line. + + This class inherits the general tests from LineMixin. It is also the class + to add any draw.line specific tests to. + """ + + def test_line_endianness(self): + """test color component order""" + for depth in (24, 32): + surface = pygame.Surface((5, 3), 0, depth) + surface.fill(pygame.Color(0, 0, 0)) + self.draw_line(surface, pygame.Color(255, 0, 0), (0, 1), (2, 1), 1) + + self.assertGreater(surface.get_at((1, 1)).r, 0, "there should be red here") + + surface.fill(pygame.Color(0, 0, 0)) + self.draw_line(surface, pygame.Color(0, 0, 255), (0, 1), (2, 1), 1) + + self.assertGreater(surface.get_at((1, 1)).b, 0, "there should be blue here") + + def test_line(self): + # (l, t), (l, t) + self.surf_size = (320, 200) + self.surf = pygame.Surface(self.surf_size, pygame.SRCALPHA) + self.color = (1, 13, 24, 205) + + drawn = draw.line(self.surf, self.color, (1, 0), (200, 0)) + self.assertEqual( + drawn.right, 201, "end point arg should be (or at least was) inclusive" + ) + + # Should be colored where it's supposed to be + for pt in test_utils.rect_area_pts(drawn): + self.assertEqual(self.surf.get_at(pt), self.color) + + # And not where it shouldn't + for pt in test_utils.rect_outer_bounds(drawn): + self.assertNotEqual(self.surf.get_at(pt), self.color) + + # Line width greater that 1 + line_width = 2 + offset = 5 + a = (offset, offset) + b = (self.surf_size[0] - offset, a[1]) + c = (a[0], self.surf_size[1] - offset) + d = (b[0], c[1]) + e = (a[0] + offset, c[1]) + f = (b[0], c[0] + 5) + lines = [ + (a, d), + (b, c), + (c, b), + (d, a), + (a, b), + (b, a), + (a, c), + (c, a), + (a, e), + (e, a), + (a, f), + (f, a), + (a, a), + ] + + for p1, p2 in lines: + msg = f"{p1} - {p2}" + if p1[0] <= p2[0]: + plow = p1 + phigh = p2 + else: + plow = p2 + phigh = p1 + + self.surf.fill((0, 0, 0)) + rec = draw.line(self.surf, (255, 255, 255), p1, p2, line_width) + xinc = yinc = 0 + + if abs(p1[0] - p2[0]) > abs(p1[1] - p2[1]): + yinc = 1 + else: + xinc = 1 + + for i in range(line_width): + p = (p1[0] + xinc * i, p1[1] + yinc * i) + self.assertEqual(self.surf.get_at(p), (255, 255, 255), msg) + + p = (p2[0] + xinc * i, p2[1] + yinc * i) + self.assertEqual(self.surf.get_at(p), (255, 255, 255), msg) + + p = (plow[0] - 1, plow[1]) + self.assertEqual(self.surf.get_at(p), (0, 0, 0), msg) + + p = (plow[0] + xinc * line_width, plow[1] + yinc * line_width) + self.assertEqual(self.surf.get_at(p), (0, 0, 0), msg) + + p = (phigh[0] + xinc * line_width, phigh[1] + yinc * line_width) + self.assertEqual(self.surf.get_at(p), (0, 0, 0), msg) + + if p1[0] < p2[0]: + rx = p1[0] + else: + rx = p2[0] + + if p1[1] < p2[1]: + ry = p1[1] + else: + ry = p2[1] + + w = abs(p2[0] - p1[0]) + 1 + xinc * (line_width - 1) + h = abs(p2[1] - p1[1]) + 1 + yinc * (line_width - 1) + msg += f", {rec}" + + self.assertEqual(rec, (rx, ry, w, h), msg) + + def test_line_for_gaps(self): + # This checks bug Thick Line Bug #448 + + width = 200 + height = 200 + surf = pygame.Surface((width, height), pygame.SRCALPHA) + + def white_surrounded_pixels(x, y): + offsets = [(1, 0), (0, 1), (-1, 0), (0, -1)] + WHITE = (255, 255, 255, 255) + return len( + [1 for dx, dy in offsets if surf.get_at((x + dx, y + dy)) == WHITE] + ) + + def check_white_line(start, end): + surf.fill((0, 0, 0)) + pygame.draw.line(surf, (255, 255, 255), start, end, 30) + + BLACK = (0, 0, 0, 255) + for x in range(1, width - 1): + for y in range(1, height - 1): + if surf.get_at((x, y)) == BLACK: + self.assertTrue(white_surrounded_pixels(x, y) < 3) + + check_white_line((50, 50), (140, 0)) + check_white_line((50, 50), (0, 120)) + check_white_line((50, 50), (199, 198)) + + +### Lines Testing ############################################################# + + +class LinesMixin(BaseLineMixin): + """Mixin test for drawing lines. + + This class contains all the general lines drawing tests. + """ + + def test_lines__args(self): + """Ensures draw lines accepts the correct args.""" + bounds_rect = self.draw_lines( + pygame.Surface((3, 3)), (0, 10, 0, 50), False, ((0, 0), (1, 1)), 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__args_without_width(self): + """Ensures draw lines accepts the args without a width.""" + bounds_rect = self.draw_lines( + pygame.Surface((2, 2)), (0, 0, 0, 50), False, ((0, 0), (1, 1)) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__kwargs(self): + """Ensures draw lines accepts the correct kwargs + with and without a width arg. + """ + surface = pygame.Surface((4, 4)) + color = pygame.Color("yellow") + points = ((0, 0), (1, 1), (2, 2)) + kwargs_list = [ + { + "surface": surface, + "color": color, + "closed": False, + "points": points, + "width": 1, + }, + {"surface": surface, "color": color, "closed": False, "points": points}, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_lines(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__kwargs_order_independent(self): + """Ensures draw lines's kwargs are not order dependent.""" + bounds_rect = self.draw_lines( + closed=1, + points=((0, 0), (1, 1), (2, 2)), + width=2, + color=(10, 20, 30), + surface=pygame.Surface((3, 2)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__args_missing(self): + """Ensures draw lines detects any missing required args.""" + surface = pygame.Surface((1, 1)) + color = pygame.Color("blue") + + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines(surface, color, 0) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines(surface, color) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines() + + def test_lines__kwargs_missing(self): + """Ensures draw lines detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((3, 2)), + "color": pygame.Color("red"), + "closed": 1, + "points": ((2, 2), (1, 1)), + "width": 1, + } + + for name in ("points", "closed", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines(**invalid_kwargs) + + def test_lines__arg_invalid_types(self): + """Ensures draw lines detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + closed = 0 + points = ((1, 2), (2, 1)) + + with self.assertRaises(TypeError): + # Invalid width. + bounds_rect = self.draw_lines(surface, color, closed, points, "1") + + with self.assertRaises(TypeError): + # Invalid points. + bounds_rect = self.draw_lines(surface, color, closed, (1, 2, 3)) + + with self.assertRaises(TypeError): + # Invalid closed. + bounds_rect = self.draw_lines(surface, color, InvalidBool(), points) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_lines(surface, 2.3, closed, points) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_lines((1, 2, 3, 4), color, closed, points) + + def test_lines__kwarg_invalid_types(self): + """Ensures draw lines detects invalid kwarg types.""" + valid_kwargs = { + "surface": pygame.Surface((3, 3)), + "color": pygame.Color("green"), + "closed": False, + "points": ((1, 2), (2, 1)), + "width": 1, + } + + invalid_kwargs = { + "surface": pygame.Surface, + "color": 2.3, + "closed": InvalidBool(), + "points": (0, 0, 0), + "width": 1.2, + } + + for kwarg in ("surface", "color", "closed", "points", "width"): + kwargs = dict(valid_kwargs) + kwargs[kwarg] = invalid_kwargs[kwarg] + + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines(**kwargs) + + def test_lines__kwarg_invalid_name(self): + """Ensures draw lines detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + closed = 1 + points = ((1, 2), (2, 1)) + kwargs_list = [ + { + "surface": surface, + "color": color, + "closed": closed, + "points": points, + "width": 1, + "invalid": 1, + }, + { + "surface": surface, + "color": color, + "closed": closed, + "points": points, + "invalid": 1, + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines(**kwargs) + + def test_lines__args_and_kwargs(self): + """Ensures draw lines accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 2)) + color = (255, 255, 0, 0) + closed = 0 + points = ((1, 2), (2, 1)) + width = 1 + kwargs = { + "surface": surface, + "color": color, + "closed": closed, + "points": points, + "width": width, + } + + for name in ("surface", "color", "closed", "points", "width"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_lines(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_lines(surface, color, **kwargs) + elif "closed" == name: + bounds_rect = self.draw_lines(surface, color, closed, **kwargs) + elif "points" == name: + bounds_rect = self.draw_lines(surface, color, closed, points, **kwargs) + else: + bounds_rect = self.draw_lines( + surface, color, closed, points, width, **kwargs + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__valid_width_values(self): + """Ensures draw lines accepts different width values.""" + line_color = pygame.Color("yellow") + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + pos = (1, 1) + kwargs = { + "surface": surface, + "color": line_color, + "closed": False, + "points": (pos, (2, 1)), + "width": None, + } + + for width in (-100, -10, -1, 0, 1, 10, 100): + surface.fill(surface_color) # Clear for each test. + kwargs["width"] = width + expected_color = line_color if width > 0 else surface_color + + bounds_rect = self.draw_lines(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__valid_points_format(self): + """Ensures draw lines accepts different points formats.""" + expected_color = (10, 20, 30, 255) + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "closed": False, + "points": None, + "width": 1, + } + + # The point type can be a tuple/list/Vector2. + point_types = ( + (tuple, tuple, tuple, tuple), # all tuples + (list, list, list, list), # all lists + (Vector2, Vector2, Vector2, Vector2), # all Vector2s + (list, Vector2, tuple, Vector2), + ) # mix + + # The point values can be ints or floats. + point_values = ( + ((1, 1), (2, 1), (2, 2), (1, 2)), + ((1, 1), (2.2, 1), (2.1, 2.2), (1, 2.1)), + ) + + # Each sequence of points can be a tuple or a list. + seq_types = (tuple, list) + + for point_type in point_types: + for values in point_values: + check_pos = values[0] + points = [point_type[i](pt) for i, pt in enumerate(values)] + + for seq_type in seq_types: + surface.fill(surface_color) # Clear for each test. + kwargs["points"] = seq_type(points) + + bounds_rect = self.draw_lines(**kwargs) + + self.assertEqual(surface.get_at(check_pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__invalid_points_formats(self): + """Ensures draw lines handles invalid points formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "closed": False, + "points": None, + "width": 1, + } + + points_fmts = ( + ((1, 1), (2,)), # Too few coords. + ((1, 1), (2, 2, 2)), # Too many coords. + ((1, 1), (2, "2")), # Wrong type. + ((1, 1), {2, 3}), # Wrong type. + ((1, 1), {2: 2, 3: 3}), # Wrong type. + {(1, 1), (1, 2)}, # Wrong type. + {1: 1, 4: 4}, + ) # Wrong type. + + for points in points_fmts: + kwargs["points"] = points + + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines(**kwargs) + + def test_lines__invalid_points_values(self): + """Ensures draw lines handles invalid points values correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "closed": False, + "points": None, + "width": 1, + } + + for points in ([], ((1, 1),)): # Too few points. + for seq_type in (tuple, list): # Test as tuples and lists. + kwargs["points"] = seq_type(points) + + with self.assertRaises(ValueError): + bounds_rect = self.draw_lines(**kwargs) + + def test_lines__valid_closed_values(self): + """Ensures draw lines accepts different closed values.""" + line_color = pygame.Color("blue") + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + pos = (1, 2) + kwargs = { + "surface": surface, + "color": line_color, + "closed": None, + "points": ((1, 1), (3, 1), (3, 3), (1, 3)), + "width": 1, + } + + true_values = (-7, 1, 10, "2", 3.1, (4,), [5], True) + false_values = (None, "", 0, (), [], False) + + for closed in true_values + false_values: + surface.fill(surface_color) # Clear for each test. + kwargs["closed"] = closed + expected_color = line_color if closed else surface_color + + bounds_rect = self.draw_lines(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__valid_color_formats(self): + """Ensures draw lines accepts different color formats.""" + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + pos = (1, 1) + kwargs = { + "surface": surface, + "color": None, + "closed": False, + "points": (pos, (2, 1)), + "width": 3, + } + greens = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in greens: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_lines(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_lines__invalid_color_formats(self): + """Ensures draw lines handles invalid color formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 3)), + "color": None, + "closed": False, + "points": ((1, 1), (1, 2)), + "width": 1, + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_lines(**kwargs) + + def test_lines__color(self): + """Tests if the lines drawn are the correct color. + + Draws lines around the border of the given surface and checks if all + borders of the surface only contain the given color. + """ + for surface in self._create_surfaces(): + for expected_color in self.COLORS: + self.draw_lines(surface, expected_color, True, corners(surface)) + + for pos, color in border_pos_and_color(surface): + self.assertEqual(color, expected_color, f"pos={pos}") + + def test_lines__color_with_thickness(self): + """Ensures thick lines are drawn using the correct color.""" + x_left = y_top = 5 + for surface in self._create_surfaces(): + x_right = surface.get_width() - 5 + y_bottom = surface.get_height() - 5 + endpoints = ( + (x_left, y_top), + (x_right, y_top), + (x_right, y_bottom), + (x_left, y_bottom), + ) + for expected_color in self.COLORS: + self.draw_lines(surface, expected_color, True, endpoints, 3) + + for t in (-1, 0, 1): + for x in range(x_left, x_right + 1): + for y in (y_top, y_bottom): + pos = (x, y + t) + self.assertEqual( + surface.get_at(pos), + expected_color, + f"pos={pos}", + ) + for y in range(y_top, y_bottom + 1): + for x in (x_left, x_right): + pos = (x + t, y) + self.assertEqual( + surface.get_at(pos), + expected_color, + f"pos={pos}", + ) + + def test_lines__gaps(self): + """Tests if the lines drawn contain any gaps. + + Draws lines around the border of the given surface and checks if + all borders of the surface contain any gaps. + """ + expected_color = (255, 255, 255) + for surface in self._create_surfaces(): + self.draw_lines(surface, expected_color, True, corners(surface)) + + for pos, color in border_pos_and_color(surface): + self.assertEqual(color, expected_color, f"pos={pos}") + + def test_lines__gaps_with_thickness(self): + """Ensures thick lines are drawn without any gaps.""" + expected_color = (255, 255, 255) + x_left = y_top = 5 + for surface in self._create_surfaces(): + h = (surface.get_width() - 11) // 5 + w = h * 5 + x_right = x_left + w + y_bottom = y_top + h + endpoints = ((x_left, y_top), (x_right, y_top), (x_right, y_bottom)) + self.draw_lines(surface, expected_color, True, endpoints, 3) + + for x in range(x_left, x_right + 1): + for t in (-1, 0, 1): + pos = (x, y_top + t) + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + pos = (x, y_top + t + ((x - 3) // 5)) + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + for y in range(y_top, y_bottom + 1): + for t in (-1, 0, 1): + pos = (x_right + t, y) + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + + def test_lines__bounding_rect(self): + """Ensures draw lines returns the correct bounding rect. + + Tests lines with endpoints on and off the surface and a range of + width/thickness values. + """ + line_color = pygame.Color("red") + surf_color = pygame.Color("black") + width = height = 30 + # Using a rect to help manage where the lines are drawn. + pos_rect = pygame.Rect((0, 0), (width, height)) + + # Testing surfaces of different sizes. One larger than the pos_rect + # and one smaller (to test lines that span the surface). + for size in ((width + 5, height + 5), (width - 5, height - 5)): + surface = pygame.Surface(size, 0, 32) + surf_rect = surface.get_rect() + + # Move pos_rect to different positions to test line endpoints on + # and off the surface. + for pos in rect_corners_mids_and_center(surf_rect): + pos_rect.center = pos + # Shape: Triangle (if closed), ^ caret (if not closed). + pts = (pos_rect.midleft, pos_rect.midtop, pos_rect.midright) + pos = pts[0] # Rect position if nothing drawn. + + # Draw using different thickness and closed values. + for thickness in range(-1, 5): + for closed in (True, False): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_lines( + surface, line_color, closed, pts, thickness + ) + + if 0 < thickness: + # Calculating the expected_rect after the lines are + # drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect( + surface, surf_color, pos + ) + else: + # Nothing drawn. + expected_rect = pygame.Rect(pos, (0, 0)) + + self.assertEqual(bounding_rect, expected_rect) + + def test_lines__surface_clip(self): + """Ensures draw lines respects a surface's clip area.""" + surfw = surfh = 30 + line_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (11, 11)) + clip_rect.center = surface.get_rect().center + pos_rect = clip_rect.copy() # Manages the lines's pos. + + # Test centering the pos_rect along the clip rect's edge to allow for + # drawing the lines over the clip_rect's bounds. + for center in rect_corners_mids_and_center(clip_rect): + pos_rect.center = center + pts = (pos_rect.midtop, pos_rect.center, pos_rect.midbottom) + + for closed in (True, False): # Test closed and not closed. + for thickness in (1, 3): # Test different line widths. + # Get the expected points by drawing the lines without the + # clip area set. + surface.set_clip(None) + surface.fill(surface_color) + self.draw_lines(surface, line_color, closed, pts, thickness) + expected_pts = get_color_points(surface, line_color, clip_rect) + + # Clear the surface and set the clip area. Redraw the lines + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_lines(surface, line_color, closed, pts, thickness) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure only the + # expected_pts are the line_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + expected_color = line_color + else: + expected_color = surface_color + + self.assertEqual(surface.get_at(pt), expected_color, pt) + + surface.unlock() + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever fully supports drawing lines. +# class PythonDrawLinesTest(LinesMixin, PythonDrawTestCase): +# """Test draw_py module function lines. +# +# This class inherits the general tests from LinesMixin. It is also the +# class to add any draw_py.draw_lines specific tests to. +# """ + + +class DrawLinesTest(LinesMixin, DrawTestCase): + """Test draw module function lines. + + This class inherits the general tests from LinesMixin. It is also the class + to add any draw.lines specific tests to. + """ + + +### AALine Testing ############################################################ + + +class AALineMixin(BaseLineMixin): + """Mixin test for drawing a single aaline. + + This class contains all the general single aaline drawing tests. + """ + + def test_aaline__args(self): + """Ensures draw aaline accepts the correct args.""" + bounds_rect = self.draw_aaline( + pygame.Surface((3, 3)), (0, 10, 0, 50), (0, 0), (1, 1), 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaline__args_without_blend(self): + """Ensures draw aaline accepts the args without a blend.""" + bounds_rect = self.draw_aaline( + pygame.Surface((2, 2)), (0, 0, 0, 50), (0, 0), (2, 2) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaline__blend_warning(self): + """From pygame 2, blend=False should raise DeprecationWarning.""" + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger DeprecationWarning. + self.draw_aaline( + pygame.Surface((2, 2)), (0, 0, 0, 50), (0, 0), (2, 2), False + ) + # Check if there is only one warning and is a DeprecationWarning. + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) + + def test_aaline__kwargs(self): + """Ensures draw aaline accepts the correct kwargs""" + surface = pygame.Surface((4, 4)) + color = pygame.Color("yellow") + start_pos = (1, 1) + end_pos = (2, 2) + kwargs_list = [ + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + }, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_aaline(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaline__kwargs_order_independent(self): + """Ensures draw aaline's kwargs are not order dependent.""" + bounds_rect = self.draw_aaline( + start_pos=(1, 2), + end_pos=(2, 1), + color=(10, 20, 30), + surface=pygame.Surface((3, 2)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaline__args_missing(self): + """Ensures draw aaline detects any missing required args.""" + surface = pygame.Surface((1, 1)) + color = pygame.Color("blue") + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(surface, color, (0, 0)) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(surface, color) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline() + + def test_aaline__kwargs_missing(self): + """Ensures draw aaline detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((3, 2)), + "color": pygame.Color("red"), + "start_pos": (2, 1), + "end_pos": (2, 2), + } + + for name in ("end_pos", "start_pos", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(**invalid_kwargs) + + def test_aaline__arg_invalid_types(self): + """Ensures draw aaline detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + start_pos = (0, 1) + end_pos = (1, 2) + + with self.assertRaises(TypeError): + # Invalid end_pos. + bounds_rect = self.draw_aaline(surface, color, start_pos, (1, 2, 3)) + + with self.assertRaises(TypeError): + # Invalid start_pos. + bounds_rect = self.draw_aaline(surface, color, (1,), end_pos) + + with self.assertRaises(ValueError): + # Invalid color. + bounds_rect = self.draw_aaline(surface, "invalid-color", start_pos, end_pos) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_aaline((1, 2, 3, 4), color, start_pos, end_pos) + + def test_aaline__kwarg_invalid_types(self): + """Ensures draw aaline detects invalid kwarg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("green") + start_pos = (1, 0) + end_pos = (2, 0) + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "start_pos": start_pos, + "end_pos": end_pos, + }, + { + "surface": surface, + "color": color, + "start_pos": (0, 0, 0), # Invalid start_pos. + "end_pos": end_pos, + }, + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": (0,), # Invalid end_pos. + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(**kwargs) + + def test_aaline__kwarg_invalid_name(self): + """Ensures draw aaline detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + start_pos = (1, 1) + end_pos = (2, 0) + kwargs_list = [ + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + "invalid": 1, + }, + { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + "invalid": 1, + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(**kwargs) + + def test_aaline__args_and_kwargs(self): + """Ensures draw aaline accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 2)) + color = (255, 255, 0, 0) + start_pos = (0, 1) + end_pos = (1, 2) + kwargs = { + "surface": surface, + "color": color, + "start_pos": start_pos, + "end_pos": end_pos, + } + + for name in ("surface", "color", "start_pos", "end_pos"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_aaline(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_aaline(surface, color, **kwargs) + elif "start_pos" == name: + bounds_rect = self.draw_aaline(surface, color, start_pos, **kwargs) + elif "end_pos" == name: + bounds_rect = self.draw_aaline( + surface, color, start_pos, end_pos, **kwargs + ) + else: + bounds_rect = self.draw_aaline( + surface, color, start_pos, end_pos, **kwargs + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaline__valid_start_pos_formats(self): + """Ensures draw aaline accepts different start_pos formats.""" + expected_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((4, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "start_pos": None, + "end_pos": (2, 2), + } + x, y = 2, 1 # start position + positions = ((x, y), (x + 0.01, y), (x, y + 0.01), (x + 0.01, y + 0.01)) + + for start_pos in positions: + for seq_type in (tuple, list, Vector2): + surface.fill(surface_color) # Clear for each test. + kwargs["start_pos"] = seq_type(start_pos) + + bounds_rect = self.draw_aaline(**kwargs) + + color = surface.get_at((x, y)) + for i, sub_color in enumerate(expected_color): + # The color could be slightly off the expected color due to + # any fractional position arguments. + self.assertGreaterEqual(color[i] + 6, sub_color, start_pos) + self.assertIsInstance(bounds_rect, pygame.Rect, start_pos) + + def test_aaline__valid_end_pos_formats(self): + """Ensures draw aaline accepts different end_pos formats.""" + expected_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((4, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "start_pos": (2, 1), + "end_pos": None, + } + x, y = 2, 2 # end position + positions = ((x, y), (x + 0.02, y), (x, y + 0.02), (x + 0.02, y + 0.02)) + + for end_pos in positions: + for seq_type in (tuple, list, Vector2): + surface.fill(surface_color) # Clear for each test. + kwargs["end_pos"] = seq_type(end_pos) + + bounds_rect = self.draw_aaline(**kwargs) + + color = surface.get_at((x, y)) + for i, sub_color in enumerate(expected_color): + # The color could be slightly off the expected color due to + # any fractional position arguments. + self.assertGreaterEqual(color[i] + 15, sub_color, end_pos) + self.assertIsInstance(bounds_rect, pygame.Rect, end_pos) + + def test_aaline__invalid_start_pos_formats(self): + """Ensures draw aaline handles invalid start_pos formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "start_pos": None, + "end_pos": (2, 2), + } + + start_pos_fmts = ( + (2,), # Too few coords. + (2, 1, 0), # Too many coords. + (2, "1"), # Wrong type. + {2, 1}, # Wrong type. + {2: 1}, + ) # Wrong type. + + for start_pos in start_pos_fmts: + kwargs["start_pos"] = start_pos + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(**kwargs) + + def test_aaline__invalid_end_pos_formats(self): + """Ensures draw aaline handles invalid end_pos formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "start_pos": (2, 2), + "end_pos": None, + } + + end_pos_fmts = ( + (2,), # Too few coords. + (2, 1, 0), # Too many coords. + (2, "1"), # Wrong type. + {2, 1}, # Wrong type. + {2: 1}, + ) # Wrong type. + + for end_pos in end_pos_fmts: + kwargs["end_pos"] = end_pos + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(**kwargs) + + def test_aaline__valid_color_formats(self): + """Ensures draw aaline accepts different color formats.""" + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + pos = (1, 1) + kwargs = { + "surface": surface, + "color": None, + "start_pos": pos, + "end_pos": (2, 1), + } + greens = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in greens: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_aaline(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aaline__invalid_color_formats(self): + """Ensures draw aaline handles invalid color formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 3)), + "color": None, + "start_pos": (1, 1), + "end_pos": (2, 1), + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aaline(**kwargs) + + def test_aaline__color(self): + """Tests if the aaline drawn is the correct color.""" + pos = (0, 0) + for surface in self._create_surfaces(): + for expected_color in self.COLORS: + self.draw_aaline(surface, expected_color, pos, (1, 0)) + + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + + def test_aaline__gaps(self): + """Tests if the aaline drawn contains any gaps. + + See: #512 + """ + expected_color = (255, 255, 255) + for surface in self._create_surfaces(): + width = surface.get_width() + self.draw_aaline(surface, expected_color, (0, 0), (width - 1, 0)) + + for x in range(width): + pos = (x, 0) + self.assertEqual(surface.get_at(pos), expected_color, f"pos={pos}") + + def test_aaline__bounding_rect(self): + """Ensures draw aaline returns the correct bounding rect. + + Tests lines with endpoints on and off the surface. + """ + line_color = pygame.Color("red") + surf_color = pygame.Color("blue") + width = height = 30 + # Using a rect to help manage where the lines are drawn. + helper_rect = pygame.Rect((0, 0), (width, height)) + + # Testing surfaces of different sizes. One larger than the helper_rect + # and one smaller (to test lines that span the surface). + for size in ((width + 5, height + 5), (width - 5, height - 5)): + surface = pygame.Surface(size, 0, 32) + surf_rect = surface.get_rect() + + # Move the helper rect to different positions to test line + # endpoints on and off the surface. + for pos in rect_corners_mids_and_center(surf_rect): + helper_rect.center = pos + + for start, end in self._rect_lines(helper_rect): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_aaline(surface, line_color, start, end) + + # Calculating the expected_rect after the line is + # drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect(surface, surf_color, start) + + self.assertEqual(bounding_rect, expected_rect) + + def test_aaline__surface_clip(self): + """Ensures draw aaline respects a surface's clip area.""" + surfw = surfh = 30 + aaline_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (11, 11)) + clip_rect.center = surface.get_rect().center + pos_rect = clip_rect.copy() # Manages the aaline's pos. + + # Test centering the pos_rect along the clip rect's edge to allow for + # drawing the aaline over the clip_rect's bounds. + for center in rect_corners_mids_and_center(clip_rect): + pos_rect.center = center + + # Get the expected points by drawing the aaline without the + # clip area set. + surface.set_clip(None) + surface.fill(surface_color) + self.draw_aaline(surface, aaline_color, pos_rect.midtop, pos_rect.midbottom) + + expected_pts = get_color_points(surface, surface_color, clip_rect, False) + + # Clear the surface and set the clip area. Redraw the aaline + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_aaline(surface, aaline_color, pos_rect.midtop, pos_rect.midbottom) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure the expected_pts + # are not surface_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + self.assertNotEqual(surface.get_at(pt), surface_color, pt) + else: + self.assertEqual(surface.get_at(pt), surface_color, pt) + + surface.unlock() + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever fully supports drawing single aalines. +# class PythonDrawAALineTest(AALineMixin, PythonDrawTestCase): +# """Test draw_py module function aaline. +# +# This class inherits the general tests from AALineMixin. It is also the +# class to add any draw_py.draw_aaline specific tests to. +# """ + + +class DrawAALineTest(AALineMixin, DrawTestCase): + """Test draw module function aaline. + + This class inherits the general tests from AALineMixin. It is also the + class to add any draw.aaline specific tests to. + """ + + def test_aaline_endianness(self): + """test color component order""" + for depth in (24, 32): + surface = pygame.Surface((5, 3), 0, depth) + surface.fill(pygame.Color(0, 0, 0)) + self.draw_aaline(surface, pygame.Color(255, 0, 0), (0, 1), (2, 1), 1) + + self.assertGreater(surface.get_at((1, 1)).r, 0, "there should be red here") + + surface.fill(pygame.Color(0, 0, 0)) + self.draw_aaline(surface, pygame.Color(0, 0, 255), (0, 1), (2, 1), 1) + + self.assertGreater(surface.get_at((1, 1)).b, 0, "there should be blue here") + + def _check_antialiasing( + self, from_point, to_point, should, check_points, set_endpoints=True + ): + """Draw a line between two points and check colors of check_points.""" + if set_endpoints: + should[from_point] = should[to_point] = FG_GREEN + + def check_one_direction(from_point, to_point, should): + self.draw_aaline(self.surface, FG_GREEN, from_point, to_point, True) + + for pt in check_points: + color = should.get(pt, BG_RED) + with self.subTest(from_pt=from_point, pt=pt, to=to_point): + self.assertEqual(self.surface.get_at(pt), color) + + # reset + draw.rect(self.surface, BG_RED, (0, 0, 10, 10), 0) + + # it is important to test also opposite direction, the algorithm + # is (#512) or was not symmetric + check_one_direction(from_point, to_point, should) + if from_point != to_point: + check_one_direction(to_point, from_point, should) + + def test_short_non_antialiased_lines(self): + """test very short not anti aliased lines in all directions.""" + + # Horizontal, vertical and diagonal lines should not be anti-aliased, + # even with draw.aaline ... + self.surface = pygame.Surface((10, 10)) + draw.rect(self.surface, BG_RED, (0, 0, 10, 10), 0) + + check_points = [(i, j) for i in range(3, 8) for j in range(3, 8)] + + def check_both_directions(from_pt, to_pt, other_points): + should = {pt: FG_GREEN for pt in other_points} + self._check_antialiasing(from_pt, to_pt, should, check_points) + + # 0. one point + check_both_directions((5, 5), (5, 5), []) + # 1. horizontal + check_both_directions((4, 7), (5, 7), []) + check_both_directions((5, 4), (7, 4), [(6, 4)]) + + # 2. vertical + check_both_directions((5, 5), (5, 6), []) + check_both_directions((6, 4), (6, 6), [(6, 5)]) + # 3. diagonals + check_both_directions((5, 5), (6, 6), []) + check_both_directions((5, 5), (7, 7), [(6, 6)]) + check_both_directions((5, 6), (6, 5), []) + check_both_directions((6, 4), (4, 6), [(5, 5)]) + + def test_short_line_anti_aliasing(self): + self.surface = pygame.Surface((10, 10)) + draw.rect(self.surface, BG_RED, (0, 0, 10, 10), 0) + + check_points = [(i, j) for i in range(3, 8) for j in range(3, 8)] + + def check_both_directions(from_pt, to_pt, should): + self._check_antialiasing(from_pt, to_pt, should, check_points) + + brown = (127, 127, 0) + reddish = (191, 63, 0) + greenish = (63, 191, 0) + + # lets say dx = abs(x0 - x1) ; dy = abs(y0 - y1) + + # dy / dx = 0.5 + check_both_directions((4, 4), (6, 5), {(5, 4): brown, (5, 5): brown}) + check_both_directions((4, 5), (6, 4), {(5, 4): brown, (5, 5): brown}) + + # dy / dx = 2 + check_both_directions((4, 4), (5, 6), {(4, 5): brown, (5, 5): brown}) + check_both_directions((5, 4), (4, 6), {(4, 5): brown, (5, 5): brown}) + + # some little longer lines; so we need to check more points: + check_points = [(i, j) for i in range(2, 9) for j in range(2, 9)] + # dy / dx = 0.25 + should = { + (4, 3): greenish, + (5, 3): brown, + (6, 3): reddish, + (4, 4): reddish, + (5, 4): brown, + (6, 4): greenish, + } + check_both_directions((3, 3), (7, 4), should) + + should = { + (4, 3): reddish, + (5, 3): brown, + (6, 3): greenish, + (4, 4): greenish, + (5, 4): brown, + (6, 4): reddish, + } + check_both_directions((3, 4), (7, 3), should) + + # dy / dx = 4 + should = { + (4, 4): greenish, + (4, 5): brown, + (4, 6): reddish, + (5, 4): reddish, + (5, 5): brown, + (5, 6): greenish, + } + check_both_directions((4, 3), (5, 7), should) + + should = { + (4, 4): reddish, + (4, 5): brown, + (4, 6): greenish, + (5, 4): greenish, + (5, 5): brown, + (5, 6): reddish, + } + check_both_directions((5, 3), (4, 7), should) + + def test_anti_aliasing_float_coordinates(self): + """Float coordinates should be blended smoothly.""" + + self.surface = pygame.Surface((10, 10)) + draw.rect(self.surface, BG_RED, (0, 0, 10, 10), 0) + + check_points = [(i, j) for i in range(5) for j in range(5)] + brown = (127, 127, 0) + reddish = (191, 63, 0) + greenish = (63, 191, 0) + + # 0. identical point : current implementation does no smoothing... + expected = {(2, 2): FG_GREEN} + self._check_antialiasing( + (1.5, 2), (1.5, 2), expected, check_points, set_endpoints=False + ) + expected = {(2, 3): FG_GREEN} + self._check_antialiasing( + (2.49, 2.7), (2.49, 2.7), expected, check_points, set_endpoints=False + ) + + # 1. horizontal lines + # a) blend endpoints + expected = {(1, 2): brown, (2, 2): FG_GREEN} + self._check_antialiasing( + (1.5, 2), (2, 2), expected, check_points, set_endpoints=False + ) + expected = {(1, 2): brown, (2, 2): FG_GREEN, (3, 2): brown} + self._check_antialiasing( + (1.5, 2), (2.5, 2), expected, check_points, set_endpoints=False + ) + expected = {(2, 2): brown, (1, 2): FG_GREEN} + self._check_antialiasing( + (1, 2), (1.5, 2), expected, check_points, set_endpoints=False + ) + expected = {(1, 2): brown, (2, 2): greenish} + self._check_antialiasing( + (1.5, 2), (1.75, 2), expected, check_points, set_endpoints=False + ) + + # b) blend y-coordinate + expected = {(x, y): brown for x in range(2, 5) for y in (1, 2)} + self._check_antialiasing( + (2, 1.5), (4, 1.5), expected, check_points, set_endpoints=False + ) + + # 2. vertical lines + # a) blend endpoints + expected = {(2, 1): brown, (2, 2): FG_GREEN, (2, 3): brown} + self._check_antialiasing( + (2, 1.5), (2, 2.5), expected, check_points, set_endpoints=False + ) + expected = {(2, 1): brown, (2, 2): greenish} + self._check_antialiasing( + (2, 1.5), (2, 1.75), expected, check_points, set_endpoints=False + ) + # b) blend x-coordinate + expected = {(x, y): brown for x in (1, 2) for y in range(2, 5)} + self._check_antialiasing( + (1.5, 2), (1.5, 4), expected, check_points, set_endpoints=False + ) + # 3. diagonal lines + # a) blend endpoints + expected = {(1, 1): brown, (2, 2): FG_GREEN, (3, 3): brown} + self._check_antialiasing( + (1.5, 1.5), (2.5, 2.5), expected, check_points, set_endpoints=False + ) + expected = {(3, 1): brown, (2, 2): FG_GREEN, (1, 3): brown} + self._check_antialiasing( + (2.5, 1.5), (1.5, 2.5), expected, check_points, set_endpoints=False + ) + # b) blend sidewards + expected = {(2, 1): brown, (2, 2): brown, (3, 2): brown, (3, 3): brown} + self._check_antialiasing( + (2, 1.5), (3, 2.5), expected, check_points, set_endpoints=False + ) + + expected = { + (2, 1): greenish, + (2, 2): reddish, + (3, 2): greenish, + (3, 3): reddish, + (4, 3): greenish, + (4, 4): reddish, + } + + self._check_antialiasing( + (2, 1.25), (4, 3.25), expected, check_points, set_endpoints=False + ) + + def test_anti_aliasing_at_and_outside_the_border(self): + """Ensures antialiasing works correct at a surface's borders.""" + + self.surface = pygame.Surface((10, 10)) + draw.rect(self.surface, BG_RED, (0, 0, 10, 10), 0) + + check_points = [(i, j) for i in range(10) for j in range(10)] + + reddish = (191, 63, 0) + brown = (127, 127, 0) + greenish = (63, 191, 0) + from_point, to_point = (3, 3), (7, 4) + should = { + (4, 3): greenish, + (5, 3): brown, + (6, 3): reddish, + (4, 4): reddish, + (5, 4): brown, + (6, 4): greenish, + } + + for dx, dy in ( + (-4, 0), + (4, 0), # moved to left and right borders + (0, -5), + (0, -4), + (0, -3), # upper border + (0, 5), + (0, 6), + (0, 7), # lower border + (-4, -4), + (-4, -3), + (-3, -4), + ): # upper left corner + first = from_point[0] + dx, from_point[1] + dy + second = to_point[0] + dx, to_point[1] + dy + expected = {(x + dx, y + dy): color for (x, y), color in should.items()} + + self._check_antialiasing(first, second, expected, check_points) + + +### AALines Testing ########################################################### + + +class AALinesMixin(BaseLineMixin): + """Mixin test for drawing aalines. + + This class contains all the general aalines drawing tests. + """ + + def test_aalines__args(self): + """Ensures draw aalines accepts the correct args.""" + bounds_rect = self.draw_aalines( + pygame.Surface((3, 3)), (0, 10, 0, 50), False, ((0, 0), (1, 1)), 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aalines__args_without_blend(self): + """Ensures draw aalines accepts the args without a blend.""" + bounds_rect = self.draw_aalines( + pygame.Surface((2, 2)), (0, 0, 0, 50), False, ((0, 0), (1, 1)) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aalines__blend_warning(self): + """From pygame 2, blend=False should raise DeprecationWarning.""" + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger DeprecationWarning. + self.draw_aalines( + pygame.Surface((2, 2)), (0, 0, 0, 50), False, ((0, 0), (1, 1)), False + ) + # Check if there is only one warning and is a DeprecationWarning. + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) + + def test_aalines__kwargs(self): + """Ensures draw aalines accepts the correct kwargs.""" + surface = pygame.Surface((4, 4)) + color = pygame.Color("yellow") + points = ((0, 0), (1, 1), (2, 2)) + kwargs_list = [ + {"surface": surface, "color": color, "closed": False, "points": points}, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_aalines(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aalines__kwargs_order_independent(self): + """Ensures draw aalines's kwargs are not order dependent.""" + bounds_rect = self.draw_aalines( + closed=1, + points=((0, 0), (1, 1), (2, 2)), + color=(10, 20, 30), + surface=pygame.Surface((3, 2)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aalines__args_missing(self): + """Ensures draw aalines detects any missing required args.""" + surface = pygame.Surface((1, 1)) + color = pygame.Color("blue") + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines(surface, color, 0) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines(surface, color) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines() + + def test_aalines__kwargs_missing(self): + """Ensures draw aalines detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((3, 2)), + "color": pygame.Color("red"), + "closed": 1, + "points": ((2, 2), (1, 1)), + } + + for name in ("points", "closed", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines(**invalid_kwargs) + + def test_aalines__arg_invalid_types(self): + """Ensures draw aalines detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + closed = 0 + points = ((1, 2), (2, 1)) + + with self.assertRaises(TypeError): + # Invalid blend. + bounds_rect = self.draw_aalines(surface, color, closed, points, "1") + + with self.assertRaises(TypeError): + # Invalid points. + bounds_rect = self.draw_aalines(surface, color, closed, (1, 2, 3)) + + with self.assertRaises(TypeError): + # Invalid closed. + bounds_rect = self.draw_aalines(surface, color, InvalidBool(), points) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_aalines(surface, 2.3, closed, points) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_aalines((1, 2, 3, 4), color, closed, points) + + def test_aalines__kwarg_invalid_types(self): + """Ensures draw aalines detects invalid kwarg types.""" + valid_kwargs = { + "surface": pygame.Surface((3, 3)), + "color": pygame.Color("green"), + "closed": False, + "points": ((1, 2), (2, 1)), + } + + invalid_kwargs = { + "surface": pygame.Surface, + "color": 2.3, + "closed": InvalidBool(), + "points": (0, 0, 0), + } + + for kwarg in ("surface", "color", "closed", "points"): + kwargs = dict(valid_kwargs) + kwargs[kwarg] = invalid_kwargs[kwarg] + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines(**kwargs) + + def test_aalines__kwarg_invalid_name(self): + """Ensures draw aalines detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + closed = 1 + points = ((1, 2), (2, 1)) + kwargs_list = [ + { + "surface": surface, + "color": color, + "closed": closed, + "points": points, + "invalid": 1, + }, + { + "surface": surface, + "color": color, + "closed": closed, + "points": points, + "invalid": 1, + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines(**kwargs) + + def test_aalines__args_and_kwargs(self): + """Ensures draw aalines accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 2)) + color = (255, 255, 0, 0) + closed = 0 + points = ((1, 2), (2, 1)) + kwargs = { + "surface": surface, + "color": color, + "closed": closed, + "points": points, + } + + for name in ("surface", "color", "closed", "points"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_aalines(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_aalines(surface, color, **kwargs) + elif "closed" == name: + bounds_rect = self.draw_aalines(surface, color, closed, **kwargs) + elif "points" == name: + bounds_rect = self.draw_aalines( + surface, color, closed, points, **kwargs + ) + else: + bounds_rect = self.draw_aalines( + surface, color, closed, points, **kwargs + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aalines__valid_points_format(self): + """Ensures draw aalines accepts different points formats.""" + expected_color = (10, 20, 30, 255) + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "closed": False, + "points": None, + } + + # The point type can be a tuple/list/Vector2. + point_types = ( + (tuple, tuple, tuple, tuple), # all tuples + (list, list, list, list), # all lists + (Vector2, Vector2, Vector2, Vector2), # all Vector2s + (list, Vector2, tuple, Vector2), + ) # mix + + # The point values can be ints or floats. + point_values = ( + ((1, 1), (2, 1), (2, 2), (1, 2)), + ((1, 1), (2.2, 1), (2.1, 2.2), (1, 2.1)), + ) + + # Each sequence of points can be a tuple or a list. + seq_types = (tuple, list) + + for point_type in point_types: + for values in point_values: + check_pos = values[0] + points = [point_type[i](pt) for i, pt in enumerate(values)] + + for seq_type in seq_types: + surface.fill(surface_color) # Clear for each test. + kwargs["points"] = seq_type(points) + + bounds_rect = self.draw_aalines(**kwargs) + + self.assertEqual(surface.get_at(check_pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aalines__invalid_points_formats(self): + """Ensures draw aalines handles invalid points formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "closed": False, + "points": None, + } + + points_fmts = ( + ((1, 1), (2,)), # Too few coords. + ((1, 1), (2, 2, 2)), # Too many coords. + ((1, 1), (2, "2")), # Wrong type. + ((1, 1), {2, 3}), # Wrong type. + ((1, 1), {2: 2, 3: 3}), # Wrong type. + {(1, 1), (1, 2)}, # Wrong type. + {1: 1, 4: 4}, + ) # Wrong type. + + for points in points_fmts: + kwargs["points"] = points + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines(**kwargs) + + def test_aalines__invalid_points_values(self): + """Ensures draw aalines handles invalid points values correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "closed": False, + "points": None, + } + + for points in ([], ((1, 1),)): # Too few points. + for seq_type in (tuple, list): # Test as tuples and lists. + kwargs["points"] = seq_type(points) + + with self.assertRaises(ValueError): + bounds_rect = self.draw_aalines(**kwargs) + + def test_aalines__valid_closed_values(self): + """Ensures draw aalines accepts different closed values.""" + line_color = pygame.Color("blue") + surface_color = pygame.Color("white") + surface = pygame.Surface((5, 5)) + pos = (1, 3) + kwargs = { + "surface": surface, + "color": line_color, + "closed": None, + "points": ((1, 1), (4, 1), (4, 4), (1, 4)), + } + + true_values = (-7, 1, 10, "2", 3.1, (4,), [5], True) + false_values = (None, "", 0, (), [], False) + + for closed in true_values + false_values: + surface.fill(surface_color) # Clear for each test. + kwargs["closed"] = closed + expected_color = line_color if closed else surface_color + + bounds_rect = self.draw_aalines(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aalines__valid_color_formats(self): + """Ensures draw aalines accepts different color formats.""" + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + pos = (1, 1) + kwargs = { + "surface": surface, + "color": None, + "closed": False, + "points": (pos, (2, 1)), + } + greens = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in greens: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_aalines(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_aalines__invalid_color_formats(self): + """Ensures draw aalines handles invalid color formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 3)), + "color": None, + "closed": False, + "points": ((1, 1), (1, 2)), + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_aalines(**kwargs) + + def test_aalines__color(self): + """Tests if the aalines drawn are the correct color. + + Draws aalines around the border of the given surface and checks if all + borders of the surface only contain the given color. + """ + for surface in self._create_surfaces(): + for expected_color in self.COLORS: + self.draw_aalines(surface, expected_color, True, corners(surface)) + + for pos, color in border_pos_and_color(surface): + self.assertEqual(color, expected_color, f"pos={pos}") + + def test_aalines__gaps(self): + """Tests if the aalines drawn contain any gaps. + + Draws aalines around the border of the given surface and checks if + all borders of the surface contain any gaps. + + See: #512 + """ + expected_color = (255, 255, 255) + for surface in self._create_surfaces(): + self.draw_aalines(surface, expected_color, True, corners(surface)) + + for pos, color in border_pos_and_color(surface): + self.assertEqual(color, expected_color, f"pos={pos}") + + def test_aalines__bounding_rect(self): + """Ensures draw aalines returns the correct bounding rect. + + Tests lines with endpoints on and off the surface and blending + enabled and disabled. + """ + line_color = pygame.Color("red") + surf_color = pygame.Color("blue") + width = height = 30 + # Using a rect to help manage where the lines are drawn. + pos_rect = pygame.Rect((0, 0), (width, height)) + + # Testing surfaces of different sizes. One larger than the pos_rect + # and one smaller (to test lines that span the surface). + for size in ((width + 5, height + 5), (width - 5, height - 5)): + surface = pygame.Surface(size, 0, 32) + surf_rect = surface.get_rect() + + # Move pos_rect to different positions to test line endpoints on + # and off the surface. + for pos in rect_corners_mids_and_center(surf_rect): + pos_rect.center = pos + # Shape: Triangle (if closed), ^ caret (if not closed). + pts = (pos_rect.midleft, pos_rect.midtop, pos_rect.midright) + pos = pts[0] # Rect position if nothing drawn. + + for closed in (True, False): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_aalines(surface, line_color, closed, pts) + + # Calculating the expected_rect after the lines are + # drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect(surface, surf_color, pos) + + self.assertEqual(bounding_rect, expected_rect) + + def test_aalines__surface_clip(self): + """Ensures draw aalines respects a surface's clip area.""" + surfw = surfh = 30 + aaline_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (11, 11)) + clip_rect.center = surface.get_rect().center + pos_rect = clip_rect.copy() # Manages the aalines's pos. + + # Test centering the pos_rect along the clip rect's edge to allow for + # drawing the aalines over the clip_rect's bounds. + for center in rect_corners_mids_and_center(clip_rect): + pos_rect.center = center + pts = (pos_rect.midtop, pos_rect.center, pos_rect.midbottom) + for closed in (True, False): # Test closed and not closed. + # Get the expected points by drawing the aalines without + # the clip area set. + surface.set_clip(None) + surface.fill(surface_color) + self.draw_aalines(surface, aaline_color, closed, pts) + + expected_pts = get_color_points( + surface, surface_color, clip_rect, False + ) + + # Clear the surface and set the clip area. Redraw the + # aalines and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_aalines(surface, aaline_color, closed, pts) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure the expected_pts + # are not surface_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + self.assertNotEqual(surface.get_at(pt), surface_color, pt) + else: + self.assertEqual(surface.get_at(pt), surface_color, pt) + + surface.unlock() + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever fully supports drawing aalines. +# class PythonDrawAALinesTest(AALinesMixin, PythonDrawTestCase): +# """Test draw_py module function aalines. +# +# This class inherits the general tests from AALinesMixin. It is also the +# class to add any draw_py.draw_aalines specific tests to. +# """ + + +class DrawAALinesTest(AALinesMixin, DrawTestCase): + """Test draw module function aalines. + + This class inherits the general tests from AALinesMixin. It is also the + class to add any draw.aalines specific tests to. + """ + + +### Polygon Testing ########################################################### + +SQUARE = ([0, 0], [3, 0], [3, 3], [0, 3]) +DIAMOND = [(1, 3), (3, 5), (5, 3), (3, 1)] +CROSS = ( + [2, 0], + [4, 0], + [4, 2], + [6, 2], + [6, 4], + [4, 4], + [4, 6], + [2, 6], + [2, 4], + [0, 4], + [0, 2], + [2, 2], +) + + +class DrawPolygonMixin: + """Mixin tests for drawing polygons. + + This class contains all the general polygon drawing tests. + """ + + def setUp(self): + self.surface = pygame.Surface((20, 20)) + + def test_polygon__args(self): + """Ensures draw polygon accepts the correct args.""" + bounds_rect = self.draw_polygon( + pygame.Surface((3, 3)), (0, 10, 0, 50), ((0, 0), (1, 1), (2, 2)), 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__args_without_width(self): + """Ensures draw polygon accepts the args without a width.""" + bounds_rect = self.draw_polygon( + pygame.Surface((2, 2)), (0, 0, 0, 50), ((0, 0), (1, 1), (2, 2)) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__kwargs(self): + """Ensures draw polygon accepts the correct kwargs + with and without a width arg. + """ + surface = pygame.Surface((4, 4)) + color = pygame.Color("yellow") + points = ((0, 0), (1, 1), (2, 2)) + kwargs_list = [ + {"surface": surface, "color": color, "points": points, "width": 1}, + {"surface": surface, "color": color, "points": points}, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_polygon(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__kwargs_order_independent(self): + """Ensures draw polygon's kwargs are not order dependent.""" + bounds_rect = self.draw_polygon( + color=(10, 20, 30), + surface=pygame.Surface((3, 2)), + width=0, + points=((0, 1), (1, 2), (2, 3)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__args_missing(self): + """Ensures draw polygon detects any missing required args.""" + surface = pygame.Surface((1, 1)) + color = pygame.Color("blue") + + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon(surface, color) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon() + + def test_polygon__kwargs_missing(self): + """Ensures draw polygon detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((1, 2)), + "color": pygame.Color("red"), + "points": ((2, 1), (2, 2), (2, 3)), + "width": 1, + } + + for name in ("points", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon(**invalid_kwargs) + + def test_polygon__arg_invalid_types(self): + """Ensures draw polygon detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + points = ((0, 1), (1, 2), (1, 3)) + + with self.assertRaises(TypeError): + # Invalid width. + bounds_rect = self.draw_polygon(surface, color, points, "1") + + with self.assertRaises(TypeError): + # Invalid points. + bounds_rect = self.draw_polygon(surface, color, (1, 2, 3)) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_polygon(surface, 2.3, points) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_polygon((1, 2, 3, 4), color, points) + + def test_polygon__kwarg_invalid_types(self): + """Ensures draw polygon detects invalid kwarg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("green") + points = ((0, 0), (1, 0), (2, 0)) + width = 1 + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "points": points, + "width": width, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "points": points, + "width": width, + }, + { + "surface": surface, + "color": color, + "points": ((1,), (1,), (1,)), # Invalid points. + "width": width, + }, + {"surface": surface, "color": color, "points": points, "width": 1.2}, + ] # Invalid width. + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon(**kwargs) + + def test_polygon__kwarg_invalid_name(self): + """Ensures draw polygon detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + points = ((1, 1), (1, 2), (1, 3)) + kwargs_list = [ + { + "surface": surface, + "color": color, + "points": points, + "width": 1, + "invalid": 1, + }, + {"surface": surface, "color": color, "points": points, "invalid": 1}, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon(**kwargs) + + def test_polygon__args_and_kwargs(self): + """Ensures draw polygon accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 1)) + color = (255, 255, 0, 0) + points = ((0, 1), (1, 2), (2, 3)) + width = 0 + kwargs = {"surface": surface, "color": color, "points": points, "width": width} + + for name in ("surface", "color", "points", "width"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_polygon(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_polygon(surface, color, **kwargs) + elif "points" == name: + bounds_rect = self.draw_polygon(surface, color, points, **kwargs) + else: + bounds_rect = self.draw_polygon(surface, color, points, width, **kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__valid_width_values(self): + """Ensures draw polygon accepts different width values.""" + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + color = (10, 20, 30, 255) + kwargs = { + "surface": surface, + "color": color, + "points": ((1, 1), (2, 1), (2, 2), (1, 2)), + "width": None, + } + pos = kwargs["points"][0] + + for width in (-100, -10, -1, 0, 1, 10, 100): + surface.fill(surface_color) # Clear for each test. + kwargs["width"] = width + expected_color = color if width >= 0 else surface_color + + bounds_rect = self.draw_polygon(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__valid_points_format(self): + """Ensures draw polygon accepts different points formats.""" + expected_color = (10, 20, 30, 255) + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "points": None, + "width": 0, + } + + # The point type can be a tuple/list/Vector2. + point_types = ( + (tuple, tuple, tuple, tuple), # all tuples + (list, list, list, list), # all lists + (Vector2, Vector2, Vector2, Vector2), # all Vector2s + (list, Vector2, tuple, Vector2), + ) # mix + + # The point values can be ints or floats. + point_values = ( + ((1, 1), (2, 1), (2, 2), (1, 2)), + ((1, 1), (2.2, 1), (2.1, 2.2), (1, 2.1)), + ) + + # Each sequence of points can be a tuple or a list. + seq_types = (tuple, list) + + for point_type in point_types: + for values in point_values: + check_pos = values[0] + points = [point_type[i](pt) for i, pt in enumerate(values)] + + for seq_type in seq_types: + surface.fill(surface_color) # Clear for each test. + kwargs["points"] = seq_type(points) + + bounds_rect = self.draw_polygon(**kwargs) + + self.assertEqual(surface.get_at(check_pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__invalid_points_formats(self): + """Ensures draw polygon handles invalid points formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "points": None, + "width": 0, + } + + points_fmts = ( + ((1, 1), (2, 1), (2,)), # Too few coords. + ((1, 1), (2, 1), (2, 2, 2)), # Too many coords. + ((1, 1), (2, 1), (2, "2")), # Wrong type. + ((1, 1), (2, 1), {2, 3}), # Wrong type. + ((1, 1), (2, 1), {2: 2, 3: 3}), # Wrong type. + {(1, 1), (2, 1), (2, 2), (1, 2)}, # Wrong type. + {1: 1, 2: 2, 3: 3, 4: 4}, + ) # Wrong type. + + for points in points_fmts: + kwargs["points"] = points + + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon(**kwargs) + + def test_polygon__invalid_points_values(self): + """Ensures draw polygon handles invalid points values correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "points": None, + "width": 0, + } + + points_fmts = ( + (), # Too few points. + ((1, 1),), # Too few points. + ((1, 1), (2, 1)), + ) # Too few points. + + for points in points_fmts: + for seq_type in (tuple, list): # Test as tuples and lists. + kwargs["points"] = seq_type(points) + + with self.assertRaises(ValueError): + bounds_rect = self.draw_polygon(**kwargs) + + def test_polygon__valid_color_formats(self): + """Ensures draw polygon accepts different color formats.""" + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": None, + "points": ((1, 1), (2, 1), (2, 2), (1, 2)), + "width": 0, + } + pos = kwargs["points"][0] + greens = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in greens: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_polygon(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_polygon__invalid_color_formats(self): + """Ensures draw polygon handles invalid color formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 3)), + "color": None, + "points": ((1, 1), (2, 1), (2, 2), (1, 2)), + "width": 0, + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_polygon(**kwargs) + + def test_draw_square(self): + self.draw_polygon(self.surface, RED, SQUARE, 0) + # note : there is a discussion (#234) if draw.polygon should include or + # not the right or lower border; here we stick with current behavior, + # eg include those borders ... + for x in range(4): + for y in range(4): + self.assertEqual(self.surface.get_at((x, y)), RED) + + def test_draw_diamond(self): + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + self.draw_polygon(self.surface, GREEN, DIAMOND, 0) + # this diamond shape is equivalent to its four corners, plus inner square + for x, y in DIAMOND: + self.assertEqual(self.surface.get_at((x, y)), GREEN, msg=str((x, y))) + for x in range(2, 5): + for y in range(2, 5): + self.assertEqual(self.surface.get_at((x, y)), GREEN) + + def test_1_pixel_high_or_wide_shapes(self): + # 1. one-pixel-high, filled + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + self.draw_polygon(self.surface, GREEN, [(x, 2) for x, _y in CROSS], 0) + cross_size = 6 # the maximum x or y coordinate of the cross + for x in range(cross_size + 1): + self.assertEqual(self.surface.get_at((x, 1)), RED) + self.assertEqual(self.surface.get_at((x, 2)), GREEN) + self.assertEqual(self.surface.get_at((x, 3)), RED) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + # 2. one-pixel-high, not filled + self.draw_polygon(self.surface, GREEN, [(x, 5) for x, _y in CROSS], 1) + for x in range(cross_size + 1): + self.assertEqual(self.surface.get_at((x, 4)), RED) + self.assertEqual(self.surface.get_at((x, 5)), GREEN) + self.assertEqual(self.surface.get_at((x, 6)), RED) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + # 3. one-pixel-wide, filled + self.draw_polygon(self.surface, GREEN, [(3, y) for _x, y in CROSS], 0) + for y in range(cross_size + 1): + self.assertEqual(self.surface.get_at((2, y)), RED) + self.assertEqual(self.surface.get_at((3, y)), GREEN) + self.assertEqual(self.surface.get_at((4, y)), RED) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + # 4. one-pixel-wide, not filled + self.draw_polygon(self.surface, GREEN, [(4, y) for _x, y in CROSS], 1) + for y in range(cross_size + 1): + self.assertEqual(self.surface.get_at((3, y)), RED) + self.assertEqual(self.surface.get_at((4, y)), GREEN) + self.assertEqual(self.surface.get_at((5, y)), RED) + + def test_draw_symetric_cross(self): + """non-regression on issue #234 : x and y where handled inconsistently. + + Also, the result is/was different whether we fill or not the polygon. + """ + # 1. case width = 1 (not filled: `polygon` calls internally the `lines` function) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + self.draw_polygon(self.surface, GREEN, CROSS, 1) + inside = [(x, 3) for x in range(1, 6)] + [(3, y) for y in range(1, 6)] + for x in range(10): + for y in range(10): + if (x, y) in inside: + self.assertEqual(self.surface.get_at((x, y)), RED) + elif (x in range(2, 5) and y < 7) or (y in range(2, 5) and x < 7): + # we are on the border of the cross: + self.assertEqual(self.surface.get_at((x, y)), GREEN) + else: + # we are outside + self.assertEqual(self.surface.get_at((x, y)), RED) + + # 2. case width = 0 (filled; this is the example from #234) + pygame.draw.rect(self.surface, RED, (0, 0, 10, 10), 0) + self.draw_polygon(self.surface, GREEN, CROSS, 0) + inside = [(x, 3) for x in range(1, 6)] + [(3, y) for y in range(1, 6)] + for x in range(10): + for y in range(10): + if (x in range(2, 5) and y < 7) or (y in range(2, 5) and x < 7): + # we are on the border of the cross: + self.assertEqual( + self.surface.get_at((x, y)), GREEN, msg=str((x, y)) + ) + else: + # we are outside + self.assertEqual(self.surface.get_at((x, y)), RED) + + def test_illumine_shape(self): + """non-regression on issue #313""" + rect = pygame.Rect((0, 0, 20, 20)) + path_data = [ + (0, 0), + (rect.width - 1, 0), # upper border + (rect.width - 5, 5 - 1), + (5 - 1, 5 - 1), # upper inner + (5 - 1, rect.height - 5), + (0, rect.height - 1), + ] # lower diagonal + # The shape looks like this (the numbers are the indices of path_data) + + # 0**********************1 <-- upper border + # *********************** + # ********************** + # ********************* + # ****3**************2 <-- upper inner border + # ***** + # ***** (more lines here) + # ***** + # ****4 + # **** + # *** + # ** + # 5 + # + + # the current bug is that the "upper inner" line is not drawn, but only + # if 4 or some lower corner exists + pygame.draw.rect(self.surface, RED, (0, 0, 20, 20), 0) + + # 1. First without the corners 4 & 5 + self.draw_polygon(self.surface, GREEN, path_data[:4], 0) + for x in range(20): + self.assertEqual(self.surface.get_at((x, 0)), GREEN) # upper border + for x in range(4, rect.width - 5 + 1): + self.assertEqual(self.surface.get_at((x, 4)), GREEN) # upper inner + + # 2. with the corners 4 & 5 + pygame.draw.rect(self.surface, RED, (0, 0, 20, 20), 0) + self.draw_polygon(self.surface, GREEN, path_data, 0) + for x in range(4, rect.width - 5 + 1): + self.assertEqual(self.surface.get_at((x, 4)), GREEN) # upper inner + + def test_invalid_points(self): + self.assertRaises( + TypeError, + lambda: self.draw_polygon( + self.surface, RED, ((0, 0), (0, 20), (20, 20), 20), 0 + ), + ) + + def test_polygon__bounding_rect(self): + """Ensures draw polygon returns the correct bounding rect. + + Tests polygons on and off the surface and a range of width/thickness + values. + """ + polygon_color = pygame.Color("red") + surf_color = pygame.Color("black") + min_width = min_height = 5 + max_width = max_height = 7 + sizes = ((min_width, min_height), (max_width, max_height)) + surface = pygame.Surface((20, 20), 0, 32) + surf_rect = surface.get_rect() + # Make a rect that is bigger than the surface to help test drawing + # polygons off and partially off the surface. + big_rect = surf_rect.inflate(min_width * 2 + 1, min_height * 2 + 1) + + for pos in rect_corners_mids_and_center( + surf_rect + ) + rect_corners_mids_and_center(big_rect): + # A rect (pos_rect) is used to help create and position the + # polygon. Each of this rect's position attributes will be set to + # the pos value. + for attr in RECT_POSITION_ATTRIBUTES: + # Test using different rect sizes and thickness values. + for width, height in sizes: + pos_rect = pygame.Rect((0, 0), (width, height)) + setattr(pos_rect, attr, pos) + # Points form a triangle with no fully + # horizontal/vertical lines. + vertices = ( + pos_rect.midleft, + pos_rect.midtop, + pos_rect.bottomright, + ) + + for thickness in range(4): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_polygon( + surface, polygon_color, vertices, thickness + ) + + # Calculating the expected_rect after the polygon + # is drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect( + surface, surf_color, vertices[0] + ) + + self.assertEqual( + bounding_rect, + expected_rect, + f"thickness={thickness}", + ) + + def test_polygon__surface_clip(self): + """Ensures draw polygon respects a surface's clip area. + + Tests drawing the polygon filled and unfilled. + """ + surfw = surfh = 30 + polygon_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (8, 10)) + clip_rect.center = surface.get_rect().center + pos_rect = clip_rect.copy() # Manages the polygon's pos. + + for width in (0, 1): # Filled and unfilled. + # Test centering the polygon along the clip rect's edge. + for center in rect_corners_mids_and_center(clip_rect): + # Get the expected points by drawing the polygon without the + # clip area set. + pos_rect.center = center + vertices = ( + pos_rect.topleft, + pos_rect.topright, + pos_rect.bottomright, + pos_rect.bottomleft, + ) + surface.set_clip(None) + surface.fill(surface_color) + self.draw_polygon(surface, polygon_color, vertices, width) + expected_pts = get_color_points(surface, polygon_color, clip_rect) + + # Clear the surface and set the clip area. Redraw the polygon + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_polygon(surface, polygon_color, vertices, width) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure only the expected_pts + # are the polygon_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + expected_color = polygon_color + else: + expected_color = surface_color + + self.assertEqual(surface.get_at(pt), expected_color, pt) + + surface.unlock() + + # Test cases for Issue #3989 (fixed on #4191) + # This tests the fill polygon bug to avoid + def test_polygon_large_coords_3989(self): + """ + Ensures draw polygon works correctly with large points. + Testing the drawings of filled polygons + """ + point_a = (600, 50) + point_b = (50, 600) + extreme_points_coords = (58000, 100000) + extreme_negative_coords = (-58000, -100000) + extreme_negative_x = (-58000, 100000) + extreme_negative_y = (58000, -100000) + + surf_w = surf_h = 650 + surface = pygame.Surface((surf_w, surf_h)) + + green = (0, 255, 0) + white = (0, 0, 0, 255) + + # Draw white background + pygame.draw.rect(surface, white, (0, 0, surf_w, surf_h), 0) + + # Extreme points case + def extreme_points(self): + self.assertEqual(surface.get_at((640, 50)), white) + self.assertEqual(surface.get_at((50, 640)), white) + + # Extreme negative points case + def extreme_negative_pass(self): + self.assertEqual(surface.get_at((600, 25)), white) + + def extreme_negative_fail(self): + self.assertNotEqual(surface.get_at((5, 5)), white) + + # Extreme negative x case + def extreme_x_pass(self): + self.assertEqual(surface.get_at((600, 600)), white) + + def extreme_x_fail(self): + self.assertNotEqual(surface.get_at((100, 640)), white) + + # Extreme negative y case + def extreme_y_pass(self): + self.assertEqual(surface.get_at((600, 600)), white) + + def extreme_y_fail(self): + self.assertNotEqual(surface.get_at((300, 300)), white) + + # Checks the surface point to ensure the polygon has been drawn correctly. + # Uses multiple passing and failing test cases depending on the polygon + self.draw_polygon(surface, green, (point_a, point_b, extreme_points_coords)) + extreme_points(self) + + pygame.draw.rect(surface, white, (0, 0, surf_w, surf_h), 0) + self.draw_polygon(surface, green, (point_a, point_b, extreme_negative_coords)) + extreme_negative_pass(self) + extreme_negative_fail(self) + + pygame.draw.rect(surface, white, (0, 0, surf_w, surf_h), 0) + self.draw_polygon(surface, green, (point_a, point_b, extreme_negative_x)) + extreme_x_pass(self) + extreme_x_fail(self) + + pygame.draw.rect(surface, white, (0, 0, surf_w, surf_h), 0) + self.draw_polygon(surface, green, (point_a, point_b, extreme_negative_y)) + extreme_y_pass(self) + extreme_y_fail(self) + + +class DrawPolygonTest(DrawPolygonMixin, DrawTestCase): + """Test draw module function polygon. + + This class inherits the general tests from DrawPolygonMixin. It is also + the class to add any draw.polygon specific tests to. + """ + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever fully supports drawing polygons. +# @unittest.skip('draw_py.draw_polygon not fully supported yet') +# class PythonDrawPolygonTest(DrawPolygonMixin, PythonDrawTestCase): +# """Test draw_py module function draw_polygon. +# +# This class inherits the general tests from DrawPolygonMixin. It is also +# the class to add any draw_py.draw_polygon specific tests to. +# """ + + +### Rect Testing ############################################################## + + +class DrawRectMixin: + """Mixin tests for drawing rects. + + This class contains all the general rect drawing tests. + """ + + def test_rect__args(self): + """Ensures draw rect accepts the correct args.""" + bounds_rect = self.draw_rect( + pygame.Surface((2, 2)), + (20, 10, 20, 150), + pygame.Rect((0, 0), (1, 1)), + 2, + 1, + 2, + 3, + 4, + 5, + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_rect__args_without_width(self): + """Ensures draw rect accepts the args without a width and borders.""" + bounds_rect = self.draw_rect( + pygame.Surface((3, 5)), (0, 0, 0, 255), pygame.Rect((0, 0), (1, 1)) + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_rect__kwargs(self): + """Ensures draw rect accepts the correct kwargs + with and without a width and border_radius arg. + """ + kwargs_list = [ + { + "surface": pygame.Surface((5, 5)), + "color": pygame.Color("red"), + "rect": pygame.Rect((0, 0), (1, 2)), + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + }, + { + "surface": pygame.Surface((1, 2)), + "color": (0, 100, 200), + "rect": (0, 0, 1, 1), + }, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_rect(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_rect__kwargs_order_independent(self): + """Ensures draw rect's kwargs are not order dependent.""" + bounds_rect = self.draw_rect( + color=(0, 1, 2), + border_radius=10, + surface=pygame.Surface((2, 3)), + border_top_left_radius=5, + width=-2, + border_top_right_radius=20, + border_bottom_right_radius=0, + rect=pygame.Rect((0, 0), (0, 0)), + border_bottom_left_radius=15, + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_rect__args_missing(self): + """Ensures draw rect detects any missing required args.""" + surface = pygame.Surface((1, 1)) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_rect(surface, pygame.Color("white")) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_rect(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_rect() + + def test_rect__kwargs_missing(self): + """Ensures draw rect detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((1, 3)), + "color": pygame.Color("red"), + "rect": pygame.Rect((0, 0), (2, 2)), + "width": 5, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + } + + for name in ("rect", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_rect(**invalid_kwargs) + + def test_rect__arg_invalid_types(self): + """Ensures draw rect detects invalid arg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("white") + rect = pygame.Rect((1, 1), (1, 1)) + + with self.assertRaises(TypeError): + # Invalid border_bottom_right_radius. + bounds_rect = self.draw_rect( + surface, color, rect, 2, border_bottom_right_radius="rad" + ) + + with self.assertRaises(TypeError): + # Invalid border_bottom_left_radius. + bounds_rect = self.draw_rect( + surface, color, rect, 2, border_bottom_left_radius="rad" + ) + + with self.assertRaises(TypeError): + # Invalid border_top_right_radius. + bounds_rect = self.draw_rect( + surface, color, rect, 2, border_top_right_radius="rad" + ) + + with self.assertRaises(TypeError): + # Invalid border_top_left_radius. + bounds_rect = self.draw_rect( + surface, color, rect, 2, border_top_left_radius="draw" + ) + + with self.assertRaises(TypeError): + # Invalid border_radius. + bounds_rect = self.draw_rect(surface, color, rect, 2, "rad") + + with self.assertRaises(TypeError): + # Invalid width. + bounds_rect = self.draw_rect(surface, color, rect, "2", 4) + + with self.assertRaises(TypeError): + # Invalid rect. + bounds_rect = self.draw_rect(surface, color, (1, 2, 3), 2, 6) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_rect(surface, 2.3, rect, 3, 8) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_rect(rect, color, rect, 4, 10) + + def test_rect__kwarg_invalid_types(self): + """Ensures draw rect detects invalid kwarg types.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("red") + rect = pygame.Rect((0, 0), (1, 1)) + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "rect": rect, + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "rect": rect, + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + }, + { + "surface": surface, + "color": color, + "rect": (1, 1, 2), # Invalid rect. + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1.1, # Invalid width. + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1, + "border_radius": 10.5, # Invalid border_radius. + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5.5, # Invalid top_left_radius. + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": "a", # Invalid top_right_radius. + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": "c", # Invalid bottom_left_radius + "border_bottom_right_radius": 0, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": "d", # Invalid bottom_right. + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_rect(**kwargs) + + def test_rect__kwarg_invalid_name(self): + """Ensures draw rect detects invalid kwarg names.""" + surface = pygame.Surface((2, 1)) + color = pygame.Color("green") + rect = pygame.Rect((0, 0), (3, 3)) + kwargs_list = [ + { + "surface": surface, + "color": color, + "rect": rect, + "width": 1, + "border_radius": 10, + "border_top_left_radius": 5, + "border_top_right_radius": 20, + "border_bottom_left_radius": 15, + "border_bottom_right_radius": 0, + "invalid": 1, + }, + {"surface": surface, "color": color, "rect": rect, "invalid": 1}, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_rect(**kwargs) + + def test_rect__args_and_kwargs(self): + """Ensures draw rect accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 1)) + color = (255, 255, 255, 0) + rect = pygame.Rect((1, 0), (2, 5)) + width = 0 + kwargs = {"surface": surface, "color": color, "rect": rect, "width": width} + + for name in ("surface", "color", "rect", "width"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_rect(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_rect(surface, color, **kwargs) + elif "rect" == name: + bounds_rect = self.draw_rect(surface, color, rect, **kwargs) + else: + bounds_rect = self.draw_rect(surface, color, rect, width, **kwargs) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_rect__valid_width_values(self): + """Ensures draw rect accepts different width values.""" + pos = (1, 1) + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + color = (1, 2, 3, 255) + kwargs = { + "surface": surface, + "color": color, + "rect": pygame.Rect(pos, (2, 2)), + "width": None, + } + + for width in (-1000, -10, -1, 0, 1, 10, 1000): + surface.fill(surface_color) # Clear for each test. + kwargs["width"] = width + expected_color = color if width >= 0 else surface_color + + bounds_rect = self.draw_rect(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_rect__valid_rect_formats(self): + """Ensures draw rect accepts different rect formats.""" + pos = (1, 1) + expected_color = pygame.Color("yellow") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + kwargs = {"surface": surface, "color": expected_color, "rect": None, "width": 0} + rects = ( + pygame.Rect(pos, (1, 1)), + (pos, (2, 2)), + (pos[0], pos[1], 3, 3), + [pos, (2.1, 2.2)], + ) + + for rect in rects: + surface.fill(surface_color) # Clear for each test. + kwargs["rect"] = rect + + bounds_rect = self.draw_rect(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_rect__invalid_rect_formats(self): + """Ensures draw rect handles invalid rect formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("red"), + "rect": None, + "width": 0, + } + + invalid_fmts = ( + [], + [1], + [1, 2], + [1, 2, 3], + [1, 2, 3, 4, 5], + {1, 2, 3, 4}, + [1, 2, 3, "4"], + ) + + for rect in invalid_fmts: + kwargs["rect"] = rect + + with self.assertRaises(TypeError): + bounds_rect = self.draw_rect(**kwargs) + + def test_rect__valid_color_formats(self): + """Ensures draw rect accepts different color formats.""" + pos = (1, 1) + red_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": None, + "rect": pygame.Rect(pos, (1, 1)), + "width": 3, + } + reds = ((255, 0, 0), (255, 0, 0, 255), surface.map_rgb(red_color), red_color) + + for color in reds: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = red_color + + bounds_rect = self.draw_rect(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_rect__invalid_color_formats(self): + """Ensures draw rect handles invalid color formats correctly.""" + pos = (1, 1) + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": None, + "rect": pygame.Rect(pos, (1, 1)), + "width": 1, + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_rect(**kwargs) + + def test_rect__fill(self): + self.surf_w, self.surf_h = self.surf_size = (320, 200) + self.surf = pygame.Surface(self.surf_size, pygame.SRCALPHA) + self.color = (1, 13, 24, 205) + rect = pygame.Rect(10, 10, 25, 20) + drawn = self.draw_rect(self.surf, self.color, rect, 0) + + self.assertEqual(drawn, rect) + + # Should be colored where it's supposed to be + for pt in test_utils.rect_area_pts(rect): + color_at_pt = self.surf.get_at(pt) + + self.assertEqual(color_at_pt, self.color) + + # And not where it shouldn't + for pt in test_utils.rect_outer_bounds(rect): + color_at_pt = self.surf.get_at(pt) + + self.assertNotEqual(color_at_pt, self.color) + + # Issue #310: Cannot draw rectangles that are 1 pixel high + bgcolor = pygame.Color("black") + self.surf.fill(bgcolor) + hrect = pygame.Rect(1, 1, self.surf_w - 2, 1) + vrect = pygame.Rect(1, 3, 1, self.surf_h - 4) + + drawn = self.draw_rect(self.surf, self.color, hrect, 0) + + self.assertEqual(drawn, hrect) + + x, y = hrect.topleft + w, h = hrect.size + + self.assertEqual(self.surf.get_at((x - 1, y)), bgcolor) + self.assertEqual(self.surf.get_at((x + w, y)), bgcolor) + for i in range(x, x + w): + self.assertEqual(self.surf.get_at((i, y)), self.color) + + drawn = self.draw_rect(self.surf, self.color, vrect, 0) + + self.assertEqual(drawn, vrect) + + x, y = vrect.topleft + w, h = vrect.size + + self.assertEqual(self.surf.get_at((x, y - 1)), bgcolor) + self.assertEqual(self.surf.get_at((x, y + h)), bgcolor) + for i in range(y, y + h): + self.assertEqual(self.surf.get_at((x, i)), self.color) + + def test_rect__one_pixel_lines(self): + self.surf = pygame.Surface((320, 200), pygame.SRCALPHA) + self.color = (1, 13, 24, 205) + + rect = pygame.Rect(10, 10, 56, 20) + + drawn = self.draw_rect(self.surf, self.color, rect, 1) + + self.assertEqual(drawn, rect) + + # Should be colored where it's supposed to be + for pt in test_utils.rect_perimeter_pts(drawn): + color_at_pt = self.surf.get_at(pt) + + self.assertEqual(color_at_pt, self.color) + + # And not where it shouldn't + for pt in test_utils.rect_outer_bounds(drawn): + color_at_pt = self.surf.get_at(pt) + + self.assertNotEqual(color_at_pt, self.color) + + def test_rect__draw_line_width(self): + surface = pygame.Surface((100, 100)) + surface.fill("black") + color = pygame.Color(255, 255, 255) + rect_width = 80 + rect_height = 50 + line_width = 10 + pygame.draw.rect( + surface, color, pygame.Rect(0, 0, rect_width, rect_height), line_width + ) + for i in range(line_width): + self.assertEqual(surface.get_at((i, i)), color) + self.assertEqual(surface.get_at((rect_width - i - 1, i)), color) + self.assertEqual(surface.get_at((i, rect_height - i - 1)), color) + self.assertEqual( + surface.get_at((rect_width - i - 1, rect_height - i - 1)), color + ) + self.assertEqual(surface.get_at((line_width, line_width)), (0, 0, 0)) + self.assertEqual( + surface.get_at((rect_width - line_width - 1, line_width)), (0, 0, 0) + ) + self.assertEqual( + surface.get_at((line_width, rect_height - line_width - 1)), (0, 0, 0) + ) + self.assertEqual( + surface.get_at((rect_width - line_width - 1, rect_height - line_width - 1)), + (0, 0, 0), + ) + + def test_rect__bounding_rect(self): + """Ensures draw rect returns the correct bounding rect. + + Tests rects on and off the surface and a range of width/thickness + values. + """ + rect_color = pygame.Color("red") + surf_color = pygame.Color("black") + min_width = min_height = 5 + max_width = max_height = 7 + sizes = ((min_width, min_height), (max_width, max_height)) + surface = pygame.Surface((20, 20), 0, 32) + surf_rect = surface.get_rect() + # Make a rect that is bigger than the surface to help test drawing + # rects off and partially off the surface. + big_rect = surf_rect.inflate(min_width * 2 + 1, min_height * 2 + 1) + + for pos in rect_corners_mids_and_center( + surf_rect + ) + rect_corners_mids_and_center(big_rect): + # Each of the rect's position attributes will be set to the pos + # value. + for attr in RECT_POSITION_ATTRIBUTES: + # Test using different rect sizes and thickness values. + for width, height in sizes: + rect = pygame.Rect((0, 0), (width, height)) + setattr(rect, attr, pos) + + for thickness in range(4): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_rect( + surface, rect_color, rect, thickness + ) + + # Calculating the expected_rect after the rect is + # drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect( + surface, surf_color, rect.topleft + ) + + self.assertEqual( + bounding_rect, + expected_rect, + f"thickness={thickness}", + ) + + def test_rect__surface_clip(self): + """Ensures draw rect respects a surface's clip area. + + Tests drawing the rect filled and unfilled. + """ + surfw = surfh = 30 + rect_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (8, 10)) + clip_rect.center = surface.get_rect().center + test_rect = clip_rect.copy() # Manages the rect's pos. + + for width in (0, 1): # Filled and unfilled. + # Test centering the rect along the clip rect's edge. + for center in rect_corners_mids_and_center(clip_rect): + # Get the expected points by drawing the rect without the + # clip area set. + test_rect.center = center + surface.set_clip(None) + surface.fill(surface_color) + self.draw_rect(surface, rect_color, test_rect, width) + expected_pts = get_color_points(surface, rect_color, clip_rect) + + # Clear the surface and set the clip area. Redraw the rect + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_rect(surface, rect_color, test_rect, width) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure only the expected_pts + # are the rect_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + expected_color = rect_color + else: + expected_color = surface_color + + self.assertEqual(surface.get_at(pt), expected_color, pt) + + surface.unlock() + + +class DrawRectTest(DrawRectMixin, DrawTestCase): + """Test draw module function rect. + + This class inherits the general tests from DrawRectMixin. It is also the + class to add any draw.rect specific tests to. + """ + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever properly supports drawing rects. +# @unittest.skip('draw_py.draw_rect not supported yet') +# class PythonDrawRectTest(DrawRectMixin, PythonDrawTestCase): +# """Test draw_py module function draw_rect. +# +# This class inherits the general tests from DrawRectMixin. It is also the +# class to add any draw_py.draw_rect specific tests to. +# """ + + +### Circle Testing ############################################################ + + +class DrawCircleMixin: + """Mixin tests for drawing circles. + + This class contains all the general circle drawing tests. + """ + + def test_circle__args(self): + """Ensures draw circle accepts the correct args.""" + bounds_rect = self.draw_circle( + pygame.Surface((3, 3)), (0, 10, 0, 50), (0, 0), 3, 1, 1, 0, 1, 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__args_without_width(self): + """Ensures draw circle accepts the args without a width and + quadrants.""" + bounds_rect = self.draw_circle(pygame.Surface((2, 2)), (0, 0, 0, 50), (1, 1), 1) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__args_with_negative_width(self): + """Ensures draw circle accepts the args with negative width.""" + bounds_rect = self.draw_circle( + pygame.Surface((2, 2)), (0, 0, 0, 50), (1, 1), 1, -1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + self.assertEqual(bounds_rect, pygame.Rect(1, 1, 0, 0)) + + def test_circle__args_with_width_gt_radius(self): + """Ensures draw circle accepts the args with width > radius.""" + bounds_rect = self.draw_circle( + pygame.Surface((2, 2)), (0, 0, 0, 50), (1, 1), 2, 3, 0, 0, 0, 0 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + self.assertEqual(bounds_rect, pygame.Rect(0, 0, 2, 2)) + + def test_circle__kwargs(self): + """Ensures draw circle accepts the correct kwargs + with and without a width and quadrant arguments. + """ + kwargs_list = [ + { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("yellow"), + "center": (2, 2), + "radius": 2, + "width": 1, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": False, + "draw_bottom_right": True, + }, + { + "surface": pygame.Surface((2, 1)), + "color": (0, 10, 20), + "center": (1, 1), + "radius": 1, + }, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_circle(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__kwargs_order_independent(self): + """Ensures draw circle's kwargs are not order dependent.""" + bounds_rect = self.draw_circle( + draw_top_right=False, + color=(10, 20, 30), + surface=pygame.Surface((3, 2)), + width=0, + draw_bottom_left=False, + center=(1, 0), + draw_bottom_right=False, + radius=2, + draw_top_left=True, + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__args_missing(self): + """Ensures draw circle detects any missing required args.""" + surface = pygame.Surface((1, 1)) + color = pygame.Color("blue") + + with self.assertRaises(TypeError): + bounds_rect = self.draw_circle(surface, color, (0, 0)) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_circle(surface, color) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_circle(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_circle() + + def test_circle__kwargs_missing(self): + """Ensures draw circle detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((1, 2)), + "color": pygame.Color("red"), + "center": (1, 0), + "radius": 2, + "width": 1, + "draw_top_right": False, + "draw_top_left": False, + "draw_bottom_left": False, + "draw_bottom_right": True, + } + + for name in ("radius", "center", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_circle(**invalid_kwargs) + + def test_circle__arg_invalid_types(self): + """Ensures draw circle detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + center = (1, 1) + radius = 1 + + with self.assertRaises(TypeError): + # Invalid draw_top_right. + bounds_rect = self.draw_circle( + surface, color, center, radius, 1, "a", 1, 1, 1 + ) + + with self.assertRaises(TypeError): + # Invalid draw_top_left. + bounds_rect = self.draw_circle( + surface, color, center, radius, 1, 1, "b", 1, 1 + ) + + with self.assertRaises(TypeError): + # Invalid draw_bottom_left. + bounds_rect = self.draw_circle( + surface, color, center, radius, 1, 1, 1, "c", 1 + ) + + with self.assertRaises(TypeError): + # Invalid draw_bottom_right. + bounds_rect = self.draw_circle( + surface, color, center, radius, 1, 1, 1, 1, "d" + ) + + with self.assertRaises(TypeError): + # Invalid width. + bounds_rect = self.draw_circle(surface, color, center, radius, "1") + + with self.assertRaises(TypeError): + # Invalid radius. + bounds_rect = self.draw_circle(surface, color, center, "2") + + with self.assertRaises(TypeError): + # Invalid center. + bounds_rect = self.draw_circle(surface, color, (1, 2, 3), radius) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_circle(surface, 2.3, center, radius) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_circle((1, 2, 3, 4), color, center, radius) + + def test_circle__kwarg_invalid_types(self): + """Ensures draw circle detects invalid kwarg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("green") + center = (0, 1) + radius = 1 + width = 1 + quadrant = 1 + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "center": center, + "radius": radius, + "width": width, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "center": center, + "radius": radius, + "width": width, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": color, + "center": (1, 1, 1), # Invalid center. + "radius": radius, + "width": width, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": color, + "center": center, + "radius": "1", # Invalid radius. + "width": width, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "width": 1.2, # Invalid width. + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "width": width, + "draw_top_right": "True", # Invalid draw_top_right + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "width": width, + "draw_top_right": True, + "draw_top_left": "True", # Invalid draw_top_left + "draw_bottom_left": True, + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "width": width, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": 3.14, # Invalid draw_bottom_left + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "width": width, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": "quadrant", # Invalid draw_bottom_right + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_circle(**kwargs) + + def test_circle__kwarg_invalid_name(self): + """Ensures draw circle detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + center = (0, 0) + radius = 2 + kwargs_list = [ + { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "width": 1, + "quadrant": 1, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + }, + { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "invalid": 1, + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_circle(**kwargs) + + def test_circle__args_and_kwargs(self): + """Ensures draw circle accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 1)) + color = (255, 255, 0, 0) + center = (1, 0) + radius = 2 + width = 0 + draw_top_right = True + draw_top_left = False + draw_bottom_left = False + draw_bottom_right = True + kwargs = { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "width": width, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + } + + for name in ( + "surface", + "color", + "center", + "radius", + "width", + "draw_top_right", + "draw_top_left", + "draw_bottom_left", + "draw_bottom_right", + ): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_circle(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_circle(surface, color, **kwargs) + elif "center" == name: + bounds_rect = self.draw_circle(surface, color, center, **kwargs) + elif "radius" == name: + bounds_rect = self.draw_circle(surface, color, center, radius, **kwargs) + elif "width" == name: + bounds_rect = self.draw_circle( + surface, color, center, radius, width, **kwargs + ) + elif "draw_top_right" == name: + bounds_rect = self.draw_circle( + surface, color, center, radius, width, draw_top_right, **kwargs + ) + elif "draw_top_left" == name: + bounds_rect = self.draw_circle( + surface, + color, + center, + radius, + width, + draw_top_right, + draw_top_left, + **kwargs, + ) + elif "draw_bottom_left" == name: + bounds_rect = self.draw_circle( + surface, + color, + center, + radius, + width, + draw_top_right, + draw_top_left, + draw_bottom_left, + **kwargs, + ) + else: + bounds_rect = self.draw_circle( + surface, + color, + center, + radius, + width, + draw_top_right, + draw_top_left, + draw_bottom_left, + draw_bottom_right, + **kwargs, + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__valid_width_values(self): + """Ensures draw circle accepts different width values.""" + center = (2, 2) + radius = 1 + pos = (center[0] - radius, center[1]) + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + color = (10, 20, 30, 255) + kwargs = { + "surface": surface, + "color": color, + "center": center, + "radius": radius, + "width": None, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + } + + for width in (-100, -10, -1, 0, 1, 10, 100): + surface.fill(surface_color) # Clear for each test. + kwargs["width"] = width + expected_color = color if width >= 0 else surface_color + + bounds_rect = self.draw_circle(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__valid_radius_values(self): + """Ensures draw circle accepts different radius values.""" + pos = center = (2, 2) + surface_color = pygame.Color("white") + surface = pygame.Surface((3, 4)) + color = (10, 20, 30, 255) + kwargs = { + "surface": surface, + "color": color, + "center": center, + "radius": None, + "width": 0, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + } + + for radius in (-10, -1, 0, 1, 10): + surface.fill(surface_color) # Clear for each test. + kwargs["radius"] = radius + expected_color = color if radius > 0 else surface_color + + bounds_rect = self.draw_circle(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__valid_center_formats(self): + """Ensures draw circle accepts different center formats.""" + expected_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((4, 4)) + kwargs = { + "surface": surface, + "color": expected_color, + "center": None, + "radius": 1, + "width": 0, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + } + x, y = 2, 2 # center position + + # The center values can be ints or floats. + for center in ((x, y), (x + 0.1, y), (x, y + 0.1), (x + 0.1, y + 0.1)): + # The center type can be a tuple/list/Vector2. + for seq_type in (tuple, list, Vector2): + surface.fill(surface_color) # Clear for each test. + kwargs["center"] = seq_type(center) + + bounds_rect = self.draw_circle(**kwargs) + + self.assertEqual(surface.get_at((x, y)), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__valid_color_formats(self): + """Ensures draw circle accepts different color formats.""" + center = (2, 2) + radius = 1 + pos = (center[0] - radius, center[1]) + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((3, 4)) + kwargs = { + "surface": surface, + "color": None, + "center": center, + "radius": radius, + "width": 0, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + } + greens = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in greens: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_circle(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_circle__invalid_color_formats(self): + """Ensures draw circle handles invalid color formats correctly.""" + kwargs = { + "surface": pygame.Surface((4, 3)), + "color": None, + "center": (1, 2), + "radius": 1, + "width": 0, + "draw_top_right": True, + "draw_top_left": True, + "draw_bottom_left": True, + "draw_bottom_right": True, + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_circle(**kwargs) + + def test_circle__floats(self): + """Ensure that floats are accepted.""" + draw.circle( + surface=pygame.Surface((4, 4)), + color=(255, 255, 127), + center=(1.5, 1.5), + radius=1.3, + width=0, + draw_top_right=True, + draw_top_left=True, + draw_bottom_left=True, + draw_bottom_right=True, + ) + + draw.circle( + surface=pygame.Surface((4, 4)), + color=(255, 255, 127), + center=Vector2(1.5, 1.5), + radius=1.3, + width=0, + draw_top_right=True, + draw_top_left=True, + draw_bottom_left=True, + draw_bottom_right=True, + ) + + draw.circle(pygame.Surface((2, 2)), (0, 0, 0, 50), (1.3, 1.3), 1.2) + + # def test_circle_clip(self): + # """ maybe useful to help work out circle clip algorithm.""" + # MAX = max + # MIN = min + # posx=30 + # posy=15 + # radius=1 + # l=29 + # t=14 + # r=30 + # b=16 + # clip_rect_x=0 + # clip_rect_y=0 + # clip_rect_w=30 + # clip_rect_h=30 + + # l = MAX(posx - radius, clip_rect_x) + # t = MAX(posy - radius, clip_rect_y) + # r = MIN(posx + radius, clip_rect_x + clip_rect_w) + # b = MIN(posy + radius, clip_rect_y + clip_rect_h) + + # l, t, MAX(r - l, 0), MAX(b - t, 0) + + def test_circle__bounding_rect(self): + """Ensures draw circle returns the correct bounding rect. + + Tests circles on and off the surface and a range of width/thickness + values. + """ + circle_color = pygame.Color("red") + surf_color = pygame.Color("black") + max_radius = 3 + surface = pygame.Surface((30, 30), 0, 32) + surf_rect = surface.get_rect() + # Make a rect that is bigger than the surface to help test drawing + # circles off and partially off the surface. Make this rect such that + # when centering the test circle on one of its corners, the circle is + # drawn fully off the test surface, but a rect bounding the circle + # would still overlap with the test surface. + big_rect = surf_rect.inflate(max_radius * 2 - 1, max_radius * 2 - 1) + + for pos in rect_corners_mids_and_center( + surf_rect + ) + rect_corners_mids_and_center(big_rect): + # Test using different radius and thickness values. + for radius in range(max_radius + 1): + for thickness in range(radius + 1): + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_circle( + surface, circle_color, pos, radius, thickness + ) + + # Calculating the expected_rect after the circle is + # drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect(surface, surf_color, pos) + + # print("pos:%s:, radius:%s:, thickness:%s:" % (pos, radius, thickness)) + # self.assertEqual(bounding_rect, expected_rect) + with self.subTest( + surface=surface, + circle_color=circle_color, + pos=pos, + radius=radius, + thickness=thickness, + ): + self.assertEqual(bounding_rect, expected_rect) + + def test_circle_negative_radius(self): + """Ensures negative radius circles return zero sized bounding rect.""" + surf = pygame.Surface((200, 200)) + color = (0, 0, 0, 50) + center = surf.get_height() // 2, surf.get_height() // 2 + + bounding_rect = self.draw_circle(surf, color, center, radius=-1, width=1) + self.assertEqual(bounding_rect.size, (0, 0)) + + def test_circle_zero_radius(self): + """Ensures zero radius circles does not draw a center pixel. + + NOTE: This is backwards incompatible behaviour with 1.9.x. + """ + surf = pygame.Surface((200, 200)) + circle_color = pygame.Color("red") + surf_color = pygame.Color("black") + center = (100, 100) + radius = 0 + width = 1 + + bounding_rect = self.draw_circle(surf, circle_color, center, radius, width) + expected_rect = create_bounding_rect(surf, surf_color, center) + self.assertEqual(bounding_rect, expected_rect) + self.assertEqual(bounding_rect, pygame.Rect(100, 100, 0, 0)) + + def test_circle__surface_clip(self): + """Ensures draw circle respects a surface's clip area. + + Tests drawing the circle filled and unfilled. + """ + surfw = surfh = 25 + circle_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (10, 10)) + clip_rect.center = surface.get_rect().center + radius = clip_rect.w // 2 + 1 + + for width in (0, 1): # Filled and unfilled. + # Test centering the circle along the clip rect's edge. + for center in rect_corners_mids_and_center(clip_rect): + # Get the expected points by drawing the circle without the + # clip area set. + surface.set_clip(None) + surface.fill(surface_color) + self.draw_circle(surface, circle_color, center, radius, width) + expected_pts = get_color_points(surface, circle_color, clip_rect) + + # Clear the surface and set the clip area. Redraw the circle + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_circle(surface, circle_color, center, radius, width) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure only the expected_pts + # are the circle_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + expected_color = circle_color + else: + expected_color = surface_color + + self.assertEqual(surface.get_at(pt), expected_color, pt) + + surface.unlock() + + def test_circle_shape(self): + """Ensures there are no holes in the circle, and no overdrawing. + + Tests drawing a thick circle. + Measures the distance of the drawn pixels from the circle center. + """ + surfw = surfh = 100 + circle_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + (cx, cy) = center = (50, 50) + radius = 45 + width = 25 + + dest_rect = self.draw_circle(surface, circle_color, center, radius, width) + + for pt in test_utils.rect_area_pts(dest_rect): + x, y = pt + sqr_distance = (x - cx) ** 2 + (y - cy) ** 2 + if (radius - width + 1) ** 2 < sqr_distance < (radius - 1) ** 2: + self.assertEqual(surface.get_at(pt), circle_color) + if ( + sqr_distance < (radius - width - 1) ** 2 + or sqr_distance > (radius + 1) ** 2 + ): + self.assertEqual(surface.get_at(pt), surface_color) + + def test_circle__diameter(self): + """Ensures draw circle is twice size of radius high and wide.""" + surf = pygame.Surface((200, 200)) + color = (0, 0, 0, 50) + center = surf.get_height() // 2, surf.get_height() // 2 + width = 1 + radius = 6 + for radius in range(1, 65): + bounding_rect = self.draw_circle(surf, color, center, radius, width) + self.assertEqual(bounding_rect.width, radius * 2) + self.assertEqual(bounding_rect.height, radius * 2) + + def test_x_bounds(self): + """ensures a circle is drawn properly when there is a negative x, or a big x.""" + + surf = pygame.Surface((200, 200)) + bgcolor = (0, 0, 0, 255) + surf.fill(bgcolor) + color = (255, 0, 0, 255) + width = 1 + radius = 10 + + where = (0, 30) + bounding_rect1 = self.draw_circle(surf, color, where, radius=radius) + self.assertEqual( + bounding_rect1, + pygame.Rect(0, where[1] - radius, where[0] + radius, radius * 2), + ) + self.assertEqual( + surf.get_at((where[0] if where[0] > 0 else 0, where[1])), color + ) + self.assertEqual(surf.get_at((where[0] + radius + 1, where[1])), bgcolor) + self.assertEqual(surf.get_at((where[0] + radius - 1, where[1])), color) + + surf.fill(bgcolor) + where = (-1e30, 80) + bounding_rect1 = self.draw_circle(surf, color, where, radius=radius) + self.assertEqual(bounding_rect1, pygame.Rect(where[0], where[1], 0, 0)) + self.assertEqual(surf.get_at((0 + radius, where[1])), bgcolor) + + surf.fill(bgcolor) + where = (surf.get_width() + radius * 2, 80) + bounding_rect1 = self.draw_circle(surf, color, where, radius=radius) + self.assertEqual(bounding_rect1, pygame.Rect(where[0], where[1], 0, 0)) + self.assertEqual(surf.get_at((0, where[1])), bgcolor) + self.assertEqual(surf.get_at((0 + radius // 2, where[1])), bgcolor) + self.assertEqual(surf.get_at((surf.get_width() - 1, where[1])), bgcolor) + self.assertEqual(surf.get_at((surf.get_width() - radius, where[1])), bgcolor) + + surf.fill(bgcolor) + where = (-1, 80) + bounding_rect1 = self.draw_circle(surf, color, where, radius=radius) + self.assertEqual( + bounding_rect1, + pygame.Rect(0, where[1] - radius, where[0] + radius, radius * 2), + ) + self.assertEqual( + surf.get_at((where[0] if where[0] > 0 else 0, where[1])), color + ) + self.assertEqual(surf.get_at((where[0] + radius, where[1])), bgcolor) + self.assertEqual(surf.get_at((where[0] + radius - 1, where[1])), color) + + +class DrawCircleTest(DrawCircleMixin, DrawTestCase): + """Test draw module function circle. + + This class inherits the general tests from DrawCircleMixin. It is also + the class to add any draw.circle specific tests to. + """ + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever properly supports drawing circles. +# @unittest.skip('draw_py.draw_circle not supported yet') +# class PythonDrawCircleTest(DrawCircleMixin, PythonDrawTestCase): +# """Test draw_py module function draw_circle." +# +# This class inherits the general tests from DrawCircleMixin. It is also +# the class to add any draw_py.draw_circle specific tests to. +# """ + + +### Arc Testing ############################################################### + + +class DrawArcMixin: + """Mixin tests for drawing arcs. + + This class contains all the general arc drawing tests. + """ + + def test_arc__args(self): + """Ensures draw arc accepts the correct args.""" + bounds_rect = self.draw_arc( + pygame.Surface((3, 3)), (0, 10, 0, 50), (1, 1, 2, 2), 0, 1, 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_arc__args_without_width(self): + """Ensures draw arc accepts the args without a width.""" + bounds_rect = self.draw_arc( + pygame.Surface((2, 2)), (1, 1, 1, 99), pygame.Rect((0, 0), (2, 2)), 1.1, 2.1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_arc__args_with_negative_width(self): + """Ensures draw arc accepts the args with negative width.""" + bounds_rect = self.draw_arc( + pygame.Surface((3, 3)), (10, 10, 50, 50), (1, 1, 2, 2), 0, 1, -1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + self.assertEqual(bounds_rect, pygame.Rect(1, 1, 0, 0)) + + def test_arc__args_with_width_gt_radius(self): + """Ensures draw arc accepts the args with + width > rect.w // 2 and width > rect.h // 2. + """ + rect = pygame.Rect((0, 0), (4, 4)) + bounds_rect = self.draw_arc( + pygame.Surface((3, 3)), (10, 10, 50, 50), rect, 0, 45, rect.w // 2 + 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + bounds_rect = self.draw_arc( + pygame.Surface((3, 3)), (10, 10, 50, 50), rect, 0, 45, rect.h // 2 + 1 + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_arc__kwargs(self): + """Ensures draw arc accepts the correct kwargs + with and without a width arg. + """ + kwargs_list = [ + { + "surface": pygame.Surface((4, 4)), + "color": pygame.Color("yellow"), + "rect": pygame.Rect((0, 0), (3, 2)), + "start_angle": 0.5, + "stop_angle": 3, + "width": 1, + }, + { + "surface": pygame.Surface((2, 1)), + "color": (0, 10, 20), + "rect": (0, 0, 2, 2), + "start_angle": 1, + "stop_angle": 3.1, + }, + ] + + for kwargs in kwargs_list: + bounds_rect = self.draw_arc(**kwargs) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_arc__kwargs_order_independent(self): + """Ensures draw arc's kwargs are not order dependent.""" + bounds_rect = self.draw_arc( + stop_angle=1, + start_angle=2.2, + color=(1, 2, 3), + surface=pygame.Surface((3, 2)), + width=1, + rect=pygame.Rect((1, 0), (2, 3)), + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_arc__args_missing(self): + """Ensures draw arc detects any missing required args.""" + surface = pygame.Surface((1, 1)) + color = pygame.Color("red") + rect = pygame.Rect((0, 0), (2, 2)) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc(surface, color, rect, 0.1) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc(surface, color, rect) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc(surface, color) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc(surface) + + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc() + + def test_arc__kwargs_missing(self): + """Ensures draw arc detects any missing required kwargs.""" + kwargs = { + "surface": pygame.Surface((1, 2)), + "color": pygame.Color("red"), + "rect": pygame.Rect((1, 0), (2, 2)), + "start_angle": 0.1, + "stop_angle": 2, + "width": 1, + } + + for name in ("stop_angle", "start_angle", "rect", "color", "surface"): + invalid_kwargs = dict(kwargs) + invalid_kwargs.pop(name) # Pop from a copy. + + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc(**invalid_kwargs) + + def test_arc__arg_invalid_types(self): + """Ensures draw arc detects invalid arg types.""" + surface = pygame.Surface((2, 2)) + color = pygame.Color("blue") + rect = pygame.Rect((1, 1), (3, 3)) + + with self.assertRaises(TypeError): + # Invalid width. + bounds_rect = self.draw_arc(surface, color, rect, 0, 1, "1") + + with self.assertRaises(TypeError): + # Invalid stop_angle. + bounds_rect = self.draw_arc(surface, color, rect, 0, "1", 1) + + with self.assertRaises(TypeError): + # Invalid start_angle. + bounds_rect = self.draw_arc(surface, color, rect, "1", 0, 1) + + with self.assertRaises(TypeError): + # Invalid rect. + bounds_rect = self.draw_arc(surface, color, (1, 2, 3, 4, 5), 0, 1, 1) + + with self.assertRaises(TypeError): + # Invalid color. + bounds_rect = self.draw_arc(surface, 2.3, rect, 0, 1, 1) + + with self.assertRaises(TypeError): + # Invalid surface. + bounds_rect = self.draw_arc(rect, color, rect, 0, 1, 1) + + def test_arc__kwarg_invalid_types(self): + """Ensures draw arc detects invalid kwarg types.""" + surface = pygame.Surface((3, 3)) + color = pygame.Color("green") + rect = pygame.Rect((0, 1), (4, 2)) + start = 3 + stop = 4 + kwargs_list = [ + { + "surface": pygame.Surface, # Invalid surface. + "color": color, + "rect": rect, + "start_angle": start, + "stop_angle": stop, + "width": 1, + }, + { + "surface": surface, + "color": 2.3, # Invalid color. + "rect": rect, + "start_angle": start, + "stop_angle": stop, + "width": 1, + }, + { + "surface": surface, + "color": color, + "rect": (0, 0, 0), # Invalid rect. + "start_angle": start, + "stop_angle": stop, + "width": 1, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "start_angle": "1", # Invalid start_angle. + "stop_angle": stop, + "width": 1, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "start_angle": start, + "stop_angle": "1", # Invalid stop_angle. + "width": 1, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "start_angle": start, + "stop_angle": stop, + "width": 1.1, + }, + ] # Invalid width. + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc(**kwargs) + + def test_arc__kwarg_invalid_name(self): + """Ensures draw arc detects invalid kwarg names.""" + surface = pygame.Surface((2, 3)) + color = pygame.Color("cyan") + rect = pygame.Rect((0, 1), (2, 2)) + start = 0.9 + stop = 2.3 + kwargs_list = [ + { + "surface": surface, + "color": color, + "rect": rect, + "start_angle": start, + "stop_angle": stop, + "width": 1, + "invalid": 1, + }, + { + "surface": surface, + "color": color, + "rect": rect, + "start_angle": start, + "stop_angle": stop, + "invalid": 1, + }, + ] + + for kwargs in kwargs_list: + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc(**kwargs) + + def test_arc__args_and_kwargs(self): + """Ensures draw arc accepts a combination of args/kwargs""" + surface = pygame.Surface((3, 1)) + color = (255, 255, 0, 0) + rect = pygame.Rect((1, 0), (2, 3)) + start = 0.6 + stop = 2 + width = 1 + kwargs = { + "surface": surface, + "color": color, + "rect": rect, + "start_angle": start, + "stop_angle": stop, + "width": width, + } + + for name in ("surface", "color", "rect", "start_angle", "stop_angle"): + kwargs.pop(name) + + if "surface" == name: + bounds_rect = self.draw_arc(surface, **kwargs) + elif "color" == name: + bounds_rect = self.draw_arc(surface, color, **kwargs) + elif "rect" == name: + bounds_rect = self.draw_arc(surface, color, rect, **kwargs) + elif "start_angle" == name: + bounds_rect = self.draw_arc(surface, color, rect, start, **kwargs) + elif "stop_angle" == name: + bounds_rect = self.draw_arc(surface, color, rect, start, stop, **kwargs) + else: + bounds_rect = self.draw_arc( + surface, color, rect, start, stop, width, **kwargs + ) + + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_arc__valid_width_values(self): + """Ensures draw arc accepts different width values.""" + arc_color = pygame.Color("yellow") + surface_color = pygame.Color("white") + surface = pygame.Surface((6, 6)) + rect = pygame.Rect((0, 0), (4, 4)) + rect.center = surface.get_rect().center + pos = rect.centerx + 1, rect.centery + 1 + kwargs = { + "surface": surface, + "color": arc_color, + "rect": rect, + "start_angle": 0, + "stop_angle": 7, + "width": None, + } + + for width in (-50, -10, -3, -2, -1, 0, 1, 2, 3, 10, 50): + msg = f"width={width}" + surface.fill(surface_color) # Clear for each test. + kwargs["width"] = width + expected_color = arc_color if width > 0 else surface_color + + bounds_rect = self.draw_arc(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color, msg) + self.assertIsInstance(bounds_rect, pygame.Rect, msg) + + def test_arc__valid_stop_angle_values(self): + """Ensures draw arc accepts different stop_angle values.""" + expected_color = pygame.Color("blue") + surface_color = pygame.Color("white") + surface = pygame.Surface((6, 6)) + rect = pygame.Rect((0, 0), (4, 4)) + rect.center = surface.get_rect().center + pos = rect.centerx, rect.centery + 1 + kwargs = { + "surface": surface, + "color": expected_color, + "rect": rect, + "start_angle": -17, + "stop_angle": None, + "width": 1, + } + + for stop_angle in (-10, -5.5, -1, 0, 1, 5.5, 10): + msg = f"stop_angle={stop_angle}" + surface.fill(surface_color) # Clear for each test. + kwargs["stop_angle"] = stop_angle + + bounds_rect = self.draw_arc(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color, msg) + self.assertIsInstance(bounds_rect, pygame.Rect, msg) + + def test_arc__valid_start_angle_values(self): + """Ensures draw arc accepts different start_angle values.""" + expected_color = pygame.Color("blue") + surface_color = pygame.Color("white") + surface = pygame.Surface((6, 6)) + rect = pygame.Rect((0, 0), (4, 4)) + rect.center = surface.get_rect().center + pos = rect.centerx + 1, rect.centery + 1 + kwargs = { + "surface": surface, + "color": expected_color, + "rect": rect, + "start_angle": None, + "stop_angle": 17, + "width": 1, + } + + for start_angle in (-10.0, -5.5, -1, 0, 1, 5.5, 10.0): + msg = f"start_angle={start_angle}" + surface.fill(surface_color) # Clear for each test. + kwargs["start_angle"] = start_angle + + bounds_rect = self.draw_arc(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color, msg) + self.assertIsInstance(bounds_rect, pygame.Rect, msg) + + def test_arc__valid_rect_formats(self): + """Ensures draw arc accepts different rect formats.""" + expected_color = pygame.Color("red") + surface_color = pygame.Color("black") + surface = pygame.Surface((6, 6)) + rect = pygame.Rect((0, 0), (4, 4)) + rect.center = surface.get_rect().center + pos = rect.centerx + 1, rect.centery + 1 + kwargs = { + "surface": surface, + "color": expected_color, + "rect": None, + "start_angle": 0, + "stop_angle": 7, + "width": 1, + } + rects = (rect, (rect.topleft, rect.size), (rect.x, rect.y, rect.w, rect.h)) + + for rect in rects: + surface.fill(surface_color) # Clear for each test. + kwargs["rect"] = rect + + bounds_rect = self.draw_arc(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_arc__valid_color_formats(self): + """Ensures draw arc accepts different color formats.""" + green_color = pygame.Color("green") + surface_color = pygame.Color("black") + surface = pygame.Surface((6, 6)) + rect = pygame.Rect((0, 0), (4, 4)) + rect.center = surface.get_rect().center + pos = rect.centerx + 1, rect.centery + 1 + kwargs = { + "surface": surface, + "color": None, + "rect": rect, + "start_angle": 0, + "stop_angle": 7, + "width": 1, + } + greens = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(green_color), + green_color, + ) + + for color in greens: + surface.fill(surface_color) # Clear for each test. + kwargs["color"] = color + + if isinstance(color, int): + expected_color = surface.unmap_rgb(color) + else: + expected_color = green_color + + bounds_rect = self.draw_arc(**kwargs) + + self.assertEqual(surface.get_at(pos), expected_color) + self.assertIsInstance(bounds_rect, pygame.Rect) + + def test_arc__invalid_color_formats(self): + """Ensures draw arc handles invalid color formats correctly.""" + pos = (1, 1) + surface = pygame.Surface((4, 3)) + kwargs = { + "surface": surface, + "color": None, + "rect": pygame.Rect(pos, (2, 2)), + "start_angle": 5, + "stop_angle": 6.1, + "width": 1, + } + + for expected_color in (2.3, self): + kwargs["color"] = expected_color + + with self.assertRaises(TypeError): + bounds_rect = self.draw_arc(**kwargs) + + def test_arc(self): + """Ensure draw arc works correctly.""" + black = pygame.Color("black") + red = pygame.Color("red") + + # create an image object of width 100, height 150, filled with black. + surface = pygame.Surface((100, 150)) + surface.fill(black) + + # rectangle that contains for the ellipse arc. + # 0 pixel from left, 0 pixel from top + # 80 pixels wide, 40 pixels high + rect = (0, 0, 80, 40) + + # angle of the arc in radians + start_angle = 0.0 + stop_angle = 3.14 + + # thickness, and it grows inward from the rectangle + width = 3 + + # draw an elliptical arc + pygame.draw.arc(surface, red, rect, start_angle, stop_angle, width) + + # Save the drawn arc + pygame.image.save(surface, "arc.png") + + # arc is red + x = 20 + for y in range(2, 5): + self.assertEqual(surface.get_at((x, y)), red) + + # the rest area in surface is black + self.assertEqual(surface.get_at((0, 0)), black) + + def test_arc__bounding_rect(self): + """Ensures draw arc returns the correct bounding rect. + + Tests arcs on and off the surface and a range of width/thickness + values. + """ + arc_color = pygame.Color("red") + surf_color = pygame.Color("black") + min_width = min_height = 5 + max_width = max_height = 7 + sizes = ((min_width, min_height), (max_width, max_height)) + surface = pygame.Surface((20, 20), 0, 32) + surf_rect = surface.get_rect() + # Make a rect that is bigger than the surface to help test drawing + # arcs off and partially off the surface. + big_rect = surf_rect.inflate(min_width * 2 + 1, min_height * 2 + 1) + + # Max angle allows for a full circle to be drawn. + start_angle = 0 + stop_angles = (0, 2, 3, 5, math.ceil(2 * math.pi)) + + for pos in rect_corners_mids_and_center( + surf_rect + ) + rect_corners_mids_and_center(big_rect): + # Each of the arc's rect position attributes will be set to the pos + # value. + for attr in RECT_POSITION_ATTRIBUTES: + # Test using different rect sizes, thickness values and stop + # angles. + for width, height in sizes: + arc_rect = pygame.Rect((0, 0), (width, height)) + setattr(arc_rect, attr, pos) + + for thickness in (0, 1, 2, 3, min(width, height)): + for stop_angle in stop_angles: + surface.fill(surf_color) # Clear for each test. + + bounding_rect = self.draw_arc( + surface, + arc_color, + arc_rect, + start_angle, + stop_angle, + thickness, + ) + + # Calculating the expected_rect after the arc + # is drawn (it uses what is actually drawn). + expected_rect = create_bounding_rect( + surface, surf_color, arc_rect.topleft + ) + + self.assertEqual( + bounding_rect, + expected_rect, + f"thickness={thickness}", + ) + + def test_arc__surface_clip(self): + """Ensures draw arc respects a surface's clip area.""" + surfw = surfh = 30 + start = 0.1 + end = 0 # end < start so a full circle will be drawn + arc_color = pygame.Color("red") + surface_color = pygame.Color("green") + surface = pygame.Surface((surfw, surfh)) + surface.fill(surface_color) + + clip_rect = pygame.Rect((0, 0), (11, 11)) + clip_rect.center = surface.get_rect().center + pos_rect = clip_rect.copy() # Manages the arc's pos. + + for thickness in (1, 3): # Different line widths. + # Test centering the arc along the clip rect's edge. + for center in rect_corners_mids_and_center(clip_rect): + # Get the expected points by drawing the arc without the + # clip area set. + pos_rect.center = center + surface.set_clip(None) + surface.fill(surface_color) + self.draw_arc(surface, arc_color, pos_rect, start, end, thickness) + expected_pts = get_color_points(surface, arc_color, clip_rect) + + # Clear the surface and set the clip area. Redraw the arc + # and check that only the clip area is modified. + surface.fill(surface_color) + surface.set_clip(clip_rect) + + self.draw_arc(surface, arc_color, pos_rect, start, end, thickness) + + surface.lock() # For possible speed up. + + # Check all the surface points to ensure only the expected_pts + # are the arc_color. + for pt in ((x, y) for x in range(surfw) for y in range(surfh)): + if pt in expected_pts: + expected_color = arc_color + else: + expected_color = surface_color + + self.assertEqual(surface.get_at(pt), expected_color, pt) + + surface.unlock() + + +class DrawArcTest(DrawArcMixin, DrawTestCase): + """Test draw module function arc. + + This class inherits the general tests from DrawArcMixin. It is also the + class to add any draw.arc specific tests to. + """ + + +# Commented out to avoid cluttering the test output. Add back in if draw_py +# ever properly supports drawing arcs. +# @unittest.skip('draw_py.draw_arc not supported yet') +# class PythonDrawArcTest(DrawArcMixin, PythonDrawTestCase): +# """Test draw_py module function draw_arc. +# +# This class inherits the general tests from DrawArcMixin. It is also the +# class to add any draw_py.draw_arc specific tests to. +# """ + + +### Draw Module Testing ####################################################### + + +class DrawModuleTest(unittest.TestCase): + """General draw module tests.""" + + def test_path_data_validation(self): + """Test validation of multi-point drawing methods. + + See bug #521 + """ + surf = pygame.Surface((5, 5)) + rect = pygame.Rect(0, 0, 5, 5) + bad_values = ( + "text", + b"bytes", + 1 + 1j, # string, bytes, complex, + object(), + (lambda x: x), + ) # object, function + bad_points = list(bad_values) + [(1,), (1, 2, 3)] # wrong tuple length + bad_points.extend((1, v) for v in bad_values) # one wrong value + good_path = [(1, 1), (1, 3), (3, 3), (3, 1)] + # A) draw.lines + check_pts = [(x, y) for x in range(5) for y in range(5)] + + for method, is_polgon in ( + (draw.lines, 0), + (draw.aalines, 0), + (draw.polygon, 1), + ): + for val in bad_values: + # 1. at the beginning + draw.rect(surf, RED, rect, 0) + with self.assertRaises(TypeError): + if is_polgon: + method(surf, GREEN, [val] + good_path, 0) + else: + method(surf, GREEN, True, [val] + good_path) + + # make sure, nothing was drawn : + self.assertTrue(all(surf.get_at(pt) == RED for pt in check_pts)) + + # 2. not at the beginning (was not checked) + draw.rect(surf, RED, rect, 0) + with self.assertRaises(TypeError): + path = good_path[:2] + [val] + good_path[2:] + if is_polgon: + method(surf, GREEN, path, 0) + else: + method(surf, GREEN, True, path) + + # make sure, nothing was drawn : + self.assertTrue(all(surf.get_at(pt) == RED for pt in check_pts)) + + def test_color_validation(self): + surf = pygame.Surface((10, 10)) + colors = 123456, (1, 10, 100), RED, "#ab12df", "red" + points = ((0, 0), (1, 1), (1, 0)) + + # 1. valid colors + for col in colors: + draw.line(surf, col, (0, 0), (1, 1)) + draw.aaline(surf, col, (0, 0), (1, 1)) + draw.aalines(surf, col, True, points) + draw.lines(surf, col, True, points) + draw.arc(surf, col, pygame.Rect(0, 0, 3, 3), 15, 150) + draw.ellipse(surf, col, pygame.Rect(0, 0, 3, 6), 1) + draw.circle(surf, col, (7, 3), 2) + draw.polygon(surf, col, points, 0) + + # 2. invalid colors + for col in (1.256, object(), None): + with self.assertRaises(TypeError): + draw.line(surf, col, (0, 0), (1, 1)) + + with self.assertRaises(TypeError): + draw.aaline(surf, col, (0, 0), (1, 1)) + + with self.assertRaises(TypeError): + draw.aalines(surf, col, True, points) + + with self.assertRaises(TypeError): + draw.lines(surf, col, True, points) + + with self.assertRaises(TypeError): + draw.arc(surf, col, pygame.Rect(0, 0, 3, 3), 15, 150) + + with self.assertRaises(TypeError): + draw.ellipse(surf, col, pygame.Rect(0, 0, 3, 6), 1) + + with self.assertRaises(TypeError): + draw.circle(surf, col, (7, 3), 2) + + with self.assertRaises(TypeError): + draw.polygon(surf, col, points, 0) + + +############################################################################### + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/event_test.py b/laplas/abstract_map/pygame/tests/event_test.py new file mode 100644 index 0000000..eb7c437 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/event_test.py @@ -0,0 +1,961 @@ +import collections +import time +import unittest +import os + +import pygame + +EVENT_TYPES = ( + # pygame.NOEVENT, + # pygame.ACTIVEEVENT, + pygame.KEYDOWN, + pygame.KEYUP, + pygame.MOUSEMOTION, + pygame.MOUSEBUTTONDOWN, + pygame.MOUSEBUTTONUP, + pygame.JOYAXISMOTION, + pygame.JOYBALLMOTION, + pygame.JOYHATMOTION, + pygame.JOYBUTTONDOWN, + pygame.JOYBUTTONUP, + pygame.VIDEORESIZE, + pygame.VIDEOEXPOSE, + pygame.QUIT, + pygame.SYSWMEVENT, + pygame.USEREVENT, + # pygame.NUMEVENTS, +) + +EVENT_TEST_PARAMS = collections.defaultdict(dict) +EVENT_TEST_PARAMS.update( + { + pygame.KEYDOWN: {"key": pygame.K_SPACE}, + pygame.KEYUP: {"key": pygame.K_SPACE}, + pygame.MOUSEMOTION: {}, + pygame.MOUSEBUTTONDOWN: {"button": 1}, + pygame.MOUSEBUTTONUP: {"button": 1}, + } +) + + +NAMES_AND_EVENTS = ( + ("NoEvent", pygame.NOEVENT), + ("ActiveEvent", pygame.ACTIVEEVENT), + ("KeyDown", pygame.KEYDOWN), + ("KeyUp", pygame.KEYUP), + ("MouseMotion", pygame.MOUSEMOTION), + ("MouseButtonDown", pygame.MOUSEBUTTONDOWN), + ("MouseButtonUp", pygame.MOUSEBUTTONUP), + ("JoyAxisMotion", pygame.JOYAXISMOTION), + ("JoyBallMotion", pygame.JOYBALLMOTION), + ("JoyHatMotion", pygame.JOYHATMOTION), + ("JoyButtonDown", pygame.JOYBUTTONDOWN), + ("JoyButtonUp", pygame.JOYBUTTONUP), + ("VideoResize", pygame.VIDEORESIZE), + ("VideoExpose", pygame.VIDEOEXPOSE), + ("Quit", pygame.QUIT), + ("SysWMEvent", pygame.SYSWMEVENT), + ("MidiIn", pygame.MIDIIN), + ("MidiOut", pygame.MIDIOUT), + ("UserEvent", pygame.USEREVENT), + ("Unknown", 0xFFFF), + ("FingerMotion", pygame.FINGERMOTION), + ("FingerDown", pygame.FINGERDOWN), + ("FingerUp", pygame.FINGERUP), + ("MultiGesture", pygame.MULTIGESTURE), + ("MouseWheel", pygame.MOUSEWHEEL), + ("TextInput", pygame.TEXTINPUT), + ("TextEditing", pygame.TEXTEDITING), + ("ControllerAxisMotion", pygame.CONTROLLERAXISMOTION), + ("ControllerButtonDown", pygame.CONTROLLERBUTTONDOWN), + ("ControllerButtonUp", pygame.CONTROLLERBUTTONUP), + ("ControllerDeviceAdded", pygame.CONTROLLERDEVICEADDED), + ("ControllerDeviceRemoved", pygame.CONTROLLERDEVICEREMOVED), + ("ControllerDeviceMapped", pygame.CONTROLLERDEVICEREMAPPED), + ("DropFile", pygame.DROPFILE), + ("AudioDeviceAdded", pygame.AUDIODEVICEADDED), + ("AudioDeviceRemoved", pygame.AUDIODEVICEREMOVED), + ("DropText", pygame.DROPTEXT), + ("DropBegin", pygame.DROPBEGIN), + ("DropComplete", pygame.DROPCOMPLETE), +) + + +class EventTypeTest(unittest.TestCase): + def test_Event(self): + """Ensure an Event object can be created.""" + e = pygame.event.Event(pygame.USEREVENT, some_attr=1, other_attr="1") + + self.assertEqual(e.some_attr, 1) + self.assertEqual(e.other_attr, "1") + + # Event now uses tp_dictoffset and tp_members: + # https://github.com/pygame/pygame/issues/62 + self.assertEqual(e.type, pygame.USEREVENT) + self.assertIs(e.dict, e.__dict__) + + e.some_attr = 12 + + self.assertEqual(e.some_attr, 12) + + e.new_attr = 15 + + self.assertEqual(e.new_attr, 15) + + self.assertRaises(AttributeError, setattr, e, "type", 0) + self.assertRaises(AttributeError, setattr, e, "dict", None) + + # Ensure attributes are visible to dir(), part of the original + # posted request. + d = dir(e) + attrs = ("type", "dict", "__dict__", "some_attr", "other_attr", "new_attr") + + for attr in attrs: + self.assertIn(attr, d) + + # redundant type field as kwarg + self.assertRaises(ValueError, pygame.event.Event, 10, type=100) + + def test_as_str(self): + # Bug reported on Pygame mailing list July 24, 2011: + # For Python 3.x str(event) to raises an UnicodeEncodeError when + # an event attribute is a string with a non-ascii character. + try: + str(pygame.event.Event(EVENT_TYPES[0], a="\xed")) + except UnicodeEncodeError: + self.fail("Event object raised exception for non-ascii character") + # Passed. + + def test_event_bool(self): + self.assertFalse(pygame.event.Event(pygame.NOEVENT)) + for event_type in [ + pygame.MOUSEBUTTONDOWN, + pygame.ACTIVEEVENT, + pygame.WINDOWLEAVE, + pygame.USEREVENT_DROPFILE, + ]: + self.assertTrue(pygame.event.Event(event_type)) + + def test_event_equality(self): + """Ensure that events can be compared correctly.""" + a = pygame.event.Event(EVENT_TYPES[0], a=1) + b = pygame.event.Event(EVENT_TYPES[0], a=1) + c = pygame.event.Event(EVENT_TYPES[1], a=1) + d = pygame.event.Event(EVENT_TYPES[0], a=2) + + # Comparing event a + self.assertEqual(a, a) # Event is equal to itself + self.assertEqual(a, b) # Same type and attributes + self.assertNotEqual(a, c) # Different type but same attributes + self.assertNotEqual(a, d) # Same type but different attributes + + # Comparing event b + self.assertEqual(b, a) # Same type and attributes + self.assertNotEqual(b, c) # Different type but same attributes + self.assertNotEqual(b, d) # Same type but different attributes + + # Comparing event c + self.assertNotEqual(c, a) # Different type but same attributes + self.assertNotEqual(c, b) # Different type but same attributes + self.assertNotEqual(c, d) # Different type and different attributes + + # Comparing event d + self.assertNotEqual(d, a) # Same type but different attributes + self.assertNotEqual(d, b) # Same type but different attributes + self.assertNotEqual(d, c) # Different type and different attributes + + +race_condition_notification = """ +This test is dependent on timing. The event queue is cleared in preparation for +tests. There is a small window where outside events from the OS may have effected +results. Try running the test again. +""" + + +class EventModuleArgsTest(unittest.TestCase): + def setUp(self): + pygame.display.init() + pygame.event.clear() + + def tearDown(self): + pygame.display.quit() + + def test_get(self): + pygame.event.get() + pygame.event.get(None) + pygame.event.get(None, True) + + pygame.event.get(pump=False) + pygame.event.get(pump=True) + pygame.event.get(eventtype=None) + pygame.event.get(eventtype=[pygame.KEYUP, pygame.KEYDOWN]) + pygame.event.get(eventtype=pygame.USEREVENT, pump=False) + + # event type out of range + self.assertRaises(ValueError, pygame.event.get, 0x00010000) + self.assertRaises(TypeError, pygame.event.get, 1 + 2j) + self.assertRaises(TypeError, pygame.event.get, "foo") + + def test_clear(self): + pygame.event.clear() + pygame.event.clear(None) + pygame.event.clear(None, True) + + pygame.event.clear(pump=False) + pygame.event.clear(pump=True) + pygame.event.clear(eventtype=None) + pygame.event.clear(eventtype=[pygame.KEYUP, pygame.KEYDOWN]) + pygame.event.clear(eventtype=pygame.USEREVENT, pump=False) + + # event type out of range + self.assertRaises(ValueError, pygame.event.clear, 0x0010FFFFF) + self.assertRaises(TypeError, pygame.event.get, ["a", "b", "c"]) + + def test_peek(self): + pygame.event.peek() + pygame.event.peek(None) + pygame.event.peek(None, True) + + pygame.event.peek(pump=False) + pygame.event.peek(pump=True) + pygame.event.peek(eventtype=None) + pygame.event.peek(eventtype=[pygame.KEYUP, pygame.KEYDOWN]) + pygame.event.peek(eventtype=pygame.USEREVENT, pump=False) + + class Foo: + pass + + # event type out of range + self.assertRaises(ValueError, pygame.event.peek, -1) + self.assertRaises(ValueError, pygame.event.peek, [-10]) + self.assertRaises(TypeError, pygame.event.peek, Foo()) + + +class EventCustomTypeTest(unittest.TestCase): + """Those tests are special in that they need the _custom_event counter to + be reset before and/or after being run.""" + + def setUp(self): + pygame.quit() + pygame.init() + pygame.display.init() + + def tearDown(self): + pygame.quit() + + def test_custom_type(self): + self.assertEqual(pygame.event.custom_type(), pygame.USEREVENT + 1) + atype = pygame.event.custom_type() + atype2 = pygame.event.custom_type() + + self.assertEqual(atype, atype2 - 1) + + ev = pygame.event.Event(atype) + pygame.event.post(ev) + queue = pygame.event.get(atype) + self.assertEqual(len(queue), 1) + self.assertEqual(queue[0].type, atype) + + def test_custom_type__end_boundary(self): + """Ensure custom_type() raises error when no more custom types. + + The last allowed custom type number should be (pygame.NUMEVENTS - 1). + """ + last = -1 + start = pygame.event.custom_type() + 1 + for _ in range(start, pygame.NUMEVENTS): + last = pygame.event.custom_type() + + self.assertEqual(last, pygame.NUMEVENTS - 1) + with self.assertRaises(pygame.error): + pygame.event.custom_type() + + def test_custom_type__reset(self): + """Ensure custom events get 'deregistered' by quit().""" + before = pygame.event.custom_type() + self.assertEqual(before, pygame.event.custom_type() - 1) + pygame.quit() + pygame.init() + pygame.display.init() + self.assertEqual(before, pygame.event.custom_type()) + + +class EventModuleTest(unittest.TestCase): + def _assertCountEqual(self, *args, **kwargs): + # Handle method name differences between Python versions. + # Is this still needed? + self.assertCountEqual(*args, **kwargs) + + def _assertExpectedEvents(self, expected, got): + """Find events like expected events, raise on unexpected or missing, + ignore additional event properties if expected properties are present.""" + + # This does greedy matching, don't encode an NP-hard problem + # into your input data, *please* + items_left = got[:] + for expected_element in expected: + for item in items_left: + for key in expected_element.__dict__: + if item.__dict__[key] != expected_element.__dict__[key]: + break + else: + # found item! + items_left.remove(item) + break + else: + raise AssertionError( + "Expected " + + str(expected_element) + + " among remaining events " + + str(items_left) + + " out of " + + str(got) + ) + if len(items_left) > 0: + raise AssertionError("Unexpected Events: " + str(items_left)) + + def setUp(self): + pygame.display.init() + pygame.event.clear() # flush events + + def tearDown(self): + pygame.event.clear() # flush events + pygame.display.quit() + + def test_event_numevents(self): + """Ensures NUMEVENTS does not exceed the maximum SDL number of events.""" + # Ref: https://www.libsdl.org/tmp/SDL/include/SDL_events.h + MAX_SDL_EVENTS = 0xFFFF # SDL_LASTEVENT = 0xFFFF + + self.assertLessEqual(pygame.NUMEVENTS, MAX_SDL_EVENTS) + + def test_event_attribute(self): + e1 = pygame.event.Event(pygame.USEREVENT, attr1="attr1") + self.assertEqual(e1.attr1, "attr1") + + def test_set_blocked(self): + """Ensure events can be blocked from the queue.""" + event = EVENT_TYPES[0] + unblocked_event = EVENT_TYPES[1] + pygame.event.set_blocked(event) + + self.assertTrue(pygame.event.get_blocked(event)) + self.assertFalse(pygame.event.get_blocked(unblocked_event)) + + posted = pygame.event.post( + pygame.event.Event(event, **EVENT_TEST_PARAMS[event]) + ) + self.assertFalse(posted) + + # post an unblocked event + posted = pygame.event.post( + pygame.event.Event(unblocked_event, **EVENT_TEST_PARAMS[unblocked_event]) + ) + self.assertTrue(posted) + + ret = pygame.event.get() + should_be_blocked = [e for e in ret if e.type == event] + should_be_allowed_types = [e.type for e in ret if e.type != event] + + self.assertEqual(should_be_blocked, []) + self.assertTrue(unblocked_event in should_be_allowed_types) + + def test_set_blocked__event_sequence(self): + """Ensure a sequence of event types can be blocked.""" + event_types = [ + pygame.KEYDOWN, + pygame.KEYUP, + pygame.MOUSEMOTION, + pygame.MOUSEBUTTONDOWN, + pygame.MOUSEBUTTONUP, + pygame.WINDOWFOCUSLOST, + pygame.USEREVENT, + ] + + pygame.event.set_blocked(event_types) + + for etype in event_types: + self.assertTrue(pygame.event.get_blocked(etype)) + + def test_set_blocked_all(self): + """Ensure all events can be unblocked at once.""" + pygame.event.set_blocked(None) + + for e in EVENT_TYPES: + self.assertTrue(pygame.event.get_blocked(e)) + + def test_post__and_poll(self): + """Ensure events can be posted to the queue.""" + e1 = pygame.event.Event(pygame.USEREVENT, attr1="attr1") + pygame.event.post(e1) + posted_event = pygame.event.poll() + + self.assertEqual(e1.attr1, posted_event.attr1, race_condition_notification) + + # fuzzing event types + for i in range(1, 13): + pygame.event.post( + pygame.event.Event(EVENT_TYPES[i], **EVENT_TEST_PARAMS[EVENT_TYPES[i]]) + ) + + self.assertEqual( + pygame.event.poll().type, EVENT_TYPES[i], race_condition_notification + ) + + def test_post_and_get_keydown(self): + """Ensure keydown events can be posted to the queue.""" + activemodkeys = pygame.key.get_mods() + + events = [ + pygame.event.Event(pygame.KEYDOWN, key=pygame.K_p), + pygame.event.Event(pygame.KEYDOWN, key=pygame.K_y, mod=activemodkeys), + pygame.event.Event(pygame.KEYDOWN, key=pygame.K_g, unicode="g"), + pygame.event.Event(pygame.KEYDOWN, key=pygame.K_a, unicode=None), + pygame.event.Event(pygame.KEYDOWN, key=pygame.K_m, mod=None, window=None), + pygame.event.Event( + pygame.KEYDOWN, key=pygame.K_e, mod=activemodkeys, unicode="e" + ), + ] + + for e in events: + pygame.event.post(e) + posted_event = pygame.event.poll() + self.assertEqual(e, posted_event, race_condition_notification) + + def test_post_large_user_event(self): + pygame.event.post( + pygame.event.Event( + pygame.USEREVENT, {"a": "a" * 1024}, test=list(range(100)) + ) + ) + e = pygame.event.poll() + + self.assertEqual(e.type, pygame.USEREVENT) + self.assertEqual(e.a, "a" * 1024) + self.assertEqual(e.test, list(range(100))) + + def test_post_blocked(self): + """ + Test blocked events are not posted. Also test whether post() + returns a boolean correctly + """ + pygame.event.set_blocked(pygame.USEREVENT) + self.assertFalse(pygame.event.post(pygame.event.Event(pygame.USEREVENT))) + self.assertFalse(pygame.event.poll()) + pygame.event.set_allowed(pygame.USEREVENT) + self.assertTrue(pygame.event.post(pygame.event.Event(pygame.USEREVENT))) + self.assertEqual(pygame.event.poll(), pygame.event.Event(pygame.USEREVENT)) + + def test_get(self): + """Ensure get() retrieves all the events on the queue.""" + event_cnt = 10 + for _ in range(event_cnt): + pygame.event.post(pygame.event.Event(pygame.USEREVENT)) + + queue = pygame.event.get() + + self.assertEqual(len(queue), event_cnt) + self.assertTrue(all(e.type == pygame.USEREVENT for e in queue)) + + def test_get_type(self): + ev = pygame.event.Event(pygame.USEREVENT) + pygame.event.post(ev) + queue = pygame.event.get(pygame.USEREVENT) + self.assertEqual(len(queue), 1) + self.assertEqual(queue[0].type, pygame.USEREVENT) + + TESTEVENTS = 10 + for _ in range(TESTEVENTS): + pygame.event.post(ev) + q = pygame.event.get([pygame.USEREVENT]) + self.assertEqual(len(q), TESTEVENTS) + for event in q: + self.assertEqual(event, ev) + + def test_get_exclude_throw(self): + self.assertRaises( + pygame.error, pygame.event.get, pygame.KEYDOWN, False, pygame.KEYUP + ) + + def test_get_exclude(self): + pygame.event.post(pygame.event.Event(pygame.USEREVENT)) + pygame.event.post(pygame.event.Event(pygame.KEYDOWN)) + + queue = pygame.event.get(exclude=pygame.KEYDOWN) + self.assertEqual(len(queue), 1) + self.assertEqual(queue[0].type, pygame.USEREVENT) + + pygame.event.post(pygame.event.Event(pygame.KEYUP)) + pygame.event.post(pygame.event.Event(pygame.USEREVENT)) + queue = pygame.event.get(exclude=(pygame.KEYDOWN, pygame.KEYUP)) + self.assertEqual(len(queue), 1) + self.assertEqual(queue[0].type, pygame.USEREVENT) + + queue = pygame.event.get() + self.assertEqual(len(queue), 2) + + def test_get__empty_queue(self): + """Ensure get() works correctly on an empty queue.""" + expected_events = [] + pygame.event.clear() + + # Ensure all events can be checked. + retrieved_events = pygame.event.get() + + self.assertListEqual(retrieved_events, expected_events) + + # Ensure events can be checked individually. + for event_type in EVENT_TYPES: + retrieved_events = pygame.event.get(event_type) + + self.assertListEqual(retrieved_events, expected_events) + + # Ensure events can be checked as a sequence. + retrieved_events = pygame.event.get(EVENT_TYPES) + + self.assertListEqual(retrieved_events, expected_events) + + def test_get__event_sequence(self): + """Ensure get() can handle a sequence of event types.""" + event_types = [pygame.KEYDOWN, pygame.KEYUP, pygame.MOUSEMOTION] + other_event_type = pygame.MOUSEBUTTONUP + + # Test when no events in the queue. + expected_events = [] + pygame.event.clear() + retrieved_events = pygame.event.get(event_types) + + # don't use self._assertCountEqual here. This checks for + # expected properties in events, and ignores unexpected ones, for + # forward compatibility with SDL2. + self._assertExpectedEvents(expected=expected_events, got=retrieved_events) + + # Test when an event type not in the list is in the queue. + expected_events = [] + pygame.event.clear() + pygame.event.post( + pygame.event.Event(other_event_type, **EVENT_TEST_PARAMS[other_event_type]) + ) + + retrieved_events = pygame.event.get(event_types) + + self._assertExpectedEvents(expected=expected_events, got=retrieved_events) + + # Test when 1 event type in the list is in the queue. + expected_events = [ + pygame.event.Event(event_types[0], **EVENT_TEST_PARAMS[event_types[0]]) + ] + pygame.event.clear() + pygame.event.post(expected_events[0]) + + retrieved_events = pygame.event.get(event_types) + + self._assertExpectedEvents(expected=expected_events, got=retrieved_events) + + # Test all events in the list are in the queue. + pygame.event.clear() + expected_events = [] + + for etype in event_types: + expected_events.append( + pygame.event.Event(etype, **EVENT_TEST_PARAMS[etype]) + ) + pygame.event.post(expected_events[-1]) + + retrieved_events = pygame.event.get(event_types) + + self._assertExpectedEvents(expected=expected_events, got=retrieved_events) + + def test_get_clears_queue(self): + """Ensure get() clears the event queue after a call""" + pygame.event.get() # should clear the queue completely by getting all events + self.assertEqual(pygame.event.get(), []) + + def test_clear(self): + """Ensure clear() removes all the events on the queue.""" + for e in EVENT_TYPES: + pygame.event.post(pygame.event.Event(e, **EVENT_TEST_PARAMS[e])) + poll_event = pygame.event.poll() + + self.assertNotEqual(poll_event.type, pygame.NOEVENT) + + pygame.event.clear() + poll_event = pygame.event.poll() + + self.assertEqual(poll_event.type, pygame.NOEVENT, race_condition_notification) + + def test_clear__empty_queue(self): + """Ensure clear() works correctly on an empty queue.""" + expected_events = [] + pygame.event.clear() + + # Test calling clear() on an already empty queue. + pygame.event.clear() + + retrieved_events = pygame.event.get() + + self.assertListEqual(retrieved_events, expected_events) + + def test_clear__event_sequence(self): + """Ensure a sequence of event types can be cleared from the queue.""" + cleared_event_types = EVENT_TYPES[:5] + expected_event_types = EVENT_TYPES[5:10] + expected_events = [] + + # Add the events to the queue. + for etype in cleared_event_types: + pygame.event.post(pygame.event.Event(etype, **EVENT_TEST_PARAMS[etype])) + + for etype in expected_events: + expected_events.append( + pygame.event.Event(etype, **EVENT_TEST_PARAMS[etype]) + ) + pygame.event.post(expected_events[-1]) + + # Clear the cleared_events from the queue. + pygame.event.clear(cleared_event_types) + + # Check the rest of the events in the queue. + remaining_events = pygame.event.get() + + self._assertCountEqual(remaining_events, expected_events) + + def test_event_name(self): + """Ensure event_name() returns the correct event name.""" + for expected_name, event in NAMES_AND_EVENTS: + self.assertEqual( + pygame.event.event_name(event), expected_name, f"0x{event:X}" + ) + + def test_event_name__userevent_range(self): + """Ensures event_name() returns the correct name for user events. + + Tests the full range of user events. + """ + expected_name = "UserEvent" + + for event in range(pygame.USEREVENT, pygame.NUMEVENTS): + self.assertEqual( + pygame.event.event_name(event), expected_name, f"0x{event:X}" + ) + + def test_event_name__userevent_boundary(self): + """Ensures event_name() does not return 'UserEvent' for events + just outside the user event range. + """ + unexpected_name = "UserEvent" + + for event in (pygame.USEREVENT - 1, pygame.NUMEVENTS): + self.assertNotEqual( + pygame.event.event_name(event), unexpected_name, f"0x{event:X}" + ) + + def test_event_name__kwargs(self): + """Ensure event_name() returns the correct event name when kwargs used.""" + for expected_name, event in NAMES_AND_EVENTS: + self.assertEqual( + pygame.event.event_name(type=event), expected_name, f"0x{event:X}" + ) + + def test_peek(self): + """Ensure queued events can be peeked at.""" + event_types = [pygame.KEYDOWN, pygame.KEYUP, pygame.MOUSEMOTION] + + for event_type in event_types: + pygame.event.post( + pygame.event.Event(event_type, **EVENT_TEST_PARAMS[event_type]) + ) + + # Ensure events can be checked individually. + for event_type in event_types: + self.assertTrue(pygame.event.peek(event_type)) + + # Ensure events can be checked as a sequence. + self.assertTrue(pygame.event.peek(event_types)) + + def test_peek__event_sequence(self): + """Ensure peek() can handle a sequence of event types.""" + event_types = [pygame.KEYDOWN, pygame.KEYUP, pygame.MOUSEMOTION] + other_event_type = pygame.MOUSEBUTTONUP + + # Test when no events in the queue. + pygame.event.clear() + peeked = pygame.event.peek(event_types) + + self.assertFalse(peeked) + + # Test when an event type not in the list is in the queue. + pygame.event.clear() + pygame.event.post( + pygame.event.Event(other_event_type, **EVENT_TEST_PARAMS[other_event_type]) + ) + + peeked = pygame.event.peek(event_types) + + self.assertFalse(peeked) + + # Test when 1 event type in the list is in the queue. + pygame.event.clear() + pygame.event.post( + pygame.event.Event(event_types[0], **EVENT_TEST_PARAMS[event_types[0]]) + ) + + peeked = pygame.event.peek(event_types) + + self.assertTrue(peeked) + + # Test all events in the list are in the queue. + pygame.event.clear() + for etype in event_types: + pygame.event.post(pygame.event.Event(etype, **EVENT_TEST_PARAMS[etype])) + + peeked = pygame.event.peek(event_types) + + self.assertTrue(peeked) + + def test_peek__empty_queue(self): + """Ensure peek() works correctly on an empty queue.""" + pygame.event.clear() + + # Ensure all events can be checked. + peeked = pygame.event.peek() + + self.assertFalse(peeked) + + # Ensure events can be checked individually. + for event_type in EVENT_TYPES: + peeked = pygame.event.peek(event_type) + self.assertFalse(peeked) + + # Ensure events can be checked as a sequence. + peeked = pygame.event.peek(EVENT_TYPES) + + self.assertFalse(peeked) + + def test_set_allowed(self): + """Ensure a blocked event type can be unblocked/allowed.""" + event = EVENT_TYPES[0] + pygame.event.set_blocked(event) + + self.assertTrue(pygame.event.get_blocked(event)) + + pygame.event.set_allowed(event) + + self.assertFalse(pygame.event.get_blocked(event)) + + def test_set_allowed__event_sequence(self): + """Ensure a sequence of blocked event types can be unblocked/allowed.""" + event_types = [ + pygame.KEYDOWN, + pygame.KEYUP, + pygame.MOUSEMOTION, + pygame.MOUSEBUTTONDOWN, + pygame.MOUSEBUTTONUP, + ] + pygame.event.set_blocked(event_types) + + pygame.event.set_allowed(event_types) + + for etype in event_types: + self.assertFalse(pygame.event.get_blocked(etype)) + + def test_set_allowed_all(self): + """Ensure all events can be unblocked/allowed at once.""" + pygame.event.set_blocked(None) + + for e in EVENT_TYPES: + self.assertTrue(pygame.event.get_blocked(e)) + + pygame.event.set_allowed(None) + + for e in EVENT_TYPES: + self.assertFalse(pygame.event.get_blocked(e)) + + def test_pump(self): + """Ensure pump() functions properly.""" + pygame.event.pump() + + # @unittest.skipIf( + # os.environ.get("SDL_VIDEODRIVER") == "dummy", + # 'requires the SDL_VIDEODRIVER to be a non "dummy" value', + # ) + # Fails on SDL 2.0.18 + @unittest.skip("flaky test, and broken on 2.0.18 windows") + def test_set_grab__and_get_symmetric(self): + """Ensure event grabbing can be enabled and disabled. + + WARNING: Moving the mouse off the display during this test can cause it + to fail. + """ + surf = pygame.display.set_mode((10, 10)) + pygame.event.set_grab(True) + + self.assertTrue(pygame.event.get_grab()) + + pygame.event.set_grab(False) + + self.assertFalse(pygame.event.get_grab()) + + def test_get_blocked(self): + """Ensure an event's blocked state can be retrieved.""" + # Test each event is not blocked. + pygame.event.set_allowed(None) + + for etype in EVENT_TYPES: + blocked = pygame.event.get_blocked(etype) + + self.assertFalse(blocked) + + # Test each event type is blocked. + pygame.event.set_blocked(None) + + for etype in EVENT_TYPES: + blocked = pygame.event.get_blocked(etype) + + self.assertTrue(blocked) + + def test_get_blocked__event_sequence(self): + """Ensure get_blocked() can handle a sequence of event types.""" + event_types = [ + pygame.KEYDOWN, + pygame.KEYUP, + pygame.MOUSEMOTION, + pygame.MOUSEBUTTONDOWN, + pygame.MOUSEBUTTONUP, + pygame.WINDOWMINIMIZED, + pygame.USEREVENT, + ] + + # Test no event types in the list are blocked. + blocked = pygame.event.get_blocked(event_types) + + self.assertFalse(blocked) + + # Test when 1 event type in the list is blocked. + pygame.event.set_blocked(event_types[2]) + + blocked = pygame.event.get_blocked(event_types) + + self.assertTrue(blocked) + + # Test all event types in the list are blocked. + pygame.event.set_blocked(event_types) + + blocked = pygame.event.get_blocked(event_types) + + self.assertTrue(blocked) + + # @unittest.skipIf( + # os.environ.get("SDL_VIDEODRIVER") == "dummy", + # 'requires the SDL_VIDEODRIVER to be a non "dummy" value', + # ) + # Fails on SDL 2.0.18 + @unittest.skip("flaky test, and broken on 2.0.18 windows") + def test_get_grab(self): + """Ensure get_grab() works as expected""" + surf = pygame.display.set_mode((10, 10)) + # Test 5 times + for i in range(5): + pygame.event.set_grab(i % 2) + self.assertEqual(pygame.event.get_grab(), i % 2) + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", + "requires the SDL_VIDEODRIVER to be a non dummy value", + ) + @unittest.skipIf(pygame.get_sdl_version() < (2, 0, 16), "Needs at least SDL 2.0.16") + def test_set_keyboard_grab_and_get_keyboard_grab(self): + """Ensure set_keyboard_grab() and get_keyboard_grab() work as expected""" + + surf = pygame.display.set_mode((10, 10)) + + pygame.event.set_keyboard_grab(True) + self.assertTrue(pygame.event.get_keyboard_grab()) + + pygame.event.set_keyboard_grab(False) + self.assertFalse(pygame.event.get_keyboard_grab()) + + def test_poll(self): + """Ensure poll() works as expected""" + pygame.event.clear() + ev = pygame.event.poll() + # poll() on empty queue should return NOEVENT + self.assertEqual(ev.type, pygame.NOEVENT) + + # test poll returns stuff in same order + e1 = pygame.event.Event(pygame.USEREVENT) + e2 = pygame.event.Event(pygame.KEYDOWN, key=pygame.K_a) + e3 = pygame.event.Event(pygame.KEYUP, key=pygame.K_a) + pygame.event.post(e1) + pygame.event.post(e2) + pygame.event.post(e3) + + self.assertEqual(pygame.event.poll().type, e1.type) + self.assertEqual(pygame.event.poll().type, e2.type) + self.assertEqual(pygame.event.poll().type, e3.type) + self.assertEqual(pygame.event.poll().type, pygame.NOEVENT) + + +class EventModuleTestsWithTiming(unittest.TestCase): + __tags__ = ["timing"] + + def setUp(self): + pygame.display.init() + pygame.event.clear() # flush events + + def tearDown(self): + pygame.event.clear() # flush events + pygame.display.quit() + + def test_event_wait(self): + """Ensure wait() waits for an event on the queue.""" + # Test case without timeout. + event = pygame.event.Event(EVENT_TYPES[0], **EVENT_TEST_PARAMS[EVENT_TYPES[0]]) + pygame.event.post(event) + wait_event = pygame.event.wait() + + self.assertEqual(wait_event.type, event.type) + + # Test case with timeout and no event in the queue. + wait_event = pygame.event.wait(100) + self.assertEqual(wait_event.type, pygame.NOEVENT) + + # Test case with timeout and an event in the queue. + event = pygame.event.Event(EVENT_TYPES[0], **EVENT_TEST_PARAMS[EVENT_TYPES[0]]) + pygame.event.post(event) + wait_event = pygame.event.wait(100) + + self.assertEqual(wait_event.type, event.type) + + # test wait with timeout waits for the correct duration + pygame.time.set_timer(pygame.USEREVENT, 50, 3) + + for wait_time, expected_type, expected_time in ( + (60, pygame.USEREVENT, 50), + (65, pygame.USEREVENT, 50), + (20, pygame.NOEVENT, 20), + (45, pygame.USEREVENT, 30), + (70, pygame.NOEVENT, 70), + ): + start_time = time.perf_counter() + self.assertEqual(pygame.event.wait(wait_time).type, expected_type) + self.assertAlmostEqual( + time.perf_counter() - start_time, expected_time / 1000, delta=0.01 + ) + + # test wait without timeout waits for the full duration + pygame.time.set_timer(pygame.USEREVENT, 100, 1) + + start_time = time.perf_counter() + self.assertEqual(pygame.event.wait().type, pygame.USEREVENT) + self.assertAlmostEqual(time.perf_counter() - start_time, 0.1, delta=0.01) + + # test wait returns no event if event is arriving later + pygame.time.set_timer(pygame.USEREVENT, 50, 1) + self.assertEqual(pygame.event.wait(40).type, pygame.NOEVENT) + + +################################################################################ + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/A_PyGameMono-8.png b/laplas/abstract_map/pygame/tests/fixtures/fonts/A_PyGameMono-8.png new file mode 100644 index 0000000..b15961f Binary files /dev/null and b/laplas/abstract_map/pygame/tests/fixtures/fonts/A_PyGameMono-8.png differ diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/PlayfairDisplaySemibold.ttf b/laplas/abstract_map/pygame/tests/fixtures/fonts/PlayfairDisplaySemibold.ttf new file mode 100644 index 0000000..c1213a7 Binary files /dev/null and b/laplas/abstract_map/pygame/tests/fixtures/fonts/PlayfairDisplaySemibold.ttf differ diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-18-100dpi.bdf b/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-18-100dpi.bdf new file mode 100644 index 0000000..a88f083 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-18-100dpi.bdf @@ -0,0 +1,165 @@ +STARTFONT 2.1 +FONT -FontForge-PyGameMono-Medium-R-Normal--25-180-100-100-M-250-ISO10646-1 +SIZE 18 100 100 +FONTBOUNDINGBOX 21 22 0 0 +COMMENT "Generated by fontforge, http://fontforge.sourceforge.net" +COMMENT "Created by Lenard Lindstrom,,, with FontForge 2.0 (http://fontforge.sf.net)" +STARTPROPERTIES 29 +FOUNDRY "FontForge" +FAMILY_NAME "PyGameMono" +WEIGHT_NAME "Medium" +SLANT "R" +SETWIDTH_NAME "Normal" +ADD_STYLE_NAME "" +PIXEL_SIZE 25 +POINT_SIZE 180 +RESOLUTION_X 100 +RESOLUTION_Y 100 +SPACING "M" +AVERAGE_WIDTH 250 +CHARSET_REGISTRY "ISO10646" +CHARSET_ENCODING "1" +FONTNAME_REGISTRY "" +CHARSET_COLLECTIONS "ISO10646-1" +FONT_NAME "PyGameMono" +FACE_NAME "PyGame Mono" +FONT_VERSION "001.000" +FONT_ASCENT 20 +FONT_DESCENT 5 +UNDERLINE_POSITION -2 +UNDERLINE_THICKNESS 2 +RAW_ASCENT 800 +RAW_DESCENT 200 +RELATIVE_WEIGHT 50 +RELATIVE_SETWIDTH 50 +FIGURE_WIDTH -1 +AVG_UPPERCASE_WIDTH 250 +ENDPROPERTIES +CHARS 5 +STARTCHAR .notdef +ENCODING 0 +SWIDTH 1000 0 +DWIDTH 25 0 +BBX 20 20 0 0 +BITMAP +FFFFF0 +FFFFF0 +FE07F0 +F801F0 +F000F0 +E00070 +E00070 +C00030 +C00030 +C00030 +C00030 +C00030 +C00030 +E00070 +E00070 +F000F0 +F801F0 +FE07F0 +FFFFF0 +FFFFF0 +ENDCHAR +STARTCHAR A +ENCODING 65 +SWIDTH 1000 0 +DWIDTH 25 0 +BBX 20 21 0 1 +BITMAP +03FC00 +1FFF80 +3FFFC0 +7C03E0 +F000F0 +E00070 +E00070 +F000F0 +FC03F0 +FFFFF0 +FFFFF0 +FFFFF0 +FF0FF0 +7C03F0 +7801E0 +7800E0 +7000E0 +700060 +600060 +200040 +200040 +ENDCHAR +STARTCHAR B +ENCODING 66 +SWIDTH 1000 0 +DWIDTH 25 0 +BBX 18 20 1 0 +BITMAP +FFFE00 +FFFF80 +7E0780 +7801C0 +7000C0 +3000C0 +3000C0 +3801C0 +3E0780 +3FFF00 +3FFF00 +3E0780 +380180 +3000C0 +3000C0 +3000C0 +7801C0 +7E07C0 +FFFF80 +FFFE00 +ENDCHAR +STARTCHAR C +ENCODING 67 +SWIDTH 1000 0 +DWIDTH 25 0 +BBX 20 20 0 0 +BITMAP +00FC00 +03FF00 +0FFF80 +1F03E0 +3E0070 +7C0010 +780000 +F80000 +F00000 +F00000 +F00000 +F00000 +F80000 +780000 +7C0010 +3E0070 +1F01E0 +0FFFC0 +03FF80 +00FE00 +ENDCHAR +STARTCHAR u13079 +ENCODING 77945 +SWIDTH 1000 0 +DWIDTH 25 0 +BBX 21 10 0 5 +BITMAP +03FC00 +0FFF80 +1E73C0 +78F8F0 +F0F878 +70F870 +3870E0 +1E03C0 +0FFF80 +03FC00 +ENDCHAR +ENDFONT diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-18-75dpi.bdf b/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-18-75dpi.bdf new file mode 100644 index 0000000..127f704 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-18-75dpi.bdf @@ -0,0 +1,143 @@ +STARTFONT 2.1 +FONT -FontForge-PyGameMono-Medium-R-Normal--19-180-75-75-M-190-ISO10646-1 +SIZE 18 75 75 +FONTBOUNDINGBOX 15 17 0 0 +COMMENT "Generated by fontforge, http://fontforge.sourceforge.net" +COMMENT "Created by Lenard Lindstrom,,, with FontForge 2.0 (http://fontforge.sf.net)" +STARTPROPERTIES 29 +FOUNDRY "FontForge" +FAMILY_NAME "PyGameMono" +WEIGHT_NAME "Medium" +SLANT "R" +SETWIDTH_NAME "Normal" +ADD_STYLE_NAME "" +PIXEL_SIZE 19 +POINT_SIZE 180 +RESOLUTION_X 75 +RESOLUTION_Y 75 +SPACING "M" +AVERAGE_WIDTH 190 +CHARSET_REGISTRY "ISO10646" +CHARSET_ENCODING "1" +FONTNAME_REGISTRY "" +CHARSET_COLLECTIONS "ISO10646-1" +FONT_NAME "PyGameMono" +FACE_NAME "PyGame Mono" +FONT_VERSION "001.000" +FONT_ASCENT 15 +FONT_DESCENT 4 +UNDERLINE_POSITION -2 +UNDERLINE_THICKNESS 1 +RAW_ASCENT 800 +RAW_DESCENT 200 +RELATIVE_WEIGHT 50 +RELATIVE_SETWIDTH 50 +FIGURE_WIDTH -1 +AVG_UPPERCASE_WIDTH 190 +ENDPROPERTIES +CHARS 5 +STARTCHAR .notdef +ENCODING 0 +SWIDTH 1000 0 +DWIDTH 19 0 +BBX 15 15 0 0 +BITMAP +FFFE +FFFE +FC7E +F01E +E00E +C006 +C006 +C006 +C006 +C006 +E00E +F01E +FC7E +FFFE +FFFE +ENDCHAR +STARTCHAR A +ENCODING 65 +SWIDTH 1000 0 +DWIDTH 19 0 +BBX 15 17 0 0 +BITMAP +0FE0 +3FF8 +783C +F01E +E00E +E00E +F01E +F83E +FFFE +FFFE +FC7E +701C +701C +600C +600C +4004 +4004 +ENDCHAR +STARTCHAR B +ENCODING 66 +SWIDTH 1000 0 +DWIDTH 19 0 +BBX 15 15 0 0 +BITMAP +FFF8 +7FFC +780E +3006 +3006 +380E +3FF8 +3FF8 +3FF8 +380E +3006 +3006 +7C1E +7FFC +FFF8 +ENDCHAR +STARTCHAR C +ENCODING 67 +SWIDTH 1000 0 +DWIDTH 19 0 +BBX 15 15 0 0 +BITMAP +03E0 +0FF8 +3C1C +7806 +7000 +E000 +E000 +E000 +E000 +E000 +7000 +7806 +3C1C +0FF8 +03E0 +ENDCHAR +STARTCHAR u13079 +ENCODING 77945 +SWIDTH 1000 0 +DWIDTH 19 0 +BBX 15 7 0 4 +BITMAP +0FE0 +3838 +638C +E38E +638C +3838 +0FE0 +ENDCHAR +ENDFONT diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-8.bdf b/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-8.bdf new file mode 100644 index 0000000..17bef06 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono-8.bdf @@ -0,0 +1,103 @@ +STARTFONT 2.1 +FONT -FontForge-PyGameMono-Medium-R-Normal--8-80-75-75-C-80-ISO10646-1 +SIZE 8 75 75 +FONTBOUNDINGBOX 6 7 0 0 +COMMENT "Generated by fontforge, http://fontforge.sourceforge.net" +COMMENT "Created by Lenard Lindstrom,,, with FontForge 2.0 (http://fontforge.sf.net)" +STARTPROPERTIES 29 +FOUNDRY "FontForge" +FAMILY_NAME "PyGameMono" +WEIGHT_NAME "Medium" +SLANT "R" +SETWIDTH_NAME "Normal" +ADD_STYLE_NAME "" +PIXEL_SIZE 8 +POINT_SIZE 80 +RESOLUTION_X 75 +RESOLUTION_Y 75 +SPACING "C" +AVERAGE_WIDTH 80 +CHARSET_REGISTRY "ISO10646" +CHARSET_ENCODING "1" +FONTNAME_REGISTRY "" +CHARSET_COLLECTIONS "ISO10646-1" +FONT_NAME "PyGameMono" +FACE_NAME "PyGame Mono" +FONT_VERSION "001.000" +FONT_ASCENT 6 +FONT_DESCENT 2 +UNDERLINE_POSITION -1 +UNDERLINE_THICKNESS 1 +RAW_ASCENT 800 +RAW_DESCENT 200 +RELATIVE_WEIGHT 50 +RELATIVE_SETWIDTH 50 +FIGURE_WIDTH -1 +AVG_UPPERCASE_WIDTH 80 +ENDPROPERTIES +CHARS 5 +STARTCHAR .notdef +ENCODING 0 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 6 6 0 0 +BITMAP +FC +84 +84 +84 +84 +FC +ENDCHAR +STARTCHAR A +ENCODING 65 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 6 7 0 0 +BITMAP +78 +84 +84 +FC +84 +84 +84 +ENDCHAR +STARTCHAR B +ENCODING 66 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 6 6 0 0 +BITMAP +FC +44 +78 +4C +44 +FC +ENDCHAR +STARTCHAR C +ENCODING 67 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 6 6 0 0 +BITMAP +78 +C4 +C0 +C0 +C4 +78 +ENDCHAR +STARTCHAR u13079 +ENCODING 77945 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 6 4 0 1 +BITMAP +78 +B4 +B4 +78 +ENDCHAR +ENDFONT diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono.otf b/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono.otf new file mode 100644 index 0000000..5e9b66c Binary files /dev/null and b/laplas/abstract_map/pygame/tests/fixtures/fonts/PyGameMono.otf differ diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/test_fixed.otf b/laplas/abstract_map/pygame/tests/fixtures/fonts/test_fixed.otf new file mode 100644 index 0000000..3488898 Binary files /dev/null and b/laplas/abstract_map/pygame/tests/fixtures/fonts/test_fixed.otf differ diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/test_sans.ttf b/laplas/abstract_map/pygame/tests/fixtures/fonts/test_sans.ttf new file mode 100644 index 0000000..09fac2f Binary files /dev/null and b/laplas/abstract_map/pygame/tests/fixtures/fonts/test_sans.ttf differ diff --git a/laplas/abstract_map/pygame/tests/fixtures/fonts/u13079_PyGameMono-8.png b/laplas/abstract_map/pygame/tests/fixtures/fonts/u13079_PyGameMono-8.png new file mode 100644 index 0000000..911da8a Binary files /dev/null and b/laplas/abstract_map/pygame/tests/fixtures/fonts/u13079_PyGameMono-8.png differ diff --git a/laplas/abstract_map/pygame/tests/fixtures/xbm_cursors/white_sizing.xbm b/laplas/abstract_map/pygame/tests/fixtures/xbm_cursors/white_sizing.xbm new file mode 100644 index 0000000..d334d8d --- /dev/null +++ b/laplas/abstract_map/pygame/tests/fixtures/xbm_cursors/white_sizing.xbm @@ -0,0 +1,8 @@ +#define resize_white_width 16 +#define resize_white_height 16 +#define resize_white_x_hot 7 +#define resize_white_y_hot 7 +static unsigned char resize_white_bits[] = { + 0xff, 0x03, 0x01, 0x02, 0xfd, 0x03, 0x05, 0x00, 0xf5, 0x0f, 0x15, 0x08, + 0xd5, 0xeb, 0x55, 0xaa, 0x55, 0xaa, 0xd7, 0xab, 0x10, 0xa8, 0xf0, 0xb7, + 0x00, 0xa8, 0xc0, 0x9f, 0x40, 0x80, 0xc0, 0xff}; diff --git a/laplas/abstract_map/pygame/tests/fixtures/xbm_cursors/white_sizing_mask.xbm b/laplas/abstract_map/pygame/tests/fixtures/xbm_cursors/white_sizing_mask.xbm new file mode 100644 index 0000000..f00bc46 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/fixtures/xbm_cursors/white_sizing_mask.xbm @@ -0,0 +1,8 @@ +#define resize_white_mask_width 16 +#define resize_white_mask_height 16 +#define resize_white_mask_x_hot 7 +#define resize_white_mask_y_hot 7 +static unsigned char resize_white_mask_bits[] = { + 0xff, 0x03, 0xff, 0x03, 0xff, 0x03, 0x07, 0x00, 0xf7, 0x0f, 0xf7, 0x0f, + 0xf7, 0xef, 0x77, 0xee, 0x77, 0xee, 0xf7, 0xef, 0xf0, 0xef, 0xf0, 0xff, + 0x00, 0xf8, 0xc0, 0xff, 0xc0, 0xff, 0xc0, 0xff}; diff --git a/laplas/abstract_map/pygame/tests/font_test.py b/laplas/abstract_map/pygame/tests/font_test.py new file mode 100644 index 0000000..8d6dc6b --- /dev/null +++ b/laplas/abstract_map/pygame/tests/font_test.py @@ -0,0 +1,748 @@ +# -*- coding: utf-8 -*- +import sys +import os +import unittest +import pathlib +import platform + +import pygame +from pygame import font as pygame_font # So font can be replaced with ftfont + + +FONTDIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures", "fonts") + + +def equal_images(s1, s2): + size = s1.get_size() + if s2.get_size() != size: + return False + w, h = size + for x in range(w): + for y in range(h): + if s1.get_at((x, y)) != s2.get_at((x, y)): + return False + return True + + +IS_PYPY = "PyPy" == platform.python_implementation() + + +@unittest.skipIf(IS_PYPY, "pypy skip known failure") # TODO +class FontModuleTest(unittest.TestCase): + def setUp(self): + pygame_font.init() + + def tearDown(self): + pygame_font.quit() + + def test_get_sdl_ttf_version(self): + def test_ver_tuple(ver): + self.assertIsInstance(ver, tuple) + self.assertEqual(len(ver), 3) + for i in ver: + self.assertIsInstance(i, int) + + if pygame_font.__name__ != "pygame.ftfont": + compiled = pygame_font.get_sdl_ttf_version() + linked = pygame_font.get_sdl_ttf_version(linked=True) + + test_ver_tuple(compiled) + test_ver_tuple(linked) + + self.assertTrue(linked >= compiled) + + def test_SysFont(self): + # Can only check that a font object is returned. + fonts = pygame_font.get_fonts() + if "arial" in fonts: + # Try to use arial font if it is there, rather than a random font + # which can be different depending on installed fonts on the system. + font_name = "arial" + else: + font_name = sorted(fonts)[0] + o = pygame_font.SysFont(font_name, 20) + self.assertTrue(isinstance(o, pygame_font.FontType)) + o = pygame_font.SysFont(font_name, 20, italic=True) + self.assertTrue(isinstance(o, pygame_font.FontType)) + o = pygame_font.SysFont(font_name, 20, bold=True) + self.assertTrue(isinstance(o, pygame_font.FontType)) + o = pygame_font.SysFont("thisisnotafont", 20) + self.assertTrue(isinstance(o, pygame_font.FontType)) + + def test_get_default_font(self): + self.assertEqual(pygame_font.get_default_font(), "freesansbold.ttf") + + def test_get_fonts_returns_something(self): + fnts = pygame_font.get_fonts() + self.assertTrue(fnts) + + # to test if some files exist... + # def XXtest_has_file_osx_10_5_sdk(self): + # import os + # f = "/Developer/SDKs/MacOSX10.5.sdk/usr/X11/include/ft2build.h" + # self.assertEqual(os.path.exists(f), True) + + # def XXtest_has_file_osx_10_4_sdk(self): + # import os + # f = "/Developer/SDKs/MacOSX10.4u.sdk/usr/X11R6/include/ft2build.h" + # self.assertEqual(os.path.exists(f), True) + + def test_get_fonts(self): + fnts = pygame_font.get_fonts() + + self.assertTrue(fnts, msg=repr(fnts)) + + for name in fnts: + # note, on ubuntu 2.6 they are all unicode strings. + + self.assertTrue(isinstance(name, str), name) + # Font names can be comprised of only numeric characters, so + # just checking name.islower() will not work as expected here. + self.assertFalse(any(c.isupper() for c in name)) + self.assertTrue(name.isalnum(), name) + + def test_get_init(self): + self.assertTrue(pygame_font.get_init()) + pygame_font.quit() + self.assertFalse(pygame_font.get_init()) + + def test_init(self): + pygame_font.init() + + def test_match_font_all_exist(self): + fonts = pygame_font.get_fonts() + + # Ensure all listed fonts are in fact available, and the returned file + # name is a full path. + for font in fonts: + path = pygame_font.match_font(font) + self.assertFalse(path is None) + self.assertTrue(os.path.isabs(path) and os.path.isfile(path)) + + def test_match_font_name(self): + """That match_font accepts names of various types""" + font = pygame_font.get_fonts()[0] + font_path = pygame_font.match_font(font) + self.assertIsNotNone(font_path) + font_b = font.encode() + not_a_font = "thisisnotafont" + not_a_font_b = b"thisisnotafont" + good_font_names = [ + # Check single name bytes. + font_b, + # Check string of comma-separated names. + ",".join([not_a_font, font, not_a_font]), + # Check list of names. + [not_a_font, font, not_a_font], + # Check generator: + (name for name in [not_a_font, font, not_a_font]), + # Check comma-separated bytes. + b",".join([not_a_font_b, font_b, not_a_font_b]), + # Check list of bytes. + [not_a_font_b, font_b, not_a_font_b], + # Check mixed list of bytes and string. + [font, not_a_font, font_b, not_a_font_b], + ] + for font_name in good_font_names: + self.assertEqual(pygame_font.match_font(font_name), font_path, font_name) + + def test_not_match_font_name(self): + """match_font return None when names of various types do not exist""" + not_a_font = "thisisnotafont" + not_a_font_b = b"thisisnotafont" + bad_font_names = [ + not_a_font, + ",".join([not_a_font, not_a_font, not_a_font]), + [not_a_font, not_a_font, not_a_font], + (name for name in [not_a_font, not_a_font, not_a_font]), + not_a_font_b, + b",".join([not_a_font_b, not_a_font_b, not_a_font_b]), + [not_a_font_b, not_a_font_b, not_a_font_b], + [not_a_font, not_a_font_b, not_a_font], + ] + for font_name in bad_font_names: + self.assertIsNone(pygame_font.match_font(font_name), font_name) + + def test_match_font_bold(self): + fonts = pygame_font.get_fonts() + + # Look for a bold font. + self.assertTrue(any(pygame_font.match_font(font, bold=True) for font in fonts)) + + def test_match_font_italic(self): + fonts = pygame_font.get_fonts() + + # Look for an italic font. + self.assertTrue( + any(pygame_font.match_font(font, italic=True) for font in fonts) + ) + + def test_issue_742(self): + """that the font background does not crash.""" + surf = pygame.Surface((320, 240)) + font = pygame_font.Font(None, 24) + image = font.render("Test", 0, (255, 255, 255), (0, 0, 0)) + self.assertIsNone(image.get_colorkey()) + image.set_alpha(255) + surf.blit(image, (0, 0)) + + # not issue 742, but be sure to test that background color is + # correctly issued on this mode + self.assertEqual(surf.get_at((0, 0)), pygame.Color(0, 0, 0)) + + def test_issue_font_alphablit(self): + """Check that blitting anti-aliased text doesn't + change the background blue""" + pygame.display.set_mode((600, 400)) + + font = pygame_font.Font(None, 24) + + (color, text, center, pos) = ((160, 200, 250), "Music", (190, 170), "midright") + img1 = font.render(text, True, color) + + img = pygame.Surface(img1.get_size(), depth=32) + pre_blit_corner_pixel = img.get_at((0, 0)) + img.blit(img1, (0, 0)) + post_blit_corner_pixel = img.get_at((0, 0)) + + self.assertEqual(pre_blit_corner_pixel, post_blit_corner_pixel) + + def test_segfault_after_reinit(self): + """Reinitialization of font module should not cause + segmentation fault""" + import gc + + font = pygame_font.Font(None, 20) + pygame_font.quit() + pygame_font.init() + del font + gc.collect() + + def test_quit(self): + pygame_font.quit() + + +@unittest.skipIf(IS_PYPY, "pypy skip known failure") # TODO +class FontTest(unittest.TestCase): + def setUp(self): + pygame_font.init() + + def tearDown(self): + pygame_font.quit() + + def test_render_args(self): + screen = pygame.display.set_mode((600, 400)) + rect = screen.get_rect() + f = pygame_font.Font(None, 20) + screen.fill((10, 10, 10)) + font_surface = f.render(" bar", True, (0, 0, 0), (255, 255, 255)) + font_rect = font_surface.get_rect() + font_rect.topleft = rect.topleft + self.assertTrue(font_surface) + screen.blit(font_surface, font_rect, font_rect) + pygame.display.update() + self.assertEqual(tuple(screen.get_at((0, 0)))[:3], (255, 255, 255)) + self.assertEqual(tuple(screen.get_at(font_rect.topleft))[:3], (255, 255, 255)) + + # If we don't have a real display, don't do this test. + # Transparent background doesn't seem to work without a read video card. + if os.environ.get("SDL_VIDEODRIVER") != "dummy": + screen.fill((10, 10, 10)) + font_surface = f.render(" bar", True, (0, 0, 0), None) + font_rect = font_surface.get_rect() + font_rect.topleft = rect.topleft + self.assertTrue(font_surface) + screen.blit(font_surface, font_rect, font_rect) + pygame.display.update() + self.assertEqual(tuple(screen.get_at((0, 0)))[:3], (10, 10, 10)) + self.assertEqual(tuple(screen.get_at(font_rect.topleft))[:3], (10, 10, 10)) + + screen.fill((10, 10, 10)) + font_surface = f.render(" bar", True, (0, 0, 0)) + font_rect = font_surface.get_rect() + font_rect.topleft = rect.topleft + self.assertTrue(font_surface) + screen.blit(font_surface, font_rect, font_rect) + pygame.display.update(rect) + self.assertEqual(tuple(screen.get_at((0, 0)))[:3], (10, 10, 10)) + self.assertEqual(tuple(screen.get_at(font_rect.topleft))[:3], (10, 10, 10)) + + +@unittest.skipIf(IS_PYPY, "pypy skip known failure") # TODO +class FontTypeTest(unittest.TestCase): + def setUp(self): + pygame_font.init() + + def tearDown(self): + pygame_font.quit() + + def test_default_parameters(self): + f = pygame_font.Font() + + def test_get_ascent(self): + # Checking ascent would need a custom test font to do properly. + f = pygame_font.Font(None, 20) + ascent = f.get_ascent() + self.assertTrue(isinstance(ascent, int)) + self.assertTrue(ascent > 0) + s = f.render("X", False, (255, 255, 255)) + self.assertTrue(s.get_size()[1] > ascent) + + def test_get_descent(self): + # Checking descent would need a custom test font to do properly. + f = pygame_font.Font(None, 20) + descent = f.get_descent() + self.assertTrue(isinstance(descent, int)) + self.assertTrue(descent < 0) + + def test_get_height(self): + # Checking height would need a custom test font to do properly. + f = pygame_font.Font(None, 20) + height = f.get_height() + self.assertTrue(isinstance(height, int)) + self.assertTrue(height > 0) + s = f.render("X", False, (255, 255, 255)) + self.assertTrue(s.get_size()[1] == height) + + def test_get_linesize(self): + # Checking linesize would need a custom test font to do properly. + # Questions: How do linesize, height and descent relate? + f = pygame_font.Font(None, 20) + linesize = f.get_linesize() + self.assertTrue(isinstance(linesize, int)) + self.assertTrue(linesize > 0) + + def test_metrics(self): + # Ensure bytes decoding works correctly. Can only compare results + # with unicode for now. + f = pygame_font.Font(None, 20) + um = f.metrics(".") + bm = f.metrics(b".") + + self.assertEqual(len(um), 1) + self.assertEqual(len(bm), 1) + self.assertIsNotNone(um[0]) + self.assertEqual(um, bm) + + u = "\u212A" + b = u.encode("UTF-16")[2:] # Keep byte order consistent. [2:] skips BOM + bm = f.metrics(b) + + self.assertEqual(len(bm), 2) + + try: # FIXME why do we do this try/except ? + um = f.metrics(u) + except pygame.error: + pass + else: + self.assertEqual(len(um), 1) + self.assertNotEqual(bm[0], um[0]) + self.assertNotEqual(bm[1], um[0]) + + u = "\U00013000" + bm = f.metrics(u) + + self.assertEqual(len(bm), 1) + self.assertIsNone(bm[0]) + + return # unfinished + # The documentation is useless here. How large a list? + # How do list positions relate to character codes? + # What about unicode characters? + + # __doc__ (as of 2008-08-02) for pygame_font.Font.metrics: + + # Font.metrics(text): return list + # Gets the metrics for each character in the passed string. + # + # The list contains tuples for each character, which contain the + # minimum X offset, the maximum X offset, the minimum Y offset, the + # maximum Y offset and the advance offset (bearing plus width) of the + # character. [(minx, maxx, miny, maxy, advance), (minx, maxx, miny, + # maxy, advance), ...] + + self.fail() + + def test_render(self): + f = pygame_font.Font(None, 20) + s = f.render("foo", True, [0, 0, 0], [255, 255, 255]) + s = f.render("xxx", True, [0, 0, 0], [255, 255, 255]) + s = f.render("", True, [0, 0, 0], [255, 255, 255]) + s = f.render("foo", False, [0, 0, 0], [255, 255, 255]) + s = f.render("xxx", False, [0, 0, 0], [255, 255, 255]) + s = f.render("xxx", False, [0, 0, 0]) + s = f.render(" ", False, [0, 0, 0]) + s = f.render(" ", False, [0, 0, 0], [255, 255, 255]) + # null text should be 0 pixel wide. + s = f.render("", False, [0, 0, 0], [255, 255, 255]) + self.assertEqual(s.get_size()[0], 0) + # None text should be 0 pixel wide. + s = f.render(None, False, [0, 0, 0], [255, 255, 255]) + self.assertEqual(s.get_size()[0], 0) + # Non-text should raise a TypeError. + self.assertRaises(TypeError, f.render, [], False, [0, 0, 0], [255, 255, 255]) + self.assertRaises(TypeError, f.render, 1, False, [0, 0, 0], [255, 255, 255]) + # is background transparent for antialiasing? + s = f.render(".", True, [255, 255, 255]) + self.assertEqual(s.get_at((0, 0))[3], 0) + # is Unicode and bytes encoding correct? + # Cannot really test if the correct characters are rendered, but + # at least can assert the encodings differ. + su = f.render(".", False, [0, 0, 0], [255, 255, 255]) + sb = f.render(b".", False, [0, 0, 0], [255, 255, 255]) + self.assertTrue(equal_images(su, sb)) + u = "\u212A" + b = u.encode("UTF-16")[2:] # Keep byte order consistent. [2:] skips BOM + sb = f.render(b, False, [0, 0, 0], [255, 255, 255]) + try: # FIXME why do we do this try/except ? + su = f.render(u, False, [0, 0, 0], [255, 255, 255]) + except pygame.error: + pass + else: + self.assertFalse(equal_images(su, sb)) + + # test for internal null bytes + self.assertRaises(ValueError, f.render, b"ab\x00cd", 0, [0, 0, 0]) + self.assertRaises(ValueError, f.render, "ab\x00cd", 0, [0, 0, 0]) + + def test_render_ucs2_ucs4(self): + """that it renders without raising if there is a new enough SDL_ttf.""" + f = pygame_font.Font(None, 20) + # If the font module is SDL_ttf < 2.0.15 based, then it only supports UCS-2 + # it will raise an exception for an out-of-range UCS-4 code point. + if hasattr(pygame_font, "UCS4"): + ucs_2 = "\uFFEE" + s = f.render(ucs_2, False, [0, 0, 0], [255, 255, 255]) + ucs_4 = "\U00010000" + s = f.render(ucs_4, False, [0, 0, 0], [255, 255, 255]) + + def test_set_bold(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.get_bold()) + f.set_bold(True) + self.assertTrue(f.get_bold()) + f.set_bold(False) + self.assertFalse(f.get_bold()) + + def test_set_italic(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.get_italic()) + f.set_italic(True) + self.assertTrue(f.get_italic()) + f.set_italic(False) + self.assertFalse(f.get_italic()) + + def test_set_underline(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.get_underline()) + f.set_underline(True) + self.assertTrue(f.get_underline()) + f.set_underline(False) + self.assertFalse(f.get_underline()) + + def test_set_strikethrough(self): + if pygame_font.__name__ != "pygame.ftfont": + f = pygame_font.Font(None, 20) + self.assertFalse(f.get_strikethrough()) + f.set_strikethrough(True) + self.assertTrue(f.get_strikethrough()) + f.set_strikethrough(False) + self.assertFalse(f.get_strikethrough()) + + def test_bold_attr(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.bold) + f.bold = True + self.assertTrue(f.bold) + f.bold = False + self.assertFalse(f.bold) + + def test_set_italic_property(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.italic) + f.italic = True + self.assertTrue(f.italic) + f.italic = False + self.assertFalse(f.italic) + + def test_set_underline_property(self): + f = pygame_font.Font(None, 20) + self.assertFalse(f.underline) + f.underline = True + self.assertTrue(f.underline) + f.underline = False + self.assertFalse(f.underline) + + def test_set_strikethrough_property(self): + if pygame_font.__name__ != "pygame.ftfont": + f = pygame_font.Font(None, 20) + self.assertFalse(f.strikethrough) + f.strikethrough = True + self.assertTrue(f.strikethrough) + f.strikethrough = False + self.assertFalse(f.strikethrough) + + def test_size(self): + f = pygame_font.Font(None, 20) + text = "Xg" + size = f.size(text) + w, h = size + s = f.render(text, False, (255, 255, 255)) + btext = text.encode("ascii") + + self.assertIsInstance(w, int) + self.assertIsInstance(h, int) + self.assertEqual(s.get_size(), size) + self.assertEqual(f.size(btext), size) + + text = "\u212A" + btext = text.encode("UTF-16")[2:] # Keep the byte order consistent. + bsize = f.size(btext) + size = f.size(text) + + self.assertNotEqual(size, bsize) + + def test_font_file_not_found(self): + # A per BUG reported by Bo Jangeborg on pygame-user mailing list, + # http://www.mail-archive.com/pygame-users@seul.org/msg11675.html + + pygame_font.init() + self.assertRaises( + FileNotFoundError, pygame_font.Font, "some-fictional-font.ttf", 20 + ) + + def test_load_from_file(self): + font_name = pygame_font.get_default_font() + font_path = os.path.join( + os.path.split(pygame.__file__)[0], pygame_font.get_default_font() + ) + f = pygame_font.Font(font_path, 20) + + def test_load_from_file_default(self): + font_name = pygame_font.get_default_font() + font_path = os.path.join( + os.path.split(pygame.__file__)[0], pygame_font.get_default_font() + ) + f = pygame_font.Font(font_path) + + def test_load_from_pathlib(self): + font_name = pygame_font.get_default_font() + font_path = os.path.join( + os.path.split(pygame.__file__)[0], pygame_font.get_default_font() + ) + f = pygame_font.Font(pathlib.Path(font_path), 20) + f = pygame_font.Font(pathlib.Path(font_path)) + + def test_load_from_pathlib_default(self): + font_name = pygame_font.get_default_font() + font_path = os.path.join( + os.path.split(pygame.__file__)[0], pygame_font.get_default_font() + ) + f = pygame_font.Font(pathlib.Path(font_path)) + + def test_load_from_file_obj(self): + font_name = pygame_font.get_default_font() + font_path = os.path.join( + os.path.split(pygame.__file__)[0], pygame_font.get_default_font() + ) + with open(font_path, "rb") as f: + font = pygame_font.Font(f, 20) + + def test_load_from_file_obj_default(self): + font_name = pygame_font.get_default_font() + font_path = os.path.join( + os.path.split(pygame.__file__)[0], pygame_font.get_default_font() + ) + with open(font_path, "rb") as f: + font = pygame_font.Font(f) + + def test_load_default_font_filename(self): + # In font_init, a special case is when the filename argument is + # identical to the default font file name. + f = pygame_font.Font(pygame_font.get_default_font(), 20) + + def test_load_default_font_filename_default(self): + # In font_init, a special case is when the filename argument is + # identical to the default font file name. + f = pygame_font.Font(pygame_font.get_default_font()) + + def _load_unicode(self, path): + import shutil + + fdir = str(FONTDIR) + temp = os.path.join(fdir, path) + pgfont = os.path.join(fdir, "test_sans.ttf") + shutil.copy(pgfont, temp) + try: + with open(temp, "rb") as f: + pass + except FileNotFoundError: + raise unittest.SkipTest("the path cannot be opened") + try: + pygame_font.Font(temp, 20) + finally: + os.remove(temp) + + def test_load_from_file_unicode_0(self): + """ASCII string as a unicode object""" + self._load_unicode("temp_file.ttf") + + def test_load_from_file_unicode_1(self): + self._load_unicode("你好.ttf") + + def test_load_from_file_bytes(self): + font_path = os.path.join( + os.path.split(pygame.__file__)[0], pygame_font.get_default_font() + ) + filesystem_encoding = sys.getfilesystemencoding() + filesystem_errors = "replace" if sys.platform == "win32" else "surrogateescape" + try: # FIXME why do we do this try/except ? + font_path = font_path.decode(filesystem_encoding, filesystem_errors) + except AttributeError: + pass + bfont_path = font_path.encode(filesystem_encoding, filesystem_errors) + f = pygame_font.Font(bfont_path, 20) + + def test_issue_3144(self): + fpath = os.path.join(FONTDIR, "PlayfairDisplaySemibold.ttf") + + # issue in SDL_ttf 2.0.18 DLL on Windows + # tested to make us aware of any regressions + for size in (60, 40, 10, 20, 70, 45, 50, 10): + font = pygame_font.Font(fpath, size) + font.render("WHERE", True, "black") + + def test_font_set_script(self): + if pygame_font.__name__ == "pygame.ftfont": + return # this ain't a pygame.ftfont thing! + + font = pygame_font.Font(None, 16) + + ttf_version = pygame_font.get_sdl_ttf_version() + if ttf_version >= (2, 20, 0): + self.assertRaises(TypeError, pygame.font.Font.set_script) + self.assertRaises(TypeError, pygame.font.Font.set_script, font) + self.assertRaises(TypeError, pygame.font.Font.set_script, "hey", "Deva") + self.assertRaises(TypeError, font.set_script, 1) + self.assertRaises(TypeError, font.set_script, ["D", "e", "v", "a"]) + + self.assertRaises(ValueError, font.set_script, "too long by far") + self.assertRaises(ValueError, font.set_script, "") + self.assertRaises(ValueError, font.set_script, "a") + + font.set_script("Deva") + else: + self.assertRaises(pygame.error, font.set_script, "Deva") + + +@unittest.skipIf(IS_PYPY, "pypy skip known failure") # TODO +class VisualTests(unittest.TestCase): + __tags__ = ["interactive"] + + screen = None + aborted = False + + def setUp(self): + if self.screen is None: + pygame.init() + self.screen = pygame.display.set_mode((600, 200)) + self.screen.fill((255, 255, 255)) + pygame.display.flip() + self.f = pygame_font.Font(None, 32) + + def abort(self): + if self.screen is not None: + pygame.quit() + self.aborted = True + + def query( + self, + bold=False, + italic=False, + underline=False, + strikethrough=False, + antialiase=False, + ): + if self.aborted: + return False + spacing = 10 + offset = 20 + y = spacing + f = self.f + screen = self.screen + screen.fill((255, 255, 255)) + pygame.display.flip() + if not (bold or italic or underline or strikethrough or antialiase): + text = "normal" + else: + modes = [] + if bold: + modes.append("bold") + if italic: + modes.append("italic") + if underline: + modes.append("underlined") + if strikethrough: + modes.append("strikethrough") + if antialiase: + modes.append("antialiased") + text = f"{'-'.join(modes)} (y/n):" + f.set_bold(bold) + f.set_italic(italic) + f.set_underline(underline) + if pygame_font.__name__ != "pygame.ftfont": + f.set_strikethrough(strikethrough) + s = f.render(text, antialiase, (0, 0, 0)) + screen.blit(s, (offset, y)) + y += s.get_size()[1] + spacing + f.set_bold(False) + f.set_italic(False) + f.set_underline(False) + if pygame_font.__name__ != "pygame.ftfont": + f.set_strikethrough(False) + s = f.render("(some comparison text)", False, (0, 0, 0)) + screen.blit(s, (offset, y)) + pygame.display.flip() + while True: + for evt in pygame.event.get(): + if evt.type == pygame.KEYDOWN: + if evt.key == pygame.K_ESCAPE: + self.abort() + return False + if evt.key == pygame.K_y: + return True + if evt.key == pygame.K_n: + return False + if evt.type == pygame.QUIT: + self.abort() + return False + + def test_bold(self): + self.assertTrue(self.query(bold=True)) + + def test_italic(self): + self.assertTrue(self.query(italic=True)) + + def test_underline(self): + self.assertTrue(self.query(underline=True)) + + def test_strikethrough(self): + if pygame_font.__name__ != "pygame.ftfont": + self.assertTrue(self.query(strikethrough=True)) + + def test_antialiase(self): + self.assertTrue(self.query(antialiase=True)) + + def test_bold_antialiase(self): + self.assertTrue(self.query(bold=True, antialiase=True)) + + def test_italic_underline(self): + self.assertTrue(self.query(italic=True, underline=True)) + + def test_bold_strikethrough(self): + if pygame_font.__name__ != "pygame.ftfont": + self.assertTrue(self.query(bold=True, strikethrough=True)) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/freetype_tags.py b/laplas/abstract_map/pygame/tests/freetype_tags.py new file mode 100644 index 0000000..d84cbb7 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/freetype_tags.py @@ -0,0 +1,11 @@ +__tags__ = ["development"] + +exclude = False + +try: + import pygame.freetype +except ImportError: + exclude = True + +if exclude: + __tags__.extend(["ignore", "subprocess_ignore"]) diff --git a/laplas/abstract_map/pygame/tests/freetype_test.py b/laplas/abstract_map/pygame/tests/freetype_test.py new file mode 100644 index 0000000..25551d8 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/freetype_test.py @@ -0,0 +1,1796 @@ +import os + +if os.environ.get("SDL_VIDEODRIVER") == "dummy": + __tags__ = ("ignore", "subprocess_ignore") + +import unittest +import ctypes +import weakref +import gc +import pathlib +import platform + +IS_PYPY = "PyPy" == platform.python_implementation() + + +try: + from pygame.tests.test_utils import arrinter +except NameError: + pass + +import pygame + +try: + import pygame.freetype as ft +except ImportError: + ft = None + + +FONTDIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures", "fonts") + + +def nullfont(): + """return an uninitialized font instance""" + return ft.Font.__new__(ft.Font) + + +max_point_size_FX6 = 0x7FFFFFFF +max_point_size = max_point_size_FX6 >> 6 +max_point_size_f = max_point_size_FX6 * 0.015625 + + +def surf_same_image(a, b): + """Return True if a's pixel buffer is identical to b's""" + + a_sz = a.get_height() * a.get_pitch() + b_sz = b.get_height() * b.get_pitch() + if a_sz != b_sz: + return False + a_bytes = ctypes.string_at(a._pixels_address, a_sz) + b_bytes = ctypes.string_at(b._pixels_address, b_sz) + return a_bytes == b_bytes + + +class FreeTypeFontTest(unittest.TestCase): + _fixed_path = os.path.join(FONTDIR, "test_fixed.otf") + _sans_path = os.path.join(FONTDIR, "test_sans.ttf") + _mono_path = os.path.join(FONTDIR, "PyGameMono.otf") + _bmp_8_75dpi_path = os.path.join(FONTDIR, "PyGameMono-8.bdf") + _bmp_18_75dpi_path = os.path.join(FONTDIR, "PyGameMono-18-75dpi.bdf") + _bmp_18_100dpi_path = os.path.join(FONTDIR, "PyGameMono-18-100dpi.bdf") + _TEST_FONTS = {} + + @classmethod + def setUpClass(cls): + ft.init() + + # Setup the test fonts. + + # Inconsolata is an open-source font designed by Raph Levien. + # Licensed under the Open Font License. + # http://www.levien.com/type/myfonts/inconsolata.html + cls._TEST_FONTS["fixed"] = ft.Font(cls._fixed_path) + + # Liberation Sans is an open-source font designed by Steve Matteson. + # Licensed under the GNU GPL. + # https://fedorahosted.org/liberation-fonts/ + cls._TEST_FONTS["sans"] = ft.Font(cls._sans_path) + + # A scalable mono test font made for pygame. It contains only + # a few glyphs: '\0', 'A', 'B', 'C', and U+13079. + # It also contains two bitmap sizes: 8.0 X 8.0 and 19.0 X 19.0. + cls._TEST_FONTS["mono"] = ft.Font(cls._mono_path) + + # A fixed size bitmap mono test font made for pygame. + # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079. + # The size is 8.0 X 8.0. + cls._TEST_FONTS["bmp-8-75dpi"] = ft.Font(cls._bmp_8_75dpi_path) + + # A fixed size bitmap mono test font made for pygame. + # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079. + # The size is 8.0 X 8.0. + cls._TEST_FONTS["bmp-18-75dpi"] = ft.Font(cls._bmp_18_75dpi_path) + + # A fixed size bitmap mono test font made for pygame. + # It contains only a few glyphs: '\0', 'A', 'B', 'C', and U+13079. + # The size is 8.0 X 8.0. + cls._TEST_FONTS["bmp-18-100dpi"] = ft.Font(cls._bmp_18_100dpi_path) + + @classmethod + def tearDownClass(cls): + ft.quit() + + def test_freetype_defaultfont(self): + font = ft.Font(None) + self.assertEqual(font.name, "FreeSans") + + def test_freetype_Font_init(self): + self.assertRaises( + FileNotFoundError, ft.Font, os.path.join(FONTDIR, "nonexistent.ttf") + ) + + f = self._TEST_FONTS["sans"] + self.assertIsInstance(f, ft.Font) + + f = self._TEST_FONTS["fixed"] + self.assertIsInstance(f, ft.Font) + + # Test keyword arguments + f = ft.Font(size=22, file=None) + self.assertEqual(f.size, 22) + f = ft.Font(font_index=0, file=None) + self.assertNotEqual(ft.get_default_resolution(), 100) + f = ft.Font(resolution=100, file=None) + self.assertEqual(f.resolution, 100) + f = ft.Font(ucs4=True, file=None) + self.assertTrue(f.ucs4) + self.assertRaises(OverflowError, ft.Font, file=None, size=(max_point_size + 1)) + self.assertRaises(OverflowError, ft.Font, file=None, size=-1) + + f = ft.Font(None, size=24) + self.assertTrue(f.height > 0) + self.assertRaises( + FileNotFoundError, f.__init__, os.path.join(FONTDIR, "nonexistent.ttf") + ) + + # Test attribute preservation during reinitalization + f = ft.Font(self._sans_path, size=24, ucs4=True) + self.assertEqual(f.name, "Liberation Sans") + self.assertTrue(f.scalable) + self.assertFalse(f.fixed_width) + self.assertTrue(f.antialiased) + self.assertFalse(f.oblique) + self.assertTrue(f.ucs4) + f.antialiased = False + f.oblique = True + f.__init__(self._mono_path) + self.assertEqual(f.name, "PyGameMono") + self.assertTrue(f.scalable) + self.assertTrue(f.fixed_width) + self.assertFalse(f.antialiased) + self.assertTrue(f.oblique) + self.assertTrue(f.ucs4) + + # For a bitmap font, the size is automatically set to the first + # size in the available sizes list. + f = ft.Font(self._bmp_8_75dpi_path) + sizes = f.get_sizes() + self.assertEqual(len(sizes), 1) + size_pt, width_px, height_px, x_ppem, y_ppem = sizes[0] + self.assertEqual(f.size, (x_ppem, y_ppem)) + f.__init__(self._bmp_8_75dpi_path, size=12) + self.assertEqual(f.size, 12.0) + + @unittest.skipIf(IS_PYPY, "PyPy doesn't use refcounting") + def test_freetype_Font_dealloc(self): + import sys + + handle = open(self._sans_path, "rb") + + def load_font(): + tempFont = ft.Font(handle) + + try: + load_font() + + self.assertEqual(sys.getrefcount(handle), 2) + finally: + # Ensures file is closed even if test fails. + handle.close() + + def test_freetype_Font_kerning(self): + """Ensures get/set works with the kerning property.""" + ft_font = self._TEST_FONTS["sans"] + + # Test default is disabled. + self.assertFalse(ft_font.kerning) + + # Test setting to True. + ft_font.kerning = True + + self.assertTrue(ft_font.kerning) + + # Test setting to False. + ft_font.kerning = False + + self.assertFalse(ft_font.kerning) + + def test_freetype_Font_kerning__enabled(self): + """Ensures exceptions are not raised when calling freetype methods + while kerning is enabled. + + Note: This does not test what changes occur to a rendered font by + having kerning enabled. + + Related to issue #367. + """ + surface = pygame.Surface((10, 10), 0, 32) + TEST_TEXT = "Freetype Font" + ft_font = self._TEST_FONTS["bmp-8-75dpi"] + + ft_font.kerning = True + + # Call different methods to ensure they don't raise an exception. + metrics = ft_font.get_metrics(TEST_TEXT) + self.assertIsInstance(metrics, list) + + rect = ft_font.get_rect(TEST_TEXT) + self.assertIsInstance(rect, pygame.Rect) + + font_surf, rect = ft_font.render(TEST_TEXT) + self.assertIsInstance(font_surf, pygame.Surface) + self.assertIsInstance(rect, pygame.Rect) + + rect = ft_font.render_to(surface, (0, 0), TEST_TEXT) + self.assertIsInstance(rect, pygame.Rect) + + buf, size = ft_font.render_raw(TEST_TEXT) + self.assertIsInstance(buf, bytes) + self.assertIsInstance(size, tuple) + + rect = ft_font.render_raw_to(surface.get_view("2"), TEST_TEXT) + self.assertIsInstance(rect, pygame.Rect) + + def test_freetype_Font_scalable(self): + f = self._TEST_FONTS["sans"] + self.assertTrue(f.scalable) + + self.assertRaises(RuntimeError, lambda: nullfont().scalable) + + def test_freetype_Font_fixed_width(self): + f = self._TEST_FONTS["sans"] + self.assertFalse(f.fixed_width) + + f = self._TEST_FONTS["mono"] + self.assertTrue(f.fixed_width) + + self.assertRaises(RuntimeError, lambda: nullfont().fixed_width) + + def test_freetype_Font_fixed_sizes(self): + f = self._TEST_FONTS["sans"] + self.assertEqual(f.fixed_sizes, 0) + f = self._TEST_FONTS["bmp-8-75dpi"] + self.assertEqual(f.fixed_sizes, 1) + f = self._TEST_FONTS["mono"] + self.assertEqual(f.fixed_sizes, 2) + + def test_freetype_Font_get_sizes(self): + f = self._TEST_FONTS["sans"] + szlist = f.get_sizes() + self.assertIsInstance(szlist, list) + self.assertEqual(len(szlist), 0) + + f = self._TEST_FONTS["bmp-8-75dpi"] + szlist = f.get_sizes() + self.assertIsInstance(szlist, list) + self.assertEqual(len(szlist), 1) + + size8 = szlist[0] + self.assertIsInstance(size8[0], int) + self.assertEqual(size8[0], 8) + self.assertIsInstance(size8[1], int) + self.assertIsInstance(size8[2], int) + self.assertIsInstance(size8[3], float) + self.assertEqual(int(size8[3] * 64.0 + 0.5), 8 * 64) + self.assertIsInstance(size8[4], float) + self.assertEqual(int(size8[4] * 64.0 + 0.5), 8 * 64) + + f = self._TEST_FONTS["mono"] + szlist = f.get_sizes() + self.assertIsInstance(szlist, list) + self.assertEqual(len(szlist), 2) + + size8 = szlist[0] + self.assertEqual(size8[3], 8) + self.assertEqual(int(size8[3] * 64.0 + 0.5), 8 * 64) + self.assertEqual(int(size8[4] * 64.0 + 0.5), 8 * 64) + + size19 = szlist[1] + self.assertEqual(size19[3], 19) + self.assertEqual(int(size19[3] * 64.0 + 0.5), 19 * 64) + self.assertEqual(int(size19[4] * 64.0 + 0.5), 19 * 64) + + def test_freetype_Font_use_bitmap_strikes(self): + f = self._TEST_FONTS["mono"] + try: + # use_bitmap_strikes == True + # + self.assertTrue(f.use_bitmap_strikes) + + # bitmap compatible properties + s_strike, sz = f.render_raw("A", size=19) + try: + f.vertical = True + s_strike_vert, sz = f.render_raw("A", size=19) + finally: + f.vertical = False + try: + f.wide = True + s_strike_wide, sz = f.render_raw("A", size=19) + finally: + f.wide = False + try: + f.underline = True + s_strike_underline, sz = f.render_raw("A", size=19) + finally: + f.underline = False + + # bitmap incompatible properties + s_strike_rot45, sz = f.render_raw("A", size=19, rotation=45) + try: + f.strong = True + s_strike_strong, sz = f.render_raw("A", size=19) + finally: + f.strong = False + try: + f.oblique = True + s_strike_oblique, sz = f.render_raw("A", size=19) + finally: + f.oblique = False + + # compare with use_bitmap_strikes == False + # + f.use_bitmap_strikes = False + self.assertFalse(f.use_bitmap_strikes) + + # bitmap compatible properties + s_outline, sz = f.render_raw("A", size=19) + self.assertNotEqual(s_outline, s_strike) + try: + f.vertical = True + s_outline, sz = f.render_raw("A", size=19) + self.assertNotEqual(s_outline, s_strike_vert) + finally: + f.vertical = False + try: + f.wide = True + s_outline, sz = f.render_raw("A", size=19) + self.assertNotEqual(s_outline, s_strike_wide) + finally: + f.wide = False + try: + f.underline = True + s_outline, sz = f.render_raw("A", size=19) + self.assertNotEqual(s_outline, s_strike_underline) + finally: + f.underline = False + + # bitmap incompatible properties + s_outline, sz = f.render_raw("A", size=19, rotation=45) + self.assertEqual(s_outline, s_strike_rot45) + try: + f.strong = True + s_outline, sz = f.render_raw("A", size=19) + self.assertEqual(s_outline, s_strike_strong) + finally: + f.strong = False + try: + f.oblique = True + s_outline, sz = f.render_raw("A", size=19) + self.assertEqual(s_outline, s_strike_oblique) + finally: + f.oblique = False + finally: + f.use_bitmap_strikes = True + + def test_freetype_Font_bitmap_files(self): + """Ensure bitmap file restrictions are caught""" + f = self._TEST_FONTS["bmp-8-75dpi"] + f_null = nullfont() + s = pygame.Surface((10, 10), 0, 32) + a = s.get_view("3") + + exception = AttributeError + self.assertRaises(exception, setattr, f, "strong", True) + self.assertRaises(exception, setattr, f, "oblique", True) + self.assertRaises(exception, setattr, f, "style", ft.STYLE_STRONG) + self.assertRaises(exception, setattr, f, "style", ft.STYLE_OBLIQUE) + exception = RuntimeError + self.assertRaises(exception, setattr, f_null, "strong", True) + self.assertRaises(exception, setattr, f_null, "oblique", True) + self.assertRaises(exception, setattr, f_null, "style", ft.STYLE_STRONG) + self.assertRaises(exception, setattr, f_null, "style", ft.STYLE_OBLIQUE) + exception = ValueError + self.assertRaises(exception, f.render, "A", (0, 0, 0), size=8, rotation=1) + self.assertRaises( + exception, f.render, "A", (0, 0, 0), size=8, style=ft.STYLE_OBLIQUE + ) + self.assertRaises( + exception, f.render, "A", (0, 0, 0), size=8, style=ft.STYLE_STRONG + ) + self.assertRaises(exception, f.render_raw, "A", size=8, rotation=1) + self.assertRaises(exception, f.render_raw, "A", size=8, style=ft.STYLE_OBLIQUE) + self.assertRaises(exception, f.render_raw, "A", size=8, style=ft.STYLE_STRONG) + self.assertRaises( + exception, f.render_to, s, (0, 0), "A", (0, 0, 0), size=8, rotation=1 + ) + self.assertRaises( + exception, + f.render_to, + s, + (0, 0), + "A", + (0, 0, 0), + size=8, + style=ft.STYLE_OBLIQUE, + ) + self.assertRaises( + exception, + f.render_to, + s, + (0, 0), + "A", + (0, 0, 0), + size=8, + style=ft.STYLE_STRONG, + ) + self.assertRaises(exception, f.render_raw_to, a, "A", size=8, rotation=1) + self.assertRaises( + exception, f.render_raw_to, a, "A", size=8, style=ft.STYLE_OBLIQUE + ) + self.assertRaises( + exception, f.render_raw_to, a, "A", size=8, style=ft.STYLE_STRONG + ) + self.assertRaises(exception, f.get_rect, "A", size=8, rotation=1) + self.assertRaises(exception, f.get_rect, "A", size=8, style=ft.STYLE_OBLIQUE) + self.assertRaises(exception, f.get_rect, "A", size=8, style=ft.STYLE_STRONG) + + # Unsupported point size + exception = pygame.error + self.assertRaises(exception, f.get_rect, "A", size=42) + self.assertRaises(exception, f.get_metrics, "A", size=42) + self.assertRaises(exception, f.get_sized_ascender, 42) + self.assertRaises(exception, f.get_sized_descender, 42) + self.assertRaises(exception, f.get_sized_height, 42) + self.assertRaises(exception, f.get_sized_glyph_height, 42) + + def test_freetype_Font_get_metrics(self): + font = self._TEST_FONTS["sans"] + + metrics = font.get_metrics("ABCD", size=24) + self.assertEqual(len(metrics), len("ABCD")) + self.assertIsInstance(metrics, list) + + for metrics_tuple in metrics: + self.assertIsInstance(metrics_tuple, tuple, metrics_tuple) + self.assertEqual(len(metrics_tuple), 6) + + for m in metrics_tuple[:4]: + self.assertIsInstance(m, int) + + for m in metrics_tuple[4:]: + self.assertIsInstance(m, float) + + # test for empty string + metrics = font.get_metrics("", size=24) + self.assertEqual(metrics, []) + + # test for invalid string + self.assertRaises(TypeError, font.get_metrics, 24, 24) + + # raises exception when uninitialized + self.assertRaises(RuntimeError, nullfont().get_metrics, "a", size=24) + + def test_freetype_Font_get_rect(self): + font = self._TEST_FONTS["sans"] + + def test_rect(r): + self.assertIsInstance(r, pygame.Rect) + + rect_default = font.get_rect("ABCDabcd", size=24) + test_rect(rect_default) + self.assertTrue(rect_default.size > (0, 0)) + self.assertTrue(rect_default.width > rect_default.height) + + rect_bigger = font.get_rect("ABCDabcd", size=32) + test_rect(rect_bigger) + self.assertTrue(rect_bigger.size > rect_default.size) + + rect_strong = font.get_rect("ABCDabcd", size=24, style=ft.STYLE_STRONG) + test_rect(rect_strong) + self.assertTrue(rect_strong.size > rect_default.size) + + font.vertical = True + rect_vert = font.get_rect("ABCDabcd", size=24) + test_rect(rect_vert) + self.assertTrue(rect_vert.width < rect_vert.height) + font.vertical = False + + rect_oblique = font.get_rect("ABCDabcd", size=24, style=ft.STYLE_OBLIQUE) + test_rect(rect_oblique) + self.assertTrue(rect_oblique.width > rect_default.width) + self.assertTrue(rect_oblique.height == rect_default.height) + + rect_under = font.get_rect("ABCDabcd", size=24, style=ft.STYLE_UNDERLINE) + test_rect(rect_under) + self.assertTrue(rect_under.width == rect_default.width) + self.assertTrue(rect_under.height > rect_default.height) + + # Rect size should change if UTF surrogate pairs are treated as + # one code point or two. + ufont = self._TEST_FONTS["mono"] + rect_utf32 = ufont.get_rect("\U00013079", size=24) + rect_utf16 = ufont.get_rect("\uD80C\uDC79", size=24) + self.assertEqual(rect_utf16, rect_utf32) + ufont.ucs4 = True + try: + rect_utf16 = ufont.get_rect("\uD80C\uDC79", size=24) + finally: + ufont.ucs4 = False + self.assertNotEqual(rect_utf16, rect_utf32) + + self.assertRaises(RuntimeError, nullfont().get_rect, "a", size=24) + + # text stretching + rect12 = font.get_rect("A", size=12.0) + rect24 = font.get_rect("A", size=24.0) + rect_x = font.get_rect("A", size=(24.0, 12.0)) + self.assertEqual(rect_x.width, rect24.width) + self.assertEqual(rect_x.height, rect12.height) + rect_y = font.get_rect("A", size=(12.0, 24.0)) + self.assertEqual(rect_y.width, rect12.width) + self.assertEqual(rect_y.height, rect24.height) + + def test_freetype_Font_height(self): + f = self._TEST_FONTS["sans"] + self.assertEqual(f.height, 2355) + + f = self._TEST_FONTS["fixed"] + self.assertEqual(f.height, 1100) + + self.assertRaises(RuntimeError, lambda: nullfont().height) + + def test_freetype_Font_name(self): + f = self._TEST_FONTS["sans"] + self.assertEqual(f.name, "Liberation Sans") + + f = self._TEST_FONTS["fixed"] + self.assertEqual(f.name, "Inconsolata") + + nf = nullfont() + self.assertEqual(nf.name, repr(nf)) + + def test_freetype_Font_size(self): + f = ft.Font(None, size=12) + self.assertEqual(f.size, 12) + f.size = 22 + self.assertEqual(f.size, 22) + f.size = 0 + self.assertEqual(f.size, 0) + f.size = max_point_size + self.assertEqual(f.size, max_point_size) + f.size = 6.5 + self.assertEqual(f.size, 6.5) + f.size = max_point_size_f + self.assertEqual(f.size, max_point_size_f) + self.assertRaises(OverflowError, setattr, f, "size", -1) + self.assertRaises(OverflowError, setattr, f, "size", (max_point_size + 1)) + + f.size = 24.0, 0 + size = f.size + self.assertIsInstance(size, float) + self.assertEqual(size, 24.0) + + f.size = 16, 16 + size = f.size + self.assertIsInstance(size, tuple) + self.assertEqual(len(size), 2) + + x, y = size + self.assertIsInstance(x, float) + self.assertEqual(x, 16.0) + self.assertIsInstance(y, float) + self.assertEqual(y, 16.0) + + f.size = 20.5, 22.25 + x, y = f.size + self.assertEqual(x, 20.5) + self.assertEqual(y, 22.25) + + f.size = 0, 0 + size = f.size + self.assertIsInstance(size, float) + self.assertEqual(size, 0.0) + self.assertRaises(ValueError, setattr, f, "size", (0, 24.0)) + self.assertRaises(TypeError, setattr, f, "size", (24.0,)) + self.assertRaises(TypeError, setattr, f, "size", (24.0, 0, 0)) + self.assertRaises(TypeError, setattr, f, "size", (24.0j, 24.0)) + self.assertRaises(TypeError, setattr, f, "size", (24.0, 24.0j)) + self.assertRaises(OverflowError, setattr, f, "size", (-1, 16)) + self.assertRaises(OverflowError, setattr, f, "size", (max_point_size + 1, 16)) + self.assertRaises(OverflowError, setattr, f, "size", (16, -1)) + self.assertRaises(OverflowError, setattr, f, "size", (16, max_point_size + 1)) + + # bitmap files with identical point size but differing ppems. + f75 = self._TEST_FONTS["bmp-18-75dpi"] + sizes = f75.get_sizes() + self.assertEqual(len(sizes), 1) + size_pt, width_px, height_px, x_ppem, y_ppem = sizes[0] + self.assertEqual(size_pt, 18) + self.assertEqual(x_ppem, 19.0) + self.assertEqual(y_ppem, 19.0) + rect = f75.get_rect("A", size=18) + rect = f75.get_rect("A", size=19) + rect = f75.get_rect("A", size=(19.0, 19.0)) + self.assertRaises(pygame.error, f75.get_rect, "A", size=17) + f100 = self._TEST_FONTS["bmp-18-100dpi"] + sizes = f100.get_sizes() + self.assertEqual(len(sizes), 1) + size_pt, width_px, height_px, x_ppem, y_ppem = sizes[0] + self.assertEqual(size_pt, 18) + self.assertEqual(x_ppem, 25.0) + self.assertEqual(y_ppem, 25.0) + rect = f100.get_rect("A", size=18) + rect = f100.get_rect("A", size=25) + rect = f100.get_rect("A", size=(25.0, 25.0)) + self.assertRaises(pygame.error, f100.get_rect, "A", size=17) + + def test_freetype_Font_rotation(self): + test_angles = [ + (30, 30), + (360, 0), + (390, 30), + (720, 0), + (764, 44), + (-30, 330), + (-360, 0), + (-390, 330), + (-720, 0), + (-764, 316), + ] + + f = ft.Font(None) + self.assertEqual(f.rotation, 0) + for r, r_reduced in test_angles: + f.rotation = r + self.assertEqual( + f.rotation, + r_reduced, + "for angle %d: %d != %d" % (r, f.rotation, r_reduced), + ) + self.assertRaises(TypeError, setattr, f, "rotation", "12") + + def test_freetype_Font_render_to(self): + # Rendering to an existing target surface is equivalent to + # blitting a surface returned by Font.render with the target. + font = self._TEST_FONTS["sans"] + + surf = pygame.Surface((800, 600)) + color = pygame.Color(0, 0, 0) + + rrect = font.render_to(surf, (32, 32), "FoobarBaz", color, None, size=24) + self.assertIsInstance(rrect, pygame.Rect) + self.assertEqual(rrect.topleft, (32, 32)) + self.assertNotEqual(rrect.bottomright, (32, 32)) + + rcopy = rrect.copy() + rcopy.topleft = (32, 32) + self.assertTrue(surf.get_rect().contains(rcopy)) + + rect = pygame.Rect(20, 20, 2, 2) + rrect = font.render_to(surf, rect, "FoobarBax", color, None, size=24) + self.assertEqual(rect.topleft, rrect.topleft) + self.assertNotEqual(rrect.size, rect.size) + rrect = font.render_to(surf, (20.1, 18.9), "FoobarBax", color, None, size=24) + + rrect = font.render_to(surf, rect, "", color, None, size=24) + self.assertFalse(rrect) + self.assertEqual(rrect.height, font.get_sized_height(24)) + + # invalid surf test + self.assertRaises(TypeError, font.render_to, "not a surface", "text", color) + self.assertRaises(TypeError, font.render_to, pygame.Surface, "text", color) + + # invalid dest test + for dest in [ + None, + 0, + "a", + "ab", + (), + (1,), + ("a", 2), + (1, "a"), + (1 + 2j, 2), + (1, 1 + 2j), + (1, int), + (int, 1), + ]: + self.assertRaises( + TypeError, font.render_to, surf, dest, "foobar", color, size=24 + ) + + # misc parameter test + self.assertRaises(ValueError, font.render_to, surf, (0, 0), "foobar", color) + self.assertRaises( + TypeError, font.render_to, surf, (0, 0), "foobar", color, 2.3, size=24 + ) + self.assertRaises( + ValueError, + font.render_to, + surf, + (0, 0), + "foobar", + color, + None, + style=42, + size=24, + ) + self.assertRaises( + TypeError, + font.render_to, + surf, + (0, 0), + "foobar", + color, + None, + style=None, + size=24, + ) + self.assertRaises( + ValueError, + font.render_to, + surf, + (0, 0), + "foobar", + color, + None, + style=97, + size=24, + ) + + def test_freetype_Font_render(self): + font = self._TEST_FONTS["sans"] + + surf = pygame.Surface((800, 600)) + color = pygame.Color(0, 0, 0) + + rend = font.render("FoobarBaz", pygame.Color(0, 0, 0), None, size=24) + self.assertIsInstance(rend, tuple) + self.assertEqual(len(rend), 2) + self.assertIsInstance(rend[0], pygame.Surface) + self.assertIsInstance(rend[1], pygame.Rect) + self.assertEqual(rend[0].get_rect().size, rend[1].size) + + s, r = font.render("", pygame.Color(0, 0, 0), None, size=24) + self.assertEqual(r.width, 0) + self.assertEqual(r.height, font.get_sized_height(24)) + self.assertEqual(s.get_size(), r.size) + self.assertEqual(s.get_bitsize(), 32) + + # misc parameter test + self.assertRaises(ValueError, font.render, "foobar", color) + self.assertRaises(TypeError, font.render, "foobar", color, 2.3, size=24) + self.assertRaises( + ValueError, font.render, "foobar", color, None, style=42, size=24 + ) + self.assertRaises( + TypeError, font.render, "foobar", color, None, style=None, size=24 + ) + self.assertRaises( + ValueError, font.render, "foobar", color, None, style=97, size=24 + ) + + # valid surrogate pairs + font2 = self._TEST_FONTS["mono"] + ucs4 = font2.ucs4 + try: + font2.ucs4 = False + rend1 = font2.render("\uD80C\uDC79", color, size=24) + rend2 = font2.render("\U00013079", color, size=24) + self.assertEqual(rend1[1], rend2[1]) + font2.ucs4 = True + rend1 = font2.render("\uD80C\uDC79", color, size=24) + self.assertNotEqual(rend1[1], rend2[1]) + finally: + font2.ucs4 = ucs4 + + # malformed surrogate pairs + self.assertRaises(UnicodeEncodeError, font.render, "\uD80C", color, size=24) + self.assertRaises(UnicodeEncodeError, font.render, "\uDCA7", color, size=24) + self.assertRaises( + UnicodeEncodeError, font.render, "\uD7FF\uDCA7", color, size=24 + ) + self.assertRaises( + UnicodeEncodeError, font.render, "\uDC00\uDCA7", color, size=24 + ) + self.assertRaises( + UnicodeEncodeError, font.render, "\uD80C\uDBFF", color, size=24 + ) + self.assertRaises( + UnicodeEncodeError, font.render, "\uD80C\uE000", color, size=24 + ) + + # raises exception when uninitialized + self.assertRaises(RuntimeError, nullfont().render, "a", (0, 0, 0), size=24) + + # Confirm the correct glyphs are returned for a couple of + # unicode code points, 'A' and '\U00023079'. For each code point + # the rendered glyph is compared with an image of glyph bitmap + # as exported by FontForge. + path = os.path.join(FONTDIR, "A_PyGameMono-8.png") + A = pygame.image.load(path) + path = os.path.join(FONTDIR, "u13079_PyGameMono-8.png") + u13079 = pygame.image.load(path) + + font = self._TEST_FONTS["mono"] + font.ucs4 = False + A_rendered, r = font.render("A", bgcolor=pygame.Color("white"), size=8) + u13079_rendered, r = font.render( + "\U00013079", bgcolor=pygame.Color("white"), size=8 + ) + + # before comparing the surfaces, make sure they are the same + # pixel format. Use 32-bit SRCALPHA to avoid row padding and + # undefined bytes (the alpha byte will be set to 255.) + bitmap = pygame.Surface(A.get_size(), pygame.SRCALPHA, 32) + bitmap.blit(A, (0, 0)) + rendering = pygame.Surface(A_rendered.get_size(), pygame.SRCALPHA, 32) + rendering.blit(A_rendered, (0, 0)) + self.assertTrue(surf_same_image(rendering, bitmap)) + bitmap = pygame.Surface(u13079.get_size(), pygame.SRCALPHA, 32) + bitmap.blit(u13079, (0, 0)) + rendering = pygame.Surface(u13079_rendered.get_size(), pygame.SRCALPHA, 32) + rendering.blit(u13079_rendered, (0, 0)) + self.assertTrue(surf_same_image(rendering, bitmap)) + + def test_freetype_Font_render_mono(self): + font = self._TEST_FONTS["sans"] + color = pygame.Color("black") + colorkey = pygame.Color("white") + text = "." + + save_antialiased = font.antialiased + font.antialiased = False + try: + surf, r = font.render(text, color, size=24) + self.assertEqual(surf.get_bitsize(), 8) + flags = surf.get_flags() + self.assertTrue(flags & pygame.SRCCOLORKEY) + self.assertFalse(flags & (pygame.SRCALPHA | pygame.HWSURFACE)) + self.assertEqual(surf.get_colorkey(), colorkey) + self.assertIsNone(surf.get_alpha()) + + translucent_color = pygame.Color(*color) + translucent_color.a = 55 + surf, r = font.render(text, translucent_color, size=24) + self.assertEqual(surf.get_bitsize(), 8) + flags = surf.get_flags() + self.assertTrue(flags & (pygame.SRCCOLORKEY | pygame.SRCALPHA)) + self.assertFalse(flags & pygame.HWSURFACE) + self.assertEqual(surf.get_colorkey(), colorkey) + self.assertEqual(surf.get_alpha(), translucent_color.a) + + surf, r = font.render(text, color, colorkey, size=24) + self.assertEqual(surf.get_bitsize(), 32) + finally: + font.antialiased = save_antialiased + + def test_freetype_Font_render_to_mono(self): + # Blitting is done in two stages. First the target is alpha filled + # with the background color, if any. Second, the foreground + # color is alpha blitted to the background. + font = self._TEST_FONTS["sans"] + text = " ." + rect = font.get_rect(text, size=24) + size = rect.size + fg = pygame.Surface((1, 1), pygame.SRCALPHA, 32) + bg = pygame.Surface((1, 1), pygame.SRCALPHA, 32) + surrogate = pygame.Surface((1, 1), pygame.SRCALPHA, 32) + surfaces = [ + pygame.Surface(size, 0, 8), + pygame.Surface(size, 0, 16), + pygame.Surface(size, pygame.SRCALPHA, 16), + pygame.Surface(size, 0, 24), + pygame.Surface(size, 0, 32), + pygame.Surface(size, pygame.SRCALPHA, 32), + ] + fg_colors = [ + surfaces[0].get_palette_at(2), + surfaces[1].unmap_rgb(surfaces[1].map_rgb((128, 64, 200))), + surfaces[2].unmap_rgb(surfaces[2].map_rgb((99, 0, 100, 64))), + (128, 97, 213), + (128, 97, 213), + (128, 97, 213, 60), + ] + fg_colors = [pygame.Color(*c) for c in fg_colors] + self.assertEqual(len(surfaces), len(fg_colors)) # integrity check + bg_colors = [ + surfaces[0].get_palette_at(4), + surfaces[1].unmap_rgb(surfaces[1].map_rgb((220, 20, 99))), + surfaces[2].unmap_rgb(surfaces[2].map_rgb((55, 200, 0, 86))), + (255, 120, 13), + (255, 120, 13), + (255, 120, 13, 180), + ] + bg_colors = [pygame.Color(*c) for c in bg_colors] + self.assertEqual(len(surfaces), len(bg_colors)) # integrity check + + save_antialiased = font.antialiased + font.antialiased = False + try: + fill_color = pygame.Color("black") + for i, surf in enumerate(surfaces): + surf.fill(fill_color) + fg_color = fg_colors[i] + fg.set_at((0, 0), fg_color) + surf.blit(fg, (0, 0)) + r_fg_color = surf.get_at((0, 0)) + surf.set_at((0, 0), fill_color) + rrect = font.render_to(surf, (0, 0), text, fg_color, size=24) + bottomleft = 0, rrect.height - 1 + self.assertEqual( + surf.get_at(bottomleft), + fill_color, + "Position: {}. Depth: {}." + " fg_color: {}.".format(bottomleft, surf.get_bitsize(), fg_color), + ) + bottomright = rrect.width - 1, rrect.height - 1 + self.assertEqual( + surf.get_at(bottomright), + r_fg_color, + "Position: {}. Depth: {}." + " fg_color: {}.".format(bottomright, surf.get_bitsize(), fg_color), + ) + for i, surf in enumerate(surfaces): + surf.fill(fill_color) + fg_color = fg_colors[i] + bg_color = bg_colors[i] + bg.set_at((0, 0), bg_color) + fg.set_at((0, 0), fg_color) + if surf.get_bitsize() == 24: + # For a 24 bit target surface test against Pygame's alpha + # blit as there appears to be a problem with SDL's alpha + # blit: + # + # self.assertEqual(surf.get_at(bottomright), r_fg_color) + # + # raises + # + # AssertionError: (128, 97, 213, 255) != (129, 98, 213, 255) + # + surrogate.set_at((0, 0), fill_color) + surrogate.blit(bg, (0, 0)) + r_bg_color = surrogate.get_at((0, 0)) + surrogate.blit(fg, (0, 0)) + r_fg_color = surrogate.get_at((0, 0)) + else: + # Surface blit values for comparison. + surf.blit(bg, (0, 0)) + r_bg_color = surf.get_at((0, 0)) + surf.blit(fg, (0, 0)) + r_fg_color = surf.get_at((0, 0)) + surf.set_at((0, 0), fill_color) + rrect = font.render_to(surf, (0, 0), text, fg_color, bg_color, size=24) + bottomleft = 0, rrect.height - 1 + self.assertEqual(surf.get_at(bottomleft), r_bg_color) + bottomright = rrect.width - 1, rrect.height - 1 + self.assertEqual(surf.get_at(bottomright), r_fg_color) + finally: + font.antialiased = save_antialiased + + def test_freetype_Font_render_raw(self): + font = self._TEST_FONTS["sans"] + + text = "abc" + size = font.get_rect(text, size=24).size + rend = font.render_raw(text, size=24) + self.assertIsInstance(rend, tuple) + self.assertEqual(len(rend), 2) + + r, s = rend + self.assertIsInstance(r, bytes) + self.assertIsInstance(s, tuple) + self.assertTrue(len(s), 2) + + w, h = s + self.assertIsInstance(w, int) + self.assertIsInstance(h, int) + self.assertEqual(s, size) + self.assertEqual(len(r), w * h) + + r, (w, h) = font.render_raw("", size=24) + self.assertEqual(w, 0) + self.assertEqual(h, font.height) + self.assertEqual(len(r), 0) + + # bug with decenders: this would crash + rend = font.render_raw("render_raw", size=24) + + # bug with non-printable characters: this would cause a crash + # because the text length was not adjusted for skipped characters. + text = "".join([chr(i) for i in range(31, 64)]) + rend = font.render_raw(text, size=10) + + def test_freetype_Font_render_raw_to(self): + # This only checks that blits do not crash. It needs to check: + # - int values + # - invert option + # + + font = self._TEST_FONTS["sans"] + text = "abc" + + # No frills antialiased render to int1 (__render_glyph_INT) + srect = font.get_rect(text, size=24) + surf = pygame.Surface(srect.size, 0, 8) + rrect = font.render_raw_to(surf.get_view("2"), text, size=24) + self.assertEqual(rrect, srect) + + for bpp in [24, 32]: + surf = pygame.Surface(srect.size, 0, bpp) + rrect = font.render_raw_to(surf.get_view("r"), text, size=24) + self.assertEqual(rrect, srect) + + # Underlining to int1 (__fill_glyph_INT) + srect = font.get_rect(text, size=24, style=ft.STYLE_UNDERLINE) + surf = pygame.Surface(srect.size, 0, 8) + rrect = font.render_raw_to( + surf.get_view("2"), text, size=24, style=ft.STYLE_UNDERLINE + ) + self.assertEqual(rrect, srect) + + for bpp in [24, 32]: + surf = pygame.Surface(srect.size, 0, bpp) + rrect = font.render_raw_to( + surf.get_view("r"), text, size=24, style=ft.STYLE_UNDERLINE + ) + self.assertEqual(rrect, srect) + + # Unaliased (mono) rendering to int1 (__render_glyph_MONO_as_INT) + font.antialiased = False + try: + srect = font.get_rect(text, size=24) + surf = pygame.Surface(srect.size, 0, 8) + rrect = font.render_raw_to(surf.get_view("2"), text, size=24) + self.assertEqual(rrect, srect) + + for bpp in [24, 32]: + surf = pygame.Surface(srect.size, 0, bpp) + rrect = font.render_raw_to(surf.get_view("r"), text, size=24) + self.assertEqual(rrect, srect) + finally: + font.antialiased = True + + # Antialiased render to ints sized greater than 1 byte + # (__render_glyph_INT) + srect = font.get_rect(text, size=24) + + for bpp in [16, 24, 32]: + surf = pygame.Surface(srect.size, 0, bpp) + rrect = font.render_raw_to(surf.get_view("2"), text, size=24) + self.assertEqual(rrect, srect) + + # Underline render to ints sized greater than 1 byte + # (__fill_glyph_INT) + srect = font.get_rect(text, size=24, style=ft.STYLE_UNDERLINE) + + for bpp in [16, 24, 32]: + surf = pygame.Surface(srect.size, 0, bpp) + rrect = font.render_raw_to( + surf.get_view("2"), text, size=24, style=ft.STYLE_UNDERLINE + ) + self.assertEqual(rrect, srect) + + # Unaliased (mono) rendering to ints greater than 1 byte + # (__render_glyph_MONO_as_INT) + font.antialiased = False + try: + srect = font.get_rect(text, size=24) + + for bpp in [16, 24, 32]: + surf = pygame.Surface(srect.size, 0, bpp) + rrect = font.render_raw_to(surf.get_view("2"), text, size=24) + self.assertEqual(rrect, srect) + finally: + font.antialiased = True + + # Invalid dest parameter test. + srect = font.get_rect(text, size=24) + surf_buf = pygame.Surface(srect.size, 0, 32).get_view("2") + + for dest in [ + 0, + "a", + "ab", + (), + (1,), + ("a", 2), + (1, "a"), + (1 + 2j, 2), + (1, 1 + 2j), + (1, int), + (int, 1), + ]: + self.assertRaises( + TypeError, font.render_raw_to, surf_buf, text, dest, size=24 + ) + + def test_freetype_Font_text_is_None_with_arr(self): + f = ft.Font(self._sans_path, 36) + f.style = ft.STYLE_NORMAL + f.rotation = 0 + text = "ABCD" + + # reference values + get_rect = f.get_rect(text) + f.vertical = True + get_rect_vert = f.get_rect(text) + + self.assertTrue(get_rect_vert.width < get_rect.width) + self.assertTrue(get_rect_vert.height > get_rect.height) + f.vertical = False + render_to_surf = pygame.Surface(get_rect.size, pygame.SRCALPHA, 32) + + if IS_PYPY: + return + + arr = arrinter.Array(get_rect.size, "u", 1) + render = f.render(text, (0, 0, 0)) + render_to = f.render_to(render_to_surf, (0, 0), text, (0, 0, 0)) + render_raw = f.render_raw(text) + render_raw_to = f.render_raw_to(arr, text) + + # comparisons + surf = pygame.Surface(get_rect.size, pygame.SRCALPHA, 32) + self.assertEqual(f.get_rect(None), get_rect) + s, r = f.render(None, (0, 0, 0)) + self.assertEqual(r, render[1]) + self.assertTrue(surf_same_image(s, render[0])) + r = f.render_to(surf, (0, 0), None, (0, 0, 0)) + self.assertEqual(r, render_to) + self.assertTrue(surf_same_image(surf, render_to_surf)) + px, sz = f.render_raw(None) + self.assertEqual(sz, render_raw[1]) + self.assertEqual(px, render_raw[0]) + sz = f.render_raw_to(arr, None) + self.assertEqual(sz, render_raw_to) + + def test_freetype_Font_text_is_None(self): + f = ft.Font(self._sans_path, 36) + f.style = ft.STYLE_NORMAL + f.rotation = 0 + text = "ABCD" + + # reference values + get_rect = f.get_rect(text) + f.vertical = True + get_rect_vert = f.get_rect(text) + + # vertical: trigger glyph positioning. + f.vertical = True + r = f.get_rect(None) + self.assertEqual(r, get_rect_vert) + f.vertical = False + + # wide style: trigger glyph reload + r = f.get_rect(None, style=ft.STYLE_WIDE) + self.assertEqual(r.height, get_rect.height) + self.assertTrue(r.width > get_rect.width) + r = f.get_rect(None) + self.assertEqual(r, get_rect) + + # rotated: trigger glyph reload + r = f.get_rect(None, rotation=90) + self.assertEqual(r.width, get_rect.height) + self.assertEqual(r.height, get_rect.width) + + # this method will not support None text + self.assertRaises(TypeError, f.get_metrics, None) + + def test_freetype_Font_fgcolor(self): + f = ft.Font(self._bmp_8_75dpi_path) + notdef = "\0" # the PyGameMono .notdef glyph has a pixel at (0, 0) + f.origin = False + f.pad = False + black = pygame.Color("black") # initial color + green = pygame.Color("green") + alpha128 = pygame.Color(10, 20, 30, 128) + + c = f.fgcolor + self.assertIsInstance(c, pygame.Color) + self.assertEqual(c, black) + + s, r = f.render(notdef) + self.assertEqual(s.get_at((0, 0)), black) + + f.fgcolor = green + self.assertEqual(f.fgcolor, green) + + s, r = f.render(notdef) + self.assertEqual(s.get_at((0, 0)), green) + + f.fgcolor = alpha128 + s, r = f.render(notdef) + self.assertEqual(s.get_at((0, 0)), alpha128) + + surf = pygame.Surface(f.get_rect(notdef).size, pygame.SRCALPHA, 32) + f.render_to(surf, (0, 0), None) + self.assertEqual(surf.get_at((0, 0)), alpha128) + + self.assertRaises(AttributeError, setattr, f, "fgcolor", None) + + def test_freetype_Font_bgcolor(self): + f = ft.Font(None, 32) + zero = "0" # the default font 0 glyph does not have a pixel at (0, 0) + f.origin = False + f.pad = False + + transparent_black = pygame.Color(0, 0, 0, 0) # initial color + green = pygame.Color("green") + alpha128 = pygame.Color(10, 20, 30, 128) + + c = f.bgcolor + self.assertIsInstance(c, pygame.Color) + self.assertEqual(c, transparent_black) + + s, r = f.render(zero, pygame.Color(255, 255, 255)) + self.assertEqual(s.get_at((0, 0)), transparent_black) + + f.bgcolor = green + self.assertEqual(f.bgcolor, green) + + s, r = f.render(zero) + self.assertEqual(s.get_at((0, 0)), green) + + f.bgcolor = alpha128 + s, r = f.render(zero) + self.assertEqual(s.get_at((0, 0)), alpha128) + + surf = pygame.Surface(f.get_rect(zero).size, pygame.SRCALPHA, 32) + f.render_to(surf, (0, 0), None) + self.assertEqual(surf.get_at((0, 0)), alpha128) + + self.assertRaises(AttributeError, setattr, f, "bgcolor", None) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + @unittest.skipIf(IS_PYPY, "pypy no likey") + def test_newbuf(self): + from pygame.tests.test_utils import buftools + + Exporter = buftools.Exporter + font = self._TEST_FONTS["sans"] + srect = font.get_rect("Hi", size=12) + for format in [ + "b", + "B", + "h", + "H", + "i", + "I", + "l", + "L", + "q", + "Q", + "x", + "1x", + "2x", + "3x", + "4x", + "5x", + "6x", + "7x", + "8x", + "9x", + "h", + "=h", + "@h", + "!h", + "1h", + "=1h", + ]: + newbuf = Exporter(srect.size, format=format) + rrect = font.render_raw_to(newbuf, "Hi", size=12) + self.assertEqual(rrect, srect) + # Some unsupported formats + for format in ["f", "d", "2h", "?", "hh"]: + newbuf = Exporter(srect.size, format=format, itemsize=4) + self.assertRaises(ValueError, font.render_raw_to, newbuf, "Hi", size=12) + + def test_freetype_Font_style(self): + font = self._TEST_FONTS["sans"] + + # make sure STYLE_NORMAL is the default value + self.assertEqual(ft.STYLE_NORMAL, font.style) + + # make sure we check for style type + with self.assertRaises(TypeError): + font.style = "None" + with self.assertRaises(TypeError): + font.style = None + + # make sure we only accept valid constants + with self.assertRaises(ValueError): + font.style = 112 + + # make assure no assignments happened + self.assertEqual(ft.STYLE_NORMAL, font.style) + + # test assignment + font.style = ft.STYLE_UNDERLINE + self.assertEqual(ft.STYLE_UNDERLINE, font.style) + + # test complex styles + st = ft.STYLE_STRONG | ft.STYLE_UNDERLINE | ft.STYLE_OBLIQUE + + font.style = st + self.assertEqual(st, font.style) + + # and that STYLE_DEFAULT has no effect (continued from above) + self.assertNotEqual(st, ft.STYLE_DEFAULT) + font.style = ft.STYLE_DEFAULT + self.assertEqual(st, font.style) + + # revert changes + font.style = ft.STYLE_NORMAL + self.assertEqual(ft.STYLE_NORMAL, font.style) + + def test_freetype_Font_resolution(self): + text = "|" # Differs in width and height + resolution = ft.get_default_resolution() + new_font = ft.Font(self._sans_path, resolution=2 * resolution) + self.assertEqual(new_font.resolution, 2 * resolution) + size_normal = self._TEST_FONTS["sans"].get_rect(text, size=24).size + size_scaled = new_font.get_rect(text, size=24).size + size_by_2 = size_normal[0] * 2 + self.assertTrue( + size_by_2 + 2 >= size_scaled[0] >= size_by_2 - 2, + "%i not equal %i" % (size_scaled[1], size_by_2), + ) + size_by_2 = size_normal[1] * 2 + self.assertTrue( + size_by_2 + 2 >= size_scaled[1] >= size_by_2 - 2, + "%i not equal %i" % (size_scaled[1], size_by_2), + ) + new_resolution = resolution + 10 + ft.set_default_resolution(new_resolution) + try: + new_font = ft.Font(self._sans_path, resolution=0) + self.assertEqual(new_font.resolution, new_resolution) + finally: + ft.set_default_resolution() + + def test_freetype_Font_path(self): + self.assertEqual(self._TEST_FONTS["sans"].path, self._sans_path) + self.assertRaises(AttributeError, getattr, nullfont(), "path") + + # This Font cache test is conditional on freetype being built by a debug + # version of Python or with the C macro PGFT_DEBUG_CACHE defined. + def test_freetype_Font_cache(self): + glyphs = "abcde" + glen = len(glyphs) + other_glyphs = "123" + oglen = len(other_glyphs) + uempty = "" + ## many_glyphs = (uempty.join([chr(i) for i in range(32,127)] + + ## [chr(i) for i in range(161,172)] + + ## [chr(i) for i in range(174,239)])) + many_glyphs = uempty.join([chr(i) for i in range(32, 127)]) + mglen = len(many_glyphs) + + count = 0 + access = 0 + hit = 0 + miss = 0 + + f = ft.Font(None, size=24, font_index=0, resolution=72, ucs4=False) + f.style = ft.STYLE_NORMAL + f.antialiased = True + + # Ensure debug counters are zero + self.assertEqual(f._debug_cache_stats, (0, 0, 0, 0, 0)) + # Load some basic glyphs + count = access = miss = glen + f.render_raw(glyphs) + self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss)) + # Vertical should not affect the cache + access += glen + hit += glen + f.vertical = True + f.render_raw(glyphs) + f.vertical = False + self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss)) + # New glyphs will + count += oglen + access += oglen + miss += oglen + f.render_raw(other_glyphs) + self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss)) + # Point size does + count += glen + access += glen + miss += glen + f.render_raw(glyphs, size=12) + self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss)) + # Underline style does not + access += oglen + hit += oglen + f.underline = True + f.render_raw(other_glyphs) + f.underline = False + self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss)) + # Oblique style does + count += glen + access += glen + miss += glen + f.oblique = True + f.render_raw(glyphs) + f.oblique = False + self.assertEqual(f._debug_cache_stats, (count, 0, access, hit, miss)) + # Strong style does; by this point cache clears can happen + count += glen + access += glen + miss += glen + f.strong = True + f.render_raw(glyphs) + f.strong = False + ccount, cdelete_count, caccess, chit, cmiss = f._debug_cache_stats + self.assertEqual( + (ccount + cdelete_count, caccess, chit, cmiss), (count, access, hit, miss) + ) + # Rotation does + count += glen + access += glen + miss += glen + f.render_raw(glyphs, rotation=10) + ccount, cdelete_count, caccess, chit, cmiss = f._debug_cache_stats + self.assertEqual( + (ccount + cdelete_count, caccess, chit, cmiss), (count, access, hit, miss) + ) + # aliased (mono) glyphs do + count += oglen + access += oglen + miss += oglen + f.antialiased = False + f.render_raw(other_glyphs) + f.antialiased = True + ccount, cdelete_count, caccess, chit, cmiss = f._debug_cache_stats + self.assertEqual( + (ccount + cdelete_count, caccess, chit, cmiss), (count, access, hit, miss) + ) + # Trigger a cleanup for sure. + count += 2 * mglen + access += 2 * mglen + miss += 2 * mglen + f.get_metrics(many_glyphs, size=8) + f.get_metrics(many_glyphs, size=10) + ccount, cdelete_count, caccess, chit, cmiss = f._debug_cache_stats + self.assertTrue(ccount < count) + self.assertEqual( + (ccount + cdelete_count, caccess, chit, cmiss), (count, access, hit, miss) + ) + + try: + ft.Font._debug_cache_stats + except AttributeError: + del test_freetype_Font_cache + + def test_undefined_character_code(self): + # To be consistent with pygame.font.Font, undefined codes + # are rendered as the undefined character, and has metrics + # of None. + font = self._TEST_FONTS["sans"] + + img, size1 = font.render(chr(1), (0, 0, 0), size=24) + img, size0 = font.render("", (0, 0, 0), size=24) + self.assertTrue(size1.width > size0.width) + + metrics = font.get_metrics(chr(1) + chr(48), size=24) + self.assertEqual(len(metrics), 2) + self.assertIsNone(metrics[0]) + self.assertIsInstance(metrics[1], tuple) + + def test_issue_242(self): + """Issue #242: get_rect() uses 0 as default style""" + + # Issue #242: freetype.Font.get_rect() ignores style defaults when + # the style argument is not given + # + # The text boundary rectangle returned by freetype.Font.get_rect() + # should match the boundary of the same text rendered directly to a + # surface. This permits accurate text positioning. To work properly, + # get_rect() should calculate the text boundary to reflect text style, + # such as underline. Instead, it ignores the style settings for the + # Font object when the style argument is omitted. + # + # When the style argument is not given, freetype.get_rect() uses + # unstyled text when calculating the boundary rectangle. This is + # because _ftfont_getrect(), in _freetype.c, set the default + # style to 0 rather than FT_STYLE_DEFAULT. + # + font = self._TEST_FONTS["sans"] + + # Try wide style on a wide character. + prev_style = font.wide + font.wide = True + try: + rect = font.get_rect("M", size=64) + surf, rrect = font.render(None, size=64) + self.assertEqual(rect, rrect) + finally: + font.wide = prev_style + + # Try strong style on several wide characters. + prev_style = font.strong + font.strong = True + try: + rect = font.get_rect("Mm_", size=64) + surf, rrect = font.render(None, size=64) + self.assertEqual(rect, rrect) + finally: + font.strong = prev_style + + # Try oblique style on a tall, narrow character. + prev_style = font.oblique + font.oblique = True + try: + rect = font.get_rect("|", size=64) + surf, rrect = font.render(None, size=64) + self.assertEqual(rect, rrect) + finally: + font.oblique = prev_style + + # Try underline style on a glyphless character. + prev_style = font.underline + font.underline = True + try: + rect = font.get_rect(" ", size=64) + surf, rrect = font.render(None, size=64) + self.assertEqual(rect, rrect) + finally: + font.underline = prev_style + + def test_issue_237(self): + """Issue #237: Memory overrun when rendered with underlining""" + + # Issue #237: Memory overrun when text without descenders is rendered + # with underlining + # + # The bug crashes the Python interpreter. The bug is caught with C + # assertions in ft_render_cb.c when the Pygame module is compiled + # for debugging. So far it is only known to affect Times New Roman. + # + name = "Times New Roman" + font = ft.SysFont(name, 19) + if font.name != name: + # The font is unavailable, so skip the test. + return + font.underline = True + s, r = font.render("Amazon", size=19) + + # Some other checks to make sure nothing else broke. + for adj in [-2, -1.9, -1, 0, 1.9, 2]: + font.underline_adjustment = adj + s, r = font.render("Amazon", size=19) + + def test_issue_243(self): + """Issue Y: trailing space ignored in boundary calculation""" + + # Issue #243: For a string with trailing spaces, freetype ignores the + # last space in boundary calculations + # + font = self._TEST_FONTS["fixed"] + r1 = font.get_rect(" ", size=64) + self.assertTrue(r1.width > 1) + r2 = font.get_rect(" ", size=64) + self.assertEqual(r2.width, 2 * r1.width) + + def test_garbage_collection(self): + """Check reference counting on returned new references""" + + def ref_items(seq): + return [weakref.ref(o) for o in seq] + + font = self._TEST_FONTS["bmp-8-75dpi"] + font.size = font.get_sizes()[0][0] + text = "A" + rect = font.get_rect(text) + surf = pygame.Surface(rect.size, pygame.SRCALPHA, 32) + refs = [] + refs.extend(ref_items(font.render(text, (0, 0, 0)))) + refs.append(weakref.ref(font.render_to(surf, (0, 0), text, (0, 0, 0)))) + refs.append(weakref.ref(font.get_rect(text))) + + n = len(refs) + self.assertTrue(n > 0) + + # for pypy we garbage collection twice. + for i in range(2): + gc.collect() + + for i in range(n): + self.assertIsNone(refs[i](), "ref %d not collected" % i) + + try: + from sys import getrefcount + except ImportError: + pass + else: + array = arrinter.Array(rect.size, "u", 1) + o = font.render_raw(text) + self.assertEqual(getrefcount(o), 2) + self.assertEqual(getrefcount(o[0]), 2) + self.assertEqual(getrefcount(o[1]), 2) + self.assertEqual(getrefcount(font.render_raw_to(array, text)), 1) + o = font.get_metrics("AB") + self.assertEqual(getrefcount(o), 2) + for i in range(len(o)): + self.assertEqual(getrefcount(o[i]), 2, "refcount fail for item %d" % i) + o = font.get_sizes() + self.assertEqual(getrefcount(o), 2) + for i in range(len(o)): + self.assertEqual(getrefcount(o[i]), 2, "refcount fail for item %d" % i) + + def test_display_surface_quit(self): + """Font.render_to() on a closed display surface""" + + # The Font.render_to() method checks that PySurfaceObject.surf is NULL + # and raise a exception if it is. This fixes a bug in Pygame revision + # 0600ea4f1cfb and earlier where Pygame segfaults instead. + null_surface = pygame.Surface.__new__(pygame.Surface) + f = self._TEST_FONTS["sans"] + self.assertRaises( + pygame.error, f.render_to, null_surface, (0, 0), "Crash!", size=12 + ) + + def test_issue_565(self): + """get_metrics supporting rotation/styles/size""" + + tests = [ + {"method": "size", "value": 36, "msg": "metrics same for size"}, + {"method": "rotation", "value": 90, "msg": "metrics same for rotation"}, + {"method": "oblique", "value": True, "msg": "metrics same for oblique"}, + ] + text = "|" + + def run_test(method, value, msg): + font = ft.Font(self._sans_path, size=24) + before = font.get_metrics(text) + font.__setattr__(method, value) + after = font.get_metrics(text) + self.assertNotEqual(before, after, msg) + + for test in tests: + run_test(test["method"], test["value"], test["msg"]) + + def test_freetype_SysFont_name(self): + """that SysFont accepts names of various types""" + fonts = pygame.font.get_fonts() + size = 12 + + # Check single name string: + font_name = ft.SysFont(fonts[0], size).name + self.assertFalse(font_name is None) + + # Check string of comma-separated names. + names = ",".join(fonts) + font_name_2 = ft.SysFont(names, size).name + self.assertEqual(font_name_2, font_name) + + # Check list of names. + font_name_2 = ft.SysFont(fonts, size).name + self.assertEqual(font_name_2, font_name) + + # Check generator: + names = (name for name in fonts) + font_name_2 = ft.SysFont(names, size).name + self.assertEqual(font_name_2, font_name) + + fonts_b = [f.encode() for f in fonts] + + # Check single name bytes. + font_name_2 = ft.SysFont(fonts_b[0], size).name + self.assertEqual(font_name_2, font_name) + + # Check comma-separated bytes. + names = b",".join(fonts_b) + font_name_2 = ft.SysFont(names, size).name + self.assertEqual(font_name_2, font_name) + + # Check list of bytes. + font_name_2 = ft.SysFont(fonts_b, size).name + self.assertEqual(font_name_2, font_name) + + # Check mixed list of bytes and string. + names = [fonts[0], fonts_b[1], fonts[2], fonts_b[3]] + font_name_2 = ft.SysFont(names, size).name + self.assertEqual(font_name_2, font_name) + + def test_pathlib(self): + f = ft.Font(pathlib.Path(self._fixed_path), 20) + + +class FreeTypeTest(unittest.TestCase): + def setUp(self): + ft.init() + + def tearDown(self): + ft.quit() + + def test_resolution(self): + try: + ft.set_default_resolution() + resolution = ft.get_default_resolution() + self.assertEqual(resolution, 72) + new_resolution = resolution + 10 + ft.set_default_resolution(new_resolution) + self.assertEqual(ft.get_default_resolution(), new_resolution) + ft.init(resolution=resolution + 20) + self.assertEqual(ft.get_default_resolution(), new_resolution) + finally: + ft.set_default_resolution() + + def test_autoinit_and_autoquit(self): + pygame.init() + self.assertTrue(ft.get_init()) + pygame.quit() + self.assertFalse(ft.get_init()) + + # Ensure autoquit is replaced at init time + pygame.init() + self.assertTrue(ft.get_init()) + pygame.quit() + self.assertFalse(ft.get_init()) + + def test_init(self): + # Test if module initialized after calling init(). + ft.quit() + ft.init() + + self.assertTrue(ft.get_init()) + + def test_init__multiple(self): + # Test if module initialized after multiple init() calls. + ft.init() + ft.init() + + self.assertTrue(ft.get_init()) + + def test_quit(self): + # Test if module uninitialized after calling quit(). + ft.quit() + + self.assertFalse(ft.get_init()) + + def test_quit__multiple(self): + # Test if module initialized after multiple quit() calls. + ft.quit() + ft.quit() + + self.assertFalse(ft.get_init()) + + def test_get_init(self): + # Test if get_init() gets the init state. + self.assertTrue(ft.get_init()) + + def test_cache_size(self): + DEFAULT_CACHE_SIZE = 64 + self.assertEqual(ft.get_cache_size(), DEFAULT_CACHE_SIZE) + ft.quit() + self.assertEqual(ft.get_cache_size(), 0) + new_cache_size = DEFAULT_CACHE_SIZE * 2 + ft.init(cache_size=new_cache_size) + self.assertEqual(ft.get_cache_size(), new_cache_size) + + def test_get_error(self): + """Ensures get_error() is initially empty (None).""" + error_msg = ft.get_error() + + self.assertIsNone(error_msg) + + def test_get_version(self): + # Test that get_version() can be called before init() + # Also tests get_version + ft.quit() + + # asserting not None just to have a test case + # there is no real fail condition other than + # raising an exception or a segfault, so a tuple of ints + # should be returned in all cases + self.assertIsNotNone(ft.get_version(linked=False)) + self.assertIsNotNone(ft.get_version(linked=True)) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/ftfont_tags.py b/laplas/abstract_map/pygame/tests/ftfont_tags.py new file mode 100644 index 0000000..0d538f4 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/ftfont_tags.py @@ -0,0 +1,11 @@ +__tags__ = ["development"] + +exclude = False + +try: + import pygame.ftfont +except ImportError: + exclude = True + +if exclude: + __tags__.extend(["ignore", "subprocess_ignore"]) diff --git a/laplas/abstract_map/pygame/tests/ftfont_test.py b/laplas/abstract_map/pygame/tests/ftfont_test.py new file mode 100644 index 0000000..cda708b --- /dev/null +++ b/laplas/abstract_map/pygame/tests/ftfont_test.py @@ -0,0 +1,17 @@ +import sys +import os +import unittest +from pygame.tests import font_test + +import pygame.ftfont + +font_test.pygame_font = pygame.ftfont + +for name in dir(font_test): + obj = getattr(font_test, name) + if isinstance(obj, type) and issubclass(obj, unittest.TestCase): # conditional and + new_name = f"Ft{name}" + globals()[new_name] = type(new_name, (obj,), {}) + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/gfxdraw_test.py b/laplas/abstract_map/pygame/tests/gfxdraw_test.py new file mode 100644 index 0000000..33ee2c5 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/gfxdraw_test.py @@ -0,0 +1,876 @@ +import unittest +import pygame +import pygame.gfxdraw +from pygame.locals import * +from pygame.tests.test_utils import SurfaceSubclass + + +def intensity(c, i): + """Return color c changed by intensity i + + For 0 <= i <= 127 the color is a shade, with 0 being black, 127 being the + unaltered color. + + For 128 <= i <= 255 the color is a tint, with 255 being white, 128 the + unaltered color. + + """ + r, g, b = c[0:3] + if 0 <= i <= 127: + # Darken + return ((r * i) // 127, (g * i) // 127, (b * i) // 127) + # Lighten + return ( + r + ((255 - r) * (255 - i)) // 127, + g + ((255 - g) * (255 - i)) // 127, + b + ((255 - b) * (255 - i)) // 127, + ) + + +class GfxdrawDefaultTest(unittest.TestCase): + is_started = False + + foreground_color = (128, 64, 8) + background_color = (255, 255, 255) + + def make_palette(base_color): + """Return color palette that is various intensities of base_color""" + # Need this function for Python 3.x so the base_color + # is within the scope of the list comprehension. + return [intensity(base_color, i) for i in range(0, 256)] + + default_palette = make_palette(foreground_color) + + default_size = (100, 100) + + def check_at(self, surf, posn, color): + sc = surf.get_at(posn) + fail_msg = "%s != %s at %s, bitsize: %i, flags: %i, masks: %s" % ( + sc, + color, + posn, + surf.get_bitsize(), + surf.get_flags(), + surf.get_masks(), + ) + self.assertEqual(sc, color, fail_msg) + + def check_not_at(self, surf, posn, color): + sc = surf.get_at(posn) + fail_msg = "%s != %s at %s, bitsize: %i, flags: %i, masks: %s" % ( + sc, + color, + posn, + surf.get_bitsize(), + surf.get_flags(), + surf.get_masks(), + ) + self.assertNotEqual(sc, color, fail_msg) + + @classmethod + def setUpClass(cls): + # Necessary for Surface.set_palette. + pygame.init() + pygame.display.set_mode((1, 1)) + + @classmethod + def tearDownClass(cls): + pygame.quit() + + def setUp(self): + # This makes sure pygame is always initialized before each test (in + # case a test calls pygame.quit()). + if not pygame.get_init(): + pygame.init() + + Surface = pygame.Surface + size = self.default_size + palette = self.default_palette + if not self.is_started: + # Create test surfaces + self.surfaces = [ + Surface(size, 0, 8), + Surface(size, SRCALPHA, 16), + Surface(size, SRCALPHA, 32), + ] + self.surfaces[0].set_palette(palette) + nonpalette_fmts = ( + # (8, (0xe0, 0x1c, 0x3, 0x0)), + (12, (0xF00, 0xF0, 0xF, 0x0)), + (15, (0x7C00, 0x3E0, 0x1F, 0x0)), + (15, (0x1F, 0x3E0, 0x7C00, 0x0)), + (16, (0xF00, 0xF0, 0xF, 0xF000)), + (16, (0xF000, 0xF00, 0xF0, 0xF)), + (16, (0xF, 0xF0, 0xF00, 0xF000)), + (16, (0xF0, 0xF00, 0xF000, 0xF)), + (16, (0x7C00, 0x3E0, 0x1F, 0x8000)), + (16, (0xF800, 0x7C0, 0x3E, 0x1)), + (16, (0x1F, 0x3E0, 0x7C00, 0x8000)), + (16, (0x3E, 0x7C0, 0xF800, 0x1)), + (16, (0xF800, 0x7E0, 0x1F, 0x0)), + (16, (0x1F, 0x7E0, 0xF800, 0x0)), + (24, (0xFF, 0xFF00, 0xFF0000, 0x0)), + (24, (0xFF0000, 0xFF00, 0xFF, 0x0)), + (32, (0xFF0000, 0xFF00, 0xFF, 0x0)), + (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)), + (32, (0xFF, 0xFF00, 0xFF0000, 0x0)), + (32, (0xFF00, 0xFF0000, 0xFF000000, 0x0)), + (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)), + (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)), + (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)), + (32, (0xFF00, 0xFF0000, 0xFF000000, 0xFF)), + ) + for bitsize, masks in nonpalette_fmts: + self.surfaces.append(Surface(size, 0, bitsize, masks)) + for surf in self.surfaces: + surf.fill(self.background_color) + + def test_gfxdraw__subclassed_surface(self): + """Ensure pygame.gfxdraw works on subclassed surfaces.""" + surface = SurfaceSubclass((11, 13), SRCALPHA, 32) + surface.fill(pygame.Color("blue")) + expected_color = pygame.Color("red") + x, y = 1, 2 + + pygame.gfxdraw.pixel(surface, x, y, expected_color) + + self.assertEqual(surface.get_at((x, y)), expected_color) + + def test_pixel(self): + """pixel(surface, x, y, color): return None""" + fg = self.foreground_color + bg = self.background_color + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.pixel(surf, 2, 2, fg) + for x in range(1, 4): + for y in range(1, 4): + if x == 2 and y == 2: + self.check_at(surf, (x, y), fg_adjusted) + else: + self.check_at(surf, (x, y), bg_adjusted) + + def test_hline(self): + """hline(surface, x1, x2, y, color): return None""" + fg = self.foreground_color + bg = self.background_color + startx = 10 + stopx = 80 + y = 50 + fg_test_points = [(startx, y), (stopx, y), ((stopx - startx) // 2, y)] + bg_test_points = [ + (startx - 1, y), + (stopx + 1, y), + (startx, y - 1), + (startx, y + 1), + (stopx, y - 1), + (stopx, y + 1), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.hline(surf, startx, stopx, y, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_vline(self): + """vline(surface, x, y1, y2, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 50 + starty = 10 + stopy = 80 + fg_test_points = [(x, starty), (x, stopy), (x, (stopy - starty) // 2)] + bg_test_points = [ + (x, starty - 1), + (x, stopy + 1), + (x - 1, starty), + (x + 1, starty), + (x - 1, stopy), + (x + 1, stopy), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.vline(surf, x, starty, stopy, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_rectangle(self): + """rectangle(surface, rect, color): return None""" + fg = self.foreground_color + bg = self.background_color + rect = pygame.Rect(10, 15, 55, 62) + rect_tuple = tuple(rect) + fg_test_points = [ + rect.topleft, + (rect.right - 1, rect.top), + (rect.left, rect.bottom - 1), + (rect.right - 1, rect.bottom - 1), + ] + bg_test_points = [ + (rect.left - 1, rect.top - 1), + (rect.left + 1, rect.top + 1), + (rect.right, rect.top - 1), + (rect.right - 2, rect.top + 1), + (rect.left - 1, rect.bottom), + (rect.left + 1, rect.bottom - 2), + (rect.right, rect.bottom), + (rect.right - 2, rect.bottom - 2), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.rectangle(surf, rect, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + surf.fill(bg) + pygame.gfxdraw.rectangle(surf, rect_tuple, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_box(self): + """box(surface, rect, color): return None""" + fg = self.foreground_color + bg = self.background_color + rect = pygame.Rect(10, 15, 55, 62) + rect_tuple = tuple(rect) + fg_test_points = [ + rect.topleft, + (rect.left + 1, rect.top + 1), + (rect.right - 1, rect.top), + (rect.right - 2, rect.top + 1), + (rect.left, rect.bottom - 1), + (rect.left + 1, rect.bottom - 2), + (rect.right - 1, rect.bottom - 1), + (rect.right - 2, rect.bottom - 2), + ] + bg_test_points = [ + (rect.left - 1, rect.top - 1), + (rect.right, rect.top - 1), + (rect.left - 1, rect.bottom), + (rect.right, rect.bottom), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.box(surf, rect, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + surf.fill(bg) + pygame.gfxdraw.box(surf, rect_tuple, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_line(self): + """line(surface, x1, y1, x2, y2, color): return None""" + fg = self.foreground_color + bg = self.background_color + x1 = 10 + y1 = 15 + x2 = 92 + y2 = 77 + fg_test_points = [(x1, y1), (x2, y2)] + bg_test_points = [ + (x1 - 1, y1), + (x1, y1 - 1), + (x1 - 1, y1 - 1), + (x2 + 1, y2), + (x2, y2 + 1), + (x2 + 1, y2 + 1), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.line(surf, x1, y1, x2, y2, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_circle(self): + """circle(surface, x, y, r, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 45 + y = 40 + r = 30 + fg_test_points = [(x, y - r), (x, y + r), (x - r, y), (x + r, y)] + bg_test_points = [ + (x, y), + (x, y - r + 1), + (x, y - r - 1), + (x, y + r + 1), + (x, y + r - 1), + (x - r - 1, y), + (x - r + 1, y), + (x + r + 1, y), + (x + r - 1, y), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.circle(surf, x, y, r, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_arc(self): + """arc(surface, x, y, r, start, end, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 45 + y = 40 + r = 30 + start = 0 # +x direction, but not (x + r, y) (?) + end = 90 # -y direction, including (x, y + r) + fg_test_points = [(x, y + r), (x + r, y + 1)] + bg_test_points = [ + (x, y), + (x, y - r), + (x - r, y), + (x, y + r + 1), + (x, y + r - 1), + (x - 1, y + r), + (x + r + 1, y), + (x + r - 1, y), + (x + r, y), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.arc(surf, x, y, r, start, end, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_aacircle(self): + """aacircle(surface, x, y, r, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 45 + y = 40 + r = 30 + fg_test_points = [(x, y - r), (x, y + r), (x - r, y), (x + r, y)] + bg_test_points = [ + (x, y), + (x, y - r + 1), + (x, y - r - 1), + (x, y + r + 1), + (x, y + r - 1), + (x - r - 1, y), + (x - r + 1, y), + (x + r + 1, y), + (x + r - 1, y), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.aacircle(surf, x, y, r, fg) + for posn in fg_test_points: + self.check_not_at(surf, posn, bg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_filled_circle(self): + """filled_circle(surface, x, y, r, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 45 + y = 40 + r = 30 + fg_test_points = [ + (x, y - r), + (x, y - r + 1), + (x, y + r), + (x, y + r - 1), + (x - r, y), + (x - r + 1, y), + (x + r, y), + (x + r - 1, y), + (x, y), + ] + bg_test_points = [ + (x, y - r - 1), + (x, y + r + 1), + (x - r - 1, y), + (x + r + 1, y), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.filled_circle(surf, x, y, r, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_ellipse(self): + """ellipse(surface, x, y, rx, ry, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 45 + y = 40 + rx = 30 + ry = 35 + fg_test_points = [(x, y - ry), (x, y + ry), (x - rx, y), (x + rx, y)] + bg_test_points = [ + (x, y), + (x, y - ry + 1), + (x, y - ry - 1), + (x, y + ry + 1), + (x, y + ry - 1), + (x - rx - 1, y), + (x - rx + 1, y), + (x + rx + 1, y), + (x + rx - 1, y), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.ellipse(surf, x, y, rx, ry, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_aaellipse(self): + """aaellipse(surface, x, y, rx, ry, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 45 + y = 40 + rx = 30 + ry = 35 + fg_test_points = [(x, y - ry), (x, y + ry), (x - rx, y), (x + rx, y)] + bg_test_points = [ + (x, y), + (x, y - ry + 1), + (x, y - ry - 1), + (x, y + ry + 1), + (x, y + ry - 1), + (x - rx - 1, y), + (x - rx + 1, y), + (x + rx + 1, y), + (x + rx - 1, y), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.aaellipse(surf, x, y, rx, ry, fg) + for posn in fg_test_points: + self.check_not_at(surf, posn, bg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_filled_ellipse(self): + """filled_ellipse(surface, x, y, rx, ry, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 45 + y = 40 + rx = 30 + ry = 35 + fg_test_points = [ + (x, y - ry), + (x, y - ry + 1), + (x, y + ry), + (x, y + ry - 1), + (x - rx, y), + (x - rx + 1, y), + (x + rx, y), + (x + rx - 1, y), + (x, y), + ] + bg_test_points = [ + (x, y - ry - 1), + (x, y + ry + 1), + (x - rx - 1, y), + (x + rx + 1, y), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.filled_ellipse(surf, x, y, rx, ry, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_pie(self): + """pie(surface, x, y, r, start, end, color): return None""" + fg = self.foreground_color + bg = self.background_color + x = 45 + y = 40 + r = 30 + start = 0 # +x direction, including (x + r, y) + end = 90 # -y direction, but not (x, y + r) (?) + fg_test_points = [(x, y), (x + 1, y), (x, y + 1), (x + r, y)] + bg_test_points = [ + (x - 1, y), + (x, y - 1), + (x - 1, y - 1), + (x + 1, y + 1), + (x + r + 1, y), + (x + r, y - 1), + (x, y + r + 1), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.pie(surf, x, y, r, start, end, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_trigon(self): + """trigon(surface, x1, y1, x2, y2, x3, y3, color): return None""" + fg = self.foreground_color + bg = self.background_color + x1 = 10 + y1 = 15 + x2 = 92 + y2 = 77 + x3 = 20 + y3 = 60 + fg_test_points = [(x1, y1), (x2, y2), (x3, y3)] + bg_test_points = [ + (x1 - 1, y1 - 1), + (x2 + 1, y2 + 1), + (x3 - 1, y3 + 1), + (x1 + 10, y1 + 30), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.trigon(surf, x1, y1, x2, y2, x3, y3, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_aatrigon(self): + """aatrigon(surface, x1, y1, x2, y2, x3, y3, color): return None""" + fg = self.foreground_color + bg = self.background_color + x1 = 10 + y1 = 15 + x2 = 92 + y2 = 77 + x3 = 20 + y3 = 60 + fg_test_points = [(x1, y1), (x2, y2), (x3, y3)] + bg_test_points = [ + (x1 - 1, y1 - 1), + (x2 + 1, y2 + 1), + (x3 - 1, y3 + 1), + (x1 + 10, y1 + 30), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.aatrigon(surf, x1, y1, x2, y2, x3, y3, fg) + for posn in fg_test_points: + self.check_not_at(surf, posn, bg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_aatrigon__with_horizontal_edge(self): + """Ensure aatrigon draws horizontal edges correctly. + + This test creates 2 surfaces and draws an aatrigon on each. The pixels + on each surface are compared to ensure they are the same. The only + difference between the 2 aatrigons is the order the points are drawn. + The order of the points should have no impact on the final drawing. + + Related to issue #622. + """ + bg_color = pygame.Color("white") + line_color = pygame.Color("black") + width, height = 11, 10 + expected_surface = pygame.Surface((width, height), 0, 32) + expected_surface.fill(bg_color) + surface = pygame.Surface((width, height), 0, 32) + surface.fill(bg_color) + + x1, y1 = width - 1, 0 + x2, y2 = (width - 1) // 2, height - 1 + x3, y3 = 0, 0 + + # The points in this order draw as expected. + pygame.gfxdraw.aatrigon(expected_surface, x1, y1, x2, y2, x3, y3, line_color) + + # The points in reverse order fail to draw the horizontal edge along + # the top. + pygame.gfxdraw.aatrigon(surface, x3, y3, x2, y2, x1, y1, line_color) + + # The surfaces are locked for a possible speed up of pixel access. + expected_surface.lock() + surface.lock() + for x in range(width): + for y in range(height): + self.assertEqual( + expected_surface.get_at((x, y)), + surface.get_at((x, y)), + f"pos=({x}, {y})", + ) + + surface.unlock() + expected_surface.unlock() + + def test_filled_trigon(self): + """filled_trigon(surface, x1, y1, x2, y2, x3, y3, color): return None""" + fg = self.foreground_color + bg = self.background_color + x1 = 10 + y1 = 15 + x2 = 92 + y2 = 77 + x3 = 20 + y3 = 60 + fg_test_points = [(x1, y1), (x2, y2), (x3, y3), (x1 + 10, y1 + 30)] + bg_test_points = [(x1 - 1, y1 - 1), (x2 + 1, y2 + 1), (x3 - 1, y3 + 1)] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.filled_trigon(surf, x1, y1, x2, y2, x3, y3, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_polygon(self): + """polygon(surface, points, color): return None""" + fg = self.foreground_color + bg = self.background_color + points = [(10, 80), (10, 15), (92, 25), (92, 80)] + fg_test_points = points + [ + (points[0][0], points[0][1] - 1), + (points[0][0] + 1, points[0][1]), + (points[3][0] - 1, points[3][1]), + (points[3][0], points[3][1] - 1), + (points[2][0], points[2][1] + 1), + ] + bg_test_points = [ + (points[0][0] - 1, points[0][1]), + (points[0][0], points[0][1] + 1), + (points[0][0] - 1, points[0][1] + 1), + (points[0][0] + 1, points[0][1] - 1), + (points[3][0] + 1, points[3][1]), + (points[3][0], points[3][1] + 1), + (points[3][0] + 1, points[3][1] + 1), + (points[3][0] - 1, points[3][1] - 1), + (points[2][0] + 1, points[2][1]), + (points[2][0] - 1, points[2][1] + 1), + (points[1][0] - 1, points[1][1]), + (points[1][0], points[1][1] - 1), + (points[1][0] - 1, points[1][1] - 1), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.polygon(surf, points, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_aapolygon(self): + """aapolygon(surface, points, color): return None""" + fg = self.foreground_color + bg = self.background_color + points = [(10, 80), (10, 15), (92, 25), (92, 80)] + fg_test_points = points + bg_test_points = [ + (points[0][0] - 1, points[0][1]), + (points[0][0], points[0][1] + 1), + (points[0][0] - 1, points[0][1] + 1), + (points[0][0] + 1, points[0][1] - 1), + (points[3][0] + 1, points[3][1]), + (points[3][0], points[3][1] + 1), + (points[3][0] + 1, points[3][1] + 1), + (points[3][0] - 1, points[3][1] - 1), + (points[2][0] + 1, points[2][1]), + (points[2][0] - 1, points[2][1] + 1), + (points[1][0] - 1, points[1][1]), + (points[1][0], points[1][1] - 1), + (points[1][0] - 1, points[1][1] - 1), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.aapolygon(surf, points, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_not_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_aapolygon__with_horizontal_edge(self): + """Ensure aapolygon draws horizontal edges correctly. + + This test creates 2 surfaces and draws a polygon on each. The pixels + on each surface are compared to ensure they are the same. The only + difference between the 2 polygons is that one is drawn using + aapolygon() and the other using multiple line() calls. They should + produce the same final drawing. + + Related to issue #622. + """ + bg_color = pygame.Color("white") + line_color = pygame.Color("black") + width, height = 11, 10 + expected_surface = pygame.Surface((width, height), 0, 32) + expected_surface.fill(bg_color) + surface = pygame.Surface((width, height), 0, 32) + surface.fill(bg_color) + + points = ((0, 0), (0, height - 1), (width - 1, height - 1), (width - 1, 0)) + + # The points are used to draw the expected aapolygon using the line() + # function. + for (x1, y1), (x2, y2) in zip(points, points[1:] + points[:1]): + pygame.gfxdraw.line(expected_surface, x1, y1, x2, y2, line_color) + + # The points in this order fail to draw the horizontal edge along + # the top. + pygame.gfxdraw.aapolygon(surface, points, line_color) + + # The surfaces are locked for a possible speed up of pixel access. + expected_surface.lock() + surface.lock() + for x in range(width): + for y in range(height): + self.assertEqual( + expected_surface.get_at((x, y)), + surface.get_at((x, y)), + f"pos=({x}, {y})", + ) + + surface.unlock() + expected_surface.unlock() + + def test_filled_polygon(self): + """filled_polygon(surface, points, color): return None""" + fg = self.foreground_color + bg = self.background_color + points = [(10, 80), (10, 15), (92, 25), (92, 80)] + fg_test_points = points + [ + (points[0][0], points[0][1] - 1), + (points[0][0] + 1, points[0][1]), + (points[0][0] + 1, points[0][1] - 1), + (points[3][0] - 1, points[3][1]), + (points[3][0], points[3][1] - 1), + (points[3][0] - 1, points[3][1] - 1), + (points[2][0], points[2][1] + 1), + (points[2][0] - 1, points[2][1] + 1), + ] + bg_test_points = [ + (points[0][0] - 1, points[0][1]), + (points[0][0], points[0][1] + 1), + (points[0][0] - 1, points[0][1] + 1), + (points[3][0] + 1, points[3][1]), + (points[3][0], points[3][1] + 1), + (points[3][0] + 1, points[3][1] + 1), + (points[2][0] + 1, points[2][1]), + (points[1][0] - 1, points[1][1]), + (points[1][0], points[1][1] - 1), + (points[1][0] - 1, points[1][1] - 1), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.filled_polygon(surf, points, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + def test_textured_polygon(self): + """textured_polygon(surface, points, texture, tx, ty): return None""" + w, h = self.default_size + fg = self.foreground_color + bg = self.background_color + tx = 0 + ty = 0 + texture = pygame.Surface((w + tx, h + ty), 0, 24) + texture.fill(fg, (0, 0, w, h)) + points = [(10, 80), (10, 15), (92, 25), (92, 80)] + # Don't know how to really check this as boarder points may + # or may not be included in the textured polygon. + fg_test_points = [(points[1][0] + 30, points[1][1] + 40)] + bg_test_points = [ + (points[0][0] - 1, points[0][1]), + (points[0][0], points[0][1] + 1), + (points[0][0] - 1, points[0][1] + 1), + (points[3][0] + 1, points[3][1]), + (points[3][0], points[3][1] + 1), + (points[3][0] + 1, points[3][1] + 1), + (points[2][0] + 1, points[2][1]), + (points[1][0] - 1, points[1][1]), + (points[1][0], points[1][1] - 1), + (points[1][0] - 1, points[1][1] - 1), + ] + for surf in self.surfaces[1:]: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.textured_polygon(surf, points, texture, -tx, -ty) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + # Alpha blit to 8 bits-per-pixel surface forbidden. + texture = pygame.Surface(self.default_size, SRCALPHA, 32) + self.assertRaises( + ValueError, + pygame.gfxdraw.textured_polygon, + self.surfaces[0], + points, + texture, + 0, + 0, + ) + + def test_bezier(self): + """bezier(surface, points, steps, color): return None""" + fg = self.foreground_color + bg = self.background_color + points = [(10, 50), (25, 15), (60, 80), (92, 30)] + fg_test_points = [points[0], points[3]] + bg_test_points = [ + (points[0][0] - 1, points[0][1]), + (points[3][0] + 1, points[3][1]), + (points[1][0], points[1][1] + 3), + (points[2][0], points[2][1] - 3), + ] + for surf in self.surfaces: + fg_adjusted = surf.unmap_rgb(surf.map_rgb(fg)) + bg_adjusted = surf.unmap_rgb(surf.map_rgb(bg)) + pygame.gfxdraw.bezier(surf, points, 30, fg) + for posn in fg_test_points: + self.check_at(surf, posn, fg_adjusted) + for posn in bg_test_points: + self.check_at(surf, posn, bg_adjusted) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/image__save_gl_surface_test.py b/laplas/abstract_map/pygame/tests/image__save_gl_surface_test.py new file mode 100644 index 0000000..2932f42 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/image__save_gl_surface_test.py @@ -0,0 +1,46 @@ +import os +import unittest + +from pygame.tests import test_utils +import pygame +from pygame.locals import * + + +@unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", + 'OpenGL requires a non-"dummy" SDL_VIDEODRIVER', +) +class GL_ImageSave(unittest.TestCase): + def test_image_save_works_with_opengl_surfaces(self): + """ + |tags:display,slow,opengl| + """ + + pygame.display.init() + screen = pygame.display.set_mode((640, 480), OPENGL | DOUBLEBUF) + pygame.display.flip() + + tmp_dir = test_utils.get_tmp_dir() + # Try the imageext module. + tmp_file = os.path.join(tmp_dir, "opengl_save_surface_test.png") + pygame.image.save(screen, tmp_file) + + self.assertTrue(os.path.exists(tmp_file)) + + os.remove(tmp_file) + + # Only test the image module. + tmp_file = os.path.join(tmp_dir, "opengl_save_surface_test.bmp") + pygame.image.save(screen, tmp_file) + + self.assertTrue(os.path.exists(tmp_file)) + + os.remove(tmp_file) + + # stops tonnes of tmp dirs building up in trunk dir + os.rmdir(tmp_dir) + pygame.display.quit() + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/image_tags.py b/laplas/abstract_map/pygame/tests/image_tags.py new file mode 100644 index 0000000..d847903 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/image_tags.py @@ -0,0 +1,7 @@ +__tags__ = [] + +import pygame +import sys + +if "pygame.image" not in sys.modules: + __tags__.extend(("ignore", "subprocess_ignore")) diff --git a/laplas/abstract_map/pygame/tests/image_test.py b/laplas/abstract_map/pygame/tests/image_test.py new file mode 100644 index 0000000..77dff79 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/image_test.py @@ -0,0 +1,1282 @@ +import array +import binascii +import io +import os +import tempfile +import unittest +import glob +import pathlib + +from pygame.tests.test_utils import example_path, png, tostring +import pygame, pygame.image, pygame.pkgdata + +sdl_image_svg_jpeg_save_bug = False +_sdl_image_ver = pygame.image.get_sdl_image_version() +if _sdl_image_ver is not None: + sdl_image_svg_jpeg_save_bug = ( + _sdl_image_ver <= (2, 0, 5) and pygame.get_sdl_byteorder() == pygame.BIG_ENDIAN + ) + + +def test_magic(f, magic_hexes): + """Tests a given file to see if the magic hex matches.""" + data = f.read(len(magic_hexes)) + if len(data) != len(magic_hexes): + return 0 + for i, magic_hex in enumerate(magic_hexes): + if magic_hex != data[i]: + return 0 + return 1 + + +class ImageModuleTest(unittest.TestCase): + def testLoadIcon(self): + """see if we can load the pygame icon.""" + f = pygame.pkgdata.getResource("pygame_icon.bmp") + self.assertEqual(f.mode, "rb") + + surf = pygame.image.load_basic(f) + + self.assertEqual(surf.get_at((0, 0)), (5, 4, 5, 255)) + self.assertEqual(surf.get_height(), 32) + self.assertEqual(surf.get_width(), 32) + + def testLoadPNG(self): + """see if we can load a png with color values in the proper channels.""" + # Create a PNG file with known colors + reddish_pixel = (210, 0, 0, 255) + greenish_pixel = (0, 220, 0, 255) + bluish_pixel = (0, 0, 230, 255) + greyish_pixel = (110, 120, 130, 140) + pixel_array = [reddish_pixel + greenish_pixel, bluish_pixel + greyish_pixel] + + f_descriptor, f_path = tempfile.mkstemp(suffix=".png") + + with os.fdopen(f_descriptor, "wb") as f: + w = png.Writer(2, 2, alpha=True) + w.write(f, pixel_array) + + # Read the PNG file and verify that pygame interprets it correctly + surf = pygame.image.load(f_path) + + self.assertEqual(surf.get_at((0, 0)), reddish_pixel) + self.assertEqual(surf.get_at((1, 0)), greenish_pixel) + self.assertEqual(surf.get_at((0, 1)), bluish_pixel) + self.assertEqual(surf.get_at((1, 1)), greyish_pixel) + + # Read the PNG file obj. and verify that pygame interprets it correctly + with open(f_path, "rb") as f: + surf = pygame.image.load(f) + + self.assertEqual(surf.get_at((0, 0)), reddish_pixel) + self.assertEqual(surf.get_at((1, 0)), greenish_pixel) + self.assertEqual(surf.get_at((0, 1)), bluish_pixel) + self.assertEqual(surf.get_at((1, 1)), greyish_pixel) + + os.remove(f_path) + + def testLoadJPG(self): + """to see if we can load a jpg.""" + f = example_path("data/alien1.jpg") + surf = pygame.image.load(f) + + with open(f, "rb") as f: + surf = pygame.image.load(f) + + def testLoadBytesIO(self): + """to see if we can load images with BytesIO.""" + files = [ + "data/alien1.png", + "data/alien1.jpg", + "data/alien1.gif", + "data/asprite.bmp", + ] + + for fname in files: + with self.subTest(fname=fname): + with open(example_path(fname), "rb") as f: + img_bytes = f.read() + img_file = io.BytesIO(img_bytes) + image = pygame.image.load(img_file) + + @unittest.skipIf( + sdl_image_svg_jpeg_save_bug, + "SDL_image 2.0.5 and older has a big endian bug in jpeg saving", + ) + def testSaveJPG(self): + """JPG equivalent to issue #211 - color channel swapping + + Make sure the SDL surface color masks represent the rgb memory format + required by the JPG library. The masks are machine endian dependent + """ + + from pygame import Color, Rect + + # The source image is a 2 by 2 square of four colors. Since JPEG is + # lossy, there can be color bleed. Make each color square 16 by 16, + # to avoid the significantly color value distorts found at color + # boundaries due to the compression value set by Pygame. + square_len = 16 + sz = 2 * square_len, 2 * square_len + + # +---------------------------------+ + # | red | green | + # |----------------+----------------| + # | blue | (255, 128, 64) | + # +---------------------------------+ + # + # as (rect, color) pairs. + def as_rect(square_x, square_y): + return Rect( + square_x * square_len, square_y * square_len, square_len, square_len + ) + + squares = [ + (as_rect(0, 0), Color("red")), + (as_rect(1, 0), Color("green")), + (as_rect(0, 1), Color("blue")), + (as_rect(1, 1), Color(255, 128, 64)), + ] + + # A surface format which is not directly usable with libjpeg. + surf = pygame.Surface(sz, 0, 32) + for rect, color in squares: + surf.fill(color, rect) + + # Assume pygame.image.Load works correctly as it is handled by the + # third party SDL_image library. + + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f: + temp_filename = f.name + + pygame.image.save(surf, temp_filename) + jpg_surf = pygame.image.load(temp_filename) + + # Allow for small differences in the restored colors. + def approx(c): + mask = 0xFC + return pygame.Color(c.r & mask, c.g & mask, c.b & mask) + + offset = square_len // 2 + for rect, color in squares: + posn = rect.move((offset, offset)).topleft + self.assertEqual(approx(jpg_surf.get_at(posn)), approx(color)) + + os.remove(temp_filename) + + def testSavePNG32(self): + """see if we can save a png with color values in the proper channels.""" + # Create a PNG file with known colors + reddish_pixel = (215, 0, 0, 255) + greenish_pixel = (0, 225, 0, 255) + bluish_pixel = (0, 0, 235, 255) + greyish_pixel = (115, 125, 135, 145) + + surf = pygame.Surface((1, 4), pygame.SRCALPHA, 32) + surf.set_at((0, 0), reddish_pixel) + surf.set_at((0, 1), greenish_pixel) + surf.set_at((0, 2), bluish_pixel) + surf.set_at((0, 3), greyish_pixel) + + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: + temp_filename = f.name + + pygame.image.save(surf, temp_filename) + + try: + # Read the PNG file and verify that pygame saved it correctly + reader = png.Reader(filename=temp_filename) + width, height, pixels, metadata = reader.asRGBA8() + + # pixels is a generator + self.assertEqual(tuple(next(pixels)), reddish_pixel) + self.assertEqual(tuple(next(pixels)), greenish_pixel) + self.assertEqual(tuple(next(pixels)), bluish_pixel) + self.assertEqual(tuple(next(pixels)), greyish_pixel) + + finally: + # Ensures proper clean up. + if not reader.file.closed: + reader.file.close() + del reader + os.remove(temp_filename) + + def testSavePNG24(self): + """see if we can save a png with color values in the proper channels.""" + # Create a PNG file with known colors + reddish_pixel = (215, 0, 0) + greenish_pixel = (0, 225, 0) + bluish_pixel = (0, 0, 235) + greyish_pixel = (115, 125, 135) + + surf = pygame.Surface((1, 4), 0, 24) + surf.set_at((0, 0), reddish_pixel) + surf.set_at((0, 1), greenish_pixel) + surf.set_at((0, 2), bluish_pixel) + surf.set_at((0, 3), greyish_pixel) + + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: + temp_filename = f.name + + pygame.image.save(surf, temp_filename) + + try: + # Read the PNG file and verify that pygame saved it correctly + reader = png.Reader(filename=temp_filename) + width, height, pixels, metadata = reader.asRGB8() + + # pixels is a generator + self.assertEqual(tuple(next(pixels)), reddish_pixel) + self.assertEqual(tuple(next(pixels)), greenish_pixel) + self.assertEqual(tuple(next(pixels)), bluish_pixel) + self.assertEqual(tuple(next(pixels)), greyish_pixel) + + finally: + # Ensures proper clean up. + if not reader.file.closed: + reader.file.close() + del reader + os.remove(temp_filename) + + def testSavePNG8(self): + """see if we can save an 8 bit png correctly""" + # Create an 8-bit PNG file with known colors + set_pixels = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (170, 146, 170)] + + size = (1, len(set_pixels)) + surf = pygame.Surface(size, depth=8) + for cnt, pix in enumerate(set_pixels): + surf.set_at((0, cnt), pix) + + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: + temp_filename = f.name + + pygame.image.save(surf, temp_filename) + + try: + # Read the PNG file and verify that pygame saved it correctly + reader = png.Reader(filename=temp_filename) + width, height, pixels, _ = reader.asRGB8() + + self.assertEqual(size, (width, height)) + + # pixels is a generator + self.assertEqual(list(map(tuple, pixels)), set_pixels) + + finally: + # Ensures proper clean up. + if not reader.file.closed: + reader.file.close() + del reader + os.remove(temp_filename) + + def testSavePaletteAsPNG8(self): + """see if we can save a png with color values in the proper channels.""" + # Create a PNG file with known colors + pygame.display.init() + + reddish_pixel = (215, 0, 0) + greenish_pixel = (0, 225, 0) + bluish_pixel = (0, 0, 235) + greyish_pixel = (115, 125, 135) + + surf = pygame.Surface((1, 4), 0, 8) + surf.set_palette_at(0, reddish_pixel) + surf.set_palette_at(1, greenish_pixel) + surf.set_palette_at(2, bluish_pixel) + surf.set_palette_at(3, greyish_pixel) + + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: + temp_filename = f.name + + pygame.image.save(surf, temp_filename) + try: + # Read the PNG file and verify that pygame saved it correctly + reader = png.Reader(filename=temp_filename) + reader.read() + palette = reader.palette() + + # pixels is a generator + self.assertEqual(tuple(next(palette)), reddish_pixel) + self.assertEqual(tuple(next(palette)), greenish_pixel) + self.assertEqual(tuple(next(palette)), bluish_pixel) + self.assertEqual(tuple(next(palette)), greyish_pixel) + + finally: + # Ensures proper clean up. + if not reader.file.closed: + reader.file.close() + del reader + os.remove(temp_filename) + + def test_save(self): + s = pygame.Surface((10, 10)) + s.fill((23, 23, 23)) + magic_hex = {} + magic_hex["jpg"] = [0xFF, 0xD8, 0xFF, 0xE0] + magic_hex["png"] = [0x89, 0x50, 0x4E, 0x47] + # magic_hex['tga'] = [0x0, 0x0, 0xa] + magic_hex["bmp"] = [0x42, 0x4D] + + formats = ["jpg", "png", "bmp"] + # uppercase too... JPG + formats = formats + [x.upper() for x in formats] + + for fmt in formats: + try: + temp_filename = f"tmpimg.{fmt}" + pygame.image.save(s, temp_filename) + + # Using 'with' ensures the file is closed even if test fails. + with open(temp_filename, "rb") as handle: + # Test the magic numbers at the start of the file to ensure + # they are saved as the correct file type. + self.assertEqual( + (1, fmt), (test_magic(handle, magic_hex[fmt.lower()]), fmt) + ) + + # load the file to make sure it was saved correctly. + # Note load can load a jpg saved with a .png file name. + s2 = pygame.image.load(temp_filename) + # compare contents, might only work reliably for png... + # but because it's all one color it seems to work with jpg. + self.assertEqual(s2.get_at((0, 0)), s.get_at((0, 0))) + finally: + # clean up the temp file, comment out to leave tmp file after run. + os.remove(temp_filename) + + def test_save_to_fileobject(self): + s = pygame.Surface((1, 1)) + s.fill((23, 23, 23)) + bytes_stream = io.BytesIO() + + pygame.image.save(s, bytes_stream) + bytes_stream.seek(0) + s2 = pygame.image.load(bytes_stream, "tga") + self.assertEqual(s.get_at((0, 0)), s2.get_at((0, 0))) + + def test_save_tga(self): + s = pygame.Surface((1, 1)) + s.fill((23, 23, 23)) + with tempfile.NamedTemporaryFile(suffix=".tga", delete=False) as f: + temp_filename = f.name + + try: + pygame.image.save(s, temp_filename) + s2 = pygame.image.load(temp_filename) + self.assertEqual(s2.get_at((0, 0)), s.get_at((0, 0))) + finally: + # clean up the temp file, even if test fails + os.remove(temp_filename) + + def test_save_pathlib(self): + surf = pygame.Surface((1, 1)) + surf.fill((23, 23, 23)) + with tempfile.NamedTemporaryFile(suffix=".tga", delete=False) as f: + temp_filename = f.name + + path = pathlib.Path(temp_filename) + try: + pygame.image.save(surf, path) + s2 = pygame.image.load(path) + self.assertEqual(s2.get_at((0, 0)), surf.get_at((0, 0))) + finally: + os.remove(temp_filename) + + def test_save__to_fileobject_w_namehint_argument(self): + s = pygame.Surface((10, 10)) + s.fill((23, 23, 23)) + magic_hex = {} + magic_hex["jpg"] = [0xFF, 0xD8, 0xFF, 0xE0] + magic_hex["png"] = [0x89, 0x50, 0x4E, 0x47] + magic_hex["bmp"] = [0x42, 0x4D] + + formats = ["tga", "jpg", "bmp", "png"] + # uppercase too... JPG + formats = formats + [x.upper() for x in formats] + + SDL_Im_version = pygame.image.get_sdl_image_version() + # We assume here that minor version and patch level of SDL_Image + # never goes above 99 + isAtLeastSDL_image_2_0_2 = (SDL_Im_version is not None) and ( + SDL_Im_version[0] * 10000 + SDL_Im_version[1] * 100 + SDL_Im_version[2] + ) >= 20002 + for fmt in formats: + tmp_file, tmp_filename = tempfile.mkstemp(suffix=f".{fmt}") + if not isAtLeastSDL_image_2_0_2 and fmt.lower() == "jpg": + with os.fdopen(tmp_file, "wb") as handle: + with self.assertRaises(pygame.error): + pygame.image.save(s, handle, tmp_filename) + else: + with os.fdopen(tmp_file, "r+b") as handle: + pygame.image.save(s, handle, tmp_filename) + + if fmt.lower() in magic_hex: + # Test the magic numbers at the start of the file to + # ensure they are saved as the correct file type. + handle.seek(0) + self.assertEqual( + (1, fmt), (test_magic(handle, magic_hex[fmt.lower()]), fmt) + ) + # load the file to make sure it was saved correctly. + handle.flush() + handle.seek(0) + s2 = pygame.image.load(handle, tmp_filename) + self.assertEqual(s2.get_at((0, 0)), s.get_at((0, 0))) + os.remove(tmp_filename) + + def test_save_colorkey(self): + """make sure the color key is not changed when saving.""" + s = pygame.Surface((10, 10), pygame.SRCALPHA, 32) + s.fill((23, 23, 23)) + s.set_colorkey((0, 0, 0)) + colorkey1 = s.get_colorkey() + p1 = s.get_at((0, 0)) + + temp_filename = "tmpimg.png" + try: + pygame.image.save(s, temp_filename) + s2 = pygame.image.load(temp_filename) + finally: + os.remove(temp_filename) + + colorkey2 = s.get_colorkey() + # check that the pixel and the colorkey is correct. + self.assertEqual(colorkey1, colorkey2) + self.assertEqual(p1, s2.get_at((0, 0))) + + def test_load_unicode_path(self): + import shutil + + orig = example_path("data/asprite.bmp") + temp = os.path.join(example_path("data"), "你好.bmp") + shutil.copy(orig, temp) + try: + im = pygame.image.load(temp) + finally: + os.remove(temp) + + def _unicode_save(self, temp_file): + im = pygame.Surface((10, 10), 0, 32) + try: + with open(temp_file, "w") as f: + pass + os.remove(temp_file) + except OSError: + raise unittest.SkipTest("the path cannot be opened") + + self.assertFalse(os.path.exists(temp_file)) + + try: + pygame.image.save(im, temp_file) + + self.assertGreater(os.path.getsize(temp_file), 10) + finally: + try: + os.remove(temp_file) + except OSError: + pass + + def test_save_unicode_path(self): + """save unicode object with non-ASCII chars""" + self._unicode_save("你好.bmp") + + def assertPremultipliedAreEqual(self, string1, string2, source_string): + self.assertEqual(len(string1), len(string2)) + block_size = 20 + if string1 != string2: + for block_start in range(0, len(string1), block_size): + block_end = min(block_start + block_size, len(string1)) + block1 = string1[block_start:block_end] + block2 = string2[block_start:block_end] + if block1 != block2: + source_block = source_string[block_start:block_end] + msg = ( + "string difference in %d to %d of %d:\n%s\n%s\nsource:\n%s" + % ( + block_start, + block_end, + len(string1), + binascii.hexlify(block1), + binascii.hexlify(block2), + binascii.hexlify(source_block), + ) + ) + self.fail(msg) + + def test_to_string__premultiplied(self): + """test to make sure we can export a surface to a premultiplied alpha string""" + + def convertRGBAtoPremultiplied(surface_to_modify): + for x in range(surface_to_modify.get_width()): + for y in range(surface_to_modify.get_height()): + color = surface_to_modify.get_at((x, y)) + premult_color = ( + color[0] * color[3] / 255, + color[1] * color[3] / 255, + color[2] * color[3] / 255, + color[3], + ) + surface_to_modify.set_at((x, y), premult_color) + + test_surface = pygame.Surface((256, 256), pygame.SRCALPHA, 32) + for x in range(test_surface.get_width()): + for y in range(test_surface.get_height()): + i = x + y * test_surface.get_width() + test_surface.set_at( + (x, y), ((i * 7) % 256, (i * 13) % 256, (i * 27) % 256, y) + ) + premultiplied_copy = test_surface.copy() + convertRGBAtoPremultiplied(premultiplied_copy) + self.assertPremultipliedAreEqual( + pygame.image.tostring(test_surface, "RGBA_PREMULT"), + pygame.image.tostring(premultiplied_copy, "RGBA"), + pygame.image.tostring(test_surface, "RGBA"), + ) + self.assertPremultipliedAreEqual( + pygame.image.tostring(test_surface, "ARGB_PREMULT"), + pygame.image.tostring(premultiplied_copy, "ARGB"), + pygame.image.tostring(test_surface, "ARGB"), + ) + + no_alpha_surface = pygame.Surface((256, 256), 0, 24) + self.assertRaises( + ValueError, pygame.image.tostring, no_alpha_surface, "RGBA_PREMULT" + ) + + # Custom assert method to check for identical surfaces. + def _assertSurfaceEqual(self, surf_a, surf_b, msg=None): + a_width, a_height = surf_a.get_width(), surf_a.get_height() + + # Check a few things to see if the surfaces are equal. + self.assertEqual(a_width, surf_b.get_width(), msg) + self.assertEqual(a_height, surf_b.get_height(), msg) + self.assertEqual(surf_a.get_size(), surf_b.get_size(), msg) + self.assertEqual(surf_a.get_rect(), surf_b.get_rect(), msg) + self.assertEqual(surf_a.get_colorkey(), surf_b.get_colorkey(), msg) + self.assertEqual(surf_a.get_alpha(), surf_b.get_alpha(), msg) + self.assertEqual(surf_a.get_flags(), surf_b.get_flags(), msg) + self.assertEqual(surf_a.get_bitsize(), surf_b.get_bitsize(), msg) + self.assertEqual(surf_a.get_bytesize(), surf_b.get_bytesize(), msg) + # Anything else? + + # Making the method lookups local for a possible speed up. + surf_a_get_at = surf_a.get_at + surf_b_get_at = surf_b.get_at + for y in range(a_height): + for x in range(a_width): + self.assertEqual( + surf_a_get_at((x, y)), + surf_b_get_at((x, y)), + "%s (pixel: %d, %d)" % (msg, x, y), + ) + + def test_fromstring__and_tostring(self): + """Ensure methods tostring() and fromstring() are symmetric.""" + + import itertools + + fmts = ("RGBA", "ARGB", "BGRA") + fmt_permutations = itertools.permutations(fmts, 2) + fmt_combinations = itertools.combinations(fmts, 2) + + def convert(fmt1, fmt2, str_buf): + pos_fmt1 = {k: v for v, k in enumerate(fmt1)} + pos_fmt2 = {k: v for v, k in enumerate(fmt2)} + byte_buf = array.array("B", str_buf) + num_quads = len(byte_buf) // 4 + for i in range(num_quads): + i4 = i * 4 + R = byte_buf[i4 + pos_fmt1["R"]] + G = byte_buf[i4 + pos_fmt1["G"]] + B = byte_buf[i4 + pos_fmt1["B"]] + A = byte_buf[i4 + pos_fmt1["A"]] + byte_buf[i4 + pos_fmt2["R"]] = R + byte_buf[i4 + pos_fmt2["G"]] = G + byte_buf[i4 + pos_fmt2["B"]] = B + byte_buf[i4 + pos_fmt2["A"]] = A + return tostring(byte_buf) + + #################################################################### + test_surface = pygame.Surface((64, 256), flags=pygame.SRCALPHA, depth=32) + for i in range(256): + for j in range(16): + intensity = j * 16 + 15 + test_surface.set_at((j + 0, i), (intensity, i, i, i)) + test_surface.set_at((j + 16, i), (i, intensity, i, i)) + test_surface.set_at((j + 32, i), (i, i, intensity, i)) + test_surface.set_at((j + 32, i), (i, i, i, intensity)) + + self._assertSurfaceEqual( + test_surface, test_surface, "failing with identical surfaces" + ) + + for pair in fmt_combinations: + fmt1_buf = pygame.image.tostring(test_surface, pair[0]) + fmt1_convert_buf = convert( + pair[1], pair[0], convert(pair[0], pair[1], fmt1_buf) + ) + test_convert_two_way = pygame.image.fromstring( + fmt1_convert_buf, test_surface.get_size(), pair[0] + ) + + self._assertSurfaceEqual( + test_surface, + test_convert_two_way, + f"converting {pair[0]} to {pair[1]} and back is not symmetric", + ) + + for pair in fmt_permutations: + fmt1_buf = pygame.image.tostring(test_surface, pair[0]) + fmt2_convert_buf = convert(pair[0], pair[1], fmt1_buf) + test_convert_one_way = pygame.image.fromstring( + fmt2_convert_buf, test_surface.get_size(), pair[1] + ) + + self._assertSurfaceEqual( + test_surface, + test_convert_one_way, + f"converting {pair[0]} to {pair[1]} failed", + ) + + for fmt in fmts: + test_buf = pygame.image.tostring(test_surface, fmt) + test_to_from_fmt_string = pygame.image.fromstring( + test_buf, test_surface.get_size(), fmt + ) + + self._assertSurfaceEqual( + test_surface, + test_to_from_fmt_string, + "tostring/fromstring functions are not " + f"symmetric with '{fmt}' format", + ) + + def test_tostring_depth_24(self): + test_surface = pygame.Surface((64, 256), depth=24) + for i in range(256): + for j in range(16): + intensity = j * 16 + 15 + test_surface.set_at((j + 0, i), (intensity, i, i, i)) + test_surface.set_at((j + 16, i), (i, intensity, i, i)) + test_surface.set_at((j + 32, i), (i, i, intensity, i)) + test_surface.set_at((j + 32, i), (i, i, i, intensity)) + + fmt = "RGB" + fmt_buf = pygame.image.tostring(test_surface, fmt) + test_to_from_fmt_string = pygame.image.fromstring( + fmt_buf, test_surface.get_size(), fmt + ) + + self._assertSurfaceEqual( + test_surface, + test_to_from_fmt_string, + f'tostring/fromstring functions are not symmetric with "{fmt}" format', + ) + + def test_frombuffer_8bit(self): + """test reading pixel data from a bytes buffer""" + pygame.display.init() + eight_bit_palette_buffer = bytearray( + [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3] + ) + + eight_bit_surf = pygame.image.frombuffer(eight_bit_palette_buffer, (4, 4), "P") + eight_bit_surf.set_palette( + [(255, 10, 20), (255, 255, 255), (0, 0, 0), (50, 200, 20)] + ) + self.assertEqual(eight_bit_surf.get_at((0, 0)), pygame.Color(255, 10, 20)) + self.assertEqual(eight_bit_surf.get_at((1, 1)), pygame.Color(255, 255, 255)) + self.assertEqual(eight_bit_surf.get_at((2, 2)), pygame.Color(0, 0, 0)) + self.assertEqual(eight_bit_surf.get_at((3, 3)), pygame.Color(50, 200, 20)) + + def test_frombuffer_RGB(self): + rgb_buffer = bytearray( + [ + 255, + 10, + 20, + 255, + 10, + 20, + 255, + 10, + 20, + 255, + 10, + 20, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 50, + 200, + 20, + 50, + 200, + 20, + 50, + 200, + 20, + 50, + 200, + 20, + ] + ) + + rgb_surf = pygame.image.frombuffer(rgb_buffer, (4, 4), "RGB") + self.assertEqual(rgb_surf.get_at((0, 0)), pygame.Color(255, 10, 20)) + self.assertEqual(rgb_surf.get_at((1, 1)), pygame.Color(255, 255, 255)) + self.assertEqual(rgb_surf.get_at((2, 2)), pygame.Color(0, 0, 0)) + self.assertEqual(rgb_surf.get_at((3, 3)), pygame.Color(50, 200, 20)) + + def test_frombuffer_BGR(self): + bgr_buffer = bytearray( + [ + 20, + 10, + 255, + 20, + 10, + 255, + 20, + 10, + 255, + 20, + 10, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 20, + 200, + 50, + 20, + 200, + 50, + 20, + 200, + 50, + 20, + 200, + 50, + ] + ) + + bgr_surf = pygame.image.frombuffer(bgr_buffer, (4, 4), "BGR") + self.assertEqual(bgr_surf.get_at((0, 0)), pygame.Color(255, 10, 20)) + self.assertEqual(bgr_surf.get_at((1, 1)), pygame.Color(255, 255, 255)) + self.assertEqual(bgr_surf.get_at((2, 2)), pygame.Color(0, 0, 0)) + self.assertEqual(bgr_surf.get_at((3, 3)), pygame.Color(50, 200, 20)) + + def test_frombuffer_BGRA(self): + bgra_buffer = bytearray( + [ + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 200, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 127, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 79, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + ] + ) + + bgra_surf = pygame.image.frombuffer(bgra_buffer, (4, 4), "BGRA") + self.assertEqual(bgra_surf.get_at((0, 0)), pygame.Color(20, 10, 255, 200)) + self.assertEqual(bgra_surf.get_at((1, 1)), pygame.Color(255, 255, 255, 127)) + self.assertEqual(bgra_surf.get_at((2, 2)), pygame.Color(0, 0, 0, 79)) + self.assertEqual(bgra_surf.get_at((3, 3)), pygame.Color(20, 200, 50, 255)) + + def test_frombuffer_RGBX(self): + rgbx_buffer = bytearray( + [ + 255, + 10, + 20, + 255, + 255, + 10, + 20, + 255, + 255, + 10, + 20, + 255, + 255, + 10, + 20, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 0, + 0, + 0, + 255, + 0, + 0, + 0, + 255, + 0, + 0, + 0, + 255, + 0, + 0, + 0, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + ] + ) + + rgbx_surf = pygame.image.frombuffer(rgbx_buffer, (4, 4), "RGBX") + self.assertEqual(rgbx_surf.get_at((0, 0)), pygame.Color(255, 10, 20, 255)) + self.assertEqual(rgbx_surf.get_at((1, 1)), pygame.Color(255, 255, 255, 255)) + self.assertEqual(rgbx_surf.get_at((2, 2)), pygame.Color(0, 0, 0, 255)) + self.assertEqual(rgbx_surf.get_at((3, 3)), pygame.Color(50, 200, 20, 255)) + + def test_frombuffer_RGBA(self): + rgba_buffer = bytearray( + [ + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 200, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 127, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 79, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + ] + ) + + rgba_surf = pygame.image.frombuffer(rgba_buffer, (4, 4), "RGBA") + self.assertEqual(rgba_surf.get_at((0, 0)), pygame.Color(255, 10, 20, 200)) + self.assertEqual(rgba_surf.get_at((1, 1)), pygame.Color(255, 255, 255, 127)) + self.assertEqual(rgba_surf.get_at((2, 2)), pygame.Color(0, 0, 0, 79)) + self.assertEqual(rgba_surf.get_at((3, 3)), pygame.Color(50, 200, 20, 255)) + + def test_frombuffer_ARGB(self): + argb_buffer = bytearray( + [ + 200, + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 200, + 255, + 10, + 20, + 127, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 127, + 255, + 255, + 255, + 79, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 79, + 0, + 0, + 0, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + 255, + 50, + 200, + 20, + ] + ) + + argb_surf = pygame.image.frombuffer(argb_buffer, (4, 4), "ARGB") + self.assertEqual(argb_surf.get_at((0, 0)), pygame.Color(255, 10, 20, 200)) + self.assertEqual(argb_surf.get_at((1, 1)), pygame.Color(255, 255, 255, 127)) + self.assertEqual(argb_surf.get_at((2, 2)), pygame.Color(0, 0, 0, 79)) + self.assertEqual(argb_surf.get_at((3, 3)), pygame.Color(50, 200, 20, 255)) + + def test_get_extended(self): + # Create a png file and try to load it. If it cannot, get_extended() should return False + raw_image = [] + raw_image.append((200, 200, 200, 255, 100, 100, 100, 255)) + + f_descriptor, f_path = tempfile.mkstemp(suffix=".png") + + with os.fdopen(f_descriptor, "wb") as file: + w = png.Writer(2, 1, alpha=True) + w.write(file, raw_image) + + try: + surf = pygame.image.load(f_path) + loaded = True + except pygame.error: + loaded = False + + self.assertEqual(pygame.image.get_extended(), loaded) + os.remove(f_path) + + def test_get_sdl_image_version(self): + # If get_extended() returns False then get_sdl_image_version() should + # return None + if not pygame.image.get_extended(): + self.assertIsNone(pygame.image.get_sdl_image_version()) + else: + expected_length = 3 + expected_type = tuple + expected_item_type = int + + version = pygame.image.get_sdl_image_version() + + self.assertIsInstance(version, expected_type) + self.assertEqual(len(version), expected_length) + + for item in version: + self.assertIsInstance(item, expected_item_type) + + def test_load_basic(self): + """to see if we can load bmp from files and/or file-like objects in memory""" + + # pygame.image.load(filename): return Surface + + # test loading from a file + s = pygame.image.load_basic(example_path("data/asprite.bmp")) + self.assertEqual(s.get_at((0, 0)), (255, 255, 255, 255)) + + # test loading from io.BufferedReader + f = pygame.pkgdata.getResource("pygame_icon.bmp") + self.assertEqual(f.mode, "rb") + + surf = pygame.image.load_basic(f) + + self.assertEqual(surf.get_at((0, 0)), (5, 4, 5, 255)) + self.assertEqual(surf.get_height(), 32) + self.assertEqual(surf.get_width(), 32) + + f.close() + + def test_load_extended(self): + """can load different format images. + + We test loading the following file types: + bmp, png, jpg, gif (non-animated), pcx, tga (uncompressed), tif, xpm, ppm, pgm + Following file types are tested when using SDL 2 + svg, pnm, webp + All the loaded images are smaller than 32 x 32 pixels. + """ + + filename_expected_color = [ + ("asprite.bmp", (255, 255, 255, 255)), + ("laplacian.png", (10, 10, 70, 255)), + ("red.jpg", (254, 0, 0, 255)), + ("blue.gif", (0, 0, 255, 255)), + ("green.pcx", (0, 255, 0, 255)), + ("yellow.tga", (255, 255, 0, 255)), + ("turquoise.tif", (0, 255, 255, 255)), + ("purple.xpm", (255, 0, 255, 255)), + ("black.ppm", (0, 0, 0, 255)), + ("grey.pgm", (120, 120, 120, 255)), + ("teal.svg", (0, 128, 128, 255)), + ("crimson.pnm", (220, 20, 60, 255)), + ("scarlet.webp", (252, 14, 53, 255)), + ] + + for filename, expected_color in filename_expected_color: + if filename.endswith("svg") and sdl_image_svg_jpeg_save_bug: + # SDL_image 2.0.5 and older has an svg loading bug on big + # endian platforms + continue + + with self.subTest( + f'Test loading a {filename.split(".")[-1]}', + filename="examples/data/" + filename, + expected_color=expected_color, + ): + surf = pygame.image.load_extended(example_path("data/" + filename)) + self.assertEqual(surf.get_at((0, 0)), expected_color) + + def test_load_pathlib(self): + """works loading using a Path argument.""" + path = pathlib.Path(example_path("data/asprite.bmp")) + surf = pygame.image.load_extended(path) + self.assertEqual(surf.get_at((0, 0)), (255, 255, 255, 255)) + + def test_save_extended(self): + surf = pygame.Surface((5, 5)) + surf.fill((23, 23, 23)) + + passing_formats = ["jpg", "png"] + passing_formats += [fmt.upper() for fmt in passing_formats] + + magic_hex = {} + magic_hex["jpg"] = [0xFF, 0xD8, 0xFF, 0xE0] + magic_hex["png"] = [0x89, 0x50, 0x4E, 0x47] + + failing_formats = ["bmp", "tga"] + failing_formats += [fmt.upper() for fmt in failing_formats] + + # check that .jpg and .png save + for fmt in passing_formats: + temp_file_name = f"temp_file.{fmt}" + # save image as .jpg and .png + pygame.image.save_extended(surf, temp_file_name) + with open(temp_file_name, "rb") as file: + # Test the magic numbers at the start of the file to ensure + # they are saved as the correct file type. + self.assertEqual(1, (test_magic(file, magic_hex[fmt.lower()]))) + # load the file to make sure it was saved correctly + loaded_file = pygame.image.load(temp_file_name) + self.assertEqual(loaded_file.get_at((0, 0)), surf.get_at((0, 0))) + # clean up the temp file + os.remove(temp_file_name) + # check that .bmp and .tga do not save + for fmt in failing_formats: + self.assertRaises( + pygame.error, pygame.image.save_extended, surf, f"temp_file.{fmt}" + ) + + def threads_load(self, images): + import pygame.threads + + for i in range(10): + surfs = pygame.threads.tmap(pygame.image.load, images) + for s in surfs: + self.assertIsInstance(s, pygame.Surface) + + def test_load_png_threads(self): + self.threads_load(glob.glob(example_path("data/*.png"))) + + def test_load_jpg_threads(self): + self.threads_load(glob.glob(example_path("data/*.jpg"))) + + def test_load_bmp_threads(self): + self.threads_load(glob.glob(example_path("data/*.bmp"))) + + def test_load_gif_threads(self): + self.threads_load(glob.glob(example_path("data/*.gif"))) + + def test_from_to_bytes_exists(self): + getattr(pygame.image, "frombytes") + getattr(pygame.image, "tobytes") + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/imageext_tags.py b/laplas/abstract_map/pygame/tests/imageext_tags.py new file mode 100644 index 0000000..25cff74 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/imageext_tags.py @@ -0,0 +1,7 @@ +__tags__ = [] + +import pygame +import sys + +if "pygame.imageext" not in sys.modules: + __tags__.extend(("ignore", "subprocess_ignore")) diff --git a/laplas/abstract_map/pygame/tests/imageext_test.py b/laplas/abstract_map/pygame/tests/imageext_test.py new file mode 100644 index 0000000..c5ce759 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/imageext_test.py @@ -0,0 +1,93 @@ +import os +import os.path +import sys +import unittest + +from pygame.tests.test_utils import example_path +import pygame, pygame.image, pygame.pkgdata + + +imageext = sys.modules["pygame.imageext"] + + +class ImageextModuleTest(unittest.TestCase): + # Most of the testing is done indirectly through image_test.py + # This just confirms file path encoding and error handling. + def test_save_non_string_file(self): + im = pygame.Surface((10, 10), 0, 32) + self.assertRaises(TypeError, imageext.save_extended, im, []) + + def test_load_non_string_file(self): + self.assertRaises(TypeError, imageext.load_extended, []) + + @unittest.skip("SDL silently removes invalid characters") + def test_save_bad_filename(self): + im = pygame.Surface((10, 10), 0, 32) + u = "a\x00b\x00c.png" + self.assertRaises(pygame.error, imageext.save_extended, im, u) + + @unittest.skip("SDL silently removes invalid characters") + def test_load_bad_filename(self): + u = "a\x00b\x00c.png" + self.assertRaises(pygame.error, imageext.load_extended, u) + + def test_save_unknown_extension(self): + im = pygame.Surface((10, 10), 0, 32) + s = "foo.bar" + self.assertRaises(pygame.error, imageext.save_extended, im, s) + + def test_load_unknown_extension(self): + s = "foo.bar" + self.assertRaises(FileNotFoundError, imageext.load_extended, s) + + def test_load_unknown_file(self): + s = "nonexistent.png" + self.assertRaises(FileNotFoundError, imageext.load_extended, s) + + def test_load_unicode_path_0(self): + u = example_path("data/alien1.png") + im = imageext.load_extended(u) + + def test_load_unicode_path_1(self): + """non-ASCII unicode""" + import shutil + + orig = example_path("data/alien1.png") + temp = os.path.join(example_path("data"), "你好.png") + shutil.copy(orig, temp) + try: + im = imageext.load_extended(temp) + finally: + os.remove(temp) + + def _unicode_save(self, temp_file): + im = pygame.Surface((10, 10), 0, 32) + try: + with open(temp_file, "w") as f: + pass + os.remove(temp_file) + except OSError: + raise unittest.SkipTest("the path cannot be opened") + + self.assertFalse(os.path.exists(temp_file)) + + try: + imageext.save_extended(im, temp_file) + + self.assertGreater(os.path.getsize(temp_file), 10) + finally: + try: + os.remove(temp_file) + except OSError: + pass + + def test_save_unicode_path_0(self): + """unicode object with ASCII chars""" + self._unicode_save("temp_file.png") + + def test_save_unicode_path_1(self): + self._unicode_save("你好.png") + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/joystick_test.py b/laplas/abstract_map/pygame/tests/joystick_test.py new file mode 100644 index 0000000..47ce3f8 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/joystick_test.py @@ -0,0 +1,166 @@ +import unittest +from pygame.tests.test_utils import question, prompt + +import pygame +import pygame._sdl2.controller + + +class JoystickTypeTest(unittest.TestCase): + def todo_test_Joystick(self): + # __doc__ (as of 2008-08-02) for pygame.joystick.Joystick: + + # pygame.joystick.Joystick(id): return Joystick + # create a new Joystick object + # + # Create a new joystick to access a physical device. The id argument + # must be a value from 0 to pygame.joystick.get_count()-1. + # + # To access most of the Joystick methods, you'll need to init() the + # Joystick. This is separate from making sure the joystick module is + # initialized. When multiple Joysticks objects are created for the + # same physical joystick device (i.e., they have the same ID number), + # the state and values for those Joystick objects will be shared. + # + # The Joystick object allows you to get information about the types of + # controls on a joystick device. Once the device is initialized the + # Pygame event queue will start receiving events about its input. + # + # You can call the Joystick.get_name() and Joystick.get_id() functions + # without initializing the Joystick object. + # + + self.fail() + + +class JoystickModuleTest(unittest.TestCase): + def test_get_init(self): + # Check that get_init() matches what is actually happening + def error_check_get_init(): + try: + pygame.joystick.get_count() + except pygame.error: + return False + return True + + # Start uninitialised + self.assertEqual(pygame.joystick.get_init(), False) + + pygame.joystick.init() + self.assertEqual(pygame.joystick.get_init(), error_check_get_init()) # True + pygame.joystick.quit() + self.assertEqual(pygame.joystick.get_init(), error_check_get_init()) # False + + pygame.joystick.init() + pygame.joystick.init() + self.assertEqual(pygame.joystick.get_init(), error_check_get_init()) # True + pygame.joystick.quit() + self.assertEqual(pygame.joystick.get_init(), error_check_get_init()) # False + + pygame.joystick.quit() + self.assertEqual(pygame.joystick.get_init(), error_check_get_init()) # False + + for i in range(100): + pygame.joystick.init() + self.assertEqual(pygame.joystick.get_init(), error_check_get_init()) # True + pygame.joystick.quit() + self.assertEqual(pygame.joystick.get_init(), error_check_get_init()) # False + + for i in range(100): + pygame.joystick.quit() + self.assertEqual(pygame.joystick.get_init(), error_check_get_init()) # False + + def test_init(self): + """ + This unit test is for joystick.init() + It was written to help reduce maintenance costs + and to help test against changes to the code or + different platforms. + """ + pygame.quit() + # test that pygame.init automatically calls joystick.init + pygame.init() + self.assertEqual(pygame.joystick.get_init(), True) + + # Controller module interferes with the joystick module. + pygame._sdl2.controller.quit() + + # test that get_count doesn't work w/o joystick init + # this is done before and after an init to test + # that init activates the joystick functions + pygame.joystick.quit() + with self.assertRaises(pygame.error): + pygame.joystick.get_count() + + # test explicit call(s) to joystick.init. + # Also test that get_count works once init is called + iterations = 20 + for i in range(iterations): + pygame.joystick.init() + self.assertEqual(pygame.joystick.get_init(), True) + self.assertIsNotNone(pygame.joystick.get_count()) + + def test_quit(self): + """Test if joystick.quit works.""" + + pygame.joystick.init() + + self.assertIsNotNone(pygame.joystick.get_count()) # Is not None before quit + + pygame.joystick.quit() + + with self.assertRaises(pygame.error): # Raises error if quit worked + pygame.joystick.get_count() + + def test_get_count(self): + # Test that get_count correctly returns a non-negative number of joysticks + pygame.joystick.init() + + try: + count = pygame.joystick.get_count() + self.assertGreaterEqual( + count, 0, ("joystick.get_count() must " "return a value >= 0") + ) + finally: + pygame.joystick.quit() + + +class JoystickInteractiveTest(unittest.TestCase): + __tags__ = ["interactive"] + + def test_get_count_interactive(self): + # Test get_count correctly identifies number of connected joysticks + prompt( + "Please connect any joysticks/controllers now before starting the " + "joystick.get_count() test." + ) + + pygame.joystick.init() + # pygame.joystick.get_count(): return count + # number of joysticks on the system, 0 means no joysticks connected + count = pygame.joystick.get_count() + + response = question( + "NOTE: Having Steam open may add an extra virtual controller for " + "each joystick/controller physically plugged in.\n" + f"joystick.get_count() thinks there is [{count}] joystick(s)/controller(s)" + "connected to this system. Is this correct?" + ) + + self.assertTrue(response) + + # When you create Joystick objects using Joystick(id), you pass an + # integer that must be lower than this count. + # Test Joystick(id) for each connected joystick + if count != 0: + for x in range(count): + pygame.joystick.Joystick(x) + with self.assertRaises(pygame.error): + pygame.joystick.Joystick(count) + + pygame.joystick.quit() + + +################################################################################ + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/key_test.py b/laplas/abstract_map/pygame/tests/key_test.py new file mode 100644 index 0000000..1899c73 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/key_test.py @@ -0,0 +1,306 @@ +import os +import time +import unittest + +import pygame +import pygame.key + +# keys that are not tested for const-name match +SKIPPED_KEYS = {"K_UNKNOWN"} + +# This is the expected compat output +KEY_NAME_COMPAT = { + "K_0": "0", + "K_1": "1", + "K_2": "2", + "K_3": "3", + "K_4": "4", + "K_5": "5", + "K_6": "6", + "K_7": "7", + "K_8": "8", + "K_9": "9", + "K_AC_BACK": "AC Back", + "K_AMPERSAND": "&", + "K_ASTERISK": "*", + "K_AT": "@", + "K_BACKQUOTE": "`", + "K_BACKSLASH": "\\", + "K_BACKSPACE": "backspace", + "K_BREAK": "break", + "K_CAPSLOCK": "caps lock", + "K_CARET": "^", + "K_CLEAR": "clear", + "K_COLON": ":", + "K_COMMA": ",", + "K_CURRENCYSUBUNIT": "CurrencySubUnit", + "K_CURRENCYUNIT": "euro", + "K_DELETE": "delete", + "K_DOLLAR": "$", + "K_DOWN": "down", + "K_END": "end", + "K_EQUALS": "=", + "K_ESCAPE": "escape", + "K_EURO": "euro", + "K_EXCLAIM": "!", + "K_F1": "f1", + "K_F10": "f10", + "K_F11": "f11", + "K_F12": "f12", + "K_F13": "f13", + "K_F14": "f14", + "K_F15": "f15", + "K_F2": "f2", + "K_F3": "f3", + "K_F4": "f4", + "K_F5": "f5", + "K_F6": "f6", + "K_F7": "f7", + "K_F8": "f8", + "K_F9": "f9", + "K_GREATER": ">", + "K_HASH": "#", + "K_HELP": "help", + "K_HOME": "home", + "K_INSERT": "insert", + "K_KP0": "[0]", + "K_KP1": "[1]", + "K_KP2": "[2]", + "K_KP3": "[3]", + "K_KP4": "[4]", + "K_KP5": "[5]", + "K_KP6": "[6]", + "K_KP7": "[7]", + "K_KP8": "[8]", + "K_KP9": "[9]", + "K_KP_0": "[0]", + "K_KP_1": "[1]", + "K_KP_2": "[2]", + "K_KP_3": "[3]", + "K_KP_4": "[4]", + "K_KP_5": "[5]", + "K_KP_6": "[6]", + "K_KP_7": "[7]", + "K_KP_8": "[8]", + "K_KP_9": "[9]", + "K_KP_DIVIDE": "[/]", + "K_KP_ENTER": "enter", + "K_KP_EQUALS": "equals", + "K_KP_MINUS": "[-]", + "K_KP_MULTIPLY": "[*]", + "K_KP_PERIOD": "[.]", + "K_KP_PLUS": "[+]", + "K_LALT": "left alt", + "K_LCTRL": "left ctrl", + "K_LEFT": "left", + "K_LEFTBRACKET": "[", + "K_LEFTPAREN": "(", + "K_LESS": "<", + "K_LGUI": "left meta", + "K_LMETA": "left meta", + "K_LSHIFT": "left shift", + "K_LSUPER": "left meta", + "K_MENU": "menu", + "K_MINUS": "-", + "K_MODE": "alt gr", + "K_NUMLOCK": "numlock", + "K_NUMLOCKCLEAR": "numlock", + "K_PAGEDOWN": "page down", + "K_PAGEUP": "page up", + "K_PAUSE": "break", + "K_PERCENT": "%", + "K_PERIOD": ".", + "K_PLUS": "+", + "K_POWER": "power", + "K_PRINT": "print screen", + "K_PRINTSCREEN": "print screen", + "K_QUESTION": "?", + "K_QUOTE": "'", + "K_QUOTEDBL": '"', + "K_RALT": "right alt", + "K_RCTRL": "right ctrl", + "K_RETURN": "return", + "K_RGUI": "right meta", + "K_RIGHT": "right", + "K_RIGHTBRACKET": "]", + "K_RIGHTPAREN": ")", + "K_RMETA": "right meta", + "K_RSHIFT": "right shift", + "K_RSUPER": "right meta", + "K_SCROLLLOCK": "scroll lock", + "K_SCROLLOCK": "scroll lock", + "K_SEMICOLON": ";", + "K_SLASH": "/", + "K_SPACE": "space", + "K_SYSREQ": "sys req", + "K_TAB": "tab", + "K_UNDERSCORE": "_", + "K_UP": "up", + "K_a": "a", + "K_b": "b", + "K_c": "c", + "K_d": "d", + "K_e": "e", + "K_f": "f", + "K_g": "g", + "K_h": "h", + "K_i": "i", + "K_j": "j", + "K_k": "k", + "K_l": "l", + "K_m": "m", + "K_n": "n", + "K_o": "o", + "K_p": "p", + "K_q": "q", + "K_r": "r", + "K_s": "s", + "K_t": "t", + "K_u": "u", + "K_v": "v", + "K_w": "w", + "K_x": "x", + "K_y": "y", + "K_z": "z", +} + + +class KeyModuleTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + pygame.init() + + @classmethod + def tearDownClass(cls): + pygame.quit() + + def setUp(self): + # This makes sure pygame is always initialized before each test (in + # case a test calls pygame.quit()). + if not pygame.get_init(): + pygame.init() + if not pygame.display.get_init(): + pygame.display.init() + + def test_import(self): + """does it import?""" + import pygame.key + + # fixme: test_get_focused failing systematically in some linux + # fixme: test_get_focused failing on SDL 2.0.18 on Windows + @unittest.skip("flaky test, and broken on 2.0.18 windows") + def test_get_focused(self): + # This test fails in SDL2 in some linux + # This test was skipped in SDL1. + focused = pygame.key.get_focused() + self.assertFalse(focused) # No window to focus + self.assertIsInstance(focused, int) + # Dummy video driver never gets keyboard focus. + if os.environ.get("SDL_VIDEODRIVER") != "dummy": + # Positive test, fullscreen with events grabbed + display_sizes = pygame.display.list_modes() + if display_sizes == -1: + display_sizes = [(500, 500)] + pygame.display.set_mode(size=display_sizes[-1], flags=pygame.FULLSCREEN) + pygame.event.set_grab(True) + # Pump event queue to get window focus on macos + pygame.event.pump() + focused = pygame.key.get_focused() + self.assertIsInstance(focused, int) + self.assertTrue(focused) + # Now test negative, iconify takes away focus + pygame.event.clear() + # TODO: iconify test fails in windows + if os.name != "nt": + pygame.display.iconify() + # Apparent need to pump event queue in order to make sure iconify + # happens. See display_test.py's test_get_active_iconify + for _ in range(50): + time.sleep(0.01) + pygame.event.pump() + self.assertFalse(pygame.key.get_focused()) + # Test if focus is returned when iconify is gone + pygame.display.set_mode(size=display_sizes[-1], flags=pygame.FULLSCREEN) + for i in range(50): + time.sleep(0.01) + pygame.event.pump() + self.assertTrue(pygame.key.get_focused()) + # Test if a quit display raises an error: + pygame.display.quit() + with self.assertRaises(pygame.error) as cm: + pygame.key.get_focused() + + def test_get_pressed(self): + states = pygame.key.get_pressed() + self.assertEqual(states[pygame.K_RIGHT], 0) + + # def test_get_pressed_not_iter(self): + # states = pygame.key.get_pressed() + # with self.assertRaises(TypeError): + # next(states) + # with self.assertRaises(TypeError): + # for k in states: + # pass + + def test_name_and_key_code(self): + for const_name in dir(pygame): + if not const_name.startswith("K_") or const_name in SKIPPED_KEYS: + continue + + try: + expected_str_name = KEY_NAME_COMPAT[const_name] + except KeyError: + self.fail( + "If you are seeing this error in a test run, you probably added a " + "new pygame key constant, but forgot to update key_test unitests" + ) + + const_val = getattr(pygame, const_name) + + # with these tests below, we also make sure that key.name and key.key_code + # can work together and handle each others outputs + + # test positional args + self.assertEqual(pygame.key.name(const_val), expected_str_name) + # test kwarg + self.assertEqual(pygame.key.name(key=const_val), expected_str_name) + + # test positional args + self.assertEqual(pygame.key.key_code(expected_str_name), const_val) + # test kwarg + self.assertEqual(pygame.key.key_code(name=expected_str_name), const_val) + + alt_name = pygame.key.name(const_val, use_compat=False) + self.assertIsInstance(alt_name, str) + + # This is a test for an implementation detail of name with use_compat=False + # If this test breaks in the future for any key, it is safe to put skips on + # failing keys (the implementation detail is documented as being unreliable) + self.assertEqual(pygame.key.key_code(alt_name), const_val) + + self.assertRaises(TypeError, pygame.key.name, "fizzbuzz") + self.assertRaises(TypeError, pygame.key.key_code, pygame.K_a) + + self.assertRaises(ValueError, pygame.key.key_code, "fizzbuzz") + + def test_set_and_get_mods(self): + pygame.key.set_mods(pygame.KMOD_CTRL) + self.assertEqual(pygame.key.get_mods(), pygame.KMOD_CTRL) + + pygame.key.set_mods(pygame.KMOD_ALT) + self.assertEqual(pygame.key.get_mods(), pygame.KMOD_ALT) + pygame.key.set_mods(pygame.KMOD_CTRL | pygame.KMOD_ALT) + self.assertEqual(pygame.key.get_mods(), pygame.KMOD_CTRL | pygame.KMOD_ALT) + + def test_set_and_get_repeat(self): + self.assertEqual(pygame.key.get_repeat(), (0, 0)) + + pygame.key.set_repeat(10, 15) + self.assertEqual(pygame.key.get_repeat(), (10, 15)) + + pygame.key.set_repeat() + self.assertEqual(pygame.key.get_repeat(), (0, 0)) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/locals_test.py b/laplas/abstract_map/pygame/tests/locals_test.py new file mode 100644 index 0000000..973c46d --- /dev/null +++ b/laplas/abstract_map/pygame/tests/locals_test.py @@ -0,0 +1,17 @@ +import unittest + +import pygame.constants +import pygame.locals + + +class LocalsTest(unittest.TestCase): + def test_locals_has_all_constants(self): + constants_set = set(pygame.constants.__all__) + locals_set = set(pygame.locals.__all__) + + # locals should have everything that constants has + self.assertEqual(constants_set - locals_set, set()) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/mask_test.py b/laplas/abstract_map/pygame/tests/mask_test.py new file mode 100644 index 0000000..bd7daf5 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/mask_test.py @@ -0,0 +1,6441 @@ +from collections import OrderedDict +import copy +import platform +import random +import unittest +import sys + +import pygame +from pygame.locals import * +from pygame.math import Vector2 + + +IS_PYPY = "PyPy" == platform.python_implementation() + + +def random_mask(size=(100, 100)): + """random_mask(size=(100,100)): return Mask + Create a mask of the given size, with roughly half the bits set at random.""" + m = pygame.Mask(size) + for i in range(size[0] * size[1] // 2): + x, y = random.randint(0, size[0] - 1), random.randint(0, size[1] - 1) + m.set_at((x, y)) + return m + + +def maskFromSurface(surface, threshold=127): + mask = pygame.Mask(surface.get_size()) + key = surface.get_colorkey() + if key: + for y in range(surface.get_height()): + for x in range(surface.get_width()): + if surface.get_at((x + 0.1, y + 0.1)) != key: + mask.set_at((x, y), 1) + else: + for y in range(surface.get_height()): + for x in range(surface.get_width()): + if surface.get_at((x, y))[3] > threshold: + mask.set_at((x, y), 1) + return mask + + +def create_bounding_rect(points): + """Creates a bounding rect from the given points.""" + xmin = xmax = points[0][0] + ymin = ymax = points[0][1] + + for x, y in points[1:]: + xmin = min(x, xmin) + xmax = max(x, xmax) + ymin = min(y, ymin) + ymax = max(y, ymax) + + return pygame.Rect((xmin, ymin), (xmax - xmin + 1, ymax - ymin + 1)) + + +def zero_size_pairs(width, height): + """Creates a generator which yields pairs of sizes. + + For each pair of sizes at least one of the sizes will have a 0 in it. + """ + sizes = ((width, height), (width, 0), (0, height), (0, 0)) + + return ((a, b) for a in sizes for b in sizes if 0 in a or 0 in b) + + +def corners(mask): + """Returns a tuple with the corner positions of the given mask. + + Clockwise from the top left corner. + """ + width, height = mask.get_size() + return ((0, 0), (width - 1, 0), (width - 1, height - 1), (0, height - 1)) + + +def off_corners(rect): + """Returns a tuple with the positions off of the corners of the given rect. + + Clockwise from the top left corner. + """ + return ( + (rect.left - 1, rect.top), + (rect.left - 1, rect.top - 1), + (rect.left, rect.top - 1), + (rect.right - 1, rect.top - 1), + (rect.right, rect.top - 1), + (rect.right, rect.top), + (rect.right, rect.bottom - 1), + (rect.right, rect.bottom), + (rect.right - 1, rect.bottom), + (rect.left, rect.bottom), + (rect.left - 1, rect.bottom), + (rect.left - 1, rect.bottom - 1), + ) + + +def assertSurfaceFilled(testcase, surface, expected_color, area_rect=None): + """Checks to see if the given surface is filled with the given color. + + If an area_rect is provided, only check that area of the surface. + """ + if area_rect is None: + x_range = range(surface.get_width()) + y_range = range(surface.get_height()) + else: + area_rect.normalize() + area_rect = area_rect.clip(surface.get_rect()) + x_range = range(area_rect.left, area_rect.right) + y_range = range(area_rect.top, area_rect.bottom) + + surface.lock() # Lock for possible speed up. + for pos in ((x, y) for y in y_range for x in x_range): + testcase.assertEqual(surface.get_at(pos), expected_color, pos) + surface.unlock() + + +def assertSurfaceFilledIgnoreArea(testcase, surface, expected_color, ignore_rect): + """Checks if the surface is filled with the given color. The + ignore_rect area is not checked. + """ + x_range = range(surface.get_width()) + y_range = range(surface.get_height()) + ignore_rect.normalize() + + surface.lock() # Lock for possible speed up. + for pos in ((x, y) for y in y_range for x in x_range): + if not ignore_rect.collidepoint(pos): + testcase.assertEqual(surface.get_at(pos), expected_color, pos) + surface.unlock() + + +def assertMaskEqual(testcase, m1, m2, msg=None): + """Checks to see if the 2 given masks are equal.""" + m1_count = m1.count() + + testcase.assertEqual(m1.get_size(), m2.get_size(), msg=msg) + testcase.assertEqual(m1_count, m2.count(), msg=msg) + testcase.assertEqual(m1_count, m1.overlap_area(m2, (0, 0)), msg=msg) + + # This can be used to help debug exact locations. + ##for i in range(m1.get_size()[0]): + ## for j in range(m1.get_size()[1]): + ## testcase.assertEqual(m1.get_at((i, j)), m2.get_at((i, j))) + + +# @unittest.skipIf(IS_PYPY, "pypy has lots of mask failures") # TODO +class MaskTypeTest(unittest.TestCase): + ORIGIN_OFFSETS = ( + (0, 0), + (0, 1), + (1, 1), + (1, 0), + (1, -1), + (0, -1), + (-1, -1), + (-1, 0), + (-1, 1), + ) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_mask(self): + """Ensure masks are created correctly without fill parameter.""" + expected_count = 0 + expected_size = (11, 23) + + mask1 = pygame.mask.Mask(expected_size) + mask2 = pygame.mask.Mask(size=expected_size) + + self.assertIsInstance(mask1, pygame.mask.Mask) + self.assertEqual(mask1.count(), expected_count) + self.assertEqual(mask1.get_size(), expected_size) + + self.assertIsInstance(mask2, pygame.mask.Mask) + self.assertEqual(mask2.count(), expected_count) + self.assertEqual(mask2.get_size(), expected_size) + + def test_mask__negative_size(self): + """Ensure the mask constructor handles negative sizes correctly.""" + for size in ((1, -1), (-1, 1), (-1, -1)): + with self.assertRaises(ValueError): + mask = pygame.Mask(size) + + def test_mask__fill_kwarg(self): + """Ensure masks are created correctly using the fill keyword.""" + width, height = 37, 47 + expected_size = (width, height) + fill_counts = {True: width * height, False: 0} + + for fill, expected_count in fill_counts.items(): + msg = f"fill={fill}" + + mask = pygame.mask.Mask(expected_size, fill=fill) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.count(), expected_count, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_mask__fill_kwarg_bit_boundaries(self): + """Ensures masks are created correctly using the fill keyword + over a range of sizes. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for height in range(1, 4): + for width in range(1, 66): + expected_count = width * height + expected_size = (width, height) + msg = f"size={expected_size}" + + mask = pygame.mask.Mask(expected_size, fill=True) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.count(), expected_count, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + + def test_mask__fill_arg(self): + """Ensure masks are created correctly using a fill arg.""" + width, height = 59, 71 + expected_size = (width, height) + fill_counts = {True: width * height, False: 0} + + for fill, expected_count in fill_counts.items(): + msg = f"fill={fill}" + + mask = pygame.mask.Mask(expected_size, fill) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.count(), expected_count, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + + def test_mask__size_kwarg(self): + """Ensure masks are created correctly using the size keyword.""" + width, height = 73, 83 + expected_size = (width, height) + fill_counts = {True: width * height, False: 0} + + for fill, expected_count in fill_counts.items(): + msg = f"fill={fill}" + + mask1 = pygame.mask.Mask(fill=fill, size=expected_size) + mask2 = pygame.mask.Mask(size=expected_size, fill=fill) + + self.assertIsInstance(mask1, pygame.mask.Mask, msg) + self.assertIsInstance(mask2, pygame.mask.Mask, msg) + self.assertEqual(mask1.count(), expected_count, msg) + self.assertEqual(mask2.count(), expected_count, msg) + self.assertEqual(mask1.get_size(), expected_size, msg) + self.assertEqual(mask2.get_size(), expected_size, msg) + + def test_copy(self): + """Ensures copy works correctly with some bits set and unset.""" + # Test different widths and heights. + for width in (31, 32, 33, 63, 64, 65): + for height in (31, 32, 33, 63, 64, 65): + mask = pygame.mask.Mask((width, height)) + + # Create a checkerboard pattern of set/unset bits. + for x in range(width): + for y in range(x & 1, height, 2): + mask.set_at((x, y)) + + # Test both the copy() and __copy__() methods. + for mask_copy in (mask.copy(), copy.copy(mask)): + self.assertIsInstance(mask_copy, pygame.mask.Mask) + self.assertIsNot(mask_copy, mask) + assertMaskEqual(self, mask_copy, mask) + + def test_copy__full(self): + """Ensures copy works correctly on a filled masked.""" + # Test different widths and heights. + for width in (31, 32, 33, 63, 64, 65): + for height in (31, 32, 33, 63, 64, 65): + mask = pygame.mask.Mask((width, height), fill=True) + + # Test both the copy() and __copy__() methods. + for mask_copy in (mask.copy(), copy.copy(mask)): + self.assertIsInstance(mask_copy, pygame.mask.Mask) + self.assertIsNot(mask_copy, mask) + assertMaskEqual(self, mask_copy, mask) + + def test_copy__empty(self): + """Ensures copy works correctly on an empty mask.""" + for width in (31, 32, 33, 63, 64, 65): + for height in (31, 32, 33, 63, 64, 65): + mask = pygame.mask.Mask((width, height)) + + # Test both the copy() and __copy__() methods. + for mask_copy in (mask.copy(), copy.copy(mask)): + self.assertIsInstance(mask_copy, pygame.mask.Mask) + self.assertIsNot(mask_copy, mask) + assertMaskEqual(self, mask_copy, mask) + + def test_copy__independent(self): + """Ensures copy makes an independent copy of the mask.""" + mask_set_pos = (64, 1) + mask_copy_set_pos = (64, 2) + mask = pygame.mask.Mask((65, 3)) + + # Test both the copy() and __copy__() methods. + mask_copies = (mask.copy(), copy.copy(mask)) + mask.set_at(mask_set_pos) + + for mask_copy in mask_copies: + mask_copy.set_at(mask_copy_set_pos) + + self.assertIsNot(mask_copy, mask) + self.assertNotEqual( + mask_copy.get_at(mask_set_pos), mask.get_at(mask_set_pos) + ) + self.assertNotEqual( + mask_copy.get_at(mask_copy_set_pos), mask.get_at(mask_copy_set_pos) + ) + + def test_get_size(self): + """Ensure a mask's size is correctly retrieved.""" + expected_size = (93, 101) + mask = pygame.mask.Mask(expected_size) + + self.assertEqual(mask.get_size(), expected_size) + + def test_get_rect(self): + """Ensures get_rect works correctly.""" + expected_rect = pygame.Rect((0, 0), (11, 13)) + + # Test on full and empty masks. + for fill in (True, False): + mask = pygame.mask.Mask(expected_rect.size, fill=fill) + + rect = mask.get_rect() + + self.assertEqual(rect, expected_rect) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_get_rect__one_kwarg(self): + """Ensures get_rect supports a single rect attribute kwarg. + + Tests all the rect attributes. + """ + # Rect attributes that take a single value. + RECT_SINGLE_VALUE_ATTRIBUTES = ( + "x", + "y", + "top", + "left", + "bottom", + "right", + "centerx", + "centery", + "width", + "height", + "w", + "h", + ) + + # Rect attributes that take 2 values. + RECT_DOUBLE_VALUE_ATTRIBUTES = ( + "topleft", + "bottomleft", + "topright", + "bottomright", + "midtop", + "midleft", + "midbottom", + "midright", + "center", + "size", + ) + + # Testing ints/floats and tuples/lists/Vector2s. + # {attribute_names : attribute_values} + rect_attributes = { + RECT_SINGLE_VALUE_ATTRIBUTES: (3, 5.1), + RECT_DOUBLE_VALUE_ATTRIBUTES: ((1, 2.2), [2.3, 3], Vector2(0, 1)), + } + + size = (7, 3) + mask = pygame.mask.Mask(size) + + for attributes, values in rect_attributes.items(): + for attribute in attributes: + for value in values: + expected_rect = pygame.Rect((0, 0), size) + setattr(expected_rect, attribute, value) + + rect = mask.get_rect(**{attribute: value}) + + self.assertEqual(rect, expected_rect) + + def test_get_rect__multiple_kwargs(self): + """Ensures get_rect supports multiple rect attribute kwargs.""" + mask = pygame.mask.Mask((5, 4)) + expected_rect = pygame.Rect((0, 0), (0, 0)) + kwargs = {"x": 7.1, "top": -1, "size": Vector2(2, 3.2)} + + for attrib, value in kwargs.items(): + setattr(expected_rect, attrib, value) + + rect = mask.get_rect(**kwargs) + + self.assertEqual(rect, expected_rect) + + def test_get_rect__no_arg_support(self): + """Ensures get_rect only supports kwargs.""" + mask = pygame.mask.Mask((4, 5)) + + with self.assertRaises(TypeError): + rect = mask.get_rect(3) + + with self.assertRaises(TypeError): + rect = mask.get_rect((1, 2)) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_get_rect__invalid_kwarg_name(self): + """Ensures get_rect detects invalid kwargs.""" + mask = pygame.mask.Mask((1, 2)) + + with self.assertRaises(AttributeError): + rect = mask.get_rect(righte=11) + + with self.assertRaises(AttributeError): + rect = mask.get_rect(toplef=(1, 1)) + + with self.assertRaises(AttributeError): + rect = mask.get_rect(move=(3, 2)) + + def test_get_rect__invalid_kwarg_format(self): + """Ensures get_rect detects invalid kwarg formats.""" + mask = pygame.mask.Mask((3, 11)) + + with self.assertRaises(TypeError): + rect = mask.get_rect(right="1") # Wrong type. + + with self.assertRaises(TypeError): + rect = mask.get_rect(bottom=(1,)) # Wrong type. + + with self.assertRaises(TypeError): + rect = mask.get_rect(centerx=(1, 1)) # Wrong type. + + with self.assertRaises(TypeError): + rect = mask.get_rect(midleft=(1, "1")) # Wrong type. + + with self.assertRaises(TypeError): + rect = mask.get_rect(topright=(1,)) # Too few. + + with self.assertRaises(TypeError): + rect = mask.get_rect(bottomleft=(1, 2, 3)) # Too many. + + with self.assertRaises(TypeError): + rect = mask.get_rect(midbottom=1) # Wrong type. + + def test_get_at(self): + """Ensure individual mask bits are correctly retrieved.""" + width, height = 5, 7 + mask0 = pygame.mask.Mask((width, height)) + mask1 = pygame.mask.Mask((width, height), fill=True) + mask0_expected_bit = 0 + mask1_expected_bit = 1 + pos = (width - 1, height - 1) + + # Check twice to make sure bits aren't toggled. + self.assertEqual(mask0.get_at(pos), mask0_expected_bit) + self.assertEqual(mask0.get_at(pos=pos), mask0_expected_bit) + self.assertEqual(mask1.get_at(Vector2(pos)), mask1_expected_bit) + self.assertEqual(mask1.get_at(pos=Vector2(pos)), mask1_expected_bit) + + def test_get_at__out_of_bounds(self): + """Ensure get_at() checks bounds.""" + width, height = 11, 3 + mask = pygame.mask.Mask((width, height)) + + with self.assertRaises(IndexError): + mask.get_at((width, 0)) + + with self.assertRaises(IndexError): + mask.get_at((0, height)) + + with self.assertRaises(IndexError): + mask.get_at((-1, 0)) + + with self.assertRaises(IndexError): + mask.get_at((0, -1)) + + def test_set_at(self): + """Ensure individual mask bits are set to 1.""" + width, height = 13, 17 + mask0 = pygame.mask.Mask((width, height)) + mask1 = pygame.mask.Mask((width, height), fill=True) + mask0_expected_count = 1 + mask1_expected_count = mask1.count() + expected_bit = 1 + pos = (width - 1, height - 1) + + mask0.set_at(pos, expected_bit) # set 0 to 1 + mask1.set_at(pos=Vector2(pos), value=expected_bit) # set 1 to 1 + + self.assertEqual(mask0.get_at(pos), expected_bit) + self.assertEqual(mask0.count(), mask0_expected_count) + self.assertEqual(mask1.get_at(pos), expected_bit) + self.assertEqual(mask1.count(), mask1_expected_count) + + def test_set_at__to_0(self): + """Ensure individual mask bits are set to 0.""" + width, height = 11, 7 + mask0 = pygame.mask.Mask((width, height)) + mask1 = pygame.mask.Mask((width, height), fill=True) + mask0_expected_count = 0 + mask1_expected_count = mask1.count() - 1 + expected_bit = 0 + pos = (width - 1, height - 1) + + mask0.set_at(pos, expected_bit) # set 0 to 0 + mask1.set_at(pos, expected_bit) # set 1 to 0 + + self.assertEqual(mask0.get_at(pos), expected_bit) + self.assertEqual(mask0.count(), mask0_expected_count) + self.assertEqual(mask1.get_at(pos), expected_bit) + self.assertEqual(mask1.count(), mask1_expected_count) + + def test_set_at__default_value(self): + """Ensure individual mask bits are set using the default value.""" + width, height = 3, 21 + mask0 = pygame.mask.Mask((width, height)) + mask1 = pygame.mask.Mask((width, height), fill=True) + mask0_expected_count = 1 + mask1_expected_count = mask1.count() + expected_bit = 1 + pos = (width - 1, height - 1) + + mask0.set_at(pos) # set 0 to 1 + mask1.set_at(pos) # set 1 to 1 + + self.assertEqual(mask0.get_at(pos), expected_bit) + self.assertEqual(mask0.count(), mask0_expected_count) + self.assertEqual(mask1.get_at(pos), expected_bit) + self.assertEqual(mask1.count(), mask1_expected_count) + + def test_set_at__out_of_bounds(self): + """Ensure set_at() checks bounds.""" + width, height = 11, 3 + mask = pygame.mask.Mask((width, height)) + + with self.assertRaises(IndexError): + mask.set_at((width, 0)) + + with self.assertRaises(IndexError): + mask.set_at((0, height)) + + with self.assertRaises(IndexError): + mask.set_at((-1, 0)) + + with self.assertRaises(IndexError): + mask.set_at((0, -1)) + + def test_overlap(self): + """Ensure the overlap intersection is correctly calculated. + + Testing the different combinations of full/empty masks: + (mask1-filled) 1 overlap 1 (mask2-filled) + (mask1-empty) 0 overlap 1 (mask2-filled) + (mask1-filled) 1 overlap 0 (mask2-empty) + (mask1-empty) 0 overlap 0 (mask2-empty) + """ + expected_size = (4, 4) + offset = (0, 0) + expected_default = None + expected_overlaps = {(True, True): offset} + + for fill2 in (True, False): + mask2 = pygame.mask.Mask(expected_size, fill=fill2) + mask2_count = mask2.count() + + for fill1 in (True, False): + key = (fill1, fill2) + msg = f"key={key}" + mask1 = pygame.mask.Mask(expected_size, fill=fill1) + mask1_count = mask1.count() + expected_pos = expected_overlaps.get(key, expected_default) + + overlap_pos = mask1.overlap(mask2, offset) + + self.assertEqual(overlap_pos, expected_pos, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), expected_size, msg) + self.assertEqual(mask2.get_size(), expected_size, msg) + + def test_overlap__offset(self): + """Ensure an offset overlap intersection is correctly calculated.""" + mask1 = pygame.mask.Mask((65, 3), fill=True) + mask2 = pygame.mask.Mask((66, 4), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + + for offset in self.ORIGIN_OFFSETS: + msg = f"offset={offset}" + expected_pos = (max(offset[0], 0), max(offset[1], 0)) + + overlap_pos = mask1.overlap(other=mask2, offset=offset) + + self.assertEqual(overlap_pos, expected_pos, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + def test_overlap__offset_with_unset_bits(self): + """Ensure an offset overlap intersection is correctly calculated + when (0, 0) bits not set.""" + mask1 = pygame.mask.Mask((65, 3), fill=True) + mask2 = pygame.mask.Mask((66, 4), fill=True) + unset_pos = (0, 0) + mask1.set_at(unset_pos, 0) + mask2.set_at(unset_pos, 0) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + + for offset in self.ORIGIN_OFFSETS: + msg = f"offset={offset}" + x, y = offset + expected_y = max(y, 0) + if 0 == y: + expected_x = max(x + 1, 1) + elif 0 < y: + expected_x = max(x + 1, 0) + else: + expected_x = max(x, 1) + + overlap_pos = mask1.overlap(mask2, Vector2(offset)) + + self.assertEqual(overlap_pos, (expected_x, expected_y), msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + self.assertEqual(mask1.get_at(unset_pos), 0, msg) + self.assertEqual(mask2.get_at(unset_pos), 0, msg) + + def test_overlap__no_overlap(self): + """Ensure an offset overlap intersection is correctly calculated + when there is no overlap.""" + mask1 = pygame.mask.Mask((65, 3), fill=True) + mask1_count = mask1.count() + mask1_size = mask1.get_size() + + mask2_w, mask2_h = 67, 5 + mask2_size = (mask2_w, mask2_h) + mask2 = pygame.mask.Mask(mask2_size) + set_pos = (mask2_w - 1, mask2_h - 1) + mask2.set_at(set_pos) + mask2_count = 1 + + for offset in self.ORIGIN_OFFSETS: + msg = f"offset={offset}" + + overlap_pos = mask1.overlap(mask2, offset) + + self.assertIsNone(overlap_pos, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + self.assertEqual(mask2.get_at(set_pos), 1, msg) + + def test_overlap__offset_boundary(self): + """Ensures overlap handles offsets and boundaries correctly.""" + mask1 = pygame.mask.Mask((13, 3), fill=True) + mask2 = pygame.mask.Mask((7, 5), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + + # Check the 4 boundaries. + offsets = ( + (mask1_size[0], 0), # off right + (0, mask1_size[1]), # off bottom + (-mask2_size[0], 0), # off left + (0, -mask2_size[1]), + ) # off top + + for offset in offsets: + msg = f"offset={offset}" + + overlap_pos = mask1.overlap(mask2, offset) + + self.assertIsNone(overlap_pos, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_overlap__bit_boundaries(self): + """Ensures overlap handles masks of different sizes correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for height in range(2, 4): + for width in range(2, 66): + mask_size = (width, height) + mask_count = width * height + mask1 = pygame.mask.Mask(mask_size, fill=True) + mask2 = pygame.mask.Mask(mask_size, fill=True) + + # Testing masks offset from each other. + for offset in self.ORIGIN_OFFSETS: + msg = f"size={mask_size}, offset={offset}" + expected_pos = (max(offset[0], 0), max(offset[1], 0)) + + overlap_pos = mask1.overlap(mask2, offset) + + self.assertEqual(overlap_pos, expected_pos, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask_count, msg) + self.assertEqual(mask2.count(), mask_count, msg) + self.assertEqual(mask1.get_size(), mask_size, msg) + self.assertEqual(mask2.get_size(), mask_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_overlap__invalid_mask_arg(self): + """Ensure overlap handles invalid mask arguments correctly.""" + size = (5, 3) + offset = (0, 0) + mask = pygame.mask.Mask(size) + invalid_mask = pygame.Surface(size) + + with self.assertRaises(TypeError): + overlap_pos = mask.overlap(invalid_mask, offset) + + def test_overlap__invalid_offset_arg(self): + """Ensure overlap handles invalid offset arguments correctly.""" + size = (2, 7) + offset = "(0, 0)" + mask1 = pygame.mask.Mask(size) + mask2 = pygame.mask.Mask(size) + + with self.assertRaises(TypeError): + overlap_pos = mask1.overlap(mask2, offset) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_overlap_area(self): + """Ensure the overlap_area is correctly calculated. + + Testing the different combinations of full/empty masks: + (mask1-filled) 1 overlap_area 1 (mask2-filled) + (mask1-empty) 0 overlap_area 1 (mask2-filled) + (mask1-filled) 1 overlap_area 0 (mask2-empty) + (mask1-empty) 0 overlap_area 0 (mask2-empty) + """ + expected_size = width, height = (4, 4) + offset = (0, 0) + expected_default = 0 + expected_counts = {(True, True): width * height} + + for fill2 in (True, False): + mask2 = pygame.mask.Mask(expected_size, fill=fill2) + mask2_count = mask2.count() + + for fill1 in (True, False): + key = (fill1, fill2) + msg = f"key={key}" + mask1 = pygame.mask.Mask(expected_size, fill=fill1) + mask1_count = mask1.count() + expected_count = expected_counts.get(key, expected_default) + + overlap_count = mask1.overlap_area(mask2, offset) + + self.assertEqual(overlap_count, expected_count, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), expected_size, msg) + self.assertEqual(mask2.get_size(), expected_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_overlap_area__offset(self): + """Ensure an offset overlap_area is correctly calculated.""" + mask1 = pygame.mask.Mask((65, 3), fill=True) + mask2 = pygame.mask.Mask((66, 4), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + for offset in self.ORIGIN_OFFSETS: + msg = f"offset={offset}" + rect2.topleft = offset + overlap_rect = rect1.clip(rect2) + expected_count = overlap_rect.w * overlap_rect.h + + overlap_count = mask1.overlap_area(other=mask2, offset=offset) + + self.assertEqual(overlap_count, expected_count, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + def test_overlap_area__offset_boundary(self): + """Ensures overlap_area handles offsets and boundaries correctly.""" + mask1 = pygame.mask.Mask((11, 3), fill=True) + mask2 = pygame.mask.Mask((5, 7), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + expected_count = 0 + + # Check the 4 boundaries. + offsets = ( + (mask1_size[0], 0), # off right + (0, mask1_size[1]), # off bottom + (-mask2_size[0], 0), # off left + (0, -mask2_size[1]), + ) # off top + + for offset in offsets: + msg = f"offset={offset}" + + overlap_count = mask1.overlap_area(mask2, Vector2(offset)) + + self.assertEqual(overlap_count, expected_count, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_overlap_area__bit_boundaries(self): + """Ensures overlap_area handles masks of different sizes correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for height in range(2, 4): + for width in range(2, 66): + mask_size = (width, height) + mask_count = width * height + mask1 = pygame.mask.Mask(mask_size, fill=True) + mask2 = pygame.mask.Mask(mask_size, fill=True) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + # Testing masks offset from each other. + for offset in self.ORIGIN_OFFSETS: + msg = f"size={mask_size}, offset={offset}" + rect2.topleft = offset + overlap_rect = rect1.clip(rect2) + expected_overlap_count = overlap_rect.w * overlap_rect.h + + overlap_count = mask1.overlap_area(mask2, offset) + + self.assertEqual(overlap_count, expected_overlap_count, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask_count, msg) + self.assertEqual(mask2.count(), mask_count, msg) + self.assertEqual(mask1.get_size(), mask_size, msg) + self.assertEqual(mask2.get_size(), mask_size, msg) + + def test_overlap_area__invalid_mask_arg(self): + """Ensure overlap_area handles invalid mask arguments correctly.""" + size = (3, 5) + offset = (0, 0) + mask = pygame.mask.Mask(size) + invalid_mask = pygame.Surface(size) + + with self.assertRaises(TypeError): + overlap_count = mask.overlap_area(invalid_mask, offset) + + def test_overlap_area__invalid_offset_arg(self): + """Ensure overlap_area handles invalid offset arguments correctly.""" + size = (7, 2) + offset = "(0, 0)" + mask1 = pygame.mask.Mask(size) + mask2 = pygame.mask.Mask(size) + + with self.assertRaises(TypeError): + overlap_count = mask1.overlap_area(mask2, offset) + + def test_overlap_mask(self): + """Ensure overlap_mask's mask has correct bits set. + + Testing the different combinations of full/empty masks: + (mask1-filled) 1 overlap_mask 1 (mask2-filled) + (mask1-empty) 0 overlap_mask 1 (mask2-filled) + (mask1-filled) 1 overlap_mask 0 (mask2-empty) + (mask1-empty) 0 overlap_mask 0 (mask2-empty) + """ + expected_size = (4, 4) + offset = (0, 0) + expected_default = pygame.mask.Mask(expected_size) + expected_masks = {(True, True): pygame.mask.Mask(expected_size, fill=True)} + + for fill2 in (True, False): + mask2 = pygame.mask.Mask(expected_size, fill=fill2) + mask2_count = mask2.count() + + for fill1 in (True, False): + key = (fill1, fill2) + msg = f"key={key}" + mask1 = pygame.mask.Mask(expected_size, fill=fill1) + mask1_count = mask1.count() + expected_mask = expected_masks.get(key, expected_default) + + overlap_mask = mask1.overlap_mask(other=mask2, offset=offset) + + self.assertIsInstance(overlap_mask, pygame.mask.Mask, msg) + assertMaskEqual(self, overlap_mask, expected_mask, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), expected_size, msg) + self.assertEqual(mask2.get_size(), expected_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_overlap_mask__bits_set(self): + """Ensure overlap_mask's mask has correct bits set.""" + mask1 = pygame.mask.Mask((50, 50), fill=True) + mask2 = pygame.mask.Mask((300, 10), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + + mask3 = mask1.overlap_mask(mask2, (-1, 0)) + + for i in range(50): + for j in range(10): + self.assertEqual(mask3.get_at((i, j)), 1, f"({i}, {j})") + + for i in range(50): + for j in range(11, 50): + self.assertEqual(mask3.get_at((i, j)), 0, f"({i}, {j})") + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count) + self.assertEqual(mask2.count(), mask2_count) + self.assertEqual(mask1.get_size(), mask1_size) + self.assertEqual(mask2.get_size(), mask2_size) + + def test_overlap_mask__offset(self): + """Ensure an offset overlap_mask's mask is correctly calculated.""" + mask1 = pygame.mask.Mask((65, 3), fill=True) + mask2 = pygame.mask.Mask((66, 4), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + expected_mask = pygame.Mask(mask1_size) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + for offset in self.ORIGIN_OFFSETS: + msg = f"offset={offset}" + rect2.topleft = offset + overlap_rect = rect1.clip(rect2) + expected_mask.clear() + expected_mask.draw( + pygame.Mask(overlap_rect.size, fill=True), overlap_rect.topleft + ) + + overlap_mask = mask1.overlap_mask(mask2, offset) + + self.assertIsInstance(overlap_mask, pygame.mask.Mask, msg) + assertMaskEqual(self, overlap_mask, expected_mask, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_overlap_mask__specific_offsets(self): + """Ensure an offset overlap_mask's mask is correctly calculated. + + Testing the specific case of: + -both masks are wider than 32 bits + -a positive offset is used + -the mask calling overlap_mask() is wider than the mask passed in + """ + mask1 = pygame.mask.Mask((65, 5), fill=True) + mask2 = pygame.mask.Mask((33, 3), fill=True) + expected_mask = pygame.Mask(mask1.get_size()) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + # This rect's corners are used to move rect2 around the inside of + # rect1. + corner_rect = rect1.inflate(-2, -2) + + for corner in ("topleft", "topright", "bottomright", "bottomleft"): + setattr(rect2, corner, getattr(corner_rect, corner)) + offset = rect2.topleft + msg = f"offset={offset}" + overlap_rect = rect1.clip(rect2) + expected_mask.clear() + expected_mask.draw( + pygame.Mask(overlap_rect.size, fill=True), overlap_rect.topleft + ) + + overlap_mask = mask1.overlap_mask(mask2, offset) + + self.assertIsInstance(overlap_mask, pygame.mask.Mask, msg) + assertMaskEqual(self, overlap_mask, expected_mask, msg) + + def test_overlap_mask__offset_boundary(self): + """Ensures overlap_mask handles offsets and boundaries correctly.""" + mask1 = pygame.mask.Mask((9, 3), fill=True) + mask2 = pygame.mask.Mask((11, 5), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + expected_count = 0 + expected_size = mask1_size + + # Check the 4 boundaries. + offsets = ( + (mask1_size[0], 0), # off right + (0, mask1_size[1]), # off bottom + (-mask2_size[0], 0), # off left + (0, -mask2_size[1]), + ) # off top + + for offset in offsets: + msg = f"offset={offset}" + + overlap_mask = mask1.overlap_mask(mask2, offset) + + self.assertIsInstance(overlap_mask, pygame.mask.Mask, msg) + self.assertEqual(overlap_mask.count(), expected_count, msg) + self.assertEqual(overlap_mask.get_size(), expected_size, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_overlap_mask__bit_boundaries(self): + """Ensures overlap_mask handles masks of different sizes correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for height in range(2, 4): + for width in range(2, 66): + mask_size = (width, height) + mask_count = width * height + mask1 = pygame.mask.Mask(mask_size, fill=True) + mask2 = pygame.mask.Mask(mask_size, fill=True) + expected_mask = pygame.Mask(mask_size) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + # Testing masks offset from each other. + for offset in self.ORIGIN_OFFSETS: + msg = f"size={mask_size}, offset={offset}" + rect2.topleft = offset + overlap_rect = rect1.clip(rect2) + expected_mask.clear() + expected_mask.draw( + pygame.Mask(overlap_rect.size, fill=True), overlap_rect.topleft + ) + + overlap_mask = mask1.overlap_mask(mask2, offset) + + self.assertIsInstance(overlap_mask, pygame.mask.Mask, msg) + assertMaskEqual(self, overlap_mask, expected_mask, msg) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask_count, msg) + self.assertEqual(mask2.count(), mask_count, msg) + self.assertEqual(mask1.get_size(), mask_size, msg) + self.assertEqual(mask2.get_size(), mask_size, msg) + + def test_overlap_mask__invalid_mask_arg(self): + """Ensure overlap_mask handles invalid mask arguments correctly.""" + size = (3, 2) + offset = (0, 0) + mask = pygame.mask.Mask(size) + invalid_mask = pygame.Surface(size) + + with self.assertRaises(TypeError): + overlap_mask = mask.overlap_mask(invalid_mask, offset) + + def test_overlap_mask__invalid_offset_arg(self): + """Ensure overlap_mask handles invalid offset arguments correctly.""" + size = (5, 2) + offset = "(0, 0)" + mask1 = pygame.mask.Mask(size) + mask2 = pygame.mask.Mask(size) + + with self.assertRaises(TypeError): + overlap_mask = mask1.overlap_mask(mask2, offset) + + def test_mask_access(self): + """do the set_at, and get_at parts work correctly?""" + m = pygame.Mask((10, 10)) + m.set_at((0, 0), 1) + self.assertEqual(m.get_at((0, 0)), 1) + m.set_at((9, 0), 1) + self.assertEqual(m.get_at((9, 0)), 1) + + # s = pygame.Surface((10,10)) + # s.set_at((1,0), (0, 0, 1, 255)) + # self.assertEqual(s.get_at((1,0)), (0, 0, 1, 255)) + # s.set_at((-1,0), (0, 0, 1, 255)) + + # out of bounds, should get IndexError + self.assertRaises(IndexError, lambda: m.get_at((-1, 0))) + self.assertRaises(IndexError, lambda: m.set_at((-1, 0), 1)) + self.assertRaises(IndexError, lambda: m.set_at((10, 0), 1)) + self.assertRaises(IndexError, lambda: m.set_at((0, 10), 1)) + + def test_fill(self): + """Ensure a mask can be filled.""" + width, height = 11, 23 + expected_count = width * height + expected_size = (width, height) + mask = pygame.mask.Mask(expected_size) + + mask.fill() + + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_fill__bit_boundaries(self): + """Ensures masks of different sizes are filled correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for height in range(1, 4): + for width in range(1, 66): + mask = pygame.mask.Mask((width, height)) + expected_count = width * height + + mask.fill() + + self.assertEqual( + mask.count(), expected_count, f"size=({width}, {height})" + ) + + def test_clear(self): + """Ensure a mask can be cleared.""" + expected_count = 0 + expected_size = (13, 27) + mask = pygame.mask.Mask(expected_size, fill=True) + + mask.clear() + + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + + def test_clear__bit_boundaries(self): + """Ensures masks of different sizes are cleared correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + expected_count = 0 + + for height in range(1, 4): + for width in range(1, 66): + mask = pygame.mask.Mask((width, height), fill=True) + + mask.clear() + + self.assertEqual( + mask.count(), expected_count, f"size=({width}, {height})" + ) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_invert(self): + """Ensure a mask can be inverted.""" + side = 73 + expected_size = (side, side) + mask1 = pygame.mask.Mask(expected_size) + mask2 = pygame.mask.Mask(expected_size, fill=True) + expected_count1 = side * side + expected_count2 = 0 + + for i in range(side): + expected_count1 -= 1 + expected_count2 += 1 + pos = (i, i) + mask1.set_at(pos) + mask2.set_at(pos, 0) + + mask1.invert() + mask2.invert() + + self.assertEqual(mask1.count(), expected_count1) + self.assertEqual(mask2.count(), expected_count2) + self.assertEqual(mask1.get_size(), expected_size) + self.assertEqual(mask2.get_size(), expected_size) + + for i in range(side): + pos = (i, i) + msg = f"pos={pos}" + + self.assertEqual(mask1.get_at(pos), 0, msg) + self.assertEqual(mask2.get_at(pos), 1, msg) + + def test_invert__full(self): + """Ensure a full mask can be inverted.""" + expected_count = 0 + expected_size = (43, 97) + mask = pygame.mask.Mask(expected_size, fill=True) + + mask.invert() + + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + + def test_invert__empty(self): + """Ensure an empty mask can be inverted.""" + width, height = 43, 97 + expected_size = (width, height) + expected_count = width * height + mask = pygame.mask.Mask(expected_size) + + mask.invert() + + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_invert__bit_boundaries(self): + """Ensures masks of different sizes are inverted correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for fill in (True, False): + for height in range(1, 4): + for width in range(1, 66): + mask = pygame.mask.Mask((width, height), fill=fill) + expected_count = 0 if fill else width * height + + mask.invert() + + self.assertEqual( + mask.count(), + expected_count, + f"fill={fill}, size=({width}, {height})", + ) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_scale(self): + """Ensure a mask can be scaled.""" + width, height = 43, 61 + original_size = (width, height) + + for fill in (True, False): + original_mask = pygame.mask.Mask(original_size, fill=fill) + original_count = width * height if fill else 0 + + # Test a range of sizes. Also tests scaling to 'same' + # size when new_w, new_h = width, height + for new_w in range(width - 10, width + 10): + for new_h in range(height - 10, height + 10): + expected_size = (new_w, new_h) + expected_count = new_w * new_h if fill else 0 + msg = f"size={expected_size}" + + mask = original_mask.scale(scale=expected_size) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.count(), expected_count, msg) + self.assertEqual(mask.get_size(), expected_size) + + # Ensure the original mask is unchanged. + self.assertEqual(original_mask.count(), original_count, msg) + self.assertEqual(original_mask.get_size(), original_size, msg) + + def test_scale__negative_size(self): + """Ensure scale handles negative sizes correctly.""" + mask = pygame.Mask((100, 100)) + + with self.assertRaises(ValueError): + mask.scale((-1, -1)) + + with self.assertRaises(ValueError): + mask.scale(Vector2(-1, 10)) + + with self.assertRaises(ValueError): + mask.scale((10, -1)) + + def test_draw(self): + """Ensure a mask can be drawn onto another mask. + + Testing the different combinations of full/empty masks: + (mask1-filled) 1 draw 1 (mask2-filled) + (mask1-empty) 0 draw 1 (mask2-filled) + (mask1-filled) 1 draw 0 (mask2-empty) + (mask1-empty) 0 draw 0 (mask2-empty) + """ + expected_size = (4, 4) + offset = (0, 0) + expected_default = pygame.mask.Mask(expected_size, fill=True) + expected_masks = {(False, False): pygame.mask.Mask(expected_size)} + + for fill2 in (True, False): + mask2 = pygame.mask.Mask(expected_size, fill=fill2) + mask2_count = mask2.count() + + for fill1 in (True, False): + key = (fill1, fill2) + msg = f"key={key}" + mask1 = pygame.mask.Mask(expected_size, fill=fill1) + expected_mask = expected_masks.get(key, expected_default) + + mask1.draw(mask2, offset) + + assertMaskEqual(self, mask1, expected_mask, msg) + + # Ensure mask2 unchanged. + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask2.get_size(), expected_size, msg) + + def test_draw__offset(self): + """Ensure an offset mask can be drawn onto another mask.""" + mask1 = pygame.mask.Mask((65, 3)) + mask2 = pygame.mask.Mask((66, 4), fill=True) + mask2_count = mask2.count() + mask2_size = mask2.get_size() + expected_mask = pygame.Mask(mask1.get_size()) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + for offset in self.ORIGIN_OFFSETS: + msg = f"offset={offset}" + rect2.topleft = offset + overlap_rect = rect1.clip(rect2) + expected_mask.clear() + + # Normally draw() could be used to set these bits, but the draw() + # method is being tested here, so a loop is used instead. + for x in range(overlap_rect.left, overlap_rect.right): + for y in range(overlap_rect.top, overlap_rect.bottom): + expected_mask.set_at((x, y)) + mask1.clear() # Ensure it's empty for testing each offset. + + mask1.draw(other=mask2, offset=offset) + + assertMaskEqual(self, mask1, expected_mask, msg) + + # Ensure mask2 unchanged. + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + def test_draw__specific_offsets(self): + """Ensure an offset mask can be drawn onto another mask. + + Testing the specific case of: + -both masks are wider than 32 bits + -a positive offset is used + -the mask calling draw() is wider than the mask passed in + """ + mask1 = pygame.mask.Mask((65, 5)) + mask2 = pygame.mask.Mask((33, 3), fill=True) + expected_mask = pygame.Mask(mask1.get_size()) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + # This rect's corners are used to move rect2 around the inside of + # rect1. + corner_rect = rect1.inflate(-2, -2) + + for corner in ("topleft", "topright", "bottomright", "bottomleft"): + setattr(rect2, corner, getattr(corner_rect, corner)) + offset = rect2.topleft + msg = f"offset={offset}" + overlap_rect = rect1.clip(rect2) + expected_mask.clear() + + # Normally draw() could be used to set these bits, but the draw() + # method is being tested here, so a loop is used instead. + for x in range(overlap_rect.left, overlap_rect.right): + for y in range(overlap_rect.top, overlap_rect.bottom): + expected_mask.set_at((x, y)) + mask1.clear() # Ensure it's empty for testing each offset. + + mask1.draw(mask2, offset) + + assertMaskEqual(self, mask1, expected_mask, msg) + + def test_draw__offset_boundary(self): + """Ensures draw handles offsets and boundaries correctly.""" + mask1 = pygame.mask.Mask((13, 5)) + mask2 = pygame.mask.Mask((7, 3), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + + # Check the 4 boundaries. + offsets = ( + (mask1_size[0], 0), # off right + (0, mask1_size[1]), # off bottom + (-mask2_size[0], 0), # off left + (0, -mask2_size[1]), + ) # off top + + for offset in offsets: + msg = f"offset={offset}" + + mask1.draw(mask2, offset) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_draw__bit_boundaries(self): + """Ensures draw handles masks of different sizes correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for height in range(2, 4): + for width in range(2, 66): + mask_size = (width, height) + mask_count = width * height + mask1 = pygame.mask.Mask(mask_size) + mask2 = pygame.mask.Mask(mask_size, fill=True) + expected_mask = pygame.Mask(mask_size) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + # Testing masks offset from each other. + for offset in self.ORIGIN_OFFSETS: + msg = f"size={mask_size}, offset={offset}" + rect2.topleft = offset + overlap_rect = rect1.clip(rect2) + expected_mask.clear() + + # Normally draw() could be used to set these bits, but the + # draw() method is being tested here, so a loop is used + # instead. + for x in range(overlap_rect.left, overlap_rect.right): + for y in range(overlap_rect.top, overlap_rect.bottom): + expected_mask.set_at((x, y)) + mask1.clear() # Ensure it's empty for each test. + + mask1.draw(mask2, offset) + + assertMaskEqual(self, mask1, expected_mask, msg) + + # Ensure mask2 unchanged. + self.assertEqual(mask2.count(), mask_count, msg) + self.assertEqual(mask2.get_size(), mask_size, msg) + + def test_draw__invalid_mask_arg(self): + """Ensure draw handles invalid mask arguments correctly.""" + size = (7, 3) + offset = (0, 0) + mask = pygame.mask.Mask(size) + invalid_mask = pygame.Surface(size) + + with self.assertRaises(TypeError): + mask.draw(invalid_mask, offset) + + def test_draw__invalid_offset_arg(self): + """Ensure draw handles invalid offset arguments correctly.""" + size = (5, 7) + offset = "(0, 0)" + mask1 = pygame.mask.Mask(size) + mask2 = pygame.mask.Mask(size) + + with self.assertRaises(TypeError): + mask1.draw(mask2, offset) + + def test_erase(self): + """Ensure a mask can erase another mask. + + Testing the different combinations of full/empty masks: + (mask1-filled) 1 erase 1 (mask2-filled) + (mask1-empty) 0 erase 1 (mask2-filled) + (mask1-filled) 1 erase 0 (mask2-empty) + (mask1-empty) 0 erase 0 (mask2-empty) + """ + expected_size = (4, 4) + offset = (0, 0) + expected_default = pygame.mask.Mask(expected_size) + expected_masks = {(True, False): pygame.mask.Mask(expected_size, fill=True)} + + for fill2 in (True, False): + mask2 = pygame.mask.Mask(expected_size, fill=fill2) + mask2_count = mask2.count() + + for fill1 in (True, False): + key = (fill1, fill2) + msg = f"key={key}" + mask1 = pygame.mask.Mask(expected_size, fill=fill1) + expected_mask = expected_masks.get(key, expected_default) + + mask1.erase(mask2, offset) + + assertMaskEqual(self, mask1, expected_mask, msg) + + # Ensure mask2 unchanged. + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask2.get_size(), expected_size, msg) + + def test_erase__offset(self): + """Ensure an offset mask can erase another mask.""" + mask1 = pygame.mask.Mask((65, 3)) + mask2 = pygame.mask.Mask((66, 4), fill=True) + mask2_count = mask2.count() + mask2_size = mask2.get_size() + expected_mask = pygame.Mask(mask1.get_size()) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + for offset in self.ORIGIN_OFFSETS: + msg = f"offset={offset}" + rect2.topleft = offset + overlap_rect = rect1.clip(rect2) + expected_mask.fill() + + # Normally erase() could be used to clear these bits, but the + # erase() method is being tested here, so a loop is used instead. + for x in range(overlap_rect.left, overlap_rect.right): + for y in range(overlap_rect.top, overlap_rect.bottom): + expected_mask.set_at((x, y), 0) + mask1.fill() # Ensure it's filled for testing each offset. + + mask1.erase(other=mask2, offset=offset) + + assertMaskEqual(self, mask1, expected_mask, msg) + + # Ensure mask2 unchanged. + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + def test_erase__specific_offsets(self): + """Ensure an offset mask can erase another mask. + + Testing the specific case of: + -both masks are wider than 32 bits + -a positive offset is used + -the mask calling erase() is wider than the mask passed in + """ + mask1 = pygame.mask.Mask((65, 5)) + mask2 = pygame.mask.Mask((33, 3), fill=True) + expected_mask = pygame.Mask(mask1.get_size()) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + # This rect's corners are used to move rect2 around the inside of + # rect1. + corner_rect = rect1.inflate(-2, -2) + + for corner in ("topleft", "topright", "bottomright", "bottomleft"): + setattr(rect2, corner, getattr(corner_rect, corner)) + offset = rect2.topleft + msg = f"offset={offset}" + overlap_rect = rect1.clip(rect2) + expected_mask.fill() + + # Normally erase() could be used to clear these bits, but the + # erase() method is being tested here, so a loop is used instead. + for x in range(overlap_rect.left, overlap_rect.right): + for y in range(overlap_rect.top, overlap_rect.bottom): + expected_mask.set_at((x, y), 0) + mask1.fill() # Ensure it's filled for testing each offset. + + mask1.erase(mask2, Vector2(offset)) + + assertMaskEqual(self, mask1, expected_mask, msg) + + def test_erase__offset_boundary(self): + """Ensures erase handles offsets and boundaries correctly.""" + mask1 = pygame.mask.Mask((7, 11), fill=True) + mask2 = pygame.mask.Mask((3, 13), fill=True) + mask1_count = mask1.count() + mask2_count = mask2.count() + mask1_size = mask1.get_size() + mask2_size = mask2.get_size() + + # Check the 4 boundaries. + offsets = ( + (mask1_size[0], 0), # off right + (0, mask1_size[1]), # off bottom + (-mask2_size[0], 0), # off left + (0, -mask2_size[1]), + ) # off top + + for offset in offsets: + msg = f"offset={offset}" + + mask1.erase(mask2, offset) + + # Ensure mask1/mask2 unchanged. + self.assertEqual(mask1.count(), mask1_count, msg) + self.assertEqual(mask2.count(), mask2_count, msg) + self.assertEqual(mask1.get_size(), mask1_size, msg) + self.assertEqual(mask2.get_size(), mask2_size, msg) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_erase__bit_boundaries(self): + """Ensures erase handles masks of different sizes correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for height in range(2, 4): + for width in range(2, 66): + mask_size = (width, height) + mask_count = width * height + mask1 = pygame.mask.Mask(mask_size) + mask2 = pygame.mask.Mask(mask_size, fill=True) + expected_mask = pygame.Mask(mask_size) + + # Using rects to help determine the overlapping area. + rect1 = mask1.get_rect() + rect2 = mask2.get_rect() + + # Testing masks offset from each other. + for offset in self.ORIGIN_OFFSETS: + msg = f"size={mask_size}, offset={offset}" + rect2.topleft = offset + overlap_rect = rect1.clip(rect2) + expected_mask.fill() + + # Normally erase() could be used to clear these bits, but + # the erase() method is being tested here, so a loop is + # used instead. + for x in range(overlap_rect.left, overlap_rect.right): + for y in range(overlap_rect.top, overlap_rect.bottom): + expected_mask.set_at((x, y), 0) + mask1.fill() # Ensure it's filled for each test. + + mask1.erase(mask2, offset) + + assertMaskEqual(self, mask1, expected_mask, msg) + + # Ensure mask2 unchanged. + self.assertEqual(mask2.count(), mask_count, msg) + self.assertEqual(mask2.get_size(), mask_size, msg) + + def test_erase__invalid_mask_arg(self): + """Ensure erase handles invalid mask arguments correctly.""" + size = (3, 7) + offset = (0, 0) + mask = pygame.mask.Mask(size) + invalid_mask = pygame.Surface(size) + + with self.assertRaises(TypeError): + mask.erase(invalid_mask, offset) + + def test_erase__invalid_offset_arg(self): + """Ensure erase handles invalid offset arguments correctly.""" + size = (7, 5) + offset = "(0, 0)" + mask1 = pygame.mask.Mask(size) + mask2 = pygame.mask.Mask(size) + + with self.assertRaises(TypeError): + mask1.erase(mask2, offset) + + def test_count(self): + """Ensure a mask's set bits are correctly counted.""" + side = 67 + expected_size = (side, side) + expected_count = 0 + mask = pygame.mask.Mask(expected_size) + + for i in range(side): + expected_count += 1 + mask.set_at((i, i)) + + count = mask.count() + + self.assertEqual(count, expected_count) + self.assertEqual(mask.get_size(), expected_size) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_count__bit_boundaries(self): + """Ensures the set bits of different sized masks are counted correctly. + + Tests masks of different sizes, including: + -masks 31 to 33 bits wide (32 bit boundaries) + -masks 63 to 65 bits wide (64 bit boundaries) + """ + for fill in (True, False): + for height in range(1, 4): + for width in range(1, 66): + mask = pygame.mask.Mask((width, height), fill=fill) + expected_count = width * height if fill else 0 + + # Test toggling each bit. + for pos in ((x, y) for y in range(height) for x in range(width)): + if fill: + mask.set_at(pos, 0) + expected_count -= 1 + else: + mask.set_at(pos, 1) + expected_count += 1 + + count = mask.count() + + self.assertEqual( + count, + expected_count, + f"fill={fill}, size=({width}, {height}), pos={pos}", + ) + + def test_count__full_mask(self): + """Ensure a full mask's set bits are correctly counted.""" + width, height = 17, 97 + expected_size = (width, height) + expected_count = width * height + mask = pygame.mask.Mask(expected_size, fill=True) + + count = mask.count() + + self.assertEqual(count, expected_count) + self.assertEqual(mask.get_size(), expected_size) + + def test_count__empty_mask(self): + """Ensure an empty mask's set bits are correctly counted.""" + expected_count = 0 + expected_size = (13, 27) + mask = pygame.mask.Mask(expected_size) + + count = mask.count() + + self.assertEqual(count, expected_count) + self.assertEqual(mask.get_size(), expected_size) + + def test_centroid(self): + """Ensure a filled mask's centroid is correctly calculated.""" + mask = pygame.mask.Mask((5, 7), fill=True) + expected_centroid = mask.get_rect().center + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + + def test_centroid__empty_mask(self): + """Ensure an empty mask's centroid is correctly calculated.""" + expected_centroid = (0, 0) + expected_size = (101, 103) + mask = pygame.mask.Mask(expected_size) + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + self.assertEqual(mask.get_size(), expected_size) + + def test_centroid__single_row(self): + """Ensure a mask's centroid is correctly calculated + when setting points along a single row.""" + width, height = (5, 7) + mask = pygame.mask.Mask((width, height)) + + for y in range(height): + mask.clear() # Clear for each row. + + for x in range(width): + mask.set_at((x, y)) + expected_centroid = (x // 2, y) + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + + def test_centroid__two_rows(self): + """Ensure a mask's centroid is correctly calculated + when setting points along two rows.""" + width, height = (5, 7) + mask = pygame.mask.Mask((width, height)) + + # The first row is tested with each of the other rows. + for y in range(1, height): + mask.clear() # Clear for each set of rows. + + for x in range(width): + mask.set_at((x, 0)) + mask.set_at((x, y)) + expected_centroid = (x // 2, y // 2) + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + + def test_centroid__single_column(self): + """Ensure a mask's centroid is correctly calculated + when setting points along a single column.""" + width, height = (5, 7) + mask = pygame.mask.Mask((width, height)) + + for x in range(width): + mask.clear() # Clear for each column. + + for y in range(height): + mask.set_at((x, y)) + expected_centroid = (x, y // 2) + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + + def test_centroid__two_columns(self): + """Ensure a mask's centroid is correctly calculated + when setting points along two columns.""" + width, height = (5, 7) + mask = pygame.mask.Mask((width, height)) + + # The first column is tested with each of the other columns. + for x in range(1, width): + mask.clear() # Clear for each set of columns. + + for y in range(height): + mask.set_at((0, y)) + mask.set_at((x, y)) + expected_centroid = (x // 2, y // 2) + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + + def test_centroid__all_corners(self): + """Ensure a mask's centroid is correctly calculated + when its corners are set.""" + mask = pygame.mask.Mask((5, 7)) + expected_centroid = mask.get_rect().center + + for corner in corners(mask): + mask.set_at(corner) + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + + def test_centroid__two_corners(self): + """Ensure a mask's centroid is correctly calculated + when only two corners are set.""" + mask = pygame.mask.Mask((5, 7)) + mask_rect = mask.get_rect() + mask_corners = corners(mask) + + for i, corner1 in enumerate(mask_corners): + for corner2 in mask_corners[i + 1 :]: + mask.clear() # Clear for each pair of corners. + mask.set_at(corner1) + mask.set_at(corner2) + + if corner1[0] == corner2[0]: + expected_centroid = (corner1[0], abs(corner1[1] - corner2[1]) // 2) + elif corner1[1] == corner2[1]: + expected_centroid = (abs(corner1[0] - corner2[0]) // 2, corner1[1]) + else: + expected_centroid = mask_rect.center + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + + def test_angle(self): + """Ensure a mask's orientation angle is correctly calculated.""" + expected_angle = -45.0 + expected_size = (100, 100) + surface = pygame.Surface(expected_size) + mask = pygame.mask.from_surface(surface) + + angle = mask.angle() # Returns the orientation of the pixels + + self.assertIsInstance(angle, float) + self.assertEqual(angle, expected_angle) + + def test_angle__empty_mask(self): + """Ensure an empty mask's angle is correctly calculated.""" + expected_angle = 0.0 + expected_size = (107, 43) + mask = pygame.mask.Mask(expected_size) + + angle = mask.angle() + + self.assertIsInstance(angle, float) + self.assertAlmostEqual(angle, expected_angle) + self.assertEqual(mask.get_size(), expected_size) + + def test_drawing(self): + """Test fill, clear, invert, draw, erase""" + m = pygame.Mask((100, 100)) + self.assertEqual(m.count(), 0) + + m.fill() + self.assertEqual(m.count(), 10000) + + m2 = pygame.Mask((10, 10), fill=True) + m.erase(m2, (50, 50)) + self.assertEqual(m.count(), 9900) + + m.invert() + self.assertEqual(m.count(), 100) + + m.draw(m2, (0, 0)) + self.assertEqual(m.count(), 200) + + m.clear() + self.assertEqual(m.count(), 0) + + def test_outline(self): + """ """ + + m = pygame.Mask((20, 20)) + self.assertEqual(m.outline(), []) + + m.set_at((10, 10), 1) + self.assertEqual(m.outline(), [(10, 10)]) + + m.set_at((10, 12), 1) + self.assertEqual(m.outline(10), [(10, 10)]) + + m.set_at((11, 11), 1) + self.assertEqual( + m.outline(), [(10, 10), (11, 11), (10, 12), (11, 11), (10, 10)] + ) + self.assertEqual(m.outline(every=2), [(10, 10), (10, 12), (10, 10)]) + + # TODO: Test more corner case outlines. + + def test_convolve__size(self): + sizes = [(1, 1), (31, 31), (32, 32), (100, 100)] + for s1 in sizes: + m1 = pygame.Mask(s1) + for s2 in sizes: + m2 = pygame.Mask(s2) + o = m1.convolve(m2) + + self.assertIsInstance(o, pygame.mask.Mask) + + for i in (0, 1): + self.assertEqual( + o.get_size()[i], m1.get_size()[i] + m2.get_size()[i] - 1 + ) + + def test_convolve__point_identities(self): + """Convolving with a single point is the identity, while convolving a point with something flips it.""" + m = random_mask((100, 100)) + k = pygame.Mask((1, 1)) + k.set_at((0, 0)) + + convolve_mask = m.convolve(k) + + self.assertIsInstance(convolve_mask, pygame.mask.Mask) + assertMaskEqual(self, m, convolve_mask) + + convolve_mask = k.convolve(k.convolve(m)) + + self.assertIsInstance(convolve_mask, pygame.mask.Mask) + assertMaskEqual(self, m, convolve_mask) + + def test_convolve__with_output(self): + """checks that convolution modifies only the correct portion of the output""" + + m = random_mask((10, 10)) + k = pygame.Mask((2, 2)) + k.set_at((0, 0)) + + o = pygame.Mask((50, 50)) + test = pygame.Mask((50, 50)) + + m.convolve(k, o) + test.draw(m, (1, 1)) + + self.assertIsInstance(o, pygame.mask.Mask) + assertMaskEqual(self, o, test) + + o.clear() + test.clear() + + m.convolve(other=k, output=o, offset=Vector2(10, 10)) + test.draw(m, (11, 11)) + + self.assertIsInstance(o, pygame.mask.Mask) + assertMaskEqual(self, o, test) + + def test_convolve__out_of_range(self): + full = pygame.Mask((2, 2), fill=True) + # Tuple of points (out of range) and the expected count for each. + pts_data = (((0, 3), 0), ((0, 2), 3), ((-2, -2), 1), ((-3, -3), 0)) + + for pt, expected_count in pts_data: + convolve_mask = full.convolve(full, None, pt) + + self.assertIsInstance(convolve_mask, pygame.mask.Mask) + self.assertEqual(convolve_mask.count(), expected_count) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_convolve(self): + """Tests the definition of convolution""" + m1 = random_mask((100, 100)) + m2 = random_mask((100, 100)) + conv = m1.convolve(m2) + + self.assertIsInstance(conv, pygame.mask.Mask) + for i in range(conv.get_size()[0]): + for j in range(conv.get_size()[1]): + self.assertEqual( + conv.get_at((i, j)) == 0, m1.overlap(m2, (i - 99, j - 99)) is None + ) + + def _draw_component_pattern_box(self, mask, size, pos, inverse=False): + # Helper method to create/draw a 'box' pattern for testing. + # + # 111 + # 101 3x3 example pattern + # 111 + pattern = pygame.mask.Mask((size, size), fill=True) + pattern.set_at((size // 2, size // 2), 0) + + if inverse: + mask.erase(pattern, pos) + pattern.invert() + else: + mask.draw(pattern, pos) + + return pattern + + def _draw_component_pattern_x(self, mask, size, pos, inverse=False): + # Helper method to create/draw an 'X' pattern for testing. + # + # 101 + # 010 3x3 example pattern + # 101 + pattern = pygame.mask.Mask((size, size)) + + ymax = size - 1 + for y in range(size): + for x in range(size): + if x in [y, ymax - y]: + pattern.set_at((x, y)) + + if inverse: + mask.erase(pattern, pos) + pattern.invert() + else: + mask.draw(pattern, pos) + + return pattern + + def _draw_component_pattern_plus(self, mask, size, pos, inverse=False): + # Helper method to create/draw a '+' pattern for testing. + # + # 010 + # 111 3x3 example pattern + # 010 + pattern = pygame.mask.Mask((size, size)) + + xmid = ymid = size // 2 + for y in range(size): + for x in range(size): + if x == xmid or y == ymid: + pattern.set_at((x, y)) + + if inverse: + mask.erase(pattern, pos) + pattern.invert() + else: + mask.draw(pattern, pos) + + return pattern + + def test_connected_component(self): + """Ensure a mask's connected component is correctly calculated.""" + width, height = 41, 27 + expected_size = (width, height) + original_mask = pygame.mask.Mask(expected_size) + patterns = [] # Patterns and offsets. + + # Draw some connected patterns on the original mask. + offset = (0, 0) + pattern = self._draw_component_pattern_x(original_mask, 3, offset) + patterns.append((pattern, offset)) + + size = 4 + offset = (width - size, 0) + pattern = self._draw_component_pattern_plus(original_mask, size, offset) + patterns.append((pattern, offset)) + + # Make this one the largest connected component. + offset = (width // 2, height // 2) + pattern = self._draw_component_pattern_box(original_mask, 7, offset) + patterns.append((pattern, offset)) + + expected_pattern, expected_offset = patterns[-1] + expected_count = expected_pattern.count() + original_count = sum(p.count() for p, _ in patterns) + + mask = original_mask.connected_component() + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + self.assertEqual( + mask.overlap_area(expected_pattern, expected_offset), expected_count + ) + + # Ensure the original mask is unchanged. + self.assertEqual(original_mask.count(), original_count) + self.assertEqual(original_mask.get_size(), expected_size) + + for pattern, offset in patterns: + self.assertEqual( + original_mask.overlap_area(pattern, offset), pattern.count() + ) + + def test_connected_component__full_mask(self): + """Ensure a mask's connected component is correctly calculated + when the mask is full. + """ + expected_size = (23, 31) + original_mask = pygame.mask.Mask(expected_size, fill=True) + expected_count = original_mask.count() + + mask = original_mask.connected_component() + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + + # Ensure the original mask is unchanged. + self.assertEqual(original_mask.count(), expected_count) + self.assertEqual(original_mask.get_size(), expected_size) + + def test_connected_component__empty_mask(self): + """Ensure a mask's connected component is correctly calculated + when the mask is empty. + """ + expected_size = (37, 43) + original_mask = pygame.mask.Mask(expected_size) + original_count = original_mask.count() + expected_count = 0 + + mask = original_mask.connected_component() + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + + # Ensure the original mask is unchanged. + self.assertEqual(original_mask.count(), original_count) + self.assertEqual(original_mask.get_size(), expected_size) + + def test_connected_component__one_set_bit(self): + """Ensure a mask's connected component is correctly calculated + when the coordinate's bit is set with a connected component of 1 bit. + """ + width, height = 71, 67 + expected_size = (width, height) + original_mask = pygame.mask.Mask(expected_size, fill=True) + xset, yset = width // 2, height // 2 + set_pos = (xset, yset) + expected_offset = (xset - 1, yset - 1) + + # This isolates the bit at set_pos from all the other bits. + expected_pattern = self._draw_component_pattern_box( + original_mask, 3, expected_offset, inverse=True + ) + expected_count = 1 + original_count = original_mask.count() + + mask = original_mask.connected_component(set_pos) + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + self.assertEqual( + mask.overlap_area(expected_pattern, expected_offset), expected_count + ) + + # Ensure the original mask is unchanged. + self.assertEqual(original_mask.count(), original_count) + self.assertEqual(original_mask.get_size(), expected_size) + self.assertEqual( + original_mask.overlap_area(expected_pattern, expected_offset), + expected_count, + ) + + def test_connected_component__multi_set_bits(self): + """Ensure a mask's connected component is correctly calculated + when the coordinate's bit is set with a connected component of > 1 bit. + """ + expected_size = (113, 67) + original_mask = pygame.mask.Mask(expected_size) + p_width, p_height = 11, 13 + set_pos = xset, yset = 11, 21 + expected_offset = (xset - 1, yset - 1) + expected_pattern = pygame.mask.Mask((p_width, p_height), fill=True) + + # Make an unsymmetrical pattern. All the set bits need to be connected + # in the resulting pattern for this to work properly. + for y in range(3, p_height): + for x in range(1, p_width): + if x in [y, y - 3, p_width - 4]: + expected_pattern.set_at((x, y), 0) + + expected_count = expected_pattern.count() + original_mask.draw(expected_pattern, expected_offset) + + mask = original_mask.connected_component(set_pos) + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + self.assertEqual( + mask.overlap_area(expected_pattern, expected_offset), expected_count + ) + + # Ensure the original mask is unchanged. + self.assertEqual(original_mask.count(), expected_count) + self.assertEqual(original_mask.get_size(), expected_size) + self.assertEqual( + original_mask.overlap_area(expected_pattern, expected_offset), + expected_count, + ) + + def test_connected_component__unset_bit(self): + """Ensure a mask's connected component is correctly calculated + when the coordinate's bit is unset. + """ + width, height = 109, 101 + expected_size = (width, height) + original_mask = pygame.mask.Mask(expected_size, fill=True) + unset_pos = (width // 2, height // 2) + original_mask.set_at(unset_pos, 0) + original_count = original_mask.count() + expected_count = 0 + + mask = original_mask.connected_component(unset_pos) + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertEqual(mask.count(), expected_count) + self.assertEqual(mask.get_size(), expected_size) + + # Ensure the original mask is unchanged. + self.assertEqual(original_mask.count(), original_count) + self.assertEqual(original_mask.get_size(), expected_size) + self.assertEqual(original_mask.get_at(unset_pos), 0) + + def test_connected_component__out_of_bounds(self): + """Ensure connected_component() checks bounds.""" + width, height = 19, 11 + original_size = (width, height) + original_mask = pygame.mask.Mask(original_size, fill=True) + original_count = original_mask.count() + + for pos in ((0, -1), (-1, 0), (0, height + 1), (width + 1, 0)): + with self.assertRaises(IndexError): + mask = original_mask.connected_component(pos) + + # Ensure the original mask is unchanged. + self.assertEqual(original_mask.count(), original_count) + self.assertEqual(original_mask.get_size(), original_size) + + def test_connected_components(self): + """ """ + m = pygame.Mask((10, 10)) + + self.assertListEqual(m.connected_components(), []) + + comp = m.connected_component() + + self.assertEqual(m.count(), comp.count()) + + m.set_at((0, 0), 1) + m.set_at((1, 1), 1) + comp = m.connected_component() + comps = m.connected_components() + comps1 = m.connected_components(1) + comps2 = m.connected_components(2) + comps3 = m.connected_components(3) + + self.assertEqual(comp.count(), comps[0].count()) + self.assertEqual(comps1[0].count(), 2) + self.assertEqual(comps2[0].count(), 2) + self.assertListEqual(comps3, []) + + m.set_at((9, 9), 1) + comp = m.connected_component() + comp1 = m.connected_component((1, 1)) + comp2 = m.connected_component((2, 2)) + comps = m.connected_components() + comps1 = m.connected_components(1) + comps2 = m.connected_components(minimum=2) + comps3 = m.connected_components(3) + + self.assertEqual(comp.count(), 2) + self.assertEqual(comp1.count(), 2) + self.assertEqual(comp2.count(), 0) + self.assertEqual(len(comps), 2) + self.assertEqual(len(comps1), 2) + self.assertEqual(len(comps2), 1) + self.assertEqual(len(comps3), 0) + + for mask in comps: + self.assertIsInstance(mask, pygame.mask.Mask) + + def test_connected_components__negative_min_with_empty_mask(self): + """Ensures connected_components() properly handles negative min values + when the mask is empty. + + Negative and zero values for the min parameter (minimum number of bits + per connected component) equate to setting it to one. + """ + expected_comps = [] + mask_count = 0 + mask_size = (65, 13) + mask = pygame.mask.Mask(mask_size) + + connected_comps = mask.connected_components(-1) + + self.assertListEqual(connected_comps, expected_comps) + + # Ensure the original mask is unchanged. + self.assertEqual(mask.count(), mask_count) + self.assertEqual(mask.get_size(), mask_size) + + def test_connected_components__negative_min_with_full_mask(self): + """Ensures connected_components() properly handles negative min values + when the mask is full. + + Negative and zero values for the min parameter (minimum number of bits + per connected component) equate to setting it to one. + """ + mask_size = (64, 11) + mask = pygame.mask.Mask(mask_size, fill=True) + mask_count = mask.count() + expected_len = 1 + + connected_comps = mask.connected_components(-2) + + self.assertEqual(len(connected_comps), expected_len) + assertMaskEqual(self, connected_comps[0], mask) + + # Ensure the original mask is unchanged. + self.assertEqual(mask.count(), mask_count) + self.assertEqual(mask.get_size(), mask_size) + + def test_connected_components__negative_min_with_some_bits_set(self): + """Ensures connected_components() properly handles negative min values + when the mask has some bits set. + + Negative and zero values for the min parameter (minimum number of bits + per connected component) equate to setting it to one. + """ + mask_size = (64, 12) + mask = pygame.mask.Mask(mask_size) + expected_comps = {} + + # Set the corners and the center positions. A new expected component + # mask is created for each point. + for corner in corners(mask): + mask.set_at(corner) + + new_mask = pygame.mask.Mask(mask_size) + new_mask.set_at(corner) + expected_comps[corner] = new_mask + + center = (mask_size[0] // 2, mask_size[1] // 2) + mask.set_at(center) + + new_mask = pygame.mask.Mask(mask_size) + new_mask.set_at(center) + expected_comps[center] = new_mask + mask_count = mask.count() + + connected_comps = mask.connected_components(-3) + + self.assertEqual(len(connected_comps), len(expected_comps)) + + for comp in connected_comps: + # Since the masks in the connected component list can be in any + # order, loop the expected components to find its match. + found = False + + for pt in tuple(expected_comps.keys()): + if comp.get_at(pt): + found = True + assertMaskEqual(self, comp, expected_comps[pt]) + del expected_comps[pt] # Entry removed so it isn't reused. + break + + self.assertTrue(found, f"missing component for pt={pt}") + + # Ensure the original mask is unchanged. + self.assertEqual(mask.count(), mask_count) + self.assertEqual(mask.get_size(), mask_size) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_get_bounding_rects(self): + """Ensures get_bounding_rects works correctly.""" + # Create masks with different set point groups. Each group of + # connected set points will be contained in its own bounding rect. + # Diagonal points are considered connected. + mask_data = [] # [((size), ((rect1_pts), ...)), ...] + + # Mask 1: + # |0123456789 + # -+---------- + # 0|1100000000 + # 1|1000000000 + # 2|0000000000 + # 3|1001000000 + # 4|0000000000 + # 5|0000000000 + # 6|0000000000 + # 7|0000000000 + # 8|0000000000 + # 9|0000000000 + mask_data.append( + ( + (10, 10), # size + # Points to set for the 3 bounding rects. + (((0, 0), (1, 0), (0, 1)), ((0, 3),), ((3, 3),)), # rect1 # rect2 + ) + ) # rect3 + + # Mask 2: + # |0123 + # -+---- + # 0|1100 + # 1|1111 + mask_data.append( + ( + (4, 2), # size + # Points to set for the 1 bounding rect. + (((0, 0), (1, 0), (0, 1), (1, 1), (2, 1), (3, 1)),), + ) + ) + + # Mask 3: + # |01234 + # -+----- + # 0|00100 + # 1|01110 + # 2|00100 + mask_data.append( + ( + (5, 3), # size + # Points to set for the 1 bounding rect. + (((2, 0), (1, 1), (2, 1), (3, 1), (2, 2)),), + ) + ) + + # Mask 4: + # |01234 + # -+----- + # 0|00010 + # 1|00100 + # 2|01000 + mask_data.append( + ( + (5, 3), # size + # Points to set for the 1 bounding rect. + (((3, 0), (2, 1), (1, 2)),), + ) + ) + + # Mask 5: + # |01234 + # -+----- + # 0|00011 + # 1|11111 + mask_data.append( + ( + (5, 2), # size + # Points to set for the 1 bounding rect. + (((3, 0), (4, 0), (0, 1), (1, 1), (2, 1), (3, 1)),), + ) + ) + + # Mask 6: + # |01234 + # -+----- + # 0|10001 + # 1|00100 + # 2|10001 + mask_data.append( + ( + (5, 3), # size + # Points to set for the 5 bounding rects. + ( + ((0, 0),), # rect1 + ((4, 0),), # rect2 + ((2, 1),), # rect3 + ((0, 2),), # rect4 + ((4, 2),), + ), + ) + ) # rect5 + + for size, rect_point_tuples in mask_data: + rects = [] + mask = pygame.Mask(size) + + for rect_points in rect_point_tuples: + rects.append(create_bounding_rect(rect_points)) + for pt in rect_points: + mask.set_at(pt) + + expected_rects = sorted(rects, key=tuple) + + rects = mask.get_bounding_rects() + + self.assertListEqual( + sorted(mask.get_bounding_rects(), key=tuple), + expected_rects, + f"size={size}", + ) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_to_surface(self): + """Ensures empty and full masks can be drawn onto surfaces.""" + expected_ref_count = 3 + size = (33, 65) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + test_fills = ((pygame.Color("white"), True), (pygame.Color("black"), False)) + + for expected_color, fill in test_fills: + surface.fill(surface_color) + mask = pygame.mask.Mask(size, fill=fill) + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__create_surface(self): + """Ensures empty and full masks can be drawn onto a created surface.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + size = (33, 65) + test_fills = ((pygame.Color("white"), True), (pygame.Color("black"), False)) + + for expected_color, fill in test_fills: + mask = pygame.mask.Mask(size, fill=fill) + + for use_arg in (True, False): + if use_arg: + to_surface = mask.to_surface(None) + else: + to_surface = mask.to_surface() + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__surface_param(self): + """Ensures to_surface accepts a surface arg/kwarg.""" + expected_ref_count = 4 + expected_color = pygame.Color("white") + surface_color = pygame.Color("red") + size = (5, 3) + mask = pygame.mask.Mask(size, fill=True) + surface = pygame.Surface(size) + kwargs = {"surface": surface} + + for use_kwargs in (True, False): + surface.fill(surface_color) + + if use_kwargs: + to_surface = mask.to_surface(**kwargs) + else: + to_surface = mask.to_surface(kwargs["surface"]) + + self.assertIs(to_surface, surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__setsurface_param(self): + """Ensures to_surface accepts a setsurface arg/kwarg.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + expected_color = pygame.Color("red") + size = (5, 3) + mask = pygame.mask.Mask(size, fill=True) + setsurface = pygame.Surface(size, expected_flag, expected_depth) + setsurface.fill(expected_color) + kwargs = {"setsurface": setsurface} + + for use_kwargs in (True, False): + if use_kwargs: + to_surface = mask.to_surface(**kwargs) + else: + to_surface = mask.to_surface(None, kwargs["setsurface"]) + + self.assertIsInstance(to_surface, pygame.Surface) + + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__unsetsurface_param(self): + """Ensures to_surface accepts a unsetsurface arg/kwarg.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + expected_color = pygame.Color("red") + size = (5, 3) + mask = pygame.mask.Mask(size) + unsetsurface = pygame.Surface(size, expected_flag, expected_depth) + unsetsurface.fill(expected_color) + kwargs = {"unsetsurface": unsetsurface} + + for use_kwargs in (True, False): + if use_kwargs: + to_surface = mask.to_surface(**kwargs) + else: + to_surface = mask.to_surface(None, None, kwargs["unsetsurface"]) + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__setcolor_param(self): + """Ensures to_surface accepts a setcolor arg/kwarg.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + expected_color = pygame.Color("red") + size = (5, 3) + mask = pygame.mask.Mask(size, fill=True) + kwargs = {"setcolor": expected_color} + + for use_kwargs in (True, False): + if use_kwargs: + to_surface = mask.to_surface(**kwargs) + else: + to_surface = mask.to_surface(None, None, None, kwargs["setcolor"]) + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__setcolor_default(self): + """Ensures the default setcolor is correct.""" + expected_color = pygame.Color("white") + size = (3, 7) + mask = pygame.mask.Mask(size, fill=True) + + to_surface = mask.to_surface( + surface=None, setsurface=None, unsetsurface=None, unsetcolor=None + ) + + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__unsetcolor_param(self): + """Ensures to_surface accepts a unsetcolor arg/kwarg.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + expected_color = pygame.Color("red") + size = (5, 3) + mask = pygame.mask.Mask(size) + kwargs = {"unsetcolor": expected_color} + + for use_kwargs in (True, False): + if use_kwargs: + to_surface = mask.to_surface(**kwargs) + else: + to_surface = mask.to_surface( + None, None, None, None, kwargs["unsetcolor"] + ) + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__unsetcolor_default(self): + """Ensures the default unsetcolor is correct.""" + expected_color = pygame.Color("black") + size = (3, 7) + mask = pygame.mask.Mask(size) + + to_surface = mask.to_surface( + surface=None, setsurface=None, unsetsurface=None, setcolor=None + ) + + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__dest_param(self): + """Ensures to_surface accepts a dest arg/kwarg.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + default_surface_color = (0, 0, 0, 0) + default_unsetcolor = pygame.Color("black") + dest = (0, 0) + size = (5, 3) + mask = pygame.mask.Mask(size) + kwargs = {"dest": dest} + + for use_kwargs in (True, False): + if use_kwargs: + expected_color = default_unsetcolor + + to_surface = mask.to_surface(**kwargs) + else: + expected_color = default_surface_color + + to_surface = mask.to_surface( + None, None, None, None, None, kwargs["dest"] + ) + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__dest_default(self): + """Ensures the default dest is correct.""" + expected_color = pygame.Color("white") + surface_color = pygame.Color("red") + + mask_size = (3, 2) + mask = pygame.mask.Mask(mask_size, fill=True) + mask_rect = mask.get_rect() + + # Make the surface bigger than the mask. + surf_size = (mask_size[0] + 2, mask_size[1] + 1) + surface = pygame.Surface(surf_size, SRCALPHA, 32) + surface.fill(surface_color) + + to_surface = mask.to_surface( + surface, setsurface=None, unsetsurface=None, unsetcolor=None + ) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), surf_size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect) + + @unittest.expectedFailure + def test_to_surface__area_param(self): + """Ensures to_surface accepts an area arg/kwarg.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + default_surface_color = (0, 0, 0, 0) + default_unsetcolor = pygame.Color("black") + size = (5, 3) + mask = pygame.mask.Mask(size) + kwargs = {"area": mask.get_rect()} + + for use_kwargs in (True, False): + if use_kwargs: + expected_color = default_unsetcolor + + to_surface = mask.to_surface(**kwargs) + else: + expected_color = default_surface_color + + to_surface = mask.to_surface( + None, None, None, None, None, (0, 0), kwargs["area"] + ) + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__area_default(self): + """Ensures the default area is correct.""" + expected_color = pygame.Color("white") + surface_color = pygame.Color("red") + + mask_size = (3, 2) + mask = pygame.mask.Mask(mask_size, fill=True) + mask_rect = mask.get_rect() + + # Make the surface bigger than the mask. The default area is the full + # area of the mask. + surf_size = (mask_size[0] + 2, mask_size[1] + 1) + surface = pygame.Surface(surf_size, SRCALPHA, 32) + surface.fill(surface_color) + + to_surface = mask.to_surface( + surface, setsurface=None, unsetsurface=None, unsetcolor=None + ) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), surf_size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect) + + def test_to_surface__kwargs(self): + """Ensures to_surface accepts the correct kwargs.""" + expected_color = pygame.Color("white") + size = (5, 3) + mask = pygame.mask.Mask(size, fill=True) + surface = pygame.Surface(size) + surface_color = pygame.Color("red") + setsurface = surface.copy() + setsurface.fill(expected_color) + + test_data = ( + (None, None), # None entry allows loop to test all kwargs on first pass. + ("dest", (0, 0)), + ("unsetcolor", pygame.Color("yellow")), + ("setcolor", expected_color), + ("unsetsurface", surface.copy()), + ("setsurface", setsurface), + ("surface", surface), + ) + + kwargs = dict(test_data) + + for name, _ in test_data: + kwargs.pop(name) + surface.fill(surface_color) # Clear for each test. + + to_surface = mask.to_surface(**kwargs) + + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__kwargs_create_surface(self): + """Ensures to_surface accepts the correct kwargs + when creating a surface. + """ + expected_color = pygame.Color("black") + size = (5, 3) + mask = pygame.mask.Mask(size) + setsurface = pygame.Surface(size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + unsetsurface = setsurface.copy() + unsetsurface.fill(expected_color) + + test_data = ( + (None, None), # None entry allows loop to test all kwargs on first pass. + ("dest", (0, 0)), + ("unsetcolor", expected_color), + ("setcolor", pygame.Color("yellow")), + ("unsetsurface", unsetsurface), + ("setsurface", setsurface), + ("surface", None), + ) + kwargs = dict(test_data) + + for name, _ in test_data: + kwargs.pop(name) + + to_surface = mask.to_surface(**kwargs) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__kwargs_order_independent(self): + """Ensures to_surface kwargs are not order dependent.""" + expected_color = pygame.Color("blue") + size = (3, 2) + mask = pygame.mask.Mask(size, fill=True) + surface = pygame.Surface(size) + + to_surface = mask.to_surface( + dest=(0, 0), + setcolor=expected_color, + unsetcolor=None, + surface=surface, + unsetsurface=pygame.Surface(size), + setsurface=None, + ) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__args_invalid_types(self): + """Ensures to_surface detects invalid kwarg types.""" + size = (3, 2) + mask = pygame.mask.Mask(size, fill=True) + invalid_surf = pygame.Color("green") + invalid_color = pygame.Surface(size) + + with self.assertRaises(TypeError): + # Invalid dest. + mask.to_surface(None, None, None, None, None, (0,)) + + with self.assertRaises(TypeError): + # Invalid unsetcolor. + mask.to_surface(None, None, None, None, invalid_color) + + with self.assertRaises(TypeError): + # Invalid setcolor. + mask.to_surface(None, None, None, invalid_color, None) + + with self.assertRaises(TypeError): + # Invalid unsetsurface. + mask.to_surface(None, None, invalid_surf, None, None) + + with self.assertRaises(TypeError): + # Invalid setsurface. + mask.to_surface(None, invalid_surf, None, None, None) + + with self.assertRaises(TypeError): + # Invalid surface. + mask.to_surface(invalid_surf, None, None, None, None) + + def test_to_surface__kwargs_invalid_types(self): + """Ensures to_surface detects invalid kwarg types.""" + size = (3, 2) + mask = pygame.mask.Mask(size) + + valid_kwargs = { + "surface": pygame.Surface(size), + "setsurface": pygame.Surface(size), + "unsetsurface": pygame.Surface(size), + "setcolor": pygame.Color("green"), + "unsetcolor": pygame.Color("green"), + "dest": (0, 0), + } + + invalid_kwargs = { + "surface": (1, 2, 3, 4), + "setsurface": pygame.Color("green"), + "unsetsurface": ((1, 2), (2, 1)), + "setcolor": pygame.Mask((1, 2)), + "unsetcolor": pygame.Surface((2, 2)), + "dest": (0, 0, 0), + } + + kwarg_order = ( + "surface", + "setsurface", + "unsetsurface", + "setcolor", + "unsetcolor", + "dest", + ) + + for kwarg in kwarg_order: + kwargs = dict(valid_kwargs) + kwargs[kwarg] = invalid_kwargs[kwarg] + + with self.assertRaises(TypeError): + mask.to_surface(**kwargs) + + def test_to_surface__kwargs_invalid_name(self): + """Ensures to_surface detects invalid kwarg names.""" + mask = pygame.mask.Mask((3, 2)) + kwargs = {"setcolour": pygame.Color("red")} + + with self.assertRaises(TypeError): + mask.to_surface(**kwargs) + + def test_to_surface__args_and_kwargs(self): + """Ensures to_surface accepts a combination of args/kwargs""" + size = (5, 3) + + surface_color = pygame.Color("red") + setsurface_color = pygame.Color("yellow") + unsetsurface_color = pygame.Color("blue") + setcolor = pygame.Color("green") + unsetcolor = pygame.Color("cyan") + + surface = pygame.Surface(size, SRCALPHA, 32) + setsurface = surface.copy() + unsetsurface = surface.copy() + + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + mask = pygame.mask.Mask(size, fill=True) + expected_color = setsurface_color + + test_data = ( + (None, None), # None entry allows loop to test all kwargs on first pass. + ("surface", surface), + ("setsurface", setsurface), + ("unsetsurface", unsetsurface), + ("setcolor", setcolor), + ("unsetcolor", unsetcolor), + ("dest", (0, 0)), + ) + + args = [] + kwargs = dict(test_data) + + # Loop gradually moves the kwargs to args. + for name, value in test_data: + if name is not None: + args.append(value) + kwargs.pop(name) + + surface.fill(surface_color) + + to_surface = mask.to_surface(*args, **kwargs) + + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__valid_setcolor_formats(self): + """Ensures to_surface handles valid setcolor formats correctly.""" + size = (5, 3) + mask = pygame.mask.Mask(size, fill=True) + surface = pygame.Surface(size, SRCALPHA, 32) + expected_color = pygame.Color("green") + test_colors = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(expected_color), + expected_color, + "green", + "#00FF00FF", + "0x00FF00FF", + ) + + for setcolor in test_colors: + to_surface = mask.to_surface(setcolor=setcolor) + + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__valid_unsetcolor_formats(self): + """Ensures to_surface handles valid unsetcolor formats correctly.""" + size = (5, 3) + mask = pygame.mask.Mask(size) + surface = pygame.Surface(size, SRCALPHA, 32) + expected_color = pygame.Color("green") + test_colors = ( + (0, 255, 0), + (0, 255, 0, 255), + surface.map_rgb(expected_color), + expected_color, + "green", + "#00FF00FF", + "0x00FF00FF", + ) + + for unsetcolor in test_colors: + to_surface = mask.to_surface(unsetcolor=unsetcolor) + + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__invalid_setcolor_formats(self): + """Ensures to_surface handles invalid setcolor formats correctly.""" + mask = pygame.mask.Mask((5, 3)) + + for setcolor in ("green color", "#00FF00FF0", "0x00FF00FF0", (1, 2)): + with self.assertRaises(ValueError): + mask.to_surface(setcolor=setcolor) + + for setcolor in (pygame.Surface((1, 2)), pygame.Mask((2, 1)), 1.1): + with self.assertRaises(TypeError): + mask.to_surface(setcolor=setcolor) + + def test_to_surface__invalid_unsetcolor_formats(self): + """Ensures to_surface handles invalid unsetcolor formats correctly.""" + mask = pygame.mask.Mask((5, 3)) + + for unsetcolor in ("green color", "#00FF00FF0", "0x00FF00FF0", (1, 2)): + with self.assertRaises(ValueError): + mask.to_surface(unsetcolor=unsetcolor) + + for unsetcolor in (pygame.Surface((1, 2)), pygame.Mask((2, 1)), 1.1): + with self.assertRaises(TypeError): + mask.to_surface(unsetcolor=unsetcolor) + + def test_to_surface__valid_dest_formats(self): + """Ensures to_surface handles valid dest formats correctly.""" + expected_color = pygame.Color("white") + mask = pygame.mask.Mask((3, 5), fill=True) + dests = ( + (0, 0), + [0, 0], + Vector2(0, 0), + (0, 0, 100, 100), + pygame.Rect((0, 0), (10, 10)), + ) + + for dest in dests: + to_surface = mask.to_surface(dest=dest) + + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__invalid_dest_formats(self): + """Ensures to_surface handles invalid dest formats correctly.""" + mask = pygame.mask.Mask((3, 5)) + invalid_dests = ( + (0,), # Incorrect size. + (0, 0, 0), # Incorrect size. + {0, 1}, # Incorrect type. + {0: 1}, # Incorrect type. + Rect, + ) # Incorrect type. + + for dest in invalid_dests: + with self.assertRaises(TypeError): + mask.to_surface(dest=dest) + + def test_to_surface__negative_sized_dest_rect(self): + """Ensures to_surface correctly handles negative sized dest rects.""" + expected_color = pygame.Color("white") + mask = pygame.mask.Mask((3, 5), fill=True) + dests = ( + pygame.Rect((0, 0), (10, -10)), + pygame.Rect((0, 0), (-10, 10)), + pygame.Rect((0, 0), (-10, -10)), + ) + + for dest in dests: + to_surface = mask.to_surface(dest=dest) + + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__zero_sized_dest_rect(self): + """Ensures to_surface correctly handles zero sized dest rects.""" + expected_color = pygame.Color("white") + mask = pygame.mask.Mask((3, 5), fill=True) + dests = ( + pygame.Rect((0, 0), (0, 10)), + pygame.Rect((0, 0), (10, 0)), + pygame.Rect((0, 0), (0, 0)), + ) + + for dest in dests: + to_surface = mask.to_surface(dest=dest) + + assertSurfaceFilled(self, to_surface, expected_color) + + @unittest.expectedFailure + def test_to_surface__valid_area_formats(self): + """Ensures to_surface handles valid area formats correctly.""" + size = (3, 5) + surface_color = pygame.Color("red") + expected_color = pygame.Color("white") + surface = pygame.Surface(size) + mask = pygame.mask.Mask(size, fill=True) + area_pos = (0, 0) + area_size = (2, 1) + areas = ( + (area_pos[0], area_pos[1], area_size[0], area_size[1]), + (area_pos, area_size), + (area_pos, list(area_size)), + (list(area_pos), area_size), + (list(area_pos), list(area_size)), + [area_pos[0], area_pos[1], area_size[0], area_size[1]], + [area_pos, area_size], + [area_pos, list(area_size)], + [list(area_pos), area_size], + [list(area_pos), list(area_size)], + pygame.Rect(area_pos, area_size), + ) + + for area in areas: + surface.fill(surface_color) + area_rect = pygame.Rect(area) + + to_surface = mask.to_surface(surface, area=area) + + assertSurfaceFilled(self, to_surface, expected_color, area_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, area_rect) + + @unittest.expectedFailure + def test_to_surface__invalid_area_formats(self): + """Ensures to_surface handles invalid area formats correctly.""" + mask = pygame.mask.Mask((3, 5)) + invalid_areas = ( + (0,), # Incorrect size. + (0, 0), # Incorrect size. + (0, 0, 1), # Incorrect size. + ((0, 0), (1,)), # Incorrect size. + ((0,), (1, 1)), # Incorrect size. + {0, 1, 2, 3}, # Incorrect type. + {0: 1, 2: 3}, # Incorrect type. + Rect, # Incorrect type. + ) + + for area in invalid_areas: + with self.assertRaisesRegex(TypeError, "invalid area argument"): + unused_to_surface = mask.to_surface(area=area) + + @unittest.expectedFailure + def test_to_surface__negative_sized_area_rect(self): + """Ensures to_surface correctly handles negative sized area rects.""" + size = (3, 5) + surface_color = pygame.Color("red") + expected_color = pygame.Color("white") + surface = pygame.Surface(size) + mask = pygame.mask.Mask(size) + mask.set_at((0, 0)) + + # These rects should cause position (0, 0) of the mask to be drawn. + areas = ( + pygame.Rect((0, 1), (1, -1)), + pygame.Rect((1, 0), (-1, 1)), + pygame.Rect((1, 1), (-1, -1)), + ) + + for area in areas: + surface.fill(surface_color) + + to_surface = mask.to_surface(surface, area=area) + + assertSurfaceFilled(self, to_surface, expected_color, area) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, area) + + @unittest.expectedFailure + def test_to_surface__zero_sized_area_rect(self): + """Ensures to_surface correctly handles zero sized area rects.""" + size = (3, 5) + expected_color = pygame.Color("red") + surface = pygame.Surface(size) + mask = pygame.mask.Mask(size, fill=True) + + # Zero sized rect areas should cause none of the mask to be drawn. + areas = ( + pygame.Rect((0, 0), (0, 1)), + pygame.Rect((0, 0), (1, 0)), + pygame.Rect((0, 0), (0, 0)), + ) + + for area in areas: + surface.fill(expected_color) + + to_surface = mask.to_surface(surface, area=area) + + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__default_surface_with_param_combinations(self): + """Ensures to_surface works with a default surface value + and combinations of other parameters. + + This tests many different parameter combinations with full and empty + masks. + """ + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + size = (5, 3) + dest = (0, 0) + + default_surface_color = (0, 0, 0, 0) + setsurface_color = pygame.Color("yellow") + unsetsurface_color = pygame.Color("blue") + setcolor = pygame.Color("green") + unsetcolor = pygame.Color("cyan") + + setsurface = pygame.Surface(size, expected_flag, expected_depth) + unsetsurface = setsurface.copy() + + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + kwargs = { + "setsurface": None, + "unsetsurface": None, + "setcolor": None, + "unsetcolor": None, + "dest": None, + } + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + + # Test different combinations of parameters. + for setsurface_param in (setsurface, None): + kwargs["setsurface"] = setsurface_param + + for unsetsurface_param in (unsetsurface, None): + kwargs["unsetsurface"] = unsetsurface_param + + for setcolor_param in (setcolor, None): + kwargs["setcolor"] = setcolor_param + + for unsetcolor_param in (unsetcolor, None): + kwargs["unsetcolor"] = unsetcolor_param + + for dest_param in (dest, None): + if dest_param is None: + kwargs.pop("dest", None) + else: + kwargs["dest"] = dest_param + + if fill: + if setsurface_param is not None: + expected_color = setsurface_color + elif setcolor_param is not None: + expected_color = setcolor + else: + expected_color = default_surface_color + else: + if unsetsurface_param is not None: + expected_color = unsetsurface_color + elif unsetcolor_param is not None: + expected_color = unsetcolor + else: + expected_color = default_surface_color + + to_surface = mask.to_surface(**kwargs) + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual( + sys.getrefcount(to_surface), expected_ref_count + ) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual( + to_surface.get_bitsize(), expected_depth + ) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__surface_with_param_combinations(self): + """Ensures to_surface works with a surface value + and combinations of other parameters. + + This tests many different parameter combinations with full and empty + masks. + """ + expected_ref_count = 4 + expected_flag = SRCALPHA + expected_depth = 32 + size = (5, 3) + dest = (0, 0) + + surface_color = pygame.Color("red") + setsurface_color = pygame.Color("yellow") + unsetsurface_color = pygame.Color("blue") + setcolor = pygame.Color("green") + unsetcolor = pygame.Color("cyan") + + surface = pygame.Surface(size, expected_flag, expected_depth) + setsurface = surface.copy() + unsetsurface = surface.copy() + + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + kwargs = { + "surface": surface, + "setsurface": None, + "unsetsurface": None, + "setcolor": None, + "unsetcolor": None, + "dest": None, + } + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + + # Test different combinations of parameters. + for setsurface_param in (setsurface, None): + kwargs["setsurface"] = setsurface_param + + for unsetsurface_param in (unsetsurface, None): + kwargs["unsetsurface"] = unsetsurface_param + + for setcolor_param in (setcolor, None): + kwargs["setcolor"] = setcolor_param + + for unsetcolor_param in (unsetcolor, None): + kwargs["unsetcolor"] = unsetcolor_param + surface.fill(surface_color) # Clear for each test. + + for dest_param in (dest, None): + if dest_param is None: + kwargs.pop("dest", None) + else: + kwargs["dest"] = dest_param + + if fill: + if setsurface_param is not None: + expected_color = setsurface_color + elif setcolor_param is not None: + expected_color = setcolor + else: + expected_color = surface_color + else: + if unsetsurface_param is not None: + expected_color = unsetsurface_color + elif unsetcolor_param is not None: + expected_color = unsetcolor + else: + expected_color = surface_color + + to_surface = mask.to_surface(**kwargs) + + self.assertIs(to_surface, surface) + if not IS_PYPY: + self.assertEqual( + sys.getrefcount(to_surface), expected_ref_count + ) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual( + to_surface.get_bitsize(), expected_depth + ) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__set_and_unset_bits(self): + """Ensures that to_surface works correctly with with set/unset bits + when using the defaults for setcolor and unsetcolor. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + width, height = size = (10, 20) + mask = pygame.mask.Mask(size) + mask_rect = mask.get_rect() + + surface = pygame.Surface(size) + surface_color = pygame.Color("red") + + # Create a checkerboard pattern of set/unset bits. + for pos in ((x, y) for x in range(width) for y in range(x & 1, height, 2)): + mask.set_at(pos) + + # Test different dest values. + for dest in self.ORIGIN_OFFSETS: + mask_rect.topleft = dest + surface.fill(surface_color) + + to_surface = mask.to_surface(surface, dest=dest) + + to_surface.lock() # Lock for possible speed up. + for pos in ((x, y) for x in range(width) for y in range(height)): + mask_pos = (pos[0] - dest[0], pos[1] - dest[1]) + if not mask_rect.collidepoint(pos): + expected_color = surface_color + elif mask.get_at(mask_pos): + expected_color = default_setcolor + else: + expected_color = default_unsetcolor + + self.assertEqual(to_surface.get_at(pos), expected_color, (dest, pos)) + to_surface.unlock() + + def test_to_surface__set_and_unset_bits_with_setsurface_unsetsurface(self): + """Ensures that to_surface works correctly with with set/unset bits + when using setsurface and unsetsurface. + """ + width, height = size = (10, 20) + mask = pygame.mask.Mask(size) + mask_rect = mask.get_rect() + + surface = pygame.Surface(size) + surface_color = pygame.Color("red") + + setsurface = surface.copy() + setsurface_color = pygame.Color("green") + setsurface.fill(setsurface_color) + + unsetsurface = surface.copy() + unsetsurface_color = pygame.Color("blue") + unsetsurface.fill(unsetsurface_color) + + # Create a checkerboard pattern of set/unset bits. + for pos in ((x, y) for x in range(width) for y in range(x & 1, height, 2)): + mask.set_at(pos) + + # Test different dest values. + for dest in self.ORIGIN_OFFSETS: + mask_rect.topleft = dest + + # Tests the color parameters set to None and also as their + # default values. Should have no effect as they are not being + # used, but this exercises different to_surface() code. + for disable_color_params in (True, False): + surface.fill(surface_color) # Clear for each test. + + if disable_color_params: + to_surface = mask.to_surface( + surface, + dest=dest, + setsurface=setsurface, + unsetsurface=unsetsurface, + setcolor=None, + unsetcolor=None, + ) + else: + to_surface = mask.to_surface( + surface, + dest=dest, + setsurface=setsurface, + unsetsurface=unsetsurface, + ) + + to_surface.lock() # Lock for possible speed up. + + for pos in ((x, y) for x in range(width) for y in range(height)): + mask_pos = (pos[0] - dest[0], pos[1] - dest[1]) + + if not mask_rect.collidepoint(pos): + expected_color = surface_color + elif mask.get_at(mask_pos): + expected_color = setsurface_color + else: + expected_color = unsetsurface_color + + self.assertEqual(to_surface.get_at(pos), expected_color) + to_surface.unlock() + + def test_to_surface__surface_narrower_than_mask(self): + """Ensures that surfaces narrower than the mask work correctly. + + For this test the surface's width is less than the mask's width. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 20) + narrow_size = (6, 20) + + surface = pygame.Surface(narrow_size) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + surface.fill(surface_color) # Clear for each test. + expected_color = default_setcolor if fill else default_unsetcolor + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), narrow_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__setsurface_narrower_than_mask(self): + """Ensures that setsurfaces narrower than the mask work correctly. + + For this test the setsurface's width is less than the mask's width. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 20) + narrow_size = (6, 20) + + setsurface = pygame.Surface(narrow_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + setsurface_rect = setsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, setsurface_color, setsurface_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_setcolor, setsurface_rect + ) + else: + assertSurfaceFilled(self, to_surface, default_unsetcolor) + + def test_to_surface__unsetsurface_narrower_than_mask(self): + """Ensures that unsetsurfaces narrower than the mask work correctly. + + For this test the unsetsurface's width is less than the mask's width. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 20) + narrow_size = (6, 20) + + unsetsurface = pygame.Surface(narrow_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + unsetsurface_rect = unsetsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, default_setcolor) + else: + assertSurfaceFilled( + self, to_surface, unsetsurface_color, unsetsurface_rect + ) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_unsetcolor, unsetsurface_rect + ) + + def test_to_surface__setsurface_narrower_than_mask_and_colors_none(self): + """Ensures that setsurfaces narrower than the mask work correctly + when setcolor and unsetcolor are set to None. + + For this test the setsurface's width is less than the mask's width. + """ + default_surface_color = (0, 0, 0, 0) + mask_size = (10, 20) + narrow_size = (6, 20) + + setsurface = pygame.Surface(narrow_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + setsurface_rect = setsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface( + setsurface=setsurface, setcolor=None, unsetcolor=None + ) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, setsurface_color, setsurface_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_surface_color, setsurface_rect + ) + else: + assertSurfaceFilled(self, to_surface, default_surface_color) + + def test_to_surface__unsetsurface_narrower_than_mask_and_colors_none(self): + """Ensures that unsetsurfaces narrower than the mask work correctly + when setcolor and unsetcolor are set to None. + + For this test the unsetsurface's width is less than the mask's width. + """ + default_surface_color = (0, 0, 0, 0) + mask_size = (10, 20) + narrow_size = (6, 20) + + unsetsurface = pygame.Surface(narrow_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + unsetsurface_rect = unsetsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface( + unsetsurface=unsetsurface, setcolor=None, unsetcolor=None + ) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, default_surface_color) + else: + assertSurfaceFilled( + self, to_surface, unsetsurface_color, unsetsurface_rect + ) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_surface_color, unsetsurface_rect + ) + + def test_to_surface__surface_wider_than_mask(self): + """Ensures that surfaces wider than the mask work correctly. + + For this test the surface's width is greater than the mask's width. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (6, 15) + wide_size = (11, 15) + + surface = pygame.Surface(wide_size) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + mask_rect = mask.get_rect() + surface.fill(surface_color) # Clear for each test. + expected_color = default_setcolor if fill else default_unsetcolor + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), wide_size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect) + + def test_to_surface__setsurface_wider_than_mask(self): + """Ensures that setsurfaces wider than the mask work correctly. + + For this test the setsurface's width is greater than the mask's width. + """ + default_unsetcolor = pygame.Color("black") + mask_size = (6, 15) + wide_size = (11, 15) + + setsurface = pygame.Surface(wide_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + expected_color = setsurface_color if fill else default_unsetcolor + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__unsetsurface_wider_than_mask(self): + """Ensures that unsetsurfaces wider than the mask work correctly. + + For this test the unsetsurface's width is greater than the mask's + width. + """ + default_setcolor = pygame.Color("white") + mask_size = (6, 15) + wide_size = (11, 15) + + unsetsurface = pygame.Surface(wide_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + expected_color = default_setcolor if fill else unsetsurface_color + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__surface_shorter_than_mask(self): + """Ensures that surfaces shorter than the mask work correctly. + + For this test the surface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 11) + short_size = (10, 6) + + surface = pygame.Surface(short_size) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + surface.fill(surface_color) # Clear for each test. + expected_color = default_setcolor if fill else default_unsetcolor + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), short_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__setsurface_shorter_than_mask(self): + """Ensures that setsurfaces shorter than the mask work correctly. + + For this test the setsurface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 11) + short_size = (10, 6) + + setsurface = pygame.Surface(short_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + setsurface_rect = setsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, setsurface_color, setsurface_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_setcolor, setsurface_rect + ) + else: + assertSurfaceFilled(self, to_surface, default_unsetcolor) + + def test_to_surface__unsetsurface_shorter_than_mask(self): + """Ensures that unsetsurfaces shorter than the mask work correctly. + + For this test the unsetsurface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 11) + short_size = (10, 6) + + unsetsurface = pygame.Surface(short_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + unsetsurface_rect = unsetsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, default_setcolor) + else: + assertSurfaceFilled( + self, to_surface, unsetsurface_color, unsetsurface_rect + ) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_unsetcolor, unsetsurface_rect + ) + + def test_to_surface__setsurface_shorter_than_mask_and_colors_none(self): + """Ensures that setsurfaces shorter than the mask work correctly + when setcolor and unsetcolor are set to None. + + For this test the setsurface's height is less than the mask's height. + """ + default_surface_color = (0, 0, 0, 0) + mask_size = (10, 11) + short_size = (10, 6) + + setsurface = pygame.Surface(short_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + setsurface_rect = setsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface( + setsurface=setsurface, setcolor=None, unsetcolor=None + ) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, setsurface_color, setsurface_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_surface_color, setsurface_rect + ) + else: + assertSurfaceFilled(self, to_surface, default_surface_color) + + def test_to_surface__unsetsurface_shorter_than_mask_and_colors_none(self): + """Ensures that unsetsurfaces shorter than the mask work correctly + when setcolor and unsetcolor are set to None. + + For this test the unsetsurface's height is less than the mask's height. + """ + default_surface_color = (0, 0, 0, 0) + mask_size = (10, 11) + short_size = (10, 6) + + unsetsurface = pygame.Surface(short_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + unsetsurface_rect = unsetsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface( + unsetsurface=unsetsurface, setcolor=None, unsetcolor=None + ) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, default_surface_color) + else: + assertSurfaceFilled( + self, to_surface, unsetsurface_color, unsetsurface_rect + ) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_surface_color, unsetsurface_rect + ) + + def test_to_surface__surface_taller_than_mask(self): + """Ensures that surfaces taller than the mask work correctly. + + For this test the surface's height is greater than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 6) + tall_size = (10, 11) + + surface = pygame.Surface(tall_size) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + mask_rect = mask.get_rect() + surface.fill(surface_color) # Clear for each test. + expected_color = default_setcolor if fill else default_unsetcolor + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), tall_size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect) + + def test_to_surface__setsurface_taller_than_mask(self): + """Ensures that setsurfaces taller than the mask work correctly. + + For this test the setsurface's height is greater than the mask's + height. + """ + default_unsetcolor = pygame.Color("black") + mask_size = (10, 6) + tall_size = (10, 11) + + setsurface = pygame.Surface(tall_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + expected_color = setsurface_color if fill else default_unsetcolor + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__unsetsurface_taller_than_mask(self): + """Ensures that unsetsurfaces taller than the mask work correctly. + + For this test the unsetsurface's height is greater than the mask's + height. + """ + default_setcolor = pygame.Color("white") + mask_size = (10, 6) + tall_size = (10, 11) + + unsetsurface = pygame.Surface(tall_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + expected_color = default_setcolor if fill else unsetsurface_color + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__surface_wider_and_taller_than_mask(self): + """Ensures that surfaces wider and taller than the mask work correctly. + + For this test the surface's width is greater than the mask's width and + the surface's height is greater than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (6, 8) + wide_tall_size = (11, 15) + + surface = pygame.Surface(wide_tall_size) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + mask_rect = mask.get_rect() + surface.fill(surface_color) # Clear for each test. + expected_color = default_setcolor if fill else default_unsetcolor + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), wide_tall_size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect) + + def test_to_surface__setsurface_wider_and_taller_than_mask(self): + """Ensures that setsurfaces wider and taller than the mask work + correctly. + + For this test the setsurface's width is greater than the mask's width + and the setsurface's height is greater than the mask's height. + """ + default_unsetcolor = pygame.Color("black") + mask_size = (6, 8) + wide_tall_size = (11, 15) + + setsurface = pygame.Surface(wide_tall_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + expected_color = setsurface_color if fill else default_unsetcolor + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__unsetsurface_wider_and_taller_than_mask(self): + """Ensures that unsetsurfaces wider and taller than the mask work + correctly. + + For this test the unsetsurface's width is greater than the mask's width + and the unsetsurface's height is greater than the mask's height. + """ + default_setcolor = pygame.Color("white") + mask_size = (6, 8) + wide_tall_size = (11, 15) + + unsetsurface = pygame.Surface(wide_tall_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + expected_color = default_setcolor if fill else unsetsurface_color + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__surface_wider_and_shorter_than_mask(self): + """Ensures that surfaces wider and shorter than the mask work + correctly. + + For this test the surface's width is greater than the mask's width and + the surface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (7, 11) + wide_short_size = (13, 6) + + surface = pygame.Surface(wide_short_size) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + mask_rect = mask.get_rect() + surface.fill(surface_color) # Clear for each test. + expected_color = default_setcolor if fill else default_unsetcolor + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), wide_short_size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect) + + def test_to_surface__setsurface_wider_and_shorter_than_mask(self): + """Ensures that setsurfaces wider and shorter than the mask work + correctly. + + For this test the setsurface's width is greater than the mask's width + and the setsurface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (7, 11) + wide_short_size = (10, 6) + + setsurface = pygame.Surface(wide_short_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + setsurface_rect = setsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, setsurface_color, setsurface_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_setcolor, setsurface_rect + ) + else: + assertSurfaceFilled(self, to_surface, default_unsetcolor) + + def test_to_surface__unsetsurface_wider_and_shorter_than_mask(self): + """Ensures that unsetsurfaces wider and shorter than the mask work + correctly. + + For this test the unsetsurface's width is greater than the mask's width + and the unsetsurface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (7, 11) + wide_short_size = (10, 6) + + unsetsurface = pygame.Surface(wide_short_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + unsetsurface_rect = unsetsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, default_setcolor) + else: + assertSurfaceFilled( + self, to_surface, unsetsurface_color, unsetsurface_rect + ) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_unsetcolor, unsetsurface_rect + ) + + def test_to_surface__surface_narrower_and_taller_than_mask(self): + """Ensures that surfaces narrower and taller than the mask work + correctly. + + For this test the surface's width is less than the mask's width and + the surface's height is greater than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 8) + narrow_tall_size = (6, 15) + + surface = pygame.Surface(narrow_tall_size) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + mask_rect = mask.get_rect() + surface.fill(surface_color) # Clear for each test. + expected_color = default_setcolor if fill else default_unsetcolor + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), narrow_tall_size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect) + + def test_to_surface__setsurface_narrower_and_taller_than_mask(self): + """Ensures that setsurfaces narrower and taller than the mask work + correctly. + + For this test the setsurface's width is less than the mask's width + and the setsurface's height is greater than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 8) + narrow_tall_size = (6, 15) + + setsurface = pygame.Surface(narrow_tall_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + setsurface_rect = setsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, setsurface_color, setsurface_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_setcolor, setsurface_rect + ) + else: + assertSurfaceFilled(self, to_surface, default_unsetcolor) + + def test_to_surface__unsetsurface_narrower_and_taller_than_mask(self): + """Ensures that unsetsurfaces narrower and taller than the mask work + correctly. + + For this test the unsetsurface's width is less than the mask's width + and the unsetsurface's height is greater than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 8) + narrow_tall_size = (6, 15) + + unsetsurface = pygame.Surface(narrow_tall_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + unsetsurface_rect = unsetsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, default_setcolor) + else: + assertSurfaceFilled( + self, to_surface, unsetsurface_color, unsetsurface_rect + ) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_unsetcolor, unsetsurface_rect + ) + + def test_to_surface__surface_narrower_and_shorter_than_mask(self): + """Ensures that surfaces narrower and shorter than the mask work + correctly. + + For this test the surface's width is less than the mask's width and + the surface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 18) + narrow_short_size = (6, 15) + + surface = pygame.Surface(narrow_short_size) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + mask_rect = mask.get_rect() + surface.fill(surface_color) # Clear for each test. + expected_color = default_setcolor if fill else default_unsetcolor + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), narrow_short_size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect) + + def test_to_surface__setsurface_narrower_and_shorter_than_mask(self): + """Ensures that setsurfaces narrower and shorter than the mask work + correctly. + + For this test the setsurface's width is less than the mask's width + and the setsurface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 18) + narrow_short_size = (6, 15) + + setsurface = pygame.Surface(narrow_short_size, SRCALPHA, 32) + setsurface_color = pygame.Color("red") + setsurface.fill(setsurface_color) + setsurface_rect = setsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, setsurface_color, setsurface_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_setcolor, setsurface_rect + ) + else: + assertSurfaceFilled(self, to_surface, default_unsetcolor) + + def test_to_surface__unsetsurface_narrower_and_shorter_than_mask(self): + """Ensures that unsetsurfaces narrower and shorter than the mask work + correctly. + + For this test the unsetsurface's width is less than the mask's width + and the unsetsurface's height is less than the mask's height. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + mask_size = (10, 18) + narrow_short_size = (6, 15) + + unsetsurface = pygame.Surface(narrow_short_size, SRCALPHA, 32) + unsetsurface_color = pygame.Color("red") + unsetsurface.fill(unsetsurface_color) + unsetsurface_rect = unsetsurface.get_rect() + + for fill in (True, False): + mask = pygame.mask.Mask(mask_size, fill=fill) + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + # Different checks depending on if the mask was filled or not. + if fill: + assertSurfaceFilled(self, to_surface, default_setcolor) + else: + assertSurfaceFilled( + self, to_surface, unsetsurface_color, unsetsurface_rect + ) + assertSurfaceFilledIgnoreArea( + self, to_surface, default_unsetcolor, unsetsurface_rect + ) + + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_to_surface__all_surfaces_different_sizes_than_mask(self): + """Ensures that all the surface parameters can be of different sizes.""" + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + surface_color = pygame.Color("red") + setsurface_color = pygame.Color("green") + unsetsurface_color = pygame.Color("blue") + + mask_size = (10, 15) + surface_size = (11, 14) + setsurface_size = (9, 8) + unsetsurface_size = (12, 16) + + surface = pygame.Surface(surface_size) + setsurface = pygame.Surface(setsurface_size) + unsetsurface = pygame.Surface(unsetsurface_size) + + surface.fill(surface_color) + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + surface_rect = surface.get_rect() + setsurface_rect = setsurface.get_rect() + unsetsurface_rect = unsetsurface.get_rect() + + # Create a mask that is filled except for a rect in the center. + mask = pygame.mask.Mask(mask_size, fill=True) + mask_rect = mask.get_rect() + unfilled_rect = pygame.Rect((0, 0), (4, 5)) + unfilled_rect.center = mask_rect.center + + for pos in ( + (x, y) + for x in range(unfilled_rect.x, unfilled_rect.w) + for y in range(unfilled_rect.y, unfilled_rect.h) + ): + mask.set_at(pos, 0) + + to_surface = mask.to_surface(surface, setsurface, unsetsurface) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), surface_size) + + # Check each surface pixel for the correct color. + to_surface.lock() # Lock for possible speed up. + + for pos in ( + (x, y) for x in range(surface_rect.w) for y in range(surface_rect.h) + ): + if not mask_rect.collidepoint(pos): + expected_color = surface_color + elif mask.get_at(pos): + # Checking set bit colors. + if setsurface_rect.collidepoint(pos): + expected_color = setsurface_color + else: + expected_color = default_setcolor + else: + # Checking unset bit colors. + if unsetsurface_rect.collidepoint(pos): + expected_color = unsetsurface_color + else: + expected_color = default_unsetcolor + + self.assertEqual(to_surface.get_at(pos), expected_color) + + to_surface.unlock() + + def test_to_surface__dest_locations(self): + """Ensures dest values can be different locations on/off the surface.""" + SIDE = 7 + surface = pygame.Surface((SIDE, SIDE)) + surface_rect = surface.get_rect() + dest_rect = surface_rect.copy() + + surface_color = pygame.Color("red") + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + + directions = ( + ((s, 0) for s in range(-SIDE, SIDE + 1)), # left to right + ((0, s) for s in range(-SIDE, SIDE + 1)), # top to bottom + ((s, s) for s in range(-SIDE, SIDE + 1)), # topleft to bottomright diag + ((-s, s) for s in range(-SIDE, SIDE + 1)), # topright to bottomleft diag + ) + + for fill in (True, False): + mask = pygame.mask.Mask((SIDE, SIDE), fill=fill) + expected_color = default_setcolor if fill else default_unsetcolor + + for direction in directions: + for pos in direction: + dest_rect.topleft = pos + overlap_rect = dest_rect.clip(surface_rect) + surface.fill(surface_color) + + to_surface = mask.to_surface(surface, dest=dest_rect) + + assertSurfaceFilled(self, to_surface, expected_color, overlap_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, overlap_rect + ) + + @unittest.expectedFailure + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_to_surface__area_locations(self): + """Ensures area rects can be different locations on/off the mask.""" + SIDE = 7 + surface = pygame.Surface((SIDE, SIDE)) + + surface_color = pygame.Color("red") + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + + directions = ( + ((s, 0) for s in range(-SIDE, SIDE + 1)), # left to right + ((0, s) for s in range(-SIDE, SIDE + 1)), # top to bottom + ((s, s) for s in range(-SIDE, SIDE + 1)), # topleft to bottomright diag + ((-s, s) for s in range(-SIDE, SIDE + 1)), # topright to bottomleft diag + ) + + for fill in (True, False): + mask = pygame.mask.Mask((SIDE, SIDE), fill=fill) + mask_rect = mask.get_rect() + area_rect = mask_rect.copy() + expected_color = default_setcolor if fill else default_unsetcolor + + for direction in directions: + for pos in direction: + area_rect.topleft = pos + overlap_rect = area_rect.clip(mask_rect) + overlap_rect.topleft = (0, 0) + surface.fill(surface_color) + + to_surface = mask.to_surface(surface, area=area_rect) + + assertSurfaceFilled(self, to_surface, expected_color, overlap_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, overlap_rect + ) + + @unittest.expectedFailure + def test_to_surface__dest_and_area_locations(self): + """Ensures dest/area values can be different locations on/off the + surface/mask. + """ + SIDE = 5 + surface = pygame.Surface((SIDE, SIDE)) + surface_rect = surface.get_rect() + dest_rect = surface_rect.copy() + + surface_color = pygame.Color("red") + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + + dest_directions = ( + ((s, 0) for s in range(-SIDE, SIDE + 1)), # left to right + ((0, s) for s in range(-SIDE, SIDE + 1)), # top to bottom + ((s, s) for s in range(-SIDE, SIDE + 1)), # topleft to bottomright diag + ((-s, s) for s in range(-SIDE, SIDE + 1)), # topright to bottomleft diag + ) + + # Using only the topleft to bottomright diagonal to test the area (to + # reduce the number of loop iterations). + area_positions = list(dest_directions[2]) + + for fill in (True, False): + mask = pygame.mask.Mask((SIDE, SIDE), fill=fill) + mask_rect = mask.get_rect() + area_rect = mask_rect.copy() + expected_color = default_setcolor if fill else default_unsetcolor + + for dest_direction in dest_directions: + for dest_pos in dest_direction: + dest_rect.topleft = dest_pos + + for area_pos in area_positions: + area_rect.topleft = area_pos + area_overlap_rect = area_rect.clip(mask_rect) + area_overlap_rect.topleft = dest_rect.topleft + dest_overlap_rect = dest_rect.clip(area_overlap_rect) + + surface.fill(surface_color) + + to_surface = mask.to_surface( + surface, dest=dest_rect, area=area_rect + ) + + assertSurfaceFilled( + self, to_surface, expected_color, dest_overlap_rect + ) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, dest_overlap_rect + ) + + @unittest.expectedFailure + def test_to_surface__area_sizes(self): + """Ensures area rects can be different sizes.""" + SIDE = 7 + SIZES = ( + (0, 0), + (0, 1), + (1, 0), + (1, 1), + (SIDE - 1, SIDE - 1), + (SIDE - 1, SIDE), + (SIDE, SIDE - 1), + (SIDE, SIDE), + (SIDE + 1, SIDE), + (SIDE, SIDE + 1), + (SIDE + 1, SIDE + 1), + ) + + surface = pygame.Surface((SIDE, SIDE)) + surface_color = pygame.Color("red") + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + + for fill in (True, False): + mask = pygame.mask.Mask((SIDE, SIDE), fill=fill) + mask_rect = mask.get_rect() + expected_color = default_setcolor if fill else default_unsetcolor + + for size in SIZES: + area_rect = pygame.Rect((0, 0), size) + + for pos in self.ORIGIN_OFFSETS: + area_rect.topleft = pos + overlap_rect = area_rect.clip(mask_rect) + overlap_rect.topleft = (0, 0) + surface.fill(surface_color) + + to_surface = mask.to_surface(surface, area=area_rect) + + assertSurfaceFilled(self, to_surface, expected_color, overlap_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, overlap_rect + ) + + def test_to_surface__surface_color_alphas(self): + """Ensures the setsurface/unsetsurface color alpha values are respected.""" + size = (13, 17) + setsurface_color = pygame.Color("green") + setsurface_color.a = 53 + unsetsurface_color = pygame.Color("blue") + unsetsurface_color.a = 109 + + setsurface = pygame.Surface(size, flags=SRCALPHA, depth=32) + unsetsurface = pygame.Surface(size, flags=SRCALPHA, depth=32) + + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + expected_color = setsurface_color if fill else unsetsurface_color + + to_surface = mask.to_surface( + setsurface=setsurface, unsetsurface=unsetsurface + ) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__color_alphas(self): + """Ensures the setcolor/unsetcolor alpha values are respected.""" + size = (13, 17) + setcolor = pygame.Color("green") + setcolor.a = 35 + unsetcolor = pygame.Color("blue") + unsetcolor.a = 213 + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + expected_color = setcolor if fill else unsetcolor + + to_surface = mask.to_surface(setcolor=setcolor, unsetcolor=unsetcolor) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__depths(self): + """Ensures to_surface works correctly with supported surface depths.""" + size = (13, 17) + surface_color = pygame.Color("red") + setsurface_color = pygame.Color("green") + unsetsurface_color = pygame.Color("blue") + + for depth in (8, 16, 24, 32): + surface = pygame.Surface(size, depth=depth) + setsurface = pygame.Surface(size, depth=depth) + unsetsurface = pygame.Surface(size, depth=depth) + + surface.fill(surface_color) + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + + # For non-32 bit depths, the actual color can be different from + # what was filled. + expected_color = ( + setsurface.get_at((0, 0)) if fill else unsetsurface.get_at((0, 0)) + ) + + to_surface = mask.to_surface(surface, setsurface, unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__different_depths(self): + """Ensures an exception is raised when surfaces have different depths.""" + size = (13, 17) + surface_color = pygame.Color("red") + setsurface_color = pygame.Color("green") + unsetsurface_color = pygame.Color("blue") + mask = pygame.mask.Mask(size) + + # Test different combinations of depths. + test_depths = ( + (8, 8, 16), # surface/setsurface/unsetsurface + (8, 8, 24), + (8, 8, 32), + (16, 16, 24), + (16, 16, 32), + (24, 16, 8), + (32, 16, 16), + (32, 32, 16), + (32, 24, 32), + ) + + for depths in test_depths: + surface = pygame.Surface(size, depth=depths[0]) + setsurface = pygame.Surface(size, depth=depths[1]) + unsetsurface = pygame.Surface(size, depth=depths[2]) + + surface.fill(surface_color) + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + with self.assertRaises(ValueError): + mask.to_surface(surface, setsurface, unsetsurface) + + def test_to_surface__different_depths_with_created_surfaces(self): + """Ensures an exception is raised when surfaces have different depths + than the created surface. + """ + size = (13, 17) + setsurface_color = pygame.Color("green") + unsetsurface_color = pygame.Color("blue") + mask = pygame.mask.Mask(size) + + # Test different combinations of depths. The created surface always has + # a depth of 32. + test_depths = ( + (8, 8), # setsurface/unsetsurface + (16, 16), + (24, 24), + (24, 16), + (32, 8), + (32, 16), + (32, 24), + (16, 32), + ) + + for set_depth, unset_depth in test_depths: + setsurface = pygame.Surface(size, depth=set_depth) + unsetsurface = pygame.Surface(size, depth=unset_depth) + + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + with self.assertRaises(ValueError): + mask.to_surface(setsurface=setsurface, unsetsurface=unsetsurface) + + def test_to_surface__same_srcalphas(self): + """Ensures to_surface works correctly when the SRCALPHA flag is set or not.""" + size = (13, 17) + surface_color = pygame.Color("red") + setsurface_color = pygame.Color("green") + unsetsurface_color = pygame.Color("blue") + + for depth in (16, 32): + for flags in (0, SRCALPHA): + surface = pygame.Surface(size, flags=flags, depth=depth) + setsurface = pygame.Surface(size, flags=flags, depth=depth) + unsetsurface = pygame.Surface(size, flags=flags, depth=depth) + + surface.fill(surface_color) + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + expected_color = setsurface_color if fill else unsetsurface_color + + to_surface = mask.to_surface(surface, setsurface, unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + if flags: + self.assertTrue(to_surface.get_flags() & flags) + + def test_to_surface__same_srcalphas_with_created_surfaces(self): + """Ensures to_surface works correctly when it creates a surface + and the SRCALPHA flag is set on both setsurface and unsetsurface. + """ + size = (13, 17) + setsurface_color = pygame.Color("green") + unsetsurface_color = pygame.Color("blue") + # The created surface always has a depth of 32 and the SRCALPHA flag set. + expected_flags = SRCALPHA + + setsurface = pygame.Surface(size, flags=expected_flags, depth=32) + unsetsurface = pygame.Surface(size, flags=expected_flags, depth=32) + + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + expected_color = setsurface_color if fill else unsetsurface_color + + to_surface = mask.to_surface( + setsurface=setsurface, unsetsurface=unsetsurface + ) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + self.assertTrue(to_surface.get_flags() & expected_flags) + + def test_to_surface__different_srcalphas(self): + """Ensures an exception is raised when surfaces have different SRCALPHA + flag settings. + """ + size = (13, 17) + surface_color = pygame.Color("red") + setsurface_color = pygame.Color("green") + unsetsurface_color = pygame.Color("blue") + mask = pygame.mask.Mask(size) + + # Test different combinations of SRCALPHA flags. + test_flags = ( + (SRCALPHA, 0, 0), # surface/setsurface/unsetsurface + (SRCALPHA, SRCALPHA, 0), + (0, SRCALPHA, SRCALPHA), + (0, 0, SRCALPHA), + ) + + for depth in (16, 32): + for flags in test_flags: + surface = pygame.Surface(size, flags=flags[0], depth=depth) + setsurface = pygame.Surface(size, flags=flags[1], depth=depth) + unsetsurface = pygame.Surface(size, flags=flags[2], depth=depth) + + surface.fill(surface_color) + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + with self.assertRaises(ValueError): + mask.to_surface(surface, setsurface, unsetsurface) + + def test_to_surface__different_srcalphas_with_created_surfaces(self): + """Ensures an exception is raised when surfaces have different SRCALPHA + flag settings than the created surface. + """ + size = (13, 17) + setsurface_color = pygame.Color("green") + unsetsurface_color = pygame.Color("blue") + mask = pygame.mask.Mask(size) + + for depth in (16, 32): + # Test different combinations of SRCALPHA flags. The created + # surface always has the SRCALPHA flag set. + for flags in ((0, 0), (SRCALPHA, 0), (0, SRCALPHA)): + setsurface = pygame.Surface(size, flags=flags[0], depth=depth) + unsetsurface = pygame.Surface(size, flags=flags[1], depth=depth) + + setsurface.fill(setsurface_color) + unsetsurface.fill(unsetsurface_color) + + with self.assertRaises(ValueError): + mask.to_surface(setsurface=setsurface, unsetsurface=unsetsurface) + + def test_to_surface__dest_on_surface(self): + """Ensures dest values on the surface work correctly + when using the defaults for setcolor and unsetcolor. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + width, height = size = (5, 9) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + mask_rect = mask.get_rect() + expected_color = default_setcolor if fill else default_unsetcolor + + # Test the dest parameter at different locations on the surface. + for dest in ((x, y) for y in range(height) for x in range(width)): + surface.fill(surface_color) # Clear for each test. + mask_rect.topleft = dest + + to_surface = mask.to_surface(surface, dest=dest) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, mask_rect + ) + + def test_to_surface__dest_on_surface_with_setsurface_unsetsurface(self): + """Ensures dest values on the surface work correctly + when using setsurface and unsetsurface. + """ + width, height = size = (5, 9) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + + setsurface = surface.copy() + setsurface_color = pygame.Color("green") + setsurface.fill(setsurface_color) + + unsetsurface = surface.copy() + unsetsurface_color = pygame.Color("blue") + unsetsurface.fill(unsetsurface_color) + + # Using different kwargs to exercise different to_surface() code. + # Should not have any impact on the resulting drawn surfaces. + kwargs = { + "surface": surface, + "setsurface": setsurface, + "unsetsurface": unsetsurface, + "dest": None, + } + + color_kwargs = dict(kwargs) + color_kwargs.update((("setcolor", None), ("unsetcolor", None))) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + mask_rect = mask.get_rect() + expected_color = setsurface_color if fill else unsetsurface_color + + # Test the dest parameter at different locations on the surface. + for dest in ((x, y) for y in range(height) for x in range(width)): + mask_rect.topleft = dest + + for use_color_params in (True, False): + surface.fill(surface_color) # Clear for each test. + + test_kwargs = color_kwargs if use_color_params else kwargs + test_kwargs["dest"] = dest + to_surface = mask.to_surface(**test_kwargs) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, mask_rect + ) + + def test_to_surface__dest_off_surface(self): + """Ensures dest values off the surface work correctly + when using the defaults for setcolor and unsetcolor. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + width, height = size = (5, 7) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + + # Test different dests off the surface. + dests = [(-width, -height), (-width, 0), (0, -height)] + dests.extend(off_corners(surface.get_rect())) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + mask_rect = mask.get_rect() + expected_color = default_setcolor if fill else default_unsetcolor + + for dest in dests: + surface.fill(surface_color) # Clear for each test. + mask_rect.topleft = dest + + to_surface = mask.to_surface(surface, dest=dest) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, mask_rect + ) + + def test_to_surface__dest_off_surface_with_setsurface_unsetsurface(self): + """Ensures dest values off the surface work correctly + when using setsurface and unsetsurface. + """ + width, height = size = (5, 7) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + + setsurface = surface.copy() + setsurface_color = pygame.Color("green") + setsurface.fill(setsurface_color) + + unsetsurface = surface.copy() + unsetsurface_color = pygame.Color("blue") + unsetsurface.fill(unsetsurface_color) + + # Test different dests off the surface. + dests = [(-width, -height), (-width, 0), (0, -height)] + dests.extend(off_corners(surface.get_rect())) + + # Using different kwargs to exercise different to_surface() code. + # Should not have any impact on the resulting drawn surfaces. + kwargs = { + "surface": surface, + "setsurface": setsurface, + "unsetsurface": unsetsurface, + "dest": None, + } + + color_kwargs = dict(kwargs) + color_kwargs.update((("setcolor", None), ("unsetcolor", None))) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + mask_rect = mask.get_rect() + expected_color = setsurface_color if fill else unsetsurface_color + + for dest in dests: + mask_rect.topleft = dest + + for use_color_params in (True, False): + surface.fill(surface_color) # Clear for each test. + test_kwargs = color_kwargs if use_color_params else kwargs + test_kwargs["dest"] = dest + to_surface = mask.to_surface(**test_kwargs) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color, mask_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, mask_rect + ) + + @unittest.expectedFailure + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_to_surface__area_on_mask(self): + """Ensures area values on the mask work correctly + when using the defaults for setcolor and unsetcolor. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + width, height = size = (5, 9) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + mask_rect = mask.get_rect() + area_rect = mask_rect.copy() + expected_color = default_setcolor if fill else default_unsetcolor + + # Testing the area parameter at different locations on the mask. + for pos in ((x, y) for y in range(height) for x in range(width)): + surface.fill(surface_color) # Clear for each test. + area_rect.topleft = pos + overlap_rect = mask_rect.clip(area_rect) + overlap_rect.topleft = (0, 0) + + to_surface = mask.to_surface(surface, area=area_rect) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color, overlap_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, overlap_rect + ) + + @unittest.expectedFailure + def test_to_surface__area_on_mask_with_setsurface_unsetsurface(self): + """Ensures area values on the mask work correctly + when using setsurface and unsetsurface. + """ + width, height = size = (5, 9) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + + setsurface = surface.copy() + setsurface_color = pygame.Color("green") + setsurface.fill(setsurface_color) + + unsetsurface = surface.copy() + unsetsurface_color = pygame.Color("blue") + unsetsurface.fill(unsetsurface_color) + + # Using the values in kwargs vs color_kwargs tests different to_surface + # code. Should not have any impact on the resulting drawn surfaces. + kwargs = { + "surface": surface, + "setsurface": setsurface, + "unsetsurface": unsetsurface, + "area": pygame.Rect((0, 0), size), + } + + color_kwargs = dict(kwargs) + color_kwargs.update((("setcolor", None), ("unsetcolor", None))) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + mask_rect = mask.get_rect() + area_rect = mask_rect.copy() + expected_color = setsurface_color if fill else unsetsurface_color + + # Testing the area parameter at different locations on the mask. + for pos in ((x, y) for y in range(height) for x in range(width)): + area_rect.topleft = pos + overlap_rect = mask_rect.clip(area_rect) + overlap_rect.topleft = (0, 0) + + for use_color_params in (True, False): + surface.fill(surface_color) # Clear for each test. + test_kwargs = color_kwargs if use_color_params else kwargs + test_kwargs["area"].topleft = pos + overlap_rect = mask_rect.clip(test_kwargs["area"]) + overlap_rect.topleft = (0, 0) + + to_surface = mask.to_surface(**test_kwargs) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color, overlap_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, overlap_rect + ) + + @unittest.expectedFailure + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_to_surface__area_off_mask(self): + """Ensures area values off the mask work correctly + when using the defaults for setcolor and unsetcolor. + """ + default_setcolor = pygame.Color("white") + default_unsetcolor = pygame.Color("black") + width, height = size = (5, 7) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + + # Testing positions off the mask. + positions = [(-width, -height), (-width, 0), (0, -height)] + positions.extend(off_corners(pygame.Rect((0, 0), (width, height)))) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + mask_rect = mask.get_rect() + area_rect = mask_rect.copy() + expected_color = default_setcolor if fill else default_unsetcolor + + for pos in positions: + surface.fill(surface_color) # Clear for each test. + area_rect.topleft = pos + overlap_rect = mask_rect.clip(area_rect) + overlap_rect.topleft = (0, 0) + + to_surface = mask.to_surface(surface, area=area_rect) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color, overlap_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, overlap_rect + ) + + @unittest.expectedFailure + @unittest.skipIf(IS_PYPY, "Segfaults on pypy") + def test_to_surface__area_off_mask_with_setsurface_unsetsurface(self): + """Ensures area values off the mask work correctly + when using setsurface and unsetsurface. + """ + width, height = size = (5, 7) + surface = pygame.Surface(size, SRCALPHA, 32) + surface_color = pygame.Color("red") + + setsurface = surface.copy() + setsurface_color = pygame.Color("green") + setsurface.fill(setsurface_color) + + unsetsurface = surface.copy() + unsetsurface_color = pygame.Color("blue") + unsetsurface.fill(unsetsurface_color) + + # Testing positions off the mask. + positions = [(-width, -height), (-width, 0), (0, -height)] + positions.extend(off_corners(pygame.Rect((0, 0), (width, height)))) + + # Using the values in kwargs vs color_kwargs tests different to_surface + # code. Should not have any impact on the resulting drawn surfaces. + kwargs = { + "surface": surface, + "setsurface": setsurface, + "unsetsurface": unsetsurface, + "area": pygame.Rect((0, 0), size), + } + + color_kwargs = dict(kwargs) + color_kwargs.update((("setcolor", None), ("unsetcolor", None))) + + for fill in (True, False): + mask = pygame.mask.Mask(size, fill=fill) + mask_rect = mask.get_rect() + expected_color = setsurface_color if fill else unsetsurface_color + + for pos in positions: + for use_color_params in (True, False): + surface.fill(surface_color) # Clear for each test. + test_kwargs = color_kwargs if use_color_params else kwargs + test_kwargs["area"].topleft = pos + overlap_rect = mask_rect.clip(test_kwargs["area"]) + overlap_rect.topleft = (0, 0) + + to_surface = mask.to_surface(**test_kwargs) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color, overlap_rect) + assertSurfaceFilledIgnoreArea( + self, to_surface, surface_color, overlap_rect + ) + + def test_to_surface__surface_with_zero_size(self): + """Ensures zero sized surfaces are handled correctly.""" + expected_ref_count = 3 + size = (0, 0) + surface = pygame.Surface(size) + mask = pygame.mask.Mask((3, 4), fill=True) + + to_surface = mask.to_surface(surface) + + self.assertIs(to_surface, surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertEqual(to_surface.get_size(), size) + + def test_to_surface__setsurface_with_zero_size(self): + """Ensures zero sized setsurfaces are handled correctly.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + expected_color = pygame.Color("white") # Default setcolor. + mask_size = (2, 4) + mask = pygame.mask.Mask(mask_size, fill=True) + setsurface = pygame.Surface((0, 0), expected_flag, expected_depth) + + to_surface = mask.to_surface(setsurface=setsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), mask_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_to_surface__unsetsurface_with_zero_size(self): + """Ensures zero sized unsetsurfaces are handled correctly.""" + expected_ref_count = 2 + expected_flag = SRCALPHA + expected_depth = 32 + expected_color = pygame.Color("black") # Default unsetcolor. + mask_size = (4, 2) + mask = pygame.mask.Mask(mask_size) + unsetsurface = pygame.Surface((0, 0), expected_flag, expected_depth) + + to_surface = mask.to_surface(unsetsurface=unsetsurface) + + self.assertIsInstance(to_surface, pygame.Surface) + if not IS_PYPY: + self.assertEqual(sys.getrefcount(to_surface), expected_ref_count) + self.assertTrue(to_surface.get_flags() & expected_flag) + self.assertEqual(to_surface.get_bitsize(), expected_depth) + self.assertEqual(to_surface.get_size(), mask_size) + assertSurfaceFilled(self, to_surface, expected_color) + + def test_zero_mask(self): + """Ensures masks can be created with zero sizes.""" + for size in ((100, 0), (0, 100), (0, 0)): + for fill in (True, False): + msg = f"size={size}, fill={fill}" + + mask = pygame.mask.Mask(size, fill=fill) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.get_size(), size, msg) + + def test_zero_mask_copy(self): + """Ensures copy correctly handles zero sized masks.""" + for expected_size in ((11, 0), (0, 11), (0, 0)): + mask = pygame.mask.Mask(expected_size) + + mask_copy = mask.copy() + + self.assertIsInstance(mask_copy, pygame.mask.Mask) + self.assertIsNot(mask_copy, mask) + assertMaskEqual(self, mask_copy, mask) + + def test_zero_mask_get_size(self): + """Ensures get_size correctly handles zero sized masks.""" + for expected_size in ((41, 0), (0, 40), (0, 0)): + mask = pygame.mask.Mask(expected_size) + + size = mask.get_size() + + self.assertEqual(size, expected_size) + + def test_zero_mask_get_rect(self): + """Ensures get_rect correctly handles zero sized masks.""" + for expected_size in ((4, 0), (0, 4), (0, 0)): + expected_rect = pygame.Rect((0, 0), expected_size) + mask = pygame.mask.Mask(expected_size) + + rect = mask.get_rect() + + self.assertEqual(rect, expected_rect) + + def test_zero_mask_get_at(self): + """Ensures get_at correctly handles zero sized masks.""" + for size in ((51, 0), (0, 50), (0, 0)): + mask = pygame.mask.Mask(size) + + with self.assertRaises(IndexError): + value = mask.get_at((0, 0)) + + def test_zero_mask_set_at(self): + """Ensures set_at correctly handles zero sized masks.""" + for size in ((31, 0), (0, 30), (0, 0)): + mask = pygame.mask.Mask(size) + + with self.assertRaises(IndexError): + mask.set_at((0, 0)) + + def test_zero_mask_overlap(self): + """Ensures overlap correctly handles zero sized masks. + + Tests combinations of sized and zero sized masks. + """ + offset = (0, 0) + + for size1, size2 in zero_size_pairs(51, 42): + msg = f"size1={size1}, size2={size2}" + mask1 = pygame.mask.Mask(size1, fill=True) + mask2 = pygame.mask.Mask(size2, fill=True) + + overlap_pos = mask1.overlap(mask2, offset) + + self.assertIsNone(overlap_pos, msg) + + def test_zero_mask_overlap_area(self): + """Ensures overlap_area correctly handles zero sized masks. + + Tests combinations of sized and zero sized masks. + """ + offset = (0, 0) + expected_count = 0 + + for size1, size2 in zero_size_pairs(41, 52): + msg = f"size1={size1}, size2={size2}" + mask1 = pygame.mask.Mask(size1, fill=True) + mask2 = pygame.mask.Mask(size2, fill=True) + + overlap_count = mask1.overlap_area(mask2, offset) + + self.assertEqual(overlap_count, expected_count, msg) + + def test_zero_mask_overlap_mask(self): + """Ensures overlap_mask correctly handles zero sized masks. + + Tests combinations of sized and zero sized masks. + """ + offset = (0, 0) + expected_count = 0 + + for size1, size2 in zero_size_pairs(43, 53): + msg = f"size1={size1}, size2={size2}" + mask1 = pygame.mask.Mask(size1, fill=True) + mask2 = pygame.mask.Mask(size2, fill=True) + + overlap_mask = mask1.overlap_mask(mask2, offset) + + self.assertIsInstance(overlap_mask, pygame.mask.Mask, msg) + self.assertEqual(overlap_mask.count(), expected_count, msg) + self.assertEqual(overlap_mask.get_size(), size1, msg) + + def test_zero_mask_fill(self): + """Ensures fill correctly handles zero sized masks.""" + expected_count = 0 + + for size in ((100, 0), (0, 100), (0, 0)): + mask = pygame.mask.Mask(size) + + mask.fill() + + self.assertEqual(mask.count(), expected_count, f"size={size}") + + def test_zero_mask_clear(self): + sizes = ((100, 0), (0, 100), (0, 0)) + + for size in sizes: + mask = pygame.mask.Mask(size) + mask.clear() + self.assertEqual(mask.count(), 0) + + def test_zero_mask_flip(self): + sizes = ((100, 0), (0, 100), (0, 0)) + + for size in sizes: + mask = pygame.mask.Mask(size) + mask.invert() + self.assertEqual(mask.count(), 0) + + def test_zero_mask_scale(self): + sizes = ((100, 0), (0, 100), (0, 0)) + + for size in sizes: + mask = pygame.mask.Mask(size) + mask2 = mask.scale((2, 3)) + + self.assertIsInstance(mask2, pygame.mask.Mask) + self.assertEqual(mask2.get_size(), (2, 3)) + + def test_zero_mask_draw(self): + """Ensures draw correctly handles zero sized masks. + + Tests combinations of sized and zero sized masks. + """ + offset = (0, 0) + + for size1, size2 in zero_size_pairs(31, 37): + msg = f"size1={size1}, size2={size2}" + mask1 = pygame.mask.Mask(size1, fill=True) + mask2 = pygame.mask.Mask(size2, fill=True) + expected_count = mask1.count() + + mask1.draw(mask2, offset) + + self.assertEqual(mask1.count(), expected_count, msg) + self.assertEqual(mask1.get_size(), size1, msg) + + def test_zero_mask_erase(self): + """Ensures erase correctly handles zero sized masks. + + Tests combinations of sized and zero sized masks. + """ + offset = (0, 0) + + for size1, size2 in zero_size_pairs(29, 23): + msg = f"size1={size1}, size2={size2}" + mask1 = pygame.mask.Mask(size1, fill=True) + mask2 = pygame.mask.Mask(size2, fill=True) + expected_count = mask1.count() + + mask1.erase(mask2, offset) + + self.assertEqual(mask1.count(), expected_count, msg) + self.assertEqual(mask1.get_size(), size1, msg) + + def test_zero_mask_count(self): + sizes = ((100, 0), (0, 100), (0, 0)) + + for size in sizes: + mask = pygame.mask.Mask(size, fill=True) + self.assertEqual(mask.count(), 0) + + def test_zero_mask_centroid(self): + sizes = ((100, 0), (0, 100), (0, 0)) + + for size in sizes: + mask = pygame.mask.Mask(size) + self.assertEqual(mask.centroid(), (0, 0)) + + def test_zero_mask_angle(self): + sizes = ((100, 0), (0, 100), (0, 0)) + + for size in sizes: + mask = pygame.mask.Mask(size) + self.assertEqual(mask.angle(), 0.0) + + def test_zero_mask_outline(self): + """Ensures outline correctly handles zero sized masks.""" + expected_points = [] + + for size in ((61, 0), (0, 60), (0, 0)): + mask = pygame.mask.Mask(size) + + points = mask.outline() + + self.assertListEqual(points, expected_points, f"size={size}") + + def test_zero_mask_outline__with_arg(self): + """Ensures outline correctly handles zero sized masks + when using the skip pixels argument.""" + expected_points = [] + + for size in ((66, 0), (0, 65), (0, 0)): + mask = pygame.mask.Mask(size) + + points = mask.outline(10) + + self.assertListEqual(points, expected_points, f"size={size}") + + def test_zero_mask_convolve(self): + """Ensures convolve correctly handles zero sized masks. + + Tests the different combinations of sized and zero sized masks. + """ + for size1 in ((17, 13), (71, 0), (0, 70), (0, 0)): + mask1 = pygame.mask.Mask(size1, fill=True) + + for size2 in ((11, 7), (81, 0), (0, 60), (0, 0)): + msg = f"sizes={size1}, {size2}" + mask2 = pygame.mask.Mask(size2, fill=True) + expected_size = ( + max(0, size1[0] + size2[0] - 1), + max(0, size1[1] + size2[1] - 1), + ) + + mask = mask1.convolve(mask2) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertIsNot(mask, mask2, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + + def test_zero_mask_convolve__with_output_mask(self): + """Ensures convolve correctly handles zero sized masks + when using an output mask argument. + + Tests the different combinations of sized and zero sized masks. + """ + for size1 in ((11, 17), (91, 0), (0, 90), (0, 0)): + mask1 = pygame.mask.Mask(size1, fill=True) + + for size2 in ((13, 11), (83, 0), (0, 62), (0, 0)): + mask2 = pygame.mask.Mask(size2, fill=True) + + for output_size in ((7, 5), (71, 0), (0, 70), (0, 0)): + msg = f"sizes={size1}, {size2}, {output_size}" + output_mask = pygame.mask.Mask(output_size) + + mask = mask1.convolve(mask2, output_mask) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertIs(mask, output_mask, msg) + self.assertEqual(mask.get_size(), output_size, msg) + + def test_zero_mask_connected_component(self): + """Ensures connected_component correctly handles zero sized masks.""" + expected_count = 0 + + for size in ((81, 0), (0, 80), (0, 0)): + msg = f"size={size}" + mask = pygame.mask.Mask(size) + + cc_mask = mask.connected_component() + + self.assertIsInstance(cc_mask, pygame.mask.Mask, msg) + self.assertEqual(cc_mask.get_size(), size) + self.assertEqual(cc_mask.count(), expected_count, msg) + + def test_zero_mask_connected_component__indexed(self): + """Ensures connected_component correctly handles zero sized masks + when using an index argument.""" + for size in ((91, 0), (0, 90), (0, 0)): + mask = pygame.mask.Mask(size) + + with self.assertRaises(IndexError): + cc_mask = mask.connected_component((0, 0)) + + def test_zero_mask_connected_components(self): + """Ensures connected_components correctly handles zero sized masks.""" + expected_cc_masks = [] + + for size in ((11, 0), (0, 10), (0, 0)): + mask = pygame.mask.Mask(size) + + cc_masks = mask.connected_components() + + self.assertListEqual(cc_masks, expected_cc_masks, f"size={size}") + + def test_zero_mask_get_bounding_rects(self): + """Ensures get_bounding_rects correctly handles zero sized masks.""" + expected_bounding_rects = [] + + for size in ((21, 0), (0, 20), (0, 0)): + mask = pygame.mask.Mask(size) + + bounding_rects = mask.get_bounding_rects() + + self.assertListEqual( + bounding_rects, expected_bounding_rects, f"size={size}" + ) + + def test_zero_mask_to_surface(self): + """Ensures to_surface correctly handles zero sized masks and surfaces.""" + mask_color = pygame.Color("blue") + surf_color = pygame.Color("red") + + for surf_size in ((7, 3), (7, 0), (0, 7), (0, 0)): + surface = pygame.Surface(surf_size, SRCALPHA, 32) + surface.fill(surf_color) + + for mask_size in ((5, 0), (0, 5), (0, 0)): + mask = pygame.mask.Mask(mask_size, fill=True) + + to_surface = mask.to_surface(surface, setcolor=mask_color) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), surf_size) + + if 0 not in surf_size: + assertSurfaceFilled(self, to_surface, surf_color) + + def test_zero_mask_to_surface__create_surface(self): + """Ensures to_surface correctly handles zero sized masks and surfaces + when it has to create a default surface. + """ + mask_color = pygame.Color("blue") + + for mask_size in ((3, 0), (0, 3), (0, 0)): + mask = pygame.mask.Mask(mask_size, fill=True) + + to_surface = mask.to_surface(setcolor=mask_color) + + self.assertIsInstance(to_surface, pygame.Surface) + self.assertEqual(to_surface.get_size(), mask_size) + + +class SubMask(pygame.mask.Mask): + """Subclass of the Mask class to help test subclassing.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_attribute = True + + +class SubMaskCopy(SubMask): + """Subclass of the Mask class to help test copying subclasses.""" + + def copy(self): + mask_copy = super().copy() + mask_copy.test_attribute = self.test_attribute + return mask_copy + + +class SubMaskDunderCopy(SubMask): + """Subclass of the Mask class to help test copying subclasses.""" + + def __copy__(self): + mask_copy = super().__copy__() + mask_copy.test_attribute = self.test_attribute + return mask_copy + + +class SubMaskCopyAndDunderCopy(SubMaskDunderCopy): + """Subclass of the Mask class to help test copying subclasses.""" + + def copy(self): + return super().copy() + + +class MaskSubclassTest(unittest.TestCase): + """Test subclassed Masks.""" + + def test_subclass_mask(self): + """Ensures the Mask class can be subclassed.""" + mask = SubMask((5, 3), fill=True) + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertIsInstance(mask, SubMask) + self.assertTrue(mask.test_attribute) + + def test_subclass_copy(self): + """Ensures copy works for subclassed Masks.""" + mask = SubMask((65, 2), fill=True) + + # Test both the copy() and __copy__() methods. + for mask_copy in (mask.copy(), copy.copy(mask)): + self.assertIsInstance(mask_copy, pygame.mask.Mask) + self.assertIsInstance(mask_copy, SubMask) + self.assertIsNot(mask_copy, mask) + assertMaskEqual(self, mask_copy, mask) + # No subclass attributes because copy()/__copy__() not overridden. + self.assertFalse(hasattr(mask_copy, "test_attribute")) + + def test_subclass_copy__override_copy(self): + """Ensures copy works for subclassed Masks overriding copy.""" + mask = SubMaskCopy((65, 2), fill=True) + + # Test both the copy() and __copy__() methods. + for i, mask_copy in enumerate((mask.copy(), copy.copy(mask))): + self.assertIsInstance(mask_copy, pygame.mask.Mask) + self.assertIsInstance(mask_copy, SubMaskCopy) + self.assertIsNot(mask_copy, mask) + assertMaskEqual(self, mask_copy, mask) + + if 1 == i: + # No subclass attributes because __copy__() not overridden. + self.assertFalse(hasattr(mask_copy, "test_attribute")) + else: + self.assertTrue(mask_copy.test_attribute) + + def test_subclass_copy__override_dunder_copy(self): + """Ensures copy works for subclassed Masks overriding __copy__.""" + mask = SubMaskDunderCopy((65, 2), fill=True) + + # Test both the copy() and __copy__() methods. + for mask_copy in (mask.copy(), copy.copy(mask)): + self.assertIsInstance(mask_copy, pygame.mask.Mask) + self.assertIsInstance(mask_copy, SubMaskDunderCopy) + self.assertIsNot(mask_copy, mask) + assertMaskEqual(self, mask_copy, mask) + # Calls to copy() eventually call __copy__() internally so the + # attributes will be copied. + self.assertTrue(mask_copy.test_attribute) + + def test_subclass_copy__override_both_copy_methods(self): + """Ensures copy works for subclassed Masks overriding copy/__copy__.""" + mask = SubMaskCopyAndDunderCopy((65, 2), fill=True) + + # Test both the copy() and __copy__() methods. + for mask_copy in (mask.copy(), copy.copy(mask)): + self.assertIsInstance(mask_copy, pygame.mask.Mask) + self.assertIsInstance(mask_copy, SubMaskCopyAndDunderCopy) + self.assertIsNot(mask_copy, mask) + assertMaskEqual(self, mask_copy, mask) + self.assertTrue(mask_copy.test_attribute) + + def test_subclass_get_size(self): + """Ensures get_size works for subclassed Masks.""" + expected_size = (2, 3) + mask = SubMask(expected_size) + + size = mask.get_size() + + self.assertEqual(size, expected_size) + + def test_subclass_mask_get_rect(self): + """Ensures get_rect works for subclassed Masks.""" + expected_rect = pygame.Rect((0, 0), (65, 33)) + mask = SubMask(expected_rect.size, fill=True) + + rect = mask.get_rect() + + self.assertEqual(rect, expected_rect) + + def test_subclass_get_at(self): + """Ensures get_at works for subclassed Masks.""" + expected_bit = 1 + mask = SubMask((3, 2), fill=True) + + bit = mask.get_at((0, 0)) + + self.assertEqual(bit, expected_bit) + + def test_subclass_set_at(self): + """Ensures set_at works for subclassed Masks.""" + expected_bit = 1 + expected_count = 1 + pos = (0, 0) + mask = SubMask(fill=False, size=(4, 2)) + + mask.set_at(pos) + + self.assertEqual(mask.get_at(pos), expected_bit) + self.assertEqual(mask.count(), expected_count) + + def test_subclass_overlap(self): + """Ensures overlap works for subclassed Masks.""" + expected_pos = (0, 0) + mask_size = (2, 3) + masks = (pygame.mask.Mask(fill=True, size=mask_size), SubMask(mask_size, True)) + arg_masks = ( + pygame.mask.Mask(fill=True, size=mask_size), + SubMask(mask_size, True), + ) + + # Test different combinations of subclassed and non-subclassed Masks. + for mask in masks: + for arg_mask in arg_masks: + overlap_pos = mask.overlap(arg_mask, (0, 0)) + + self.assertEqual(overlap_pos, expected_pos) + + def test_subclass_overlap_area(self): + """Ensures overlap_area works for subclassed Masks.""" + mask_size = (3, 2) + expected_count = mask_size[0] * mask_size[1] + masks = (pygame.mask.Mask(fill=True, size=mask_size), SubMask(mask_size, True)) + arg_masks = ( + pygame.mask.Mask(fill=True, size=mask_size), + SubMask(mask_size, True), + ) + + # Test different combinations of subclassed and non-subclassed Masks. + for mask in masks: + for arg_mask in arg_masks: + overlap_count = mask.overlap_area(arg_mask, (0, 0)) + + self.assertEqual(overlap_count, expected_count) + + def test_subclass_overlap_mask(self): + """Ensures overlap_mask works for subclassed Masks.""" + expected_size = (4, 5) + expected_count = expected_size[0] * expected_size[1] + masks = ( + pygame.mask.Mask(fill=True, size=expected_size), + SubMask(expected_size, True), + ) + arg_masks = ( + pygame.mask.Mask(fill=True, size=expected_size), + SubMask(expected_size, True), + ) + + # Test different combinations of subclassed and non-subclassed Masks. + for mask in masks: + for arg_mask in arg_masks: + overlap_mask = mask.overlap_mask(arg_mask, (0, 0)) + + self.assertIsInstance(overlap_mask, pygame.mask.Mask) + self.assertNotIsInstance(overlap_mask, SubMask) + self.assertEqual(overlap_mask.count(), expected_count) + self.assertEqual(overlap_mask.get_size(), expected_size) + + def test_subclass_fill(self): + """Ensures fill works for subclassed Masks.""" + mask_size = (2, 4) + expected_count = mask_size[0] * mask_size[1] + mask = SubMask(fill=False, size=mask_size) + + mask.fill() + + self.assertEqual(mask.count(), expected_count) + + def test_subclass_clear(self): + """Ensures clear works for subclassed Masks.""" + mask_size = (4, 3) + expected_count = 0 + mask = SubMask(mask_size, True) + + mask.clear() + + self.assertEqual(mask.count(), expected_count) + + def test_subclass_invert(self): + """Ensures invert works for subclassed Masks.""" + mask_size = (1, 4) + expected_count = mask_size[0] * mask_size[1] + mask = SubMask(fill=False, size=mask_size) + + mask.invert() + + self.assertEqual(mask.count(), expected_count) + + def test_subclass_scale(self): + """Ensures scale works for subclassed Masks.""" + expected_size = (5, 2) + mask = SubMask((1, 4)) + + scaled_mask = mask.scale(expected_size) + + self.assertIsInstance(scaled_mask, pygame.mask.Mask) + self.assertNotIsInstance(scaled_mask, SubMask) + self.assertEqual(scaled_mask.get_size(), expected_size) + + def test_subclass_draw(self): + """Ensures draw works for subclassed Masks.""" + mask_size = (5, 4) + expected_count = mask_size[0] * mask_size[1] + arg_masks = ( + pygame.mask.Mask(fill=True, size=mask_size), + SubMask(mask_size, True), + ) + + # Test different combinations of subclassed and non-subclassed Masks. + for mask in (pygame.mask.Mask(mask_size), SubMask(mask_size)): + for arg_mask in arg_masks: + mask.clear() # Clear for each test. + + mask.draw(arg_mask, (0, 0)) + + self.assertEqual(mask.count(), expected_count) + + def test_subclass_erase(self): + """Ensures erase works for subclassed Masks.""" + mask_size = (3, 4) + expected_count = 0 + masks = (pygame.mask.Mask(mask_size, True), SubMask(mask_size, True)) + arg_masks = (pygame.mask.Mask(mask_size, True), SubMask(mask_size, True)) + + # Test different combinations of subclassed and non-subclassed Masks. + for mask in masks: + for arg_mask in arg_masks: + mask.fill() # Fill for each test. + + mask.erase(arg_mask, (0, 0)) + + self.assertEqual(mask.count(), expected_count) + + def test_subclass_count(self): + """Ensures count works for subclassed Masks.""" + mask_size = (5, 2) + expected_count = mask_size[0] * mask_size[1] - 1 + mask = SubMask(fill=True, size=mask_size) + mask.set_at((1, 1), 0) + + count = mask.count() + + self.assertEqual(count, expected_count) + + def test_subclass_centroid(self): + """Ensures centroid works for subclassed Masks.""" + expected_centroid = (0, 0) + mask_size = (3, 2) + mask = SubMask((3, 2)) + + centroid = mask.centroid() + + self.assertEqual(centroid, expected_centroid) + + def test_subclass_angle(self): + """Ensures angle works for subclassed Masks.""" + expected_angle = 0.0 + mask = SubMask(size=(5, 4)) + + angle = mask.angle() + + self.assertAlmostEqual(angle, expected_angle) + + def test_subclass_outline(self): + """Ensures outline works for subclassed Masks.""" + expected_outline = [] + mask = SubMask((3, 4)) + + outline = mask.outline() + + self.assertListEqual(outline, expected_outline) + + def test_subclass_convolve(self): + """Ensures convolve works for subclassed Masks.""" + width, height = 7, 5 + mask_size = (width, height) + expected_count = 0 + expected_size = (max(0, width * 2 - 1), max(0, height * 2 - 1)) + + arg_masks = (pygame.mask.Mask(mask_size), SubMask(mask_size)) + output_masks = (pygame.mask.Mask(mask_size), SubMask(mask_size)) + + # Test different combinations of subclassed and non-subclassed Masks. + for mask in (pygame.mask.Mask(mask_size), SubMask(mask_size)): + for arg_mask in arg_masks: + convolve_mask = mask.convolve(arg_mask) + + self.assertIsInstance(convolve_mask, pygame.mask.Mask) + self.assertNotIsInstance(convolve_mask, SubMask) + self.assertEqual(convolve_mask.count(), expected_count) + self.assertEqual(convolve_mask.get_size(), expected_size) + + # Test subclassed masks for the output_mask as well. + for output_mask in output_masks: + convolve_mask = mask.convolve(arg_mask, output_mask) + + self.assertIsInstance(convolve_mask, pygame.mask.Mask) + self.assertEqual(convolve_mask.count(), expected_count) + self.assertEqual(convolve_mask.get_size(), mask_size) + + if isinstance(output_mask, SubMask): + self.assertIsInstance(convolve_mask, SubMask) + else: + self.assertNotIsInstance(convolve_mask, SubMask) + + def test_subclass_connected_component(self): + """Ensures connected_component works for subclassed Masks.""" + expected_count = 0 + expected_size = (3, 4) + mask = SubMask(expected_size) + + cc_mask = mask.connected_component() + + self.assertIsInstance(cc_mask, pygame.mask.Mask) + self.assertNotIsInstance(cc_mask, SubMask) + self.assertEqual(cc_mask.count(), expected_count) + self.assertEqual(cc_mask.get_size(), expected_size) + + def test_subclass_connected_components(self): + """Ensures connected_components works for subclassed Masks.""" + expected_ccs = [] + mask = SubMask((5, 4)) + + ccs = mask.connected_components() + + self.assertListEqual(ccs, expected_ccs) + + def test_subclass_get_bounding_rects(self): + """Ensures get_bounding_rects works for subclassed Masks.""" + expected_bounding_rects = [] + mask = SubMask((3, 2)) + + bounding_rects = mask.get_bounding_rects() + + self.assertListEqual(bounding_rects, expected_bounding_rects) + + def test_subclass_to_surface(self): + """Ensures to_surface works for subclassed Masks.""" + expected_color = pygame.Color("blue") + size = (5, 3) + mask = SubMask(size, fill=True) + surface = pygame.Surface(size, SRCALPHA, 32) + surface.fill(pygame.Color("red")) + + to_surface = mask.to_surface(surface, setcolor=expected_color) + + self.assertIs(to_surface, surface) + self.assertEqual(to_surface.get_size(), size) + assertSurfaceFilled(self, to_surface, expected_color) + + +@unittest.skipIf(IS_PYPY, "pypy has lots of mask failures") # TODO +class MaskModuleTest(unittest.TestCase): + def test_from_surface(self): + """Ensures from_surface creates a mask with the correct bits set. + + This test checks the masks created by the from_surface function using + 16 and 32 bit surfaces. Each alpha value (0-255) is tested against + several different threshold values. + Note: On 16 bit surface the requested alpha value can differ from what + is actually set. This test uses the value read from the surface. + """ + threshold_count = 256 + surface_color = [55, 155, 255, 0] + expected_size = (11, 9) + all_set_count = expected_size[0] * expected_size[1] + none_set_count = 0 + + for depth in (16, 32): + surface = pygame.Surface(expected_size, SRCALPHA, depth) + + for alpha in range(threshold_count): + surface_color[3] = alpha + surface.fill(surface_color) + + if depth < 32: + # On surfaces with depths < 32 the requested alpha can be + # different than what gets set. Use the value read from the + # surface. + alpha = surface.get_at((0, 0))[3] + + # Test the mask created at threshold values low, high and + # around alpha. + threshold_test_values = {-1, 0, alpha - 1, alpha, alpha + 1, 255, 256} + + for threshold in threshold_test_values: + msg = f"depth={depth}, alpha={alpha}, threshold={threshold}" + + if alpha > threshold: + expected_count = all_set_count + else: + expected_count = none_set_count + + mask = pygame.mask.from_surface( + surface=surface, threshold=threshold + ) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + self.assertEqual(mask.count(), expected_count, msg) + + def test_from_surface__different_alphas_32bit(self): + """Ensures from_surface creates a mask with the correct bits set + when pixels have different alpha values (32 bits surfaces). + + This test checks the masks created by the from_surface function using + a 32 bit surface. The surface is created with each pixel having a + different alpha value (0-255). This surface is tested over a range + of threshold values (0-255). + """ + offset = (0, 0) + threshold_count = 256 + surface_color = [10, 20, 30, 0] + expected_size = (threshold_count, 1) + expected_mask = pygame.Mask(expected_size, fill=True) + surface = pygame.Surface(expected_size, SRCALPHA, 32) + + # Give each pixel a different alpha. + surface.lock() # Lock for possible speed up. + for a in range(threshold_count): + surface_color[3] = a + surface.set_at((a, 0), surface_color) + surface.unlock() + + # Test the mask created for each different alpha threshold. + for threshold in range(threshold_count): + msg = f"threshold={threshold}" + expected_mask.set_at((threshold, 0), 0) + expected_count = expected_mask.count() + + mask = pygame.mask.from_surface(surface, threshold) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + self.assertEqual(mask.count(), expected_count, msg) + self.assertEqual( + mask.overlap_area(expected_mask, offset), expected_count, msg + ) + + def test_from_surface__different_alphas_16bit(self): + """Ensures from_surface creates a mask with the correct bits set + when pixels have different alpha values (16 bit surfaces). + + This test checks the masks created by the from_surface function using + a 16 bit surface. Each pixel of the surface is set with a different + alpha value (0-255), but since this is a 16 bit surface the requested + alpha value can differ from what is actually set. The resulting surface + will have groups of alpha values which complicates the test as the + alpha groups will all be set/unset at a given threshold. The setup + calculates these groups and an expected mask for each. This test data + is then used to test each alpha grouping over a range of threshold + values. + """ + threshold_count = 256 + surface_color = [110, 120, 130, 0] + expected_size = (threshold_count, 1) + surface = pygame.Surface(expected_size, SRCALPHA, 16) + + # Give each pixel a different alpha. + surface.lock() # Lock for possible speed up. + for a in range(threshold_count): + surface_color[3] = a + surface.set_at((a, 0), surface_color) + surface.unlock() + + alpha_thresholds = OrderedDict() + special_thresholds = set() + + # Create the threshold ranges and identify any thresholds that need + # special handling. + for threshold in range(threshold_count): + # On surfaces with depths < 32 the requested alpha can be different + # than what gets set. Use the value read from the surface. + alpha = surface.get_at((threshold, 0))[3] + + if alpha not in alpha_thresholds: + alpha_thresholds[alpha] = [threshold] + else: + alpha_thresholds[alpha].append(threshold) + + if threshold < alpha: + special_thresholds.add(threshold) + + # Use each threshold group to create an expected mask. + test_data = [] # [(from_threshold, to_threshold, expected_mask), ...] + offset = (0, 0) + erase_mask = pygame.Mask(expected_size) + exp_mask = pygame.Mask(expected_size, fill=True) + + for thresholds in alpha_thresholds.values(): + for threshold in thresholds: + if threshold in special_thresholds: + # Any special thresholds just reuse previous exp_mask. + test_data.append((threshold, threshold + 1, exp_mask)) + else: + to_threshold = thresholds[-1] + 1 + + # Make the expected mask by erasing the unset bits. + for thres in range(to_threshold): + erase_mask.set_at((thres, 0), 1) + + exp_mask = pygame.Mask(expected_size, fill=True) + exp_mask.erase(erase_mask, offset) + test_data.append((threshold, to_threshold, exp_mask)) + break + + # All the setup is done. Now test the masks created over the threshold + # ranges. + for from_threshold, to_threshold, expected_mask in test_data: + expected_count = expected_mask.count() + + for threshold in range(from_threshold, to_threshold): + msg = f"threshold={threshold}" + + mask = pygame.mask.from_surface(surface, threshold) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + self.assertEqual(mask.count(), expected_count, msg) + self.assertEqual( + mask.overlap_area(expected_mask, offset), expected_count, msg + ) + + def test_from_surface__with_colorkey_mask_cleared(self): + """Ensures from_surface creates a mask with the correct bits set + when the surface uses a colorkey. + + The surface is filled with the colorkey color so the resulting masks + are expected to have no bits set. + """ + colorkeys = ((0, 0, 0), (1, 2, 3), (50, 100, 200), (255, 255, 255)) + expected_size = (7, 11) + expected_count = 0 + + for depth in (8, 16, 24, 32): + msg = f"depth={depth}" + surface = pygame.Surface(expected_size, 0, depth) + + for colorkey in colorkeys: + surface.set_colorkey(colorkey) + # With some depths (i.e. 8 and 16) the actual colorkey can be + # different than what was requested via the set. + surface.fill(surface.get_colorkey()) + + mask = pygame.mask.from_surface(surface) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + self.assertEqual(mask.count(), expected_count, msg) + + def test_from_surface__with_colorkey_mask_filled(self): + """Ensures from_surface creates a mask with the correct bits set + when the surface uses a colorkey. + + The surface is filled with a color that is not the colorkey color so + the resulting masks are expected to have all bits set. + """ + colorkeys = ((0, 0, 0), (1, 2, 3), (10, 100, 200), (255, 255, 255)) + surface_color = (50, 100, 200) + expected_size = (11, 7) + expected_count = expected_size[0] * expected_size[1] + + for depth in (8, 16, 24, 32): + msg = f"depth={depth}" + surface = pygame.Surface(expected_size, 0, depth) + surface.fill(surface_color) + + for colorkey in colorkeys: + surface.set_colorkey(colorkey) + + mask = pygame.mask.from_surface(surface) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + self.assertEqual(mask.count(), expected_count, msg) + + def test_from_surface__with_colorkey_mask_pattern(self): + """Ensures from_surface creates a mask with the correct bits set + when the surface uses a colorkey. + + The surface is filled with alternating pixels of colorkey and + non-colorkey colors, so the resulting masks are expected to have + alternating bits set. + """ + + def alternate(func, set_value, unset_value, width, height): + # Helper function to set alternating values. + setbit = False + for pos in ((x, y) for x in range(width) for y in range(height)): + func(pos, set_value if setbit else unset_value) + setbit = not setbit + + surface_color = (5, 10, 20) + colorkey = (50, 60, 70) + expected_size = (11, 2) + expected_mask = pygame.mask.Mask(expected_size) + alternate(expected_mask.set_at, 1, 0, *expected_size) + expected_count = expected_mask.count() + offset = (0, 0) + + for depth in (8, 16, 24, 32): + msg = f"depth={depth}" + surface = pygame.Surface(expected_size, 0, depth) + # Fill the surface with alternating colors. + alternate(surface.set_at, surface_color, colorkey, *expected_size) + surface.set_colorkey(colorkey) + + mask = pygame.mask.from_surface(surface) + + self.assertIsInstance(mask, pygame.mask.Mask, msg) + self.assertEqual(mask.get_size(), expected_size, msg) + self.assertEqual(mask.count(), expected_count, msg) + self.assertEqual( + mask.overlap_area(expected_mask, offset), expected_count, msg + ) + + def test_from_threshold(self): + """Does mask.from_threshold() work correctly?""" + + a = [16, 24, 32] + + for i in a: + surf = pygame.surface.Surface((70, 70), 0, i) + surf.fill((100, 50, 200), (20, 20, 20, 20)) + mask = pygame.mask.from_threshold( + surf, (100, 50, 200, 255), (10, 10, 10, 255) + ) + + rects = mask.get_bounding_rects() + + self.assertEqual(mask.count(), 400) + self.assertEqual(mask.get_bounding_rects(), [pygame.Rect((20, 20, 20, 20))]) + + for i in a: + surf = pygame.surface.Surface((70, 70), 0, i) + surf2 = pygame.surface.Surface((70, 70), 0, i) + surf.fill((100, 100, 100)) + surf2.fill((150, 150, 150)) + surf2.fill((100, 100, 100), (40, 40, 10, 10)) + mask = pygame.mask.from_threshold( + surface=surf, + color=(0, 0, 0, 0), + threshold=(10, 10, 10, 255), + othersurface=surf2, + ) + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertEqual(mask.count(), 100) + self.assertEqual(mask.get_bounding_rects(), [pygame.Rect((40, 40, 10, 10))]) + + def test_zero_size_from_surface(self): + """Ensures from_surface can create masks from zero sized surfaces.""" + for size in ((100, 0), (0, 100), (0, 0)): + mask = pygame.mask.from_surface(pygame.Surface(size)) + + self.assertIsInstance(mask, pygame.mask.MaskType, f"size={size}") + self.assertEqual(mask.get_size(), size) + + def test_zero_size_from_threshold(self): + a = [16, 24, 32] + sizes = ((100, 0), (0, 100), (0, 0)) + + for size in sizes: + for i in a: + surf = pygame.surface.Surface(size, 0, i) + surf.fill((100, 50, 200), (20, 20, 20, 20)) + mask = pygame.mask.from_threshold( + surf, (100, 50, 200, 255), (10, 10, 10, 255) + ) + + self.assertEqual(mask.count(), 0) + + rects = mask.get_bounding_rects() + self.assertEqual(rects, []) + + for i in a: + surf = pygame.surface.Surface(size, 0, i) + surf2 = pygame.surface.Surface(size, 0, i) + surf.fill((100, 100, 100)) + surf2.fill((150, 150, 150)) + surf2.fill((100, 100, 100), (40, 40, 10, 10)) + mask = pygame.mask.from_threshold( + surf, (0, 0, 0, 0), (10, 10, 10, 255), surf2 + ) + + self.assertIsInstance(mask, pygame.mask.Mask) + self.assertEqual(mask.count(), 0) + + rects = mask.get_bounding_rects() + self.assertEqual(rects, []) + + def test_buffer_interface(self): + size = (1000, 100) + pixels_set = ((0, 1), (100, 10), (173, 90)) + pixels_unset = ((0, 0), (101, 10), (173, 91)) + + mask = pygame.Mask(size) + for point in pixels_set: + mask.set_at(point, 1) + + view = memoryview(mask) + intwidth = 8 * view.strides[1] + + for point in pixels_set: + x, y = point + col = x // intwidth + self.assertEqual( + (view[col, y] >> (x % intwidth)) & 1, + 1, + f"the pixel at {point} is not set to 1", + ) + + for point in pixels_unset: + x, y = point + col = x // intwidth + self.assertEqual( + (view[col, y] >> (x % intwidth)) & 1, + 0, + f"the pixel at {point} is not set to 0", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/math_test.py b/laplas/abstract_map/pygame/tests/math_test.py new file mode 100644 index 0000000..3cedf23 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/math_test.py @@ -0,0 +1,2929 @@ +import math +import platform +import unittest +from collections.abc import Collection, Sequence + +import pygame.math +from pygame.math import Vector2, Vector3 + +IS_PYPY = "PyPy" == platform.python_implementation() + + +class MathModuleTest(unittest.TestCase): + """Math module tests.""" + + def test_lerp(self): + result = pygame.math.lerp(10, 100, 0.5) # 55.0 + self.assertAlmostEqual(result, 55.0) + + result = pygame.math.lerp(10, 100, 0.0) # 10 + self.assertAlmostEqual(result, 10.0) + + result = pygame.math.lerp(10, 100, 1.0) # 100 + self.assertAlmostEqual(result, 100.0) + + # Not enough args + self.assertRaises(TypeError, pygame.math.lerp, 1) + + # Wrong arg type + self.assertRaises(TypeError, pygame.math.lerp, "str", "str", "str") + + # Percent outside range [0, 1] + self.assertRaises(ValueError, pygame.math.lerp, 10, 100, 1.1) + self.assertRaises(ValueError, pygame.math.lerp, 10, 100, -0.5) + + def test_clamp(self): + """Test clamp function.""" + + # Int tests + # Test going above max + result = pygame.math.clamp(10, 1, 5) + self.assertEqual(result, 5) + # Test going below min + result = pygame.math.clamp(-10, 1, 5) + self.assertEqual(result, 1) + # Test equal to max + result = pygame.math.clamp(5, 1, 5) + self.assertEqual(result, 5) + # Test equal to min + result = pygame.math.clamp(1, 1, 5) + self.assertEqual(result, 1) + # Test between min and max + result = pygame.math.clamp(3, 1, 5) + self.assertEqual(result, 3) + + # Float tests + # Test going above max + result = pygame.math.clamp(10.0, 1.12, 5.0) + self.assertAlmostEqual(result, 5.0) + # Test going below min + result = pygame.math.clamp(-10.0, 1.12, 5.0) + self.assertAlmostEqual(result, 1.12) + # Test equal to max + result = pygame.math.clamp(5.0, 1.12, 5.0) + self.assertAlmostEqual(result, 5.0) + # Test equal to min + result = pygame.math.clamp(1.12, 1.12, 5.0) + self.assertAlmostEqual(result, 1.12) + # Test between min and max + result = pygame.math.clamp(2.5, 1.12, 5.0) + self.assertAlmostEqual(result, 2.5) + + # Error tests + # Not enough args + self.assertRaises(TypeError, pygame.math.clamp, 10) + # Non numeric args + self.assertRaises(TypeError, pygame.math.clamp, "hello", "py", "thon") + + +class Vector2TypeTest(unittest.TestCase): + def setUp(self): + self.zeroVec = Vector2() + self.e1 = Vector2(1, 0) + self.e2 = Vector2(0, 1) + self.t1 = (1.2, 3.4) + self.l1 = list(self.t1) + self.v1 = Vector2(self.t1) + self.t2 = (5.6, 7.8) + self.l2 = list(self.t2) + self.v2 = Vector2(self.t2) + self.s1 = 5.6 + self.s2 = 7.8 + + def testConstructionDefault(self): + v = Vector2() + self.assertEqual(v.x, 0.0) + self.assertEqual(v.y, 0.0) + + def testConstructionScalar(self): + v = Vector2(1) + self.assertEqual(v.x, 1.0) + self.assertEqual(v.y, 1.0) + + def testConstructionScalarKeywords(self): + v = Vector2(x=1) + self.assertEqual(v.x, 1.0) + self.assertEqual(v.y, 1.0) + + def testConstructionKeywords(self): + v = Vector2(x=1, y=2) + self.assertEqual(v.x, 1.0) + self.assertEqual(v.y, 2.0) + + def testConstructionXY(self): + v = Vector2(1.2, 3.4) + self.assertEqual(v.x, 1.2) + self.assertEqual(v.y, 3.4) + + def testConstructionTuple(self): + v = Vector2((1.2, 3.4)) + self.assertEqual(v.x, 1.2) + self.assertEqual(v.y, 3.4) + + def testConstructionList(self): + v = Vector2([1.2, 3.4]) + self.assertEqual(v.x, 1.2) + self.assertEqual(v.y, 3.4) + + def testConstructionVector2(self): + v = Vector2(Vector2(1.2, 3.4)) + self.assertEqual(v.x, 1.2) + self.assertEqual(v.y, 3.4) + + def testAttributeAccess(self): + tmp = self.v1.x + self.assertEqual(tmp, self.v1.x) + self.assertEqual(tmp, self.v1[0]) + tmp = self.v1.y + self.assertEqual(tmp, self.v1.y) + self.assertEqual(tmp, self.v1[1]) + self.v1.x = 3.141 + self.assertEqual(self.v1.x, 3.141) + self.v1.y = 3.141 + self.assertEqual(self.v1.y, 3.141) + + def assign_nonfloat(): + v = Vector2() + v.x = "spam" + + self.assertRaises(TypeError, assign_nonfloat) + + def test___round___basic(self): + self.assertEqual(round(pygame.Vector2(0.0, 0.0)), pygame.Vector2(0.0, 0.0)) + self.assertEqual(type(round(pygame.Vector2(0.0, 0.0))), pygame.Vector2) + self.assertEqual( + round(pygame.Vector2(1.0, 1.0)), round(pygame.Vector2(1.0, 1.0)) + ) + self.assertEqual( + round(pygame.Vector2(10.0, 10.0)), round(pygame.Vector2(10.0, 10.0)) + ) + self.assertEqual( + round(pygame.Vector2(1000000000.0, 1000000000.0)), + pygame.Vector2(1000000000.0, 1000000000.0), + ) + self.assertEqual(round(pygame.Vector2(1e20, 1e20)), pygame.Vector2(1e20, 1e20)) + + self.assertEqual(round(pygame.Vector2(-1.0, -1.0)), pygame.Vector2(-1.0, -1.0)) + self.assertEqual( + round(pygame.Vector2(-10.0, -10.0)), pygame.Vector2(-10.0, -10.0) + ) + self.assertEqual( + round(pygame.Vector2(-1000000000.0, -1000000000.0)), + pygame.Vector2(-1000000000.0, -1000000000.0), + ) + self.assertEqual( + round(pygame.Vector2(-1e20, -1e20)), pygame.Vector2(-1e20, -1e20) + ) + + self.assertEqual(round(pygame.Vector2(0.1, 0.1)), pygame.Vector2(0.0, 0.0)) + self.assertEqual(round(pygame.Vector2(1.1, 1.1)), pygame.Vector2(1.0, 1.0)) + self.assertEqual(round(pygame.Vector2(10.1, 10.1)), pygame.Vector2(10.0, 10.0)) + self.assertEqual( + round(pygame.Vector2(1000000000.1, 1000000000.1)), + pygame.Vector2(1000000000.0, 1000000000.0), + ) + + self.assertEqual(round(pygame.Vector2(-1.1, -1.1)), pygame.Vector2(-1.0, -1.0)) + self.assertEqual( + round(pygame.Vector2(-10.1, -10.1)), pygame.Vector2(-10.0, -10.0) + ) + self.assertEqual( + round(pygame.Vector2(-1000000000.1, -1000000000.1)), + pygame.Vector2(-1000000000.0, -1000000000.0), + ) + + self.assertEqual(round(pygame.Vector2(0.9, 0.9)), pygame.Vector2(1.0, 1.0)) + self.assertEqual(round(pygame.Vector2(9.9, 9.9)), pygame.Vector2(10.0, 10.0)) + self.assertEqual( + round(pygame.Vector2(999999999.9, 999999999.9)), + pygame.Vector2(1000000000.0, 1000000000.0), + ) + + self.assertEqual(round(pygame.Vector2(-0.9, -0.9)), pygame.Vector2(-1.0, -1.0)) + self.assertEqual( + round(pygame.Vector2(-9.9, -9.9)), pygame.Vector2(-10.0, -10.0) + ) + self.assertEqual( + round(pygame.Vector2(-999999999.9, -999999999.9)), + pygame.Vector2(-1000000000.0, -1000000000.0), + ) + + self.assertEqual( + round(pygame.Vector2(-8.0, -8.0), -1), pygame.Vector2(-10.0, -10.0) + ) + self.assertEqual(type(round(pygame.Vector2(-8.0, -8.0), -1)), pygame.Vector2) + + self.assertEqual(type(round(pygame.Vector2(-8.0, -8.0), 0)), pygame.Vector2) + self.assertEqual(type(round(pygame.Vector2(-8.0, -8.0), 1)), pygame.Vector2) + + # Check even / odd rounding behaviour + self.assertEqual(round(pygame.Vector2(5.5, 5.5)), pygame.Vector2(6, 6)) + self.assertEqual(round(pygame.Vector2(5.4, 5.4)), pygame.Vector2(5.0, 5.0)) + self.assertEqual(round(pygame.Vector2(5.6, 5.6)), pygame.Vector2(6.0, 6.0)) + self.assertEqual(round(pygame.Vector2(-5.5, -5.5)), pygame.Vector2(-6, -6)) + self.assertEqual(round(pygame.Vector2(-5.4, -5.4)), pygame.Vector2(-5, -5)) + self.assertEqual(round(pygame.Vector2(-5.6, -5.6)), pygame.Vector2(-6, -6)) + + self.assertRaises(TypeError, round, pygame.Vector2(1.0, 1.0), 1.5) + self.assertRaises(TypeError, round, pygame.Vector2(1.0, 1.0), "a") + + def testCopy(self): + v_copy0 = Vector2(2004.0, 2022.0) + v_copy1 = v_copy0.copy() + self.assertEqual(v_copy0.x, v_copy1.x) + self.assertEqual(v_copy0.y, v_copy1.y) + + def test_move_towards_basic(self): + expected = Vector2(8.08, 2006.87) + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + change_ip = Vector2(7.22, 2004.0) + + change = origin.move_towards(target, 3) + change_ip.move_towards_ip(target, 3) + + self.assertEqual(round(change.x, 2), expected.x) + self.assertEqual(round(change.y, 2), expected.y) + self.assertEqual(round(change_ip.x, 2), expected.x) + self.assertEqual(round(change_ip.y, 2), expected.y) + + def test_move_towards_max_distance(self): + expected = Vector2(12.30, 2021) + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + change_ip = Vector2(7.22, 2004.0) + + change = origin.move_towards(target, 25) + change_ip.move_towards_ip(target, 25) + + self.assertEqual(round(change.x, 2), expected.x) + self.assertEqual(round(change.y, 2), expected.y) + self.assertEqual(round(change_ip.x, 2), expected.x) + self.assertEqual(round(change_ip.y, 2), expected.y) + + def test_move_nowhere(self): + expected = Vector2(7.22, 2004.0) + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + change_ip = Vector2(7.22, 2004.0) + + change = origin.move_towards(target, 0) + change_ip.move_towards_ip(target, 0) + + self.assertEqual(round(change.x, 2), expected.x) + self.assertEqual(round(change.y, 2), expected.y) + self.assertEqual(round(change_ip.x, 2), expected.x) + self.assertEqual(round(change_ip.y, 2), expected.y) + + def test_move_away(self): + expected = Vector2(6.36, 2001.13) + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + change_ip = Vector2(7.22, 2004.0) + + change = origin.move_towards(target, -3) + change_ip.move_towards_ip(target, -3) + + self.assertEqual(round(change.x, 2), expected.x) + self.assertEqual(round(change.y, 2), expected.y) + self.assertEqual(round(change_ip.x, 2), expected.x) + self.assertEqual(round(change_ip.y, 2), expected.y) + + def test_move_towards_self(self): + vec = Vector2(6.36, 2001.13) + vec2 = vec.copy() + for dist in (-3.54, -1, 0, 0.234, 12): + self.assertEqual(vec.move_towards(vec2, dist), vec) + vec2.move_towards_ip(vec, dist) + self.assertEqual(vec, vec2) + + def test_move_towards_errors(self): + def overpopulate(): + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + origin.move_towards(target, 3, 2) + + def overpopulate_ip(): + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + origin.move_towards_ip(target, 3, 2) + + def invalid_types1(): + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + origin.move_towards(target, "novial") + + def invalid_types_ip1(): + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + origin.move_towards_ip(target, "is") + + def invalid_types2(): + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + origin.move_towards("kinda", 3) + + def invalid_types_ip2(): + origin = Vector2(7.22, 2004.0) + target = Vector2(12.30, 2021.0) + origin.move_towards_ip("cool", 3) + + self.assertRaises(TypeError, overpopulate) + self.assertRaises(TypeError, overpopulate_ip) + self.assertRaises(TypeError, invalid_types1) + self.assertRaises(TypeError, invalid_types_ip1) + self.assertRaises(TypeError, invalid_types2) + self.assertRaises(TypeError, invalid_types_ip2) + + def testSequence(self): + v = Vector2(1.2, 3.4) + Vector2()[:] + self.assertEqual(len(v), 2) + self.assertEqual(v[0], 1.2) + self.assertEqual(v[1], 3.4) + self.assertRaises(IndexError, lambda: v[2]) + self.assertEqual(v[-1], 3.4) + self.assertEqual(v[-2], 1.2) + self.assertRaises(IndexError, lambda: v[-3]) + self.assertEqual(v[:], [1.2, 3.4]) + self.assertEqual(v[1:], [3.4]) + self.assertEqual(v[:1], [1.2]) + self.assertEqual(list(v), [1.2, 3.4]) + self.assertEqual(tuple(v), (1.2, 3.4)) + v[0] = 5.6 + v[1] = 7.8 + self.assertEqual(v.x, 5.6) + self.assertEqual(v.y, 7.8) + v[:] = [9.1, 11.12] + self.assertEqual(v.x, 9.1) + self.assertEqual(v.y, 11.12) + + def overpopulate(): + v = Vector2() + v[:] = [1, 2, 3] + + self.assertRaises(ValueError, overpopulate) + + def underpopulate(): + v = Vector2() + v[:] = [1] + + self.assertRaises(ValueError, underpopulate) + + def assign_nonfloat(): + v = Vector2() + v[0] = "spam" + + self.assertRaises(TypeError, assign_nonfloat) + + def testExtendedSlicing(self): + # deletion + def delSlice(vec, start=None, stop=None, step=None): + if start is not None and stop is not None and step is not None: + del vec[start:stop:step] + elif start is not None and stop is None and step is not None: + del vec[start::step] + elif start is None and stop is None and step is not None: + del vec[::step] + + v = Vector2(self.v1) + self.assertRaises(TypeError, delSlice, v, None, None, 2) + self.assertRaises(TypeError, delSlice, v, 1, None, 2) + self.assertRaises(TypeError, delSlice, v, 1, 2, 1) + + # assignment + v = Vector2(self.v1) + v[::2] = [-1] + self.assertEqual(v, [-1, self.v1.y]) + v = Vector2(self.v1) + v[::-2] = [10] + self.assertEqual(v, [self.v1.x, 10]) + v = Vector2(self.v1) + v[::-1] = v + self.assertEqual(v, [self.v1.y, self.v1.x]) + a = Vector2(self.v1) + b = Vector2(self.v1) + c = Vector2(self.v1) + a[1:2] = [2.2] + b[slice(1, 2)] = [2.2] + c[1:2:] = (2.2,) + self.assertEqual(a, b) + self.assertEqual(a, c) + self.assertEqual(type(a), type(self.v1)) + self.assertEqual(type(b), type(self.v1)) + self.assertEqual(type(c), type(self.v1)) + + def test_contains(self): + c = Vector2(0, 1) + + # call __contains__ explicitly to test that it is defined + self.assertTrue(c.__contains__(0)) + self.assertTrue(0 in c) + self.assertTrue(1 in c) + self.assertTrue(2 not in c) + self.assertFalse(c.__contains__(2)) + + self.assertRaises(TypeError, lambda: "string" in c) + self.assertRaises(TypeError, lambda: 3 + 4j in c) + + def testAdd(self): + v3 = self.v1 + self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x + self.v2.x) + self.assertEqual(v3.y, self.v1.y + self.v2.y) + v3 = self.v1 + self.t2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x + self.t2[0]) + self.assertEqual(v3.y, self.v1.y + self.t2[1]) + v3 = self.v1 + self.l2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x + self.l2[0]) + self.assertEqual(v3.y, self.v1.y + self.l2[1]) + v3 = self.t1 + self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.t1[0] + self.v2.x) + self.assertEqual(v3.y, self.t1[1] + self.v2.y) + v3 = self.l1 + self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.l1[0] + self.v2.x) + self.assertEqual(v3.y, self.l1[1] + self.v2.y) + + def testSub(self): + v3 = self.v1 - self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x - self.v2.x) + self.assertEqual(v3.y, self.v1.y - self.v2.y) + v3 = self.v1 - self.t2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x - self.t2[0]) + self.assertEqual(v3.y, self.v1.y - self.t2[1]) + v3 = self.v1 - self.l2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x - self.l2[0]) + self.assertEqual(v3.y, self.v1.y - self.l2[1]) + v3 = self.t1 - self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.t1[0] - self.v2.x) + self.assertEqual(v3.y, self.t1[1] - self.v2.y) + v3 = self.l1 - self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.l1[0] - self.v2.x) + self.assertEqual(v3.y, self.l1[1] - self.v2.y) + + def testScalarMultiplication(self): + v = self.s1 * self.v1 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertEqual(v.x, self.s1 * self.v1.x) + self.assertEqual(v.y, self.s1 * self.v1.y) + v = self.v1 * self.s2 + self.assertEqual(v.x, self.v1.x * self.s2) + self.assertEqual(v.y, self.v1.y * self.s2) + + def testScalarDivision(self): + v = self.v1 / self.s1 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertAlmostEqual(v.x, self.v1.x / self.s1) + self.assertAlmostEqual(v.y, self.v1.y / self.s1) + v = self.v1 // self.s2 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertEqual(v.x, self.v1.x // self.s2) + self.assertEqual(v.y, self.v1.y // self.s2) + + def testBool(self): + self.assertEqual(bool(self.zeroVec), False) + self.assertEqual(bool(self.v1), True) + self.assertTrue(not self.zeroVec) + self.assertTrue(self.v1) + + def testUnary(self): + v = +self.v1 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertEqual(v.x, self.v1.x) + self.assertEqual(v.y, self.v1.y) + self.assertNotEqual(id(v), id(self.v1)) + v = -self.v1 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertEqual(v.x, -self.v1.x) + self.assertEqual(v.y, -self.v1.y) + self.assertNotEqual(id(v), id(self.v1)) + + def testCompare(self): + int_vec = Vector2(3, -2) + flt_vec = Vector2(3.0, -2.0) + zero_vec = Vector2(0, 0) + self.assertEqual(int_vec == flt_vec, True) + self.assertEqual(int_vec != flt_vec, False) + self.assertEqual(int_vec != zero_vec, True) + self.assertEqual(flt_vec == zero_vec, False) + self.assertEqual(int_vec == (3, -2), True) + self.assertEqual(int_vec != (3, -2), False) + self.assertEqual(int_vec != [0, 0], True) + self.assertEqual(int_vec == [0, 0], False) + self.assertEqual(int_vec != 5, True) + self.assertEqual(int_vec == 5, False) + self.assertEqual(int_vec != [3, -2, 0], True) + self.assertEqual(int_vec == [3, -2, 0], False) + + def testStr(self): + v = Vector2(1.2, 3.4) + self.assertEqual(str(v), "[1.2, 3.4]") + + def testRepr(self): + v = Vector2(1.2, 3.4) + self.assertEqual(v.__repr__(), "") + self.assertEqual(v, Vector2(v.__repr__())) + + def testIter(self): + it = self.v1.__iter__() + next_ = it.__next__ + self.assertEqual(next_(), self.v1[0]) + self.assertEqual(next_(), self.v1[1]) + self.assertRaises(StopIteration, lambda: next_()) + it1 = self.v1.__iter__() + it2 = self.v1.__iter__() + self.assertNotEqual(id(it1), id(it2)) + self.assertEqual(id(it1), id(it1.__iter__())) + self.assertEqual(list(it1), list(it2)) + self.assertEqual(list(self.v1.__iter__()), self.l1) + idx = 0 + for val in self.v1: + self.assertEqual(val, self.v1[idx]) + idx += 1 + + def test_rotate(self): + v1 = Vector2(1, 0) + v2 = v1.rotate(90) + v3 = v1.rotate(90 + 360) + self.assertEqual(v1.x, 1) + self.assertEqual(v1.y, 0) + self.assertEqual(v2.x, 0) + self.assertEqual(v2.y, 1) + self.assertEqual(v3.x, v2.x) + self.assertEqual(v3.y, v2.y) + v1 = Vector2(-1, -1) + v2 = v1.rotate(-90) + self.assertEqual(v2.x, -1) + self.assertEqual(v2.y, 1) + v2 = v1.rotate(360) + self.assertEqual(v1.x, v2.x) + self.assertEqual(v1.y, v2.y) + v2 = v1.rotate(0) + self.assertEqual(v1.x, v2.x) + self.assertEqual(v1.y, v2.y) + # issue 214 + self.assertEqual(Vector2(0, 1).rotate(359.99999999), Vector2(0, 1)) + + def test_rotate_rad(self): + tests = ( + ((1, 0), math.pi), + ((1, 0), math.pi / 2), + ((1, 0), -math.pi / 2), + ((1, 0), math.pi / 4), + ) + for initialVec, radians in tests: + self.assertEqual( + Vector2(initialVec).rotate_rad(radians), + (math.cos(radians), math.sin(radians)), + ) + + def test_rotate_ip(self): + v = Vector2(1, 0) + self.assertEqual(v.rotate_ip(90), None) + self.assertEqual(v.x, 0) + self.assertEqual(v.y, 1) + v = Vector2(-1, -1) + v.rotate_ip(-90) + self.assertEqual(v.x, -1) + self.assertEqual(v.y, 1) + + def test_rotate_rad_ip(self): + tests = ( + ((1, 0), math.pi), + ((1, 0), math.pi / 2), + ((1, 0), -math.pi / 2), + ((1, 0), math.pi / 4), + ) + for initialVec, radians in tests: + vec = Vector2(initialVec) + vec.rotate_rad_ip(radians) + self.assertEqual(vec, (math.cos(radians), math.sin(radians))) + + def test_normalize(self): + v = self.v1.normalize() + # length is 1 + self.assertAlmostEqual(v.x * v.x + v.y * v.y, 1.0) + # v1 is unchanged + self.assertEqual(self.v1.x, self.l1[0]) + self.assertEqual(self.v1.y, self.l1[1]) + # v2 is parallel to v1 + self.assertAlmostEqual(self.v1.x * v.y - self.v1.y * v.x, 0.0) + self.assertRaises(ValueError, lambda: self.zeroVec.normalize()) + + def test_normalize_ip(self): + v = +self.v1 + # v has length != 1 before normalizing + self.assertNotEqual(v.x * v.x + v.y * v.y, 1.0) + # inplace operations should return None + self.assertEqual(v.normalize_ip(), None) + # length is 1 + self.assertAlmostEqual(v.x * v.x + v.y * v.y, 1.0) + # v2 is parallel to v1 + self.assertAlmostEqual(self.v1.x * v.y - self.v1.y * v.x, 0.0) + self.assertRaises(ValueError, lambda: self.zeroVec.normalize_ip()) + + def test_is_normalized(self): + self.assertEqual(self.v1.is_normalized(), False) + v = self.v1.normalize() + self.assertEqual(v.is_normalized(), True) + self.assertEqual(self.e2.is_normalized(), True) + self.assertEqual(self.zeroVec.is_normalized(), False) + + def test_cross(self): + self.assertEqual( + self.v1.cross(self.v2), self.v1.x * self.v2.y - self.v1.y * self.v2.x + ) + self.assertEqual( + self.v1.cross(self.l2), self.v1.x * self.l2[1] - self.v1.y * self.l2[0] + ) + self.assertEqual( + self.v1.cross(self.t2), self.v1.x * self.t2[1] - self.v1.y * self.t2[0] + ) + self.assertEqual(self.v1.cross(self.v2), -self.v2.cross(self.v1)) + self.assertEqual(self.v1.cross(self.v1), 0) + + def test_dot(self): + self.assertAlmostEqual( + self.v1.dot(self.v2), self.v1.x * self.v2.x + self.v1.y * self.v2.y + ) + self.assertAlmostEqual( + self.v1.dot(self.l2), self.v1.x * self.l2[0] + self.v1.y * self.l2[1] + ) + self.assertAlmostEqual( + self.v1.dot(self.t2), self.v1.x * self.t2[0] + self.v1.y * self.t2[1] + ) + self.assertEqual(self.v1.dot(self.v2), self.v2.dot(self.v1)) + self.assertEqual(self.v1.dot(self.v2), self.v1 * self.v2) + + def test_angle_to(self): + self.assertEqual( + self.v1.rotate(self.v1.angle_to(self.v2)).normalize(), self.v2.normalize() + ) + self.assertEqual(Vector2(1, 1).angle_to((-1, 1)), 90) + self.assertEqual(Vector2(1, 0).angle_to((0, -1)), -90) + self.assertEqual(Vector2(1, 0).angle_to((-1, 1)), 135) + self.assertEqual(abs(Vector2(1, 0).angle_to((-1, 0))), 180) + + def test_scale_to_length(self): + v = Vector2(1, 1) + v.scale_to_length(2.5) + self.assertEqual(v, Vector2(2.5, 2.5) / math.sqrt(2)) + self.assertRaises(ValueError, lambda: self.zeroVec.scale_to_length(1)) + self.assertEqual(v.scale_to_length(0), None) + self.assertEqual(v, self.zeroVec) + + def test_length(self): + self.assertEqual(Vector2(3, 4).length(), 5) + self.assertEqual(Vector2(-3, 4).length(), 5) + self.assertEqual(self.zeroVec.length(), 0) + + def test_length_squared(self): + self.assertEqual(Vector2(3, 4).length_squared(), 25) + self.assertEqual(Vector2(-3, 4).length_squared(), 25) + self.assertEqual(self.zeroVec.length_squared(), 0) + + def test_reflect(self): + v = Vector2(1, -1) + n = Vector2(0, 1) + self.assertEqual(v.reflect(n), Vector2(1, 1)) + self.assertEqual(v.reflect(3 * n), v.reflect(n)) + self.assertEqual(v.reflect(-v), -v) + self.assertRaises(ValueError, lambda: v.reflect(self.zeroVec)) + + def test_reflect_ip(self): + v1 = Vector2(1, -1) + v2 = Vector2(v1) + n = Vector2(0, 1) + self.assertEqual(v2.reflect_ip(n), None) + self.assertEqual(v2, Vector2(1, 1)) + v2 = Vector2(v1) + v2.reflect_ip(3 * n) + self.assertEqual(v2, v1.reflect(n)) + v2 = Vector2(v1) + v2.reflect_ip(-v1) + self.assertEqual(v2, -v1) + self.assertRaises(ValueError, lambda: v2.reflect_ip(Vector2())) + + def test_distance_to(self): + diff = self.v1 - self.v2 + self.assertEqual(self.e1.distance_to(self.e2), math.sqrt(2)) + self.assertEqual(self.e1.distance_to((0, 1)), math.sqrt(2)) + self.assertEqual(self.e1.distance_to([0, 1]), math.sqrt(2)) + self.assertAlmostEqual( + self.v1.distance_to(self.v2), math.sqrt(diff.x * diff.x + diff.y * diff.y) + ) + self.assertAlmostEqual( + self.v1.distance_to(self.t2), math.sqrt(diff.x * diff.x + diff.y * diff.y) + ) + self.assertAlmostEqual( + self.v1.distance_to(self.l2), math.sqrt(diff.x * diff.x + diff.y * diff.y) + ) + self.assertEqual(self.v1.distance_to(self.v1), 0) + self.assertEqual(self.v1.distance_to(self.t1), 0) + self.assertEqual(self.v1.distance_to(self.l1), 0) + self.assertEqual(self.v1.distance_to(self.t2), self.v2.distance_to(self.t1)) + self.assertEqual(self.v1.distance_to(self.l2), self.v2.distance_to(self.l1)) + self.assertEqual(self.v1.distance_to(self.v2), self.v2.distance_to(self.v1)) + + def test_distance_squared_to(self): + diff = self.v1 - self.v2 + self.assertEqual(self.e1.distance_squared_to(self.e2), 2) + self.assertEqual(self.e1.distance_squared_to((0, 1)), 2) + self.assertEqual(self.e1.distance_squared_to([0, 1]), 2) + self.assertAlmostEqual( + self.v1.distance_squared_to(self.v2), diff.x * diff.x + diff.y * diff.y + ) + self.assertAlmostEqual( + self.v1.distance_squared_to(self.t2), diff.x * diff.x + diff.y * diff.y + ) + self.assertAlmostEqual( + self.v1.distance_squared_to(self.l2), diff.x * diff.x + diff.y * diff.y + ) + self.assertEqual(self.v1.distance_squared_to(self.v1), 0) + self.assertEqual(self.v1.distance_squared_to(self.t1), 0) + self.assertEqual(self.v1.distance_squared_to(self.l1), 0) + self.assertEqual( + self.v1.distance_squared_to(self.v2), self.v2.distance_squared_to(self.v1) + ) + self.assertEqual( + self.v1.distance_squared_to(self.t2), self.v2.distance_squared_to(self.t1) + ) + self.assertEqual( + self.v1.distance_squared_to(self.l2), self.v2.distance_squared_to(self.l1) + ) + + def test_update(self): + v = Vector2(3, 4) + v.update(0) + self.assertEqual(v, Vector2((0, 0))) + v.update(5, 1) + self.assertEqual(v, Vector2(5, 1)) + v.update((4, 1)) + self.assertNotEqual(v, Vector2((5, 1))) + + def test_swizzle(self): + self.assertEqual(self.v1.yx, (self.v1.y, self.v1.x)) + self.assertEqual( + self.v1.xxyyxy, + (self.v1.x, self.v1.x, self.v1.y, self.v1.y, self.v1.x, self.v1.y), + ) + self.v1.xy = self.t2 + self.assertEqual(self.v1, self.t2) + self.v1.yx = self.t2 + self.assertEqual(self.v1, (self.t2[1], self.t2[0])) + self.assertEqual(type(self.v1), Vector2) + + def invalidSwizzleX(): + Vector2().xx = (1, 2) + + def invalidSwizzleY(): + Vector2().yy = (1, 2) + + self.assertRaises(AttributeError, invalidSwizzleX) + self.assertRaises(AttributeError, invalidSwizzleY) + + def invalidAssignment(): + Vector2().xy = 3 + + self.assertRaises(TypeError, invalidAssignment) + + def unicodeAttribute(): + getattr(Vector2(), "ä") + + self.assertRaises(AttributeError, unicodeAttribute) + + def test_swizzle_return_types(self): + self.assertEqual(type(self.v1.x), float) + self.assertEqual(type(self.v1.xy), Vector2) + self.assertEqual(type(self.v1.xyx), Vector3) + # but we don't have vector4 or above... so tuple. + self.assertEqual(type(self.v1.xyxy), tuple) + self.assertEqual(type(self.v1.xyxyx), tuple) + + def test_elementwise(self): + v1 = self.v1 + v2 = self.v2 + s1 = self.s1 + s2 = self.s2 + # behaviour for "elementwise op scalar" + self.assertEqual(v1.elementwise() + s1, (v1.x + s1, v1.y + s1)) + self.assertEqual(v1.elementwise() - s1, (v1.x - s1, v1.y - s1)) + self.assertEqual(v1.elementwise() * s2, (v1.x * s2, v1.y * s2)) + self.assertEqual(v1.elementwise() / s2, (v1.x / s2, v1.y / s2)) + self.assertEqual(v1.elementwise() // s1, (v1.x // s1, v1.y // s1)) + self.assertEqual(v1.elementwise() ** s1, (v1.x**s1, v1.y**s1)) + self.assertEqual(v1.elementwise() % s1, (v1.x % s1, v1.y % s1)) + self.assertEqual(v1.elementwise() > s1, v1.x > s1 and v1.y > s1) + self.assertEqual(v1.elementwise() < s1, v1.x < s1 and v1.y < s1) + self.assertEqual(v1.elementwise() == s1, v1.x == s1 and v1.y == s1) + self.assertEqual(v1.elementwise() != s1, s1 not in [v1.x, v1.y]) + self.assertEqual(v1.elementwise() >= s1, v1.x >= s1 and v1.y >= s1) + self.assertEqual(v1.elementwise() <= s1, v1.x <= s1 and v1.y <= s1) + self.assertEqual(v1.elementwise() != s1, s1 not in [v1.x, v1.y]) + # behaviour for "scalar op elementwise" + self.assertEqual(s1 + v1.elementwise(), (s1 + v1.x, s1 + v1.y)) + self.assertEqual(s1 - v1.elementwise(), (s1 - v1.x, s1 - v1.y)) + self.assertEqual(s1 * v1.elementwise(), (s1 * v1.x, s1 * v1.y)) + self.assertEqual(s1 / v1.elementwise(), (s1 / v1.x, s1 / v1.y)) + self.assertEqual(s1 // v1.elementwise(), (s1 // v1.x, s1 // v1.y)) + self.assertEqual(s1 ** v1.elementwise(), (s1**v1.x, s1**v1.y)) + self.assertEqual(s1 % v1.elementwise(), (s1 % v1.x, s1 % v1.y)) + self.assertEqual(s1 < v1.elementwise(), s1 < v1.x and s1 < v1.y) + self.assertEqual(s1 > v1.elementwise(), s1 > v1.x and s1 > v1.y) + self.assertEqual(s1 == v1.elementwise(), s1 == v1.x and s1 == v1.y) + self.assertEqual(s1 != v1.elementwise(), s1 not in [v1.x, v1.y]) + self.assertEqual(s1 <= v1.elementwise(), s1 <= v1.x and s1 <= v1.y) + self.assertEqual(s1 >= v1.elementwise(), s1 >= v1.x and s1 >= v1.y) + self.assertEqual(s1 != v1.elementwise(), s1 not in [v1.x, v1.y]) + + # behaviour for "elementwise op vector" + self.assertEqual(type(v1.elementwise() * v2), type(v1)) + self.assertEqual(v1.elementwise() + v2, v1 + v2) + self.assertEqual(v1.elementwise() - v2, v1 - v2) + self.assertEqual(v1.elementwise() * v2, (v1.x * v2.x, v1.y * v2.y)) + self.assertEqual(v1.elementwise() / v2, (v1.x / v2.x, v1.y / v2.y)) + self.assertEqual(v1.elementwise() // v2, (v1.x // v2.x, v1.y // v2.y)) + self.assertEqual(v1.elementwise() ** v2, (v1.x**v2.x, v1.y**v2.y)) + self.assertEqual(v1.elementwise() % v2, (v1.x % v2.x, v1.y % v2.y)) + self.assertEqual(v1.elementwise() > v2, v1.x > v2.x and v1.y > v2.y) + self.assertEqual(v1.elementwise() < v2, v1.x < v2.x and v1.y < v2.y) + self.assertEqual(v1.elementwise() >= v2, v1.x >= v2.x and v1.y >= v2.y) + self.assertEqual(v1.elementwise() <= v2, v1.x <= v2.x and v1.y <= v2.y) + self.assertEqual(v1.elementwise() == v2, v1.x == v2.x and v1.y == v2.y) + self.assertEqual(v1.elementwise() != v2, v1.x != v2.x and v1.y != v2.y) + # behaviour for "vector op elementwise" + self.assertEqual(v2 + v1.elementwise(), v2 + v1) + self.assertEqual(v2 - v1.elementwise(), v2 - v1) + self.assertEqual(v2 * v1.elementwise(), (v2.x * v1.x, v2.y * v1.y)) + self.assertEqual(v2 / v1.elementwise(), (v2.x / v1.x, v2.y / v1.y)) + self.assertEqual(v2 // v1.elementwise(), (v2.x // v1.x, v2.y // v1.y)) + self.assertEqual(v2 ** v1.elementwise(), (v2.x**v1.x, v2.y**v1.y)) + self.assertEqual(v2 % v1.elementwise(), (v2.x % v1.x, v2.y % v1.y)) + self.assertEqual(v2 < v1.elementwise(), v2.x < v1.x and v2.y < v1.y) + self.assertEqual(v2 > v1.elementwise(), v2.x > v1.x and v2.y > v1.y) + self.assertEqual(v2 <= v1.elementwise(), v2.x <= v1.x and v2.y <= v1.y) + self.assertEqual(v2 >= v1.elementwise(), v2.x >= v1.x and v2.y >= v1.y) + self.assertEqual(v2 == v1.elementwise(), v2.x == v1.x and v2.y == v1.y) + self.assertEqual(v2 != v1.elementwise(), v2.x != v1.x and v2.y != v1.y) + + # behaviour for "elementwise op elementwise" + self.assertEqual(v2.elementwise() + v1.elementwise(), v2 + v1) + self.assertEqual(v2.elementwise() - v1.elementwise(), v2 - v1) + self.assertEqual( + v2.elementwise() * v1.elementwise(), (v2.x * v1.x, v2.y * v1.y) + ) + self.assertEqual( + v2.elementwise() / v1.elementwise(), (v2.x / v1.x, v2.y / v1.y) + ) + self.assertEqual( + v2.elementwise() // v1.elementwise(), (v2.x // v1.x, v2.y // v1.y) + ) + self.assertEqual(v2.elementwise() ** v1.elementwise(), (v2.x**v1.x, v2.y**v1.y)) + self.assertEqual( + v2.elementwise() % v1.elementwise(), (v2.x % v1.x, v2.y % v1.y) + ) + self.assertEqual( + v2.elementwise() < v1.elementwise(), v2.x < v1.x and v2.y < v1.y + ) + self.assertEqual( + v2.elementwise() > v1.elementwise(), v2.x > v1.x and v2.y > v1.y + ) + self.assertEqual( + v2.elementwise() <= v1.elementwise(), v2.x <= v1.x and v2.y <= v1.y + ) + self.assertEqual( + v2.elementwise() >= v1.elementwise(), v2.x >= v1.x and v2.y >= v1.y + ) + self.assertEqual( + v2.elementwise() == v1.elementwise(), v2.x == v1.x and v2.y == v1.y + ) + self.assertEqual( + v2.elementwise() != v1.elementwise(), v2.x != v1.x and v2.y != v1.y + ) + + # other behaviour + self.assertEqual(abs(v1.elementwise()), (abs(v1.x), abs(v1.y))) + self.assertEqual(-v1.elementwise(), -v1) + self.assertEqual(+v1.elementwise(), +v1) + self.assertEqual(bool(v1.elementwise()), bool(v1)) + self.assertEqual(bool(Vector2().elementwise()), bool(Vector2())) + self.assertEqual(self.zeroVec.elementwise() ** 0, (1, 1)) + self.assertRaises(ValueError, lambda: pow(Vector2(-1, 0).elementwise(), 1.2)) + self.assertRaises(ZeroDivisionError, lambda: self.zeroVec.elementwise() ** -1) + self.assertRaises(ZeroDivisionError, lambda: self.zeroVec.elementwise() ** -1) + self.assertRaises(ZeroDivisionError, lambda: Vector2(1, 1).elementwise() / 0) + self.assertRaises(ZeroDivisionError, lambda: Vector2(1, 1).elementwise() // 0) + self.assertRaises(ZeroDivisionError, lambda: Vector2(1, 1).elementwise() % 0) + self.assertRaises( + ZeroDivisionError, lambda: Vector2(1, 1).elementwise() / self.zeroVec + ) + self.assertRaises( + ZeroDivisionError, lambda: Vector2(1, 1).elementwise() // self.zeroVec + ) + self.assertRaises( + ZeroDivisionError, lambda: Vector2(1, 1).elementwise() % self.zeroVec + ) + self.assertRaises(ZeroDivisionError, lambda: 2 / self.zeroVec.elementwise()) + self.assertRaises(ZeroDivisionError, lambda: 2 // self.zeroVec.elementwise()) + self.assertRaises(ZeroDivisionError, lambda: 2 % self.zeroVec.elementwise()) + + def test_slerp(self): + self.assertRaises(ValueError, lambda: self.zeroVec.slerp(self.v1, 0.5)) + self.assertRaises(ValueError, lambda: self.v1.slerp(self.zeroVec, 0.5)) + self.assertRaises(ValueError, lambda: self.zeroVec.slerp(self.zeroVec, 0.5)) + v1 = Vector2(1, 0) + v2 = Vector2(0, 1) + steps = 10 + angle_step = v1.angle_to(v2) / steps + for i, u in ((i, v1.slerp(v2, i / float(steps))) for i in range(steps + 1)): + self.assertAlmostEqual(u.length(), 1) + self.assertAlmostEqual(v1.angle_to(u), i * angle_step) + self.assertEqual(u, v2) + + v1 = Vector2(100, 0) + v2 = Vector2(0, 10) + radial_factor = v2.length() / v1.length() + for i, u in ((i, v1.slerp(v2, -i / float(steps))) for i in range(steps + 1)): + self.assertAlmostEqual( + u.length(), + (v2.length() - v1.length()) * (float(i) / steps) + v1.length(), + ) + self.assertEqual(u, v2) + self.assertEqual(v1.slerp(v1, 0.5), v1) + self.assertEqual(v2.slerp(v2, 0.5), v2) + self.assertRaises(ValueError, lambda: v1.slerp(-v1, 0.5)) + + def test_lerp(self): + v1 = Vector2(0, 0) + v2 = Vector2(10, 10) + self.assertEqual(v1.lerp(v2, 0.5), (5, 5)) + self.assertRaises(ValueError, lambda: v1.lerp(v2, 2.5)) + + v1 = Vector2(-10, -5) + v2 = Vector2(10, 10) + self.assertEqual(v1.lerp(v2, 0.5), (0, 2.5)) + + def test_polar(self): + v = Vector2() + v.from_polar(self.v1.as_polar()) + self.assertEqual(self.v1, v) + self.assertEqual(self.v1, Vector2.from_polar(self.v1.as_polar())) + self.assertEqual(self.e1.as_polar(), (1, 0)) + self.assertEqual(self.e2.as_polar(), (1, 90)) + self.assertEqual((2 * self.e2).as_polar(), (2, 90)) + self.assertRaises(TypeError, lambda: v.from_polar((None, None))) + self.assertRaises(TypeError, lambda: v.from_polar("ab")) + self.assertRaises(TypeError, lambda: v.from_polar((None, 1))) + self.assertRaises(TypeError, lambda: v.from_polar((1, 2, 3))) + self.assertRaises(TypeError, lambda: v.from_polar((1,))) + self.assertRaises(TypeError, lambda: v.from_polar(1, 2)) + self.assertRaises(TypeError, lambda: Vector2.from_polar((None, None))) + self.assertRaises(TypeError, lambda: Vector2.from_polar("ab")) + self.assertRaises(TypeError, lambda: Vector2.from_polar((None, 1))) + self.assertRaises(TypeError, lambda: Vector2.from_polar((1, 2, 3))) + self.assertRaises(TypeError, lambda: Vector2.from_polar((1,))) + self.assertRaises(TypeError, lambda: Vector2.from_polar(1, 2)) + v.from_polar((0.5, 90)) + self.assertEqual(v, 0.5 * self.e2) + self.assertEqual(Vector2.from_polar((0.5, 90)), 0.5 * self.e2) + self.assertEqual(Vector2.from_polar((0.5, 90)), v) + v.from_polar((1, 0)) + self.assertEqual(v, self.e1) + self.assertEqual(Vector2.from_polar((1, 0)), self.e1) + self.assertEqual(Vector2.from_polar((1, 0)), v) + + def test_subclass_operation(self): + class Vector(pygame.math.Vector2): + pass + + vec_a = Vector(2, 0) + vec_b = Vector(0, 1) + + result_add = vec_a + vec_b + self.assertEqual(result_add, Vector(2, 1)) + + vec_a *= 2 + self.assertEqual(vec_a, Vector(4, 0)) + + def test_project_v2_onto_x_axis(self): + """Project onto x-axis, e.g. get the component pointing in the x-axis direction.""" + # arrange + v = Vector2(2, 2) + x_axis = Vector2(10, 0) + + # act + actual = v.project(x_axis) + + # assert + self.assertEqual(v.x, actual.x) + self.assertEqual(0, actual.y) + + def test_project_v2_onto_y_axis(self): + """Project onto y-axis, e.g. get the component pointing in the y-axis direction.""" + # arrange + v = Vector2(2, 2) + y_axis = Vector2(0, 100) + + # act + actual = v.project(y_axis) + + # assert + self.assertEqual(0, actual.x) + self.assertEqual(v.y, actual.y) + + def test_project_v2_onto_other(self): + """Project onto other vector.""" + # arrange + v = Vector2(2, 3) + other = Vector2(3, 5) + + # act + actual = v.project(other) + + # assert + expected = v.dot(other) / other.dot(other) * other + self.assertEqual(expected.x, actual.x) + self.assertEqual(expected.y, actual.y) + + def test_project_v2_onto_other_as_tuple(self): + """Project onto other tuple as vector.""" + # arrange + v = Vector2(2, 3) + other = Vector2(3, 5) + + # act + actual = v.project(tuple(other)) + + # assert + expected = v.dot(other) / other.dot(other) * other + self.assertEqual(expected.x, actual.x) + self.assertEqual(expected.y, actual.y) + + def test_project_v2_onto_other_as_list(self): + """Project onto other list as vector.""" + # arrange + v = Vector2(2, 3) + other = Vector2(3, 5) + + # act + actual = v.project(list(other)) + + # assert + expected = v.dot(other) / other.dot(other) * other + self.assertEqual(expected.x, actual.x) + self.assertEqual(expected.y, actual.y) + + def test_project_v2_raises_if_other_has_zero_length(self): + """Check if exception is raise when projected on vector has zero length.""" + # arrange + v = Vector2(2, 3) + other = Vector2(0, 0) + + # act / assert + self.assertRaises(ValueError, v.project, other) + + def test_project_v2_raises_if_other_is_not_iterable(self): + """Check if exception is raise when projected on vector is not iterable.""" + # arrange + v = Vector2(2, 3) + other = 10 + + # act / assert + self.assertRaises(TypeError, v.project, other) + + def test_collection_abc(self): + v = Vector2(3, 4) + self.assertTrue(isinstance(v, Collection)) + self.assertFalse(isinstance(v, Sequence)) + + def test_clamp_mag_v2_max(self): + v1 = Vector2(7, 2) + v2 = v1.clamp_magnitude(5) + v3 = v1.clamp_magnitude(0, 5) + self.assertEqual(v2, v3) + + v1.clamp_magnitude_ip(5) + self.assertEqual(v1, v2) + + v1.clamp_magnitude_ip(0, 5) + self.assertEqual(v1, v2) + + expected_v2 = Vector2(4.807619738204116, 1.3736056394868903) + self.assertEqual(expected_v2, v2) + + def test_clamp_mag_v2_min(self): + v1 = Vector2(1, 2) + v2 = v1.clamp_magnitude(3, 5) + v1.clamp_magnitude_ip(3, 5) + expected_v2 = Vector2(1.3416407864998738, 2.6832815729997477) + self.assertEqual(expected_v2, v2) + self.assertEqual(expected_v2, v1) + + def test_clamp_mag_v2_no_change(self): + v1 = Vector2(1, 2) + for args in ( + (1, 6), + (1.12, 3.55), + (0.93, 2.83), + (7.6,), + ): + with self.subTest(args=args): + v2 = v1.clamp_magnitude(*args) + v1.clamp_magnitude_ip(*args) + self.assertEqual(v1, v2) + self.assertEqual(v1, Vector2(1, 2)) + + def test_clamp_mag_v2_edge_cases(self): + v1 = Vector2(1, 2) + v2 = v1.clamp_magnitude(6, 6) + v1.clamp_magnitude_ip(6, 6) + self.assertEqual(v1, v2) + self.assertAlmostEqual(v1.length(), 6) + + v2 = v1.clamp_magnitude(0) + v1.clamp_magnitude_ip(0, 0) + self.assertEqual(v1, v2) + self.assertEqual(v1, Vector2()) + + def test_clamp_mag_v2_errors(self): + v1 = Vector2(1, 2) + for invalid_args in ( + ("foo", "bar"), + (1, 2, 3), + (342.234, "test"), + ): + with self.subTest(invalid_args=invalid_args): + self.assertRaises(TypeError, v1.clamp_magnitude, *invalid_args) + self.assertRaises(TypeError, v1.clamp_magnitude_ip, *invalid_args) + + for invalid_args in ( + (-1,), + (4, 3), # min > max + (-4, 10), + (-4, -2), + ): + with self.subTest(invalid_args=invalid_args): + self.assertRaises(ValueError, v1.clamp_magnitude, *invalid_args) + self.assertRaises(ValueError, v1.clamp_magnitude_ip, *invalid_args) + + # 0 vector + v2 = Vector2() + self.assertRaises(ValueError, v2.clamp_magnitude, 3) + self.assertRaises(ValueError, v2.clamp_magnitude_ip, 4) + + def test_subclassing_v2(self): + """Check if Vector2 is subclassable""" + v = Vector2(4, 2) + + class TestVector(Vector2): + def supermariobrosiscool(self): + return 722 + + other = TestVector(4, 1) + + self.assertEqual(other.supermariobrosiscool(), 722) + self.assertNotEqual(type(v), TestVector) + self.assertNotEqual(type(v), type(other.copy())) + self.assertEqual(TestVector, type(other.reflect(v))) + self.assertEqual(TestVector, type(other.lerp(v, 1))) + self.assertEqual(TestVector, type(other.slerp(v, 1))) + self.assertEqual(TestVector, type(other.rotate(5))) + self.assertEqual(TestVector, type(other.rotate_rad(5))) + self.assertEqual(TestVector, type(other.project(v))) + self.assertEqual(TestVector, type(other.move_towards(v, 5))) + self.assertEqual(TestVector, type(other.clamp_magnitude(5))) + self.assertEqual(TestVector, type(other.clamp_magnitude(1, 5))) + self.assertEqual(TestVector, type(other.elementwise() + other)) + + other1 = TestVector(4, 2) + + self.assertEqual(type(other + other1), TestVector) + self.assertEqual(type(other - other1), TestVector) + self.assertEqual(type(other * 3), TestVector) + self.assertEqual(type(other / 3), TestVector) + self.assertEqual(type(other.elementwise() ** 3), TestVector) + + +class Vector3TypeTest(unittest.TestCase): + def setUp(self): + self.zeroVec = Vector3() + self.e1 = Vector3(1, 0, 0) + self.e2 = Vector3(0, 1, 0) + self.e3 = Vector3(0, 0, 1) + self.t1 = (1.2, 3.4, 9.6) + self.l1 = list(self.t1) + self.v1 = Vector3(self.t1) + self.t2 = (5.6, 7.8, 2.1) + self.l2 = list(self.t2) + self.v2 = Vector3(self.t2) + self.s1 = 5.6 + self.s2 = 7.8 + + def testConstructionDefault(self): + v = Vector3() + self.assertEqual(v.x, 0.0) + self.assertEqual(v.y, 0.0) + self.assertEqual(v.z, 0.0) + + def testConstructionXYZ(self): + v = Vector3(1.2, 3.4, 9.6) + self.assertEqual(v.x, 1.2) + self.assertEqual(v.y, 3.4) + self.assertEqual(v.z, 9.6) + + def testConstructionTuple(self): + v = Vector3((1.2, 3.4, 9.6)) + self.assertEqual(v.x, 1.2) + self.assertEqual(v.y, 3.4) + self.assertEqual(v.z, 9.6) + + def testConstructionList(self): + v = Vector3([1.2, 3.4, -9.6]) + self.assertEqual(v.x, 1.2) + self.assertEqual(v.y, 3.4) + self.assertEqual(v.z, -9.6) + + def testConstructionVector3(self): + v = Vector3(Vector3(1.2, 3.4, -9.6)) + self.assertEqual(v.x, 1.2) + self.assertEqual(v.y, 3.4) + self.assertEqual(v.z, -9.6) + + def testConstructionScalar(self): + v = Vector3(1) + self.assertEqual(v.x, 1.0) + self.assertEqual(v.y, 1.0) + self.assertEqual(v.z, 1.0) + + def testConstructionScalarKeywords(self): + v = Vector3(x=1) + self.assertEqual(v.x, 1.0) + self.assertEqual(v.y, 1.0) + self.assertEqual(v.z, 1.0) + + def testConstructionKeywords(self): + v = Vector3(x=1, y=2, z=3) + self.assertEqual(v.x, 1.0) + self.assertEqual(v.y, 2.0) + self.assertEqual(v.z, 3.0) + + def testConstructionMissing(self): + self.assertRaises(ValueError, Vector3, 1, 2) + self.assertRaises(ValueError, Vector3, x=1, y=2) + + def testAttributeAccess(self): + tmp = self.v1.x + self.assertEqual(tmp, self.v1.x) + self.assertEqual(tmp, self.v1[0]) + tmp = self.v1.y + self.assertEqual(tmp, self.v1.y) + self.assertEqual(tmp, self.v1[1]) + tmp = self.v1.z + self.assertEqual(tmp, self.v1.z) + self.assertEqual(tmp, self.v1[2]) + self.v1.x = 3.141 + self.assertEqual(self.v1.x, 3.141) + self.v1.y = 3.141 + self.assertEqual(self.v1.y, 3.141) + self.v1.z = 3.141 + self.assertEqual(self.v1.z, 3.141) + + def assign_nonfloat(): + v = Vector2() + v.x = "spam" + + self.assertRaises(TypeError, assign_nonfloat) + + def testCopy(self): + v_copy0 = Vector3(2014.0, 2032.0, 2076.0) + v_copy1 = v_copy0.copy() + self.assertEqual(v_copy0.x, v_copy1.x) + self.assertEqual(v_copy0.y, v_copy1.y) + self.assertEqual(v_copy0.z, v_copy1.z) + + def testSequence(self): + v = Vector3(1.2, 3.4, -9.6) + self.assertEqual(len(v), 3) + self.assertEqual(v[0], 1.2) + self.assertEqual(v[1], 3.4) + self.assertEqual(v[2], -9.6) + self.assertRaises(IndexError, lambda: v[3]) + self.assertEqual(v[-1], -9.6) + self.assertEqual(v[-2], 3.4) + self.assertEqual(v[-3], 1.2) + self.assertRaises(IndexError, lambda: v[-4]) + self.assertEqual(v[:], [1.2, 3.4, -9.6]) + self.assertEqual(v[1:], [3.4, -9.6]) + self.assertEqual(v[:1], [1.2]) + self.assertEqual(v[:-1], [1.2, 3.4]) + self.assertEqual(v[1:2], [3.4]) + self.assertEqual(list(v), [1.2, 3.4, -9.6]) + self.assertEqual(tuple(v), (1.2, 3.4, -9.6)) + v[0] = 5.6 + v[1] = 7.8 + v[2] = -2.1 + self.assertEqual(v.x, 5.6) + self.assertEqual(v.y, 7.8) + self.assertEqual(v.z, -2.1) + v[:] = [9.1, 11.12, -13.41] + self.assertEqual(v.x, 9.1) + self.assertEqual(v.y, 11.12) + self.assertEqual(v.z, -13.41) + + def overpopulate(): + v = Vector3() + v[:] = [1, 2, 3, 4] + + self.assertRaises(ValueError, overpopulate) + + def underpopulate(): + v = Vector3() + v[:] = [1] + + self.assertRaises(ValueError, underpopulate) + + def assign_nonfloat(): + v = Vector2() + v[0] = "spam" + + self.assertRaises(TypeError, assign_nonfloat) + + def testExtendedSlicing(self): + # deletion + def delSlice(vec, start=None, stop=None, step=None): + if start is not None and stop is not None and step is not None: + del vec[start:stop:step] + elif start is not None and stop is None and step is not None: + del vec[start::step] + elif start is None and stop is None and step is not None: + del vec[::step] + + v = Vector3(self.v1) + self.assertRaises(TypeError, delSlice, v, None, None, 2) + self.assertRaises(TypeError, delSlice, v, 1, None, 2) + self.assertRaises(TypeError, delSlice, v, 1, 2, 1) + + # assignment + v = Vector3(self.v1) + v[::2] = [-1.1, -2.2] + self.assertEqual(v, [-1.1, self.v1.y, -2.2]) + v = Vector3(self.v1) + v[::-2] = [10, 20] + self.assertEqual(v, [20, self.v1.y, 10]) + v = Vector3(self.v1) + v[::-1] = v + self.assertEqual(v, [self.v1.z, self.v1.y, self.v1.x]) + a = Vector3(self.v1) + b = Vector3(self.v1) + c = Vector3(self.v1) + a[1:2] = [2.2] + b[slice(1, 2)] = [2.2] + c[1:2:] = (2.2,) + self.assertEqual(a, b) + self.assertEqual(a, c) + self.assertEqual(type(a), type(self.v1)) + self.assertEqual(type(b), type(self.v1)) + self.assertEqual(type(c), type(self.v1)) + + def test_contains(self): + c = Vector3(0, 1, 2) + + # call __contains__ explicitly to test that it is defined + self.assertTrue(c.__contains__(0)) + self.assertTrue(0 in c) + self.assertTrue(1 in c) + self.assertTrue(2 in c) + self.assertTrue(3 not in c) + self.assertFalse(c.__contains__(10)) + + self.assertRaises(TypeError, lambda: "string" in c) + self.assertRaises(TypeError, lambda: 3 + 4j in c) + + def testAdd(self): + v3 = self.v1 + self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x + self.v2.x) + self.assertEqual(v3.y, self.v1.y + self.v2.y) + self.assertEqual(v3.z, self.v1.z + self.v2.z) + v3 = self.v1 + self.t2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x + self.t2[0]) + self.assertEqual(v3.y, self.v1.y + self.t2[1]) + self.assertEqual(v3.z, self.v1.z + self.t2[2]) + v3 = self.v1 + self.l2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x + self.l2[0]) + self.assertEqual(v3.y, self.v1.y + self.l2[1]) + self.assertEqual(v3.z, self.v1.z + self.l2[2]) + v3 = self.t1 + self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.t1[0] + self.v2.x) + self.assertEqual(v3.y, self.t1[1] + self.v2.y) + self.assertEqual(v3.z, self.t1[2] + self.v2.z) + v3 = self.l1 + self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.l1[0] + self.v2.x) + self.assertEqual(v3.y, self.l1[1] + self.v2.y) + self.assertEqual(v3.z, self.l1[2] + self.v2.z) + + def testSub(self): + v3 = self.v1 - self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x - self.v2.x) + self.assertEqual(v3.y, self.v1.y - self.v2.y) + self.assertEqual(v3.z, self.v1.z - self.v2.z) + v3 = self.v1 - self.t2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x - self.t2[0]) + self.assertEqual(v3.y, self.v1.y - self.t2[1]) + self.assertEqual(v3.z, self.v1.z - self.t2[2]) + v3 = self.v1 - self.l2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.v1.x - self.l2[0]) + self.assertEqual(v3.y, self.v1.y - self.l2[1]) + self.assertEqual(v3.z, self.v1.z - self.l2[2]) + v3 = self.t1 - self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.t1[0] - self.v2.x) + self.assertEqual(v3.y, self.t1[1] - self.v2.y) + self.assertEqual(v3.z, self.t1[2] - self.v2.z) + v3 = self.l1 - self.v2 + self.assertTrue(isinstance(v3, type(self.v1))) + self.assertEqual(v3.x, self.l1[0] - self.v2.x) + self.assertEqual(v3.y, self.l1[1] - self.v2.y) + self.assertEqual(v3.z, self.l1[2] - self.v2.z) + + def testScalarMultiplication(self): + v = self.s1 * self.v1 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertEqual(v.x, self.s1 * self.v1.x) + self.assertEqual(v.y, self.s1 * self.v1.y) + self.assertEqual(v.z, self.s1 * self.v1.z) + v = self.v1 * self.s2 + self.assertEqual(v.x, self.v1.x * self.s2) + self.assertEqual(v.y, self.v1.y * self.s2) + self.assertEqual(v.z, self.v1.z * self.s2) + + def testScalarDivision(self): + v = self.v1 / self.s1 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertAlmostEqual(v.x, self.v1.x / self.s1) + self.assertAlmostEqual(v.y, self.v1.y / self.s1) + self.assertAlmostEqual(v.z, self.v1.z / self.s1) + v = self.v1 // self.s2 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertEqual(v.x, self.v1.x // self.s2) + self.assertEqual(v.y, self.v1.y // self.s2) + self.assertEqual(v.z, self.v1.z // self.s2) + + def testBool(self): + self.assertEqual(bool(self.zeroVec), False) + self.assertEqual(bool(self.v1), True) + self.assertTrue(not self.zeroVec) + self.assertTrue(self.v1) + + def testUnary(self): + v = +self.v1 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertEqual(v.x, self.v1.x) + self.assertEqual(v.y, self.v1.y) + self.assertEqual(v.z, self.v1.z) + self.assertNotEqual(id(v), id(self.v1)) + v = -self.v1 + self.assertTrue(isinstance(v, type(self.v1))) + self.assertEqual(v.x, -self.v1.x) + self.assertEqual(v.y, -self.v1.y) + self.assertEqual(v.z, -self.v1.z) + self.assertNotEqual(id(v), id(self.v1)) + + def testCompare(self): + int_vec = Vector3(3, -2, 13) + flt_vec = Vector3(3.0, -2.0, 13.0) + zero_vec = Vector3(0, 0, 0) + self.assertEqual(int_vec == flt_vec, True) + self.assertEqual(int_vec != flt_vec, False) + self.assertEqual(int_vec != zero_vec, True) + self.assertEqual(flt_vec == zero_vec, False) + self.assertEqual(int_vec == (3, -2, 13), True) + self.assertEqual(int_vec != (3, -2, 13), False) + self.assertEqual(int_vec != [0, 0], True) + self.assertEqual(int_vec == [0, 0], False) + self.assertEqual(int_vec != 5, True) + self.assertEqual(int_vec == 5, False) + self.assertEqual(int_vec != [3, -2, 0, 1], True) + self.assertEqual(int_vec == [3, -2, 0, 1], False) + + def testStr(self): + v = Vector3(1.2, 3.4, 5.6) + self.assertEqual(str(v), "[1.2, 3.4, 5.6]") + + def testRepr(self): + v = Vector3(1.2, 3.4, -9.6) + self.assertEqual(v.__repr__(), "") + self.assertEqual(v, Vector3(v.__repr__())) + + def testIter(self): + it = self.v1.__iter__() + next_ = it.__next__ + self.assertEqual(next_(), self.v1[0]) + self.assertEqual(next_(), self.v1[1]) + self.assertEqual(next_(), self.v1[2]) + self.assertRaises(StopIteration, lambda: next_()) + it1 = self.v1.__iter__() + it2 = self.v1.__iter__() + self.assertNotEqual(id(it1), id(it2)) + self.assertEqual(id(it1), id(it1.__iter__())) + self.assertEqual(list(it1), list(it2)) + self.assertEqual(list(self.v1.__iter__()), self.l1) + idx = 0 + for val in self.v1: + self.assertEqual(val, self.v1[idx]) + idx += 1 + + def test___round___basic(self): + self.assertEqual( + round(pygame.Vector3(0.0, 0.0, 0.0)), pygame.Vector3(0.0, 0.0, 0.0) + ) + self.assertEqual(type(round(pygame.Vector3(0.0, 0.0, 0.0))), pygame.Vector3) + self.assertEqual( + round(pygame.Vector3(1.0, 1.0, 1.0)), round(pygame.Vector3(1.0, 1.0, 1.0)) + ) + self.assertEqual( + round(pygame.Vector3(10.0, 10.0, 10.0)), + round(pygame.Vector3(10.0, 10.0, 10.0)), + ) + self.assertEqual( + round(pygame.Vector3(1000000000.0, 1000000000.0, 1000000000.0)), + pygame.Vector3(1000000000.0, 1000000000.0, 1000000000.0), + ) + self.assertEqual( + round(pygame.Vector3(1e20, 1e20, 1e20)), pygame.Vector3(1e20, 1e20, 1e20) + ) + + self.assertEqual( + round(pygame.Vector3(-1.0, -1.0, -1.0)), pygame.Vector3(-1.0, -1.0, -1.0) + ) + self.assertEqual( + round(pygame.Vector3(-10.0, -10.0, -10.0)), + pygame.Vector3(-10.0, -10.0, -10.0), + ) + self.assertEqual( + round(pygame.Vector3(-1000000000.0, -1000000000.0, -1000000000.0)), + pygame.Vector3(-1000000000.0, -1000000000.0, -1000000000.0), + ) + self.assertEqual( + round(pygame.Vector3(-1e20, -1e20, -1e20)), + pygame.Vector3(-1e20, -1e20, -1e20), + ) + + self.assertEqual( + round(pygame.Vector3(0.1, 0.1, 0.1)), pygame.Vector3(0.0, 0.0, 0.0) + ) + self.assertEqual( + round(pygame.Vector3(1.1, 1.1, 1.1)), pygame.Vector3(1.0, 1.0, 1.0) + ) + self.assertEqual( + round(pygame.Vector3(10.1, 10.1, 10.1)), pygame.Vector3(10.0, 10.0, 10.0) + ) + self.assertEqual( + round(pygame.Vector3(1000000000.1, 1000000000.1, 1000000000.1)), + pygame.Vector3(1000000000.0, 1000000000.0, 1000000000.0), + ) + + self.assertEqual( + round(pygame.Vector3(-1.1, -1.1, -1.1)), pygame.Vector3(-1.0, -1.0, -1.0) + ) + self.assertEqual( + round(pygame.Vector3(-10.1, -10.1, -10.1)), + pygame.Vector3(-10.0, -10.0, -10.0), + ) + self.assertEqual( + round(pygame.Vector3(-1000000000.1, -1000000000.1, -1000000000.1)), + pygame.Vector3(-1000000000.0, -1000000000.0, -1000000000.0), + ) + + self.assertEqual( + round(pygame.Vector3(0.9, 0.9, 0.9)), pygame.Vector3(1.0, 1.0, 1.0) + ) + self.assertEqual( + round(pygame.Vector3(9.9, 9.9, 9.9)), pygame.Vector3(10.0, 10.0, 10.0) + ) + self.assertEqual( + round(pygame.Vector3(999999999.9, 999999999.9, 999999999.9)), + pygame.Vector3(1000000000.0, 1000000000.0, 1000000000.0), + ) + + self.assertEqual( + round(pygame.Vector3(-0.9, -0.9, -0.9)), pygame.Vector3(-1.0, -1.0, -1.0) + ) + self.assertEqual( + round(pygame.Vector3(-9.9, -9.9, -9.9)), pygame.Vector3(-10.0, -10.0, -10.0) + ) + self.assertEqual( + round(pygame.Vector3(-999999999.9, -999999999.9, -999999999.9)), + pygame.Vector3(-1000000000.0, -1000000000.0, -1000000000.0), + ) + + self.assertEqual( + round(pygame.Vector3(-8.0, -8.0, -8.0), -1), + pygame.Vector3(-10.0, -10.0, -10.0), + ) + self.assertEqual( + type(round(pygame.Vector3(-8.0, -8.0, -8.0), -1)), pygame.Vector3 + ) + + self.assertEqual( + type(round(pygame.Vector3(-8.0, -8.0, -8.0), 0)), pygame.Vector3 + ) + self.assertEqual( + type(round(pygame.Vector3(-8.0, -8.0, -8.0), 1)), pygame.Vector3 + ) + + # Check even / odd rounding behaviour + self.assertEqual(round(pygame.Vector3(5.5, 5.5, 5.5)), pygame.Vector3(6, 6, 6)) + self.assertEqual( + round(pygame.Vector3(5.4, 5.4, 5.4)), pygame.Vector3(5.0, 5.0, 5.0) + ) + self.assertEqual( + round(pygame.Vector3(5.6, 5.6, 5.6)), pygame.Vector3(6.0, 6.0, 6.0) + ) + self.assertEqual( + round(pygame.Vector3(-5.5, -5.5, -5.5)), pygame.Vector3(-6, -6, -6) + ) + self.assertEqual( + round(pygame.Vector3(-5.4, -5.4, -5.4)), pygame.Vector3(-5, -5, -5) + ) + self.assertEqual( + round(pygame.Vector3(-5.6, -5.6, -5.6)), pygame.Vector3(-6, -6, -6) + ) + + self.assertRaises(TypeError, round, pygame.Vector3(1.0, 1.0, 1.0), 1.5) + self.assertRaises(TypeError, round, pygame.Vector3(1.0, 1.0, 1.0), "a") + + def test_rotate(self): + v1 = Vector3(1, 0, 0) + axis = Vector3(0, 1, 0) + v2 = v1.rotate(90, axis) + v3 = v1.rotate(90 + 360, axis) + self.assertEqual(v1.x, 1) + self.assertEqual(v1.y, 0) + self.assertEqual(v1.z, 0) + self.assertEqual(v2.x, 0) + self.assertEqual(v2.y, 0) + self.assertEqual(v2.z, -1) + self.assertEqual(v3.x, v2.x) + self.assertEqual(v3.y, v2.y) + self.assertEqual(v3.z, v2.z) + v1 = Vector3(-1, -1, -1) + v2 = v1.rotate(-90, axis) + self.assertEqual(v2.x, 1) + self.assertEqual(v2.y, -1) + self.assertEqual(v2.z, -1) + v2 = v1.rotate(360, axis) + self.assertEqual(v1.x, v2.x) + self.assertEqual(v1.y, v2.y) + self.assertEqual(v1.z, v2.z) + v2 = v1.rotate(0, axis) + self.assertEqual(v1.x, v2.x) + self.assertEqual(v1.y, v2.y) + self.assertEqual(v1.z, v2.z) + # issue 214 + self.assertEqual( + Vector3(0, 1, 0).rotate(359.9999999, Vector3(0, 0, 1)), Vector3(0, 1, 0) + ) + + def test_rotate_rad(self): + axis = Vector3(0, 0, 1) + tests = ( + ((1, 0, 0), math.pi), + ((1, 0, 0), math.pi / 2), + ((1, 0, 0), -math.pi / 2), + ((1, 0, 0), math.pi / 4), + ) + for initialVec, radians in tests: + vec = Vector3(initialVec).rotate_rad(radians, axis) + self.assertEqual(vec, (math.cos(radians), math.sin(radians), 0)) + + def test_rotate_ip(self): + v = Vector3(1, 0, 0) + axis = Vector3(0, 1, 0) + self.assertEqual(v.rotate_ip(90, axis), None) + self.assertEqual(v.x, 0) + self.assertEqual(v.y, 0) + self.assertEqual(v.z, -1) + v = Vector3(-1, -1, 1) + v.rotate_ip(-90, axis) + self.assertEqual(v.x, -1) + self.assertEqual(v.y, -1) + self.assertEqual(v.z, -1) + + def test_rotate_rad_ip(self): + axis = Vector3(0, 0, 1) + tests = ( + ((1, 0, 0), math.pi), + ((1, 0, 0), math.pi / 2), + ((1, 0, 0), -math.pi / 2), + ((1, 0, 0), math.pi / 4), + ) + for initialVec, radians in tests: + vec = Vector3(initialVec) + vec.rotate_rad_ip(radians, axis) + self.assertEqual(vec, (math.cos(radians), math.sin(radians), 0)) + + def test_rotate_x(self): + v1 = Vector3(1, 0, 0) + v2 = v1.rotate_x(90) + v3 = v1.rotate_x(90 + 360) + self.assertEqual(v1.x, 1) + self.assertEqual(v1.y, 0) + self.assertEqual(v1.z, 0) + self.assertEqual(v2.x, 1) + self.assertEqual(v2.y, 0) + self.assertEqual(v2.z, 0) + self.assertEqual(v3.x, v2.x) + self.assertEqual(v3.y, v2.y) + self.assertEqual(v3.z, v2.z) + v1 = Vector3(-1, -1, -1) + v2 = v1.rotate_x(-90) + self.assertEqual(v2.x, -1) + self.assertAlmostEqual(v2.y, -1) + self.assertAlmostEqual(v2.z, 1) + v2 = v1.rotate_x(360) + self.assertAlmostEqual(v1.x, v2.x) + self.assertAlmostEqual(v1.y, v2.y) + self.assertAlmostEqual(v1.z, v2.z) + v2 = v1.rotate_x(0) + self.assertEqual(v1.x, v2.x) + self.assertAlmostEqual(v1.y, v2.y) + self.assertAlmostEqual(v1.z, v2.z) + + def test_rotate_x_rad(self): + vec = Vector3(0, 1, 0) + result = vec.rotate_x_rad(math.pi / 2) + self.assertEqual(result, (0, 0, 1)) + + def test_rotate_x_ip(self): + v = Vector3(1, 0, 0) + self.assertEqual(v.rotate_x_ip(90), None) + self.assertEqual(v.x, 1) + self.assertEqual(v.y, 0) + self.assertEqual(v.z, 0) + v = Vector3(-1, -1, 1) + v.rotate_x_ip(-90) + self.assertEqual(v.x, -1) + self.assertAlmostEqual(v.y, 1) + self.assertAlmostEqual(v.z, 1) + + def test_rotate_x_rad_ip(self): + vec = Vector3(0, 1, 0) + vec.rotate_x_rad_ip(math.pi / 2) + self.assertEqual(vec, (0, 0, 1)) + + def test_rotate_y(self): + v1 = Vector3(1, 0, 0) + v2 = v1.rotate_y(90) + v3 = v1.rotate_y(90 + 360) + self.assertEqual(v1.x, 1) + self.assertEqual(v1.y, 0) + self.assertEqual(v1.z, 0) + self.assertAlmostEqual(v2.x, 0) + self.assertEqual(v2.y, 0) + self.assertAlmostEqual(v2.z, -1) + self.assertAlmostEqual(v3.x, v2.x) + self.assertEqual(v3.y, v2.y) + self.assertAlmostEqual(v3.z, v2.z) + v1 = Vector3(-1, -1, -1) + v2 = v1.rotate_y(-90) + self.assertAlmostEqual(v2.x, 1) + self.assertEqual(v2.y, -1) + self.assertAlmostEqual(v2.z, -1) + v2 = v1.rotate_y(360) + self.assertAlmostEqual(v1.x, v2.x) + self.assertEqual(v1.y, v2.y) + self.assertAlmostEqual(v1.z, v2.z) + v2 = v1.rotate_y(0) + self.assertEqual(v1.x, v2.x) + self.assertEqual(v1.y, v2.y) + self.assertEqual(v1.z, v2.z) + + def test_rotate_y_rad(self): + vec = Vector3(1, 0, 0) + result = vec.rotate_y_rad(math.pi / 2) + self.assertEqual(result, (0, 0, -1)) + + def test_rotate_y_ip(self): + v = Vector3(1, 0, 0) + self.assertEqual(v.rotate_y_ip(90), None) + self.assertAlmostEqual(v.x, 0) + self.assertEqual(v.y, 0) + self.assertAlmostEqual(v.z, -1) + v = Vector3(-1, -1, 1) + v.rotate_y_ip(-90) + self.assertAlmostEqual(v.x, -1) + self.assertEqual(v.y, -1) + self.assertAlmostEqual(v.z, -1) + + def test_rotate_y_rad_ip(self): + vec = Vector3(1, 0, 0) + vec.rotate_y_rad_ip(math.pi / 2) + self.assertEqual(vec, (0, 0, -1)) + + def test_rotate_z(self): + v1 = Vector3(1, 0, 0) + v2 = v1.rotate_z(90) + v3 = v1.rotate_z(90 + 360) + self.assertEqual(v1.x, 1) + self.assertEqual(v1.y, 0) + self.assertEqual(v1.z, 0) + self.assertAlmostEqual(v2.x, 0) + self.assertAlmostEqual(v2.y, 1) + self.assertEqual(v2.z, 0) + self.assertAlmostEqual(v3.x, v2.x) + self.assertAlmostEqual(v3.y, v2.y) + self.assertEqual(v3.z, v2.z) + v1 = Vector3(-1, -1, -1) + v2 = v1.rotate_z(-90) + self.assertAlmostEqual(v2.x, -1) + self.assertAlmostEqual(v2.y, 1) + self.assertEqual(v2.z, -1) + v2 = v1.rotate_z(360) + self.assertAlmostEqual(v1.x, v2.x) + self.assertAlmostEqual(v1.y, v2.y) + self.assertEqual(v1.z, v2.z) + v2 = v1.rotate_z(0) + self.assertAlmostEqual(v1.x, v2.x) + self.assertAlmostEqual(v1.y, v2.y) + self.assertEqual(v1.z, v2.z) + + def test_rotate_z_rad(self): + vec = Vector3(1, 0, 0) + result = vec.rotate_z_rad(math.pi / 2) + self.assertEqual(result, (0, 1, 0)) + + def test_rotate_z_ip(self): + v = Vector3(1, 0, 0) + self.assertEqual(v.rotate_z_ip(90), None) + self.assertAlmostEqual(v.x, 0) + self.assertAlmostEqual(v.y, 1) + self.assertEqual(v.z, 0) + v = Vector3(-1, -1, 1) + v.rotate_z_ip(-90) + self.assertAlmostEqual(v.x, -1) + self.assertAlmostEqual(v.y, 1) + self.assertEqual(v.z, 1) + + def test_rotate_z_rad_ip(self): + vec = Vector3(1, 0, 0) + vec.rotate_z_rad_ip(math.pi / 2) + self.assertEqual(vec, (0, 1, 0)) + + def test_normalize(self): + v = self.v1.normalize() + # length is 1 + self.assertAlmostEqual(v.x * v.x + v.y * v.y + v.z * v.z, 1.0) + # v1 is unchanged + self.assertEqual(self.v1.x, self.l1[0]) + self.assertEqual(self.v1.y, self.l1[1]) + self.assertEqual(self.v1.z, self.l1[2]) + # v2 is parallel to v1 (tested via cross product) + cross = ( + (self.v1.y * v.z - self.v1.z * v.y) ** 2 + + (self.v1.z * v.x - self.v1.x * v.z) ** 2 + + (self.v1.x * v.y - self.v1.y * v.x) ** 2 + ) + self.assertAlmostEqual(cross, 0.0) + self.assertRaises(ValueError, lambda: self.zeroVec.normalize()) + + def test_normalize_ip(self): + v = +self.v1 + # v has length != 1 before normalizing + self.assertNotEqual(v.x * v.x + v.y * v.y + v.z * v.z, 1.0) + # inplace operations should return None + self.assertEqual(v.normalize_ip(), None) + # length is 1 + self.assertAlmostEqual(v.x * v.x + v.y * v.y + v.z * v.z, 1.0) + # v2 is parallel to v1 (tested via cross product) + cross = ( + (self.v1.y * v.z - self.v1.z * v.y) ** 2 + + (self.v1.z * v.x - self.v1.x * v.z) ** 2 + + (self.v1.x * v.y - self.v1.y * v.x) ** 2 + ) + self.assertAlmostEqual(cross, 0.0) + self.assertRaises(ValueError, lambda: self.zeroVec.normalize_ip()) + + def test_is_normalized(self): + self.assertEqual(self.v1.is_normalized(), False) + v = self.v1.normalize() + self.assertEqual(v.is_normalized(), True) + self.assertEqual(self.e2.is_normalized(), True) + self.assertEqual(self.zeroVec.is_normalized(), False) + + def test_cross(self): + def cross(a, b): + return Vector3( + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ) + + self.assertEqual(self.v1.cross(self.v2), cross(self.v1, self.v2)) + self.assertEqual(self.v1.cross(self.l2), cross(self.v1, self.l2)) + self.assertEqual(self.v1.cross(self.t2), cross(self.v1, self.t2)) + self.assertEqual(self.v1.cross(self.v2), -self.v2.cross(self.v1)) + self.assertEqual(self.v1.cross(self.v1), self.zeroVec) + + def test_dot(self): + self.assertAlmostEqual( + self.v1.dot(self.v2), + self.v1.x * self.v2.x + self.v1.y * self.v2.y + self.v1.z * self.v2.z, + ) + self.assertAlmostEqual( + self.v1.dot(self.l2), + self.v1.x * self.l2[0] + self.v1.y * self.l2[1] + self.v1.z * self.l2[2], + ) + self.assertAlmostEqual( + self.v1.dot(self.t2), + self.v1.x * self.t2[0] + self.v1.y * self.t2[1] + self.v1.z * self.t2[2], + ) + self.assertAlmostEqual(self.v1.dot(self.v2), self.v2.dot(self.v1)) + self.assertAlmostEqual(self.v1.dot(self.v2), self.v1 * self.v2) + + def test_angle_to(self): + self.assertEqual(Vector3(1, 1, 0).angle_to((-1, 1, 0)), 90) + self.assertEqual(Vector3(1, 0, 0).angle_to((0, 0, -1)), 90) + self.assertEqual(Vector3(1, 0, 0).angle_to((-1, 0, 1)), 135) + self.assertEqual(abs(Vector3(1, 0, 1).angle_to((-1, 0, -1))), 180) + # if we rotate v1 by the angle_to v2 around their cross product + # we should look in the same direction + self.assertEqual( + self.v1.rotate( + self.v1.angle_to(self.v2), self.v1.cross(self.v2) + ).normalize(), + self.v2.normalize(), + ) + + def test_scale_to_length(self): + v = Vector3(1, 1, 1) + v.scale_to_length(2.5) + self.assertEqual(v, Vector3(2.5, 2.5, 2.5) / math.sqrt(3)) + self.assertRaises(ValueError, lambda: self.zeroVec.scale_to_length(1)) + self.assertEqual(v.scale_to_length(0), None) + self.assertEqual(v, self.zeroVec) + + def test_length(self): + self.assertEqual(Vector3(3, 4, 5).length(), math.sqrt(3 * 3 + 4 * 4 + 5 * 5)) + self.assertEqual(Vector3(-3, 4, 5).length(), math.sqrt(-3 * -3 + 4 * 4 + 5 * 5)) + self.assertEqual(self.zeroVec.length(), 0) + + def test_length_squared(self): + self.assertEqual(Vector3(3, 4, 5).length_squared(), 3 * 3 + 4 * 4 + 5 * 5) + self.assertEqual(Vector3(-3, 4, 5).length_squared(), -3 * -3 + 4 * 4 + 5 * 5) + self.assertEqual(self.zeroVec.length_squared(), 0) + + def test_reflect(self): + v = Vector3(1, -1, 1) + n = Vector3(0, 1, 0) + self.assertEqual(v.reflect(n), Vector3(1, 1, 1)) + self.assertEqual(v.reflect(3 * n), v.reflect(n)) + self.assertEqual(v.reflect(-v), -v) + self.assertRaises(ValueError, lambda: v.reflect(self.zeroVec)) + + def test_reflect_ip(self): + v1 = Vector3(1, -1, 1) + v2 = Vector3(v1) + n = Vector3(0, 1, 0) + self.assertEqual(v2.reflect_ip(n), None) + self.assertEqual(v2, Vector3(1, 1, 1)) + v2 = Vector3(v1) + v2.reflect_ip(3 * n) + self.assertEqual(v2, v1.reflect(n)) + v2 = Vector3(v1) + v2.reflect_ip(-v1) + self.assertEqual(v2, -v1) + self.assertRaises(ValueError, lambda: v2.reflect_ip(self.zeroVec)) + + def test_distance_to(self): + diff = self.v1 - self.v2 + self.assertEqual(self.e1.distance_to(self.e2), math.sqrt(2)) + self.assertEqual(self.e1.distance_to((0, 1, 0)), math.sqrt(2)) + self.assertEqual(self.e1.distance_to([0, 1, 0]), math.sqrt(2)) + self.assertEqual( + self.v1.distance_to(self.v2), + math.sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z), + ) + self.assertEqual( + self.v1.distance_to(self.t2), + math.sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z), + ) + self.assertEqual( + self.v1.distance_to(self.l2), + math.sqrt(diff.x * diff.x + diff.y * diff.y + diff.z * diff.z), + ) + self.assertEqual(self.v1.distance_to(self.v1), 0) + self.assertEqual(self.v1.distance_to(self.t1), 0) + self.assertEqual(self.v1.distance_to(self.l1), 0) + self.assertEqual(self.v1.distance_to(self.v2), self.v2.distance_to(self.v1)) + self.assertEqual(self.v1.distance_to(self.t2), self.v2.distance_to(self.t1)) + self.assertEqual(self.v1.distance_to(self.l2), self.v2.distance_to(self.l1)) + + def test_distance_to_exceptions(self): + v2 = Vector2(10, 10) + v3 = Vector3(1, 1, 1) + + # illegal distance Vector3-Vector2 / Vector2-Vector3 + self.assertRaises(ValueError, v2.distance_to, v3) + self.assertRaises(ValueError, v3.distance_to, v2) + + # distance to illegal tuple/list positions + self.assertRaises(ValueError, v2.distance_to, (1, 1, 1)) + self.assertRaises(ValueError, v2.distance_to, (1, 1, 0)) + self.assertRaises(ValueError, v2.distance_to, (1,)) + self.assertRaises(ValueError, v2.distance_to, [1, 1, 1]) + self.assertRaises(ValueError, v2.distance_to, [1, 1, 0]) + self.assertRaises( + ValueError, + v2.distance_to, + [ + 1, + ], + ) + self.assertRaises(ValueError, v2.distance_to, (1, 1, 1)) + # vec3 + self.assertRaises(ValueError, v3.distance_to, (1, 1)) + self.assertRaises(ValueError, v3.distance_to, (1,)) + self.assertRaises(ValueError, v3.distance_to, [1, 1]) + self.assertRaises( + ValueError, + v3.distance_to, + [ + 1, + ], + ) + + # illegal types as positions + self.assertRaises(TypeError, v2.distance_to, (1, "hello")) + self.assertRaises(TypeError, v2.distance_to, ([], [])) + self.assertRaises(TypeError, v2.distance_to, (1, ("hello",))) + + # illegal args number + self.assertRaises(TypeError, v2.distance_to) + self.assertRaises(TypeError, v2.distance_to, (1, 1), (1, 2)) + self.assertRaises(TypeError, v2.distance_to, (1, 1), (1, 2), 1) + + def test_distance_squared_to_exceptions(self): + v2 = Vector2(10, 10) + v3 = Vector3(1, 1, 1) + dist_t = v2.distance_squared_to + dist_t3 = v3.distance_squared_to + # illegal distance Vector3-Vector2 / Vector2-Vector3 + self.assertRaises(ValueError, dist_t, v3) + self.assertRaises(ValueError, dist_t3, v2) + + # distance to illegal tuple/list positions + self.assertRaises(ValueError, dist_t, (1, 1, 1)) + self.assertRaises(ValueError, dist_t, (1, 1, 0)) + self.assertRaises(ValueError, dist_t, (1,)) + self.assertRaises(ValueError, dist_t, [1, 1, 1]) + self.assertRaises(ValueError, dist_t, [1, 1, 0]) + self.assertRaises( + ValueError, + dist_t, + [ + 1, + ], + ) + self.assertRaises(ValueError, dist_t, (1, 1, 1)) + # vec3 + self.assertRaises(ValueError, dist_t3, (1, 1)) + self.assertRaises(ValueError, dist_t3, (1,)) + self.assertRaises(ValueError, dist_t3, [1, 1]) + self.assertRaises( + ValueError, + dist_t3, + [ + 1, + ], + ) + + # illegal types as positions + self.assertRaises(TypeError, dist_t, (1, "hello")) + self.assertRaises(TypeError, dist_t, ([], [])) + self.assertRaises(TypeError, dist_t, (1, ("hello",))) + + # illegal args number + self.assertRaises(TypeError, dist_t) + self.assertRaises(TypeError, dist_t, (1, 1), (1, 2)) + self.assertRaises(TypeError, dist_t, (1, 1), (1, 2), 1) + + def test_distance_squared_to(self): + diff = self.v1 - self.v2 + self.assertEqual(self.e1.distance_squared_to(self.e2), 2) + self.assertEqual(self.e1.distance_squared_to((0, 1, 0)), 2) + self.assertEqual(self.e1.distance_squared_to([0, 1, 0]), 2) + self.assertAlmostEqual( + self.v1.distance_squared_to(self.v2), + diff.x * diff.x + diff.y * diff.y + diff.z * diff.z, + ) + self.assertAlmostEqual( + self.v1.distance_squared_to(self.t2), + diff.x * diff.x + diff.y * diff.y + diff.z * diff.z, + ) + self.assertAlmostEqual( + self.v1.distance_squared_to(self.l2), + diff.x * diff.x + diff.y * diff.y + diff.z * diff.z, + ) + self.assertEqual(self.v1.distance_squared_to(self.v1), 0) + self.assertEqual(self.v1.distance_squared_to(self.t1), 0) + self.assertEqual(self.v1.distance_squared_to(self.l1), 0) + self.assertEqual( + self.v1.distance_squared_to(self.v2), self.v2.distance_squared_to(self.v1) + ) + self.assertEqual( + self.v1.distance_squared_to(self.t2), self.v2.distance_squared_to(self.t1) + ) + self.assertEqual( + self.v1.distance_squared_to(self.l2), self.v2.distance_squared_to(self.l1) + ) + + def test_swizzle(self): + self.assertEqual(self.v1.yxz, (self.v1.y, self.v1.x, self.v1.z)) + self.assertEqual( + self.v1.xxyyzzxyz, + ( + self.v1.x, + self.v1.x, + self.v1.y, + self.v1.y, + self.v1.z, + self.v1.z, + self.v1.x, + self.v1.y, + self.v1.z, + ), + ) + self.v1.xyz = self.t2 + self.assertEqual(self.v1, self.t2) + self.v1.zxy = self.t2 + self.assertEqual(self.v1, (self.t2[1], self.t2[2], self.t2[0])) + self.v1.yz = self.t2[:2] + self.assertEqual(self.v1, (self.t2[1], self.t2[0], self.t2[1])) + self.assertEqual(type(self.v1), Vector3) + + @unittest.skipIf(IS_PYPY, "known pypy failure") + def test_invalid_swizzle(self): + def invalidSwizzleX(): + Vector3().xx = (1, 2) + + def invalidSwizzleY(): + Vector3().yy = (1, 2) + + def invalidSwizzleZ(): + Vector3().zz = (1, 2) + + def invalidSwizzleW(): + Vector3().ww = (1, 2) + + self.assertRaises(AttributeError, invalidSwizzleX) + self.assertRaises(AttributeError, invalidSwizzleY) + self.assertRaises(AttributeError, invalidSwizzleZ) + self.assertRaises(AttributeError, invalidSwizzleW) + + def invalidAssignment(): + Vector3().xy = 3 + + self.assertRaises(TypeError, invalidAssignment) + + def test_swizzle_return_types(self): + self.assertEqual(type(self.v1.x), float) + self.assertEqual(type(self.v1.xy), Vector2) + self.assertEqual(type(self.v1.xyz), Vector3) + # but we don't have vector4 or above... so tuple. + self.assertEqual(type(self.v1.xyxy), tuple) + self.assertEqual(type(self.v1.xyxyx), tuple) + + def test_dir_works(self): + # not every single one of the attributes... + attributes = {"lerp", "normalize", "normalize_ip", "reflect", "slerp", "x", "y"} + # check if this selection of attributes are all there. + self.assertTrue(attributes.issubset(set(dir(self.v1)))) + + def test_elementwise(self): + # behaviour for "elementwise op scalar" + self.assertEqual( + self.v1.elementwise() + self.s1, + (self.v1.x + self.s1, self.v1.y + self.s1, self.v1.z + self.s1), + ) + self.assertEqual( + self.v1.elementwise() - self.s1, + (self.v1.x - self.s1, self.v1.y - self.s1, self.v1.z - self.s1), + ) + self.assertEqual( + self.v1.elementwise() * self.s2, + (self.v1.x * self.s2, self.v1.y * self.s2, self.v1.z * self.s2), + ) + self.assertEqual( + self.v1.elementwise() / self.s2, + (self.v1.x / self.s2, self.v1.y / self.s2, self.v1.z / self.s2), + ) + self.assertEqual( + self.v1.elementwise() // self.s1, + (self.v1.x // self.s1, self.v1.y // self.s1, self.v1.z // self.s1), + ) + self.assertEqual( + self.v1.elementwise() ** self.s1, + (self.v1.x**self.s1, self.v1.y**self.s1, self.v1.z**self.s1), + ) + self.assertEqual( + self.v1.elementwise() % self.s1, + (self.v1.x % self.s1, self.v1.y % self.s1, self.v1.z % self.s1), + ) + self.assertEqual( + self.v1.elementwise() > self.s1, + self.v1.x > self.s1 and self.v1.y > self.s1 and self.v1.z > self.s1, + ) + self.assertEqual( + self.v1.elementwise() < self.s1, + self.v1.x < self.s1 and self.v1.y < self.s1 and self.v1.z < self.s1, + ) + self.assertEqual( + self.v1.elementwise() == self.s1, + self.v1.x == self.s1 and self.v1.y == self.s1 and self.v1.z == self.s1, + ) + self.assertEqual( + self.v1.elementwise() != self.s1, + self.v1.x != self.s1 and self.v1.y != self.s1 and self.v1.z != self.s1, + ) + self.assertEqual( + self.v1.elementwise() >= self.s1, + self.v1.x >= self.s1 and self.v1.y >= self.s1 and self.v1.z >= self.s1, + ) + self.assertEqual( + self.v1.elementwise() <= self.s1, + self.v1.x <= self.s1 and self.v1.y <= self.s1 and self.v1.z <= self.s1, + ) + # behaviour for "scalar op elementwise" + self.assertEqual(5 + self.v1.elementwise(), Vector3(5, 5, 5) + self.v1) + self.assertEqual(3.5 - self.v1.elementwise(), Vector3(3.5, 3.5, 3.5) - self.v1) + self.assertEqual(7.5 * self.v1.elementwise(), 7.5 * self.v1) + self.assertEqual( + -3.5 / self.v1.elementwise(), + (-3.5 / self.v1.x, -3.5 / self.v1.y, -3.5 / self.v1.z), + ) + self.assertEqual( + -3.5 // self.v1.elementwise(), + (-3.5 // self.v1.x, -3.5 // self.v1.y, -3.5 // self.v1.z), + ) + self.assertEqual( + -(3.5 ** self.v1.elementwise()), + (-(3.5**self.v1.x), -(3.5**self.v1.y), -(3.5**self.v1.z)), + ) + self.assertEqual( + 3 % self.v1.elementwise(), (3 % self.v1.x, 3 % self.v1.y, 3 % self.v1.z) + ) + self.assertEqual( + 2 < self.v1.elementwise(), 2 < self.v1.x and 2 < self.v1.y and 2 < self.v1.z + ) + self.assertEqual( + 2 > self.v1.elementwise(), 2 > self.v1.x and 2 > self.v1.y and 2 > self.v1.z + ) + self.assertEqual( + 1 == self.v1.elementwise(), + 1 == self.v1.x and 1 == self.v1.y and 1 == self.v1.z, + ) + self.assertEqual( + 1 != self.v1.elementwise(), + 1 != self.v1.x and 1 != self.v1.y and 1 != self.v1.z, + ) + self.assertEqual( + 2 <= self.v1.elementwise(), + 2 <= self.v1.x and 2 <= self.v1.y and 2 <= self.v1.z, + ) + self.assertEqual( + -7 >= self.v1.elementwise(), + -7 >= self.v1.x and -7 >= self.v1.y and -7 >= self.v1.z, + ) + self.assertEqual( + -7 != self.v1.elementwise(), + -7 != self.v1.x and -7 != self.v1.y and -7 != self.v1.z, + ) + + # behaviour for "elementwise op vector" + self.assertEqual(type(self.v1.elementwise() * self.v2), type(self.v1)) + self.assertEqual(self.v1.elementwise() + self.v2, self.v1 + self.v2) + self.assertEqual(self.v1.elementwise() + self.v2, self.v1 + self.v2) + self.assertEqual(self.v1.elementwise() - self.v2, self.v1 - self.v2) + self.assertEqual( + self.v1.elementwise() * self.v2, + (self.v1.x * self.v2.x, self.v1.y * self.v2.y, self.v1.z * self.v2.z), + ) + self.assertEqual( + self.v1.elementwise() / self.v2, + (self.v1.x / self.v2.x, self.v1.y / self.v2.y, self.v1.z / self.v2.z), + ) + self.assertEqual( + self.v1.elementwise() // self.v2, + (self.v1.x // self.v2.x, self.v1.y // self.v2.y, self.v1.z // self.v2.z), + ) + self.assertEqual( + self.v1.elementwise() ** self.v2, + (self.v1.x**self.v2.x, self.v1.y**self.v2.y, self.v1.z**self.v2.z), + ) + self.assertEqual( + self.v1.elementwise() % self.v2, + (self.v1.x % self.v2.x, self.v1.y % self.v2.y, self.v1.z % self.v2.z), + ) + self.assertEqual( + self.v1.elementwise() > self.v2, + self.v1.x > self.v2.x and self.v1.y > self.v2.y and self.v1.z > self.v2.z, + ) + self.assertEqual( + self.v1.elementwise() < self.v2, + self.v1.x < self.v2.x and self.v1.y < self.v2.y and self.v1.z < self.v2.z, + ) + self.assertEqual( + self.v1.elementwise() >= self.v2, + self.v1.x >= self.v2.x + and self.v1.y >= self.v2.y + and self.v1.z >= self.v2.z, + ) + self.assertEqual( + self.v1.elementwise() <= self.v2, + self.v1.x <= self.v2.x + and self.v1.y <= self.v2.y + and self.v1.z <= self.v2.z, + ) + self.assertEqual( + self.v1.elementwise() == self.v2, + self.v1.x == self.v2.x + and self.v1.y == self.v2.y + and self.v1.z == self.v2.z, + ) + self.assertEqual( + self.v1.elementwise() != self.v2, + self.v1.x != self.v2.x + and self.v1.y != self.v2.y + and self.v1.z != self.v2.z, + ) + # behaviour for "vector op elementwise" + self.assertEqual(self.v2 + self.v1.elementwise(), self.v2 + self.v1) + self.assertEqual(self.v2 - self.v1.elementwise(), self.v2 - self.v1) + self.assertEqual( + self.v2 * self.v1.elementwise(), + (self.v2.x * self.v1.x, self.v2.y * self.v1.y, self.v2.z * self.v1.z), + ) + self.assertEqual( + self.v2 / self.v1.elementwise(), + (self.v2.x / self.v1.x, self.v2.y / self.v1.y, self.v2.z / self.v1.z), + ) + self.assertEqual( + self.v2 // self.v1.elementwise(), + (self.v2.x // self.v1.x, self.v2.y // self.v1.y, self.v2.z // self.v1.z), + ) + self.assertEqual( + self.v2 ** self.v1.elementwise(), + (self.v2.x**self.v1.x, self.v2.y**self.v1.y, self.v2.z**self.v1.z), + ) + self.assertEqual( + self.v2 % self.v1.elementwise(), + (self.v2.x % self.v1.x, self.v2.y % self.v1.y, self.v2.z % self.v1.z), + ) + self.assertEqual( + self.v2 < self.v1.elementwise(), + self.v2.x < self.v1.x and self.v2.y < self.v1.y and self.v2.z < self.v1.z, + ) + self.assertEqual( + self.v2 > self.v1.elementwise(), + self.v2.x > self.v1.x and self.v2.y > self.v1.y and self.v2.z > self.v1.z, + ) + self.assertEqual( + self.v2 <= self.v1.elementwise(), + self.v2.x <= self.v1.x + and self.v2.y <= self.v1.y + and self.v2.z <= self.v1.z, + ) + self.assertEqual( + self.v2 >= self.v1.elementwise(), + self.v2.x >= self.v1.x + and self.v2.y >= self.v1.y + and self.v2.z >= self.v1.z, + ) + self.assertEqual( + self.v2 == self.v1.elementwise(), + self.v2.x == self.v1.x + and self.v2.y == self.v1.y + and self.v2.z == self.v1.z, + ) + self.assertEqual( + self.v2 != self.v1.elementwise(), + self.v2.x != self.v1.x + and self.v2.y != self.v1.y + and self.v2.z != self.v1.z, + ) + + # behaviour for "elementwise op elementwise" + self.assertEqual( + self.v2.elementwise() + self.v1.elementwise(), self.v2 + self.v1 + ) + self.assertEqual( + self.v2.elementwise() - self.v1.elementwise(), self.v2 - self.v1 + ) + self.assertEqual( + self.v2.elementwise() * self.v1.elementwise(), + (self.v2.x * self.v1.x, self.v2.y * self.v1.y, self.v2.z * self.v1.z), + ) + self.assertEqual( + self.v2.elementwise() / self.v1.elementwise(), + (self.v2.x / self.v1.x, self.v2.y / self.v1.y, self.v2.z / self.v1.z), + ) + self.assertEqual( + self.v2.elementwise() // self.v1.elementwise(), + (self.v2.x // self.v1.x, self.v2.y // self.v1.y, self.v2.z // self.v1.z), + ) + self.assertEqual( + self.v2.elementwise() ** self.v1.elementwise(), + (self.v2.x**self.v1.x, self.v2.y**self.v1.y, self.v2.z**self.v1.z), + ) + self.assertEqual( + self.v2.elementwise() % self.v1.elementwise(), + (self.v2.x % self.v1.x, self.v2.y % self.v1.y, self.v2.z % self.v1.z), + ) + self.assertEqual( + self.v2.elementwise() < self.v1.elementwise(), + self.v2.x < self.v1.x and self.v2.y < self.v1.y and self.v2.z < self.v1.z, + ) + self.assertEqual( + self.v2.elementwise() > self.v1.elementwise(), + self.v2.x > self.v1.x and self.v2.y > self.v1.y and self.v2.z > self.v1.z, + ) + self.assertEqual( + self.v2.elementwise() <= self.v1.elementwise(), + self.v2.x <= self.v1.x + and self.v2.y <= self.v1.y + and self.v2.z <= self.v1.z, + ) + self.assertEqual( + self.v2.elementwise() >= self.v1.elementwise(), + self.v2.x >= self.v1.x + and self.v2.y >= self.v1.y + and self.v2.z >= self.v1.z, + ) + self.assertEqual( + self.v2.elementwise() == self.v1.elementwise(), + self.v2.x == self.v1.x + and self.v2.y == self.v1.y + and self.v2.z == self.v1.z, + ) + self.assertEqual( + self.v2.elementwise() != self.v1.elementwise(), + self.v2.x != self.v1.x + and self.v2.y != self.v1.y + and self.v2.z != self.v1.z, + ) + + # other behaviour + self.assertEqual( + abs(self.v1.elementwise()), (abs(self.v1.x), abs(self.v1.y), abs(self.v1.z)) + ) + self.assertEqual(-self.v1.elementwise(), -self.v1) + self.assertEqual(+self.v1.elementwise(), +self.v1) + self.assertEqual(bool(self.v1.elementwise()), bool(self.v1)) + self.assertEqual(bool(Vector3().elementwise()), bool(Vector3())) + self.assertEqual(self.zeroVec.elementwise() ** 0, (1, 1, 1)) + self.assertRaises(ValueError, lambda: pow(Vector3(-1, 0, 0).elementwise(), 1.2)) + self.assertRaises(ZeroDivisionError, lambda: self.zeroVec.elementwise() ** -1) + self.assertRaises(ZeroDivisionError, lambda: Vector3(1, 1, 1).elementwise() / 0) + self.assertRaises( + ZeroDivisionError, lambda: Vector3(1, 1, 1).elementwise() // 0 + ) + self.assertRaises(ZeroDivisionError, lambda: Vector3(1, 1, 1).elementwise() % 0) + self.assertRaises( + ZeroDivisionError, lambda: Vector3(1, 1, 1).elementwise() / self.zeroVec + ) + self.assertRaises( + ZeroDivisionError, lambda: Vector3(1, 1, 1).elementwise() // self.zeroVec + ) + self.assertRaises( + ZeroDivisionError, lambda: Vector3(1, 1, 1).elementwise() % self.zeroVec + ) + self.assertRaises(ZeroDivisionError, lambda: 2 / self.zeroVec.elementwise()) + self.assertRaises(ZeroDivisionError, lambda: 2 // self.zeroVec.elementwise()) + self.assertRaises(ZeroDivisionError, lambda: 2 % self.zeroVec.elementwise()) + + def test_slerp(self): + self.assertRaises(ValueError, lambda: self.zeroVec.slerp(self.v1, 0.5)) + self.assertRaises(ValueError, lambda: self.v1.slerp(self.zeroVec, 0.5)) + self.assertRaises(ValueError, lambda: self.zeroVec.slerp(self.zeroVec, 0.5)) + steps = 10 + angle_step = self.e1.angle_to(self.e2) / steps + for i, u in ( + (i, self.e1.slerp(self.e2, i / float(steps))) for i in range(steps + 1) + ): + self.assertAlmostEqual(u.length(), 1) + self.assertAlmostEqual(self.e1.angle_to(u), i * angle_step) + self.assertEqual(u, self.e2) + + v1 = Vector3(100, 0, 0) + v2 = Vector3(0, 10, 7) + radial_factor = v2.length() / v1.length() + for i, u in ((i, v1.slerp(v2, -i / float(steps))) for i in range(steps + 1)): + self.assertAlmostEqual( + u.length(), + (v2.length() - v1.length()) * (float(i) / steps) + v1.length(), + ) + self.assertEqual(u, v2) + self.assertEqual(v1.slerp(v1, 0.5), v1) + self.assertEqual(v2.slerp(v2, 0.5), v2) + self.assertRaises(ValueError, lambda: v1.slerp(-v1, 0.5)) + + def test_lerp(self): + v1 = Vector3(0, 0, 0) + v2 = Vector3(10, 10, 10) + self.assertEqual(v1.lerp(v2, 0.5), (5, 5, 5)) + self.assertRaises(ValueError, lambda: v1.lerp(v2, 2.5)) + + v1 = Vector3(-10, -5, -20) + v2 = Vector3(10, 10, -20) + self.assertEqual(v1.lerp(v2, 0.5), (0, 2.5, -20)) + + def test_spherical(self): + v = Vector3() + v.from_spherical(self.v1.as_spherical()) + self.assertEqual(self.v1, v) + self.assertEqual(self.v1, Vector3.from_spherical(self.v1.as_spherical())) + self.assertEqual(self.e1.as_spherical(), (1, 90, 0)) + self.assertEqual(self.e2.as_spherical(), (1, 90, 90)) + self.assertEqual(self.e3.as_spherical(), (1, 0, 0)) + self.assertEqual((2 * self.e2).as_spherical(), (2, 90, 90)) + self.assertRaises(TypeError, lambda: v.from_spherical((None, None, None))) + self.assertRaises(TypeError, lambda: v.from_spherical("abc")) + self.assertRaises(TypeError, lambda: v.from_spherical((None, 1, 2))) + self.assertRaises(TypeError, lambda: v.from_spherical((1, 2, 3, 4))) + self.assertRaises(TypeError, lambda: v.from_spherical((1, 2))) + self.assertRaises(TypeError, lambda: v.from_spherical(1, 2, 3)) + self.assertRaises(TypeError, lambda: Vector3.from_spherical((None, None, None))) + self.assertRaises(TypeError, lambda: Vector3.from_spherical("abc")) + self.assertRaises(TypeError, lambda: Vector3.from_spherical((None, 1, 2))) + self.assertRaises(TypeError, lambda: Vector3.from_spherical((1, 2, 3, 4))) + self.assertRaises(TypeError, lambda: Vector3.from_spherical((1, 2))) + self.assertRaises(TypeError, lambda: Vector3.from_spherical(1, 2, 3)) + v.from_spherical((0.5, 90, 90)) + self.assertEqual(v, 0.5 * self.e2) + self.assertEqual(Vector3.from_spherical((0.5, 90, 90)), 0.5 * self.e2) + self.assertEqual(Vector3.from_spherical((0.5, 90, 90)), v) + + def test_inplace_operators(self): + v = Vector3(1, 1, 1) + v *= 2 + self.assertEqual(v, (2.0, 2.0, 2.0)) + + v = Vector3(4, 4, 4) + v /= 2 + self.assertEqual(v, (2.0, 2.0, 2.0)) + + v = Vector3(3.0, 3.0, 3.0) + v -= (1, 1, 1) + self.assertEqual(v, (2.0, 2.0, 2.0)) + + v = Vector3(3.0, 3.0, 3.0) + v += (1, 1, 1) + self.assertEqual(v, (4.0, 4.0, 4.0)) + + def test_pickle(self): + import pickle + + v2 = Vector2(1, 2) + v3 = Vector3(1, 2, 3) + self.assertEqual(pickle.loads(pickle.dumps(v2)), v2) + self.assertEqual(pickle.loads(pickle.dumps(v3)), v3) + + def test_subclass_operation(self): + class Vector(pygame.math.Vector3): + pass + + v = Vector(2.0, 2.0, 2.0) + v *= 2 + self.assertEqual(v, (4.0, 4.0, 4.0)) + + def test_swizzle_constants(self): + """We can get constant values from a swizzle.""" + v = Vector2(7, 6) + self.assertEqual( + v.xy1, + (7.0, 6.0, 1.0), + ) + + def test_swizzle_four_constants(self): + """We can get 4 constant values from a swizzle.""" + v = Vector2(7, 6) + self.assertEqual( + v.xy01, + (7.0, 6.0, 0.0, 1.0), + ) + + def test_swizzle_oob(self): + """An out-of-bounds swizzle raises an AttributeError.""" + v = Vector2(7, 6) + with self.assertRaises(AttributeError): + v.xyz + + @unittest.skipIf(IS_PYPY, "known pypy failure") + def test_swizzle_set_oob(self): + """An out-of-bounds swizzle set raises an AttributeError.""" + v = Vector2(7, 6) + with self.assertRaises(AttributeError): + v.xz = (1, 1) + + def test_project_v3_onto_x_axis(self): + """Project onto x-axis, e.g. get the component pointing in the x-axis direction.""" + # arrange + v = Vector3(2, 3, 4) + x_axis = Vector3(10, 0, 0) + + # act + actual = v.project(x_axis) + + # assert + self.assertEqual(v.x, actual.x) + self.assertEqual(0, actual.y) + self.assertEqual(0, actual.z) + + def test_project_v3_onto_y_axis(self): + """Project onto y-axis, e.g. get the component pointing in the y-axis direction.""" + # arrange + v = Vector3(2, 3, 4) + y_axis = Vector3(0, 100, 0) + + # act + actual = v.project(y_axis) + + # assert + self.assertEqual(0, actual.x) + self.assertEqual(v.y, actual.y) + self.assertEqual(0, actual.z) + + def test_project_v3_onto_z_axis(self): + """Project onto z-axis, e.g. get the component pointing in the z-axis direction.""" + # arrange + v = Vector3(2, 3, 4) + y_axis = Vector3(0, 0, 77) + + # act + actual = v.project(y_axis) + + # assert + self.assertEqual(0, actual.x) + self.assertEqual(0, actual.y) + self.assertEqual(v.z, actual.z) + + def test_project_v3_onto_other(self): + """Project onto other vector.""" + # arrange + v = Vector3(2, 3, 4) + other = Vector3(3, 5, 7) + + # act + actual = v.project(other) + + # assert + expected = v.dot(other) / other.dot(other) * other + self.assertAlmostEqual(expected.x, actual.x) + self.assertAlmostEqual(expected.y, actual.y) + self.assertAlmostEqual(expected.z, actual.z) + + def test_project_v3_onto_other_as_tuple(self): + """Project onto other tuple as vector.""" + # arrange + v = Vector3(2, 3, 4) + other = Vector3(3, 5, 7) + + # act + actual = v.project(tuple(other)) + + # assert + expected = v.dot(other) / other.dot(other) * other + self.assertAlmostEqual(expected.x, actual.x) + self.assertAlmostEqual(expected.y, actual.y) + self.assertAlmostEqual(expected.z, actual.z) + + def test_project_v3_onto_other_as_list(self): + """Project onto other list as vector.""" + # arrange + v = Vector3(2, 3, 4) + other = Vector3(3, 5, 7) + + # act + actual = v.project(list(other)) + + # assert + expected = v.dot(other) / other.dot(other) * other + self.assertAlmostEqual(expected.x, actual.x) + self.assertAlmostEqual(expected.y, actual.y) + self.assertAlmostEqual(expected.z, actual.z) + + def test_project_v3_raises_if_other_has_zero_length(self): + """Check if exception is raise when projected on vector has zero length.""" + # arrange + v = Vector3(2, 3, 4) + other = Vector3(0, 0, 0) + + # act / assert + self.assertRaises(ValueError, v.project, other) + + def test_project_v3_raises_if_other_is_not_iterable(self): + """Check if exception is raise when projected on vector is not iterable.""" + # arrange + v = Vector3(2, 3, 4) + other = 10 + + # act / assert + self.assertRaises(TypeError, v.project, other) + + def test_collection_abc(self): + v = Vector3(3, 4, 5) + self.assertTrue(isinstance(v, Collection)) + self.assertFalse(isinstance(v, Sequence)) + + def test_clamp_mag_v3_max(self): + v1 = Vector3(7, 2, 2) + v2 = v1.clamp_magnitude(5) + v3 = v1.clamp_magnitude(0, 5) + self.assertEqual(v2, v3) + + v1.clamp_magnitude_ip(5) + self.assertEqual(v1, v2) + + v1.clamp_magnitude_ip(0, 5) + self.assertEqual(v1, v2) + + expected_v2 = Vector3(4.635863249727653, 1.3245323570650438, 1.3245323570650438) + self.assertEqual(expected_v2, v2) + + def test_clamp_mag_v3_min(self): + v1 = Vector3(3, 1, 2) + v2 = v1.clamp_magnitude(5, 10) + v1.clamp_magnitude_ip(5, 10) + expected_v2 = Vector3(4.008918628686366, 1.3363062095621219, 2.6726124191242437) + self.assertEqual(expected_v2, v1) + self.assertEqual(expected_v2, v2) + + def test_clamp_mag_v3_no_change(self): + v1 = Vector3(1, 2, 3) + for args in ( + (1, 6), + (1.12, 5.55), + (0.93, 6.83), + (7.6,), + ): + with self.subTest(args=args): + v2 = v1.clamp_magnitude(*args) + v1.clamp_magnitude_ip(*args) + self.assertEqual(v1, v2) + self.assertEqual(v1, Vector3(1, 2, 3)) + + def test_clamp_mag_v3_edge_cases(self): + v1 = Vector3(1, 2, 1) + v2 = v1.clamp_magnitude(6, 6) + v1.clamp_magnitude_ip(6, 6) + self.assertEqual(v1, v2) + self.assertAlmostEqual(v1.length(), 6) + + v2 = v1.clamp_magnitude(0) + v1.clamp_magnitude_ip(0, 0) + self.assertEqual(v1, v2) + self.assertEqual(v1, Vector3()) + + def test_clamp_mag_v3_errors(self): + v1 = Vector3(1, 2, 2) + for invalid_args in ( + ("foo", "bar"), + (1, 2, 3), + (342.234, "test"), + ): + with self.subTest(invalid_args=invalid_args): + self.assertRaises(TypeError, v1.clamp_magnitude, *invalid_args) + self.assertRaises(TypeError, v1.clamp_magnitude_ip, *invalid_args) + + for invalid_args in ( + (-1,), + (4, 3), # min > max + (-4, 10), + (-4, -2), + ): + with self.subTest(invalid_args=invalid_args): + self.assertRaises(ValueError, v1.clamp_magnitude, *invalid_args) + self.assertRaises(ValueError, v1.clamp_magnitude_ip, *invalid_args) + + # 0 vector + v2 = Vector3() + self.assertRaises(ValueError, v2.clamp_magnitude, 3) + self.assertRaises(ValueError, v2.clamp_magnitude_ip, 4) + + def test_subclassing_v3(self): + """Check if Vector3 is subclassable""" + v = Vector3(4, 2, 0) + + class TestVector(Vector3): + def supermariobrosiscool(self): + return 722 + + other = TestVector(4, 1, 0) + + self.assertEqual(other.supermariobrosiscool(), 722) + self.assertNotEqual(type(v), TestVector) + self.assertNotEqual(type(v), type(other.copy())) + self.assertEqual(TestVector, type(other.reflect(v))) + self.assertEqual(TestVector, type(other.lerp(v, 1))) + self.assertEqual(TestVector, type(other.slerp(v, 1))) + self.assertEqual(TestVector, type(other.rotate(5, v))) + self.assertEqual(TestVector, type(other.rotate_rad(5, v))) + self.assertEqual(TestVector, type(other.project(v))) + self.assertEqual(TestVector, type(other.move_towards(v, 5))) + self.assertEqual(TestVector, type(other.clamp_magnitude(5))) + self.assertEqual(TestVector, type(other.clamp_magnitude(1, 5))) + self.assertEqual(TestVector, type(other.elementwise() + other)) + + other1 = TestVector(4, 2, 0) + + self.assertEqual(type(other + other1), TestVector) + self.assertEqual(type(other - other1), TestVector) + self.assertEqual(type(other * 3), TestVector) + self.assertEqual(type(other / 3), TestVector) + self.assertEqual(type(other.elementwise() ** 3), TestVector) + + def test_move_towards_basic(self): + expected = Vector3(7.93205057, 2006.38284641, 43.80780420) + origin = Vector3(7.22, 2004.0, 42.13) + target = Vector3(12.30, 2021.0, 54.1) + change_ip = origin.copy() + + change = origin.move_towards(target, 3) + change_ip.move_towards_ip(target, 3) + + self.assertEqual(change, expected) + self.assertEqual(change_ip, expected) + + def test_move_towards_max_distance(self): + expected = Vector3(12.30, 2021, 42.5) + origin = Vector3(7.22, 2004.0, 17.5) + change_ip = origin.copy() + + change = origin.move_towards(expected, 100) + change_ip.move_towards_ip(expected, 100) + + self.assertEqual(change, expected) + self.assertEqual(change_ip, expected) + + def test_move_nowhere(self): + origin = Vector3(7.22, 2004.0, 24.5) + target = Vector3(12.30, 2021.0, 3.2) + change_ip = origin.copy() + + change = origin.move_towards(target, 0) + change_ip.move_towards_ip(target, 0) + + self.assertEqual(change, origin) + self.assertEqual(change_ip, origin) + + def test_move_away(self): + expected = Vector3(6.74137906, 2002.39831577, 49.70890994) + origin = Vector3(7.22, 2004.0, 52.2) + target = Vector3(12.30, 2021.0, 78.64) + change_ip = origin.copy() + + change = origin.move_towards(target, -3) + change_ip.move_towards_ip(target, -3) + + self.assertEqual(change, expected) + self.assertEqual(change_ip, expected) + + def test_move_towards_self(self): + vec = Vector3(6.36, 2001.13, -123.14) + vec2 = vec.copy() + for dist in (-3.54, -1, 0, 0.234, 12): + self.assertEqual(vec.move_towards(vec2, dist), vec) + vec2.move_towards_ip(vec, dist) + self.assertEqual(vec, vec2) + + def test_move_towards_errors(self): + origin = Vector3(7.22, 2004.0, 4.1) + target = Vector3(12.30, 2021.0, -421.5) + + self.assertRaises(TypeError, origin.move_towards, target, 3, 2) + self.assertRaises(TypeError, origin.move_towards_ip, target, 3, 2) + self.assertRaises(TypeError, origin.move_towards, target, "a") + self.assertRaises(TypeError, origin.move_towards_ip, target, "b") + self.assertRaises(TypeError, origin.move_towards, "c", 3) + self.assertRaises(TypeError, origin.move_towards_ip, "d", 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/midi_test.py b/laplas/abstract_map/pygame/tests/midi_test.py new file mode 100644 index 0000000..f4189a2 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/midi_test.py @@ -0,0 +1,463 @@ +import unittest + + +import pygame + + +class MidiInputTest(unittest.TestCase): + __tags__ = ["interactive"] + + def setUp(self): + import pygame.midi + + pygame.midi.init() + in_id = pygame.midi.get_default_input_id() + if in_id != -1: + self.midi_input = pygame.midi.Input(in_id) + else: + self.midi_input = None + + def tearDown(self): + if self.midi_input: + self.midi_input.close() + pygame.midi.quit() + + def test_Input(self): + i = pygame.midi.get_default_input_id() + if self.midi_input: + self.assertEqual(self.midi_input.device_id, i) + + # try feeding it an input id. + i = pygame.midi.get_default_output_id() + + # can handle some invalid input too. + self.assertRaises(pygame.midi.MidiException, pygame.midi.Input, i) + self.assertRaises(pygame.midi.MidiException, pygame.midi.Input, 9009) + self.assertRaises(pygame.midi.MidiException, pygame.midi.Input, -1) + self.assertRaises(TypeError, pygame.midi.Input, "1234") + self.assertRaises(OverflowError, pygame.midi.Input, pow(2, 99)) + + def test_poll(self): + if not self.midi_input: + self.skipTest("No midi Input device") + + self.assertFalse(self.midi_input.poll()) + # TODO fake some incoming data + + pygame.midi.quit() + self.assertRaises(RuntimeError, self.midi_input.poll) + # set midi_input to None to avoid error in tearDown + self.midi_input = None + + def test_read(self): + if not self.midi_input: + self.skipTest("No midi Input device") + + read = self.midi_input.read(5) + self.assertEqual(read, []) + # TODO fake some incoming data + + pygame.midi.quit() + self.assertRaises(RuntimeError, self.midi_input.read, 52) + # set midi_input to None to avoid error in tearDown + self.midi_input = None + + def test_close(self): + if not self.midi_input: + self.skipTest("No midi Input device") + + self.assertIsNotNone(self.midi_input._input) + self.midi_input.close() + self.assertIsNone(self.midi_input._input) + + +class MidiOutputTest(unittest.TestCase): + __tags__ = ["interactive"] + + def setUp(self): + import pygame.midi + + pygame.midi.init() + m_out_id = pygame.midi.get_default_output_id() + if m_out_id != -1: + self.midi_output = pygame.midi.Output(m_out_id) + else: + self.midi_output = None + + def tearDown(self): + if self.midi_output: + self.midi_output.close() + pygame.midi.quit() + + def test_Output(self): + i = pygame.midi.get_default_output_id() + if self.midi_output: + self.assertEqual(self.midi_output.device_id, i) + + # try feeding it an input id. + i = pygame.midi.get_default_input_id() + + # can handle some invalid input too. + self.assertRaises(pygame.midi.MidiException, pygame.midi.Output, i) + self.assertRaises(pygame.midi.MidiException, pygame.midi.Output, 9009) + self.assertRaises(pygame.midi.MidiException, pygame.midi.Output, -1) + self.assertRaises(TypeError, pygame.midi.Output, "1234") + self.assertRaises(OverflowError, pygame.midi.Output, pow(2, 99)) + + def test_note_off(self): + if self.midi_output: + out = self.midi_output + out.note_on(5, 30, 0) + out.note_off(5, 30, 0) + with self.assertRaises(ValueError) as cm: + out.note_off(5, 30, 25) + self.assertEqual(str(cm.exception), "Channel not between 0 and 15.") + with self.assertRaises(ValueError) as cm: + out.note_off(5, 30, -1) + self.assertEqual(str(cm.exception), "Channel not between 0 and 15.") + + def test_note_on(self): + if self.midi_output: + out = self.midi_output + out.note_on(5, 30, 0) + out.note_on(5, 42, 10) + with self.assertRaises(ValueError) as cm: + out.note_on(5, 30, 25) + self.assertEqual(str(cm.exception), "Channel not between 0 and 15.") + with self.assertRaises(ValueError) as cm: + out.note_on(5, 30, -1) + self.assertEqual(str(cm.exception), "Channel not between 0 and 15.") + + def test_set_instrument(self): + if not self.midi_output: + self.skipTest("No midi device") + out = self.midi_output + out.set_instrument(5) + out.set_instrument(42, channel=2) + with self.assertRaises(ValueError) as cm: + out.set_instrument(-6) + self.assertEqual(str(cm.exception), "Undefined instrument id: -6") + with self.assertRaises(ValueError) as cm: + out.set_instrument(156) + self.assertEqual(str(cm.exception), "Undefined instrument id: 156") + with self.assertRaises(ValueError) as cm: + out.set_instrument(5, -1) + self.assertEqual(str(cm.exception), "Channel not between 0 and 15.") + with self.assertRaises(ValueError) as cm: + out.set_instrument(5, 16) + self.assertEqual(str(cm.exception), "Channel not between 0 and 15.") + + def test_write(self): + if not self.midi_output: + self.skipTest("No midi device") + + out = self.midi_output + out.write([[[0xC0, 0, 0], 20000]]) + # is equivalent to + out.write([[[0xC0], 20000]]) + # example from the docstring : + # 1. choose program change 1 at time 20000 and + # 2. send note 65 with velocity 100 500 ms later + out.write([[[0xC0, 0, 0], 20000], [[0x90, 60, 100], 20500]]) + + out.write([]) + verrry_long = [[[0x90, 60, i % 100], 20000 + 100 * i] for i in range(1024)] + out.write(verrry_long) + + too_long = [[[0x90, 60, i % 100], 20000 + 100 * i] for i in range(1025)] + self.assertRaises(IndexError, out.write, too_long) + # test wrong data + with self.assertRaises(TypeError) as cm: + out.write("Non sens ?") + error_msg = "unsupported operand type(s) for &: 'str' and 'int'" + self.assertEqual(str(cm.exception), error_msg) + + with self.assertRaises(TypeError) as cm: + out.write(["Hey what's that?"]) + self.assertEqual(str(cm.exception), error_msg) + + def test_write_short(self): + if not self.midi_output: + self.skipTest("No midi device") + + out = self.midi_output + # program change + out.write_short(0xC0) + # put a note on, then off. + out.write_short(0x90, 65, 100) + out.write_short(0x80, 65, 100) + out.write_short(0x90) + + def test_write_sys_ex(self): + if not self.midi_output: + self.skipTest("No midi device") + + out = self.midi_output + out.write_sys_ex(pygame.midi.time(), [0xF0, 0x7D, 0x10, 0x11, 0x12, 0x13, 0xF7]) + + def test_pitch_bend(self): + # FIXME : pitch_bend in the code, but not in documentation + if not self.midi_output: + self.skipTest("No midi device") + + out = self.midi_output + with self.assertRaises(ValueError) as cm: + out.pitch_bend(5, channel=-1) + self.assertEqual(str(cm.exception), "Channel not between 0 and 15.") + with self.assertRaises(ValueError) as cm: + out.pitch_bend(5, channel=16) + with self.assertRaises(ValueError) as cm: + out.pitch_bend(-10001, 1) + self.assertEqual( + str(cm.exception), + "Pitch bend value must be between " "-8192 and +8191, not -10001.", + ) + with self.assertRaises(ValueError) as cm: + out.pitch_bend(10665, 2) + + def test_close(self): + if not self.midi_output: + self.skipTest("No midi device") + self.assertIsNotNone(self.midi_output._output) + self.midi_output.close() + self.assertIsNone(self.midi_output._output) + + def test_abort(self): + if not self.midi_output: + self.skipTest("No midi device") + self.assertEqual(self.midi_output._aborted, 0) + self.midi_output.abort() + self.assertEqual(self.midi_output._aborted, 1) + + +class MidiModuleTest(unittest.TestCase): + """Midi module tests that require midi hardware or midi.init(). + + See MidiModuleNonInteractiveTest for non-interactive module tests. + """ + + __tags__ = ["interactive"] + + def setUp(self): + import pygame.midi + + pygame.midi.init() + + def tearDown(self): + pygame.midi.quit() + + def test_get_count(self): + c = pygame.midi.get_count() + self.assertIsInstance(c, int) + self.assertTrue(c >= 0) + + def test_get_default_input_id(self): + midin_id = pygame.midi.get_default_input_id() + # if there is a not None return make sure it is an int. + self.assertIsInstance(midin_id, int) + self.assertTrue(midin_id >= -1) + pygame.midi.quit() + self.assertRaises(RuntimeError, pygame.midi.get_default_output_id) + + def test_get_default_output_id(self): + c = pygame.midi.get_default_output_id() + self.assertIsInstance(c, int) + self.assertTrue(c >= -1) + pygame.midi.quit() + self.assertRaises(RuntimeError, pygame.midi.get_default_output_id) + + def test_get_device_info(self): + an_id = pygame.midi.get_default_output_id() + if an_id != -1: + interf, name, input, output, opened = pygame.midi.get_device_info(an_id) + self.assertEqual(output, 1) + self.assertEqual(input, 0) + self.assertEqual(opened, 0) + + an_in_id = pygame.midi.get_default_input_id() + if an_in_id != -1: + r = pygame.midi.get_device_info(an_in_id) + # if r is None, it means that the id is out of range. + interf, name, input, output, opened = r + + self.assertEqual(output, 0) + self.assertEqual(input, 1) + self.assertEqual(opened, 0) + out_of_range = pygame.midi.get_count() + for num in range(out_of_range): + self.assertIsNotNone(pygame.midi.get_device_info(num)) + info = pygame.midi.get_device_info(out_of_range) + self.assertIsNone(info) + + def test_init(self): + pygame.midi.quit() + self.assertRaises(RuntimeError, pygame.midi.get_count) + # initialising many times should be fine. + pygame.midi.init() + pygame.midi.init() + pygame.midi.init() + pygame.midi.init() + + self.assertTrue(pygame.midi.get_init()) + + def test_quit(self): + # It is safe to call this more than once. + pygame.midi.quit() + pygame.midi.init() + pygame.midi.quit() + pygame.midi.quit() + pygame.midi.init() + pygame.midi.init() + pygame.midi.quit() + + self.assertFalse(pygame.midi.get_init()) + + def test_get_init(self): + # Already initialized as pygame.midi.init() was called in setUp(). + self.assertTrue(pygame.midi.get_init()) + + def test_time(self): + mtime = pygame.midi.time() + self.assertIsInstance(mtime, int) + # should be close to 2-3... since the timer is just init'd. + self.assertTrue(0 <= mtime < 100) + + +class MidiModuleNonInteractiveTest(unittest.TestCase): + """Midi module tests that do not require midi hardware or midi.init(). + + See MidiModuleTest for interactive module tests. + """ + + def setUp(self): + import pygame.midi + + def test_midiin(self): + """Ensures the MIDIIN event id exists in the midi module. + + The MIDIIN event id can be accessed via the midi module for backward + compatibility. + """ + self.assertEqual(pygame.midi.MIDIIN, pygame.MIDIIN) + self.assertEqual(pygame.midi.MIDIIN, pygame.locals.MIDIIN) + + self.assertNotEqual(pygame.midi.MIDIIN, pygame.MIDIOUT) + self.assertNotEqual(pygame.midi.MIDIIN, pygame.locals.MIDIOUT) + + def test_midiout(self): + """Ensures the MIDIOUT event id exists in the midi module. + + The MIDIOUT event id can be accessed via the midi module for backward + compatibility. + """ + self.assertEqual(pygame.midi.MIDIOUT, pygame.MIDIOUT) + self.assertEqual(pygame.midi.MIDIOUT, pygame.locals.MIDIOUT) + + self.assertNotEqual(pygame.midi.MIDIOUT, pygame.MIDIIN) + self.assertNotEqual(pygame.midi.MIDIOUT, pygame.locals.MIDIIN) + + def test_MidiException(self): + """Ensures the MidiException is raised as expected.""" + + def raiseit(): + raise pygame.midi.MidiException("Hello Midi param") + + with self.assertRaises(pygame.midi.MidiException) as cm: + raiseit() + + self.assertEqual(cm.exception.parameter, "Hello Midi param") + + def test_midis2events(self): + """Ensures midi events are properly converted to pygame events.""" + # List/tuple indexes. + MIDI_DATA = 0 + MD_STATUS = 0 + MD_DATA1 = 1 + MD_DATA2 = 2 + MD_DATA3 = 3 + + TIMESTAMP = 1 + + # Midi events take the form of: + # ((status, data1, data2, data3), timestamp) + midi_events = ( + ((0xC0, 0, 1, 2), 20000), + ((0x90, 60, 1000, "string_data"), 20001), + (("0", "1", "2", "3"), "4"), + ) + expected_num_events = len(midi_events) + + # Test different device ids. + for device_id in range(3): + pg_events = pygame.midi.midis2events(midi_events, device_id) + + self.assertEqual(len(pg_events), expected_num_events) + + for i, pg_event in enumerate(pg_events): + # Get the original midi data for comparison. + midi_event = midi_events[i] + midi_event_data = midi_event[MIDI_DATA] + + # Can't directly check event instance as pygame.event.Event is + # a function. + # self.assertIsInstance(pg_event, pygame.event.Event) + self.assertEqual(pg_event.__class__.__name__, "Event") + self.assertEqual(pg_event.type, pygame.MIDIIN) + self.assertEqual(pg_event.status, midi_event_data[MD_STATUS]) + self.assertEqual(pg_event.data1, midi_event_data[MD_DATA1]) + self.assertEqual(pg_event.data2, midi_event_data[MD_DATA2]) + self.assertEqual(pg_event.data3, midi_event_data[MD_DATA3]) + self.assertEqual(pg_event.timestamp, midi_event[TIMESTAMP]) + self.assertEqual(pg_event.vice_id, device_id) + + def test_midis2events__missing_event_data(self): + """Ensures midi events with missing values are handled properly.""" + midi_event_missing_data = ((0xC0, 0, 1), 20000) + midi_event_missing_timestamp = ((0xC0, 0, 1, 2),) + + for midi_event in (midi_event_missing_data, midi_event_missing_timestamp): + with self.assertRaises(ValueError): + events = pygame.midi.midis2events([midi_event], 0) + + def test_midis2events__extra_event_data(self): + """Ensures midi events with extra values are handled properly.""" + midi_event_extra_data = ((0xC0, 0, 1, 2, "extra"), 20000) + midi_event_extra_timestamp = ((0xC0, 0, 1, 2), 20000, "extra") + + for midi_event in (midi_event_extra_data, midi_event_extra_timestamp): + with self.assertRaises(ValueError): + events = pygame.midi.midis2events([midi_event], 0) + + def test_midis2events__extra_event_data_missing_timestamp(self): + """Ensures midi events with extra data and no timestamps are handled + properly. + """ + midi_event_extra_data_no_timestamp = ((0xC0, 0, 1, 2, "extra"),) + + with self.assertRaises(ValueError): + events = pygame.midi.midis2events([midi_event_extra_data_no_timestamp], 0) + + def test_conversions(self): + """of frequencies to midi note numbers and ansi note names.""" + from pygame.midi import frequency_to_midi, midi_to_frequency, midi_to_ansi_note + + self.assertEqual(frequency_to_midi(27.5), 21) + self.assertEqual(frequency_to_midi(36.7), 26) + self.assertEqual(frequency_to_midi(4186.0), 108) + self.assertEqual(midi_to_frequency(21), 27.5) + self.assertEqual(midi_to_frequency(26), 36.7) + self.assertEqual(midi_to_frequency(108), 4186.0) + self.assertEqual(midi_to_ansi_note(21), "A0") + self.assertEqual(midi_to_ansi_note(71), "B4") + self.assertEqual(midi_to_ansi_note(82), "A#5") + self.assertEqual(midi_to_ansi_note(83), "B5") + self.assertEqual(midi_to_ansi_note(93), "A6") + self.assertEqual(midi_to_ansi_note(94), "A#6") + self.assertEqual(midi_to_ansi_note(95), "B6") + self.assertEqual(midi_to_ansi_note(96), "C7") + self.assertEqual(midi_to_ansi_note(102), "F#7") + self.assertEqual(midi_to_ansi_note(108), "C8") + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/mixer_music_tags.py b/laplas/abstract_map/pygame/tests/mixer_music_tags.py new file mode 100644 index 0000000..30f6893 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/mixer_music_tags.py @@ -0,0 +1,7 @@ +__tags__ = [] + +import pygame +import sys + +if "pygame.mixer_music" not in sys.modules: + __tags__.extend(("ignore", "subprocess_ignore")) diff --git a/laplas/abstract_map/pygame/tests/mixer_music_test.py b/laplas/abstract_map/pygame/tests/mixer_music_test.py new file mode 100644 index 0000000..8d32030 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/mixer_music_test.py @@ -0,0 +1,493 @@ +import os +import sys +import platform +import unittest +import time + +from pygame.tests.test_utils import example_path +import pygame + + +class MixerMusicModuleTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Initializing the mixer is slow, so minimize the times it is called. + pygame.mixer.init() + + @classmethod + def tearDownClass(cls): + pygame.mixer.quit() + + def setUp(cls): + # This makes sure the mixer is always initialized before each test (in + # case a test calls pygame.mixer.quit()). + if pygame.mixer.get_init() is None: + pygame.mixer.init() + + def test_load_mp3(self): + "|tags:music|" + self.music_load("mp3") + + def test_load_ogg(self): + "|tags:music|" + self.music_load("ogg") + + def test_load_wav(self): + "|tags:music|" + self.music_load("wav") + + def music_load(self, format): + data_fname = example_path("data") + + path = os.path.join(data_fname, f"house_lo.{format}") + if os.sep == "\\": + path = path.replace("\\", "\\\\") + umusfn = str(path) + bmusfn = umusfn.encode() + + pygame.mixer.music.load(umusfn) + pygame.mixer.music.load(bmusfn) + + def test_load_object(self): + """test loading music from file-like objects.""" + formats = ["ogg", "wav"] + data_fname = example_path("data") + for f in formats: + path = os.path.join(data_fname, f"house_lo.{f}") + if os.sep == "\\": + path = path.replace("\\", "\\\\") + bmusfn = path.encode() + + with open(bmusfn, "rb") as musf: + pygame.mixer.music.load(musf) + + def test_object_namehint(self): + """test loading & queuing music from file-like objects with namehint argument.""" + formats = ["wav", "ogg"] + data_fname = example_path("data") + for f in formats: + path = os.path.join(data_fname, f"house_lo.{f}") + if os.sep == "\\": + path = path.replace("\\", "\\\\") + bmusfn = path.encode() + + # these two "with open" blocks need to be separate, which is kinda weird + with open(bmusfn, "rb") as musf: + pygame.mixer.music.load(musf, f) + + with open(bmusfn, "rb") as musf: + pygame.mixer.music.queue(musf, f) + + with open(bmusfn, "rb") as musf: + pygame.mixer.music.load(musf, namehint=f) + + with open(bmusfn, "rb") as musf: + pygame.mixer.music.queue(musf, namehint=f) + + def test_load_unicode(self): + """test non-ASCII unicode path""" + import shutil + + ep = example_path("data") + temp_file = os.path.join(ep, "你好.wav") + org_file = os.path.join(ep, "house_lo.wav") + try: + with open(temp_file, "w") as f: + pass + os.remove(temp_file) + except OSError: + raise unittest.SkipTest("the path cannot be opened") + shutil.copy(org_file, temp_file) + try: + pygame.mixer.music.load(temp_file) + pygame.mixer.music.load(org_file) # unload + finally: + os.remove(temp_file) + + def test_unload(self): + import shutil + import tempfile + + ep = example_path("data") + org_file = os.path.join(ep, "house_lo.wav") + tmpfd, tmppath = tempfile.mkstemp(".wav") + os.close(tmpfd) + shutil.copy(org_file, tmppath) + try: + pygame.mixer.music.load(tmppath) + pygame.mixer.music.unload() + finally: + os.remove(tmppath) + + def test_queue_mp3(self): + """Ensures queue() accepts mp3 files. + + |tags:music| + """ + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.queue(filename) + + def test_queue_ogg(self): + """Ensures queue() accepts ogg files. + + |tags:music| + """ + filename = example_path(os.path.join("data", "house_lo.ogg")) + pygame.mixer.music.queue(filename) + + def test_queue_wav(self): + """Ensures queue() accepts wav files. + + |tags:music| + """ + filename = example_path(os.path.join("data", "house_lo.wav")) + pygame.mixer.music.queue(filename) + + def test_queue__multiple_calls(self): + """Ensures queue() can be called multiple times.""" + ogg_file = example_path(os.path.join("data", "house_lo.ogg")) + wav_file = example_path(os.path.join("data", "house_lo.wav")) + + pygame.mixer.music.queue(ogg_file) + pygame.mixer.music.queue(wav_file) + + def test_queue__arguments(self): + """Ensures queue() can be called with proper arguments.""" + wav_file = example_path(os.path.join("data", "house_lo.wav")) + + pygame.mixer.music.queue(wav_file, loops=2) + pygame.mixer.music.queue(wav_file, namehint="") + pygame.mixer.music.queue(wav_file, "") + pygame.mixer.music.queue(wav_file, "", 2) + + def test_queue__no_file(self): + """Ensures queue() correctly handles missing the file argument.""" + with self.assertRaises(TypeError): + pygame.mixer.music.queue() + + def test_queue__invalid_sound_type(self): + """Ensures queue() correctly handles invalid file types.""" + not_a_sound_file = example_path(os.path.join("data", "city.png")) + + with self.assertRaises(pygame.error): + pygame.mixer.music.queue(not_a_sound_file) + + def test_queue__invalid_filename(self): + """Ensures queue() correctly handles invalid filenames.""" + with self.assertRaises(pygame.error): + pygame.mixer.music.queue("") + + def test_music_pause__unpause(self): + """Ensure music has the correct position immediately after unpausing + + |tags:music| + """ + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.load(filename) + pygame.mixer.music.play() + + # Wait 0.05s, then pause + time.sleep(0.05) + pygame.mixer.music.pause() + # Wait 0.05s, get position, unpause, then get position again + time.sleep(0.05) + before_unpause = pygame.mixer.music.get_pos() + pygame.mixer.music.unpause() + after_unpause = pygame.mixer.music.get_pos() + + self.assertEqual(before_unpause, after_unpause) + + def test_stop(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.stop: + + # Stops the music playback if it is currently playing. + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.load(filename) + pygame.mixer.music.play() + + pygame.mixer.music.stop() + self.assertEqual(pygame.mixer.music.get_busy(), False) + + def test_rewind(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.rewind: + + # Resets playback of the current music to the beginning. + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.load(filename) + pygame.mixer.music.play() + + # The music be played for some time + time.sleep(3) + # Then it is rewinded + pygame.mixer.music.rewind() + # Since the sound is 7s long, if it is busy after 6s it means it has been restarted + time.sleep(6.9) + self.assertTrue(pygame.mixer.music.get_busy()) + pygame.mixer.music.stop() + + # Testing that if the music is paused, rewind works but keep the music paused + pygame.mixer.music.play() + time.sleep(2) + pygame.mixer.music.pause() + pygame.mixer.music.rewind() + self.assertFalse(pygame.mixer.music.get_busy()) + time.sleep(1) + pygame.mixer.music.unpause() + time.sleep(6.9) + self.assertTrue(pygame.mixer.music.get_busy()) + + def todo_test_get_pos(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.get_pos: + + # This gets the number of milliseconds that the music has been playing + # for. The returned time only represents how long the music has been + # playing; it does not take into account any starting position + # offsets. + # + + self.fail() + + # def test_fadeout(self): + # filename = example_path(os.path.join("data", "house_lo.mp3")) + # pygame.mixer.music.load(filename) + # pygame.mixer.music.play() + + # pygame.mixer.music.fadeout(50) + # time.sleep(0.3) + # self.assertEqual(pygame.mixer.music.get_busy(), False) + + @unittest.skipIf( + os.environ.get("SDL_AUDIODRIVER") == "disk", + 'disk audio driver "playback" writing to disk is slow', + ) + def test_play__start_time(self): + pygame.display.init() + + # music file is 7 seconds long + filename = example_path(os.path.join("data", "house_lo.ogg")) + pygame.mixer.music.load(filename) + start_time_in_seconds = 6.0 # 6 seconds + + music_finished = False + clock = pygame.time.Clock() + start_time_in_ms = clock.tick() + # should play the last 1 second + pygame.mixer.music.play(0, start=start_time_in_seconds) + running = True + while running: + pygame.event.pump() + + if not (pygame.mixer.music.get_busy() or music_finished): + music_finished = True + time_to_finish = (clock.tick() - start_time_in_ms) // 1000 + self.assertEqual(time_to_finish, 1) + running = False + + def test_play(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.play: + + # This will play the loaded music stream. If the music is already + # playing it will be restarted. + # + # The loops argument controls the number of repeats a music will play. + # play(5) will cause the music to played once, then repeated five + # times, for a total of six. If the loops is -1 then the music will + # repeat indefinitely. + # + # The starting position argument controls where in the music the song + # starts playing. The starting position is dependent on the format of + # music playing. MP3 and OGG use the position as time (in seconds). + # MOD music it is the pattern order number. Passing a startpos will + # raise a NotImplementedError if it cannot set the start position + # + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.load(filename) + pygame.mixer.music.play() + + self.assertTrue(pygame.mixer.music.get_busy()) + + pygame.mixer.music.stop() + + def test_load(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.load: + + # This will load a music file and prepare it for playback. If a music + # stream is already playing it will be stopped. This does not start + # the music playing. + # + # Music can only be loaded from filenames, not python file objects + # like the other pygame loading functions. + # + + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.load(filename) + self.assertFalse(pygame.mixer.music.get_busy()) + pygame.mixer.music.play() + self.assertTrue(pygame.mixer.music.get_busy()) + + filename = example_path(os.path.join("data", "house_lo.wav")) + pygame.mixer.music.load(filename) + self.assertFalse(pygame.mixer.music.get_busy()) + + def test_get_volume(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.get_volume: + + # Returns the current volume for the mixer. The value will be between + # 0.0 and 1.0. + # + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.load(filename) + pygame.mixer.music.play() + + vol = pygame.mixer.music.get_volume() + self.assertGreaterEqual(vol, 0) + self.assertLessEqual(vol, 1) + + pygame.mixer.music.stop() + + def test_pause(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.pause: + + # Temporarily stop playback of the music stream. It can be resumed + # with the pygame.mixer.music.unpause() function. + # + self.music_load("ogg") + self.assertFalse(pygame.mixer.music.get_busy()) + pygame.mixer.music.play() + self.assertTrue(pygame.mixer.music.get_busy()) + pygame.mixer.music.pause() + self.assertFalse(pygame.mixer.music.get_busy()) + + def test_get_busy(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.get_busy: + + # Returns True when the music stream is actively playing. When the + # music is idle this returns False. + # + + self.music_load("ogg") + self.assertFalse(pygame.mixer.music.get_busy()) + pygame.mixer.music.play() + self.assertTrue(pygame.mixer.music.get_busy()) + pygame.mixer.music.pause() + self.assertFalse(pygame.mixer.music.get_busy()) + + def test_unpause(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.unpause: + + # This will resume the playback of a music stream after it has been paused. + + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.load(filename) + pygame.mixer.music.play() + self.assertTrue(pygame.mixer.music.get_busy()) + time.sleep(0.1) + pygame.mixer.music.pause() + self.assertFalse(pygame.mixer.music.get_busy()) + before = pygame.mixer.music.get_pos() + pygame.mixer.music.unpause() + after = pygame.mixer.music.get_pos() + self.assertTrue(pygame.mixer.music.get_busy()) + # It could rarely be that it is +/- 1 different + # But mostly, after should equal before. + self.assertTrue(before - 1 <= after <= before + 1) + + pygame.mixer.music.stop() + + def test_set_volume(self): + # __doc__ (as of 2008-08-02) for pygame.mixer_music.set_volume: + + # Set the volume of the music playback. The value argument is between + # 0.0 and 1.0. When new music is loaded the volume is reset. + # + filename = example_path(os.path.join("data", "house_lo.mp3")) + pygame.mixer.music.load(filename) + pygame.mixer.music.play() + + pygame.mixer.music.set_volume(0.5) + vol = pygame.mixer.music.get_volume() + self.assertEqual(vol, 0.5) + + pygame.mixer.music.stop() + + def todo_test_set_pos(self): + # __doc__ (as of 2010-24-05) for pygame.mixer_music.set_pos: + + # This sets the position in the music file where playback will start. The + # meaning of "pos", a float (or a number that can be converted to a float), + # depends on the music format. Newer versions of SDL_mixer have better + # positioning support than earlier. An SDLError is raised if a particular + # format does not support positioning. + # + + self.fail() + + def test_init(self): + """issue #955. unload music whenever mixer.quit() is called""" + import tempfile + import shutil + + testfile = example_path(os.path.join("data", "house_lo.wav")) + tempcopy = os.path.join(tempfile.gettempdir(), "tempfile.wav") + + for i in range(10): + pygame.mixer.init() + try: + shutil.copy2(testfile, tempcopy) + pygame.mixer.music.load(tempcopy) + pygame.mixer.quit() + finally: + os.remove(tempcopy) + + +# class MixerMusicEndEventTest(unittest.TestCase): +# def setUp(self): +# pygame.display.init() +# pygame.display.set_mode((40, 40)) +# if pygame.mixer.get_init() is None: +# pygame.mixer.init() + +# def tearDown(self): +# pygame.display.quit() +# pygame.mixer.quit() + +# def test_get_endevent(self): +# # __doc__ (as of 2008-08-02) for pygame.mixer_music.get_endevent: + +# # Returns the event type to be sent every time the music finishes +# # playback. If there is no endevent the function returns +# # pygame.NOEVENT. +# # +# filename = example_path(os.path.join("data", "car_door.wav")) +# pygame.mixer.music.load(filename) +# pygame.mixer.music.play() +# no_event = pygame.mixer_music.get_endevent() +# self.assertEqual(pygame.NOEVENT, no_event) + +# event_type = pygame.USEREVENT +# pygame.mixer_music.set_endevent(event_type) +# end_event = pygame.mixer_music.get_endevent() +# self.assertEqual(event_type, end_event) + +# def test_set_endevent(self): +# # __doc__ (as of 2008-08-02) for pygame.mixer_music.set_endevent: + +# # This causes Pygame to signal (by means of the event queue) when the +# # music is done playing. The argument determines the type of event +# # that will be queued. +# # +# # The event will be queued every time the music finishes, not just the +# # first time. To stop the event from being queued, call this method +# # with no argument. +# # +# filename = example_path(os.path.join("data", "house_lo.wav")) +# pygame.mixer.music.load(filename) +# pygame.mixer.music.play() +# event_type = pygame.USEREVENT +# pygame.mixer_music.set_endevent(event_type) +# end_event = pygame.mixer_music.get_endevent() +# self.assertEqual(event_type, end_event) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/mixer_tags.py b/laplas/abstract_map/pygame/tests/mixer_tags.py new file mode 100644 index 0000000..06a9de2 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/mixer_tags.py @@ -0,0 +1,7 @@ +__tags__ = [] + +import pygame +import sys + +if "pygame.mixer" not in sys.modules: + __tags__.extend(("ignore", "subprocess_ignore")) diff --git a/laplas/abstract_map/pygame/tests/mixer_test.py b/laplas/abstract_map/pygame/tests/mixer_test.py new file mode 100644 index 0000000..0b11c5a --- /dev/null +++ b/laplas/abstract_map/pygame/tests/mixer_test.py @@ -0,0 +1,1479 @@ +import sys +import os +import unittest +import pathlib +import platform +import time +from array import array + +from pygame.tests.test_utils import example_path + +import pygame +from pygame import mixer + +IS_PYPY = "PyPy" == platform.python_implementation() + +################################### CONSTANTS ################################## + +FREQUENCIES = [11025, 22050, 44100, 48000] +SIZES = [-16, -8, 8, 16] # fixme +# size 32 failed in test_get_init__returns_exact_values_used_for_init +CHANNELS = [1, 2] +BUFFERS = [3024] + +CONFIGS = [ + {"frequency": f, "size": s, "channels": c} + for f in FREQUENCIES + for s in SIZES + for c in CHANNELS +] +# Using all CONFIGS fails on a Mac; probably older SDL_mixer; we could do: +# if platform.system() == 'Darwin': +# But using all CONFIGS is very slow (> 10 sec for example) +# And probably, we don't need to be so exhaustive, hence: + +CONFIG = {"frequency": 44100, "size": 32, "channels": 2, "allowedchanges": 0} + + +class InvalidBool: + """To help test invalid bool values.""" + + __bool__ = None + + +############################## MODULE LEVEL TESTS ############################# + + +class MixerModuleTest(unittest.TestCase): + def tearDown(self): + mixer.quit() + mixer.pre_init(0, 0, 0, 0) + + def test_init__keyword_args(self): + # note: this test used to loop over all CONFIGS, but it's very slow.. + mixer.init(**CONFIG) + mixer_conf = mixer.get_init() + + self.assertEqual(mixer_conf[0], CONFIG["frequency"]) + # Not all "sizes" are supported on all systems, hence "abs". + self.assertEqual(abs(mixer_conf[1]), abs(CONFIG["size"])) + self.assertGreaterEqual(mixer_conf[2], CONFIG["channels"]) + + def test_pre_init__keyword_args(self): + # note: this test used to loop over all CONFIGS, but it's very slow.. + mixer.pre_init(**CONFIG) + mixer.init() + + mixer_conf = mixer.get_init() + + self.assertEqual(mixer_conf[0], CONFIG["frequency"]) + # Not all "sizes" are supported on all systems, hence "abs". + self.assertEqual(abs(mixer_conf[1]), abs(CONFIG["size"])) + self.assertGreaterEqual(mixer_conf[2], CONFIG["channels"]) + + def test_pre_init__zero_values(self): + # Ensure that argument values of 0 are replaced with + # default values. No way to check buffer size though. + mixer.pre_init(22050, -8, 1) # Non default values + mixer.pre_init(0, 0, 0) # Should reset to default values + mixer.init(allowedchanges=0) + self.assertEqual(mixer.get_init()[0], 44100) + self.assertEqual(mixer.get_init()[1], -16) + self.assertGreaterEqual(mixer.get_init()[2], 2) + + def test_init__zero_values(self): + # Ensure that argument values of 0 are replaced with + # preset values. No way to check buffer size though. + mixer.pre_init(44100, 8, 1, allowedchanges=0) # None default values + mixer.init(0, 0, 0) + self.assertEqual(mixer.get_init(), (44100, 8, 1)) + + # def test_get_init__returns_exact_values_used_for_init(self): + # # TODO: size 32 fails in this test (maybe SDL_mixer bug) + + # # TODO: 2) When you start the mixer, you request the settings. + # # But it can be that a sound system doesn’t support what you request… + # # and it gives you back something close to what you request but not equal. + # # So, you can’t test for equality. + # # See allowedchanges + + # for init_conf in CONFIGS: + # frequency, size, channels = init_conf.values() + # if (frequency, size) == (22050, 16): + # continue + # mixer.init(frequency, size, channels) + + # mixer_conf = mixer.get_init() + # self.assertEqual(tuple(init_conf.values()), mixer_conf) + # mixer.quit() + + def test_get_init__returns_None_if_mixer_not_initialized(self): + self.assertIsNone(mixer.get_init()) + + def test_get_num_channels__defaults_eight_after_init(self): + mixer.init() + self.assertEqual(mixer.get_num_channels(), 8) + + def test_set_num_channels(self): + mixer.init() + + default_num_channels = mixer.get_num_channels() + for i in range(1, default_num_channels + 1): + mixer.set_num_channels(i) + self.assertEqual(mixer.get_num_channels(), i) + + def test_quit(self): + """get_num_channels() Should throw pygame.error if uninitialized + after mixer.quit()""" + mixer.init() + mixer.quit() + self.assertRaises(pygame.error, mixer.get_num_channels) + + # TODO: FIXME: appveyor and pypy (on linux) fails here sometimes. + @unittest.skipIf(sys.platform.startswith("win"), "See github issue 892.") + @unittest.skipIf(IS_PYPY, "random errors here with pypy") + def test_sound_args(self): + def get_bytes(snd): + return snd.get_raw() + + mixer.init() + + sample = b"\x00\xff" * 24 + wave_path = example_path(os.path.join("data", "house_lo.wav")) + uwave_path = str(wave_path) + bwave_path = uwave_path.encode(sys.getfilesystemencoding()) + snd = mixer.Sound(file=wave_path) + self.assertTrue(snd.get_length() > 0.5) + snd_bytes = get_bytes(snd) + self.assertTrue(len(snd_bytes) > 1000) + + self.assertEqual(get_bytes(mixer.Sound(wave_path)), snd_bytes) + + self.assertEqual(get_bytes(mixer.Sound(file=uwave_path)), snd_bytes) + self.assertEqual(get_bytes(mixer.Sound(uwave_path)), snd_bytes) + arg_emsg = "Sound takes either 1 positional or 1 keyword argument" + + with self.assertRaises(TypeError) as cm: + mixer.Sound() + self.assertEqual(str(cm.exception), arg_emsg) + with self.assertRaises(TypeError) as cm: + mixer.Sound(wave_path, buffer=sample) + self.assertEqual(str(cm.exception), arg_emsg) + with self.assertRaises(TypeError) as cm: + mixer.Sound(sample, file=wave_path) + self.assertEqual(str(cm.exception), arg_emsg) + with self.assertRaises(TypeError) as cm: + mixer.Sound(buffer=sample, file=wave_path) + self.assertEqual(str(cm.exception), arg_emsg) + + with self.assertRaises(TypeError) as cm: + mixer.Sound(foobar=sample) + self.assertEqual(str(cm.exception), "Unrecognized keyword argument 'foobar'") + + snd = mixer.Sound(wave_path, **{}) + self.assertEqual(get_bytes(snd), snd_bytes) + snd = mixer.Sound(*[], **{"file": wave_path}) + + with self.assertRaises(TypeError) as cm: + mixer.Sound([]) + self.assertEqual(str(cm.exception), "Unrecognized argument (type list)") + + with self.assertRaises(TypeError) as cm: + snd = mixer.Sound(buffer=[]) + emsg = "Expected object with buffer interface: got a list" + self.assertEqual(str(cm.exception), emsg) + + ufake_path = "12345678" + self.assertRaises(IOError, mixer.Sound, ufake_path) + self.assertRaises(IOError, mixer.Sound, "12345678") + + with self.assertRaises(TypeError) as cm: + mixer.Sound(buffer="something") + emsg = "Unicode object not allowed as buffer object" + self.assertEqual(str(cm.exception), emsg) + self.assertEqual(get_bytes(mixer.Sound(buffer=sample)), sample) + if type(sample) != str: + somebytes = get_bytes(mixer.Sound(sample)) + # on python 2 we do not allow using string except as file name. + self.assertEqual(somebytes, sample) + self.assertEqual(get_bytes(mixer.Sound(file=bwave_path)), snd_bytes) + self.assertEqual(get_bytes(mixer.Sound(bwave_path)), snd_bytes) + + snd = mixer.Sound(wave_path) + with self.assertRaises(TypeError) as cm: + mixer.Sound(wave_path, array=snd) + self.assertEqual(str(cm.exception), arg_emsg) + with self.assertRaises(TypeError) as cm: + mixer.Sound(buffer=sample, array=snd) + self.assertEqual(str(cm.exception), arg_emsg) + snd2 = mixer.Sound(array=snd) + self.assertEqual(snd.get_raw(), snd2.get_raw()) + + def test_sound_unicode(self): + """test non-ASCII unicode path""" + mixer.init() + import shutil + + ep = example_path("data") + temp_file = os.path.join(ep, "你好.wav") + org_file = os.path.join(ep, "house_lo.wav") + shutil.copy(org_file, temp_file) + try: + with open(temp_file, "rb") as f: + pass + except OSError: + raise unittest.SkipTest("the path cannot be opened") + + try: + sound = mixer.Sound(temp_file) + del sound + finally: + os.remove(temp_file) + + @unittest.skipIf( + os.environ.get("SDL_AUDIODRIVER") == "disk", + "this test fails without real sound card", + ) + def test_array_keyword(self): + try: + from numpy import ( + array, + arange, + zeros, + int8, + uint8, + int16, + uint16, + int32, + uint32, + ) + except ImportError: + self.skipTest("requires numpy") + + freq = 22050 + format_list = [-8, 8, -16, 16] + channels_list = [1, 2] + + a_lists = {f: [] for f in format_list} + a32u_mono = arange(0, 256, 1, uint32) + a16u_mono = a32u_mono.astype(uint16) + a8u_mono = a32u_mono.astype(uint8) + au_list_mono = [(1, a) for a in [a8u_mono, a16u_mono, a32u_mono]] + for format in format_list: + if format > 0: + a_lists[format].extend(au_list_mono) + a32s_mono = arange(-128, 128, 1, int32) + a16s_mono = a32s_mono.astype(int16) + a8s_mono = a32s_mono.astype(int8) + as_list_mono = [(1, a) for a in [a8s_mono, a16s_mono, a32s_mono]] + for format in format_list: + if format < 0: + a_lists[format].extend(as_list_mono) + a32u_stereo = zeros([a32u_mono.shape[0], 2], uint32) + a32u_stereo[:, 0] = a32u_mono + a32u_stereo[:, 1] = 255 - a32u_mono + a16u_stereo = a32u_stereo.astype(uint16) + a8u_stereo = a32u_stereo.astype(uint8) + au_list_stereo = [(2, a) for a in [a8u_stereo, a16u_stereo, a32u_stereo]] + for format in format_list: + if format > 0: + a_lists[format].extend(au_list_stereo) + a32s_stereo = zeros([a32s_mono.shape[0], 2], int32) + a32s_stereo[:, 0] = a32s_mono + a32s_stereo[:, 1] = -1 - a32s_mono + a16s_stereo = a32s_stereo.astype(int16) + a8s_stereo = a32s_stereo.astype(int8) + as_list_stereo = [(2, a) for a in [a8s_stereo, a16s_stereo, a32s_stereo]] + for format in format_list: + if format < 0: + a_lists[format].extend(as_list_stereo) + + for format in format_list: + for channels in channels_list: + try: + mixer.init(freq, format, channels) + except pygame.error: + # Some formats (e.g. 16) may not be supported. + continue + try: + __, f, c = mixer.get_init() + if f != format or c != channels: + # Some formats (e.g. -8) may not be supported. + continue + for c, a in a_lists[format]: + self._test_array_argument(format, a, c == channels) + finally: + mixer.quit() + + def _test_array_argument(self, format, a, test_pass): + from numpy import array, all as all_ + + try: + snd = mixer.Sound(array=a) + except ValueError: + if not test_pass: + return + self.fail("Raised ValueError: Format %i, dtype %s" % (format, a.dtype)) + if not test_pass: + self.fail( + "Did not raise ValueError: Format %i, dtype %s" % (format, a.dtype) + ) + a2 = array(snd) + a3 = a.astype(a2.dtype) + lshift = abs(format) - 8 * a.itemsize + if lshift >= 0: + # This is asymmetric with respect to downcasting. + a3 <<= lshift + self.assertTrue(all_(a2 == a3), "Format %i, dtype %s" % (format, a.dtype)) + + def _test_array_interface_fail(self, a): + self.assertRaises(ValueError, mixer.Sound, array=a) + + def test_array_interface(self): + mixer.init(22050, -16, 1, allowedchanges=0) + snd = mixer.Sound(buffer=b"\x00\x7f" * 20) + d = snd.__array_interface__ + self.assertTrue(isinstance(d, dict)) + if pygame.get_sdl_byteorder() == pygame.LIL_ENDIAN: + typestr = "") if is_lil_endian else (">", "<") + shape = (10, channels)[:ndim] + strides = (channels * itemsize, itemsize)[2 - ndim :] + exp = Exporter(shape, format=frev + "i") + snd = mixer.Sound(array=exp) + buflen = len(exp) * itemsize * channels + imp = Importer(snd, buftools.PyBUF_SIMPLE) + self.assertEqual(imp.ndim, 0) + self.assertTrue(imp.format is None) + self.assertEqual(imp.len, buflen) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.shape is None) + self.assertTrue(imp.strides is None) + self.assertTrue(imp.suboffsets is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.buf, snd._samples_address) + imp = Importer(snd, buftools.PyBUF_WRITABLE) + self.assertEqual(imp.ndim, 0) + self.assertTrue(imp.format is None) + self.assertEqual(imp.len, buflen) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.shape is None) + self.assertTrue(imp.strides is None) + self.assertTrue(imp.suboffsets is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.buf, snd._samples_address) + imp = Importer(snd, buftools.PyBUF_FORMAT) + self.assertEqual(imp.ndim, 0) + self.assertEqual(imp.format, format) + self.assertEqual(imp.len, buflen) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.shape is None) + self.assertTrue(imp.strides is None) + self.assertTrue(imp.suboffsets is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.buf, snd._samples_address) + imp = Importer(snd, buftools.PyBUF_ND) + self.assertEqual(imp.ndim, ndim) + self.assertTrue(imp.format is None) + self.assertEqual(imp.len, buflen) + self.assertEqual(imp.itemsize, itemsize) + self.assertEqual(imp.shape, shape) + self.assertTrue(imp.strides is None) + self.assertTrue(imp.suboffsets is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.buf, snd._samples_address) + imp = Importer(snd, buftools.PyBUF_STRIDES) + self.assertEqual(imp.ndim, ndim) + self.assertTrue(imp.format is None) + self.assertEqual(imp.len, buflen) + self.assertEqual(imp.itemsize, itemsize) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertTrue(imp.suboffsets is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.buf, snd._samples_address) + imp = Importer(snd, buftools.PyBUF_FULL_RO) + self.assertEqual(imp.ndim, ndim) + self.assertEqual(imp.format, format) + self.assertEqual(imp.len, buflen) + self.assertEqual(imp.itemsize, 2) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertTrue(imp.suboffsets is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.buf, snd._samples_address) + imp = Importer(snd, buftools.PyBUF_FULL_RO) + self.assertEqual(imp.ndim, ndim) + self.assertEqual(imp.format, format) + self.assertEqual(imp.len, buflen) + self.assertEqual(imp.itemsize, itemsize) + self.assertEqual(imp.shape, exp.shape) + self.assertEqual(imp.strides, strides) + self.assertTrue(imp.suboffsets is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.buf, snd._samples_address) + imp = Importer(snd, buftools.PyBUF_C_CONTIGUOUS) + self.assertEqual(imp.ndim, ndim) + self.assertTrue(imp.format is None) + self.assertEqual(imp.strides, strides) + imp = Importer(snd, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertEqual(imp.ndim, ndim) + self.assertTrue(imp.format is None) + self.assertEqual(imp.strides, strides) + if ndim == 1: + imp = Importer(snd, buftools.PyBUF_F_CONTIGUOUS) + self.assertEqual(imp.ndim, 1) + self.assertTrue(imp.format is None) + self.assertEqual(imp.strides, strides) + else: + self.assertRaises(BufferError, Importer, snd, buftools.PyBUF_F_CONTIGUOUS) + + def test_fadeout(self): + """Ensure pygame.mixer.fadeout() stops playback after fading out the sound.""" + if mixer.get_init() is None: + mixer.init() + sound = pygame.mixer.Sound(example_path("data/house_lo.wav")) + channel = pygame.mixer.find_channel() + channel.play(sound) + fadeout_time = 200 # milliseconds + channel.fadeout(fadeout_time) + pygame.time.wait(fadeout_time + 30) + + # Ensure the channel is no longer busy + self.assertFalse(channel.get_busy()) + + def test_find_channel(self): + # __doc__ (as of 2008-08-02) for pygame.mixer.find_channel: + + # pygame.mixer.find_channel(force=False): return Channel + # find an unused channel + mixer.init() + + filename = example_path(os.path.join("data", "house_lo.wav")) + sound = mixer.Sound(file=filename) + + num_channels = mixer.get_num_channels() + + if num_channels > 0: + found_channel = mixer.find_channel() + self.assertIsNotNone(found_channel) + + # try playing on all channels + channels = [] + for channel_id in range(0, num_channels): + channel = mixer.Channel(channel_id) + channel.play(sound) + channels.append(channel) + + # should fail without being forceful + found_channel = mixer.find_channel() + self.assertIsNone(found_channel) + + # try forcing without keyword + found_channel = mixer.find_channel(True) + self.assertIsNotNone(found_channel) + + # try forcing with keyword + found_channel = mixer.find_channel(force=True) + self.assertIsNotNone(found_channel) + + for channel in channels: + channel.stop() + found_channel = mixer.find_channel() + self.assertIsNotNone(found_channel) + + @unittest.expectedFailure + def test_pause(self): + """Ensure pygame.mixer.pause() temporarily stops playback of all sound channels.""" + if mixer.get_init() is None: + mixer.init() + sound = mixer.Sound(example_path("data/house_lo.wav")) + channel = mixer.find_channel() + channel.play(sound) + + mixer.pause() + + # TODO: this currently fails? + # Ensure the channel is paused + self.assertFalse(channel.get_busy()) + + mixer.unpause() + + # Ensure the channel is no longer paused + self.assertTrue(channel.get_busy()) + + def test_set_reserved(self): + """Ensure pygame.mixer.set_reserved() reserves the given number of channels.""" + + # pygame.mixer.set_reserved(count): return count + mixer.init() + default_num_channels = mixer.get_num_channels() + + # try reserving all the channels + result = mixer.set_reserved(default_num_channels) + self.assertEqual(result, default_num_channels) + + # try reserving all the channels + 1 + result = mixer.set_reserved(default_num_channels + 1) + # should still be default + self.assertEqual(result, default_num_channels) + + # try unreserving all + result = mixer.set_reserved(0) + # should still be default + self.assertEqual(result, 0) + + # try reserving half + result = mixer.set_reserved(int(default_num_channels / 2)) + # should still be default + self.assertEqual(result, int(default_num_channels / 2)) + + def test_stop(self): + """Stops playback of all active sound channels.""" + if mixer.get_init() is None: + mixer.init() + sound = pygame.mixer.Sound(example_path("data/house_lo.wav")) + channel = pygame.mixer.Channel(0) + channel.play(sound) + pygame.mixer.stop() + for i in range(pygame.mixer.get_num_channels()): + self.assertFalse(pygame.mixer.Channel(i).get_busy()) + + def test_get_sdl_mixer_version(self): + """Ensures get_sdl_mixer_version works correctly with no args.""" + expected_length = 3 + expected_type = tuple + expected_item_type = int + + version = pygame.mixer.get_sdl_mixer_version() + + self.assertIsInstance(version, expected_type) + self.assertEqual(len(version), expected_length) + + for item in version: + self.assertIsInstance(item, expected_item_type) + + def test_get_sdl_mixer_version__args(self): + """Ensures get_sdl_mixer_version works correctly using args.""" + expected_length = 3 + expected_type = tuple + expected_item_type = int + + for value in (True, False): + version = pygame.mixer.get_sdl_mixer_version(value) + + self.assertIsInstance(version, expected_type) + self.assertEqual(len(version), expected_length) + + for item in version: + self.assertIsInstance(item, expected_item_type) + + def test_get_sdl_mixer_version__kwargs(self): + """Ensures get_sdl_mixer_version works correctly using kwargs.""" + expected_length = 3 + expected_type = tuple + expected_item_type = int + + for value in (True, False): + version = pygame.mixer.get_sdl_mixer_version(linked=value) + + self.assertIsInstance(version, expected_type) + self.assertEqual(len(version), expected_length) + + for item in version: + self.assertIsInstance(item, expected_item_type) + + def test_get_sdl_mixer_version__invalid_args_kwargs(self): + """Ensures get_sdl_mixer_version handles invalid args and kwargs.""" + invalid_bool = InvalidBool() + + with self.assertRaises(TypeError): + version = pygame.mixer.get_sdl_mixer_version(invalid_bool) + + with self.assertRaises(TypeError): + version = pygame.mixer.get_sdl_mixer_version(linked=invalid_bool) + + def test_get_sdl_mixer_version__linked_equals_compiled(self): + """Ensures get_sdl_mixer_version's linked/compiled versions are equal.""" + linked_version = pygame.mixer.get_sdl_mixer_version(linked=True) + complied_version = pygame.mixer.get_sdl_mixer_version(linked=False) + + self.assertTupleEqual(linked_version, complied_version) + + +############################## CHANNEL CLASS TESTS ############################# + + +class ChannelTypeTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Initializing the mixer is slow, so minimize the times it is called. + mixer.init() + + @classmethod + def tearDownClass(cls): + mixer.quit() + + def setUp(cls): + # This makes sure the mixer is always initialized before each test (in + # case a test calls pygame.mixer.quit()). + if mixer.get_init() is None: + mixer.init() + + def test_channel(self): + """Ensure Channel() creation works.""" + channel = mixer.Channel(0) + + self.assertIsInstance(channel, mixer.ChannelType) + self.assertEqual(channel.__class__.__name__, "Channel") + + def test_channel__without_arg(self): + """Ensure exception for Channel() creation with no argument.""" + with self.assertRaises(TypeError): + mixer.Channel() + + def test_channel__invalid_id(self): + """Ensure exception for Channel() creation with an invalid id.""" + with self.assertRaises(IndexError): + mixer.Channel(-1) + + def test_channel__before_init(self): + """Ensure exception for Channel() creation with non-init mixer.""" + mixer.quit() + + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + mixer.Channel(0) + + def test_fadeout(self): + """Ensure Channel.fadeout() stops playback after fading out.""" + channel = mixer.Channel(0) + sound = mixer.Sound(example_path("data/house_lo.wav")) + channel.play(sound) + + fadeout_time = 1000 + channel.fadeout(fadeout_time) + + # Wait for the fadeout to complete. + pygame.time.wait(fadeout_time + 100) + + self.assertFalse(channel.get_busy()) + + def test_get_busy(self): + """Ensure an idle channel's busy state is correct.""" + expected_busy = False + channel = mixer.Channel(0) + + busy = channel.get_busy() + + self.assertEqual(busy, expected_busy) + + def test_get_busy__active(self): + """Ensure an active channel's busy state is correct.""" + channel = mixer.Channel(0) + sound = mixer.Sound(example_path("data/house_lo.wav")) + channel.play(sound) + + self.assertTrue(channel.get_busy()) + + def todo_test_get_endevent(self): + # __doc__ (as of 2008-08-02) for pygame.mixer.Channel.get_endevent: + + # Channel.get_endevent(): return type + # get the event a channel sends when playback stops + # + # Returns the event type to be sent every time the Channel finishes + # playback of a Sound. If there is no endevent the function returns + # pygame.NOEVENT. + # + + self.fail() + + def test_get_queue(self): + """Ensure Channel.get_queue() returns any queued Sound.""" + channel = mixer.Channel(0) + frequency, format, channels = mixer.get_init() + sound_length_in_ms = 200 + sound_length_in_ms_2 = 400 + bytes_per_ms = int((frequency / 1000) * channels * (abs(format) // 8)) + sound1 = mixer.Sound(b"\x00" * int(sound_length_in_ms * bytes_per_ms)) + sound2 = mixer.Sound(b"\x00" * (int(sound_length_in_ms_2 * bytes_per_ms))) + + channel.play(sound1) + channel.queue(sound2) + + # Ensure the second queued sound is returned. + self.assertEqual(channel.get_queue().get_length(), sound2.get_length()) + + pygame.time.wait(sound_length_in_ms + 100) + self.assertIsNone(channel.get_queue()) + + # the second sound is now playing + self.assertEqual(channel.get_sound().get_length(), sound2.get_length()) + pygame.time.wait((sound_length_in_ms_2) + 100) + + # Now there is nothing on the queue. + self.assertIsNone(channel.get_queue()) + + def test_get_sound(self): + """Ensure Channel.get_sound() returns the currently playing Sound.""" + channel = mixer.Channel(0) + sound = mixer.Sound(example_path("data/house_lo.wav")) + channel.play(sound) + + # Ensure the correct Sound object is returned. + got_sound = channel.get_sound() + self.assertEqual(got_sound, sound) + + # Stop the sound and ensure None is returned. + channel.stop() + got_sound = channel.get_sound() + self.assertIsNone(got_sound) + + def test_get_volume(self): + """Ensure a channel's volume can be retrieved.""" + expected_volume = 1.0 # default + channel = mixer.Channel(0) + + volume = channel.get_volume() + + self.assertAlmostEqual(volume, expected_volume) + + def test_pause_unpause(self): + """ + Test if the Channel can be paused and unpaused. + """ + if mixer.get_init() is None: + mixer.init() + sound = pygame.mixer.Sound(example_path("data/house_lo.wav")) + channel = sound.play() + channel.pause() + self.assertTrue( + channel.get_busy(), msg="Channel should be paused but it's not." + ) + channel.unpause() + self.assertTrue( + channel.get_busy(), msg="Channel should be unpaused but it's not." + ) + sound.stop() + + def test_pause_unpause__before_init(self): + """ + Ensure exception for Channel.pause() with non-init mixer. + """ + sound = mixer.Sound(example_path("data/house_lo.wav")) + channel = sound.play() + mixer.quit() + + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + channel.pause() + + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + channel.unpause() + + def test_queue(self): + """ + Ensure the Channel.queue() works correctly + """ + if os.environ.get("PYGAME_MSYS2") == "1": + self.skipTest("Skip test on MSYS2") + + # Setup + channel = mixer.Channel(0) + frequency, format, channels = mixer.get_init() + sound_length_in_ms = 200 + sound_length_in_ms_2 = 400 + sound_length_in_ms_3 = 300 + bytes_per_ms = int((frequency / 1000) * channels * (abs(format) // 8)) + sound1 = mixer.Sound(b"\x00" * int(sound_length_in_ms * bytes_per_ms)) + sound2 = mixer.Sound(b"\x00" * (int(sound_length_in_ms_2 * bytes_per_ms))) + sound3 = mixer.Sound(b"\x00" * (int(sound_length_in_ms_3 * bytes_per_ms))) + + # Test that the sound is played when the first one stop and the queue is cleared + channel.play(sound1) + channel.queue(sound2) + pygame.time.wait(sound_length_in_ms + 100) + self.assertTrue(channel.get_busy()) + self.assertEqual(channel.get_sound(), sound2) + self.assertIsNone(channel.get_queue()) + + pygame.time.wait(sound_length_in_ms_2 + 100) + + # Test when no sound playing + channel.queue(sound1) + self.assertTrue(channel.get_busy()) + self.assertEqual(channel.get_sound(), sound1) + + pygame.time.wait(sound_length_in_ms + 100) + + # Test that the queue is discarded if Channel.play() is used + channel.play(sound1) + channel.queue(sound2) + channel.play(sound3) + self.assertIsNone(channel.get_queue()) + + pygame.time.wait(sound_length_in_ms_3 + 100) + self.assertFalse(channel.get_busy()) + + # Test that the queue is discarded if Channel.stop() is used + channel.play(sound1) + channel.queue(sound2) + channel.stop() + self.assertIsNone(channel.get_queue()) + self.assertFalse(channel.get_busy()) + + # Test that when there is already a queue(), the ancient queue get discarded + channel.play(sound1) + channel.queue(sound2) + channel.queue(sound3) + self.assertEqual(channel.get_sound(), sound1) + self.assertEqual(channel.get_queue(), sound3) + + def test_stop(self): + # __doc__ (as of 2008-08-02) for pygame.mixer.Channel.stop: + + # Channel.stop(): return None + # stop playback on a Channel + # + # Stop sound playback on a channel. After playback is stopped the + # channel becomes available for new Sounds to play on it. + # + + channel = mixer.Channel(0) + sound = mixer.Sound(example_path("data/house_lo.wav")) + + # simple check + channel.play(sound) + channel.stop() + self.assertFalse(channel.get_busy()) + # check that queued sounds also stop + channel.queue(sound) + channel.stop() + self.assertFalse(channel.get_busy()) + # check that new sounds can be played + channel.play(sound) + self.assertTrue(channel.get_busy()) + + +class ChannelSetVolumeTest(unittest.TestCase): + def setUp(self): + mixer.init() + self.channel = pygame.mixer.Channel(0) + self.sound = pygame.mixer.Sound(example_path("data/boom.wav")) + + def tearDown(self): + mixer.quit() + + def test_set_volume_with_one_argument(self): + self.channel.play(self.sound) + self.channel.set_volume(0.5) + self.assertEqual(self.channel.get_volume(), 0.5) + + @unittest.expectedFailure + def test_set_volume_with_two_arguments(self): + # TODO: why doesn't this work? Seems to ignore stereo setting. + # https://www.pygame.org/docs/ref/mixer.html#pygame.mixer.Channel.set_volume + self.channel.play(self.sound) + self.channel.set_volume(0.3, 0.7) + self.assertEqual(self.channel.get_volume(), (0.3, 0.7)) + + +class ChannelEndEventTest(unittest.TestCase): + def setUp(self): + pygame.display.init() + pygame.display.set_mode((40, 40)) + if mixer.get_init() is None: + mixer.init() + + def tearDown(self): + pygame.display.quit() + mixer.quit() + + def test_get_endevent(self): + """Ensure Channel.get_endevent() returns the correct event type.""" + channel = mixer.Channel(0) + sound = mixer.Sound(example_path("data/house_lo.wav")) + channel.play(sound) + + # Set the end event for the channel. + END_EVENT = pygame.USEREVENT + 1 + channel.set_endevent(END_EVENT) + got_end_event = channel.get_endevent() + self.assertEqual(got_end_event, END_EVENT) + + # Wait for the sound to finish playing. + channel.stop() + while channel.get_busy(): + pygame.time.wait(10) + + # Check that the end event was sent. + events = pygame.event.get(got_end_event) + self.assertTrue(len(events) > 0) + + +############################### SOUND CLASS TESTS ############################## + + +class TestSoundPlay(unittest.TestCase): + def setUp(self): + mixer.init() + self.filename = example_path(os.path.join("data", "house_lo.wav")) + self.sound = mixer.Sound(file=self.filename) + + def tearDown(self): + mixer.quit() + + def test_play_once(self): + """Test playing a sound once.""" + channel = self.sound.play() + self.assertIsInstance(channel, pygame.mixer.Channel) + self.assertTrue(channel.get_busy()) + + def test_play_multiple_times(self): + """Test playing a sound multiple times.""" + + # create a sound 100ms long + frequency, format, channels = mixer.get_init() + sound_length_in_ms = 100 + bytes_per_ms = int((frequency / 1000) * channels * (abs(format) // 8)) + sound = mixer.Sound(b"\x00" * int(sound_length_in_ms * bytes_per_ms)) + + self.assertAlmostEqual( + sound.get_length(), sound_length_in_ms / 1000.0, places=2 + ) + + num_loops = 5 + channel = sound.play(loops=num_loops) + self.assertIsInstance(channel, pygame.mixer.Channel) + + # the sound should be playing + pygame.time.wait((sound_length_in_ms * num_loops) - 100) + self.assertTrue(channel.get_busy()) + + # the sound should not be playing anymore + pygame.time.wait(sound_length_in_ms + 200) + self.assertFalse(channel.get_busy()) + + def test_play_indefinitely(self): + """Test playing a sound indefinitely.""" + frequency, format, channels = mixer.get_init() + sound_length_in_ms = 100 + bytes_per_ms = int((frequency / 1000) * channels * (abs(format) // 8)) + sound = mixer.Sound(b"\x00" * int(sound_length_in_ms * bytes_per_ms)) + + channel = sound.play(loops=-1) + self.assertIsInstance(channel, pygame.mixer.Channel) + + # we can't wait forever... so we wait 2 loops + for _ in range(2): + self.assertTrue(channel.get_busy()) + pygame.time.wait(sound_length_in_ms) + + def test_play_with_maxtime(self): + """Test playing a sound with maxtime.""" + channel = self.sound.play(maxtime=200) + self.assertIsInstance(channel, pygame.mixer.Channel) + self.assertTrue(channel.get_busy()) + pygame.time.wait(200 + 50) + self.assertFalse(channel.get_busy()) + + def test_play_with_fade_ms(self): + """Test playing a sound with fade_ms.""" + channel = self.sound.play(fade_ms=500) + self.assertIsInstance(channel, pygame.mixer.Channel) + self.assertTrue(channel.get_busy()) + pygame.time.wait(250) + + self.assertGreater(channel.get_volume(), 0.3) + self.assertLess(channel.get_volume(), 0.80) + + pygame.time.wait(300) + self.assertEqual(channel.get_volume(), 1.0) + + def test_play_with_invalid_loops(self): + """Test playing a sound with invalid loops.""" + with self.assertRaises(TypeError): + self.sound.play(loops="invalid") + + def test_play_with_invalid_maxtime(self): + """Test playing a sound with invalid maxtime.""" + with self.assertRaises(TypeError): + self.sound.play(maxtime="invalid") + + def test_play_with_invalid_fade_ms(self): + """Test playing a sound with invalid fade_ms.""" + with self.assertRaises(TypeError): + self.sound.play(fade_ms="invalid") + + +class SoundTypeTest(unittest.TestCase): + @classmethod + def tearDownClass(cls): + mixer.quit() + + def setUp(cls): + # This makes sure the mixer is always initialized before each test (in + # case a test calls pygame.mixer.quit()). + if mixer.get_init() is None: + mixer.init() + + # See MixerModuleTest's methods test_sound_args(), test_sound_unicode(), + # and test_array_keyword() for additional testing of Sound() creation. + def test_sound(self): + """Ensure Sound() creation with a filename works.""" + filename = example_path(os.path.join("data", "house_lo.wav")) + sound1 = mixer.Sound(filename) + sound2 = mixer.Sound(file=filename) + + self.assertIsInstance(sound1, mixer.Sound) + self.assertIsInstance(sound2, mixer.Sound) + + def test_sound__from_file_object(self): + """Ensure Sound() creation with a file object works.""" + filename = example_path(os.path.join("data", "house_lo.wav")) + + # Using 'with' ensures the file is closed even if test fails. + with open(filename, "rb") as file_obj: + sound = mixer.Sound(file_obj) + + self.assertIsInstance(sound, mixer.Sound) + + def test_sound__from_sound_object(self): + """Ensure Sound() creation with a Sound() object works.""" + filename = example_path(os.path.join("data", "house_lo.wav")) + sound_obj = mixer.Sound(file=filename) + + sound = mixer.Sound(sound_obj) + + self.assertIsInstance(sound, mixer.Sound) + + def test_sound__from_pathlib(self): + """Ensure Sound() creation with a pathlib.Path object works.""" + path = pathlib.Path(example_path(os.path.join("data", "house_lo.wav"))) + sound1 = mixer.Sound(path) + sound2 = mixer.Sound(file=path) + self.assertIsInstance(sound1, mixer.Sound) + self.assertIsInstance(sound2, mixer.Sound) + + def todo_test_sound__from_buffer(self): + """Ensure Sound() creation with a buffer works.""" + self.fail() + + def test_sound__from_array(self): + """Ensure Sound() creation with an array works.""" + array1 = array('u', example_path(os.path.join("data", "house_lo.wav"))) + sound1 = mixer.Sound(array1) + self.assertIsInstance(sound1, mixer.Sound) + + def test_sound__without_arg(self): + """Ensure exception raised for Sound() creation with no argument.""" + with self.assertRaises(TypeError): + mixer.Sound() + + def test_sound__before_init(self): + """Ensure exception raised for Sound() creation with non-init mixer.""" + mixer.quit() + filename = example_path(os.path.join("data", "house_lo.wav")) + + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + mixer.Sound(file=filename) + + @unittest.skipIf(IS_PYPY, "pypy skip") + def test_samples_address(self): + """Test the _samples_address getter.""" + try: + from ctypes import pythonapi, c_void_p, py_object + + Bytes_FromString = pythonapi.PyBytes_FromString + + Bytes_FromString.restype = c_void_p + Bytes_FromString.argtypes = [py_object] + samples = b"abcdefgh" # keep byte size a multiple of 4 + sample_bytes = Bytes_FromString(samples) + + snd = mixer.Sound(buffer=samples) + + self.assertNotEqual(snd._samples_address, sample_bytes) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + snd._samples_address + + def test_get_length(self): + """Tests if get_length returns a correct length.""" + try: + for size in SIZES: + pygame.mixer.quit() + pygame.mixer.init(size=size) + filename = example_path(os.path.join("data", "punch.wav")) + sound = mixer.Sound(file=filename) + # The sound data is in the mixer output format. So dividing the + # length of the raw sound data by the mixer settings gives + # the expected length of the sound. + sound_bytes = sound.get_raw() + mix_freq, mix_bits, mix_channels = pygame.mixer.get_init() + mix_bytes = abs(mix_bits) / 8 + expected_length = ( + float(len(sound_bytes)) / mix_freq / mix_bytes / mix_channels + ) + self.assertAlmostEqual(expected_length, sound.get_length()) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + sound.get_length() + + def test_get_num_channels(self): + """ + Tests if Sound.get_num_channels returns the correct number + of channels playing a specific sound. + """ + try: + filename = example_path(os.path.join("data", "house_lo.wav")) + sound = mixer.Sound(file=filename) + + self.assertEqual(sound.get_num_channels(), 0) + sound.play() + self.assertEqual(sound.get_num_channels(), 1) + sound.play() + self.assertEqual(sound.get_num_channels(), 2) + sound.stop() + self.assertEqual(sound.get_num_channels(), 0) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + sound.get_num_channels() + + def test_get_volume(self): + """Ensure a sound's volume can be retrieved.""" + try: + expected_volume = 1.0 # default + filename = example_path(os.path.join("data", "house_lo.wav")) + sound = mixer.Sound(file=filename) + + volume = sound.get_volume() + + self.assertAlmostEqual(volume, expected_volume) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + sound.get_volume() + + def test_get_volume__while_playing(self): + """Ensure a sound's volume can be retrieved while playing.""" + try: + expected_volume = 1.0 # default + filename = example_path(os.path.join("data", "house_lo.wav")) + sound = mixer.Sound(file=filename) + sound.play(-1) + + volume = sound.get_volume() + + self.assertAlmostEqual(volume, expected_volume) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + sound.get_volume() + + def test_set_volume(self): + """Ensure a sound's volume can be set.""" + try: + float_delta = 1.0 / 128 # SDL volume range is 0 to 128 + filename = example_path(os.path.join("data", "house_lo.wav")) + sound = mixer.Sound(file=filename) + current_volume = sound.get_volume() + + # (volume_set_value : expected_volume) + volumes = ( + (-1, current_volume), # value < 0 won't change volume + (0, 0.0), + (0.01, 0.01), + (0.1, 0.1), + (0.5, 0.5), + (0.9, 0.9), + (0.99, 0.99), + (1, 1.0), + (1.1, 1.0), + (2.0, 1.0), + ) + + for volume_set_value, expected_volume in volumes: + sound.set_volume(volume_set_value) + + self.assertAlmostEqual( + sound.get_volume(), expected_volume, delta=float_delta + ) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + sound.set_volume(1) + + def test_set_volume__while_playing(self): + """Ensure a sound's volume can be set while playing.""" + try: + float_delta = 1.0 / 128 # SDL volume range is 0 to 128 + filename = example_path(os.path.join("data", "house_lo.wav")) + sound = mixer.Sound(file=filename) + current_volume = sound.get_volume() + + # (volume_set_value : expected_volume) + volumes = ( + (-1, current_volume), # value < 0 won't change volume + (0, 0.0), + (0.01, 0.01), + (0.1, 0.1), + (0.5, 0.5), + (0.9, 0.9), + (0.99, 0.99), + (1, 1.0), + (1.1, 1.0), + (2.0, 1.0), + ) + + sound.play(loops=-1) + for volume_set_value, expected_volume in volumes: + sound.set_volume(volume_set_value) + + self.assertAlmostEqual( + sound.get_volume(), expected_volume, delta=float_delta + ) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + sound.set_volume(1) + + def test_stop(self): + """Ensure stop can be called while not playing a sound.""" + try: + expected_channels = 0 + filename = example_path(os.path.join("data", "house_lo.wav")) + sound = mixer.Sound(file=filename) + + sound.stop() + + self.assertEqual(sound.get_num_channels(), expected_channels) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + sound.stop() + + def test_stop__while_playing(self): + """Ensure stop stops a playing sound.""" + try: + expected_channels = 0 + filename = example_path(os.path.join("data", "house_lo.wav")) + sound = mixer.Sound(file=filename) + + sound.play(-1) + sound.stop() + + self.assertEqual(sound.get_num_channels(), expected_channels) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + sound.stop() + + def test_get_raw(self): + """Ensure get_raw returns the correct bytestring.""" + try: + samples = b"abcdefgh" # keep byte size a multiple of 4 + snd = mixer.Sound(buffer=samples) + + raw = snd.get_raw() + + self.assertIsInstance(raw, bytes) + self.assertEqual(raw, samples) + finally: + pygame.mixer.quit() + with self.assertRaisesRegex(pygame.error, "mixer not initialized"): + snd.get_raw() + + def test_correct_subclassing(self): + class CorrectSublass(mixer.Sound): + def __init__(self, file): + super().__init__(file=file) + + filename = example_path(os.path.join("data", "house_lo.wav")) + correct = CorrectSublass(filename) + + try: + correct.get_volume() + except Exception: + self.fail("This should not raise an exception.") + + def test_incorrect_subclassing(self): + class IncorrectSuclass(mixer.Sound): + def __init__(self): + pass + + incorrect = IncorrectSuclass() + + self.assertRaises(RuntimeError, incorrect.get_volume) + + +class TestSoundFadeout(unittest.TestCase): + def setUp(self): + if mixer.get_init() is None: + pygame.mixer.init() + + def tearDown(self): + pygame.mixer.quit() + + def test_fadeout_with_valid_time(self): + """Tests if fadeout stops sound playback after fading it out over the time argument in milliseconds.""" + filename = example_path(os.path.join("data", "punch.wav")) + sound = mixer.Sound(file=filename) + channel = sound.play() + channel.fadeout(1000) + pygame.time.wait(2000) + self.assertFalse(channel.get_busy()) + + # TODO: this fails. + # def test_fadeout_with_zero_time(self): + # """Tests if fadeout stops sound playback immediately when time argument is zero.""" + # filename = example_path(os.path.join("data", "punch.wav")) + # sound = mixer.Sound(file=filename) + # channel = sound.play() + # channel.fadeout(0) + # self.assertFalse(channel.get_busy()) + + # TODO: this fails. + # def test_fadeout_with_negative_time(self): + # """Tests if fadeout stops sound playback immediately when time argument is negative.""" + # filename = example_path(os.path.join("data", "punch.wav")) + # sound = mixer.Sound(file=filename) + # channel = sound.play() + # channel.fadeout(-1000) + # self.assertFalse(channel.get_busy()) + + # TODO: What should happen here? + # def test_fadeout_with_large_time(self): + # """Tests if fadeout stops sound playback after fading it out over the time argument in milliseconds, even if time is larger than the sound length.""" + # filename = example_path(os.path.join("data", "punch.wav")) + # sound = mixer.Sound(file=filename) + # channel = sound.play() + # channel.fadeout(...?) + # pygame.time.wait(...?) + # self.assertFalse(channel.get_busy()) + + +class TestGetBusy(unittest.TestCase): + """Test pygame.mixer.get_busy. + + |tags:slow| + """ + + def setUp(self): + pygame.mixer.init() + + def tearDown(self): + pygame.mixer.quit() + + def test_no_sound_playing(self): + """ + Test that get_busy returns False when no sound is playing. + """ + self.assertFalse(pygame.mixer.get_busy()) + + def test_one_sound_playing(self): + """ + Test that get_busy returns True when one sound is playing. + """ + sound = pygame.mixer.Sound(example_path("data/house_lo.wav")) + sound.play() + time.sleep(0.2) + self.assertTrue(pygame.mixer.get_busy()) + sound.stop() + + def test_multiple_sounds_playing(self): + """ + Test that get_busy returns True when multiple sounds are playing. + """ + sound1 = pygame.mixer.Sound(example_path("data/house_lo.wav")) + sound2 = pygame.mixer.Sound(example_path("data/house_lo.wav")) + sound1.play() + sound2.play() + time.sleep(0.2) + self.assertTrue(pygame.mixer.get_busy()) + sound1.stop() + sound2.stop() + + def test_all_sounds_stopped(self): + """ + Test that get_busy returns False when all sounds are stopped. + """ + sound1 = pygame.mixer.Sound(example_path("data/house_lo.wav")) + sound2 = pygame.mixer.Sound(example_path("data/house_lo.wav")) + sound1.play() + sound2.play() + time.sleep(0.2) + sound1.stop() + sound2.stop() + time.sleep(0.2) + self.assertFalse(pygame.mixer.get_busy()) + + def test_all_sounds_stopped_with_fadeout(self): + """ + Test that get_busy returns False when all sounds are stopped with + fadeout. + """ + sound1 = pygame.mixer.Sound(example_path("data/house_lo.wav")) + sound2 = pygame.mixer.Sound(example_path("data/house_lo.wav")) + sound1.play() + sound2.play() + time.sleep(0.2) + sound1.fadeout(100) + sound2.fadeout(100) + time.sleep(0.3) + self.assertFalse(pygame.mixer.get_busy()) + + def test_sound_fading_out(self): + """Tests that get_busy() returns True when a sound is fading out""" + sound = pygame.mixer.Sound(example_path("data/house_lo.wav")) + sound.play(fade_ms=1000) + time.sleep(1.1) + self.assertTrue(pygame.mixer.get_busy()) + sound.stop() + + +##################################### MAIN ##################################### + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/mouse_test.py b/laplas/abstract_map/pygame/tests/mouse_test.py new file mode 100644 index 0000000..53eac3a --- /dev/null +++ b/laplas/abstract_map/pygame/tests/mouse_test.py @@ -0,0 +1,348 @@ +import unittest +import os +import platform +import warnings +import pygame + + +DARWIN = "Darwin" in platform.platform() + + +class MouseTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + # The display needs to be initialized for mouse functions. + pygame.display.init() + + @classmethod + def tearDownClass(cls): + pygame.display.quit() + + +class MouseModuleInteractiveTest(MouseTests): + __tags__ = ["interactive"] + + def test_set_pos(self): + """Ensures set_pos works correctly. + Requires tester to move the mouse to be on the window. + """ + pygame.display.set_mode((500, 500)) + pygame.event.get() # Pump event queue to make window get focus on macos. + + if not pygame.mouse.get_focused(): + # The window needs to be focused for the mouse.set_pos to work on macos. + return + clock = pygame.time.Clock() + + expected_pos = ((10, 0), (0, 0), (499, 0), (499, 499), (341, 143), (94, 49)) + + for x, y in expected_pos: + pygame.mouse.set_pos(x, y) + pygame.event.get() + found_pos = pygame.mouse.get_pos() + + clock.tick() + time_passed = 0.0 + ready_to_test = False + + while not ready_to_test and time_passed <= 1000.0: # Avoid endless loop + time_passed += clock.tick() + for event in pygame.event.get(): + if event.type == pygame.MOUSEMOTION: + ready_to_test = True + + self.assertEqual(found_pos, (x, y)) + + +class MouseModuleTest(MouseTests): + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER", "") == "dummy", + "Cursors not supported on headless test machines", + ) + def test_get_cursor(self): + """Ensures get_cursor works correctly.""" + + # error should be raised when the display is uninitialized + with self.assertRaises(pygame.error): + pygame.display.quit() + pygame.mouse.get_cursor() + + pygame.display.init() + + size = (8, 8) + hotspot = (0, 0) + xormask = (0, 96, 120, 126, 112, 96, 0, 0) + andmask = (224, 240, 254, 255, 254, 240, 96, 0) + + expected_length = 4 + expected_cursor = pygame.cursors.Cursor(size, hotspot, xormask, andmask) + pygame.mouse.set_cursor(expected_cursor) + + try: + cursor = pygame.mouse.get_cursor() + + self.assertIsInstance(cursor, pygame.cursors.Cursor) + self.assertEqual(len(cursor), expected_length) + + for info in cursor: + self.assertIsInstance(info, tuple) + + pygame.mouse.set_cursor(size, hotspot, xormask, andmask) + self.assertEqual(pygame.mouse.get_cursor(), expected_cursor) + + # SDLError should be raised when the mouse cursor is NULL + except pygame.error: + with self.assertRaises(pygame.error): + pygame.mouse.get_cursor() + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER", "") == "dummy", + "mouse.set_system_cursor only available in SDL2", + ) + def test_set_system_cursor(self): + """Ensures set_system_cursor works correctly.""" + + with warnings.catch_warnings(record=True) as w: + """From Pygame 2.0.1, set_system_cursor() should raise a deprecation warning""" + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + + # Error should be raised when the display is uninitialized + with self.assertRaises(pygame.error): + pygame.display.quit() + pygame.mouse.set_system_cursor(pygame.SYSTEM_CURSOR_HAND) + + pygame.display.init() + + # TypeError raised when PyArg_ParseTuple fails to parse parameters + with self.assertRaises(TypeError): + pygame.mouse.set_system_cursor("b") + with self.assertRaises(TypeError): + pygame.mouse.set_system_cursor(None) + with self.assertRaises(TypeError): + pygame.mouse.set_system_cursor((8, 8), (0, 0)) + + # Right type, invalid value + with self.assertRaises(pygame.error): + pygame.mouse.set_system_cursor(2000) + + # Working as intended + self.assertEqual( + pygame.mouse.set_system_cursor(pygame.SYSTEM_CURSOR_ARROW), None + ) + + # Making sure the warnings are working properly + self.assertEqual(len(w), 6) + self.assertTrue( + all(issubclass(warn.category, DeprecationWarning) for warn in w) + ) + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER", "") == "dummy", + "Cursors not supported on headless test machines", + ) + def test_set_cursor(self): + """Ensures set_cursor works correctly.""" + + # Bitmap cursor information + size = (8, 8) + hotspot = (0, 0) + xormask = (0, 126, 64, 64, 32, 16, 0, 0) + andmask = (254, 255, 254, 112, 56, 28, 12, 0) + bitmap_cursor = pygame.cursors.Cursor(size, hotspot, xormask, andmask) + + # System cursor information + constant = pygame.SYSTEM_CURSOR_ARROW + system_cursor = pygame.cursors.Cursor(constant) + + # Color cursor information (also uses hotspot variable from Bitmap cursor info) + surface = pygame.Surface((10, 10)) + color_cursor = pygame.cursors.Cursor(hotspot, surface) + + pygame.display.quit() + + # Bitmap: Error should be raised when the display is uninitialized + with self.assertRaises(pygame.error): + pygame.mouse.set_cursor(bitmap_cursor) + + # System: Error should be raised when the display is uninitialized + with self.assertRaises(pygame.error): + pygame.mouse.set_cursor(system_cursor) + + # Color: Error should be raised when the display is uninitialized + with self.assertRaises(pygame.error): + pygame.mouse.set_cursor(color_cursor) + + pygame.display.init() + + # Bitmap: TypeError raised when PyArg_ParseTuple fails to parse parameters + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(("w", "h"), hotspot, xormask, andmask) + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(size, ("0", "0"), xormask, andmask) + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(size, ("x", "y", "z"), xormask, andmask) + + # Bitmap: TypeError raised when either mask is not a sequence + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(size, hotspot, 12345678, andmask) + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(size, hotspot, xormask, 12345678) + + # Bitmap: TypeError raised when element of mask is not an integer + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(size, hotspot, "00000000", andmask) + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(size, hotspot, xormask, (2, [0], 4, 0, 0, 8, 0, 1)) + + # Bitmap: ValueError raised when width not divisible by 8 + with self.assertRaises(ValueError): + pygame.mouse.set_cursor((3, 8), hotspot, xormask, andmask) + + # Bitmap: ValueError raised when length of either mask != width * height / 8 + with self.assertRaises(ValueError): + pygame.mouse.set_cursor((16, 2), hotspot, (128, 64, 32), andmask) + with self.assertRaises(ValueError): + pygame.mouse.set_cursor((16, 2), hotspot, xormask, (192, 96, 48, 0, 1)) + + # Bitmap: Working as intended + self.assertEqual( + pygame.mouse.set_cursor((16, 1), hotspot, (8, 0), (0, 192)), None + ) + pygame.mouse.set_cursor(size, hotspot, xormask, andmask) + self.assertEqual(pygame.mouse.get_cursor(), bitmap_cursor) + + # Bitmap: Working as intended + lists + masks with no references + pygame.mouse.set_cursor(size, hotspot, list(xormask), list(andmask)) + self.assertEqual(pygame.mouse.get_cursor(), bitmap_cursor) + + # System: TypeError raised when constant is invalid + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(-50021232) + with self.assertRaises(TypeError): + pygame.mouse.set_cursor("yellow") + + # System: Working as intended + self.assertEqual(pygame.mouse.set_cursor(constant), None) + pygame.mouse.set_cursor(constant) + self.assertEqual(pygame.mouse.get_cursor(), system_cursor) + pygame.mouse.set_cursor(system_cursor) + self.assertEqual(pygame.mouse.get_cursor(), system_cursor) + + # Color: TypeError raised with invalid parameters + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(("x", "y"), surface) + with self.assertRaises(TypeError): + pygame.mouse.set_cursor(hotspot, "not_a_surface") + + # Color: Working as intended + self.assertEqual(pygame.mouse.set_cursor(hotspot, surface), None) + pygame.mouse.set_cursor(hotspot, surface) + self.assertEqual(pygame.mouse.get_cursor(), color_cursor) + pygame.mouse.set_cursor(color_cursor) + self.assertEqual(pygame.mouse.get_cursor(), color_cursor) + + # Color: Working as intended + Surface with no references is returned okay + pygame.mouse.set_cursor((0, 0), pygame.Surface((20, 20))) + cursor = pygame.mouse.get_cursor() + self.assertEqual(cursor.type, "color") + self.assertEqual(cursor.data[0], (0, 0)) + self.assertEqual(cursor.data[1].get_size(), (20, 20)) + + def test_get_focused(self): + """Ensures get_focused returns the correct type.""" + focused = pygame.mouse.get_focused() + + self.assertIsInstance(focused, int) + + def test_get_pressed(self): + """Ensures get_pressed returns the correct types.""" + expected_length = 3 + buttons_pressed = pygame.mouse.get_pressed() + self.assertIsInstance(buttons_pressed, tuple) + self.assertEqual(len(buttons_pressed), expected_length) + for value in buttons_pressed: + self.assertIsInstance(value, bool) + + expected_length = 5 + buttons_pressed = pygame.mouse.get_pressed(num_buttons=5) + self.assertIsInstance(buttons_pressed, tuple) + self.assertEqual(len(buttons_pressed), expected_length) + for value in buttons_pressed: + self.assertIsInstance(value, bool) + + expected_length = 3 + buttons_pressed = pygame.mouse.get_pressed(3) + self.assertIsInstance(buttons_pressed, tuple) + self.assertEqual(len(buttons_pressed), expected_length) + for value in buttons_pressed: + self.assertIsInstance(value, bool) + + expected_length = 5 + buttons_pressed = pygame.mouse.get_pressed(5) + self.assertIsInstance(buttons_pressed, tuple) + self.assertEqual(len(buttons_pressed), expected_length) + for value in buttons_pressed: + self.assertIsInstance(value, bool) + + with self.assertRaises(ValueError): + pygame.mouse.get_pressed(4) + + def test_get_pos(self): + """Ensures get_pos returns the correct types.""" + expected_length = 2 + + pos = pygame.mouse.get_pos() + + self.assertIsInstance(pos, tuple) + self.assertEqual(len(pos), expected_length) + for value in pos: + self.assertIsInstance(value, int) + + def test_set_pos__invalid_pos(self): + """Ensures set_pos handles invalid positions correctly.""" + for invalid_pos in ((1,), [1, 2, 3], 1, "1", (1, "1"), []): + with self.assertRaises(TypeError): + pygame.mouse.set_pos(invalid_pos) + + def test_get_rel(self): + """Ensures get_rel returns the correct types.""" + expected_length = 2 + + rel = pygame.mouse.get_rel() + + self.assertIsInstance(rel, tuple) + self.assertEqual(len(rel), expected_length) + for value in rel: + self.assertIsInstance(value, int) + + def test_get_visible(self): + """Ensures get_visible works correctly.""" + for expected_value in (False, True): + pygame.mouse.set_visible(expected_value) + + visible = pygame.mouse.get_visible() + + self.assertEqual(visible, expected_value) + + def test_set_visible(self): + """Ensures set_visible returns the correct values.""" + # Set to a known state. + pygame.mouse.set_visible(True) + + for expected_visible in (False, True): + prev_visible = pygame.mouse.set_visible(expected_visible) + + self.assertEqual(prev_visible, not expected_visible) + + def test_set_visible__invalid_value(self): + """Ensures set_visible handles invalid positions correctly.""" + for invalid_value in ((1,), [1, 2, 3], 1.1, "1", (1, "1"), []): + with self.assertRaises(TypeError): + prev_visible = pygame.mouse.set_visible(invalid_value) + + +################################################################################ + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/pixelarray_test.py b/laplas/abstract_map/pygame/tests/pixelarray_test.py new file mode 100644 index 0000000..7b1cf42 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/pixelarray_test.py @@ -0,0 +1,1667 @@ +import gc +import operator +import platform +import sys +import unittest +import weakref +from functools import reduce + +from pygame.tests.test_utils import SurfaceSubclass + +try: + from pygame.tests.test_utils import arrinter +except NameError: + pass + +import pygame + + +IS_PYPY = "PyPy" == platform.python_implementation() + + +class TestMixin: + def assert_surfaces_equal(self, s1, s2, msg=None): + """Checks if two surfaces are equal in size and color.""" + w, h = s1.get_size() + + self.assertTupleEqual((w, h), s2.get_size(), msg) + + msg = "" if msg is None else f"{msg}, " + msg += f"size: ({w}, {h})" + + for x in range(w): + for y in range(h): + self.assertEqual( + s1.get_at((x, y)), + s2.get_at((x, y)), + f"{msg}, position: ({x}, {y})", + ) + + def assert_surface_filled(self, surface, expected_color, msg=None): + """Checks if the surface is filled with the given color.""" + width, height = surface.get_size() + + surface.lock() # Lock for possible speed up. + for pos in ((x, y) for y in range(height) for x in range(width)): + self.assertEqual(surface.get_at(pos), expected_color, msg) + surface.unlock() + + +@unittest.skipIf(IS_PYPY, "pypy having issues") +class PixelArrayTypeTest(unittest.TestCase, TestMixin): + def test_compare(self): + # __doc__ (as of 2008-06-25) for pygame.pixelarray.PixelArray.compare: + + # PixelArray.compare (array, distance=0, weights=(0.299, 0.587, 0.114)): Return PixelArray + # Compares the PixelArray with another one. + + w = 10 + h = 20 + size = w, h + sf = pygame.Surface(size, 0, 32) + ar = pygame.PixelArray(sf) + sf2 = pygame.Surface(size, 0, 32) + self.assertRaises(TypeError, ar.compare, sf2) + ar2 = pygame.PixelArray(sf2) + ar3 = ar.compare(ar2) + self.assertTrue(isinstance(ar3, pygame.PixelArray)) + self.assertEqual(ar3.shape, size) + sf2.fill(pygame.Color("white")) + self.assert_surfaces_equal(sf2, ar3.surface) + del ar3 + r = pygame.Rect(2, 5, 6, 13) + sf.fill(pygame.Color("blue"), r) + sf2.fill(pygame.Color("red")) + sf2.fill(pygame.Color("blue"), r) + ar3 = ar.compare(ar2) + sf.fill(pygame.Color("white"), r) + self.assert_surfaces_equal(sf, ar3.surface) + + # FINISH ME! + # Test other bit depths, slices, and distance != 0. + + def test_compare__same_colors_within_distance(self): + """Ensures compare works correctly with same colored surfaces.""" + size = (3, 5) + pixelarray_result_color = pygame.Color("white") + surface_color = (127, 127, 127, 255) + + for depth in (8, 16, 24, 32): + expected_pixelarray_surface = pygame.Surface(size, depth=depth) + expected_pixelarray_surface.fill(pixelarray_result_color) + + # Copy the surface to ensure same dimensions/formatting. + surf_a = expected_pixelarray_surface.copy() + surf_a.fill(surface_color) + # For non-32 bit depths, the actual color can be different from what + # was filled. + expected_surface_color = surf_a.get_at((0, 0)) + + pixelarray_a = pygame.PixelArray(surf_a) + pixelarray_b = pygame.PixelArray(surf_a.copy()) + + for distance in (0.0, 0.01, 0.1, 1.0): + pixelarray_result = pixelarray_a.compare( + pixelarray_b, distance=distance + ) + + # Ensure the resulting pixelarray is correct and that the original + # surfaces were not changed. + self.assert_surfaces_equal( + pixelarray_result.surface, + expected_pixelarray_surface, + (depth, distance), + ) + self.assert_surface_filled( + pixelarray_a.surface, expected_surface_color, (depth, distance) + ) + self.assert_surface_filled( + pixelarray_b.surface, expected_surface_color, (depth, distance) + ) + + pixelarray_a.close() + pixelarray_b.close() + pixelarray_result.close() + + def test_compare__different_colors_within_distance(self): + """Ensures compare works correctly with different colored surfaces + and the color difference is within the given distance. + """ + size = (3, 5) + pixelarray_result_color = pygame.Color("white") + surface_a_color = (127, 127, 127, 255) + surface_b_color = (128, 127, 127, 255) + + for depth in (8, 16, 24, 32): + expected_pixelarray_surface = pygame.Surface(size, depth=depth) + expected_pixelarray_surface.fill(pixelarray_result_color) + + # Copy the surface to ensure same dimensions/formatting. + surf_a = expected_pixelarray_surface.copy() + surf_a.fill(surface_a_color) + # For non-32 bit depths, the actual color can be different from what + # was filled. + expected_surface_a_color = surf_a.get_at((0, 0)) + pixelarray_a = pygame.PixelArray(surf_a) + + surf_b = expected_pixelarray_surface.copy() + surf_b.fill(surface_b_color) + # For non-32 bit depths, the actual color can be different from what + # was filled. + expected_surface_b_color = surf_b.get_at((0, 0)) + pixelarray_b = pygame.PixelArray(surf_b) + + for distance in (0.2, 0.3, 0.5, 1.0): + pixelarray_result = pixelarray_a.compare( + pixelarray_b, distance=distance + ) + + # Ensure the resulting pixelarray is correct and that the original + # surfaces were not changed. + self.assert_surfaces_equal( + pixelarray_result.surface, + expected_pixelarray_surface, + (depth, distance), + ) + self.assert_surface_filled( + pixelarray_a.surface, expected_surface_a_color, (depth, distance) + ) + self.assert_surface_filled( + pixelarray_b.surface, expected_surface_b_color, (depth, distance) + ) + + pixelarray_a.close() + pixelarray_b.close() + pixelarray_result.close() + + def test_compare__different_colors_not_within_distance(self): + """Ensures compare works correctly with different colored surfaces + and the color difference is not within the given distance. + """ + size = (3, 5) + pixelarray_result_color = pygame.Color("black") + surface_a_color = (127, 127, 127, 255) + surface_b_color = (128, 127, 127, 255) + + for depth in (8, 16, 24, 32): + expected_pixelarray_surface = pygame.Surface(size, depth=depth) + expected_pixelarray_surface.fill(pixelarray_result_color) + + # Copy the surface to ensure same dimensions/formatting. + surf_a = expected_pixelarray_surface.copy() + surf_a.fill(surface_a_color) + # For non-32 bit depths, the actual color can be different from what + # was filled. + expected_surface_a_color = surf_a.get_at((0, 0)) + pixelarray_a = pygame.PixelArray(surf_a) + + surf_b = expected_pixelarray_surface.copy() + surf_b.fill(surface_b_color) + # For non-32 bit depths, the actual color can be different from what + # was filled. + expected_surface_b_color = surf_b.get_at((0, 0)) + pixelarray_b = pygame.PixelArray(surf_b) + + for distance in (0.0, 0.00001, 0.0001, 0.001): + pixelarray_result = pixelarray_a.compare( + pixelarray_b, distance=distance + ) + + # Ensure the resulting pixelarray is correct and that the original + # surfaces were not changed. + self.assert_surfaces_equal( + pixelarray_result.surface, + expected_pixelarray_surface, + (depth, distance), + ) + self.assert_surface_filled( + pixelarray_a.surface, expected_surface_a_color, (depth, distance) + ) + self.assert_surface_filled( + pixelarray_b.surface, expected_surface_b_color, (depth, distance) + ) + + pixelarray_a.close() + pixelarray_b.close() + pixelarray_result.close() + + def test_close(self): + """does not crash when it is deleted.""" + s = pygame.Surface((10, 10)) + a = pygame.PixelArray(s) + a.close() + del a + + def test_close_raises(self): + """when you try to do an operation after it is closed.""" + s = pygame.Surface((10, 10)) + a = pygame.PixelArray(s) + a.close() + + def access_after(): + a[:] + + self.assertRaises(ValueError, access_after) + + def assign_all_after(): + a[:] = 1 + + self.assertRaises(ValueError, assign_all_after) + + def make_surface_after(): + a.make_surface() + + self.assertRaises(ValueError, make_surface_after) + + def iter_after(): + for x in a: + pass + + self.assertRaises(ValueError, iter_after) + + def close_after(): + a.close() + + self.assertRaises(ValueError, close_after) + + def surface_after(): + a.surface + + self.assertRaises(ValueError, surface_after) + + def itemsize_after(): + a.itemsize + + self.assertRaises(ValueError, itemsize_after) + + def transpose_after(): + a.transpose() + + self.assertRaises(ValueError, transpose_after) + + def test_context_manager(self): + """closes properly.""" + s = pygame.Surface((10, 10)) + with pygame.PixelArray(s) as a: + a[:] + + # Test pixel array write... will also catch refcount issues and + # segfault + with pygame.PixelArray(s) as a: + a[:] = pygame.Color("deepskyblue") + + def test_pixel_array(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((10, 20), 0, bpp) + sf.fill((0, 0, 0)) + ar = pygame.PixelArray(sf) + + self.assertEqual(ar._pixels_address, sf._pixels_address) + + if sf.mustlock(): + self.assertTrue(sf.get_locked()) + + self.assertEqual(len(ar), 10) + + del ar + if sf.mustlock(): + self.assertFalse(sf.get_locked()) + + def test_as_class(self): + # Check general new-style class freatures. + sf = pygame.Surface((2, 3), 0, 32) + ar = pygame.PixelArray(sf) + self.assertRaises(AttributeError, getattr, ar, "nonnative") + ar.nonnative = "value" + self.assertEqual(ar.nonnative, "value") + r = weakref.ref(ar) + self.assertTrue(r() is ar) + del ar + gc.collect() + self.assertTrue(r() is None) + + class C(pygame.PixelArray): + def __str__(self): + return "string (%i, %i)" % self.shape + + ar = C(sf) + self.assertEqual(str(ar), "string (2, 3)") + r = weakref.ref(ar) + self.assertTrue(r() is ar) + del ar + gc.collect() + self.assertTrue(r() is None) + + def test_pixelarray__subclassed_surface(self): + """Ensure the PixelArray constructor accepts subclassed surfaces.""" + surface = SurfaceSubclass((3, 5), 0, 32) + pixelarray = pygame.PixelArray(surface) + + self.assertIsInstance(pixelarray, pygame.PixelArray) + + # Sequence interfaces + def test_get_column(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((6, 8), 0, bpp) + sf.fill((0, 0, 255)) + val = sf.map_rgb((0, 0, 255)) + ar = pygame.PixelArray(sf) + + ar2 = ar.__getitem__(1) + self.assertEqual(len(ar2), 8) + self.assertEqual(ar2.__getitem__(0), val) + self.assertEqual(ar2.__getitem__(1), val) + self.assertEqual(ar2.__getitem__(2), val) + + ar2 = ar.__getitem__(-1) + self.assertEqual(len(ar2), 8) + self.assertEqual(ar2.__getitem__(0), val) + self.assertEqual(ar2.__getitem__(1), val) + self.assertEqual(ar2.__getitem__(2), val) + + @unittest.skipIf(IS_PYPY, "pypy malloc abort") + def test_get_pixel(self): + w = 10 + h = 20 + size = w, h + bg_color = (0, 0, 255) + fg_color_y = (0, 0, 128) + fg_color_x = (0, 0, 11) + for bpp in (8, 16, 24, 32): + sf = pygame.Surface(size, 0, bpp) + mapped_bg_color = sf.map_rgb(bg_color) + mapped_fg_color_y = sf.map_rgb(fg_color_y) + mapped_fg_color_x = sf.map_rgb(fg_color_x) + self.assertNotEqual( + mapped_fg_color_y, + mapped_bg_color, + "Unusable test colors for bpp %i" % (bpp,), + ) + self.assertNotEqual( + mapped_fg_color_x, + mapped_bg_color, + "Unusable test colors for bpp %i" % (bpp,), + ) + self.assertNotEqual( + mapped_fg_color_y, + mapped_fg_color_x, + "Unusable test colors for bpp %i" % (bpp,), + ) + sf.fill(bg_color) + + ar = pygame.PixelArray(sf) + + ar_y = ar.__getitem__(1) + for y in range(h): + ar2 = ar_y.__getitem__(y) + self.assertEqual( + ar2, + mapped_bg_color, + "ar[1][%i] == %i, mapped_bg_color == %i" + % (y, ar2, mapped_bg_color), + ) + + sf.set_at((1, y), fg_color_y) + ar2 = ar_y.__getitem__(y) + self.assertEqual( + ar2, + mapped_fg_color_y, + "ar[1][%i] == %i, mapped_fg_color_y == %i" + % (y, ar2, mapped_fg_color_y), + ) + + sf.set_at((1, 1), bg_color) + for x in range(w): + ar2 = ar.__getitem__(x).__getitem__(1) + self.assertEqual( + ar2, + mapped_bg_color, + "ar[%i][1] = %i, mapped_bg_color = %i" % (x, ar2, mapped_bg_color), + ) + sf.set_at((x, 1), fg_color_x) + ar2 = ar.__getitem__(x).__getitem__(1) + self.assertEqual( + ar2, + mapped_fg_color_x, + "ar[%i][1] = %i, mapped_fg_color_x = %i" + % (x, ar2, mapped_fg_color_x), + ) + + ar2 = ar.__getitem__(0).__getitem__(0) + self.assertEqual(ar2, mapped_bg_color, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(1).__getitem__(0) + self.assertEqual(ar2, mapped_fg_color_y, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(-4).__getitem__(1) + self.assertEqual(ar2, mapped_fg_color_x, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(-4).__getitem__(5) + self.assertEqual(ar2, mapped_bg_color, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(-4).__getitem__(0) + self.assertEqual(ar2, mapped_bg_color, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(-w + 1).__getitem__(0) + self.assertEqual(ar2, mapped_fg_color_y, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(-w).__getitem__(0) + self.assertEqual(ar2, mapped_bg_color, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(5).__getitem__(-4) + self.assertEqual(ar2, mapped_bg_color, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(5).__getitem__(-h + 1) + self.assertEqual(ar2, mapped_fg_color_x, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(5).__getitem__(-h) + self.assertEqual(ar2, mapped_bg_color, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(0).__getitem__(-h + 1) + self.assertEqual(ar2, mapped_fg_color_x, "bpp = %i" % (bpp,)) + + ar2 = ar.__getitem__(0).__getitem__(-h) + self.assertEqual(ar2, mapped_bg_color, "bpp = %i" % (bpp,)) + + def test_set_pixel(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((10, 20), 0, bpp) + sf.fill((0, 0, 0)) + ar = pygame.PixelArray(sf) + + ar.__getitem__(0).__setitem__(0, (0, 255, 0)) + self.assertEqual(ar[0][0], sf.map_rgb((0, 255, 0))) + + ar.__getitem__(1).__setitem__(1, (128, 128, 128)) + self.assertEqual(ar[1][1], sf.map_rgb((128, 128, 128))) + + ar.__getitem__(-1).__setitem__(-1, (128, 128, 128)) + self.assertEqual(ar[9][19], sf.map_rgb((128, 128, 128))) + + ar.__getitem__(-2).__setitem__(-2, (128, 128, 128)) + self.assertEqual(ar[8][-2], sf.map_rgb((128, 128, 128))) + + def test_set_column(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((6, 8), 0, bpp) + sf.fill((0, 0, 0)) + ar = pygame.PixelArray(sf) + + sf2 = pygame.Surface((6, 8), 0, bpp) + sf2.fill((0, 255, 255)) + ar2 = pygame.PixelArray(sf2) + + # Test single value assignment + ar.__setitem__(2, (128, 128, 128)) + self.assertEqual(ar[2][0], sf.map_rgb((128, 128, 128))) + self.assertEqual(ar[2][1], sf.map_rgb((128, 128, 128))) + + ar.__setitem__(-1, (0, 255, 255)) + self.assertEqual(ar[5][0], sf.map_rgb((0, 255, 255))) + self.assertEqual(ar[-1][1], sf.map_rgb((0, 255, 255))) + + ar.__setitem__(-2, (255, 255, 0)) + self.assertEqual(ar[4][0], sf.map_rgb((255, 255, 0))) + self.assertEqual(ar[-2][1], sf.map_rgb((255, 255, 0))) + + # Test list assignment. + ar.__setitem__(0, [(255, 255, 255)] * 8) + self.assertEqual(ar[0][0], sf.map_rgb((255, 255, 255))) + self.assertEqual(ar[0][1], sf.map_rgb((255, 255, 255))) + + # Test tuple assignment. + # Changed in Pygame 1.9.2 - Raises an exception. + self.assertRaises( + ValueError, + ar.__setitem__, + 1, + ( + (204, 0, 204), + (17, 17, 17), + (204, 0, 204), + (17, 17, 17), + (204, 0, 204), + (17, 17, 17), + (204, 0, 204), + (17, 17, 17), + ), + ) + + # Test pixel array assignment. + ar.__setitem__(1, ar2.__getitem__(3)) + self.assertEqual(ar[1][0], sf.map_rgb((0, 255, 255))) + self.assertEqual(ar[1][1], sf.map_rgb((0, 255, 255))) + + def test_get_slice(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((10, 20), 0, bpp) + sf.fill((0, 0, 0)) + ar = pygame.PixelArray(sf) + + self.assertEqual(len(ar[0:2]), 2) + self.assertEqual(len(ar[3:7][3]), 20) + + self.assertEqual(ar[0:0], None) + self.assertEqual(ar[5:5], None) + self.assertEqual(ar[9:9], None) + + # Has to resolve to ar[7:8] + self.assertEqual(len(ar[-3:-2]), 1) # 2D + self.assertEqual(len(ar[-3:-2][0]), 20) # 1D + + # Try assignments. + + # 2D assignment. + ar[2:5] = (255, 255, 255) + + # 1D assignment + ar[3][3:7] = (10, 10, 10) + self.assertEqual(ar[3][5], sf.map_rgb((10, 10, 10))) + self.assertEqual(ar[3][6], sf.map_rgb((10, 10, 10))) + + @unittest.skipIf(IS_PYPY, "skipping for PyPy (segfaults on mac pypy3 6.0.0)") + def test_contains(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((10, 20), 0, bpp) + sf.fill((0, 0, 0)) + sf.set_at((8, 8), (255, 255, 255)) + + ar = pygame.PixelArray(sf) + self.assertTrue((0, 0, 0) in ar) + self.assertTrue((255, 255, 255) in ar) + self.assertFalse((255, 255, 0) in ar) + self.assertFalse(0x0000FF in ar) + + # Test sliced array + self.assertTrue((0, 0, 0) in ar[8]) + self.assertTrue((255, 255, 255) in ar[8]) + self.assertFalse((255, 255, 0) in ar[8]) + self.assertFalse(0x0000FF in ar[8]) + + def test_get_surface(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((10, 20), 0, bpp) + sf.fill((0, 0, 0)) + ar = pygame.PixelArray(sf) + self.assertTrue(ar.surface is sf) + + def test_get_surface__subclassed_surface(self): + """Ensure the surface attribute can handle subclassed surfaces.""" + expected_surface = SurfaceSubclass((5, 3), 0, 32) + pixelarray = pygame.PixelArray(expected_surface) + + surface = pixelarray.surface + + self.assertIs(surface, expected_surface) + self.assertIsInstance(surface, pygame.Surface) + self.assertIsInstance(surface, SurfaceSubclass) + + def test_set_slice(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((6, 8), 0, bpp) + sf.fill((0, 0, 0)) + ar = pygame.PixelArray(sf) + + # Test single value assignment + val = sf.map_rgb((128, 128, 128)) + ar[0:2] = val + self.assertEqual(ar[0][0], val) + self.assertEqual(ar[0][1], val) + self.assertEqual(ar[1][0], val) + self.assertEqual(ar[1][1], val) + + val = sf.map_rgb((0, 255, 255)) + ar[-3:-1] = val + self.assertEqual(ar[3][0], val) + self.assertEqual(ar[-2][1], val) + + val = sf.map_rgb((255, 255, 255)) + ar[-3:] = (255, 255, 255) + self.assertEqual(ar[4][0], val) + self.assertEqual(ar[-1][1], val) + + # Test array size mismatch. + # Changed in ver. 1.9.2 + # (was "Test list assignment, this is a vertical assignment.") + val = sf.map_rgb((0, 255, 0)) + self.assertRaises(ValueError, ar.__setitem__, slice(2, 4), [val] * 8) + + # And the horizontal assignment. + val = sf.map_rgb((255, 0, 0)) + val2 = sf.map_rgb((128, 0, 255)) + ar[0:2] = [val, val2] + self.assertEqual(ar[0][0], val) + self.assertEqual(ar[1][0], val2) + self.assertEqual(ar[0][1], val) + self.assertEqual(ar[1][1], val2) + self.assertEqual(ar[0][4], val) + self.assertEqual(ar[1][4], val2) + self.assertEqual(ar[0][5], val) + self.assertEqual(ar[1][5], val2) + + # Test pixelarray assignment. + ar[:] = (0, 0, 0) + sf2 = pygame.Surface((6, 8), 0, bpp) + sf2.fill((255, 0, 255)) + + val = sf.map_rgb((255, 0, 255)) + ar2 = pygame.PixelArray(sf2) + + ar[:] = ar2[:] + self.assertEqual(ar[0][0], val) + self.assertEqual(ar[5][7], val) + + # Ensure p1 ... pn are freed for array[...] = [p1, ..., pn] + # Bug fix: reference counting. + if hasattr(sys, "getrefcount"): + + class Int(int): + """Unique int instances""" + + pass + + sf = pygame.Surface((5, 2), 0, 32) + ar = pygame.PixelArray(sf) + pixel_list = [Int(i) for i in range(ar.shape[0])] + refcnts_before = [sys.getrefcount(i) for i in pixel_list] + ar[...] = pixel_list + refcnts_after = [sys.getrefcount(i) for i in pixel_list] + gc.collect() + self.assertEqual(refcnts_after, refcnts_before) + + def test_subscript(self): + # By default we do not need to work with any special __***__ + # methods as map subscripts are the first looked up by the + # object system. + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((6, 8), 0, bpp) + sf.set_at((1, 3), (0, 255, 0)) + sf.set_at((0, 0), (0, 255, 0)) + sf.set_at((4, 4), (0, 255, 0)) + val = sf.map_rgb((0, 255, 0)) + + ar = pygame.PixelArray(sf) + + # Test single value requests. + self.assertEqual(ar[1, 3], val) + self.assertEqual(ar[0, 0], val) + self.assertEqual(ar[4, 4], val) + self.assertEqual(ar[1][3], val) + self.assertEqual(ar[0][0], val) + self.assertEqual(ar[4][4], val) + + # Test ellipse working. + self.assertEqual(len(ar[..., ...]), 6) + self.assertEqual(len(ar[1, ...]), 8) + self.assertEqual(len(ar[..., 3]), 6) + + # Test simple slicing + self.assertEqual(len(ar[:, :]), 6) + self.assertEqual( + len(ar[:,]), + 6, + ) + self.assertEqual(len(ar[1, :]), 8) + self.assertEqual(len(ar[:, 2]), 6) + # Empty slices + self.assertEqual( + ar[4:4,], + None, + ) + self.assertEqual(ar[4:4, ...], None) + self.assertEqual(ar[4:4, 2:2], None) + self.assertEqual(ar[4:4, 1:4], None) + self.assertEqual( + ar[4:4:2,], + None, + ) + self.assertEqual( + ar[4:4:-2,], + None, + ) + self.assertEqual(ar[4:4:1, ...], None) + self.assertEqual(ar[4:4:-1, ...], None) + self.assertEqual(ar[4:4:1, 2:2], None) + self.assertEqual(ar[4:4:-1, 1:4], None) + self.assertEqual(ar[..., 4:4], None) + self.assertEqual(ar[1:4, 4:4], None) + self.assertEqual(ar[..., 4:4:1], None) + self.assertEqual(ar[..., 4:4:-1], None) + self.assertEqual(ar[2:2, 4:4:1], None) + self.assertEqual(ar[1:4, 4:4:-1], None) + + # Test advanced slicing + ar[0] = 0 + ar[1] = 1 + ar[2] = 2 + ar[3] = 3 + ar[4] = 4 + ar[5] = 5 + + # We should receive something like [0,2,4] + self.assertEqual(ar[::2, 1][0], 0) + self.assertEqual(ar[::2, 1][1], 2) + self.assertEqual(ar[::2, 1][2], 4) + # We should receive something like [2,2,2] + self.assertEqual(ar[2, ::2][0], 2) + self.assertEqual(ar[2, ::2][1], 2) + self.assertEqual(ar[2, ::2][2], 2) + + # Should create a 3x3 array of [0,2,4] + ar2 = ar[::2, ::2] + self.assertEqual(len(ar2), 3) + self.assertEqual(ar2[0][0], 0) + self.assertEqual(ar2[0][1], 0) + self.assertEqual(ar2[0][2], 0) + self.assertEqual(ar2[2][0], 4) + self.assertEqual(ar2[2][1], 4) + self.assertEqual(ar2[2][2], 4) + self.assertEqual(ar2[1][0], 2) + self.assertEqual(ar2[2][0], 4) + self.assertEqual(ar2[1][1], 2) + + # Should create a reversed 3x8 array over X of [1,2,3] -> [3,2,1] + ar2 = ar[3:0:-1] + self.assertEqual(len(ar2), 3) + self.assertEqual(ar2[0][0], 3) + self.assertEqual(ar2[0][1], 3) + self.assertEqual(ar2[0][2], 3) + self.assertEqual(ar2[0][7], 3) + self.assertEqual(ar2[2][0], 1) + self.assertEqual(ar2[2][1], 1) + self.assertEqual(ar2[2][2], 1) + self.assertEqual(ar2[2][7], 1) + self.assertEqual(ar2[1][0], 2) + self.assertEqual(ar2[1][1], 2) + # Should completely reverse the array over X -> [5,4,3,2,1,0] + ar2 = ar[::-1] + self.assertEqual(len(ar2), 6) + self.assertEqual(ar2[0][0], 5) + self.assertEqual(ar2[0][1], 5) + self.assertEqual(ar2[0][3], 5) + self.assertEqual(ar2[0][-1], 5) + self.assertEqual(ar2[1][0], 4) + self.assertEqual(ar2[1][1], 4) + self.assertEqual(ar2[1][3], 4) + self.assertEqual(ar2[1][-1], 4) + self.assertEqual(ar2[-1][-1], 0) + self.assertEqual(ar2[-2][-2], 1) + self.assertEqual(ar2[-3][-1], 2) + + # Test advanced slicing + ar[:] = 0 + ar2 = ar[:, 1] + ar2[:] = [99] * len(ar2) + self.assertEqual(ar2[0], 99) + self.assertEqual(ar2[-1], 99) + self.assertEqual(ar2[-2], 99) + self.assertEqual(ar2[2], 99) + self.assertEqual(ar[0, 1], 99) + self.assertEqual(ar[1, 1], 99) + self.assertEqual(ar[2, 1], 99) + self.assertEqual(ar[-1, 1], 99) + self.assertEqual(ar[-2, 1], 99) + + # Cases where a 2d array should have a dimension of length 1. + ar2 = ar[1:2, :] + self.assertEqual(ar2.shape, (1, ar.shape[1])) + ar2 = ar[:, 1:2] + self.assertEqual(ar2.shape, (ar.shape[0], 1)) + sf2 = pygame.Surface((1, 5), 0, 32) + ar2 = pygame.PixelArray(sf2) + self.assertEqual(ar2.shape, sf2.get_size()) + sf2 = pygame.Surface((7, 1), 0, 32) + ar2 = pygame.PixelArray(sf2) + self.assertEqual(ar2.shape, sf2.get_size()) + + # Array has a single ellipsis subscript: the identity operator + ar2 = ar[...] + self.assertTrue(ar2 is ar) + + # Ensure x and y are freed for p = array[x, y] + # Bug fix: reference counting + if hasattr(sys, "getrefcount"): + + class Int(int): + """Unique int instances""" + + pass + + sf = pygame.Surface((2, 2), 0, 32) + ar = pygame.PixelArray(sf) + x, y = Int(0), Int(1) + rx_before, ry_before = sys.getrefcount(x), sys.getrefcount(y) + p = ar[x, y] + rx_after, ry_after = sys.getrefcount(x), sys.getrefcount(y) + self.assertEqual(rx_after, rx_before) + self.assertEqual(ry_after, ry_before) + + def test_ass_subscript(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((6, 8), 0, bpp) + sf.fill((255, 255, 255)) + ar = pygame.PixelArray(sf) + + # Test ellipse working + ar[..., ...] = (0, 0, 0) + self.assertEqual(ar[0, 0], 0) + self.assertEqual(ar[1, 0], 0) + self.assertEqual(ar[-1, -1], 0) + ar[...,] = (0, 0, 255) + self.assertEqual(ar[0, 0], sf.map_rgb((0, 0, 255))) + self.assertEqual(ar[1, 0], sf.map_rgb((0, 0, 255))) + self.assertEqual(ar[-1, -1], sf.map_rgb((0, 0, 255))) + ar[:, ...] = (255, 0, 0) + self.assertEqual(ar[0, 0], sf.map_rgb((255, 0, 0))) + self.assertEqual(ar[1, 0], sf.map_rgb((255, 0, 0))) + self.assertEqual(ar[-1, -1], sf.map_rgb((255, 0, 0))) + ar[...] = (0, 255, 0) + self.assertEqual(ar[0, 0], sf.map_rgb((0, 255, 0))) + self.assertEqual(ar[1, 0], sf.map_rgb((0, 255, 0))) + self.assertEqual(ar[-1, -1], sf.map_rgb((0, 255, 0))) + + # Ensure x and y are freed for array[x, y] = p + # Bug fix: reference counting + if hasattr(sys, "getrefcount"): + + class Int(int): + """Unique int instances""" + + pass + + sf = pygame.Surface((2, 2), 0, 32) + ar = pygame.PixelArray(sf) + x, y = Int(0), Int(1) + rx_before, ry_before = sys.getrefcount(x), sys.getrefcount(y) + ar[x, y] = 0 + rx_after, ry_after = sys.getrefcount(x), sys.getrefcount(y) + self.assertEqual(rx_after, rx_before) + self.assertEqual(ry_after, ry_before) + + def test_pixels_field(self): + for bpp in [1, 2, 3, 4]: + sf = pygame.Surface((11, 7), 0, bpp * 8) + ar = pygame.PixelArray(sf) + ar2 = ar[1:, :] + self.assertEqual(ar2._pixels_address - ar._pixels_address, ar.itemsize) + ar2 = ar[:, 1:] + self.assertEqual(ar2._pixels_address - ar._pixels_address, ar.strides[1]) + ar2 = ar[::-1, :] + self.assertEqual( + ar2._pixels_address - ar._pixels_address, + (ar.shape[0] - 1) * ar.itemsize, + ) + ar2 = ar[::-2, :] + self.assertEqual( + ar2._pixels_address - ar._pixels_address, + (ar.shape[0] - 1) * ar.itemsize, + ) + ar2 = ar[:, ::-1] + self.assertEqual( + ar2._pixels_address - ar._pixels_address, + (ar.shape[1] - 1) * ar.strides[1], + ) + ar3 = ar2[::-1, :] + self.assertEqual( + ar3._pixels_address - ar._pixels_address, + (ar.shape[0] - 1) * ar.strides[0] + (ar.shape[1] - 1) * ar.strides[1], + ) + ar2 = ar[:, ::-2] + self.assertEqual( + ar2._pixels_address - ar._pixels_address, + (ar.shape[1] - 1) * ar.strides[1], + ) + ar2 = ar[2::, 3::] + self.assertEqual( + ar2._pixels_address - ar._pixels_address, + ar.strides[0] * 2 + ar.strides[1] * 3, + ) + ar2 = ar[2::2, 3::4] + self.assertEqual( + ar2._pixels_address - ar._pixels_address, + ar.strides[0] * 2 + ar.strides[1] * 3, + ) + ar2 = ar[9:2:-1, :] + self.assertEqual( + ar2._pixels_address - ar._pixels_address, ar.strides[0] * 9 + ) + ar2 = ar[:, 5:2:-1] + self.assertEqual( + ar2._pixels_address - ar._pixels_address, ar.strides[1] * 5 + ) + ##? ar2 = ar[:,9:2:-1] + + def test_make_surface(self): + bg_color = pygame.Color(255, 255, 255) + fg_color = pygame.Color(128, 100, 0) + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((10, 20), 0, bpp) + bg_color_adj = sf.unmap_rgb(sf.map_rgb(bg_color)) + fg_color_adj = sf.unmap_rgb(sf.map_rgb(fg_color)) + sf.fill(bg_color_adj) + sf.fill(fg_color_adj, (2, 5, 4, 11)) + ar = pygame.PixelArray(sf) + newsf = ar[::2, ::2].make_surface() + rect = newsf.get_rect() + self.assertEqual(rect.width, 5) + self.assertEqual(rect.height, 10) + for p in [ + (0, 2), + (0, 3), + (1, 2), + (2, 2), + (3, 2), + (3, 3), + (0, 7), + (0, 8), + (1, 8), + (2, 8), + (3, 8), + (3, 7), + ]: + self.assertEqual(newsf.get_at(p), bg_color_adj) + for p in [(1, 3), (2, 3), (1, 5), (2, 5), (1, 7), (2, 7)]: + self.assertEqual(newsf.get_at(p), fg_color_adj) + + # Bug when array width is not a multiple of the slice step. + w = 17 + lst = list(range(w)) + w_slice = len(lst[::2]) + h = 3 + sf = pygame.Surface((w, h), 0, 32) + ar = pygame.PixelArray(sf) + ar2 = ar[::2, :] + sf2 = ar2.make_surface() + w2, h2 = sf2.get_size() + self.assertEqual(w2, w_slice) + self.assertEqual(h2, h) + + # Bug when array height is not a multiple of the slice step. + # This can hang the Python interpreter. + h = 17 + lst = list(range(h)) + h_slice = len(lst[::2]) + w = 3 + sf = pygame.Surface((w, h), 0, 32) + ar = pygame.PixelArray(sf) + ar2 = ar[:, ::2] + sf2 = ar2.make_surface() # Hangs here. + w2, h2 = sf2.get_size() + self.assertEqual(w2, w) + self.assertEqual(h2, h_slice) + + def test_make_surface__subclassed_surface(self): + """Ensure make_surface can handle subclassed surfaces.""" + expected_size = (3, 5) + expected_flags = 0 + expected_depth = 32 + original_surface = SurfaceSubclass( + expected_size, expected_flags, expected_depth + ) + pixelarray = pygame.PixelArray(original_surface) + + surface = pixelarray.make_surface() + + self.assertIsNot(surface, original_surface) + self.assertIsInstance(surface, pygame.Surface) + self.assertNotIsInstance(surface, SurfaceSubclass) + self.assertEqual(surface.get_size(), expected_size) + self.assertEqual(surface.get_flags(), expected_flags) + self.assertEqual(surface.get_bitsize(), expected_depth) + + def test_iter(self): + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((5, 10), 0, bpp) + ar = pygame.PixelArray(sf) + iterations = 0 + for col in ar: + self.assertEqual(len(col), 10) + iterations += 1 + self.assertEqual(iterations, 5) + + def test_replace(self): + # print("replace start") + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((10, 10), 0, bpp) + sf.fill((255, 0, 0)) + rval = sf.map_rgb((0, 0, 255)) + oval = sf.map_rgb((255, 0, 0)) + ar = pygame.PixelArray(sf) + ar[::2].replace((255, 0, 0), (0, 0, 255)) + self.assertEqual(ar[0][0], rval) + self.assertEqual(ar[1][0], oval) + self.assertEqual(ar[2][3], rval) + self.assertEqual(ar[3][6], oval) + self.assertEqual(ar[8][9], rval) + self.assertEqual(ar[9][9], oval) + + ar[::2].replace((0, 0, 255), (255, 0, 0), weights=(10, 20, 50)) + self.assertEqual(ar[0][0], oval) + self.assertEqual(ar[2][3], oval) + self.assertEqual(ar[3][6], oval) + self.assertEqual(ar[8][9], oval) + self.assertEqual(ar[9][9], oval) + # print("replace end") + + def test_extract(self): + # print("extract start") + for bpp in (8, 16, 24, 32): + sf = pygame.Surface((10, 10), 0, bpp) + sf.fill((0, 0, 255)) + sf.fill((255, 0, 0), (2, 2, 6, 6)) + + white = sf.map_rgb((255, 255, 255)) + black = sf.map_rgb((0, 0, 0)) + + ar = pygame.PixelArray(sf) + newar = ar.extract((255, 0, 0)) + + self.assertEqual(newar[0][0], black) + self.assertEqual(newar[1][0], black) + self.assertEqual(newar[2][3], white) + self.assertEqual(newar[3][6], white) + self.assertEqual(newar[8][9], black) + self.assertEqual(newar[9][9], black) + + newar = ar.extract((255, 0, 0), weights=(10, 0.1, 50)) + self.assertEqual(newar[0][0], black) + self.assertEqual(newar[1][0], black) + self.assertEqual(newar[2][3], white) + self.assertEqual(newar[3][6], white) + self.assertEqual(newar[8][9], black) + self.assertEqual(newar[9][9], black) + # print("extract end") + + def test_2dslice_assignment(self): + w = 2 * 5 * 8 + h = 3 * 5 * 9 + sf = pygame.Surface((w, h), 0, 32) + ar = pygame.PixelArray(sf) + size = (w, h) + strides = (1, w) + offset = 0 + self._test_assignment(sf, ar, size, strides, offset) + xslice = slice(None, None, 2) + yslice = slice(None, None, 3) + ar, size, strides, offset = self._array_slice( + ar, size, (xslice, yslice), strides, offset + ) + self._test_assignment(sf, ar, size, strides, offset) + xslice = slice(5, None, 5) + yslice = slice(5, None, 5) + ar, size, strides, offset = self._array_slice( + ar, size, (xslice, yslice), strides, offset + ) + self._test_assignment(sf, ar, size, strides, offset) + + def _test_assignment(self, sf, ar, ar_size, ar_strides, ar_offset): + self.assertEqual(ar.shape, ar_size) + ar_w, ar_h = ar_size + ar_xstride, ar_ystride = ar_strides + sf_w, sf_h = sf.get_size() + black = pygame.Color("black") + color = pygame.Color(0, 0, 12) + pxcolor = sf.map_rgb(color) + sf.fill(black) + for ar_x, ar_y in [ + (0, 0), + (0, ar_h - 4), + (ar_w - 3, 0), + (0, ar_h - 1), + (ar_w - 1, 0), + (ar_w - 1, ar_h - 1), + ]: + sf_offset = ar_offset + ar_x * ar_xstride + ar_y * ar_ystride + sf_y = sf_offset // sf_w + sf_x = sf_offset - sf_y * sf_w + sf_posn = (sf_x, sf_y) + sf_pix = sf.get_at(sf_posn) + self.assertEqual( + sf_pix, + black, + "at pixarr posn (%i, %i) (surf posn (%i, %i)): " + "%s != %s" % (ar_x, ar_y, sf_x, sf_y, sf_pix, black), + ) + ar[ar_x, ar_y] = pxcolor + sf_pix = sf.get_at(sf_posn) + self.assertEqual( + sf_pix, + color, + "at pixarr posn (%i, %i) (surf posn (%i, %i)): " + "%s != %s" % (ar_x, ar_y, sf_x, sf_y, sf_pix, color), + ) + + def _array_slice(self, ar, size, slices, strides, offset): + ar = ar[slices] + xslice, yslice = slices + w, h = size + xstart, xstop, xstep = xslice.indices(w) + ystart, ystop, ystep = yslice.indices(h) + w = (xstop - xstart + xstep - 1) // xstep + h = (ystop - ystart + ystep - 1) // ystep + xstride, ystride = strides + offset += xstart * xstride + ystart * ystride + xstride *= xstep + ystride *= ystep + return ar, (w, h), (xstride, ystride), offset + + def test_array_properties(self): + # itemsize, ndim, shape, and strides. + for bpp in [1, 2, 3, 4]: + sf = pygame.Surface((2, 2), 0, bpp * 8) + ar = pygame.PixelArray(sf) + self.assertEqual(ar.itemsize, bpp) + + for shape in [(4, 16), (5, 13)]: + w, h = shape + sf = pygame.Surface(shape, 0, 32) + bpp = sf.get_bytesize() + pitch = sf.get_pitch() + ar = pygame.PixelArray(sf) + self.assertEqual(ar.ndim, 2) + self.assertEqual(ar.shape, shape) + self.assertEqual(ar.strides, (bpp, pitch)) + ar2 = ar[::2, :] + w2 = len(([0] * w)[::2]) + self.assertEqual(ar2.ndim, 2) + self.assertEqual(ar2.shape, (w2, h)) + self.assertEqual(ar2.strides, (2 * bpp, pitch)) + ar2 = ar[:, ::2] + h2 = len(([0] * h)[::2]) + self.assertEqual(ar2.ndim, 2) + self.assertEqual(ar2.shape, (w, h2)) + self.assertEqual(ar2.strides, (bpp, 2 * pitch)) + ar2 = ar[1] + self.assertEqual(ar2.ndim, 1) + self.assertEqual(ar2.shape, (h,)) + self.assertEqual(ar2.strides, (pitch,)) + ar2 = ar[:, 1] + self.assertEqual(ar2.ndim, 1) + self.assertEqual(ar2.shape, (w,)) + self.assertEqual(ar2.strides, (bpp,)) + + def test_self_assign(self): + # This differs from NumPy arrays. + w = 10 + max_x = w - 1 + h = 20 + max_y = h - 1 + for bpp in [1, 2, 3, 4]: + sf = pygame.Surface((w, h), 0, bpp * 8) + ar = pygame.PixelArray(sf) + for i in range(w * h): + ar[i % w, i // w] = i + ar[:, :] = ar[::-1, :] + for i in range(w * h): + self.assertEqual(ar[max_x - i % w, i // w], i) + ar = pygame.PixelArray(sf) + for i in range(w * h): + ar[i % w, i // w] = i + ar[:, :] = ar[:, ::-1] + for i in range(w * h): + self.assertEqual(ar[i % w, max_y - i // w], i) + ar = pygame.PixelArray(sf) + for i in range(w * h): + ar[i % w, i // w] = i + ar[:, :] = ar[::-1, ::-1] + for i in range(w * h): + self.assertEqual(ar[max_x - i % w, max_y - i // w], i) + + def test_color_value(self): + # Confirm that a PixelArray slice assignment distinguishes between + # pygame.Color and tuple objects as single (r, g, b[, a]) colors + # and other sequences as sequences of colors to be treated as + # slices. + sf = pygame.Surface((5, 5), 0, 32) + ar = pygame.PixelArray(sf) + index = slice(None, None, 1) + ar.__setitem__(index, (1, 2, 3)) + self.assertEqual(ar[0, 0], sf.map_rgb((1, 2, 3))) + ar.__setitem__(index, pygame.Color(10, 11, 12)) + self.assertEqual(ar[0, 0], sf.map_rgb((10, 11, 12))) + self.assertRaises(ValueError, ar.__setitem__, index, (1, 2, 3, 4, 5)) + self.assertRaises(ValueError, ar.__setitem__, (index, index), (1, 2, 3, 4, 5)) + self.assertRaises(ValueError, ar.__setitem__, index, [1, 2, 3]) + self.assertRaises(ValueError, ar.__setitem__, (index, index), [1, 2, 3]) + sf = pygame.Surface((3, 3), 0, 32) + ar = pygame.PixelArray(sf) + ar[:] = (20, 30, 40) + self.assertEqual(ar[0, 0], sf.map_rgb((20, 30, 40))) + ar[:] = [20, 30, 40] + self.assertEqual(ar[0, 0], 20) + self.assertEqual(ar[1, 0], 30) + self.assertEqual(ar[2, 0], 40) + + def test_transpose(self): + # PixelArray.transpose(): swap axis on a 2D array, add a length + # 1 x axis to a 1D array. + sf = pygame.Surface((3, 7), 0, 32) + ar = pygame.PixelArray(sf) + w, h = ar.shape + dx, dy = ar.strides + for i in range(w * h): + x = i % w + y = i // w + ar[x, y] = i + ar_t = ar.transpose() + self.assertEqual(ar_t.shape, (h, w)) + self.assertEqual(ar_t.strides, (dy, dx)) + for i in range(w * h): + x = i % w + y = i // w + self.assertEqual(ar_t[y, x], ar[x, y]) + ar1D = ar[0] + ar2D = ar1D.transpose() + self.assertEqual(ar2D.shape, (1, h)) + for y in range(h): + self.assertEqual(ar1D[y], ar2D[0, y]) + ar1D = ar[:, 0] + ar2D = ar1D.transpose() + self.assertEqual(ar2D.shape, (1, w)) + for x in range(2): + self.assertEqual(ar1D[x], ar2D[0, x]) + + def test_length_1_dimension_broadcast(self): + w = 5 + sf = pygame.Surface((w, w), 0, 32) + ar = pygame.PixelArray(sf) + # y-axis broadcast. + sf_x = pygame.Surface((w, 1), 0, 32) + ar_x = pygame.PixelArray(sf_x) + for i in range(w): + ar_x[i, 0] = (w + 1) * 10 + ar[...] = ar_x + for y in range(w): + for x in range(w): + self.assertEqual(ar[x, y], ar_x[x, 0]) + # x-axis broadcast. + ar[...] = 0 + sf_y = pygame.Surface((1, w), 0, 32) + ar_y = pygame.PixelArray(sf_y) + for i in range(w): + ar_y[0, i] = (w + 1) * 10 + ar[...] = ar_y + for x in range(w): + for y in range(w): + self.assertEqual(ar[x, y], ar_y[0, y]) + # (1, 1) array broadcast. + ar[...] = 0 + sf_1px = pygame.Surface((1, 1), 0, 32) + ar_1px = pygame.PixelArray(sf_1px) + ar_1px[0, 0] = 42 # Well it had to show up somewhere. + ar[...] = ar_1px + for y in range(w): + for x in range(w): + self.assertEqual(ar[x, y], 42) + + def test_assign_size_mismatch(self): + sf = pygame.Surface((7, 11), 0, 32) + ar = pygame.PixelArray(sf) + self.assertRaises(ValueError, ar.__setitem__, Ellipsis, ar[:, 0:2]) + self.assertRaises(ValueError, ar.__setitem__, Ellipsis, ar[0:2, :]) + + def test_repr(self): + # Python 3.x bug: the tp_repr slot function returned NULL instead + # of a Unicode string, triggering an exception. + sf = pygame.Surface((3, 1), pygame.SRCALPHA, 16) + ar = pygame.PixelArray(sf) + ar[...] = 42 + pixel = sf.get_at_mapped((0, 0)) + self.assertEqual(repr(ar), type(ar).__name__ + "([\n [42, 42, 42]]\n)") + + +@unittest.skipIf(IS_PYPY, "pypy having issues") +class PixelArrayArrayInterfaceTest(unittest.TestCase, TestMixin): + @unittest.skipIf(IS_PYPY, "skipping for PyPy (why?)") + def test_basic(self): + # Check unchanging fields. + sf = pygame.Surface((2, 2), 0, 32) + ar = pygame.PixelArray(sf) + + ai = arrinter.ArrayInterface(ar) + self.assertEqual(ai.two, 2) + self.assertEqual(ai.typekind, "u") + self.assertEqual(ai.nd, 2) + self.assertEqual(ai.data, ar._pixels_address) + + @unittest.skipIf(IS_PYPY, "skipping for PyPy (why?)") + def test_shape(self): + for shape in [[4, 16], [5, 13]]: + w, h = shape + sf = pygame.Surface(shape, 0, 32) + ar = pygame.PixelArray(sf) + ai = arrinter.ArrayInterface(ar) + ai_shape = [ai.shape[i] for i in range(ai.nd)] + self.assertEqual(ai_shape, shape) + ar2 = ar[::2, :] + ai2 = arrinter.ArrayInterface(ar2) + w2 = len(([0] * w)[::2]) + ai_shape = [ai2.shape[i] for i in range(ai2.nd)] + self.assertEqual(ai_shape, [w2, h]) + ar2 = ar[:, ::2] + ai2 = arrinter.ArrayInterface(ar2) + h2 = len(([0] * h)[::2]) + ai_shape = [ai2.shape[i] for i in range(ai2.nd)] + self.assertEqual(ai_shape, [w, h2]) + + @unittest.skipIf(IS_PYPY, "skipping for PyPy (why?)") + def test_itemsize(self): + for bytes_per_pixel in range(1, 5): + bits_per_pixel = 8 * bytes_per_pixel + sf = pygame.Surface((2, 2), 0, bits_per_pixel) + ar = pygame.PixelArray(sf) + ai = arrinter.ArrayInterface(ar) + self.assertEqual(ai.itemsize, bytes_per_pixel) + + @unittest.skipIf(IS_PYPY, "skipping for PyPy (why?)") + def test_flags(self): + aim = arrinter + common_flags = aim.PAI_NOTSWAPPED | aim.PAI_WRITEABLE | aim.PAI_ALIGNED + s = pygame.Surface((10, 2), 0, 32) + ar = pygame.PixelArray(s) + ai = aim.ArrayInterface(ar) + self.assertEqual(ai.flags, common_flags | aim.PAI_FORTRAN) + + ar2 = ar[::2, :] + ai = aim.ArrayInterface(ar2) + self.assertEqual(ai.flags, common_flags) + + s = pygame.Surface((8, 2), 0, 24) + ar = pygame.PixelArray(s) + ai = aim.ArrayInterface(ar) + self.assertEqual(ai.flags, common_flags | aim.PAI_FORTRAN) + + s = pygame.Surface((7, 2), 0, 24) + ar = pygame.PixelArray(s) + ai = aim.ArrayInterface(ar) + self.assertEqual(ai.flags, common_flags) + + def test_slicing(self): + # This will implicitly test data and strides fields. + # + # Need an 8 bit test surfaces because pixelcopy.make_surface + # returns an 8 bit surface for a 2d array. + + factors = [7, 3, 11] + + w = reduce(operator.mul, factors, 1) + h = 13 + sf = pygame.Surface((w, h), 0, 8) + color = sf.map_rgb((1, 17, 128)) + ar = pygame.PixelArray(sf) + for f in factors[:-1]: + w = w // f + sf.fill((0, 0, 0)) + ar = ar[f : f + w, :] + ar[0][0] = color + ar[-1][-2] = color + ar[0][-3] = color + sf2 = ar.make_surface() + sf3 = pygame.pixelcopy.make_surface(ar) + self.assert_surfaces_equal(sf3, sf2) + + h = reduce(operator.mul, factors, 1) + w = 13 + sf = pygame.Surface((w, h), 0, 8) + color = sf.map_rgb((1, 17, 128)) + ar = pygame.PixelArray(sf) + for f in factors[:-1]: + h = h // f + sf.fill((0, 0, 0)) + ar = ar[:, f : f + h] + ar[0][0] = color + ar[-1][-2] = color + ar[0][-3] = color + sf2 = ar.make_surface() + sf3 = pygame.pixelcopy.make_surface(ar) + self.assert_surfaces_equal(sf3, sf2) + + w = 20 + h = 10 + sf = pygame.Surface((w, h), 0, 8) + color = sf.map_rgb((1, 17, 128)) + ar = pygame.PixelArray(sf) + for slices in [ + (slice(w), slice(h)), + (slice(0, w, 2), slice(h)), + (slice(0, w, 3), slice(h)), + (slice(w), slice(0, h, 2)), + (slice(w), slice(0, h, 3)), + (slice(0, w, 2), slice(0, h, 2)), + (slice(0, w, 3), slice(0, h, 3)), + ]: + sf.fill((0, 0, 0)) + ar2 = ar[slices] + ar2[0][0] = color + ar2[-1][-2] = color + ar2[0][-3] = color + sf2 = ar2.make_surface() + sf3 = pygame.pixelcopy.make_surface(ar2) + self.assert_surfaces_equal(sf3, sf2) + + +@unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") +@unittest.skipIf(IS_PYPY, "pypy having issues") +class PixelArrayNewBufferTest(unittest.TestCase, TestMixin): + if pygame.HAVE_NEWBUF: + from pygame.tests.test_utils import buftools + + bitsize_to_format = {8: "B", 16: "=H", 24: "3x", 32: "=I"} + + def test_newbuf_2D(self): + buftools = self.buftools + Importer = buftools.Importer + + for bit_size in [8, 16, 24, 32]: + s = pygame.Surface((10, 2), 0, bit_size) + ar = pygame.PixelArray(s) + format = self.bitsize_to_format[bit_size] + itemsize = ar.itemsize + shape = ar.shape + w, h = shape + strides = ar.strides + length = w * h * itemsize + imp = Importer(ar, buftools.PyBUF_FULL) + self.assertTrue(imp.obj, ar) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 2) + self.assertEqual(imp.itemsize, itemsize) + self.assertEqual(imp.format, format) + self.assertFalse(imp.readonly) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertTrue(imp.suboffsets is None) + self.assertEqual(imp.buf, s._pixels_address) + + s = pygame.Surface((8, 16), 0, 32) + ar = pygame.PixelArray(s) + format = self.bitsize_to_format[s.get_bitsize()] + itemsize = ar.itemsize + shape = ar.shape + w, h = shape + strides = ar.strides + length = w * h * itemsize + imp = Importer(ar, buftools.PyBUF_SIMPLE) + self.assertTrue(imp.obj, ar) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 0) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.format is None) + self.assertFalse(imp.readonly) + self.assertTrue(imp.shape is None) + self.assertTrue(imp.strides is None) + self.assertTrue(imp.suboffsets is None) + self.assertEqual(imp.buf, s._pixels_address) + imp = Importer(ar, buftools.PyBUF_FORMAT) + self.assertEqual(imp.ndim, 0) + self.assertEqual(imp.format, format) + imp = Importer(ar, buftools.PyBUF_WRITABLE) + self.assertEqual(imp.ndim, 0) + self.assertTrue(imp.format is None) + imp = Importer(ar, buftools.PyBUF_F_CONTIGUOUS) + self.assertEqual(imp.ndim, 2) + self.assertTrue(imp.format is None) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + imp = Importer(ar, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertEqual(imp.ndim, 2) + self.assertTrue(imp.format is None) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_ND) + + ar_sliced = ar[:, ::2] + format = self.bitsize_to_format[s.get_bitsize()] + itemsize = ar_sliced.itemsize + shape = ar_sliced.shape + w, h = shape + strides = ar_sliced.strides + length = w * h * itemsize + imp = Importer(ar_sliced, buftools.PyBUF_STRIDED) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 2) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.format is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertEqual(imp.buf, s._pixels_address) + self.assertRaises(BufferError, Importer, ar_sliced, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, ar_sliced, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, ar_sliced, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, ar_sliced, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises( + BufferError, Importer, ar_sliced, buftools.PyBUF_ANY_CONTIGUOUS + ) + + ar_sliced = ar[::2, :] + format = self.bitsize_to_format[s.get_bitsize()] + itemsize = ar_sliced.itemsize + shape = ar_sliced.shape + w, h = shape + strides = ar_sliced.strides + length = w * h * itemsize + imp = Importer(ar_sliced, buftools.PyBUF_STRIDED) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 2) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.format is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertEqual(imp.buf, s._pixels_address) + self.assertRaises(BufferError, Importer, ar_sliced, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, ar_sliced, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, ar_sliced, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, ar_sliced, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises( + BufferError, Importer, ar_sliced, buftools.PyBUF_ANY_CONTIGUOUS + ) + + s2 = s.subsurface((2, 3, 5, 7)) + ar = pygame.PixelArray(s2) + format = self.bitsize_to_format[s.get_bitsize()] + itemsize = ar.itemsize + shape = ar.shape + w, h = shape + strides = ar.strides + length = w * h * itemsize + imp = Importer(ar, buftools.PyBUF_STRIDES) + self.assertTrue(imp.obj, ar) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 2) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.format is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertTrue(imp.suboffsets is None) + self.assertEqual(imp.buf, s2._pixels_address) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_FORMAT) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_WRITABLE) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_ANY_CONTIGUOUS) + + def test_newbuf_1D(self): + buftools = self.buftools + Importer = buftools.Importer + + s = pygame.Surface((2, 16), 0, 32) + ar_2D = pygame.PixelArray(s) + x = 0 + ar = ar_2D[x] + format = self.bitsize_to_format[s.get_bitsize()] + itemsize = ar.itemsize + shape = ar.shape + h = shape[0] + strides = ar.strides + length = h * itemsize + buf = s._pixels_address + x * itemsize + imp = Importer(ar, buftools.PyBUF_STRIDES) + self.assertTrue(imp.obj, ar) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 1) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.format is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertTrue(imp.suboffsets is None) + self.assertEqual(imp.buf, buf) + imp = Importer(ar, buftools.PyBUF_FULL) + self.assertEqual(imp.ndim, 1) + self.assertEqual(imp.format, format) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_FORMAT) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_WRITABLE) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, ar, buftools.PyBUF_ANY_CONTIGUOUS) + y = 10 + ar = ar_2D[:, y] + shape = ar.shape + w = shape[0] + strides = ar.strides + length = w * itemsize + buf = s._pixels_address + y * s.get_pitch() + imp = Importer(ar, buftools.PyBUF_FULL) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 1) + self.assertEqual(imp.itemsize, itemsize) + self.assertEqual(imp.format, format) + self.assertFalse(imp.readonly) + self.assertEqual(imp.shape, shape) + self.assertEqual(imp.strides, strides) + self.assertEqual(imp.buf, buf) + self.assertTrue(imp.suboffsets is None) + imp = Importer(ar, buftools.PyBUF_SIMPLE) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 0) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.format is None) + self.assertFalse(imp.readonly) + self.assertTrue(imp.shape is None) + self.assertTrue(imp.strides is None) + imp = Importer(ar, buftools.PyBUF_ND) + self.assertEqual(imp.len, length) + self.assertEqual(imp.ndim, 1) + self.assertEqual(imp.itemsize, itemsize) + self.assertTrue(imp.format is None) + self.assertFalse(imp.readonly) + self.assertEqual(imp.shape, shape) + self.assertTrue(imp.strides is None) + imp = Importer(ar, buftools.PyBUF_C_CONTIGUOUS) + self.assertEqual(imp.ndim, 1) + imp = Importer(ar, buftools.PyBUF_F_CONTIGUOUS) + self.assertEqual(imp.ndim, 1) + imp = Importer(ar, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertEqual(imp.ndim, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/pixelcopy_test.py b/laplas/abstract_map/pygame/tests/pixelcopy_test.py new file mode 100644 index 0000000..46051cd --- /dev/null +++ b/laplas/abstract_map/pygame/tests/pixelcopy_test.py @@ -0,0 +1,710 @@ +import platform +import unittest + +try: + from pygame.tests.test_utils import arrinter +except NameError: + pass +import pygame +from pygame.locals import * +from pygame.pixelcopy import surface_to_array, map_array, array_to_surface, make_surface + +IS_PYPY = "PyPy" == platform.python_implementation() + + +def unsigned32(i): + """cast signed 32 bit integer to an unsigned integer""" + return i & 0xFFFFFFFF + + +@unittest.skipIf(IS_PYPY, "pypy having illegal instruction on mac") +class PixelcopyModuleTest(unittest.TestCase): + bitsizes = [8, 16, 32] + + test_palette = [ + (0, 0, 0, 255), + (10, 30, 60, 255), + (25, 75, 100, 255), + (100, 150, 200, 255), + (0, 100, 200, 255), + ] + + surf_size = (10, 12) + test_points = [ + ((0, 0), 1), + ((4, 5), 1), + ((9, 0), 2), + ((5, 5), 2), + ((0, 11), 3), + ((4, 6), 3), + ((9, 11), 4), + ((5, 6), 4), + ] + + def __init__(self, *args, **kwds): + pygame.display.init() + try: + unittest.TestCase.__init__(self, *args, **kwds) + self.sources = [ + self._make_src_surface(8), + self._make_src_surface(16), + self._make_src_surface(16, srcalpha=True), + self._make_src_surface(24), + self._make_src_surface(32), + self._make_src_surface(32, srcalpha=True), + ] + finally: + pygame.display.quit() + + def _make_surface(self, bitsize, srcalpha=False, palette=None): + if palette is None: + palette = self.test_palette + flags = 0 + if srcalpha: + flags |= SRCALPHA + surf = pygame.Surface(self.surf_size, flags, bitsize) + if bitsize == 8: + surf.set_palette([c[:3] for c in palette]) + return surf + + def _fill_surface(self, surf, palette=None): + if palette is None: + palette = self.test_palette + surf.fill(palette[1], (0, 0, 5, 6)) + surf.fill(palette[2], (5, 0, 5, 6)) + surf.fill(palette[3], (0, 6, 5, 6)) + surf.fill(palette[4], (5, 6, 5, 6)) + + def _make_src_surface(self, bitsize, srcalpha=False, palette=None): + surf = self._make_surface(bitsize, srcalpha, palette) + self._fill_surface(surf, palette) + return surf + + def setUp(self): + pygame.display.init() + + def tearDown(self): + pygame.display.quit() + + def test_surface_to_array_2d(self): + alpha_color = (0, 0, 0, 128) + + for surf in self.sources: + src_bitsize = surf.get_bitsize() + for dst_bitsize in self.bitsizes: + # dst in a surface standing in for a 2 dimensional array + # of unsigned integers. The byte order is system dependent. + dst = pygame.Surface(surf.get_size(), 0, dst_bitsize) + dst.fill((0, 0, 0, 0)) + view = dst.get_view("2") + self.assertFalse(surf.get_locked()) + if dst_bitsize < src_bitsize: + self.assertRaises(ValueError, surface_to_array, view, surf) + self.assertFalse(surf.get_locked()) + continue + surface_to_array(view, surf) + self.assertFalse(surf.get_locked()) + for posn, i in self.test_points: + sp = surf.get_at_mapped(posn) + dp = dst.get_at_mapped(posn) + self.assertEqual( + dp, + sp, + "%s != %s: flags: %i" + ", bpp: %i, posn: %s" + % (dp, sp, surf.get_flags(), surf.get_bitsize(), posn), + ) + del view + + if surf.get_masks()[3]: + dst.fill((0, 0, 0, 0)) + view = dst.get_view("2") + posn = (2, 1) + surf.set_at(posn, alpha_color) + self.assertFalse(surf.get_locked()) + surface_to_array(view, surf) + self.assertFalse(surf.get_locked()) + sp = surf.get_at_mapped(posn) + dp = dst.get_at_mapped(posn) + self.assertEqual( + dp, sp, "%s != %s: bpp: %i" % (dp, sp, surf.get_bitsize()) + ) + + if IS_PYPY: + return + # Swapped endian destination array + pai_flags = arrinter.PAI_ALIGNED | arrinter.PAI_WRITEABLE + for surf in self.sources: + for itemsize in [1, 2, 4, 8]: + if itemsize < surf.get_bytesize(): + continue + a = arrinter.Array(surf.get_size(), "u", itemsize, flags=pai_flags) + surface_to_array(a, surf) + for posn, i in self.test_points: + sp = unsigned32(surf.get_at_mapped(posn)) + dp = a[posn] + self.assertEqual( + dp, + sp, + "%s != %s: itemsize: %i, flags: %i" + ", bpp: %i, posn: %s" + % ( + dp, + sp, + itemsize, + surf.get_flags(), + surf.get_bitsize(), + posn, + ), + ) + + def test_surface_to_array_3d(self): + self.iter_surface_to_array_3d((0xFF, 0xFF00, 0xFF0000, 0)) + self.iter_surface_to_array_3d((0xFF0000, 0xFF00, 0xFF, 0)) + + def iter_surface_to_array_3d(self, rgba_masks): + dst = pygame.Surface(self.surf_size, 0, 24, masks=rgba_masks) + + for surf in self.sources: + dst.fill((0, 0, 0, 0)) + src_bitsize = surf.get_bitsize() + view = dst.get_view("3") + self.assertFalse(surf.get_locked()) + surface_to_array(view, surf) + self.assertFalse(surf.get_locked()) + for posn, i in self.test_points: + sc = surf.get_at(posn)[0:3] + dc = dst.get_at(posn)[0:3] + self.assertEqual( + dc, + sc, + "%s != %s: flags: %i" + ", bpp: %i, posn: %s" + % (dc, sc, surf.get_flags(), surf.get_bitsize(), posn), + ) + view = None + + def test_map_array(self): + targets = [ + self._make_surface(8), + self._make_surface(16), + self._make_surface(16, srcalpha=True), + self._make_surface(24), + self._make_surface(32), + self._make_surface(32, srcalpha=True), + ] + source = pygame.Surface( + self.surf_size, 0, 24, masks=[0xFF, 0xFF00, 0xFF0000, 0] + ) + self._fill_surface(source) + source_view = source.get_view("3") # (w, h, 3) + for t in targets: + map_array(t.get_view("2"), source_view, t) + for posn, i in self.test_points: + sc = t.map_rgb(source.get_at(posn)) + dc = t.get_at_mapped(posn) + self.assertEqual( + dc, + sc, + "%s != %s: flags: %i" + ", bpp: %i, posn: %s" + % (dc, sc, t.get_flags(), t.get_bitsize(), posn), + ) + + color = pygame.Color("salmon") + color.set_length(3) + for t in targets: + map_array(t.get_view("2"), color, t) + sc = t.map_rgb(color) + for posn, i in self.test_points: + dc = t.get_at_mapped(posn) + self.assertEqual( + dc, + sc, + "%s != %s: flags: %i" + ", bpp: %i, posn: %s" + % (dc, sc, t.get_flags(), t.get_bitsize(), posn), + ) + + # mismatched shapes + w, h = source.get_size() + target = pygame.Surface((w, h + 1), 0, 32) + self.assertRaises(ValueError, map_array, target, source, target) + target = pygame.Surface((w - 1, h), 0, 32) + self.assertRaises(ValueError, map_array, target, source, target) + + def test_array_to_surface_broadcasting(self): + # target surfaces + targets = [ + self._make_surface(8), + self._make_surface(16), + self._make_surface(16, srcalpha=True), + self._make_surface(24), + self._make_surface(32), + self._make_surface(32, srcalpha=True), + ] + + w, h = self.surf_size + + # broadcast column + column = pygame.Surface((1, h), 0, 32) + for target in targets: + source = pygame.Surface((1, h), 0, target) + for y in range(h): + source.set_at((0, y), pygame.Color(y + 1, y + h + 1, y + 2 * h + 1)) + pygame.pixelcopy.surface_to_array(column.get_view("2"), source) + pygame.pixelcopy.array_to_surface(target, column.get_view("2")) + for x in range(w): + for y in range(h): + self.assertEqual( + target.get_at_mapped((x, y)), column.get_at_mapped((0, y)) + ) + + # broadcast row + row = pygame.Surface((w, 1), 0, 32) + for target in targets: + source = pygame.Surface((w, 1), 0, target) + for x in range(w): + source.set_at((x, 0), pygame.Color(x + 1, x + w + 1, x + 2 * w + 1)) + pygame.pixelcopy.surface_to_array(row.get_view("2"), source) + pygame.pixelcopy.array_to_surface(target, row.get_view("2")) + for x in range(w): + for y in range(h): + self.assertEqual( + target.get_at_mapped((x, y)), row.get_at_mapped((x, 0)) + ) + + # broadcast pixel + pixel = pygame.Surface((1, 1), 0, 32) + for target in targets: + source = pygame.Surface((1, 1), 0, target) + source.set_at((0, 0), pygame.Color(13, 47, 101)) + pygame.pixelcopy.surface_to_array(pixel.get_view("2"), source) + pygame.pixelcopy.array_to_surface(target, pixel.get_view("2")) + p = pixel.get_at_mapped((0, 0)) + for x in range(w): + for y in range(h): + self.assertEqual(target.get_at_mapped((x, y)), p) + + +@unittest.skipIf(IS_PYPY, "pypy having illegal instruction on mac") +class PixelCopyTestWithArrayNumpy(unittest.TestCase): + try: + import numpy + except ImportError: + __tags__ = ["ignore", "subprocess_ignore"] + else: + pygame.surfarray.use_arraytype("numpy") + + bitsizes = [8, 16, 32] + + test_palette = [ + (0, 0, 0, 255), + (10, 30, 60, 255), + (25, 75, 100, 255), + (100, 150, 200, 255), + (0, 100, 200, 255), + ] + + surf_size = (10, 12) + test_points = [ + ((0, 0), 1), + ((4, 5), 1), + ((9, 0), 2), + ((5, 5), 2), + ((0, 11), 3), + ((4, 6), 3), + ((9, 11), 4), + ((5, 6), 4), + ] + + pixels2d = {8, 16, 32} + pixels3d = {24, 32} + array2d = {8, 16, 24, 32} + array3d = {24, 32} + + def __init__(self, *args, **kwds): + import numpy + + self.dst_types = [numpy.uint8, numpy.uint16, numpy.uint32] + try: + self.dst_types.append(numpy.uint64) + except AttributeError: + pass + pygame.display.init() + try: + unittest.TestCase.__init__(self, *args, **kwds) + self.sources = [ + self._make_src_surface(8), + self._make_src_surface(16), + self._make_src_surface(16, srcalpha=True), + self._make_src_surface(24), + self._make_src_surface(32), + self._make_src_surface(32, srcalpha=True), + ] + finally: + pygame.display.quit() + + def _make_surface(self, bitsize, srcalpha=False, palette=None): + if palette is None: + palette = self.test_palette + flags = 0 + if srcalpha: + flags |= SRCALPHA + surf = pygame.Surface(self.surf_size, flags, bitsize) + if bitsize == 8: + surf.set_palette([c[:3] for c in palette]) + return surf + + def _fill_surface(self, surf, palette=None): + if palette is None: + palette = self.test_palette + surf.fill(palette[1], (0, 0, 5, 6)) + surf.fill(palette[2], (5, 0, 5, 6)) + surf.fill(palette[3], (0, 6, 5, 6)) + surf.fill(palette[4], (5, 6, 5, 6)) + + def _make_src_surface(self, bitsize, srcalpha=False, palette=None): + surf = self._make_surface(bitsize, srcalpha, palette) + self._fill_surface(surf, palette) + return surf + + def setUp(self): + pygame.display.init() + + def tearDown(self): + pygame.display.quit() + + def test_surface_to_array_2d(self): + try: + from numpy import empty, dtype + except ImportError: + return + + palette = self.test_palette + alpha_color = (0, 0, 0, 128) + + dst_dims = self.surf_size + destinations = [empty(dst_dims, t) for t in self.dst_types] + if pygame.get_sdl_byteorder() == pygame.LIL_ENDIAN: + swapped_dst = empty(dst_dims, dtype(">u4")) + else: + swapped_dst = empty(dst_dims, dtype("u4")) + else: + swapped_dst = empty(dst_dims, dtype("i", + "!i", + "1i", + "=1i", + "@q", + "q", + "4x", + "8x", + ]: + surface.fill((255, 254, 253)) + exp = Exporter(shape, format=format) + exp._buf[:] = [42] * exp.buflen + array_to_surface(surface, exp) + for x in range(w): + for y in range(h): + self.assertEqual(surface.get_at((x, y)), (42, 42, 42, 255)) + # Some unsupported formats for array_to_surface and a 32 bit surface + for format in ["f", "d", "?", "x", "1x", "2x", "3x", "5x", "6x", "7x", "9x"]: + exp = Exporter(shape, format=format) + self.assertRaises(ValueError, array_to_surface, surface, exp) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/rect_test.py b/laplas/abstract_map/pygame/tests/rect_test.py new file mode 100644 index 0000000..71a7521 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/rect_test.py @@ -0,0 +1,3312 @@ +import math +import unittest +from collections.abc import Collection, Sequence +import platform +import random + +from pygame import Rect, Vector2 +from pygame.tests import test_utils + +IS_PYPY = "PyPy" == platform.python_implementation() + +# todo can they be different on different platforms? +_int_min = -2147483647 - 1 # min value of int in C +_int_max = 2147483647 # max value of int in C + + +def _random_int(): + return random.randint(_int_min, _int_max) + + +class RectTypeTest(unittest.TestCase): + def _assertCountEqual(self, *args, **kwargs): + self.assertCountEqual(*args, **kwargs) + + def testConstructionXYWidthHeight(self): + r = Rect(1, 2, 3, 4) + self.assertEqual(1, r.left) + self.assertEqual(2, r.top) + self.assertEqual(3, r.width) + self.assertEqual(4, r.height) + + def testConstructionTopLeftSize(self): + r = Rect((1, 2), (3, 4)) + self.assertEqual(1, r.left) + self.assertEqual(2, r.top) + self.assertEqual(3, r.width) + self.assertEqual(4, r.height) + + def testCalculatedAttributes(self): + r = Rect(1, 2, 3, 4) + + self.assertEqual(r.left + r.width, r.right) + self.assertEqual(r.top + r.height, r.bottom) + self.assertEqual((r.width, r.height), r.size) + self.assertEqual((r.left, r.top), r.topleft) + self.assertEqual((r.right, r.top), r.topright) + self.assertEqual((r.left, r.bottom), r.bottomleft) + self.assertEqual((r.right, r.bottom), r.bottomright) + + midx = r.left + r.width // 2 + midy = r.top + r.height // 2 + + self.assertEqual(midx, r.centerx) + self.assertEqual(midy, r.centery) + self.assertEqual((r.centerx, r.centery), r.center) + self.assertEqual((r.centerx, r.top), r.midtop) + self.assertEqual((r.centerx, r.bottom), r.midbottom) + self.assertEqual((r.left, r.centery), r.midleft) + self.assertEqual((r.right, r.centery), r.midright) + + def test_rect_iter(self): + rect = Rect(50, 100, 150, 200) + + # call __iter__ explicitly to test that it is defined + rect_iterator = rect.__iter__() + for i, val in enumerate(rect_iterator): + self.assertEqual(rect[i], val) + + def test_normalize(self): + """Ensures normalize works when width and height are both negative.""" + test_rect = Rect((1, 2), (-3, -6)) + expected_normalized_rect = ( + (test_rect.x + test_rect.w, test_rect.y + test_rect.h), + (-test_rect.w, -test_rect.h), + ) + + test_rect.normalize() + + self.assertEqual(test_rect, expected_normalized_rect) + + def test_normalize__positive_height(self): + """Ensures normalize works with a negative width and a positive height.""" + test_rect = Rect((1, 2), (-3, 6)) + expected_normalized_rect = ( + (test_rect.x + test_rect.w, test_rect.y), + (-test_rect.w, test_rect.h), + ) + + test_rect.normalize() + + self.assertEqual(test_rect, expected_normalized_rect) + + def test_normalize__positive_width(self): + """Ensures normalize works with a positive width and a negative height.""" + test_rect = Rect((1, 2), (3, -6)) + expected_normalized_rect = ( + (test_rect.x, test_rect.y + test_rect.h), + (test_rect.w, -test_rect.h), + ) + + test_rect.normalize() + + self.assertEqual(test_rect, expected_normalized_rect) + + def test_normalize__zero_height(self): + """Ensures normalize works with a negative width and a zero height.""" + test_rect = Rect((1, 2), (-3, 0)) + expected_normalized_rect = ( + (test_rect.x + test_rect.w, test_rect.y), + (-test_rect.w, test_rect.h), + ) + + test_rect.normalize() + + self.assertEqual(test_rect, expected_normalized_rect) + + def test_normalize__zero_width(self): + """Ensures normalize works with a zero width and a negative height.""" + test_rect = Rect((1, 2), (0, -6)) + expected_normalized_rect = ( + (test_rect.x, test_rect.y + test_rect.h), + (test_rect.w, -test_rect.h), + ) + + test_rect.normalize() + + self.assertEqual(test_rect, expected_normalized_rect) + + def test_normalize__non_negative(self): + """Ensures normalize works when width and height are both non-negative. + + Tests combinations of positive and zero values for width and height. + The normalize method has no impact when both width and height are + non-negative. + """ + for size in ((3, 6), (3, 0), (0, 6), (0, 0)): + test_rect = Rect((1, 2), size) + expected_normalized_rect = Rect(test_rect) + + test_rect.normalize() + + self.assertEqual(test_rect, expected_normalized_rect) + + def test_x(self): + """Ensures changing the x attribute moves the rect and does not change + the rect's size. + """ + expected_x = 10 + expected_y = 2 + expected_size = (3, 4) + r = Rect((1, expected_y), expected_size) + + r.x = expected_x + + self.assertEqual(r.x, expected_x) + self.assertEqual(r.x, r.left) + self.assertEqual(r.y, expected_y) + self.assertEqual(r.size, expected_size) + + def test_x__invalid_value(self): + """Ensures the x attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.x = value + + def test_x__del(self): + """Ensures the x attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.x + + def test_y(self): + """Ensures changing the y attribute moves the rect and does not change + the rect's size. + """ + expected_x = 1 + expected_y = 20 + expected_size = (3, 4) + r = Rect((expected_x, 2), expected_size) + + r.y = expected_y + + self.assertEqual(r.y, expected_y) + self.assertEqual(r.y, r.top) + self.assertEqual(r.x, expected_x) + self.assertEqual(r.size, expected_size) + + def test_y__invalid_value(self): + """Ensures the y attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.y = value + + def test_y__del(self): + """Ensures the y attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.y + + def test_left(self): + """Changing the left attribute moves the rect and does not change + the rect's width + """ + r = Rect(1, 2, 3, 4) + new_left = 10 + + r.left = new_left + self.assertEqual(new_left, r.left) + self.assertEqual(Rect(new_left, 2, 3, 4), r) + + def test_left__invalid_value(self): + """Ensures the left attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.left = value + + def test_left__del(self): + """Ensures the left attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.left + + def test_right(self): + """Changing the right attribute moves the rect and does not change + the rect's width + """ + r = Rect(1, 2, 3, 4) + new_right = r.right + 20 + expected_left = r.left + 20 + old_width = r.width + + r.right = new_right + self.assertEqual(new_right, r.right) + self.assertEqual(expected_left, r.left) + self.assertEqual(old_width, r.width) + + def test_right__invalid_value(self): + """Ensures the right attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.right = value + + def test_right__del(self): + """Ensures the right attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.right + + def test_top(self): + """Changing the top attribute moves the rect and does not change + the rect's width + """ + r = Rect(1, 2, 3, 4) + new_top = 10 + + r.top = new_top + self.assertEqual(Rect(1, new_top, 3, 4), r) + self.assertEqual(new_top, r.top) + + def test_top__invalid_value(self): + """Ensures the top attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.top = value + + def test_top__del(self): + """Ensures the top attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.top + + def test_bottom(self): + """Changing the bottom attribute moves the rect and does not change + the rect's height + """ + r = Rect(1, 2, 3, 4) + new_bottom = r.bottom + 20 + expected_top = r.top + 20 + old_height = r.height + + r.bottom = new_bottom + self.assertEqual(new_bottom, r.bottom) + self.assertEqual(expected_top, r.top) + self.assertEqual(old_height, r.height) + + def test_bottom__invalid_value(self): + """Ensures the bottom attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.bottom = value + + def test_bottom__del(self): + """Ensures the bottom attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.bottom + + def test_centerx(self): + """Changing the centerx attribute moves the rect and does not change + the rect's width + """ + r = Rect(1, 2, 3, 4) + new_centerx = r.centerx + 20 + expected_left = r.left + 20 + old_width = r.width + + r.centerx = new_centerx + self.assertEqual(new_centerx, r.centerx) + self.assertEqual(expected_left, r.left) + self.assertEqual(old_width, r.width) + + def test_centerx__invalid_value(self): + """Ensures the centerx attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.centerx = value + + def test_centerx__del(self): + """Ensures the centerx attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.centerx + + def test_centery(self): + """Changing the centery attribute moves the rect and does not change + the rect's width + """ + r = Rect(1, 2, 3, 4) + new_centery = r.centery + 20 + expected_top = r.top + 20 + old_height = r.height + + r.centery = new_centery + self.assertEqual(new_centery, r.centery) + self.assertEqual(expected_top, r.top) + self.assertEqual(old_height, r.height) + + def test_centery__invalid_value(self): + """Ensures the centery attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.centery = value + + def test_centery__del(self): + """Ensures the centery attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.centery + + def test_topleft(self): + """Changing the topleft attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.topleft = new_topleft + self.assertEqual(new_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_topleft__invalid_value(self): + """Ensures the topleft attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.topleft = value + + def test_topleft__del(self): + """Ensures the topleft attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.topleft + + def test_bottomleft(self): + """Changing the bottomleft attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_bottomleft = (r.left + 20, r.bottom + 30) + expected_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.bottomleft = new_bottomleft + self.assertEqual(new_bottomleft, r.bottomleft) + self.assertEqual(expected_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_bottomleft__invalid_value(self): + """Ensures the bottomleft attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.bottomleft = value + + def test_bottomleft__del(self): + """Ensures the bottomleft attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.bottomleft + + def test_topright(self): + """Changing the topright attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_topright = (r.right + 20, r.top + 30) + expected_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.topright = new_topright + self.assertEqual(new_topright, r.topright) + self.assertEqual(expected_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_topright__invalid_value(self): + """Ensures the topright attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.topright = value + + def test_topright__del(self): + """Ensures the topright attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.topright + + def test_bottomright(self): + """Changing the bottomright attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_bottomright = (r.right + 20, r.bottom + 30) + expected_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.bottomright = new_bottomright + self.assertEqual(new_bottomright, r.bottomright) + self.assertEqual(expected_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_bottomright__invalid_value(self): + """Ensures the bottomright attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.bottomright = value + + def test_bottomright__del(self): + """Ensures the bottomright attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.bottomright + + def test_center(self): + """Changing the center attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_center = (r.centerx + 20, r.centery + 30) + expected_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.center = new_center + self.assertEqual(new_center, r.center) + self.assertEqual(expected_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_center__invalid_value(self): + """Ensures the center attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.center = value + + def test_center__del(self): + """Ensures the center attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.center + + def test_midleft(self): + """Changing the midleft attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_midleft = (r.left + 20, r.centery + 30) + expected_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.midleft = new_midleft + self.assertEqual(new_midleft, r.midleft) + self.assertEqual(expected_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_midleft__invalid_value(self): + """Ensures the midleft attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.midleft = value + + def test_midleft__del(self): + """Ensures the midleft attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.midleft + + def test_midright(self): + """Changing the midright attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_midright = (r.right + 20, r.centery + 30) + expected_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.midright = new_midright + self.assertEqual(new_midright, r.midright) + self.assertEqual(expected_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_midright__invalid_value(self): + """Ensures the midright attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.midright = value + + def test_midright__del(self): + """Ensures the midright attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.midright + + def test_midtop(self): + """Changing the midtop attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_midtop = (r.centerx + 20, r.top + 30) + expected_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.midtop = new_midtop + self.assertEqual(new_midtop, r.midtop) + self.assertEqual(expected_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_midtop__invalid_value(self): + """Ensures the midtop attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.midtop = value + + def test_midtop__del(self): + """Ensures the midtop attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.midtop + + def test_midbottom(self): + """Changing the midbottom attribute moves the rect and does not change + the rect's size + """ + r = Rect(1, 2, 3, 4) + new_midbottom = (r.centerx + 20, r.bottom + 30) + expected_topleft = (r.left + 20, r.top + 30) + old_size = r.size + + r.midbottom = new_midbottom + self.assertEqual(new_midbottom, r.midbottom) + self.assertEqual(expected_topleft, r.topleft) + self.assertEqual(old_size, r.size) + + def test_midbottom__invalid_value(self): + """Ensures the midbottom attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.midbottom = value + + def test_midbottom__del(self): + """Ensures the midbottom attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.midbottom + + def test_width(self): + """Changing the width resizes the rect from the top-left corner""" + r = Rect(1, 2, 3, 4) + new_width = 10 + old_topleft = r.topleft + old_height = r.height + + r.width = new_width + self.assertEqual(new_width, r.width) + self.assertEqual(old_height, r.height) + self.assertEqual(old_topleft, r.topleft) + + def test_width__invalid_value(self): + """Ensures the width attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.width = value + + def test_width__del(self): + """Ensures the width attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.width + + def test_height(self): + """Changing the height resizes the rect from the top-left corner""" + r = Rect(1, 2, 3, 4) + new_height = 10 + old_topleft = r.topleft + old_width = r.width + + r.height = new_height + self.assertEqual(new_height, r.height) + self.assertEqual(old_width, r.width) + self.assertEqual(old_topleft, r.topleft) + + def test_height__invalid_value(self): + """Ensures the height attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.height = value + + def test_height__del(self): + """Ensures the height attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.height + + def test_size(self): + """Changing the size resizes the rect from the top-left corner""" + r = Rect(1, 2, 3, 4) + new_size = (10, 20) + old_topleft = r.topleft + + r.size = new_size + self.assertEqual(new_size, r.size) + self.assertEqual(old_topleft, r.topleft) + + def test_size__invalid_value(self): + """Ensures the size attribute handles invalid values correctly.""" + r = Rect(0, 0, 1, 1) + + for value in (None, [], "1", 1, (1,), [1, 2, 3]): + with self.assertRaises(TypeError): + r.size = value + + def test_size__del(self): + """Ensures the size attribute can't be deleted.""" + r = Rect(0, 0, 1, 1) + + with self.assertRaises(AttributeError): + del r.size + + def test_contains(self): + r = Rect(1, 2, 3, 4) + + self.assertTrue( + r.contains(Rect(2, 3, 1, 1)), "r does not contain Rect(2, 3, 1, 1)" + ) + self.assertTrue(Rect(2, 3, 1, 1) in r, "r does not contain Rect(2, 3, 1, 1) 2") + self.assertTrue( + r.contains(Rect(r)), "r does not contain the same rect as itself" + ) + self.assertTrue(r in Rect(r), "r does not contain the same rect as itself") + self.assertTrue( + r.contains(Rect(2, 3, 0, 0)), + "r does not contain an empty rect within its bounds", + ) + self.assertTrue( + Rect(2, 3, 0, 0) in r, + "r does not contain an empty rect within its bounds", + ) + self.assertFalse(r.contains(Rect(0, 0, 1, 2)), "r contains Rect(0, 0, 1, 2)") + self.assertFalse(r.contains(Rect(4, 6, 1, 1)), "r contains Rect(4, 6, 1, 1)") + self.assertFalse(r.contains(Rect(4, 6, 0, 0)), "r contains Rect(4, 6, 0, 0)") + self.assertFalse(Rect(0, 0, 1, 2) in r, "r contains Rect(0, 0, 1, 2)") + self.assertFalse(Rect(4, 6, 1, 1) in r, "r contains Rect(4, 6, 1, 1)") + self.assertFalse(Rect(4, 6, 0, 0) in r, "r contains Rect(4, 6, 0, 0)") + self.assertTrue(2 in Rect(0, 0, 1, 2), "r does not contain 2") + self.assertFalse(3 in Rect(0, 0, 1, 2), "r contains 3") + + self.assertRaises(TypeError, lambda: "string" in Rect(0, 0, 1, 2)) + self.assertRaises(TypeError, lambda: 4 + 3j in Rect(0, 0, 1, 2)) + + def test_collidepoint(self): + r = Rect(1, 2, 3, 4) + + self.assertTrue( + r.collidepoint(r.left, r.top), "r does not collide with point (left, top)" + ) + self.assertFalse( + r.collidepoint(r.left - 1, r.top), "r collides with point (left - 1, top)" + ) + self.assertFalse( + r.collidepoint(r.left, r.top - 1), "r collides with point (left, top - 1)" + ) + self.assertFalse( + r.collidepoint(r.left - 1, r.top - 1), + "r collides with point (left - 1, top - 1)", + ) + + self.assertTrue( + r.collidepoint(r.right - 1, r.bottom - 1), + "r does not collide with point (right - 1, bottom - 1)", + ) + self.assertFalse( + r.collidepoint(r.right, r.bottom), "r collides with point (right, bottom)" + ) + self.assertFalse( + r.collidepoint(r.right - 1, r.bottom), + "r collides with point (right - 1, bottom)", + ) + self.assertFalse( + r.collidepoint(r.right, r.bottom - 1), + "r collides with point (right, bottom - 1)", + ) + + def test_inflate__larger(self): + """The inflate method inflates around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r2 = r.inflate(4, 6) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left - 2, r2.left) + self.assertEqual(r.top - 3, r2.top) + self.assertEqual(r.right + 2, r2.right) + self.assertEqual(r.bottom + 3, r2.bottom) + self.assertEqual(r.width + 4, r2.width) + self.assertEqual(r.height + 6, r2.height) + + def test_inflate__smaller(self): + """The inflate method inflates around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r2 = r.inflate(-4, -6) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left + 2, r2.left) + self.assertEqual(r.top + 3, r2.top) + self.assertEqual(r.right - 2, r2.right) + self.assertEqual(r.bottom - 3, r2.bottom) + self.assertEqual(r.width - 4, r2.width) + self.assertEqual(r.height - 6, r2.height) + + def test_inflate_ip__larger(self): + """The inflate_ip method inflates around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r2 = Rect(r) + r2.inflate_ip(-4, -6) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left + 2, r2.left) + self.assertEqual(r.top + 3, r2.top) + self.assertEqual(r.right - 2, r2.right) + self.assertEqual(r.bottom - 3, r2.bottom) + self.assertEqual(r.width - 4, r2.width) + self.assertEqual(r.height - 6, r2.height) + + def test_inflate_ip__smaller(self): + """The inflate method inflates around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r2 = Rect(r) + r2.inflate_ip(-4, -6) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left + 2, r2.left) + self.assertEqual(r.top + 3, r2.top) + self.assertEqual(r.right - 2, r2.right) + self.assertEqual(r.bottom - 3, r2.bottom) + self.assertEqual(r.width - 4, r2.width) + self.assertEqual(r.height - 6, r2.height) + + def test_scale_by__larger_single_argument(self): + """The scale method scales around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r2 = r.scale_by(2) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left - 3, r2.left) + self.assertEqual(r.top - 4, r2.top) + self.assertEqual(r.right + 3, r2.right) + self.assertEqual(r.bottom + 4, r2.bottom) + self.assertEqual(r.width * 2, r2.width) + self.assertEqual(r.height * 2, r2.height) + + def test_scale_by__larger_single_argument_kwarg(self): + """The scale method scales around the center of the rectangle using + keyword arguments 'x' and 'y'""" + r = Rect(2, 4, 6, 8) + r2 = r.scale_by(x=2) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left - 3, r2.left) + self.assertEqual(r.top - 4, r2.top) + self.assertEqual(r.right + 3, r2.right) + self.assertEqual(r.bottom + 4, r2.bottom) + self.assertEqual(r.width * 2, r2.width) + self.assertEqual(r.height * 2, r2.height) + + def test_scale_by__smaller_single_argument(self): + """The scale method scales around the center of the rectangle""" + r = Rect(2, 4, 8, 8) + r2 = r.scale_by(0.5) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left + 2, r2.left) + self.assertEqual(r.top + 2, r2.top) + self.assertEqual(r.right - 2, r2.right) + self.assertEqual(r.bottom - 2, r2.bottom) + self.assertEqual(r.width - 4, r2.width) + self.assertEqual(r.height - 4, r2.height) + + def test_scale_by__larger(self): + """The scale method scales around the center of the rectangle""" + # arrange + r = Rect(2, 4, 6, 8) + # act + r2 = r.scale_by(2, 4) + # assert + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left - 3, r2.left) + self.assertEqual(r.centery - r.h * 4 / 2, r2.top) + self.assertEqual(r.right + 3, r2.right) + self.assertEqual(r.centery + r.h * 4 / 2, r2.bottom) + self.assertEqual(r.width * 2, r2.width) + self.assertEqual(r.height * 4, r2.height) + + def test_scale_by__larger_kwargs_scale_by(self): + """ + The scale method scales around the center of the rectangle + Uses 'scale_by' kwarg. + """ + # arrange + r = Rect(2, 4, 6, 8) + # act + r2 = r.scale_by(scale_by=(2, 4)) + # assert + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left - 3, r2.left) + self.assertEqual(r.centery - r.h * 4 / 2, r2.top) + self.assertEqual(r.right + 3, r2.right) + self.assertEqual(r.centery + r.h * 4 / 2, r2.bottom) + self.assertEqual(r.width * 2, r2.width) + self.assertEqual(r.height * 4, r2.height) + + def test_scale_by__larger_kwargs(self): + """ + The scale method scales around the center of the rectangle + Uses 'x' and 'y' kwargs. + """ + # arrange + r = Rect(2, 4, 6, 8) + # act + r2 = r.scale_by(x=2, y=4) + # assert + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left - 3, r2.left) + self.assertEqual(r.centery - r.h * 4 / 2, r2.top) + self.assertEqual(r.right + 3, r2.right) + self.assertEqual(r.centery + r.h * 4 / 2, r2.bottom) + self.assertEqual(r.width * 2, r2.width) + self.assertEqual(r.height * 4, r2.height) + + def test_scale_by__smaller(self): + """The scale method scales around the center of the rectangle""" + # arrange + r = Rect(2, 4, 8, 8) + # act + r2 = r.scale_by(0.5, 0.25) + # assert + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left + 2, r2.left) + self.assertEqual(r.centery - r.h / 4 / 2, r2.top) + self.assertEqual(r.right - 2, r2.right) + self.assertEqual(r.centery + r.h / 4 / 2, r2.bottom) + self.assertEqual(r.width - 4, r2.width) + self.assertEqual(r.height // 4, r2.height) + + def test_scale_by__subzero(self): + """The scale method scales around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r.scale_by(0) + r.scale_by(-1) + r.scale_by(-0.000001) + r.scale_by(0.00001) + + rx1 = r.scale_by(10, 1) + self.assertEqual(r.centerx - r.w * 10 / 2, rx1.x) + self.assertEqual(r.y, rx1.y) + self.assertEqual(r.w * 10, rx1.w) + self.assertEqual(r.h, rx1.h) + rx2 = r.scale_by(-10, 1) + self.assertEqual(rx1.x, rx2.x) + self.assertEqual(rx1.y, rx2.y) + self.assertEqual(rx1.w, rx2.w) + self.assertEqual(rx1.h, rx2.h) + + ry1 = r.scale_by(1, 10) + self.assertEqual(r.x, ry1.x) + self.assertEqual(r.centery - r.h * 10 / 2, ry1.y) + self.assertEqual(r.w, ry1.w) + self.assertEqual(r.h * 10, ry1.h) + ry2 = r.scale_by(1, -10) + self.assertEqual(ry1.x, ry2.x) + self.assertEqual(ry1.y, ry2.y) + self.assertEqual(ry1.w, ry2.w) + self.assertEqual(ry1.h, ry2.h) + + r1 = r.scale_by(10) + self.assertEqual(r.centerx - r.w * 10 / 2, r1.x) + self.assertEqual(r.centery - r.h * 10 / 2, r1.y) + self.assertEqual(r.w * 10, r1.w) + self.assertEqual(r.h * 10, r1.h) + + def test_scale_by_identity(self): + """The scale method scales around the center of the rectangle""" + # arrange + r = Rect(2, 4, 6, 8) + # act + actual = r.scale_by(1, 1) + # assert + self.assertEqual(r.x, actual.x) + self.assertEqual(r.y, actual.y) + self.assertEqual(r.w, actual.w) + self.assertEqual(r.h, actual.h) + + def test_scale_by_negative_identity(self): + """The scale method scales around the center of the rectangle""" + # arrange + r = Rect(2, 4, 6, 8) + # act + actual = r.scale_by(-1, -1) + # assert + self.assertEqual(r.x, actual.x) + self.assertEqual(r.y, actual.y) + self.assertEqual(r.w, actual.w) + self.assertEqual(r.h, actual.h) + + def test_scale_by_identity_single_argument(self): + """The scale method scales around the center of the rectangle""" + # arrange + r = Rect(2, 4, 6, 8) + # act + actual = r.scale_by(1) + # assert + self.assertEqual(r.x, actual.x) + self.assertEqual(r.y, actual.y) + self.assertEqual(r.w, actual.w) + self.assertEqual(r.h, actual.h) + + def test_scale_by_negative_identity_single_argment(self): + """The scale method scales around the center of the rectangle""" + # arrange + r = Rect(2, 4, 6, 8) + # act + actual = r.scale_by(-1) + # assert + self.assertEqual(r.x, actual.x) + self.assertEqual(r.y, actual.y) + self.assertEqual(r.w, actual.w) + self.assertEqual(r.h, actual.h) + + def test_scale_by_ip__larger(self): + """The scale method scales around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r2 = Rect(r) + r2.scale_by_ip(2) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left - 3, r2.left) + self.assertEqual(r.top - 4, r2.top) + self.assertEqual(r.right + 3, r2.right) + self.assertEqual(r.bottom + 4, r2.bottom) + self.assertEqual(r.width * 2, r2.width) + self.assertEqual(r.height * 2, r2.height) + + def test_scale_by_ip__smaller(self): + """The scale method scales around the center of the rectangle""" + r = Rect(2, 4, 8, 8) + r2 = Rect(r) + r2.scale_by_ip(0.5) + + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left + 2, r2.left) + self.assertEqual(r.top + 2, r2.top) + self.assertEqual(r.right - 2, r2.right) + self.assertEqual(r.bottom - 2, r2.bottom) + self.assertEqual(r.width / 2, r2.width) + self.assertEqual(r.height / 2, r2.height) + + def test_scale_by_ip__subzero(self): + """The scale method scales around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r.scale_by_ip(0) + r.scale_by_ip(-1) + r.scale_by_ip(-0.000001) + r.scale_by_ip(0.00001) + + def test_scale_by_ip__kwargs(self): + """The scale method scales around the center of the rectangle""" + r = Rect(2, 4, 6, 8) + r2 = Rect(r) + r2.scale_by_ip(x=2, y=4) + + # assert + self.assertEqual(r.center, r2.center) + self.assertEqual(r.left - 3, r2.left) + self.assertEqual(r.centery - r.h * 4 / 2, r2.top) + self.assertEqual(r.right + 3, r2.right) + self.assertEqual(r.centery + r.h * 4 / 2, r2.bottom) + self.assertEqual(r.width * 2, r2.width) + self.assertEqual(r.height * 4, r2.height) + + def test_scale_by_ip__kwarg_exceptions(self): + """The scale method scales around the center of the rectangle using + keyword argument 'scale_by'. Tests for incorrect keyword args""" + r = Rect(2, 4, 6, 8) + + with self.assertRaises(TypeError): + r.scale_by_ip(scale_by=2) + + with self.assertRaises(TypeError): + r.scale_by_ip(scale_by=(1, 2), y=1) + + def test_clamp(self): + r = Rect(10, 10, 10, 10) + c = Rect(19, 12, 5, 5).clamp(r) + self.assertEqual(c.right, r.right) + self.assertEqual(c.top, 12) + c = Rect(1, 2, 3, 4).clamp(r) + self.assertEqual(c.topleft, r.topleft) + c = Rect(5, 500, 22, 33).clamp(r) + self.assertEqual(c.center, r.center) + + def test_clamp_ip(self): + r = Rect(10, 10, 10, 10) + c = Rect(19, 12, 5, 5) + c.clamp_ip(r) + self.assertEqual(c.right, r.right) + self.assertEqual(c.top, 12) + c = Rect(1, 2, 3, 4) + c.clamp_ip(r) + self.assertEqual(c.topleft, r.topleft) + c = Rect(5, 500, 22, 33) + c.clamp_ip(r) + self.assertEqual(c.center, r.center) + + def test_clip(self): + r1 = Rect(1, 2, 3, 4) + self.assertEqual(Rect(1, 2, 2, 2), r1.clip(Rect(0, 0, 3, 4))) + self.assertEqual(Rect(2, 2, 2, 4), r1.clip(Rect(2, 2, 10, 20))) + self.assertEqual(Rect(2, 3, 1, 2), r1.clip(Rect(2, 3, 1, 2))) + self.assertEqual((0, 0), r1.clip(20, 30, 5, 6).size) + self.assertEqual( + r1, r1.clip(Rect(r1)), "r1 does not clip an identical rect to itself" + ) + + def test_clipline(self): + """Ensures clipline handles four int parameters. + + Tests the clipline(x1, y1, x2, y2) format. + """ + rect = Rect((1, 2), (35, 40)) + x1 = 5 + y1 = 6 + x2 = 11 + y2 = 19 + expected_line = ((x1, y1), (x2, y2)) + + clipped_line = rect.clipline(x1, y1, x2, y2) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__two_sequences(self): + """Ensures clipline handles a sequence of two sequences. + + Tests the clipline((x1, y1), (x2, y2)) format. + Tests the sequences as different types. + """ + rect = Rect((1, 2), (35, 40)) + pt1 = (5, 6) + pt2 = (11, 19) + + INNER_SEQUENCES = (list, tuple, Vector2) + expected_line = (pt1, pt2) + + for inner_seq1 in INNER_SEQUENCES: + endpt1 = inner_seq1(pt1) + + for inner_seq2 in INNER_SEQUENCES: + clipped_line = rect.clipline((endpt1, inner_seq2(pt2))) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__two_sequences_kwarg(self): + """Ensures clipline handles a sequence of two sequences using kwargs. + + Tests the clipline((x1, y1), (x2, y2)) format. + Tests the sequences as different types. + """ + rect = Rect((1, 2), (35, 40)) + pt1 = (5, 6) + pt2 = (11, 19) + + INNER_SEQUENCES = (list, tuple, Vector2) + expected_line = (pt1, pt2) + + for inner_seq1 in INNER_SEQUENCES: + endpt1 = inner_seq1(pt1) + + for inner_seq2 in INNER_SEQUENCES: + clipped_line = rect.clipline( + first_coordinate=endpt1, second_coordinate=inner_seq2(pt2) + ) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__sequence_of_four_ints(self): + """Ensures clipline handles a sequence of four ints. + + Tests the clipline((x1, y1, x2, y2)) format. + Tests the sequence as different types. + """ + rect = Rect((1, 2), (35, 40)) + line = (5, 6, 11, 19) + expected_line = ((line[0], line[1]), (line[2], line[3])) + + for outer_seq in (list, tuple): + clipped_line = rect.clipline(outer_seq(line)) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__sequence_of_four_ints_kwargs(self): + """Ensures clipline handles a sequence of four ints using kwargs. + + Tests the clipline((x1, y1, x2, y2)) format. + Tests the sequence as different types. + """ + rect = Rect((1, 2), (35, 40)) + line = (5, 6, 11, 19) + expected_line = ((line[0], line[1]), (line[2], line[3])) + + for outer_seq in (list, tuple): + clipped_line = rect.clipline(rect_arg=outer_seq(line)) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__sequence_of_two_sequences(self): + """Ensures clipline handles a sequence of two sequences. + + Tests the clipline(((x1, y1), (x2, y2))) format. + Tests the sequences as different types. + """ + rect = Rect((1, 2), (35, 40)) + pt1 = (5, 6) + pt2 = (11, 19) + + INNER_SEQUENCES = (list, tuple, Vector2) + expected_line = (pt1, pt2) + + for inner_seq1 in INNER_SEQUENCES: + endpt1 = inner_seq1(pt1) + + for inner_seq2 in INNER_SEQUENCES: + endpt2 = inner_seq2(pt2) + + for outer_seq in (list, tuple): + clipped_line = rect.clipline(outer_seq((endpt1, endpt2))) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__sequence_of_two_sequences_kwargs(self): + """Ensures clipline handles a sequence of two sequences using kwargs. + + Tests the clipline(((x1, y1), (x2, y2))) format. + Tests the sequences as different types. + """ + rect = Rect((1, 2), (35, 40)) + pt1 = (5, 6) + pt2 = (11, 19) + + INNER_SEQUENCES = (list, tuple, Vector2) + expected_line = (pt1, pt2) + + for inner_seq1 in INNER_SEQUENCES: + endpt1 = inner_seq1(pt1) + + for inner_seq2 in INNER_SEQUENCES: + endpt2 = inner_seq2(pt2) + + for outer_seq in (list, tuple): + clipped_line = rect.clipline(x1=outer_seq((endpt1, endpt2))) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__floats(self): + """Ensures clipline handles float parameters.""" + rect = Rect((1, 2), (35, 40)) + x1 = 5.9 + y1 = 6.9 + x2 = 11.9 + y2 = 19.9 + + # Floats are truncated. + expected_line = ( + (math.floor(x1), math.floor(y1)), + (math.floor(x2), math.floor(y2)), + ) + + clipped_line = rect.clipline(x1, y1, x2, y2) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__floats_kwargs(self): + """Ensures clipline handles four float parameters. + + Tests the clipline(x1, y1, x2, y2) format. + """ + rect = Rect((1, 2), (35, 40)) + x1 = 5.9 + y1 = 6.9 + x2 = 11.9 + y2 = 19.9 + + # Floats are truncated. + expected_line = ( + (math.floor(x1), math.floor(y1)), + (math.floor(x2), math.floor(y2)), + ) + + clipped_line = rect.clipline(x1=x1, x2=y1, x3=x2, x4=y2) + + self.assertIsInstance(clipped_line, tuple) + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__kwarg_exceptions(self): + """Ensure clipline handles incorrect keyword arguments""" + r = Rect(2, 4, 6, 8) + + with self.assertRaises(TypeError): + r.clipline(x1=0) + + with self.assertRaises(TypeError): + r.clipline(first_coordinate=(1, 3, 5, 4), second_coordinate=(1, 2)) + + with self.assertRaises(TypeError): + r.clipline(first_coordinate=(1, 3), second_coordinate=(2, 2), x1=1) + + with self.assertRaises(TypeError): + r.clipline(rect_arg=(1, 3, 5)) + + with self.assertRaises(TypeError): + r.clipline(rect_arg=(1, 3, 5, 4), second_coordinate=(2, 2)) + + def test_clipline__no_overlap(self): + """Ensures lines that do not overlap the rect are not clipped.""" + rect = Rect((10, 25), (15, 20)) + # Use a bigger rect to help create test lines. + big_rect = rect.inflate(2, 2) + lines = ( + (big_rect.bottomleft, big_rect.topleft), # Left edge. + (big_rect.topleft, big_rect.topright), # Top edge. + (big_rect.topright, big_rect.bottomright), # Right edge. + (big_rect.bottomright, big_rect.bottomleft), + ) # Bottom edge. + expected_line = () + + # Test lines outside rect. + for line in lines: + clipped_line = rect.clipline(line) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__both_endpoints_outside(self): + """Ensures lines that overlap the rect are clipped. + + Testing lines with both endpoints outside the rect. + """ + rect = Rect((0, 0), (20, 20)) + # Use a bigger rect to help create test lines. + big_rect = rect.inflate(2, 2) + + # Create a dict of lines and expected results. + line_dict = { + (big_rect.midleft, big_rect.midright): ( + rect.midleft, + (rect.midright[0] - 1, rect.midright[1]), + ), + (big_rect.midtop, big_rect.midbottom): ( + rect.midtop, + (rect.midbottom[0], rect.midbottom[1] - 1), + ), + # Diagonals. + (big_rect.topleft, big_rect.bottomright): ( + rect.topleft, + (rect.bottomright[0] - 1, rect.bottomright[1] - 1), + ), + # This line needs a small adjustment to make sure it intersects + # the rect correctly. + ( + (big_rect.topright[0] - 1, big_rect.topright[1]), + (big_rect.bottomleft[0], big_rect.bottomleft[1] - 1), + ): ( + (rect.topright[0] - 1, rect.topright[1]), + (rect.bottomleft[0], rect.bottomleft[1] - 1), + ), + } + + for line, expected_line in line_dict.items(): + clipped_line = rect.clipline(line) + + self.assertTupleEqual(clipped_line, expected_line) + + # Swap endpoints to test for symmetry. + expected_line = (expected_line[1], expected_line[0]) + + clipped_line = rect.clipline((line[1], line[0])) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__both_endpoints_inside(self): + """Ensures lines that overlap the rect are clipped. + + Testing lines with both endpoints inside the rect. + """ + rect = Rect((-10, -5), (20, 20)) + # Use a smaller rect to help create test lines. + small_rect = rect.inflate(-2, -2) + + lines = ( + (small_rect.midleft, small_rect.midright), + (small_rect.midtop, small_rect.midbottom), + # Diagonals. + (small_rect.topleft, small_rect.bottomright), + (small_rect.topright, small_rect.bottomleft), + ) + + for line in lines: + expected_line = line + + clipped_line = rect.clipline(line) + + self.assertTupleEqual(clipped_line, expected_line) + + # Swap endpoints to test for symmetry. + expected_line = (expected_line[1], expected_line[0]) + + clipped_line = rect.clipline((line[1], line[0])) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__endpoints_inside_and_outside(self): + """Ensures lines that overlap the rect are clipped. + + Testing lines with one endpoint outside the rect and the other is + inside the rect. + """ + rect = Rect((0, 0), (21, 21)) + # Use a bigger rect to help create test lines. + big_rect = rect.inflate(2, 2) + + # Create a dict of lines and expected results. + line_dict = { + (big_rect.midleft, rect.center): (rect.midleft, rect.center), + (big_rect.midtop, rect.center): (rect.midtop, rect.center), + (big_rect.midright, rect.center): ( + (rect.midright[0] - 1, rect.midright[1]), + rect.center, + ), + (big_rect.midbottom, rect.center): ( + (rect.midbottom[0], rect.midbottom[1] - 1), + rect.center, + ), + # Diagonals. + (big_rect.topleft, rect.center): (rect.topleft, rect.center), + (big_rect.topright, rect.center): ( + (rect.topright[0] - 1, rect.topright[1]), + rect.center, + ), + (big_rect.bottomright, rect.center): ( + (rect.bottomright[0] - 1, rect.bottomright[1] - 1), + rect.center, + ), + # This line needs a small adjustment to make sure it intersects + # the rect correctly. + ((big_rect.bottomleft[0], big_rect.bottomleft[1] - 1), rect.center): ( + (rect.bottomleft[0], rect.bottomleft[1] - 1), + rect.center, + ), + } + + for line, expected_line in line_dict.items(): + clipped_line = rect.clipline(line) + + self.assertTupleEqual(clipped_line, expected_line) + + # Swap endpoints to test for symmetry. + expected_line = (expected_line[1], expected_line[0]) + + clipped_line = rect.clipline((line[1], line[0])) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__edges(self): + """Ensures clipline properly clips line that are along the rect edges.""" + rect = Rect((10, 25), (15, 20)) + + # Create a dict of edges and expected results. + edge_dict = { + # Left edge. + (rect.bottomleft, rect.topleft): ( + (rect.bottomleft[0], rect.bottomleft[1] - 1), + rect.topleft, + ), + # Top edge. + (rect.topleft, rect.topright): ( + rect.topleft, + (rect.topright[0] - 1, rect.topright[1]), + ), + # Right edge. + (rect.topright, rect.bottomright): (), + # Bottom edge. + (rect.bottomright, rect.bottomleft): (), + } + + for edge, expected_line in edge_dict.items(): + clipped_line = rect.clipline(edge) + + self.assertTupleEqual(clipped_line, expected_line) + + # Swap endpoints to test for symmetry. + if expected_line: + expected_line = (expected_line[1], expected_line[0]) + + clipped_line = rect.clipline((edge[1], edge[0])) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__equal_endpoints_with_overlap(self): + """Ensures clipline handles lines with both endpoints the same. + + Testing lines that overlap the rect. + """ + rect = Rect((10, 25), (15, 20)) + + # Test all the points in and on a rect. + pts = ( + (x, y) + for x in range(rect.left, rect.right) + for y in range(rect.top, rect.bottom) + ) + + for pt in pts: + expected_line = (pt, pt) + + clipped_line = rect.clipline((pt, pt)) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__equal_endpoints_no_overlap(self): + """Ensures clipline handles lines with both endpoints the same. + + Testing lines that do not overlap the rect. + """ + expected_line = () + rect = Rect((10, 25), (15, 20)) + + # Test points outside rect. + for pt in test_utils.rect_perimeter_pts(rect.inflate(2, 2)): + clipped_line = rect.clipline((pt, pt)) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__zero_size_rect(self): + """Ensures clipline handles zero sized rects correctly.""" + expected_line = () + + for size in ((0, 15), (15, 0), (0, 0)): + rect = Rect((10, 25), size) + + clipped_line = rect.clipline(rect.topleft, rect.topleft) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__negative_size_rect(self): + """Ensures clipline handles negative sized rects correctly.""" + expected_line = () + + for size in ((-15, 20), (15, -20), (-15, -20)): + rect = Rect((10, 25), size) + norm_rect = rect.copy() + norm_rect.normalize() + # Use a bigger rect to help create test lines. + big_rect = norm_rect.inflate(2, 2) + + # Create a dict of lines and expected results. Some line have both + # endpoints outside the rect and some have one inside and one + # outside. + line_dict = { + (big_rect.midleft, big_rect.midright): ( + norm_rect.midleft, + (norm_rect.midright[0] - 1, norm_rect.midright[1]), + ), + (big_rect.midtop, big_rect.midbottom): ( + norm_rect.midtop, + (norm_rect.midbottom[0], norm_rect.midbottom[1] - 1), + ), + (big_rect.midleft, norm_rect.center): ( + norm_rect.midleft, + norm_rect.center, + ), + (big_rect.midtop, norm_rect.center): ( + norm_rect.midtop, + norm_rect.center, + ), + (big_rect.midright, norm_rect.center): ( + (norm_rect.midright[0] - 1, norm_rect.midright[1]), + norm_rect.center, + ), + (big_rect.midbottom, norm_rect.center): ( + (norm_rect.midbottom[0], norm_rect.midbottom[1] - 1), + norm_rect.center, + ), + } + + for line, expected_line in line_dict.items(): + clipped_line = rect.clipline(line) + + # Make sure rect wasn't normalized. + self.assertNotEqual(rect, norm_rect) + self.assertTupleEqual(clipped_line, expected_line) + + # Swap endpoints to test for symmetry. + expected_line = (expected_line[1], expected_line[0]) + + clipped_line = rect.clipline((line[1], line[0])) + + self.assertTupleEqual(clipped_line, expected_line) + + def test_clipline__invalid_line(self): + """Ensures clipline handles invalid lines correctly.""" + rect = Rect((0, 0), (10, 20)) + invalid_lines = ( + (), + (1,), + (1, 2), + (1, 2, 3), + (1, 2, 3, 4, 5), + ((1, 2),), + ((1, 2), (3,)), + ((1, 2), 3), + ((1, 2, 5), (3, 4)), + ((1, 2), (3, 4, 5)), + ((1, 2), (3, 4), (5, 6)), + ) + + for line in invalid_lines: + with self.assertRaises(TypeError): + clipped_line = rect.clipline(line) + + with self.assertRaises(TypeError): + clipped_line = rect.clipline(*line) + + def test_move(self): + r = Rect(1, 2, 3, 4) + move_x = 10 + move_y = 20 + r2 = r.move(move_x, move_y) + expected_r2 = Rect(r.left + move_x, r.top + move_y, r.width, r.height) + self.assertEqual(expected_r2, r2) + + def test_move_ip(self): + r = Rect(1, 2, 3, 4) + r2 = Rect(r) + move_x = 10 + move_y = 20 + r2.move_ip(move_x, move_y) + expected_r2 = Rect(r.left + move_x, r.top + move_y, r.width, r.height) + self.assertEqual(expected_r2, r2) + + @unittest.skipIf( + IS_PYPY, "fails on pypy (but only for: bottom, right, centerx, centery)" + ) + def test_set_float_values(self): + zero = 0 + pos = 124 + neg = -432 + # (initial, increment, expected, other) + data_rows = [ + (zero, 0.1, zero, _random_int()), + (zero, 0.4, zero, _random_int()), + (zero, 0.5, zero + 1, _random_int()), + (zero, 1.1, zero + 1, _random_int()), + (zero, 1.5, zero + 2, _random_int()), # >0f + (zero, -0.1, zero, _random_int()), + (zero, -0.4, zero, _random_int()), + (zero, -0.5, zero - 1, _random_int()), + (zero, -0.6, zero - 1, _random_int()), + (zero, -1.6, zero - 2, _random_int()), # <0f + (zero, 1, zero + 1, _random_int()), + (zero, 4, zero + 4, _random_int()), # >0i + (zero, -1, zero - 1, _random_int()), + (zero, -4, zero - 4, _random_int()), # <0i + (pos, 0.1, pos, _random_int()), + (pos, 0.4, pos, _random_int()), + (pos, 0.5, pos + 1, _random_int()), + (pos, 1.1, pos + 1, _random_int()), + (pos, 1.5, pos + 2, _random_int()), # >0f + (pos, -0.1, pos, _random_int()), + (pos, -0.4, pos, _random_int()), + (pos, -0.5, pos, _random_int()), + (pos, -0.6, pos - 1, _random_int()), + (pos, -1.6, pos - 2, _random_int()), # <0f + (pos, 1, pos + 1, _random_int()), + (pos, 4, pos + 4, _random_int()), # >0i + (pos, -1, pos - 1, _random_int()), + (pos, -4, pos - 4, _random_int()), # <0i + (neg, 0.1, neg, _random_int()), + (neg, 0.4, neg, _random_int()), + (neg, 0.5, neg, _random_int()), + (neg, 1.1, neg + 1, _random_int()), + (neg, 1.5, neg + 1, _random_int()), # >0f + (neg, -0.1, neg, _random_int()), + (neg, -0.4, neg, _random_int()), + (neg, -0.5, neg - 1, _random_int()), + (neg, -0.6, neg - 1, _random_int()), + (neg, -1.6, neg - 2, _random_int()), # <0f + (neg, 1, neg + 1, _random_int()), + (neg, 4, neg + 4, _random_int()), # >0i + (neg, -1, neg - 1, _random_int()), + (neg, -4, neg - 4, _random_int()), # <0i + ] + + single_value_attribute_names = [ + "x", + "y", + "w", + "h", + "width", + "height", + "top", + "left", + "bottom", + "right", + "centerx", + "centery", + ] + + tuple_value_attribute_names = [ + "topleft", + "topright", + "bottomleft", + "bottomright", + "midtop", + "midleft", + "midbottom", + "midright", + "size", + "center", + ] + + for row in data_rows: + initial, inc, expected, other = row + new_value = initial + inc + for attribute_name in single_value_attribute_names: + with self.subTest(row=row, name=f"r.{attribute_name}"): + actual = Rect( + _random_int(), _random_int(), _random_int(), _random_int() + ) + # act + setattr(actual, attribute_name, new_value) + # assert + self.assertEqual(expected, getattr(actual, attribute_name)) + + for attribute_name in tuple_value_attribute_names: + with self.subTest(row=row, name=f"r.{attribute_name}[0]"): + actual = Rect( + _random_int(), _random_int(), _random_int(), _random_int() + ) + # act + setattr(actual, attribute_name, (new_value, other)) + # assert + self.assertEqual((expected, other), getattr(actual, attribute_name)) + + with self.subTest(row=row, name=f"r.{attribute_name}[1]"): + actual = Rect( + _random_int(), _random_int(), _random_int(), _random_int() + ) + # act + setattr(actual, attribute_name, (other, new_value)) + # assert + self.assertEqual((other, expected), getattr(actual, attribute_name)) + + def test_set_out_of_range_number_raises_exception(self): + i = 0 + # (initial, expected) + data_rows = [ + (_int_max + 1, TypeError), + (_int_max + 0.00001, TypeError), + (_int_max, None), + (_int_max - 1, None), + (_int_max - 2, None), + (_int_max - 10, None), + (_int_max - 63, None), + (_int_max - 64, None), + (_int_max - 65, None), + (_int_min - 1, TypeError), + (_int_min - 0.00001, TypeError), + (_int_min, None), + (_int_min + 1, None), + (_int_min + 2, None), + (_int_min + 10, None), + (_int_min + 62, None), + (_int_min + 63, None), + (_int_min + 64, None), + (0, None), + (100000, None), + (-100000, None), + ] + + single_attribute_names = [ + "x", + "y", + "w", + "h", + "width", + "height", + "top", + "left", + "bottom", + "right", + "centerx", + "centery", + ] + + tuple_value_attribute_names = [ + "topleft", + "topright", + "bottomleft", + "bottomright", + "midtop", + "midleft", + "midbottom", + "midright", + "size", + "center", + ] + + for row in data_rows: + for attribute_name in single_attribute_names: + value, expected = row + with self.subTest(row=row, name=f"r.{attribute_name}"): + actual = Rect(0, 0, 0, 0) + if expected: + # act/ assert + self.assertRaises( + TypeError, setattr, actual, attribute_name, value + ) + else: + # act + setattr(actual, attribute_name, value) + # assert + self.assertEqual(value, getattr(actual, attribute_name)) + other = _random_int() + + for attribute_name in tuple_value_attribute_names: + value, expected = row + with self.subTest(row=row, name=f"r.{attribute_name}[0]"): + actual = Rect(0, 0, 0, 0) + # act/ assert + if expected: + # act/ assert + self.assertRaises( + TypeError, setattr, actual, attribute_name, (value, other) + ) + else: + # act + setattr(actual, attribute_name, (value, other)) + # assert + self.assertEqual( + (value, other), getattr(actual, attribute_name) + ) + with self.subTest(row=row, name=f"r.{attribute_name}[1]"): + actual = Rect(0, 0, 0, 0) + # act/ assert + if expected: + # act/ assert + self.assertRaises( + TypeError, setattr, actual, attribute_name, (other, value) + ) + else: + # act + setattr(actual, attribute_name, (other, value)) + # assert + self.assertEqual( + (other, value), getattr(actual, attribute_name) + ) + + def test_update_XYWidthHeight(self): + """Test update with 4 int values(x, y, w, h)""" + rect = Rect(0, 0, 1, 1) + rect.update(1, 2, 3, 4) + + self.assertEqual(1, rect.left) + self.assertEqual(2, rect.top) + self.assertEqual(3, rect.width) + self.assertEqual(4, rect.height) + + def test_update__TopLeftSize(self): + """Test update with 2 tuples((x, y), (w, h))""" + rect = Rect(0, 0, 1, 1) + rect.update((1, 2), (3, 4)) + + self.assertEqual(1, rect.left) + self.assertEqual(2, rect.top) + self.assertEqual(3, rect.width) + self.assertEqual(4, rect.height) + + def test_update__List(self): + """Test update with list""" + rect = Rect(0, 0, 1, 1) + rect2 = [1, 2, 3, 4] + rect.update(rect2) + + self.assertEqual(1, rect.left) + self.assertEqual(2, rect.top) + self.assertEqual(3, rect.width) + self.assertEqual(4, rect.height) + + def test_update__RectObject(self): + """Test update with other rect object""" + rect = Rect(0, 0, 1, 1) + rect2 = Rect(1, 2, 3, 4) + rect.update(rect2) + + self.assertEqual(1, rect.left) + self.assertEqual(2, rect.top) + self.assertEqual(3, rect.width) + self.assertEqual(4, rect.height) + + def test_union(self): + r1 = Rect(1, 1, 1, 2) + r2 = Rect(-2, -2, 1, 2) + self.assertEqual(Rect(-2, -2, 4, 5), r1.union(r2)) + + def test_union__with_identical_Rect(self): + r1 = Rect(1, 2, 3, 4) + self.assertEqual(r1, r1.union(Rect(r1))) + + def test_union_ip(self): + r1 = Rect(1, 1, 1, 2) + r2 = Rect(-2, -2, 1, 2) + r1.union_ip(r2) + self.assertEqual(Rect(-2, -2, 4, 5), r1) + + def test_unionall(self): + r1 = Rect(0, 0, 1, 1) + r2 = Rect(-2, -2, 1, 1) + r3 = Rect(2, 2, 1, 1) + + r4 = r1.unionall([r2, r3]) + self.assertEqual(Rect(-2, -2, 5, 5), r4) + + def test_unionall__invalid_rect_format(self): + """Ensures unionall correctly handles invalid rect parameters.""" + numbers = [0, 1.2, 2, 3.3] + strs = ["a", "b", "c"] + nones = [None, None] + + for invalid_rects in (numbers, strs, nones): + with self.assertRaises(TypeError): + Rect(0, 0, 1, 1).unionall(invalid_rects) + + def test_unionall__kwargs(self): + r1 = Rect(0, 0, 1, 1) + r2 = Rect(-2, -2, 1, 1) + r3 = Rect(2, 2, 1, 1) + + r4 = r1.unionall(rect=[r2, r3]) + self.assertEqual(Rect(-2, -2, 5, 5), r4) + + def test_unionall_ip(self): + r1 = Rect(0, 0, 1, 1) + r2 = Rect(-2, -2, 1, 1) + r3 = Rect(2, 2, 1, 1) + + r1.unionall_ip([r2, r3]) + self.assertEqual(Rect(-2, -2, 5, 5), r1) + + # Bug for an empty list. Would return a Rect instead of None. + self.assertTrue(r1.unionall_ip([]) is None) + + def test_unionall_ip__kwargs(self): + r1 = Rect(0, 0, 1, 1) + r2 = Rect(-2, -2, 1, 1) + r3 = Rect(2, 2, 1, 1) + + r1.unionall_ip(rects=[r2, r3]) + self.assertEqual(Rect(-2, -2, 5, 5), r1) + + # Bug for an empty list. Would return a Rect instead of None. + self.assertTrue(r1.unionall_ip([]) is None) + + def test_colliderect(self): + r1 = Rect(1, 2, 3, 4) + self.assertTrue( + r1.colliderect(Rect(0, 0, 2, 3)), + "r1 does not collide with Rect(0, 0, 2, 3)", + ) + self.assertFalse( + r1.colliderect(Rect(0, 0, 1, 2)), "r1 collides with Rect(0, 0, 1, 2)" + ) + self.assertFalse( + r1.colliderect(Rect(r1.right, r1.bottom, 2, 2)), + "r1 collides with Rect(r1.right, r1.bottom, 2, 2)", + ) + self.assertTrue( + r1.colliderect(Rect(r1.left + 1, r1.top + 1, r1.width - 2, r1.height - 2)), + "r1 does not collide with Rect(r1.left + 1, r1.top + 1, " + + "r1.width - 2, r1.height - 2)", + ) + self.assertTrue( + r1.colliderect(Rect(r1.left - 1, r1.top - 1, r1.width + 2, r1.height + 2)), + "r1 does not collide with Rect(r1.left - 1, r1.top - 1, " + + "r1.width + 2, r1.height + 2)", + ) + self.assertTrue( + r1.colliderect(Rect(r1)), "r1 does not collide with an identical rect" + ) + self.assertFalse( + r1.colliderect(Rect(r1.right, r1.bottom, 0, 0)), + "r1 collides with Rect(r1.right, r1.bottom, 0, 0)", + ) + self.assertFalse( + r1.colliderect(Rect(r1.right, r1.bottom, 1, 1)), + "r1 collides with Rect(r1.right, r1.bottom, 1, 1)", + ) + + def testEquals(self): + """check to see how the rect uses __eq__""" + r1 = Rect(1, 2, 3, 4) + r2 = Rect(10, 20, 30, 40) + r3 = (10, 20, 30, 40) + r4 = Rect(10, 20, 30, 40) + + class foo(Rect): + def __eq__(self, other): + return id(self) == id(other) + + def __ne__(self, other): + return id(self) != id(other) + + class foo2(Rect): + pass + + r5 = foo(10, 20, 30, 40) + r6 = foo2(10, 20, 30, 40) + + self.assertNotEqual(r5, r2) + + # because we define equality differently for this subclass. + self.assertEqual(r6, r2) + + rect_list = [r1, r2, r3, r4, r6] + + # see if we can remove 4 of these. + rect_list.remove(r2) + rect_list.remove(r2) + rect_list.remove(r2) + rect_list.remove(r2) + self.assertRaises(ValueError, rect_list.remove, r2) + + def test_collidedict(self): + """Ensures collidedict detects collisions.""" + rect = Rect(1, 1, 10, 10) + + collide_item1 = ("collide 1", rect.copy()) + collide_item2 = ("collide 2", Rect(5, 5, 10, 10)) + no_collide_item1 = ("no collide 1", Rect(60, 60, 10, 10)) + no_collide_item2 = ("no collide 2", Rect(70, 70, 10, 10)) + + # Dict to check collisions with values. + rect_values = dict( + (collide_item1, collide_item2, no_collide_item1, no_collide_item2) + ) + value_collide_items = (collide_item1, collide_item2) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + key_collide_items = tuple((tuple(v), k) for k, v in value_collide_items) + + for use_values in (True, False): + if use_values: + expected_items = value_collide_items + d = rect_values + else: + expected_items = key_collide_items + d = rect_keys + + collide_item = rect.collidedict(d, use_values) + + # The detected collision could be any of the possible items. + self.assertIn(collide_item, expected_items) + + def test_collidedict__no_collision(self): + """Ensures collidedict returns None when no collisions.""" + rect = Rect(1, 1, 10, 10) + + no_collide_item1 = ("no collide 1", Rect(50, 50, 10, 10)) + no_collide_item2 = ("no collide 2", Rect(60, 60, 10, 10)) + no_collide_item3 = ("no collide 3", Rect(70, 70, 10, 10)) + + # Dict to check collisions with values. + rect_values = dict((no_collide_item1, no_collide_item2, no_collide_item3)) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + + for use_values in (True, False): + d = rect_values if use_values else rect_keys + + collide_item = rect.collidedict(d, use_values) + + self.assertIsNone(collide_item) + + def test_collidedict__barely_touching(self): + """Ensures collidedict works correctly for rects that barely touch.""" + rect = Rect(1, 1, 10, 10) + # Small rect to test barely touching collisions. + collide_rect = Rect(0, 0, 1, 1) + + collide_item1 = ("collide 1", collide_rect) + no_collide_item1 = ("no collide 1", Rect(50, 50, 10, 10)) + no_collide_item2 = ("no collide 2", Rect(60, 60, 10, 10)) + no_collide_item3 = ("no collide 3", Rect(70, 70, 10, 10)) + + # Dict to check collisions with values. + no_collide_rect_values = dict( + (no_collide_item1, no_collide_item2, no_collide_item3) + ) + + # Dict to check collisions with keys. + no_collide_rect_keys = {tuple(v): k for k, v in no_collide_rect_values.items()} + + # Tests the collide_rect on each of the rect's corners. + for attr in ("topleft", "topright", "bottomright", "bottomleft"): + setattr(collide_rect, attr, getattr(rect, attr)) + + for use_values in (True, False): + if use_values: + expected_item = collide_item1 + d = dict(no_collide_rect_values) + else: + expected_item = (tuple(collide_item1[1]), collide_item1[0]) + d = dict(no_collide_rect_keys) + + d.update((expected_item,)) # Add in the expected item. + + collide_item = rect.collidedict(d, use_values) + + self.assertTupleEqual(collide_item, expected_item) + + def test_collidedict__zero_sized_rects(self): + """Ensures collidedict works correctly with zero sized rects. + + There should be no collisions with zero sized rects. + """ + zero_rect1 = Rect(1, 1, 0, 0) + zero_rect2 = Rect(1, 1, 1, 0) + zero_rect3 = Rect(1, 1, 0, 1) + zero_rect4 = Rect(1, 1, -1, 0) + zero_rect5 = Rect(1, 1, 0, -1) + + no_collide_item1 = ("no collide 1", zero_rect1.copy()) + no_collide_item2 = ("no collide 2", zero_rect2.copy()) + no_collide_item3 = ("no collide 3", zero_rect3.copy()) + no_collide_item4 = ("no collide 4", zero_rect4.copy()) + no_collide_item5 = ("no collide 5", zero_rect5.copy()) + no_collide_item6 = ("no collide 6", Rect(0, 0, 10, 10)) + no_collide_item7 = ("no collide 7", Rect(0, 0, 2, 2)) + + # Dict to check collisions with values. + rect_values = dict( + ( + no_collide_item1, + no_collide_item2, + no_collide_item3, + no_collide_item4, + no_collide_item5, + no_collide_item6, + no_collide_item7, + ) + ) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + + for use_values in (True, False): + d = rect_values if use_values else rect_keys + + for zero_rect in ( + zero_rect1, + zero_rect2, + zero_rect3, + zero_rect4, + zero_rect5, + ): + collide_item = zero_rect.collidedict(d, use_values) + + self.assertIsNone(collide_item) + + def test_collidedict__zero_sized_rects_as_args(self): + """Ensures collidedict works correctly with zero sized rects as args. + + There should be no collisions with zero sized rects. + """ + rect = Rect(0, 0, 10, 10) + + no_collide_item1 = ("no collide 1", Rect(1, 1, 0, 0)) + no_collide_item2 = ("no collide 2", Rect(1, 1, 1, 0)) + no_collide_item3 = ("no collide 3", Rect(1, 1, 0, 1)) + + # Dict to check collisions with values. + rect_values = dict((no_collide_item1, no_collide_item2, no_collide_item3)) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + + for use_values in (True, False): + d = rect_values if use_values else rect_keys + + collide_item = rect.collidedict(d, use_values) + + self.assertIsNone(collide_item) + + def test_collidedict__negative_sized_rects(self): + """Ensures collidedict works correctly with negative sized rects.""" + neg_rect = Rect(1, 1, -1, -1) + + collide_item1 = ("collide 1", neg_rect.copy()) + collide_item2 = ("collide 2", Rect(0, 0, 10, 10)) + no_collide_item1 = ("no collide 1", Rect(1, 1, 10, 10)) + + # Dict to check collisions with values. + rect_values = dict((collide_item1, collide_item2, no_collide_item1)) + value_collide_items = (collide_item1, collide_item2) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + key_collide_items = tuple((tuple(v), k) for k, v in value_collide_items) + + for use_values in (True, False): + if use_values: + collide_items = value_collide_items + d = rect_values + else: + collide_items = key_collide_items + d = rect_keys + + collide_item = neg_rect.collidedict(d, use_values) + + # The detected collision could be any of the possible items. + self.assertIn(collide_item, collide_items) + + def test_collidedict__negative_sized_rects_as_args(self): + """Ensures collidedict works correctly with negative sized rect args.""" + rect = Rect(0, 0, 10, 10) + + collide_item1 = ("collide 1", Rect(1, 1, -1, -1)) + no_collide_item1 = ("no collide 1", Rect(1, 1, -1, 0)) + no_collide_item2 = ("no collide 2", Rect(1, 1, 0, -1)) + + # Dict to check collisions with values. + rect_values = dict((collide_item1, no_collide_item1, no_collide_item2)) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + + for use_values in (True, False): + if use_values: + expected_item = collide_item1 + d = rect_values + else: + expected_item = (tuple(collide_item1[1]), collide_item1[0]) + d = rect_keys + + collide_item = rect.collidedict(d, use_values) + + self.assertTupleEqual(collide_item, expected_item) + + def test_collidedict__invalid_dict_format(self): + """Ensures collidedict correctly handles invalid dict parameters.""" + rect = Rect(0, 0, 10, 10) + + invalid_value_dict = ("collide", rect.copy()) + invalid_key_dict = tuple(invalid_value_dict[1]), invalid_value_dict[0] + + for use_values in (True, False): + d = invalid_value_dict if use_values else invalid_key_dict + + with self.assertRaises(TypeError): + collide_item = rect.collidedict(d, use_values) + + def test_collidedict__invalid_dict_value_format(self): + """Ensures collidedict correctly handles dicts with invalid values.""" + rect = Rect(0, 0, 10, 10) + rect_keys = {tuple(rect): "collide"} + + with self.assertRaises(TypeError): + collide_item = rect.collidedict(rect_keys, 1) + + def test_collidedict__invalid_dict_key_format(self): + """Ensures collidedict correctly handles dicts with invalid keys.""" + rect = Rect(0, 0, 10, 10) + rect_values = {"collide": rect.copy()} + + with self.assertRaises(TypeError): + collide_item = rect.collidedict(rect_values) + + def test_collidedict__invalid_use_values_format(self): + """Ensures collidedict correctly handles invalid use_values parameters.""" + rect = Rect(0, 0, 1, 1) + d = {} + + for invalid_param in (None, d, 1.1): + with self.assertRaises(TypeError): + collide_item = rect.collidedict(d, invalid_param) + + def test_collidedict__kwargs(self): + """Ensures collidedict detects collisions via keyword arguments.""" + rect = Rect(1, 1, 10, 10) + + collide_item1 = ("collide 1", rect.copy()) + collide_item2 = ("collide 2", Rect(5, 5, 10, 10)) + no_collide_item1 = ("no collide 1", Rect(60, 60, 10, 10)) + no_collide_item2 = ("no collide 2", Rect(70, 70, 10, 10)) + + # Dict to check collisions with values. + rect_values = dict( + (collide_item1, collide_item2, no_collide_item1, no_collide_item2) + ) + value_collide_items = (collide_item1, collide_item2) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + key_collide_items = tuple((tuple(v), k) for k, v in value_collide_items) + + for use_values in (True, False): + if use_values: + expected_items = value_collide_items + d = rect_values + else: + expected_items = key_collide_items + d = rect_keys + + collide_item = rect.collidedict(rect_dict=d, values=use_values) + + # The detected collision could be any of the possible items. + self.assertIn(collide_item, expected_items) + + def test_collidedictall(self): + """Ensures collidedictall detects collisions.""" + rect = Rect(1, 1, 10, 10) + + collide_item1 = ("collide 1", rect.copy()) + collide_item2 = ("collide 2", Rect(5, 5, 10, 10)) + no_collide_item1 = ("no collide 1", Rect(60, 60, 20, 20)) + no_collide_item2 = ("no collide 2", Rect(70, 70, 20, 20)) + + # Dict to check collisions with values. + rect_values = dict( + (collide_item1, collide_item2, no_collide_item1, no_collide_item2) + ) + value_collide_items = [collide_item1, collide_item2] + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + key_collide_items = [(tuple(v), k) for k, v in value_collide_items] + + for use_values in (True, False): + if use_values: + expected_items = value_collide_items + d = rect_values + else: + expected_items = key_collide_items + d = rect_keys + + collide_items = rect.collidedictall(d, use_values) + + self._assertCountEqual(collide_items, expected_items) + + def test_collidedictall__no_collision(self): + """Ensures collidedictall returns an empty list when no collisions.""" + rect = Rect(1, 1, 10, 10) + + no_collide_item1 = ("no collide 1", Rect(50, 50, 20, 20)) + no_collide_item2 = ("no collide 2", Rect(60, 60, 20, 20)) + no_collide_item3 = ("no collide 3", Rect(70, 70, 20, 20)) + + # Dict to check collisions with values. + rect_values = dict((no_collide_item1, no_collide_item2, no_collide_item3)) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + + expected_items = [] + + for use_values in (True, False): + d = rect_values if use_values else rect_keys + + collide_items = rect.collidedictall(d, use_values) + + self._assertCountEqual(collide_items, expected_items) + + def test_collidedictall__barely_touching(self): + """Ensures collidedictall works correctly for rects that barely touch.""" + rect = Rect(1, 1, 10, 10) + # Small rect to test barely touching collisions. + collide_rect = Rect(0, 0, 1, 1) + + collide_item1 = ("collide 1", collide_rect) + no_collide_item1 = ("no collide 1", Rect(50, 50, 20, 20)) + no_collide_item2 = ("no collide 2", Rect(60, 60, 20, 20)) + no_collide_item3 = ("no collide 3", Rect(70, 70, 20, 20)) + + # Dict to check collisions with values. + no_collide_rect_values = dict( + (no_collide_item1, no_collide_item2, no_collide_item3) + ) + + # Dict to check collisions with keys. + no_collide_rect_keys = {tuple(v): k for k, v in no_collide_rect_values.items()} + + # Tests the collide_rect on each of the rect's corners. + for attr in ("topleft", "topright", "bottomright", "bottomleft"): + setattr(collide_rect, attr, getattr(rect, attr)) + + for use_values in (True, False): + if use_values: + expected_items = [collide_item1] + d = dict(no_collide_rect_values) + else: + expected_items = [(tuple(collide_item1[1]), collide_item1[0])] + d = dict(no_collide_rect_keys) + + d.update(expected_items) # Add in the expected items. + + collide_items = rect.collidedictall(d, use_values) + + self._assertCountEqual(collide_items, expected_items) + + def test_collidedictall__zero_sized_rects(self): + """Ensures collidedictall works correctly with zero sized rects. + + There should be no collisions with zero sized rects. + """ + zero_rect1 = Rect(2, 2, 0, 0) + zero_rect2 = Rect(2, 2, 2, 0) + zero_rect3 = Rect(2, 2, 0, 2) + zero_rect4 = Rect(2, 2, -2, 0) + zero_rect5 = Rect(2, 2, 0, -2) + + no_collide_item1 = ("no collide 1", zero_rect1.copy()) + no_collide_item2 = ("no collide 2", zero_rect2.copy()) + no_collide_item3 = ("no collide 3", zero_rect3.copy()) + no_collide_item4 = ("no collide 4", zero_rect4.copy()) + no_collide_item5 = ("no collide 5", zero_rect5.copy()) + no_collide_item6 = ("no collide 6", Rect(0, 0, 10, 10)) + no_collide_item7 = ("no collide 7", Rect(0, 0, 2, 2)) + + # Dict to check collisions with values. + rect_values = dict( + ( + no_collide_item1, + no_collide_item2, + no_collide_item3, + no_collide_item4, + no_collide_item5, + no_collide_item6, + no_collide_item7, + ) + ) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + + expected_items = [] + + for use_values in (True, False): + d = rect_values if use_values else rect_keys + + for zero_rect in ( + zero_rect1, + zero_rect2, + zero_rect3, + zero_rect4, + zero_rect5, + ): + collide_items = zero_rect.collidedictall(d, use_values) + + self._assertCountEqual(collide_items, expected_items) + + def test_collidedictall__zero_sized_rects_as_args(self): + """Ensures collidedictall works correctly with zero sized rects + as args. + + There should be no collisions with zero sized rects. + """ + rect = Rect(0, 0, 20, 20) + + no_collide_item1 = ("no collide 1", Rect(2, 2, 0, 0)) + no_collide_item2 = ("no collide 2", Rect(2, 2, 2, 0)) + no_collide_item3 = ("no collide 3", Rect(2, 2, 0, 2)) + + # Dict to check collisions with values. + rect_values = dict((no_collide_item1, no_collide_item2, no_collide_item3)) + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + + expected_items = [] + + for use_values in (True, False): + d = rect_values if use_values else rect_keys + + collide_items = rect.collidedictall(d, use_values) + + self._assertCountEqual(collide_items, expected_items) + + def test_collidedictall__negative_sized_rects(self): + """Ensures collidedictall works correctly with negative sized rects.""" + neg_rect = Rect(2, 2, -2, -2) + + collide_item1 = ("collide 1", neg_rect.copy()) + collide_item2 = ("collide 2", Rect(0, 0, 20, 20)) + no_collide_item1 = ("no collide 1", Rect(2, 2, 20, 20)) + + # Dict to check collisions with values. + rect_values = dict((collide_item1, collide_item2, no_collide_item1)) + value_collide_items = [collide_item1, collide_item2] + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + key_collide_items = [(tuple(v), k) for k, v in value_collide_items] + + for use_values in (True, False): + if use_values: + expected_items = value_collide_items + d = rect_values + else: + expected_items = key_collide_items + d = rect_keys + + collide_items = neg_rect.collidedictall(d, use_values) + + self._assertCountEqual(collide_items, expected_items) + + def test_collidedictall__negative_sized_rects_as_args(self): + """Ensures collidedictall works correctly with negative sized rect + args. + """ + rect = Rect(0, 0, 10, 10) + + collide_item1 = ("collide 1", Rect(1, 1, -1, -1)) + no_collide_item1 = ("no collide 1", Rect(1, 1, -1, 0)) + no_collide_item2 = ("no collide 2", Rect(1, 1, 0, -1)) + + # Dict to check collisions with values. + rect_values = dict((collide_item1, no_collide_item1, no_collide_item2)) + value_collide_items = [collide_item1] + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + key_collide_items = [(tuple(v), k) for k, v in value_collide_items] + + for use_values in (True, False): + if use_values: + expected_items = value_collide_items + d = rect_values + else: + expected_items = key_collide_items + d = rect_keys + + collide_items = rect.collidedictall(d, use_values) + + self._assertCountEqual(collide_items, expected_items) + + def test_collidedictall__invalid_dict_format(self): + """Ensures collidedictall correctly handles invalid dict parameters.""" + rect = Rect(0, 0, 10, 10) + + invalid_value_dict = ("collide", rect.copy()) + invalid_key_dict = tuple(invalid_value_dict[1]), invalid_value_dict[0] + + for use_values in (True, False): + d = invalid_value_dict if use_values else invalid_key_dict + + with self.assertRaises(TypeError): + collide_item = rect.collidedictall(d, use_values) + + def test_collidedictall__invalid_dict_value_format(self): + """Ensures collidedictall correctly handles dicts with invalid values.""" + rect = Rect(0, 0, 10, 10) + rect_keys = {tuple(rect): "collide"} + + with self.assertRaises(TypeError): + collide_items = rect.collidedictall(rect_keys, 1) + + def test_collidedictall__invalid_dict_key_format(self): + """Ensures collidedictall correctly handles dicts with invalid keys.""" + rect = Rect(0, 0, 10, 10) + rect_values = {"collide": rect.copy()} + + with self.assertRaises(TypeError): + collide_items = rect.collidedictall(rect_values) + + def test_collidedictall__invalid_use_values_format(self): + """Ensures collidedictall correctly handles invalid use_values + parameters. + """ + rect = Rect(0, 0, 1, 1) + d = {} + + for invalid_param in (None, d, 1.1): + with self.assertRaises(TypeError): + collide_items = rect.collidedictall(d, invalid_param) + + def test_collidedictall__kwargs(self): + """Ensures collidedictall detects collisions via keyword arguments.""" + rect = Rect(1, 1, 10, 10) + + collide_item1 = ("collide 1", rect.copy()) + collide_item2 = ("collide 2", Rect(5, 5, 10, 10)) + no_collide_item1 = ("no collide 1", Rect(60, 60, 20, 20)) + no_collide_item2 = ("no collide 2", Rect(70, 70, 20, 20)) + + # Dict to check collisions with values. + rect_values = dict( + (collide_item1, collide_item2, no_collide_item1, no_collide_item2) + ) + value_collide_items = [collide_item1, collide_item2] + + # Dict to check collisions with keys. + rect_keys = {tuple(v): k for k, v in rect_values.items()} + key_collide_items = [(tuple(v), k) for k, v in value_collide_items] + + for use_values in (True, False): + if use_values: + expected_items = value_collide_items + d = rect_values + else: + expected_items = key_collide_items + d = rect_keys + + collide_items = rect.collidedictall(rect_dict=d, values=use_values) + + self._assertCountEqual(collide_items, expected_items) + + def test_collidelist(self): + # __doc__ (as of 2008-08-02) for pygame.rect.Rect.collidelist: + + # Rect.collidelist(list): return index + # test if one rectangle in a list intersects + # + # Test whether the rectangle collides with any in a sequence of + # rectangles. The index of the first collision found is returned. If + # no collisions are found an index of -1 is returned. + + r = Rect(1, 1, 10, 10) + l = [Rect(50, 50, 1, 1), Rect(5, 5, 10, 10), Rect(15, 15, 1, 1)] + + self.assertEqual(r.collidelist(l), 1) + + f = [Rect(50, 50, 1, 1), (100, 100, 4, 4)] + self.assertEqual(r.collidelist(f), -1) + + def test_collidelist__kwargs(self): + # Rect.collidelist(list): return index + # test if one rectangle in a list intersects + # + # Test whether the rectangle collides with any in a sequence of + # rectangles using keyword arguments. The index of the first collision + # found is returned. If no collisions are found an index + # of -1 is returned. + + r = Rect(1, 1, 10, 10) + l = [Rect(50, 50, 1, 1), Rect(5, 5, 10, 10), Rect(15, 15, 1, 1)] + + self.assertEqual(r.collidelist(l), 1) + + f = [Rect(50, 50, 1, 1), (100, 100, 4, 4)] + self.assertEqual(r.collidelist(rects=f), -1) + + def test_collidelistall(self): + # __doc__ (as of 2008-08-02) for pygame.rect.Rect.collidelistall: + + # Rect.collidelistall(list): return indices + # test if all rectangles in a list intersect + # + # Returns a list of all the indices that contain rectangles that + # collide with the Rect. If no intersecting rectangles are found, an + # empty list is returned. + + r = Rect(1, 1, 10, 10) + + l = [ + Rect(1, 1, 10, 10), + Rect(5, 5, 10, 10), + Rect(15, 15, 1, 1), + Rect(2, 2, 1, 1), + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [Rect(50, 50, 1, 1), Rect(20, 20, 5, 5)] + self.assertFalse(r.collidelistall(f)) + + def test_collidelistall_returns_empty_list(self): + r = Rect(1, 1, 10, 10) + + l = [ + Rect(112, 1, 10, 10), + Rect(50, 5, 10, 10), + Rect(15, 15, 1, 1), + Rect(-20, 2, 1, 1), + ] + self.assertEqual(r.collidelistall(l), []) + + def test_collidelistall_list_of_tuples(self): + r = Rect(1, 1, 10, 10) + + l = [ + (1, 1, 10, 10), + (5, 5, 10, 10), + (15, 15, 1, 1), + (2, 2, 1, 1), + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [(50, 50, 1, 1), (20, 20, 5, 5)] + self.assertFalse(r.collidelistall(f)) + + def test_collidelistall_list_of_two_tuples(self): + r = Rect(1, 1, 10, 10) + + l = [ + ((1, 1), (10, 10)), + ((5, 5), (10, 10)), + ((15, 15), (1, 1)), + ((2, 2), (1, 1)), + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [((50, 50), (1, 1)), ((20, 20), (5, 5))] + self.assertFalse(r.collidelistall(f)) + + def test_collidelistall_list_of_lists(self): + r = Rect(1, 1, 10, 10) + + l = [ + [1, 1, 10, 10], + [5, 5, 10, 10], + [15, 15, 1, 1], + [2, 2, 1, 1], + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [[50, 50, 1, 1], [20, 20, 5, 5]] + self.assertFalse(r.collidelistall(f)) + + class _ObjectWithRectAttribute: + def __init__(self, r): + self.rect = r + + class _ObjectWithCallableRectAttribute: + def __init__(self, r): + self._rect = r + + def rect(self): + return self._rect + + class _ObjectWithRectProperty: + def __init__(self, r): + self._rect = r + + @property + def rect(self): + return self._rect + + class _ObjectWithMultipleRectAttribute: + def __init__(self, r1, r2, r3): + self.rect1 = r1 + self.rect2 = r2 + self.rect3 = r3 + + def test_collidelistall_list_of_object_with_rect_attribute(self): + r = Rect(1, 1, 10, 10) + + l = [ + self._ObjectWithRectAttribute(Rect(1, 1, 10, 10)), + self._ObjectWithRectAttribute(Rect(5, 5, 10, 10)), + self._ObjectWithRectAttribute(Rect(15, 15, 1, 1)), + self._ObjectWithRectAttribute(Rect(2, 2, 1, 1)), + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [ + self._ObjectWithRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithRectAttribute(Rect(20, 20, 5, 5)), + ] + self.assertFalse(r.collidelistall(f)) + + def test_collidelistall_list_of_object_with_callable_rect_attribute(self): + r = Rect(1, 1, 10, 10) + + l = [ + self._ObjectWithCallableRectAttribute(Rect(1, 1, 10, 10)), + self._ObjectWithCallableRectAttribute(Rect(5, 5, 10, 10)), + self._ObjectWithCallableRectAttribute(Rect(15, 15, 1, 1)), + self._ObjectWithCallableRectAttribute(Rect(2, 2, 1, 1)), + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [ + self._ObjectWithCallableRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithCallableRectAttribute(Rect(20, 20, 5, 5)), + ] + self.assertFalse(r.collidelistall(f)) + + def test_collidelistall_list_of_object_with_callable_rect_returning_object_with_rect_attribute( + self, + ): + r = Rect(1, 1, 10, 10) + + l = [ + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(1, 1, 10, 10)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(5, 5, 10, 10)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(15, 15, 1, 1)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(2, 2, 1, 1)) + ), + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [ + self._ObjectWithCallableRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithCallableRectAttribute(Rect(20, 20, 5, 5)), + ] + self.assertFalse(r.collidelistall(f)) + + def test_collidelistall_list_of_object_with_rect_property(self): + r = Rect(1, 1, 10, 10) + + l = [ + self._ObjectWithRectProperty(Rect(1, 1, 10, 10)), + self._ObjectWithRectProperty(Rect(5, 5, 10, 10)), + self._ObjectWithRectProperty(Rect(15, 15, 1, 1)), + self._ObjectWithRectProperty(Rect(2, 2, 1, 1)), + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [ + self._ObjectWithRectProperty(Rect(50, 50, 1, 1)), + self._ObjectWithRectProperty(Rect(20, 20, 5, 5)), + ] + self.assertFalse(r.collidelistall(f)) + + def test_collidelistall__kwargs(self): + # Rect.collidelistall(list): return indices + # test if all rectangles in a list intersect using keyword arguments. + # + # Returns a list of all the indices that contain rectangles that + # collide with the Rect. If no intersecting rectangles are found, an + # empty list is returned. + + r = Rect(1, 1, 10, 10) + + l = [ + Rect(1, 1, 10, 10), + Rect(5, 5, 10, 10), + Rect(15, 15, 1, 1), + Rect(2, 2, 1, 1), + ] + self.assertEqual(r.collidelistall(l), [0, 1, 3]) + + f = [Rect(50, 50, 1, 1), Rect(20, 20, 5, 5)] + self.assertFalse(r.collidelistall(rects=f)) + + def test_collideobjects_call_variants(self): + # arrange + r = Rect(1, 1, 10, 10) + rects = [Rect(1, 2, 3, 4), Rect(10, 20, 30, 40)] + objects = [ + self._ObjectWithMultipleRectAttribute( + Rect(1, 2, 3, 4), Rect(10, 20, 30, 40), Rect(100, 200, 300, 400) + ), + self._ObjectWithMultipleRectAttribute( + Rect(1, 2, 3, 4), Rect(10, 20, 30, 40), Rect(100, 200, 300, 400) + ), + ] + + # act / verify + r.collideobjects(rects) + r.collideobjects(rects, key=None) + r.collideobjects(objects, key=lambda o: o.rect1) + self.assertRaises(TypeError, r.collideobjects, objects) + + def test_collideobjects_without_key(self): + r = Rect(1, 1, 10, 10) + types_to_test = [ + [Rect(50, 50, 1, 1), Rect(5, 5, 10, 10), Rect(4, 4, 1, 1)], + [ + self._ObjectWithRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithRectAttribute(Rect(5, 5, 10, 10)), + self._ObjectWithRectAttribute(Rect(4, 4, 1, 1)), + ], + [ + self._ObjectWithRectProperty(Rect(50, 50, 1, 1)), + self._ObjectWithRectProperty(Rect(5, 5, 10, 10)), + self._ObjectWithRectProperty(Rect(4, 4, 1, 1)), + ], + [ + self._ObjectWithCallableRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithCallableRectAttribute(Rect(5, 5, 10, 10)), + self._ObjectWithCallableRectAttribute(Rect(4, 4, 1, 1)), + ], + [ + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(50, 50, 1, 1)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(5, 5, 10, 10)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(4, 4, 1, 1)) + ), + ], + [(50, 50, 1, 1), (5, 5, 10, 10), (4, 4, 1, 1)], + [((50, 50), (1, 1)), ((5, 5), (10, 10)), ((4, 4), (1, 1))], + [[50, 50, 1, 1], [5, 5, 10, 10], [4, 4, 1, 1]], + [ + Rect(50, 50, 1, 1), + self._ObjectWithRectAttribute(Rect(5, 5, 10, 10)), + (4, 4, 1, 1), + ], # mix + ] + + for l in types_to_test: + with self.subTest(type=l[0].__class__.__name__): + # act + actual = r.collideobjects(l) + # assert + self.assertEqual(actual, l[1]) + + types_to_test = [ + [Rect(50, 50, 1, 1), Rect(100, 100, 4, 4)], + [ + self._ObjectWithRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithRectAttribute(Rect(100, 100, 4, 4)), + ], + [ + self._ObjectWithRectProperty(Rect(50, 50, 1, 1)), + self._ObjectWithRectProperty(Rect(100, 100, 4, 4)), + ], + [ + self._ObjectWithCallableRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithCallableRectAttribute(Rect(100, 100, 4, 4)), + ], + [ + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(50, 50, 1, 1)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(100, 100, 4, 4)) + ), + ], + [(50, 50, 1, 1), (100, 100, 4, 4)], + [((50, 50), (1, 1)), ((100, 100), (4, 4))], + [[50, 50, 1, 1], [100, 100, 4, 4]], + [Rect(50, 50, 1, 1), [100, 100, 4, 4]], # mix + ] + + for f in types_to_test: + with self.subTest(type=f[0].__class__.__name__, expected=None): + # act + actual = r.collideobjects(f) + # assert + self.assertEqual(actual, None) + + def test_collideobjects_list_of_object_with_multiple_rect_attribute(self): + r = Rect(1, 1, 10, 10) + + things = [ + self._ObjectWithMultipleRectAttribute( + Rect(1, 1, 10, 10), Rect(5, 5, 1, 1), Rect(-73, 3, 3, 3) + ), + self._ObjectWithMultipleRectAttribute( + Rect(5, 5, 10, 10), Rect(-5, -5, 10, 10), Rect(3, 3, 3, 3) + ), + self._ObjectWithMultipleRectAttribute( + Rect(15, 15, 1, 1), Rect(100, 1, 1, 1), Rect(3, 83, 3, 3) + ), + self._ObjectWithMultipleRectAttribute( + Rect(2, 2, 1, 1), Rect(1, -81, 10, 10), Rect(3, 8, 3, 3) + ), + ] + self.assertEqual(r.collideobjects(things, key=lambda o: o.rect1), things[0]) + self.assertEqual(r.collideobjects(things, key=lambda o: o.rect2), things[0]) + self.assertEqual(r.collideobjects(things, key=lambda o: o.rect3), things[1]) + + f = [ + self._ObjectWithMultipleRectAttribute( + Rect(50, 50, 1, 1), Rect(11, 1, 1, 1), Rect(2, -32, 2, 2) + ), + self._ObjectWithMultipleRectAttribute( + Rect(20, 20, 5, 5), Rect(1, 11, 1, 1), Rect(-20, 2, 2, 2) + ), + ] + self.assertFalse(r.collideobjectsall(f, key=lambda o: o.rect1)) + self.assertFalse(r.collideobjectsall(f, key=lambda o: o.rect2)) + self.assertFalse(r.collideobjectsall(f, key=lambda o: o.rect3)) + + def test_collideobjectsall_call_variants(self): + # arrange + r = Rect(1, 1, 10, 10) + rects = [Rect(1, 2, 3, 4), Rect(10, 20, 30, 40)] + objects = [ + self._ObjectWithMultipleRectAttribute( + Rect(1, 2, 3, 4), Rect(10, 20, 30, 40), Rect(100, 200, 300, 400) + ), + self._ObjectWithMultipleRectAttribute( + Rect(1, 2, 3, 4), Rect(10, 20, 30, 40), Rect(100, 200, 300, 400) + ), + ] + + # act / verify + r.collideobjectsall(rects) + r.collideobjectsall(rects, key=None) + r.collideobjectsall(objects, key=lambda o: o.rect1) + self.assertRaises(TypeError, r.collideobjectsall, objects) + + def test_collideobjectsall(self): + r = Rect(1, 1, 10, 10) + + types_to_test = [ + [ + Rect(1, 1, 10, 10), + Rect(5, 5, 10, 10), + Rect(15, 15, 1, 1), + Rect(2, 2, 1, 1), + ], + [ + (1, 1, 10, 10), + (5, 5, 10, 10), + (15, 15, 1, 1), + (2, 2, 1, 1), + ], + [ + ((1, 1), (10, 10)), + ((5, 5), (10, 10)), + ((15, 15), (1, 1)), + ((2, 2), (1, 1)), + ], + [ + [1, 1, 10, 10], + [5, 5, 10, 10], + [15, 15, 1, 1], + [2, 2, 1, 1], + ], + [ + self._ObjectWithRectAttribute(Rect(1, 1, 10, 10)), + self._ObjectWithRectAttribute(Rect(5, 5, 10, 10)), + self._ObjectWithRectAttribute(Rect(15, 15, 1, 1)), + self._ObjectWithRectAttribute(Rect(2, 2, 1, 1)), + ], + [ + self._ObjectWithCallableRectAttribute(Rect(1, 1, 10, 10)), + self._ObjectWithCallableRectAttribute(Rect(5, 5, 10, 10)), + self._ObjectWithCallableRectAttribute(Rect(15, 15, 1, 1)), + self._ObjectWithCallableRectAttribute(Rect(2, 2, 1, 1)), + ], + [ + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(1, 1, 10, 10)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(5, 5, 10, 10)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(15, 15, 1, 1)) + ), + self._ObjectWithCallableRectAttribute( + self._ObjectWithRectAttribute(Rect(2, 2, 1, 1)) + ), + ], + [ + self._ObjectWithRectProperty(Rect(1, 1, 10, 10)), + self._ObjectWithRectProperty(Rect(5, 5, 10, 10)), + self._ObjectWithRectProperty(Rect(15, 15, 1, 1)), + self._ObjectWithRectProperty(Rect(2, 2, 1, 1)), + ], + ] + for things in types_to_test: + with self.subTest(type=things[0].__class__.__name__): + # act + actual = r.collideobjectsall(things, key=None) + # assert + self.assertEqual(actual, [things[0], things[1], things[3]]) + + types_to_test = [ + [Rect(50, 50, 1, 1), Rect(20, 20, 5, 5)], + [(50, 50, 1, 1), (20, 20, 5, 5)], + [((50, 50), (1, 1)), ((20, 20), (5, 5))], + [[50, 50, 1, 1], [20, 20, 5, 5]], + [ + self._ObjectWithRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithRectAttribute(Rect(20, 20, 5, 5)), + ], + [ + self._ObjectWithCallableRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithCallableRectAttribute(Rect(20, 20, 5, 5)), + ], + [ + self._ObjectWithCallableRectAttribute(Rect(50, 50, 1, 1)), + self._ObjectWithCallableRectAttribute(Rect(20, 20, 5, 5)), + ], + [ + self._ObjectWithRectProperty(Rect(50, 50, 1, 1)), + self._ObjectWithRectProperty(Rect(20, 20, 5, 5)), + ], + ] + for f in types_to_test: + with self.subTest(type=f[0].__class__.__name__, expected=None): + # act + actual = r.collideobjectsall(f) + # assert + self.assertFalse(actual) + + def test_collideobjectsall_list_of_object_with_multiple_rect_attribute(self): + r = Rect(1, 1, 10, 10) + + things = [ + self._ObjectWithMultipleRectAttribute( + Rect(1, 1, 10, 10), Rect(5, 5, 1, 1), Rect(-73, 3, 3, 3) + ), + self._ObjectWithMultipleRectAttribute( + Rect(5, 5, 10, 10), Rect(-5, -5, 10, 10), Rect(3, 3, 3, 3) + ), + self._ObjectWithMultipleRectAttribute( + Rect(15, 15, 1, 1), Rect(100, 1, 1, 1), Rect(3, 83, 3, 3) + ), + self._ObjectWithMultipleRectAttribute( + Rect(2, 2, 1, 1), Rect(1, -81, 10, 10), Rect(3, 8, 3, 3) + ), + ] + self.assertEqual( + r.collideobjectsall(things, key=lambda o: o.rect1), + [things[0], things[1], things[3]], + ) + self.assertEqual( + r.collideobjectsall(things, key=lambda o: o.rect2), [things[0], things[1]] + ) + self.assertEqual( + r.collideobjectsall(things, key=lambda o: o.rect3), [things[1], things[3]] + ) + + f = [ + self._ObjectWithMultipleRectAttribute( + Rect(50, 50, 1, 1), Rect(11, 1, 1, 1), Rect(2, -32, 2, 2) + ), + self._ObjectWithMultipleRectAttribute( + Rect(20, 20, 5, 5), Rect(1, 11, 1, 1), Rect(-20, 2, 2, 2) + ), + ] + self.assertFalse(r.collideobjectsall(f, key=lambda o: o.rect1)) + self.assertFalse(r.collideobjectsall(f, key=lambda o: o.rect2)) + self.assertFalse(r.collideobjectsall(f, key=lambda o: o.rect3)) + + def test_fit(self): + # __doc__ (as of 2008-08-02) for pygame.rect.Rect.fit: + + # Rect.fit(Rect): return Rect + # resize and move a rectangle with aspect ratio + # + # Returns a new rectangle that is moved and resized to fit another. + # The aspect ratio of the original Rect is preserved, so the new + # rectangle may be smaller than the target in either width or height. + + r = Rect(10, 10, 30, 30) + + r2 = Rect(30, 30, 15, 10) + + f = r.fit(r2) + self.assertTrue(r2.contains(f)) + + f2 = r2.fit(r) + self.assertTrue(r.contains(f2)) + + def test_copy(self): + r = Rect(1, 2, 10, 20) + c = r.copy() + self.assertEqual(c, r) + + def test_subscript(self): + r = Rect(1, 2, 3, 4) + self.assertEqual(r[0], 1) + self.assertEqual(r[1], 2) + self.assertEqual(r[2], 3) + self.assertEqual(r[3], 4) + self.assertEqual(r[-1], 4) + self.assertEqual(r[-2], 3) + self.assertEqual(r[-4], 1) + self.assertRaises(IndexError, r.__getitem__, 5) + self.assertRaises(IndexError, r.__getitem__, -5) + self.assertEqual(r[0:2], [1, 2]) + self.assertEqual(r[0:4], [1, 2, 3, 4]) + self.assertEqual(r[0:-1], [1, 2, 3]) + self.assertEqual(r[:], [1, 2, 3, 4]) + self.assertEqual(r[...], [1, 2, 3, 4]) + self.assertEqual(r[0:4:2], [1, 3]) + self.assertEqual(r[0:4:3], [1, 4]) + self.assertEqual(r[3::-1], [4, 3, 2, 1]) + self.assertRaises(TypeError, r.__getitem__, None) + + def test_ass_subscript(self): + r = Rect(0, 0, 0, 0) + r[...] = 1, 2, 3, 4 + self.assertEqual(r, [1, 2, 3, 4]) + self.assertRaises(TypeError, r.__setitem__, None, 0) + self.assertEqual(r, [1, 2, 3, 4]) + self.assertRaises(TypeError, r.__setitem__, 0, "") + self.assertEqual(r, [1, 2, 3, 4]) + self.assertRaises(IndexError, r.__setitem__, 4, 0) + self.assertEqual(r, [1, 2, 3, 4]) + self.assertRaises(IndexError, r.__setitem__, -5, 0) + self.assertEqual(r, [1, 2, 3, 4]) + r[0] = 10 + self.assertEqual(r, [10, 2, 3, 4]) + r[3] = 40 + self.assertEqual(r, [10, 2, 3, 40]) + r[-1] = 400 + self.assertEqual(r, [10, 2, 3, 400]) + r[-4] = 100 + self.assertEqual(r, [100, 2, 3, 400]) + r[1:3] = 0 + self.assertEqual(r, [100, 0, 0, 400]) + r[...] = 0 + self.assertEqual(r, [0, 0, 0, 0]) + r[:] = 9 + self.assertEqual(r, [9, 9, 9, 9]) + r[:] = 11, 12, 13, 14 + self.assertEqual(r, [11, 12, 13, 14]) + r[::-1] = r + self.assertEqual(r, [14, 13, 12, 11]) + + def test_ass_subscript_deletion(self): + r = Rect(0, 0, 0, 0) + with self.assertRaises(TypeError): + del r[0] + + with self.assertRaises(TypeError): + del r[0:2] + + with self.assertRaises(TypeError): + del r[...] + + def test_collection_abc(self): + r = Rect(64, 70, 75, 30) + self.assertTrue(isinstance(r, Collection)) + self.assertFalse(isinstance(r, Sequence)) + + +class SubclassTest(unittest.TestCase): + class MyRect(Rect): + def __init__(self, *args, **kwds): + super(SubclassTest.MyRect, self).__init__(*args, **kwds) + self.an_attribute = True + + def test_copy(self): + mr1 = self.MyRect(1, 2, 10, 20) + self.assertTrue(mr1.an_attribute) + mr2 = mr1.copy() + self.assertTrue(isinstance(mr2, self.MyRect)) + self.assertRaises(AttributeError, getattr, mr2, "an_attribute") + + def test_move(self): + mr1 = self.MyRect(1, 2, 10, 20) + self.assertTrue(mr1.an_attribute) + mr2 = mr1.move(1, 2) + self.assertTrue(isinstance(mr2, self.MyRect)) + self.assertRaises(AttributeError, getattr, mr2, "an_attribute") + + def test_inflate(self): + mr1 = self.MyRect(1, 2, 10, 20) + self.assertTrue(mr1.an_attribute) + mr2 = mr1.inflate(2, 4) + self.assertTrue(isinstance(mr2, self.MyRect)) + self.assertRaises(AttributeError, getattr, mr2, "an_attribute") + + def test_clamp(self): + mr1 = self.MyRect(19, 12, 5, 5) + self.assertTrue(mr1.an_attribute) + mr2 = mr1.clamp(Rect(10, 10, 10, 10)) + self.assertTrue(isinstance(mr2, self.MyRect)) + self.assertRaises(AttributeError, getattr, mr2, "an_attribute") + + def test_clip(self): + mr1 = self.MyRect(1, 2, 3, 4) + self.assertTrue(mr1.an_attribute) + mr2 = mr1.clip(Rect(0, 0, 3, 4)) + self.assertTrue(isinstance(mr2, self.MyRect)) + self.assertRaises(AttributeError, getattr, mr2, "an_attribute") + + def test_union(self): + mr1 = self.MyRect(1, 1, 1, 2) + self.assertTrue(mr1.an_attribute) + mr2 = mr1.union(Rect(-2, -2, 1, 2)) + self.assertTrue(isinstance(mr2, self.MyRect)) + self.assertRaises(AttributeError, getattr, mr2, "an_attribute") + + def test_unionall(self): + mr1 = self.MyRect(0, 0, 1, 1) + self.assertTrue(mr1.an_attribute) + mr2 = mr1.unionall([Rect(-2, -2, 1, 1), Rect(2, 2, 1, 1)]) + self.assertTrue(isinstance(mr2, self.MyRect)) + self.assertRaises(AttributeError, getattr, mr2, "an_attribute") + + def test_fit(self): + mr1 = self.MyRect(10, 10, 30, 30) + self.assertTrue(mr1.an_attribute) + mr2 = mr1.fit(Rect(30, 30, 15, 10)) + self.assertTrue(isinstance(mr2, self.MyRect)) + self.assertRaises(AttributeError, getattr, mr2, "an_attribute") + + def test_collection_abc(self): + mr1 = self.MyRect(64, 70, 75, 30) + self.assertTrue(isinstance(mr1, Collection)) + self.assertFalse(isinstance(mr1, Sequence)) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_2_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_3_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_3_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_3_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_4_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_4_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_4_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_5_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_5_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_5_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_6_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_6_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/fake_6_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/no_assertions__ret_code_of_1__test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/no_assertions__ret_code_of_1__test.py new file mode 100644 index 0000000..0ba0e94 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/no_assertions__ret_code_of_1__test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + pass + + def test_get_mods(self): + pass + + def test_get_pressed(self): + pass + + def test_name(self): + pass + + def test_set_mods(self): + pass + + def test_set_repeat(self): + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/zero_tests_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/zero_tests_test.py new file mode 100644 index 0000000..649055a --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/all_ok/zero_tests_test.py @@ -0,0 +1,23 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/everything/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/everything/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/fake_2_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/everything/incomplete_todo_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/incomplete_todo_test.py new file mode 100644 index 0000000..bdd8a3b --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/incomplete_todo_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def todo_test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def todo_test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/everything/magic_tag_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/magic_tag_test.py new file mode 100644 index 0000000..126bc2b --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/magic_tag_test.py @@ -0,0 +1,38 @@ +__tags__ = ["magic"] + +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/everything/sleep_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/sleep_test.py new file mode 100644 index 0000000..468c75f --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/everything/sleep_test.py @@ -0,0 +1,29 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + +import time + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + stop_time = time.time() + 10.0 + while time.time() < stop_time: + time.sleep(1) + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/fake_2_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/invisible_tag_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/invisible_tag_test.py new file mode 100644 index 0000000..3ef959a --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/invisible_tag_test.py @@ -0,0 +1,41 @@ +__tags__ = ["invisible"] + +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/magic_tag_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/magic_tag_test.py new file mode 100644 index 0000000..126bc2b --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/exclude/magic_tag_test.py @@ -0,0 +1,38 @@ +__tags__ = ["magic"] + +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_2_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_3_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_3_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_3_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_4_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_4_test.py new file mode 100644 index 0000000..1e75fea --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/failures1/fake_4_test.py @@ -0,0 +1,41 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(False, "Some Jibberish") + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + if 1: + if 1: + assert False + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/fake_2_test.py new file mode 100644 index 0000000..b88f1ae --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def todo_test_get_pressed(self): + self.fail() + + def test_name(self): + self.assertTrue(True) + + def todo_test_set_mods(self): + self.fail() + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/fake_3_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/fake_3_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete/fake_3_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/fake_2_test.py new file mode 100644 index 0000000..bdd8a3b --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def todo_test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def todo_test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/fake_3_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/fake_3_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/incomplete_todo/fake_3_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/fake_1_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/fake_1_test.py new file mode 100644 index 0000000..3e9e936 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/fake_1_test.py @@ -0,0 +1,40 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + while True: + pass + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/fake_2_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/infinite_loop/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_2_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_3_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_3_test.py new file mode 100644 index 0000000..f59ad40 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_3_test.py @@ -0,0 +1,41 @@ +import sys + +if __name__ == "__main__": + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + sys.stderr.write("jibberish messes things up\n") + self.assertTrue(False) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_4_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_4_test.py new file mode 100644 index 0000000..1e75fea --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stderr/fake_4_test.py @@ -0,0 +1,41 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(False, "Some Jibberish") + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + if 1: + if 1: + assert False + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_2_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_3_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_3_test.py new file mode 100644 index 0000000..467c725 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_3_test.py @@ -0,0 +1,42 @@ +import sys + +if __name__ == "__main__": + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + sys.stdout.write("jibberish ruins everything\n") + self.assertTrue(False) + + def test_name(self): + sys.stdout.write("forgot to remove debug crap\n") + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_4_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_4_test.py new file mode 100644 index 0000000..1e75fea --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/print_stdout/fake_4_test.py @@ -0,0 +1,41 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(False, "Some Jibberish") + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + if 1: + if 1: + assert False + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/run_tests__test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/run_tests__test.py new file mode 100644 index 0000000..a1eadd1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/run_tests__test.py @@ -0,0 +1,145 @@ +################################################################################ + +import subprocess, os, sys, re, difflib + +################################################################################ + +IGNORE = (".svn", "infinite_loop") +NORMALIZERS = ( + (r"Ran (\d+) tests in (\d+\.\d+)s", "Ran \\1 tests in X.XXXs"), + (r'File ".*?([^/\\.]+\.py)"', 'File "\\1"'), +) + +################################################################################ + + +def norm_result(result): + "normalize differences, such as timing between output" + for normalizer, replacement in NORMALIZERS: + if hasattr(normalizer, "__call__"): + result = normalizer(result) + else: + result = re.sub(normalizer, replacement, result) + + return result + + +def call_proc(cmd, cd=None): + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=cd, + universal_newlines=True, + ) + if proc.wait(): + print(f"{cmd} {proc.wait()}") + raise Exception(proc.stdout.read()) + + return proc.stdout.read() + + +################################################################################ + +unnormed_diff = "-u" in sys.argv +verbose = "-v" in sys.argv or unnormed_diff +if "-h" in sys.argv or "--help" in sys.argv: + sys.exit( + "\nCOMPARES OUTPUT OF SINGLE VS SUBPROCESS MODE OF RUN_TESTS.PY\n\n" + "-v, to output diffs even on success\n" + "-u, to output diffs of unnormalized tests\n\n" + "Each line of a Differ delta begins with a two-letter code:\n\n" + " '- ' line unique to sequence 1\n" + " '+ ' line unique to sequence 2\n" + " ' ' line common to both sequences\n" + " '? ' line not present in either input sequence\n" + ) + +main_dir = os.path.split(os.path.abspath(sys.argv[0]))[0] +trunk_dir = os.path.normpath(os.path.join(main_dir, "../../")) + +test_suite_dirs = [ + x + for x in os.listdir(main_dir) + if os.path.isdir(os.path.join(main_dir, x)) and x not in IGNORE +] + + +################################################################################ + + +def assert_on_results(suite, single, sub): + test = globals().get(f"{suite}_test") + if hasattr(test, "__call_"): + test(suite, single, sub) + print(f"assertions on {suite} OK") + + +# Don't modify tests in suites below. These assertions are in place to make sure +# that tests are actually being ran + + +def all_ok_test(suite, *args): + for results in args: + assert "Ran 36 tests" in results # some tests are running + assert "OK" in results # OK + + +def failures1_test(suite, *args): + for results in args: + assert "FAILED (failures=2)" in results + assert "Ran 18 tests" in results + + +################################################################################ +# Test that output is the same in single process and subprocess modes +# + +base_cmd = [sys.executable, "run_tests.py", "-i"] + +cmd = base_cmd + ["-n", "-f"] +sub_cmd = base_cmd + ["-f"] +time_out_cmd = base_cmd + ["-t", "4", "-f", "infinite_loop"] + +passes = 0 +failed = False + +for suite in test_suite_dirs: + single = call_proc(cmd + [suite], trunk_dir) + subs = call_proc(sub_cmd + [suite], trunk_dir) + + normed_single, normed_subs = map(norm_result, (single, subs)) + + failed = normed_single != normed_subs + if failed: + print(f"{suite} suite comparison FAILED\n") + else: + passes += 1 + print(f"{suite} suite comparison OK") + + assert_on_results(suite, single, subs) + + if verbose or failed: + print("difflib.Differ().compare(single, suprocessed):\n") + print( + "".join( + list( + difflib.Differ().compare( + (unnormed_diff and single or normed_single).splitlines(1), + (unnormed_diff and subs or normed_subs).splitlines(1), + ) + ) + ) + ) + +sys.stdout.write("infinite_loop suite (subprocess mode timeout) ") +loop_test = call_proc(time_out_cmd, trunk_dir) +assert "successfully terminated" in loop_test +passes += 1 +print("OK") + +print(f"\n{passes}/{len(test_suite_dirs) + 1} suites pass") + +print("\n-h for help") + +################################################################################ diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/__init__.py b/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/__init__.py new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/__init__.py @@ -0,0 +1 @@ +# empty diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/fake_2_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/fake_2_test.py new file mode 100644 index 0000000..3be92e1 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/fake_2_test.py @@ -0,0 +1,39 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + self.assertTrue(True) + + def test_get_mods(self): + self.assertTrue(True) + + def test_get_pressed(self): + self.assertTrue(True) + + def test_name(self): + self.assertTrue(True) + + def test_set_mods(self): + self.assertTrue(True) + + def test_set_repeat(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/sleep_test.py b/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/sleep_test.py new file mode 100644 index 0000000..bab528a --- /dev/null +++ b/laplas/abstract_map/pygame/tests/run_tests__tests/timeout/sleep_test.py @@ -0,0 +1,30 @@ +if __name__ == "__main__": + import sys + import os + + pkg_dir = os.path.split( + os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + )[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import unittest + +import time + + +class KeyModuleTest(unittest.TestCase): + def test_get_focused(self): + stop_time = time.time() + 10.0 + while time.time() < stop_time: + time.sleep(1) + + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/rwobject_test.py b/laplas/abstract_map/pygame/tests/rwobject_test.py new file mode 100644 index 0000000..31723ae --- /dev/null +++ b/laplas/abstract_map/pygame/tests/rwobject_test.py @@ -0,0 +1,139 @@ +import pathlib +import unittest + +from pygame import encode_string, encode_file_path + + +class RWopsEncodeStringTest(unittest.TestCase): + global getrefcount + + def test_obj_None(self): + encoded_string = encode_string(None) + + self.assertIsNone(encoded_string) + + def test_returns_bytes(self): + u = "Hello" + encoded_string = encode_string(u) + + self.assertIsInstance(encoded_string, bytes) + + def test_obj_bytes(self): + b = b"encyclop\xE6dia" + encoded_string = encode_string(b, "ascii", "strict") + + self.assertIs(encoded_string, b) + + def test_encode_unicode(self): + u = "\u00DEe Olde Komp\u00FCter Shoppe" + b = u.encode("utf-8") + self.assertEqual(encode_string(u, "utf-8"), b) + + def test_error_fowarding(self): + self.assertRaises(SyntaxError, encode_string) + + def test_errors(self): + u = "abc\u0109defg\u011Dh\u0125ij\u0135klmnoprs\u015Dtu\u016Dvz" + b = u.encode("ascii", "ignore") + self.assertEqual(encode_string(u, "ascii", "ignore"), b) + + def test_encoding_error(self): + u = "a\x80b" + encoded_string = encode_string(u, "ascii", "strict") + + self.assertIsNone(encoded_string) + + def test_check_defaults(self): + u = "a\u01F7b" + b = u.encode("unicode_escape", "backslashreplace") + encoded_string = encode_string(u) + + self.assertEqual(encoded_string, b) + + def test_etype(self): + u = "a\x80b" + self.assertRaises(SyntaxError, encode_string, u, "ascii", "strict", SyntaxError) + + def test_etype__invalid(self): + """Ensures invalid etypes are properly handled.""" + + for etype in ("SyntaxError", self): + self.assertRaises(TypeError, encode_string, "test", etype=etype) + + def test_string_with_null_bytes(self): + b = b"a\x00b\x00c" + encoded_string = encode_string(b, etype=SyntaxError) + encoded_decode_string = encode_string(b.decode(), "ascii", "strict") + + self.assertIs(encoded_string, b) + self.assertEqual(encoded_decode_string, b) + + try: + from sys import getrefcount as _g + + getrefcount = _g # This nonsense is for Python 3.x + except ImportError: + pass + else: + + def test_refcount(self): + bpath = b" This is a string that is not cached."[1:] + upath = bpath.decode("ascii") + before = getrefcount(bpath) + bpath = encode_string(bpath) + self.assertEqual(getrefcount(bpath), before) + bpath = encode_string(upath) + self.assertEqual(getrefcount(bpath), before) + + def test_smp(self): + utf_8 = b"a\xF0\x93\x82\xA7b" + u = "a\U000130A7b" + b = encode_string(u, "utf-8", "strict", AssertionError) + self.assertEqual(b, utf_8) + + def test_pathlib_obj(self): + """Test loading string representation of pathlib object""" + """ + We do this because pygame functions internally use pg_EncodeString + to decode the filenames passed to them. So if we test that here, we + can safely assume that all those functions do not have any issues + with pathlib objects + """ + encoded = encode_string(pathlib.PurePath("foo"), "utf-8") + self.assertEqual(encoded, b"foo") + + encoded = encode_string(pathlib.Path("baz")) + self.assertEqual(encoded, b"baz") + + +class RWopsEncodeFilePathTest(unittest.TestCase): + # Most tests can be skipped since RWopsEncodeFilePath wraps + # RWopsEncodeString + def test_encoding(self): + u = "Hello" + encoded_file_path = encode_file_path(u) + + self.assertIsInstance(encoded_file_path, bytes) + + def test_error_fowarding(self): + self.assertRaises(SyntaxError, encode_file_path) + + def test_path_with_null_bytes(self): + b = b"a\x00b\x00c" + encoded_file_path = encode_file_path(b) + + self.assertIsNone(encoded_file_path) + + def test_etype(self): + b = b"a\x00b\x00c" + self.assertRaises(TypeError, encode_file_path, b, TypeError) + + def test_etype__invalid(self): + """Ensures invalid etypes are properly handled.""" + + for etype in ("SyntaxError", self): + self.assertRaises(TypeError, encode_file_path, "test", etype) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/scrap_tags.py b/laplas/abstract_map/pygame/tests/scrap_tags.py new file mode 100644 index 0000000..17a82ff --- /dev/null +++ b/laplas/abstract_map/pygame/tests/scrap_tags.py @@ -0,0 +1,26 @@ +__tags__ = ["ignore", "subprocess_ignore"] + +# TODO: make scrap_test.py work +# This test used to work only on linux and windows. +# Currently it only work in windows, and in linux it throws: +# `pygame.error: content could not be placed in clipboard.` +# The old test and tags kept here for reference when fixing. + +# import sys +# +# exclude = False +# +# if sys.platform == "win32" or sys.platform.startswith("linux"): +# try: +# import pygame +# +# pygame.scrap._NOT_IMPLEMENTED_ +# except AttributeError: +# pass +# else: +# exclude = True +# else: +# exclude = True +# +# if exclude: +# __tags__.extend(["ignore", "subprocess_ignore"]) diff --git a/laplas/abstract_map/pygame/tests/scrap_test.py b/laplas/abstract_map/pygame/tests/scrap_test.py new file mode 100644 index 0000000..6b7f6fa --- /dev/null +++ b/laplas/abstract_map/pygame/tests/scrap_test.py @@ -0,0 +1,301 @@ +import os +import sys + +if os.environ.get("SDL_VIDEODRIVER") == "dummy": + __tags__ = ("ignore", "subprocess_ignore") +import unittest +from pygame.tests.test_utils import trunk_relative_path + +import pygame +from pygame import scrap + + +class ScrapModuleTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + pygame.display.init() + pygame.display.set_mode((1, 1)) + scrap.init() + + @classmethod + def tearDownClass(cls): + # scrap.quit() # Does not exist! + pygame.display.quit() + + def test_init(self): + """Ensures scrap module still initialized after multiple init calls.""" + scrap.init() + scrap.init() + + self.assertTrue(scrap.get_init()) + + def test_init__reinit(self): + """Ensures reinitializing the scrap module doesn't clear its data.""" + data_type = pygame.SCRAP_TEXT + expected_data = b"test_init__reinit" + scrap.put(data_type, expected_data) + + scrap.init() + + self.assertEqual(scrap.get(data_type), expected_data) + + def test_get_init(self): + """Ensures get_init gets the init state.""" + self.assertTrue(scrap.get_init()) + + def todo_test_contains(self): + """Ensures contains works as expected.""" + self.fail() + + def todo_test_get(self): + """Ensures get works as expected.""" + self.fail() + + def test_get__owned_empty_type(self): + """Ensures get works when there is no data of the requested type + in the clipboard and the clipboard is owned by the pygame application. + """ + # Use a unique data type identifier to ensure there is no preexisting + # data. + DATA_TYPE = "test_get__owned_empty_type" + + if scrap.lost(): + # Try to acquire the clipboard. + scrap.put(pygame.SCRAP_TEXT, b"text to clipboard") + + if scrap.lost(): + self.skipTest("requires the pygame application to own the clipboard") + + data = scrap.get(DATA_TYPE) + + self.assertIsNone(data) + + def todo_test_get_types(self): + """Ensures get_types works as expected.""" + self.fail() + + def todo_test_lost(self): + """Ensures lost works as expected.""" + self.fail() + + def test_set_mode(self): + """Ensures set_mode works as expected.""" + scrap.set_mode(pygame.SCRAP_SELECTION) + scrap.set_mode(pygame.SCRAP_CLIPBOARD) + + self.assertRaises(ValueError, scrap.set_mode, 1099) + + def test_put__text(self): + """Ensures put can place text into the clipboard.""" + scrap.put(pygame.SCRAP_TEXT, b"Hello world") + + self.assertEqual(scrap.get(pygame.SCRAP_TEXT), b"Hello world") + + scrap.put(pygame.SCRAP_TEXT, b"Another String") + + self.assertEqual(scrap.get(pygame.SCRAP_TEXT), b"Another String") + + @unittest.skipIf("pygame.image" not in sys.modules, "requires pygame.image module") + def test_put__bmp_image(self): + """Ensures put can place a BMP image into the clipboard.""" + sf = pygame.image.load(trunk_relative_path("examples/data/asprite.bmp")) + expected_string = pygame.image.tostring(sf, "RGBA") + scrap.put(pygame.SCRAP_BMP, expected_string) + + self.assertEqual(scrap.get(pygame.SCRAP_BMP), expected_string) + + def test_put(self): + """Ensures put can place data into the clipboard + when using a user defined type identifier. + """ + DATA_TYPE = "arbitrary buffer" + + scrap.put(DATA_TYPE, b"buf") + r = scrap.get(DATA_TYPE) + + self.assertEqual(r, b"buf") + + +class ScrapModuleClipboardNotOwnedTest(unittest.TestCase): + """Test the scrap module's functionality when the pygame application is + not the current owner of the clipboard. + + A separate class is used to prevent tests that acquire the clipboard from + interfering with these tests. + """ + + @classmethod + def setUpClass(cls): + pygame.display.init() + pygame.display.set_mode((1, 1)) + scrap.init() + + @classmethod + def tearDownClass(cls): + # scrap.quit() # Does not exist! + pygame.quit() + pygame.display.quit() + + def _skip_if_clipboard_owned(self): + # Skip test if the pygame application owns the clipboard. Currently, + # there is no way to give up ownership. + if not scrap.lost(): + self.skipTest("requires the pygame application to not own the clipboard") + + def test_get__not_owned(self): + """Ensures get works when there is no data of the requested type + in the clipboard and the clipboard is not owned by the pygame + application. + """ + self._skip_if_clipboard_owned() + + # Use a unique data type identifier to ensure there is no preexisting + # data. + DATA_TYPE = "test_get__not_owned" + + data = scrap.get(DATA_TYPE) + + self.assertIsNone(data) + + def test_get_types__not_owned(self): + """Ensures get_types works when the clipboard is not owned + by the pygame application. + """ + self._skip_if_clipboard_owned() + + data_types = scrap.get_types() + + self.assertIsInstance(data_types, list) + + def test_contains__not_owned(self): + """Ensures contains works when the clipboard is not owned + by the pygame application. + """ + self._skip_if_clipboard_owned() + + # Use a unique data type identifier to ensure there is no preexisting + # data. + DATA_TYPE = "test_contains__not_owned" + + contains = scrap.contains(DATA_TYPE) + + self.assertFalse(contains) + + def test_lost__not_owned(self): + """Ensures lost works when the clipboard is not owned + by the pygame application. + """ + self._skip_if_clipboard_owned() + + lost = scrap.lost() + + self.assertTrue(lost) + + +class X11InteractiveTest(unittest.TestCase): + __tags__ = ["ignore", "subprocess_ignore"] + try: + pygame.display.init() + except Exception: + pass + else: + if pygame.display.get_driver() == "x11": + __tags__ = ["interactive"] + pygame.display.quit() + + def test_issue_208(self): + """PATCH: pygame.scrap on X11, fix copying into PRIMARY selection + + Copying into theX11 PRIMARY selection (mouse copy/paste) would not + work due to a confusion between content type and clipboard type. + + """ + + from pygame import display, event, freetype + from pygame.locals import SCRAP_SELECTION, SCRAP_TEXT + from pygame.locals import KEYDOWN, K_y, QUIT + + success = False + freetype.init() + font = freetype.Font(None, 24) + display.init() + display.set_caption("Interactive X11 Paste Test") + screen = display.set_mode((600, 200)) + screen.fill(pygame.Color("white")) + text = "Scrap put() succeeded." + msg = ( + "Some text has been placed into the X11 clipboard." + " Please click the center mouse button in an open" + " text window to retrieve it." + '\n\nDid you get "{}"? (y/n)' + ).format(text) + word_wrap(screen, msg, font, 6) + display.flip() + event.pump() + scrap.init() + scrap.set_mode(SCRAP_SELECTION) + scrap.put(SCRAP_TEXT, text.encode("UTF-8")) + while True: + e = event.wait() + if e.type == QUIT: + break + if e.type == KEYDOWN: + success = e.key == K_y + break + pygame.display.quit() + self.assertTrue(success) + + +def word_wrap(surf, text, font, margin=0, color=(0, 0, 0)): + font.origin = True + surf_width, surf_height = surf.get_size() + width = surf_width - 2 * margin + height = surf_height - 2 * margin + line_spacing = int(1.25 * font.get_sized_height()) + x, y = margin, margin + line_spacing + space = font.get_rect(" ") + for word in iwords(text): + if word == "\n": + x, y = margin, y + line_spacing + else: + bounds = font.get_rect(word) + if x + bounds.width + bounds.x >= width: + x, y = margin, y + line_spacing + if x + bounds.width + bounds.x >= width: + raise ValueError("word too wide for the surface") + if y + bounds.height - bounds.y >= height: + raise ValueError("text to long for the surface") + font.render_to(surf, (x, y), None, color) + x += bounds.width + space.width + return x, y + + +def iwords(text): + # r"\n|[^ ]+" + # + head = 0 + tail = head + end = len(text) + while head < end: + if text[head] == " ": + head += 1 + tail = head + 1 + elif text[head] == "\n": + head += 1 + yield "\n" + tail = head + 1 + elif tail == end: + yield text[head:] + head = end + elif text[tail] == "\n": + yield text[head:tail] + head = tail + elif text[tail] == " ": + yield text[head:tail] + head = tail + else: + tail += 1 + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/sndarray_tags.py b/laplas/abstract_map/pygame/tests/sndarray_tags.py new file mode 100644 index 0000000..68fa7a5 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/sndarray_tags.py @@ -0,0 +1,12 @@ +__tags__ = ["array"] + +exclude = False + +try: + import pygame.mixer + import numpy +except ImportError: + exclude = True + +if exclude: + __tags__.extend(("ignore", "subprocess_ignore")) diff --git a/laplas/abstract_map/pygame/tests/sndarray_test.py b/laplas/abstract_map/pygame/tests/sndarray_test.py new file mode 100644 index 0000000..57eb71c --- /dev/null +++ b/laplas/abstract_map/pygame/tests/sndarray_test.py @@ -0,0 +1,154 @@ +import unittest + +from numpy import int8, int16, uint8, uint16, float32, array, all as alltrue + +import pygame +import pygame.sndarray + + +class SndarrayTest(unittest.TestCase): + array_dtypes = {8: uint8, -8: int8, 16: uint16, -16: int16, 32: float32} + + def _assert_compatible(self, arr, size): + dtype = self.array_dtypes[size] + self.assertEqual(arr.dtype, dtype) + + def test_array(self): + def check_array(size, channels, test_data): + try: + pygame.mixer.init(22050, size, channels, allowedchanges=0) + except pygame.error: + # Not all sizes are supported on all systems. + return + try: + __, sz, __ = pygame.mixer.get_init() + if sz == size: + srcarr = array(test_data, self.array_dtypes[size]) + snd = pygame.sndarray.make_sound(srcarr) + arr = pygame.sndarray.array(snd) + self._assert_compatible(arr, size) + self.assertTrue( + alltrue(arr == srcarr), + "size: %i\n%s\n%s" % (size, arr, test_data), + ) + finally: + pygame.mixer.quit() + + check_array(8, 1, [0, 0x0F, 0xF0, 0xFF]) + check_array(8, 2, [[0, 0x80], [0x2D, 0x41], [0x64, 0xA1], [0xFF, 0x40]]) + check_array(16, 1, [0, 0x00FF, 0xFF00, 0xFFFF]) + check_array( + 16, 2, [[0, 0xFFFF], [0xFFFF, 0], [0x00FF, 0xFF00], [0x0F0F, 0xF0F0]] + ) + check_array(-8, 1, [0, -0x80, 0x7F, 0x64]) + check_array(-8, 2, [[0, -0x80], [-0x64, 0x64], [0x25, -0x50], [-1, 0]]) + check_array(-16, 1, [0, 0x7FFF, -0x7FFF, -1]) + check_array(-16, 2, [[0, -0x7FFF], [-0x7FFF, 0], [0x7FFF, 0], [0, 0x7FFF]]) + + def test_get_arraytype(self): + array_type = pygame.sndarray.get_arraytype() + + self.assertEqual(array_type, "numpy", f"unknown array type {array_type}") + + def test_get_arraytypes(self): + arraytypes = pygame.sndarray.get_arraytypes() + self.assertIn("numpy", arraytypes) + + for atype in arraytypes: + self.assertEqual(atype, "numpy", f"unknown array type {atype}") + + def test_make_sound(self): + def check_sound(size, channels, test_data): + try: + pygame.mixer.init(22050, size, channels, allowedchanges=0) + except pygame.error: + # Not all sizes are supported on all systems. + return + try: + __, sz, __ = pygame.mixer.get_init() + if sz == size: + srcarr = array(test_data, self.array_dtypes[size]) + snd = pygame.sndarray.make_sound(srcarr) + arr = pygame.sndarray.samples(snd) + self.assertTrue( + alltrue(arr == srcarr), + "size: %i\n%s\n%s" % (size, arr, test_data), + ) + finally: + pygame.mixer.quit() + + check_sound(8, 1, [0, 0x0F, 0xF0, 0xFF]) + check_sound(8, 2, [[0, 0x80], [0x2D, 0x41], [0x64, 0xA1], [125, 0x40]]) + check_sound(16, 1, [0, 0x00FF, 0xFF00, 0xFFFF]) + check_sound( + 16, 2, [[0, 0xFFFF], [0xFFFF, 0], [0x00FF, 0xFF00], [0x0F0F, 0xF0F0]] + ) + check_sound(-8, 1, [0, -0x80, 0x7F, 0x64]) + check_sound(-8, 2, [[0, -0x80], [-0x64, 0x64], [0x25, -0x50], [-1, 0]]) + check_sound(-16, 1, [0, 0x7FFF, -0x7FFF, -1]) + check_sound(-16, 2, [[0, -0x7FFF], [-0x7FFF, 0], [0x7FFF, 0], [0, 0x7FFF]]) + check_sound(32, 2, [[0.0, -1.0], [-1.0, 0], [1.0, 0], [0, 1.0]]) + + def test_samples(self): + null_byte = b"\x00" + + def check_sample(size, channels, test_data): + try: + pygame.mixer.init(22050, size, channels, allowedchanges=0) + except pygame.error: + # Not all sizes are supported on all systems. + return + try: + __, sz, __ = pygame.mixer.get_init() + if sz == size: + zeroed = null_byte * ((abs(size) // 8) * len(test_data) * channels) + snd = pygame.mixer.Sound(buffer=zeroed) + samples = pygame.sndarray.samples(snd) + self._assert_compatible(samples, size) + ##print('X %s' % (samples.shape,)) + ##print('Y %s' % (test_data,)) + samples[...] = test_data + arr = pygame.sndarray.array(snd) + self.assertTrue( + alltrue(samples == arr), + "size: %i\n%s\n%s" % (size, arr, test_data), + ) + finally: + pygame.mixer.quit() + + check_sample(8, 1, [0, 0x0F, 0xF0, 0xFF]) + check_sample(8, 2, [[0, 0x80], [0x2D, 0x41], [0x64, 0xA1], [0xFF, 0x40]]) + check_sample(16, 1, [0, 0x00FF, 0xFF00, 0xFFFF]) + check_sample( + 16, 2, [[0, 0xFFFF], [0xFFFF, 0], [0x00FF, 0xFF00], [0x0F0F, 0xF0F0]] + ) + check_sample(-8, 1, [0, -0x80, 0x7F, 0x64]) + check_sample(-8, 2, [[0, -0x80], [-0x64, 0x64], [0x25, -0x50], [-1, 0]]) + check_sample(-16, 1, [0, 0x7FFF, -0x7FFF, -1]) + check_sample(-16, 2, [[0, -0x7FFF], [-0x7FFF, 0], [0x7FFF, 0], [0, 0x7FFF]]) + check_sample(32, 2, [[0.0, -1.0], [-1.0, 0], [1.0, 0], [0, 1.0]]) + + def test_use_arraytype(self): + def do_use_arraytype(atype): + pygame.sndarray.use_arraytype(atype) + + pygame.sndarray.use_arraytype("numpy") + self.assertEqual(pygame.sndarray.get_arraytype(), "numpy") + + self.assertRaises(ValueError, do_use_arraytype, "not an option") + + def test_float32(self): + """sized arrays work with Sounds and 32bit float arrays.""" + try: + pygame.mixer.init(22050, 32, 2, allowedchanges=0) + except pygame.error: + # Not all sizes are supported on all systems. + self.skipTest("unsupported mixer configuration") + + arr = array([[0.0, -1.0], [-1.0, 0], [1.0, 0], [0, 1.0]], float32) + newsound = pygame.mixer.Sound(array=arr) + pygame.mixer.quit() + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/sprite_test.py b/laplas/abstract_map/pygame/tests/sprite_test.py new file mode 100644 index 0000000..531263c --- /dev/null +++ b/laplas/abstract_map/pygame/tests/sprite_test.py @@ -0,0 +1,1412 @@ +#################################### IMPORTS ################################### + + +import unittest + +import pygame +from pygame import sprite + + +################################# MODULE LEVEL ################################# + + +class SpriteModuleTest(unittest.TestCase): + pass + + +######################### SPRITECOLLIDE FUNCTIONS TEST ######################### + + +class SpriteCollideTest(unittest.TestCase): + def setUp(self): + self.ag = sprite.AbstractGroup() + self.ag2 = sprite.AbstractGroup() + self.s1 = sprite.Sprite(self.ag) + self.s2 = sprite.Sprite(self.ag2) + self.s3 = sprite.Sprite(self.ag2) + + self.s1.image = pygame.Surface((50, 10), pygame.SRCALPHA, 32) + self.s2.image = pygame.Surface((10, 10), pygame.SRCALPHA, 32) + self.s3.image = pygame.Surface((10, 10), pygame.SRCALPHA, 32) + + self.s1.rect = self.s1.image.get_rect() + self.s2.rect = self.s2.image.get_rect() + self.s3.rect = self.s3.image.get_rect() + self.s2.rect.move_ip(40, 0) + self.s3.rect.move_ip(100, 100) + + def test_spritecollide__works_if_collided_cb_is_None(self): + # Test that sprites collide without collided function. + self.assertEqual( + sprite.spritecollide(self.s1, self.ag2, dokill=False, collided=None), + [self.s2], + ) + + def test_spritecollide__works_if_collided_cb_not_passed(self): + # Should also work when collided function isn't passed at all. + self.assertEqual( + sprite.spritecollide(self.s1, self.ag2, dokill=False), [self.s2] + ) + + def test_spritecollide__collided_must_be_a_callable(self): + # Need to pass a callable. + self.assertRaises( + TypeError, sprite.spritecollide, self.s1, self.ag2, dokill=False, collided=1 + ) + + def test_spritecollide__collided_defaults_to_collide_rect(self): + # collide_rect should behave the same as default. + self.assertEqual( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=sprite.collide_rect + ), + [self.s2], + ) + + def test_collide_rect_ratio__ratio_of_one_like_default(self): + # collide_rect_ratio should behave the same as default at a 1.0 ratio. + self.assertEqual( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=sprite.collide_rect_ratio(1.0) + ), + [self.s2], + ) + + def test_collide_rect_ratio__collides_all_at_ratio_of_twenty(self): + # collide_rect_ratio should collide all at a 20.0 ratio. + collided_func = sprite.collide_rect_ratio(20.0) + expected_sprites = sorted(self.ag2.sprites(), key=id) + + collided_sprites = sorted( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=collided_func + ), + key=id, + ) + + self.assertListEqual(collided_sprites, expected_sprites) + + def test_collide_circle__no_radius_set(self): + # collide_circle with no radius set. + self.assertEqual( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=sprite.collide_circle + ), + [self.s2], + ) + + def test_collide_circle_ratio__no_radius_and_ratio_of_one(self): + # collide_circle_ratio with no radius set, at a 1.0 ratio. + self.assertEqual( + sprite.spritecollide( + self.s1, + self.ag2, + dokill=False, + collided=sprite.collide_circle_ratio(1.0), + ), + [self.s2], + ) + + def test_collide_circle_ratio__no_radius_and_ratio_of_twenty(self): + # collide_circle_ratio with no radius set, at a 20.0 ratio. + collided_func = sprite.collide_circle_ratio(20.0) + expected_sprites = sorted(self.ag2.sprites(), key=id) + + collided_sprites = sorted( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=collided_func + ), + key=id, + ) + + self.assertListEqual(expected_sprites, collided_sprites) + + def test_collide_circle__radius_set_by_collide_circle_ratio(self): + # Call collide_circle_ratio with no radius set, at a 20.0 ratio. + # That should return group ag2 AND set the radius attribute of the + # sprites in such a way that collide_circle would give same result as + # if it had been called without the radius being set. + collided_func = sprite.collide_circle_ratio(20.0) + + sprite.spritecollide(self.s1, self.ag2, dokill=False, collided=collided_func) + + self.assertEqual( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=sprite.collide_circle + ), + [self.s2], + ) + + def test_collide_circle_ratio__no_radius_and_ratio_of_two_twice(self): + # collide_circle_ratio with no radius set, at a 2.0 ratio, + # called twice to check if the bug where the calculated radius + # is not stored correctly in the radius attribute of each sprite. + collided_func = sprite.collide_circle_ratio(2.0) + + # Calling collide_circle_ratio will set the radius attribute of the + # sprites. If an incorrect value is stored then we will not get the + # same result next time it is called: + expected_sprites = sorted( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=collided_func + ), + key=id, + ) + collided_sprites = sorted( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=collided_func + ), + key=id, + ) + + self.assertListEqual(expected_sprites, collided_sprites) + + def test_collide_circle__with_radii_set(self): + # collide_circle with a radius set. + self.s1.radius = 50 + self.s2.radius = 10 + self.s3.radius = 400 + collided_func = sprite.collide_circle + expected_sprites = sorted(self.ag2.sprites(), key=id) + + collided_sprites = sorted( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=collided_func + ), + key=id, + ) + + self.assertListEqual(expected_sprites, collided_sprites) + + def test_collide_circle_ratio__with_radii_set(self): + # collide_circle_ratio with a radius set. + self.s1.radius = 50 + self.s2.radius = 10 + self.s3.radius = 400 + collided_func = sprite.collide_circle_ratio(0.5) + expected_sprites = sorted(self.ag2.sprites(), key=id) + + collided_sprites = sorted( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=collided_func + ), + key=id, + ) + + self.assertListEqual(expected_sprites, collided_sprites) + + def test_collide_mask__opaque(self): + # make some fully opaque sprites that will collide with masks. + self.s1.image.fill((255, 255, 255, 255)) + self.s2.image.fill((255, 255, 255, 255)) + self.s3.image.fill((255, 255, 255, 255)) + + # masks should be autogenerated from image if they don't exist. + self.assertEqual( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=sprite.collide_mask + ), + [self.s2], + ) + + self.s1.mask = pygame.mask.from_surface(self.s1.image) + self.s2.mask = pygame.mask.from_surface(self.s2.image) + self.s3.mask = pygame.mask.from_surface(self.s3.image) + + # with set masks. + self.assertEqual( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=sprite.collide_mask + ), + [self.s2], + ) + + def test_collide_mask__transparent(self): + # make some sprites that are fully transparent, so they won't collide. + self.s1.image.fill((255, 255, 255, 0)) + self.s2.image.fill((255, 255, 255, 0)) + self.s3.image.fill((255, 255, 255, 0)) + + self.s1.mask = pygame.mask.from_surface(self.s1.image, 255) + self.s2.mask = pygame.mask.from_surface(self.s2.image, 255) + self.s3.mask = pygame.mask.from_surface(self.s3.image, 255) + + self.assertFalse( + sprite.spritecollide( + self.s1, self.ag2, dokill=False, collided=sprite.collide_mask + ) + ) + + def test_spritecollideany__without_collided_callback(self): + # pygame.sprite.spritecollideany(sprite, group) -> sprite + # finds any sprites that collide + + # if collided is not passed, all + # sprites must have a "rect" value, which is a + # rectangle of the sprite area, which will be used + # to calculate the collision. + + # s2 in, s3 out + expected_sprite = self.s2 + collided_sprite = sprite.spritecollideany(self.s1, self.ag2) + + self.assertEqual(collided_sprite, expected_sprite) + + # s2 and s3 out + self.s2.rect.move_ip(0, 10) + collided_sprite = sprite.spritecollideany(self.s1, self.ag2) + + self.assertIsNone(collided_sprite) + + # s2 out, s3 in + self.s3.rect.move_ip(-105, -105) + expected_sprite = self.s3 + collided_sprite = sprite.spritecollideany(self.s1, self.ag2) + + self.assertEqual(collided_sprite, expected_sprite) + + # s2 and s3 in + self.s2.rect.move_ip(0, -10) + expected_sprite_choices = self.ag2.sprites() + collided_sprite = sprite.spritecollideany(self.s1, self.ag2) + + self.assertIn(collided_sprite, expected_sprite_choices) + + def test_spritecollideany__with_collided_callback(self): + # pygame.sprite.spritecollideany(sprite, group) -> sprite + # finds any sprites that collide + + # collided is a callback function used to calculate if + # two sprites are colliding. it should take two sprites + # as values, and return a bool value indicating if + # they are colliding. + + # This collision test can be faster than pygame.sprite.spritecollide() + # since it has less work to do. + + arg_dict_a = {} + arg_dict_b = {} + return_container = [True] + + # This function is configurable using the mutable default arguments! + def collided_callback( + spr_a, + spr_b, + arg_dict_a=arg_dict_a, + arg_dict_b=arg_dict_b, + return_container=return_container, + ): + count = arg_dict_a.get(spr_a, 0) + arg_dict_a[spr_a] = 1 + count + + count = arg_dict_b.get(spr_b, 0) + arg_dict_b[spr_b] = 1 + count + + return return_container[0] + + # This should return a sprite from self.ag2 because the callback + # function (collided_callback()) currently returns True. + expected_sprite_choices = self.ag2.sprites() + collided_sprite = sprite.spritecollideany(self.s1, self.ag2, collided_callback) + + self.assertIn(collided_sprite, expected_sprite_choices) + + # The callback function should have been called only once, so self.s1 + # should have only been passed as an argument once + self.assertEqual(len(arg_dict_a), 1) + self.assertEqual(arg_dict_a[self.s1], 1) + + # The callback function should have been called only once, so self.s2 + # exclusive-or self.s3 should have only been passed as an argument + # once + self.assertEqual(len(arg_dict_b), 1) + self.assertEqual(list(arg_dict_b.values())[0], 1) + self.assertTrue(self.s2 in arg_dict_b or self.s3 in arg_dict_b) + + arg_dict_a.clear() + arg_dict_b.clear() + return_container[0] = False + + # This should return None because the callback function + # (collided_callback()) currently returns False. + collided_sprite = sprite.spritecollideany(self.s1, self.ag2, collided_callback) + + self.assertIsNone(collided_sprite) + + # The callback function should have been called as many times as + # there are sprites in self.ag2 + self.assertEqual(len(arg_dict_a), 1) + self.assertEqual(arg_dict_a[self.s1], len(self.ag2)) + self.assertEqual(len(arg_dict_b), len(self.ag2)) + + # Each sprite in self.ag2 should be called once. + for s in self.ag2: + self.assertEqual(arg_dict_b[s], 1) + + def test_groupcollide__without_collided_callback(self): + # pygame.sprite.groupcollide(groupa, groupb, dokilla, dokillb) -> dict + # collision detection between group and group + + # test no kill + expected_dict = {self.s1: [self.s2]} + crashed = pygame.sprite.groupcollide(self.ag, self.ag2, False, False) + + self.assertDictEqual(expected_dict, crashed) + + crashed = pygame.sprite.groupcollide(self.ag, self.ag2, False, False) + + self.assertDictEqual(expected_dict, crashed) + + # Test dokill2=True (kill colliding sprites in second group). + crashed = pygame.sprite.groupcollide(self.ag, self.ag2, False, True) + + self.assertDictEqual(expected_dict, crashed) + + expected_dict = {} + crashed = pygame.sprite.groupcollide(self.ag, self.ag2, False, False) + + self.assertDictEqual(expected_dict, crashed) + + # Test dokill1=True (kill colliding sprites in first group). + self.s3.rect.move_ip(-100, -100) + expected_dict = {self.s1: [self.s3]} + crashed = pygame.sprite.groupcollide(self.ag, self.ag2, True, False) + + self.assertDictEqual(expected_dict, crashed) + + expected_dict = {} + crashed = pygame.sprite.groupcollide(self.ag, self.ag2, False, False) + + self.assertDictEqual(expected_dict, crashed) + + def test_groupcollide__with_collided_callback(self): + collided_callback_true = lambda spr_a, spr_b: True + collided_callback_false = lambda spr_a, spr_b: False + + # test no kill + expected_dict = {} + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, False, False, collided_callback_false + ) + + self.assertDictEqual(expected_dict, crashed) + + expected_dict = {self.s1: sorted(self.ag2.sprites(), key=id)} + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, False, False, collided_callback_true + ) + for value in crashed.values(): + value.sort(key=id) + + self.assertDictEqual(expected_dict, crashed) + + # expected_dict is the same again for this collide + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, False, False, collided_callback_true + ) + for value in crashed.values(): + value.sort(key=id) + + self.assertDictEqual(expected_dict, crashed) + + # Test dokill2=True (kill colliding sprites in second group). + expected_dict = {} + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, False, True, collided_callback_false + ) + + self.assertDictEqual(expected_dict, crashed) + + expected_dict = {self.s1: sorted(self.ag2.sprites(), key=id)} + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, False, True, collided_callback_true + ) + for value in crashed.values(): + value.sort(key=id) + + self.assertDictEqual(expected_dict, crashed) + + expected_dict = {} + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, False, True, collided_callback_true + ) + + self.assertDictEqual(expected_dict, crashed) + + # Test dokill1=True (kill colliding sprites in first group). + self.ag.add(self.s2) + self.ag2.add(self.s3) + expected_dict = {} + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, True, False, collided_callback_false + ) + + self.assertDictEqual(expected_dict, crashed) + + expected_dict = {self.s1: [self.s3], self.s2: [self.s3]} + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, True, False, collided_callback_true + ) + + self.assertDictEqual(expected_dict, crashed) + + expected_dict = {} + crashed = pygame.sprite.groupcollide( + self.ag, self.ag2, True, False, collided_callback_true + ) + + self.assertDictEqual(expected_dict, crashed) + + def test_collide_rect(self): + # Test colliding - some edges touching + self.assertTrue(pygame.sprite.collide_rect(self.s1, self.s2)) + self.assertTrue(pygame.sprite.collide_rect(self.s2, self.s1)) + + # Test colliding - all edges touching + self.s2.rect.center = self.s3.rect.center + + self.assertTrue(pygame.sprite.collide_rect(self.s2, self.s3)) + self.assertTrue(pygame.sprite.collide_rect(self.s3, self.s2)) + + # Test colliding - no edges touching + self.s2.rect.inflate_ip(10, 10) + + self.assertTrue(pygame.sprite.collide_rect(self.s2, self.s3)) + self.assertTrue(pygame.sprite.collide_rect(self.s3, self.s2)) + + # Test colliding - some edges intersecting + self.s2.rect.center = (self.s1.rect.right, self.s1.rect.bottom) + + self.assertTrue(pygame.sprite.collide_rect(self.s1, self.s2)) + self.assertTrue(pygame.sprite.collide_rect(self.s2, self.s1)) + + # Test not colliding + self.assertFalse(pygame.sprite.collide_rect(self.s1, self.s3)) + self.assertFalse(pygame.sprite.collide_rect(self.s3, self.s1)) + + +################################################################################ + + +class AbstractGroupTypeTest(unittest.TestCase): + def setUp(self): + self.ag = sprite.AbstractGroup() + self.ag2 = sprite.AbstractGroup() + self.s1 = sprite.Sprite(self.ag) + self.s2 = sprite.Sprite(self.ag) + self.s3 = sprite.Sprite(self.ag2) + self.s4 = sprite.Sprite(self.ag2) + + self.s1.image = pygame.Surface((10, 10)) + self.s1.image.fill(pygame.Color("red")) + self.s1.rect = self.s1.image.get_rect() + + self.s2.image = pygame.Surface((10, 10)) + self.s2.image.fill(pygame.Color("green")) + self.s2.rect = self.s2.image.get_rect() + self.s2.rect.left = 10 + + self.s3.image = pygame.Surface((10, 10)) + self.s3.image.fill(pygame.Color("blue")) + self.s3.rect = self.s3.image.get_rect() + self.s3.rect.top = 10 + + self.s4.image = pygame.Surface((10, 10)) + self.s4.image.fill(pygame.Color("white")) + self.s4.rect = self.s4.image.get_rect() + self.s4.rect.left = 10 + self.s4.rect.top = 10 + + self.bg = pygame.Surface((20, 20)) + self.scr = pygame.Surface((20, 20)) + self.scr.fill(pygame.Color("grey")) + + def test_has(self): + "See if AbstractGroup.has() works as expected." + + self.assertEqual(True, self.s1 in self.ag) + + self.assertEqual(True, self.ag.has(self.s1)) + + self.assertEqual(True, self.ag.has([self.s1, self.s2])) + + # see if one of them not being in there. + self.assertNotEqual(True, self.ag.has([self.s1, self.s2, self.s3])) + self.assertNotEqual(True, self.ag.has(self.s1, self.s2, self.s3)) + self.assertNotEqual(True, self.ag.has(self.s1, sprite.Group(self.s2, self.s3))) + self.assertNotEqual(True, self.ag.has(self.s1, [self.s2, self.s3])) + + # test empty list processing + self.assertFalse(self.ag.has(*[])) + self.assertFalse(self.ag.has([])) + self.assertFalse(self.ag.has([[]])) + + # see if a second AbstractGroup works. + self.assertEqual(True, self.ag2.has(self.s3)) + + def test_add(self): + ag3 = sprite.AbstractGroup() + sprites = (self.s1, self.s2, self.s3, self.s4) + + for s in sprites: + self.assertNotIn(s, ag3) + + ag3.add(self.s1, [self.s2], self.ag2) + + for s in sprites: + self.assertIn(s, ag3) + + def test_add_internal(self): + self.assertNotIn(self.s1, self.ag2) + + self.ag2.add_internal(self.s1) + + self.assertIn(self.s1, self.ag2) + + def test_clear(self): + self.ag.draw(self.scr) + self.ag.clear(self.scr, self.bg) + self.assertEqual((0, 0, 0, 255), self.scr.get_at((5, 5))) + self.assertEqual((0, 0, 0, 255), self.scr.get_at((15, 5))) + + def test_draw(self): + self.ag.draw(self.scr) + self.assertEqual((255, 0, 0, 255), self.scr.get_at((5, 5))) + self.assertEqual((0, 255, 0, 255), self.scr.get_at((15, 5))) + + self.assertEqual(self.ag.spritedict[self.s1], pygame.Rect(0, 0, 10, 10)) + self.assertEqual(self.ag.spritedict[self.s2], pygame.Rect(10, 0, 10, 10)) + + def test_empty(self): + self.ag.empty() + self.assertFalse(self.s1 in self.ag) + self.assertFalse(self.s2 in self.ag) + + def test_has_internal(self): + self.assertTrue(self.ag.has_internal(self.s1)) + self.assertFalse(self.ag.has_internal(self.s3)) + + def test_remove(self): + # Test removal of 1 sprite + self.ag.remove(self.s1) + self.assertFalse(self.ag in self.s1.groups()) + self.assertFalse(self.ag.has(self.s1)) + + # Test removal of 2 sprites as 2 arguments + self.ag2.remove(self.s3, self.s4) + self.assertFalse(self.ag2 in self.s3.groups()) + self.assertFalse(self.ag2 in self.s4.groups()) + self.assertFalse(self.ag2.has(self.s3, self.s4)) + + # Test removal of 4 sprites as a list containing a sprite and a group + # containing a sprite and another group containing 2 sprites. + self.ag.add(self.s1, self.s3, self.s4) + self.ag2.add(self.s3, self.s4) + g = sprite.Group(self.s2) + self.ag.remove([self.s1, g], self.ag2) + self.assertFalse(self.ag in self.s1.groups()) + self.assertFalse(self.ag in self.s2.groups()) + self.assertFalse(self.ag in self.s3.groups()) + self.assertFalse(self.ag in self.s4.groups()) + self.assertFalse(self.ag.has(self.s1, self.s2, self.s3, self.s4)) + + def test_remove_internal(self): + self.ag.remove_internal(self.s1) + self.assertFalse(self.ag.has_internal(self.s1)) + + def test_sprites(self): + expected_sprites = sorted((self.s1, self.s2), key=id) + sprite_list = sorted(self.ag.sprites(), key=id) + + self.assertListEqual(sprite_list, expected_sprites) + + def test_update(self): + class test_sprite(pygame.sprite.Sprite): + sink = [] + + def __init__(self, *groups): + pygame.sprite.Sprite.__init__(self, *groups) + + def update(self, *args): + self.sink += args + + s = test_sprite(self.ag) + self.ag.update(1, 2, 3) + + self.assertEqual(test_sprite.sink, [1, 2, 3]) + + def test_update_with_kwargs(self): + class test_sprite(pygame.sprite.Sprite): + sink = [] + sink_kwargs = {} + + def __init__(self, *groups): + pygame.sprite.Sprite.__init__(self, *groups) + + def update(self, *args, **kwargs): + self.sink += args + self.sink_kwargs.update(kwargs) + + s = test_sprite(self.ag) + self.ag.update(1, 2, 3, foo=4, bar=5) + + self.assertEqual(test_sprite.sink, [1, 2, 3]) + self.assertEqual(test_sprite.sink_kwargs, {"foo": 4, "bar": 5}) + + +################################################################################ + +# A base class to share tests between similar classes + + +class LayeredGroupBase: + def test_get_layer_of_sprite(self): + expected_layer = 666 + spr = self.sprite() + self.LG.add(spr, layer=expected_layer) + layer = self.LG.get_layer_of_sprite(spr) + + self.assertEqual(len(self.LG._spritelist), 1) + self.assertEqual(layer, self.LG.get_layer_of_sprite(spr)) + self.assertEqual(layer, expected_layer) + self.assertEqual(layer, self.LG._spritelayers[spr]) + + def test_add(self): + expected_layer = self.LG._default_layer + spr = self.sprite() + self.LG.add(spr) + layer = self.LG.get_layer_of_sprite(spr) + + self.assertEqual(len(self.LG._spritelist), 1) + self.assertEqual(layer, expected_layer) + + def test_add__sprite_with_layer_attribute(self): + expected_layer = 100 + spr = self.sprite() + spr._layer = expected_layer + self.LG.add(spr) + layer = self.LG.get_layer_of_sprite(spr) + + self.assertEqual(len(self.LG._spritelist), 1) + self.assertEqual(layer, expected_layer) + + def test_add__passing_layer_keyword(self): + expected_layer = 100 + spr = self.sprite() + self.LG.add(spr, layer=expected_layer) + layer = self.LG.get_layer_of_sprite(spr) + + self.assertEqual(len(self.LG._spritelist), 1) + self.assertEqual(layer, expected_layer) + + def test_add__overriding_sprite_layer_attr(self): + expected_layer = 200 + spr = self.sprite() + spr._layer = 100 + self.LG.add(spr, layer=expected_layer) + layer = self.LG.get_layer_of_sprite(spr) + + self.assertEqual(len(self.LG._spritelist), 1) + self.assertEqual(layer, expected_layer) + + def test_add__adding_sprite_on_init(self): + spr = self.sprite() + lrg2 = sprite.LayeredUpdates(spr) + expected_layer = lrg2._default_layer + layer = lrg2._spritelayers[spr] + + self.assertEqual(len(lrg2._spritelist), 1) + self.assertEqual(layer, expected_layer) + + def test_add__sprite_init_layer_attr(self): + expected_layer = 20 + spr = self.sprite() + spr._layer = expected_layer + lrg2 = sprite.LayeredUpdates(spr) + layer = lrg2._spritelayers[spr] + + self.assertEqual(len(lrg2._spritelist), 1) + self.assertEqual(layer, expected_layer) + + def test_add__sprite_init_passing_layer(self): + expected_layer = 33 + spr = self.sprite() + lrg2 = sprite.LayeredUpdates(spr, layer=expected_layer) + layer = lrg2._spritelayers[spr] + + self.assertEqual(len(lrg2._spritelist), 1) + self.assertEqual(layer, expected_layer) + + def test_add__sprite_init_overiding_layer(self): + expected_layer = 33 + spr = self.sprite() + spr._layer = 55 + lrg2 = sprite.LayeredUpdates(spr, layer=expected_layer) + layer = lrg2._spritelayers[spr] + + self.assertEqual(len(lrg2._spritelist), 1) + self.assertEqual(layer, expected_layer) + + def test_add__spritelist(self): + expected_layer = self.LG._default_layer + sprite_count = 10 + sprites = [self.sprite() for _ in range(sprite_count)] + + self.LG.add(sprites) + + self.assertEqual(len(self.LG._spritelist), sprite_count) + + for i in range(sprite_count): + layer = self.LG.get_layer_of_sprite(sprites[i]) + + self.assertEqual(layer, expected_layer) + + def test_add__spritelist_with_layer_attr(self): + sprites = [] + sprite_and_layer_count = 10 + for i in range(sprite_and_layer_count): + sprites.append(self.sprite()) + sprites[-1]._layer = i + + self.LG.add(sprites) + + self.assertEqual(len(self.LG._spritelist), sprite_and_layer_count) + + for i in range(sprite_and_layer_count): + layer = self.LG.get_layer_of_sprite(sprites[i]) + + self.assertEqual(layer, i) + + def test_add__spritelist_passing_layer(self): + expected_layer = 33 + sprite_count = 10 + sprites = [self.sprite() for _ in range(sprite_count)] + + self.LG.add(sprites, layer=expected_layer) + + self.assertEqual(len(self.LG._spritelist), sprite_count) + + for i in range(sprite_count): + layer = self.LG.get_layer_of_sprite(sprites[i]) + + self.assertEqual(layer, expected_layer) + + def test_add__spritelist_overriding_layer(self): + expected_layer = 33 + sprites = [] + sprite_and_layer_count = 10 + for i in range(sprite_and_layer_count): + sprites.append(self.sprite()) + sprites[-1].layer = i + + self.LG.add(sprites, layer=expected_layer) + + self.assertEqual(len(self.LG._spritelist), sprite_and_layer_count) + + for i in range(sprite_and_layer_count): + layer = self.LG.get_layer_of_sprite(sprites[i]) + + self.assertEqual(layer, expected_layer) + + def test_add__spritelist_init(self): + sprite_count = 10 + sprites = [self.sprite() for _ in range(sprite_count)] + + lrg2 = sprite.LayeredUpdates(sprites) + expected_layer = lrg2._default_layer + + self.assertEqual(len(lrg2._spritelist), sprite_count) + + for i in range(sprite_count): + layer = lrg2.get_layer_of_sprite(sprites[i]) + + self.assertEqual(layer, expected_layer) + + def test_remove__sprite(self): + sprites = [] + sprite_count = 10 + for i in range(sprite_count): + sprites.append(self.sprite()) + sprites[-1].rect = pygame.Rect((0, 0, 0, 0)) + + self.LG.add(sprites) + + self.assertEqual(len(self.LG._spritelist), sprite_count) + + for i in range(sprite_count): + self.LG.remove(sprites[i]) + + self.assertEqual(len(self.LG._spritelist), 0) + + def test_sprites(self): + sprites = [] + sprite_and_layer_count = 10 + for i in range(sprite_and_layer_count, 0, -1): + sprites.append(self.sprite()) + sprites[-1]._layer = i + + self.LG.add(sprites) + + self.assertEqual(len(self.LG._spritelist), sprite_and_layer_count) + + # Sprites should be ordered based on their layer (bottom to top), + # which is the reverse order of the sprites list. + expected_sprites = list(reversed(sprites)) + actual_sprites = self.LG.sprites() + + self.assertListEqual(actual_sprites, expected_sprites) + + def test_layers(self): + sprites = [] + expected_layers = [] + layer_count = 10 + for i in range(layer_count): + expected_layers.append(i) + for j in range(5): + sprites.append(self.sprite()) + sprites[-1]._layer = i + self.LG.add(sprites) + + layers = self.LG.layers() + + self.assertListEqual(layers, expected_layers) + + def test_add__layers_are_correct(self): + layers = [1, 4, 6, 8, 3, 6, 2, 6, 4, 5, 6, 1, 0, 9, 7, 6, 54, 8, 2, 43, 6, 1] + for lay in layers: + self.LG.add(self.sprite(), layer=lay) + layers.sort() + + for idx, spr in enumerate(self.LG.sprites()): + layer = self.LG.get_layer_of_sprite(spr) + + self.assertEqual(layer, layers[idx]) + + def test_change_layer(self): + expected_layer = 99 + spr = self.sprite() + self.LG.add(spr, layer=expected_layer) + + self.assertEqual(self.LG._spritelayers[spr], expected_layer) + + expected_layer = 44 + self.LG.change_layer(spr, expected_layer) + + self.assertEqual(self.LG._spritelayers[spr], expected_layer) + + expected_layer = 77 + spr2 = self.sprite() + spr2.layer = 55 + self.LG.add(spr2) + self.LG.change_layer(spr2, expected_layer) + + self.assertEqual(spr2.layer, expected_layer) + + def test_get_sprites_at(self): + sprites = [] + expected_sprites = [] + for i in range(3): + spr = self.sprite() + spr.rect = pygame.Rect(i * 50, i * 50, 100, 100) + sprites.append(spr) + if i < 2: + expected_sprites.append(spr) + self.LG.add(sprites) + result = self.LG.get_sprites_at((50, 50)) + self.assertEqual(result, expected_sprites) + + def test_get_top_layer(self): + layers = [1, 5, 2, 8, 4, 5, 3, 88, 23, 0] + for i in layers: + self.LG.add(self.sprite(), layer=i) + top_layer = self.LG.get_top_layer() + + self.assertEqual(top_layer, self.LG.get_top_layer()) + self.assertEqual(top_layer, max(layers)) + self.assertEqual(top_layer, max(self.LG._spritelayers.values())) + self.assertEqual(top_layer, self.LG._spritelayers[self.LG._spritelist[-1]]) + + def test_get_bottom_layer(self): + layers = [1, 5, 2, 8, 4, 5, 3, 88, 23, 0] + for i in layers: + self.LG.add(self.sprite(), layer=i) + bottom_layer = self.LG.get_bottom_layer() + + self.assertEqual(bottom_layer, self.LG.get_bottom_layer()) + self.assertEqual(bottom_layer, min(layers)) + self.assertEqual(bottom_layer, min(self.LG._spritelayers.values())) + self.assertEqual(bottom_layer, self.LG._spritelayers[self.LG._spritelist[0]]) + + def test_move_to_front(self): + layers = [1, 5, 2, 8, 4, 5, 3, 88, 23, 0] + for i in layers: + self.LG.add(self.sprite(), layer=i) + spr = self.sprite() + self.LG.add(spr, layer=3) + + self.assertNotEqual(spr, self.LG._spritelist[-1]) + + self.LG.move_to_front(spr) + + self.assertEqual(spr, self.LG._spritelist[-1]) + + def test_move_to_back(self): + layers = [1, 5, 2, 8, 4, 5, 3, 88, 23, 0] + for i in layers: + self.LG.add(self.sprite(), layer=i) + spr = self.sprite() + self.LG.add(spr, layer=55) + + self.assertNotEqual(spr, self.LG._spritelist[0]) + + self.LG.move_to_back(spr) + + self.assertEqual(spr, self.LG._spritelist[0]) + + def test_get_top_sprite(self): + layers = [1, 5, 2, 8, 4, 5, 3, 88, 23, 0] + for i in layers: + self.LG.add(self.sprite(), layer=i) + expected_layer = self.LG.get_top_layer() + layer = self.LG.get_layer_of_sprite(self.LG.get_top_sprite()) + + self.assertEqual(layer, expected_layer) + + def test_get_sprites_from_layer(self): + sprites = {} + layers = [ + 1, + 4, + 5, + 6, + 3, + 7, + 8, + 2, + 1, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 0, + 1, + 6, + 5, + 4, + 3, + 2, + ] + for lay in layers: + spr = self.sprite() + spr._layer = lay + self.LG.add(spr) + if lay not in sprites: + sprites[lay] = [] + sprites[lay].append(spr) + + for lay in self.LG.layers(): + for spr in self.LG.get_sprites_from_layer(lay): + self.assertIn(spr, sprites[lay]) + + sprites[lay].remove(spr) + if len(sprites[lay]) == 0: + del sprites[lay] + + self.assertEqual(len(sprites.values()), 0) + + def test_switch_layer(self): + sprites1 = [] + sprites2 = [] + layers = [3, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 3, 2, 2, 3, 2, 3] + for lay in layers: + spr = self.sprite() + spr._layer = lay + self.LG.add(spr) + if lay == 2: + sprites1.append(spr) + else: + sprites2.append(spr) + + sprites1.sort(key=id) + sprites2.sort(key=id) + layer2_sprites = sorted(self.LG.get_sprites_from_layer(2), key=id) + layer3_sprites = sorted(self.LG.get_sprites_from_layer(3), key=id) + + self.assertListEqual(sprites1, layer2_sprites) + self.assertListEqual(sprites2, layer3_sprites) + self.assertEqual(len(self.LG), len(sprites1) + len(sprites2)) + + self.LG.switch_layer(2, 3) + layer2_sprites = sorted(self.LG.get_sprites_from_layer(2), key=id) + layer3_sprites = sorted(self.LG.get_sprites_from_layer(3), key=id) + + self.assertListEqual(sprites1, layer3_sprites) + self.assertListEqual(sprites2, layer2_sprites) + self.assertEqual(len(self.LG), len(sprites1) + len(sprites2)) + + def test_copy(self): + self.LG.add(self.sprite()) + spr = self.LG.sprites()[0] + lg_copy = self.LG.copy() + + self.assertIsInstance(lg_copy, type(self.LG)) + self.assertIn(spr, lg_copy) + self.assertIn(lg_copy, spr.groups()) + + +########################## LAYERED RENDER GROUP TESTS ########################## + + +class LayeredUpdatesTypeTest__SpriteTest(LayeredGroupBase, unittest.TestCase): + sprite = sprite.Sprite + + def setUp(self): + self.LG = sprite.LayeredUpdates() + + +class LayeredUpdatesTypeTest__DirtySprite(LayeredGroupBase, unittest.TestCase): + sprite = sprite.DirtySprite + + def setUp(self): + self.LG = sprite.LayeredUpdates() + + +class LayeredDirtyTypeTest__DirtySprite(LayeredGroupBase, unittest.TestCase): + sprite = sprite.DirtySprite + + def setUp(self): + self.LG = sprite.LayeredDirty() + + def test_repaint_rect(self): + group = self.LG + surface = pygame.Surface((100, 100)) + + group.repaint_rect(pygame.Rect(0, 0, 100, 100)) + group.draw(surface) + + def test_repaint_rect_with_clip(self): + group = self.LG + surface = pygame.Surface((100, 100)) + + group.set_clip(pygame.Rect(0, 0, 100, 100)) + group.repaint_rect(pygame.Rect(0, 0, 100, 100)) + group.draw(surface) + + def _nondirty_intersections_redrawn(self, use_source_rect=False): + # Helper method to ensure non-dirty sprites are redrawn correctly. + # + # Parameters: + # use_source_rect - allows non-dirty sprites to be tested + # with (True) and without (False) a source_rect + # + # This test was written to reproduce the behavior seen in issue #898. + # A non-dirty sprite (using source_rect) was being redrawn incorrectly + # after a dirty sprite intersected with it. + # + # This test does the following. + # 1. Creates a surface filled with white. Also creates an image_source + # with a default fill color of yellow and adds 2 images to it + # (red and blue rectangles). + # 2. Creates 2 DirtySprites (red_sprite and blue_sprite) using the + # image_source and adds them to a LayeredDirty group. + # 3. Moves the red_sprite and calls LayeredDirty.draw(surface) a few + # times. + # 4. Checks to make sure the sprites were redrawn correctly. + RED = pygame.Color("red") + BLUE = pygame.Color("blue") + WHITE = pygame.Color("white") + YELLOW = pygame.Color("yellow") + + surface = pygame.Surface((60, 80)) + surface.fill(WHITE) + start_pos = (10, 10) + + # These rects define each sprite's image area in the image_source. + red_sprite_source = pygame.Rect((45, 0), (5, 4)) + blue_sprite_source = pygame.Rect((0, 40), (20, 10)) + + # Create a source image/surface. + image_source = pygame.Surface((50, 50)) + image_source.fill(YELLOW) + image_source.fill(RED, red_sprite_source) + image_source.fill(BLUE, blue_sprite_source) + + # The blue_sprite is stationary and will not reset its dirty flag. It + # will be the non-dirty sprite in this test. Its values are dependent + # on the use_source_rect flag. + blue_sprite = pygame.sprite.DirtySprite(self.LG) + + if use_source_rect: + blue_sprite.image = image_source + # The rect is a bit smaller than the source_rect to make sure + # LayeredDirty.draw() is using the correct dimensions. + blue_sprite.rect = pygame.Rect( + start_pos, (blue_sprite_source.w - 7, blue_sprite_source.h - 7) + ) + blue_sprite.source_rect = blue_sprite_source + start_x, start_y = blue_sprite.rect.topleft + end_x = start_x + blue_sprite.source_rect.w + end_y = start_y + blue_sprite.source_rect.h + else: + blue_sprite.image = image_source.subsurface(blue_sprite_source) + blue_sprite.rect = pygame.Rect(start_pos, blue_sprite_source.size) + start_x, start_y = blue_sprite.rect.topleft + end_x, end_y = blue_sprite.rect.bottomright + + # The red_sprite is moving and will always be dirty. + red_sprite = pygame.sprite.DirtySprite(self.LG) + red_sprite.image = image_source + red_sprite.rect = pygame.Rect(start_pos, red_sprite_source.size) + red_sprite.source_rect = red_sprite_source + red_sprite.dirty = 2 + + # Draw the red_sprite as it moves a few steps. + for _ in range(4): + red_sprite.rect.move_ip(2, 1) + + # This is the method being tested. + self.LG.draw(surface) + + # Check colors where the blue_sprite is drawn. We expect red where the + # red_sprite is drawn over the blue_sprite, but the rest should be + # blue. + surface.lock() # Lock surface for possible speed up. + try: + for y in range(start_y, end_y): + for x in range(start_x, end_x): + if red_sprite.rect.collidepoint(x, y): + expected_color = RED + else: + expected_color = BLUE + + color = surface.get_at((x, y)) + + self.assertEqual(color, expected_color, f"pos=({x}, {y})") + finally: + surface.unlock() + + def test_nondirty_intersections_redrawn(self): + """Ensure non-dirty sprites are correctly redrawn + when dirty sprites intersect with them. + """ + self._nondirty_intersections_redrawn() + + def test_nondirty_intersections_redrawn__with_source_rect(self): + """Ensure non-dirty sprites using source_rects are correctly redrawn + when dirty sprites intersect with them. + + Related to issue #898. + """ + self._nondirty_intersections_redrawn(True) + + +############################### SPRITE BASE CLASS ############################## +# +# tests common between sprite classes + + +class SpriteBase: + def setUp(self): + self.groups = [] + for Group in self.Groups: + self.groups.append(Group()) + + self.sprite = self.Sprite() + + def test_add_internal(self): + for g in self.groups: + self.sprite.add_internal(g) + + for g in self.groups: + self.assertIn(g, self.sprite.groups()) + + def test_remove_internal(self): + for g in self.groups: + self.sprite.add_internal(g) + + for g in self.groups: + self.sprite.remove_internal(g) + + for g in self.groups: + self.assertFalse(g in self.sprite.groups()) + + def test_update(self): + # What does this and the next test actually test? + class test_sprite(pygame.sprite.Sprite): + sink = [] + + def __init__(self, *groups): + pygame.sprite.Sprite.__init__(self, *groups) + + def update(self, *args): + self.sink += args + + s = test_sprite() + s.update(1, 2, 3) + + self.assertEqual(test_sprite.sink, [1, 2, 3]) + + def test_update_with_kwargs(self): + class test_sprite(pygame.sprite.Sprite): + sink = [] + sink_dict = {} + + def __init__(self, *groups): + pygame.sprite.Sprite.__init__(self, *groups) + + def update(self, *args, **kwargs): + self.sink += args + self.sink_dict.update(kwargs) + + s = test_sprite() + s.update(1, 2, 3, foo=4, bar=5) + + self.assertEqual(test_sprite.sink, [1, 2, 3]) + self.assertEqual(test_sprite.sink_dict, {"foo": 4, "bar": 5}) + + def test___init____added_to_groups_passed(self): + expected_groups = sorted(self.groups, key=id) + sprite = self.Sprite(self.groups) + groups = sorted(sprite.groups(), key=id) + + self.assertListEqual(groups, expected_groups) + + def test_add(self): + expected_groups = sorted(self.groups, key=id) + self.sprite.add(self.groups) + groups = sorted(self.sprite.groups(), key=id) + + self.assertListEqual(groups, expected_groups) + + def test_alive(self): + self.assertFalse( + self.sprite.alive(), "Sprite should not be alive if in no groups" + ) + + self.sprite.add(self.groups) + + self.assertTrue(self.sprite.alive()) + + def test_groups(self): + for i, g in enumerate(self.groups): + expected_groups = sorted(self.groups[: i + 1], key=id) + self.sprite.add(g) + groups = sorted(self.sprite.groups(), key=id) + + self.assertListEqual(groups, expected_groups) + + def test_kill(self): + self.sprite.add(self.groups) + + self.assertTrue(self.sprite.alive()) + + self.sprite.kill() + + self.assertListEqual(self.sprite.groups(), []) + self.assertFalse(self.sprite.alive()) + + def test_remove(self): + self.sprite.add(self.groups) + self.sprite.remove(self.groups) + + self.assertListEqual(self.sprite.groups(), []) + + +############################## SPRITE CLASS TESTS ############################## + + +class SpriteTypeTest(SpriteBase, unittest.TestCase): + Sprite = sprite.Sprite + + Groups = [ + sprite.Group, + sprite.LayeredUpdates, + sprite.RenderUpdates, + sprite.OrderedUpdates, + ] + + +class DirtySpriteTypeTest(SpriteBase, unittest.TestCase): + Sprite = sprite.DirtySprite + + Groups = [ + sprite.Group, + sprite.LayeredUpdates, + sprite.RenderUpdates, + sprite.OrderedUpdates, + sprite.LayeredDirty, + ] + + +class WeakSpriteTypeTest(SpriteTypeTest): + Sprite = sprite.WeakSprite + + def test_weak_group_ref(self): + """ + We create a list of groups, add them to the sprite. + When we then delete the groups, the sprite should be "dead" + """ + import gc + + groups = [Group() for Group in self.Groups] + self.sprite.add(groups) + del groups + gc.collect() + self.assertFalse(self.sprite.alive()) + + +class DirtyWeakSpriteTypeTest(DirtySpriteTypeTest, WeakSpriteTypeTest): + Sprite = sprite.WeakDirtySprite + + +############################## BUG TESTS ####################################### + + +class SingleGroupBugsTest(unittest.TestCase): + def test_memoryleak_bug(self): + # For memoryleak bug posted to mailing list by Tobias Steinrücken on 16/11/10. + # Fixed in revision 2953. + + import weakref + import gc + + class MySprite(sprite.Sprite): + def __init__(self, *args, **kwargs): + sprite.Sprite.__init__(self, *args, **kwargs) + self.image = pygame.Surface((2, 4), 0, 24) + self.rect = self.image.get_rect() + + g = sprite.GroupSingle() + screen = pygame.Surface((4, 8), 0, 24) + s = MySprite() + r = weakref.ref(s) + g.sprite = s + del s + gc.collect() + + self.assertIsNotNone(r()) + + g.update() + g.draw(screen) + g.sprite = MySprite() + gc.collect() + + self.assertIsNone(r()) + + +################################################################################ + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/surface_test.py b/laplas/abstract_map/pygame/tests/surface_test.py new file mode 100644 index 0000000..b1147d2 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/surface_test.py @@ -0,0 +1,4064 @@ +import os +import unittest +from pygame.tests import test_utils +from pygame.tests.test_utils import ( + example_path, + SurfaceSubclass, +) + +try: + from pygame.tests.test_utils.arrinter import * +except (ImportError, NameError): + pass +import pygame +from pygame.locals import * +from pygame.bufferproxy import BufferProxy + +import platform +import gc +import weakref +import ctypes + +IS_PYPY = "PyPy" == platform.python_implementation() + + +class SurfaceTypeTest(unittest.TestCase): + def test_surface__pixel_format_as_surface_subclass(self): + """Ensure a subclassed surface can be used for pixel format + when creating a new surface.""" + expected_depth = 16 + expected_flags = SRCALPHA + expected_size = (13, 37) + depth_surface = SurfaceSubclass((11, 21), expected_flags, expected_depth) + + surface = pygame.Surface(expected_size, expected_flags, depth_surface) + + self.assertIsNot(surface, depth_surface) + self.assertIsInstance(surface, pygame.Surface) + self.assertNotIsInstance(surface, SurfaceSubclass) + self.assertEqual(surface.get_size(), expected_size) + self.assertEqual(surface.get_flags(), expected_flags) + self.assertEqual(surface.get_bitsize(), expected_depth) + + def test_surface_created_opaque_black(self): + surf = pygame.Surface((20, 20)) + self.assertEqual(surf.get_at((0, 0)), (0, 0, 0, 255)) + + # See https://github.com/pygame/pygame/issues/1395 + pygame.display.set_mode((500, 500)) + surf = pygame.Surface((20, 20)) + self.assertEqual(surf.get_at((0, 0)), (0, 0, 0, 255)) + + def test_set_clip(self): + """see if surface.set_clip(None) works correctly.""" + s = pygame.Surface((800, 600)) + r = pygame.Rect(10, 10, 10, 10) + s.set_clip(r) + r.move_ip(10, 0) + s.set_clip(None) + res = s.get_clip() + # this was garbled before. + self.assertEqual(res[0], 0) + self.assertEqual(res[2], 800) + + def test_print(self): + surf = pygame.Surface((70, 70), 0, 32) + self.assertEqual(repr(surf), "") + + def test_keyword_arguments(self): + surf = pygame.Surface((70, 70), flags=SRCALPHA, depth=32) + self.assertEqual(surf.get_flags() & SRCALPHA, SRCALPHA) + self.assertEqual(surf.get_bitsize(), 32) + + # sanity check to make sure the check below is valid + surf_16 = pygame.Surface((70, 70), 0, 16) + self.assertEqual(surf_16.get_bytesize(), 2) + + # try again with an argument list + surf_16 = pygame.Surface((70, 70), depth=16) + self.assertEqual(surf_16.get_bytesize(), 2) + + def test_set_at(self): + # 24bit surfaces + s = pygame.Surface((100, 100), 0, 24) + s.fill((0, 0, 0)) + + # set it with a tuple. + s.set_at((0, 0), (10, 10, 10, 255)) + r = s.get_at((0, 0)) + self.assertIsInstance(r, pygame.Color) + self.assertEqual(r, (10, 10, 10, 255)) + + # try setting a color with a single integer. + s.fill((0, 0, 0, 255)) + s.set_at((10, 1), 0x0000FF) + r = s.get_at((10, 1)) + self.assertEqual(r, (0, 0, 255, 255)) + + def test_set_at__big_endian(self): + """png files are loaded in big endian format (BGR rather than RGB)""" + pygame.display.init() + try: + image = pygame.image.load(example_path(os.path.join("data", "BGR.png"))) + # Check they start red, green and blue + self.assertEqual(image.get_at((10, 10)), pygame.Color(255, 0, 0)) + self.assertEqual(image.get_at((10, 20)), pygame.Color(0, 255, 0)) + self.assertEqual(image.get_at((10, 40)), pygame.Color(0, 0, 255)) + # Set three pixels that are already red, green, blue + # to red, green and, blue with set_at: + image.set_at((10, 10), pygame.Color(255, 0, 0)) + image.set_at((10, 20), pygame.Color(0, 255, 0)) + image.set_at((10, 40), pygame.Color(0, 0, 255)) + + # Check they still are + self.assertEqual(image.get_at((10, 10)), pygame.Color(255, 0, 0)) + self.assertEqual(image.get_at((10, 20)), pygame.Color(0, 255, 0)) + self.assertEqual(image.get_at((10, 40)), pygame.Color(0, 0, 255)) + + finally: + pygame.display.quit() + + def test_SRCALPHA(self): + # has the flag been passed in ok? + surf = pygame.Surface((70, 70), SRCALPHA, 32) + self.assertEqual(surf.get_flags() & SRCALPHA, SRCALPHA) + + # 24bit surfaces can not have SRCALPHA. + self.assertRaises(ValueError, pygame.Surface, (100, 100), pygame.SRCALPHA, 24) + + # if we have a 32 bit surface, the SRCALPHA should have worked too. + surf2 = pygame.Surface((70, 70), SRCALPHA) + if surf2.get_bitsize() == 32: + self.assertEqual(surf2.get_flags() & SRCALPHA, SRCALPHA) + + def test_flags_default0_nodisplay(self): + """is set to zero, and SRCALPHA is not set by default with no display initialized.""" + pygame.display.quit() + surf = pygame.Surface((70, 70)) + self.assertEqual(surf.get_flags() & SRCALPHA, 0) + + def test_flags_default0_display(self): + """is set to zero, and SRCALPH is not set by default even when the display is initialized.""" + pygame.display.set_mode((320, 200)) + try: + surf = pygame.Surface((70, 70)) + self.assertEqual(surf.get_flags() & SRCALPHA, 0) + finally: + pygame.display.quit() + + def test_masks(self): + def make_surf(bpp, flags, masks): + pygame.Surface((10, 10), flags, bpp, masks) + + # With some masks SDL_CreateRGBSurface does not work properly. + masks = (0xFF000000, 0xFF0000, 0xFF00, 0) + self.assertEqual(make_surf(32, 0, masks), None) + # For 24 and 32 bit surfaces Pygame assumes no losses. + masks = (0x7F0000, 0xFF00, 0xFF, 0) + self.assertRaises(ValueError, make_surf, 24, 0, masks) + self.assertRaises(ValueError, make_surf, 32, 0, masks) + # What contiguous bits in a mask. + masks = (0x6F0000, 0xFF00, 0xFF, 0) + self.assertRaises(ValueError, make_surf, 32, 0, masks) + + def test_get_bounding_rect(self): + surf = pygame.Surface((70, 70), SRCALPHA, 32) + surf.fill((0, 0, 0, 0)) + bound_rect = surf.get_bounding_rect() + self.assertEqual(bound_rect.width, 0) + self.assertEqual(bound_rect.height, 0) + surf.set_at((30, 30), (255, 255, 255, 1)) + bound_rect = surf.get_bounding_rect() + self.assertEqual(bound_rect.left, 30) + self.assertEqual(bound_rect.top, 30) + self.assertEqual(bound_rect.width, 1) + self.assertEqual(bound_rect.height, 1) + surf.set_at((29, 29), (255, 255, 255, 1)) + bound_rect = surf.get_bounding_rect() + self.assertEqual(bound_rect.left, 29) + self.assertEqual(bound_rect.top, 29) + self.assertEqual(bound_rect.width, 2) + self.assertEqual(bound_rect.height, 2) + + surf = pygame.Surface((70, 70), 0, 24) + surf.fill((0, 0, 0)) + bound_rect = surf.get_bounding_rect() + self.assertEqual(bound_rect.width, surf.get_width()) + self.assertEqual(bound_rect.height, surf.get_height()) + + surf.set_colorkey((0, 0, 0)) + bound_rect = surf.get_bounding_rect() + self.assertEqual(bound_rect.width, 0) + self.assertEqual(bound_rect.height, 0) + surf.set_at((30, 30), (255, 255, 255)) + bound_rect = surf.get_bounding_rect() + self.assertEqual(bound_rect.left, 30) + self.assertEqual(bound_rect.top, 30) + self.assertEqual(bound_rect.width, 1) + self.assertEqual(bound_rect.height, 1) + surf.set_at((60, 60), (255, 255, 255)) + bound_rect = surf.get_bounding_rect() + self.assertEqual(bound_rect.left, 30) + self.assertEqual(bound_rect.top, 30) + self.assertEqual(bound_rect.width, 31) + self.assertEqual(bound_rect.height, 31) + + # Issue #180 + pygame.display.init() + try: + surf = pygame.Surface((4, 1), 0, 8) + surf.fill((255, 255, 255)) + surf.get_bounding_rect() # Segfault. + finally: + pygame.display.quit() + + def test_copy(self): + """Ensure a surface can be copied.""" + color = (25, 25, 25, 25) + s1 = pygame.Surface((32, 32), pygame.SRCALPHA, 32) + s1.fill(color) + + s2 = s1.copy() + + s1rect = s1.get_rect() + s2rect = s2.get_rect() + + self.assertEqual(s1rect.size, s2rect.size) + self.assertEqual(s2.get_at((10, 10)), color) + + def test_fill(self): + """Ensure a surface can be filled.""" + color = (25, 25, 25, 25) + fill_rect = pygame.Rect(0, 0, 16, 16) + s1 = pygame.Surface((32, 32), pygame.SRCALPHA, 32) + s1.fill(color, fill_rect) + + for pt in test_utils.rect_area_pts(fill_rect): + self.assertEqual(s1.get_at(pt), color) + + for pt in test_utils.rect_outer_bounds(fill_rect): + self.assertNotEqual(s1.get_at(pt), color) + + def test_fill_rle(self): + """Test RLEACCEL flag with fill()""" + color = (250, 25, 25, 255) + + surf = pygame.Surface((32, 32)) + blit_surf = pygame.Surface((32, 32)) + + blit_surf.set_colorkey((255, 0, 255), pygame.RLEACCEL) + self.assertTrue(blit_surf.get_flags() & pygame.RLEACCELOK) + surf.blit(blit_surf, (0, 0)) + blit_surf.fill(color) + self.assertEqual( + blit_surf.mustlock(), (blit_surf.get_flags() & pygame.RLEACCEL) != 0 + ) + self.assertTrue(blit_surf.get_flags() & pygame.RLEACCEL) + + def test_mustlock_rle(self): + """Test RLEACCEL flag with mustlock()""" + surf = pygame.Surface((100, 100)) + blit_surf = pygame.Surface((100, 100)) + blit_surf.set_colorkey((0, 0, 255), pygame.RLEACCEL) + self.assertTrue(blit_surf.get_flags() & pygame.RLEACCELOK) + surf.blit(blit_surf, (0, 0)) + self.assertTrue(blit_surf.get_flags() & pygame.RLEACCEL) + self.assertTrue(blit_surf.mustlock()) + + def test_mustlock_surf_alpha_rle(self): + """Test RLEACCEL flag with mustlock() on a surface + with per pixel alpha - new feature in SDL2""" + surf = pygame.Surface((100, 100)) + blit_surf = pygame.Surface((100, 100), depth=32, flags=pygame.SRCALPHA) + blit_surf.set_colorkey((192, 191, 192, 255), pygame.RLEACCEL) + self.assertTrue(blit_surf.get_flags() & pygame.RLEACCELOK) + surf.blit(blit_surf, (0, 0)) + self.assertTrue(blit_surf.get_flags() & pygame.RLEACCEL) + self.assertTrue(blit_surf.get_flags() & pygame.SRCALPHA) + self.assertTrue(blit_surf.mustlock()) + + def test_copy_rle(self): + """Test copying a surface set to use run length encoding""" + s1 = pygame.Surface((32, 32), 24) + s1.set_colorkey((255, 0, 255), pygame.RLEACCEL) + self.assertTrue(s1.get_flags() & pygame.RLEACCELOK) + + newsurf = s1.copy() + self.assertTrue(s1.get_flags() & pygame.RLEACCELOK) + self.assertTrue(newsurf.get_flags() & pygame.RLEACCELOK) + + def test_subsurface_rle(self): + """Ensure an RLE sub-surface works independently of its parent.""" + color = (250, 25, 25, 255) + color2 = (200, 200, 250, 255) + sub_rect = pygame.Rect(16, 16, 16, 16) + s0 = pygame.Surface((32, 32), 24) + s1 = pygame.Surface((32, 32), 24) + s1.set_colorkey((255, 0, 255), pygame.RLEACCEL) + s1.fill(color) + s2 = s1.subsurface(sub_rect) + s2.fill(color2) + s0.blit(s1, (0, 0)) + self.assertTrue(s1.get_flags() & pygame.RLEACCEL) + self.assertTrue(not s2.get_flags() & pygame.RLEACCEL) + + def test_subsurface_rle2(self): + """Ensure an RLE sub-surface works independently of its parent.""" + color = (250, 25, 25, 255) + color2 = (200, 200, 250, 255) + sub_rect = pygame.Rect(16, 16, 16, 16) + + s0 = pygame.Surface((32, 32), 24) + s1 = pygame.Surface((32, 32), 24) + s1.set_colorkey((255, 0, 255), pygame.RLEACCEL) + s1.fill(color) + s2 = s1.subsurface(sub_rect) + s2.fill(color2) + s0.blit(s2, (0, 0)) + self.assertTrue(s1.get_flags() & pygame.RLEACCELOK) + self.assertTrue(not s2.get_flags() & pygame.RLEACCELOK) + + def test_solarwolf_rle_usage(self): + """Test for error/crash when calling set_colorkey() followed + by convert twice in succession. Code originally taken + from solarwolf.""" + + def optimize(img): + clear = img.get_colorkey() + img.set_colorkey(clear, RLEACCEL) + self.assertEqual(img.get_colorkey(), clear) + return img.convert() + + pygame.display.init() + try: + pygame.display.set_mode((640, 480)) + + image = pygame.image.load(example_path(os.path.join("data", "alien1.png"))) + image = image.convert() + orig_colorkey = image.get_colorkey() + + image = optimize(image) + image = optimize(image) + self.assertTrue(image.get_flags() & pygame.RLEACCELOK) + self.assertTrue(not image.get_flags() & pygame.RLEACCEL) + self.assertEqual(image.get_colorkey(), orig_colorkey) + self.assertTrue(isinstance(image, pygame.Surface)) + finally: + pygame.display.quit() + + def test_solarwolf_rle_usage_2(self): + """Test for RLE status after setting alpha""" + + pygame.display.init() + try: + pygame.display.set_mode((640, 480), depth=32) + blit_to_surf = pygame.Surface((100, 100)) + + image = pygame.image.load(example_path(os.path.join("data", "alien1.png"))) + image = image.convert() + orig_colorkey = image.get_colorkey() + + # set the colorkey with RLEACCEL, should add the RLEACCELOK flag + image.set_colorkey(orig_colorkey, RLEACCEL) + self.assertTrue(image.get_flags() & pygame.RLEACCELOK) + self.assertTrue(not image.get_flags() & pygame.RLEACCEL) + + # now blit the surface - should add the RLEACCEL flag + blit_to_surf.blit(image, (0, 0)) + self.assertTrue(image.get_flags() & pygame.RLEACCELOK) + self.assertTrue(image.get_flags() & pygame.RLEACCEL) + + # Now set the alpha, without RLE acceleration - should strip all + # RLE flags + image.set_alpha(90) + self.assertTrue(not image.get_flags() & pygame.RLEACCELOK) + self.assertTrue(not image.get_flags() & pygame.RLEACCEL) + + finally: + pygame.display.quit() + + def test_set_alpha__set_colorkey_rle(self): + pygame.display.init() + try: + pygame.display.set_mode((640, 480)) + blit_to_surf = pygame.Surface((80, 71)) + blit_to_surf.fill((255, 255, 255)) + + image = pygame.image.load(example_path(os.path.join("data", "alien1.png"))) + image = image.convert() + orig_colorkey = image.get_colorkey() + + # Add the RLE flag while setting alpha for the whole surface + image.set_alpha(90, RLEACCEL) + blit_to_surf.blit(image, (0, 0)) + sample_pixel_rle = blit_to_surf.get_at((50, 50)) + + # Now reset the colorkey to the original value with RLE + self.assertEqual(image.get_colorkey(), orig_colorkey) + image.set_colorkey(orig_colorkey, RLEACCEL) + blit_to_surf.fill((255, 255, 255)) + blit_to_surf.blit(image, (0, 0)) + sample_pixel_no_rle = blit_to_surf.get_at((50, 50)) + + self.assertAlmostEqual(sample_pixel_rle.r, sample_pixel_no_rle.r, delta=2) + self.assertAlmostEqual(sample_pixel_rle.g, sample_pixel_no_rle.g, delta=2) + self.assertAlmostEqual(sample_pixel_rle.b, sample_pixel_no_rle.b, delta=2) + + finally: + pygame.display.quit() + + def test_fill_negative_coordinates(self): + # negative coordinates should be clipped by fill, and not draw outside the surface. + color = (25, 25, 25, 25) + color2 = (20, 20, 20, 25) + fill_rect = pygame.Rect(-10, -10, 16, 16) + + s1 = pygame.Surface((32, 32), pygame.SRCALPHA, 32) + r1 = s1.fill(color, fill_rect) + c = s1.get_at((0, 0)) + self.assertEqual(c, color) + + # make subsurface in the middle to test it doesn't over write. + s2 = s1.subsurface((5, 5, 5, 5)) + r2 = s2.fill(color2, (-3, -3, 5, 5)) + c2 = s1.get_at((4, 4)) + self.assertEqual(c, color) + + # rect returns the area we actually fill. + r3 = s2.fill(color2, (-30, -30, 5, 5)) + # since we are using negative coords, it should be an zero sized rect. + self.assertEqual(tuple(r3), (0, 0, 0, 0)) + + def test_fill_keyword_args(self): + """Ensure fill() accepts keyword arguments.""" + color = (1, 2, 3, 255) + area = (1, 1, 2, 2) + s1 = pygame.Surface((4, 4), 0, 32) + s1.fill(special_flags=pygame.BLEND_ADD, color=color, rect=area) + + self.assertEqual(s1.get_at((0, 0)), (0, 0, 0, 255)) + self.assertEqual(s1.get_at((1, 1)), color) + + ######################################################################## + + def test_get_alpha(self): + """Ensure a surface's alpha value can be retrieved.""" + s1 = pygame.Surface((32, 32), pygame.SRCALPHA, 32) + + self.assertEqual(s1.get_alpha(), 255) + + for alpha in (0, 32, 127, 255): + s1.set_alpha(alpha) + for t in range(4): + s1.set_alpha(s1.get_alpha()) + + self.assertEqual(s1.get_alpha(), alpha) + + ######################################################################## + + def test_get_bytesize(self): + """Ensure a surface's bit and byte sizes can be retrieved.""" + pygame.display.init() + try: + depth = 32 + depth_bytes = 4 + s1 = pygame.Surface((32, 32), pygame.SRCALPHA, depth) + + self.assertEqual(s1.get_bytesize(), depth_bytes) + self.assertEqual(s1.get_bitsize(), depth) + + depth = 15 + depth_bytes = 2 + s1 = pygame.Surface((32, 32), 0, depth) + + self.assertEqual(s1.get_bytesize(), depth_bytes) + self.assertEqual(s1.get_bitsize(), depth) + + depth = 12 + depth_bytes = 2 + s1 = pygame.Surface((32, 32), 0, depth) + + self.assertEqual(s1.get_bytesize(), depth_bytes) + self.assertEqual(s1.get_bitsize(), depth) + + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode() + pygame.display.quit() + surface.get_bytesize() + finally: + pygame.display.quit() + + ######################################################################## + + def test_get_flags(self): + """Ensure a surface's flags can be retrieved.""" + s1 = pygame.Surface((32, 32), pygame.SRCALPHA, 32) + + self.assertEqual(s1.get_flags(), pygame.SRCALPHA) + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", + 'requires a non-"dummy" SDL_VIDEODRIVER', + ) + def test_get_flags__display_surf(self): + pygame.display.init() + try: + # FULLSCREEN + screen_surf = pygame.display.set_mode((600, 400), flags=0) + self.assertFalse(screen_surf.get_flags() & pygame.FULLSCREEN) + + screen_surf = pygame.display.set_mode((600, 400), flags=pygame.FULLSCREEN) + self.assertTrue(screen_surf.get_flags() & pygame.FULLSCREEN) + + # NOFRAME + screen_surf = pygame.display.set_mode((600, 400), flags=0) + self.assertFalse(screen_surf.get_flags() & pygame.NOFRAME) + + screen_surf = pygame.display.set_mode((600, 400), flags=pygame.NOFRAME) + self.assertTrue(screen_surf.get_flags() & pygame.NOFRAME) + + # RESIZABLE + screen_surf = pygame.display.set_mode((600, 400), flags=0) + self.assertFalse(screen_surf.get_flags() & pygame.RESIZABLE) + + screen_surf = pygame.display.set_mode((600, 400), flags=pygame.RESIZABLE) + self.assertTrue(screen_surf.get_flags() & pygame.RESIZABLE) + + # OPENGL + screen_surf = pygame.display.set_mode((600, 400), flags=0) + # it can have an OPENGL flag by default on Macos? + if not (screen_surf.get_flags() & pygame.OPENGL): + self.assertFalse(screen_surf.get_flags() & pygame.OPENGL) + + try: + pygame.display.set_mode((200, 200), pygame.OPENGL, 32) + except pygame.error: + pass # If we can't create OPENGL surface don't try this test + else: + self.assertTrue(screen_surf.get_flags() & pygame.OPENGL) + finally: + pygame.display.quit() + + ######################################################################## + + def test_get_parent(self): + """Ensure a surface's parent can be retrieved.""" + pygame.display.init() + try: + parent = pygame.Surface((16, 16)) + child = parent.subsurface((0, 0, 5, 5)) + + self.assertIs(child.get_parent(), parent) + + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode() + pygame.display.quit() + surface.get_parent() + finally: + pygame.display.quit() + + ######################################################################## + + def test_get_rect(self): + """Ensure a surface's rect can be retrieved.""" + size = (16, 16) + surf = pygame.Surface(size) + rect = surf.get_rect() + + self.assertEqual(rect.size, size) + + ######################################################################## + + def test_get_width__size_and_height(self): + """Ensure a surface's size, width and height can be retrieved.""" + for w in range(0, 255, 32): + for h in range(0, 127, 15): + s = pygame.Surface((w, h)) + self.assertEqual(s.get_width(), w) + self.assertEqual(s.get_height(), h) + self.assertEqual(s.get_size(), (w, h)) + + def test_get_view(self): + """Ensure a buffer view of the surface's pixels can be retrieved.""" + # Check that BufferProxys are returned when array depth is supported, + # ValueErrors returned otherwise. + Error = ValueError + s = pygame.Surface((5, 7), 0, 8) + v2 = s.get_view("2") + + self.assertRaises(Error, s.get_view, "0") + self.assertRaises(Error, s.get_view, "1") + self.assertIsInstance(v2, BufferProxy) + self.assertRaises(Error, s.get_view, "3") + + s = pygame.Surface((8, 7), 0, 8) + length = s.get_bytesize() * s.get_width() * s.get_height() + v0 = s.get_view("0") + v1 = s.get_view("1") + + self.assertIsInstance(v0, BufferProxy) + self.assertEqual(v0.length, length) + self.assertIsInstance(v1, BufferProxy) + self.assertEqual(v1.length, length) + + s = pygame.Surface((5, 7), 0, 16) + v2 = s.get_view("2") + + self.assertRaises(Error, s.get_view, "0") + self.assertRaises(Error, s.get_view, "1") + self.assertIsInstance(v2, BufferProxy) + self.assertRaises(Error, s.get_view, "3") + + s = pygame.Surface((8, 7), 0, 16) + length = s.get_bytesize() * s.get_width() * s.get_height() + v0 = s.get_view("0") + v1 = s.get_view("1") + + self.assertIsInstance(v0, BufferProxy) + self.assertEqual(v0.length, length) + self.assertIsInstance(v1, BufferProxy) + self.assertEqual(v1.length, length) + + s = pygame.Surface((5, 7), pygame.SRCALPHA, 16) + v2 = s.get_view("2") + + self.assertIsInstance(v2, BufferProxy) + self.assertRaises(Error, s.get_view, "3") + + s = pygame.Surface((5, 7), 0, 24) + v2 = s.get_view("2") + v3 = s.get_view("3") + + self.assertRaises(Error, s.get_view, "0") + self.assertRaises(Error, s.get_view, "1") + self.assertIsInstance(v2, BufferProxy) + self.assertIsInstance(v3, BufferProxy) + + s = pygame.Surface((8, 7), 0, 24) + length = s.get_bytesize() * s.get_width() * s.get_height() + v0 = s.get_view("0") + v1 = s.get_view("1") + + self.assertIsInstance(v0, BufferProxy) + self.assertEqual(v0.length, length) + self.assertIsInstance(v1, BufferProxy) + self.assertEqual(v1.length, length) + + s = pygame.Surface((5, 7), 0, 32) + length = s.get_bytesize() * s.get_width() * s.get_height() + v0 = s.get_view("0") + v1 = s.get_view("1") + v2 = s.get_view("2") + v3 = s.get_view("3") + + self.assertIsInstance(v0, BufferProxy) + self.assertEqual(v0.length, length) + self.assertIsInstance(v1, BufferProxy) + self.assertEqual(v1.length, length) + self.assertIsInstance(v2, BufferProxy) + self.assertIsInstance(v3, BufferProxy) + + s2 = s.subsurface((0, 0, 4, 7)) + + self.assertRaises(Error, s2.get_view, "0") + self.assertRaises(Error, s2.get_view, "1") + + s2 = None + s = pygame.Surface((5, 7), pygame.SRCALPHA, 32) + + for kind in ("2", "3", "a", "A", "r", "R", "g", "G", "b", "B"): + self.assertIsInstance(s.get_view(kind), BufferProxy) + + # Check default argument value: '2' + s = pygame.Surface((2, 4), 0, 32) + v = s.get_view() + if not IS_PYPY: + ai = ArrayInterface(v) + self.assertEqual(ai.nd, 2) + + # Check locking. + s = pygame.Surface((2, 4), 0, 32) + + self.assertFalse(s.get_locked()) + + v = s.get_view("2") + + self.assertFalse(s.get_locked()) + + c = v.__array_interface__ + + self.assertTrue(s.get_locked()) + + c = None + gc.collect() + + self.assertTrue(s.get_locked()) + + v = None + gc.collect() + + self.assertFalse(s.get_locked()) + + # Check invalid view kind values. + s = pygame.Surface((2, 4), pygame.SRCALPHA, 32) + self.assertRaises(TypeError, s.get_view, "") + self.assertRaises(TypeError, s.get_view, "9") + self.assertRaises(TypeError, s.get_view, "RGBA") + self.assertRaises(TypeError, s.get_view, 2) + + # Both unicode and bytes strings are allowed for kind. + s = pygame.Surface((2, 4), 0, 32) + s.get_view("2") + s.get_view(b"2") + + # Garbage collection + s = pygame.Surface((2, 4), 0, 32) + weak_s = weakref.ref(s) + v = s.get_view("3") + weak_v = weakref.ref(v) + gc.collect() + self.assertTrue(weak_s() is s) + self.assertTrue(weak_v() is v) + del v + gc.collect() + self.assertTrue(weak_s() is s) + self.assertTrue(weak_v() is None) + del s + gc.collect() + self.assertTrue(weak_s() is None) + + def test_get_buffer(self): + # Check that get_buffer works for all pixel sizes and for a subsurface. + + # Check for all pixel sizes + for bitsize in [8, 16, 24, 32]: + s = pygame.Surface((5, 7), 0, bitsize) + length = s.get_pitch() * s.get_height() + v = s.get_buffer() + + self.assertIsInstance(v, BufferProxy) + self.assertEqual(v.length, length) + self.assertEqual(repr(v), f"") + + # Check for a subsurface (not contiguous) + s = pygame.Surface((7, 10), 0, 32) + s2 = s.subsurface((1, 2, 5, 7)) + length = s2.get_pitch() * s2.get_height() + v = s2.get_buffer() + + self.assertIsInstance(v, BufferProxy) + self.assertEqual(v.length, length) + + # Check locking. + s = pygame.Surface((2, 4), 0, 32) + v = s.get_buffer() + self.assertTrue(s.get_locked()) + v = None + gc.collect() + self.assertFalse(s.get_locked()) + + OLDBUF = hasattr(pygame.bufferproxy, "get_segcount") + + @unittest.skipIf(not OLDBUF, "old buffer not available") + def test_get_buffer_oldbuf(self): + from pygame.bufferproxy import get_segcount, get_write_buffer + + s = pygame.Surface((2, 4), pygame.SRCALPHA, 32) + v = s.get_buffer() + segcount, buflen = get_segcount(v) + self.assertEqual(segcount, 1) + self.assertEqual(buflen, s.get_pitch() * s.get_height()) + seglen, segaddr = get_write_buffer(v, 0) + self.assertEqual(segaddr, s._pixels_address) + self.assertEqual(seglen, buflen) + + @unittest.skipIf(not OLDBUF, "old buffer not available") + def test_get_view_oldbuf(self): + from pygame.bufferproxy import get_segcount, get_write_buffer + + s = pygame.Surface((2, 4), pygame.SRCALPHA, 32) + v = s.get_view("1") + segcount, buflen = get_segcount(v) + self.assertEqual(segcount, 8) + self.assertEqual(buflen, s.get_pitch() * s.get_height()) + seglen, segaddr = get_write_buffer(v, 7) + self.assertEqual(segaddr, s._pixels_address + s.get_bytesize() * 7) + self.assertEqual(seglen, s.get_bytesize()) + + def test_set_colorkey(self): + # __doc__ (as of 2008-06-25) for pygame.surface.Surface.set_colorkey: + + # Surface.set_colorkey(Color, flags=0): return None + # Surface.set_colorkey(None): return None + # Set the transparent colorkey + + s = pygame.Surface((16, 16), pygame.SRCALPHA, 32) + + colorkeys = ((20, 189, 20, 255), (128, 50, 50, 255), (23, 21, 255, 255)) + + for colorkey in colorkeys: + s.set_colorkey(colorkey) + + for t in range(4): + s.set_colorkey(s.get_colorkey()) + + self.assertEqual(s.get_colorkey(), colorkey) + + def test_set_masks(self): + s = pygame.Surface((32, 32)) + r, g, b, a = s.get_masks() + self.assertRaises(TypeError, s.set_masks, (b, g, r, a)) + + def test_set_shifts(self): + s = pygame.Surface((32, 32)) + r, g, b, a = s.get_shifts() + self.assertRaises(TypeError, s.set_shifts, (b, g, r, a)) + + def test_blit_keyword_args(self): + color = (1, 2, 3, 255) + s1 = pygame.Surface((4, 4), 0, 32) + s2 = pygame.Surface((2, 2), 0, 32) + s2.fill((1, 2, 3)) + s1.blit(special_flags=BLEND_ADD, source=s2, dest=(1, 1), area=s2.get_rect()) + self.assertEqual(s1.get_at((0, 0)), (0, 0, 0, 255)) + self.assertEqual(s1.get_at((1, 1)), color) + + def test_blit_big_rects(self): + """SDL2 can have more than 16 bits for x, y, width, height.""" + big_surf = pygame.Surface((100, 68000), 0, 32) + big_surf_color = (255, 0, 0) + big_surf.fill(big_surf_color) + + background = pygame.Surface((500, 500), 0, 32) + background_color = (0, 255, 0) + background.fill(background_color) + + # copy parts of the big_surf using more than 16bit parts. + background.blit(big_surf, (100, 100), area=(0, 16000, 100, 100)) + background.blit(big_surf, (200, 200), area=(0, 32000, 100, 100)) + background.blit(big_surf, (300, 300), area=(0, 66000, 100, 100)) + + # check that all three areas are drawn. + self.assertEqual(background.get_at((101, 101)), big_surf_color) + self.assertEqual(background.get_at((201, 201)), big_surf_color) + self.assertEqual(background.get_at((301, 301)), big_surf_color) + + # areas outside the 3 blitted areas not covered by those blits. + self.assertEqual(background.get_at((400, 301)), background_color) + self.assertEqual(background.get_at((400, 201)), background_color) + self.assertEqual(background.get_at((100, 201)), background_color) + self.assertEqual(background.get_at((99, 99)), background_color) + self.assertEqual(background.get_at((450, 450)), background_color) + + +class TestSurfaceBlit(unittest.TestCase): + """Tests basic blitting functionality and options.""" + + # __doc__ (as of 2008-08-02) for pygame.surface.Surface.blit: + + # Surface.blit(source, dest, area=None, special_flags = 0): return Rect + # draw one image onto another + # + # Draws a source Surface onto this Surface. The draw can be positioned + # with the dest argument. Dest can either be pair of coordinates + # representing the upper left corner of the source. A Rect can also be + # passed as the destination and the topleft corner of the rectangle + # will be used as the position for the blit. The size of the + # destination rectangle does not effect the blit. + # + # An optional area rectangle can be passed as well. This represents a + # smaller portion of the source Surface to draw. + # + # An optional special flags is for passing in new in 1.8.0: BLEND_ADD, + # BLEND_SUB, BLEND_MULT, BLEND_MIN, BLEND_MAX new in 1.8.1: + # BLEND_RGBA_ADD, BLEND_RGBA_SUB, BLEND_RGBA_MULT, BLEND_RGBA_MIN, + # BLEND_RGBA_MAX BLEND_RGB_ADD, BLEND_RGB_SUB, BLEND_RGB_MULT, + # BLEND_RGB_MIN, BLEND_RGB_MAX With other special blitting flags + # perhaps added in the future. + # + # The return rectangle is the area of the affected pixels, excluding + # any pixels outside the destination Surface, or outside the clipping + # area. + # + # Pixel alphas will be ignored when blitting to an 8 bit Surface. + # special_flags new in pygame 1.8. + + def setUp(self): + """Resets starting surfaces.""" + self.src_surface = pygame.Surface((256, 256), 32) + self.src_surface.fill(pygame.Color(255, 255, 255)) + self.dst_surface = pygame.Surface((64, 64), 32) + self.dst_surface.fill(pygame.Color(0, 0, 0)) + + def test_blit_overflow_coord(self): + """Full coverage w/ overflow, specified with Coordinate""" + result = self.dst_surface.blit(self.src_surface, (0, 0)) + self.assertIsInstance(result, pygame.Rect) + self.assertEqual(result.size, (64, 64)) + for k in [(x, x) for x in range(64)]: + self.assertEqual(self.dst_surface.get_at(k), (255, 255, 255)) + + def test_blit_overflow_rect(self): + """Full coverage w/ overflow, specified with a Rect""" + result = self.dst_surface.blit(self.src_surface, pygame.Rect(-1, -1, 300, 300)) + self.assertIsInstance(result, pygame.Rect) + self.assertEqual(result.size, (64, 64)) + for k in [(x, x) for x in range(64)]: + self.assertEqual(self.dst_surface.get_at(k), (255, 255, 255)) + + def test_blit_overflow_nonorigin(self): + """Test Rectangle Dest, with overflow but with starting rect with top-left at (1,1)""" + result = self.dst_surface.blit(self.src_surface, dest=pygame.Rect((1, 1, 1, 1))) + self.assertIsInstance(result, pygame.Rect) + self.assertEqual(result.size, (63, 63)) + self.assertEqual(self.dst_surface.get_at((0, 0)), (0, 0, 0)) + self.assertEqual(self.dst_surface.get_at((63, 0)), (0, 0, 0)) + self.assertEqual(self.dst_surface.get_at((0, 63)), (0, 0, 0)) + self.assertEqual(self.dst_surface.get_at((1, 1)), (255, 255, 255)) + self.assertEqual(self.dst_surface.get_at((63, 63)), (255, 255, 255)) + + def test_blit_area_contraint(self): + """Testing area constraint""" + result = self.dst_surface.blit( + self.src_surface, + dest=pygame.Rect((1, 1, 1, 1)), + area=pygame.Rect((2, 2, 2, 2)), + ) + self.assertIsInstance(result, pygame.Rect) + self.assertEqual(result.size, (2, 2)) + self.assertEqual(self.dst_surface.get_at((0, 0)), (0, 0, 0)) # Corners + self.assertEqual(self.dst_surface.get_at((63, 0)), (0, 0, 0)) + self.assertEqual(self.dst_surface.get_at((0, 63)), (0, 0, 0)) + self.assertEqual(self.dst_surface.get_at((63, 63)), (0, 0, 0)) + self.assertEqual( + self.dst_surface.get_at((1, 1)), (255, 255, 255) + ) # Blitted Area + self.assertEqual(self.dst_surface.get_at((2, 2)), (255, 255, 255)) + self.assertEqual(self.dst_surface.get_at((3, 3)), (0, 0, 0)) + # Should stop short of filling in (3,3) + + def test_blit_zero_overlap(self): + """Testing zero-overlap condition.""" + result = self.dst_surface.blit( + self.src_surface, + dest=pygame.Rect((-256, -256, 1, 1)), + area=pygame.Rect((2, 2, 256, 256)), + ) + self.assertIsInstance(result, pygame.Rect) + self.assertEqual(result.size, (0, 0)) # No blitting expected + for k in [(x, x) for x in range(64)]: + self.assertEqual(self.dst_surface.get_at(k), (0, 0, 0)) # Diagonal + self.assertEqual( + self.dst_surface.get_at((63, 0)), (0, 0, 0) + ) # Remaining corners + self.assertEqual(self.dst_surface.get_at((0, 63)), (0, 0, 0)) + + def test_blit__SRCALPHA_opaque_source(self): + src = pygame.Surface((256, 256), SRCALPHA, 32) + dst = src.copy() + + for i, j in test_utils.rect_area_pts(src.get_rect()): + dst.set_at((i, j), (i, 0, 0, j)) + src.set_at((i, j), (0, i, 0, 255)) + + dst.blit(src, (0, 0)) + + for pt in test_utils.rect_area_pts(src.get_rect()): + self.assertEqual(dst.get_at(pt)[1], src.get_at(pt)[1]) + + def test_blit__blit_to_self(self): + """Test that blit operation works on self, alpha value is + correct, and that no RGB distortion occurs.""" + test_surface = pygame.Surface((128, 128), SRCALPHA, 32) + area = test_surface.get_rect() + + for pt, test_color in test_utils.gradient(area.width, area.height): + test_surface.set_at(pt, test_color) + + reference_surface = test_surface.copy() + + test_surface.blit(test_surface, (0, 0)) + + for x in range(area.width): + for y in range(area.height): + (r, g, b, a) = reference_color = reference_surface.get_at((x, y)) + expected_color = (r, g, b, (a + (a * ((256 - a) // 256)))) + self.assertEqual(reference_color, expected_color) + + self.assertEqual(reference_surface.get_rect(), test_surface.get_rect()) + + def test_blit__SRCALPHA_to_SRCALPHA_non_zero(self): + """Tests blitting a nonzero alpha surface to another nonzero alpha surface + both straight alpha compositing method. Test is fuzzy (+/- 1/256) to account for + different implementations in SDL1 and SDL2. + """ + + size = (32, 32) + + def check_color_diff(color1, color2): + """Returns True if two colors are within (1, 1, 1, 1) of each other.""" + for val in color1 - color2: + if abs(val) > 1: + return False + return True + + def high_a_onto_low(high, low): + """Tests straight alpha case. Source is low alpha, destination is high alpha""" + high_alpha_surface = pygame.Surface(size, pygame.SRCALPHA, 32) + low_alpha_surface = high_alpha_surface.copy() + high_alpha_color = Color( + (high, high, low, high) + ) # Injecting some RGB variance. + low_alpha_color = Color((high, low, low, low)) + high_alpha_surface.fill(high_alpha_color) + low_alpha_surface.fill(low_alpha_color) + + high_alpha_surface.blit(low_alpha_surface, (0, 0)) + + expected_color = low_alpha_color + Color( + tuple( + ((x * (255 - low_alpha_color.a)) // 255) for x in high_alpha_color + ) + ) + self.assertTrue( + check_color_diff(high_alpha_surface.get_at((0, 0)), expected_color) + ) + + def low_a_onto_high(high, low): + """Tests straight alpha case. Source is high alpha, destination is low alpha""" + high_alpha_surface = pygame.Surface(size, pygame.SRCALPHA, 32) + low_alpha_surface = high_alpha_surface.copy() + high_alpha_color = Color( + (high, high, low, high) + ) # Injecting some RGB variance. + low_alpha_color = Color((high, low, low, low)) + high_alpha_surface.fill(high_alpha_color) + low_alpha_surface.fill(low_alpha_color) + + low_alpha_surface.blit(high_alpha_surface, (0, 0)) + + expected_color = high_alpha_color + Color( + tuple( + ((x * (255 - high_alpha_color.a)) // 255) for x in low_alpha_color + ) + ) + self.assertTrue( + check_color_diff(low_alpha_surface.get_at((0, 0)), expected_color) + ) + + for low_a in range(0, 128): + for high_a in range(128, 256): + high_a_onto_low(high_a, low_a) + low_a_onto_high(high_a, low_a) + + def test_blit__SRCALPHA32_to_8(self): + # Bug: fatal + # SDL_DisplayConvert segfaults when video is uninitialized. + target = pygame.Surface((11, 8), 0, 8) + test_color = target.get_palette_at(2) + source = pygame.Surface((1, 1), pygame.SRCALPHA, 32) + source.set_at((0, 0), test_color) + target.blit(source, (0, 0)) + + +class GeneralSurfaceTests(unittest.TestCase): + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", + 'requires a non-"dummy" SDL_VIDEODRIVER', + ) + def test_image_convert_bug_131(self): + # bug #131: Unable to Surface.convert(32) some 1-bit images. + # https://github.com/pygame/pygame/issues/131 + + pygame.display.init() + try: + pygame.display.set_mode((640, 480)) + + im = pygame.image.load(example_path(os.path.join("data", "city.png"))) + im2 = pygame.image.load(example_path(os.path.join("data", "brick.png"))) + + self.assertEqual(im.get_palette(), ((0, 0, 0, 255), (255, 255, 255, 255))) + self.assertEqual(im2.get_palette(), ((0, 0, 0, 255), (0, 0, 0, 255))) + + self.assertEqual(repr(im.convert(32)), "") + self.assertEqual(repr(im2.convert(32)), "") + + # Ensure a palette format to palette format works. + im3 = im.convert(8) + self.assertEqual(repr(im3), "") + self.assertEqual(im3.get_palette(), im.get_palette()) + + finally: + pygame.display.quit() + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", + 'requires a non-"dummy" SDL_VIDEODRIVER', + ) + def test_convert_init(self): + """Ensure initialization exceptions are raised + for surf.convert().""" + pygame.display.quit() + surf = pygame.Surface((1, 1)) + + self.assertRaisesRegex(pygame.error, "display initialized", surf.convert) + + pygame.display.init() + try: + try: + surf.convert(32) + surf.convert(pygame.Surface((1, 1))) + except pygame.error: + self.fail("convert() should not raise an exception here.") + + self.assertRaisesRegex(pygame.error, "No video mode", surf.convert) + + pygame.display.set_mode((640, 480)) + try: + surf.convert() + except pygame.error: + self.fail("convert() should not raise an exception here.") + finally: + pygame.display.quit() + + @unittest.skipIf( + os.environ.get("SDL_VIDEODRIVER") == "dummy", + 'requires a non-"dummy" SDL_VIDEODRIVER', + ) + def test_convert_alpha_init(self): + """Ensure initialization exceptions are raised + for surf.convert_alpha().""" + pygame.display.quit() + surf = pygame.Surface((1, 1)) + + self.assertRaisesRegex(pygame.error, "display initialized", surf.convert_alpha) + + pygame.display.init() + try: + self.assertRaisesRegex(pygame.error, "No video mode", surf.convert_alpha) + + pygame.display.set_mode((640, 480)) + try: + surf.convert_alpha() + except pygame.error: + self.fail("convert_alpha() should not raise an exception here.") + finally: + pygame.display.quit() + + def test_convert_alpha_SRCALPHA(self): + """Ensure that the surface returned by surf.convert_alpha() + has alpha blending enabled""" + pygame.display.init() + try: + pygame.display.set_mode((640, 480)) + + s1 = pygame.Surface((100, 100), 0, 32) + # s2=pygame.Surface((100,100), pygame.SRCALPHA, 32) + s1_alpha = s1.convert_alpha() + self.assertEqual(s1_alpha.get_flags() & SRCALPHA, SRCALPHA) + self.assertEqual(s1_alpha.get_alpha(), 255) + finally: + pygame.display.quit() + + def test_src_alpha_issue_1289(self): + """blit should be white.""" + surf1 = pygame.Surface((1, 1), pygame.SRCALPHA, 32) + surf1.fill((255, 255, 255, 100)) + + surf2 = pygame.Surface((1, 1), pygame.SRCALPHA, 32) + self.assertEqual(surf2.get_at((0, 0)), (0, 0, 0, 0)) + surf2.blit(surf1, (0, 0)) + + self.assertEqual(surf1.get_at((0, 0)), (255, 255, 255, 100)) + self.assertEqual(surf2.get_at((0, 0)), (255, 255, 255, 100)) + + def test_src_alpha_compatible(self): + """ "What pygame 1.9.x did". Is the alpha blitter as before?""" + + # The table below was generated with the SDL1 blit. + # def print_table(): + # nums = [0, 1, 65, 126, 127, 199, 254, 255] + # results = {} + # for dest_r, dest_b, dest_a in zip(nums, reversed(nums), reversed(nums)): + # for src_r, src_b, src_a in zip(nums, reversed(nums), nums): + # src_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 32) + # src_surf.fill((src_r, 255, src_b, src_a)) + # dest_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 32) + # dest_surf.fill((dest_r, 255, dest_b, dest_a)) + # dest_surf.blit(src_surf, (0, 0)) + # key = ((dest_r, dest_b, dest_a), (src_r, src_b, src_a)) + # results[key] = dest_surf.get_at((65, 33)) + # print("(dest_r, dest_b, dest_a), (src_r, src_b, src_a): color") + # pprint(results) + + results_expected = { + ((0, 255, 255), (0, 255, 0)): (0, 255, 255, 255), + ((0, 255, 255), (1, 254, 1)): (0, 255, 255, 255), + ((0, 255, 255), (65, 199, 65)): (16, 255, 241, 255), + ((0, 255, 255), (126, 127, 126)): (62, 255, 192, 255), + ((0, 255, 255), (127, 126, 127)): (63, 255, 191, 255), + ((0, 255, 255), (199, 65, 199)): (155, 255, 107, 255), + ((0, 255, 255), (254, 1, 254)): (253, 255, 2, 255), + ((0, 255, 255), (255, 0, 255)): (255, 255, 0, 255), + ((1, 254, 254), (0, 255, 0)): (1, 255, 254, 254), + ((1, 254, 254), (1, 254, 1)): (1, 255, 254, 255), + ((1, 254, 254), (65, 199, 65)): (17, 255, 240, 255), + ((1, 254, 254), (126, 127, 126)): (63, 255, 191, 255), + ((1, 254, 254), (127, 126, 127)): (64, 255, 190, 255), + ((1, 254, 254), (199, 65, 199)): (155, 255, 107, 255), + ((1, 254, 254), (254, 1, 254)): (253, 255, 2, 255), + ((1, 254, 254), (255, 0, 255)): (255, 255, 0, 255), + ((65, 199, 199), (0, 255, 0)): (65, 255, 199, 199), + ((65, 199, 199), (1, 254, 1)): (64, 255, 200, 200), + ((65, 199, 199), (65, 199, 65)): (65, 255, 199, 214), + ((65, 199, 199), (126, 127, 126)): (95, 255, 164, 227), + ((65, 199, 199), (127, 126, 127)): (96, 255, 163, 227), + ((65, 199, 199), (199, 65, 199)): (169, 255, 95, 243), + ((65, 199, 199), (254, 1, 254)): (253, 255, 2, 255), + ((65, 199, 199), (255, 0, 255)): (255, 255, 0, 255), + ((126, 127, 127), (0, 255, 0)): (126, 255, 127, 127), + ((126, 127, 127), (1, 254, 1)): (125, 255, 128, 128), + ((126, 127, 127), (65, 199, 65)): (110, 255, 146, 160), + ((126, 127, 127), (126, 127, 126)): (126, 255, 127, 191), + ((126, 127, 127), (127, 126, 127)): (126, 255, 126, 191), + ((126, 127, 127), (199, 65, 199)): (183, 255, 79, 227), + ((126, 127, 127), (254, 1, 254)): (253, 255, 1, 255), + ((126, 127, 127), (255, 0, 255)): (255, 255, 0, 255), + ((127, 126, 126), (0, 255, 0)): (127, 255, 126, 126), + ((127, 126, 126), (1, 254, 1)): (126, 255, 127, 127), + ((127, 126, 126), (65, 199, 65)): (111, 255, 145, 159), + ((127, 126, 126), (126, 127, 126)): (127, 255, 126, 190), + ((127, 126, 126), (127, 126, 127)): (127, 255, 126, 191), + ((127, 126, 126), (199, 65, 199)): (183, 255, 78, 227), + ((127, 126, 126), (254, 1, 254)): (254, 255, 1, 255), + ((127, 126, 126), (255, 0, 255)): (255, 255, 0, 255), + ((199, 65, 65), (0, 255, 0)): (199, 255, 65, 65), + ((199, 65, 65), (1, 254, 1)): (198, 255, 66, 66), + ((199, 65, 65), (65, 199, 65)): (165, 255, 99, 114), + ((199, 65, 65), (126, 127, 126)): (163, 255, 96, 159), + ((199, 65, 65), (127, 126, 127)): (163, 255, 95, 160), + ((199, 65, 65), (199, 65, 199)): (199, 255, 65, 214), + ((199, 65, 65), (254, 1, 254)): (254, 255, 1, 255), + ((199, 65, 65), (255, 0, 255)): (255, 255, 0, 255), + ((254, 1, 1), (0, 255, 0)): (254, 255, 1, 1), + ((254, 1, 1), (1, 254, 1)): (253, 255, 2, 2), + ((254, 1, 1), (65, 199, 65)): (206, 255, 52, 66), + ((254, 1, 1), (126, 127, 126)): (191, 255, 63, 127), + ((254, 1, 1), (127, 126, 127)): (191, 255, 63, 128), + ((254, 1, 1), (199, 65, 199)): (212, 255, 51, 200), + ((254, 1, 1), (254, 1, 254)): (254, 255, 1, 255), + ((254, 1, 1), (255, 0, 255)): (255, 255, 0, 255), + ((255, 0, 0), (0, 255, 0)): (0, 255, 255, 0), + ((255, 0, 0), (1, 254, 1)): (1, 255, 254, 1), + ((255, 0, 0), (65, 199, 65)): (65, 255, 199, 65), + ((255, 0, 0), (126, 127, 126)): (126, 255, 127, 126), + ((255, 0, 0), (127, 126, 127)): (127, 255, 126, 127), + ((255, 0, 0), (199, 65, 199)): (199, 255, 65, 199), + ((255, 0, 0), (254, 1, 254)): (254, 255, 1, 254), + ((255, 0, 0), (255, 0, 255)): (255, 255, 0, 255), + } + + # chosen because they contain edge cases. + nums = [0, 1, 65, 126, 127, 199, 254, 255] + results = {} + for dst_r, dst_b, dst_a in zip(nums, reversed(nums), reversed(nums)): + for src_r, src_b, src_a in zip(nums, reversed(nums), nums): + with self.subTest( + src_r=src_r, + src_b=src_b, + src_a=src_a, + dest_r=dst_r, + dest_b=dst_b, + dest_a=dst_a, + ): + src_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 32) + src_surf.fill((src_r, 255, src_b, src_a)) + dest_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 32) + dest_surf.fill((dst_r, 255, dst_b, dst_a)) + + dest_surf.blit(src_surf, (0, 0)) + key = ((dst_r, dst_b, dst_a), (src_r, src_b, src_a)) + results[key] = dest_surf.get_at((65, 33)) + self.assertEqual(results[key], results_expected[key]) + + self.assertEqual(results, results_expected) + + def test_src_alpha_compatible_16bit(self): + """ "What pygame 1.9.x did". Is the alpha blitter as before?""" + + # The table below was generated with the SDL1 blit. + # def print_table(): + # nums = [0, 1, 65, 126, 127, 199, 254, 255] + # results = {} + # for dest_r, dest_b, dest_a in zip(nums, reversed(nums), reversed(nums)): + # for src_r, src_b, src_a in zip(nums, reversed(nums), nums): + # src_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 16) + # src_surf.fill((src_r, 255, src_b, src_a)) + # dest_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 16) + # dest_surf.fill((dest_r, 255, dest_b, dest_a)) + # dest_surf.blit(src_surf, (0, 0)) + # key = ((dest_r, dest_b, dest_a), (src_r, src_b, src_a)) + # results[key] = dest_surf.get_at((65, 33)) + # print("(dest_r, dest_b, dest_a), (src_r, src_b, src_a): color") + # pprint(results) + + results_expected = { + ((0, 255, 255), (0, 255, 0)): (0, 255, 255, 255), + ((0, 255, 255), (1, 254, 1)): (0, 255, 255, 255), + ((0, 255, 255), (65, 199, 65)): (17, 255, 255, 255), + ((0, 255, 255), (126, 127, 126)): (51, 255, 204, 255), + ((0, 255, 255), (127, 126, 127)): (51, 255, 204, 255), + ((0, 255, 255), (199, 65, 199)): (170, 255, 102, 255), + ((0, 255, 255), (254, 1, 254)): (255, 255, 0, 255), + ((0, 255, 255), (255, 0, 255)): (255, 255, 0, 255), + ((1, 254, 254), (0, 255, 0)): (0, 255, 255, 255), + ((1, 254, 254), (1, 254, 1)): (0, 255, 255, 255), + ((1, 254, 254), (65, 199, 65)): (17, 255, 255, 255), + ((1, 254, 254), (126, 127, 126)): (51, 255, 204, 255), + ((1, 254, 254), (127, 126, 127)): (51, 255, 204, 255), + ((1, 254, 254), (199, 65, 199)): (170, 255, 102, 255), + ((1, 254, 254), (254, 1, 254)): (255, 255, 0, 255), + ((1, 254, 254), (255, 0, 255)): (255, 255, 0, 255), + ((65, 199, 199), (0, 255, 0)): (68, 255, 204, 204), + ((65, 199, 199), (1, 254, 1)): (68, 255, 204, 204), + ((65, 199, 199), (65, 199, 65)): (68, 255, 204, 221), + ((65, 199, 199), (126, 127, 126)): (85, 255, 170, 238), + ((65, 199, 199), (127, 126, 127)): (85, 255, 170, 238), + ((65, 199, 199), (199, 65, 199)): (187, 255, 85, 255), + ((65, 199, 199), (254, 1, 254)): (255, 255, 0, 255), + ((65, 199, 199), (255, 0, 255)): (255, 255, 0, 255), + ((126, 127, 127), (0, 255, 0)): (119, 255, 119, 119), + ((126, 127, 127), (1, 254, 1)): (119, 255, 119, 119), + ((126, 127, 127), (65, 199, 65)): (102, 255, 136, 153), + ((126, 127, 127), (126, 127, 126)): (119, 255, 119, 187), + ((126, 127, 127), (127, 126, 127)): (119, 255, 119, 187), + ((126, 127, 127), (199, 65, 199)): (187, 255, 68, 238), + ((126, 127, 127), (254, 1, 254)): (255, 255, 0, 255), + ((126, 127, 127), (255, 0, 255)): (255, 255, 0, 255), + ((127, 126, 126), (0, 255, 0)): (119, 255, 119, 119), + ((127, 126, 126), (1, 254, 1)): (119, 255, 119, 119), + ((127, 126, 126), (65, 199, 65)): (102, 255, 136, 153), + ((127, 126, 126), (126, 127, 126)): (119, 255, 119, 187), + ((127, 126, 126), (127, 126, 127)): (119, 255, 119, 187), + ((127, 126, 126), (199, 65, 199)): (187, 255, 68, 238), + ((127, 126, 126), (254, 1, 254)): (255, 255, 0, 255), + ((127, 126, 126), (255, 0, 255)): (255, 255, 0, 255), + ((199, 65, 65), (0, 255, 0)): (204, 255, 68, 68), + ((199, 65, 65), (1, 254, 1)): (204, 255, 68, 68), + ((199, 65, 65), (65, 199, 65)): (170, 255, 102, 119), + ((199, 65, 65), (126, 127, 126)): (170, 255, 85, 153), + ((199, 65, 65), (127, 126, 127)): (170, 255, 85, 153), + ((199, 65, 65), (199, 65, 199)): (204, 255, 68, 221), + ((199, 65, 65), (254, 1, 254)): (255, 255, 0, 255), + ((199, 65, 65), (255, 0, 255)): (255, 255, 0, 255), + ((254, 1, 1), (0, 255, 0)): (0, 255, 255, 0), + ((254, 1, 1), (1, 254, 1)): (0, 255, 255, 0), + ((254, 1, 1), (65, 199, 65)): (68, 255, 204, 68), + ((254, 1, 1), (126, 127, 126)): (119, 255, 119, 119), + ((254, 1, 1), (127, 126, 127)): (119, 255, 119, 119), + ((254, 1, 1), (199, 65, 199)): (204, 255, 68, 204), + ((254, 1, 1), (254, 1, 254)): (255, 255, 0, 255), + ((254, 1, 1), (255, 0, 255)): (255, 255, 0, 255), + ((255, 0, 0), (0, 255, 0)): (0, 255, 255, 0), + ((255, 0, 0), (1, 254, 1)): (0, 255, 255, 0), + ((255, 0, 0), (65, 199, 65)): (68, 255, 204, 68), + ((255, 0, 0), (126, 127, 126)): (119, 255, 119, 119), + ((255, 0, 0), (127, 126, 127)): (119, 255, 119, 119), + ((255, 0, 0), (199, 65, 199)): (204, 255, 68, 204), + ((255, 0, 0), (254, 1, 254)): (255, 255, 0, 255), + ((255, 0, 0), (255, 0, 255)): (255, 255, 0, 255), + } + + # chosen because they contain edge cases. + nums = [0, 1, 65, 126, 127, 199, 254, 255] + results = {} + for dst_r, dst_b, dst_a in zip(nums, reversed(nums), reversed(nums)): + for src_r, src_b, src_a in zip(nums, reversed(nums), nums): + with self.subTest( + src_r=src_r, + src_b=src_b, + src_a=src_a, + dest_r=dst_r, + dest_b=dst_b, + dest_a=dst_a, + ): + src_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 16) + src_surf.fill((src_r, 255, src_b, src_a)) + dest_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 16) + dest_surf.fill((dst_r, 255, dst_b, dst_a)) + + dest_surf.blit(src_surf, (0, 0)) + key = ((dst_r, dst_b, dst_a), (src_r, src_b, src_a)) + results[key] = dest_surf.get_at((65, 33)) + self.assertEqual(results[key], results_expected[key]) + + self.assertEqual(results, results_expected) + + def test_sdl1_mimic_blitter_with_set_alpha(self): + """does the SDL 1 style blitter in pygame 2 work with set_alpha(), + this feature only exists in pygame 2/SDL2 SDL1 did not support + combining surface and pixel alpha""" + + results_expected = { + ((0, 255, 255), (0, 255, 0)): (0, 255, 255, 255), + ((0, 255, 255), (1, 254, 1)): (0, 255, 255, 255), + ((0, 255, 255), (65, 199, 65)): (16, 255, 241, 255), + ((0, 255, 255), (126, 127, 126)): (62, 255, 192, 255), + ((0, 255, 255), (127, 126, 127)): (63, 255, 191, 255), + ((0, 255, 255), (199, 65, 199)): (155, 255, 107, 255), + ((0, 255, 255), (254, 1, 254)): (253, 255, 2, 255), + ((0, 255, 255), (255, 0, 255)): (255, 255, 0, 255), + ((1, 254, 254), (0, 255, 0)): (1, 255, 254, 254), + ((1, 254, 254), (1, 254, 1)): (1, 255, 254, 255), + ((1, 254, 254), (65, 199, 65)): (17, 255, 240, 255), + ((1, 254, 254), (126, 127, 126)): (63, 255, 191, 255), + ((1, 254, 254), (127, 126, 127)): (64, 255, 190, 255), + ((1, 254, 254), (199, 65, 199)): (155, 255, 107, 255), + ((1, 254, 254), (254, 1, 254)): (253, 255, 2, 255), + ((1, 254, 254), (255, 0, 255)): (255, 255, 0, 255), + ((65, 199, 199), (0, 255, 0)): (65, 255, 199, 199), + ((65, 199, 199), (1, 254, 1)): (64, 255, 200, 200), + ((65, 199, 199), (65, 199, 65)): (65, 255, 199, 214), + ((65, 199, 199), (126, 127, 126)): (95, 255, 164, 227), + ((65, 199, 199), (127, 126, 127)): (96, 255, 163, 227), + ((65, 199, 199), (199, 65, 199)): (169, 255, 95, 243), + ((65, 199, 199), (254, 1, 254)): (253, 255, 2, 255), + ((65, 199, 199), (255, 0, 255)): (255, 255, 0, 255), + ((126, 127, 127), (0, 255, 0)): (126, 255, 127, 127), + ((126, 127, 127), (1, 254, 1)): (125, 255, 128, 128), + ((126, 127, 127), (65, 199, 65)): (110, 255, 146, 160), + ((126, 127, 127), (126, 127, 126)): (126, 255, 127, 191), + ((126, 127, 127), (127, 126, 127)): (126, 255, 126, 191), + ((126, 127, 127), (199, 65, 199)): (183, 255, 79, 227), + ((126, 127, 127), (254, 1, 254)): (253, 255, 1, 255), + ((126, 127, 127), (255, 0, 255)): (255, 255, 0, 255), + ((127, 126, 126), (0, 255, 0)): (127, 255, 126, 126), + ((127, 126, 126), (1, 254, 1)): (126, 255, 127, 127), + ((127, 126, 126), (65, 199, 65)): (111, 255, 145, 159), + ((127, 126, 126), (126, 127, 126)): (127, 255, 126, 190), + ((127, 126, 126), (127, 126, 127)): (127, 255, 126, 191), + ((127, 126, 126), (199, 65, 199)): (183, 255, 78, 227), + ((127, 126, 126), (254, 1, 254)): (254, 255, 1, 255), + ((127, 126, 126), (255, 0, 255)): (255, 255, 0, 255), + ((199, 65, 65), (0, 255, 0)): (199, 255, 65, 65), + ((199, 65, 65), (1, 254, 1)): (198, 255, 66, 66), + ((199, 65, 65), (65, 199, 65)): (165, 255, 99, 114), + ((199, 65, 65), (126, 127, 126)): (163, 255, 96, 159), + ((199, 65, 65), (127, 126, 127)): (163, 255, 95, 160), + ((199, 65, 65), (199, 65, 199)): (199, 255, 65, 214), + ((199, 65, 65), (254, 1, 254)): (254, 255, 1, 255), + ((199, 65, 65), (255, 0, 255)): (255, 255, 0, 255), + ((254, 1, 1), (0, 255, 0)): (254, 255, 1, 1), + ((254, 1, 1), (1, 254, 1)): (253, 255, 2, 2), + ((254, 1, 1), (65, 199, 65)): (206, 255, 52, 66), + ((254, 1, 1), (126, 127, 126)): (191, 255, 63, 127), + ((254, 1, 1), (127, 126, 127)): (191, 255, 63, 128), + ((254, 1, 1), (199, 65, 199)): (212, 255, 51, 200), + ((254, 1, 1), (254, 1, 254)): (254, 255, 1, 255), + ((254, 1, 1), (255, 0, 255)): (255, 255, 0, 255), + ((255, 0, 0), (0, 255, 0)): (0, 255, 255, 0), + ((255, 0, 0), (1, 254, 1)): (1, 255, 254, 1), + ((255, 0, 0), (65, 199, 65)): (65, 255, 199, 65), + ((255, 0, 0), (126, 127, 126)): (126, 255, 127, 126), + ((255, 0, 0), (127, 126, 127)): (127, 255, 126, 127), + ((255, 0, 0), (199, 65, 199)): (199, 255, 65, 199), + ((255, 0, 0), (254, 1, 254)): (254, 255, 1, 254), + ((255, 0, 0), (255, 0, 255)): (255, 255, 0, 255), + } + + # chosen because they contain edge cases. + nums = [0, 1, 65, 126, 127, 199, 254, 255] + results = {} + for dst_r, dst_b, dst_a in zip(nums, reversed(nums), reversed(nums)): + for src_r, src_b, src_a in zip(nums, reversed(nums), nums): + with self.subTest( + src_r=src_r, + src_b=src_b, + src_a=src_a, + dest_r=dst_r, + dest_b=dst_b, + dest_a=dst_a, + ): + src_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 32) + src_surf.fill((src_r, 255, src_b, 255)) + src_surf.set_alpha(src_a) + dest_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 32) + dest_surf.fill((dst_r, 255, dst_b, dst_a)) + + dest_surf.blit(src_surf, (0, 0)) + key = ((dst_r, dst_b, dst_a), (src_r, src_b, src_a)) + results[key] = dest_surf.get_at((65, 33)) + self.assertEqual(results[key], results_expected[key]) + + self.assertEqual(results, results_expected) + + @unittest.skipIf( + "arm" in platform.machine() or "aarch64" in platform.machine(), + "sdl2 blitter produces different results on arm", + ) + def test_src_alpha_sdl2_blitter(self): + """Checking that the BLEND_ALPHA_SDL2 flag works - this feature + only exists when using SDL2""" + + results_expected = { + ((0, 255, 255), (0, 255, 0)): (0, 255, 255, 255), + ((0, 255, 255), (1, 254, 1)): (0, 253, 253, 253), + ((0, 255, 255), (65, 199, 65)): (16, 253, 239, 253), + ((0, 255, 255), (126, 127, 126)): (62, 253, 190, 253), + ((0, 255, 255), (127, 126, 127)): (63, 253, 189, 253), + ((0, 255, 255), (199, 65, 199)): (154, 253, 105, 253), + ((0, 255, 255), (254, 1, 254)): (252, 253, 0, 253), + ((0, 255, 255), (255, 0, 255)): (255, 255, 0, 255), + ((1, 254, 254), (0, 255, 0)): (1, 255, 254, 254), + ((1, 254, 254), (1, 254, 1)): (0, 253, 252, 252), + ((1, 254, 254), (65, 199, 65)): (16, 253, 238, 252), + ((1, 254, 254), (126, 127, 126)): (62, 253, 189, 252), + ((1, 254, 254), (127, 126, 127)): (63, 253, 189, 253), + ((1, 254, 254), (199, 65, 199)): (154, 253, 105, 253), + ((1, 254, 254), (254, 1, 254)): (252, 253, 0, 253), + ((1, 254, 254), (255, 0, 255)): (255, 255, 0, 255), + ((65, 199, 199), (0, 255, 0)): (65, 255, 199, 199), + ((65, 199, 199), (1, 254, 1)): (64, 253, 197, 197), + ((65, 199, 199), (65, 199, 65)): (64, 253, 197, 211), + ((65, 199, 199), (126, 127, 126)): (94, 253, 162, 225), + ((65, 199, 199), (127, 126, 127)): (95, 253, 161, 225), + ((65, 199, 199), (199, 65, 199)): (168, 253, 93, 241), + ((65, 199, 199), (254, 1, 254)): (252, 253, 0, 253), + ((65, 199, 199), (255, 0, 255)): (255, 255, 0, 255), + ((126, 127, 127), (0, 255, 0)): (126, 255, 127, 127), + ((126, 127, 127), (1, 254, 1)): (125, 253, 126, 126), + ((126, 127, 127), (65, 199, 65)): (109, 253, 144, 158), + ((126, 127, 127), (126, 127, 126)): (125, 253, 125, 188), + ((126, 127, 127), (127, 126, 127)): (126, 253, 125, 189), + ((126, 127, 127), (199, 65, 199)): (181, 253, 77, 225), + ((126, 127, 127), (254, 1, 254)): (252, 253, 0, 253), + ((126, 127, 127), (255, 0, 255)): (255, 255, 0, 255), + ((127, 126, 126), (0, 255, 0)): (127, 255, 126, 126), + ((127, 126, 126), (1, 254, 1)): (126, 253, 125, 125), + ((127, 126, 126), (65, 199, 65)): (110, 253, 143, 157), + ((127, 126, 126), (126, 127, 126)): (125, 253, 125, 188), + ((127, 126, 126), (127, 126, 127)): (126, 253, 125, 189), + ((127, 126, 126), (199, 65, 199)): (181, 253, 77, 225), + ((127, 126, 126), (254, 1, 254)): (252, 253, 0, 253), + ((127, 126, 126), (255, 0, 255)): (255, 255, 0, 255), + ((199, 65, 65), (0, 255, 0)): (199, 255, 65, 65), + ((199, 65, 65), (1, 254, 1)): (197, 253, 64, 64), + ((199, 65, 65), (65, 199, 65)): (163, 253, 98, 112), + ((199, 65, 65), (126, 127, 126)): (162, 253, 94, 157), + ((199, 65, 65), (127, 126, 127)): (162, 253, 94, 158), + ((199, 65, 65), (199, 65, 199)): (197, 253, 64, 212), + ((199, 65, 65), (254, 1, 254)): (252, 253, 0, 253), + ((199, 65, 65), (255, 0, 255)): (255, 255, 0, 255), + ((254, 1, 1), (0, 255, 0)): (254, 255, 1, 1), + ((254, 1, 1), (1, 254, 1)): (252, 253, 0, 0), + ((254, 1, 1), (65, 199, 65)): (204, 253, 50, 64), + ((254, 1, 1), (126, 127, 126)): (189, 253, 62, 125), + ((254, 1, 1), (127, 126, 127)): (190, 253, 62, 126), + ((254, 1, 1), (199, 65, 199)): (209, 253, 50, 198), + ((254, 1, 1), (254, 1, 254)): (252, 253, 0, 253), + ((254, 1, 1), (255, 0, 255)): (255, 255, 0, 255), + ((255, 0, 0), (0, 255, 0)): (255, 255, 0, 0), + ((255, 0, 0), (1, 254, 1)): (253, 253, 0, 0), + ((255, 0, 0), (65, 199, 65)): (205, 253, 50, 64), + ((255, 0, 0), (126, 127, 126)): (190, 253, 62, 125), + ((255, 0, 0), (127, 126, 127)): (190, 253, 62, 126), + ((255, 0, 0), (199, 65, 199)): (209, 253, 50, 198), + ((255, 0, 0), (254, 1, 254)): (252, 253, 0, 253), + ((255, 0, 0), (255, 0, 255)): (255, 255, 0, 255), + } + + # chosen because they contain edge cases. + nums = [0, 1, 65, 126, 127, 199, 254, 255] + results = {} + for dst_r, dst_b, dst_a in zip(nums, reversed(nums), reversed(nums)): + for src_r, src_b, src_a in zip(nums, reversed(nums), nums): + with self.subTest( + src_r=src_r, + src_b=src_b, + src_a=src_a, + dest_r=dst_r, + dest_b=dst_b, + dest_a=dst_a, + ): + src_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 32) + src_surf.fill((src_r, 255, src_b, src_a)) + dest_surf = pygame.Surface((66, 66), pygame.SRCALPHA, 32) + dest_surf.fill((dst_r, 255, dst_b, dst_a)) + + dest_surf.blit( + src_surf, (0, 0), special_flags=pygame.BLEND_ALPHA_SDL2 + ) + key = ((dst_r, dst_b, dst_a), (src_r, src_b, src_a)) + results[key] = tuple(dest_surf.get_at((65, 33))) + for i in range(4): + self.assertAlmostEqual( + results[key][i], results_expected[key][i], delta=4 + ) + + # print("(dest_r, dest_b, dest_a), (src_r, src_b, src_a): color") + # pprint(results) + + def test_opaque_destination_blit_with_set_alpha(self): + # no set_alpha() + src_surf = pygame.Surface((32, 32), pygame.SRCALPHA, 32) + src_surf.fill((255, 255, 255, 200)) + dest_surf = pygame.Surface((32, 32)) + dest_surf.fill((100, 100, 100)) + + dest_surf.blit(src_surf, (0, 0)) + + no_surf_alpha_col = dest_surf.get_at((0, 0)) + + dest_surf.fill((100, 100, 100)) + dest_surf.set_alpha(200) + dest_surf.blit(src_surf, (0, 0)) + + surf_alpha_col = dest_surf.get_at((0, 0)) + + self.assertEqual(no_surf_alpha_col, surf_alpha_col) + + def todo_test_convert(self): + self.fail() + + # Below should not use a display Surface, but create one and check it is converted + # to the depth of the display surface. + + # def test_convert(self): + # """Ensure to creates a new copy of the Surface with the pixel format changed""" + # width = 23 + # height = 17 + # size = (width, height) + # flags = 0 + # depth = 32 + # pygame.display.init() + + # try: + # convert_surface = pygame.display.set_mode(size) + # surface = pygame.surface.Surface.convert(convert_surface) + # self.assertIsNot(surface, convert_surface) + # self.assertNotEqual(surface.get_size(), size) + + # depth_surface = pygame.display.set_mode(size, flags, depth) + # surface2 = pygame.surface.Surface.convert(depth_surface) + # self.assertIsNot(surface2, depth_surface) + # self.assertEqual(surface2.get_size(), size) + # finally: + # pygame.display.quit() + + def test_convert__pixel_format_as_surface_subclass(self): + """Ensure convert accepts a Surface subclass argument.""" + expected_size = (23, 17) + convert_surface = SurfaceSubclass(expected_size, 0, 32) + depth_surface = SurfaceSubclass((31, 61), 0, 32) + + pygame.display.init() + try: + surface = convert_surface.convert(depth_surface) + + self.assertIsNot(surface, depth_surface) + self.assertIsNot(surface, convert_surface) + self.assertIsInstance(surface, pygame.Surface) + self.assertIsInstance(surface, SurfaceSubclass) + self.assertEqual(surface.get_size(), expected_size) + finally: + pygame.display.quit() + + def test_convert_alpha(self): + """Ensure the surface returned by surf.convert_alpha + has alpha values added""" + pygame.display.init() + try: + pygame.display.set_mode((640, 480)) + + s1 = pygame.Surface((100, 100), 0, 32) + s1_alpha = pygame.Surface.convert_alpha(s1) + + s2 = pygame.Surface((100, 100), 0, 32) + s2_alpha = s2.convert_alpha() + + s3 = pygame.Surface((100, 100), 0, 8) + s3_alpha = s3.convert_alpha() + + s4 = pygame.Surface((100, 100), 0, 12) + s4_alpha = s4.convert_alpha() + + s5 = pygame.Surface((100, 100), 0, 15) + s5_alpha = s5.convert_alpha() + + s6 = pygame.Surface((100, 100), 0, 16) + s6_alpha = s6.convert_alpha() + + s7 = pygame.Surface((100, 100), 0, 24) + s7_alpha = s7.convert_alpha() + + self.assertEqual(s1_alpha.get_alpha(), 255) + self.assertEqual(s2_alpha.get_alpha(), 255) + self.assertEqual(s3_alpha.get_alpha(), 255) + self.assertEqual(s4_alpha.get_alpha(), 255) + self.assertEqual(s5_alpha.get_alpha(), 255) + self.assertEqual(s6_alpha.get_alpha(), 255) + self.assertEqual(s7_alpha.get_alpha(), 255) + + self.assertEqual(s1_alpha.get_bitsize(), 32) + self.assertEqual(s2_alpha.get_bitsize(), 32) + self.assertEqual(s3_alpha.get_bitsize(), 32) + self.assertEqual(s4_alpha.get_bitsize(), 32) + self.assertEqual(s5_alpha.get_bitsize(), 32) + self.assertEqual(s6_alpha.get_bitsize(), 32) + self.assertEqual(s6_alpha.get_bitsize(), 32) + + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode() + pygame.display.quit() + surface.convert_alpha() + + finally: + pygame.display.quit() + + def test_convert_alpha__pixel_format_as_surface_subclass(self): + """Ensure convert_alpha accepts a Surface subclass argument.""" + expected_size = (23, 17) + convert_surface = SurfaceSubclass(expected_size, SRCALPHA, 32) + depth_surface = SurfaceSubclass((31, 57), SRCALPHA, 32) + + pygame.display.init() + try: + pygame.display.set_mode((60, 60)) + + # This is accepted as an argument, but its values are ignored. + # See issue #599. + surface = convert_surface.convert_alpha(depth_surface) + + self.assertIsNot(surface, depth_surface) + self.assertIsNot(surface, convert_surface) + self.assertIsInstance(surface, pygame.Surface) + self.assertIsInstance(surface, SurfaceSubclass) + self.assertEqual(surface.get_size(), expected_size) + finally: + pygame.display.quit() + + def test_get_abs_offset(self): + pygame.display.init() + try: + parent = pygame.Surface((64, 64), SRCALPHA, 32) + + # Stack bunch of subsurfaces + sub_level_1 = parent.subsurface((2, 2), (34, 37)) + sub_level_2 = sub_level_1.subsurface((0, 0), (30, 29)) + sub_level_3 = sub_level_2.subsurface((3, 7), (20, 21)) + sub_level_4 = sub_level_3.subsurface((6, 1), (14, 14)) + sub_level_5 = sub_level_4.subsurface((5, 6), (3, 4)) + + # Parent is always (0, 0) + self.assertEqual(parent.get_abs_offset(), (0, 0)) + # Total offset: (0+2, 0+2) = (2, 2) + self.assertEqual(sub_level_1.get_abs_offset(), (2, 2)) + # Total offset: (0+2+0, 0+2+0) = (2, 2) + self.assertEqual(sub_level_2.get_abs_offset(), (2, 2)) + # Total offset: (0+2+0+3, 0+2+0+7) = (5, 9) + self.assertEqual(sub_level_3.get_abs_offset(), (5, 9)) + # Total offset: (0+2+0+3+6, 0+2+0+7+1) = (11, 10) + self.assertEqual(sub_level_4.get_abs_offset(), (11, 10)) + # Total offset: (0+2+0+3+6+5, 0+2+0+7+1+6) = (16, 16) + self.assertEqual(sub_level_5.get_abs_offset(), (16, 16)) + + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode() + pygame.display.quit() + surface.get_abs_offset() + finally: + pygame.display.quit() + + def test_get_abs_parent(self): + pygame.display.init() + try: + parent = pygame.Surface((32, 32), SRCALPHA, 32) + + # Stack bunch of subsurfaces + sub_level_1 = parent.subsurface((1, 1), (15, 15)) + sub_level_2 = sub_level_1.subsurface((1, 1), (12, 12)) + sub_level_3 = sub_level_2.subsurface((1, 1), (9, 9)) + sub_level_4 = sub_level_3.subsurface((1, 1), (8, 8)) + sub_level_5 = sub_level_4.subsurface((2, 2), (3, 4)) + sub_level_6 = sub_level_5.subsurface((0, 0), (2, 1)) + + # Can't have subsurfaces bigger than parents + self.assertRaises(ValueError, parent.subsurface, (5, 5), (100, 100)) + self.assertRaises(ValueError, sub_level_3.subsurface, (0, 0), (11, 5)) + self.assertRaises(ValueError, sub_level_6.subsurface, (0, 0), (5, 5)) + + # Calling get_abs_parent on parent should return itself + self.assertEqual(parent.get_abs_parent(), parent) + + # On subclass "depth" of 1, get_abs_parent and get_parent should return the same + self.assertEqual(sub_level_1.get_abs_parent(), sub_level_1.get_parent()) + self.assertEqual(sub_level_2.get_abs_parent(), parent) + self.assertEqual(sub_level_3.get_abs_parent(), parent) + self.assertEqual(sub_level_4.get_abs_parent(), parent) + self.assertEqual(sub_level_5.get_abs_parent(), parent) + self.assertEqual( + sub_level_6.get_abs_parent(), sub_level_6.get_parent().get_abs_parent() + ) + + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode() + pygame.display.quit() + surface.get_abs_parent() + finally: + pygame.display.quit() + + def test_get_at(self): + surf = pygame.Surface((2, 2), 0, 24) + c00 = pygame.Color(1, 2, 3) + c01 = pygame.Color(5, 10, 15) + c10 = pygame.Color(100, 50, 0) + c11 = pygame.Color(4, 5, 6) + surf.set_at((0, 0), c00) + surf.set_at((0, 1), c01) + surf.set_at((1, 0), c10) + surf.set_at((1, 1), c11) + c = surf.get_at((0, 0)) + self.assertIsInstance(c, pygame.Color) + self.assertEqual(c, c00) + self.assertEqual(surf.get_at((0, 1)), c01) + self.assertEqual(surf.get_at((1, 0)), c10) + self.assertEqual(surf.get_at((1, 1)), c11) + for p in [(-1, 0), (0, -1), (2, 0), (0, 2)]: + self.assertRaises(IndexError, surf.get_at, p) + + def test_get_at_mapped(self): + color = pygame.Color(10, 20, 30) + for bitsize in [8, 16, 24, 32]: + surf = pygame.Surface((2, 2), 0, bitsize) + surf.fill(color) + pixel = surf.get_at_mapped((0, 0)) + self.assertEqual( + pixel, + surf.map_rgb(color), + "%i != %i, bitsize: %i" % (pixel, surf.map_rgb(color), bitsize), + ) + + def test_get_bitsize(self): + pygame.display.init() + try: + expected_size = (11, 21) + + # Check that get_bitsize returns passed depth + expected_depth = 32 + surface = pygame.Surface(expected_size, pygame.SRCALPHA, expected_depth) + self.assertEqual(surface.get_size(), expected_size) + self.assertEqual(surface.get_bitsize(), expected_depth) + + expected_depth = 16 + surface = pygame.Surface(expected_size, pygame.SRCALPHA, expected_depth) + self.assertEqual(surface.get_size(), expected_size) + self.assertEqual(surface.get_bitsize(), expected_depth) + + expected_depth = 15 + surface = pygame.Surface(expected_size, 0, expected_depth) + self.assertEqual(surface.get_size(), expected_size) + self.assertEqual(surface.get_bitsize(), expected_depth) + # Check for invalid depths + expected_depth = -1 + self.assertRaises( + ValueError, pygame.Surface, expected_size, 0, expected_depth + ) + expected_depth = 11 + self.assertRaises( + ValueError, pygame.Surface, expected_size, 0, expected_depth + ) + expected_depth = 1024 + self.assertRaises( + ValueError, pygame.Surface, expected_size, 0, expected_depth + ) + + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode() + pygame.display.quit() + surface.get_bitsize() + finally: + pygame.display.quit() + + def test_get_clip(self): + s = pygame.Surface((800, 600)) + rectangle = s.get_clip() + self.assertEqual(rectangle, (0, 0, 800, 600)) + + def test_get_colorkey(self): + pygame.display.init() + try: + # if set_colorkey is not used + s = pygame.Surface((800, 600), 0, 32) + self.assertIsNone(s.get_colorkey()) + + # if set_colorkey is used + s.set_colorkey(None) + self.assertIsNone(s.get_colorkey()) + + # setting up remainder of tests... + r, g, b, a = 20, 40, 60, 12 + colorkey = pygame.Color(r, g, b) + s.set_colorkey(colorkey) + + # test for ideal case + self.assertEqual(s.get_colorkey(), (r, g, b, 255)) + + # test for if the color_key is set using pygame.RLEACCEL + s.set_colorkey(colorkey, pygame.RLEACCEL) + self.assertEqual(s.get_colorkey(), (r, g, b, 255)) + + # test for if the color key is not what's expected + s.set_colorkey(pygame.Color(r + 1, g + 1, b + 1)) + self.assertNotEqual(s.get_colorkey(), (r, g, b, 255)) + + s.set_colorkey(pygame.Color(r, g, b, a)) + # regardless of whether alpha is not 255 + # colorkey returned from surface is always 255 + self.assertEqual(s.get_colorkey(), (r, g, b, 255)) + finally: + # test for using method after display.quit() is called... + s = pygame.display.set_mode((200, 200), 0, 32) + pygame.display.quit() + with self.assertRaises(pygame.error): + s.get_colorkey() + + def test_get_height(self): + sizes = ((1, 1), (119, 10), (10, 119), (1, 1000), (1000, 1), (1000, 1000)) + for width, height in sizes: + surf = pygame.Surface((width, height)) + found_height = surf.get_height() + self.assertEqual(height, found_height) + + def test_get_locked(self): + def blit_locked_test(surface): + newSurf = pygame.Surface((10, 10)) + try: + newSurf.blit(surface, (0, 0)) + except pygame.error: + return True + else: + return False + + surf = pygame.Surface((100, 100)) + + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Unlocked + # Surface should lock + surf.lock() + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Locked + # Surface should unlock + surf.unlock() + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Unlocked + + # Check multiple locks + surf = pygame.Surface((100, 100)) + surf.lock() + surf.lock() + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Locked + surf.unlock() + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Locked + surf.unlock() + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Unlocked + + # Check many locks + surf = pygame.Surface((100, 100)) + for i in range(1000): + surf.lock() + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Locked + for i in range(1000): + surf.unlock() + self.assertFalse(surf.get_locked()) # Unlocked + + # Unlocking an unlocked surface + surf = pygame.Surface((100, 100)) + surf.unlock() + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Unlocked + surf.unlock() + self.assertIs(surf.get_locked(), blit_locked_test(surf)) # Unlocked + + def test_get_locks(self): + # __doc__ (as of 2008-08-02) for pygame.surface.Surface.get_locks: + + # Surface.get_locks(): return tuple + # Gets the locks for the Surface + # + # Returns the currently existing locks for the Surface. + + # test on a surface that is not initially locked + surface = pygame.Surface((100, 100)) + self.assertEqual(surface.get_locks(), ()) + + # test on the same surface after it has been locked + surface.lock() + self.assertEqual(surface.get_locks(), (surface,)) + + # test on the same surface after it has been unlocked + surface.unlock() + self.assertEqual(surface.get_locks(), ()) + + # test with PixelArray initialization: locks surface + pxarray = pygame.PixelArray(surface) + self.assertNotEqual(surface.get_locks(), ()) + + # closing the PixelArray releases the surface lock + pxarray.close() + self.assertEqual(surface.get_locks(), ()) + + # AttributeError raised when called on invalid object type (i.e. not a pygame.Surface object) + with self.assertRaises(AttributeError): + "DUMMY".get_locks() + + # test multiple locks and unlocks on the same surface + surface.lock() + surface.lock() + surface.lock() + self.assertEqual(surface.get_locks(), (surface, surface, surface)) + + surface.unlock() + surface.unlock() + self.assertEqual(surface.get_locks(), (surface,)) + surface.unlock() + self.assertEqual(surface.get_locks(), ()) + + def test_get_losses(self): + """Ensure a surface's losses can be retrieved""" + pygame.display.init() + try: + # Masks for different color component configurations + mask8 = (224, 28, 3, 0) + mask15 = (31744, 992, 31, 0) + mask16 = (63488, 2016, 31, 0) + mask24 = (16711680, 65280, 255, 0) + mask32 = (4278190080, 16711680, 65280, 255) + + # Surfaces with standard depths and masks + display_surf = pygame.display.set_mode((100, 100)) + surf = pygame.Surface((100, 100)) + surf_8bit = pygame.Surface((100, 100), depth=8, masks=mask8) + surf_15bit = pygame.Surface((100, 100), depth=15, masks=mask15) + surf_16bit = pygame.Surface((100, 100), depth=16, masks=mask16) + surf_24bit = pygame.Surface((100, 100), depth=24, masks=mask24) + surf_32bit = pygame.Surface((100, 100), depth=32, masks=mask32) + + # Test output is correct type, length, and value range + losses = surf.get_losses() + self.assertIsInstance(losses, tuple) + self.assertEqual(len(losses), 4) + for loss in losses: + self.assertIsInstance(loss, int) + self.assertGreaterEqual(loss, 0) + self.assertLessEqual(loss, 8) + + # Test each surface for correct losses + # Display surface losses gives idea of default surface losses + if display_surf.get_losses() == (0, 0, 0, 8): + self.assertEqual(losses, (0, 0, 0, 8)) + elif display_surf.get_losses() == (8, 8, 8, 8): + self.assertEqual(losses, (8, 8, 8, 8)) + + self.assertEqual(surf_8bit.get_losses(), (5, 5, 6, 8)) + self.assertEqual(surf_15bit.get_losses(), (3, 3, 3, 8)) + self.assertEqual(surf_16bit.get_losses(), (3, 2, 3, 8)) + self.assertEqual(surf_24bit.get_losses(), (0, 0, 0, 8)) + self.assertEqual(surf_32bit.get_losses(), (0, 0, 0, 0)) + + # Method should fail when display is not initialized + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode((100, 100)) + pygame.display.quit() + surface.get_losses() + finally: + pygame.display.quit() + + def test_get_masks__rgba(self): + """ + Ensure that get_mask can return RGBA mask. + """ + masks = [ + (0x0F00, 0x00F0, 0x000F, 0xF000), + (0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000), + ] + depths = [16, 32] + for expected, depth in list(zip(masks, depths)): + surface = pygame.Surface((10, 10), pygame.SRCALPHA, depth) + self.assertEqual(expected, surface.get_masks()) + + def test_get_masks__rgb(self): + """ + Ensure that get_mask can return RGB mask. + """ + masks = [ + (0x60, 0x1C, 0x03, 0x00), + (0xF00, 0x0F0, 0x00F, 0x000), + (0x7C00, 0x03E0, 0x001F, 0x0000), + (0xF800, 0x07E0, 0x001F, 0x0000), + (0xFF0000, 0x00FF00, 0x0000FF, 0x000000), + (0xFF0000, 0x00FF00, 0x0000FF, 0x000000), + ] + depths = [8, 12, 15, 16, 24, 32] + for expected, depth in list(zip(masks, depths)): + surface = pygame.Surface((10, 10), 0, depth) + if depth == 8: + expected = (0x00, 0x00, 0x00, 0x00) + self.assertEqual(expected, surface.get_masks()) + + def test_get_masks__no_surface(self): + """ + Ensure that after display.quit, calling get_masks raises pygame.error. + """ + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode((10, 10)) + pygame.display.quit() + surface.get_masks() + + def test_get_offset(self): + """get_offset returns the (0,0) if surface is not a child + returns the position of child subsurface inside of parent + """ + pygame.display.init() + try: + surf = pygame.Surface((100, 100)) + self.assertEqual(surf.get_offset(), (0, 0)) + + # subsurface offset test + subsurf = surf.subsurface(1, 1, 10, 10) + self.assertEqual(subsurf.get_offset(), (1, 1)) + + with self.assertRaises(pygame.error): + surface = pygame.display.set_mode() + pygame.display.quit() + surface.get_offset() + finally: + pygame.display.quit() + + def test_get_palette(self): + palette = [Color(i, i, i) for i in range(256)] + surf = pygame.Surface((2, 2), 0, 8) + surf.set_palette(palette) + palette2 = surf.get_palette() + + self.assertEqual(len(palette2), len(palette)) + for c2, c in zip(palette2, palette): + self.assertEqual(c2, c) + for c in palette2: + self.assertIsInstance(c, pygame.Color) + + def test_get_palette_at(self): + # See also test_get_palette + surf = pygame.Surface((2, 2), 0, 8) + color = pygame.Color(1, 2, 3, 255) + surf.set_palette_at(0, color) + color2 = surf.get_palette_at(0) + self.assertIsInstance(color2, pygame.Color) + self.assertEqual(color2, color) + self.assertRaises(IndexError, surf.get_palette_at, -1) + self.assertRaises(IndexError, surf.get_palette_at, 256) + + def test_get_pitch(self): + # Test get_pitch() on several surfaces of varying size/depth + sizes = ((2, 2), (7, 33), (33, 7), (2, 734), (734, 2), (734, 734)) + depths = [8, 24, 32] + for width, height in sizes: + for depth in depths: + # Test get_pitch() on parent surface + surf = pygame.Surface((width, height), depth=depth) + buff = surf.get_buffer() + pitch = buff.length / surf.get_height() + test_pitch = surf.get_pitch() + self.assertEqual(pitch, test_pitch) + # Test get_pitch() on subsurface with same rect as parent + rect1 = surf.get_rect() + subsurf1 = surf.subsurface(rect1) + sub_buff1 = subsurf1.get_buffer() + sub_pitch1 = sub_buff1.length / subsurf1.get_height() + test_sub_pitch1 = subsurf1.get_pitch() + self.assertEqual(sub_pitch1, test_sub_pitch1) + # Test get_pitch on subsurface with modified rect + rect2 = rect1.inflate(-width / 2, -height / 2) + subsurf2 = surf.subsurface(rect2) + sub_buff2 = subsurf2.get_buffer() + sub_pitch2 = sub_buff2.length / float(subsurf2.get_height()) + test_sub_pitch2 = subsurf2.get_pitch() + self.assertEqual(sub_pitch2, test_sub_pitch2) + + def test_get_shifts(self): + """ + Tests whether Surface.get_shifts returns proper + RGBA shifts under various conditions. + """ + # __doc__ (as of 2008-08-02) for pygame.surface.Surface.get_shifts: + # Surface.get_shifts(): return (R, G, B, A) + # the bit shifts needed to convert between color and mapped integer. + # Returns the pixel shifts need to convert between each color and a + # mapped integer. + # This value is not needed for normal Pygame usage. + + # Test for SDL2 on surfaces with various depths and alpha on/off + depths = [8, 24, 32] + alpha = 128 + off = None + for bit_depth in depths: + surface = pygame.Surface((32, 32), depth=bit_depth) + surface.set_alpha(alpha) + r1, g1, b1, a1 = surface.get_shifts() + surface.set_alpha(off) + r2, g2, b2, a2 = surface.get_shifts() + self.assertEqual((r1, g1, b1, a1), (r2, g2, b2, a2)) + + def test_get_size(self): + sizes = ((1, 1), (119, 10), (1000, 1000), (1, 5000), (1221, 1), (99, 999)) + for width, height in sizes: + surf = pygame.Surface((width, height)) + found_size = surf.get_size() + self.assertEqual((width, height), found_size) + + def test_lock(self): + # __doc__ (as of 2008-08-02) for pygame.surface.Surface.lock: + + # Surface.lock(): return None + # lock the Surface memory for pixel access + # + # Lock the pixel data of a Surface for access. On accelerated + # Surfaces, the pixel data may be stored in volatile video memory or + # nonlinear compressed forms. When a Surface is locked the pixel + # memory becomes available to access by regular software. Code that + # reads or writes pixel values will need the Surface to be locked. + # + # Surfaces should not remain locked for more than necessary. A locked + # Surface can often not be displayed or managed by Pygame. + # + # Not all Surfaces require locking. The Surface.mustlock() method can + # determine if it is actually required. There is no performance + # penalty for locking and unlocking a Surface that does not need it. + # + # All pygame functions will automatically lock and unlock the Surface + # data as needed. If a section of code is going to make calls that + # will repeatedly lock and unlock the Surface many times, it can be + # helpful to wrap the block inside a lock and unlock pair. + # + # It is safe to nest locking and unlocking calls. The surface will + # only be unlocked after the final lock is released. + # + + # Basic + surf = pygame.Surface((100, 100)) + surf.lock() + self.assertTrue(surf.get_locked()) + + # Nested + surf = pygame.Surface((100, 100)) + surf.lock() + surf.lock() + surf.unlock() + self.assertTrue(surf.get_locked()) + surf.unlock() + surf.lock() + surf.lock() + self.assertTrue(surf.get_locked()) + surf.unlock() + self.assertTrue(surf.get_locked()) + surf.unlock() + self.assertFalse(surf.get_locked()) + + # Already Locked + surf = pygame.Surface((100, 100)) + surf.lock() + surf.lock() + self.assertTrue(surf.get_locked()) + surf.unlock() + self.assertTrue(surf.get_locked()) + surf.unlock() + self.assertFalse(surf.get_locked()) + + def test_map_rgb(self): + color = Color(0, 128, 255, 64) + surf = pygame.Surface((5, 5), SRCALPHA, 32) + c = surf.map_rgb(color) + self.assertEqual(surf.unmap_rgb(c), color) + + self.assertEqual(surf.get_at((0, 0)), (0, 0, 0, 0)) + surf.fill(c) + self.assertEqual(surf.get_at((0, 0)), color) + + surf.fill((0, 0, 0, 0)) + self.assertEqual(surf.get_at((0, 0)), (0, 0, 0, 0)) + surf.set_at((0, 0), c) + self.assertEqual(surf.get_at((0, 0)), color) + + def test_mustlock(self): + # Test that subsurfaces mustlock + surf = pygame.Surface((1024, 1024)) + subsurf = surf.subsurface((0, 0, 1024, 1024)) + self.assertTrue(subsurf.mustlock()) + self.assertFalse(surf.mustlock()) + # Tests nested subsurfaces + rects = ((0, 0, 512, 512), (0, 0, 256, 256), (0, 0, 128, 128)) + surf_stack = [] + surf_stack.append(surf) + surf_stack.append(subsurf) + for rect in rects: + surf_stack.append(surf_stack[-1].subsurface(rect)) + self.assertTrue(surf_stack[-1].mustlock()) + self.assertTrue(surf_stack[-2].mustlock()) + + def test_set_alpha_none(self): + """surf.set_alpha(None) disables blending""" + s = pygame.Surface((1, 1), SRCALPHA, 32) + s.fill((0, 255, 0, 128)) + s.set_alpha(None) + self.assertEqual(None, s.get_alpha()) + + s2 = pygame.Surface((1, 1), SRCALPHA, 32) + s2.fill((255, 0, 0, 255)) + s2.blit(s, (0, 0)) + self.assertEqual(s2.get_at((0, 0))[0], 0, "the red component should be 0") + + def test_set_alpha_value(self): + """surf.set_alpha(x), where x != None, enables blending""" + s = pygame.Surface((1, 1), SRCALPHA, 32) + s.fill((0, 255, 0, 128)) + s.set_alpha(255) + + s2 = pygame.Surface((1, 1), SRCALPHA, 32) + s2.fill((255, 0, 0, 255)) + s2.blit(s, (0, 0)) + self.assertGreater( + s2.get_at((0, 0))[0], 0, "the red component should be above 0" + ) + + def test_palette_colorkey(self): + """test bug discovered by robertpfeiffer + https://github.com/pygame/pygame/issues/721 + """ + surf = pygame.image.load(example_path(os.path.join("data", "alien2.png"))) + key = surf.get_colorkey() + self.assertEqual(surf.get_palette()[surf.map_rgb(key)], key) + + def test_palette_colorkey_set_px(self): + surf = pygame.image.load(example_path(os.path.join("data", "alien2.png"))) + key = surf.get_colorkey() + surf.set_at((0, 0), key) + self.assertEqual(surf.get_at((0, 0)), key) + + def test_palette_colorkey_fill(self): + surf = pygame.image.load(example_path(os.path.join("data", "alien2.png"))) + key = surf.get_colorkey() + surf.fill(key) + self.assertEqual(surf.get_at((0, 0)), key) + + def test_set_palette(self): + palette = [pygame.Color(i, i, i) for i in range(256)] + palette[10] = tuple(palette[10]) # 4 element tuple + palette[11] = tuple(palette[11])[0:3] # 3 element tuple + + surf = pygame.Surface((2, 2), 0, 8) + surf.set_palette(palette) + for i in range(256): + self.assertEqual(surf.map_rgb(palette[i]), i, "palette color %i" % (i,)) + c = palette[i] + surf.fill(c) + self.assertEqual(surf.get_at((0, 0)), c, "palette color %i" % (i,)) + for i in range(10): + palette[i] = pygame.Color(255 - i, 0, 0) + surf.set_palette(palette[0:10]) + for i in range(256): + self.assertEqual(surf.map_rgb(palette[i]), i, "palette color %i" % (i,)) + c = palette[i] + surf.fill(c) + self.assertEqual(surf.get_at((0, 0)), c, "palette color %i" % (i,)) + self.assertRaises(ValueError, surf.set_palette, [Color(1, 2, 3, 254)]) + self.assertRaises(ValueError, surf.set_palette, (1, 2, 3, 254)) + + def test_set_palette__fail(self): + palette = 256 * [(10, 20, 30)] + surf = pygame.Surface((2, 2), 0, 32) + self.assertRaises(pygame.error, surf.set_palette, palette) + + def test_set_palette__set_at(self): + surf = pygame.Surface((2, 2), depth=8) + palette = 256 * [(10, 20, 30)] + palette[1] = (50, 40, 30) + surf.set_palette(palette) + + # calling set_at on a palettized surface should set the pixel to + # the closest color in the palette. + surf.set_at((0, 0), (60, 50, 40)) + self.assertEqual(surf.get_at((0, 0)), (50, 40, 30, 255)) + self.assertEqual(surf.get_at((1, 0)), (10, 20, 30, 255)) + + def test_set_palette_at(self): + surf = pygame.Surface((2, 2), 0, 8) + original = surf.get_palette_at(10) + replacement = Color(1, 1, 1, 255) + if replacement == original: + replacement = Color(2, 2, 2, 255) + surf.set_palette_at(10, replacement) + self.assertEqual(surf.get_palette_at(10), replacement) + next = tuple(original) + surf.set_palette_at(10, next) + self.assertEqual(surf.get_palette_at(10), next) + next = tuple(original)[0:3] + surf.set_palette_at(10, next) + self.assertEqual(surf.get_palette_at(10), next) + self.assertRaises(IndexError, surf.set_palette_at, 256, replacement) + self.assertRaises(IndexError, surf.set_palette_at, -1, replacement) + + def test_subsurface(self): + # __doc__ (as of 2008-08-02) for pygame.surface.Surface.subsurface: + + # Surface.subsurface(Rect): return Surface + # create a new surface that references its parent + # + # Returns a new Surface that shares its pixels with its new parent. + # The new Surface is considered a child of the original. Modifications + # to either Surface pixels will effect each other. Surface information + # like clipping area and color keys are unique to each Surface. + # + # The new Surface will inherit the palette, color key, and alpha + # settings from its parent. + # + # It is possible to have any number of subsurfaces and subsubsurfaces + # on the parent. It is also possible to subsurface the display Surface + # if the display mode is not hardware accelerated. + # + # See the Surface.get_offset(), Surface.get_parent() to learn more + # about the state of a subsurface. + # + + surf = pygame.Surface((16, 16)) + s = surf.subsurface(0, 0, 1, 1) + s = surf.subsurface((0, 0, 1, 1)) + + # s = surf.subsurface((0,0,1,1), 1) + # This form is not acceptable. + # s = surf.subsurface(0,0,10,10, 1) + + self.assertRaises(ValueError, surf.subsurface, (0, 0, 1, 1, 666)) + + self.assertEqual(s.get_shifts(), surf.get_shifts()) + self.assertEqual(s.get_masks(), surf.get_masks()) + self.assertEqual(s.get_losses(), surf.get_losses()) + + # Issue https://github.com/pygame/pygame/issues/2 + surf = pygame.Surface.__new__(pygame.Surface) + self.assertRaises(pygame.error, surf.subsurface, (0, 0, 0, 0)) + + def test_unlock(self): + # Basic + surf = pygame.Surface((100, 100)) + surf.lock() + surf.unlock() + self.assertFalse(surf.get_locked()) + + # Nested + surf = pygame.Surface((100, 100)) + surf.lock() + surf.lock() + surf.unlock() + self.assertTrue(surf.get_locked()) + surf.unlock() + self.assertFalse(surf.get_locked()) + + # Already Unlocked + surf = pygame.Surface((100, 100)) + surf.unlock() + self.assertFalse(surf.get_locked()) + surf.unlock() + self.assertFalse(surf.get_locked()) + + # Surface can be relocked + surf = pygame.Surface((100, 100)) + surf.lock() + surf.unlock() + self.assertFalse(surf.get_locked()) + surf.lock() + surf.unlock() + self.assertFalse(surf.get_locked()) + + def test_unmap_rgb(self): + # Special case, 8 bit-per-pixel surface (has a palette). + surf = pygame.Surface((2, 2), 0, 8) + c = (1, 1, 1) # Unlikely to be in a default palette. + i = 67 + surf.set_palette_at(i, c) + unmapped_c = surf.unmap_rgb(i) + self.assertEqual(unmapped_c, c) + # Confirm it is a Color instance + self.assertIsInstance(unmapped_c, pygame.Color) + + # Remaining, non-pallete, cases. + c = (128, 64, 12, 255) + formats = [(0, 16), (0, 24), (0, 32), (SRCALPHA, 16), (SRCALPHA, 32)] + for flags, bitsize in formats: + surf = pygame.Surface((2, 2), flags, bitsize) + unmapped_c = surf.unmap_rgb(surf.map_rgb(c)) + surf.fill(c) + comparison_c = surf.get_at((0, 0)) + self.assertEqual( + unmapped_c, + comparison_c, + "%s != %s, flags: %i, bitsize: %i" + % (unmapped_c, comparison_c, flags, bitsize), + ) + # Confirm it is a Color instance + self.assertIsInstance(unmapped_c, pygame.Color) + + def test_scroll(self): + scrolls = [ + (8, 2, 3), + (16, 2, 3), + (24, 2, 3), + (32, 2, 3), + (32, -1, -3), + (32, 0, 0), + (32, 11, 0), + (32, 0, 11), + (32, -11, 0), + (32, 0, -11), + (32, -11, 2), + (32, 2, -11), + ] + for bitsize, dx, dy in scrolls: + surf = pygame.Surface((10, 10), 0, bitsize) + surf.fill((255, 0, 0)) + surf.fill((0, 255, 0), (2, 2, 2, 2)) + comp = surf.copy() + comp.blit(surf, (dx, dy)) + surf.scroll(dx, dy) + w, h = surf.get_size() + for x in range(w): + for y in range(h): + with self.subTest(x=x, y=y): + self.assertEqual( + surf.get_at((x, y)), + comp.get_at((x, y)), + "%s != %s, bpp:, %i, x: %i, y: %i" + % ( + surf.get_at((x, y)), + comp.get_at((x, y)), + bitsize, + dx, + dy, + ), + ) + # Confirm clip rect containment + surf = pygame.Surface((20, 13), 0, 32) + surf.fill((255, 0, 0)) + surf.fill((0, 255, 0), (7, 1, 6, 6)) + comp = surf.copy() + clip = Rect(3, 1, 8, 14) + surf.set_clip(clip) + comp.set_clip(clip) + comp.blit(surf, (clip.x + 2, clip.y + 3), surf.get_clip()) + surf.scroll(2, 3) + w, h = surf.get_size() + for x in range(w): + for y in range(h): + self.assertEqual(surf.get_at((x, y)), comp.get_at((x, y))) + # Confirm keyword arguments and per-pixel alpha + spot_color = (0, 255, 0, 128) + surf = pygame.Surface((4, 4), pygame.SRCALPHA, 32) + surf.fill((255, 0, 0, 255)) + surf.set_at((1, 1), spot_color) + surf.scroll(dx=1) + self.assertEqual(surf.get_at((2, 1)), spot_color) + surf.scroll(dy=1) + self.assertEqual(surf.get_at((2, 2)), spot_color) + surf.scroll(dy=1, dx=1) + self.assertEqual(surf.get_at((3, 3)), spot_color) + surf.scroll(dx=-3, dy=-3) + self.assertEqual(surf.get_at((0, 0)), spot_color) + + +class SurfaceSubtypeTest(unittest.TestCase): + """Issue #280: Methods that return a new Surface preserve subclasses""" + + def setUp(self): + pygame.display.init() + + def tearDown(self): + pygame.display.quit() + + def test_copy(self): + """Ensure method copy() preserves the surface's class + + When Surface is subclassed, the inherited copy() method will return + instances of the subclass. Non Surface fields are uncopied, however. + This includes instance attributes. + """ + expected_size = (32, 32) + ms1 = SurfaceSubclass(expected_size, SRCALPHA, 32) + ms2 = ms1.copy() + + self.assertIsNot(ms1, ms2) + self.assertIsInstance(ms1, pygame.Surface) + self.assertIsInstance(ms2, pygame.Surface) + self.assertIsInstance(ms1, SurfaceSubclass) + self.assertIsInstance(ms2, SurfaceSubclass) + self.assertTrue(ms1.test_attribute) + self.assertRaises(AttributeError, getattr, ms2, "test_attribute") + self.assertEqual(ms2.get_size(), expected_size) + + def test_convert(self): + """Ensure method convert() preserves the surface's class + + When Surface is subclassed, the inherited convert() method will return + instances of the subclass. Non Surface fields are omitted, however. + This includes instance attributes. + """ + expected_size = (32, 32) + ms1 = SurfaceSubclass(expected_size, 0, 24) + ms2 = ms1.convert(24) + + self.assertIsNot(ms1, ms2) + self.assertIsInstance(ms1, pygame.Surface) + self.assertIsInstance(ms2, pygame.Surface) + self.assertIsInstance(ms1, SurfaceSubclass) + self.assertIsInstance(ms2, SurfaceSubclass) + self.assertTrue(ms1.test_attribute) + self.assertRaises(AttributeError, getattr, ms2, "test_attribute") + self.assertEqual(ms2.get_size(), expected_size) + + def test_convert_alpha(self): + """Ensure method convert_alpha() preserves the surface's class + + When Surface is subclassed, the inherited convert_alpha() method will + return instances of the subclass. Non Surface fields are omitted, + however. This includes instance attributes. + """ + pygame.display.set_mode((40, 40)) + expected_size = (32, 32) + s = pygame.Surface(expected_size, SRCALPHA, 16) + ms1 = SurfaceSubclass(expected_size, SRCALPHA, 32) + ms2 = ms1.convert_alpha(s) + + self.assertIsNot(ms1, ms2) + self.assertIsInstance(ms1, pygame.Surface) + self.assertIsInstance(ms2, pygame.Surface) + self.assertIsInstance(ms1, SurfaceSubclass) + self.assertIsInstance(ms2, SurfaceSubclass) + self.assertTrue(ms1.test_attribute) + self.assertRaises(AttributeError, getattr, ms2, "test_attribute") + self.assertEqual(ms2.get_size(), expected_size) + + def test_subsurface(self): + """Ensure method subsurface() preserves the surface's class + + When Surface is subclassed, the inherited subsurface() method will + return instances of the subclass. Non Surface fields are uncopied, + however. This includes instance attributes. + """ + expected_size = (10, 12) + ms1 = SurfaceSubclass((32, 32), SRCALPHA, 32) + ms2 = ms1.subsurface((4, 5), expected_size) + + self.assertIsNot(ms1, ms2) + self.assertIsInstance(ms1, pygame.Surface) + self.assertIsInstance(ms2, pygame.Surface) + self.assertIsInstance(ms1, SurfaceSubclass) + self.assertIsInstance(ms2, SurfaceSubclass) + self.assertTrue(ms1.test_attribute) + self.assertRaises(AttributeError, getattr, ms2, "test_attribute") + self.assertEqual(ms2.get_size(), expected_size) + + +class SurfaceGetBufferTest(unittest.TestCase): + # These tests requires ctypes. They are disabled if ctypes + # is not installed. + try: + ArrayInterface + except NameError: + __tags__ = ("ignore", "subprocess_ignore") + + lilendian = pygame.get_sdl_byteorder() == pygame.LIL_ENDIAN + + def _check_interface_2D(self, s): + s_w, s_h = s.get_size() + s_bytesize = s.get_bytesize() + s_pitch = s.get_pitch() + s_pixels = s._pixels_address + + # check the array interface structure fields. + v = s.get_view("2") + if not IS_PYPY: + flags = PAI_ALIGNED | PAI_NOTSWAPPED | PAI_WRITEABLE + if s.get_pitch() == s_w * s_bytesize: + flags |= PAI_FORTRAN + + inter = ArrayInterface(v) + + self.assertEqual(inter.two, 2) + self.assertEqual(inter.nd, 2) + self.assertEqual(inter.typekind, "u") + self.assertEqual(inter.itemsize, s_bytesize) + self.assertEqual(inter.shape[0], s_w) + self.assertEqual(inter.shape[1], s_h) + self.assertEqual(inter.strides[0], s_bytesize) + self.assertEqual(inter.strides[1], s_pitch) + self.assertEqual(inter.flags, flags) + self.assertEqual(inter.data, s_pixels) + + def _check_interface_3D(self, s): + s_w, s_h = s.get_size() + s_bytesize = s.get_bytesize() + s_pitch = s.get_pitch() + s_pixels = s._pixels_address + s_shifts = list(s.get_shifts()) + + # Check for RGB or BGR surface. + if s_shifts[0:3] == [0, 8, 16]: + if self.lilendian: + # RGB + offset = 0 + step = 1 + else: + # BGR + offset = s_bytesize - 1 + step = -1 + elif s_shifts[0:3] == [8, 16, 24]: + if self.lilendian: + # xRGB + offset = 1 + step = 1 + else: + # BGRx + offset = s_bytesize - 2 + step = -1 + elif s_shifts[0:3] == [16, 8, 0]: + if self.lilendian: + # BGR + offset = 2 + step = -1 + else: + # RGB + offset = s_bytesize - 3 + step = 1 + elif s_shifts[0:3] == [24, 16, 8]: + if self.lilendian: + # BGRx + offset = 2 + step = -1 + else: + # RGBx + offset = s_bytesize - 4 + step = -1 + else: + return + + # check the array interface structure fields. + v = s.get_view("3") + if not IS_PYPY: + inter = ArrayInterface(v) + flags = PAI_ALIGNED | PAI_NOTSWAPPED | PAI_WRITEABLE + self.assertEqual(inter.two, 2) + self.assertEqual(inter.nd, 3) + self.assertEqual(inter.typekind, "u") + self.assertEqual(inter.itemsize, 1) + self.assertEqual(inter.shape[0], s_w) + self.assertEqual(inter.shape[1], s_h) + self.assertEqual(inter.shape[2], 3) + self.assertEqual(inter.strides[0], s_bytesize) + self.assertEqual(inter.strides[1], s_pitch) + self.assertEqual(inter.strides[2], step) + self.assertEqual(inter.flags, flags) + self.assertEqual(inter.data, s_pixels + offset) + + def _check_interface_rgba(self, s, plane): + s_w, s_h = s.get_size() + s_bytesize = s.get_bytesize() + s_pitch = s.get_pitch() + s_pixels = s._pixels_address + s_shifts = s.get_shifts() + s_masks = s.get_masks() + + # Find the color plane position within the pixel. + if not s_masks[plane]: + return + alpha_shift = s_shifts[plane] + offset = alpha_shift // 8 + if not self.lilendian: + offset = s_bytesize - offset - 1 + + # check the array interface structure fields. + v = s.get_view("rgba"[plane]) + if not IS_PYPY: + inter = ArrayInterface(v) + flags = PAI_ALIGNED | PAI_NOTSWAPPED | PAI_WRITEABLE + self.assertEqual(inter.two, 2) + self.assertEqual(inter.nd, 2) + self.assertEqual(inter.typekind, "u") + self.assertEqual(inter.itemsize, 1) + self.assertEqual(inter.shape[0], s_w) + self.assertEqual(inter.shape[1], s_h) + self.assertEqual(inter.strides[0], s_bytesize) + self.assertEqual(inter.strides[1], s_pitch) + self.assertEqual(inter.flags, flags) + self.assertEqual(inter.data, s_pixels + offset) + + def test_array_interface(self): + self._check_interface_2D(pygame.Surface((5, 7), 0, 8)) + self._check_interface_2D(pygame.Surface((5, 7), 0, 16)) + self._check_interface_2D(pygame.Surface((5, 7), pygame.SRCALPHA, 16)) + self._check_interface_3D(pygame.Surface((5, 7), 0, 24)) + self._check_interface_3D(pygame.Surface((8, 4), 0, 24)) # No gaps + self._check_interface_2D(pygame.Surface((5, 7), 0, 32)) + self._check_interface_3D(pygame.Surface((5, 7), 0, 32)) + self._check_interface_2D(pygame.Surface((5, 7), pygame.SRCALPHA, 32)) + self._check_interface_3D(pygame.Surface((5, 7), pygame.SRCALPHA, 32)) + + def test_array_interface_masks(self): + """Test non-default color byte orders on 3D views""" + + sz = (5, 7) + # Reversed RGB byte order + s = pygame.Surface(sz, 0, 32) + s_masks = list(s.get_masks()) + masks = [0xFF, 0xFF00, 0xFF0000] + if s_masks[0:3] == masks or s_masks[0:3] == masks[::-1]: + masks = s_masks[2::-1] + s_masks[3:4] + self._check_interface_3D(pygame.Surface(sz, 0, 32, masks)) + s = pygame.Surface(sz, 0, 24) + s_masks = list(s.get_masks()) + masks = [0xFF, 0xFF00, 0xFF0000] + if s_masks[0:3] == masks or s_masks[0:3] == masks[::-1]: + masks = s_masks[2::-1] + s_masks[3:4] + self._check_interface_3D(pygame.Surface(sz, 0, 24, masks)) + + masks = [0xFF00, 0xFF0000, 0xFF000000, 0] + self._check_interface_3D(pygame.Surface(sz, 0, 32, masks)) + + def test_array_interface_alpha(self): + for shifts in [[0, 8, 16, 24], [8, 16, 24, 0], [24, 16, 8, 0], [16, 8, 0, 24]]: + masks = [0xFF << s for s in shifts] + s = pygame.Surface((4, 2), pygame.SRCALPHA, 32, masks) + self._check_interface_rgba(s, 3) + + def test_array_interface_rgb(self): + for shifts in [[0, 8, 16, 24], [8, 16, 24, 0], [24, 16, 8, 0], [16, 8, 0, 24]]: + masks = [0xFF << s for s in shifts] + masks[3] = 0 + for plane in range(3): + s = pygame.Surface((4, 2), 0, 24) + self._check_interface_rgba(s, plane) + s = pygame.Surface((4, 2), 0, 32) + self._check_interface_rgba(s, plane) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def test_newbuf_PyBUF_flags_bytes(self): + from pygame.tests.test_utils import buftools + + Importer = buftools.Importer + s = pygame.Surface((10, 6), 0, 32) + a = s.get_buffer() + b = Importer(a, buftools.PyBUF_SIMPLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 1) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address) + b = Importer(a, buftools.PyBUF_WRITABLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertFalse(b.readonly) + b = Importer(a, buftools.PyBUF_FORMAT) + self.assertEqual(b.ndim, 0) + self.assertEqual(b.format, "B") + b = Importer(a, buftools.PyBUF_ND) + self.assertEqual(b.ndim, 1) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 1) + self.assertEqual(b.shape, (a.length,)) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address) + b = Importer(a, buftools.PyBUF_STRIDES) + self.assertEqual(b.ndim, 1) + self.assertTrue(b.format is None) + self.assertEqual(b.strides, (1,)) + s2 = s.subsurface((1, 1, 7, 4)) # Not contiguous + a = s2.get_buffer() + b = Importer(a, buftools.PyBUF_SIMPLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 1) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s2._pixels_address) + b = Importer(a, buftools.PyBUF_C_CONTIGUOUS) + self.assertEqual(b.ndim, 1) + self.assertEqual(b.strides, (1,)) + b = Importer(a, buftools.PyBUF_F_CONTIGUOUS) + self.assertEqual(b.ndim, 1) + self.assertEqual(b.strides, (1,)) + b = Importer(a, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertEqual(b.ndim, 1) + self.assertEqual(b.strides, (1,)) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def test_newbuf_PyBUF_flags_0D(self): + # This is the same handler as used by get_buffer(), so just + # confirm that it succeeds for one case. + from pygame.tests.test_utils import buftools + + Importer = buftools.Importer + s = pygame.Surface((10, 6), 0, 32) + a = s.get_view("0") + b = Importer(a, buftools.PyBUF_SIMPLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 1) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def test_newbuf_PyBUF_flags_1D(self): + from pygame.tests.test_utils import buftools + + Importer = buftools.Importer + s = pygame.Surface((10, 6), 0, 32) + a = s.get_view("1") + b = Importer(a, buftools.PyBUF_SIMPLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, s.get_bytesize()) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address) + b = Importer(a, buftools.PyBUF_WRITABLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertFalse(b.readonly) + b = Importer(a, buftools.PyBUF_FORMAT) + self.assertEqual(b.ndim, 0) + self.assertEqual(b.format, "=I") + b = Importer(a, buftools.PyBUF_ND) + self.assertEqual(b.ndim, 1) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, s.get_bytesize()) + self.assertEqual(b.shape, (s.get_width() * s.get_height(),)) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address) + b = Importer(a, buftools.PyBUF_STRIDES) + self.assertEqual(b.ndim, 1) + self.assertTrue(b.format is None) + self.assertEqual(b.strides, (s.get_bytesize(),)) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def test_newbuf_PyBUF_flags_2D(self): + from pygame.tests.test_utils import buftools + + Importer = buftools.Importer + s = pygame.Surface((10, 6), 0, 32) + a = s.get_view("2") + # Non dimensional requests, no PyDEF_ND, are handled by the + # 1D surface buffer code, so only need to confirm a success. + b = Importer(a, buftools.PyBUF_SIMPLE) + self.assertEqual(b.ndim, 0) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, s.get_bytesize()) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address) + # Uniquely 2D + b = Importer(a, buftools.PyBUF_STRIDES) + self.assertEqual(b.ndim, 2) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, s.get_bytesize()) + self.assertEqual(b.shape, s.get_size()) + self.assertEqual(b.strides, (s.get_bytesize(), s.get_pitch())) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address) + b = Importer(a, buftools.PyBUF_RECORDS_RO) + self.assertEqual(b.ndim, 2) + self.assertEqual(b.format, "=I") + self.assertEqual(b.strides, (s.get_bytesize(), s.get_pitch())) + b = Importer(a, buftools.PyBUF_RECORDS) + self.assertEqual(b.ndim, 2) + self.assertEqual(b.format, "=I") + self.assertEqual(b.strides, (s.get_bytesize(), s.get_pitch())) + b = Importer(a, buftools.PyBUF_F_CONTIGUOUS) + self.assertEqual(b.ndim, 2) + self.assertEqual(b.format, None) + self.assertEqual(b.strides, (s.get_bytesize(), s.get_pitch())) + b = Importer(a, buftools.PyBUF_ANY_CONTIGUOUS) + self.assertEqual(b.ndim, 2) + self.assertEqual(b.format, None) + self.assertEqual(b.strides, (s.get_bytesize(), s.get_pitch())) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_C_CONTIGUOUS) + s2 = s.subsurface((1, 1, 7, 4)) # Not contiguous + a = s2.get_view("2") + b = Importer(a, buftools.PyBUF_STRIDES) + self.assertEqual(b.ndim, 2) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, s2.get_bytesize()) + self.assertEqual(b.shape, s2.get_size()) + self.assertEqual(b.strides, (s2.get_bytesize(), s.get_pitch())) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s2._pixels_address) + b = Importer(a, buftools.PyBUF_RECORDS) + self.assertEqual(b.ndim, 2) + self.assertEqual(b.format, "=I") + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_FORMAT) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_WRITABLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ANY_CONTIGUOUS) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def test_newbuf_PyBUF_flags_3D(self): + from pygame.tests.test_utils import buftools + + Importer = buftools.Importer + s = pygame.Surface((12, 6), 0, 24) + rmask, gmask, bmask, amask = s.get_masks() + if self.lilendian: + if rmask == 0x0000FF: + color_step = 1 + addr_offset = 0 + else: + color_step = -1 + addr_offset = 2 + else: + if rmask == 0xFF0000: + color_step = 1 + addr_offset = 0 + else: + color_step = -1 + addr_offset = 2 + a = s.get_view("3") + b = Importer(a, buftools.PyBUF_STRIDES) + w, h = s.get_size() + shape = w, h, 3 + strides = 3, s.get_pitch(), color_step + self.assertEqual(b.ndim, 3) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 1) + self.assertEqual(b.shape, shape) + self.assertEqual(b.strides, strides) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address + addr_offset) + b = Importer(a, buftools.PyBUF_RECORDS_RO) + self.assertEqual(b.ndim, 3) + self.assertEqual(b.format, "B") + self.assertEqual(b.strides, strides) + b = Importer(a, buftools.PyBUF_RECORDS) + self.assertEqual(b.ndim, 3) + self.assertEqual(b.format, "B") + self.assertEqual(b.strides, strides) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_FORMAT) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_WRITABLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ANY_CONTIGUOUS) + + @unittest.skipIf(not pygame.HAVE_NEWBUF, "newbuf not implemented") + def test_newbuf_PyBUF_flags_rgba(self): + # All color plane views are handled by the same routine, + # so only one plane need be checked. + from pygame.tests.test_utils import buftools + + Importer = buftools.Importer + s = pygame.Surface((12, 6), 0, 24) + rmask, gmask, bmask, amask = s.get_masks() + if self.lilendian: + if rmask == 0x0000FF: + addr_offset = 0 + else: + addr_offset = 2 + else: + if rmask == 0xFF0000: + addr_offset = 0 + else: + addr_offset = 2 + a = s.get_view("R") + b = Importer(a, buftools.PyBUF_STRIDES) + w, h = s.get_size() + shape = w, h + strides = s.get_bytesize(), s.get_pitch() + self.assertEqual(b.ndim, 2) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.length) + self.assertEqual(b.itemsize, 1) + self.assertEqual(b.shape, shape) + self.assertEqual(b.strides, strides) + self.assertTrue(b.suboffsets is None) + self.assertFalse(b.readonly) + self.assertEqual(b.buf, s._pixels_address + addr_offset) + b = Importer(a, buftools.PyBUF_RECORDS_RO) + self.assertEqual(b.ndim, 2) + self.assertEqual(b.format, "B") + self.assertEqual(b.strides, strides) + b = Importer(a, buftools.PyBUF_RECORDS) + self.assertEqual(b.ndim, 2) + self.assertEqual(b.format, "B") + self.assertEqual(b.strides, strides) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_FORMAT) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_WRITABLE) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ND) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, buftools.PyBUF_ANY_CONTIGUOUS) + + +class SurfaceBlendTest(unittest.TestCase): + def setUp(self): + # Needed for 8 bits-per-pixel color palette surface tests. + pygame.display.init() + + def tearDown(self): + pygame.display.quit() + + _test_palette = [ + (0, 0, 0, 255), + (10, 30, 60, 0), + (25, 75, 100, 128), + (200, 150, 100, 200), + (0, 100, 200, 255), + ] + surf_size = (10, 12) + _test_points = [ + ((0, 0), 1), + ((4, 5), 1), + ((9, 0), 2), + ((5, 5), 2), + ((0, 11), 3), + ((4, 6), 3), + ((9, 11), 4), + ((5, 6), 4), + ] + + def _make_surface(self, bitsize, srcalpha=False, palette=None): + if palette is None: + palette = self._test_palette + flags = 0 + if srcalpha: + flags |= SRCALPHA + surf = pygame.Surface(self.surf_size, flags, bitsize) + if bitsize == 8: + surf.set_palette([c[:3] for c in palette]) + return surf + + def _fill_surface(self, surf, palette=None): + if palette is None: + palette = self._test_palette + surf.fill(palette[1], (0, 0, 5, 6)) + surf.fill(palette[2], (5, 0, 5, 6)) + surf.fill(palette[3], (0, 6, 5, 6)) + surf.fill(palette[4], (5, 6, 5, 6)) + + def _make_src_surface(self, bitsize, srcalpha=False, palette=None): + surf = self._make_surface(bitsize, srcalpha, palette) + self._fill_surface(surf, palette) + return surf + + def _assert_surface(self, surf, palette=None, msg=""): + if palette is None: + palette = self._test_palette + if surf.get_bitsize() == 16: + palette = [surf.unmap_rgb(surf.map_rgb(c)) for c in palette] + for posn, i in self._test_points: + self.assertEqual( + surf.get_at(posn), + palette[i], + "%s != %s: flags: %i, bpp: %i, posn: %s%s" + % ( + surf.get_at(posn), + palette[i], + surf.get_flags(), + surf.get_bitsize(), + posn, + msg, + ), + ) + + def test_blit_blend(self): + sources = [ + self._make_src_surface(8), + self._make_src_surface(16), + self._make_src_surface(16, srcalpha=True), + self._make_src_surface(24), + self._make_src_surface(32), + self._make_src_surface(32, srcalpha=True), + ] + destinations = [ + self._make_surface(8), + self._make_surface(16), + self._make_surface(16, srcalpha=True), + self._make_surface(24), + self._make_surface(32), + self._make_surface(32, srcalpha=True), + ] + blend = [ + ("BLEND_ADD", (0, 25, 100, 255), lambda a, b: min(a + b, 255)), + ("BLEND_SUB", (100, 25, 0, 100), lambda a, b: max(a - b, 0)), + ("BLEND_MULT", (100, 200, 0, 0), lambda a, b: ((a * b) + 255) >> 8), + ("BLEND_MIN", (255, 0, 0, 255), min), + ("BLEND_MAX", (0, 255, 0, 255), max), + ] + + for src in sources: + src_palette = [src.unmap_rgb(src.map_rgb(c)) for c in self._test_palette] + for dst in destinations: + for blend_name, dst_color, op in blend: + dc = dst.unmap_rgb(dst.map_rgb(dst_color)) + p = [] + for sc in src_palette: + c = [op(dc[i], sc[i]) for i in range(3)] + if dst.get_masks()[3]: + c.append(dc[3]) + else: + c.append(255) + c = dst.unmap_rgb(dst.map_rgb(c)) + p.append(c) + dst.fill(dst_color) + dst.blit(src, (0, 0), special_flags=getattr(pygame, blend_name)) + self._assert_surface( + dst, + p, + ( + ", op: %s, src bpp: %i" + ", src flags: %i" + % (blend_name, src.get_bitsize(), src.get_flags()) + ), + ) + + src = self._make_src_surface(32) + masks = src.get_masks() + dst = pygame.Surface( + src.get_size(), 0, 32, [masks[2], masks[1], masks[0], masks[3]] + ) + for blend_name, dst_color, op in blend: + p = [] + for src_color in self._test_palette: + c = [op(dst_color[i], src_color[i]) for i in range(3)] + c.append(255) + p.append(tuple(c)) + dst.fill(dst_color) + dst.blit(src, (0, 0), special_flags=getattr(pygame, blend_name)) + self._assert_surface(dst, p, f", {blend_name}") + + # Blend blits are special cased for 32 to 32 bit surfaces. + # + # Confirm that it works when the rgb bytes are not the + # least significant bytes. + pat = self._make_src_surface(32) + masks = pat.get_masks() + if min(masks) == 0xFF000000: + masks = [m >> 8 for m in masks] + else: + masks = [m << 8 for m in masks] + src = pygame.Surface(pat.get_size(), 0, 32, masks) + self._fill_surface(src) + dst = pygame.Surface(src.get_size(), 0, 32, masks) + for blend_name, dst_color, op in blend: + p = [] + for src_color in self._test_palette: + c = [op(dst_color[i], src_color[i]) for i in range(3)] + c.append(255) + p.append(tuple(c)) + dst.fill(dst_color) + dst.blit(src, (0, 0), special_flags=getattr(pygame, blend_name)) + self._assert_surface(dst, p, f", {blend_name}") + + def test_blit_blend_rgba(self): + sources = [ + self._make_src_surface(8), + self._make_src_surface(16), + self._make_src_surface(16, srcalpha=True), + self._make_src_surface(24), + self._make_src_surface(32), + self._make_src_surface(32, srcalpha=True), + ] + destinations = [ + self._make_surface(8), + self._make_surface(16), + self._make_surface(16, srcalpha=True), + self._make_surface(24), + self._make_surface(32), + self._make_surface(32, srcalpha=True), + ] + blend = [ + ("BLEND_RGBA_ADD", (0, 25, 100, 255), lambda a, b: min(a + b, 255)), + ("BLEND_RGBA_SUB", (0, 25, 100, 255), lambda a, b: max(a - b, 0)), + ("BLEND_RGBA_MULT", (0, 7, 100, 255), lambda a, b: ((a * b) + 255) >> 8), + ("BLEND_RGBA_MIN", (0, 255, 0, 255), min), + ("BLEND_RGBA_MAX", (0, 255, 0, 255), max), + ] + + for src in sources: + src_palette = [src.unmap_rgb(src.map_rgb(c)) for c in self._test_palette] + for dst in destinations: + for blend_name, dst_color, op in blend: + dc = dst.unmap_rgb(dst.map_rgb(dst_color)) + p = [] + for sc in src_palette: + c = [op(dc[i], sc[i]) for i in range(4)] + if not dst.get_masks()[3]: + c[3] = 255 + c = dst.unmap_rgb(dst.map_rgb(c)) + p.append(c) + dst.fill(dst_color) + dst.blit(src, (0, 0), special_flags=getattr(pygame, blend_name)) + self._assert_surface( + dst, + p, + ( + ", op: %s, src bpp: %i" + ", src flags: %i" + % (blend_name, src.get_bitsize(), src.get_flags()) + ), + ) + + # Blend blits are special cased for 32 to 32 bit surfaces + # with per-pixel alpha. + # + # Confirm the general case is used instead when the formats differ. + src = self._make_src_surface(32, srcalpha=True) + masks = src.get_masks() + dst = pygame.Surface( + src.get_size(), SRCALPHA, 32, (masks[2], masks[1], masks[0], masks[3]) + ) + for blend_name, dst_color, op in blend: + p = [ + tuple(op(dst_color[i], src_color[i]) for i in range(4)) + for src_color in self._test_palette + ] + dst.fill(dst_color) + dst.blit(src, (0, 0), special_flags=getattr(pygame, blend_name)) + self._assert_surface(dst, p, f", {blend_name}") + + # Confirm this special case handles subsurfaces. + src = pygame.Surface((8, 10), SRCALPHA, 32) + dst = pygame.Surface((8, 10), SRCALPHA, 32) + tst = pygame.Surface((8, 10), SRCALPHA, 32) + src.fill((1, 2, 3, 4)) + dst.fill((40, 30, 20, 10)) + subsrc = src.subsurface((2, 3, 4, 4)) + subdst = dst.subsurface((2, 3, 4, 4)) + subdst.blit(subsrc, (0, 0), special_flags=BLEND_RGBA_ADD) + tst.fill((40, 30, 20, 10)) + tst.fill((41, 32, 23, 14), (2, 3, 4, 4)) + for x in range(8): + for y in range(10): + self.assertEqual( + dst.get_at((x, y)), + tst.get_at((x, y)), + "%s != %s at (%i, %i)" + % (dst.get_at((x, y)), tst.get_at((x, y)), x, y), + ) + + def test_blit_blend_premultiplied(self): + def test_premul_surf( + src_col, + dst_col, + src_size=(16, 16), + dst_size=(16, 16), + src_bit_depth=32, + dst_bit_depth=32, + src_has_alpha=True, + dst_has_alpha=True, + ): + if src_bit_depth == 8: + src = pygame.Surface(src_size, 0, src_bit_depth) + palette = [src_col, dst_col] + src.set_palette(palette) + src.fill(palette[0]) + elif src_has_alpha: + src = pygame.Surface(src_size, SRCALPHA, src_bit_depth) + src.fill(src_col) + else: + src = pygame.Surface(src_size, 0, src_bit_depth) + src.fill(src_col) + + if dst_bit_depth == 8: + dst = pygame.Surface(dst_size, 0, dst_bit_depth) + palette = [src_col, dst_col] + dst.set_palette(palette) + dst.fill(palette[1]) + elif dst_has_alpha: + dst = pygame.Surface(dst_size, SRCALPHA, dst_bit_depth) + dst.fill(dst_col) + else: + dst = pygame.Surface(dst_size, 0, dst_bit_depth) + dst.fill(dst_col) + + dst.blit(src, (0, 0), special_flags=BLEND_PREMULTIPLIED) + + actual_col = dst.get_at( + (int(float(src_size[0] / 2.0)), int(float(src_size[0] / 2.0))) + ) + + # This is the blend pre-multiplied formula + if src_col.a == 0: + expected_col = dst_col + elif src_col.a == 255: + expected_col = src_col + else: + # sC + dC - (((dC + 1) * sA >> 8) + expected_col = pygame.Color( + (src_col.r + dst_col.r - ((dst_col.r + 1) * src_col.a >> 8)), + (src_col.g + dst_col.g - ((dst_col.g + 1) * src_col.a >> 8)), + (src_col.b + dst_col.b - ((dst_col.b + 1) * src_col.a >> 8)), + (src_col.a + dst_col.a - ((dst_col.a + 1) * src_col.a >> 8)), + ) + if not dst_has_alpha: + expected_col.a = 255 + + return (expected_col, actual_col) + + # # Colour Tests + self.assertEqual( + *test_premul_surf(pygame.Color(40, 20, 0, 51), pygame.Color(40, 20, 0, 51)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(0, 0, 0, 0), pygame.Color(40, 20, 0, 51)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(40, 20, 0, 51), pygame.Color(0, 0, 0, 0)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(0, 0, 0, 0), pygame.Color(0, 0, 0, 0)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(2, 2, 2, 2), pygame.Color(40, 20, 0, 51)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(40, 20, 0, 51), pygame.Color(2, 2, 2, 2)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(2, 2, 2, 2), pygame.Color(2, 2, 2, 2)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(9, 9, 9, 9), pygame.Color(40, 20, 0, 51)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(40, 20, 0, 51), pygame.Color(9, 9, 9, 9)) + ) + + self.assertEqual( + *test_premul_surf(pygame.Color(9, 9, 9, 9), pygame.Color(9, 9, 9, 9)) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(127, 127, 127, 127), pygame.Color(40, 20, 0, 51) + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(40, 20, 0, 51), pygame.Color(127, 127, 127, 127) + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(127, 127, 127, 127), pygame.Color(127, 127, 127, 127) + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(200, 200, 200, 200), pygame.Color(40, 20, 0, 51) + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(40, 20, 0, 51), pygame.Color(200, 200, 200, 200) + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(200, 200, 200, 200), pygame.Color(200, 200, 200, 200) + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(255, 255, 255, 255), pygame.Color(40, 20, 0, 51) + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(40, 20, 0, 51), pygame.Color(255, 255, 255, 255) + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(255, 255, 255, 255), pygame.Color(255, 255, 255, 255) + ) + ) + + # Surface format tests + self.assertRaises( + IndexError, + test_premul_surf, + pygame.Color(255, 255, 255, 255), + pygame.Color(255, 255, 255, 255), + src_size=(0, 0), + dst_size=(0, 0), + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(40, 20, 0, 51), + pygame.Color(30, 20, 0, 51), + src_size=(4, 4), + dst_size=(9, 9), + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 51), + pygame.Color(40, 20, 0, 51), + src_size=(17, 67), + dst_size=(69, 69), + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 255), + pygame.Color(40, 20, 0, 51), + src_size=(17, 67), + dst_size=(69, 69), + src_has_alpha=True, + ) + ) + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 51), + pygame.Color(40, 20, 0, 255), + src_size=(17, 67), + dst_size=(69, 69), + dst_has_alpha=False, + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 255), + pygame.Color(40, 20, 0, 255), + src_size=(17, 67), + dst_size=(69, 69), + src_has_alpha=False, + dst_has_alpha=False, + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 255), + pygame.Color(40, 20, 0, 255), + src_size=(17, 67), + dst_size=(69, 69), + dst_bit_depth=24, + src_has_alpha=True, + dst_has_alpha=False, + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 255), + pygame.Color(40, 20, 0, 255), + src_size=(17, 67), + dst_size=(69, 69), + src_bit_depth=24, + src_has_alpha=False, + dst_has_alpha=True, + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 255), + pygame.Color(40, 20, 0, 255), + src_size=(17, 67), + dst_size=(69, 69), + src_bit_depth=24, + dst_bit_depth=24, + src_has_alpha=False, + dst_has_alpha=False, + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 255), + pygame.Color(40, 20, 0, 255), + src_size=(17, 67), + dst_size=(69, 69), + src_bit_depth=8, + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 255), + pygame.Color(40, 20, 0, 255), + src_size=(17, 67), + dst_size=(69, 69), + dst_bit_depth=8, + ) + ) + + self.assertEqual( + *test_premul_surf( + pygame.Color(30, 20, 0, 255), + pygame.Color(40, 20, 0, 255), + src_size=(17, 67), + dst_size=(69, 69), + src_bit_depth=8, + dst_bit_depth=8, + ) + ) + + def test_blit_blend_big_rect(self): + """test that an oversized rect works ok.""" + color = (1, 2, 3, 255) + area = (1, 1, 30, 30) + s1 = pygame.Surface((4, 4), 0, 32) + r = s1.fill(special_flags=pygame.BLEND_ADD, color=color, rect=area) + + self.assertEqual(pygame.Rect((1, 1, 3, 3)), r) + self.assertEqual(s1.get_at((0, 0)), (0, 0, 0, 255)) + self.assertEqual(s1.get_at((1, 1)), color) + + black = pygame.Color("black") + red = pygame.Color("red") + self.assertNotEqual(black, red) + + surf = pygame.Surface((10, 10), 0, 32) + surf.fill(black) + subsurf = surf.subsurface(pygame.Rect(0, 1, 10, 8)) + self.assertEqual(surf.get_at((0, 0)), black) + self.assertEqual(surf.get_at((0, 9)), black) + + subsurf.fill(red, (0, -1, 10, 1), pygame.BLEND_RGB_ADD) + self.assertEqual(surf.get_at((0, 0)), black) + self.assertEqual(surf.get_at((0, 9)), black) + + subsurf.fill(red, (0, 8, 10, 1), pygame.BLEND_RGB_ADD) + self.assertEqual(surf.get_at((0, 0)), black) + self.assertEqual(surf.get_at((0, 9)), black) + + def test_GET_PIXELVALS(self): + # surface.h GET_PIXELVALS bug regarding whether of not + # a surface has per-pixel alpha. Looking at the Amask + # is not enough. The surface's SRCALPHA flag must also + # be considered. Fix rev. 1923. + src = self._make_surface(32, srcalpha=True) + src.fill((0, 0, 0, 128)) + src.set_alpha(None) # Clear SRCALPHA flag. + dst = self._make_surface(32, srcalpha=True) + dst.blit(src, (0, 0), special_flags=BLEND_RGBA_ADD) + self.assertEqual(dst.get_at((0, 0)), (0, 0, 0, 255)) + + def test_fill_blend(self): + destinations = [ + self._make_surface(8), + self._make_surface(16), + self._make_surface(16, srcalpha=True), + self._make_surface(24), + self._make_surface(32), + self._make_surface(32, srcalpha=True), + ] + blend = [ + ("BLEND_ADD", (0, 25, 100, 255), lambda a, b: min(a + b, 255)), + ("BLEND_SUB", (0, 25, 100, 255), lambda a, b: max(a - b, 0)), + ("BLEND_MULT", (0, 7, 100, 255), lambda a, b: ((a * b) + 255) >> 8), + ("BLEND_MIN", (0, 255, 0, 255), min), + ("BLEND_MAX", (0, 255, 0, 255), max), + ] + + for dst in destinations: + dst_palette = [dst.unmap_rgb(dst.map_rgb(c)) for c in self._test_palette] + for blend_name, fill_color, op in blend: + fc = dst.unmap_rgb(dst.map_rgb(fill_color)) + self._fill_surface(dst) + p = [] + for dc in dst_palette: + c = [op(dc[i], fc[i]) for i in range(3)] + if dst.get_masks()[3]: + c.append(dc[3]) + else: + c.append(255) + c = dst.unmap_rgb(dst.map_rgb(c)) + p.append(c) + dst.fill(fill_color, special_flags=getattr(pygame, blend_name)) + self._assert_surface(dst, p, f", {blend_name}") + + def test_fill_blend_rgba(self): + destinations = [ + self._make_surface(8), + self._make_surface(16), + self._make_surface(16, srcalpha=True), + self._make_surface(24), + self._make_surface(32), + self._make_surface(32, srcalpha=True), + ] + blend = [ + ("BLEND_RGBA_ADD", (0, 25, 100, 255), lambda a, b: min(a + b, 255)), + ("BLEND_RGBA_SUB", (0, 25, 100, 255), lambda a, b: max(a - b, 0)), + ("BLEND_RGBA_MULT", (0, 7, 100, 255), lambda a, b: ((a * b) + 255) >> 8), + ("BLEND_RGBA_MIN", (0, 255, 0, 255), min), + ("BLEND_RGBA_MAX", (0, 255, 0, 255), max), + ] + + for dst in destinations: + dst_palette = [dst.unmap_rgb(dst.map_rgb(c)) for c in self._test_palette] + for blend_name, fill_color, op in blend: + fc = dst.unmap_rgb(dst.map_rgb(fill_color)) + self._fill_surface(dst) + p = [] + for dc in dst_palette: + c = [op(dc[i], fc[i]) for i in range(4)] + if not dst.get_masks()[3]: + c[3] = 255 + c = dst.unmap_rgb(dst.map_rgb(c)) + p.append(c) + dst.fill(fill_color, special_flags=getattr(pygame, blend_name)) + self._assert_surface(dst, p, f", {blend_name}") + + def test_surface_premul_alpha(self): + """Ensure that .premul_alpha() works correctly""" + + # basic functionality at valid bit depths - 32, 16 & 8 + s1 = pygame.Surface((100, 100), pygame.SRCALPHA, 32) + s1.fill(pygame.Color(255, 255, 255, 100)) + s1_alpha = s1.premul_alpha() + self.assertEqual(s1_alpha.get_at((50, 50)), pygame.Color(100, 100, 100, 100)) + + # 16 bit colour has less precision + s2 = pygame.Surface((100, 100), pygame.SRCALPHA, 16) + s2.fill(pygame.Color(255, 255, 255, 170)) + s2_alpha = s2.premul_alpha() + self.assertEqual(s2_alpha.get_at((50, 50)), pygame.Color(170, 170, 170, 170)) + + # invalid surface - we need alpha to pre-multiply + invalid_surf = pygame.Surface((100, 100), 0, 32) + invalid_surf.fill(pygame.Color(255, 255, 255, 100)) + with self.assertRaises(ValueError): + invalid_surf.premul_alpha() + + # churn a bunch of values + test_colors = [ + (200, 30, 74), + (76, 83, 24), + (184, 21, 6), + (74, 4, 74), + (76, 83, 24), + (184, 21, 234), + (160, 30, 74), + (96, 147, 204), + (198, 201, 60), + (132, 89, 74), + (245, 9, 224), + (184, 112, 6), + ] + + for r, g, b in test_colors: + for a in range(255): + with self.subTest(r=r, g=g, b=b, a=a): + surf = pygame.Surface((10, 10), pygame.SRCALPHA, 32) + surf.fill(pygame.Color(r, g, b, a)) + surf = surf.premul_alpha() + self.assertEqual( + surf.get_at((5, 5)), + Color( + ((r + 1) * a) >> 8, + ((g + 1) * a) >> 8, + ((b + 1) * a) >> 8, + a, + ), + ) + + +class SurfaceSelfBlitTest(unittest.TestCase): + """Blit to self tests. + + This test case is in response to https://github.com/pygame/pygame/issues/19 + """ + + def setUp(self): + # Needed for 8 bits-per-pixel color palette surface tests. + pygame.display.init() + + def tearDown(self): + pygame.display.quit() + + _test_palette = [(0, 0, 0, 255), (255, 0, 0, 0), (0, 255, 0, 255)] + surf_size = (9, 6) + + def _fill_surface(self, surf, palette=None): + if palette is None: + palette = self._test_palette + surf.fill(palette[1]) + surf.fill(palette[2], (1, 2, 1, 2)) + + def _make_surface(self, bitsize, srcalpha=False, palette=None): + if palette is None: + palette = self._test_palette + flags = 0 + if srcalpha: + flags |= SRCALPHA + surf = pygame.Surface(self.surf_size, flags, bitsize) + if bitsize == 8: + surf.set_palette([c[:3] for c in palette]) + self._fill_surface(surf, palette) + return surf + + def _assert_same(self, a, b): + w, h = a.get_size() + for x in range(w): + for y in range(h): + self.assertEqual( + a.get_at((x, y)), + b.get_at((x, y)), + ( + "%s != %s, bpp: %i" + % (a.get_at((x, y)), b.get_at((x, y)), a.get_bitsize()) + ), + ) + + def test_overlap_check(self): + # Ensure overlapping blits are properly detected. There are two + # places where this is done, within SoftBlitPyGame() in alphablit.c + # and PySurface_Blit() in surface.c. SoftBlitPyGame should catch the + # per-pixel alpha surface, PySurface_Blit the colorkey and blanket + # alpha surface. per-pixel alpha and blanket alpha self blits are + # not properly handled by SDL 1.2.13, so Pygame does them. + bgc = (0, 0, 0, 255) + rectc_left = (128, 64, 32, 255) + rectc_right = (255, 255, 255, 255) + colors = [(255, 255, 255, 255), (128, 64, 32, 255)] + overlaps = [ + (0, 0, 1, 0, (50, 0)), + (0, 0, 49, 1, (98, 2)), + (0, 0, 49, 49, (98, 98)), + (49, 0, 0, 1, (0, 2)), + (49, 0, 0, 49, (0, 98)), + ] + surfs = [pygame.Surface((100, 100), SRCALPHA, 32)] + surf = pygame.Surface((100, 100), 0, 32) + surf.set_alpha(255) + surfs.append(surf) + surf = pygame.Surface((100, 100), 0, 32) + surf.set_colorkey((0, 1, 0)) + surfs.append(surf) + for surf in surfs: + for s_x, s_y, d_x, d_y, test_posn in overlaps: + surf.fill(bgc) + surf.fill(rectc_right, (25, 0, 25, 50)) + surf.fill(rectc_left, (0, 0, 25, 50)) + surf.blit(surf, (d_x, d_y), (s_x, s_y, 50, 50)) + self.assertEqual(surf.get_at(test_posn), rectc_right) + + # https://github.com/pygame/pygame/issues/370#issuecomment-364625291 + @unittest.skipIf("ppc64le" in platform.uname(), "known ppc64le issue") + def test_colorkey(self): + # Check a workaround for an SDL 1.2.13 surface self-blit problem + # https://github.com/pygame/pygame/issues/19 + pygame.display.set_mode((100, 50)) # Needed for 8bit surface + bitsizes = [8, 16, 24, 32] + for bitsize in bitsizes: + surf = self._make_surface(bitsize) + surf.set_colorkey(self._test_palette[1]) + surf.blit(surf, (3, 0)) + p = [] + for c in self._test_palette: + c = surf.unmap_rgb(surf.map_rgb(c)) + p.append(c) + p[1] = (p[1][0], p[1][1], p[1][2], 0) + tmp = self._make_surface(32, srcalpha=True, palette=p) + tmp.blit(tmp, (3, 0)) + tmp.set_alpha(None) + comp = self._make_surface(bitsize) + comp.blit(tmp, (0, 0)) + self._assert_same(surf, comp) + + # https://github.com/pygame/pygame/issues/370#issuecomment-364625291 + @unittest.skipIf("ppc64le" in platform.uname(), "known ppc64le issue") + def test_blanket_alpha(self): + # Check a workaround for an SDL 1.2.13 surface self-blit problem + # https://github.com/pygame/pygame/issues/19 + pygame.display.set_mode((100, 50)) # Needed for 8bit surface + bitsizes = [8, 16, 24, 32] + for bitsize in bitsizes: + surf = self._make_surface(bitsize) + surf.set_alpha(128) + surf.blit(surf, (3, 0)) + p = [] + for c in self._test_palette: + c = surf.unmap_rgb(surf.map_rgb(c)) + p.append((c[0], c[1], c[2], 128)) + tmp = self._make_surface(32, srcalpha=True, palette=p) + tmp.blit(tmp, (3, 0)) + tmp.set_alpha(None) + comp = self._make_surface(bitsize) + comp.blit(tmp, (0, 0)) + self._assert_same(surf, comp) + + def test_pixel_alpha(self): + bitsizes = [16, 32] + for bitsize in bitsizes: + surf = self._make_surface(bitsize, srcalpha=True) + comp = self._make_surface(bitsize, srcalpha=True) + comp.blit(surf, (3, 0)) + surf.blit(surf, (3, 0)) + self._assert_same(surf, comp) + + def test_blend(self): + bitsizes = [8, 16, 24, 32] + blends = ["BLEND_ADD", "BLEND_SUB", "BLEND_MULT", "BLEND_MIN", "BLEND_MAX"] + for bitsize in bitsizes: + surf = self._make_surface(bitsize) + comp = self._make_surface(bitsize) + for blend in blends: + self._fill_surface(surf) + self._fill_surface(comp) + comp.blit(surf, (3, 0), special_flags=getattr(pygame, blend)) + surf.blit(surf, (3, 0), special_flags=getattr(pygame, blend)) + self._assert_same(surf, comp) + + def test_blend_rgba(self): + bitsizes = [16, 32] + blends = [ + "BLEND_RGBA_ADD", + "BLEND_RGBA_SUB", + "BLEND_RGBA_MULT", + "BLEND_RGBA_MIN", + "BLEND_RGBA_MAX", + ] + for bitsize in bitsizes: + surf = self._make_surface(bitsize, srcalpha=True) + comp = self._make_surface(bitsize, srcalpha=True) + for blend in blends: + self._fill_surface(surf) + self._fill_surface(comp) + comp.blit(surf, (3, 0), special_flags=getattr(pygame, blend)) + surf.blit(surf, (3, 0), special_flags=getattr(pygame, blend)) + self._assert_same(surf, comp) + + def test_subsurface(self): + # Blitting a surface to its subsurface is allowed. + surf = self._make_surface(32, srcalpha=True) + comp = surf.copy() + comp.blit(surf, (3, 0)) + sub = surf.subsurface((3, 0, 6, 6)) + sub.blit(surf, (0, 0)) + del sub + self._assert_same(surf, comp) + + # Blitting a subsurface to its owner is forbidden because of + # lock conflicts. This limitation allows the overlap check + # in PySurface_Blit of alphablit.c to be simplified. + def do_blit(d, s): + d.blit(s, (0, 0)) + + sub = surf.subsurface((1, 1, 2, 2)) + self.assertRaises(pygame.error, do_blit, surf, sub) + + def test_copy_alpha(self): + """issue 581: alpha of surface copy with SRCALPHA is set to 0.""" + surf = pygame.Surface((16, 16), pygame.SRCALPHA, 32) + self.assertEqual(surf.get_alpha(), 255) + surf2 = surf.copy() + self.assertEqual(surf2.get_alpha(), 255) + + +class SurfaceFillTest(unittest.TestCase): + def setUp(self): + pygame.display.init() + + def tearDown(self): + pygame.display.quit() + + def test_fill(self): + screen = pygame.display.set_mode((640, 480)) + + # Green and blue test pattern + screen.fill((0, 255, 0), (0, 0, 320, 240)) + screen.fill((0, 255, 0), (320, 240, 320, 240)) + screen.fill((0, 0, 255), (320, 0, 320, 240)) + screen.fill((0, 0, 255), (0, 240, 320, 240)) + + # Now apply a clip rect, such that only the left side of the + # screen should be effected by blit operations. + screen.set_clip((0, 0, 320, 480)) + + # Test fills with each special flag, and additionally without any. + screen.fill((255, 0, 0, 127), (160, 0, 320, 30), 0) + screen.fill((255, 0, 0, 127), (160, 30, 320, 30), pygame.BLEND_ADD) + screen.fill((0, 127, 127, 127), (160, 60, 320, 30), pygame.BLEND_SUB) + screen.fill((0, 63, 63, 127), (160, 90, 320, 30), pygame.BLEND_MULT) + screen.fill((0, 127, 127, 127), (160, 120, 320, 30), pygame.BLEND_MIN) + screen.fill((127, 0, 0, 127), (160, 150, 320, 30), pygame.BLEND_MAX) + screen.fill((255, 0, 0, 127), (160, 180, 320, 30), pygame.BLEND_RGBA_ADD) + screen.fill((0, 127, 127, 127), (160, 210, 320, 30), pygame.BLEND_RGBA_SUB) + screen.fill((0, 63, 63, 127), (160, 240, 320, 30), pygame.BLEND_RGBA_MULT) + screen.fill((0, 127, 127, 127), (160, 270, 320, 30), pygame.BLEND_RGBA_MIN) + screen.fill((127, 0, 0, 127), (160, 300, 320, 30), pygame.BLEND_RGBA_MAX) + screen.fill((255, 0, 0, 127), (160, 330, 320, 30), pygame.BLEND_RGB_ADD) + screen.fill((0, 127, 127, 127), (160, 360, 320, 30), pygame.BLEND_RGB_SUB) + screen.fill((0, 63, 63, 127), (160, 390, 320, 30), pygame.BLEND_RGB_MULT) + screen.fill((0, 127, 127, 127), (160, 420, 320, 30), pygame.BLEND_RGB_MIN) + screen.fill((255, 0, 0, 127), (160, 450, 320, 30), pygame.BLEND_RGB_MAX) + + # Update the display so we can see the results + pygame.display.flip() + + # Compare colors on both sides of window + for y in range(5, 480, 10): + self.assertEqual(screen.get_at((10, y)), screen.get_at((330, 480 - y))) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/surfarray_tags.py b/laplas/abstract_map/pygame/tests/surfarray_tags.py new file mode 100644 index 0000000..baa535c --- /dev/null +++ b/laplas/abstract_map/pygame/tests/surfarray_tags.py @@ -0,0 +1,16 @@ +__tags__ = ["array"] + +exclude = False + +try: + import numpy +except ImportError: + exclude = True +else: + try: + import pygame.pixelcopy + except ImportError: + exclude = True + +if exclude: + __tags__.extend(("ignore", "subprocess_ignore")) diff --git a/laplas/abstract_map/pygame/tests/surfarray_test.py b/laplas/abstract_map/pygame/tests/surfarray_test.py new file mode 100644 index 0000000..ee74290 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/surfarray_test.py @@ -0,0 +1,743 @@ +import unittest +import platform + +from numpy import ( + uint8, + uint16, + uint32, + uint64, + zeros, + float32, + float64, + all as alltrue, + rint, + arange, +) + +import pygame +from pygame.locals import * + +import pygame.surfarray + + +IS_PYPY = "PyPy" == platform.python_implementation() + + +@unittest.skipIf(IS_PYPY, "pypy skip known failure") # TODO +class SurfarrayModuleTest(unittest.TestCase): + pixels2d = {8: True, 16: True, 24: False, 32: True} + pixels3d = {8: False, 16: False, 24: True, 32: True} + array2d = {8: True, 16: True, 24: True, 32: True} + array3d = {8: False, 16: False, 24: True, 32: True} + + test_palette = [ + (0, 0, 0, 255), + (10, 30, 60, 255), + (25, 75, 100, 255), + (100, 150, 200, 255), + (0, 100, 200, 255), + ] + surf_size = (10, 12) + test_points = [ + ((0, 0), 1), + ((4, 5), 1), + ((9, 0), 2), + ((5, 5), 2), + ((0, 11), 3), + ((4, 6), 3), + ((9, 11), 4), + ((5, 6), 4), + ] + + @classmethod + def setUpClass(cls): + # Needed for 8 bits-per-pixel color palette surface tests. + pygame.init() + + @classmethod + def tearDownClass(cls): + pygame.quit() + + def setUp(cls): + # This makes sure pygame is always initialized before each test (in + # case a test calls pygame.quit()). + if not pygame.get_init(): + pygame.init() + + def _make_surface(self, bitsize, srcalpha=False, palette=None): + if palette is None: + palette = self.test_palette + flags = 0 + if srcalpha: + flags |= SRCALPHA + surf = pygame.Surface(self.surf_size, flags, bitsize) + if bitsize == 8: + surf.set_palette([c[:3] for c in palette]) + return surf + + def _fill_surface(self, surf, palette=None): + if palette is None: + palette = self.test_palette + surf.fill(palette[1], (0, 0, 5, 6)) + surf.fill(palette[2], (5, 0, 5, 6)) + surf.fill(palette[3], (0, 6, 5, 6)) + surf.fill(palette[4], (5, 6, 5, 6)) + + def _make_src_surface(self, bitsize, srcalpha=False, palette=None): + surf = self._make_surface(bitsize, srcalpha, palette) + self._fill_surface(surf, palette) + return surf + + def _assert_surface(self, surf, palette=None, msg=""): + if palette is None: + palette = self.test_palette + if surf.get_bitsize() == 16: + palette = [surf.unmap_rgb(surf.map_rgb(c)) for c in palette] + for posn, i in self.test_points: + self.assertEqual( + surf.get_at(posn), + palette[i], + "%s != %s: flags: %i, bpp: %i, posn: %s%s" + % ( + surf.get_at(posn), + palette[i], + surf.get_flags(), + surf.get_bitsize(), + posn, + msg, + ), + ) + + def _make_array3d(self, dtype): + return zeros((self.surf_size[0], self.surf_size[1], 3), dtype) + + def _fill_array2d(self, arr, surf): + palette = self.test_palette + arr[:5, :6] = surf.map_rgb(palette[1]) & 0xFFFFFFFF + arr[5:, :6] = surf.map_rgb(palette[2]) & 0xFFFFFFFF + arr[:5, 6:] = surf.map_rgb(palette[3]) & 0xFFFFFFFF + arr[5:, 6:] = surf.map_rgb(palette[4]) & 0xFFFFFFFF + + def _fill_array3d(self, arr): + palette = self.test_palette + arr[:5, :6] = palette[1][:3] + arr[5:, :6] = palette[2][:3] + arr[:5, 6:] = palette[3][:3] + arr[5:, 6:] = palette[4][:3] + + def _make_src_array3d(self, dtype): + arr = self._make_array3d(dtype) + self._fill_array3d(arr) + return arr + + def _make_array2d(self, dtype): + return zeros(self.surf_size, dtype) + + def test_array2d(self): + sources = [ + self._make_src_surface(8), + self._make_src_surface(16), + self._make_src_surface(16, srcalpha=True), + self._make_src_surface(24), + self._make_src_surface(32), + self._make_src_surface(32, srcalpha=True), + ] + palette = self.test_palette + alpha_color = (0, 0, 0, 128) + + for surf in sources: + arr = pygame.surfarray.array2d(surf) + for posn, i in self.test_points: + self.assertEqual( + arr[posn], + surf.get_at_mapped(posn), + "%s != %s: flags: %i, bpp: %i, posn: %s" + % ( + arr[posn], + surf.get_at_mapped(posn), + surf.get_flags(), + surf.get_bitsize(), + posn, + ), + ) + + if surf.get_masks()[3]: + surf.fill(alpha_color) + arr = pygame.surfarray.array2d(surf) + posn = (0, 0) + self.assertEqual( + arr[posn], + surf.get_at_mapped(posn), + "%s != %s: bpp: %i" + % (arr[posn], surf.get_at_mapped(posn), surf.get_bitsize()), + ) + + def test_array3d(self): + sources = [ + self._make_src_surface(16), + self._make_src_surface(16, srcalpha=True), + self._make_src_surface(24), + self._make_src_surface(32), + self._make_src_surface(32, srcalpha=True), + ] + palette = self.test_palette + + for surf in sources: + arr = pygame.surfarray.array3d(surf) + + def same_color(ac, sc): + return ac[0] == sc[0] and ac[1] == sc[1] and ac[2] == sc[2] + + for posn, i in self.test_points: + self.assertTrue( + same_color(arr[posn], surf.get_at(posn)), + "%s != %s: flags: %i, bpp: %i, posn: %s" + % ( + tuple(arr[posn]), + surf.get_at(posn), + surf.get_flags(), + surf.get_bitsize(), + posn, + ), + ) + + def test_array_alpha(self): + palette = [ + (0, 0, 0, 0), + (10, 50, 100, 255), + (60, 120, 240, 130), + (64, 128, 255, 0), + (255, 128, 0, 65), + ] + targets = [ + self._make_src_surface(8, palette=palette), + self._make_src_surface(16, palette=palette), + self._make_src_surface(16, palette=palette, srcalpha=True), + self._make_src_surface(24, palette=palette), + self._make_src_surface(32, palette=palette), + self._make_src_surface(32, palette=palette, srcalpha=True), + ] + + for surf in targets: + p = palette + if surf.get_bitsize() == 16: + p = [surf.unmap_rgb(surf.map_rgb(c)) for c in p] + arr = pygame.surfarray.array_alpha(surf) + if surf.get_masks()[3]: + for (x, y), i in self.test_points: + self.assertEqual( + arr[x, y], + p[i][3], + ( + "%i != %i, posn: (%i, %i), " + "bitsize: %i" + % (arr[x, y], p[i][3], x, y, surf.get_bitsize()) + ), + ) + else: + self.assertTrue(alltrue(arr == 255)) + + # No per-pixel alpha when blanket alpha is None. + for surf in targets: + blanket_alpha = surf.get_alpha() + surf.set_alpha(None) + arr = pygame.surfarray.array_alpha(surf) + self.assertTrue( + alltrue(arr == 255), + "All alpha values should be 255 when" + " surf.set_alpha(None) has been set." + " bitsize: %i, flags: %i" % (surf.get_bitsize(), surf.get_flags()), + ) + surf.set_alpha(blanket_alpha) + + # Bug for per-pixel alpha surface when blanket alpha 0. + for surf in targets: + blanket_alpha = surf.get_alpha() + surf.set_alpha(0) + arr = pygame.surfarray.array_alpha(surf) + if surf.get_masks()[3]: + self.assertFalse( + alltrue(arr == 255), + "bitsize: %i, flags: %i" % (surf.get_bitsize(), surf.get_flags()), + ) + else: + self.assertTrue( + alltrue(arr == 255), + "bitsize: %i, flags: %i" % (surf.get_bitsize(), surf.get_flags()), + ) + surf.set_alpha(blanket_alpha) + + def test_array_colorkey(self): + palette = [ + (0, 0, 0, 0), + (10, 50, 100, 255), + (60, 120, 240, 130), + (64, 128, 255, 0), + (255, 128, 0, 65), + ] + targets = [ + self._make_src_surface(8, palette=palette), + self._make_src_surface(16, palette=palette), + self._make_src_surface(16, palette=palette, srcalpha=True), + self._make_src_surface(24, palette=palette), + self._make_src_surface(32, palette=palette), + self._make_src_surface(32, palette=palette, srcalpha=True), + ] + + for surf in targets: + p = palette + if surf.get_bitsize() == 16: + p = [surf.unmap_rgb(surf.map_rgb(c)) for c in p] + surf.set_colorkey(None) + arr = pygame.surfarray.array_colorkey(surf) + self.assertTrue(alltrue(arr == 255)) + + for i in range(1, len(palette)): + surf.set_colorkey(p[i]) + alphas = [255] * len(p) + alphas[i] = 0 + arr = pygame.surfarray.array_colorkey(surf) + for (x, y), j in self.test_points: + self.assertEqual( + arr[x, y], + alphas[j], + ( + "%i != %i, posn: (%i, %i), " + "bitsize: %i" + % (arr[x, y], alphas[j], x, y, surf.get_bitsize()) + ), + ) + + def test_array_red(self): + self._test_array_rgb("red", 0) + + def test_array_green(self): + self._test_array_rgb("green", 1) + + def test_array_blue(self): + self._test_array_rgb("blue", 2) + + def _test_array_rgb(self, operation, mask_posn): + method_name = "array_" + operation + + array_rgb = getattr(pygame.surfarray, method_name) + palette = [ + (0, 0, 0, 255), + (5, 13, 23, 255), + (29, 31, 37, 255), + (131, 157, 167, 255), + (179, 191, 251, 255), + ] + plane = [c[mask_posn] for c in palette] + + targets = [ + self._make_src_surface(24, palette=palette), + self._make_src_surface(32, palette=palette), + self._make_src_surface(32, palette=palette, srcalpha=True), + ] + + for surf in targets: + self.assertFalse(surf.get_locked()) + for (x, y), i in self.test_points: + surf.fill(palette[i]) + arr = array_rgb(surf) + self.assertEqual(arr[x, y], plane[i]) + surf.fill((100, 100, 100, 250)) + self.assertEqual(arr[x, y], plane[i]) + self.assertFalse(surf.get_locked()) + del arr + + def test_blit_array(self): + s = pygame.Surface((10, 10), 0, 24) + a = pygame.surfarray.array3d(s) + pygame.surfarray.blit_array(s, a) + + # target surfaces + targets = [ + self._make_surface(8), + self._make_surface(16), + self._make_surface(16, srcalpha=True), + self._make_surface(24), + self._make_surface(32), + self._make_surface(32, srcalpha=True), + ] + + # source arrays + arrays3d = [] + dtypes = [(8, uint8), (16, uint16), (32, uint32)] + try: + dtypes.append((64, uint64)) + except NameError: + pass + arrays3d = [(self._make_src_array3d(dtype), None) for __, dtype in dtypes] + for bitsize in [8, 16, 24, 32]: + palette = None + if bitsize == 16: + s = pygame.Surface((1, 1), 0, 16) + palette = [s.unmap_rgb(s.map_rgb(c)) for c in self.test_palette] + if self.pixels3d[bitsize]: + surf = self._make_src_surface(bitsize) + arr = pygame.surfarray.pixels3d(surf) + arrays3d.append((arr, palette)) + if self.array3d[bitsize]: + surf = self._make_src_surface(bitsize) + arr = pygame.surfarray.array3d(surf) + arrays3d.append((arr, palette)) + for sz, dtype in dtypes: + arrays3d.append((arr.astype(dtype), palette)) + + # tests on arrays + def do_blit(surf, arr): + pygame.surfarray.blit_array(surf, arr) + + for surf in targets: + bitsize = surf.get_bitsize() + for arr, palette in arrays3d: + surf.fill((0, 0, 0, 0)) + if bitsize == 8: + self.assertRaises(ValueError, do_blit, surf, arr) + else: + pygame.surfarray.blit_array(surf, arr) + self._assert_surface(surf, palette) + + if self.pixels2d[bitsize]: + surf.fill((0, 0, 0, 0)) + s = self._make_src_surface(bitsize, surf.get_flags() & SRCALPHA) + arr = pygame.surfarray.pixels2d(s) + pygame.surfarray.blit_array(surf, arr) + self._assert_surface(surf) + + if self.array2d[bitsize]: + s = self._make_src_surface(bitsize, surf.get_flags() & SRCALPHA) + arr = pygame.surfarray.array2d(s) + for sz, dtype in dtypes: + surf.fill((0, 0, 0, 0)) + if sz >= bitsize: + pygame.surfarray.blit_array(surf, arr.astype(dtype)) + self._assert_surface(surf) + else: + self.assertRaises( + ValueError, do_blit, surf, self._make_array2d(dtype) + ) + + # Check alpha for 2D arrays + surf = self._make_surface(16, srcalpha=True) + arr = zeros(surf.get_size(), uint16) + arr[...] = surf.map_rgb((0, 128, 255, 64)) + color = surf.unmap_rgb(arr[0, 0]) + pygame.surfarray.blit_array(surf, arr) + self.assertEqual(surf.get_at((5, 5)), color) + + surf = self._make_surface(32, srcalpha=True) + arr = zeros(surf.get_size(), uint32) + color = (0, 111, 255, 63) + arr[...] = surf.map_rgb(color) + pygame.surfarray.blit_array(surf, arr) + self.assertEqual(surf.get_at((5, 5)), color) + + # Check shifts + arr3d = self._make_src_array3d(uint8) + + shift_tests = [ + (16, [12, 0, 8, 4], [0xF000, 0xF, 0xF00, 0xF0]), + (24, [16, 0, 8, 0], [0xFF0000, 0xFF, 0xFF00, 0]), + (32, [0, 16, 24, 8], [0xFF, 0xFF0000, 0xFF000000, 0xFF00]), + ] + + for bitsize, shifts, masks in shift_tests: + surf = self._make_surface(bitsize, srcalpha=(shifts[3] != 0)) + palette = None + if bitsize == 16: + palette = [surf.unmap_rgb(surf.map_rgb(c)) for c in self.test_palette] + + self.assertRaises(TypeError, surf.set_shifts, shifts) + self.assertRaises(TypeError, surf.set_masks, masks) + + # Invalid arrays + surf = pygame.Surface((1, 1), 0, 32) + t = "abcd" + self.assertRaises(ValueError, do_blit, surf, t) + + surf_size = self.surf_size + surf = pygame.Surface(surf_size, 0, 32) + arr = zeros([surf_size[0], surf_size[1] + 1, 3], uint32) + self.assertRaises(ValueError, do_blit, surf, arr) + arr = zeros([surf_size[0] + 1, surf_size[1], 3], uint32) + self.assertRaises(ValueError, do_blit, surf, arr) + + surf = pygame.Surface((1, 4), 0, 32) + arr = zeros((4,), uint32) + self.assertRaises(ValueError, do_blit, surf, arr) + arr.shape = (1, 1, 1, 4) + self.assertRaises(ValueError, do_blit, surf, arr) + + # Issue #81: round from float to int + try: + rint + except NameError: + pass + else: + surf = pygame.Surface((10, 10), pygame.SRCALPHA, 32) + w, h = surf.get_size() + length = w * h + for dtype in [float32, float64]: + surf.fill((255, 255, 255, 0)) + farr = arange(0, length, dtype=dtype) + farr.shape = w, h + pygame.surfarray.blit_array(surf, farr) + for x in range(w): + for y in range(h): + self.assertEqual( + surf.get_at_mapped((x, y)), int(rint(farr[x, y])) + ) + + # this test should be removed soon, when the function is deleted + def test_get_arraytype(self): + array_type = pygame.surfarray.get_arraytype() + + self.assertEqual(array_type, "numpy", f"unknown array type {array_type}") + + # this test should be removed soon, when the function is deleted + def test_get_arraytypes(self): + arraytypes = pygame.surfarray.get_arraytypes() + self.assertIn("numpy", arraytypes) + + for atype in arraytypes: + self.assertEqual(atype, "numpy", f"unknown array type {atype}") + + def test_make_surface(self): + # How does one properly test this with 2d arrays. It makes no sense + # since the pixel format is not entirely dependent on element size. + # Just make sure the surface pixel size is at least as large as the + # array element size I guess. + # + for bitsize, dtype in [(8, uint8), (16, uint16), (24, uint32)]: + ## Even this simple assertion fails for 2d arrays. Where's the problem? + ## surf = pygame.surfarray.make_surface(self._make_array2d(dtype)) + ## self.assertGreaterEqual(surf.get_bitsize(), bitsize, + ## "not %i >= %i)" % (surf.get_bitsize(), bitsize)) + ## + surf = pygame.surfarray.make_surface(self._make_src_array3d(dtype)) + self._assert_surface(surf) + + # Issue #81: round from float to int + try: + rint + except NameError: + pass + else: + w = 9 + h = 11 + length = w * h + for dtype in [float32, float64]: + farr = arange(0, length, dtype=dtype) + farr.shape = w, h + surf = pygame.surfarray.make_surface(farr) + for x in range(w): + for y in range(h): + self.assertEqual( + surf.get_at_mapped((x, y)), int(rint(farr[x, y])) + ) + + def test_map_array(self): + arr3d = self._make_src_array3d(uint8) + targets = [ + self._make_surface(8), + self._make_surface(16), + self._make_surface(16, srcalpha=True), + self._make_surface(24), + self._make_surface(32), + self._make_surface(32, srcalpha=True), + ] + palette = self.test_palette + + for surf in targets: + arr2d = pygame.surfarray.map_array(surf, arr3d) + for posn, i in self.test_points: + self.assertEqual( + arr2d[posn], + surf.map_rgb(palette[i]), + "%i != %i, bitsize: %i, flags: %i" + % ( + arr2d[posn], + surf.map_rgb(palette[i]), + surf.get_bitsize(), + surf.get_flags(), + ), + ) + + # Exception checks + self.assertRaises( + ValueError, + pygame.surfarray.map_array, + self._make_surface(32), + self._make_array2d(uint8), + ) + + def test_pixels2d(self): + sources = [ + self._make_surface(8), + self._make_surface(16, srcalpha=True), + self._make_surface(32, srcalpha=True), + ] + + for surf in sources: + self.assertFalse(surf.get_locked()) + arr = pygame.surfarray.pixels2d(surf) + self.assertTrue(surf.get_locked()) + self._fill_array2d(arr, surf) + surf.unlock() + self.assertTrue(surf.get_locked()) + del arr + self.assertFalse(surf.get_locked()) + self.assertEqual(surf.get_locks(), ()) + self._assert_surface(surf) + + # Error checks + self.assertRaises(ValueError, pygame.surfarray.pixels2d, self._make_surface(24)) + + def test_pixels3d(self): + sources = [self._make_surface(24), self._make_surface(32)] + + for surf in sources: + self.assertFalse(surf.get_locked()) + arr = pygame.surfarray.pixels3d(surf) + self.assertTrue(surf.get_locked()) + self._fill_array3d(arr) + surf.unlock() + self.assertTrue(surf.get_locked()) + del arr + self.assertFalse(surf.get_locked()) + self.assertEqual(surf.get_locks(), ()) + self._assert_surface(surf) + + # Alpha check + color = (1, 2, 3, 0) + surf = self._make_surface(32, srcalpha=True) + arr = pygame.surfarray.pixels3d(surf) + arr[0, 0] = color[:3] + self.assertEqual(surf.get_at((0, 0)), color) + + # Error checks + def do_pixels3d(surf): + pygame.surfarray.pixels3d(surf) + + self.assertRaises(ValueError, do_pixels3d, self._make_surface(8)) + self.assertRaises(ValueError, do_pixels3d, self._make_surface(16)) + + def test_pixels_alpha(self): + palette = [ + (0, 0, 0, 0), + (127, 127, 127, 0), + (127, 127, 127, 85), + (127, 127, 127, 170), + (127, 127, 127, 255), + ] + alphas = [0, 45, 86, 99, 180] + + surf = self._make_src_surface(32, srcalpha=True, palette=palette) + + self.assertFalse(surf.get_locked()) + arr = pygame.surfarray.pixels_alpha(surf) + self.assertTrue(surf.get_locked()) + surf.unlock() + self.assertTrue(surf.get_locked()) + + for (x, y), i in self.test_points: + self.assertEqual(arr[x, y], palette[i][3]) + + for (x, y), i in self.test_points: + alpha = alphas[i] + arr[x, y] = alpha + color = (127, 127, 127, alpha) + self.assertEqual(surf.get_at((x, y)), color, "posn: (%i, %i)" % (x, y)) + + del arr + self.assertFalse(surf.get_locked()) + self.assertEqual(surf.get_locks(), ()) + + # Check exceptions. + def do_pixels_alpha(surf): + pygame.surfarray.pixels_alpha(surf) + + targets = [(8, False), (16, False), (16, True), (24, False), (32, False)] + + for bitsize, srcalpha in targets: + self.assertRaises( + ValueError, do_pixels_alpha, self._make_surface(bitsize, srcalpha) + ) + + def test_pixels_red(self): + self._test_pixels_rgb("red", 0) + + def test_pixels_green(self): + self._test_pixels_rgb("green", 1) + + def test_pixels_blue(self): + self._test_pixels_rgb("blue", 2) + + def _test_pixels_rgb(self, operation, mask_posn): + method_name = "pixels_" + operation + + pixels_rgb = getattr(pygame.surfarray, method_name) + palette = [ + (0, 0, 0, 255), + (5, 13, 23, 255), + (29, 31, 37, 255), + (131, 157, 167, 255), + (179, 191, 251, 255), + ] + plane = [c[mask_posn] for c in palette] + + surf24 = self._make_src_surface(24, srcalpha=False, palette=palette) + surf32 = self._make_src_surface(32, srcalpha=False, palette=palette) + surf32a = self._make_src_surface(32, srcalpha=True, palette=palette) + + for surf in [surf24, surf32, surf32a]: + self.assertFalse(surf.get_locked()) + arr = pixels_rgb(surf) + self.assertTrue(surf.get_locked()) + surf.unlock() + self.assertTrue(surf.get_locked()) + + for (x, y), i in self.test_points: + self.assertEqual(arr[x, y], plane[i]) + + del arr + self.assertFalse(surf.get_locked()) + self.assertEqual(surf.get_locks(), ()) + + # Check exceptions. + targets = [(8, False), (16, False), (16, True)] + + for bitsize, srcalpha in targets: + self.assertRaises( + ValueError, pixels_rgb, self._make_surface(bitsize, srcalpha) + ) + + def test_use_arraytype(self): + def do_use_arraytype(atype): + pygame.surfarray.use_arraytype(atype) + + pygame.surfarray.use_arraytype("numpy") + self.assertEqual(pygame.surfarray.get_arraytype(), "numpy") + self.assertRaises(ValueError, do_use_arraytype, "not an option") + + def test_surf_lock(self): + sf = pygame.Surface((5, 5), 0, 32) + for atype in pygame.surfarray.get_arraytypes(): + pygame.surfarray.use_arraytype(atype) + + ar = pygame.surfarray.pixels2d(sf) + self.assertTrue(sf.get_locked()) + + sf.unlock() + self.assertTrue(sf.get_locked()) + + del ar + self.assertFalse(sf.get_locked()) + self.assertEqual(sf.get_locks(), ()) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/surflock_test.py b/laplas/abstract_map/pygame/tests/surflock_test.py new file mode 100644 index 0000000..19f354b --- /dev/null +++ b/laplas/abstract_map/pygame/tests/surflock_test.py @@ -0,0 +1,144 @@ +import unittest +import sys +import platform + +import pygame + +IS_PYPY = "PyPy" == platform.python_implementation() + + +@unittest.skipIf(IS_PYPY, "pypy skip known failure") # TODO +class SurfaceLockTest(unittest.TestCase): + def test_lock(self): + sf = pygame.Surface((5, 5)) + + sf.lock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (sf,)) + + sf.lock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (sf, sf)) + + sf.unlock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (sf,)) + + sf.unlock() + self.assertEqual(sf.get_locked(), False) + self.assertEqual(sf.get_locks(), ()) + + def test_subsurface_lock(self): + sf = pygame.Surface((5, 5)) + subsf = sf.subsurface((1, 1, 2, 2)) + sf2 = pygame.Surface((5, 5)) + + # Simple blits, nothing should happen here. + sf2.blit(subsf, (0, 0)) + sf2.blit(sf, (0, 0)) + + # Test blitting on self: + self.assertRaises(pygame.error, sf.blit, subsf, (0, 0)) + # self.assertRaises(pygame.error, subsf.blit, sf, (0, 0)) + # ^ Fails although it should not in my opinion. If I cannot + # blit the subsurface to the surface, it should not be allowed + # the other way around as well. + + # Test additional locks. + sf.lock() + sf2.blit(subsf, (0, 0)) + self.assertRaises(pygame.error, sf2.blit, sf, (0, 0)) + + subsf.lock() + self.assertRaises(pygame.error, sf2.blit, subsf, (0, 0)) + self.assertRaises(pygame.error, sf2.blit, sf, (0, 0)) + + # sf and subsf are now explicitly locked. Unlock sf, so we can + # (assume) to blit it. + # It will fail though as the subsurface still has a lock around, + # which is okay and correct behaviour. + sf.unlock() + self.assertRaises(pygame.error, sf2.blit, subsf, (0, 0)) + self.assertRaises(pygame.error, sf2.blit, sf, (0, 0)) + + # Run a second unlock on the surface. This should ideally have + # no effect as the subsurface is the locking reason! + sf.unlock() + self.assertRaises(pygame.error, sf2.blit, sf, (0, 0)) + self.assertRaises(pygame.error, sf2.blit, subsf, (0, 0)) + subsf.unlock() + + sf.lock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (sf,)) + self.assertEqual(subsf.get_locked(), False) + self.assertEqual(subsf.get_locks(), ()) + + subsf.lock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (sf, subsf)) + self.assertEqual(subsf.get_locked(), True) + self.assertEqual(subsf.get_locks(), (subsf,)) + + sf.unlock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (subsf,)) + self.assertEqual(subsf.get_locked(), True) + self.assertEqual(subsf.get_locks(), (subsf,)) + + subsf.unlock() + self.assertEqual(sf.get_locked(), False) + self.assertEqual(sf.get_locks(), ()) + self.assertEqual(subsf.get_locked(), False) + self.assertEqual(subsf.get_locks(), ()) + + subsf.lock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (subsf,)) + self.assertEqual(subsf.get_locked(), True) + self.assertEqual(subsf.get_locks(), (subsf,)) + + subsf.lock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (subsf, subsf)) + self.assertEqual(subsf.get_locked(), True) + self.assertEqual(subsf.get_locks(), (subsf, subsf)) + + def test_pxarray_ref(self): + sf = pygame.Surface((5, 5)) + ar = pygame.PixelArray(sf) + ar2 = pygame.PixelArray(sf) + + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (ar, ar2)) + + del ar + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (ar2,)) + + ar = ar2[:] + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (ar2,)) + + del ar + self.assertEqual(sf.get_locked(), True) + self.assertEqual(len(sf.get_locks()), 1) + + def test_buffer(self): + sf = pygame.Surface((5, 5)) + buf = sf.get_buffer() + + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (buf,)) + + sf.unlock() + self.assertEqual(sf.get_locked(), True) + self.assertEqual(sf.get_locks(), (buf,)) + + del buf + self.assertEqual(sf.get_locked(), False) + self.assertEqual(sf.get_locks(), ()) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/sysfont_test.py b/laplas/abstract_map/pygame/tests/sysfont_test.py new file mode 100644 index 0000000..0ae380a --- /dev/null +++ b/laplas/abstract_map/pygame/tests/sysfont_test.py @@ -0,0 +1,51 @@ +import unittest +import platform + + +class SysfontModuleTest(unittest.TestCase): + def test_create_aliases(self): + import pygame.sysfont + + pygame.sysfont.initsysfonts() + pygame.sysfont.create_aliases() + self.assertTrue(len(pygame.sysfont.Sysalias) > 0) + + def test_initsysfonts(self): + import pygame.sysfont + + pygame.sysfont.initsysfonts() + self.assertTrue(len(pygame.sysfont.get_fonts()) > 0) + + @unittest.skipIf("Darwin" not in platform.platform(), "Not mac we skip.") + def test_initsysfonts_darwin(self): + import pygame.sysfont + + self.assertTrue(len(pygame.sysfont.get_fonts()) > 10) + + def test_sysfont(self): + import pygame.font + + pygame.font.init() + arial = pygame.font.SysFont("Arial", 40) + self.assertTrue(isinstance(arial, pygame.font.Font)) + + @unittest.skipIf( + ("Darwin" in platform.platform() or "Windows" in platform.platform()), + "Not unix we skip.", + ) + def test_initsysfonts_unix(self): + import pygame.sysfont + + self.assertTrue(len(pygame.sysfont.get_fonts()) > 0) + + @unittest.skipIf("Windows" not in platform.platform(), "Not windows we skip.") + def test_initsysfonts_win32(self): + import pygame.sysfont + + self.assertTrue(len(pygame.sysfont.get_fonts()) > 10) + + +############################################################################### + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/test_utils/__init__.py b/laplas/abstract_map/pygame/tests/test_utils/__init__.py new file mode 100644 index 0000000..a4994f2 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/__init__.py @@ -0,0 +1,201 @@ +import os +import pygame +import sys +import tempfile +import time + +is_pygame_pkg = __name__.startswith("pygame.tests.") + +############################################################################### + + +def tostring(row): + """Convert row of bytes to string. Expects `row` to be an + ``array``. + """ + return row.tobytes() + + +def geterror(): + return sys.exc_info()[1] + + +############################################################################### + +this_dir = os.path.dirname(os.path.abspath(__file__)) +trunk_dir = os.path.split(os.path.split(this_dir)[0])[0] +if is_pygame_pkg: + test_module = "tests" +else: + test_module = "test" + + +def trunk_relative_path(relative): + return os.path.normpath(os.path.join(trunk_dir, relative)) + + +def fixture_path(path): + return trunk_relative_path(os.path.join(test_module, "fixtures", path)) + + +def example_path(path): + return trunk_relative_path(os.path.join("examples", path)) + + +sys.path.insert(0, trunk_relative_path(".")) + + +################################## TEMP FILES ################################# + + +def get_tmp_dir(): + return tempfile.mkdtemp() + + +############################################################################### + + +def question(q): + return input(f"\n{q.rstrip(' ')} (y/n): ").lower().strip() == "y" + + +def prompt(p): + return input(f"\n{p.rstrip(' ')} (press enter to continue): ") + + +#################################### HELPERS ################################## + + +def rgba_between(value, minimum=0, maximum=255): + if value < minimum: + return minimum + elif value > maximum: + return maximum + else: + return value + + +def combinations(seqs): + """ + + Recipe 496807 from ActiveState Python CookBook + + Non recursive technique for getting all possible combinations of a sequence + of sequences. + + """ + + r = [[]] + for x in seqs: + r = [i + [y] for y in x for i in r] + return r + + +def gradient(width, height): + """ + + Yields a pt and corresponding RGBA tuple, for every (width, height) combo. + Useful for generating gradients. + + Actual gradient may be changed, no tests rely on specific values. + + Used in transform.rotate lossless tests to generate a fixture. + + """ + + for l in range(width): + for t in range(height): + yield (l, t), tuple(map(rgba_between, (l, t, l, l + t))) + + +def rect_area_pts(rect): + for l in range(rect.left, rect.right): + for t in range(rect.top, rect.bottom): + yield l, t + + +def rect_perimeter_pts(rect): + """ + + Returns pts ((L, T) tuples) encompassing the perimeter of a rect. + + The order is clockwise: + + topleft to topright + topright to bottomright + bottomright to bottomleft + bottomleft to topleft + + Duplicate pts are not returned + + """ + clock_wise_from_top_left = ( + [(l, rect.top) for l in range(rect.left, rect.right)], + [(rect.right - 1, t) for t in range(rect.top + 1, rect.bottom)], + [(l, rect.bottom - 1) for l in range(rect.right - 2, rect.left - 1, -1)], + [(rect.left, t) for t in range(rect.bottom - 2, rect.top, -1)], + ) + + for line in clock_wise_from_top_left: + yield from line + + +def rect_outer_bounds(rect): + """ + + Returns topleft outerbound if possible and then the other pts, that are + "exclusive" bounds of the rect + + ?------O + |RECT| ?|0)uterbound + |----| + O O + + """ + return ([(rect.left - 1, rect.top)] if rect.left else []) + [ + rect.topright, + rect.bottomleft, + rect.bottomright, + ] + + +def import_submodule(module): + m = __import__(module) + for n in module.split(".")[1:]: + m = getattr(m, n) + return m + + +class SurfaceSubclass(pygame.Surface): + """A subclassed Surface to test inheritance.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.test_attribute = True + + +def test(): + """ + + Lightweight test for helpers + + """ + + r = pygame.Rect(0, 0, 10, 10) + assert rect_outer_bounds(r) == [(10, 0), (0, 10), (10, 10)] # tr # bl # br + + assert len(list(rect_area_pts(r))) == 100 + + r = pygame.Rect(0, 0, 3, 3) + assert list(rect_perimeter_pts(r)) == [ + (0, 0), + (1, 0), + (2, 0), # tl -> tr + (2, 1), + (2, 2), # tr -> br + (1, 2), + (0, 2), # br -> bl + (0, 1), # bl -> tl + ] + + print("Tests: OK") diff --git a/laplas/abstract_map/pygame/tests/test_utils/arrinter.py b/laplas/abstract_map/pygame/tests/test_utils/arrinter.py new file mode 100644 index 0000000..626913c --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/arrinter.py @@ -0,0 +1,438 @@ +import sys +import ctypes +from ctypes import * +import unittest + +__all__ = [ + "PAI_CONTIGUOUS", + "PAI_FORTRAN", + "PAI_ALIGNED", + "PAI_NOTSWAPPED", + "PAI_WRITEABLE", + "PAI_ARR_HAS_DESCR", + "ArrayInterface", +] + +if sizeof(c_uint) == sizeof(c_void_p): + c_size_t = c_uint + c_ssize_t = c_int +elif sizeof(c_ulong) == sizeof(c_void_p): + c_size_t = c_ulong + c_ssize_t = c_long +elif sizeof(c_ulonglong) == sizeof(c_void_p): + c_size_t = c_ulonglong + c_ssize_t = c_longlong + + +SIZEOF_VOID_P = sizeof(c_void_p) +if SIZEOF_VOID_P <= sizeof(c_int): + Py_intptr_t = c_int +elif SIZEOF_VOID_P <= sizeof(c_long): + Py_intptr_t = c_long +elif "c_longlong" in globals() and SIZEOF_VOID_P <= sizeof(c_longlong): + Py_intptr_t = c_longlong +else: + raise RuntimeError("Unrecognized pointer size %i" % (SIZEOF_VOID_P,)) + + +class PyArrayInterface(Structure): + _fields_ = [ + ("two", c_int), + ("nd", c_int), + ("typekind", c_char), + ("itemsize", c_int), + ("flags", c_int), + ("shape", POINTER(Py_intptr_t)), + ("strides", POINTER(Py_intptr_t)), + ("data", c_void_p), + ("descr", py_object), + ] + + +PAI_Ptr = POINTER(PyArrayInterface) + +try: + PyCObject_AsVoidPtr = pythonapi.PyCObject_AsVoidPtr +except AttributeError: + + def PyCObject_AsVoidPtr(o): + raise TypeError("Not available") + +else: + PyCObject_AsVoidPtr.restype = c_void_p + PyCObject_AsVoidPtr.argtypes = [py_object] + PyCObject_GetDesc = pythonapi.PyCObject_GetDesc + PyCObject_GetDesc.restype = c_void_p + PyCObject_GetDesc.argtypes = [py_object] + +try: + PyCapsule_IsValid = pythonapi.PyCapsule_IsValid +except AttributeError: + + def PyCapsule_IsValid(capsule, name): + return 0 + +else: + PyCapsule_IsValid.restype = c_int + PyCapsule_IsValid.argtypes = [py_object, c_char_p] + PyCapsule_GetPointer = pythonapi.PyCapsule_GetPointer + PyCapsule_GetPointer.restype = c_void_p + PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + PyCapsule_GetContext = pythonapi.PyCapsule_GetContext + PyCapsule_GetContext.restype = c_void_p + PyCapsule_GetContext.argtypes = [py_object] + +PyCapsule_Destructor = CFUNCTYPE(None, py_object) +PyCapsule_New = pythonapi.PyCapsule_New +PyCapsule_New.restype = py_object +PyCapsule_New.argtypes = [c_void_p, c_char_p, POINTER(PyCapsule_Destructor)] + + +def capsule_new(p): + return PyCapsule_New(addressof(p), None, None) + + +PAI_CONTIGUOUS = 0x01 +PAI_FORTRAN = 0x02 +PAI_ALIGNED = 0x100 +PAI_NOTSWAPPED = 0x200 +PAI_WRITEABLE = 0x400 +PAI_ARR_HAS_DESCR = 0x800 + + +class ArrayInterface: + def __init__(self, arr): + try: + self._cobj = arr.__array_struct__ + except AttributeError: + raise TypeError("The array object lacks an array structure") + if not self._cobj: + raise TypeError("The array object has a NULL array structure value") + try: + vp = PyCObject_AsVoidPtr(self._cobj) + except TypeError: + if PyCapsule_IsValid(self._cobj, None): + vp = PyCapsule_GetPointer(self._cobj, None) + else: + raise TypeError("The array object has an invalid array structure") + self.desc = PyCapsule_GetContext(self._cobj) + else: + self.desc = PyCObject_GetDesc(self._cobj) + self._inter = cast(vp, PAI_Ptr)[0] + + def __getattr__(self, name): + if name == "typekind": + return self._inter.typekind.decode("latin-1") + return getattr(self._inter, name) + + def __str__(self): + if isinstance(self.desc, tuple): + ver = self.desc[0] + else: + ver = "N/A" + return ( + "nd: %i\n" + "typekind: %s\n" + "itemsize: %i\n" + "flags: %s\n" + "shape: %s\n" + "strides: %s\n" + "ver: %s\n" + % ( + self.nd, + self.typekind, + self.itemsize, + format_flags(self.flags), + format_shape(self.nd, self.shape), + format_strides(self.nd, self.strides), + ver, + ) + ) + + +def format_flags(flags): + names = [] + for flag, name in [ + (PAI_CONTIGUOUS, "CONTIGUOUS"), + (PAI_FORTRAN, "FORTRAN"), + (PAI_ALIGNED, "ALIGNED"), + (PAI_NOTSWAPPED, "NOTSWAPPED"), + (PAI_WRITEABLE, "WRITEABLE"), + (PAI_ARR_HAS_DESCR, "ARR_HAS_DESCR"), + ]: + if flag & flags: + names.append(name) + return ", ".join(names) + + +def format_shape(nd, shape): + return ", ".join([str(shape[i]) for i in range(nd)]) + + +def format_strides(nd, strides): + return ", ".join([str(strides[i]) for i in range(nd)]) + + +class Exporter: + def __init__( + self, shape, typekind=None, itemsize=None, strides=None, descr=None, flags=None + ): + if typekind is None: + typekind = "u" + if itemsize is None: + itemsize = 1 + if flags is None: + flags = PAI_WRITEABLE | PAI_ALIGNED | PAI_NOTSWAPPED + if descr is not None: + flags |= PAI_ARR_HAS_DESCR + if len(typekind) != 1: + raise ValueError("Argument 'typekind' must be length 1 string") + nd = len(shape) + self.typekind = typekind + self.itemsize = itemsize + self.nd = nd + self.shape = tuple(shape) + self._shape = (c_ssize_t * self.nd)(*self.shape) + if strides is None: + self._strides = (c_ssize_t * self.nd)() + self._strides[self.nd - 1] = self.itemsize + for i in range(self.nd - 1, 0, -1): + self._strides[i - 1] = self.shape[i] * self._strides[i] + strides = tuple(self._strides) + self.strides = strides + elif len(strides) == nd: + self.strides = tuple(strides) + self._strides = (c_ssize_t * self.nd)(*self.strides) + else: + raise ValueError("Mismatch in length of strides and shape") + self.descr = descr + if self.is_contiguous("C"): + flags |= PAI_CONTIGUOUS + if self.is_contiguous("F"): + flags |= PAI_FORTRAN + self.flags = flags + sz = max(shape[i] * strides[i] for i in range(nd)) + self._data = (c_ubyte * sz)() + self.data = addressof(self._data) + self._inter = PyArrayInterface( + 2, + nd, + typekind.encode("latin_1"), + itemsize, + flags, + self._shape, + self._strides, + self.data, + descr, + ) + self.len = itemsize + for i in range(nd): + self.len *= self.shape[i] + + __array_struct__ = property(lambda self: capsule_new(self._inter)) + + def is_contiguous(self, fortran): + if fortran in "CA": + if self.strides[-1] == self.itemsize: + for i in range(self.nd - 1, 0, -1): + if self.strides[i - 1] != self.shape[i] * self.strides[i]: + break + else: + return True + if fortran in "FA": + if self.strides[0] == self.itemsize: + for i in range(0, self.nd - 1): + if self.strides[i + 1] != self.shape[i] * self.strides[i]: + break + else: + return True + return False + + +class Array(Exporter): + _ctypes = { + ("u", 1): c_uint8, + ("u", 2): c_uint16, + ("u", 4): c_uint32, + ("u", 8): c_uint64, + ("i", 1): c_int8, + ("i", 2): c_int16, + ("i", 4): c_int32, + ("i", 8): c_int64, + } + + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) + try: + if self.flags & PAI_NOTSWAPPED: + ct = self._ctypes[self.typekind, self.itemsize] + elif c_int.__ctype_le__ is c_int: + ct = self._ctypes[self.typekind, self.itemsize].__ctype_be__ + else: + ct = self._ctypes[self.typekind, self.itemsize].__ctype_le__ + except KeyError: + ct = c_uint8 * self.itemsize + self._ctype = ct + self._ctype_p = POINTER(ct) + + def __getitem__(self, key): + return cast(self._addr_at(key), self._ctype_p)[0] + + def __setitem__(self, key, value): + cast(self._addr_at(key), self._ctype_p)[0] = value + + def _addr_at(self, key): + if not isinstance(key, tuple): + key = (key,) + if len(key) != self.nd: + raise ValueError("wrong number of indexes") + for i in range(self.nd): + if not (0 <= key[i] < self.shape[i]): + raise IndexError(f"index {i} out of range") + return self.data + sum(i * s for i, s in zip(key, self.strides)) + + +class ExporterTest(unittest.TestCase): + def test_strides(self): + self.check_args(0, (10,), "u", (2,), 20, 20, 2) + self.check_args(0, (5, 3), "u", (6, 2), 30, 30, 2) + self.check_args(0, (7, 3, 5), "u", (30, 10, 2), 210, 210, 2) + self.check_args(0, (13, 5, 11, 3), "u", (330, 66, 6, 2), 4290, 4290, 2) + self.check_args(3, (7, 3, 5), "i", (2, 14, 42), 210, 210, 2) + self.check_args(3, (7, 3, 5), "x", (2, 16, 48), 210, 240, 2) + self.check_args(3, (13, 5, 11, 3), "%", (440, 88, 8, 2), 4290, 5720, 2) + self.check_args(3, (7, 5), "-", (15, 3), 105, 105, 3) + self.check_args(3, (7, 5), "*", (3, 21), 105, 105, 3) + self.check_args(3, (7, 5), " ", (3, 24), 105, 120, 3) + + def test_is_contiguous(self): + a = Exporter((10,), itemsize=2) + self.assertTrue(a.is_contiguous("C")) + self.assertTrue(a.is_contiguous("F")) + self.assertTrue(a.is_contiguous("A")) + a = Exporter((10, 4), itemsize=2) + self.assertTrue(a.is_contiguous("C")) + self.assertTrue(a.is_contiguous("A")) + self.assertFalse(a.is_contiguous("F")) + a = Exporter((13, 5, 11, 3), itemsize=2, strides=(330, 66, 6, 2)) + self.assertTrue(a.is_contiguous("C")) + self.assertTrue(a.is_contiguous("A")) + self.assertFalse(a.is_contiguous("F")) + a = Exporter((10, 4), itemsize=2, strides=(2, 20)) + self.assertTrue(a.is_contiguous("F")) + self.assertTrue(a.is_contiguous("A")) + self.assertFalse(a.is_contiguous("C")) + a = Exporter((13, 5, 11, 3), itemsize=2, strides=(2, 26, 130, 1430)) + self.assertTrue(a.is_contiguous("F")) + self.assertTrue(a.is_contiguous("A")) + self.assertFalse(a.is_contiguous("C")) + a = Exporter((2, 11, 6, 4), itemsize=2, strides=(576, 48, 8, 2)) + self.assertFalse(a.is_contiguous("A")) + a = Exporter((2, 11, 6, 4), itemsize=2, strides=(2, 4, 48, 288)) + self.assertFalse(a.is_contiguous("A")) + a = Exporter((3, 2, 2), itemsize=2, strides=(16, 8, 4)) + self.assertFalse(a.is_contiguous("A")) + a = Exporter((3, 2, 2), itemsize=2, strides=(4, 12, 24)) + self.assertFalse(a.is_contiguous("A")) + + def check_args( + self, call_flags, shape, typekind, strides, length, bufsize, itemsize, offset=0 + ): + if call_flags & 1: + typekind_arg = typekind + else: + typekind_arg = None + if call_flags & 2: + strides_arg = strides + else: + strides_arg = None + a = Exporter(shape, itemsize=itemsize, strides=strides_arg) + self.assertEqual(sizeof(a._data), bufsize) + self.assertEqual(a.data, ctypes.addressof(a._data) + offset) + m = ArrayInterface(a) + self.assertEqual(m.data, a.data) + self.assertEqual(m.itemsize, itemsize) + self.assertEqual(tuple(m.shape[0 : m.nd]), shape) + self.assertEqual(tuple(m.strides[0 : m.nd]), strides) + + +class ArrayTest(unittest.TestCase): + def __init__(self, *args, **kwds): + unittest.TestCase.__init__(self, *args, **kwds) + self.a = Array((20, 15), "i", 4) + + def setUp(self): + # Every test starts with a zeroed array. + memset(self.a.data, 0, sizeof(self.a._data)) + + def test__addr_at(self): + a = self.a + self.assertEqual(a._addr_at((0, 0)), a.data) + self.assertEqual(a._addr_at((0, 1)), a.data + 4) + self.assertEqual(a._addr_at((1, 0)), a.data + 60) + self.assertEqual(a._addr_at((1, 1)), a.data + 64) + + def test_indices(self): + a = self.a + self.assertEqual(a[0, 0], 0) + self.assertEqual(a[19, 0], 0) + self.assertEqual(a[0, 14], 0) + self.assertEqual(a[19, 14], 0) + self.assertEqual(a[5, 8], 0) + a[0, 0] = 12 + a[5, 8] = 99 + self.assertEqual(a[0, 0], 12) + self.assertEqual(a[5, 8], 99) + self.assertRaises(IndexError, a.__getitem__, (-1, 0)) + self.assertRaises(IndexError, a.__getitem__, (0, -1)) + self.assertRaises(IndexError, a.__getitem__, (20, 0)) + self.assertRaises(IndexError, a.__getitem__, (0, 15)) + self.assertRaises(ValueError, a.__getitem__, 0) + self.assertRaises(ValueError, a.__getitem__, (0, 0, 0)) + a = Array((3,), "i", 4) + a[1] = 333 + self.assertEqual(a[1], 333) + + def test_typekind(self): + a = Array((1,), "i", 4) + self.assertTrue(a._ctype is c_int32) + self.assertTrue(a._ctype_p is POINTER(c_int32)) + a = Array((1,), "u", 4) + self.assertTrue(a._ctype is c_uint32) + self.assertTrue(a._ctype_p is POINTER(c_uint32)) + a = Array((1,), "f", 4) # float types unsupported: size system dependent + ct = a._ctype + self.assertTrue(issubclass(ct, ctypes.Array)) + self.assertEqual(sizeof(ct), 4) + + def test_itemsize(self): + for size in [1, 2, 4, 8]: + a = Array((1,), "i", size) + ct = a._ctype + self.assertTrue(issubclass(ct, ctypes._SimpleCData)) + self.assertEqual(sizeof(ct), size) + + def test_oddball_itemsize(self): + for size in [3, 5, 6, 7, 9]: + a = Array((1,), "i", size) + ct = a._ctype + self.assertTrue(issubclass(ct, ctypes.Array)) + self.assertEqual(sizeof(ct), size) + + def test_byteswapped(self): + a = Array((1,), "u", 4, flags=(PAI_ALIGNED | PAI_WRITEABLE)) + ct = a._ctype + self.assertTrue(ct is not c_uint32) + if sys.byteorder == "little": + self.assertTrue(ct is c_uint32.__ctype_be__) + else: + self.assertTrue(ct is c_uint32.__ctype_le__) + i = 0xA0B0C0D + n = c_uint32(i) + a[0] = i + self.assertEqual(a[0], i) + self.assertEqual(a._data[0:4], cast(addressof(n), POINTER(c_uint8))[3:-1:-1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/test_utils/async_sub.py b/laplas/abstract_map/pygame/tests/test_utils/async_sub.py new file mode 100644 index 0000000..560d377 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/async_sub.py @@ -0,0 +1,301 @@ +################################################################################ +""" + +Modification of http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440554 + +""" + +#################################### IMPORTS ################################### + +import os +import platform +import subprocess +import errno +import time +import sys +import unittest +import tempfile + + +def geterror(): + return sys.exc_info()[1] + + +null_byte = "\x00".encode("ascii") + +if platform.system() == "Windows": + + def encode(s): + return s.encode("ascii") + + def decode(b): + return b.decode("ascii") + + try: + import ctypes + from ctypes.wintypes import DWORD + + kernel32 = ctypes.windll.kernel32 + TerminateProcess = ctypes.windll.kernel32.TerminateProcess + + def WriteFile(handle, data, ol=None): + c_written = DWORD() + success = ctypes.windll.kernel32.WriteFile( + handle, + ctypes.create_string_buffer(encode(data)), + len(data), + ctypes.byref(c_written), + ol, + ) + return ctypes.windll.kernel32.GetLastError(), c_written.value + + def ReadFile(handle, desired_bytes, ol=None): + c_read = DWORD() + buffer = ctypes.create_string_buffer(desired_bytes + 1) + success = ctypes.windll.kernel32.ReadFile( + handle, buffer, desired_bytes, ctypes.byref(c_read), ol + ) + buffer[c_read.value] = null_byte + return ctypes.windll.kernel32.GetLastError(), decode(buffer.value) + + def PeekNamedPipe(handle, desired_bytes): + c_avail = DWORD() + c_message = DWORD() + if desired_bytes > 0: + c_read = DWORD() + buffer = ctypes.create_string_buffer(desired_bytes + 1) + success = ctypes.windll.kernel32.PeekNamedPipe( + handle, + buffer, + desired_bytes, + ctypes.byref(c_read), + ctypes.byref(c_avail), + ctypes.byref(c_message), + ) + buffer[c_read.value] = null_byte + return decode(buffer.value), c_avail.value, c_message.value + else: + success = ctypes.windll.kernel32.PeekNamedPipe( + handle, + None, + desired_bytes, + None, + ctypes.byref(c_avail), + ctypes.byref(c_message), + ) + return "", c_avail.value, c_message.value + + except ImportError: + from win32file import ReadFile, WriteFile + from win32pipe import PeekNamedPipe + from win32api import TerminateProcess + import msvcrt + +else: + from signal import SIGINT, SIGTERM, SIGKILL + import select + import fcntl + +################################### CONSTANTS ################################## + +PIPE = subprocess.PIPE + +################################################################################ + + +class Popen(subprocess.Popen): + def recv(self, maxsize=None): + return self._recv("stdout", maxsize) + + def recv_err(self, maxsize=None): + return self._recv("stderr", maxsize) + + def send_recv(self, input="", maxsize=None): + return self.send(input), self.recv(maxsize), self.recv_err(maxsize) + + def read_async(self, wait=0.1, e=1, tr=5, stderr=0): + if tr < 1: + tr = 1 + x = time.time() + wait + y = [] + r = "" + pr = self.recv + if stderr: + pr = self.recv_err + while time.time() < x or r: + r = pr() + if r is None: + if e: + raise Exception("Other end disconnected!") + else: + break + elif r: + y.append(r) + else: + time.sleep(max((x - time.time()) / tr, 0)) + return "".join(y) + + def send_all(self, data): + while len(data): + sent = self.send(data) + if sent is None: + raise Exception("Other end disconnected!") + data = memoryview(data, sent) + + def get_conn_maxsize(self, which, maxsize): + if maxsize is None: + maxsize = 1024 + elif maxsize < 1: + maxsize = 1 + return getattr(self, which), maxsize + + def _close(self, which): + getattr(self, which).close() + setattr(self, which, None) + + if platform.system() == "Windows": + + def kill(self): + # Recipes + # http://me.in-berlin.de/doc/python/faq/windows.html#how-do-i-emulate-os-kill-in-windows + # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/347462 + + """kill function for Win32""" + TerminateProcess(int(self._handle), 0) # returns None + + def send(self, input): + if not self.stdin: + return None + + try: + x = msvcrt.get_osfhandle(self.stdin.fileno()) + (errCode, written) = WriteFile(x, input) + except ValueError: + return self._close("stdin") + except (subprocess.pywintypes.error, Exception): + if geterror()[0] in (109, errno.ESHUTDOWN): + return self._close("stdin") + raise + + return written + + def _recv(self, which, maxsize): + conn, maxsize = self.get_conn_maxsize(which, maxsize) + if conn is None: + return None + + try: + x = msvcrt.get_osfhandle(conn.fileno()) + (read, nAvail, nMessage) = PeekNamedPipe(x, 0) + if maxsize < nAvail: + nAvail = maxsize + if nAvail > 0: + (errCode, read) = ReadFile(x, nAvail, None) + except ValueError: + return self._close(which) + except (subprocess.pywintypes.error, Exception): + if geterror()[0] in (109, errno.ESHUTDOWN): + return self._close(which) + raise + + if self.universal_newlines: + # Translate newlines. For Python 3.x assume read is text. + # If bytes then another solution is needed. + read = read.replace("\r\n", "\n").replace("\r", "\n") + return read + + else: + + def kill(self): + for i, sig in enumerate([SIGTERM, SIGKILL] * 2): + if i % 2 == 0: + os.kill(self.pid, sig) + time.sleep((i * (i % 2) / 5.0) + 0.01) + + killed_pid, stat = os.waitpid(self.pid, os.WNOHANG) + if killed_pid != 0: + return + + def send(self, input): + if not self.stdin: + return None + + if not select.select([], [self.stdin], [], 0)[1]: + return 0 + + try: + written = os.write(self.stdin.fileno(), input) + except OSError: + if geterror()[0] == errno.EPIPE: # broken pipe + return self._close("stdin") + raise + + return written + + def _recv(self, which, maxsize): + conn, maxsize = self.get_conn_maxsize(which, maxsize) + if conn is None: + return None + + if not select.select([conn], [], [], 0)[0]: + return "" + + r = conn.read(maxsize) + if not r: + return self._close(which) + + if self.universal_newlines: + r = r.replace("\r\n", "\n").replace("\r", "\n") + return r + + +################################################################################ + + +def proc_in_time_or_kill(cmd, time_out, wd=None, env=None): + proc = Popen( + cmd, + cwd=wd, + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=1, + ) + + ret_code = None + response = [] + + t = time.time() + while ret_code is None and ((time.time() - t) < time_out): + ret_code = proc.poll() + response += [proc.read_async(wait=0.1, e=0)] + + if ret_code is None: + ret_code = f'"Process timed out (time_out = {time_out} secs) ' + try: + proc.kill() + ret_code += 'and was successfully terminated"' + except Exception: + ret_code += f'and termination failed (exception: {geterror()})"' + + return ret_code, "".join(response) + + +################################################################################ + + +class AsyncTest(unittest.TestCase): + def test_proc_in_time_or_kill(self): + ret_code, response = proc_in_time_or_kill( + [sys.executable, "-c", "while True: pass"], time_out=1 + ) + + self.assertIn("rocess timed out", ret_code) + self.assertIn("successfully terminated", ret_code) + + +################################################################################ + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/test_utils/buftools.py b/laplas/abstract_map/pygame/tests/test_utils/buftools.py new file mode 100644 index 0000000..de1f316 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/buftools.py @@ -0,0 +1,607 @@ +"""Module pygame.tests.test_utils.array + +Export the Exporter and Importer classes. + +Class Exporter has configurable shape and strides. Exporter objects +provide a convenient target for unit tests on Pygame objects and functions +that import a new buffer interface. + +Class Importer imports a buffer interface with the given PyBUF_* flags. +It returns NULL Py_buffer fields as None. The shape, strides, and suboffsets +arrays are returned as tuples of ints. All Py_buffer field properties are +read-only. This class is useful in comparing exported buffer interfaces +with the actual request. The simular Python builtin memoryview currently +does not support configurable PyBUF_* flags. + +This module contains its own unit tests. When Pygame is installed, these tests +can be run with the following command line statement: + +python -m pygame.tests.test_utils.array + +""" + +import pygame + +if not pygame.HAVE_NEWBUF: + emsg = "This Pygame build does not support the new buffer protocol" + raise ImportError(emsg) +import pygame.newbuffer +from pygame.newbuffer import ( + PyBUF_SIMPLE, + PyBUF_FORMAT, + PyBUF_ND, + PyBUF_WRITABLE, + PyBUF_STRIDES, + PyBUF_C_CONTIGUOUS, + PyBUF_F_CONTIGUOUS, + PyBUF_ANY_CONTIGUOUS, + PyBUF_INDIRECT, + PyBUF_STRIDED, + PyBUF_STRIDED_RO, + PyBUF_RECORDS, + PyBUF_RECORDS_RO, + PyBUF_FULL, + PyBUF_FULL_RO, + PyBUF_CONTIG, + PyBUF_CONTIG_RO, +) + +import unittest +import ctypes +import operator +from functools import reduce + +__all__ = ["Exporter", "Importer"] + +try: + ctypes.c_ssize_t +except AttributeError: + void_p_sz = ctypes.sizeof(ctypes.c_void_p) + if ctypes.sizeof(ctypes.c_short) == void_p_sz: + ctypes.c_ssize_t = ctypes.c_short + elif ctypes.sizeof(ctypes.c_int) == void_p_sz: + ctypes.c_ssize_t = ctypes.c_int + elif ctypes.sizeof(ctypes.c_long) == void_p_sz: + ctypes.c_ssize_t = ctypes.c_long + elif ctypes.sizeof(ctypes.c_longlong) == void_p_sz: + ctypes.c_ssize_t = ctypes.c_longlong + else: + raise RuntimeError("Cannot set c_ssize_t: sizeof(void *) is %i" % void_p_sz) + + +def _prop_get(fn): + return property(fn) + + +class Exporter(pygame.newbuffer.BufferMixin): + """An object that exports a multi-dimension new buffer interface + + The only array operation this type supports is to export a buffer. + """ + + prefixes = { + "@": "", + "=": "=", + "<": "=", + ">": "=", + "!": "=", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9", + } + types = { + "c": ctypes.c_char, + "b": ctypes.c_byte, + "B": ctypes.c_ubyte, + "=c": ctypes.c_int8, + "=b": ctypes.c_int8, + "=B": ctypes.c_uint8, + "?": ctypes.c_bool, + "=?": ctypes.c_int8, + "h": ctypes.c_short, + "H": ctypes.c_ushort, + "=h": ctypes.c_int16, + "=H": ctypes.c_uint16, + "i": ctypes.c_int, + "I": ctypes.c_uint, + "=i": ctypes.c_int32, + "=I": ctypes.c_uint32, + "l": ctypes.c_long, + "L": ctypes.c_ulong, + "=l": ctypes.c_int32, + "=L": ctypes.c_uint32, + "q": ctypes.c_longlong, + "Q": ctypes.c_ulonglong, + "=q": ctypes.c_int64, + "=Q": ctypes.c_uint64, + "f": ctypes.c_float, + "d": ctypes.c_double, + "P": ctypes.c_void_p, + "x": ctypes.c_ubyte * 1, + "2x": ctypes.c_ubyte * 2, + "3x": ctypes.c_ubyte * 3, + "4x": ctypes.c_ubyte * 4, + "5x": ctypes.c_ubyte * 5, + "6x": ctypes.c_ubyte * 6, + "7x": ctypes.c_ubyte * 7, + "8x": ctypes.c_ubyte * 8, + "9x": ctypes.c_ubyte * 9, + } + + def __init__(self, shape, format=None, strides=None, readonly=None, itemsize=None): + if format is None: + format = "B" + if readonly is None: + readonly = False + prefix = "" + typecode = "" + i = 0 + if i < len(format): + try: + prefix = self.prefixes[format[i]] + i += 1 + except LookupError: + pass + if i < len(format) and format[i] == "1": + i += 1 + if i == len(format) - 1: + typecode = format[i] + if itemsize is None: + try: + itemsize = ctypes.sizeof(self.types[prefix + typecode]) + except KeyError: + raise ValueError("Unknown item format '" + format + "'") + self.readonly = bool(readonly) + self.format = format + self._format = ctypes.create_string_buffer(format.encode("latin_1")) + self.ndim = len(shape) + self.itemsize = itemsize + self.len = reduce(operator.mul, shape, 1) * self.itemsize + self.shape = tuple(shape) + self._shape = (ctypes.c_ssize_t * self.ndim)(*self.shape) + if strides is None: + self._strides = (ctypes.c_ssize_t * self.ndim)() + self._strides[self.ndim - 1] = itemsize + for i in range(self.ndim - 1, 0, -1): + self._strides[i - 1] = self.shape[i] * self._strides[i] + self.strides = tuple(self._strides) + elif len(strides) == self.ndim: + self.strides = tuple(strides) + self._strides = (ctypes.c_ssize_t * self.ndim)(*self.strides) + else: + raise ValueError("Mismatch in length of strides and shape") + buflen = max(d * abs(s) for d, s in zip(self.shape, self.strides)) + self.buflen = buflen + self._buf = (ctypes.c_ubyte * buflen)() + offset = sum( + (d - 1) * abs(s) for d, s in zip(self.shape, self.strides) if s < 0 + ) + self.buf = ctypes.addressof(self._buf) + offset + + def buffer_info(self): + return (ctypes.addressof(self.buffer), self.shape[0]) + + def tobytes(self): + return ctypes.cast(self.buffer, ctypes.POINTER(ctypes.c_char))[0 : self._len] + + def __len__(self): + return self.shape[0] + + def _get_buffer(self, view, flags): + from ctypes import addressof + + if (flags & PyBUF_WRITABLE) == PyBUF_WRITABLE and self.readonly: + raise BufferError("buffer is read-only") + if ( + flags & PyBUF_C_CONTIGUOUS + ) == PyBUF_C_CONTIGUOUS and not self.is_contiguous("C"): + raise BufferError("data is not C contiguous") + if ( + flags & PyBUF_F_CONTIGUOUS + ) == PyBUF_F_CONTIGUOUS and not self.is_contiguous("F"): + raise BufferError("data is not F contiguous") + if ( + flags & PyBUF_ANY_CONTIGUOUS + ) == PyBUF_ANY_CONTIGUOUS and not self.is_contiguous("A"): + raise BufferError("data is not contiguous") + view.buf = self.buf + view.readonly = self.readonly + view.len = self.len + if flags | PyBUF_WRITABLE == PyBUF_WRITABLE: + view.ndim = 0 + else: + view.ndim = self.ndim + view.itemsize = self.itemsize + if (flags & PyBUF_FORMAT) == PyBUF_FORMAT: + view.format = addressof(self._format) + else: + view.format = None + if (flags & PyBUF_ND) == PyBUF_ND: + view.shape = addressof(self._shape) + elif self.is_contiguous("C"): + view.shape = None + else: + raise BufferError(f"shape required for {self.ndim} dimensional data") + if (flags & PyBUF_STRIDES) == PyBUF_STRIDES: + view.strides = ctypes.addressof(self._strides) + elif view.shape is None or self.is_contiguous("C"): + view.strides = None + else: + raise BufferError("strides required for none C contiguous data") + view.suboffsets = None + view.internal = None + view.obj = self + + def is_contiguous(self, fortran): + if fortran in "CA": + if self.strides[-1] == self.itemsize: + for i in range(self.ndim - 1, 0, -1): + if self.strides[i - 1] != self.shape[i] * self.strides[i]: + break + else: + return True + if fortran in "FA": + if self.strides[0] == self.itemsize: + for i in range(0, self.ndim - 1): + if self.strides[i + 1] != self.shape[i] * self.strides[i]: + break + else: + return True + return False + + +class Importer: + """An object that imports a new buffer interface + + The fields of the Py_buffer C struct are exposed by identically + named Importer read-only properties. + """ + + def __init__(self, obj, flags): + self._view = pygame.newbuffer.Py_buffer() + self._view.get_buffer(obj, flags) + + @property + def obj(self): + """return object or None for NULL field""" + return self._view.obj + + @property + def buf(self): + """return int or None for NULL field""" + return self._view.buf + + @property + def len(self): + """return int""" + return self._view.len + + @property + def readonly(self): + """return bool""" + return self._view.readonly + + @property + def format(self): + """return bytes or None for NULL field""" + format_addr = self._view.format + if format_addr is None: + return None + return ctypes.cast(format_addr, ctypes.c_char_p).value.decode("ascii") + + @property + def itemsize(self): + """return int""" + return self._view.itemsize + + @property + def ndim(self): + """return int""" + return self._view.ndim + + @property + def shape(self): + """return int tuple or None for NULL field""" + return self._to_ssize_tuple(self._view.shape) + + @property + def strides(self): + """return int tuple or None for NULL field""" + return self._to_ssize_tuple(self._view.strides) + + @property + def suboffsets(self): + """return int tuple or None for NULL field""" + return self._to_ssize_tuple(self._view.suboffsets) + + @property + def internal(self): + """return int or None for NULL field""" + return self._view.internal + + def _to_ssize_tuple(self, addr): + from ctypes import cast, POINTER, c_ssize_t + + if addr is None: + return None + return tuple(cast(addr, POINTER(c_ssize_t))[0 : self._view.ndim]) + + +class ExporterTest(unittest.TestCase): + """Class Exporter unit tests""" + + def test_formats(self): + char_sz = ctypes.sizeof(ctypes.c_char) + short_sz = ctypes.sizeof(ctypes.c_short) + int_sz = ctypes.sizeof(ctypes.c_int) + long_sz = ctypes.sizeof(ctypes.c_long) + longlong_sz = ctypes.sizeof(ctypes.c_longlong) + float_sz = ctypes.sizeof(ctypes.c_float) + double_sz = ctypes.sizeof(ctypes.c_double) + voidp_sz = ctypes.sizeof(ctypes.c_void_p) + bool_sz = ctypes.sizeof(ctypes.c_bool) + + self.check_args(0, (1,), "B", (1,), 1, 1, 1) + self.check_args(1, (1,), "b", (1,), 1, 1, 1) + self.check_args(1, (1,), "B", (1,), 1, 1, 1) + self.check_args(1, (1,), "c", (char_sz,), char_sz, char_sz, char_sz) + self.check_args(1, (1,), "h", (short_sz,), short_sz, short_sz, short_sz) + self.check_args(1, (1,), "H", (short_sz,), short_sz, short_sz, short_sz) + self.check_args(1, (1,), "i", (int_sz,), int_sz, int_sz, int_sz) + self.check_args(1, (1,), "I", (int_sz,), int_sz, int_sz, int_sz) + self.check_args(1, (1,), "l", (long_sz,), long_sz, long_sz, long_sz) + self.check_args(1, (1,), "L", (long_sz,), long_sz, long_sz, long_sz) + self.check_args( + 1, (1,), "q", (longlong_sz,), longlong_sz, longlong_sz, longlong_sz + ) + self.check_args( + 1, (1,), "Q", (longlong_sz,), longlong_sz, longlong_sz, longlong_sz + ) + self.check_args(1, (1,), "f", (float_sz,), float_sz, float_sz, float_sz) + self.check_args(1, (1,), "d", (double_sz,), double_sz, double_sz, double_sz) + self.check_args(1, (1,), "x", (1,), 1, 1, 1) + self.check_args(1, (1,), "P", (voidp_sz,), voidp_sz, voidp_sz, voidp_sz) + self.check_args(1, (1,), "?", (bool_sz,), bool_sz, bool_sz, bool_sz) + self.check_args(1, (1,), "@b", (1,), 1, 1, 1) + self.check_args(1, (1,), "@B", (1,), 1, 1, 1) + self.check_args(1, (1,), "@c", (char_sz,), char_sz, char_sz, char_sz) + self.check_args(1, (1,), "@h", (short_sz,), short_sz, short_sz, short_sz) + self.check_args(1, (1,), "@H", (short_sz,), short_sz, short_sz, short_sz) + self.check_args(1, (1,), "@i", (int_sz,), int_sz, int_sz, int_sz) + self.check_args(1, (1,), "@I", (int_sz,), int_sz, int_sz, int_sz) + self.check_args(1, (1,), "@l", (long_sz,), long_sz, long_sz, long_sz) + self.check_args(1, (1,), "@L", (long_sz,), long_sz, long_sz, long_sz) + self.check_args( + 1, (1,), "@q", (longlong_sz,), longlong_sz, longlong_sz, longlong_sz + ) + self.check_args( + 1, (1,), "@Q", (longlong_sz,), longlong_sz, longlong_sz, longlong_sz + ) + self.check_args(1, (1,), "@f", (float_sz,), float_sz, float_sz, float_sz) + self.check_args(1, (1,), "@d", (double_sz,), double_sz, double_sz, double_sz) + self.check_args(1, (1,), "@?", (bool_sz,), bool_sz, bool_sz, bool_sz) + self.check_args(1, (1,), "=b", (1,), 1, 1, 1) + self.check_args(1, (1,), "=B", (1,), 1, 1, 1) + self.check_args(1, (1,), "=c", (1,), 1, 1, 1) + self.check_args(1, (1,), "=h", (2,), 2, 2, 2) + self.check_args(1, (1,), "=H", (2,), 2, 2, 2) + self.check_args(1, (1,), "=i", (4,), 4, 4, 4) + self.check_args(1, (1,), "=I", (4,), 4, 4, 4) + self.check_args(1, (1,), "=l", (4,), 4, 4, 4) + self.check_args(1, (1,), "=L", (4,), 4, 4, 4) + self.check_args(1, (1,), "=q", (8,), 8, 8, 8) + self.check_args(1, (1,), "=Q", (8,), 8, 8, 8) + self.check_args(1, (1,), "=?", (1,), 1, 1, 1) + self.check_args(1, (1,), "h", (2,), 2, 2, 2) + self.check_args(1, (1,), "!h", (2,), 2, 2, 2) + self.check_args(1, (1,), "q", (8,), 8, 8, 8) + self.check_args(1, (1,), "!q", (8,), 8, 8, 8) + self.check_args(1, (1,), "1x", (1,), 1, 1, 1) + self.check_args(1, (1,), "2x", (2,), 2, 2, 2) + self.check_args(1, (1,), "3x", (3,), 3, 3, 3) + self.check_args(1, (1,), "4x", (4,), 4, 4, 4) + self.check_args(1, (1,), "5x", (5,), 5, 5, 5) + self.check_args(1, (1,), "6x", (6,), 6, 6, 6) + self.check_args(1, (1,), "7x", (7,), 7, 7, 7) + self.check_args(1, (1,), "8x", (8,), 8, 8, 8) + self.check_args(1, (1,), "9x", (9,), 9, 9, 9) + self.check_args(1, (1,), "1h", (2,), 2, 2, 2) + self.check_args(1, (1,), "=1h", (2,), 2, 2, 2) + self.assertRaises(ValueError, Exporter, (2, 1), "") + self.assertRaises(ValueError, Exporter, (2, 1), "W") + self.assertRaises(ValueError, Exporter, (2, 1), "^Q") + self.assertRaises(ValueError, Exporter, (2, 1), "=W") + self.assertRaises(ValueError, Exporter, (2, 1), "=f") + self.assertRaises(ValueError, Exporter, (2, 1), "=d") + self.assertRaises(ValueError, Exporter, (2, 1), "f") + self.assertRaises(ValueError, Exporter, (2, 1), ">d") + self.assertRaises(ValueError, Exporter, (2, 1), "!f") + self.assertRaises(ValueError, Exporter, (2, 1), "!d") + self.assertRaises(ValueError, Exporter, (2, 1), "0x") + self.assertRaises(ValueError, Exporter, (2, 1), "11x") + self.assertRaises(ValueError, Exporter, (2, 1), "BB") + + def test_strides(self): + self.check_args(1, (10,), "=h", (2,), 20, 20, 2) + self.check_args(1, (5, 3), "=h", (6, 2), 30, 30, 2) + self.check_args(1, (7, 3, 5), "=h", (30, 10, 2), 210, 210, 2) + self.check_args(1, (13, 5, 11, 3), "=h", (330, 66, 6, 2), 4290, 4290, 2) + self.check_args(3, (7, 3, 5), "=h", (2, 14, 42), 210, 210, 2) + self.check_args(3, (7, 3, 5), "=h", (2, 16, 48), 210, 240, 2) + self.check_args(3, (13, 5, 11, 3), "=h", (440, 88, 8, 2), 4290, 5720, 2) + self.check_args(3, (7, 5), "3x", (15, 3), 105, 105, 3) + self.check_args(3, (7, 5), "3x", (3, 21), 105, 105, 3) + self.check_args(3, (7, 5), "3x", (3, 24), 105, 120, 3) + + def test_readonly(self): + a = Exporter((2,), "h", readonly=True) + self.assertTrue(a.readonly) + b = Importer(a, PyBUF_STRIDED_RO) + self.assertRaises(BufferError, Importer, a, PyBUF_STRIDED) + b = Importer(a, PyBUF_STRIDED_RO) + + def test_is_contiguous(self): + a = Exporter((10,), "=h") + self.assertTrue(a.is_contiguous("C")) + self.assertTrue(a.is_contiguous("F")) + self.assertTrue(a.is_contiguous("A")) + a = Exporter((10, 4), "=h") + self.assertTrue(a.is_contiguous("C")) + self.assertTrue(a.is_contiguous("A")) + self.assertFalse(a.is_contiguous("F")) + a = Exporter((13, 5, 11, 3), "=h", (330, 66, 6, 2)) + self.assertTrue(a.is_contiguous("C")) + self.assertTrue(a.is_contiguous("A")) + self.assertFalse(a.is_contiguous("F")) + a = Exporter((10, 4), "=h", (2, 20)) + self.assertTrue(a.is_contiguous("F")) + self.assertTrue(a.is_contiguous("A")) + self.assertFalse(a.is_contiguous("C")) + a = Exporter((13, 5, 11, 3), "=h", (2, 26, 130, 1430)) + self.assertTrue(a.is_contiguous("F")) + self.assertTrue(a.is_contiguous("A")) + self.assertFalse(a.is_contiguous("C")) + a = Exporter((2, 11, 6, 4), "=h", (576, 48, 8, 2)) + self.assertFalse(a.is_contiguous("A")) + a = Exporter((2, 11, 6, 4), "=h", (2, 4, 48, 288)) + self.assertFalse(a.is_contiguous("A")) + a = Exporter((3, 2, 2), "=h", (16, 8, 4)) + self.assertFalse(a.is_contiguous("A")) + a = Exporter((3, 2, 2), "=h", (4, 12, 24)) + self.assertFalse(a.is_contiguous("A")) + + def test_PyBUF_flags(self): + a = Exporter((10, 2), "d") + b = Importer(a, PyBUF_SIMPLE) + self.assertTrue(b.obj is a) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.len) + self.assertEqual(b.itemsize, a.itemsize) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertTrue(b.internal is None) + self.assertFalse(b.readonly) + b = Importer(a, PyBUF_WRITABLE) + self.assertTrue(b.obj is a) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.len) + self.assertEqual(b.itemsize, a.itemsize) + self.assertTrue(b.shape is None) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertTrue(b.internal is None) + self.assertFalse(b.readonly) + b = Importer(a, PyBUF_ND) + self.assertTrue(b.obj is a) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.len) + self.assertEqual(b.itemsize, a.itemsize) + self.assertEqual(b.shape, a.shape) + self.assertTrue(b.strides is None) + self.assertTrue(b.suboffsets is None) + self.assertTrue(b.internal is None) + self.assertFalse(b.readonly) + a = Exporter((5, 10), "=h", (24, 2)) + b = Importer(a, PyBUF_STRIDES) + self.assertTrue(b.obj is a) + self.assertTrue(b.format is None) + self.assertEqual(b.len, a.len) + self.assertEqual(b.itemsize, a.itemsize) + self.assertEqual(b.shape, a.shape) + self.assertEqual(b.strides, a.strides) + self.assertTrue(b.suboffsets is None) + self.assertTrue(b.internal is None) + self.assertFalse(b.readonly) + b = Importer(a, PyBUF_FULL) + self.assertTrue(b.obj is a) + self.assertEqual(b.format, "=h") + self.assertEqual(b.len, a.len) + self.assertEqual(b.itemsize, a.itemsize) + self.assertEqual(b.shape, a.shape) + self.assertEqual(b.strides, a.strides) + self.assertTrue(b.suboffsets is None) + self.assertTrue(b.internal is None) + self.assertFalse(b.readonly) + self.assertRaises(BufferError, Importer, a, PyBUF_SIMPLE) + self.assertRaises(BufferError, Importer, a, PyBUF_WRITABLE) + self.assertRaises(BufferError, Importer, a, PyBUF_ND) + self.assertRaises(BufferError, Importer, a, PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, PyBUF_ANY_CONTIGUOUS) + self.assertRaises(BufferError, Importer, a, PyBUF_CONTIG) + + def test_negative_strides(self): + self.check_args(3, (3, 5, 4), "B", (20, 4, -1), 60, 60, 1, 3) + self.check_args(3, (3, 5, 3), "B", (20, 4, -1), 45, 60, 1, 2) + self.check_args(3, (3, 5, 4), "B", (20, -4, 1), 60, 60, 1, 16) + self.check_args(3, (3, 5, 4), "B", (-20, -4, -1), 60, 60, 1, 59) + self.check_args(3, (3, 5, 3), "B", (-20, -4, -1), 45, 60, 1, 58) + + def test_attributes(self): + a = Exporter((13, 5, 11, 3), "=h", (440, 88, 8, 2)) + self.assertEqual(a.ndim, 4) + self.assertEqual(a.itemsize, 2) + self.assertFalse(a.readonly) + self.assertEqual(a.shape, (13, 5, 11, 3)) + self.assertEqual(a.format, "=h") + self.assertEqual(a.strides, (440, 88, 8, 2)) + self.assertEqual(a.len, 4290) + self.assertEqual(a.buflen, 5720) + self.assertEqual(a.buf, ctypes.addressof(a._buf)) + a = Exporter((8,)) + self.assertEqual(a.ndim, 1) + self.assertEqual(a.itemsize, 1) + self.assertFalse(a.readonly) + self.assertEqual(a.shape, (8,)) + self.assertEqual(a.format, "B") + self.assertTrue(isinstance(a.strides, tuple)) + self.assertEqual(a.strides, (1,)) + self.assertEqual(a.len, 8) + self.assertEqual(a.buflen, 8) + a = Exporter([13, 5, 11, 3], "=h", [440, 88, 8, 2]) + self.assertTrue(isinstance(a.shape, tuple)) + self.assertTrue(isinstance(a.strides, tuple)) + self.assertEqual(a.shape, (13, 5, 11, 3)) + self.assertEqual(a.strides, (440, 88, 8, 2)) + + def test_itemsize(self): + exp = Exporter((4, 5), format="B", itemsize=8) + imp = Importer(exp, PyBUF_RECORDS) + self.assertEqual(imp.itemsize, 8) + self.assertEqual(imp.format, "B") + self.assertEqual(imp.strides, (40, 8)) + exp = Exporter((4, 5), format="weird", itemsize=5) + imp = Importer(exp, PyBUF_RECORDS) + self.assertEqual(imp.itemsize, 5) + self.assertEqual(imp.format, "weird") + self.assertEqual(imp.strides, (25, 5)) + + def check_args( + self, call_flags, shape, format, strides, length, bufsize, itemsize, offset=0 + ): + format_arg = format if call_flags & 1 else None + strides_arg = strides if call_flags & 2 else None + a = Exporter(shape, format_arg, strides_arg) + self.assertEqual(a.buflen, bufsize) + self.assertEqual(a.buf, ctypes.addressof(a._buf) + offset) + m = Importer(a, PyBUF_RECORDS_RO) + self.assertEqual(m.buf, a.buf) + self.assertEqual(m.len, length) + self.assertEqual(m.format, format) + self.assertEqual(m.itemsize, itemsize) + self.assertEqual(m.shape, shape) + self.assertEqual(m.strides, strides) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/test_utils/endian.py b/laplas/abstract_map/pygame/tests/test_utils/endian.py new file mode 100644 index 0000000..64ba1b3 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/endian.py @@ -0,0 +1,20 @@ +# Module pygame.tests.test_utils.endian +# +# Machine independent conversion to little-endian and big-endian Python +# integer values. + +import struct + + +def little_endian_uint32(i): + """Return the 32 bit unsigned integer little-endian representation of i""" + + s = struct.pack("I", i) + return struct.unpack("=I", s)[0] diff --git a/laplas/abstract_map/pygame/tests/test_utils/png.py b/laplas/abstract_map/pygame/tests/test_utils/png.py new file mode 100644 index 0000000..5d055ea --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/png.py @@ -0,0 +1,4005 @@ +#!/usr/bin/env python + +# $URL: http://pypng.googlecode.com/svn/trunk/code/png.py $ +# $Rev: 228 $ + +# png.py - PNG encoder/decoder in pure Python +# +# Modified for Pygame in Oct., 2012 to work with Python 3.x. +# +# Copyright (C) 2006 Johann C. Rocholl +# Portions Copyright (C) 2009 David Jones +# And probably portions Copyright (C) 2006 Nicko van Someren +# +# Original concept by Johann C. Rocholl. +# +# LICENSE (The MIT License) +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Changelog (recent first): +# 2009-03-11 David: interlaced bit depth < 8 (writing). +# 2009-03-10 David: interlaced bit depth < 8 (reading). +# 2009-03-04 David: Flat and Boxed pixel formats. +# 2009-02-26 David: Palette support (writing). +# 2009-02-23 David: Bit-depths < 8; better PNM support. +# 2006-06-17 Nicko: Reworked into a class, faster interlacing. +# 2006-06-17 Johann: Very simple prototype PNG decoder. +# 2006-06-17 Nicko: Test suite with various image generators. +# 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support. +# 2006-06-15 Johann: Scanline iterator interface for large input files. +# 2006-06-09 Johann: Very simple prototype PNG encoder. + +# Incorporated into Bangai-O Development Tools by drj on 2009-02-11 from +# http://trac.browsershots.org/browser/trunk/pypng/lib/png.py?rev=2885 + +# Incorporated into pypng by drj on 2009-03-12 from +# //depot/prj/bangaio/master/code/png.py#67 + + +""" +Pure Python PNG Reader/Writer + +This Python module implements support for PNG images (see PNG +specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads +and writes PNG files with all allowable bit depths (1/2/4/8/16/24/32/48/64 +bits per pixel) and colour combinations: greyscale (1/2/4/8/16 bit); RGB, +RGBA, LA (greyscale with alpha) with 8/16 bits per channel; colour mapped +images (1/2/4/8 bit). Adam7 interlacing is supported for reading and +writing. A number of optional chunks can be specified (when writing) +and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``. + +For help, type ``import png; help(png)`` in your python interpreter. + +A good place to start is the :class:`Reader` and :class:`Writer` classes. + +This file can also be used as a command-line utility to convert +`Netpbm `_ PNM files to PNG, and the reverse conversion from PNG to +PNM. The interface is similar to that of the ``pnmtopng`` program from +Netpbm. Type ``python png.py --help`` at the shell prompt +for usage and a list of options. + +A note on spelling and terminology +---------------------------------- + +Generally British English spelling is used in the documentation. So +that's "greyscale" and "colour". This not only matches the author's +native language, it's also used by the PNG specification. + +The major colour models supported by PNG (and hence by PyPNG) are: +greyscale, RGB, greyscale--alpha, RGB--alpha. These are sometimes +referred to using the abbreviations: L, RGB, LA, RGBA. In this case +each letter abbreviates a single channel: *L* is for Luminance or Luma or +Lightness which is the channel used in greyscale images; *R*, *G*, *B* stand +for Red, Green, Blue, the components of a colour image; *A* stands for +Alpha, the opacity channel (used for transparency effects, but higher +values are more opaque, so it makes sense to call it opacity). + +A note on formats +----------------- + +When getting pixel data out of this module (reading) and presenting +data to this module (writing) there are a number of ways the data could +be represented as a Python value. Generally this module uses one of +three formats called "flat row flat pixel", "boxed row flat pixel", and +"boxed row boxed pixel". Basically the concern is whether each pixel +and each row comes in its own little tuple (box), or not. + +Consider an image that is 3 pixels wide by 2 pixels high, and each pixel +has RGB components: + +Boxed row flat pixel:: + + list([R,G,B, R,G,B, R,G,B], + [R,G,B, R,G,B, R,G,B]) + +Each row appears as its own list, but the pixels are flattened so that +three values for one pixel simply follow the three values for the previous +pixel. This is the most common format used, because it provides a good +compromise between space and convenience. PyPNG regards itself as +at liberty to replace any sequence type with any sufficiently compatible +other sequence type; in practice each row is an array (from the array +module), and the outer list is sometimes an iterator rather than an +explicit list (so that streaming is possible). + +Flat row flat pixel:: + + [R,G,B, R,G,B, R,G,B, + R,G,B, R,G,B, R,G,B] + +The entire image is one single giant sequence of colour values. +Generally an array will be used (to save space), not a list. + +Boxed row boxed pixel:: + + list([ (R,G,B), (R,G,B), (R,G,B) ], + [ (R,G,B), (R,G,B), (R,G,B) ]) + +Each row appears in its own list, but each pixel also appears in its own +tuple. A serious memory burn in Python. + +In all cases the top row comes first, and for each row the pixels are +ordered from left-to-right. Within a pixel the values appear in the +order, R-G-B-A (or L-A for greyscale--alpha). + +There is a fourth format, mentioned because it is used internally, +is close to what lies inside a PNG file itself, and has some support +from the public API. This format is called packed. When packed, +each row is a sequence of bytes (integers from 0 to 255), just as +it is before PNG scanline filtering is applied. When the bit depth +is 8 this is essentially the same as boxed row flat pixel; when the +bit depth is less than 8, several pixels are packed into each byte; +when the bit depth is 16 (the only value more than 8 that is supported +by the PNG image format) each pixel value is decomposed into 2 bytes +(and `packed` is a misnomer). This format is used by the +:meth:`Writer.write_packed` method. It isn't usually a convenient +format, but may be just right if the source data for the PNG image +comes from something that uses a similar format (for example, 1-bit +BMPs, or another PNG file). + +And now, my famous members +-------------------------- +""" + +__version__ = "$URL: http://pypng.googlecode.com/svn/trunk/code/png.py $ $Rev: 228 $" + +import io +import itertools +import math +import operator +import struct +import sys +import zlib +import warnings +from array import array +from functools import reduce + +from pygame.tests.test_utils import tostring + +__all__ = ["Image", "Reader", "Writer", "write_chunks", "from_array"] + + +# The PNG signature. +# http://www.w3.org/TR/PNG/#5PNG-file-signature +_signature = struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10) + +_adam7 = ( + (0, 0, 8, 8), + (4, 0, 8, 8), + (0, 4, 4, 8), + (2, 0, 4, 4), + (0, 2, 2, 4), + (1, 0, 2, 2), + (0, 1, 1, 2), +) + + +def group(s, n): + # See + # http://www.python.org/doc/2.6/library/functions.html#zip + return zip(*[iter(s)] * n) + + +def isarray(x): + """Same as ``isinstance(x, array)``.""" + return isinstance(x, array) + + +# Conditionally convert to bytes. Works on Python 2 and Python 3. +try: + bytes("", "ascii") + + def strtobytes(x): + return bytes(x, "iso8859-1") + + def bytestostr(x): + return str(x, "iso8859-1") + +except: + strtobytes = str + bytestostr = str + + +def interleave_planes(ipixels, apixels, ipsize, apsize): + """ + Interleave (colour) planes, e.g. RGB + A = RGBA. + + Return an array of pixels consisting of the `ipsize` elements of data + from each pixel in `ipixels` followed by the `apsize` elements of data + from each pixel in `apixels`. Conventionally `ipixels` and + `apixels` are byte arrays so the sizes are bytes, but it actually + works with any arrays of the same type. The returned array is the + same type as the input arrays which should be the same type as each other. + """ + + itotal = len(ipixels) + atotal = len(apixels) + newtotal = itotal + atotal + newpsize = ipsize + apsize + # Set up the output buffer + # See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356 + out = array(ipixels.typecode) + # It's annoying that there is no cheap way to set the array size :-( + out.extend(ipixels) + out.extend(apixels) + # Interleave in the pixel data + for i in range(ipsize): + out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize] + for i in range(apsize): + out[i + ipsize : newtotal : newpsize] = apixels[i:atotal:apsize] + return out + + +def check_palette(palette): + """Check a palette argument (to the :class:`Writer` class) for validity. + Returns the palette as a list if okay; raises an exception otherwise. + """ + + # None is the default and is allowed. + if palette is None: + return None + + p = list(palette) + if not (0 < len(p) <= 256): + raise ValueError("a palette must have between 1 and 256 entries") + seen_triple = False + for i, t in enumerate(p): + if len(t) not in (3, 4): + raise ValueError("palette entry %d: entries must be 3- or 4-tuples." % i) + if len(t) == 3: + seen_triple = True + if seen_triple and len(t) == 4: + raise ValueError( + "palette entry %d: all 4-tuples must precede all 3-tuples" % i + ) + for x in t: + if int(x) != x or not (0 <= x <= 255): + raise ValueError( + "palette entry %d: values must be integer: 0 <= x <= 255" % i + ) + return p + + +class Error(Exception): + prefix = "Error" + + def __str__(self): + return f'{self.prefix}: {" ".join(self.args)}' + + +class FormatError(Error): + """Problem with input file format. In other words, PNG file does + not conform to the specification in some way and is invalid. + """ + + prefix = "FormatError" + + +class ChunkError(FormatError): + prefix = "ChunkError" + + +class Writer: + """ + PNG encoder in pure Python. + """ + + def __init__( + self, + width=None, + height=None, + size=None, + greyscale=False, + alpha=False, + bitdepth=8, + palette=None, + transparent=None, + background=None, + gamma=None, + compression=None, + interlace=False, + bytes_per_sample=None, # deprecated + planes=None, + colormap=None, + maxval=None, + chunk_limit=2**20, + ): + """ + Create a PNG encoder object. + + Arguments: + + width, height + Image size in pixels, as two separate arguments. + size + Image size (w,h) in pixels, as single argument. + greyscale + Input data is greyscale, not RGB. + alpha + Input data has alpha channel (RGBA or LA). + bitdepth + Bit depth: from 1 to 16. + palette + Create a palette for a colour mapped image (colour type 3). + transparent + Specify a transparent colour (create a ``tRNS`` chunk). + background + Specify a default background colour (create a ``bKGD`` chunk). + gamma + Specify a gamma value (create a ``gAMA`` chunk). + compression + zlib compression level (1-9). + interlace + Create an interlaced image. + chunk_limit + Write multiple ``IDAT`` chunks to save memory. + + The image size (in pixels) can be specified either by using the + `width` and `height` arguments, or with the single `size` + argument. If `size` is used it should be a pair (*width*, + *height*). + + `greyscale` and `alpha` are booleans that specify whether + an image is greyscale (or colour), and whether it has an + alpha channel (or not). + + `bitdepth` specifies the bit depth of the source pixel values. + Each source pixel value must be an integer between 0 and + ``2**bitdepth-1``. For example, 8-bit images have values + between 0 and 255. PNG only stores images with bit depths of + 1,2,4,8, or 16. When `bitdepth` is not one of these values, + the next highest valid bit depth is selected, and an ``sBIT`` + (significant bits) chunk is generated that specifies the original + precision of the source image. In this case the supplied pixel + values will be rescaled to fit the range of the selected bit depth. + + The details of which bit depth / colour model combinations the + PNG file format supports directly, are somewhat arcane + (refer to the PNG specification for full details). Briefly: + "small" bit depths (1,2,4) are only allowed with greyscale and + colour mapped images; colour mapped images cannot have bit depth + 16. + + For colour mapped images (in other words, when the `palette` + argument is specified) the `bitdepth` argument must match one of + the valid PNG bit depths: 1, 2, 4, or 8. (It is valid to have a + PNG image with a palette and an ``sBIT`` chunk, but the meaning + is slightly different; it would be awkward to press the + `bitdepth` argument into service for this.) + + The `palette` option, when specified, causes a colour mapped image + to be created: the PNG colour type is set to 3; greyscale + must not be set; alpha must not be set; transparent must + not be set; the bit depth must be 1,2,4, or 8. When a colour + mapped image is created, the pixel values are palette indexes + and the `bitdepth` argument specifies the size of these indexes + (not the size of the colour values in the palette). + + The palette argument value should be a sequence of 3- or + 4-tuples. 3-tuples specify RGB palette entries; 4-tuples + specify RGBA palette entries. If both 4-tuples and 3-tuples + appear in the sequence then all the 4-tuples must come + before all the 3-tuples. A ``PLTE`` chunk is created; if there + are 4-tuples then a ``tRNS`` chunk is created as well. The + ``PLTE`` chunk will contain all the RGB triples in the same + sequence; the ``tRNS`` chunk will contain the alpha channel for + all the 4-tuples, in the same sequence. Palette entries + are always 8-bit. + + If specified, the `transparent` and `background` parameters must + be a tuple with three integer values for red, green, blue, or + a simple integer (or singleton tuple) for a greyscale image. + + If specified, the `gamma` parameter must be a positive number + (generally, a float). A ``gAMA`` chunk will be created. Note that + this will not change the values of the pixels as they appear in + the PNG file, they are assumed to have already been converted + appropriately for the gamma specified. + + The `compression` argument specifies the compression level + to be used by the ``zlib`` module. Higher values are likely + to compress better, but will be slower to compress. The + default for this argument is ``None``; this does not mean + no compression, rather it means that the default from the + ``zlib`` module is used (which is generally acceptable). + + If `interlace` is true then an interlaced image is created + (using PNG's so far only interlace method, *Adam7*). This does not + affect how the pixels should be presented to the encoder, rather + it changes how they are arranged into the PNG file. On slow + connexions interlaced images can be partially decoded by the + browser to give a rough view of the image that is successively + refined as more image data appears. + + .. note :: + + Enabling the `interlace` option requires the entire image + to be processed in working memory. + + `chunk_limit` is used to limit the amount of memory used whilst + compressing the image. In order to avoid using large amounts of + memory, multiple ``IDAT`` chunks may be created. + """ + + # At the moment the `planes` argument is ignored; + # its purpose is to act as a dummy so that + # ``Writer(x, y, **info)`` works, where `info` is a dictionary + # returned by Reader.read and friends. + # Ditto for `colormap`. + + # A couple of helper functions come first. Best skipped if you + # are reading through. + + def isinteger(x): + try: + return int(x) == x + except: + return False + + def check_color(c, which): + """Checks that a colour argument for transparent or + background options is the right form. Also "corrects" bare + integers to 1-tuples. + """ + + if c is None: + return c + if greyscale: + try: + l = len(c) + except TypeError: + c = (c,) + if len(c) != 1: + raise ValueError(f"{which} for greyscale must be 1-tuple") + if not isinteger(c[0]): + raise ValueError(f"{which} colour for greyscale must be integer") + else: + if not ( + len(c) == 3 + and isinteger(c[0]) + and isinteger(c[1]) + and isinteger(c[2]) + ): + raise ValueError(f"{which} colour must be a triple of integers") + return c + + if size: + if len(size) != 2: + raise ValueError("size argument should be a pair (width, height)") + if width is not None and width != size[0]: + raise ValueError( + "size[0] (%r) and width (%r) should match when both are used." + % (size[0], width) + ) + if height is not None and height != size[1]: + raise ValueError( + "size[1] (%r) and height (%r) should match when both are used." + % (size[1], height) + ) + width, height = size + del size + + if width <= 0 or height <= 0: + raise ValueError("width and height must be greater than zero") + if not isinteger(width) or not isinteger(height): + raise ValueError("width and height must be integers") + # http://www.w3.org/TR/PNG/#7Integers-and-byte-order + if width > 2**32 - 1 or height > 2**32 - 1: + raise ValueError("width and height cannot exceed 2**32-1") + + if alpha and transparent is not None: + raise ValueError("transparent colour not allowed with alpha channel") + + if bytes_per_sample is not None: + warnings.warn( + "please use bitdepth instead of bytes_per_sample", DeprecationWarning + ) + if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2): + raise ValueError("bytes per sample must be .125, .25, .5, 1, or 2") + bitdepth = int(8 * bytes_per_sample) + del bytes_per_sample + if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth: + raise ValueError( + f"bitdepth ({bitdepth!r}) must be a positive integer <= 16" + ) + + self.rescale = None + if palette: + if bitdepth not in (1, 2, 4, 8): + raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8") + if transparent is not None: + raise ValueError("transparent and palette not compatible") + if alpha: + raise ValueError("alpha and palette not compatible") + if greyscale: + raise ValueError("greyscale and palette not compatible") + else: + # No palette, check for sBIT chunk generation. + if alpha or not greyscale: + if bitdepth not in (8, 16): + targetbitdepth = (8, 16)[bitdepth > 8] + self.rescale = (bitdepth, targetbitdepth) + bitdepth = targetbitdepth + del targetbitdepth + else: + assert greyscale + assert not alpha + if bitdepth not in (1, 2, 4, 8, 16): + if bitdepth > 8: + targetbitdepth = 16 + elif bitdepth == 3: + targetbitdepth = 4 + else: + assert bitdepth in (5, 6, 7) + targetbitdepth = 8 + self.rescale = (bitdepth, targetbitdepth) + bitdepth = targetbitdepth + del targetbitdepth + + if bitdepth < 8 and (alpha or not greyscale and not palette): + raise ValueError("bitdepth < 8 only permitted with greyscale or palette") + if bitdepth > 8 and palette: + raise ValueError("bit depth must be 8 or less for images with palette") + + transparent = check_color(transparent, "transparent") + background = check_color(background, "background") + + # It's important that the true boolean values (greyscale, alpha, + # colormap, interlace) are converted to bool because Iverson's + # convention is relied upon later on. + self.width = width + self.height = height + self.transparent = transparent + self.background = background + self.gamma = gamma + self.greyscale = bool(greyscale) + self.alpha = bool(alpha) + self.colormap = bool(palette) + self.bitdepth = int(bitdepth) + self.compression = compression + self.chunk_limit = chunk_limit + self.interlace = bool(interlace) + self.palette = check_palette(palette) + + self.color_type = 4 * self.alpha + 2 * (not greyscale) + 1 * self.colormap + assert self.color_type in (0, 2, 3, 4, 6) + + self.color_planes = (3, 1)[self.greyscale or self.colormap] + self.planes = self.color_planes + self.alpha + # :todo: fix for bitdepth < 8 + self.psize = (self.bitdepth / 8) * self.planes + + def make_palette(self): + """Create the byte sequences for a ``PLTE`` and if necessary a + ``tRNS`` chunk. Returned as a pair (*p*, *t*). *t* will be + ``None`` if no ``tRNS`` chunk is necessary. + """ + + p = array("B") + t = array("B") + + for x in self.palette: + p.extend(x[0:3]) + if len(x) > 3: + t.append(x[3]) + p = tostring(p) + t = tostring(t) + if t: + return p, t + return p, None + + def write(self, outfile, rows): + """Write a PNG image to the output file. `rows` should be + an iterable that yields each row in boxed row flat pixel format. + The rows should be the rows of the original image, so there + should be ``self.height`` rows of ``self.width * self.planes`` values. + If `interlace` is specified (when creating the instance), then + an interlaced PNG file will be written. Supply the rows in the + normal image order; the interlacing is carried out internally. + + .. note :: + + Interlacing will require the entire image to be in working memory. + """ + + if self.interlace: + fmt = "BH"[self.bitdepth > 8] + a = array(fmt, itertools.chain(*rows)) + return self.write_array(outfile, a) + else: + nrows = self.write_passes(outfile, rows) + if nrows != self.height: + raise ValueError( + "rows supplied (%d) does not match height (%d)" + % (nrows, self.height) + ) + + def write_passes(self, outfile, rows, packed=False): + """ + Write a PNG image to the output file. + + Most users are expected to find the :meth:`write` or + :meth:`write_array` method more convenient. + + The rows should be given to this method in the order that + they appear in the output file. For straightlaced images, + this is the usual top to bottom ordering, but for interlaced + images the rows should have already been interlaced before + passing them to this function. + + `rows` should be an iterable that yields each row. When + `packed` is ``False`` the rows should be in boxed row flat pixel + format; when `packed` is ``True`` each row should be a packed + sequence of bytes. + + """ + + # http://www.w3.org/TR/PNG/#5PNG-file-signature + outfile.write(_signature) + + # http://www.w3.org/TR/PNG/#11IHDR + write_chunk( + outfile, + "IHDR", + struct.pack( + "!2I5B", + self.width, + self.height, + self.bitdepth, + self.color_type, + 0, + 0, + self.interlace, + ), + ) + + # See :chunk:order + # http://www.w3.org/TR/PNG/#11gAMA + if self.gamma is not None: + write_chunk( + outfile, "gAMA", struct.pack("!L", int(round(self.gamma * 1e5))) + ) + + # See :chunk:order + # http://www.w3.org/TR/PNG/#11sBIT + if self.rescale: + write_chunk( + outfile, + "sBIT", + struct.pack("%dB" % self.planes, *[self.rescale[0]] * self.planes), + ) + + # :chunk:order: Without a palette (PLTE chunk), ordering is + # relatively relaxed. With one, gAMA chunk must precede PLTE + # chunk which must precede tRNS and bKGD. + # See http://www.w3.org/TR/PNG/#5ChunkOrdering + if self.palette: + p, t = self.make_palette() + write_chunk(outfile, "PLTE", p) + if t: + # tRNS chunk is optional. Only needed if palette entries + # have alpha. + write_chunk(outfile, "tRNS", t) + + # http://www.w3.org/TR/PNG/#11tRNS + if self.transparent is not None: + if self.greyscale: + write_chunk(outfile, "tRNS", struct.pack("!1H", *self.transparent)) + else: + write_chunk(outfile, "tRNS", struct.pack("!3H", *self.transparent)) + + # http://www.w3.org/TR/PNG/#11bKGD + if self.background is not None: + if self.greyscale: + write_chunk(outfile, "bKGD", struct.pack("!1H", *self.background)) + else: + write_chunk(outfile, "bKGD", struct.pack("!3H", *self.background)) + + # http://www.w3.org/TR/PNG/#11IDAT + if self.compression is not None: + compressor = zlib.compressobj(self.compression) + else: + compressor = zlib.compressobj() + + # Choose an extend function based on the bitdepth. The extend + # function packs/decomposes the pixel values into bytes and + # stuffs them onto the data array. + data = array("B") + if self.bitdepth == 8 or packed: + extend = data.extend + elif self.bitdepth == 16: + # Decompose into bytes + def extend(sl): + fmt = f"!{len(sl)}H" + data.extend(array("B", struct.pack(fmt, *sl))) + + else: + # Pack into bytes + assert self.bitdepth < 8 + # samples per byte + spb = int(8 / self.bitdepth) + + def extend(sl): + a = array("B", sl) + # Adding padding bytes so we can group into a whole + # number of spb-tuples. + l = float(len(a)) + extra = math.ceil(l / float(spb)) * spb - l + a.extend([0] * int(extra)) + # Pack into bytes + l = group(a, spb) + l = (reduce(lambda x, y: (x << self.bitdepth) + y, e) for e in l) + data.extend(l) + + if self.rescale: + oldextend = extend + factor = float(2 ** self.rescale[1] - 1) / float(2 ** self.rescale[0] - 1) + + def extend(sl): + oldextend((int(round(factor * x)) for x in sl)) + + # Build the first row, testing mostly to see if we need to + # changed the extend function to cope with NumPy integer types + # (they cause our ordinary definition of extend to fail, so we + # wrap it). See + # http://code.google.com/p/pypng/issues/detail?id=44 + enumrows = enumerate(rows) + del rows + + # First row's filter type. + data.append(0) + # :todo: Certain exceptions in the call to ``.next()`` or the + # following try would indicate no row data supplied. + # Should catch. + i, row = next(enumrows) + try: + # If this fails... + extend(row) + except: + # ... try a version that converts the values to int first. + # Not only does this work for the (slightly broken) NumPy + # types, there are probably lots of other, unknown, "nearly" + # int types it works for. + def wrapmapint(f): + return lambda sl: f(map(int, sl)) + + extend = wrapmapint(extend) + del wrapmapint + extend(row) + + for i, row in enumrows: + # Add "None" filter type. Currently, it's essential that + # this filter type be used for every scanline as we do not + # mark the first row of a reduced pass image; that means we + # could accidentally compute the wrong filtered scanline if + # we used "up", "average", or "paeth" on such a line. + data.append(0) + extend(row) + if len(data) > self.chunk_limit: + compressed = compressor.compress(tostring(data)) + if len(compressed): + # print(len(data), len(compressed), file= >> sys.stderr) + write_chunk(outfile, "IDAT", compressed) + # Because of our very witty definition of ``extend``, + # above, we must reuse the same ``data`` object. Hence + # we use ``del`` to empty this one, rather than create a + # fresh one (which would be my natural FP instinct). + del data[:] + if len(data): + compressed = compressor.compress(tostring(data)) + else: + compressed = "" + flushed = compressor.flush() + if len(compressed) or len(flushed): + # print(len(data), len(compressed), len(flushed), file=sys.stderr) + write_chunk(outfile, "IDAT", compressed + flushed) + # http://www.w3.org/TR/PNG/#11IEND + write_chunk(outfile, "IEND") + return i + 1 + + def write_array(self, outfile, pixels): + """ + Write an array in flat row flat pixel format as a PNG file on + the output file. See also :meth:`write` method. + """ + + if self.interlace: + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.array_scanlines(pixels)) + + def write_packed(self, outfile, rows): + """ + Write PNG file to `outfile`. The pixel data comes from `rows` + which should be in boxed row packed format. Each row should be + a sequence of packed bytes. + + Technically, this method does work for interlaced images but it + is best avoided. For interlaced images, the rows should be + presented in the order that they appear in the file. + + This method should not be used when the source image bit depth + is not one naturally supported by PNG; the bit depth should be + 1, 2, 4, 8, or 16. + """ + + if self.rescale: + raise Error( + "write_packed method not suitable for bit depth %d" % self.rescale[0] + ) + return self.write_passes(outfile, rows, packed=True) + + def convert_pnm(self, infile, outfile): + """ + Convert a PNM file containing raw pixel data into a PNG file + with the parameters set in the writer object. Works for + (binary) PGM, PPM, and PAM formats. + """ + + if self.interlace: + pixels = array("B") + pixels.fromfile( + infile, + (self.bitdepth / 8) * self.color_planes * self.width * self.height, + ) + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.file_scanlines(infile)) + + def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile): + """ + Convert a PPM and PGM file containing raw pixel data into a + PNG outfile with the parameters set in the writer object. + """ + pixels = array("B") + pixels.fromfile( + ppmfile, (self.bitdepth / 8) * self.color_planes * self.width * self.height + ) + apixels = array("B") + apixels.fromfile(pgmfile, (self.bitdepth / 8) * self.width * self.height) + pixels = interleave_planes( + pixels, + apixels, + (self.bitdepth / 8) * self.color_planes, + (self.bitdepth / 8), + ) + if self.interlace: + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.array_scanlines(pixels)) + + def file_scanlines(self, infile): + """ + Generates boxed rows in flat pixel format, from the input file + `infile`. It assumes that the input file is in a "Netpbm-like" + binary format, and is positioned at the beginning of the first + pixel. The number of pixels to read is taken from the image + dimensions (`width`, `height`, `planes`) and the number of bytes + per value is implied by the image `bitdepth`. + """ + + # Values per row + vpr = self.width * self.planes + row_bytes = vpr + if self.bitdepth > 8: + assert self.bitdepth == 16 + row_bytes *= 2 + fmt = ">%dH" % vpr + + def line(): + return array("H", struct.unpack(fmt, infile.read(row_bytes))) + + else: + + def line(): + scanline = array("B", infile.read(row_bytes)) + return scanline + + for y in range(self.height): + yield line() + + def array_scanlines(self, pixels): + """ + Generates boxed rows (flat pixels) from flat rows (flat pixels) + in an array. + """ + + # Values per row + vpr = self.width * self.planes + stop = 0 + for y in range(self.height): + start = stop + stop = start + vpr + yield pixels[start:stop] + + def array_scanlines_interlace(self, pixels): + """ + Generator for interlaced scanlines from an array. `pixels` is + the full source image in flat row flat pixel format. The + generator yields each scanline of the reduced passes in turn, in + boxed row flat pixel format. + """ + + # http://www.w3.org/TR/PNG/#8InterlaceMethods + # Array type. + fmt = "BH"[self.bitdepth > 8] + # Value per row + vpr = self.width * self.planes + for xstart, ystart, xstep, ystep in _adam7: + if xstart >= self.width: + continue + # Pixels per row (of reduced image) + ppr = int(math.ceil((self.width - xstart) / float(xstep))) + # number of values in reduced image row. + row_len = ppr * self.planes + for y in range(ystart, self.height, ystep): + if xstep == 1: + offset = y * vpr + yield pixels[offset : offset + vpr] + else: + row = array(fmt) + # There's no easier way to set the length of an array + row.extend(pixels[0:row_len]) + offset = y * vpr + xstart * self.planes + end_offset = (y + 1) * vpr + skip = self.planes * xstep + for i in range(self.planes): + row[i :: self.planes] = pixels[offset + i : end_offset : skip] + yield row + + +def write_chunk(outfile, tag, data=strtobytes("")): + """ + Write a PNG chunk to the output file, including length and + checksum. + """ + + # http://www.w3.org/TR/PNG/#5Chunk-layout + outfile.write(struct.pack("!I", len(data))) + tag = strtobytes(tag) + outfile.write(tag) + outfile.write(data) + checksum = zlib.crc32(tag) + checksum = zlib.crc32(data, checksum) + checksum &= 2**32 - 1 + outfile.write(struct.pack("!I", checksum)) + + +def write_chunks(out, chunks): + """Create a PNG file by writing out the chunks.""" + + out.write(_signature) + for chunk in chunks: + write_chunk(out, *chunk) + + +def filter_scanline(type, line, fo, prev=None): + """Apply a scanline filter to a scanline. `type` specifies the + filter type (0 to 4); `line` specifies the current (unfiltered) + scanline as a sequence of bytes; `prev` specifies the previous + (unfiltered) scanline as a sequence of bytes. `fo` specifies the + filter offset; normally this is size of a pixel in bytes (the number + of bytes per sample times the number of channels), but when this is + < 1 (for bit depths < 8) then the filter offset is 1. + """ + + assert 0 <= type < 5 + + # The output array. Which, pathetically, we extend one-byte at a + # time (fortunately this is linear). + out = array("B", [type]) + + def sub(): + ai = -fo + for x in line: + if ai >= 0: + x = (x - line[ai]) & 0xFF + out.append(x) + ai += 1 + + def up(): + for i, x in enumerate(line): + x = (x - prev[i]) & 0xFF + out.append(x) + + def average(): + ai = -fo + for i, x in enumerate(line): + if ai >= 0: + x = (x - ((line[ai] + prev[i]) >> 1)) & 0xFF + else: + x = (x - (prev[i] >> 1)) & 0xFF + out.append(x) + ai += 1 + + def paeth(): + # http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth + ai = -fo # also used for ci + for i, x in enumerate(line): + a = 0 + b = prev[i] + c = 0 + + if ai >= 0: + a = line[ai] + c = prev[ai] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + Pr = a + elif pb <= pc: + Pr = b + else: + Pr = c + + x = (x - Pr) & 0xFF + out.append(x) + ai += 1 + + if not prev: + # We're on the first line. Some of the filters can be reduced + # to simpler cases which makes handling the line "off the top" + # of the image simpler. "up" becomes "none"; "paeth" becomes + # "left" (non-trivial, but true). "average" needs to be handled + # specially. + if type == 2: # "up" + return line # type = 0 + elif type == 3: + prev = [0] * len(line) + elif type == 4: # "paeth" + type = 1 + if type == 0: + out.extend(line) + elif type == 1: + sub() + elif type == 2: + up() + elif type == 3: + average() + else: # type == 4 + paeth() + return out + + +def from_array(a, mode=None, info={}): + """Create a PNG :class:`Image` object from a 2- or 3-dimensional array. + One application of this function is easy PIL-style saving: + ``png.from_array(pixels, 'L').save('foo.png')``. + + .. note : + + The use of the term *3-dimensional* is for marketing purposes + only. It doesn't actually work. Please bear with us. Meanwhile + enjoy the complimentary snacks (on request) and please use a + 2-dimensional array. + + Unless they are specified using the *info* parameter, the PNG's + height and width are taken from the array size. For a 3 dimensional + array the first axis is the height; the second axis is the width; + and the third axis is the channel number. Thus an RGB image that is + 16 pixels high and 8 wide will use an array that is 16x8x3. For 2 + dimensional arrays the first axis is the height, but the second axis + is ``width*channels``, so an RGB image that is 16 pixels high and 8 + wide will use a 2-dimensional array that is 16x24 (each row will be + 8*3==24 sample values). + + *mode* is a string that specifies the image colour format in a + PIL-style mode. It can be: + + ``'L'`` + greyscale (1 channel) + ``'LA'`` + greyscale with alpha (2 channel) + ``'RGB'`` + colour image (3 channel) + ``'RGBA'`` + colour image with alpha (4 channel) + + The mode string can also specify the bit depth (overriding how this + function normally derives the bit depth, see below). Appending + ``';16'`` to the mode will cause the PNG to be 16 bits per channel; + any decimal from 1 to 16 can be used to specify the bit depth. + + When a 2-dimensional array is used *mode* determines how many + channels the image has, and so allows the width to be derived from + the second array dimension. + + The array is expected to be a ``numpy`` array, but it can be any + suitable Python sequence. For example, a list of lists can be used: + ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``. The exact + rules are: ``len(a)`` gives the first dimension, height; + ``len(a[0])`` gives the second dimension; ``len(a[0][0])`` gives the + third dimension, unless an exception is raised in which case a + 2-dimensional array is assumed. It's slightly more complicated than + that because an iterator of rows can be used, and it all still + works. Using an iterator allows data to be streamed efficiently. + + The bit depth of the PNG is normally taken from the array element's + datatype (but if *mode* specifies a bitdepth then that is used + instead). The array element's datatype is determined in a way which + is supposed to work both for ``numpy`` arrays and for Python + ``array.array`` objects. A 1 byte datatype will give a bit depth of + 8, a 2 byte datatype will give a bit depth of 16. If the datatype + does not have an implicit size, for example it is a plain Python + list of lists, as above, then a default of 8 is used. + + The *info* parameter is a dictionary that can be used to specify + metadata (in the same style as the arguments to the + :class:``png.Writer`` class). For this function the keys that are + useful are: + + height + overrides the height derived from the array dimensions and allows + *a* to be an iterable. + width + overrides the width derived from the array dimensions. + bitdepth + overrides the bit depth derived from the element datatype (but + must match *mode* if that also specifies a bit depth). + + Generally anything specified in the + *info* dictionary will override any implicit choices that this + function would otherwise make, but must match any explicit ones. + For example, if the *info* dictionary has a ``greyscale`` key then + this must be true when mode is ``'L'`` or ``'LA'`` and false when + mode is ``'RGB'`` or ``'RGBA'``. + """ + + # We abuse the *info* parameter by modifying it. Take a copy here. + # (Also typechecks *info* to some extent). + info = dict(info) + + # Syntax check mode string. + bitdepth = None + try: + mode = mode.split(";") + if len(mode) not in (1, 2): + raise Error() + if mode[0] not in ("L", "LA", "RGB", "RGBA"): + raise Error() + if len(mode) == 2: + try: + bitdepth = int(mode[1]) + except: + raise Error() + except Error: + raise Error("mode string should be 'RGB' or 'L;16' or similar.") + mode = mode[0] + + # Get bitdepth from *mode* if possible. + if bitdepth: + if info.get("bitdepth") and bitdepth != info["bitdepth"]: + raise Error( + "mode bitdepth (%d) should match info bitdepth (%d)." + % (bitdepth, info["bitdepth"]) + ) + info["bitdepth"] = bitdepth + + # Fill in and/or check entries in *info*. + # Dimensions. + if "size" in info: + # Check width, height, size all match where used. + for dimension, axis in [("width", 0), ("height", 1)]: + if dimension in info: + if info[dimension] != info["size"][axis]: + raise Error( + f"info[{dimension!r}] should match info['size'][{axis!r}]." + ) + info["width"], info["height"] = info["size"] + if "height" not in info: + try: + l = len(a) + except: + raise Error("len(a) does not work, supply info['height'] instead.") + info["height"] = l + # Colour format. + if "greyscale" in info: + if bool(info["greyscale"]) != ("L" in mode): + raise Error("info['greyscale'] should match mode.") + info["greyscale"] = "L" in mode + if "alpha" in info: + if bool(info["alpha"]) != ("A" in mode): + raise Error("info['alpha'] should match mode.") + info["alpha"] = "A" in mode + + planes = len(mode) + if "planes" in info: + if info["planes"] != planes: + raise Error("info['planes'] should match mode.") + + # In order to work out whether we the array is 2D or 3D we need its + # first row, which requires that we take a copy of its iterator. + # We may also need the first row to derive width and bitdepth. + a, t = itertools.tee(a) + row = next(t) + del t + try: + row[0][0] + threed = True + testelement = row[0] + except: + threed = False + testelement = row + if "width" not in info: + if threed: + width = len(row) + else: + width = len(row) // planes + info["width"] = width + + # Not implemented yet + assert not threed + + if "bitdepth" not in info: + try: + dtype = testelement.dtype + # goto the "else:" clause. Sorry. + except: + try: + # Try a Python array.array. + bitdepth = 8 * testelement.itemsize + except: + # We can't determine it from the array element's + # datatype, use a default of 8. + bitdepth = 8 + else: + # If we got here without exception, we now assume that + # the array is a numpy array. + if dtype.kind == "b": + bitdepth = 1 + else: + bitdepth = 8 * dtype.itemsize + info["bitdepth"] = bitdepth + + for thing in "width height bitdepth greyscale alpha".split(): + assert thing in info + return Image(a, info) + + +# So that refugee's from PIL feel more at home. Not documented. +fromarray = from_array + + +class Image: + """A PNG image. + You can create an :class:`Image` object from an array of pixels by calling + :meth:`png.from_array`. It can be saved to disk with the + :meth:`save` method.""" + + def __init__(self, rows, info): + """ + .. note :: + + The constructor is not public. Please do not call it. + """ + + self.rows = rows + self.info = info + + def save(self, file): + """Save the image to *file*. If *file* looks like an open file + descriptor then it is used, otherwise it is treated as a + filename and a fresh file is opened. + + In general, you can only call this method once; after it has + been called the first time and the PNG image has been saved, the + source data will have been streamed, and cannot be streamed + again. + """ + + w = Writer(**self.info) + + try: + file.write + + def close(): + pass + + except: + file = open(file, "wb") + + def close(): + file.close() + + try: + w.write(file, self.rows) + finally: + close() + + +class _readable: + """ + A simple file-like interface for strings and arrays. + """ + + def __init__(self, buf): + self.buf = buf + self.offset = 0 + + def read(self, n): + r = self.buf[self.offset : self.offset + n] + if isarray(r): + r = tostring(r) + self.offset += n + return r + + +class Reader: + """ + PNG decoder in pure Python. + """ + + def __init__(self, _guess=None, **kw): + """ + Create a PNG decoder object. + + The constructor expects exactly one keyword argument. If you + supply a positional argument instead, it will guess the input + type. You can choose among the following keyword arguments: + + filename + Name of input file (a PNG file). + file + A file-like object (object with a read() method). + bytes + ``array`` or ``string`` with PNG data. + + """ + if (_guess is not None and len(kw) != 0) or (_guess is None and len(kw) != 1): + raise TypeError("Reader() takes exactly 1 argument") + + # Will be the first 8 bytes, later on. See validate_signature. + self.signature = None + self.transparent = None + # A pair of (len,type) if a chunk has been read but its data and + # checksum have not (in other words the file position is just + # past the 4 bytes that specify the chunk type). See preamble + # method for how this is used. + self.atchunk = None + + if _guess is not None: + if isarray(_guess): + kw["bytes"] = _guess + elif isinstance(_guess, str): + kw["filename"] = _guess + elif isinstance(_guess, io.IOBase): + kw["file"] = _guess + + if "filename" in kw: + self.file = open(kw["filename"], "rb") + elif "file" in kw: + self.file = kw["file"] + elif "bytes" in kw: + self.file = _readable(kw["bytes"]) + else: + raise TypeError("expecting filename, file or bytes array") + + def chunk(self, seek=None): + """ + Read the next PNG chunk from the input file; returns a + (*type*,*data*) tuple. *type* is the chunk's type as a string + (all PNG chunk types are 4 characters long). *data* is the + chunk's data content, as a string. + + If the optional `seek` argument is + specified then it will keep reading chunks until it either runs + out of file or finds the type specified by the argument. Note + that in general the order of chunks in PNGs is unspecified, so + using `seek` can cause you to miss chunks. + """ + + self.validate_signature() + + while True: + # http://www.w3.org/TR/PNG/#5Chunk-layout + if not self.atchunk: + self.atchunk = self.chunklentype() + length, type = self.atchunk + self.atchunk = None + data = self.file.read(length) + if len(data) != length: + raise ChunkError( + "Chunk %s too short for required %i octets." % (type, length) + ) + checksum = self.file.read(4) + if len(checksum) != 4: + raise ValueError("Chunk %s too short for checksum.", checksum) + if seek and type != seek: + continue + verify = zlib.crc32(strtobytes(type)) + verify = zlib.crc32(data, verify) + # Whether the output from zlib.crc32 is signed or not varies + # according to hideous implementation details, see + # http://bugs.python.org/issue1202 . + # We coerce it to be positive here (in a way which works on + # Python 2.3 and older). + verify &= 2**32 - 1 + verify = struct.pack("!I", verify) + if checksum != verify: + # print(repr(checksum)) + (a,) = struct.unpack("!I", checksum) + (b,) = struct.unpack("!I", verify) + raise ChunkError( + f"Checksum error in {type} chunk: 0x{a:08X} != 0x{b:08X}." + ) + return type, data + + def chunks(self): + """Return an iterator that will yield each chunk as a + (*chunktype*, *content*) pair. + """ + + while True: + t, v = self.chunk() + yield t, v + if t == "IEND": + break + + def undo_filter(self, filter_type, scanline, previous): + """Undo the filter for a scanline. `scanline` is a sequence of + bytes that does not include the initial filter type byte. + `previous` is decoded previous scanline (for straightlaced + images this is the previous pixel row, but for interlaced + images, it is the previous scanline in the reduced image, which + in general is not the previous pixel row in the final image). + When there is no previous scanline (the first row of a + straightlaced image, or the first row in one of the passes in an + interlaced image), then this argument should be ``None``. + + The scanline will have the effects of filtering removed, and the + result will be returned as a fresh sequence of bytes. + """ + + # :todo: Would it be better to update scanline in place? + + # Create the result byte array. It seems that the best way to + # create the array to be the right size is to copy from an + # existing sequence. *sigh* + # If we fill the result with scanline, then this allows a + # micro-optimisation in the "null" and "sub" cases. + result = array("B", scanline) + + if filter_type == 0: + # And here, we _rely_ on filling the result with scanline, + # above. + return result + + if filter_type not in (1, 2, 3, 4): + raise FormatError( + "Invalid PNG Filter Type." + " See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." + ) + + # Filter unit. The stride from one pixel to the corresponding + # byte from the previous previous. Normally this is the pixel + # size in bytes, but when this is smaller than 1, the previous + # byte is used instead. + fu = max(1, self.psize) + + # For the first line of a pass, synthesize a dummy previous + # line. An alternative approach would be to observe that on the + # first line 'up' is the same as 'null', 'paeth' is the same + # as 'sub', with only 'average' requiring any special case. + if not previous: + previous = array("B", [0] * len(scanline)) + + def sub(): + """Undo sub filter.""" + + ai = 0 + # Loops starts at index fu. Observe that the initial part + # of the result is already filled in correctly with + # scanline. + for i in range(fu, len(result)): + x = scanline[i] + a = result[ai] + result[i] = (x + a) & 0xFF + ai += 1 + + def up(): + """Undo up filter.""" + for i in range(len(result)): # pylint: disable=consider-using-enumerate + x = scanline[i] + b = previous[i] + result[i] = (x + b) & 0xFF + + def average(): + """Undo average filter.""" + + ai = -fu + for i in range(len(result)): # pylint: disable=consider-using-enumerate + x = scanline[i] + if ai < 0: + a = 0 + else: + a = result[ai] + b = previous[i] + result[i] = (x + ((a + b) >> 1)) & 0xFF + ai += 1 + + def paeth(): + """Undo Paeth filter.""" + + # Also used for ci. + ai = -fu + for i in range(len(result)): # pylint: disable=consider-using-enumerate + x = scanline[i] + if ai < 0: + a = c = 0 + else: + a = result[ai] + c = previous[ai] + b = previous[i] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + result[i] = (x + pr) & 0xFF + ai += 1 + + # Call appropriate filter algorithm. Note that 0 has already + # been dealt with. + (None, sub, up, average, paeth)[filter_type]() + return result + + def deinterlace(self, raw): + """ + Read raw pixel data, undo filters, deinterlace, and flatten. + Return in flat row flat pixel format. + """ + + # print("Reading interlaced, w=%s, r=%s, planes=%s, bpp=%s" + # % (self.width, self.height, self.planes, self.bps, file=sys.stderr)) + # Values per row (of the target image) + vpr = self.width * self.planes + + # Make a result array, and make it big enough. Interleaving + # writes to the output array randomly (well, not quite), so the + # entire output array must be in memory. + fmt = "BH"[self.bitdepth > 8] + a = array(fmt, [0] * vpr * self.height) + source_offset = 0 + + for xstart, ystart, xstep, ystep in _adam7: + # print("Adam7: start=%s,%s step=%s,%s" % ( + # xstart, ystart, xstep, ystep, file=sys.stderr)) + if xstart >= self.width: + continue + # The previous (reconstructed) scanline. None at the + # beginning of a pass to indicate that there is no previous + # line. + recon = None + # Pixels per row (reduced pass image) + ppr = int(math.ceil((self.width - xstart) / float(xstep))) + # Row size in bytes for this pass. + row_size = int(math.ceil(self.psize * ppr)) + for y in range(ystart, self.height, ystep): + filter_type = raw[source_offset] + source_offset += 1 + scanline = raw[source_offset : source_offset + row_size] + source_offset += row_size + recon = self.undo_filter(filter_type, scanline, recon) + # Convert so that there is one element per pixel value + flat = self.serialtoflat(recon, ppr) + if xstep == 1: + assert xstart == 0 + offset = y * vpr + a[offset : offset + vpr] = flat + else: + offset = y * vpr + xstart * self.planes + end_offset = (y + 1) * vpr + skip = self.planes * xstep + for i in range(self.planes): + a[offset + i : end_offset : skip] = flat[i :: self.planes] + return a + + def iterboxed(self, rows): + """Iterator that yields each scanline in boxed row flat pixel + format. `rows` should be an iterator that yields the bytes of + each row in turn. + """ + + def asvalues(raw): + """Convert a row of raw bytes into a flat row. Result may + or may not share with argument""" + + if self.bitdepth == 8: + return raw + if self.bitdepth == 16: + raw = tostring(raw) + return array("H", struct.unpack("!%dH" % (len(raw) // 2), raw)) + assert self.bitdepth < 8 + width = self.width + # Samples per byte + spb = 8 // self.bitdepth + out = array("B") + mask = 2**self.bitdepth - 1 + shifts = map(self.bitdepth.__mul__, reversed(range(spb))) + for o in raw: + out.extend((mask & (o >> i) for i in shifts)) + return out[:width] + + return map(asvalues, rows) + + def serialtoflat(self, bytes, width=None): + """Convert serial format (byte stream) pixel data to flat row + flat pixel. + """ + + if self.bitdepth == 8: + return bytes + if self.bitdepth == 16: + bytes = tostring(bytes) + return array("H", struct.unpack("!%dH" % (len(bytes) // 2), bytes)) + assert self.bitdepth < 8 + if width is None: + width = self.width + # Samples per byte + spb = 8 // self.bitdepth + out = array("B") + mask = 2**self.bitdepth - 1 + shifts = map(self.bitdepth.__mul__, reversed(range(spb))) + l = width + for o in bytes: + out.extend([(mask & (o >> s)) for s in shifts][:l]) + l -= spb + if l <= 0: + l = width + return out + + def iterstraight(self, raw): + """Iterator that undoes the effect of filtering, and yields each + row in serialised format (as a sequence of bytes). Assumes input + is straightlaced. `raw` should be an iterable that yields the + raw bytes in chunks of arbitrary size.""" + + # length of row, in bytes + rb = self.row_bytes + a = array("B") + # The previous (reconstructed) scanline. None indicates first + # line of image. + recon = None + for some in raw: + a.extend(some) + while len(a) >= rb + 1: + filter_type = a[0] + scanline = a[1 : rb + 1] + del a[: rb + 1] + recon = self.undo_filter(filter_type, scanline, recon) + yield recon + if len(a) != 0: + # :file:format We get here with a file format error: when the + # available bytes (after decompressing) do not pack into exact + # rows. + raise FormatError("Wrong size for decompressed IDAT chunk.") + assert len(a) == 0 + + def validate_signature(self): + """If signature (header) has not been read then read and + validate it; otherwise do nothing. + """ + + if self.signature: + return + self.signature = self.file.read(8) + if self.signature != _signature: + raise FormatError("PNG file has invalid signature.") + + def preamble(self): + """ + Extract the image metadata by reading the initial part of the PNG + file up to the start of the ``IDAT`` chunk. All the chunks that + precede the ``IDAT`` chunk are read and either processed for + metadata or discarded. + """ + + self.validate_signature() + + while True: + if not self.atchunk: + self.atchunk = self.chunklentype() + if self.atchunk is None: + raise FormatError("This PNG file has no IDAT chunks.") + if self.atchunk[1] == "IDAT": + return + self.process_chunk() + + def chunklentype(self): + """Reads just enough of the input to determine the next + chunk's length and type, returned as a (*length*, *type*) pair + where *type* is a string. If there are no more chunks, ``None`` + is returned. + """ + + x = self.file.read(8) + if not x: + return None + if len(x) != 8: + raise FormatError("End of file whilst reading chunk length and type.") + length, type = struct.unpack("!I4s", x) + type = bytestostr(type) + if length > 2**31 - 1: + raise FormatError("Chunk %s is too large: %d." % (type, length)) + return length, type + + def process_chunk(self): + """Process the next chunk and its data. This only processes the + following chunk types, all others are ignored: ``IHDR``, + ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``. + """ + + type, data = self.chunk() + if type == "IHDR": + # http://www.w3.org/TR/PNG/#11IHDR + if len(data) != 13: + raise FormatError("IHDR chunk has incorrect length.") + ( + self.width, + self.height, + self.bitdepth, + self.color_type, + self.compression, + self.filter, + self.interlace, + ) = struct.unpack("!2I5B", data) + + # Check that the header specifies only valid combinations. + if self.bitdepth not in (1, 2, 4, 8, 16): + raise Error("invalid bit depth %d" % self.bitdepth) + if self.color_type not in (0, 2, 3, 4, 6): + raise Error("invalid colour type %d" % self.color_type) + # Check indexed (palettized) images have 8 or fewer bits + # per pixel; check only indexed or greyscale images have + # fewer than 8 bits per pixel. + if (self.color_type & 1 and self.bitdepth > 8) or ( + self.bitdepth < 8 and self.color_type not in (0, 3) + ): + raise FormatError( + "Illegal combination of bit depth (%d)" + " and colour type (%d)." + " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." + % (self.bitdepth, self.color_type) + ) + if self.compression != 0: + raise Error("unknown compression method %d" % self.compression) + if self.filter != 0: + raise FormatError( + "Unknown filter method %d," + " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." + % self.filter + ) + if self.interlace not in (0, 1): + raise FormatError( + "Unknown interlace method %d," + " see http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods ." + % self.interlace + ) + + # Derived values + # http://www.w3.org/TR/PNG/#6Colour-values + colormap = bool(self.color_type & 1) + greyscale = not (self.color_type & 2) + alpha = bool(self.color_type & 4) + color_planes = (3, 1)[greyscale or colormap] + planes = color_planes + alpha + + self.colormap = colormap + self.greyscale = greyscale + self.alpha = alpha + self.color_planes = color_planes + self.planes = planes + self.psize = float(self.bitdepth) / float(8) * planes + if int(self.psize) == self.psize: + self.psize = int(self.psize) + self.row_bytes = int(math.ceil(self.width * self.psize)) + # Stores PLTE chunk if present, and is used to check + # chunk ordering constraints. + self.plte = None + # Stores tRNS chunk if present, and is used to check chunk + # ordering constraints. + self.trns = None + # Stores sbit chunk if present. + self.sbit = None + elif type == "PLTE": + # http://www.w3.org/TR/PNG/#11PLTE + if self.plte: + warnings.warn("Multiple PLTE chunks present.") + self.plte = data + if len(data) % 3 != 0: + raise FormatError("PLTE chunk's length should be a multiple of 3.") + if len(data) > (2**self.bitdepth) * 3: + raise FormatError("PLTE chunk is too long.") + if len(data) == 0: + raise FormatError("Empty PLTE is not allowed.") + elif type == "bKGD": + try: + if self.colormap: + if not self.plte: + warnings.warn("PLTE chunk is required before bKGD chunk.") + self.background = struct.unpack("B", data) + else: + self.background = struct.unpack("!%dH" % self.color_planes, data) + except struct.error: + raise FormatError("bKGD chunk has incorrect length.") + elif type == "tRNS": + # http://www.w3.org/TR/PNG/#11tRNS + self.trns = data + if self.colormap: + if not self.plte: + warnings.warn("PLTE chunk is required before tRNS chunk.") + else: + if len(data) > len(self.plte) / 3: + # Was warning, but promoted to Error as it + # would otherwise cause pain later on. + raise FormatError("tRNS chunk is too long.") + else: + if self.alpha: + raise FormatError( + "tRNS chunk is not valid with colour type %d." % self.color_type + ) + try: + self.transparent = struct.unpack("!%dH" % self.color_planes, data) + except struct.error: + raise FormatError("tRNS chunk has incorrect length.") + elif type == "gAMA": + try: + self.gamma = struct.unpack("!L", data)[0] / 100000.0 + except struct.error: + raise FormatError("gAMA chunk has incorrect length.") + elif type == "sBIT": + self.sbit = data + if ( + self.colormap + and len(data) != 3 + or not self.colormap + and len(data) != self.planes + ): + raise FormatError("sBIT chunk has incorrect length.") + + def read(self): + """ + Read the PNG file and decode it. Returns (`width`, `height`, + `pixels`, `metadata`). + + May use excessive memory. + + `pixels` are returned in boxed row flat pixel format. + """ + + def iteridat(): + """Iterator that yields all the ``IDAT`` chunks as strings.""" + while True: + try: + type, data = self.chunk() + except ValueError as e: + raise ChunkError(e.args[0]) + if type == "IEND": + # http://www.w3.org/TR/PNG/#11IEND + break + if type != "IDAT": + continue + # type == 'IDAT' + # http://www.w3.org/TR/PNG/#11IDAT + if self.colormap and not self.plte: + warnings.warn("PLTE chunk is required before IDAT chunk") + yield data + + def iterdecomp(idat): + """Iterator that yields decompressed strings. `idat` should + be an iterator that yields the ``IDAT`` chunk data. + """ + + # Currently, with no max_length parameter to decompress, this + # routine will do one yield per IDAT chunk. So not very + # incremental. + d = zlib.decompressobj() + # Each IDAT chunk is passed to the decompressor, then any + # remaining state is decompressed out. + for data in idat: + # :todo: add a max_length argument here to limit output + # size. + yield array("B", d.decompress(data)) + yield array("B", d.flush()) + + self.preamble() + raw = iterdecomp(iteridat()) + + if self.interlace: + raw = array("B", itertools.chain(*raw)) + arraycode = "BH"[self.bitdepth > 8] + # Like :meth:`group` but producing an array.array object for + # each row. + pixels = map( + lambda *row: array(arraycode, row), + *[iter(self.deinterlace(raw))] * self.width * self.planes, + ) + else: + pixels = self.iterboxed(self.iterstraight(raw)) + meta = {} + for attr in "greyscale alpha planes bitdepth interlace".split(): + meta[attr] = getattr(self, attr) + meta["size"] = (self.width, self.height) + for attr in "gamma transparent background".split(): + a = getattr(self, attr, None) + if a is not None: + meta[attr] = a + return self.width, self.height, pixels, meta + + def read_flat(self): + """ + Read a PNG file and decode it into flat row flat pixel format. + Returns (*width*, *height*, *pixels*, *metadata*). + + May use excessive memory. + + `pixels` are returned in flat row flat pixel format. + + See also the :meth:`read` method which returns pixels in the + more stream-friendly boxed row flat pixel format. + """ + + x, y, pixel, meta = self.read() + arraycode = "BH"[meta["bitdepth"] > 8] + pixel = array(arraycode, itertools.chain(*pixel)) + return x, y, pixel, meta + + def palette(self, alpha="natural"): + """Returns a palette that is a sequence of 3-tuples or 4-tuples, + synthesizing it from the ``PLTE`` and ``tRNS`` chunks. These + chunks should have already been processed (for example, by + calling the :meth:`preamble` method). All the tuples are the + same size: 3-tuples if there is no ``tRNS`` chunk, 4-tuples when + there is a ``tRNS`` chunk. Assumes that the image is colour type + 3 and therefore a ``PLTE`` chunk is required. + + If the `alpha` argument is ``'force'`` then an alpha channel is + always added, forcing the result to be a sequence of 4-tuples. + """ + + if not self.plte: + raise FormatError("Required PLTE chunk is missing in colour type 3 image.") + plte = group(array("B", self.plte), 3) + if self.trns or alpha == "force": + trns = array("B", self.trns or "") + trns.extend([255] * (len(plte) - len(trns))) + plte = map(operator.add, plte, group(trns, 1)) + return plte + + def asDirect(self): + """Returns the image data as a direct representation of an + ``x * y * planes`` array. This method is intended to remove the + need for callers to deal with palettes and transparency + themselves. Images with a palette (colour type 3) + are converted to RGB or RGBA; images with transparency (a + ``tRNS`` chunk) are converted to LA or RGBA as appropriate. + When returned in this format the pixel values represent the + colour value directly without needing to refer to palettes or + transparency information. + + Like the :meth:`read` method this method returns a 4-tuple: + + (*width*, *height*, *pixels*, *meta*) + + This method normally returns pixel values with the bit depth + they have in the source image, but when the source PNG has an + ``sBIT`` chunk it is inspected and can reduce the bit depth of + the result pixels; pixel values will be reduced according to + the bit depth specified in the ``sBIT`` chunk (PNG nerds should + note a single result bit depth is used for all channels; the + maximum of the ones specified in the ``sBIT`` chunk. An RGB565 + image will be rescaled to 6-bit RGB666). + + The *meta* dictionary that is returned reflects the `direct` + format and not the original source image. For example, an RGB + source image with a ``tRNS`` chunk to represent a transparent + colour, will have ``planes=3`` and ``alpha=False`` for the + source image, but the *meta* dictionary returned by this method + will have ``planes=4`` and ``alpha=True`` because an alpha + channel is synthesized and added. + + *pixels* is the pixel data in boxed row flat pixel format (just + like the :meth:`read` method). + + All the other aspects of the image data are not changed. + """ + + self.preamble() + + # Simple case, no conversion necessary. + if not self.colormap and not self.trns and not self.sbit: + return self.read() + + x, y, pixels, meta = self.read() + + if self.colormap: + meta["colormap"] = False + meta["alpha"] = bool(self.trns) + meta["bitdepth"] = 8 + meta["planes"] = 3 + bool(self.trns) + plte = list(self.palette()) + + def iterpal(pixels): + for row in pixels: + row = map(plte.__getitem__, row) + yield array("B", itertools.chain(*row)) + + pixels = iterpal(pixels) + elif self.trns: + # It would be nice if there was some reasonable way of doing + # this without generating a whole load of intermediate tuples. + # But tuples does seem like the easiest way, with no other way + # clearly much simpler or much faster. (Actually, the L to LA + # conversion could perhaps go faster (all those 1-tuples!), but + # I still wonder whether the code proliferation is worth it) + it = self.transparent + maxval = 2 ** meta["bitdepth"] - 1 + planes = meta["planes"] + meta["alpha"] = True + meta["planes"] += 1 + typecode = "BH"[meta["bitdepth"] > 8] + + def itertrns(pixels): + for row in pixels: + # For each row we group it into pixels, then form a + # characterisation vector that says whether each pixel + # is opaque or not. Then we convert True/False to + # 0/maxval (by multiplication), and add it as the extra + # channel. + row = group(row, planes) + opa = map(it.__ne__, row) + opa = map(maxval.__mul__, opa) + opa = zip(opa) # convert to 1-tuples + yield array(typecode, itertools.chain(*map(operator.add, row, opa))) + + pixels = itertrns(pixels) + targetbitdepth = None + if self.sbit: + sbit = struct.unpack(f"{len(self.sbit)}B", self.sbit) + targetbitdepth = max(sbit) + if targetbitdepth > meta["bitdepth"]: + raise Error("sBIT chunk %r exceeds bitdepth %d" % (sbit, self.bitdepth)) + if min(sbit) <= 0: + raise Error(f"sBIT chunk {sbit!r} has a 0-entry") + if targetbitdepth == meta["bitdepth"]: + targetbitdepth = None + if targetbitdepth: + shift = meta["bitdepth"] - targetbitdepth + meta["bitdepth"] = targetbitdepth + + def itershift(pixels): + for row in pixels: + yield map(shift.__rrshift__, row) + + pixels = itershift(pixels) + return x, y, pixels, meta + + def asFloat(self, maxval=1.0): + """Return image pixels as per :meth:`asDirect` method, but scale + all pixel values to be floating point values between 0.0 and + *maxval*. + """ + + x, y, pixels, info = self.asDirect() + sourcemaxval = 2 ** info["bitdepth"] - 1 + del info["bitdepth"] + info["maxval"] = float(maxval) + factor = float(maxval) / float(sourcemaxval) + + def iterfloat(): + for row in pixels: + yield map(factor.__mul__, row) + + return x, y, iterfloat(), info + + def _as_rescale(self, get, targetbitdepth): + """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`.""" + + width, height, pixels, meta = get() + maxval = 2 ** meta["bitdepth"] - 1 + targetmaxval = 2**targetbitdepth - 1 + factor = float(targetmaxval) / float(maxval) + meta["bitdepth"] = targetbitdepth + + def iterscale(): + for row in pixels: + yield (int(round(x * factor)) for x in row) + + return width, height, iterscale(), meta + + def asRGB8(self): + """Return the image data as an RGB pixels with 8-bits per + sample. This is like the :meth:`asRGB` method except that + this method additionally rescales the values so that they + are all between 0 and 255 (8-bit). In the case where the + source image has a bit depth < 8 the transformation preserves + all the information; where the source image has bit depth + > 8, then rescaling to 8-bit values loses precision. No + dithering is performed. Like :meth:`asRGB`, an alpha channel + in the source image will raise an exception. + + This function returns a 4-tuple: + (*width*, *height*, *pixels*, *metadata*). + *width*, *height*, *metadata* are as per the :meth:`read` method. + + *pixels* is the pixel data in boxed row flat pixel format. + """ + + return self._as_rescale(self.asRGB, 8) + + def asRGBA8(self): + """Return the image data as RGBA pixels with 8-bits per + sample. This method is similar to :meth:`asRGB8` and + :meth:`asRGBA`: The result pixels have an alpha channel, *and* + values are rescaled to the range 0 to 255. The alpha channel is + synthesized if necessary (with a small speed penalty). + """ + + return self._as_rescale(self.asRGBA, 8) + + def asRGB(self): + """Return image as RGB pixels. RGB colour images are passed + through unchanged; greyscales are expanded into RGB + triplets (there is a small speed overhead for doing this). + + An alpha channel in the source image will raise an + exception. + + The return values are as for the :meth:`read` method + except that the *metadata* reflect the returned pixels, not the + source image. In particular, for this method + ``metadata['greyscale']`` will be ``False``. + """ + + width, height, pixels, meta = self.asDirect() + if meta["alpha"]: + raise Error("will not convert image with alpha channel to RGB") + if not meta["greyscale"]: + return width, height, pixels, meta + meta["greyscale"] = False + typecode = "BH"[meta["bitdepth"] > 8] + + def iterrgb(): + for row in pixels: + a = array(typecode, [0]) * 3 * width + for i in range(3): + a[i::3] = row + yield a + + return width, height, iterrgb(), meta + + def asRGBA(self): + """Return image as RGBA pixels. Greyscales are expanded into + RGB triplets; an alpha channel is synthesized if necessary. + The return values are as for the :meth:`read` method + except that the *metadata* reflect the returned pixels, not the + source image. In particular, for this method + ``metadata['greyscale']`` will be ``False``, and + ``metadata['alpha']`` will be ``True``. + """ + + width, height, pixels, meta = self.asDirect() + if meta["alpha"] and not meta["greyscale"]: + return width, height, pixels, meta + typecode = "BH"[meta["bitdepth"] > 8] + maxval = 2 ** meta["bitdepth"] - 1 + + def newarray(): + return array(typecode, [0]) * 4 * width + + if meta["alpha"] and meta["greyscale"]: + # LA to RGBA + def convert(): + for row in pixels: + # Create a fresh target row, then copy L channel + # into first three target channels, and A channel + # into fourth channel. + a = newarray() + for i in range(3): + a[i::4] = row[0::2] + a[3::4] = row[1::2] + yield a + + elif meta["greyscale"]: + # L to RGBA + def convert(): + for row in pixels: + a = newarray() + for i in range(3): + a[i::4] = row + a[3::4] = array(typecode, [maxval]) * width + yield a + + else: + assert not meta["alpha"] and not meta["greyscale"] + + # RGB to RGBA + def convert(): + for row in pixels: + a = newarray() + for i in range(3): + a[i::4] = row[i::3] + a[3::4] = array(typecode, [maxval]) * width + yield a + + meta["alpha"] = True + meta["greyscale"] = False + return width, height, convert(), meta + + +# === Internal Test Support === + +# This section comprises the tests that are internally validated (as +# opposed to tests which produce output files that are externally +# validated). Primarily they are unittests. + +# Note that it is difficult to internally validate the results of +# writing a PNG file. The only thing we can do is read it back in +# again, which merely checks consistency, not that the PNG file we +# produce is valid. + +# Run the tests from the command line: +# python -c 'import png;png.test()' + +# (For an in-memory binary file IO object) We use BytesIO where +# available, otherwise we use StringIO, but name it BytesIO. +try: + from io import BytesIO +except: + from StringIO import StringIO as BytesIO +import tempfile +import unittest + + +def test(): + unittest.main(__name__) + + +def topngbytes(name, rows, x, y, **k): + """Convenience function for creating a PNG file "in memory" as a + string. Creates a :class:`Writer` instance using the keyword arguments, + then passes `rows` to its :meth:`Writer.write` method. The resulting + PNG file is returned as a string. `name` is used to identify the file for + debugging. + """ + + import os + + print(name) + f = BytesIO() + w = Writer(x, y, **k) + w.write(f, rows) + if os.environ.get("PYPNG_TEST_TMP"): + w = open(name, "wb") + w.write(f.getvalue()) + w.close() + return f.getvalue() + + +def testWithIO(inp, out, f): + """Calls the function `f` with ``sys.stdin`` changed to `inp` + and ``sys.stdout`` changed to `out`. They are restored when `f` + returns. This function returns whatever `f` returns. + """ + + import os + + try: + oldin, sys.stdin = sys.stdin, inp + oldout, sys.stdout = sys.stdout, out + x = f() + finally: + sys.stdin = oldin + sys.stdout = oldout + if os.environ.get("PYPNG_TEST_TMP") and hasattr(out, "getvalue"): + name = mycallersname() + if name: + w = open(name + ".png", "wb") + w.write(out.getvalue()) + w.close() + return x + + +def mycallersname(): + """Returns the name of the caller of the caller of this function + (hence the name of the caller of the function in which + "mycallersname()" textually appears). Returns None if this cannot + be determined.""" + + # http://docs.python.org/library/inspect.html#the-interpreter-stack + import inspect + + frame = inspect.currentframe() + if not frame: + return None + frame_, filename_, lineno_, funname, linelist_, listi_ = inspect.getouterframes( + frame + )[2] + return funname + + +def seqtobytes(s): + """Convert a sequence of integers to a *bytes* instance. Good for + plastering over Python 2 / Python 3 cracks. + """ + + return strtobytes("".join(chr(x) for x in s)) + + +class Test(unittest.TestCase): + # This member is used by the superclass. If we don't define a new + # class here then when we use self.assertRaises() and the PyPNG code + # raises an assertion then we get no proper traceback. I can't work + # out why, but defining a new class here means we get a proper + # traceback. + class failureException(Exception): + pass + + def helperLN(self, n): + mask = (1 << n) - 1 + # Use small chunk_limit so that multiple chunk writing is + # tested. Making it a test for Issue 20. + w = Writer(15, 17, greyscale=True, bitdepth=n, chunk_limit=99) + f = BytesIO() + w.write_array(f, array("B", map(mask.__and__, range(1, 256)))) + r = Reader(bytes=f.getvalue()) + x, y, pixels, meta = r.read() + self.assertEqual(x, 15) + self.assertEqual(y, 17) + self.assertEqual( + list(itertools.chain(*pixels)), map(mask.__and__, range(1, 256)) + ) + + def testL8(self): + return self.helperLN(8) + + def testL4(self): + return self.helperLN(4) + + def testL2(self): + "Also tests asRGB8." + w = Writer(1, 4, greyscale=True, bitdepth=2) + f = BytesIO() + w.write_array(f, array("B", range(4))) + r = Reader(bytes=f.getvalue()) + x, y, pixels, meta = r.asRGB8() + self.assertEqual(x, 1) + self.assertEqual(y, 4) + for i, row in enumerate(pixels): + self.assertEqual(len(row), 3) + self.assertEqual(list(row), [0x55 * i] * 3) + + def testP2(self): + "2-bit palette." + a = (255, 255, 255) + b = (200, 120, 120) + c = (50, 99, 50) + w = Writer(1, 4, bitdepth=2, palette=[a, b, c]) + f = BytesIO() + w.write_array(f, array("B", (0, 1, 1, 2))) + r = Reader(bytes=f.getvalue()) + x, y, pixels, meta = r.asRGB8() + self.assertEqual(x, 1) + self.assertEqual(y, 4) + self.assertEqual(list(pixels), map(list, [a, b, b, c])) + + def testPtrns(self): + "Test colour type 3 and tRNS chunk (and 4-bit palette)." + a = (50, 99, 50, 50) + b = (200, 120, 120, 80) + c = (255, 255, 255) + d = (200, 120, 120) + e = (50, 99, 50) + w = Writer(3, 3, bitdepth=4, palette=[a, b, c, d, e]) + f = BytesIO() + w.write_array(f, array("B", (4, 3, 2, 3, 2, 0, 2, 0, 1))) + r = Reader(bytes=f.getvalue()) + x, y, pixels, meta = r.asRGBA8() + self.assertEqual(x, 3) + self.assertEqual(y, 3) + c = c + (255,) + d = d + (255,) + e = e + (255,) + boxed = [(e, d, c), (d, c, a), (c, a, b)] + flat = (itertools.chain(*row) for row in boxed) + self.assertEqual(map(list, pixels), map(list, flat)) + + def testRGBtoRGBA(self): + "asRGBA8() on colour type 2 source." "" + # Test for Issue 26 + r = Reader(bytes=_pngsuite["basn2c08"]) + x, y, pixels, meta = r.asRGBA8() + # Test the pixels at row 9 columns 0 and 1. + row9 = list(pixels)[9] + self.assertEqual(row9[0:8], [0xFF, 0xDF, 0xFF, 0xFF, 0xFF, 0xDE, 0xFF, 0xFF]) + + def testLtoRGBA(self): + "asRGBA() on grey source." "" + # Test for Issue 60 + r = Reader(bytes=_pngsuite["basi0g08"]) + x, y, pixels, meta = r.asRGBA() + row9 = list(list(pixels)[9]) + self.assertEqual(row9[0:8], [222, 222, 222, 255, 221, 221, 221, 255]) + + def testCtrns(self): + "Test colour type 2 and tRNS chunk." + # Test for Issue 25 + r = Reader(bytes=_pngsuite["tbrn2c08"]) + x, y, pixels, meta = r.asRGBA8() + # I just happen to know that the first pixel is transparent. + # In particular it should be #7f7f7f00 + row0 = list(pixels)[0] + self.assertEqual(tuple(row0[0:4]), (0x7F, 0x7F, 0x7F, 0x00)) + + def testAdam7read(self): + """Adam7 interlace reading. + Specifically, test that for images in the PngSuite that + have both an interlaced and straightlaced pair that both + images from the pair produce the same array of pixels.""" + for candidate in _pngsuite: + if not candidate.startswith("basn"): + continue + candi = candidate.replace("n", "i") + if candi not in _pngsuite: + continue + print(f"adam7 read {candidate}") + straight = Reader(bytes=_pngsuite[candidate]) + adam7 = Reader(bytes=_pngsuite[candi]) + # Just compare the pixels. Ignore x,y (because they're + # likely to be correct?); metadata is ignored because the + # "interlace" member differs. Lame. + straight = straight.read()[2] + adam7 = adam7.read()[2] + self.assertEqual(map(list, straight), map(list, adam7)) + + def testAdam7write(self): + """Adam7 interlace writing. + For each test image in the PngSuite, write an interlaced + and a straightlaced version. Decode both, and compare results. + """ + # Not such a great test, because the only way we can check what + # we have written is to read it back again. + + for name, bytes in _pngsuite.items(): + # Only certain colour types supported for this test. + if name[3:5] not in ["n0", "n2", "n4", "n6"]: + continue + it = Reader(bytes=bytes) + x, y, pixels, meta = it.read() + pngi = topngbytes( + f"adam7wn{name}.png", + pixels, + x=x, + y=y, + bitdepth=it.bitdepth, + greyscale=it.greyscale, + alpha=it.alpha, + transparent=it.transparent, + interlace=False, + ) + x, y, ps, meta = Reader(bytes=pngi).read() + it = Reader(bytes=bytes) + x, y, pixels, meta = it.read() + pngs = topngbytes( + f"adam7wi{name}.png", + pixels, + x=x, + y=y, + bitdepth=it.bitdepth, + greyscale=it.greyscale, + alpha=it.alpha, + transparent=it.transparent, + interlace=True, + ) + x, y, pi, meta = Reader(bytes=pngs).read() + self.assertEqual(map(list, ps), map(list, pi)) + + def testPGMin(self): + """Test that the command line tool can read PGM files.""" + + def do(): + return _main(["testPGMin"]) + + s = BytesIO() + s.write(strtobytes("P5 2 2 3\n")) + s.write(strtobytes("\x00\x01\x02\x03")) + s.flush() + s.seek(0) + o = BytesIO() + testWithIO(s, o, do) + r = Reader(bytes=o.getvalue()) + x, y, pixels, meta = r.read() + self.assertTrue(r.greyscale) + self.assertEqual(r.bitdepth, 2) + + def testPAMin(self): + """Test that the command line tool can read PAM file.""" + + def do(): + return _main(["testPAMin"]) + + s = BytesIO() + s.write( + strtobytes( + "P7\nWIDTH 3\nHEIGHT 1\nDEPTH 4\nMAXVAL 255\n" + "TUPLTYPE RGB_ALPHA\nENDHDR\n" + ) + ) + # The pixels in flat row flat pixel format + flat = [255, 0, 0, 255, 0, 255, 0, 120, 0, 0, 255, 30] + asbytes = seqtobytes(flat) + s.write(asbytes) + s.flush() + s.seek(0) + o = BytesIO() + testWithIO(s, o, do) + r = Reader(bytes=o.getvalue()) + x, y, pixels, meta = r.read() + self.assertTrue(r.alpha) + self.assertTrue(not r.greyscale) + self.assertEqual(list(itertools.chain(*pixels)), flat) + + def testLA4(self): + """Create an LA image with bitdepth 4.""" + bytes = topngbytes( + "la4.png", [[5, 12]], 1, 1, greyscale=True, alpha=True, bitdepth=4 + ) + sbit = Reader(bytes=bytes).chunk("sBIT")[1] + self.assertEqual(sbit, strtobytes("\x04\x04")) + + def testPNMsbit(self): + """Test that PNM files can generates sBIT chunk.""" + + def do(): + return _main(["testPNMsbit"]) + + s = BytesIO() + s.write(strtobytes("P6 8 1 1\n")) + for pixel in range(8): + s.write(struct.pack(" 255: + a = array("H") + else: + a = array("B") + fw = float(width) + fh = float(height) + pfun = test_patterns[pattern] + for y in range(height): + fy = float(y) / fh + for x in range(width): + a.append(int(round(pfun(float(x) / fw, fy) * maxval))) + return a + + def test_rgba(size=256, bitdepth=8, red="GTB", green="GLR", blue="RTL", alpha=None): + """ + Create a test image. Each channel is generated from the + specified pattern; any channel apart from red can be set to + None, which will cause it not to be in the image. It + is possible to create all PNG channel types (L, RGB, LA, RGBA), + as well as non PNG channel types (RGA, and so on). + """ + + i = test_pattern(size, size, bitdepth, red) + psize = 1 + for channel in (green, blue, alpha): + if channel: + c = test_pattern(size, size, bitdepth, channel) + i = interleave_planes(i, c, psize, 1) + psize += 1 + return i + + def pngsuite_image(name): + """ + Create a test image by reading an internal copy of the files + from the PngSuite. Returned in flat row flat pixel format. + """ + + if name not in _pngsuite: + raise NotImplementedError( + f"cannot find PngSuite file {name} (use -L for a list)" + ) + r = Reader(bytes=_pngsuite[name]) + w, h, pixels, meta = r.asDirect() + assert w == h + # LAn for n < 8 is a special case for which we need to rescale + # the data. + if meta["greyscale"] and meta["alpha"] and meta["bitdepth"] < 8: + factor = 255 // (2 ** meta["bitdepth"] - 1) + + def rescale(data): + for row in data: + yield map(factor.__mul__, row) + + pixels = rescale(pixels) + meta["bitdepth"] = 8 + arraycode = "BH"[meta["bitdepth"] > 8] + return w, array(arraycode, itertools.chain(*pixels)), meta + + # The body of test_suite() + size = 256 + if options.test_size: + size = options.test_size + options.bitdepth = options.test_depth + options.greyscale = bool(options.test_black) + + kwargs = {} + if options.test_red: + kwargs["red"] = options.test_red + if options.test_green: + kwargs["green"] = options.test_green + if options.test_blue: + kwargs["blue"] = options.test_blue + if options.test_alpha: + kwargs["alpha"] = options.test_alpha + if options.greyscale: + if options.test_red or options.test_green or options.test_blue: + raise ValueError( + "cannot specify colours (R, G, B) when greyscale image (black channel, K) is specified" + ) + kwargs["red"] = options.test_black + kwargs["green"] = None + kwargs["blue"] = None + options.alpha = bool(options.test_alpha) + if not args: + pixels = test_rgba(size, options.bitdepth, **kwargs) + else: + size, pixels, meta = pngsuite_image(args[0]) + for k in ["bitdepth", "alpha", "greyscale"]: + setattr(options, k, meta[k]) + + writer = Writer( + size, + size, + bitdepth=options.bitdepth, + transparent=options.transparent, + background=options.background, + gamma=options.gamma, + greyscale=options.greyscale, + alpha=options.alpha, + compression=options.compression, + interlace=options.interlace, + ) + writer.write_array(sys.stdout, pixels) + + +def read_pam_header(infile): + """ + Read (the rest of a) PAM header. `infile` should be positioned + immediately after the initial 'P7' line (at the beginning of the + second line). Returns are as for `read_pnm_header`. + """ + + # Unlike PBM, PGM, and PPM, we can read the header a line at a time. + header = {} + while True: + l = infile.readline().strip() + if l == strtobytes("ENDHDR"): + break + if not l: + raise EOFError("PAM ended prematurely") + if l[0] == strtobytes("#"): + continue + l = l.split(None, 1) + if l[0] not in header: + header[l[0]] = l[1] + else: + header[l[0]] += strtobytes(" ") + l[1] + + required = ["WIDTH", "HEIGHT", "DEPTH", "MAXVAL"] + required = [strtobytes(x) for x in required] + WIDTH, HEIGHT, DEPTH, MAXVAL = required + present = [x for x in required if x in header] + if len(present) != len(required): + raise Error("PAM file must specify WIDTH, HEIGHT, DEPTH, and MAXVAL") + width = int(header[WIDTH]) + height = int(header[HEIGHT]) + depth = int(header[DEPTH]) + maxval = int(header[MAXVAL]) + if width <= 0 or height <= 0 or depth <= 0 or maxval <= 0: + raise Error("WIDTH, HEIGHT, DEPTH, MAXVAL must all be positive integers") + return "P7", width, height, depth, maxval + + +def read_pnm_header(infile, supported=("P5", "P6")): + """ + Read a PNM header, returning (format,width,height,depth,maxval). + `width` and `height` are in pixels. `depth` is the number of + channels in the image; for PBM and PGM it is synthesized as 1, for + PPM as 3; for PAM images it is read from the header. `maxval` is + synthesized (as 1) for PBM images. + """ + + # Generally, see http://netpbm.sourceforge.net/doc/ppm.html + # and http://netpbm.sourceforge.net/doc/pam.html + + supported = [strtobytes(x) for x in supported] + + # Technically 'P7' must be followed by a newline, so by using + # rstrip() we are being liberal in what we accept. I think this + # is acceptable. + type = infile.read(3).rstrip() + if type not in supported: + raise NotImplementedError(f"file format {type} not supported") + if type == strtobytes("P7"): + # PAM header parsing is completely different. + return read_pam_header(infile) + # Expected number of tokens in header (3 for P4, 4 for P6) + expected = 4 + pbm = ("P1", "P4") + if type in pbm: + expected = 3 + header = [type] + + # We have to read the rest of the header byte by byte because the + # final whitespace character (immediately following the MAXVAL in + # the case of P6) may not be a newline. Of course all PNM files in + # the wild use a newline at this point, so it's tempting to use + # readline; but it would be wrong. + def getc(): + c = infile.read(1) + if not c: + raise Error("premature EOF reading PNM header") + return c + + c = getc() + while True: + # Skip whitespace that precedes a token. + while c.isspace(): + c = getc() + # Skip comments. + while c == "#": + while c not in "\n\r": + c = getc() + if not c.isdigit(): + raise Error(f"unexpected character {c} found in header") + # According to the specification it is legal to have comments + # that appear in the middle of a token. + # This is bonkers; I've never seen it; and it's a bit awkward to + # code good lexers in Python (no goto). So we break on such + # cases. + token = strtobytes("") + while c.isdigit(): + token += c + c = getc() + # Slight hack. All "tokens" are decimal integers, so convert + # them here. + header.append(int(token)) + if len(header) == expected: + break + # Skip comments (again) + while c == "#": + while c not in "\n\r": + c = getc() + if not c.isspace(): + raise Error(f"expected header to end with whitespace, not {c}") + + if type in pbm: + # synthesize a MAXVAL + header.append(1) + depth = (1, 3)[type == strtobytes("P6")] + return header[0], header[1], header[2], depth, header[3] + + +def write_pnm(file, width, height, pixels, meta): + """Write a Netpbm PNM/PAM file.""" + + bitdepth = meta["bitdepth"] + maxval = 2**bitdepth - 1 + # Rudely, the number of image planes can be used to determine + # whether we are L (PGM), LA (PAM), RGB (PPM), or RGBA (PAM). + planes = meta["planes"] + # Can be an assert as long as we assume that pixels and meta came + # from a PNG file. + assert planes in (1, 2, 3, 4) + if planes in (1, 3): + if 1 == planes: + # PGM + # Could generate PBM if maxval is 1, but we don't (for one + # thing, we'd have to convert the data, not just blat it + # out). + fmt = "P5" + else: + # PPM + fmt = "P6" + file.write("%s %d %d %d\n" % (fmt, width, height, maxval)) + if planes in (2, 4): + # PAM + # See http://netpbm.sourceforge.net/doc/pam.html + if 2 == planes: + tupltype = "GRAYSCALE_ALPHA" + else: + tupltype = "RGB_ALPHA" + file.write( + "P7\nWIDTH %d\nHEIGHT %d\nDEPTH %d\nMAXVAL %d\n" + "TUPLTYPE %s\nENDHDR\n" % (width, height, planes, maxval, tupltype) + ) + # Values per row + vpr = planes * width + # struct format + fmt = ">%d" % vpr + if maxval > 0xFF: + fmt = fmt + "H" + else: + fmt = fmt + "B" + for row in pixels: + file.write(struct.pack(fmt, *row)) + file.flush() + + +def color_triple(color): + """ + Convert a command line colour value to a RGB triple of integers. + FIXME: Somewhere we need support for greyscale backgrounds etc. + """ + if color.startswith("#") and len(color) == 4: + return (int(color[1], 16), int(color[2], 16), int(color[3], 16)) + if color.startswith("#") and len(color) == 7: + return (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)) + elif color.startswith("#") and len(color) == 13: + return (int(color[1:5], 16), int(color[5:9], 16), int(color[9:13], 16)) + + +def _main(argv): + """ + Run the PNG encoder with options from the command line. + """ + + # Parse command line arguments + from optparse import OptionParser + import re + + version = "%prog " + re.sub(r"( ?\$|URL: |Rev:)", "", __version__) + parser = OptionParser(version=version) + parser.set_usage("%prog [options] [imagefile]") + parser.add_option( + "-r", + "--read-png", + default=False, + action="store_true", + help="Read PNG, write PNM", + ) + parser.add_option( + "-i", + "--interlace", + default=False, + action="store_true", + help="create an interlaced PNG file (Adam7)", + ) + parser.add_option( + "-t", + "--transparent", + action="store", + type="string", + metavar="color", + help="mark the specified colour (#RRGGBB) as transparent", + ) + parser.add_option( + "-b", + "--background", + action="store", + type="string", + metavar="color", + help="save the specified background colour", + ) + parser.add_option( + "-a", + "--alpha", + action="store", + type="string", + metavar="pgmfile", + help="alpha channel transparency (RGBA)", + ) + parser.add_option( + "-g", + "--gamma", + action="store", + type="float", + metavar="value", + help="save the specified gamma value", + ) + parser.add_option( + "-c", + "--compression", + action="store", + type="int", + metavar="level", + help="zlib compression level (0-9)", + ) + parser.add_option( + "-T", + "--test", + default=False, + action="store_true", + help="create a test image (a named PngSuite image if an argument is supplied)", + ) + parser.add_option( + "-L", + "--list", + default=False, + action="store_true", + help="print list of named test images", + ) + parser.add_option( + "-R", + "--test-red", + action="store", + type="string", + metavar="pattern", + help="test pattern for the red image layer", + ) + parser.add_option( + "-G", + "--test-green", + action="store", + type="string", + metavar="pattern", + help="test pattern for the green image layer", + ) + parser.add_option( + "-B", + "--test-blue", + action="store", + type="string", + metavar="pattern", + help="test pattern for the blue image layer", + ) + parser.add_option( + "-A", + "--test-alpha", + action="store", + type="string", + metavar="pattern", + help="test pattern for the alpha image layer", + ) + parser.add_option( + "-K", + "--test-black", + action="store", + type="string", + metavar="pattern", + help="test pattern for greyscale image", + ) + parser.add_option( + "-d", + "--test-depth", + default=8, + action="store", + type="int", + metavar="NBITS", + help="create test PNGs that are NBITS bits per channel", + ) + parser.add_option( + "-S", + "--test-size", + action="store", + type="int", + metavar="size", + help="width and height of the test image", + ) + (options, args) = parser.parse_args(args=argv[1:]) + + # Convert options + if options.transparent is not None: + options.transparent = color_triple(options.transparent) + if options.background is not None: + options.background = color_triple(options.background) + + if options.list: + names = list(_pngsuite) + names.sort() + for name in names: + print(name) + return + + # Run regression tests + if options.test: + return test_suite(options, args) + + # Prepare input and output files + if len(args) == 0: + infilename = "-" + infile = sys.stdin + elif len(args) == 1: + infilename = args[0] + infile = open(infilename, "rb") + else: + parser.error("more than one input file") + outfile = sys.stdout + + if options.read_png: + # Encode PNG to PPM + png = Reader(file=infile) + width, height, pixels, meta = png.asDirect() + write_pnm(outfile, width, height, pixels, meta) + else: + # Encode PNM to PNG + format, width, height, depth, maxval = read_pnm_header( + infile, ("P5", "P6", "P7") + ) + # When it comes to the variety of input formats, we do something + # rather rude. Observe that L, LA, RGB, RGBA are the 4 colour + # types supported by PNG and that they correspond to 1, 2, 3, 4 + # channels respectively. So we use the number of channels in + # the source image to determine which one we have. We do not + # care about TUPLTYPE. + greyscale = depth <= 2 + pamalpha = depth in (2, 4) + supported = (2**x - 1 for x in range(1, 17)) + try: + mi = supported.index(maxval) + except ValueError: + raise NotImplementedError( + f"your maxval ({maxval}) not in supported list {str(supported)}" + ) + bitdepth = mi + 1 + writer = Writer( + width, + height, + greyscale=greyscale, + bitdepth=bitdepth, + interlace=options.interlace, + transparent=options.transparent, + background=options.background, + alpha=bool(pamalpha or options.alpha), + gamma=options.gamma, + compression=options.compression, + ) + if options.alpha: + pgmfile = open(options.alpha, "rb") + format, awidth, aheight, adepth, amaxval = read_pnm_header(pgmfile, "P5") + if amaxval != "255": + raise NotImplementedError( + f"maxval {amaxval} not supported for alpha channel" + ) + if (awidth, aheight) != (width, height): + raise ValueError( + "alpha channel image size mismatch" + " (%s has %sx%s but %s has %sx%s)" + % (infilename, width, height, options.alpha, awidth, aheight) + ) + writer.convert_ppm_and_pgm(infile, pgmfile, outfile) + else: + writer.convert_pnm(infile, outfile) + + +if __name__ == "__main__": + try: + _main(sys.argv) + except Error as e: + sys.stderr.write(f"{e}\n") diff --git a/laplas/abstract_map/pygame/tests/test_utils/run_tests.py b/laplas/abstract_map/pygame/tests/test_utils/run_tests.py new file mode 100644 index 0000000..69c1abb --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/run_tests.py @@ -0,0 +1,350 @@ +import sys + +if __name__ == "__main__": + raise RuntimeError("This module is for import only") +test_pkg_name = ".".join(__name__.split(".")[0:-2]) +is_pygame_pkg = test_pkg_name == "pygame.tests" +test_runner_mod = test_pkg_name + ".test_utils.test_runner" + +if is_pygame_pkg: + from pygame.tests.test_utils import import_submodule + from pygame.tests.test_utils.test_runner import ( + prepare_test_env, + run_test, + combine_results, + get_test_results, + TEST_RESULTS_START, + ) +else: + from test.test_utils import import_submodule + from test.test_utils.test_runner import ( + prepare_test_env, + run_test, + combine_results, + get_test_results, + TEST_RESULTS_START, + ) +import pygame +import pygame.threads + +import os +import re +import shutil +import tempfile +import time +import random +from pprint import pformat + +was_run = False + + +def run(*args, **kwds): + """Run the Pygame unit test suite and return (total tests run, fails dict) + + Positional arguments (optional): + The names of tests to include. If omitted then all tests are run. Test + names need not include the trailing '_test'. + + Keyword arguments: + incomplete - fail incomplete tests (default False) + usesubprocess - run all test suites in the current process + (default False, use separate subprocesses) + dump - dump failures/errors as dict ready to eval (default False) + file - if provided, the name of a file into which to dump failures/errors + timings - if provided, the number of times to run each individual test to + get an average run time (default is run each test once) + exclude - A list of TAG names to exclude from the run. The items may be + comma or space separated. + show_output - show silenced stderr/stdout on errors (default False) + all - dump all results, not just errors (default False) + randomize - randomize order of tests (default False) + seed - if provided, a seed randomizer integer + multi_thread - if provided, the number of THREADS in which to run + subprocessed tests + time_out - if subprocess is True then the time limit in seconds before + killing a test (default 30) + fake - if provided, the name of the fake tests package in the + run_tests__tests subpackage to run instead of the normal + Pygame tests + python - the path to a python executable to run subprocessed tests + (default sys.executable) + interactive - allow tests tagged 'interactive'. + + Return value: + A tuple of total number of tests run, dictionary of error information. The + dictionary is empty if no errors were recorded. + + By default individual test modules are run in separate subprocesses. This + recreates normal Pygame usage where pygame.init() and pygame.quit() are + called only once per program execution, and avoids unfortunate + interactions between test modules. Also, a time limit is placed on test + execution, so frozen tests are killed when there time allotment expired. + Use the single process option if threading is not working properly or if + tests are taking too long. It is not guaranteed that all tests will pass + in single process mode. + + Tests are run in a randomized order if the randomize argument is True or a + seed argument is provided. If no seed integer is provided then the system + time is used. + + Individual test modules may have a corresponding *_tags.py module, + defining a __tags__ attribute, a list of tag strings used to selectively + omit modules from a run. By default only the 'interactive', 'ignore', and + 'subprocess_ignore' tags are ignored. 'interactive' is for modules that + take user input, like cdrom_test.py. 'ignore' and 'subprocess_ignore' for + for disabling modules for foreground and subprocess modes respectively. + These are for disabling tests on optional modules or for experimental + modules with known problems. These modules can be run from the console as + a Python program. + + This function can only be called once per Python session. It is not + reentrant. + + """ + + global was_run + + if was_run: + raise RuntimeError("run() was already called this session") + was_run = True + + options = kwds.copy() + option_usesubprocess = options.get("usesubprocess", False) + option_dump = options.pop("dump", False) + option_file = options.pop("file", None) + option_randomize = options.get("randomize", False) + option_seed = options.get("seed", None) + option_multi_thread = options.pop("multi_thread", 1) + option_time_out = options.pop("time_out", 120) + option_fake = options.pop("fake", None) + option_python = options.pop("python", sys.executable) + option_exclude = options.pop("exclude", ()) + option_interactive = options.pop("interactive", False) + + if not option_interactive and "interactive" not in option_exclude: + option_exclude += ("interactive",) + if option_usesubprocess and "subprocess_ignore" not in option_exclude: + option_exclude += ("subprocess_ignore",) + elif "ignore" not in option_exclude: + option_exclude += ("ignore",) + + option_exclude += ("python3_ignore",) + option_exclude += ("SDL2_ignore",) + + main_dir, test_subdir, fake_test_subdir = prepare_test_env() + + ########################################################################### + # Compile a list of test modules. If fake, then compile list of fake + # xxxx_test.py from run_tests__tests + + TEST_MODULE_RE = re.compile(r"^(.+_test)\.py$") + + test_mods_pkg_name = test_pkg_name + + working_dir_temp = tempfile.mkdtemp() + + if option_fake is not None: + test_mods_pkg_name = ".".join( + [test_mods_pkg_name, "run_tests__tests", option_fake] + ) + test_subdir = os.path.join(fake_test_subdir, option_fake) + working_dir = test_subdir + else: + working_dir = working_dir_temp + + # Added in because some machines will need os.environ else there will be + # false failures in subprocess mode. Same issue as python2.6. Needs some + # env vars. + + test_env = os.environ + + fmt1 = "%s.%%s" % test_mods_pkg_name + fmt2 = "%s.%%s_test" % test_mods_pkg_name + if args: + test_modules = [m.endswith("_test") and (fmt1 % m) or (fmt2 % m) for m in args] + else: + test_modules = [] + for f in sorted(os.listdir(test_subdir)): + for match in TEST_MODULE_RE.findall(f): + test_modules.append(fmt1 % (match,)) + + ########################################################################### + # Remove modules to be excluded. + + tmp = test_modules + test_modules = [] + for name in tmp: + tag_module_name = f"{name[0:-5]}_tags" + try: + tag_module = import_submodule(tag_module_name) + except ImportError: + test_modules.append(name) + else: + try: + tags = tag_module.__tags__ + except AttributeError: + print(f"{tag_module_name} has no tags: ignoring") + test_modules.append(name) + else: + for tag in tags: + if tag in option_exclude: + print(f"skipping {name} (tag '{tag}')") + break + else: + test_modules.append(name) + del tmp, tag_module_name, name + + ########################################################################### + # Meta results + + results = {} + meta_results = {"__meta__": {}} + meta = meta_results["__meta__"] + + ########################################################################### + # Randomization + + if option_randomize or option_seed is not None: + if option_seed is None: + option_seed = time.time() + meta["random_seed"] = option_seed + print(f"\nRANDOM SEED USED: {option_seed}\n") + random.seed(option_seed) + random.shuffle(test_modules) + + ########################################################################### + # Single process mode + + if not option_usesubprocess: + options["exclude"] = option_exclude + t = time.time() + for module in test_modules: + results.update(run_test(module, **options)) + t = time.time() - t + + ########################################################################### + # Subprocess mode + # + + else: + if is_pygame_pkg: + from pygame.tests.test_utils.async_sub import proc_in_time_or_kill + else: + from test.test_utils.async_sub import proc_in_time_or_kill + + pass_on_args = ["--exclude", ",".join(option_exclude)] + [ + "--" + field + for field in ("randomize", "incomplete", "unbuffered", "verbosity") + if kwds.get(field) + ] + + def sub_test(module): + print(f"loading {module}") + + cmd = [option_python, "-m", test_runner_mod, module] + pass_on_args + + return ( + module, + (cmd, test_env, working_dir), + proc_in_time_or_kill( + cmd, option_time_out, env=test_env, wd=working_dir + ), + ) + + if option_multi_thread > 1: + + def tmap(f, args): + return pygame.threads.tmap( + f, args, stop_on_error=False, num_workers=option_multi_thread + ) + + else: + tmap = map + + t = time.time() + + for module, cmd, (return_code, raw_return) in tmap(sub_test, test_modules): + test_file = f"{os.path.join(test_subdir, module)}.py" + cmd, test_env, working_dir = cmd + + test_results = get_test_results(raw_return) + if test_results: + results.update(test_results) + else: + results[module] = {} + + results[module].update( + { + "return_code": return_code, + "raw_return": raw_return, + "cmd": cmd, + "test_file": test_file, + "test_env": test_env, + "working_dir": working_dir, + "module": module, + } + ) + + t = time.time() - t + + ########################################################################### + # Output Results + # + + untrusty_total, combined = combine_results(results, t) + total, n_errors, n_failures = count_results(results) + + meta["total_tests"] = total + meta["combined"] = combined + meta["total_errors"] = n_errors + meta["total_failures"] = n_failures + results.update(meta_results) + + if not option_usesubprocess and total != untrusty_total: + raise AssertionError( + "Something went wrong in the Test Machinery:\n" + "total: %d != untrusty_total: %d" % (total, untrusty_total) + ) + + if not option_dump: + print(combined) + else: + print(TEST_RESULTS_START) + print(pformat(results)) + + if option_file is not None: + results_file = open(option_file, "w") + try: + results_file.write(pformat(results)) + finally: + results_file.close() + + shutil.rmtree(working_dir_temp) + + return total, n_errors + n_failures + + +def count_results(results): + total = errors = failures = 0 + for result in results.values(): + if result.get("return_code", 0): + total += 1 + errors += 1 + else: + total += result["num_tests"] + errors += result["num_errors"] + failures += result["num_failures"] + + return total, errors, failures + + +def run_and_exit(*args, **kwargs): + """Run the tests, and if there are failures, exit with a return code of 1. + + This is needed for various buildbots to recognise that the tests have + failed. + """ + total, fails = run(*args, **kwargs) + if fails: + sys.exit(1) + sys.exit(0) diff --git a/laplas/abstract_map/pygame/tests/test_utils/test_machinery.py b/laplas/abstract_map/pygame/tests/test_utils/test_machinery.py new file mode 100644 index 0000000..0531cc2 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/test_machinery.py @@ -0,0 +1,89 @@ +import inspect +import random +import re +import unittest + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from . import import_submodule + + +class PygameTestLoader(unittest.TestLoader): + def __init__( + self, randomize_tests=False, include_incomplete=False, exclude=("interactive",) + ): + super().__init__() + self.randomize_tests = randomize_tests + + if exclude is None: + self.exclude = set() + else: + self.exclude = set(exclude) + + if include_incomplete: + self.testMethodPrefix = ("test", "todo_") + + def getTestCaseNames(self, testCaseClass): + res = [] + for name in super().getTestCaseNames(testCaseClass): + tags = get_tags(testCaseClass, getattr(testCaseClass, name)) + if self.exclude.isdisjoint(tags): + res.append(name) + + if self.randomize_tests: + random.shuffle(res) + + return res + + +# Exclude by tags: + +TAGS_RE = re.compile(r"\|[tT]ags:(-?[ a-zA-Z,0-9_\n]+)\|", re.M) + + +class TestTags: + def __init__(self): + self.memoized = {} + self.parent_modules = {} + + def get_parent_module(self, class_): + if class_ not in self.parent_modules: + self.parent_modules[class_] = import_submodule(class_.__module__) + return self.parent_modules[class_] + + def __call__(self, parent_class, meth): + key = (parent_class, meth.__name__) + if key not in self.memoized: + parent_module = self.get_parent_module(parent_class) + + module_tags = getattr(parent_module, "__tags__", []) + class_tags = getattr(parent_class, "__tags__", []) + + tags = TAGS_RE.search(inspect.getdoc(meth) or "") + if tags: + test_tags = [t.strip() for t in tags.group(1).split(",")] + else: + test_tags = [] + + combined = set() + for tags in (module_tags, class_tags, test_tags): + if not tags: + continue + + add = {t for t in tags if not t.startswith("-")} + remove = {t[1:] for t in tags if t not in add} + + if add: + combined.update(add) + if remove: + combined.difference_update(remove) + + self.memoized[key] = combined + + return self.memoized[key] + + +get_tags = TestTags() diff --git a/laplas/abstract_map/pygame/tests/test_utils/test_runner.py b/laplas/abstract_map/pygame/tests/test_utils/test_runner.py new file mode 100644 index 0000000..a19d7b0 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/test_utils/test_runner.py @@ -0,0 +1,324 @@ +import sys +import os + +if __name__ == "__main__": + pkg_dir = os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + parent_dir, pkg_name = os.path.split(pkg_dir) + is_pygame_pkg = pkg_name == "tests" and os.path.split(parent_dir)[1] == "pygame" + if not is_pygame_pkg: + sys.path.insert(0, parent_dir) +else: + is_pygame_pkg = __name__.startswith("pygame.tests.") + +import io +import optparse +import re +import unittest +from pprint import pformat + +from .test_machinery import PygameTestLoader + + +def prepare_test_env(): + test_subdir = os.path.split(os.path.split(os.path.abspath(__file__))[0])[0] + main_dir = os.path.split(test_subdir)[0] + sys.path.insert(0, test_subdir) + fake_test_subdir = os.path.join(test_subdir, "run_tests__tests") + return main_dir, test_subdir, fake_test_subdir + + +main_dir, test_subdir, fake_test_subdir = prepare_test_env() + +################################################################################ +# Set the command line options +# +# options are shared with run_tests.py so make sure not to conflict +# in time more will be added here + +TAG_PAT = r"-?[a-zA-Z0-9_]+" +TAG_RE = re.compile(TAG_PAT) +EXCLUDE_RE = re.compile(rf"({TAG_PAT},?\s*)+$") + + +def exclude_callback(option, opt, value, parser): + if EXCLUDE_RE.match(value) is None: + raise optparse.OptionValueError(f"{opt} argument has invalid value") + parser.values.exclude = TAG_RE.findall(value) + + +opt_parser = optparse.OptionParser() + +opt_parser.add_option( + "-i", "--incomplete", action="store_true", help="fail incomplete tests" +) + +opt_parser.add_option( + "-s", + "--usesubprocess", + action="store_true", + help="run everything in a single process " " (default: use no subprocesses)", +) + +opt_parser.add_option( + "-e", + "--exclude", + action="callback", + type="string", + help="exclude tests containing any of TAGS", + callback=exclude_callback, +) + +opt_parser.add_option( + "-u", + "--unbuffered", + action="store_true", + help="Show stdout/stderr as tests run, rather than storing it and showing on failures", +) + +opt_parser.add_option( + "-v", + "--verbose", + dest="verbosity", + action="store_const", + const=2, + help="Verbose output", +) +opt_parser.add_option( + "-q", + "--quiet", + dest="verbosity", + action="store_const", + const=0, + help="Quiet output", +) + +opt_parser.add_option( + "-r", "--randomize", action="store_true", help="randomize order of tests" +) + +################################################################################ +# If an xxxx_test.py takes longer than TIME_OUT seconds it will be killed +# This is only the default, can be over-ridden on command line + +TIME_OUT = 30 + +# DEFAULTS + +################################################################################ +# Human readable output +# + +COMPLETE_FAILURE_TEMPLATE = """ +====================================================================== +ERROR: all_tests_for (%(module)s.AllTestCases) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "test/%(module)s.py", line 1, in all_tests_for +subprocess completely failed with return code of %(return_code)s +cmd: %(cmd)s +test_env: %(test_env)s +working_dir: %(working_dir)s +return (first 10 and last 10 lines): +%(raw_return)s + +""" # Leave that last empty line else build page regex won't match +# Text also needs to be vertically compressed + + +RAN_TESTS_DIV = (70 * "-") + "\nRan" + +DOTS = re.compile("^([FE.sux]*)$", re.MULTILINE) + + +def extract_tracebacks(output): + """from test runner output return the tracebacks.""" + verbose_mode = " ..." in output + + if verbose_mode: + if "ERROR" in output or "FAILURE" in output: + return "\n\n==".join(output.split("\n\n==")[1:]) + else: + dots = DOTS.search(output).group(1) + if "E" in dots or "F" in dots: + return output[len(dots) + 1 :].split(RAN_TESTS_DIV)[0] + return "" + + +def output_into_dots(output): + """convert the test runner output into dots.""" + # verbose_mode = ") ..." in output + verbose_mode = " ..." in output + + if verbose_mode: + # a map from the verbose output to the dots output. + reasons = { + "... ERROR": "E", + "... unexpected success": "u", + "... skipped": "s", + "... expected failure": "x", + "... ok": ".", + "... FAIL": "F", + } + results = output.split("\n\n==")[0] + lines = [l for l in results.split("\n") if l and "..." in l] + dotlist = [] + for l in lines: + found = False + for reason in reasons: + if reason in l: + dotlist.append(reasons[reason]) + found = True + break + if not found: + raise ValueError(f"Not sure what this is. Add to reasons. :{l}") + + return "".join(dotlist) + dots = DOTS.search(output).group(1) + return dots + + +def combine_results(all_results, t): + """ + + Return pieced together results in a form fit for human consumption. Don't + rely on results if piecing together subprocessed results (single process + mode is fine). Was originally meant for that purpose but was found to be + unreliable. See the dump option for reliable results. + + """ + + all_dots = "" + failures = [] + + for module, results in sorted(all_results.items()): + output, return_code, raw_return = map( + results.get, ("output", "return_code", "raw_return") + ) + + if not output or (return_code and RAN_TESTS_DIV not in output): + # would this effect the original dict? TODO + output_lines = raw_return.splitlines() + if len(output_lines) > 20: + results["raw_return"] = "\n".join( + output_lines[:10] + ["..."] + output_lines[-10:] + ) + failures.append(COMPLETE_FAILURE_TEMPLATE % results) + all_dots += "E" + continue + + dots = output_into_dots(output) + all_dots += dots + tracebacks = extract_tracebacks(output) + if tracebacks: + failures.append(tracebacks) + + total_fails, total_errors = map(all_dots.count, "FE") + total_tests = len(all_dots) + + combined = [all_dots] + if failures: + combined += ["".join(failures).lstrip("\n")[:-1]] + combined += [f"{RAN_TESTS_DIV} {total_tests} tests in {t:.3f}s\n"] + + if failures: + infos = ([f"failures={total_fails}"] if total_fails else []) + ( + [f"errors={total_errors}"] if total_errors else [] + ) + combined += [f"FAILED ({', '.join(infos)})\n"] + else: + combined += ["OK\n"] + + return total_tests, "\n".join(combined) + + +################################################################################ + +TEST_RESULTS_START = "<--!! TEST RESULTS START HERE !!-->" +TEST_RESULTS_END = "<--!! TEST RESULTS END HERE !!-->" +_test_re_str = f"{TEST_RESULTS_START}\n(.*){TEST_RESULTS_END}" +TEST_RESULTS_RE = re.compile(_test_re_str, re.DOTALL | re.M) + + +def get_test_results(raw_return): + test_results = TEST_RESULTS_RE.search(raw_return) + if test_results: + try: + return eval(test_results.group(1)) + except: + print(f"BUGGY TEST RESULTS EVAL:\n {test_results.group(1)}") + raise + + +################################################################################ + + +def run_test( + module, + incomplete=False, + usesubprocess=True, + randomize=False, + exclude=("interactive",), + buffer=True, + unbuffered=None, + verbosity=1, +): + """Run a unit test module""" + suite = unittest.TestSuite() + + if verbosity is None: + verbosity = 1 + + if verbosity: + print(f"loading {module}") + + loader = PygameTestLoader( + randomize_tests=randomize, include_incomplete=incomplete, exclude=exclude + ) + suite.addTest(loader.loadTestsFromName(module)) + + output = io.StringIO() + runner = unittest.TextTestRunner(stream=output, buffer=buffer, verbosity=verbosity) + results = runner.run(suite) + + if verbosity == 2: + output.seek(0) + print(output.read()) + output.seek(0) + + results = { + module: { + "output": output.getvalue(), + "num_tests": results.testsRun, + "num_errors": len(results.errors), + "num_failures": len(results.failures), + } + } + + if usesubprocess: + print(TEST_RESULTS_START) + print(pformat(results)) + print(TEST_RESULTS_END) + else: + return results + + +################################################################################ + +if __name__ == "__main__": + options, args = opt_parser.parse_args() + if not args: + if is_pygame_pkg: + run_from = "pygame.tests.go" + else: + run_from = os.path.join(main_dir, "run_tests.py") + sys.exit(f"No test module provided; consider using {run_from} instead") + run_test( + args[0], + incomplete=options.incomplete, + usesubprocess=options.usesubprocess, + randomize=options.randomize, + exclude=options.exclude, + buffer=(not options.unbuffered), + ) + +################################################################################ diff --git a/laplas/abstract_map/pygame/tests/threads_test.py b/laplas/abstract_map/pygame/tests/threads_test.py new file mode 100644 index 0000000..ca9e1ae --- /dev/null +++ b/laplas/abstract_map/pygame/tests/threads_test.py @@ -0,0 +1,238 @@ +import unittest +from pygame.threads import FuncResult, tmap, WorkerQueue, Empty, STOP +from pygame import threads, Surface, transform + + +import time + + +class WorkerQueueTypeTest(unittest.TestCase): + def test_usage_with_different_functions(self): + def f(x): + return x + 1 + + def f2(x): + return x + 2 + + wq = WorkerQueue() + fr = FuncResult(f) + fr2 = FuncResult(f2) + wq.do(fr, 1) + wq.do(fr2, 1) + wq.wait() + wq.stop() + + self.assertEqual(fr.result, 2) + self.assertEqual(fr2.result, 3) + + def test_do(self): + """Tests function placement on queue and execution after blocking function completion.""" + # __doc__ (as of 2008-06-28) for pygame.threads.WorkerQueue.do: + + # puts a function on a queue for running _later_. + + # TODO: This tests needs refactoring to avoid sleep. + # sleep is slow and unreliable (especially on VMs). + + # def sleep_test(): + # time.sleep(0.5) + + # def calc_test(x): + # return x + 1 + + # worker_queue = WorkerQueue(num_workers=1) + # sleep_return = FuncResult(sleep_test) + # calc_return = FuncResult(calc_test) + # init_time = time.time() + # worker_queue.do(sleep_return) + # worker_queue.do(calc_return, 1) + # worker_queue.wait() + # worker_queue.stop() + # time_diff = time.time() - init_time + + # self.assertEqual(sleep_return.result, None) + # self.assertEqual(calc_return.result, 2) + # self.assertGreaterEqual(time_diff, 0.5) + + def test_stop(self): + """Ensure stop() stops the worker queue""" + wq = WorkerQueue() + + self.assertGreater(len(wq.pool), 0) + + for t in wq.pool: + self.assertTrue(t.is_alive()) + + for i in range(200): + wq.do(lambda x: x + 1, i) + + wq.stop() + + for t in wq.pool: + self.assertFalse(t.is_alive()) + + self.assertIs(wq.queue.get(), STOP) + + def test_threadloop(self): + # __doc__ (as of 2008-06-28) for pygame.threads.WorkerQueue.threadloop: + + # Loops until all of the tasks are finished. + + # Make a worker queue with only one thread + wq = WorkerQueue(1) + + # Ocuppy the one worker with the threadloop + # wq threads are just threadloop, so this makes an embedded threadloop + wq.do(wq.threadloop) + + # Make sure wq can still do work + # If wq can still do work, threadloop works + l = [] + wq.do(l.append, 1) + # Wait won't work because the primary thread is in an infinite loop + time.sleep(0.5) + self.assertEqual(l[0], 1) + + # Kill the embedded threadloop by sending stop onto the stack + # Threadloop puts STOP back onto the queue when it STOPs so this kills both loops + wq.stop() + + # Make sure wq has stopped + self.assertFalse(wq.pool[0].is_alive()) + + def test_wait(self): + # __doc__ (as of 2008-06-28) for pygame.threads.WorkerQueue.wait: + + # waits until all tasks are complete. + + wq = WorkerQueue() + + for i in range(2000): + wq.do(lambda x: x + 1, i) + wq.wait() + + self.assertRaises(Empty, wq.queue.get_nowait) + + wq.stop() + + +class ThreadsModuleTest(unittest.TestCase): + def test_benchmark_workers(self): + """Ensure benchmark_workers performance measure functions properly with both default and specified inputs""" + "tags:long_running" + + # __doc__ (as of 2008-06-28) for pygame.threads.benchmark_workers: + + # does a little test to see if workers are at all faster. + # Returns the number of workers which works best. + # Takes a little bit of time to run, so you should only really call + # it once. + # You can pass in benchmark data, and functions if you want. + # a_bench_func - f(data) + # the_data - data to work on. + optimal_workers = threads.benchmark_workers() + self.assertIsInstance(optimal_workers, int) + self.assertTrue(0 <= optimal_workers < 64) + + # Test passing benchmark data and function explicitly + def smooth_scale_bench(data): + transform.smoothscale(data, (128, 128)) + + surf_data = [Surface((x, x), 0, 32) for x in range(12, 64, 12)] + best_num_workers = threads.benchmark_workers(smooth_scale_bench, surf_data) + self.assertIsInstance(best_num_workers, int) + + def test_init(self): + """Ensure init() sets up the worker queue""" + threads.init(8) + + self.assertIsInstance(threads._wq, WorkerQueue) + + threads.quit() + + def test_quit(self): + """Ensure quit() cleans up the worker queue""" + threads.init(8) + threads.quit() + + self.assertIsNone(threads._wq) + + def test_tmap(self): + # __doc__ (as of 2008-06-28) for pygame.threads.tmap: + + # like map, but uses a thread pool to execute. + # num_workers - the number of worker threads that will be used. If pool + # is passed in, then the num_workers arg is ignored. + # worker_queue - you can optionally pass in an existing WorkerQueue. + # wait - True means that the results are returned when everything is finished. + # False means that we return the [worker_queue, results] right away instead. + # results, is returned as a list of FuncResult instances. + # stop_on_error - + + ## test that the outcomes of map and tmap are the same + func, data = lambda x: x + 1, range(100) + + tmapped = list(tmap(func, data)) + mapped = list(map(func, data)) + + self.assertEqual(tmapped, mapped) + + ## Test that setting tmap to not stop on errors produces the expected result + data2 = range(100) + always_excepts = lambda x: 1 / 0 + + tmapped2 = list(tmap(always_excepts, data2, stop_on_error=False)) + + # Use list comprehension to check all entries are None as all function + # calls made by tmap will have thrown an exception (ZeroDivisionError) + # Condense to single bool with `all`, which will return true if all + # entries are true + self.assertTrue(all(x is None for x in tmapped2)) + + def todo_test_tmap__None_func_and_multiple_sequences(self): + """Using a None as func and multiple sequences""" + self.fail() + + res = tmap(None, [1, 2, 3, 4]) + res2 = tmap(None, [1, 2, 3, 4], [22, 33, 44, 55]) + res3 = tmap(None, [1, 2, 3, 4], [22, 33, 44, 55, 66]) + res4 = tmap(None, [1, 2, 3, 4, 5], [22, 33, 44, 55]) + + self.assertEqual([1, 2, 3, 4], res) + self.assertEqual([(1, 22), (2, 33), (3, 44), (4, 55)], res2) + self.assertEqual([(1, 22), (2, 33), (3, 44), (4, 55), (None, 66)], res3) + self.assertEqual([(1, 22), (2, 33), (3, 44), (4, 55), (5, None)], res4) + + def test_tmap__wait(self): + r = range(1000) + wq, results = tmap(lambda x: x, r, num_workers=5, wait=False) + wq.wait() + r2 = (x.result for x in results) + self.assertEqual(list(r), list(r2)) + + def test_FuncResult(self): + """Ensure FuncResult sets its result and exception attributes""" + # Results are stored in result attribute + fr = FuncResult(lambda x: x + 1) + fr(2) + + self.assertEqual(fr.result, 3) + + # Exceptions are store in exception attribute + self.assertIsNone(fr.exception, "no exception should be raised") + + exception = ValueError("rast") + + def x(sdf): + raise exception + + fr = FuncResult(x) + fr(None) + + self.assertIs(fr.exception, exception) + + +################################################################################ + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/time_test.py b/laplas/abstract_map/pygame/tests/time_test.py new file mode 100644 index 0000000..95e04f0 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/time_test.py @@ -0,0 +1,410 @@ +import os +import platform +import unittest +import pygame +import time + +Clock = pygame.time.Clock + + +class ClockTypeTest(unittest.TestCase): + __tags__ = ["timing"] + + def test_construction(self): + """Ensure a Clock object can be created""" + c = Clock() + + self.assertTrue(c, "Clock cannot be constructed") + + def test_get_fps(self): + """test_get_fps tests pygame.time.get_fps()""" + # Initialization check, first call should return 0 fps + c = Clock() + self.assertEqual(c.get_fps(), 0) + # Type check get_fps should return float + self.assertTrue(type(c.get_fps()) == float) + # Allowable margin of error in percentage + delta = 0.30 + # Test fps correctness for 100, 60 and 30 fps + self._fps_test(c, 100, delta) + self._fps_test(c, 60, delta) + self._fps_test(c, 30, delta) + + def _fps_test(self, clock, fps, delta): + """ticks fps times each second, hence get_fps() should return fps""" + delay_per_frame = 1.0 / fps + for f in range(fps): # For one second tick and sleep + clock.tick() + time.sleep(delay_per_frame) + # We should get around fps (+- fps*delta -- delta % of fps) + self.assertAlmostEqual(clock.get_fps(), fps, delta=fps * delta) + + def test_get_rawtime(self): + iterations = 10 + delay = 0.1 + delay_miliseconds = delay * (10**3) # actual time difference between ticks + framerate_limit = 5 + delta = 50 # allowable error in milliseconds + + # Testing Clock Initialization + c = Clock() + self.assertEqual(c.get_rawtime(), 0) + + # Testing Raw Time with Frame Delay + for f in range(iterations): + time.sleep(delay) + c.tick(framerate_limit) + c1 = c.get_rawtime() + self.assertAlmostEqual(delay_miliseconds, c1, delta=delta) + + # Testing get_rawtime() = get_time() + for f in range(iterations): + time.sleep(delay) + c.tick() + c1 = c.get_rawtime() + c2 = c.get_time() + self.assertAlmostEqual(c1, c2, delta=delta) + + @unittest.skipIf(platform.machine() == "s390x", "Fails on s390x") + @unittest.skipIf( + os.environ.get("CI", None), "CI can have variable time slices, slow." + ) + def test_get_time(self): + # Testing parameters + delay = 0.1 # seconds + delay_miliseconds = delay * (10**3) + iterations = 10 + delta = 50 # milliseconds + + # Testing Clock Initialization + c = Clock() + self.assertEqual(c.get_time(), 0) + + # Testing within delay parameter range + for i in range(iterations): + time.sleep(delay) + c.tick() + c1 = c.get_time() + self.assertAlmostEqual(delay_miliseconds, c1, delta=delta) + + # Comparing get_time() results with the 'time' module + for i in range(iterations): + t0 = time.time() + time.sleep(delay) + c.tick() + t1 = time.time() + c1 = c.get_time() # elapsed time in milliseconds + d0 = (t1 - t0) * ( + 10**3 + ) #'time' module elapsed time converted to milliseconds + self.assertAlmostEqual(d0, c1, delta=delta) + + @unittest.skipIf(platform.machine() == "s390x", "Fails on s390x") + @unittest.skipIf( + os.environ.get("CI", None), "CI can have variable time slices, slow." + ) + def test_tick(self): + """Tests time.Clock.tick()""" + """ + Loops with a set delay a few times then checks what tick reports to + verify its accuracy. Then calls tick with a desired frame-rate and + verifies it is not faster than the desired frame-rate nor is it taking + a dramatically long time to complete + """ + + # Adjust this value to increase the acceptable sleep jitter + epsilon = 5 # 1.5 + + # Adjust this value to increase the acceptable locked frame-rate jitter + epsilon2 = 0.3 + # adjust this value to increase the acceptable frame-rate margin + epsilon3 = 20 + testing_framerate = 60 + milliseconds = 5.0 + + collection = [] + c = Clock() + + # verify time.Clock.tick() will measure the time correctly + c.tick() + for i in range(100): + time.sleep(milliseconds / 1000) # convert to seconds + collection.append(c.tick()) + + # removes the first highest and lowest value + for outlier in [min(collection), max(collection)]: + if outlier != milliseconds: + collection.remove(outlier) + + average_time = float(sum(collection)) / len(collection) + + # assert the deviation from the intended frame-rate is within the + # acceptable amount (the delay is not taking a dramatically long time) + self.assertAlmostEqual(average_time, milliseconds, delta=epsilon) + + # verify tick will control the frame-rate + + c = Clock() + collection = [] + + start = time.time() + + for i in range(testing_framerate): + collection.append(c.tick(testing_framerate)) + + # remove the highest and lowest outliers + for outlier in [min(collection), max(collection)]: + if outlier != round(1000 / testing_framerate): + collection.remove(outlier) + + end = time.time() + + # Since calling tick with a desired fps will prevent the program from + # running at greater than the given fps, 100 iterations at 100 fps + # should last no less than 1 second + self.assertAlmostEqual(end - start, 1, delta=epsilon2) + + average_tick_time = float(sum(collection)) / len(collection) + self.assertAlmostEqual( + 1000 / average_tick_time, testing_framerate, delta=epsilon3 + ) + + def test_tick_busy_loop(self): + """Test tick_busy_loop""" + + c = Clock() + + # Test whether the return value of tick_busy_loop is equal to + # (FPS is accurate) or greater than (slower than the set FPS) + # with a small margin for error based on differences in how this + # test runs in practise - it either sometimes runs slightly fast + # or seems to based on a rounding error. + second_length = 1000 + shortfall_tolerance = 1 # (ms) The amount of time a tick is allowed to run short of, to account for underlying rounding errors + sample_fps = 40 + + self.assertGreaterEqual( + c.tick_busy_loop(sample_fps), + (second_length / sample_fps) - shortfall_tolerance, + ) + pygame.time.wait(10) # incur delay between ticks that's faster than sample_fps + self.assertGreaterEqual( + c.tick_busy_loop(sample_fps), + (second_length / sample_fps) - shortfall_tolerance, + ) + pygame.time.wait(200) # incur delay between ticks that's slower than sample_fps + self.assertGreaterEqual( + c.tick_busy_loop(sample_fps), + (second_length / sample_fps) - shortfall_tolerance, + ) + + high_fps = 500 + self.assertGreaterEqual( + c.tick_busy_loop(high_fps), (second_length / high_fps) - shortfall_tolerance + ) + + low_fps = 1 + self.assertGreaterEqual( + c.tick_busy_loop(low_fps), (second_length / low_fps) - shortfall_tolerance + ) + + low_non_factor_fps = 35 # 1000/35 makes 28.5714285714 + frame_length_without_decimal_places = int( + second_length / low_non_factor_fps + ) # Same result as math.floor + self.assertGreaterEqual( + c.tick_busy_loop(low_non_factor_fps), + frame_length_without_decimal_places - shortfall_tolerance, + ) + + high_non_factor_fps = 750 # 1000/750 makes 1.3333... + frame_length_without_decimal_places_2 = int( + second_length / high_non_factor_fps + ) # Same result as math.floor + self.assertGreaterEqual( + c.tick_busy_loop(high_non_factor_fps), + frame_length_without_decimal_places_2 - shortfall_tolerance, + ) + + zero_fps = 0 + self.assertEqual(c.tick_busy_loop(zero_fps), 0) + + # Check behaviour of unexpected values + + negative_fps = -1 + self.assertEqual(c.tick_busy_loop(negative_fps), 0) + + fractional_fps = 32.75 + frame_length_without_decimal_places_3 = int(second_length / fractional_fps) + self.assertGreaterEqual( + c.tick_busy_loop(fractional_fps), + frame_length_without_decimal_places_3 - shortfall_tolerance, + ) + + bool_fps = True + self.assertGreaterEqual( + c.tick_busy_loop(bool_fps), (second_length / bool_fps) - shortfall_tolerance + ) + + +class TimeModuleTest(unittest.TestCase): + __tags__ = ["timing"] + + @unittest.skipIf(platform.machine() == "s390x", "Fails on s390x") + @unittest.skipIf( + os.environ.get("CI", None), "CI can have variable time slices, slow." + ) + def test_delay(self): + """Tests time.delay() function.""" + millis = 50 # millisecond to wait on each iteration + iterations = 20 # number of iterations + delta = 150 # Represents acceptable margin of error for wait in ms + # Call checking function + self._wait_delay_check(pygame.time.delay, millis, iterations, delta) + # After timing behaviour, check argument type exceptions + self._type_error_checks(pygame.time.delay) + + def test_get_ticks(self): + """Tests time.get_ticks()""" + """ + Iterates and delays for arbitrary amount of time for each iteration, + check get_ticks to equal correct gap time + """ + iterations = 20 + millis = 50 + delta = 15 # Acceptable margin of error in ms + # Assert return type to be int + self.assertTrue(type(pygame.time.get_ticks()) == int) + for i in range(iterations): + curr_ticks = pygame.time.get_ticks() # Save current tick count + curr_time = time.time() # Save current time + pygame.time.delay(millis) # Delay for millis + # Time and Ticks difference from start of the iteration + time_diff = round((time.time() - curr_time) * 1000) + ticks_diff = pygame.time.get_ticks() - curr_ticks + # Assert almost equality of the ticking time and time difference + self.assertAlmostEqual(ticks_diff, time_diff, delta=delta) + + @unittest.skipIf(platform.machine() == "s390x", "Fails on s390x") + @unittest.skipIf( + os.environ.get("CI", None), "CI can have variable time slices, slow." + ) + def test_set_timer(self): + """Tests time.set_timer()""" + """ + Tests if a timer will post the correct amount of eventid events in + the specified delay. Test is posting event objects work. + Also tests if setting milliseconds to 0 stops the timer and if + the once argument and repeat arguments work. + """ + pygame.init() + TIMER_EVENT_TYPE = pygame.event.custom_type() + timer_event = pygame.event.Event(TIMER_EVENT_TYPE) + delta = 50 + timer_delay = 100 + test_number = 8 # Number of events to read for the test + events = 0 # Events read + + pygame.event.clear() + pygame.time.set_timer(TIMER_EVENT_TYPE, timer_delay) + + # Test that 'test_number' events are posted in the right amount of time + t1 = pygame.time.get_ticks() + max_test_time = t1 + timer_delay * test_number + delta + while events < test_number: + for event in pygame.event.get(): + if event == timer_event: + events += 1 + + # The test takes too much time + if pygame.time.get_ticks() > max_test_time: + break + + pygame.time.set_timer(TIMER_EVENT_TYPE, 0) + t2 = pygame.time.get_ticks() + # Is the number ef events and the timing right? + self.assertEqual(events, test_number) + self.assertAlmostEqual(timer_delay * test_number, t2 - t1, delta=delta) + + # Test that the timer stopped when set with 0ms delay. + pygame.time.delay(200) + self.assertNotIn(timer_event, pygame.event.get()) + + # Test that the old timer for an event is deleted when a new timer is set + pygame.time.set_timer(TIMER_EVENT_TYPE, timer_delay) + pygame.time.delay(int(timer_delay * 3.5)) + self.assertEqual(pygame.event.get().count(timer_event), 3) + pygame.time.set_timer(TIMER_EVENT_TYPE, timer_delay * 10) # long wait time + pygame.time.delay(timer_delay * 5) + self.assertNotIn(timer_event, pygame.event.get()) + pygame.time.set_timer(TIMER_EVENT_TYPE, timer_delay * 3) + pygame.time.delay(timer_delay * 7) + self.assertEqual(pygame.event.get().count(timer_event), 2) + pygame.time.set_timer(TIMER_EVENT_TYPE, timer_delay) + pygame.time.delay(int(timer_delay * 5.5)) + self.assertEqual(pygame.event.get().count(timer_event), 5) + + # Test that the loops=True works + pygame.time.set_timer(TIMER_EVENT_TYPE, 10, True) + pygame.time.delay(40) + self.assertEqual(pygame.event.get().count(timer_event), 1) + + # Test a variety of event objects, test loops argument + events_to_test = [ + pygame.event.Event(TIMER_EVENT_TYPE), + pygame.event.Event( + TIMER_EVENT_TYPE, foo="9gwz5", baz=12, lol=[124, (34, "")] + ), + pygame.event.Event(pygame.KEYDOWN, key=pygame.K_a, unicode="a"), + ] + repeat = 3 + millis = 50 + for e in events_to_test: + pygame.time.set_timer(e, millis, loops=repeat) + pygame.time.delay(2 * millis * repeat) + self.assertEqual(pygame.event.get().count(e), repeat) + pygame.quit() + + def test_wait(self): + """Tests time.wait() function.""" + millis = 100 # millisecond to wait on each iteration + iterations = 10 # number of iterations + delta = 50 # Represents acceptable margin of error for wait in ms + # Call checking function + self._wait_delay_check(pygame.time.wait, millis, iterations, delta) + # After timing behaviour, check argument type exceptions + self._type_error_checks(pygame.time.wait) + + def _wait_delay_check(self, func_to_check, millis, iterations, delta): + """ " + call func_to_check(millis) "iterations" times and check each time if + function "waited" for given millisecond (+- delta). At the end, take + average time for each call (whole_duration/iterations), which should + be equal to millis (+- delta - acceptable margin of error). + *Created to avoid code duplication during delay and wait tests + """ + # take starting time for duration calculation + start_time = time.time() + for i in range(iterations): + wait_time = func_to_check(millis) + # Check equality of wait_time and millis with margin of error delta + self.assertAlmostEqual(wait_time, millis, delta=delta) + stop_time = time.time() + # Cycle duration in millisecond + duration = round((stop_time - start_time) * 1000) + # Duration/Iterations should be (almost) equal to predefined millis + self.assertAlmostEqual(duration / iterations, millis, delta=delta) + + def _type_error_checks(self, func_to_check): + """Checks 3 TypeError (float, tuple, string) for the func_to_check""" + """Intended for time.delay and time.wait functions""" + # Those methods throw no exceptions on negative integers + self.assertRaises(TypeError, func_to_check, 0.1) # check float + self.assertRaises(TypeError, pygame.time.delay, (0, 1)) # check tuple + self.assertRaises(TypeError, pygame.time.delay, "10") # check string + + +############################################################################### + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/touch_test.py b/laplas/abstract_map/pygame/tests/touch_test.py new file mode 100644 index 0000000..259a2c7 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/touch_test.py @@ -0,0 +1,97 @@ +import unittest +import os +import pygame +from pygame._sdl2 import touch +from pygame.tests.test_utils import question + + +has_touchdevice = touch.get_num_devices() > 0 + + +class TouchTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + pygame.display.init() + + @classmethod + def tearDownClass(cls): + pygame.display.quit() + + def test_num_devices(self): + touch.get_num_devices() + + @unittest.skipIf(not has_touchdevice, "no touch devices found") + def test_get_device(self): + touch.get_device(0) + + def test_get_device__invalid(self): + self.assertRaises(pygame.error, touch.get_device, -1234) + self.assertRaises(TypeError, touch.get_device, "test") + + @unittest.skipIf(not has_touchdevice, "no touch devices found") + def test_num_fingers(self): + touch.get_num_fingers(touch.get_device(0)) + + def test_num_fingers__invalid(self): + self.assertRaises(TypeError, touch.get_num_fingers, "test") + self.assertRaises(pygame.error, touch.get_num_fingers, -1234) + + +class TouchInteractiveTest(unittest.TestCase): + __tags__ = ["interactive"] + + @unittest.skipIf(not has_touchdevice, "no touch devices found") + def test_get_finger(self): + """ask for touch input and check the dict""" + + pygame.display.init() + pygame.font.init() + + os.environ["SDL_VIDEO_WINDOW_POS"] = "50,50" + screen = pygame.display.set_mode((800, 600)) + screen.fill((255, 255, 255)) + + font = pygame.font.Font(None, 32) + instructions_str_1 = "Please place some fingers on your touch device" + instructions_str_2 = ( + "Close the window when finished, " "and answer the question" + ) + inst_1_render = font.render(instructions_str_1, True, pygame.Color("#000000")) + inst_2_render = font.render(instructions_str_2, True, pygame.Color("#000000")) + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + finger_data_renders = [] + num_devices = pygame._sdl2.touch.get_num_devices() + if num_devices > 0: + first_device = pygame._sdl2.touch.get_device(0) + num_fingers = pygame._sdl2.touch.get_num_fingers(first_device) + if num_fingers > 0: + for finger_index in range(0, num_fingers): + data = pygame._sdl2.touch.get_finger(first_device, finger_index) + render = font.render( + f"finger - {data}", True, pygame.Color("#000000") + ) + + finger_data_renders.append(render) + + screen.fill((255, 255, 255)) + screen.blit(inst_1_render, (5, 5)) + screen.blit(inst_2_render, (5, 40)) + for index, finger in enumerate(finger_data_renders): + screen.blit(finger, (5, 80 + (index * 40))) + + pygame.display.update() + + response = question("Does the finger data seem correct?") + self.assertTrue(response) + + pygame.display.quit() + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/transform_test.py b/laplas/abstract_map/pygame/tests/transform_test.py new file mode 100644 index 0000000..b7c64e9 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/transform_test.py @@ -0,0 +1,1420 @@ +import unittest +import os +import platform + +from pygame.tests import test_utils +from pygame.tests.test_utils import example_path + +import pygame +import pygame.transform +from pygame.locals import * + + +def show_image(s, images=[]): + # pygame.display.init() + size = s.get_rect()[2:] + screen = pygame.display.set_mode(size) + screen.blit(s, (0, 0)) + pygame.display.flip() + pygame.event.pump() + going = True + idx = 0 + while going: + events = pygame.event.get() + for e in events: + if e.type == QUIT: + going = False + if e.type == KEYDOWN: + if e.key in [K_s, K_a]: + if e.key == K_s: + idx += 1 + if e.key == K_a: + idx -= 1 + s = images[idx] + screen.blit(s, (0, 0)) + pygame.display.flip() + pygame.event.pump() + elif e.key in [K_ESCAPE]: + going = False + pygame.display.quit() + pygame.display.init() + + +def threshold( + return_surf, + surf, + color, + threshold=(0, 0, 0), + diff_color=(0, 0, 0), + change_return=True, +): + """given the color it makes return_surf only have areas with the given colour.""" + + width, height = surf.get_width(), surf.get_height() + + if change_return: + return_surf.fill(diff_color) + + try: + r, g, b = color + except ValueError: + r, g, b, a = color + + try: + tr, tg, tb = color + except ValueError: + tr, tg, tb, ta = color + + similar = 0 + for y in range(height): + for x in range(width): + c1 = surf.get_at((x, y)) + + if (abs(c1[0] - r) < tr) & (abs(c1[1] - g) < tg) & (abs(c1[2] - b) < tb): + # this pixel is within the threshold. + if change_return: + return_surf.set_at((x, y), c1) + similar += 1 + # else: + # print(c1, c2) + + return similar + + +class TransformModuleTest(unittest.TestCase): + def test_scale__alpha(self): + """see if set_alpha information is kept.""" + + s = pygame.Surface((32, 32)) + s.set_alpha(55) + self.assertEqual(s.get_alpha(), 55) + + s = pygame.Surface((32, 32)) + s.set_alpha(55) + s2 = pygame.transform.scale(s, (64, 64)) + s3 = s.copy() + self.assertEqual(s.get_alpha(), s3.get_alpha()) + self.assertEqual(s.get_alpha(), s2.get_alpha()) + + def test_scale__destination(self): + """see if the destination surface can be passed in to use.""" + + s = pygame.Surface((32, 32)) + s2 = pygame.transform.scale(s, (64, 64)) + s3 = s2.copy() + + # Also validate keyword arguments + s3 = pygame.transform.scale(surface=s, size=(64, 64), dest_surface=s3) + pygame.transform.scale(s, (64, 64), s2) + + # the wrong size surface is past in. Should raise an error. + self.assertRaises(ValueError, pygame.transform.scale, s, (33, 64), s3) + + s = pygame.Surface((32, 32)) + s2 = pygame.transform.smoothscale(s, (64, 64)) + s3 = s2.copy() + + # Also validate keyword arguments + s3 = pygame.transform.smoothscale(surface=s, size=(64, 64), dest_surface=s3) + + # the wrong size surface is past in. Should raise an error. + self.assertRaises(ValueError, pygame.transform.smoothscale, s, (33, 64), s3) + + def test_scale__vector2(self): + s = pygame.Surface((32, 32)) + s2 = pygame.transform.scale(s, pygame.Vector2(64, 64)) + s3 = pygame.transform.smoothscale(s, pygame.Vector2(64, 64)) + + self.assertEqual((64, 64), s2.get_size()) + self.assertEqual((64, 64), s3.get_size()) + + def test_scale__zero_surface_transform(self): + tmp_surface = pygame.transform.scale(pygame.Surface((128, 128)), (0, 0)) + self.assertEqual(tmp_surface.get_size(), (0, 0)) + tmp_surface = pygame.transform.scale(tmp_surface, (128, 128)) + self.assertEqual(tmp_surface.get_size(), (128, 128)) + + def test_scale_by(self): + s = pygame.Surface((32, 32)) + + s2 = pygame.transform.scale_by(s, 2) + self.assertEqual((64, 64), s2.get_size()) + + s2 = pygame.transform.scale_by(s, factor=(2.0, 1.5)) + self.assertEqual((64, 48), s2.get_size()) + + dest = pygame.Surface((64, 48)) + pygame.transform.scale_by(s, (2.0, 1.5), dest_surface=dest) + + def test_smoothscale_by(self): + s = pygame.Surface((32, 32)) + + s2 = pygame.transform.smoothscale_by(s, 2) + self.assertEqual((64, 64), s2.get_size()) + + s2 = pygame.transform.smoothscale_by(s, factor=(2.0, 1.5)) + self.assertEqual((64, 48), s2.get_size()) + + dest = pygame.Surface((64, 48)) + pygame.transform.smoothscale_by(s, (2.0, 1.5), dest_surface=dest) + + def test_grayscale(self): + s = pygame.Surface((32, 32)) + s.fill((255, 0, 0)) + + s2 = pygame.transform.grayscale(s) + self.assertEqual(pygame.transform.average_color(s2)[0], 76) + self.assertEqual(pygame.transform.average_color(s2)[1], 76) + self.assertEqual(pygame.transform.average_color(s2)[2], 76) + + dest = pygame.Surface((32, 32), depth=32) + pygame.transform.grayscale(s, dest) + self.assertEqual(pygame.transform.average_color(dest)[0], 76) + self.assertEqual(pygame.transform.average_color(dest)[1], 76) + self.assertEqual(pygame.transform.average_color(dest)[2], 76) + + dest = pygame.Surface((32, 32), depth=32) + s.fill((34, 12, 65)) + pygame.transform.grayscale(s, dest) + self.assertEqual(pygame.transform.average_color(dest)[0], 24) + self.assertEqual(pygame.transform.average_color(dest)[1], 24) + self.assertEqual(pygame.transform.average_color(dest)[2], 24) + + dest = pygame.Surface((32, 32), depth=32) + s.fill((123, 123, 123)) + pygame.transform.grayscale(s, dest) + self.assertIn(pygame.transform.average_color(dest)[0], [123, 122]) + self.assertIn(pygame.transform.average_color(dest)[1], [123, 122]) + self.assertIn(pygame.transform.average_color(dest)[2], [123, 122]) + + s = pygame.Surface((32, 32), depth=24) + s.fill((255, 0, 0)) + dest = pygame.Surface((32, 32), depth=24) + pygame.transform.grayscale(s, dest) + self.assertEqual(pygame.transform.average_color(dest)[0], 76) + self.assertEqual(pygame.transform.average_color(dest)[1], 76) + self.assertEqual(pygame.transform.average_color(dest)[2], 76) + + s = pygame.Surface((32, 32), depth=16) + s.fill((255, 0, 0)) + dest = pygame.Surface((32, 32), depth=16) + pygame.transform.grayscale(s, dest) + self.assertEqual(pygame.transform.average_color(dest)[0], 72) + self.assertEqual(pygame.transform.average_color(dest)[1], 76) + self.assertEqual(pygame.transform.average_color(dest)[2], 72) + + def test_threshold__honors_third_surface(self): + # __doc__ for threshold as of Tue 07/15/2008 + + # pygame.transform.threshold(DestSurface, Surface, color, threshold = + # (0,0,0,0), diff_color = (0,0,0,0), change_return = True, Surface = + # None): return num_threshold_pixels + + # When given the optional third + # surface, it would use the colors in that rather than the "color" + # specified in the function to check against. + + # New in pygame 1.8 + + ################################################################ + # Sizes + (w, h) = size = (32, 32) + + # the original_color is within the threshold of the threshold_color + threshold = (20, 20, 20, 20) + + original_color = (25, 25, 25, 25) + threshold_color = (10, 10, 10, 10) + + # Surfaces + original_surface = pygame.Surface(size, pygame.SRCALPHA, 32) + dest_surface = pygame.Surface(size, pygame.SRCALPHA, 32) + + # Third surface is used in lieu of 3rd position arg color + third_surface = pygame.Surface(size, pygame.SRCALPHA, 32) + + # Color filling + original_surface.fill(original_color) + third_surface.fill(threshold_color) + + ################################################################ + # All pixels for color should be within threshold + # + pixels_within_threshold = pygame.transform.threshold( + dest_surface=None, + surface=original_surface, + search_color=threshold_color, + threshold=threshold, + set_color=None, + set_behavior=0, + ) + + self.assertEqual(w * h, pixels_within_threshold) + + ################################################################ + # This should respect third_surface colors in place of 3rd arg + # color Should be the same as: surface.fill(threshold_color) + # all within threshold + + pixels_within_threshold = pygame.transform.threshold( + dest_surface=None, + surface=original_surface, + search_color=None, + threshold=threshold, + set_color=None, + set_behavior=0, + search_surf=third_surface, + ) + self.assertEqual(w * h, pixels_within_threshold) + + def test_threshold_dest_surf_not_change(self): + """the pixels within the threshold. + + All pixels not within threshold are changed to set_color. + So there should be none changed in this test. + """ + (w, h) = size = (32, 32) + threshold = (20, 20, 20, 20) + original_color = (25, 25, 25, 25) + original_dest_color = (65, 65, 65, 55) + threshold_color = (10, 10, 10, 10) + set_color = (255, 10, 10, 10) + + surf = pygame.Surface(size, pygame.SRCALPHA, 32) + dest_surf = pygame.Surface(size, pygame.SRCALPHA, 32) + search_surf = pygame.Surface(size, pygame.SRCALPHA, 32) + + surf.fill(original_color) + search_surf.fill(threshold_color) + dest_surf.fill(original_dest_color) + + # set_behavior=1, set dest_surface from set_color. + # all within threshold of third_surface, so no color is set. + + THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR = 1 + pixels_within_threshold = pygame.transform.threshold( + dest_surface=dest_surf, + surface=surf, + search_color=None, + threshold=threshold, + set_color=set_color, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR, + search_surf=search_surf, + ) + + # # Return, of pixels within threshold is correct + self.assertEqual(w * h, pixels_within_threshold) + + # # Size of dest surface is correct + dest_rect = dest_surf.get_rect() + dest_size = dest_rect.size + self.assertEqual(size, dest_size) + + # The color is not the change_color specified for every pixel As all + # pixels are within threshold + + for pt in test_utils.rect_area_pts(dest_rect): + self.assertNotEqual(dest_surf.get_at(pt), set_color) + self.assertEqual(dest_surf.get_at(pt), original_dest_color) + + def test_threshold_dest_surf_all_changed(self): + """Lowering the threshold, expecting changed surface""" + + (w, h) = size = (32, 32) + threshold = (20, 20, 20, 20) + original_color = (25, 25, 25, 25) + original_dest_color = (65, 65, 65, 55) + threshold_color = (10, 10, 10, 10) + set_color = (255, 10, 10, 10) + + surf = pygame.Surface(size, pygame.SRCALPHA, 32) + dest_surf = pygame.Surface(size, pygame.SRCALPHA, 32) + search_surf = pygame.Surface(size, pygame.SRCALPHA, 32) + + surf.fill(original_color) + search_surf.fill(threshold_color) + dest_surf.fill(original_dest_color) + + THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR = 1 + pixels_within_threshold = pygame.transform.threshold( + dest_surf, + surf, + search_color=None, + set_color=set_color, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR, + search_surf=search_surf, + ) + + self.assertEqual(0, pixels_within_threshold) + + dest_rect = dest_surf.get_rect() + dest_size = dest_rect.size + self.assertEqual(size, dest_size) + + # The color is the set_color specified for every pixel As all + # pixels are not within threshold + for pt in test_utils.rect_area_pts(dest_rect): + self.assertEqual(dest_surf.get_at(pt), set_color) + + def test_threshold_count(self): + """counts the colors, and not changes them.""" + surf_size = (32, 32) + surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32) + search_surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32) + search_color = (55, 55, 55, 255) + original_color = (10, 10, 10, 255) + + surf.fill(original_color) + # set 2 pixels to the color we are searching for. + surf.set_at((0, 0), search_color) + surf.set_at((12, 5), search_color) + + # There is no destination surface, but we ask to change it. + # This should be an error. + self.assertRaises( + TypeError, pygame.transform.threshold, None, surf, search_color + ) + # from pygame.transform import THRESHOLD_BEHAVIOR_COUNT + THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF = 2 + self.assertRaises( + TypeError, + pygame.transform.threshold, + None, + surf, + search_color, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF, + ) + + THRESHOLD_BEHAVIOR_COUNT = 0 + num_threshold_pixels = pygame.transform.threshold( + dest_surface=None, + surface=surf, + search_color=search_color, + set_behavior=THRESHOLD_BEHAVIOR_COUNT, + ) + self.assertEqual(num_threshold_pixels, 2) + + def test_threshold_search_surf(self): + surf_size = (32, 32) + surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32) + search_surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32) + dest_surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32) + + original_color = (10, 10, 10, 255) + search_color = (55, 55, 55, 255) + + surf.fill(original_color) + dest_surf.fill(original_color) + # set 2 pixels to the color we are searching for. + surf.set_at((0, 0), search_color) + surf.set_at((12, 5), search_color) + + search_surf.fill(search_color) + + # We look in the other surface for matching colors. + # Change it in dest_surf + THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF = 2 + + # TypeError: if search_surf is used, search_color should be None + self.assertRaises( + TypeError, + pygame.transform.threshold, + dest_surf, + surf, + search_color, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF, + search_surf=search_surf, + ) + + # surf, dest_surf, and search_surf should all be the same size. + # Check surface sizes are the same size. + different_sized_surf = pygame.Surface((22, 33), pygame.SRCALPHA, 32) + self.assertRaises( + TypeError, + pygame.transform.threshold, + different_sized_surf, + surf, + search_color=None, + set_color=None, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF, + search_surf=search_surf, + ) + + self.assertRaises( + TypeError, + pygame.transform.threshold, + dest_surf, + surf, + search_color=None, + set_color=None, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF, + search_surf=different_sized_surf, + ) + + # We look to see if colors in search_surf are in surf. + num_threshold_pixels = pygame.transform.threshold( + dest_surface=dest_surf, + surface=surf, + search_color=None, + set_color=None, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF, + search_surf=search_surf, + ) + + num_pixels_within = 2 + self.assertEqual(num_threshold_pixels, num_pixels_within) + + dest_surf.fill(original_color) + num_threshold_pixels = pygame.transform.threshold( + dest_surf, + surf, + search_color=None, + set_color=None, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF, + search_surf=search_surf, + inverse_set=True, + ) + + self.assertEqual(num_threshold_pixels, 2) + + def test_threshold_inverse_set(self): + """changes the pixels within the threshold, and not outside.""" + surf_size = (32, 32) + _dest_surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32) + _surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32) + + dest_surf = _dest_surf # surface we are changing. + surf = _surf # surface we are looking at + search_color = (55, 55, 55, 255) # color we are searching for. + threshold = (0, 0, 0, 0) # within this distance from search_color. + set_color = (245, 245, 245, 255) # color we set. + inverse_set = 1 # pixels within threshold are changed to 'set_color' + + original_color = (10, 10, 10, 255) + surf.fill(original_color) + # set 2 pixels to the color we are searching for. + surf.set_at((0, 0), search_color) + surf.set_at((12, 5), search_color) + + dest_surf.fill(original_color) + # set 2 pixels to the color we are searching for. + dest_surf.set_at((0, 0), search_color) + dest_surf.set_at((12, 5), search_color) + + THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR = 1 + num_threshold_pixels = pygame.transform.threshold( + dest_surf, + surf, + search_color=search_color, + threshold=threshold, + set_color=set_color, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR, + inverse_set=1, + ) + + self.assertEqual(num_threshold_pixels, 2) + # only two pixels changed to diff_color. + self.assertEqual(dest_surf.get_at((0, 0)), set_color) + self.assertEqual(dest_surf.get_at((12, 5)), set_color) + + # other pixels should be the same as they were before. + # We just check one other pixel, not all of them. + self.assertEqual(dest_surf.get_at((2, 2)), original_color) + + # XXX + def test_threshold_non_src_alpha(self): + result = pygame.Surface((10, 10)) + s1 = pygame.Surface((10, 10)) + s2 = pygame.Surface((10, 10)) + s3 = pygame.Surface((10, 10)) + s4 = pygame.Surface((10, 10)) + + x = s1.fill((0, 0, 0)) + s1.set_at((0, 0), (32, 20, 0)) + + x = s2.fill((0, 20, 0)) + x = s3.fill((0, 0, 0)) + x = s4.fill((0, 0, 0)) + s2.set_at((0, 0), (33, 21, 0)) + s2.set_at((3, 0), (63, 61, 0)) + s3.set_at((0, 0), (112, 31, 0)) + s4.set_at((0, 0), (11, 31, 0)) + s4.set_at((1, 1), (12, 31, 0)) + + self.assertEqual(s1.get_at((0, 0)), (32, 20, 0, 255)) + self.assertEqual(s2.get_at((0, 0)), (33, 21, 0, 255)) + self.assertEqual((0, 0), (s1.get_flags(), s2.get_flags())) + + similar_color = (255, 255, 255, 255) + diff_color = (222, 0, 0, 255) + threshold_color = (20, 20, 20, 255) + + THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR = 1 + num_threshold_pixels = pygame.transform.threshold( + dest_surface=result, + surface=s1, + search_color=similar_color, + threshold=threshold_color, + set_color=diff_color, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR, + ) + self.assertEqual(num_threshold_pixels, 0) + + num_threshold_pixels = pygame.transform.threshold( + dest_surface=result, + surface=s1, + search_color=(40, 40, 0), + threshold=threshold_color, + set_color=diff_color, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_COLOR, + ) + self.assertEqual(num_threshold_pixels, 1) + + self.assertEqual(result.get_at((0, 0)), diff_color) + + def test_threshold__uneven_colors(self): + (w, h) = size = (16, 16) + + original_surface = pygame.Surface(size, pygame.SRCALPHA, 32) + dest_surface = pygame.Surface(size, pygame.SRCALPHA, 32) + + original_surface.fill(0) + + threshold_color_template = [5, 5, 5, 5] + threshold_template = [6, 6, 6, 6] + + ################################################################ + + for pos in range(len("rgb")): + threshold_color = threshold_color_template[:] + threshold = threshold_template[:] + + threshold_color[pos] = 45 + threshold[pos] = 50 + + pixels_within_threshold = pygame.transform.threshold( + None, + original_surface, + threshold_color, + threshold, + set_color=None, + set_behavior=0, + ) + + self.assertEqual(w * h, pixels_within_threshold) + + ################################################################ + + def test_threshold_set_behavior2(self): + """raises an error when set_behavior=2 and set_color is not None.""" + from pygame.transform import threshold + + s1 = pygame.Surface((32, 32), SRCALPHA, 32) + s2 = pygame.Surface((32, 32), SRCALPHA, 32) + THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF = 2 + self.assertRaises( + TypeError, + threshold, + dest_surface=s2, + surface=s1, + search_color=(30, 30, 30), + threshold=(11, 11, 11), + set_color=(255, 0, 0), + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF, + ) + + def test_threshold_set_behavior0(self): + """raises an error when set_behavior=1 + and set_color is not None, + and dest_surf is not None. + """ + from pygame.transform import threshold + + s1 = pygame.Surface((32, 32), SRCALPHA, 32) + s2 = pygame.Surface((32, 32), SRCALPHA, 32) + THRESHOLD_BEHAVIOR_COUNT = 0 + + self.assertRaises( + TypeError, + threshold, + dest_surface=None, + surface=s2, + search_color=(30, 30, 30), + threshold=(11, 11, 11), + set_color=(0, 0, 0), + set_behavior=THRESHOLD_BEHAVIOR_COUNT, + ) + + self.assertRaises( + TypeError, + threshold, + dest_surface=s1, + surface=s2, + search_color=(30, 30, 30), + threshold=(11, 11, 11), + set_color=None, + set_behavior=THRESHOLD_BEHAVIOR_COUNT, + ) + + threshold( + dest_surface=None, + surface=s2, + search_color=(30, 30, 30), + threshold=(11, 11, 11), + set_color=None, + set_behavior=THRESHOLD_BEHAVIOR_COUNT, + ) + + def test_threshold_from_surface(self): + """Set similar pixels in 'dest_surf' to color in the 'surf'.""" + from pygame.transform import threshold + + surf = pygame.Surface((32, 32), SRCALPHA, 32) + dest_surf = pygame.Surface((32, 32), SRCALPHA, 32) + surf_color = (40, 40, 40, 255) + dest_color = (255, 255, 255) + surf.fill(surf_color) + dest_surf.fill(dest_color) + THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF = 2 + + num_threshold_pixels = threshold( + dest_surface=dest_surf, + surface=surf, + search_color=(30, 30, 30), + threshold=(11, 11, 11), + set_color=None, + set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF, + inverse_set=1, + ) + + self.assertEqual( + num_threshold_pixels, dest_surf.get_height() * dest_surf.get_width() + ) + self.assertEqual(dest_surf.get_at((0, 0)), surf_color) + + def test_threshold__surface(self): + """ """ + from pygame.transform import threshold + + s1 = pygame.Surface((32, 32), SRCALPHA, 32) + s2 = pygame.Surface((32, 32), SRCALPHA, 32) + s3 = pygame.Surface((1, 1), SRCALPHA, 32) + THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF = 2 + + # # only one pixel should not be changed. + # s1.fill((40,40,40)) + # s2.fill((255,255,255)) + # s1.set_at( (0,0), (170, 170, 170) ) + # # set the similar pixels in destination surface to the color + # # in the first surface. + # num_threshold_pixels = threshold( + # dest_surface=s2, + # surface=s1, + # search_color=(30,30,30), + # threshold=(11,11,11), + # set_color=None, + # set_behavior=THRESHOLD_BEHAVIOR_FROM_SEARCH_SURF) + + # #num_threshold_pixels = threshold(s2, s1, (30,30,30)) + # self.assertEqual(num_threshold_pixels, (s1.get_height() * s1.get_width()) -1) + # self.assertEqual(s2.get_at((0,0)), (0,0,0, 255)) + # self.assertEqual(s2.get_at((0,1)), (40, 40, 40, 255)) + # self.assertEqual(s2.get_at((17,1)), (40, 40, 40, 255)) + + # # abs(40 - 255) < 100 + # #(abs(c1[0] - r) < tr) + + # s1.fill((160,160,160)) + # s2.fill((255,255,255)) + # num_threshold_pixels = threshold(s2, s1, (255,255,255), (100,100,100), (0,0,0), True) + + # self.assertEqual(num_threshold_pixels, (s1.get_height() * s1.get_width())) + + # only one pixel should not be changed. + s1.fill((40, 40, 40)) + s1.set_at((0, 0), (170, 170, 170)) + THRESHOLD_BEHAVIOR_COUNT = 0 + + num_threshold_pixels = threshold( + dest_surface=None, + surface=s1, + search_color=(30, 30, 30), + threshold=(11, 11, 11), + set_color=None, + set_behavior=THRESHOLD_BEHAVIOR_COUNT, + ) + + # num_threshold_pixels = threshold(s2, s1, (30,30,30)) + self.assertEqual(num_threshold_pixels, (s1.get_height() * s1.get_width()) - 1) + + # test end markers. 0, and 255 + + # the pixels are different by 1. + s1.fill((254, 254, 254)) + s2.fill((255, 255, 255)) + s3.fill((255, 255, 255)) + s1.set_at((0, 0), (170, 170, 170)) + num_threshold_pixels = threshold( + None, s1, (254, 254, 254), (1, 1, 1), None, THRESHOLD_BEHAVIOR_COUNT + ) + self.assertEqual(num_threshold_pixels, (s1.get_height() * s1.get_width()) - 1) + + # compare the two surfaces. Should be all but one matching. + num_threshold_pixels = threshold( + None, s1, None, (1, 1, 1), None, THRESHOLD_BEHAVIOR_COUNT, s2 + ) + self.assertEqual(num_threshold_pixels, (s1.get_height() * s1.get_width()) - 1) + + # within (0,0,0) threshold? Should match no pixels. + num_threshold_pixels = threshold( + None, s1, (253, 253, 253), (0, 0, 0), None, THRESHOLD_BEHAVIOR_COUNT + ) + self.assertEqual(num_threshold_pixels, 0) + + # other surface within (0,0,0) threshold? Should match no pixels. + num_threshold_pixels = threshold( + None, s1, None, (0, 0, 0), None, THRESHOLD_BEHAVIOR_COUNT, s2 + ) + self.assertEqual(num_threshold_pixels, 0) + + def test_threshold__subclassed_surface(self): + """Ensure threshold accepts subclassed surfaces.""" + expected_size = (13, 11) + expected_flags = 0 + expected_depth = 32 + expected_color = (90, 80, 70, 255) + expected_count = 0 + surface = test_utils.SurfaceSubclass( + expected_size, expected_flags, expected_depth + ) + dest_surface = test_utils.SurfaceSubclass( + expected_size, expected_flags, expected_depth + ) + search_surface = test_utils.SurfaceSubclass( + expected_size, expected_flags, expected_depth + ) + surface.fill((10, 10, 10)) + dest_surface.fill((255, 255, 255)) + search_surface.fill((20, 20, 20)) + + count = pygame.transform.threshold( + dest_surface=dest_surface, + surface=surface, + threshold=(1, 1, 1), + set_color=expected_color, + search_color=None, + search_surf=search_surface, + ) + + self.assertIsInstance(dest_surface, pygame.Surface) + self.assertIsInstance(dest_surface, test_utils.SurfaceSubclass) + self.assertEqual(count, expected_count) + self.assertEqual(dest_surface.get_at((0, 0)), expected_color) + self.assertEqual(dest_surface.get_bitsize(), expected_depth) + self.assertEqual(dest_surface.get_size(), expected_size) + self.assertEqual(dest_surface.get_flags(), expected_flags) + + def test_laplacian(self): + """ """ + + SIZE = 32 + s1 = pygame.Surface((SIZE, SIZE)) + s2 = pygame.Surface((SIZE, SIZE)) + s1.fill((10, 10, 70)) + pygame.draw.line(s1, (255, 0, 0), (3, 10), (20, 20)) + + # a line at the last row of the image. + pygame.draw.line(s1, (255, 0, 0), (0, 31), (31, 31)) + + pygame.transform.laplacian(s1, s2) + + # show_image(s1) + # show_image(s2) + + self.assertEqual(s2.get_at((0, 0)), (0, 0, 0, 255)) + self.assertEqual(s2.get_at((3, 10)), (255, 0, 0, 255)) + self.assertEqual(s2.get_at((0, 31)), (255, 0, 0, 255)) + self.assertEqual(s2.get_at((31, 31)), (255, 0, 0, 255)) + + # here we create the return surface. + s2 = pygame.transform.laplacian(s1) + + self.assertEqual(s2.get_at((0, 0)), (0, 0, 0, 255)) + self.assertEqual(s2.get_at((3, 10)), (255, 0, 0, 255)) + self.assertEqual(s2.get_at((0, 31)), (255, 0, 0, 255)) + self.assertEqual(s2.get_at((31, 31)), (255, 0, 0, 255)) + + def test_laplacian__24_big_endian(self): + """ """ + pygame.display.init() + try: + surf_1 = pygame.image.load( + example_path(os.path.join("data", "laplacian.png")) + ) + SIZE = 32 + surf_2 = pygame.Surface((SIZE, SIZE), 0, 24) + # s1.fill((10, 10, 70)) + # pygame.draw.line(s1, (255, 0, 0), (3, 10), (20, 20)) + + # a line at the last row of the image. + # pygame.draw.line(s1, (255, 0, 0), (0, 31), (31, 31)) + + # Also validate keyword arguments + pygame.transform.laplacian(surface=surf_1, dest_surface=surf_2) + + # show_image(s1) + # show_image(s2) + + self.assertEqual(surf_2.get_at((0, 0)), (0, 0, 0, 255)) + self.assertEqual(surf_2.get_at((3, 10)), (255, 0, 0, 255)) + self.assertEqual(surf_2.get_at((0, 31)), (255, 0, 0, 255)) + self.assertEqual(surf_2.get_at((31, 31)), (255, 0, 0, 255)) + + # here we create the return surface. + surf_2 = pygame.transform.laplacian(surf_1) + + self.assertEqual(surf_2.get_at((0, 0)), (0, 0, 0, 255)) + self.assertEqual(surf_2.get_at((3, 10)), (255, 0, 0, 255)) + self.assertEqual(surf_2.get_at((0, 31)), (255, 0, 0, 255)) + self.assertEqual(surf_2.get_at((31, 31)), (255, 0, 0, 255)) + finally: + pygame.display.quit() + + def test_average_surfaces(self): + """ """ + + SIZE = 32 + s1 = pygame.Surface((SIZE, SIZE)) + s2 = pygame.Surface((SIZE, SIZE)) + s3 = pygame.Surface((SIZE, SIZE)) + s1.fill((10, 10, 70)) + s2.fill((10, 20, 70)) + s3.fill((10, 130, 10)) + + surfaces = [s1, s2, s3] + surfaces = [s1, s2] + sr = pygame.transform.average_surfaces(surfaces) + + self.assertEqual(sr.get_at((0, 0)), (10, 15, 70, 255)) + + self.assertRaises(TypeError, pygame.transform.average_surfaces, 1) + self.assertRaises(TypeError, pygame.transform.average_surfaces, []) + + self.assertRaises(TypeError, pygame.transform.average_surfaces, [1]) + self.assertRaises(TypeError, pygame.transform.average_surfaces, [s1, 1]) + self.assertRaises(TypeError, pygame.transform.average_surfaces, [1, s1]) + self.assertRaises(TypeError, pygame.transform.average_surfaces, [s1, s2, 1]) + + self.assertRaises( + TypeError, pygame.transform.average_surfaces, (s for s in [s1, s2, s3]) + ) + + def test_average_surfaces__24(self): + SIZE = 32 + depth = 24 + s1 = pygame.Surface((SIZE, SIZE), 0, depth) + s2 = pygame.Surface((SIZE, SIZE), 0, depth) + s3 = pygame.Surface((SIZE, SIZE), 0, depth) + s1.fill((10, 10, 70, 255)) + s2.fill((10, 20, 70, 255)) + s3.fill((10, 130, 10, 255)) + + surfaces = [s1, s2, s3] + sr = pygame.transform.average_surfaces(surfaces) + self.assertEqual(sr.get_masks(), s1.get_masks()) + self.assertEqual(sr.get_flags(), s1.get_flags()) + self.assertEqual(sr.get_losses(), s1.get_losses()) + + if 0: + print(sr, s1) + print(sr.get_masks(), s1.get_masks()) + print(sr.get_flags(), s1.get_flags()) + print(sr.get_losses(), s1.get_losses()) + print(sr.get_shifts(), s1.get_shifts()) + + self.assertEqual(sr.get_at((0, 0)), (10, 53, 50, 255)) + + def test_average_surfaces__24_big_endian(self): + pygame.display.init() + try: + surf_1 = pygame.image.load(example_path(os.path.join("data", "BGR.png"))) + + surf_2 = surf_1.copy() + + surfaces = [surf_1, surf_2] + self.assertEqual(surf_1.get_at((0, 0)), (255, 0, 0, 255)) + self.assertEqual(surf_2.get_at((0, 0)), (255, 0, 0, 255)) + + surf_av = pygame.transform.average_surfaces(surfaces) + self.assertEqual(surf_av.get_masks(), surf_1.get_masks()) + self.assertEqual(surf_av.get_flags(), surf_1.get_flags()) + self.assertEqual(surf_av.get_losses(), surf_1.get_losses()) + + self.assertEqual(surf_av.get_at((0, 0)), (255, 0, 0, 255)) + finally: + pygame.display.quit() + + def test_average_surfaces__subclassed_surfaces(self): + """Ensure average_surfaces accepts subclassed surfaces.""" + expected_size = (23, 17) + expected_flags = 0 + expected_depth = 32 + expected_color = (50, 50, 50, 255) + surfaces = [] + + for color in ((40, 60, 40), (60, 40, 60)): + s = test_utils.SurfaceSubclass( + expected_size, expected_flags, expected_depth + ) + s.fill(color) + surfaces.append(s) + + surface = pygame.transform.average_surfaces(surfaces) + + self.assertIsInstance(surface, pygame.Surface) + self.assertNotIsInstance(surface, test_utils.SurfaceSubclass) + self.assertEqual(surface.get_at((0, 0)), expected_color) + self.assertEqual(surface.get_bitsize(), expected_depth) + self.assertEqual(surface.get_size(), expected_size) + self.assertEqual(surface.get_flags(), expected_flags) + + def test_average_surfaces__subclassed_destination_surface(self): + """Ensure average_surfaces accepts a destination subclassed surface.""" + expected_size = (13, 27) + expected_flags = 0 + expected_depth = 32 + expected_color = (15, 15, 15, 255) + surfaces = [] + + for color in ((10, 10, 20), (20, 20, 10), (30, 30, 30)): + s = test_utils.SurfaceSubclass( + expected_size, expected_flags, expected_depth + ) + s.fill(color) + surfaces.append(s) + expected_dest_surface = surfaces.pop() + + # Also validate keyword arguments + dest_surface = pygame.transform.average_surfaces( + surfaces=surfaces, dest_surface=expected_dest_surface + ) + + self.assertIsInstance(dest_surface, pygame.Surface) + self.assertIsInstance(dest_surface, test_utils.SurfaceSubclass) + self.assertIs(dest_surface, expected_dest_surface) + self.assertEqual(dest_surface.get_at((0, 0)), expected_color) + self.assertEqual(dest_surface.get_bitsize(), expected_depth) + self.assertEqual(dest_surface.get_size(), expected_size) + self.assertEqual(dest_surface.get_flags(), expected_flags) + + def test_average_color(self): + """ """ + for i in (24, 32): + with self.subTest(f"Testing {i}-bit surface"): + s = pygame.Surface((32, 32), 0, i) + s.fill((0, 100, 200)) + s.fill((10, 50, 100), (0, 0, 16, 32)) + + self.assertEqual(pygame.transform.average_color(s), (5, 75, 150, 0)) + + # Also validate keyword arguments + avg_color = pygame.transform.average_color( + surface=s, rect=(16, 0, 16, 32) + ) + self.assertEqual(avg_color, (0, 100, 200, 0)) + + def test_average_color_considering_alpha_all_pixels_opaque(self): + """ """ + s = pygame.Surface((32, 32), pygame.SRCALPHA, 32) + s.fill((0, 100, 200, 255)) + s.fill((10, 50, 100, 255), (0, 0, 16, 32)) + + self.assertEqual( + pygame.transform.average_color(s, consider_alpha=True), (5, 75, 150, 255) + ) + + # Also validate keyword arguments + avg_color = pygame.transform.average_color( + surface=s, rect=(16, 0, 16, 32), consider_alpha=True + ) + self.assertEqual(avg_color, (0, 100, 200, 255)) + + def test_average_color_considering_alpha(self): + """ """ + s = pygame.Surface((32, 32), pygame.SRCALPHA, 32) + s.fill((0, 100, 200, 255)) + s.fill((10, 50, 100, 128), (0, 0, 16, 32)) + + # formula for this example of half filled square + # n = number of pixels, e.g. 32 * 32 + # rgb = (n/2 * ( a_left * rgb_left) + n/2 (a_right * rgb_right) ) / (n/2 * a_left + n/2 * a_right) + # a = (n/2 * a_left + n/2 * a_right) / n + self.assertEqual( + pygame.transform.average_color(s, consider_alpha=True), (3, 83, 166, 191) + ) + + # Also validate keyword arguments + avg_color = pygame.transform.average_color( + surface=s, rect=(0, 0, 16, 32), consider_alpha=True + ) + self.assertEqual(avg_color, (10, 50, 100, 128)) + + def test_rotate(self): + # setting colors and canvas + blue = (0, 0, 255, 255) + red = (255, 0, 0, 255) + black = (0, 0, 0) + canvas = pygame.Surface((3, 3)) + rotation = 0 + + canvas.set_at((2, 0), blue) + canvas.set_at((0, 2), red) + + self.assertEqual(canvas.get_at((0, 0)), black) + self.assertEqual(canvas.get_at((2, 0)), blue) + self.assertEqual(canvas.get_at((0, 2)), red) + + for i in range(0, 4): + if i % 2 == 0: + self.assertEqual(canvas.get_at((0, 0)), black) + elif i == 1: + self.assertEqual(canvas.get_at((0, 0)), blue) + elif i == 3: + self.assertEqual(canvas.get_at((0, 0)), red) + + rotation += 90 + # Also validate keyword arguments + canvas = pygame.transform.rotate(surface=canvas, angle=90) + + self.assertEqual(canvas.get_at((0, 0)), black) + + def test_rotate_of_0_sized_surface(self): + # This function just tests possible Segmentation Fault + canvas1 = pygame.Surface((0, 1)) + canvas2 = pygame.Surface((1, 0)) + pygame.transform.rotate(canvas1, 42) + pygame.transform.rotate(canvas2, 42) + + def test_rotate__lossless_at_90_degrees(self): + w, h = 32, 32 + s = pygame.Surface((w, h), pygame.SRCALPHA) + + gradient = list(test_utils.gradient(w, h)) + + for pt, color in gradient: + s.set_at(pt, color) + + for rotation in (90, -90): + s = pygame.transform.rotate(s, rotation) + + for pt, color in gradient: + self.assertTrue(s.get_at(pt) == color) + + def test_scale2x(self): + # __doc__ (as of 2008-06-25) for pygame.transform.scale2x: + + # pygame.transform.scale2x(Surface, DestSurface = None): Surface + # specialized image doubler + + w, h = 32, 32 + s = pygame.Surface((w, h), pygame.SRCALPHA, 32) + + # s.set_at((0,0), (20, 20, 20, 255)) + + s1 = pygame.transform.scale2x(s) + # Also validate keyword arguments + s2 = pygame.transform.scale2x(surface=s) + self.assertEqual(s1.get_rect().size, (64, 64)) + self.assertEqual(s2.get_rect().size, (64, 64)) + + def test_scale2xraw(self): + w, h = 32, 32 + s = pygame.Surface((w, h), pygame.SRCALPHA, 32) + s.fill((0, 0, 0)) + pygame.draw.circle(s, (255, 0, 0), (w // 2, h // 2), (w // 3)) + + s2 = pygame.transform.scale(s, (w * 2, h * 2)) + s2_2 = pygame.transform.scale(s2, (w * 4, h * 4)) + s4 = pygame.transform.scale(s, (w * 4, h * 4)) + + self.assertEqual(s2_2.get_rect().size, (128, 128)) + + for pt in test_utils.rect_area_pts(s2_2.get_rect()): + self.assertEqual(s2_2.get_at(pt), s4.get_at(pt)) + + def test_get_smoothscale_backend(self): + filter_type = pygame.transform.get_smoothscale_backend() + self.assertTrue(filter_type in ["GENERIC", "MMX", "SSE"]) + # It would be nice to test if a non-generic type corresponds to an x86 + # processor. But there is no simple test for this. platform.machine() + # returns process version specific information, like 'i686'. + + def test_set_smoothscale_backend(self): + # All machines should allow 'GENERIC'. + original_type = pygame.transform.get_smoothscale_backend() + pygame.transform.set_smoothscale_backend("GENERIC") + filter_type = pygame.transform.get_smoothscale_backend() + self.assertEqual(filter_type, "GENERIC") + # All machines should allow returning to original value. + # Also check that keyword argument works. + pygame.transform.set_smoothscale_backend(backend=original_type) + + # Something invalid. + def change(): + pygame.transform.set_smoothscale_backend("mmx") + + self.assertRaises(ValueError, change) + + # Invalid argument keyword. + def change(): + pygame.transform.set_smoothscale_backend(t="GENERIC") + + self.assertRaises(TypeError, change) + + # Invalid argument type. + def change(): + pygame.transform.set_smoothscale_backend(1) + + self.assertRaises(TypeError, change) + # Unsupported type, if possible. + if original_type != "SSE": + + def change(): + pygame.transform.set_smoothscale_backend("SSE") + + self.assertRaises(ValueError, change) + # Should be back where we started. + filter_type = pygame.transform.get_smoothscale_backend() + self.assertEqual(filter_type, original_type) + + def test_chop(self): + original_surface = pygame.Surface((20, 20)) + pygame.draw.rect(original_surface, (255, 0, 0), (0, 0, 10, 10)) + pygame.draw.rect(original_surface, (0, 255, 0), (0, 10, 10, 10)) + pygame.draw.rect(original_surface, (0, 0, 255), (10, 0, 10, 10)) + pygame.draw.rect(original_surface, (255, 255, 0), (10, 10, 10, 10)) + # Test chopping the corner of image + rect = pygame.Rect(0, 0, 5, 15) + test_surface = pygame.transform.chop(original_surface, rect) + # Check the size of chopped image + self.assertEqual(test_surface.get_size(), (15, 5)) + # Check if the colors of the chopped image are correct + for x in range(15): + for y in range(5): + if x < 5: + self.assertEqual(test_surface.get_at((x, y)), (0, 255, 0)) + else: + self.assertEqual(test_surface.get_at((x, y)), (255, 255, 0)) + # Check if the original image stayed the same + self.assertEqual(original_surface.get_size(), (20, 20)) + for x in range(20): + for y in range(20): + if x < 10 and y < 10: + self.assertEqual(original_surface.get_at((x, y)), (255, 0, 0)) + if x < 10 < y: + self.assertEqual(original_surface.get_at((x, y)), (0, 255, 0)) + if x > 10 > y: + self.assertEqual(original_surface.get_at((x, y)), (0, 0, 255)) + if x > 10 and y > 10: + self.assertEqual(original_surface.get_at((x, y)), (255, 255, 0)) + # Test chopping the center of the surface: + rect = pygame.Rect(0, 0, 10, 10) + rect.center = original_surface.get_rect().center + # Also validate keyword arguments + test_surface = pygame.transform.chop(surface=original_surface, rect=rect) + self.assertEqual(test_surface.get_size(), (10, 10)) + for x in range(10): + for y in range(10): + if x < 5 and y < 5: + self.assertEqual(test_surface.get_at((x, y)), (255, 0, 0)) + if x < 5 < y: + self.assertEqual(test_surface.get_at((x, y)), (0, 255, 0)) + if x > 5 > y: + self.assertEqual(test_surface.get_at((x, y)), (0, 0, 255)) + if x > 5 and y > 5: + self.assertEqual(test_surface.get_at((x, y)), (255, 255, 0)) + # Test chopping with the empty rect + rect = pygame.Rect(10, 10, 0, 0) + test_surface = pygame.transform.chop(original_surface, rect) + self.assertEqual(test_surface.get_size(), (20, 20)) + # Test chopping the entire surface + rect = pygame.Rect(0, 0, 20, 20) + test_surface = pygame.transform.chop(original_surface, rect) + self.assertEqual(test_surface.get_size(), (0, 0)) + # Test chopping outside of surface + rect = pygame.Rect(5, 15, 20, 20) + test_surface = pygame.transform.chop(original_surface, rect) + self.assertEqual(test_surface.get_size(), (5, 15)) + rect = pygame.Rect(400, 400, 10, 10) + test_surface = pygame.transform.chop(original_surface, rect) + self.assertEqual(test_surface.get_size(), (20, 20)) + + def test_rotozoom(self): + # __doc__ (as of 2008-08-02) for pygame.transform.rotozoom: + + # pygame.transform.rotozoom(Surface, angle, scale): return Surface + # filtered scale and rotation + # + # This is a combined scale and rotation transform. The resulting + # Surface will be a filtered 32-bit Surface. The scale argument is a + # floating point value that will be multiplied by the current + # resolution. The angle argument is a floating point value that + # represents the counterclockwise degrees to rotate. A negative + # rotation angle will rotate clockwise. + + s = pygame.Surface((10, 0)) + pygame.transform.scale(s, (10, 2)) + s1 = pygame.transform.rotozoom(s, 30, 1) + # Also validate keyword arguments + s2 = pygame.transform.rotozoom(surface=s, angle=30, scale=1) + + self.assertEqual(s1.get_rect(), pygame.Rect(0, 0, 0, 0)) + self.assertEqual(s2.get_rect(), pygame.Rect(0, 0, 0, 0)) + + def test_smoothscale(self): + """Tests the stated boundaries, sizing, and color blending of smoothscale function""" + # __doc__ (as of 2008-08-02) for pygame.transform.smoothscale: + + # pygame.transform.smoothscale(Surface, (width, height), DestSurface = + # None): return Surface + # + # scale a surface to an arbitrary size smoothly + # + # Uses one of two different algorithms for scaling each dimension of + # the input surface as required. For shrinkage, the output pixels are + # area averages of the colors they cover. For expansion, a bilinear + # filter is used. For the amd64 and i686 architectures, optimized MMX + # routines are included and will run much faster than other machine + # types. The size is a 2 number sequence for (width, height). This + # function only works for 24-bit or 32-bit surfaces. An exception + # will be thrown if the input surface bit depth is less than 24. + # + # New in pygame 1.8 + + # check stated exceptions + def smoothscale_low_bpp(): + starting_surface = pygame.Surface((20, 20), depth=12) + smoothscaled_surface = pygame.transform.smoothscale( + starting_surface, (10, 10) + ) + + self.assertRaises(ValueError, smoothscale_low_bpp) + + def smoothscale_high_bpp(): + starting_surface = pygame.Surface((20, 20), depth=48) + smoothscaled_surface = pygame.transform.smoothscale( + starting_surface, (10, 10) + ) + + self.assertRaises(ValueError, smoothscale_high_bpp) + + def smoothscale_invalid_scale(): + starting_surface = pygame.Surface((20, 20), depth=32) + smoothscaled_surface = pygame.transform.smoothscale( + starting_surface, (-1, -1) + ) + + self.assertRaises(ValueError, smoothscale_invalid_scale) + + # Test Color Blending Scaling-Up + two_pixel_surface = pygame.Surface((2, 1), depth=32) + two_pixel_surface.fill(pygame.Color(0, 0, 0), pygame.Rect(0, 0, 1, 1)) + two_pixel_surface.fill(pygame.Color(255, 255, 255), pygame.Rect(1, 0, 1, 1)) + for k in [2**x for x in range(5, 8)]: # Enlarge to targets 32, 64...256 + bigger_surface = pygame.transform.smoothscale(two_pixel_surface, (k, 1)) + self.assertEqual( + bigger_surface.get_at((k // 2, 0)), pygame.Color(127, 127, 127) + ) + self.assertEqual(bigger_surface.get_size(), (k, 1)) + # Test Color Blending Scaling-Down + two_five_six_surf = pygame.Surface((256, 1), depth=32) + two_five_six_surf.fill(pygame.Color(0, 0, 0), pygame.Rect(0, 0, 128, 1)) + two_five_six_surf.fill(pygame.Color(255, 255, 255), pygame.Rect(128, 0, 128, 1)) + for k in range(3, 11, 2): # Shrink to targets 3, 5...11 pixels wide + smaller_surface = pygame.transform.smoothscale(two_five_six_surf, (k, 1)) + self.assertEqual( + smaller_surface.get_at(((k // 2), 0)), pygame.Color(127, 127, 127) + ) + self.assertEqual(smaller_surface.get_size(), (k, 1)) + + +class TransformDisplayModuleTest(unittest.TestCase): + def setUp(self): + pygame.display.init() + pygame.display.set_mode((320, 200)) + + def tearDown(self): + pygame.display.quit() + + def test_flip(self): + """honors the set_color key on the returned surface from flip.""" + image_loaded = pygame.image.load(example_path("data/chimp.png")) + + image = pygame.Surface(image_loaded.get_size(), 0, 32) + image.blit(image_loaded, (0, 0)) + + image_converted = image_loaded.convert() + + self.assertFalse(image.get_flags() & pygame.SRCALPHA) + self.assertFalse(image_converted.get_flags() & pygame.SRCALPHA) + + surf = pygame.Surface(image.get_size(), 0, 32) + surf2 = pygame.Surface(image.get_size(), 0, 32) + + surf.fill((255, 255, 255)) + surf2.fill((255, 255, 255)) + + colorkey = image.get_at((0, 0)) + image.set_colorkey(colorkey, RLEACCEL) + timage = pygame.transform.flip(image, 1, 0) + + colorkey = image_converted.get_at((0, 0)) + image_converted.set_colorkey(colorkey, RLEACCEL) + # Also validate keyword arguments + timage_converted = pygame.transform.flip( + surface=image_converted, flip_x=1, flip_y=0 + ) + + # blit the flipped surface, and non flipped surface. + surf.blit(timage, (0, 0)) + surf2.blit(image, (0, 0)) + + # the results should be the same. + self.assertEqual(surf.get_at((0, 0)), surf2.get_at((0, 0))) + self.assertEqual(surf2.get_at((0, 0)), (255, 255, 255, 255)) + + # now we test the convert() ed image also works. + surf.fill((255, 255, 255)) + surf2.fill((255, 255, 255)) + surf.blit(timage_converted, (0, 0)) + surf2.blit(image_converted, (0, 0)) + self.assertEqual(surf.get_at((0, 0)), surf2.get_at((0, 0))) + + def test_flip_alpha(self): + """returns a surface with the same properties as the input.""" + image_loaded = pygame.image.load(example_path("data/chimp.png")) + + image_alpha = pygame.Surface(image_loaded.get_size(), pygame.SRCALPHA, 32) + image_alpha.blit(image_loaded, (0, 0)) + + surf = pygame.Surface(image_loaded.get_size(), 0, 32) + surf2 = pygame.Surface(image_loaded.get_size(), 0, 32) + + colorkey = image_alpha.get_at((0, 0)) + image_alpha.set_colorkey(colorkey, RLEACCEL) + timage_alpha = pygame.transform.flip(image_alpha, 1, 0) + + self.assertTrue(image_alpha.get_flags() & pygame.SRCALPHA) + self.assertTrue(timage_alpha.get_flags() & pygame.SRCALPHA) + + # now we test the alpha image works. + surf.fill((255, 255, 255)) + surf2.fill((255, 255, 255)) + surf.blit(timage_alpha, (0, 0)) + surf2.blit(image_alpha, (0, 0)) + self.assertEqual(surf.get_at((0, 0)), surf2.get_at((0, 0))) + self.assertEqual(surf2.get_at((0, 0)), (255, 0, 0, 255)) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/version_test.py b/laplas/abstract_map/pygame/tests/version_test.py new file mode 100644 index 0000000..ba0bb3d --- /dev/null +++ b/laplas/abstract_map/pygame/tests/version_test.py @@ -0,0 +1,48 @@ +import os +import unittest + + +pg_header = os.path.join("src_c", "include", "_pygame.h") + + +class VersionTest(unittest.TestCase): + @unittest.skipIf( + not os.path.isfile(pg_header), "Skipping because we cannot find _pygame.h" + ) + def test_pg_version_consistency(self): + from pygame import version + + pgh_major = -1 + pgh_minor = -1 + pgh_patch = -1 + import re + + major_exp_search = re.compile(r"define\s+PG_MAJOR_VERSION\s+([0-9]+)").search + minor_exp_search = re.compile(r"define\s+PG_MINOR_VERSION\s+([0-9]+)").search + patch_exp_search = re.compile(r"define\s+PG_PATCH_VERSION\s+([0-9]+)").search + with open(pg_header) as f: + for line in f: + if pgh_major == -1: + m = major_exp_search(line) + if m: + pgh_major = int(m.group(1)) + if pgh_minor == -1: + m = minor_exp_search(line) + if m: + pgh_minor = int(m.group(1)) + if pgh_patch == -1: + m = patch_exp_search(line) + if m: + pgh_patch = int(m.group(1)) + self.assertEqual(pgh_major, version.vernum[0]) + self.assertEqual(pgh_minor, version.vernum[1]) + self.assertEqual(pgh_patch, version.vernum[2]) + + def test_sdl_version(self): + from pygame import version + + self.assertEqual(len(version.SDL), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/abstract_map/pygame/tests/video_test.py b/laplas/abstract_map/pygame/tests/video_test.py new file mode 100644 index 0000000..55ef414 --- /dev/null +++ b/laplas/abstract_map/pygame/tests/video_test.py @@ -0,0 +1,26 @@ +import unittest +import sys +import pygame + +from pygame._sdl2 import video + + +class VideoModuleTest(unittest.TestCase): + default_caption = "pygame window" + + @unittest.skipIf( + not (sys.maxsize > 2**32), + "32 bit SDL 2.0.16 has an issue.", + ) + def test_renderer_set_viewport(self): + """works.""" + window = video.Window(title=self.default_caption, size=(800, 600)) + renderer = video.Renderer(window=window) + renderer.logical_size = (1920, 1080) + rect = pygame.Rect(0, 0, 1920, 1080) + renderer.set_viewport(rect) + self.assertEqual(renderer.get_viewport(), (0, 0, 1920, 1080)) + + +if __name__ == "__main__": + unittest.main() diff --git a/laplas/tools/abstract_map/pygame/threads/__init__.py b/laplas/abstract_map/pygame/threads/__init__.py similarity index 100% rename from laplas/tools/abstract_map/pygame/threads/__init__.py rename to laplas/abstract_map/pygame/threads/__init__.py diff --git a/laplas/tools/abstract_map/pygame/time.pyi b/laplas/abstract_map/pygame/time.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/time.pyi rename to laplas/abstract_map/pygame/time.pyi diff --git a/laplas/tools/abstract_map/pygame/transform.pyi b/laplas/abstract_map/pygame/transform.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/transform.pyi rename to laplas/abstract_map/pygame/transform.pyi diff --git a/laplas/tools/abstract_map/pygame/version.py b/laplas/abstract_map/pygame/version.py similarity index 100% rename from laplas/tools/abstract_map/pygame/version.py rename to laplas/abstract_map/pygame/version.py diff --git a/laplas/tools/abstract_map/pygame/version.pyi b/laplas/abstract_map/pygame/version.pyi similarity index 100% rename from laplas/tools/abstract_map/pygame/version.pyi rename to laplas/abstract_map/pygame/version.pyi diff --git a/laplas/tools/abstract_map/pygame/zlib1.dll b/laplas/abstract_map/pygame/zlib1.dll similarity index 100% rename from laplas/tools/abstract_map/pygame/zlib1.dll rename to laplas/abstract_map/pygame/zlib1.dll diff --git a/laplas/abstract_map/server.py b/laplas/abstract_map/server.py new file mode 100644 index 0000000..62d883d --- /dev/null +++ b/laplas/abstract_map/server.py @@ -0,0 +1,146 @@ +import threading +import pygame +from http.server import BaseHTTPRequestHandler, HTTPServer +import urllib.parse +import io + +SUCCESS = "success" +FAILED = "failed" +ERROR = "error" +ACCESS_DENIED = "Access to map denied due to wrong key!" +VISUAL = True + +acess_key = None +map_instance = None +process = True + +def handle_ping(): + pass +# return 'PINGED' + +def handle_set_key(query): + global acess_key + key = urllib.parse.parse_qs(query).get('key', [''])[0].strip() + if acess_key: + return 'ABSTRACT MAP ERROR: ATTEMPT REPLACE EXISTED ACCESS KEY', 400 + if not key: + return 'ABSTRACT MAP ERROR: ATTEMPT SET HTTP KEY WITH UNEXISTED STRING', 400 + acess_key = key + return SUCCESS, 200 + +def handle_reset_key(query): + global acess_key + key = urllib.parse.parse_qs(query).get('key', [''])[0].strip() + if key == acess_key: + acess_key = None + return SUCCESS, 200 + return FAILED, 400 + +def handle_init_map(query): + global map_instance + params = urllib.parse.parse_qs(query) + key = params.get('key', [''])[0] + size_x = int(params.get('size_x', ['1'])[0]) + size_y = int(params.get('size_y', ['1'])[0]) + if key != acess_key: + return ACCESS_DENIED, 403 + map_instance = overmap(size_x, size_y, VISUAL) + return SUCCESS, 200 + +def handle_create_obj(query): + return 'OBJECT CREATION PLACEHOLDER', 200 + +def handle_move_obj(query): + return 'OBJECT MOVEMENT PLACEHOLDER', 200 + +# Роутер запросов +ROUTES = { + '/': { + 'GET': handle_ping, + 'POST': handle_ping + }, + '/set_key': { + 'GET': handle_set_key, + 'POST': handle_set_key + }, + '/reset_key': { + 'GET': handle_reset_key, + 'POST': handle_reset_key + }, + '/init_map': { + 'GET': handle_init_map, + 'POST': handle_init_map + }, + '/create_obj': { + 'GET': handle_create_obj, + 'POST': handle_create_obj + }, + '/move_obj': { + 'GET': handle_move_obj, + 'POST': handle_move_obj + } +} + +class RequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.handle_request(self.path, self.command, self.rfile, self.headers) + + def do_POST(self): + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length).decode() + self.handle_request(self.path, 'POST', io.StringIO(post_data), self.headers) + + def handle_request(self, path, method, data_source, headers): + route = ROUTES.get(path) + if route: + handler = route.get(method) + if handler: + if method == 'POST': + query = data_source.getvalue() + else: + query = urllib.parse.urlparse(path).query + + response, status_code = handler(query) + self.send_response(status_code) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(response.encode()) + return + + self.send_response(404) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(b'Not Found') + +def run_http_server(): + server_address = ("127.0.0.1", 5000) + print(f"Running on: http://{server_address[0]}:{server_address[1]}/") + httpd = HTTPServer(server_address, RequestHandler) + print("Starting HTTP server") + httpd.serve_forever() + +from overmap import overmap + +def game_loop(): + global process + pygame.init() + map_instance = overmap(1000, 1000, True) + + while process: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + process = False + pygame.quit() + break + + if map_instance: + map_instance.process() + + pygame.display.flip() + pygame.time.wait(10) + +if __name__ == '__main__': + game_thread = threading.Thread(target=game_loop) + game_thread.start() + + run_http_server() diff --git a/laplas/code/modules/abstract_overmap/SSabstract_overmap.dm b/laplas/code/modules/abstract_overmap/SSabstract_overmap.dm index f39e237..c56841e 100644 --- a/laplas/code/modules/abstract_overmap/SSabstract_overmap.dm +++ b/laplas/code/modules/abstract_overmap/SSabstract_overmap.dm @@ -35,7 +35,6 @@ SUBSYSTEM_DEF(abstract_overmap) CRASH("[name] failed to install new secure key!") init_map() - /datum/controller/subsystem/abstract_overmap/proc/init_map() . = "" . += "key = [http_key]," diff --git a/laplas/tools/abstract_map/camera.py b/laplas/tools/abstract_map/camera.py deleted file mode 100644 index f285afb..0000000 --- a/laplas/tools/abstract_map/camera.py +++ /dev/null @@ -1,40 +0,0 @@ -import pygame - -class camera: - def __init__(self, screen, surface, new_x, new_y) -> None: - self.width = 800 - self.height = 600 - self.dragging = False - self.last_mouse_x = 0 - self.last_mouse_y = 0 - - self.x = new_x - self.y = new_y - - self.screen = screen - self.render_surface = surface - self.camera_rect = pygame.Rect(self.x, self.y, self.width, self.height) - - def process(self): - for event in pygame.event.get(): - if(event.type == pygame.MOUSEBUTTONDOWN): - if(event.button == 1): - self.dragging = True - self.last_mouse_x, self.last_mouse_y = event.pos - - if(event.type == pygame.MOUSEBUTTONUP): - if(event.button == 1): # Левая кнопка мыши - self.dragging = False - - if(event.type == pygame.MOUSEMOTION): - if(self.dragging): - mouse_x, mouse_y = event.pos - dx = self.last_mouse_x - mouse_x - dy = self.last_mouse_y - mouse_y - self.x += dx - self.y += dy - self.last_mouse_x, self.last_mouse_y = self.x, self.y - - self.camera_rect.topleft = (self.x, self.y) - self.screen.blit(self.render_surface, (0, 0), self.camera_rect) - pygame.display.flip() diff --git a/laplas/tools/abstract_map/flask/__init__.py b/laplas/tools/abstract_map/flask/__init__.py deleted file mode 100644 index feb5334..0000000 --- a/laplas/tools/abstract_map/flask/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -from markupsafe import escape -from markupsafe import Markup -from werkzeug.exceptions import abort as abort -from werkzeug.utils import redirect as redirect - -from . import json as json -from .app import Flask as Flask -from .app import Request as Request -from .app import Response as Response -from .blueprints import Blueprint as Blueprint -from .config import Config as Config -from .ctx import after_this_request as after_this_request -from .ctx import copy_current_request_context as copy_current_request_context -from .ctx import has_app_context as has_app_context -from .ctx import has_request_context as has_request_context -from .globals import _app_ctx_stack as _app_ctx_stack -from .globals import _request_ctx_stack as _request_ctx_stack -from .globals import current_app as current_app -from .globals import g as g -from .globals import request as request -from .globals import session as session -from .helpers import flash as flash -from .helpers import get_flashed_messages as get_flashed_messages -from .helpers import get_template_attribute as get_template_attribute -from .helpers import make_response as make_response -from .helpers import safe_join as safe_join -from .helpers import send_file as send_file -from .helpers import send_from_directory as send_from_directory -from .helpers import stream_with_context as stream_with_context -from .helpers import url_for as url_for -from .json import jsonify as jsonify -from .signals import appcontext_popped as appcontext_popped -from .signals import appcontext_pushed as appcontext_pushed -from .signals import appcontext_tearing_down as appcontext_tearing_down -from .signals import before_render_template as before_render_template -from .signals import got_request_exception as got_request_exception -from .signals import message_flashed as message_flashed -from .signals import request_finished as request_finished -from .signals import request_started as request_started -from .signals import request_tearing_down as request_tearing_down -from .signals import signals_available as signals_available -from .signals import template_rendered as template_rendered -from .templating import render_template as render_template -from .templating import render_template_string as render_template_string - -__version__ = "2.0.3" diff --git a/laplas/tools/abstract_map/flask/__main__.py b/laplas/tools/abstract_map/flask/__main__.py deleted file mode 100644 index 4e28416..0000000 --- a/laplas/tools/abstract_map/flask/__main__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .cli import main - -main() diff --git a/laplas/tools/abstract_map/flask/app.py b/laplas/tools/abstract_map/flask/app.py deleted file mode 100644 index 23b99e2..0000000 --- a/laplas/tools/abstract_map/flask/app.py +++ /dev/null @@ -1,2091 +0,0 @@ -import functools -import inspect -import logging -import os -import sys -import typing as t -import weakref -from datetime import timedelta -from itertools import chain -from threading import Lock -from types import TracebackType - -from werkzeug.datastructures import Headers -from werkzeug.datastructures import ImmutableDict -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import BadRequestKeyError -from werkzeug.exceptions import HTTPException -from werkzeug.exceptions import InternalServerError -from werkzeug.local import ContextVar -from werkzeug.routing import BuildError -from werkzeug.routing import Map -from werkzeug.routing import MapAdapter -from werkzeug.routing import RequestRedirect -from werkzeug.routing import RoutingException -from werkzeug.routing import Rule -from werkzeug.wrappers import Response as BaseResponse - -from . import cli -from . import json -from .config import Config -from .config import ConfigAttribute -from .ctx import _AppCtxGlobals -from .ctx import AppContext -from .ctx import RequestContext -from .globals import _request_ctx_stack -from .globals import g -from .globals import request -from .globals import session -from .helpers import _split_blueprint_path -from .helpers import get_debug_flag -from .helpers import get_env -from .helpers import get_flashed_messages -from .helpers import get_load_dotenv -from .helpers import locked_cached_property -from .helpers import url_for -from .json import jsonify -from .logging import create_logger -from .scaffold import _endpoint_from_view_func -from .scaffold import _sentinel -from .scaffold import find_package -from .scaffold import Scaffold -from .scaffold import setupmethod -from .sessions import SecureCookieSessionInterface -from .signals import appcontext_tearing_down -from .signals import got_request_exception -from .signals import request_finished -from .signals import request_started -from .signals import request_tearing_down -from .templating import DispatchingJinjaLoader -from .templating import Environment -from .typing import BeforeFirstRequestCallable -from .typing import ResponseReturnValue -from .typing import TeardownCallable -from .typing import TemplateFilterCallable -from .typing import TemplateGlobalCallable -from .typing import TemplateTestCallable -from .wrappers import Request -from .wrappers import Response - -if t.TYPE_CHECKING: - import typing_extensions as te - from .blueprints import Blueprint - from .testing import FlaskClient - from .testing import FlaskCliRunner - from .typing import ErrorHandlerCallable - -if sys.version_info >= (3, 8): - iscoroutinefunction = inspect.iscoroutinefunction -else: - - def iscoroutinefunction(func: t.Any) -> bool: - while inspect.ismethod(func): - func = func.__func__ - - while isinstance(func, functools.partial): - func = func.func - - return inspect.iscoroutinefunction(func) - - -def _make_timedelta(value: t.Optional[timedelta]) -> t.Optional[timedelta]: - if value is None or isinstance(value, timedelta): - return value - - return timedelta(seconds=value) - - -class Flask(Scaffold): - """The flask object implements a WSGI application and acts as the central - object. It is passed the name of the module or package of the - application. Once it is created it will act as a central registry for - the view functions, the URL rules, template configuration and much more. - - The name of the package is used to resolve resources from inside the - package or the folder the module is contained in depending on if the - package parameter resolves to an actual python package (a folder with - an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). - - For more information about resource loading, see :func:`open_resource`. - - Usually you create a :class:`Flask` instance in your main module or - in the :file:`__init__.py` file of your package like this:: - - from flask import Flask - app = Flask(__name__) - - .. admonition:: About the First Parameter - - The idea of the first parameter is to give Flask an idea of what - belongs to your application. This name is used to find resources - on the filesystem, can be used by extensions to improve debugging - information and a lot more. - - So it's important what you provide there. If you are using a single - module, `__name__` is always the correct value. If you however are - using a package, it's usually recommended to hardcode the name of - your package there. - - For example if your application is defined in :file:`yourapplication/app.py` - you should create it with one of the two versions below:: - - app = Flask('yourapplication') - app = Flask(__name__.split('.')[0]) - - Why is that? The application will work even with `__name__`, thanks - to how resources are looked up. However it will make debugging more - painful. Certain extensions can make assumptions based on the - import name of your application. For example the Flask-SQLAlchemy - extension will look for the code in your application that triggered - an SQL query in debug mode. If the import name is not properly set - up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplication.app` and not - `yourapplication.views.frontend`) - - .. versionadded:: 0.7 - The `static_url_path`, `static_folder`, and `template_folder` - parameters were added. - - .. versionadded:: 0.8 - The `instance_path` and `instance_relative_config` parameters were - added. - - .. versionadded:: 0.11 - The `root_path` parameter was added. - - .. versionadded:: 1.0 - The ``host_matching`` and ``static_host`` parameters were added. - - .. versionadded:: 1.0 - The ``subdomain_matching`` parameter was added. Subdomain - matching needs to be enabled manually now. Setting - :data:`SERVER_NAME` does not implicitly enable it. - - :param import_name: the name of the application package - :param static_url_path: can be used to specify a different path for the - static files on the web. Defaults to the name - of the `static_folder` folder. - :param static_folder: The folder with static files that is served at - ``static_url_path``. Relative to the application ``root_path`` - or an absolute path. Defaults to ``'static'``. - :param static_host: the host to use when adding the static route. - Defaults to None. Required when using ``host_matching=True`` - with a ``static_folder`` configured. - :param host_matching: set ``url_map.host_matching`` attribute. - Defaults to False. - :param subdomain_matching: consider the subdomain relative to - :data:`SERVER_NAME` when matching routes. Defaults to False. - :param template_folder: the folder that contains the templates that should - be used by the application. Defaults to - ``'templates'`` folder in the root path of the - application. - :param instance_path: An alternative instance path for the application. - By default the folder ``'instance'`` next to the - package or module is assumed to be the instance - path. - :param instance_relative_config: if set to ``True`` relative filenames - for loading the config are assumed to - be relative to the instance path instead - of the application root. - :param root_path: The path to the root of the application files. - This should only be set manually when it can't be detected - automatically, such as for namespace packages. - """ - - #: The class that is used for request objects. See :class:`~flask.Request` - #: for more information. - request_class = Request - - #: The class that is used for response objects. See - #: :class:`~flask.Response` for more information. - response_class = Response - - #: The class that is used for the Jinja environment. - #: - #: .. versionadded:: 0.11 - jinja_environment = Environment - - #: The class that is used for the :data:`~flask.g` instance. - #: - #: Example use cases for a custom class: - #: - #: 1. Store arbitrary attributes on flask.g. - #: 2. Add a property for lazy per-request database connectors. - #: 3. Return None instead of AttributeError on unexpected attributes. - #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. - #: - #: In Flask 0.9 this property was called `request_globals_class` but it - #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the - #: flask.g object is now application context scoped. - #: - #: .. versionadded:: 0.10 - app_ctx_globals_class = _AppCtxGlobals - - #: The class that is used for the ``config`` attribute of this app. - #: Defaults to :class:`~flask.Config`. - #: - #: Example use cases for a custom class: - #: - #: 1. Default values for certain config options. - #: 2. Access to config values through attributes in addition to keys. - #: - #: .. versionadded:: 0.11 - config_class = Config - - #: The testing flag. Set this to ``True`` to enable the test mode of - #: Flask extensions (and in the future probably also Flask itself). - #: For example this might activate test helpers that have an - #: additional runtime cost which should not be enabled by default. - #: - #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the - #: default it's implicitly enabled. - #: - #: This attribute can also be configured from the config with the - #: ``TESTING`` configuration key. Defaults to ``False``. - testing = ConfigAttribute("TESTING") - - #: If a secret key is set, cryptographic components can use this to - #: sign cookies and other things. Set this to a complex random value - #: when you want to use the secure cookie for instance. - #: - #: This attribute can also be configured from the config with the - #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. - secret_key = ConfigAttribute("SECRET_KEY") - - #: The secure cookie uses this for the name of the session cookie. - #: - #: This attribute can also be configured from the config with the - #: ``SESSION_COOKIE_NAME`` configuration key. Defaults to ``'session'`` - session_cookie_name = ConfigAttribute("SESSION_COOKIE_NAME") - - #: A :class:`~datetime.timedelta` which is used to set the expiration - #: date of a permanent session. The default is 31 days which makes a - #: permanent session survive for roughly one month. - #: - #: This attribute can also be configured from the config with the - #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to - #: ``timedelta(days=31)`` - permanent_session_lifetime = ConfigAttribute( - "PERMANENT_SESSION_LIFETIME", get_converter=_make_timedelta - ) - - #: A :class:`~datetime.timedelta` or number of seconds which is used - #: as the default ``max_age`` for :func:`send_file`. The default is - #: ``None``, which tells the browser to use conditional requests - #: instead of a timed cache. - #: - #: Configured with the :data:`SEND_FILE_MAX_AGE_DEFAULT` - #: configuration key. - #: - #: .. versionchanged:: 2.0 - #: Defaults to ``None`` instead of 12 hours. - send_file_max_age_default = ConfigAttribute( - "SEND_FILE_MAX_AGE_DEFAULT", get_converter=_make_timedelta - ) - - #: Enable this if you want to use the X-Sendfile feature. Keep in - #: mind that the server has to support this. This only affects files - #: sent with the :func:`send_file` method. - #: - #: .. versionadded:: 0.2 - #: - #: This attribute can also be configured from the config with the - #: ``USE_X_SENDFILE`` configuration key. Defaults to ``False``. - use_x_sendfile = ConfigAttribute("USE_X_SENDFILE") - - #: The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`. - #: - #: .. versionadded:: 0.10 - json_encoder = json.JSONEncoder - - #: The JSON decoder class to use. Defaults to :class:`~flask.json.JSONDecoder`. - #: - #: .. versionadded:: 0.10 - json_decoder = json.JSONDecoder - - #: Options that are passed to the Jinja environment in - #: :meth:`create_jinja_environment`. Changing these options after - #: the environment is created (accessing :attr:`jinja_env`) will - #: have no effect. - #: - #: .. versionchanged:: 1.1.0 - #: This is a ``dict`` instead of an ``ImmutableDict`` to allow - #: easier configuration. - #: - jinja_options: dict = {} - - #: Default configuration parameters. - default_config = ImmutableDict( - { - "ENV": None, - "DEBUG": None, - "TESTING": False, - "PROPAGATE_EXCEPTIONS": None, - "PRESERVE_CONTEXT_ON_EXCEPTION": None, - "SECRET_KEY": None, - "PERMANENT_SESSION_LIFETIME": timedelta(days=31), - "USE_X_SENDFILE": False, - "SERVER_NAME": None, - "APPLICATION_ROOT": "/", - "SESSION_COOKIE_NAME": "session", - "SESSION_COOKIE_DOMAIN": None, - "SESSION_COOKIE_PATH": None, - "SESSION_COOKIE_HTTPONLY": True, - "SESSION_COOKIE_SECURE": False, - "SESSION_COOKIE_SAMESITE": None, - "SESSION_REFRESH_EACH_REQUEST": True, - "MAX_CONTENT_LENGTH": None, - "SEND_FILE_MAX_AGE_DEFAULT": None, - "TRAP_BAD_REQUEST_ERRORS": None, - "TRAP_HTTP_EXCEPTIONS": False, - "EXPLAIN_TEMPLATE_LOADING": False, - "PREFERRED_URL_SCHEME": "http", - "JSON_AS_ASCII": True, - "JSON_SORT_KEYS": True, - "JSONIFY_PRETTYPRINT_REGULAR": False, - "JSONIFY_MIMETYPE": "application/json", - "TEMPLATES_AUTO_RELOAD": None, - "MAX_COOKIE_SIZE": 4093, - } - ) - - #: The rule object to use for URL rules created. This is used by - #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. - #: - #: .. versionadded:: 0.7 - url_rule_class = Rule - - #: The map object to use for storing the URL rules and routing - #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. - #: - #: .. versionadded:: 1.1.0 - url_map_class = Map - - #: The :meth:`test_client` method creates an instance of this test - #: client class. Defaults to :class:`~flask.testing.FlaskClient`. - #: - #: .. versionadded:: 0.7 - test_client_class: t.Optional[t.Type["FlaskClient"]] = None - - #: The :class:`~click.testing.CliRunner` subclass, by default - #: :class:`~flask.testing.FlaskCliRunner` that is used by - #: :meth:`test_cli_runner`. Its ``__init__`` method should take a - #: Flask app object as the first argument. - #: - #: .. versionadded:: 1.0 - test_cli_runner_class: t.Optional[t.Type["FlaskCliRunner"]] = None - - #: the session interface to use. By default an instance of - #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. - #: - #: .. versionadded:: 0.8 - session_interface = SecureCookieSessionInterface() - - def __init__( - self, - import_name: str, - static_url_path: t.Optional[str] = None, - static_folder: t.Optional[t.Union[str, os.PathLike]] = "static", - static_host: t.Optional[str] = None, - host_matching: bool = False, - subdomain_matching: bool = False, - template_folder: t.Optional[str] = "templates", - instance_path: t.Optional[str] = None, - instance_relative_config: bool = False, - root_path: t.Optional[str] = None, - ): - super().__init__( - import_name=import_name, - static_folder=static_folder, - static_url_path=static_url_path, - template_folder=template_folder, - root_path=root_path, - ) - - if instance_path is None: - instance_path = self.auto_find_instance_path() - elif not os.path.isabs(instance_path): - raise ValueError( - "If an instance path is provided it must be absolute." - " A relative path was given instead." - ) - - #: Holds the path to the instance folder. - #: - #: .. versionadded:: 0.8 - self.instance_path = instance_path - - #: The configuration dictionary as :class:`Config`. This behaves - #: exactly like a regular dictionary but supports additional methods - #: to load a config from files. - self.config = self.make_config(instance_relative_config) - - #: A list of functions that are called when :meth:`url_for` raises a - #: :exc:`~werkzeug.routing.BuildError`. Each function registered here - #: is called with `error`, `endpoint` and `values`. If a function - #: returns ``None`` or raises a :exc:`BuildError` the next function is - #: tried. - #: - #: .. versionadded:: 0.9 - self.url_build_error_handlers: t.List[ - t.Callable[[Exception, str, dict], str] - ] = [] - - #: A list of functions that will be called at the beginning of the - #: first request to this instance. To register a function, use the - #: :meth:`before_first_request` decorator. - #: - #: .. versionadded:: 0.8 - self.before_first_request_funcs: t.List[BeforeFirstRequestCallable] = [] - - #: A list of functions that are called when the application context - #: is destroyed. Since the application context is also torn down - #: if the request ends this is the place to store code that disconnects - #: from databases. - #: - #: .. versionadded:: 0.9 - self.teardown_appcontext_funcs: t.List[TeardownCallable] = [] - - #: A list of shell context processor functions that should be run - #: when a shell context is created. - #: - #: .. versionadded:: 0.11 - self.shell_context_processors: t.List[t.Callable[[], t.Dict[str, t.Any]]] = [] - - #: Maps registered blueprint names to blueprint objects. The - #: dict retains the order the blueprints were registered in. - #: Blueprints can be registered multiple times, this dict does - #: not track how often they were attached. - #: - #: .. versionadded:: 0.7 - self.blueprints: t.Dict[str, "Blueprint"] = {} - - #: a place where extensions can store application specific state. For - #: example this is where an extension could store database engines and - #: similar things. - #: - #: The key must match the name of the extension module. For example in - #: case of a "Flask-Foo" extension in `flask_foo`, the key would be - #: ``'foo'``. - #: - #: .. versionadded:: 0.7 - self.extensions: dict = {} - - #: The :class:`~werkzeug.routing.Map` for this instance. You can use - #: this to change the routing converters after the class was created - #: but before any routes are connected. Example:: - #: - #: from werkzeug.routing import BaseConverter - #: - #: class ListConverter(BaseConverter): - #: def to_python(self, value): - #: return value.split(',') - #: def to_url(self, values): - #: return ','.join(super(ListConverter, self).to_url(value) - #: for value in values) - #: - #: app = Flask(__name__) - #: app.url_map.converters['list'] = ListConverter - self.url_map = self.url_map_class() - - self.url_map.host_matching = host_matching - self.subdomain_matching = subdomain_matching - - # tracks internally if the application already handled at least one - # request. - self._got_first_request = False - self._before_request_lock = Lock() - - # Add a static route using the provided static_url_path, static_host, - # and static_folder if there is a configured static_folder. - # Note we do this without checking if static_folder exists. - # For one, it might be created while the server is running (e.g. during - # development). Also, Google App Engine stores static files somewhere - if self.has_static_folder: - assert ( - bool(static_host) == host_matching - ), "Invalid static_host/host_matching combination" - # Use a weakref to avoid creating a reference cycle between the app - # and the view function (see #3761). - self_ref = weakref.ref(self) - self.add_url_rule( - f"{self.static_url_path}/", - endpoint="static", - host=static_host, - view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 - ) - - # Set the name of the Click group in case someone wants to add - # the app's commands to another CLI tool. - self.cli.name = self.name - - def _is_setup_finished(self) -> bool: - return self.debug and self._got_first_request - - @locked_cached_property - def name(self) -> str: # type: ignore - """The name of the application. This is usually the import name - with the difference that it's guessed from the run file if the - import name is main. This name is used as a display name when - Flask needs the name of the application. It can be set and overridden - to change the value. - - .. versionadded:: 0.8 - """ - if self.import_name == "__main__": - fn = getattr(sys.modules["__main__"], "__file__", None) - if fn is None: - return "__main__" - return os.path.splitext(os.path.basename(fn))[0] - return self.import_name - - @property - def propagate_exceptions(self) -> bool: - """Returns the value of the ``PROPAGATE_EXCEPTIONS`` configuration - value in case it's set, otherwise a sensible default is returned. - - .. versionadded:: 0.7 - """ - rv = self.config["PROPAGATE_EXCEPTIONS"] - if rv is not None: - return rv - return self.testing or self.debug - - @property - def preserve_context_on_exception(self) -> bool: - """Returns the value of the ``PRESERVE_CONTEXT_ON_EXCEPTION`` - configuration value in case it's set, otherwise a sensible default - is returned. - - .. versionadded:: 0.7 - """ - rv = self.config["PRESERVE_CONTEXT_ON_EXCEPTION"] - if rv is not None: - return rv - return self.debug - - @locked_cached_property - def logger(self) -> logging.Logger: - """A standard Python :class:`~logging.Logger` for the app, with - the same name as :attr:`name`. - - In debug mode, the logger's :attr:`~logging.Logger.level` will - be set to :data:`~logging.DEBUG`. - - If there are no handlers configured, a default handler will be - added. See :doc:`/logging` for more information. - - .. versionchanged:: 1.1.0 - The logger takes the same name as :attr:`name` rather than - hard-coding ``"flask.app"``. - - .. versionchanged:: 1.0.0 - Behavior was simplified. The logger is always named - ``"flask.app"``. The level is only set during configuration, - it doesn't check ``app.debug`` each time. Only one format is - used, not different ones depending on ``app.debug``. No - handlers are removed, and a handler is only added if no - handlers are already configured. - - .. versionadded:: 0.3 - """ - return create_logger(self) - - @locked_cached_property - def jinja_env(self) -> Environment: - """The Jinja environment used to load templates. - - The environment is created the first time this property is - accessed. Changing :attr:`jinja_options` after that will have no - effect. - """ - return self.create_jinja_environment() - - @property - def got_first_request(self) -> bool: - """This attribute is set to ``True`` if the application started - handling the first request. - - .. versionadded:: 0.8 - """ - return self._got_first_request - - def make_config(self, instance_relative: bool = False) -> Config: - """Used to create the config attribute by the Flask constructor. - The `instance_relative` parameter is passed in from the constructor - of Flask (there named `instance_relative_config`) and indicates if - the config should be relative to the instance path or the root path - of the application. - - .. versionadded:: 0.8 - """ - root_path = self.root_path - if instance_relative: - root_path = self.instance_path - defaults = dict(self.default_config) - defaults["ENV"] = get_env() - defaults["DEBUG"] = get_debug_flag() - return self.config_class(root_path, defaults) - - def auto_find_instance_path(self) -> str: - """Tries to locate the instance path if it was not provided to the - constructor of the application class. It will basically calculate - the path to a folder named ``instance`` next to your main file or - the package. - - .. versionadded:: 0.8 - """ - prefix, package_path = find_package(self.import_name) - if prefix is None: - return os.path.join(package_path, "instance") - return os.path.join(prefix, "var", f"{self.name}-instance") - - def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: - """Opens a resource from the application's instance folder - (:attr:`instance_path`). Otherwise works like - :meth:`open_resource`. Instance resources can also be opened for - writing. - - :param resource: the name of the resource. To access resources within - subfolders use forward slashes as separator. - :param mode: resource file opening mode, default is 'rb'. - """ - return open(os.path.join(self.instance_path, resource), mode) - - @property - def templates_auto_reload(self) -> bool: - """Reload templates when they are changed. Used by - :meth:`create_jinja_environment`. - - This attribute can be configured with :data:`TEMPLATES_AUTO_RELOAD`. If - not set, it will be enabled in debug mode. - - .. versionadded:: 1.0 - This property was added but the underlying config and behavior - already existed. - """ - rv = self.config["TEMPLATES_AUTO_RELOAD"] - return rv if rv is not None else self.debug - - @templates_auto_reload.setter - def templates_auto_reload(self, value: bool) -> None: - self.config["TEMPLATES_AUTO_RELOAD"] = value - - def create_jinja_environment(self) -> Environment: - """Create the Jinja environment based on :attr:`jinja_options` - and the various Jinja-related methods of the app. Changing - :attr:`jinja_options` after this will have no effect. Also adds - Flask-related globals and filters to the environment. - - .. versionchanged:: 0.11 - ``Environment.auto_reload`` set in accordance with - ``TEMPLATES_AUTO_RELOAD`` configuration option. - - .. versionadded:: 0.5 - """ - options = dict(self.jinja_options) - - if "autoescape" not in options: - options["autoescape"] = self.select_jinja_autoescape - - if "auto_reload" not in options: - options["auto_reload"] = self.templates_auto_reload - - rv = self.jinja_environment(self, **options) - rv.globals.update( - url_for=url_for, - get_flashed_messages=get_flashed_messages, - config=self.config, - # request, session and g are normally added with the - # context processor for efficiency reasons but for imported - # templates we also want the proxies in there. - request=request, - session=session, - g=g, - ) - rv.policies["json.dumps_function"] = json.dumps - return rv - - def create_global_jinja_loader(self) -> DispatchingJinjaLoader: - """Creates the loader for the Jinja2 environment. Can be used to - override just the loader and keeping the rest unchanged. It's - discouraged to override this function. Instead one should override - the :meth:`jinja_loader` function instead. - - The global loader dispatches between the loaders of the application - and the individual blueprints. - - .. versionadded:: 0.7 - """ - return DispatchingJinjaLoader(self) - - def select_jinja_autoescape(self, filename: str) -> bool: - """Returns ``True`` if autoescaping should be active for the given - template name. If no template name is given, returns `True`. - - .. versionadded:: 0.5 - """ - if filename is None: - return True - return filename.endswith((".html", ".htm", ".xml", ".xhtml")) - - def update_template_context(self, context: dict) -> None: - """Update the template context with some commonly used variables. - This injects request, session, config and g into the template - context as well as everything template context processors want - to inject. Note that the as of Flask 0.6, the original values - in the context will not be overridden if a context processor - decides to return a value with the same key. - - :param context: the context as a dictionary that is updated in place - to add extra variables. - """ - names: t.Iterable[t.Optional[str]] = (None,) - - # A template may be rendered outside a request context. - if request: - names = chain(names, reversed(request.blueprints)) - - # The values passed to render_template take precedence. Keep a - # copy to re-apply after all context functions. - orig_ctx = context.copy() - - for name in names: - if name in self.template_context_processors: - for func in self.template_context_processors[name]: - context.update(func()) - - context.update(orig_ctx) - - def make_shell_context(self) -> dict: - """Returns the shell context for an interactive shell for this - application. This runs all the registered shell context - processors. - - .. versionadded:: 0.11 - """ - rv = {"app": self, "g": g} - for processor in self.shell_context_processors: - rv.update(processor()) - return rv - - #: What environment the app is running in. Flask and extensions may - #: enable behaviors based on the environment, such as enabling debug - #: mode. This maps to the :data:`ENV` config key. This is set by the - #: :envvar:`FLASK_ENV` environment variable and may not behave as - #: expected if set in code. - #: - #: **Do not enable development when deploying in production.** - #: - #: Default: ``'production'`` - env = ConfigAttribute("ENV") - - @property - def debug(self) -> bool: - """Whether debug mode is enabled. When using ``flask run`` to start - the development server, an interactive debugger will be shown for - unhandled exceptions, and the server will be reloaded when code - changes. This maps to the :data:`DEBUG` config key. This is - enabled when :attr:`env` is ``'development'`` and is overridden - by the ``FLASK_DEBUG`` environment variable. It may not behave as - expected if set in code. - - **Do not enable debug mode when deploying in production.** - - Default: ``True`` if :attr:`env` is ``'development'``, or - ``False`` otherwise. - """ - return self.config["DEBUG"] - - @debug.setter - def debug(self, value: bool) -> None: - self.config["DEBUG"] = value - self.jinja_env.auto_reload = self.templates_auto_reload - - def run( - self, - host: t.Optional[str] = None, - port: t.Optional[int] = None, - debug: t.Optional[bool] = None, - load_dotenv: bool = True, - **options: t.Any, - ) -> None: - """Runs the application on a local development server. - - Do not use ``run()`` in a production setting. It is not intended to - meet security and performance requirements for a production server. - Instead, see :doc:`/deploying/index` for WSGI server recommendations. - - If the :attr:`debug` flag is set the server will automatically reload - for code changes and show a debugger in case an exception happened. - - If you want to run the application in debug mode, but disable the - code execution on the interactive debugger, you can pass - ``use_evalex=False`` as parameter. This will keep the debugger's - traceback screen active, but disable code execution. - - It is not recommended to use this function for development with - automatic reloading as this is badly supported. Instead you should - be using the :command:`flask` command line script's ``run`` support. - - .. admonition:: Keep in Mind - - Flask will suppress any server error with a generic error page - unless it is in debug mode. As such to enable just the - interactive debugger without the code reloading, you have to - invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. - Setting ``use_debugger`` to ``True`` without being in debug mode - won't catch any exceptions because there won't be any to - catch. - - :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to - have the server available externally as well. Defaults to - ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable - if present. - :param port: the port of the webserver. Defaults to ``5000`` or the - port defined in the ``SERVER_NAME`` config variable if present. - :param debug: if given, enable or disable debug mode. See - :attr:`debug`. - :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` - files to set environment variables. Will also change the working - directory to the directory containing the first file found. - :param options: the options to be forwarded to the underlying Werkzeug - server. See :func:`werkzeug.serving.run_simple` for more - information. - - .. versionchanged:: 1.0 - If installed, python-dotenv will be used to load environment - variables from :file:`.env` and :file:`.flaskenv` files. - - If set, the :envvar:`FLASK_ENV` and :envvar:`FLASK_DEBUG` - environment variables will override :attr:`env` and - :attr:`debug`. - - Threaded mode is enabled by default. - - .. versionchanged:: 0.10 - The default port is now picked from the ``SERVER_NAME`` - variable. - """ - # Change this into a no-op if the server is invoked from the - # command line. Have a look at cli.py for more information. - if os.environ.get("FLASK_RUN_FROM_CLI") == "true": - from .debughelpers import explain_ignored_app_run - - explain_ignored_app_run() - return - - if get_load_dotenv(load_dotenv): - cli.load_dotenv() - - # if set, let env vars override previous values - if "FLASK_ENV" in os.environ: - self.env = get_env() - self.debug = get_debug_flag() - elif "FLASK_DEBUG" in os.environ: - self.debug = get_debug_flag() - - # debug passed to method overrides all other sources - if debug is not None: - self.debug = bool(debug) - - server_name = self.config.get("SERVER_NAME") - sn_host = sn_port = None - - if server_name: - sn_host, _, sn_port = server_name.partition(":") - - if not host: - if sn_host: - host = sn_host - else: - host = "127.0.0.1" - - if port or port == 0: - port = int(port) - elif sn_port: - port = int(sn_port) - else: - port = 5000 - - options.setdefault("use_reloader", self.debug) - options.setdefault("use_debugger", self.debug) - options.setdefault("threaded", True) - - cli.show_server_banner(self.env, self.debug, self.name, False) - - from werkzeug.serving import run_simple - - try: - run_simple(t.cast(str, host), port, self, **options) - finally: - # reset the first request information if the development server - # reset normally. This makes it possible to restart the server - # without reloader and that stuff from an interactive shell. - self._got_first_request = False - - def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> "FlaskClient": - """Creates a test client for this application. For information - about unit testing head over to :doc:`/testing`. - - Note that if you are testing for assertions or exceptions in your - application code, you must set ``app.testing = True`` in order for the - exceptions to propagate to the test client. Otherwise, the exception - will be handled by the application (not visible to the test client) and - the only indication of an AssertionError or other exception will be a - 500 status code response to the test client. See the :attr:`testing` - attribute. For example:: - - app.testing = True - client = app.test_client() - - The test client can be used in a ``with`` block to defer the closing down - of the context until the end of the ``with`` block. This is useful if - you want to access the context locals for testing:: - - with app.test_client() as c: - rv = c.get('/?vodka=42') - assert request.args['vodka'] == '42' - - Additionally, you may pass optional keyword arguments that will then - be passed to the application's :attr:`test_client_class` constructor. - For example:: - - from flask.testing import FlaskClient - - class CustomClient(FlaskClient): - def __init__(self, *args, **kwargs): - self._authentication = kwargs.pop("authentication") - super(CustomClient,self).__init__( *args, **kwargs) - - app.test_client_class = CustomClient - client = app.test_client(authentication='Basic ....') - - See :class:`~flask.testing.FlaskClient` for more information. - - .. versionchanged:: 0.4 - added support for ``with`` block usage for the client. - - .. versionadded:: 0.7 - The `use_cookies` parameter was added as well as the ability - to override the client to be used by setting the - :attr:`test_client_class` attribute. - - .. versionchanged:: 0.11 - Added `**kwargs` to support passing additional keyword arguments to - the constructor of :attr:`test_client_class`. - """ - cls = self.test_client_class - if cls is None: - from .testing import FlaskClient as cls # type: ignore - return cls( # type: ignore - self, self.response_class, use_cookies=use_cookies, **kwargs - ) - - def test_cli_runner(self, **kwargs: t.Any) -> "FlaskCliRunner": - """Create a CLI runner for testing CLI commands. - See :ref:`testing-cli`. - - Returns an instance of :attr:`test_cli_runner_class`, by default - :class:`~flask.testing.FlaskCliRunner`. The Flask app object is - passed as the first argument. - - .. versionadded:: 1.0 - """ - cls = self.test_cli_runner_class - - if cls is None: - from .testing import FlaskCliRunner as cls # type: ignore - - return cls(self, **kwargs) # type: ignore - - @setupmethod - def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: - """Register a :class:`~flask.Blueprint` on the application. Keyword - arguments passed to this method will override the defaults set on the - blueprint. - - Calls the blueprint's :meth:`~flask.Blueprint.register` method after - recording the blueprint in the application's :attr:`blueprints`. - - :param blueprint: The blueprint to register. - :param url_prefix: Blueprint routes will be prefixed with this. - :param subdomain: Blueprint routes will match on this subdomain. - :param url_defaults: Blueprint routes will use these default values for - view arguments. - :param options: Additional keyword arguments are passed to - :class:`~flask.blueprints.BlueprintSetupState`. They can be - accessed in :meth:`~flask.Blueprint.record` callbacks. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionadded:: 0.7 - """ - blueprint.register(self, options) - - def iter_blueprints(self) -> t.ValuesView["Blueprint"]: - """Iterates over all blueprints by the order they were registered. - - .. versionadded:: 0.11 - """ - return self.blueprints.values() - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, - provide_automatic_options: t.Optional[bool] = None, - **options: t.Any, - ) -> None: - if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) # type: ignore - options["endpoint"] = endpoint - methods = options.pop("methods", None) - - # if the methods are not given and the view_func object knows its - # methods we can use that instead. If neither exists, we go with - # a tuple of only ``GET`` as default. - if methods is None: - methods = getattr(view_func, "methods", None) or ("GET",) - if isinstance(methods, str): - raise TypeError( - "Allowed methods must be a list of strings, for" - ' example: @app.route(..., methods=["POST"])' - ) - methods = {item.upper() for item in methods} - - # Methods that should always be added - required_methods = set(getattr(view_func, "required_methods", ())) - - # starting with Flask 0.8 the view_func object can disable and - # force-enable the automatic options handling. - if provide_automatic_options is None: - provide_automatic_options = getattr( - view_func, "provide_automatic_options", None - ) - - if provide_automatic_options is None: - if "OPTIONS" not in methods: - provide_automatic_options = True - required_methods.add("OPTIONS") - else: - provide_automatic_options = False - - # Add the required methods now. - methods |= required_methods - - rule = self.url_rule_class(rule, methods=methods, **options) - rule.provide_automatic_options = provide_automatic_options # type: ignore - - self.url_map.add(rule) - if view_func is not None: - old_func = self.view_functions.get(endpoint) - if old_func is not None and old_func != view_func: - raise AssertionError( - "View function mapping is overwriting an existing" - f" endpoint function: {endpoint}" - ) - self.view_functions[endpoint] = view_func - - @setupmethod - def template_filter( - self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: - """A decorator that is used to register custom template filter. - You can specify a name for the filter, otherwise the function - name will be used. Example:: - - @app.template_filter() - def reverse(s): - return s[::-1] - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - - def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: - self.add_template_filter(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_filter( - self, f: TemplateFilterCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template filter. Works exactly like the - :meth:`template_filter` decorator. - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - self.jinja_env.filters[name or f.__name__] = f - - @setupmethod - def template_test( - self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: - """A decorator that is used to register custom template test. - You can specify a name for the test, otherwise the function - name will be used. Example:: - - @app.template_test() - def is_prime(n): - if n == 2: - return True - for i in range(2, int(math.ceil(math.sqrt(n))) + 1): - if n % i == 0: - return False - return True - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - - def decorator(f: TemplateTestCallable) -> TemplateTestCallable: - self.add_template_test(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_test( - self, f: TemplateTestCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template test. Works exactly like the - :meth:`template_test` decorator. - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - self.jinja_env.tests[name or f.__name__] = f - - @setupmethod - def template_global( - self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: - """A decorator that is used to register a custom template global function. - You can specify a name for the global function, otherwise the function - name will be used. Example:: - - @app.template_global() - def double(n): - return 2 * n - - .. versionadded:: 0.10 - - :param name: the optional name of the global function, otherwise the - function name will be used. - """ - - def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: - self.add_template_global(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_global( - self, f: TemplateGlobalCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template global function. Works exactly like the - :meth:`template_global` decorator. - - .. versionadded:: 0.10 - - :param name: the optional name of the global function, otherwise the - function name will be used. - """ - self.jinja_env.globals[name or f.__name__] = f - - @setupmethod - def before_first_request( - self, f: BeforeFirstRequestCallable - ) -> BeforeFirstRequestCallable: - """Registers a function to be run before the first request to this - instance of the application. - - The function will be called without any arguments and its return - value is ignored. - - .. versionadded:: 0.8 - """ - self.before_first_request_funcs.append(f) - return f - - @setupmethod - def teardown_appcontext(self, f: TeardownCallable) -> TeardownCallable: - """Registers a function to be called when the application context - ends. These functions are typically also called when the request - context is popped. - - Example:: - - ctx = app.app_context() - ctx.push() - ... - ctx.pop() - - When ``ctx.pop()`` is executed in the above example, the teardown - functions are called just before the app context moves from the - stack of active contexts. This becomes relevant if you are using - such constructs in tests. - - Since a request context typically also manages an application - context it would also be called when you pop a request context. - - When a teardown function was called because of an unhandled exception - it will be passed an error object. If an :meth:`errorhandler` is - registered, it will handle the exception and the teardown will not - receive it. - - The return values of teardown functions are ignored. - - .. versionadded:: 0.9 - """ - self.teardown_appcontext_funcs.append(f) - return f - - @setupmethod - def shell_context_processor(self, f: t.Callable) -> t.Callable: - """Registers a shell context processor function. - - .. versionadded:: 0.11 - """ - self.shell_context_processors.append(f) - return f - - def _find_error_handler( - self, e: Exception - ) -> t.Optional["ErrorHandlerCallable[Exception]"]: - """Return a registered error handler for an exception in this order: - blueprint handler for a specific code, app handler for a specific code, - blueprint handler for an exception class, app handler for an exception - class, or ``None`` if a suitable handler is not found. - """ - exc_class, code = self._get_exc_class_and_code(type(e)) - names = (*request.blueprints, None) - - for c in (code, None) if code is not None else (None,): - for name in names: - handler_map = self.error_handler_spec[name][c] - - if not handler_map: - continue - - for cls in exc_class.__mro__: - handler = handler_map.get(cls) - - if handler is not None: - return handler - return None - - def handle_http_exception( - self, e: HTTPException - ) -> t.Union[HTTPException, ResponseReturnValue]: - """Handles an HTTP exception. By default this will invoke the - registered error handlers and fall back to returning the - exception as response. - - .. versionchanged:: 1.0.3 - ``RoutingException``, used internally for actions such as - slash redirects during routing, is not passed to error - handlers. - - .. versionchanged:: 1.0 - Exceptions are looked up by code *and* by MRO, so - ``HTTPException`` subclasses can be handled with a catch-all - handler for the base ``HTTPException``. - - .. versionadded:: 0.3 - """ - # Proxy exceptions don't have error codes. We want to always return - # those unchanged as errors - if e.code is None: - return e - - # RoutingExceptions are used internally to trigger routing - # actions, such as slash redirects raising RequestRedirect. They - # are not raised or handled in user code. - if isinstance(e, RoutingException): - return e - - handler = self._find_error_handler(e) - if handler is None: - return e - return self.ensure_sync(handler)(e) - - def trap_http_exception(self, e: Exception) -> bool: - """Checks if an HTTP exception should be trapped or not. By default - this will return ``False`` for all exceptions except for a bad request - key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It - also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. - - This is called for all HTTP exceptions raised by a view function. - If it returns ``True`` for any exception the error handler for this - exception is not called and it shows up as regular exception in the - traceback. This is helpful for debugging implicitly raised HTTP - exceptions. - - .. versionchanged:: 1.0 - Bad request errors are not trapped by default in debug mode. - - .. versionadded:: 0.8 - """ - if self.config["TRAP_HTTP_EXCEPTIONS"]: - return True - - trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] - - # if unset, trap key errors in debug mode - if ( - trap_bad_request is None - and self.debug - and isinstance(e, BadRequestKeyError) - ): - return True - - if trap_bad_request: - return isinstance(e, BadRequest) - - return False - - def handle_user_exception( - self, e: Exception - ) -> t.Union[HTTPException, ResponseReturnValue]: - """This method is called whenever an exception occurs that - should be handled. A special case is :class:`~werkzeug - .exceptions.HTTPException` which is forwarded to the - :meth:`handle_http_exception` method. This function will either - return a response value or reraise the exception with the same - traceback. - - .. versionchanged:: 1.0 - Key errors raised from request data like ``form`` show the - bad key in debug mode rather than a generic bad request - message. - - .. versionadded:: 0.7 - """ - if isinstance(e, BadRequestKeyError) and ( - self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] - ): - e.show_exception = True - - if isinstance(e, HTTPException) and not self.trap_http_exception(e): - return self.handle_http_exception(e) - - handler = self._find_error_handler(e) - - if handler is None: - raise - - return self.ensure_sync(handler)(e) - - def handle_exception(self, e: Exception) -> Response: - """Handle an exception that did not have an error handler - associated with it, or that was raised from an error handler. - This always causes a 500 ``InternalServerError``. - - Always sends the :data:`got_request_exception` signal. - - If :attr:`propagate_exceptions` is ``True``, such as in debug - mode, the error will be re-raised so that the debugger can - display it. Otherwise, the original exception is logged, and - an :exc:`~werkzeug.exceptions.InternalServerError` is returned. - - If an error handler is registered for ``InternalServerError`` or - ``500``, it will be used. For consistency, the handler will - always receive the ``InternalServerError``. The original - unhandled exception is available as ``e.original_exception``. - - .. versionchanged:: 1.1.0 - Always passes the ``InternalServerError`` instance to the - handler, setting ``original_exception`` to the unhandled - error. - - .. versionchanged:: 1.1.0 - ``after_request`` functions and other finalization is done - even for the default 500 response when there is no handler. - - .. versionadded:: 0.3 - """ - exc_info = sys.exc_info() - got_request_exception.send(self, exception=e) - - if self.propagate_exceptions: - # Re-raise if called with an active exception, otherwise - # raise the passed in exception. - if exc_info[1] is e: - raise - - raise e - - self.log_exception(exc_info) - server_error: t.Union[InternalServerError, ResponseReturnValue] - server_error = InternalServerError(original_exception=e) - handler = self._find_error_handler(server_error) - - if handler is not None: - server_error = self.ensure_sync(handler)(server_error) - - return self.finalize_request(server_error, from_error_handler=True) - - def log_exception( - self, - exc_info: t.Union[ - t.Tuple[type, BaseException, TracebackType], t.Tuple[None, None, None] - ], - ) -> None: - """Logs an exception. This is called by :meth:`handle_exception` - if debugging is disabled and right before the handler is called. - The default implementation logs the exception as error on the - :attr:`logger`. - - .. versionadded:: 0.8 - """ - self.logger.error( - f"Exception on {request.path} [{request.method}]", exc_info=exc_info - ) - - def raise_routing_exception(self, request: Request) -> "te.NoReturn": - """Exceptions that are recording during routing are reraised with - this method. During debug we are not reraising redirect requests - for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising - a different error instead to help debug situations. - - :internal: - """ - if ( - not self.debug - or not isinstance(request.routing_exception, RequestRedirect) - or request.method in ("GET", "HEAD", "OPTIONS") - ): - raise request.routing_exception # type: ignore - - from .debughelpers import FormDataRoutingRedirect - - raise FormDataRoutingRedirect(request) - - def dispatch_request(self) -> ResponseReturnValue: - """Does the request dispatching. Matches the URL and returns the - return value of the view or error handler. This does not have to - be a response object. In order to convert the return value to a - proper response object, call :func:`make_response`. - - .. versionchanged:: 0.7 - This no longer does the exception handling, this code was - moved to the new :meth:`full_dispatch_request`. - """ - req = _request_ctx_stack.top.request - if req.routing_exception is not None: - self.raise_routing_exception(req) - rule = req.url_rule - # if we provide automatic options for this URL and the - # request came with the OPTIONS method, reply automatically - if ( - getattr(rule, "provide_automatic_options", False) - and req.method == "OPTIONS" - ): - return self.make_default_options_response() - # otherwise dispatch to the handler for that endpoint - return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args) - - def full_dispatch_request(self) -> Response: - """Dispatches the request and on top of that performs request - pre and postprocessing as well as HTTP exception catching and - error handling. - - .. versionadded:: 0.7 - """ - self.try_trigger_before_first_request_functions() - try: - request_started.send(self) - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - except Exception as e: - rv = self.handle_user_exception(e) - return self.finalize_request(rv) - - def finalize_request( - self, - rv: t.Union[ResponseReturnValue, HTTPException], - from_error_handler: bool = False, - ) -> Response: - """Given the return value from a view function this finalizes - the request by converting it into a response and invoking the - postprocessing functions. This is invoked for both normal - request dispatching as well as error handlers. - - Because this means that it might be called as a result of a - failure a special safe mode is available which can be enabled - with the `from_error_handler` flag. If enabled, failures in - response processing will be logged and otherwise ignored. - - :internal: - """ - response = self.make_response(rv) - try: - response = self.process_response(response) - request_finished.send(self, response=response) - except Exception: - if not from_error_handler: - raise - self.logger.exception( - "Request finalizing failed with an error while handling an error" - ) - return response - - def try_trigger_before_first_request_functions(self) -> None: - """Called before each request and will ensure that it triggers - the :attr:`before_first_request_funcs` and only exactly once per - application instance (which means process usually). - - :internal: - """ - if self._got_first_request: - return - with self._before_request_lock: - if self._got_first_request: - return - for func in self.before_first_request_funcs: - self.ensure_sync(func)() - self._got_first_request = True - - def make_default_options_response(self) -> Response: - """This method is called to create the default ``OPTIONS`` response. - This can be changed through subclassing to change the default - behavior of ``OPTIONS`` responses. - - .. versionadded:: 0.7 - """ - adapter = _request_ctx_stack.top.url_adapter - methods = adapter.allowed_methods() - rv = self.response_class() - rv.allow.update(methods) - return rv - - def should_ignore_error(self, error: t.Optional[BaseException]) -> bool: - """This is called to figure out if an error should be ignored - or not as far as the teardown system is concerned. If this - function returns ``True`` then the teardown handlers will not be - passed the error. - - .. versionadded:: 0.10 - """ - return False - - def ensure_sync(self, func: t.Callable) -> t.Callable: - """Ensure that the function is synchronous for WSGI workers. - Plain ``def`` functions are returned as-is. ``async def`` - functions are wrapped to run and wait for the response. - - Override this method to change how the app runs async views. - - .. versionadded:: 2.0 - """ - if iscoroutinefunction(func): - return self.async_to_sync(func) - - return func - - def async_to_sync( - self, func: t.Callable[..., t.Coroutine] - ) -> t.Callable[..., t.Any]: - """Return a sync function that will run the coroutine function. - - .. code-block:: python - - result = app.async_to_sync(func)(*args, **kwargs) - - Override this method to change how the app converts async code - to be synchronously callable. - - .. versionadded:: 2.0 - """ - try: - from asgiref.sync import async_to_sync as asgiref_async_to_sync - except ImportError: - raise RuntimeError( - "Install Flask with the 'async' extra in order to use async views." - ) from None - - # Check that Werkzeug isn't using its fallback ContextVar class. - if ContextVar.__module__ == "werkzeug.local": - raise RuntimeError( - "Async cannot be used with this combination of Python " - "and Greenlet versions." - ) - - return asgiref_async_to_sync(func) - - def make_response(self, rv: ResponseReturnValue) -> Response: - """Convert the return value from a view function to an instance of - :attr:`response_class`. - - :param rv: the return value from the view function. The view function - must return a response. Returning ``None``, or the view ending - without returning, is not allowed. The following types are allowed - for ``view_rv``: - - ``str`` - A response object is created with the string encoded to UTF-8 - as the body. - - ``bytes`` - A response object is created with the bytes as the body. - - ``dict`` - A dictionary that will be jsonify'd before being returned. - - ``tuple`` - Either ``(body, status, headers)``, ``(body, status)``, or - ``(body, headers)``, where ``body`` is any of the other types - allowed here, ``status`` is a string or an integer, and - ``headers`` is a dictionary or a list of ``(key, value)`` - tuples. If ``body`` is a :attr:`response_class` instance, - ``status`` overwrites the exiting value and ``headers`` are - extended. - - :attr:`response_class` - The object is returned unchanged. - - other :class:`~werkzeug.wrappers.Response` class - The object is coerced to :attr:`response_class`. - - :func:`callable` - The function is called as a WSGI application. The result is - used to create a response object. - - .. versionchanged:: 0.9 - Previously a tuple was interpreted as the arguments for the - response object. - """ - - status = headers = None - - # unpack tuple returns - if isinstance(rv, tuple): - len_rv = len(rv) - - # a 3-tuple is unpacked directly - if len_rv == 3: - rv, status, headers = rv - # decide if a 2-tuple has status or headers - elif len_rv == 2: - if isinstance(rv[1], (Headers, dict, tuple, list)): - rv, headers = rv - else: - rv, status = rv - # other sized tuples are not allowed - else: - raise TypeError( - "The view function did not return a valid response tuple." - " The tuple must have the form (body, status, headers)," - " (body, status), or (body, headers)." - ) - - # the body must not be None - if rv is None: - raise TypeError( - f"The view function for {request.endpoint!r} did not" - " return a valid response. The function either returned" - " None or ended without a return statement." - ) - - # make sure the body is an instance of the response class - if not isinstance(rv, self.response_class): - if isinstance(rv, (str, bytes, bytearray)): - # let the response class set the status and headers instead of - # waiting to do it manually, so that the class can handle any - # special logic - rv = self.response_class(rv, status=status, headers=headers) - status = headers = None - elif isinstance(rv, dict): - rv = jsonify(rv) - elif isinstance(rv, BaseResponse) or callable(rv): - # evaluate a WSGI callable, or coerce a different response - # class to the correct type - try: - rv = self.response_class.force_type(rv, request.environ) # type: ignore # noqa: B950 - except TypeError as e: - raise TypeError( - f"{e}\nThe view function did not return a valid" - " response. The return type must be a string," - " dict, tuple, Response instance, or WSGI" - f" callable, but it was a {type(rv).__name__}." - ).with_traceback(sys.exc_info()[2]) from None - else: - raise TypeError( - "The view function did not return a valid" - " response. The return type must be a string," - " dict, tuple, Response instance, or WSGI" - f" callable, but it was a {type(rv).__name__}." - ) - - rv = t.cast(Response, rv) - # prefer the status if it was provided - if status is not None: - if isinstance(status, (str, bytes, bytearray)): - rv.status = status # type: ignore - else: - rv.status_code = status - - # extend existing headers with provided headers - if headers: - rv.headers.update(headers) - - return rv - - def create_url_adapter( - self, request: t.Optional[Request] - ) -> t.Optional[MapAdapter]: - """Creates a URL adapter for the given request. The URL adapter - is created at a point where the request context is not yet set - up so the request is passed explicitly. - - .. versionadded:: 0.6 - - .. versionchanged:: 0.9 - This can now also be called without a request object when the - URL adapter is created for the application context. - - .. versionchanged:: 1.0 - :data:`SERVER_NAME` no longer implicitly enables subdomain - matching. Use :attr:`subdomain_matching` instead. - """ - if request is not None: - # If subdomain matching is disabled (the default), use the - # default subdomain in all cases. This should be the default - # in Werkzeug but it currently does not have that feature. - if not self.subdomain_matching: - subdomain = self.url_map.default_subdomain or None - else: - subdomain = None - - return self.url_map.bind_to_environ( - request.environ, - server_name=self.config["SERVER_NAME"], - subdomain=subdomain, - ) - # We need at the very least the server name to be set for this - # to work. - if self.config["SERVER_NAME"] is not None: - return self.url_map.bind( - self.config["SERVER_NAME"], - script_name=self.config["APPLICATION_ROOT"], - url_scheme=self.config["PREFERRED_URL_SCHEME"], - ) - - return None - - def inject_url_defaults(self, endpoint: str, values: dict) -> None: - """Injects the URL defaults for the given endpoint directly into - the values dictionary passed. This is used internally and - automatically called on URL building. - - .. versionadded:: 0.7 - """ - names: t.Iterable[t.Optional[str]] = (None,) - - # url_for may be called outside a request context, parse the - # passed endpoint instead of using request.blueprints. - if "." in endpoint: - names = chain( - names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) - ) - - for name in names: - if name in self.url_default_functions: - for func in self.url_default_functions[name]: - func(endpoint, values) - - def handle_url_build_error( - self, error: Exception, endpoint: str, values: dict - ) -> str: - """Handle :class:`~werkzeug.routing.BuildError` on - :meth:`url_for`. - """ - for handler in self.url_build_error_handlers: - try: - rv = handler(error, endpoint, values) - except BuildError as e: - # make error available outside except block - error = e - else: - if rv is not None: - return rv - - # Re-raise if called with an active exception, otherwise raise - # the passed in exception. - if error is sys.exc_info()[1]: - raise - - raise error - - def preprocess_request(self) -> t.Optional[ResponseReturnValue]: - """Called before the request is dispatched. Calls - :attr:`url_value_preprocessors` registered with the app and the - current blueprint (if any). Then calls :attr:`before_request_funcs` - registered with the app and the blueprint. - - If any :meth:`before_request` handler returns a non-None value, the - value is handled as if it was the return value from the view, and - further request handling is stopped. - """ - names = (None, *reversed(request.blueprints)) - - for name in names: - if name in self.url_value_preprocessors: - for url_func in self.url_value_preprocessors[name]: - url_func(request.endpoint, request.view_args) - - for name in names: - if name in self.before_request_funcs: - for before_func in self.before_request_funcs[name]: - rv = self.ensure_sync(before_func)() - - if rv is not None: - return rv - - return None - - def process_response(self, response: Response) -> Response: - """Can be overridden in order to modify the response object - before it's sent to the WSGI server. By default this will - call all the :meth:`after_request` decorated functions. - - .. versionchanged:: 0.5 - As of Flask 0.5 the functions registered for after request - execution are called in reverse order of registration. - - :param response: a :attr:`response_class` object. - :return: a new response object or the same, has to be an - instance of :attr:`response_class`. - """ - ctx = _request_ctx_stack.top - - for func in ctx._after_request_functions: - response = self.ensure_sync(func)(response) - - for name in chain(request.blueprints, (None,)): - if name in self.after_request_funcs: - for func in reversed(self.after_request_funcs[name]): - response = self.ensure_sync(func)(response) - - if not self.session_interface.is_null_session(ctx.session): - self.session_interface.save_session(self, ctx.session, response) - - return response - - def do_teardown_request( - self, exc: t.Optional[BaseException] = _sentinel # type: ignore - ) -> None: - """Called after the request is dispatched and the response is - returned, right before the request context is popped. - - This calls all functions decorated with - :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` - if a blueprint handled the request. Finally, the - :data:`request_tearing_down` signal is sent. - - This is called by - :meth:`RequestContext.pop() `, - which may be delayed during testing to maintain access to - resources. - - :param exc: An unhandled exception raised while dispatching the - request. Detected from the current exception information if - not passed. Passed to each teardown function. - - .. versionchanged:: 0.9 - Added the ``exc`` argument. - """ - if exc is _sentinel: - exc = sys.exc_info()[1] - - for name in chain(request.blueprints, (None,)): - if name in self.teardown_request_funcs: - for func in reversed(self.teardown_request_funcs[name]): - self.ensure_sync(func)(exc) - - request_tearing_down.send(self, exc=exc) - - def do_teardown_appcontext( - self, exc: t.Optional[BaseException] = _sentinel # type: ignore - ) -> None: - """Called right before the application context is popped. - - When handling a request, the application context is popped - after the request context. See :meth:`do_teardown_request`. - - This calls all functions decorated with - :meth:`teardown_appcontext`. Then the - :data:`appcontext_tearing_down` signal is sent. - - This is called by - :meth:`AppContext.pop() `. - - .. versionadded:: 0.9 - """ - if exc is _sentinel: - exc = sys.exc_info()[1] - - for func in reversed(self.teardown_appcontext_funcs): - self.ensure_sync(func)(exc) - - appcontext_tearing_down.send(self, exc=exc) - - def app_context(self) -> AppContext: - """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` - block to push the context, which will make :data:`current_app` - point at this application. - - An application context is automatically pushed by - :meth:`RequestContext.push() ` - when handling a request, and when running a CLI command. Use - this to manually create a context outside of these situations. - - :: - - with app.app_context(): - init_db() - - See :doc:`/appcontext`. - - .. versionadded:: 0.9 - """ - return AppContext(self) - - def request_context(self, environ: dict) -> RequestContext: - """Create a :class:`~flask.ctx.RequestContext` representing a - WSGI environment. Use a ``with`` block to push the context, - which will make :data:`request` point at this request. - - See :doc:`/reqcontext`. - - Typically you should not call this from your own code. A request - context is automatically pushed by the :meth:`wsgi_app` when - handling a request. Use :meth:`test_request_context` to create - an environment and context instead of this method. - - :param environ: a WSGI environment - """ - return RequestContext(self, environ) - - def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: - """Create a :class:`~flask.ctx.RequestContext` for a WSGI - environment created from the given values. This is mostly useful - during testing, where you may want to run a function that uses - request data without dispatching a full request. - - See :doc:`/reqcontext`. - - Use a ``with`` block to push the context, which will make - :data:`request` point at the request for the created - environment. :: - - with test_request_context(...): - generate_report() - - When using the shell, it may be easier to push and pop the - context manually to avoid indentation. :: - - ctx = app.test_request_context(...) - ctx.push() - ... - ctx.pop() - - Takes the same arguments as Werkzeug's - :class:`~werkzeug.test.EnvironBuilder`, with some defaults from - the application. See the linked Werkzeug docs for most of the - available arguments. Flask-specific behavior is listed here. - - :param path: URL path being requested. - :param base_url: Base URL where the app is being served, which - ``path`` is relative to. If not given, built from - :data:`PREFERRED_URL_SCHEME`, ``subdomain``, - :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. - :param subdomain: Subdomain name to append to - :data:`SERVER_NAME`. - :param url_scheme: Scheme to use instead of - :data:`PREFERRED_URL_SCHEME`. - :param data: The request body, either as a string or a dict of - form keys and values. - :param json: If given, this is serialized as JSON and passed as - ``data``. Also defaults ``content_type`` to - ``application/json``. - :param args: other positional arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - :param kwargs: other keyword arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - """ - from .testing import EnvironBuilder - - builder = EnvironBuilder(self, *args, **kwargs) - - try: - return self.request_context(builder.get_environ()) - finally: - builder.close() - - def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: - """The actual WSGI application. This is not implemented in - :meth:`__call__` so that middlewares can be applied without - losing a reference to the app object. Instead of doing this:: - - app = MyMiddleware(app) - - It's a better idea to do this instead:: - - app.wsgi_app = MyMiddleware(app.wsgi_app) - - Then you still have the original application object around and - can continue to call methods on it. - - .. versionchanged:: 0.7 - Teardown events for the request and app contexts are called - even if an unhandled error occurs. Other events may not be - called depending on when an error occurs during dispatch. - See :ref:`callbacks-and-errors`. - - :param environ: A WSGI environment. - :param start_response: A callable accepting a status code, - a list of headers, and an optional exception context to - start the response. - """ - ctx = self.request_context(environ) - error: t.Optional[BaseException] = None - try: - try: - ctx.push() - response = self.full_dispatch_request() - except Exception as e: - error = e - response = self.handle_exception(e) - except: # noqa: B001 - error = sys.exc_info()[1] - raise - return response(environ, start_response) - finally: - if self.should_ignore_error(error): - error = None - ctx.auto_pop(error) - - def __call__(self, environ: dict, start_response: t.Callable) -> t.Any: - """The WSGI server calls the Flask application object as the - WSGI application. This calls :meth:`wsgi_app`, which can be - wrapped to apply middleware. - """ - return self.wsgi_app(environ, start_response) diff --git a/laplas/tools/abstract_map/flask/blueprints.py b/laplas/tools/abstract_map/flask/blueprints.py deleted file mode 100644 index 5c23a73..0000000 --- a/laplas/tools/abstract_map/flask/blueprints.py +++ /dev/null @@ -1,609 +0,0 @@ -import os -import typing as t -from collections import defaultdict -from functools import update_wrapper - -from .scaffold import _endpoint_from_view_func -from .scaffold import _sentinel -from .scaffold import Scaffold -from .typing import AfterRequestCallable -from .typing import BeforeFirstRequestCallable -from .typing import BeforeRequestCallable -from .typing import TeardownCallable -from .typing import TemplateContextProcessorCallable -from .typing import TemplateFilterCallable -from .typing import TemplateGlobalCallable -from .typing import TemplateTestCallable -from .typing import URLDefaultCallable -from .typing import URLValuePreprocessorCallable - -if t.TYPE_CHECKING: - from .app import Flask - from .typing import ErrorHandlerCallable - -DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] - - -class BlueprintSetupState: - """Temporary holder object for registering a blueprint with the - application. An instance of this class is created by the - :meth:`~flask.Blueprint.make_setup_state` method and later passed - to all register callback functions. - """ - - def __init__( - self, - blueprint: "Blueprint", - app: "Flask", - options: t.Any, - first_registration: bool, - ) -> None: - #: a reference to the current application - self.app = app - - #: a reference to the blueprint that created this setup state. - self.blueprint = blueprint - - #: a dictionary with all options that were passed to the - #: :meth:`~flask.Flask.register_blueprint` method. - self.options = options - - #: as blueprints can be registered multiple times with the - #: application and not everything wants to be registered - #: multiple times on it, this attribute can be used to figure - #: out if the blueprint was registered in the past already. - self.first_registration = first_registration - - subdomain = self.options.get("subdomain") - if subdomain is None: - subdomain = self.blueprint.subdomain - - #: The subdomain that the blueprint should be active for, ``None`` - #: otherwise. - self.subdomain = subdomain - - url_prefix = self.options.get("url_prefix") - if url_prefix is None: - url_prefix = self.blueprint.url_prefix - #: The prefix that should be used for all URLs defined on the - #: blueprint. - self.url_prefix = url_prefix - - self.name = self.options.get("name", blueprint.name) - self.name_prefix = self.options.get("name_prefix", "") - - #: A dictionary with URL defaults that is added to each and every - #: URL that was defined with the blueprint. - self.url_defaults = dict(self.blueprint.url_values_defaults) - self.url_defaults.update(self.options.get("url_defaults", ())) - - def add_url_rule( - self, - rule: str, - endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, - **options: t.Any, - ) -> None: - """A helper method to register a rule (and optionally a view function) - to the application. The endpoint is automatically prefixed with the - blueprint's name. - """ - if self.url_prefix is not None: - if rule: - rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) - else: - rule = self.url_prefix - options.setdefault("subdomain", self.subdomain) - if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) # type: ignore - defaults = self.url_defaults - if "defaults" in options: - defaults = dict(defaults, **options.pop("defaults")) - - self.app.add_url_rule( - rule, - f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), - view_func, - defaults=defaults, - **options, - ) - - -class Blueprint(Scaffold): - """Represents a blueprint, a collection of routes and other - app-related functions that can be registered on a real application - later. - - A blueprint is an object that allows defining application functions - without requiring an application object ahead of time. It uses the - same decorators as :class:`~flask.Flask`, but defers the need for an - application by recording them for later registration. - - Decorating a function with a blueprint creates a deferred function - that is called with :class:`~flask.blueprints.BlueprintSetupState` - when the blueprint is registered on an application. - - See :doc:`/blueprints` for more information. - - :param name: The name of the blueprint. Will be prepended to each - endpoint name. - :param import_name: The name of the blueprint package, usually - ``__name__``. This helps locate the ``root_path`` for the - blueprint. - :param static_folder: A folder with static files that should be - served by the blueprint's static route. The path is relative to - the blueprint's root path. Blueprint static files are disabled - by default. - :param static_url_path: The url to serve static files from. - Defaults to ``static_folder``. If the blueprint does not have - a ``url_prefix``, the app's static route will take precedence, - and the blueprint's static files won't be accessible. - :param template_folder: A folder with templates that should be added - to the app's template search path. The path is relative to the - blueprint's root path. Blueprint templates are disabled by - default. Blueprint templates have a lower precedence than those - in the app's templates folder. - :param url_prefix: A path to prepend to all of the blueprint's URLs, - to make them distinct from the rest of the app's routes. - :param subdomain: A subdomain that blueprint routes will match on by - default. - :param url_defaults: A dict of default values that blueprint routes - will receive by default. - :param root_path: By default, the blueprint will automatically set - this based on ``import_name``. In certain situations this - automatic detection can fail, so the path can be specified - manually instead. - - .. versionchanged:: 1.1.0 - Blueprints have a ``cli`` group to register nested CLI commands. - The ``cli_group`` parameter controls the name of the group under - the ``flask`` command. - - .. versionadded:: 0.7 - """ - - warn_on_modifications = False - _got_registered_once = False - - #: Blueprint local JSON encoder class to use. Set to ``None`` to use - #: the app's :class:`~flask.Flask.json_encoder`. - json_encoder = None - #: Blueprint local JSON decoder class to use. Set to ``None`` to use - #: the app's :class:`~flask.Flask.json_decoder`. - json_decoder = None - - def __init__( - self, - name: str, - import_name: str, - static_folder: t.Optional[t.Union[str, os.PathLike]] = None, - static_url_path: t.Optional[str] = None, - template_folder: t.Optional[str] = None, - url_prefix: t.Optional[str] = None, - subdomain: t.Optional[str] = None, - url_defaults: t.Optional[dict] = None, - root_path: t.Optional[str] = None, - cli_group: t.Optional[str] = _sentinel, # type: ignore - ): - super().__init__( - import_name=import_name, - static_folder=static_folder, - static_url_path=static_url_path, - template_folder=template_folder, - root_path=root_path, - ) - - if "." in name: - raise ValueError("'name' may not contain a dot '.' character.") - - self.name = name - self.url_prefix = url_prefix - self.subdomain = subdomain - self.deferred_functions: t.List[DeferredSetupFunction] = [] - - if url_defaults is None: - url_defaults = {} - - self.url_values_defaults = url_defaults - self.cli_group = cli_group - self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] - - def _is_setup_finished(self) -> bool: - return self.warn_on_modifications and self._got_registered_once - - def record(self, func: t.Callable) -> None: - """Registers a function that is called when the blueprint is - registered on the application. This function is called with the - state as argument as returned by the :meth:`make_setup_state` - method. - """ - if self._got_registered_once and self.warn_on_modifications: - from warnings import warn - - warn( - Warning( - "The blueprint was already registered once but is" - " getting modified now. These changes will not show" - " up." - ) - ) - self.deferred_functions.append(func) - - def record_once(self, func: t.Callable) -> None: - """Works like :meth:`record` but wraps the function in another - function that will ensure the function is only called once. If the - blueprint is registered a second time on the application, the - function passed is not called. - """ - - def wrapper(state: BlueprintSetupState) -> None: - if state.first_registration: - func(state) - - return self.record(update_wrapper(wrapper, func)) - - def make_setup_state( - self, app: "Flask", options: dict, first_registration: bool = False - ) -> BlueprintSetupState: - """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` - object that is later passed to the register callback functions. - Subclasses can override this to return a subclass of the setup state. - """ - return BlueprintSetupState(self, app, options, first_registration) - - def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: - """Register a :class:`~flask.Blueprint` on this blueprint. Keyword - arguments passed to this method will override the defaults set - on the blueprint. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionadded:: 2.0 - """ - if blueprint is self: - raise ValueError("Cannot register a blueprint on itself") - self._blueprints.append((blueprint, options)) - - def register(self, app: "Flask", options: dict) -> None: - """Called by :meth:`Flask.register_blueprint` to register all - views and callbacks registered on the blueprint with the - application. Creates a :class:`.BlueprintSetupState` and calls - each :meth:`record` callback with it. - - :param app: The application this blueprint is being registered - with. - :param options: Keyword arguments forwarded from - :meth:`~Flask.register_blueprint`. - - .. versionchanged:: 2.0.1 - Nested blueprints are registered with their dotted name. - This allows different blueprints with the same name to be - nested at different locations. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionchanged:: 2.0.1 - Registering the same blueprint with the same name multiple - times is deprecated and will become an error in Flask 2.1. - """ - name_prefix = options.get("name_prefix", "") - self_name = options.get("name", self.name) - name = f"{name_prefix}.{self_name}".lstrip(".") - - if name in app.blueprints: - existing_at = f" '{name}'" if self_name != name else "" - - if app.blueprints[name] is not self: - raise ValueError( - f"The name '{self_name}' is already registered for" - f" a different blueprint{existing_at}. Use 'name='" - " to provide a unique name." - ) - else: - import warnings - - warnings.warn( - f"The name '{self_name}' is already registered for" - f" this blueprint{existing_at}. Use 'name=' to" - " provide a unique name. This will become an error" - " in Flask 2.1.", - stacklevel=4, - ) - - first_bp_registration = not any(bp is self for bp in app.blueprints.values()) - first_name_registration = name not in app.blueprints - - app.blueprints[name] = self - self._got_registered_once = True - state = self.make_setup_state(app, options, first_bp_registration) - - if self.has_static_folder: - state.add_url_rule( - f"{self.static_url_path}/", - view_func=self.send_static_file, - endpoint="static", - ) - - # Merge blueprint data into parent. - if first_bp_registration or first_name_registration: - - def extend(bp_dict, parent_dict): - for key, values in bp_dict.items(): - key = name if key is None else f"{name}.{key}" - parent_dict[key].extend(values) - - for key, value in self.error_handler_spec.items(): - key = name if key is None else f"{name}.{key}" - value = defaultdict( - dict, - { - code: { - exc_class: func for exc_class, func in code_values.items() - } - for code, code_values in value.items() - }, - ) - app.error_handler_spec[key] = value - - for endpoint, func in self.view_functions.items(): - app.view_functions[endpoint] = func - - extend(self.before_request_funcs, app.before_request_funcs) - extend(self.after_request_funcs, app.after_request_funcs) - extend( - self.teardown_request_funcs, - app.teardown_request_funcs, - ) - extend(self.url_default_functions, app.url_default_functions) - extend(self.url_value_preprocessors, app.url_value_preprocessors) - extend(self.template_context_processors, app.template_context_processors) - - for deferred in self.deferred_functions: - deferred(state) - - cli_resolved_group = options.get("cli_group", self.cli_group) - - if self.cli.commands: - if cli_resolved_group is None: - app.cli.commands.update(self.cli.commands) - elif cli_resolved_group is _sentinel: - self.cli.name = name - app.cli.add_command(self.cli) - else: - self.cli.name = cli_resolved_group - app.cli.add_command(self.cli) - - for blueprint, bp_options in self._blueprints: - bp_options = bp_options.copy() - bp_url_prefix = bp_options.get("url_prefix") - - if bp_url_prefix is None: - bp_url_prefix = blueprint.url_prefix - - if state.url_prefix is not None and bp_url_prefix is not None: - bp_options["url_prefix"] = ( - state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") - ) - elif bp_url_prefix is not None: - bp_options["url_prefix"] = bp_url_prefix - elif state.url_prefix is not None: - bp_options["url_prefix"] = state.url_prefix - - bp_options["name_prefix"] = name - blueprint.register(app, bp_options) - - def add_url_rule( - self, - rule: str, - endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, - provide_automatic_options: t.Optional[bool] = None, - **options: t.Any, - ) -> None: - """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for - the :func:`url_for` function is prefixed with the name of the blueprint. - """ - if endpoint and "." in endpoint: - raise ValueError("'endpoint' may not contain a dot '.' character.") - - if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: - raise ValueError("'view_func' name may not contain a dot '.' character.") - - self.record( - lambda s: s.add_url_rule( - rule, - endpoint, - view_func, - provide_automatic_options=provide_automatic_options, - **options, - ) - ) - - def app_template_filter( - self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: - """Register a custom template filter, available application wide. Like - :meth:`Flask.template_filter` but for a blueprint. - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - - def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: - self.add_app_template_filter(f, name=name) - return f - - return decorator - - def add_app_template_filter( - self, f: TemplateFilterCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template filter, available application wide. Like - :meth:`Flask.add_template_filter` but for a blueprint. Works exactly - like the :meth:`app_template_filter` decorator. - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - - def register_template(state: BlueprintSetupState) -> None: - state.app.jinja_env.filters[name or f.__name__] = f - - self.record_once(register_template) - - def app_template_test( - self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: - """Register a custom template test, available application wide. Like - :meth:`Flask.template_test` but for a blueprint. - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - - def decorator(f: TemplateTestCallable) -> TemplateTestCallable: - self.add_app_template_test(f, name=name) - return f - - return decorator - - def add_app_template_test( - self, f: TemplateTestCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template test, available application wide. Like - :meth:`Flask.add_template_test` but for a blueprint. Works exactly - like the :meth:`app_template_test` decorator. - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - - def register_template(state: BlueprintSetupState) -> None: - state.app.jinja_env.tests[name or f.__name__] = f - - self.record_once(register_template) - - def app_template_global( - self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: - """Register a custom template global, available application wide. Like - :meth:`Flask.template_global` but for a blueprint. - - .. versionadded:: 0.10 - - :param name: the optional name of the global, otherwise the - function name will be used. - """ - - def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: - self.add_app_template_global(f, name=name) - return f - - return decorator - - def add_app_template_global( - self, f: TemplateGlobalCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template global, available application wide. Like - :meth:`Flask.add_template_global` but for a blueprint. Works exactly - like the :meth:`app_template_global` decorator. - - .. versionadded:: 0.10 - - :param name: the optional name of the global, otherwise the - function name will be used. - """ - - def register_template(state: BlueprintSetupState) -> None: - state.app.jinja_env.globals[name or f.__name__] = f - - self.record_once(register_template) - - def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: - """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a blueprint. - """ - self.record_once( - lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) - ) - return f - - def before_app_first_request( - self, f: BeforeFirstRequestCallable - ) -> BeforeFirstRequestCallable: - """Like :meth:`Flask.before_first_request`. Such a function is - executed before the first request to the application. - """ - self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) - return f - - def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable: - """Like :meth:`Flask.after_request` but for a blueprint. Such a function - is executed after each request, even if outside of the blueprint. - """ - self.record_once( - lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) - ) - return f - - def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: - """Like :meth:`Flask.teardown_request` but for a blueprint. Such a - function is executed when tearing down each request, even if outside of - the blueprint. - """ - self.record_once( - lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) - ) - return f - - def app_context_processor( - self, f: TemplateContextProcessorCallable - ) -> TemplateContextProcessorCallable: - """Like :meth:`Flask.context_processor` but for a blueprint. Such a - function is executed each request, even if outside of the blueprint. - """ - self.record_once( - lambda s: s.app.template_context_processors.setdefault(None, []).append(f) - ) - return f - - def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable: - """Like :meth:`Flask.errorhandler` but for a blueprint. This - handler is used for all requests, even if outside of the blueprint. - """ - - def decorator( - f: "ErrorHandlerCallable[Exception]", - ) -> "ErrorHandlerCallable[Exception]": - self.record_once(lambda s: s.app.errorhandler(code)(f)) - return f - - return decorator - - def app_url_value_preprocessor( - self, f: URLValuePreprocessorCallable - ) -> URLValuePreprocessorCallable: - """Same as :meth:`url_value_preprocessor` but application wide.""" - self.record_once( - lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) - ) - return f - - def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: - """Same as :meth:`url_defaults` but application wide.""" - self.record_once( - lambda s: s.app.url_default_functions.setdefault(None, []).append(f) - ) - return f diff --git a/laplas/tools/abstract_map/flask/cli.py b/laplas/tools/abstract_map/flask/cli.py deleted file mode 100644 index 8e21532..0000000 --- a/laplas/tools/abstract_map/flask/cli.py +++ /dev/null @@ -1,999 +0,0 @@ -import ast -import inspect -import os -import platform -import re -import sys -import traceback -import warnings -from functools import update_wrapper -from operator import attrgetter -from threading import Lock -from threading import Thread - -import click -from werkzeug.utils import import_string - -from .globals import current_app -from .helpers import get_debug_flag -from .helpers import get_env -from .helpers import get_load_dotenv - -try: - import dotenv -except ImportError: - dotenv = None - -try: - import ssl -except ImportError: - ssl = None # type: ignore - - -class NoAppException(click.UsageError): - """Raised if an application cannot be found or loaded.""" - - -def find_best_app(script_info, module): - """Given a module instance this tries to find the best possible - application in the module or raises an exception. - """ - from . import Flask - - # Search for the most common names first. - for attr_name in ("app", "application"): - app = getattr(module, attr_name, None) - - if isinstance(app, Flask): - return app - - # Otherwise find the only object that is a Flask instance. - matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] - - if len(matches) == 1: - return matches[0] - elif len(matches) > 1: - raise NoAppException( - "Detected multiple Flask applications in module" - f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'" - f" to specify the correct one." - ) - - # Search for app factory functions. - for attr_name in ("create_app", "make_app"): - app_factory = getattr(module, attr_name, None) - - if inspect.isfunction(app_factory): - try: - app = call_factory(script_info, app_factory) - - if isinstance(app, Flask): - return app - except TypeError as e: - if not _called_with_wrong_args(app_factory): - raise - - raise NoAppException( - f"Detected factory {attr_name!r} in module {module.__name__!r}," - " but could not call it without arguments. Use" - f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\"" - " to specify arguments." - ) from e - - raise NoAppException( - "Failed to find Flask application or factory in module" - f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'" - " to specify one." - ) - - -def call_factory(script_info, app_factory, args=None, kwargs=None): - """Takes an app factory, a ``script_info` object and optionally a tuple - of arguments. Checks for the existence of a script_info argument and calls - the app_factory depending on that and the arguments provided. - """ - sig = inspect.signature(app_factory) - args = [] if args is None else args - kwargs = {} if kwargs is None else kwargs - - if "script_info" in sig.parameters: - warnings.warn( - "The 'script_info' argument is deprecated and will not be" - " passed to the app factory function in Flask 2.1.", - DeprecationWarning, - ) - kwargs["script_info"] = script_info - - if not args and len(sig.parameters) == 1: - first_parameter = next(iter(sig.parameters.values())) - - if ( - first_parameter.default is inspect.Parameter.empty - # **kwargs is reported as an empty default, ignore it - and first_parameter.kind is not inspect.Parameter.VAR_KEYWORD - ): - warnings.warn( - "Script info is deprecated and will not be passed as the" - " single argument to the app factory function in Flask" - " 2.1.", - DeprecationWarning, - ) - args.append(script_info) - - return app_factory(*args, **kwargs) - - -def _called_with_wrong_args(f): - """Check whether calling a function raised a ``TypeError`` because - the call failed or because something in the factory raised the - error. - - :param f: The function that was called. - :return: ``True`` if the call failed. - """ - tb = sys.exc_info()[2] - - try: - while tb is not None: - if tb.tb_frame.f_code is f.__code__: - # In the function, it was called successfully. - return False - - tb = tb.tb_next - - # Didn't reach the function. - return True - finally: - # Delete tb to break a circular reference. - # https://docs.python.org/2/library/sys.html#sys.exc_info - del tb - - -def find_app_by_string(script_info, module, app_name): - """Check if the given string is a variable name or a function. Call - a function to get the app instance, or return the variable directly. - """ - from . import Flask - - # Parse app_name as a single expression to determine if it's a valid - # attribute name or function call. - try: - expr = ast.parse(app_name.strip(), mode="eval").body - except SyntaxError: - raise NoAppException( - f"Failed to parse {app_name!r} as an attribute name or function call." - ) from None - - if isinstance(expr, ast.Name): - name = expr.id - args = kwargs = None - elif isinstance(expr, ast.Call): - # Ensure the function name is an attribute name only. - if not isinstance(expr.func, ast.Name): - raise NoAppException( - f"Function reference must be a simple name: {app_name!r}." - ) - - name = expr.func.id - - # Parse the positional and keyword arguments as literals. - try: - args = [ast.literal_eval(arg) for arg in expr.args] - kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords} - except ValueError: - # literal_eval gives cryptic error messages, show a generic - # message with the full expression instead. - raise NoAppException( - f"Failed to parse arguments as literal values: {app_name!r}." - ) from None - else: - raise NoAppException( - f"Failed to parse {app_name!r} as an attribute name or function call." - ) - - try: - attr = getattr(module, name) - except AttributeError as e: - raise NoAppException( - f"Failed to find attribute {name!r} in {module.__name__!r}." - ) from e - - # If the attribute is a function, call it with any args and kwargs - # to get the real application. - if inspect.isfunction(attr): - try: - app = call_factory(script_info, attr, args, kwargs) - except TypeError as e: - if not _called_with_wrong_args(attr): - raise - - raise NoAppException( - f"The factory {app_name!r} in module" - f" {module.__name__!r} could not be called with the" - " specified arguments." - ) from e - else: - app = attr - - if isinstance(app, Flask): - return app - - raise NoAppException( - "A valid Flask application was not obtained from" - f" '{module.__name__}:{app_name}'." - ) - - -def prepare_import(path): - """Given a filename this will try to calculate the python path, add it - to the search path and return the actual module name that is expected. - """ - path = os.path.realpath(path) - - fname, ext = os.path.splitext(path) - if ext == ".py": - path = fname - - if os.path.basename(path) == "__init__": - path = os.path.dirname(path) - - module_name = [] - - # move up until outside package structure (no __init__.py) - while True: - path, name = os.path.split(path) - module_name.append(name) - - if not os.path.exists(os.path.join(path, "__init__.py")): - break - - if sys.path[0] != path: - sys.path.insert(0, path) - - return ".".join(module_name[::-1]) - - -def locate_app(script_info, module_name, app_name, raise_if_not_found=True): - __traceback_hide__ = True # noqa: F841 - - try: - __import__(module_name) - except ImportError: - # Reraise the ImportError if it occurred within the imported module. - # Determine this by checking whether the trace has a depth > 1. - if sys.exc_info()[2].tb_next: - raise NoAppException( - f"While importing {module_name!r}, an ImportError was" - f" raised:\n\n{traceback.format_exc()}" - ) from None - elif raise_if_not_found: - raise NoAppException(f"Could not import {module_name!r}.") from None - else: - return - - module = sys.modules[module_name] - - if app_name is None: - return find_best_app(script_info, module) - else: - return find_app_by_string(script_info, module, app_name) - - -def get_version(ctx, param, value): - if not value or ctx.resilient_parsing: - return - - import werkzeug - from . import __version__ - - click.echo( - f"Python {platform.python_version()}\n" - f"Flask {__version__}\n" - f"Werkzeug {werkzeug.__version__}", - color=ctx.color, - ) - ctx.exit() - - -version_option = click.Option( - ["--version"], - help="Show the flask version", - expose_value=False, - callback=get_version, - is_flag=True, - is_eager=True, -) - - -class DispatchingApp: - """Special application that dispatches to a Flask application which - is imported by name in a background thread. If an error happens - it is recorded and shown as part of the WSGI handling which in case - of the Werkzeug debugger means that it shows up in the browser. - """ - - def __init__(self, loader, use_eager_loading=None): - self.loader = loader - self._app = None - self._lock = Lock() - self._bg_loading_exc = None - - if use_eager_loading is None: - use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true" - - if use_eager_loading: - self._load_unlocked() - else: - self._load_in_background() - - def _load_in_background(self): - def _load_app(): - __traceback_hide__ = True # noqa: F841 - with self._lock: - try: - self._load_unlocked() - except Exception as e: - self._bg_loading_exc = e - - t = Thread(target=_load_app, args=()) - t.start() - - def _flush_bg_loading_exception(self): - __traceback_hide__ = True # noqa: F841 - exc = self._bg_loading_exc - - if exc is not None: - self._bg_loading_exc = None - raise exc - - def _load_unlocked(self): - __traceback_hide__ = True # noqa: F841 - self._app = rv = self.loader() - self._bg_loading_exc = None - return rv - - def __call__(self, environ, start_response): - __traceback_hide__ = True # noqa: F841 - if self._app is not None: - return self._app(environ, start_response) - self._flush_bg_loading_exception() - with self._lock: - if self._app is not None: - rv = self._app - else: - rv = self._load_unlocked() - return rv(environ, start_response) - - -class ScriptInfo: - """Helper object to deal with Flask applications. This is usually not - necessary to interface with as it's used internally in the dispatching - to click. In future versions of Flask this object will most likely play - a bigger role. Typically it's created automatically by the - :class:`FlaskGroup` but you can also manually create it and pass it - onwards as click object. - """ - - def __init__(self, app_import_path=None, create_app=None, set_debug_flag=True): - #: Optionally the import path for the Flask application. - self.app_import_path = app_import_path or os.environ.get("FLASK_APP") - #: Optionally a function that is passed the script info to create - #: the instance of the application. - self.create_app = create_app - #: A dictionary with arbitrary data that can be associated with - #: this script info. - self.data = {} - self.set_debug_flag = set_debug_flag - self._loaded_app = None - - def load_app(self): - """Loads the Flask app (if not yet loaded) and returns it. Calling - this multiple times will just result in the already loaded app to - be returned. - """ - __traceback_hide__ = True # noqa: F841 - - if self._loaded_app is not None: - return self._loaded_app - - if self.create_app is not None: - app = call_factory(self, self.create_app) - else: - if self.app_import_path: - path, name = ( - re.split(r":(?![\\/])", self.app_import_path, 1) + [None] - )[:2] - import_name = prepare_import(path) - app = locate_app(self, import_name, name) - else: - for path in ("wsgi.py", "app.py"): - import_name = prepare_import(path) - app = locate_app(self, import_name, None, raise_if_not_found=False) - - if app: - break - - if not app: - raise NoAppException( - "Could not locate a Flask application. You did not provide " - 'the "FLASK_APP" environment variable, and a "wsgi.py" or ' - '"app.py" module was not found in the current directory.' - ) - - if self.set_debug_flag: - # Update the app's debug flag through the descriptor so that - # other values repopulate as well. - app.debug = get_debug_flag() - - self._loaded_app = app - return app - - -pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) - - -def with_appcontext(f): - """Wraps a callback so that it's guaranteed to be executed with the - script's application context. If callbacks are registered directly - to the ``app.cli`` object then they are wrapped with this function - by default unless it's disabled. - """ - - @click.pass_context - def decorator(__ctx, *args, **kwargs): - with __ctx.ensure_object(ScriptInfo).load_app().app_context(): - return __ctx.invoke(f, *args, **kwargs) - - return update_wrapper(decorator, f) - - -class AppGroup(click.Group): - """This works similar to a regular click :class:`~click.Group` but it - changes the behavior of the :meth:`command` decorator so that it - automatically wraps the functions in :func:`with_appcontext`. - - Not to be confused with :class:`FlaskGroup`. - """ - - def command(self, *args, **kwargs): - """This works exactly like the method of the same name on a regular - :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` - unless it's disabled by passing ``with_appcontext=False``. - """ - wrap_for_ctx = kwargs.pop("with_appcontext", True) - - def decorator(f): - if wrap_for_ctx: - f = with_appcontext(f) - return click.Group.command(self, *args, **kwargs)(f) - - return decorator - - def group(self, *args, **kwargs): - """This works exactly like the method of the same name on a regular - :class:`click.Group` but it defaults the group class to - :class:`AppGroup`. - """ - kwargs.setdefault("cls", AppGroup) - return click.Group.group(self, *args, **kwargs) - - -class FlaskGroup(AppGroup): - """Special subclass of the :class:`AppGroup` group that supports - loading more commands from the configured Flask app. Normally a - developer does not have to interface with this class but there are - some very advanced use cases for which it makes sense to create an - instance of this. see :ref:`custom-scripts`. - - :param add_default_commands: if this is True then the default run and - shell commands will be added. - :param add_version_option: adds the ``--version`` option. - :param create_app: an optional callback that is passed the script info and - returns the loaded app. - :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` - files to set environment variables. Will also change the working - directory to the directory containing the first file found. - :param set_debug_flag: Set the app's debug flag based on the active - environment - - .. versionchanged:: 1.0 - If installed, python-dotenv will be used to load environment variables - from :file:`.env` and :file:`.flaskenv` files. - """ - - def __init__( - self, - add_default_commands=True, - create_app=None, - add_version_option=True, - load_dotenv=True, - set_debug_flag=True, - **extra, - ): - params = list(extra.pop("params", None) or ()) - - if add_version_option: - params.append(version_option) - - AppGroup.__init__(self, params=params, **extra) - self.create_app = create_app - self.load_dotenv = load_dotenv - self.set_debug_flag = set_debug_flag - - if add_default_commands: - self.add_command(run_command) - self.add_command(shell_command) - self.add_command(routes_command) - - self._loaded_plugin_commands = False - - def _load_plugin_commands(self): - if self._loaded_plugin_commands: - return - try: - import pkg_resources - except ImportError: - self._loaded_plugin_commands = True - return - - for ep in pkg_resources.iter_entry_points("flask.commands"): - self.add_command(ep.load(), ep.name) - self._loaded_plugin_commands = True - - def get_command(self, ctx, name): - self._load_plugin_commands() - # Look up built-in and plugin commands, which should be - # available even if the app fails to load. - rv = super().get_command(ctx, name) - - if rv is not None: - return rv - - info = ctx.ensure_object(ScriptInfo) - - # Look up commands provided by the app, showing an error and - # continuing if the app couldn't be loaded. - try: - return info.load_app().cli.get_command(ctx, name) - except NoAppException as e: - click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") - - def list_commands(self, ctx): - self._load_plugin_commands() - # Start with the built-in and plugin commands. - rv = set(super().list_commands(ctx)) - info = ctx.ensure_object(ScriptInfo) - - # Add commands provided by the app, showing an error and - # continuing if the app couldn't be loaded. - try: - rv.update(info.load_app().cli.list_commands(ctx)) - except NoAppException as e: - # When an app couldn't be loaded, show the error message - # without the traceback. - click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") - except Exception: - # When any other errors occurred during loading, show the - # full traceback. - click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") - - return sorted(rv) - - def main(self, *args, **kwargs): - # Set a global flag that indicates that we were invoked from the - # command line interface. This is detected by Flask.run to make the - # call into a no-op. This is necessary to avoid ugly errors when the - # script that is loaded here also attempts to start a server. - os.environ["FLASK_RUN_FROM_CLI"] = "true" - - if get_load_dotenv(self.load_dotenv): - load_dotenv() - - obj = kwargs.get("obj") - - if obj is None: - obj = ScriptInfo( - create_app=self.create_app, set_debug_flag=self.set_debug_flag - ) - - kwargs["obj"] = obj - kwargs.setdefault("auto_envvar_prefix", "FLASK") - return super().main(*args, **kwargs) - - -def _path_is_ancestor(path, other): - """Take ``other`` and remove the length of ``path`` from it. Then join it - to ``path``. If it is the original value, ``path`` is an ancestor of - ``other``.""" - return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other - - -def load_dotenv(path=None): - """Load "dotenv" files in order of precedence to set environment variables. - - If an env var is already set it is not overwritten, so earlier files in the - list are preferred over later files. - - This is a no-op if `python-dotenv`_ is not installed. - - .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme - - :param path: Load the file at this location instead of searching. - :return: ``True`` if a file was loaded. - - .. versionchanged:: 1.1.0 - Returns ``False`` when python-dotenv is not installed, or when - the given path isn't a file. - - .. versionchanged:: 2.0 - When loading the env files, set the default encoding to UTF-8. - - .. versionadded:: 1.0 - """ - if dotenv is None: - if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): - click.secho( - " * Tip: There are .env or .flaskenv files present." - ' Do "pip install python-dotenv" to use them.', - fg="yellow", - err=True, - ) - - return False - - # if the given path specifies the actual file then return True, - # else False - if path is not None: - if os.path.isfile(path): - return dotenv.load_dotenv(path, encoding="utf-8") - - return False - - new_dir = None - - for name in (".env", ".flaskenv"): - path = dotenv.find_dotenv(name, usecwd=True) - - if not path: - continue - - if new_dir is None: - new_dir = os.path.dirname(path) - - dotenv.load_dotenv(path, encoding="utf-8") - - return new_dir is not None # at least one file was located and loaded - - -def show_server_banner(env, debug, app_import_path, eager_loading): - """Show extra startup messages the first time the server is run, - ignoring the reloader. - """ - if os.environ.get("WERKZEUG_RUN_MAIN") == "true": - return - - if app_import_path is not None: - message = f" * Serving Flask app {app_import_path!r}" - - if not eager_loading: - message += " (lazy loading)" - - click.echo(message) - - click.echo(f" * Environment: {env}") - - if env == "production": - click.secho( - " WARNING: This is a development server. Do not use it in" - " a production deployment.", - fg="red", - ) - click.secho(" Use a production WSGI server instead.", dim=True) - - if debug is not None: - click.echo(f" * Debug mode: {'on' if debug else 'off'}") - - -class CertParamType(click.ParamType): - """Click option type for the ``--cert`` option. Allows either an - existing file, the string ``'adhoc'``, or an import for a - :class:`~ssl.SSLContext` object. - """ - - name = "path" - - def __init__(self): - self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) - - def convert(self, value, param, ctx): - if ssl is None: - raise click.BadParameter( - 'Using "--cert" requires Python to be compiled with SSL support.', - ctx, - param, - ) - - try: - return self.path_type(value, param, ctx) - except click.BadParameter: - value = click.STRING(value, param, ctx).lower() - - if value == "adhoc": - try: - import cryptography # noqa: F401 - except ImportError: - raise click.BadParameter( - "Using ad-hoc certificates requires the cryptography library.", - ctx, - param, - ) from None - - return value - - obj = import_string(value, silent=True) - - if isinstance(obj, ssl.SSLContext): - return obj - - raise - - -def _validate_key(ctx, param, value): - """The ``--key`` option must be specified when ``--cert`` is a file. - Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. - """ - cert = ctx.params.get("cert") - is_adhoc = cert == "adhoc" - is_context = ssl and isinstance(cert, ssl.SSLContext) - - if value is not None: - if is_adhoc: - raise click.BadParameter( - 'When "--cert" is "adhoc", "--key" is not used.', ctx, param - ) - - if is_context: - raise click.BadParameter( - 'When "--cert" is an SSLContext object, "--key is not used.', ctx, param - ) - - if not cert: - raise click.BadParameter('"--cert" must also be specified.', ctx, param) - - ctx.params["cert"] = cert, value - - else: - if cert and not (is_adhoc or is_context): - raise click.BadParameter('Required when using "--cert".', ctx, param) - - return value - - -class SeparatedPathType(click.Path): - """Click option type that accepts a list of values separated by the - OS's path separator (``:``, ``;`` on Windows). Each value is - validated as a :class:`click.Path` type. - """ - - def convert(self, value, param, ctx): - items = self.split_envvar_value(value) - super_convert = super().convert - return [super_convert(item, param, ctx) for item in items] - - -@click.command("run", short_help="Run a development server.") -@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") -@click.option("--port", "-p", default=5000, help="The port to bind to.") -@click.option( - "--cert", type=CertParamType(), help="Specify a certificate file to use HTTPS." -) -@click.option( - "--key", - type=click.Path(exists=True, dir_okay=False, resolve_path=True), - callback=_validate_key, - expose_value=False, - help="The key file to use when specifying a certificate.", -) -@click.option( - "--reload/--no-reload", - default=None, - help="Enable or disable the reloader. By default the reloader " - "is active if debug is enabled.", -) -@click.option( - "--debugger/--no-debugger", - default=None, - help="Enable or disable the debugger. By default the debugger " - "is active if debug is enabled.", -) -@click.option( - "--eager-loading/--lazy-loading", - default=None, - help="Enable or disable eager loading. By default eager " - "loading is enabled if the reloader is disabled.", -) -@click.option( - "--with-threads/--without-threads", - default=True, - help="Enable or disable multithreading.", -) -@click.option( - "--extra-files", - default=None, - type=SeparatedPathType(), - help=( - "Extra files that trigger a reload on change. Multiple paths" - f" are separated by {os.path.pathsep!r}." - ), -) -@pass_script_info -def run_command( - info, host, port, reload, debugger, eager_loading, with_threads, cert, extra_files -): - """Run a local development server. - - This server is for development purposes only. It does not provide - the stability, security, or performance of production WSGI servers. - - The reloader and debugger are enabled by default if - FLASK_ENV=development or FLASK_DEBUG=1. - """ - debug = get_debug_flag() - - if reload is None: - reload = debug - - if debugger is None: - debugger = debug - - show_server_banner(get_env(), debug, info.app_import_path, eager_loading) - app = DispatchingApp(info.load_app, use_eager_loading=eager_loading) - - from werkzeug.serving import run_simple - - run_simple( - host, - port, - app, - use_reloader=reload, - use_debugger=debugger, - threaded=with_threads, - ssl_context=cert, - extra_files=extra_files, - ) - - -@click.command("shell", short_help="Run a shell in the app context.") -@with_appcontext -def shell_command() -> None: - """Run an interactive Python shell in the context of a given - Flask application. The application will populate the default - namespace of this shell according to its configuration. - - This is useful for executing small snippets of management code - without having to manually configure the application. - """ - import code - from .globals import _app_ctx_stack - - app = _app_ctx_stack.top.app - banner = ( - f"Python {sys.version} on {sys.platform}\n" - f"App: {app.import_name} [{app.env}]\n" - f"Instance: {app.instance_path}" - ) - ctx: dict = {} - - # Support the regular Python interpreter startup script if someone - # is using it. - startup = os.environ.get("PYTHONSTARTUP") - if startup and os.path.isfile(startup): - with open(startup) as f: - eval(compile(f.read(), startup, "exec"), ctx) - - ctx.update(app.make_shell_context()) - - # Site, customize, or startup script can set a hook to call when - # entering interactive mode. The default one sets up readline with - # tab and history completion. - interactive_hook = getattr(sys, "__interactivehook__", None) - - if interactive_hook is not None: - try: - import readline - from rlcompleter import Completer - except ImportError: - pass - else: - # rlcompleter uses __main__.__dict__ by default, which is - # flask.__main__. Use the shell context instead. - readline.set_completer(Completer(ctx).complete) - - interactive_hook() - - code.interact(banner=banner, local=ctx) - - -@click.command("routes", short_help="Show the routes for the app.") -@click.option( - "--sort", - "-s", - type=click.Choice(("endpoint", "methods", "rule", "match")), - default="endpoint", - help=( - 'Method to sort routes by. "match" is the order that Flask will match ' - "routes when dispatching a request." - ), -) -@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") -@with_appcontext -def routes_command(sort: str, all_methods: bool) -> None: - """Show all registered routes with endpoints and methods.""" - - rules = list(current_app.url_map.iter_rules()) - if not rules: - click.echo("No routes were registered.") - return - - ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS")) - - if sort in ("endpoint", "rule"): - rules = sorted(rules, key=attrgetter(sort)) - elif sort == "methods": - rules = sorted(rules, key=lambda rule: sorted(rule.methods)) # type: ignore - - rule_methods = [ - ", ".join(sorted(rule.methods - ignored_methods)) # type: ignore - for rule in rules - ] - - headers = ("Endpoint", "Methods", "Rule") - widths = ( - max(len(rule.endpoint) for rule in rules), - max(len(methods) for methods in rule_methods), - max(len(rule.rule) for rule in rules), - ) - widths = [max(len(h), w) for h, w in zip(headers, widths)] - row = "{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}".format(*widths) - - click.echo(row.format(*headers).strip()) - click.echo(row.format(*("-" * width for width in widths))) - - for rule, methods in zip(rules, rule_methods): - click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip()) - - -cli = FlaskGroup( - help="""\ -A general utility script for Flask applications. - -Provides commands from Flask, extensions, and the application. Loads the -application defined in the FLASK_APP environment variable, or from a wsgi.py -file. Setting the FLASK_ENV environment variable to 'development' will enable -debug mode. - -\b - {prefix}{cmd} FLASK_APP=hello.py - {prefix}{cmd} FLASK_ENV=development - {prefix}flask run -""".format( - cmd="export" if os.name == "posix" else "set", - prefix="$ " if os.name == "posix" else "> ", - ) -) - - -def main() -> None: - if int(click.__version__[0]) < 8: - warnings.warn( - "Using the `flask` cli with Click 7 is deprecated and" - " will not be supported starting with Flask 2.1." - " Please upgrade to Click 8 as soon as possible.", - DeprecationWarning, - ) - # TODO omit sys.argv once https://github.com/pallets/click/issues/536 is fixed - cli.main(args=sys.argv[1:]) - - -if __name__ == "__main__": - main() diff --git a/laplas/tools/abstract_map/flask/config.py b/laplas/tools/abstract_map/flask/config.py deleted file mode 100644 index ca76902..0000000 --- a/laplas/tools/abstract_map/flask/config.py +++ /dev/null @@ -1,295 +0,0 @@ -import errno -import os -import types -import typing as t - -from werkzeug.utils import import_string - - -class ConfigAttribute: - """Makes an attribute forward to the config""" - - def __init__(self, name: str, get_converter: t.Optional[t.Callable] = None) -> None: - self.__name__ = name - self.get_converter = get_converter - - def __get__(self, obj: t.Any, owner: t.Any = None) -> t.Any: - if obj is None: - return self - rv = obj.config[self.__name__] - if self.get_converter is not None: - rv = self.get_converter(rv) - return rv - - def __set__(self, obj: t.Any, value: t.Any) -> None: - obj.config[self.__name__] = value - - -class Config(dict): - """Works exactly like a dict but provides ways to fill it from files - or special dictionaries. There are two common patterns to populate the - config. - - Either you can fill the config from a config file:: - - app.config.from_pyfile('yourconfig.cfg') - - Or alternatively you can define the configuration options in the - module that calls :meth:`from_object` or provide an import path to - a module that should be loaded. It is also possible to tell it to - use the same module and with that provide the configuration values - just before the call:: - - DEBUG = True - SECRET_KEY = 'development key' - app.config.from_object(__name__) - - In both cases (loading from any Python file or loading from modules), - only uppercase keys are added to the config. This makes it possible to use - lowercase values in the config file for temporary values that are not added - to the config or to define the config keys in the same file that implements - the application. - - Probably the most interesting way to load configurations is from an - environment variable pointing to a file:: - - app.config.from_envvar('YOURAPPLICATION_SETTINGS') - - In this case before launching the application you have to set this - environment variable to the file you want to use. On Linux and OS X - use the export statement:: - - export YOURAPPLICATION_SETTINGS='/path/to/config/file' - - On windows use `set` instead. - - :param root_path: path to which files are read relative from. When the - config object is created by the application, this is - the application's :attr:`~flask.Flask.root_path`. - :param defaults: an optional dictionary of default values - """ - - def __init__(self, root_path: str, defaults: t.Optional[dict] = None) -> None: - dict.__init__(self, defaults or {}) - self.root_path = root_path - - def from_envvar(self, variable_name: str, silent: bool = False) -> bool: - """Loads a configuration from an environment variable pointing to - a configuration file. This is basically just a shortcut with nicer - error messages for this line of code:: - - app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) - - :param variable_name: name of the environment variable - :param silent: set to ``True`` if you want silent failure for missing - files. - :return: ``True`` if the file was loaded successfully. - """ - rv = os.environ.get(variable_name) - if not rv: - if silent: - return False - raise RuntimeError( - f"The environment variable {variable_name!r} is not set" - " and as such configuration could not be loaded. Set" - " this variable and make it point to a configuration" - " file" - ) - return self.from_pyfile(rv, silent=silent) - - def from_pyfile(self, filename: str, silent: bool = False) -> bool: - """Updates the values in the config from a Python file. This function - behaves as if the file was imported as module with the - :meth:`from_object` function. - - :param filename: the filename of the config. This can either be an - absolute filename or a filename relative to the - root path. - :param silent: set to ``True`` if you want silent failure for missing - files. - :return: ``True`` if the file was loaded successfully. - - .. versionadded:: 0.7 - `silent` parameter. - """ - filename = os.path.join(self.root_path, filename) - d = types.ModuleType("config") - d.__file__ = filename - try: - with open(filename, mode="rb") as config_file: - exec(compile(config_file.read(), filename, "exec"), d.__dict__) - except OSError as e: - if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): - return False - e.strerror = f"Unable to load configuration file ({e.strerror})" - raise - self.from_object(d) - return True - - def from_object(self, obj: t.Union[object, str]) -> None: - """Updates the values from the given object. An object can be of one - of the following two types: - - - a string: in this case the object with that name will be imported - - an actual object reference: that object is used directly - - Objects are usually either modules or classes. :meth:`from_object` - loads only the uppercase attributes of the module/class. A ``dict`` - object will not work with :meth:`from_object` because the keys of a - ``dict`` are not attributes of the ``dict`` class. - - Example of module-based configuration:: - - app.config.from_object('yourapplication.default_config') - from yourapplication import default_config - app.config.from_object(default_config) - - Nothing is done to the object before loading. If the object is a - class and has ``@property`` attributes, it needs to be - instantiated before being passed to this method. - - You should not use this function to load the actual configuration but - rather configuration defaults. The actual config should be loaded - with :meth:`from_pyfile` and ideally from a location not within the - package because the package might be installed system wide. - - See :ref:`config-dev-prod` for an example of class-based configuration - using :meth:`from_object`. - - :param obj: an import name or object - """ - if isinstance(obj, str): - obj = import_string(obj) - for key in dir(obj): - if key.isupper(): - self[key] = getattr(obj, key) - - def from_file( - self, - filename: str, - load: t.Callable[[t.IO[t.Any]], t.Mapping], - silent: bool = False, - ) -> bool: - """Update the values in the config from a file that is loaded - using the ``load`` parameter. The loaded data is passed to the - :meth:`from_mapping` method. - - .. code-block:: python - - import toml - app.config.from_file("config.toml", load=toml.load) - - :param filename: The path to the data file. This can be an - absolute path or relative to the config root path. - :param load: A callable that takes a file handle and returns a - mapping of loaded data from the file. - :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` - implements a ``read`` method. - :param silent: Ignore the file if it doesn't exist. - :return: ``True`` if the file was loaded successfully. - - .. versionadded:: 2.0 - """ - filename = os.path.join(self.root_path, filename) - - try: - with open(filename) as f: - obj = load(f) - except OSError as e: - if silent and e.errno in (errno.ENOENT, errno.EISDIR): - return False - - e.strerror = f"Unable to load configuration file ({e.strerror})" - raise - - return self.from_mapping(obj) - - def from_json(self, filename: str, silent: bool = False) -> bool: - """Update the values in the config from a JSON file. The loaded - data is passed to the :meth:`from_mapping` method. - - :param filename: The path to the JSON file. This can be an - absolute path or relative to the config root path. - :param silent: Ignore the file if it doesn't exist. - :return: ``True`` if the file was loaded successfully. - - .. deprecated:: 2.0.0 - Will be removed in Flask 2.1. Use :meth:`from_file` instead. - This was removed early in 2.0.0, was added back in 2.0.1. - - .. versionadded:: 0.11 - """ - import warnings - from . import json - - warnings.warn( - "'from_json' is deprecated and will be removed in Flask" - " 2.1. Use 'from_file(path, json.load)' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self.from_file(filename, json.load, silent=silent) - - def from_mapping( - self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any - ) -> bool: - """Updates the config like :meth:`update` ignoring items with non-upper - keys. - :return: Always returns ``True``. - - .. versionadded:: 0.11 - """ - mappings: t.Dict[str, t.Any] = {} - if mapping is not None: - mappings.update(mapping) - mappings.update(kwargs) - for key, value in mappings.items(): - if key.isupper(): - self[key] = value - return True - - def get_namespace( - self, namespace: str, lowercase: bool = True, trim_namespace: bool = True - ) -> t.Dict[str, t.Any]: - """Returns a dictionary containing a subset of configuration options - that match the specified namespace/prefix. Example usage:: - - app.config['IMAGE_STORE_TYPE'] = 'fs' - app.config['IMAGE_STORE_PATH'] = '/var/app/images' - app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' - image_store_config = app.config.get_namespace('IMAGE_STORE_') - - The resulting dictionary `image_store_config` would look like:: - - { - 'type': 'fs', - 'path': '/var/app/images', - 'base_url': 'http://img.website.com' - } - - This is often useful when configuration options map directly to - keyword arguments in functions or class constructors. - - :param namespace: a configuration namespace - :param lowercase: a flag indicating if the keys of the resulting - dictionary should be lowercase - :param trim_namespace: a flag indicating if the keys of the resulting - dictionary should not include the namespace - - .. versionadded:: 0.11 - """ - rv = {} - for k, v in self.items(): - if not k.startswith(namespace): - continue - if trim_namespace: - key = k[len(namespace) :] - else: - key = k - if lowercase: - key = key.lower() - rv[key] = v - return rv - - def __repr__(self) -> str: - return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/laplas/tools/abstract_map/flask/ctx.py b/laplas/tools/abstract_map/flask/ctx.py deleted file mode 100644 index 47465fd..0000000 --- a/laplas/tools/abstract_map/flask/ctx.py +++ /dev/null @@ -1,489 +0,0 @@ -import sys -import typing as t -from functools import update_wrapper -from types import TracebackType - -from werkzeug.exceptions import HTTPException - -from .globals import _app_ctx_stack -from .globals import _request_ctx_stack -from .signals import appcontext_popped -from .signals import appcontext_pushed -from .typing import AfterRequestCallable - -if t.TYPE_CHECKING: - from .app import Flask - from .sessions import SessionMixin - from .wrappers import Request - - -# a singleton sentinel value for parameter defaults -_sentinel = object() - - -class _AppCtxGlobals: - """A plain object. Used as a namespace for storing data during an - application context. - - Creating an app context automatically creates this object, which is - made available as the :data:`g` proxy. - - .. describe:: 'key' in g - - Check whether an attribute is present. - - .. versionadded:: 0.10 - - .. describe:: iter(g) - - Return an iterator over the attribute names. - - .. versionadded:: 0.10 - """ - - # Define attr methods to let mypy know this is a namespace object - # that has arbitrary attributes. - - def __getattr__(self, name: str) -> t.Any: - try: - return self.__dict__[name] - except KeyError: - raise AttributeError(name) from None - - def __setattr__(self, name: str, value: t.Any) -> None: - self.__dict__[name] = value - - def __delattr__(self, name: str) -> None: - try: - del self.__dict__[name] - except KeyError: - raise AttributeError(name) from None - - def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any: - """Get an attribute by name, or a default value. Like - :meth:`dict.get`. - - :param name: Name of attribute to get. - :param default: Value to return if the attribute is not present. - - .. versionadded:: 0.10 - """ - return self.__dict__.get(name, default) - - def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: - """Get and remove an attribute by name. Like :meth:`dict.pop`. - - :param name: Name of attribute to pop. - :param default: Value to return if the attribute is not present, - instead of raising a ``KeyError``. - - .. versionadded:: 0.11 - """ - if default is _sentinel: - return self.__dict__.pop(name) - else: - return self.__dict__.pop(name, default) - - def setdefault(self, name: str, default: t.Any = None) -> t.Any: - """Get the value of an attribute if it is present, otherwise - set and return a default value. Like :meth:`dict.setdefault`. - - :param name: Name of attribute to get. - :param default: Value to set and return if the attribute is not - present. - - .. versionadded:: 0.11 - """ - return self.__dict__.setdefault(name, default) - - def __contains__(self, item: str) -> bool: - return item in self.__dict__ - - def __iter__(self) -> t.Iterator[str]: - return iter(self.__dict__) - - def __repr__(self) -> str: - top = _app_ctx_stack.top - if top is not None: - return f"" - return object.__repr__(self) - - -def after_this_request(f: AfterRequestCallable) -> AfterRequestCallable: - """Executes a function after this request. This is useful to modify - response objects. The function is passed the response object and has - to return the same or a new one. - - Example:: - - @app.route('/') - def index(): - @after_this_request - def add_header(response): - response.headers['X-Foo'] = 'Parachute' - return response - return 'Hello World!' - - This is more useful if a function other than the view function wants to - modify a response. For instance think of a decorator that wants to add - some headers without converting the return value into a response object. - - .. versionadded:: 0.9 - """ - top = _request_ctx_stack.top - - if top is None: - raise RuntimeError( - "This decorator can only be used when a request context is" - " active, such as within a view function." - ) - - top._after_request_functions.append(f) - return f - - -def copy_current_request_context(f: t.Callable) -> t.Callable: - """A helper function that decorates a function to retain the current - request context. This is useful when working with greenlets. The moment - the function is decorated a copy of the request context is created and - then pushed when the function is called. The current session is also - included in the copied request context. - - Example:: - - import gevent - from flask import copy_current_request_context - - @app.route('/') - def index(): - @copy_current_request_context - def do_some_work(): - # do some work here, it can access flask.request or - # flask.session like you would otherwise in the view function. - ... - gevent.spawn(do_some_work) - return 'Regular response' - - .. versionadded:: 0.10 - """ - top = _request_ctx_stack.top - - if top is None: - raise RuntimeError( - "This decorator can only be used when a request context is" - " active, such as within a view function." - ) - - reqctx = top.copy() - - def wrapper(*args, **kwargs): - with reqctx: - return f(*args, **kwargs) - - return update_wrapper(wrapper, f) - - -def has_request_context() -> bool: - """If you have code that wants to test if a request context is there or - not this function can be used. For instance, you may want to take advantage - of request information if the request object is available, but fail - silently if it is unavailable. - - :: - - class User(db.Model): - - def __init__(self, username, remote_addr=None): - self.username = username - if remote_addr is None and has_request_context(): - remote_addr = request.remote_addr - self.remote_addr = remote_addr - - Alternatively you can also just test any of the context bound objects - (such as :class:`request` or :class:`g`) for truthness:: - - class User(db.Model): - - def __init__(self, username, remote_addr=None): - self.username = username - if remote_addr is None and request: - remote_addr = request.remote_addr - self.remote_addr = remote_addr - - .. versionadded:: 0.7 - """ - return _request_ctx_stack.top is not None - - -def has_app_context() -> bool: - """Works like :func:`has_request_context` but for the application - context. You can also just do a boolean check on the - :data:`current_app` object instead. - - .. versionadded:: 0.9 - """ - return _app_ctx_stack.top is not None - - -class AppContext: - """The application context binds an application object implicitly - to the current thread or greenlet, similar to how the - :class:`RequestContext` binds request information. The application - context is also implicitly created if a request context is created - but the application is not on top of the individual application - context. - """ - - def __init__(self, app: "Flask") -> None: - self.app = app - self.url_adapter = app.create_url_adapter(None) - self.g = app.app_ctx_globals_class() - - # Like request context, app contexts can be pushed multiple times - # but there a basic "refcount" is enough to track them. - self._refcnt = 0 - - def push(self) -> None: - """Binds the app context to the current context.""" - self._refcnt += 1 - _app_ctx_stack.push(self) - appcontext_pushed.send(self.app) - - def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore - """Pops the app context.""" - try: - self._refcnt -= 1 - if self._refcnt <= 0: - if exc is _sentinel: - exc = sys.exc_info()[1] - self.app.do_teardown_appcontext(exc) - finally: - rv = _app_ctx_stack.pop() - assert rv is self, f"Popped wrong app context. ({rv!r} instead of {self!r})" - appcontext_popped.send(self.app) - - def __enter__(self) -> "AppContext": - self.push() - return self - - def __exit__( - self, exc_type: type, exc_value: BaseException, tb: TracebackType - ) -> None: - self.pop(exc_value) - - -class RequestContext: - """The request context contains all request relevant information. It is - created at the beginning of the request and pushed to the - `_request_ctx_stack` and removed at the end of it. It will create the - URL adapter and request object for the WSGI environment provided. - - Do not attempt to use this class directly, instead use - :meth:`~flask.Flask.test_request_context` and - :meth:`~flask.Flask.request_context` to create this object. - - When the request context is popped, it will evaluate all the - functions registered on the application for teardown execution - (:meth:`~flask.Flask.teardown_request`). - - The request context is automatically popped at the end of the request - for you. In debug mode the request context is kept around if - exceptions happen so that interactive debuggers have a chance to - introspect the data. With 0.4 this can also be forced for requests - that did not fail and outside of ``DEBUG`` mode. By setting - ``'flask._preserve_context'`` to ``True`` on the WSGI environment the - context will not pop itself at the end of the request. This is used by - the :meth:`~flask.Flask.test_client` for example to implement the - deferred cleanup functionality. - - You might find this helpful for unittests where you need the - information from the context local around for a little longer. Make - sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in - that situation, otherwise your unittests will leak memory. - """ - - def __init__( - self, - app: "Flask", - environ: dict, - request: t.Optional["Request"] = None, - session: t.Optional["SessionMixin"] = None, - ) -> None: - self.app = app - if request is None: - request = app.request_class(environ) - self.request = request - self.url_adapter = None - try: - self.url_adapter = app.create_url_adapter(self.request) - except HTTPException as e: - self.request.routing_exception = e - self.flashes = None - self.session = session - - # Request contexts can be pushed multiple times and interleaved with - # other request contexts. Now only if the last level is popped we - # get rid of them. Additionally if an application context is missing - # one is created implicitly so for each level we add this information - self._implicit_app_ctx_stack: t.List[t.Optional["AppContext"]] = [] - - # indicator if the context was preserved. Next time another context - # is pushed the preserved context is popped. - self.preserved = False - - # remembers the exception for pop if there is one in case the context - # preservation kicks in. - self._preserved_exc = None - - # Functions that should be executed after the request on the response - # object. These will be called before the regular "after_request" - # functions. - self._after_request_functions: t.List[AfterRequestCallable] = [] - - @property - def g(self) -> AppContext: - return _app_ctx_stack.top.g - - @g.setter - def g(self, value: AppContext) -> None: - _app_ctx_stack.top.g = value - - def copy(self) -> "RequestContext": - """Creates a copy of this request context with the same request object. - This can be used to move a request context to a different greenlet. - Because the actual request object is the same this cannot be used to - move a request context to a different thread unless access to the - request object is locked. - - .. versionadded:: 0.10 - - .. versionchanged:: 1.1 - The current session object is used instead of reloading the original - data. This prevents `flask.session` pointing to an out-of-date object. - """ - return self.__class__( - self.app, - environ=self.request.environ, - request=self.request, - session=self.session, - ) - - def match_request(self) -> None: - """Can be overridden by a subclass to hook into the matching - of the request. - """ - try: - result = self.url_adapter.match(return_rule=True) # type: ignore - self.request.url_rule, self.request.view_args = result # type: ignore - except HTTPException as e: - self.request.routing_exception = e - - def push(self) -> None: - """Binds the request context to the current context.""" - # If an exception occurs in debug mode or if context preservation is - # activated under exception situations exactly one context stays - # on the stack. The rationale is that you want to access that - # information under debug situations. However if someone forgets to - # pop that context again we want to make sure that on the next push - # it's invalidated, otherwise we run at risk that something leaks - # memory. This is usually only a problem in test suite since this - # functionality is not active in production environments. - top = _request_ctx_stack.top - if top is not None and top.preserved: - top.pop(top._preserved_exc) - - # Before we push the request context we have to ensure that there - # is an application context. - app_ctx = _app_ctx_stack.top - if app_ctx is None or app_ctx.app != self.app: - app_ctx = self.app.app_context() - app_ctx.push() - self._implicit_app_ctx_stack.append(app_ctx) - else: - self._implicit_app_ctx_stack.append(None) - - _request_ctx_stack.push(self) - - # Open the session at the moment that the request context is available. - # This allows a custom open_session method to use the request context. - # Only open a new session if this is the first time the request was - # pushed, otherwise stream_with_context loses the session. - if self.session is None: - session_interface = self.app.session_interface - self.session = session_interface.open_session(self.app, self.request) - - if self.session is None: - self.session = session_interface.make_null_session(self.app) - - # Match the request URL after loading the session, so that the - # session is available in custom URL converters. - if self.url_adapter is not None: - self.match_request() - - def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore - """Pops the request context and unbinds it by doing that. This will - also trigger the execution of functions registered by the - :meth:`~flask.Flask.teardown_request` decorator. - - .. versionchanged:: 0.9 - Added the `exc` argument. - """ - app_ctx = self._implicit_app_ctx_stack.pop() - clear_request = False - - try: - if not self._implicit_app_ctx_stack: - self.preserved = False - self._preserved_exc = None - if exc is _sentinel: - exc = sys.exc_info()[1] - self.app.do_teardown_request(exc) - - request_close = getattr(self.request, "close", None) - if request_close is not None: - request_close() - clear_request = True - finally: - rv = _request_ctx_stack.pop() - - # get rid of circular dependencies at the end of the request - # so that we don't require the GC to be active. - if clear_request: - rv.request.environ["werkzeug.request"] = None - - # Get rid of the app as well if necessary. - if app_ctx is not None: - app_ctx.pop(exc) - - assert ( - rv is self - ), f"Popped wrong request context. ({rv!r} instead of {self!r})" - - def auto_pop(self, exc: t.Optional[BaseException]) -> None: - if self.request.environ.get("flask._preserve_context") or ( - exc is not None and self.app.preserve_context_on_exception - ): - self.preserved = True - self._preserved_exc = exc # type: ignore - else: - self.pop(exc) - - def __enter__(self) -> "RequestContext": - self.push() - return self - - def __exit__( - self, exc_type: type, exc_value: BaseException, tb: TracebackType - ) -> None: - # do not pop the request stack if we are in debug mode and an - # exception happened. This will allow the debugger to still - # access the request object in the interactive shell. Furthermore - # the context can be force kept alive for the test client. - # See flask.testing for how this works. - self.auto_pop(exc_value) - - def __repr__(self) -> str: - return ( - f"<{type(self).__name__} {self.request.url!r}" - f" [{self.request.method}] of {self.app.name}>" - ) diff --git a/laplas/tools/abstract_map/flask/debughelpers.py b/laplas/tools/abstract_map/flask/debughelpers.py deleted file mode 100644 index 212f7d7..0000000 --- a/laplas/tools/abstract_map/flask/debughelpers.py +++ /dev/null @@ -1,172 +0,0 @@ -import os -import typing as t -from warnings import warn - -from .app import Flask -from .blueprints import Blueprint -from .globals import _request_ctx_stack - - -class UnexpectedUnicodeError(AssertionError, UnicodeError): - """Raised in places where we want some better error reporting for - unexpected unicode or binary data. - """ - - -class DebugFilesKeyError(KeyError, AssertionError): - """Raised from request.files during debugging. The idea is that it can - provide a better error message than just a generic KeyError/BadRequest. - """ - - def __init__(self, request, key): - form_matches = request.form.getlist(key) - buf = [ - f"You tried to access the file {key!r} in the request.files" - " dictionary but it does not exist. The mimetype for the" - f" request is {request.mimetype!r} instead of" - " 'multipart/form-data' which means that no file contents" - " were transmitted. To fix this error you should provide" - ' enctype="multipart/form-data" in your form.' - ] - if form_matches: - names = ", ".join(repr(x) for x in form_matches) - buf.append( - "\n\nThe browser instead transmitted some file names. " - f"This was submitted: {names}" - ) - self.msg = "".join(buf) - - def __str__(self): - return self.msg - - -class FormDataRoutingRedirect(AssertionError): - """This exception is raised by Flask in debug mode if it detects a - redirect caused by the routing system when the request method is not - GET, HEAD or OPTIONS. Reasoning: form data will be dropped. - """ - - def __init__(self, request): - exc = request.routing_exception - buf = [ - f"A request was sent to this URL ({request.url}) but a" - " redirect was issued automatically by the routing system" - f" to {exc.new_url!r}." - ] - - # In case just a slash was appended we can be extra helpful - if f"{request.base_url}/" == exc.new_url.split("?")[0]: - buf.append( - " The URL was defined with a trailing slash so Flask" - " will automatically redirect to the URL with the" - " trailing slash if it was accessed without one." - ) - - buf.append( - " Make sure to directly send your" - f" {request.method}-request to this URL since we can't make" - " browsers or HTTP clients redirect with form data reliably" - " or without user interaction." - ) - buf.append("\n\nNote: this exception is only raised in debug mode") - AssertionError.__init__(self, "".join(buf).encode("utf-8")) - - -def attach_enctype_error_multidict(request): - """Since Flask 0.8 we're monkeypatching the files object in case a - request is detected that does not use multipart form data but the files - object is accessed. - """ - oldcls = request.files.__class__ - - class newcls(oldcls): - def __getitem__(self, key): - try: - return oldcls.__getitem__(self, key) - except KeyError as e: - if key not in request.form: - raise - - raise DebugFilesKeyError(request, key) from e - - newcls.__name__ = oldcls.__name__ - newcls.__module__ = oldcls.__module__ - request.files.__class__ = newcls - - -def _dump_loader_info(loader) -> t.Generator: - yield f"class: {type(loader).__module__}.{type(loader).__name__}" - for key, value in sorted(loader.__dict__.items()): - if key.startswith("_"): - continue - if isinstance(value, (tuple, list)): - if not all(isinstance(x, str) for x in value): - continue - yield f"{key}:" - for item in value: - yield f" - {item}" - continue - elif not isinstance(value, (str, int, float, bool)): - continue - yield f"{key}: {value!r}" - - -def explain_template_loading_attempts(app: Flask, template, attempts) -> None: - """This should help developers understand what failed""" - info = [f"Locating template {template!r}:"] - total_found = 0 - blueprint = None - reqctx = _request_ctx_stack.top - if reqctx is not None and reqctx.request.blueprint is not None: - blueprint = reqctx.request.blueprint - - for idx, (loader, srcobj, triple) in enumerate(attempts): - if isinstance(srcobj, Flask): - src_info = f"application {srcobj.import_name!r}" - elif isinstance(srcobj, Blueprint): - src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" - else: - src_info = repr(srcobj) - - info.append(f"{idx + 1:5}: trying loader of {src_info}") - - for line in _dump_loader_info(loader): - info.append(f" {line}") - - if triple is None: - detail = "no match" - else: - detail = f"found ({triple[1] or ''!r})" - total_found += 1 - info.append(f" -> {detail}") - - seems_fishy = False - if total_found == 0: - info.append("Error: the template could not be found.") - seems_fishy = True - elif total_found > 1: - info.append("Warning: multiple loaders returned a match for the template.") - seems_fishy = True - - if blueprint is not None and seems_fishy: - info.append( - " The template was looked up from an endpoint that belongs" - f" to the blueprint {blueprint!r}." - ) - info.append(" Maybe you did not place a template in the right folder?") - info.append(" See https://flask.palletsprojects.com/blueprints/#templates") - - app.logger.info("\n".join(info)) - - -def explain_ignored_app_run() -> None: - if os.environ.get("WERKZEUG_RUN_MAIN") != "true": - warn( - Warning( - "Silently ignoring app.run() because the application is" - " run from the flask command line executable. Consider" - ' putting app.run() behind an if __name__ == "__main__"' - " guard to silence this warning." - ), - stacklevel=3, - ) diff --git a/laplas/tools/abstract_map/flask/globals.py b/laplas/tools/abstract_map/flask/globals.py deleted file mode 100644 index 6d91c75..0000000 --- a/laplas/tools/abstract_map/flask/globals.py +++ /dev/null @@ -1,59 +0,0 @@ -import typing as t -from functools import partial - -from werkzeug.local import LocalProxy -from werkzeug.local import LocalStack - -if t.TYPE_CHECKING: - from .app import Flask - from .ctx import _AppCtxGlobals - from .sessions import SessionMixin - from .wrappers import Request - -_request_ctx_err_msg = """\ -Working outside of request context. - -This typically means that you attempted to use functionality that needed -an active HTTP request. Consult the documentation on testing for -information about how to avoid this problem.\ -""" -_app_ctx_err_msg = """\ -Working outside of application context. - -This typically means that you attempted to use functionality that needed -to interface with the current application object in some way. To solve -this, set up an application context with app.app_context(). See the -documentation for more information.\ -""" - - -def _lookup_req_object(name): - top = _request_ctx_stack.top - if top is None: - raise RuntimeError(_request_ctx_err_msg) - return getattr(top, name) - - -def _lookup_app_object(name): - top = _app_ctx_stack.top - if top is None: - raise RuntimeError(_app_ctx_err_msg) - return getattr(top, name) - - -def _find_app(): - top = _app_ctx_stack.top - if top is None: - raise RuntimeError(_app_ctx_err_msg) - return top.app - - -# context locals -_request_ctx_stack = LocalStack() -_app_ctx_stack = LocalStack() -current_app: "Flask" = LocalProxy(_find_app) # type: ignore -request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore -session: "SessionMixin" = LocalProxy( # type: ignore - partial(_lookup_req_object, "session") -) -g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore diff --git a/laplas/tools/abstract_map/flask/helpers.py b/laplas/tools/abstract_map/flask/helpers.py deleted file mode 100644 index 4359780..0000000 --- a/laplas/tools/abstract_map/flask/helpers.py +++ /dev/null @@ -1,836 +0,0 @@ -import os -import pkgutil -import socket -import sys -import typing as t -import warnings -from datetime import datetime -from datetime import timedelta -from functools import lru_cache -from functools import update_wrapper -from threading import RLock - -import werkzeug.utils -from werkzeug.exceptions import NotFound -from werkzeug.routing import BuildError -from werkzeug.urls import url_quote - -from .globals import _app_ctx_stack -from .globals import _request_ctx_stack -from .globals import current_app -from .globals import request -from .globals import session -from .signals import message_flashed - -if t.TYPE_CHECKING: - from .wrappers import Response - - -def get_env() -> str: - """Get the environment the app is running in, indicated by the - :envvar:`FLASK_ENV` environment variable. The default is - ``'production'``. - """ - return os.environ.get("FLASK_ENV") or "production" - - -def get_debug_flag() -> bool: - """Get whether debug mode should be enabled for the app, indicated - by the :envvar:`FLASK_DEBUG` environment variable. The default is - ``True`` if :func:`.get_env` returns ``'development'``, or ``False`` - otherwise. - """ - val = os.environ.get("FLASK_DEBUG") - - if not val: - return get_env() == "development" - - return val.lower() not in ("0", "false", "no") - - -def get_load_dotenv(default: bool = True) -> bool: - """Get whether the user has disabled loading dotenv files by setting - :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load the - files. - - :param default: What to return if the env var isn't set. - """ - val = os.environ.get("FLASK_SKIP_DOTENV") - - if not val: - return default - - return val.lower() in ("0", "false", "no") - - -def stream_with_context( - generator_or_function: t.Union[ - t.Iterator[t.AnyStr], t.Callable[..., t.Iterator[t.AnyStr]] - ] -) -> t.Iterator[t.AnyStr]: - """Request contexts disappear when the response is started on the server. - This is done for efficiency reasons and to make it less likely to encounter - memory leaks with badly written WSGI middlewares. The downside is that if - you are using streamed responses, the generator cannot access request bound - information any more. - - This function however can help you keep the context around for longer:: - - from flask import stream_with_context, request, Response - - @app.route('/stream') - def streamed_response(): - @stream_with_context - def generate(): - yield 'Hello ' - yield request.args['name'] - yield '!' - return Response(generate()) - - Alternatively it can also be used around a specific generator:: - - from flask import stream_with_context, request, Response - - @app.route('/stream') - def streamed_response(): - def generate(): - yield 'Hello ' - yield request.args['name'] - yield '!' - return Response(stream_with_context(generate())) - - .. versionadded:: 0.9 - """ - try: - gen = iter(generator_or_function) # type: ignore - except TypeError: - - def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: - gen = generator_or_function(*args, **kwargs) # type: ignore - return stream_with_context(gen) - - return update_wrapper(decorator, generator_or_function) # type: ignore - - def generator() -> t.Generator: - ctx = _request_ctx_stack.top - if ctx is None: - raise RuntimeError( - "Attempted to stream with context but " - "there was no context in the first place to keep around." - ) - with ctx: - # Dummy sentinel. Has to be inside the context block or we're - # not actually keeping the context around. - yield None - - # The try/finally is here so that if someone passes a WSGI level - # iterator in we're still running the cleanup logic. Generators - # don't need that because they are closed on their destruction - # automatically. - try: - yield from gen - finally: - if hasattr(gen, "close"): - gen.close() # type: ignore - - # The trick is to start the generator. Then the code execution runs until - # the first dummy None is yielded at which point the context was already - # pushed. This item is discarded. Then when the iteration continues the - # real generator is executed. - wrapped_g = generator() - next(wrapped_g) - return wrapped_g - - -def make_response(*args: t.Any) -> "Response": - """Sometimes it is necessary to set additional headers in a view. Because - views do not have to return response objects but can return a value that - is converted into a response object by Flask itself, it becomes tricky to - add headers to it. This function can be called instead of using a return - and you will get a response object which you can use to attach headers. - - If view looked like this and you want to add a new header:: - - def index(): - return render_template('index.html', foo=42) - - You can now do something like this:: - - def index(): - response = make_response(render_template('index.html', foo=42)) - response.headers['X-Parachutes'] = 'parachutes are cool' - return response - - This function accepts the very same arguments you can return from a - view function. This for example creates a response with a 404 error - code:: - - response = make_response(render_template('not_found.html'), 404) - - The other use case of this function is to force the return value of a - view function into a response which is helpful with view - decorators:: - - response = make_response(view_function()) - response.headers['X-Parachutes'] = 'parachutes are cool' - - Internally this function does the following things: - - - if no arguments are passed, it creates a new response argument - - if one argument is passed, :meth:`flask.Flask.make_response` - is invoked with it. - - if more than one argument is passed, the arguments are passed - to the :meth:`flask.Flask.make_response` function as tuple. - - .. versionadded:: 0.6 - """ - if not args: - return current_app.response_class() - if len(args) == 1: - args = args[0] - return current_app.make_response(args) - - -def url_for(endpoint: str, **values: t.Any) -> str: - """Generates a URL to the given endpoint with the method provided. - - Variable arguments that are unknown to the target endpoint are appended - to the generated URL as query arguments. If the value of a query argument - is ``None``, the whole pair is skipped. In case blueprints are active - you can shortcut references to the same blueprint by prefixing the - local endpoint with a dot (``.``). - - This will reference the index function local to the current blueprint:: - - url_for('.index') - - See :ref:`url-building`. - - Configuration values ``APPLICATION_ROOT`` and ``SERVER_NAME`` are only used when - generating URLs outside of a request context. - - To integrate applications, :class:`Flask` has a hook to intercept URL build - errors through :attr:`Flask.url_build_error_handlers`. The `url_for` - function results in a :exc:`~werkzeug.routing.BuildError` when the current - app does not have a URL for the given endpoint and values. When it does, the - :data:`~flask.current_app` calls its :attr:`~Flask.url_build_error_handlers` if - it is not ``None``, which can return a string to use as the result of - `url_for` (instead of `url_for`'s default to raise the - :exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception. - An example:: - - def external_url_handler(error, endpoint, values): - "Looks up an external URL when `url_for` cannot build a URL." - # This is an example of hooking the build_error_handler. - # Here, lookup_url is some utility function you've built - # which looks up the endpoint in some external URL registry. - url = lookup_url(endpoint, **values) - if url is None: - # External lookup did not have a URL. - # Re-raise the BuildError, in context of original traceback. - exc_type, exc_value, tb = sys.exc_info() - if exc_value is error: - raise exc_type(exc_value).with_traceback(tb) - else: - raise error - # url_for will use this result, instead of raising BuildError. - return url - - app.url_build_error_handlers.append(external_url_handler) - - Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and - `endpoint` and `values` are the arguments passed into `url_for`. Note - that this is for building URLs outside the current application, and not for - handling 404 NotFound errors. - - .. versionadded:: 0.10 - The `_scheme` parameter was added. - - .. versionadded:: 0.9 - The `_anchor` and `_method` parameters were added. - - .. versionadded:: 0.9 - Calls :meth:`Flask.handle_build_error` on - :exc:`~werkzeug.routing.BuildError`. - - :param endpoint: the endpoint of the URL (name of the function) - :param values: the variable arguments of the URL rule - :param _external: if set to ``True``, an absolute URL is generated. Server - address can be changed via ``SERVER_NAME`` configuration variable which - falls back to the `Host` header, then to the IP and port of the request. - :param _scheme: a string specifying the desired URL scheme. The `_external` - parameter must be set to ``True`` or a :exc:`ValueError` is raised. The default - behavior uses the same scheme as the current request, or - :data:`PREFERRED_URL_SCHEME` if no request context is available. - This also can be set to an empty string to build protocol-relative - URLs. - :param _anchor: if provided this is added as anchor to the URL. - :param _method: if provided this explicitly specifies an HTTP method. - """ - appctx = _app_ctx_stack.top - reqctx = _request_ctx_stack.top - - if appctx is None: - raise RuntimeError( - "Attempted to generate a URL without the application context being" - " pushed. This has to be executed when application context is" - " available." - ) - - # If request specific information is available we have some extra - # features that support "relative" URLs. - if reqctx is not None: - url_adapter = reqctx.url_adapter - blueprint_name = request.blueprint - - if endpoint[:1] == ".": - if blueprint_name is not None: - endpoint = f"{blueprint_name}{endpoint}" - else: - endpoint = endpoint[1:] - - external = values.pop("_external", False) - - # Otherwise go with the url adapter from the appctx and make - # the URLs external by default. - else: - url_adapter = appctx.url_adapter - - if url_adapter is None: - raise RuntimeError( - "Application was not able to create a URL adapter for request" - " independent URL generation. You might be able to fix this by" - " setting the SERVER_NAME config variable." - ) - - external = values.pop("_external", True) - - anchor = values.pop("_anchor", None) - method = values.pop("_method", None) - scheme = values.pop("_scheme", None) - appctx.app.inject_url_defaults(endpoint, values) - - # This is not the best way to deal with this but currently the - # underlying Werkzeug router does not support overriding the scheme on - # a per build call basis. - old_scheme = None - if scheme is not None: - if not external: - raise ValueError("When specifying _scheme, _external must be True") - old_scheme = url_adapter.url_scheme - url_adapter.url_scheme = scheme - - try: - try: - rv = url_adapter.build( - endpoint, values, method=method, force_external=external - ) - finally: - if old_scheme is not None: - url_adapter.url_scheme = old_scheme - except BuildError as error: - # We need to inject the values again so that the app callback can - # deal with that sort of stuff. - values["_external"] = external - values["_anchor"] = anchor - values["_method"] = method - values["_scheme"] = scheme - return appctx.app.handle_url_build_error(error, endpoint, values) - - if anchor is not None: - rv += f"#{url_quote(anchor)}" - return rv - - -def get_template_attribute(template_name: str, attribute: str) -> t.Any: - """Loads a macro (or variable) a template exports. This can be used to - invoke a macro from within Python code. If you for example have a - template named :file:`_cider.html` with the following contents: - - .. sourcecode:: html+jinja - - {% macro hello(name) %}Hello {{ name }}!{% endmacro %} - - You can access this from Python code like this:: - - hello = get_template_attribute('_cider.html', 'hello') - return hello('World') - - .. versionadded:: 0.2 - - :param template_name: the name of the template - :param attribute: the name of the variable of macro to access - """ - return getattr(current_app.jinja_env.get_template(template_name).module, attribute) - - -def flash(message: str, category: str = "message") -> None: - """Flashes a message to the next request. In order to remove the - flashed message from the session and to display it to the user, - the template has to call :func:`get_flashed_messages`. - - .. versionchanged:: 0.3 - `category` parameter added. - - :param message: the message to be flashed. - :param category: the category for the message. The following values - are recommended: ``'message'`` for any kind of message, - ``'error'`` for errors, ``'info'`` for information - messages and ``'warning'`` for warnings. However any - kind of string can be used as category. - """ - # Original implementation: - # - # session.setdefault('_flashes', []).append((category, message)) - # - # This assumed that changes made to mutable structures in the session are - # always in sync with the session object, which is not true for session - # implementations that use external storage for keeping their keys/values. - flashes = session.get("_flashes", []) - flashes.append((category, message)) - session["_flashes"] = flashes - message_flashed.send( - current_app._get_current_object(), # type: ignore - message=message, - category=category, - ) - - -def get_flashed_messages( - with_categories: bool = False, category_filter: t.Iterable[str] = () -) -> t.Union[t.List[str], t.List[t.Tuple[str, str]]]: - """Pulls all flashed messages from the session and returns them. - Further calls in the same request to the function will return - the same messages. By default just the messages are returned, - but when `with_categories` is set to ``True``, the return value will - be a list of tuples in the form ``(category, message)`` instead. - - Filter the flashed messages to one or more categories by providing those - categories in `category_filter`. This allows rendering categories in - separate html blocks. The `with_categories` and `category_filter` - arguments are distinct: - - * `with_categories` controls whether categories are returned with message - text (``True`` gives a tuple, where ``False`` gives just the message text). - * `category_filter` filters the messages down to only those matching the - provided categories. - - See :doc:`/patterns/flashing` for examples. - - .. versionchanged:: 0.3 - `with_categories` parameter added. - - .. versionchanged:: 0.9 - `category_filter` parameter added. - - :param with_categories: set to ``True`` to also receive categories. - :param category_filter: filter of categories to limit return values. Only - categories in the list will be returned. - """ - flashes = _request_ctx_stack.top.flashes - if flashes is None: - _request_ctx_stack.top.flashes = flashes = ( - session.pop("_flashes") if "_flashes" in session else [] - ) - if category_filter: - flashes = list(filter(lambda f: f[0] in category_filter, flashes)) - if not with_categories: - return [x[1] for x in flashes] - return flashes - - -def _prepare_send_file_kwargs( - download_name: t.Optional[str] = None, - attachment_filename: t.Optional[str] = None, - etag: t.Optional[t.Union[bool, str]] = None, - add_etags: t.Optional[t.Union[bool]] = None, - max_age: t.Optional[ - t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] - ] = None, - cache_timeout: t.Optional[int] = None, - **kwargs: t.Any, -) -> t.Dict[str, t.Any]: - if attachment_filename is not None: - warnings.warn( - "The 'attachment_filename' parameter has been renamed to" - " 'download_name'. The old name will be removed in Flask" - " 2.1.", - DeprecationWarning, - stacklevel=3, - ) - download_name = attachment_filename - - if cache_timeout is not None: - warnings.warn( - "The 'cache_timeout' parameter has been renamed to" - " 'max_age'. The old name will be removed in Flask 2.1.", - DeprecationWarning, - stacklevel=3, - ) - max_age = cache_timeout - - if add_etags is not None: - warnings.warn( - "The 'add_etags' parameter has been renamed to 'etag'. The" - " old name will be removed in Flask 2.1.", - DeprecationWarning, - stacklevel=3, - ) - etag = add_etags - - if max_age is None: - max_age = current_app.get_send_file_max_age - - kwargs.update( - environ=request.environ, - download_name=download_name, - etag=etag, - max_age=max_age, - use_x_sendfile=current_app.use_x_sendfile, - response_class=current_app.response_class, - _root_path=current_app.root_path, # type: ignore - ) - return kwargs - - -def send_file( - path_or_file: t.Union[os.PathLike, str, t.BinaryIO], - mimetype: t.Optional[str] = None, - as_attachment: bool = False, - download_name: t.Optional[str] = None, - attachment_filename: t.Optional[str] = None, - conditional: bool = True, - etag: t.Union[bool, str] = True, - add_etags: t.Optional[bool] = None, - last_modified: t.Optional[t.Union[datetime, int, float]] = None, - max_age: t.Optional[ - t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] - ] = None, - cache_timeout: t.Optional[int] = None, -): - """Send the contents of a file to the client. - - The first argument can be a file path or a file-like object. Paths - are preferred in most cases because Werkzeug can manage the file and - get extra information from the path. Passing a file-like object - requires that the file is opened in binary mode, and is mostly - useful when building a file in memory with :class:`io.BytesIO`. - - Never pass file paths provided by a user. The path is assumed to be - trusted, so a user could craft a path to access a file you didn't - intend. Use :func:`send_from_directory` to safely serve - user-requested paths from within a directory. - - If the WSGI server sets a ``file_wrapper`` in ``environ``, it is - used, otherwise Werkzeug's built-in wrapper is used. Alternatively, - if the HTTP server supports ``X-Sendfile``, configuring Flask with - ``USE_X_SENDFILE = True`` will tell the server to send the given - path, which is much more efficient than reading it in Python. - - :param path_or_file: The path to the file to send, relative to the - current working directory if a relative path is given. - Alternatively, a file-like object opened in binary mode. Make - sure the file pointer is seeked to the start of the data. - :param mimetype: The MIME type to send for the file. If not - provided, it will try to detect it from the file name. - :param as_attachment: Indicate to a browser that it should offer to - save the file instead of displaying it. - :param download_name: The default name browsers will use when saving - the file. Defaults to the passed file name. - :param conditional: Enable conditional and range responses based on - request headers. Requires passing a file path and ``environ``. - :param etag: Calculate an ETag for the file, which requires passing - a file path. Can also be a string to use instead. - :param last_modified: The last modified time to send for the file, - in seconds. If not provided, it will try to detect it from the - file path. - :param max_age: How long the client should cache the file, in - seconds. If set, ``Cache-Control`` will be ``public``, otherwise - it will be ``no-cache`` to prefer conditional caching. - - .. versionchanged:: 2.0 - ``download_name`` replaces the ``attachment_filename`` - parameter. If ``as_attachment=False``, it is passed with - ``Content-Disposition: inline`` instead. - - .. versionchanged:: 2.0 - ``max_age`` replaces the ``cache_timeout`` parameter. - ``conditional`` is enabled and ``max_age`` is not set by - default. - - .. versionchanged:: 2.0 - ``etag`` replaces the ``add_etags`` parameter. It can be a - string to use instead of generating one. - - .. versionchanged:: 2.0 - Passing a file-like object that inherits from - :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather - than sending an empty file. - - .. versionadded:: 2.0 - Moved the implementation to Werkzeug. This is now a wrapper to - pass some Flask-specific arguments. - - .. versionchanged:: 1.1 - ``filename`` may be a :class:`~os.PathLike` object. - - .. versionchanged:: 1.1 - Passing a :class:`~io.BytesIO` object supports range requests. - - .. versionchanged:: 1.0.3 - Filenames are encoded with ASCII instead of Latin-1 for broader - compatibility with WSGI servers. - - .. versionchanged:: 1.0 - UTF-8 filenames as specified in :rfc:`2231` are supported. - - .. versionchanged:: 0.12 - The filename is no longer automatically inferred from file - objects. If you want to use automatic MIME and etag support, - pass a filename via ``filename_or_fp`` or - ``attachment_filename``. - - .. versionchanged:: 0.12 - ``attachment_filename`` is preferred over ``filename`` for MIME - detection. - - .. versionchanged:: 0.9 - ``cache_timeout`` defaults to - :meth:`Flask.get_send_file_max_age`. - - .. versionchanged:: 0.7 - MIME guessing and etag support for file-like objects was - deprecated because it was unreliable. Pass a filename if you are - able to, otherwise attach an etag yourself. - - .. versionchanged:: 0.5 - The ``add_etags``, ``cache_timeout`` and ``conditional`` - parameters were added. The default behavior is to add etags. - - .. versionadded:: 0.2 - """ - return werkzeug.utils.send_file( - **_prepare_send_file_kwargs( - path_or_file=path_or_file, - environ=request.environ, - mimetype=mimetype, - as_attachment=as_attachment, - download_name=download_name, - attachment_filename=attachment_filename, - conditional=conditional, - etag=etag, - add_etags=add_etags, - last_modified=last_modified, - max_age=max_age, - cache_timeout=cache_timeout, - ) - ) - - -def safe_join(directory: str, *pathnames: str) -> str: - """Safely join zero or more untrusted path components to a base - directory to avoid escaping the base directory. - - :param directory: The trusted base directory. - :param pathnames: The untrusted path components relative to the - base directory. - :return: A safe path, otherwise ``None``. - """ - warnings.warn( - "'flask.helpers.safe_join' is deprecated and will be removed in" - " Flask 2.1. Use 'werkzeug.utils.safe_join' instead.", - DeprecationWarning, - stacklevel=2, - ) - path = werkzeug.utils.safe_join(directory, *pathnames) - - if path is None: - raise NotFound() - - return path - - -def send_from_directory( - directory: t.Union[os.PathLike, str], - path: t.Union[os.PathLike, str], - filename: t.Optional[str] = None, - **kwargs: t.Any, -) -> "Response": - """Send a file from within a directory using :func:`send_file`. - - .. code-block:: python - - @app.route("/uploads/") - def download_file(name): - return send_from_directory( - app.config['UPLOAD_FOLDER'], name, as_attachment=True - ) - - This is a secure way to serve files from a folder, such as static - files or uploads. Uses :func:`~werkzeug.security.safe_join` to - ensure the path coming from the client is not maliciously crafted to - point outside the specified directory. - - If the final path does not point to an existing regular file, - raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. - - :param directory: The directory that ``path`` must be located under. - :param path: The path to the file to send, relative to - ``directory``. - :param kwargs: Arguments to pass to :func:`send_file`. - - .. versionchanged:: 2.0 - ``path`` replaces the ``filename`` parameter. - - .. versionadded:: 2.0 - Moved the implementation to Werkzeug. This is now a wrapper to - pass some Flask-specific arguments. - - .. versionadded:: 0.5 - """ - if filename is not None: - warnings.warn( - "The 'filename' parameter has been renamed to 'path'. The" - " old name will be removed in Flask 2.1.", - DeprecationWarning, - stacklevel=2, - ) - path = filename - - return werkzeug.utils.send_from_directory( # type: ignore - directory, path, **_prepare_send_file_kwargs(**kwargs) - ) - - -def get_root_path(import_name: str) -> str: - """Find the root path of a package, or the path that contains a - module. If it cannot be found, returns the current working - directory. - - Not to be confused with the value returned by :func:`find_package`. - - :meta private: - """ - # Module already imported and has a file attribute. Use that first. - mod = sys.modules.get(import_name) - - if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: - return os.path.dirname(os.path.abspath(mod.__file__)) - - # Next attempt: check the loader. - loader = pkgutil.get_loader(import_name) - - # Loader does not exist or we're referring to an unloaded main - # module or a main module without path (interactive sessions), go - # with the current working directory. - if loader is None or import_name == "__main__": - return os.getcwd() - - if hasattr(loader, "get_filename"): - filepath = loader.get_filename(import_name) # type: ignore - else: - # Fall back to imports. - __import__(import_name) - mod = sys.modules[import_name] - filepath = getattr(mod, "__file__", None) - - # If we don't have a file path it might be because it is a - # namespace package. In this case pick the root path from the - # first module that is contained in the package. - if filepath is None: - raise RuntimeError( - "No root path can be found for the provided module" - f" {import_name!r}. This can happen because the module" - " came from an import hook that does not provide file" - " name information or because it's a namespace package." - " In this case the root path needs to be explicitly" - " provided." - ) - - # filepath is import_name.py for a module, or __init__.py for a package. - return os.path.dirname(os.path.abspath(filepath)) - - -class locked_cached_property(werkzeug.utils.cached_property): - """A :func:`property` that is only evaluated once. Like - :class:`werkzeug.utils.cached_property` except access uses a lock - for thread safety. - - .. versionchanged:: 2.0 - Inherits from Werkzeug's ``cached_property`` (and ``property``). - """ - - def __init__( - self, - fget: t.Callable[[t.Any], t.Any], - name: t.Optional[str] = None, - doc: t.Optional[str] = None, - ) -> None: - super().__init__(fget, name=name, doc=doc) - self.lock = RLock() - - def __get__(self, obj: object, type: type = None) -> t.Any: # type: ignore - if obj is None: - return self - - with self.lock: - return super().__get__(obj, type=type) - - def __set__(self, obj: object, value: t.Any) -> None: - with self.lock: - super().__set__(obj, value) - - def __delete__(self, obj: object) -> None: - with self.lock: - super().__delete__(obj) - - -def total_seconds(td: timedelta) -> int: - """Returns the total seconds from a timedelta object. - - :param timedelta td: the timedelta to be converted in seconds - - :returns: number of seconds - :rtype: int - - .. deprecated:: 2.0 - Will be removed in Flask 2.1. Use - :meth:`timedelta.total_seconds` instead. - """ - warnings.warn( - "'total_seconds' is deprecated and will be removed in Flask" - " 2.1. Use 'timedelta.total_seconds' instead.", - DeprecationWarning, - stacklevel=2, - ) - return td.days * 60 * 60 * 24 + td.seconds - - -def is_ip(value: str) -> bool: - """Determine if the given string is an IP address. - - :param value: value to check - :type value: str - - :return: True if string is an IP address - :rtype: bool - """ - for family in (socket.AF_INET, socket.AF_INET6): - try: - socket.inet_pton(family, value) - except OSError: - pass - else: - return True - - return False - - -@lru_cache(maxsize=None) -def _split_blueprint_path(name: str) -> t.List[str]: - out: t.List[str] = [name] - - if "." in name: - out.extend(_split_blueprint_path(name.rpartition(".")[0])) - - return out diff --git a/laplas/tools/abstract_map/flask/json/__init__.py b/laplas/tools/abstract_map/flask/json/__init__.py deleted file mode 100644 index ccb9efb..0000000 --- a/laplas/tools/abstract_map/flask/json/__init__.py +++ /dev/null @@ -1,363 +0,0 @@ -import decimal -import io -import json as _json -import typing as t -import uuid -import warnings -from datetime import date - -from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps -from werkzeug.http import http_date - -from ..globals import current_app -from ..globals import request - -if t.TYPE_CHECKING: - from ..app import Flask - from ..wrappers import Response - -try: - import dataclasses -except ImportError: - # Python < 3.7 - dataclasses = None # type: ignore - - -class JSONEncoder(_json.JSONEncoder): - """The default JSON encoder. Handles extra types compared to the - built-in :class:`json.JSONEncoder`. - - - :class:`datetime.datetime` and :class:`datetime.date` are - serialized to :rfc:`822` strings. This is the same as the HTTP - date format. - - :class:`uuid.UUID` is serialized to a string. - - :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. - - :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - - Assign a subclass of this to :attr:`flask.Flask.json_encoder` or - :attr:`flask.Blueprint.json_encoder` to override the default. - """ - - def default(self, o: t.Any) -> t.Any: - """Convert ``o`` to a JSON serializable type. See - :meth:`json.JSONEncoder.default`. Python does not support - overriding how basic types like ``str`` or ``list`` are - serialized, they are handled before this method. - """ - if isinstance(o, date): - return http_date(o) - if isinstance(o, (decimal.Decimal, uuid.UUID)): - return str(o) - if dataclasses and dataclasses.is_dataclass(o): - return dataclasses.asdict(o) - if hasattr(o, "__html__"): - return str(o.__html__()) - return super().default(o) - - -class JSONDecoder(_json.JSONDecoder): - """The default JSON decoder. - - This does not change any behavior from the built-in - :class:`json.JSONDecoder`. - - Assign a subclass of this to :attr:`flask.Flask.json_decoder` or - :attr:`flask.Blueprint.json_decoder` to override the default. - """ - - -def _dump_arg_defaults( - kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None -) -> None: - """Inject default arguments for dump functions.""" - if app is None: - app = current_app - - if app: - cls = app.json_encoder - bp = app.blueprints.get(request.blueprint) if request else None # type: ignore - if bp is not None and bp.json_encoder is not None: - cls = bp.json_encoder - - # Only set a custom encoder if it has custom behavior. This is - # faster on PyPy. - if cls is not _json.JSONEncoder: - kwargs.setdefault("cls", cls) - - kwargs.setdefault("cls", cls) - kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"]) - kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"]) - else: - kwargs.setdefault("sort_keys", True) - kwargs.setdefault("cls", JSONEncoder) - - -def _load_arg_defaults( - kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None -) -> None: - """Inject default arguments for load functions.""" - if app is None: - app = current_app - - if app: - cls = app.json_decoder - bp = app.blueprints.get(request.blueprint) if request else None # type: ignore - if bp is not None and bp.json_decoder is not None: - cls = bp.json_decoder - - # Only set a custom decoder if it has custom behavior. This is - # faster on PyPy. - if cls not in {JSONDecoder, _json.JSONDecoder}: - kwargs.setdefault("cls", cls) - - -def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str: - """Serialize an object to a string of JSON. - - Takes the same arguments as the built-in :func:`json.dumps`, with - some defaults from application configuration. - - :param obj: Object to serialize to JSON. - :param app: Use this app's config instead of the active app context - or defaults. - :param kwargs: Extra arguments passed to :func:`json.dumps`. - - .. versionchanged:: 2.0.2 - :class:`decimal.Decimal` is supported by converting to a string. - - .. versionchanged:: 2.0 - ``encoding`` is deprecated and will be removed in Flask 2.1. - - .. versionchanged:: 1.0.3 - ``app`` can be passed directly, rather than requiring an app - context for configuration. - """ - _dump_arg_defaults(kwargs, app=app) - encoding = kwargs.pop("encoding", None) - rv = _json.dumps(obj, **kwargs) - - if encoding is not None: - warnings.warn( - "'encoding' is deprecated and will be removed in Flask 2.1.", - DeprecationWarning, - stacklevel=2, - ) - - if isinstance(rv, str): - return rv.encode(encoding) # type: ignore - - return rv - - -def dump( - obj: t.Any, fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any -) -> None: - """Serialize an object to JSON written to a file object. - - Takes the same arguments as the built-in :func:`json.dump`, with - some defaults from application configuration. - - :param obj: Object to serialize to JSON. - :param fp: File object to write JSON to. - :param app: Use this app's config instead of the active app context - or defaults. - :param kwargs: Extra arguments passed to :func:`json.dump`. - - .. versionchanged:: 2.0 - Writing to a binary file, and the ``encoding`` argument, is - deprecated and will be removed in Flask 2.1. - """ - _dump_arg_defaults(kwargs, app=app) - encoding = kwargs.pop("encoding", None) - show_warning = encoding is not None - - try: - fp.write("") - except TypeError: - show_warning = True - fp = io.TextIOWrapper(fp, encoding or "utf-8") # type: ignore - - if show_warning: - warnings.warn( - "Writing to a binary file, and the 'encoding' argument, is" - " deprecated and will be removed in Flask 2.1.", - DeprecationWarning, - stacklevel=2, - ) - - _json.dump(obj, fp, **kwargs) - - -def loads(s: str, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any: - """Deserialize an object from a string of JSON. - - Takes the same arguments as the built-in :func:`json.loads`, with - some defaults from application configuration. - - :param s: JSON string to deserialize. - :param app: Use this app's config instead of the active app context - or defaults. - :param kwargs: Extra arguments passed to :func:`json.loads`. - - .. versionchanged:: 2.0 - ``encoding`` is deprecated and will be removed in Flask 2.1. The - data must be a string or UTF-8 bytes. - - .. versionchanged:: 1.0.3 - ``app`` can be passed directly, rather than requiring an app - context for configuration. - """ - _load_arg_defaults(kwargs, app=app) - encoding = kwargs.pop("encoding", None) - - if encoding is not None: - warnings.warn( - "'encoding' is deprecated and will be removed in Flask 2.1." - " The data must be a string or UTF-8 bytes.", - DeprecationWarning, - stacklevel=2, - ) - - if isinstance(s, bytes): - s = s.decode(encoding) - - return _json.loads(s, **kwargs) - - -def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any: - """Deserialize an object from JSON read from a file object. - - Takes the same arguments as the built-in :func:`json.load`, with - some defaults from application configuration. - - :param fp: File object to read JSON from. - :param app: Use this app's config instead of the active app context - or defaults. - :param kwargs: Extra arguments passed to :func:`json.load`. - - .. versionchanged:: 2.0 - ``encoding`` is deprecated and will be removed in Flask 2.1. The - file must be text mode, or binary mode with UTF-8 bytes. - """ - _load_arg_defaults(kwargs, app=app) - encoding = kwargs.pop("encoding", None) - - if encoding is not None: - warnings.warn( - "'encoding' is deprecated and will be removed in Flask 2.1." - " The file must be text mode, or binary mode with UTF-8" - " bytes.", - DeprecationWarning, - stacklevel=2, - ) - - if isinstance(fp.read(0), bytes): - fp = io.TextIOWrapper(fp, encoding) # type: ignore - - return _json.load(fp, **kwargs) - - -def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str: - """Serialize an object to a string of JSON with :func:`dumps`, then - replace HTML-unsafe characters with Unicode escapes and mark the - result safe with :class:`~markupsafe.Markup`. - - This is available in templates as the ``|tojson`` filter. - - The returned string is safe to render in HTML documents and - ``