Coding Standard

  • Follow PEP-8

Modules

Each value type is given its own module.

Functions that reside in these modules include:

  • Creation
  • Conversion
  • Manipulation

Functions

  • Existing numpy operations shall be wrapped where it may not be obvious how to perform the operation.

A good example:

def multiply(m1, m2):
    # it may not be obvious that the 'dot' operator is the
    # equivalent of multiplication when using arrays as matrices
    # so this is good to help point users in the right direction
    return numpy.dot(m1, m2)

A bad example:

def add(v1, v2):
    # this functionality is too obvious and too basic
    return v1 + v2
  • Functions shall not modify data in-place.
  • Procedural and Class implementations shall remain in lock-step.

Function names

  • Each type shall provide convenience create functions for conversions and other initialisations.
  • Each type shall have a basic create function which returns a zero-ed type, where it makes sense.

A good example:

# vector3.py
def create(x=0., y=0., z=0., dtype=None):
    if isinstance(x, (list, np.ndarray)):
        raise ValueError('Function requires non-list arguments')
    return np.array([x,y,z], dtype=dtype)

def create_unit_length_x(dtype=None):
    return np.array([1.0, 0.0, 0.0], dtype=dtype)

def create_unit_length_y(dtype=None):
    return np.array([0.0, 1.0, 0.0], dtype=dtype)

def create_unit_length_z(dtype=None):
    return np.array([0.0, 0.0, 1.0], dtype=dtype)

A bad example:

# matrix33.py
def create():
    # matrices aren't initialised manually
    # so this isn't ok
    pass
  • Conversion functions shall be prefixed with create_from_ followed by the type being converted from:

    def create_from_matrix(matrix):
        # converts from one type to another
        # this would have to support both matrix33 and matrix44
        pass
    
    def create_from_matrix33(matrix):
        # converts from a very specific type
        # only has to support matrix33
        pass
    

Supplimentary Data

When dealing with arrays and other complex data types, it is helpful to provide methods to identify which array index relates to what data.

A good method to do this is to provide a class definition which contains these values:

class index:
    x = 0
    y = 1
    z = 2
    w = 3

Tests

  • A test class for each module shall be provided in the pyrr/test directory.
  • A test class shall be the only class in the test module.
  • Each source file shall have its own test file.
  • Each test function shall have a test case associated with it
  • All test cases for a function shall be in a single test function

Layout

These are not strict rules, but are merely suggestions to keep the layout of code in Pyrr consistent.

  • Code shall be spaced vertically where doing so helps the readability of complex mathematical functions. Vertical spacing shall be performed at variable or data type boundaries.

A good example:

# laying out over multiple lines helps improve readability.
# brackets and parenthesis are laid out to more clearly indicate
# the end of an array / type.
# where appropriate, values are still laid out horizontally.
# provide links where appropriate
#  http://www.example.com/a/link/to/a/relevant/explanation/of/this/code
my_value = numpy.array([
    # X = some comment about how X is calculated
    (0.0, 0.0, 0.0),
    # Y = some comment about how Y is calculated
    (1.0, 1.0, 1.0)
], dtype=[('position', 'float32', (3,))]
)

# laying out parameters vertically can improve readability.
# we'll be less likely to accidentally pass an invalid value
# and we can more easily, and more clearly, add logic to the parameters.
some_complex_function_call(
    param_one,
    param_two,
    param_three,
    param_four,
    True if param_five else False,
)

A more complicated example:

return np.array(
    [
        # m1
        [
            # m11 = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
            1.0 - 2.0 * (y2 + z2),
            # m21 = 2.0 * (q.x * q.y + q.w * q.z)
            2.0 * (xy + wz),
            # m31 = 2.0 * (q.x * q.z - q.w * q.y)
            2.0 * (xz - wy),
        ],
        # m2
        [
            # m12 = 2.0 * (q.x * q.y - q.w * q.z)
            2.0 * (xy - wz),
            # m22 = 1.0 - 2.0 * (q.x * q.x + q.z * q.z)
            1.0 - 2.0 * (x2 + z2),
            # m32 = 2.0 * (q.y * q.z + q.w * q.x)
            2.0 * (yz + wx),
        ],
        # m3
        [
            # m13 = 2.0 * ( q.x * q.z + q.w * q.y)
            2.0 * (xz + wy),
            # m23 = 2.0 * (q.y * q.z - q.w * q.x)
            2.0 * (yz - wx),
            # m33 = 1.0 - 2.0 * (q.x * q.x + q.y * q.y)
            1.0 - 2.0 * (x2 + y2),
        ]
    ],
    dtype=dtype
)

A bad example:

# leaving this on a single line would not compromise readability
my_value = numpy.empty(
    (3,)
)

The same applies to function definitions:

def some_function(that_takes, many_parameters, and_is, hard_to_read, because, its_so, big):
    pass

Should become:

def some_function(
    that_takes,
    many_parameters,
    and_is,
    hard_to_read,
    because,
    its_so,
    big
):
    pass
  • Code may extend beyond 80 columns, where appropriate.