diff --git a/docs/source/_templates/api_reference_arrays_equality.rst b/docs/source/_templates/api_reference_arrays_equality.rst new file mode 100644 index 0000000..1f137d5 --- /dev/null +++ b/docs/source/_templates/api_reference_arrays_equality.rst @@ -0,0 +1,10 @@ +.. spelling:word-list:: + + np + + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/docs/source/_templates/api_reference_arrays_max_len.rst b/docs/source/_templates/api_reference_arrays_size.rst similarity index 100% rename from docs/source/_templates/api_reference_arrays_max_len.rst rename to docs/source/_templates/api_reference_arrays_size.rst diff --git a/docs/source/api_reference/arrays.rst b/docs/source/api_reference/arrays.rst index 25df92c..ac5cd24 100644 --- a/docs/source/api_reference/arrays.rst +++ b/docs/source/api_reference/arrays.rst @@ -1,3 +1,10 @@ +.. spelling:word-list:: + + args + np + tol + + pyxx.arrays =========== @@ -18,6 +25,19 @@ convert one or more arrays of one type to a different type. convert_to_tuple +Array Equality +-------------- + +The functions in this section are intended to check whether arrays have equal +size and/or content. + +.. autosummary:: + :toctree: ./api + :template: ../_templates/api_reference_arrays_equality.rst + + is_array_equal + + Array Size ---------- @@ -26,6 +46,8 @@ array-like objects. .. autosummary:: :toctree: ./api - :template: ../_templates/api_reference_arrays_max_len.rst + :template: ../_templates/api_reference_arrays_size.rst + check_len_equal + is_len_equal max_list_item_len diff --git a/docs/source/conf.py b/docs/source/conf.py index 1842751..421162c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,6 +10,7 @@ sys.path.append(str(pathlib.Path(__file__).resolve().parents[2])) import pyxx +from pyxx.arrays.functions.equality import Array_or_Number_or_String # -- Project information ----------------------------------------------------- @@ -127,6 +128,11 @@ 'show-inheritance': True, } +# Type aliases +autodoc_type_aliases = { + Array_or_Number_or_String: Array_or_Number_or_String, +} + # -- Matplotlib plotting extension options ----------------------------------- # https://matplotlib.org/stable/api/sphinxext_plot_directive_api.html diff --git a/pyxx/__init__.py b/pyxx/__init__.py index 6909bff..1b10948 100644 --- a/pyxx/__init__.py +++ b/pyxx/__init__.py @@ -6,8 +6,4 @@ # PROGRAM VERSION ------------------------------------------------------------ -_VERSION_MAJOR = 1 -_VERSION_MINOR = 0 -_VERSION_PATCH = 0 - -__version__ = f'{_VERSION_MAJOR}.{_VERSION_MINOR}.{_VERSION_PATCH}' +__version__ = '1.0.0' diff --git a/pyxx/arrays/__init__.py b/pyxx/arrays/__init__.py index 6aa51b7..5199bb8 100644 --- a/pyxx/arrays/__init__.py +++ b/pyxx/arrays/__init__.py @@ -5,4 +5,9 @@ """ from .functions.convert import convert_to_tuple -from .functions.size import max_list_item_len +from .functions.equality import is_array_equal +from .functions.size import ( + check_len_equal, + is_len_equal, + max_list_item_len, +) diff --git a/pyxx/arrays/functions/equality.py b/pyxx/arrays/functions/equality.py new file mode 100644 index 0000000..609d521 --- /dev/null +++ b/pyxx/arrays/functions/equality.py @@ -0,0 +1,157 @@ +"""Functions for evaluating equality of array-like objects +""" + +from __future__ import annotations +from numbers import Number +from typing import List, Tuple, Union + +import numpy as np + +from .size import is_len_equal + + +# Type alias: type for each `item` argument in `is_array_equal()` +__element_types = Union[Number, np.ndarray, str] +Array_or_Number_or_String = Union[List[__element_types], + Tuple[__element_types], + __element_types] + + +def is_array_equal(item1: Array_or_Number_or_String, + item2: Array_or_Number_or_String, + *args: Array_or_Number_or_String, + tol: float = 1e-16): + """Checks that arrays are equal in shape and content + + Returns ``True`` if all arrays passed as arguments are of the same shape + and all elements are equal within a given tolerance ``tol`` (for numeric + elements) or exactly equal (for string elements), and returns ``False`` + otherwise. Inputs can be lists, tuples, NumPy arrays, numbers, strings, + or nested arrays composed of any of these types. + + Parameters + ---------- + item1 : list or tuple or np.ndarray or Number or str + First array to evaluate + item2 : list or tuple or np.ndarray or Number or str + Second item to evaluate + *args : list or tuple or np.ndarray or Number or str, optional + Any other arrays to be evaluated + tol : float, optional + Maximum difference between numeric values to consider equivalent + (default is ``1e-16``) + + Returns + ------- + bool + Whether ``item1``, ``item2``, ``*args`` have the same shape, and + all elements are equal within tolerance ``tol`` (for numeric elements) + and exactly equal (for string elements) + + Warnings + -------- + - The shape of the input arrays must be identical for the arrays to be + considered equal. The shape of numbers is considered different from the + shape of lists, so observe that ``0`` and ``[0]`` are **not** considered + equal in shape. + + - By default, NumPy arrays are of homogeneous type. This means that, for + instance, ``pyxx.arrays.is_array_equal(np.array([1, 'a']), [1, 'a'])`` + evaluates to ``False`` (because the NumPy array is converted to all + strings). To avoid this issue, it is possible to create NumPy arrays + with the ``dtype=object`` argument and allow mixed types. For example, + ``pyxx.arrays.is_array_equal(np.array([1, 'a'], dtype=object), [1, 'a'])`` + evaluates to ``True``. + + Notes + ----- + **Recursion Limit:** Internally, :py:func:`is_array_equal` is a recursive + function. It is possible that for extremely large nested arrays, Python's + recursion limit may be reached. If this occurs and it is necessary to + compare such a large array, consider increasing the recursion limit using + the `sys.setrecursionlimit() `__ function. + + **Purpose:** One question that may arise is, *why is this function + necessary?* NumPy already offers functions like `numpy.array_equal() + `__, `numpy.isclose() `__, and `numpy.allclose() + `__. + + There are several main advantages of :py:func:`is_array_equal`: + + - NumPy requires that arrays are numeric and are not "ragged" (sub-lists + must all have the same length, recursively. For example, the array + ``x = [[1,2,3], [1,2]]`` is "ragged" since ``len(x[0]) != len(x[1])``). + In contrast, :py:func:`is_array_equal` can compare arrays with a mix of + strings, numbers, lists, and tuples, as well as "ragged" arrays. + + - The NumPy functions mentioned will typically throw an exception if the + array sizes being compared differ, but :py:func:`is_array_equal` simply + returns ``False`` in this case. This can eliminate the need to catch + exceptions for certain applications. + """ + # Create list of array(s) to compare with `item1` + items = [item2] + list(args) + + # Check whether each of the input arguments is an array-like object + is_array = [isinstance(x, (list, tuple, np.ndarray)) + for x in [item1, *items]] + + # If inputs are numbers, directly compare them (requiring difference + # between numbers to be less than or equal to `tol` to consider the + # inputs equal) + if isinstance(item1, Number) \ + or (isinstance(item1, np.ndarray) and item1.ndim == 0): + for x in items: + # Check whether `item2` or any of `args` are an array. If so, + # this indicates that the array shapes are not equal + if isinstance(x, (list, tuple)) \ + or (isinstance(x, np.ndarray) and x.ndim > 0): + return False + + # Argument `item1` is known to be a number, so attempt to see + # whether each corresponding element in the other input arrays + # is within tolerance `tol` + try: + # Disable Mypy warnings on the following line, since errors + # will be handled with the try statement + if abs(x - item1) > tol: # type: ignore + return False + + except TypeError: + return False + + return True + + # If inputs are array-like objects, compare their contents + elif any(is_array): + # Verify that inputs are array-like objects + if not all(is_array): + return False + + # Verify that all inputs have equal length + if not is_len_equal(item1, *items): + return False + + # Check whether each sub-array's elements are equal (recursively) + for i, x in enumerate(item1): + + # Disable Mypy warnings on the following line, since we've + # already checked that the lengths of `x` and all elements + # in `items` are equal + if not is_array_equal(x, *[item[i] for item in items], # type: ignore + tol=tol): + return False + + return True + + else: + # Inputs are not numbers or array-like objects, so try to directly + # compare them. This allows strings, user-defined classes/types, + # or other objects to be compared + for x in items: + if item1 != x: + return False + return True diff --git a/pyxx/arrays/functions/size.py b/pyxx/arrays/functions/size.py index 162867e..71abdb6 100644 --- a/pyxx/arrays/functions/size.py +++ b/pyxx/arrays/functions/size.py @@ -1,7 +1,88 @@ """Functions for evaluating or comparing sizes of array-like objects """ -from typing import Union +from typing import Any, Union + + +def check_len_equal(item1: Any, item2: Any, *args: Any): + """Checks whether the lengths of a set of sequence-type objects + are equal + + Evaluates the lengths of a set of items (such as lists, tuples, or + strings), returning whether all items have the same length as well as + either the length of all items (if all lengths are equal) or a list + containing the lengths of each item (if they are not equal). + + Parameters + ---------- + item1 : Any + First item whose length to evaluate + item2 : Any + Second item whose length to evaluate + *args : Any, optional + Any other items whose lengths are to be evaluated + + Returns + ------- + bool + Whether all items have the same length + int or list + Returns an integer containing the length of all items (if all + lengths are equal), or a list containing the lengths of each + item (if lengths differ) + + See Also + -------- + is_len_equal : + Identical functionality, but returns only the ``bool`` output and + may theoretically run slightly faster in cases where the length(s) + of the inputs does not need to be returned + """ + lengths = [len(item1)] + [len(item2)] + [len(i) for i in args] + + if len(set(lengths)) == 1: + return True, lengths[0] + else: + return False, lengths + + +def is_len_equal(item1: Any, item2: Any, *args: Any): + """Checks whether the lengths of a set of sequence-type objects + are equal + + Evaluates the lengths of a set of items (such as lists, tuples, or + strings), returning whether all items have the same length. This + function should be slightly faster than :py:func:`check_len_equal` + for applications where the lengths of the input arguments do not + need to be returned. + + Parameters + ---------- + item1 : Any + First item whose length to evaluate + item2 : Any + Second item whose length to evaluate + *args : Any, optional + Any other items whose lengths are to be evaluated + + Returns + ------- + bool + Whether all items have the same length + + See Also + -------- + check_len_equal : + Identical functionality, but additionally returns the length of the + input arguments + """ + length1 = len(item1) + + for item in [item2] + list(args): + if not len(item) == length1: + return False + + return True def max_list_item_len(input_list: Union[list, tuple]): diff --git a/requirements.txt b/requirements.txt index 4fef116..e04198b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ # that you use the `.vscode/requirements.txt` file to install dependencies. ############################################################################## + +numpy diff --git a/tests/__init__.py b/tests/__init__.py index fab85a8..ae5cc46 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,8 @@ +import io import pathlib import os import shutil +import sys # Define variables available to all tests @@ -10,6 +12,17 @@ # Define context managers to facilitate testing +class CapturePrint: + """Captures text printed to the terminal when running commands""" + def __enter__(self): + self.terminal_stdout = io.StringIO() + sys.stdout = self.terminal_stdout + return self.terminal_stdout + + def __exit__(self, *args, **kwargs): + sys.stdout = sys.__stdout__ + + class CreateTempTestDir: """Sets up temporary folder for reading/writing test files""" def __enter__(self): diff --git a/tests/arrays/functions/__init__.py b/tests/arrays/functions/__init__.py index a124800..15899ad 100644 --- a/tests/arrays/functions/__init__.py +++ b/tests/arrays/functions/__init__.py @@ -1,2 +1,3 @@ from .test_convert import * +from .test_equality import * from .test_size import * diff --git a/tests/arrays/functions/test_equality.py b/tests/arrays/functions/test_equality.py new file mode 100644 index 0000000..b7b72ae --- /dev/null +++ b/tests/arrays/functions/test_equality.py @@ -0,0 +1,281 @@ +import unittest +import warnings + +import numpy as np + +from pyxx.arrays import is_array_equal + + +class Test_NumPyListEqual(unittest.TestCase): + def setUp(self): + self.array = np.array([[ 3, 6, -3.213, 0], + [3.23, 1, -1.42e-3, 4e6]]) + + self.array_uneq_shape = np.array([[3, 6, -3.213, 0], + [3.23, 1, -1.42e-3, 4e6], + [1, 2, 3, 4]]) + + self.array_uneq_val = np.array([[ 3, 6, -3.213, 0], + [3.23, 1, -1.41e-3, 4e6]]) + + def test_equal(self): + # Verifies that arrays of equal shape and values are evaluated as equal + with self.subTest(args=2): + self.assertTrue(is_array_equal(self.array, self.array)) + + with self.subTest(args=3): + self.assertTrue(is_array_equal(self.array, self.array, self.array)) + + with self.subTest(args=4): + self.assertTrue(is_array_equal(self.array, self.array, self.array, self.array)) + + def test_unequal_shape(self): + # Verifies that arrays of different shape are evaluated as not equal + with self.subTest(args=2): + self.assertFalse(is_array_equal(self.array, self.array_uneq_shape)) + + with self.subTest(args=3): + self.assertFalse(is_array_equal( + self.array_uneq_shape, self.array, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array_uneq_shape, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array, self.array_uneq_shape)) + + with self.subTest(args=4): + self.assertFalse(is_array_equal( + self.array_uneq_shape, self.array, self.array, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array_uneq_shape, self.array, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array, self.array_uneq_shape, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array, self.array, self.array_uneq_shape)) + + with self.subTest(comment='number_and_list'): + self.assertFalse(is_array_equal( + [1, [2, 3, 4]], [[1], [2, 3, 4]])) + self.assertFalse(is_array_equal( + [[1], [2, 3, 4]], [1, [2, 3, 4]])) + self.assertFalse(is_array_equal( + [[1, 2, 3], [4, [5, 6, 7]]], [[1, 2, 3], [4, [5, 7]]] + )) + + with self.subTest(comment='string_and_char_array'): + self.assertFalse(is_array_equal( + 'myString', ['m', 'y', 'S', 't', 'r', 'i', 'n', 'g'])) + self.assertFalse(is_array_equal( + ['m', 'y', 'S', 't', 'r', 'i', 'n', 'g'], 'myString')) + + def test_unequal_values(self): + # Verifies that arrays with different values are evaluated as not equal + with self.subTest(args=2): + self.assertFalse(is_array_equal(self.array, self.array_uneq_val)) + + with self.subTest(args=3): + self.assertFalse(is_array_equal( + self.array_uneq_val, self.array, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array_uneq_val, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array, self.array_uneq_val)) + + with self.subTest(args=4): + self.assertFalse(is_array_equal( + self.array_uneq_val, self.array, self.array, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array_uneq_val, self.array, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array, self.array_uneq_val, self.array)) + self.assertFalse(is_array_equal( + self.array, self.array, self.array, self.array_uneq_val)) + + def test_tolerance(self): + # Verifies that setting tolerance for equality functions as expected + diff = np.abs(self.array_uneq_val - self.array) + + with self.subTest(error='positive'): + self.assertTrue( + is_array_equal(self.array, self.array + diff, tol=0.000011)) + + self.assertFalse( + is_array_equal(self.array, self.array + 2*diff, tol=0.000011)) + + with self.subTest(error='negative'): + self.assertTrue( + is_array_equal(self.array, self.array - diff, tol=0.000011)) + + self.assertFalse( + is_array_equal(self.array, self.array - 2*diff, tol=0.000011)) + + def test_list(self): + # Verifies that equality can be checked for lists + with self.subTest(result='equal'): + self.assertTrue(is_array_equal( + [0, 4.2, 9, -0.323, 1e5], + [0, 4.2, 9, -0.323, 1e5], + [0, 4.2, 9, -0.323, 1e5] + )) + + with self.subTest(result='unequal_shape'): + self.assertFalse(is_array_equal( + [0, 4.2, 9, -0.323, 1e5], + [0, 4.2, 9, -0.323], + [0, 4.2, 9, -0.323, 1e5] + )) + + with self.subTest(result='unequal_value'): + self.assertFalse(is_array_equal( + [0, 4.2, 9, -0.3233, 1e5], + [0, 4.2, 9, -0.323, 1e5], + [0, 4.2, 9, -0.323, 1e5] + )) + + def test_tuple(self): + # Verifies that equality can be checked for tuples + with self.subTest(result='equal'): + self.assertTrue(is_array_equal( + (0, 4.2, 9, -0.323, 1e5), + (0, 4.2, 9, -0.323, 1e5), + (0, 4.2, 9, -0.323, 1e5) + )) + + with self.subTest(result='unequal_shape'): + self.assertFalse(is_array_equal( + (0, 4.2, 9, -0.323, 1e5), + (0, 4.2, 9, -0.323), + (0, 4.2, 9, -0.323, 1e5) + )) + + with self.subTest(result='unequal_value'): + self.assertFalse(is_array_equal( + (0, 4.2, 9, -0.3233, 1e5), + (0, 4.2, 9, -0.323, 1e5), + (0, 4.2, 9, -0.323, 1e5) + )) + + def test_number(self): + # Verifies that equality can be checked for numbers + with self.subTest(result='equal'): + self.assertTrue(is_array_equal(3.142, 3.142)) + self.assertTrue(is_array_equal(3.142, 3.141, tol=0.0011)) + self.assertTrue(is_array_equal(-5, -5)) + self.assertTrue(is_array_equal(-5.0, -5)) + + with self.subTest(result='equal_numpy'): + self.assertTrue(is_array_equal(-5.0, np.array(-5))) + self.assertTrue(is_array_equal(-5.0, np.array(-5), np.int32(-5))) + + with self.subTest(result='unequal'): + self.assertFalse(is_array_equal(3.142, 3.1)) + self.assertFalse(is_array_equal(-5, 5)) + + def test_string(self): + # Verifies that equality can be checked for strings + with self.subTest(result='equal_str'): + self.assertTrue(is_array_equal('myString', 'myString')) + self.assertTrue(is_array_equal('ab', 'ab', 'ab')) + self.assertTrue(is_array_equal('ab\n', 'ab\n', 'ab\n', 'ab\n')) + + with self.subTest(result='equal_str_array'): + self.assertTrue(is_array_equal( + ['myString', 'a', ['bc', 'd']], ['myString', 'a', ['bc', 'd']])) + + with self.subTest(result='unequal_str'): + self.assertFalse(is_array_equal('myStrinG', 'myString')) + self.assertFalse(is_array_equal('abc', 'ab', 'ab')) + self.assertFalse(is_array_equal('ab', 'abc', 'ab')) + self.assertFalse(is_array_equal('ab', 'ab', 'abc')) + self.assertFalse(is_array_equal('ab\nc', 'ab\n', 'ab\n', 'ab\n')) + self.assertFalse(is_array_equal('ab\n', 'ab\nc', 'ab\n', 'ab\n')) + self.assertFalse(is_array_equal('ab\n', 'ab\n', 'ab\nc', 'ab\n')) + self.assertFalse(is_array_equal('ab\n', 'ab\n', 'ab\n', 'ab\nc')) + + with self.subTest(result='unequal_str_array'): + self.assertFalse(is_array_equal( + ['myString', 'a', ['b', 'c', 'd']], ['myString', 'a', ['bc', 'd']])) + + self.assertFalse(is_array_equal( + ['myString', 'a', ['bc', 'e']], ['myString', 'a', ['bc', 'd']])) + + def test_mixed_type_numeric(self): + # Verifies that equality can be checked for mixed types of arrays + + # Disable display of NumPy warnings when creating "ragged" array + warnings.filterwarnings('ignore', category=np.VisibleDeprecationWarning) + + with self.subTest(dim=1): + self.assertTrue(is_array_equal( + [3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], + (3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), + np.array([3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13]) + )) + + with self.subTest(dim=2): + self.assertTrue(is_array_equal( + [[3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], [4, 3, 2, 1]], + ((3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), [4, 3, 2, 1]), + ((3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), (4, 3, 2, 1)), + np.array([[3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], [4, 3, 2, 1]]) + )) + + with self.subTest(dim=3): + self.assertTrue(is_array_equal( + [[3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], [[3, 6], 9], [4, 3, 2, 1]], + ((3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), [np.array([3, 6]), np.array(9)], [4, 3, 2, 1]), + ((3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13), ([3, 6], 9), (4, 3, 2, 1)), + np.array([[3.14, 1/2, 0, 5, -6.28, 2e10, 1e-13], ((3, 6), 9), [4, 3, 2, 1]]) + )) + + def test_mixed_type_numeric_str(self): + # Verifies that equality can be checked for mixed types of arrays + # with both numbers and strings + + # Disable display of NumPy warnings when creating "ragged" array + warnings.filterwarnings('ignore', category=np.VisibleDeprecationWarning) + + with self.subTest(comment='no_numpy_array'): + self.assertTrue(is_array_equal( + [[3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13], [4, 3, 2, 1]], + ((3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13), [4, 3, 2, 1]), + ((3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13), (4, 3, 2, 1)) + )) + + with self.subTest(comment='numpy_no_dtype_object'): + self.assertFalse(is_array_equal(np.array([1, 'a']), [1, 'a'])) + + with self.subTest(comment='numpy_dtype_object'): + self.assertTrue(is_array_equal(np.array([1, 'a'], dtype=object), [1, 'a'])) + self.assertTrue(is_array_equal( + [[3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13], [4, 3, 2, 1]], + ((3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13), [4, 3, 2, 1]), + ((3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13), (4, 3, 2, 1)), + np.array([[3.14, 1/2, 0, 5, 'myString', ['a,', 3], -6.28, 2e10, 1e-13], [4, 3, 2, 1]], + dtype=object) + )) + + def test_incompatible_type(self): + # Verifies that arrays are assessed as not equal if they require + # comparing types that are not exactly equal or cannot be subtracted + with self.subTest(comment='int_str'): + self.assertFalse(is_array_equal([1.0, (2, 3)], ('1', [2, 3]))) + + with self.subTest(comment='number_type'): + self.assertFalse(is_array_equal(int, 3, 0)) + self.assertFalse(is_array_equal([1, 2, 3], [1, 2, float])) + + def test_unspecified_type(self): + # Verifies that objects that are evaluated as equal (even if not + # numbers or strings) can be compared + with self.subTest(comment='single_value'): + self.assertTrue(is_array_equal(int, int)) + + with self.subTest(comment='array'): + self.assertTrue(is_array_equal([int, float], [int, float])) + + def test_empty(self): + # Verifies that empty arrays are evaluated as equal + self.assertTrue(is_array_equal([], [])) + self.assertTrue(is_array_equal((), ())) + self.assertTrue(is_array_equal(np.array([]), np.array([]))) + self.assertTrue(is_array_equal(np.array([]), [], ())) diff --git a/tests/arrays/functions/test_size.py b/tests/arrays/functions/test_size.py index 836d1cd..c61f384 100644 --- a/tests/arrays/functions/test_size.py +++ b/tests/arrays/functions/test_size.py @@ -1,7 +1,169 @@ from typing import List import unittest -from pyxx.arrays import max_list_item_len +from pyxx.arrays import ( + check_len_equal, + is_len_equal, + max_list_item_len, +) + + +class Test_CheckLenEqual(unittest.TestCase): + def test_check_list_equal(self): + # Verifies that lengths of lists are evaluated correctly + with self.subTest(num_lists=2): + self.assertTupleEqual( + check_len_equal(['a', 'b', 'c'], [1, 2, 3]), + (True, 3) + ) + self.assertTupleEqual(check_len_equal(['a'], [1]), (True, 1)) + + with self.subTest(num_lists=3): + self.assertTupleEqual( + check_len_equal(['a', 'b'], [1, 2], ['cd', 3]), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(['a'], [1], ['c']), (True, 1)) + + with self.subTest(num_lists=4): + self.assertTupleEqual( + check_len_equal(['a', 'b'], [1, 2], ['cd', 3], [None, None]), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(['a'], [1], ['c'], [None]), (True, 1)) + + def test_check_tuple_equal(self): + # Verifies that lengths of tuples are evaluated correctly + with self.subTest(num_tuples=2): + self.assertTupleEqual( + check_len_equal(('a', 'b', 'c'), (1, 2, 3)), + (True, 3) + ) + self.assertTupleEqual(check_len_equal(('a',), (1,)), (True, 1)) + + with self.subTest(num_tuples=3): + self.assertTupleEqual( + check_len_equal(('a', 'b'), (1, 2), ('cd', 3)), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(('a',), (1,), ('cd',)), (True, 1)) + + with self.subTest(num_tuples=4): + self.assertTupleEqual( + check_len_equal(('a', 'b'), (1, 2), ('cd', 3), (None, None)), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(('a',), (1,), ('cd',), (None,)), (True, 1)) + + def test_check_str_equal(self): + # Verifies that lengths of strings are evaluated correctly + with self.subTest(num_strings=2): + self.assertTupleEqual( + check_len_equal('abc', '123'), + (True, 3) + ) + + with self.subTest(num_strings=3): + self.assertTupleEqual( + check_len_equal('ab', '12', 'c3'), + (True, 2) + ) + + with self.subTest(num_strings=4): + self.assertTupleEqual( + check_len_equal('ab', '12', 'c3', 'No'), + (True, 2) + ) + + def test_check_mixed_type_equal(self): + # Verifies that lengths of mixed list/tuple/string arguments + # are evaluated correctly + self.assertTupleEqual( + check_len_equal(['a', 'b'], (1, 2), ('cd', 3), (None, None), 'ce'), + (True, 2) + ) + self.assertTupleEqual(check_len_equal(('a',), (1,), ('cd',), (None,), 'c'), (True, 1)) + + def test_check_mixed_type_unequal(self): + # Verifies that lengths of mixed list/tuple/string arguments with + # different lengths are compared correctly + with self.subTest(num_args=2): + self.assertTupleEqual( + check_len_equal((1, 2), 'abcdefjkl'), + (False, [2, 9]) + ) + + with self.subTest(num_args=3): + self.assertTupleEqual( + check_len_equal(('cd', 3), (None, None), 'abcdefjkl'), + (False, [2, 2, 9]) + ) + + with self.subTest(num_args=5): + self.assertTupleEqual( + check_len_equal(['a', 'b', 'c'], (1, 2), ('cd', 3), (None, None), 'abcdefjkl'), + (False, [3, 2, 2, 2, 9]) + ) + + +class Test_IsLenEqual(unittest.TestCase): + def test_is_list_equal(self): + # Verifies that equality of lengths of lists is evaluated correctly + with self.subTest(num_lists=2): + self.assertTrue(is_len_equal(['a', 'b', 'c'], [1, 2, 3])) + self.assertTrue(is_len_equal(['a'], [1])) + + with self.subTest(num_lists=3): + self.assertTrue(is_len_equal(['a', 'b'], [1, 2], ['cd', 3])) + self.assertTrue(is_len_equal(['a'], [1], ['c'])) + + with self.subTest(num_lists=4): + self.assertTrue(is_len_equal(['a', 'b'], [1, 2], ['cd', 3], [None, None])) + self.assertTrue(is_len_equal(['a'], [1], ['c'], [None])) + + def test_is_tuple_equal(self): + # Verifies that equality of lengths of tuples is evaluated correctly + with self.subTest(num_tuples=2): + self.assertTrue(is_len_equal(('a', 'b', 'c'), (1, 2, 3))) + self.assertTrue(is_len_equal(('a',), (1,))) + + with self.subTest(num_tuples=3): + self.assertTrue(is_len_equal(('a', 'b'), (1, 2), ('cd', 3))) + self.assertTrue(is_len_equal(('a',), (1,), ('cd',))) + + with self.subTest(num_tuples=4): + self.assertTrue(is_len_equal(('a', 'b'), (1, 2), ('cd', 3), (None, None))) + self.assertTrue(is_len_equal(('a',), (1,), ('cd',), (None,))) + + def test_is_str_equal(self): + # Verifies that equality of lengths of strings is evaluated correctly + with self.subTest(num_strings=2): + self.assertTrue(is_len_equal('abc', '123')) + + with self.subTest(num_strings=3): + self.assertTrue(is_len_equal('ab', '12', 'c3')) + + with self.subTest(num_strings=4): + self.assertTrue(is_len_equal('ab', '12', 'c3', 'No')) + + def test_is_mixed_type_equal(self): + # Verifies that equality of lengths of mixed list/tuple/string arguments + # is evaluated correctly + self.assertTrue(is_len_equal(['a', 'b'], (1, 2), ('cd', 3), (None, None), 'ce')) + self.assertTrue(is_len_equal(('a',), (1,), ('cd',), (None,), 'c')) + + def test_is_mixed_type_unequal(self): + # Verifies that equality of lengths of mixed list/tuple/string arguments with + # different lengths is evaluated correctly + with self.subTest(num_args=2): + self.assertFalse(is_len_equal((1, 2), 'abcdefjkl')) + + with self.subTest(num_args=3): + self.assertFalse(is_len_equal(('cd', 3), (None, None), 'abcdefjkl')) + + with self.subTest(num_args=5): + self.assertFalse(is_len_equal(['a', 'b', 'c'], (1, 2), ('cd', 3), + (None, None), 'abcdefjkl')) class Test_MaxListLength(unittest.TestCase): diff --git a/tests/files/classes/test_file.py b/tests/files/classes/test_file.py index 93f95c9..92873b1 100644 --- a/tests/files/classes/test_file.py +++ b/tests/files/classes/test_file.py @@ -30,6 +30,13 @@ def setUp(self): 'str': self.file_from_str, } + def test_file_repr_empty(self): + # Verifies that the file object string representation is computed + # correctly if `path` attribute has not been assigned a value + self.assertEqual( + self.file_empty.__repr__(), + "") + def test_file_repr_before(self): # Verifies that file object descriptor is computed correctly before # computing file hashes