-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for pygame.math. #74
Conversation
@ajoubertza I created a pull request for this so we can see how builds are going and have a unified place to make comments. Thanks for working on this! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a bunch of small comments. Generally looking like a good start though!
pygame/mask.py
Outdated
@@ -1,6 +1,6 @@ | |||
# Implement the pygame API for the bitmask functions | |||
|
|||
from math import atan2, pi | |||
import math |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is intended to import from pygame.math
it should use an explicit relative import, otherwise we should probably for from __future__ import absolute_import
to avoid confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mask.py is intending to import the standard math module - this code was there before the local math module was added. Should I still add the absolute_import? That's default behaviour isn't it, or are we aiming to support Python < 2.7?
pygame/math.py
Outdated
|
||
def __getitem__(self, key): | ||
"""Provide indexed read access (vec[0] is x vec[1] is y).""" | ||
if isinstance(key, slice): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could implement this whole function as return [self.x, self.y][key]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, that's a simple way to do it! I tried it, and it works.
pygame/math.py
Outdated
except TypeError: | ||
# Doesn't seem to be vector2-like, so NotImplemented | ||
return False | ||
return False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should return NotImplemented
instead of return False
in both cases (see https://docs.python.org/2/library/stdtypes.html#the-notimplemented-object).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If pygame already returns False
here it's probably slightly better to follow its implementation though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried with NotImplemented, but the existing unit tests prefer returning False, so leaving it that way.
pygame/math.py
Outdated
return False | ||
|
||
def __ne__(self, other): | ||
return not self.__eq__(other) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to do something like return not (self == other)
here so that the comparison dispatch can do its thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If pygame already implements it this way, then maybe it's better to keep it this way.
pygame/math.py
Outdated
self.y = args[1] | ||
|
||
def __repr__(self): | ||
return "<Vector2({}, {})>".format(self.x, self.y) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably use self._x
and self._y
whenever we read x
or y
internally to avoid going through the property lookup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Will do.
Thanks for the review and suggestions @hodgestar. I'll have a look at in the course of the week. |
@ajoubertza How is this going? :) |
Hi @hodgestar. Hasn't been going for a while, but am looking at it again now! |
@ajoubertza Definitely happy with that for a first pass. We can decide later if it's worth refactoring. |
- Comparisons with NaNs and small deltas were not handled the same as the original pygame implementation. - Some of the "elementwise op vector" tests were passing even though the comparison functions returned NotImplemented. Added reverse checks that break in that case. Fixed the elementwise operations in those cases.
- Fixed imports for math_test, so it can also be run as part of the whole test module, as Travis does. - Draw tests run after test_circle_limits were failing because subsequent Surface constructor calls got back an 8 bpp surface, from sdl.SDL_GetVideoSurface() instead of making a new one. With the 8 bpp surface an exception is raised in _get_default_masks(..., alpha=True). Fix is to use the surface already created in setUp() instead of accessing the screen. - Fix some Python 2/3 compatibility issues.
# Conflicts: # pygame/draw.py
@@ -0,0 +1,1529 @@ | |||
""" Math module """ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a suitable LGPL header to this file, please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Whose names do I include?
For math_test.py, most of it came from the original pygame repo, so leave it as is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You've done the vast majority of the work on math.py, so add whichever names you think are relevant.
For the test cases, we've been leaving largely unchanged, so leaving math_test.py as is seems fine.
pygame/math.py
Outdated
return Vector2(self.vector.x ** other.x, self.vector.y ** other.y) | ||
elif isinstance(other, ElementwiseVector2Proxy): | ||
return Vector2(self.vector.x ** other.vector.x, | ||
self.vector.y ** other.vector.y) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pygame explicity checks for a complex result and returns a ValueError to handle python3's behaviour. We should probably do the same here (pygame/src/math.c:3395, for example)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The behaviour of the power operator, '**' and built-in pow() function, changed in Python 3.x. In Python 3.x raising a negative number to a fractional power results in a complex number, while in earlier versions it raised a ValueError. Matching the behaviour of pygame, as per the math unit tests, requires a ValueError instead of a complex result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few discrepencies between pygame and our behaviour that need to be either fixed or documented, but overall this looks really good.
format(args[0])) | ||
else: | ||
raise TypeError("Invalid string argument - not like __repr__ ({}).". | ||
format(args[0])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pygame supports initiliasing Vector2 with a single number:
>>> Vector2(2)
<Vector2(2, 0)>
This should either be supported, or the discrepency should be noted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'll add that, and a test. Same for Vector3.
pygame/math.py
Outdated
self.x = args[0] | ||
self.y = args[1] | ||
self.z = args[2] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise pygame supports initiliasing Vector3 with either 1 or 2 numbers. Again this should be supported or the discrepency should be documented.
r = draw.circle(screen, (0xff, 0xff, 0xff), (((2**16)//2 + 1), | ||
((2**16)//2 + 1)), 0) | ||
r = draw.circle(self.surf, (0xff, 0xff, 0xff), (((2**16)//2 + 1), | ||
((2**16)//2 + 1)), 0) | ||
self.assertEqual(r.x, 32769) | ||
self.assertEqual(r.y, 32769) | ||
self.assertEqual(r.w, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to handle this bug fix as a separate pull request, rather than including it with the math work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, will do that. I was just trying to get the tests to pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See PR #81.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@drnlm Would you like me to revert the draw_test.py changes on this branch as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just merging in master is good enough.
pygame/math.py
Outdated
from pygame.compat import string_types, pow_compat | ||
|
||
VECTOR_EPSILON = 1e-6 # For equality tests | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in pygame, VECTOR_EPSILON is not directly exposed to the module since it's part of the C implementaton.
We may want to use __all__
to define the public API we expose explicitly.
The same concern applies to the pow_compat helper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that you want to hide some of these details, but not sure how to use __all__
for this. If I add something like __all__ = ['enable_swizzling', 'disable_swizzling', 'Vector2', 'Vector3']
to math.py, then the dir() and ipython autocomplete output is still the same. It only makes a difference if I do from pygame.math import *
. Adding __all__
to the top level __init__.py
file would be hard, since I'd have to list every single function in the package.
How closely do we need to match pygame?
E.g. It provides:
In [15]: dir(pygame.math)
Out[15]:
['Vector2',
'Vector3',
'VectorElementwiseProxy',
'VectorIterator',
'_PYGAME_C_API',
'__doc__',
'__file__',
'__name__',
'__package__',
'disable_swizzling',
'enable_swizzling']
So far, this module provides:
In [2]: dir(pygame.math)
Out[2]:
['ElementwiseVector2Proxy',
'ElementwiseVector3Proxy',
'ElementwiseVectorProxyBase',
'Number',
'VECTOR_EPSILON',
'Vector2',
'Vector3',
'__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'_rotate_2d',
'absolute_import',
'disable_swizzling',
'division',
'enable_swizzling',
'math',
'pow_compat',
'string_types']
I prefer @hodgestar's suggestion to use the _
prefix to imply private use. Is the same required for imports, e.g. from numbers import Number
-> from numbers import Number as _Number
, so that they don't show up so easily?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to match pygaqme exactly, but we do want to make the public API we expose explicit, so it's clear when people are poking at something internal.
Using _VECTOR_EPSILON
is fine, since it's then clear that it's internal. I would personally also mark classes like ElementwiseVector2Proxy, which are not intended to be used outside of math.py as such, even though that difffers from what pygame itself exposes.
pygame/math.py
Outdated
abs(dx) < VECTOR_EPSILON and | ||
abs(dy) < VECTOR_EPSILON and | ||
abs(dz) < VECTOR_EPSILON) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pygame exposes epsilon as a attribute on Vector2 and Vector3 to control the comparison accuracy
>>> from pygame.math import Vector2
>>> s=Vector2(2,2)
>>> s2=s+Vector2(1e-8,1e-8)
>>> s2 == s
True
>>> s2.epsilon = 1e-10
>>> s2 == s
False
>>> s == s2
True
We should either match this behaviour, or document the discrepency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll add that too, and a test.
Another option would be to rename `VECTOR_EPSILON` to `_VECTOR_EPSILON` to
indicate that it's an internal implementation detail?
|
That would also work, yes. |
Thanks for the review @drnlm |
- Rename internals with underscores to indicate usage - Add epsilon attribute to vectors - Allow initialisation for partial values (zero the rest) - Include math module in __init__.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good
Thanks
Great. Thanks @drnlm. And it only took me a mere 5 months! 🐌 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left a couple of additional comments, but in general looks awesome.
There are quite a few places where we might we able to speed things up considerably by avoiding the property look up for vec.x, vec.y, vec.z
but I think that is best tackled in a separate branch later if we want to (we can either carefully use _x
a lot, or we can remove _x
and try use just a setter for x
without a getter -- which is a bit fiddly to arrange, but doable).
@@ -1,3 +1,4 @@ | |||
from __future__ import absolute_import |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're missing a blank line after the future import (at least we seem to do that elsewhere).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I like consistency too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tx.
# fractional power results in a complex number, while in earlier versions | ||
# it raised a ValueError. Matching the behaviour of pygame requires | ||
# the ValueError instead of a complex result. The compatibility function | ||
# defined below is provided for this purpose. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like math.pow(-2.2, 1.1)
raises the ValueError
on both Python 2.7.12 and 3.5.2, so maybe we can just use math.pow
instead of our own function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While math.pow will return a ValueError, it is also using the underlying C math library and will have a different precision from pow.
pygame is explicitly using pow underneath it's implementation, so if we use math.pow(), there will be differences between the implementations. Whether these will matter is another question, since they'll arguably only show up in odd corner cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried math.pow()
, but there's an issue with 0 ** -1
, which raises a ZeroDivisionError
, while the math module raises a ValueError
. The unit tests have a check or the former.
Interesting point about the implementation differences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tx for the discussion. Happy for it to stay this way then.
pygame/mask.py
Outdated
@@ -18,7 +18,8 @@ | |||
|
|||
# Implement the pygame API for the bitmask functions | |||
|
|||
from math import atan2, pi | |||
from __future__ import absolute_import | |||
import math |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another place where I think we should have a blank line for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few more of these cases, but I'm not going to comment on them all individually. :)
r = draw.circle(screen, (0xff, 0xff, 0xff), (((2**16)//2 + 1), | ||
((2**16)//2 + 1)), 0) | ||
r = draw.circle(self.surf, (0xff, 0xff, 0xff), (((2**16)//2 + 1), | ||
((2**16)//2 + 1)), 0) | ||
self.assertEqual(r.x, 32769) | ||
self.assertEqual(r.y, 32769) | ||
self.assertEqual(r.w, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just merging in master is good enough.
Vector2.__getattr__ = Vector2.__getattr_swizzle__ | ||
Vector3.__getattr__ = Vector3.__getattr_swizzle__ | ||
|
||
if '__oldsetattr__' not in dir(Vector2): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not following why we need to have __oldsetattr__
if __getattr__
is not set when swizzle is disabled? We could just set and delete __getattr__
to enable / disable swizzling. If this is just following the Python API pygame exposes, then I think that's a good enough reason to keep them, if not, I'd remove the extra complexity. Also, maybe I'm just missing something entirely. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To enable swizzling we need to replace both __getattr__
and __setattr__
. When we disable we are deleting the replacement __getattr__
, and restoring the old __setattr__
. Does that help?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is the original __setattr__
defined? Possibly I'm still being silly but my Ctrl-F def __setattr__
is not finding anything. :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't define one, it is just the built in one we inherit from our parent, object
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/me learns something new about Python objects. :)
Looks good to me. Landing! |
Yay! Thanks for the reviews. |
Thank you for doing the work and putting up with silly review comments. :)
|
Work in progress.