diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index bd1ae279..d2060715 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -23,7 +23,7 @@ jobs: matrix: # Each option you define in the matrix has a key and value - python-version: [ 3.8 ] + python-version: [ 3.9 ] # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 00000000..a35e1609 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,29 @@ +name: Types check with myPy + +on: [push, pull_request] + +jobs: + mypy: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.9] + + steps: + - name: Set up Git repository + uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel setuptools + python -m pip install -r requirements.txt + python -m pip install mypy + + - name: Run MyPy + run: | + mypy project/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..21476aef --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +name: Run Tests +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install pytest + run: pip install pytest + - name: Run tests using project script + run: | + python ./scripts/run_tests.py diff --git a/project/Task1/__init__.py b/project/Task1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project/Task1/matrix_operations.py b/project/Task1/matrix_operations.py new file mode 100644 index 00000000..2caccee0 --- /dev/null +++ b/project/Task1/matrix_operations.py @@ -0,0 +1,129 @@ +def matrix_add(matrix1, matrix2): + """ + Add two matrices element-wise. + + Args: + matrix1 (list): First matrix as list of lists. + matrix2 (list): Second matrix as list of lists. + + Returns: + list: Resulting matrix after addition. + + Raises: + ValueError: If matrices have different dimensions. + """ + if len(matrix1) != len(matrix2) or len(matrix1[0]) != len(matrix2[0]): + raise ValueError("Matrices must have the same dimensions") + + return [ + [matrix1[i][j] + matrix2[i][j] for j in range(len(matrix1[0]))] + for i in range(len(matrix1)) + ] + + +def matrix_multiply(matrix1, matrix2): + """ + Multiply two matrices. + + Args: + matrix1 (list): First matrix. + matrix2 (list): Second matrix. + + Returns: + list: Resulting matrix after multiplication. + + Raises: + ValueError: If number of columns in first matrix doesn't match + number of rows in second matrix. + """ + if len(matrix1[0]) != len(matrix2): + raise ValueError( + "Number of columns in first matrix must equal " + "number of rows in second matrix" + ) + + result = [[0] * len(matrix2[0]) for _ in range(len(matrix1))] + for i in range(len(matrix1)): + for k in range(len(matrix2)): + for j in range(len(matrix2[0])): + result[i][j] += matrix1[i][k] * matrix2[k][j] + return result + + +def matrix_transpose(matrix): + """ + Transpose a matrix (swap rows and columns). + + Args: + matrix (list): Input matrix. + + Returns: + list: Transposed matrix. + """ + return [list(row) for row in zip(*matrix)] + + +class Matrix: + """ + A class representing a mathematical matrix. + + Attributes: + data (list): Matrix data as list of lists. + """ + + def __init__(self, data): + """ + Initialize a matrix with given data. + + Args: + data (list): Matrix data as list of lists. + + Raises: + ValueError: If rows have inconsistent lengths. + """ + if not all(len(row) == len(data[0]) for row in data): + raise ValueError("All rows must have the same length") + self.data = data + self.rows = len(data) + self.cols = len(data[0]) + + def add(self, other): + """ + Add another matrix to this matrix. + + Args: + other (Matrix): Another matrix. + + Returns: + Matrix: Result of addition. + """ + return Matrix(matrix_add(self.data, other.data)) + + def multiply(self, other): + """ + Multiply with another matrix. + + Args: + other (Matrix): Another matrix. + + Returns: + Matrix: Result of multiplication. + """ + return Matrix(matrix_multiply(self.data, other.data)) + + def transpose(self): + """ + Transpose the matrix. + + Returns: + Matrix: Transposed matrix. + """ + return Matrix(matrix_transpose(self.data)) + + def __repr__(self): + return f"Matrix({self.data})" + + def __eq__(self, other): + if not isinstance(other, Matrix): + return False + return self.data == other.data diff --git a/project/Task1/vector_operations.py b/project/Task1/vector_operations.py new file mode 100644 index 00000000..4b7e451b --- /dev/null +++ b/project/Task1/vector_operations.py @@ -0,0 +1,120 @@ +import math + + +def dot_product(vector1, vector2): + """ + Calculate the dot product of two vectors. + + Args: + vector1 (list): First vector as a list of numbers. + vector2 (list): Second vector as a list of numbers. + + Returns: + float: Dot product of the two vectors. + + Raises: + ValueError: If vectors have different lengths. + """ + if len(vector1) != len(vector2): + raise ValueError("Vectors must have the same length") + return sum(v1 * v2 for v1, v2 in zip(vector1, vector2)) + + +def vector_length(vector): + """ + Calculate the Euclidean length (magnitude) of a vector. + + Args: + vector (list): Vector as a list of numbers. + + Returns: + float: Length of the vector. + """ + return math.sqrt(sum(x * x for x in vector)) + + +def angle_between(vector1, vector2): + """ + Calculate the angle between two vectors in radians. + + Args: + vector1 (list): First vector. + vector2 (list): Second vector. + + Returns: + float: Angle between vectors in radians. + + Raises: + ValueError: If either vector is a zero vector. + """ + dot = dot_product(vector1, vector2) + len1 = vector_length(vector1) + len2 = vector_length(vector2) + + if len1 == 0 or len2 == 0: + raise ValueError("Vectors cannot be zero vectors") + + # Ensure cosine value is within valid range to avoid floating point errors + cos_angle = dot / (len1 * len2) + cos_angle = max(min(cos_angle, 1.0), -1.0) + + return math.acos(cos_angle) + + +class Vector: + """ + A class representing a mathematical vector. + + Attributes: + components (list): The components of the vector. + """ + + def __init__(self, components): + """ + Initialize a vector with given components. + + Args: + components (list): List of numerical components. + """ + self.components = list(components) + + def dot(self, other): + """ + Calculate dot product with another vector. + + Args: + other (Vector): Another vector. + + Returns: + float: Dot product. + """ + return dot_product(self.components, other.components) + + def length(self): + """ + Calculate the length of the vector. + + Returns: + float: Vector length. + """ + return vector_length(self.components) + + def angle_with(self, other): + """ + Calculate angle with another vector. + + Args: + other (Vector): Another vector. + + Returns: + float: Angle in radians. + """ + return angle_between(self.components, other.components) + + def __repr__(self): + return f"Vector({self.components})" + + def __eq__(self, other): + if not isinstance(other, Vector): + return False + return self.components == other.components diff --git a/project/Task2/__init__.py b/project/Task2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project/Task2/generator.py b/project/Task2/generator.py new file mode 100644 index 00000000..ea0eb1b8 --- /dev/null +++ b/project/Task2/generator.py @@ -0,0 +1,124 @@ +from functools import reduce +from typing import Any, Callable, Iterable, Iterator, List + + +def data_generator(start: int = 0, end: int = 10) -> Iterator[int]: + """Generator that yields numbers from start to end-1.""" + for i in range(start, end): + yield i + + +def pipeline(source: Iterable, *operations: Callable) -> Iterable: + """ + Takes a data source and sequence of operations, + applies them sequentially, returning a lazy generator. + Operations are applied in the order they are provided. + """ + stream = source + for operation in operations: + stream = operation(stream) + return stream + + +def map_operation(func: Callable) -> Callable[[Iterable], Iterator]: + """Map operation - applies function to each element.""" + + def _inner(stream: Iterable) -> Iterator: + for item in stream: + yield func(item) + + return _inner + + +def filter_operation(predicate: Callable) -> Callable[[Iterable], Iterator]: + """Filter operation - filters elements based on condition.""" + + def _inner(stream: Iterable) -> Iterator: + for item in stream: + if predicate(item): + yield item + + return _inner + + +def zip_operation(*others: Iterable) -> Callable[[Iterable], Iterator]: + """Zip operation - combines with other iterables.""" + + def _inner(stream: Iterable) -> Iterator: + return zip(stream, *others) + + return _inner + + +def reduce_operation( + func: Callable, initializer: Any = None +) -> Callable[[Iterable], Any]: + """ + Reduce operation - reduces sequence to single value. + Terminal operation - should be used at the end of pipeline. + """ + + def _inner(stream: Iterable) -> Any: + if initializer is not None: + return reduce(func, stream, initializer) + return reduce(func, stream) + + return _inner + + +def take_operation(n: int) -> Callable[[Iterable], Iterator]: + """Takes first n elements from the stream.""" + + def _inner(stream: Iterable) -> Iterator: + for i, item in enumerate(stream): + if i < n: + yield item + else: + break + + return _inner + + +def skip_operation(n: int) -> Callable[[Iterable], Iterator]: + """Skips first n elements.""" + + def _inner(stream: Iterable) -> Iterator: + for i, item in enumerate(stream): + if i >= n: + yield item + + return _inner + + +def enumerate_operation(start: int = 0) -> Callable[[Iterable], Iterator]: + """Adds index to each element.""" + + def _inner(stream: Iterable) -> Iterator: + for i, item in enumerate(stream, start): + yield (i, item) + + return _inner + + +def custom_operation(func: Callable) -> Callable[[Iterable], Iterator]: + """Wrapper for custom stream processing functions.""" + + def _inner(stream: Iterable) -> Iterator: + yield from func(stream) + + return _inner + + +def collect(stream: Iterable, collection_type: type = list) -> Any: + """Collects lazy generator results into specified collection.""" + return collection_type(stream) + + +def collect_to_list(stream: Iterable) -> List: + """Collects results into list.""" + return list(stream) + + +def count(stream: Iterable) -> int: + """Counts number of elements in the stream.""" + return sum(1 for _ in stream) diff --git a/requirements.txt b/requirements.txt index 30544ac2..61500b18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ black +mypy pre-commit pytest diff --git a/tests/Tests1/__init__.py b/tests/Tests1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Tests1/matrix_tests.py b/tests/Tests1/matrix_tests.py new file mode 100644 index 00000000..fd246dd3 --- /dev/null +++ b/tests/Tests1/matrix_tests.py @@ -0,0 +1,103 @@ +import pytest +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../")) +from project.Task1.matrix_operations import ( + matrix_add, + matrix_multiply, + matrix_transpose, + Matrix, +) + + +class TestMatrixOperations: + """Test cases for matrix operations.""" + + def test_matrix_add(self): + """Test matrix addition.""" + result = matrix_add([[1, 2], [3, 4]], [[5, 6], [7, 8]]) + assert result == [[6, 8], [10, 12]] + + def test_matrix_add_invalid_dimensions(self): + """Test matrix addition with invalid dimensions.""" + with pytest.raises(ValueError, match="Matrices must have the same dimensions"): + matrix_add([[1, 2]], [[3, 4, 5]]) + + def test_matrix_multiply(self): + """Test matrix multiplication.""" + result = matrix_multiply([[1, 2], [3, 4]], [[2, 0], [1, 2]]) + assert result == [[4, 4], [10, 8]] + + # Test identity matrix multiplication + identity = [[1, 0], [0, 1]] + matrix = [[1, 2], [3, 4]] + result = matrix_multiply(matrix, identity) + assert result == matrix + + def test_matrix_multiply_invalid_dimensions(self): + """Test matrix multiplication with invalid dimensions.""" + with pytest.raises(ValueError): + matrix_multiply([[1, 2]], [[3, 4, 5]]) + + def test_matrix_transpose(self): + """Test matrix transposition.""" + result = matrix_transpose([[1, 2, 3], [4, 5, 6]]) + assert result == [[1, 4], [2, 5], [3, 6]] + + # Test square matrix transpose + result = matrix_transpose([[1, 2], [3, 4]]) + assert result == [[1, 3], [2, 4]] + + +class TestMatrixClass: + """Test cases for Matrix class.""" + + def test_matrix_creation(self): + """Test Matrix object creation.""" + m = Matrix([[1, 2], [3, 4]]) + assert m.data == [[1, 2], [3, 4]] + assert m.rows == 2 + assert m.cols == 2 + + def test_matrix_creation_invalid(self): + """Test Matrix creation with invalid data.""" + with pytest.raises(ValueError, match="All rows must have the same length"): + Matrix([[1, 2], [3, 4, 5]]) + + def test_matrix_addition(self): + """Test matrix addition using Matrix class.""" + m1 = Matrix([[1, 2], [3, 4]]) + m2 = Matrix([[5, 6], [7, 8]]) + result = m1.add(m2) + expected = Matrix([[6, 8], [10, 12]]) + assert result == expected + + def test_matrix_multiplication(self): + """Test matrix multiplication using Matrix class.""" + m1 = Matrix([[1, 2], [3, 4]]) + m2 = Matrix([[2, 0], [1, 2]]) + result = m1.multiply(m2) + expected = Matrix([[4, 4], [10, 8]]) + assert result == expected + + def test_matrix_transpose_method(self): + """Test matrix transposition using Matrix class.""" + m = Matrix([[1, 2, 3], [4, 5, 6]]) + result = m.transpose() + expected = Matrix([[1, 4], [2, 5], [3, 6]]) + assert result == expected + + def test_matrix_repr(self): + """Test Matrix string representation.""" + m = Matrix([[1, 2], [3, 4]]) + assert repr(m) == "Matrix([[1, 2], [3, 4]])" + + def test_matrix_equality(self): + """Test Matrix equality comparison.""" + m1 = Matrix([[1, 2], [3, 4]]) + m2 = Matrix([[1, 2], [3, 4]]) + m3 = Matrix([[5, 6], [7, 8]]) + assert m1 == m2 + assert m1 != m3 + assert m1 != "not a matrix" diff --git a/tests/Tests1/vector_tests.py b/tests/Tests1/vector_tests.py new file mode 100644 index 00000000..211f055e --- /dev/null +++ b/tests/Tests1/vector_tests.py @@ -0,0 +1,51 @@ +import pytest +import math +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../")) +from project.Task1.vector_operations import ( + dot_product, + vector_length, + angle_between, + Vector, +) + + +class TestVectorOperations: + """Test cases for vector operations.""" + + def test_dot_product(self): + """Test dot product calculation.""" + assert dot_product([1, 2], [3, 4]) == 11 + assert dot_product([0, 0], [1, 2]) == 0 + + def test_dot_product_invalid_input(self): + """Test dot product with invalid inputs.""" + with pytest.raises(ValueError): + dot_product([1, 2], [1]) + + def test_vector_length(self): + """Test vector length calculation.""" + assert vector_length([3, 4]) == 5.0 + assert vector_length([0]) == 0.0 + + def test_angle_between(self): + """Test angle between vectors calculation.""" + assert math.isclose(angle_between([1, 0], [0, 1]), math.pi / 2) + assert angle_between([1, 0], [1, 0]) == 0.0 + + +class TestVectorClass: + """Test cases for Vector class.""" + + def test_vector_creation(self): + """Test Vector object creation.""" + v = Vector([1, 2, 3]) + assert v.components == [1, 2, 3] + + def test_vector_dot_product(self): + """Test dot product using Vector class.""" + v1 = Vector([1, 2]) + v2 = Vector([3, 4]) + assert v1.dot(v2) == 11 diff --git a/tests/Tests2/__init__.py b/tests/Tests2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Tests2/generator_tests.py b/tests/Tests2/generator_tests.py new file mode 100644 index 00000000..30367bb9 --- /dev/null +++ b/tests/Tests2/generator_tests.py @@ -0,0 +1,236 @@ +import pytest +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../")) +from project.Task2.generator import * + + +class TestPipeline: + """Simplified tests for data processing pipeline.""" + + @pytest.fixture + def sample_data(self): + return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + @pytest.fixture + def empty_data(self): + return [] + + @pytest.mark.parametrize( + "start,end,expected", + [ + (0, 5, [0, 1, 2, 3, 4]), + (1, 4, [1, 2, 3]), + (-2, 3, [-2, -1, 0, 1, 2]), + ], + ) + def test_data_generator(self, start, end, expected): + """Test data generator with different ranges.""" + result = list(data_generator(start, end)) + assert result == expected + + @pytest.mark.parametrize( + "func,expected", + [ + (lambda x: x * 2, [2, 4, 6, 8, 10]), + (lambda x: x**2, [1, 4, 9, 16, 25]), + (str, ["1", "2", "3", "4", "5"]), + ], + ) + def test_map_operation(self, func, expected): + """Test map operation with different functions.""" + data = [1, 2, 3, 4, 5] + mapped = map_operation(func)(data) + result = list(mapped) + assert result == expected + + @pytest.mark.parametrize( + "predicate,expected", + [ + (lambda x: x % 2 == 0, [2, 4, 6, 8, 10]), + (lambda x: x > 5, [6, 7, 8, 9, 10]), + (lambda x: x < 3, [1, 2]), + ], + ) + def test_filter_operation(self, sample_data, predicate, expected): + """Test filter operation with different predicates.""" + filtered = filter_operation(predicate)(sample_data) + result = list(filtered) + assert result == expected + + @pytest.mark.parametrize( + "n,expected", + [ + (0, []), + (1, [1]), + (3, [1, 2, 3]), + (5, [1, 2, 3, 4, 5]), + (100, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + ], + ) + def test_take_operation(self, sample_data, n, expected): + """Test take operation with different counts.""" + taken = take_operation(n)(sample_data) + result = list(taken) + assert result == expected + + @pytest.mark.parametrize( + "n,expected", + [ + (0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + (1, [2, 3, 4, 5, 6, 7, 8, 9, 10]), + (5, [6, 7, 8, 9, 10]), + (9, [10]), + (10, []), + ], + ) + def test_skip_operation(self, sample_data, n, expected): + """Test skip operation with different counts.""" + skipped = skip_operation(n)(sample_data) + result = list(skipped) + assert result == expected + + @pytest.mark.parametrize( + "start,expected", + [ + (0, [(0, 1), (1, 2), (2, 3)]), + (1, [(1, 1), (2, 2), (3, 3)]), + (10, [(10, 1), (11, 2), (12, 3)]), + ], + ) + def test_enumerate_operation(self, start, expected): + """Test enumerate operation with different start values.""" + data = [1, 2, 3] + enumerated = enumerate_operation(start)(data) + result = list(enumerated) + assert result == expected + + @pytest.mark.parametrize( + "func,initializer,expected", + [ + (lambda x, y: x + y, None, 55), + (lambda x, y: x + y, 100, 155), + (lambda x, y: x * y, None, 3628800), + (lambda x, y: x if x > y else y, None, 10), + ], + ) + def test_reduce_operation(self, sample_data, func, initializer, expected): + """Test reduce operation with different functions and initializers.""" + if initializer is not None: + reducer = reduce_operation(func, initializer) + else: + reducer = reduce_operation(func) + + result = reducer(sample_data) + assert result == expected + + @pytest.mark.parametrize( + "collection_type,expected_type", + [ + (list, list), + (tuple, tuple), + (set, set), + ], + ) + def test_collect_different_types(self, sample_data, collection_type, expected_type): + """Test collect operation with different collection types.""" + stream = filter_operation(lambda x: x % 2 == 0)(sample_data) + result = collect(stream, collection_type) + assert isinstance(result, expected_type) + + @pytest.mark.parametrize( + "data,expected_count", + [ + ([1, 2, 3, 4, 5], 5), + ([], 0), + ([1], 1), + (range(10), 10), + ], + ) + def test_count_operation(self, data, expected_count): + """Test count operation with different data.""" + result = count(data) + assert result == expected_count + + def test_zip_operation(self): + """Test zip operation.""" + data1 = [1, 2, 3] + data2 = [10, 20, 30] + zipped = zip_operation(data2)(data1) + result = list(zipped) + assert result == [(1, 10), (2, 20), (3, 30)] + + def test_custom_operation(self): + """Test custom operation.""" + + def double_batch(stream): + for item in stream: + yield item * 2 + + custom_op = custom_operation(double_batch) + result = list(custom_op([1, 2, 3])) + assert result == [2, 4, 6] + + def test_pipeline_basic(self, sample_data): + """Test basic pipeline.""" + result = list(pipeline(sample_data)) + assert result == sample_data + + def test_pipeline_with_operations(self, sample_data): + """Test pipeline with operations.""" + result = list( + pipeline( + sample_data, + filter_operation(lambda x: x % 2 == 0), + map_operation(lambda x: x * 3), + take_operation(3), + ) + ) + assert result == [6, 12, 18] + + def test_pipeline_complex(self, sample_data): + """Test complex pipeline.""" + result = list( + pipeline( + sample_data, + filter_operation(lambda x: x > 3), + skip_operation(2), + take_operation(3), + map_operation(str), + enumerate_operation(1), + ) + ) + assert result == [(1, "6"), (2, "7"), (3, "8")] + + def test_empty_data(self, empty_data): + """Test with empty data.""" + result = list( + pipeline( + empty_data, + map_operation(lambda x: x * 2), + filter_operation(lambda x: x > 0), + take_operation(5), + ) + ) + assert result == [] + + assert count(empty_data) == 0 + + def test_lazy_evaluation(self): + """Test lazy evaluation.""" + processed_items = [] + + def track_processing(x): + processed_items.append(x) + return x * 2 + + gen = data_generator(1, 1000) + stream = pipeline(gen, map_operation(track_processing), take_operation(3)) + + assert len(processed_items) == 0 + + result = list(stream) + + assert len(processed_items) == 3 + assert result == [2, 4, 6]