diff --git a/pyproject.toml b/pyproject.toml index bca22759..7d36060a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,7 @@ lint.select = [ "I", # isort "N", # pep8-naming "PTH", # flake8-use-pathlib + "RUF", # ruff-specific rules "SIM", # flake8-simplify "TRY", # tryceratops "UP", # pyupgrade diff --git a/src/data_morph/bounds/bounding_box.py b/src/data_morph/bounds/bounding_box.py index b05f2c82..bbb30d18 100644 --- a/src/data_morph/bounds/bounding_box.py +++ b/src/data_morph/bounds/bounding_box.py @@ -97,7 +97,7 @@ def __eq__(self, other: BoundingBox) -> bool: def __repr__(self) -> str: return '\n' f' x={self.x_bounds}' '\n' f' y={self.y_bounds}' - def adjust_bounds(self, x: Number = None, y: Number = None) -> None: + def adjust_bounds(self, x: Number | None = None, y: Number | None = None) -> None: """ Adjust bounding box range. diff --git a/src/data_morph/data/dataset.py b/src/data_morph/data/dataset.py index 4a0ac297..aa26c834 100644 --- a/src/data_morph/data/dataset.py +++ b/src/data_morph/data/dataset.py @@ -1,5 +1,7 @@ """Class representing a dataset for morphing.""" +from __future__ import annotations + from numbers import Number import matplotlib.pyplot as plt @@ -39,13 +41,13 @@ class Dataset: Utility for creating :class:`Dataset` objects from CSV files. """ - _REQUIRED_COLUMNS = ['x', 'y'] + _REQUIRED_COLUMNS = ('x', 'y') def __init__( self, name: str, df: pd.DataFrame, - scale: Number = None, + scale: Number | None = None, ) -> None: self.df: pd.DataFrame = self._validate_data(df).pipe(self._scale_data, scale) """pandas.DataFrame: DataFrame containing columns x and y.""" @@ -181,7 +183,7 @@ def _validate_data(self, data: pd.DataFrame) -> pd.DataFrame: @plot_with_custom_style def plot( - self, ax: Axes = None, show_bounds: bool = True, title: str = 'default' + self, ax: Axes | None = None, show_bounds: bool = True, title: str = 'default' ) -> Axes: """ Plot the dataset and its bounds. diff --git a/src/data_morph/data/loader.py b/src/data_morph/data/loader.py index 064c099e..cb625824 100644 --- a/src/data_morph/data/loader.py +++ b/src/data_morph/data/loader.py @@ -1,9 +1,12 @@ """Load data for morphing.""" +from __future__ import annotations + from importlib.resources import files from itertools import zip_longest from numbers import Number from pathlib import Path +from typing import ClassVar import matplotlib.pyplot as plt import numpy as np @@ -41,7 +44,7 @@ class DataLoader: """ _DATA_PATH: str = 'data/starter_shapes/' - _DATASETS: dict = { + _DATASETS: ClassVar[dict[str, str]] = { 'bunny': 'bunny.csv', 'cat': 'cat.csv', 'dino': 'dino.csv', @@ -66,7 +69,7 @@ def __init__(self) -> None: def load_dataset( cls, dataset: str, - scale: Number = None, + scale: Number | None = None, ) -> Dataset: """ Load dataset. diff --git a/src/data_morph/shapes/bases/line_collection.py b/src/data_morph/shapes/bases/line_collection.py index 04f88c93..671f74ce 100644 --- a/src/data_morph/shapes/bases/line_collection.py +++ b/src/data_morph/shapes/bases/line_collection.py @@ -1,5 +1,7 @@ """Base class for shapes that are composed of lines.""" +from __future__ import annotations + from collections.abc import Iterable from numbers import Number @@ -97,7 +99,7 @@ def distance(self, x: Number, y: Number) -> float: ) @plot_with_custom_style - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. diff --git a/src/data_morph/shapes/bases/point_collection.py b/src/data_morph/shapes/bases/point_collection.py index 9103747e..ede45963 100644 --- a/src/data_morph/shapes/bases/point_collection.py +++ b/src/data_morph/shapes/bases/point_collection.py @@ -1,5 +1,7 @@ """Base class for shapes that are composed of points.""" +from __future__ import annotations + from collections.abc import Iterable from numbers import Number @@ -52,7 +54,7 @@ def distance(self, x: Number, y: Number) -> float: ) @plot_with_custom_style - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. diff --git a/src/data_morph/shapes/bases/shape.py b/src/data_morph/shapes/bases/shape.py index b629ce4b..36690f60 100644 --- a/src/data_morph/shapes/bases/shape.py +++ b/src/data_morph/shapes/bases/shape.py @@ -103,7 +103,7 @@ def _recursive_repr(self, attr: str | None = None) -> str: ) @abstractmethod - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. diff --git a/src/data_morph/shapes/circles.py b/src/data_morph/shapes/circles.py index 12bdb46b..ce665373 100644 --- a/src/data_morph/shapes/circles.py +++ b/src/data_morph/shapes/circles.py @@ -1,5 +1,7 @@ """Shapes that are circular in nature.""" +from __future__ import annotations + from numbers import Number import matplotlib.pyplot as plt @@ -33,7 +35,7 @@ class Circle(Shape): The radius of the circle. """ - def __init__(self, dataset: Dataset, radius: Number = None) -> None: + def __init__(self, dataset: Dataset, radius: Number | None = None) -> None: self.center: np.ndarray = dataset.df[['x', 'y']].mean().to_numpy() """numpy.ndarray: The (x, y) coordinates of the circle's center.""" @@ -63,7 +65,7 @@ def distance(self, x: Number, y: Number) -> float: ) @plot_with_custom_style - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. @@ -159,7 +161,7 @@ def distance(self, x: Number, y: Number) -> float: ) @plot_with_custom_style - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. diff --git a/src/data_morph/shapes/factory.py b/src/data_morph/shapes/factory.py index ca2f1d23..a0c8be23 100644 --- a/src/data_morph/shapes/factory.py +++ b/src/data_morph/shapes/factory.py @@ -2,6 +2,7 @@ from itertools import zip_longest from numbers import Number +from typing import ClassVar import matplotlib.pyplot as plt import numpy as np @@ -56,7 +57,7 @@ class ShapeFactory: The starting dataset to morph into other shapes. """ - _SHAPE_MAPPING: dict = { + _SHAPE_MAPPING: ClassVar[dict[str, type[Shape]]] = { 'bullseye': Bullseye, 'circle': Circle, 'high_lines': HighLines, diff --git a/tests/data/test_dataset.py b/tests/data/test_dataset.py index 5e2374a7..38d6e26c 100644 --- a/tests/data/test_dataset.py +++ b/tests/data/test_dataset.py @@ -62,7 +62,7 @@ def test_validate_data_fix_column_casing(self, starter_shapes_dir): df = pd.read_csv(starter_shapes_dir / 'dino.csv').rename(columns={'x': 'X'}) dataset = Dataset('dino', df) - assert not dataset.df[dataset._REQUIRED_COLUMNS].empty + assert not dataset.df[list(dataset._REQUIRED_COLUMNS)].empty @pytest.mark.bounds @pytest.mark.parametrize( diff --git a/tests/shapes/test_circles.py b/tests/shapes/test_circles.py index d45bdc32..9deb814c 100644 --- a/tests/shapes/test_circles.py +++ b/tests/shapes/test_circles.py @@ -1,7 +1,6 @@ """Test circles module.""" import re -from collections.abc import Iterable from numbers import Number import numpy as np @@ -16,7 +15,7 @@ class CirclesModuleTestBase: """Base for testing circle shapes.""" shape_name: str - distance_test_cases: Iterable[tuple[Iterable[Number], float]] + distance_test_cases: tuple[tuple[tuple[Number], float]] repr_regex: str @pytest.fixture(scope='class') @@ -40,7 +39,7 @@ class TestBullseye(CirclesModuleTestBase): """Test the Bullseye class.""" shape_name = 'bullseye' - distance_test_cases = [[(20, 50), 3.660254], [(10, 25), 9.08004]] + distance_test_cases = (((20, 50), 3.660254), ((10, 25), 9.08004)) repr_regex = ( r'^\n' r' circles=\n' @@ -61,7 +60,7 @@ class TestCircle(CirclesModuleTestBase): """Test the Circle class.""" shape_name = 'circle' - distance_test_cases = [[(20, 50), 10.490381], [(10, 25), 15.910168]] + distance_test_cases = (((20, 50), 10.490381), ((10, 25), 15.910168)) repr_regex = '^' + CIRCLE_REPR + '$' def test_is_circle(self, shape): @@ -79,7 +78,7 @@ class TestRings(CirclesModuleTestBase): """Test the Rings class.""" shape_name = 'rings' - distance_test_cases = [[(20, 50), 3.16987], [(10, 25), 9.08004]] + distance_test_cases = (((20, 50), 3.16987), ((10, 25), 9.08004)) repr_regex = ( r'^\n' r' circles=\n' diff --git a/tests/shapes/test_lines.py b/tests/shapes/test_lines.py index b20d860b..d285d82e 100644 --- a/tests/shapes/test_lines.py +++ b/tests/shapes/test_lines.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Iterable from numbers import Number import numpy as np @@ -15,9 +14,9 @@ class LinesModuleTestBase: """Base for testing line-based shapes.""" shape_name: str - distance_test_cases: Iterable[tuple[Iterable[Number], float]] + distance_test_cases: tuple[tuple[tuple[Number], float]] expected_line_count: int - expected_slopes: Iterable[Number] | Number + expected_slopes: tuple[Number] | Number @pytest.fixture(scope='class') def shape(self, shape_factory): @@ -67,7 +66,7 @@ class TestHighLines(ParallelLinesModuleTestBase): """Test the HighLines class.""" shape_name = 'high_lines' - distance_test_cases = [[(20, 50), 6.0], [(30, 60), 4.0]] + distance_test_cases = (((20, 50), 6.0), ((30, 60), 4.0)) expected_line_count = 2 expected_slopes = 0 @@ -76,7 +75,7 @@ class TestHorizontalLines(ParallelLinesModuleTestBase): """Test the HorizontalLines class.""" shape_name = 'h_lines' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 2.5]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.5)) expected_line_count = 5 expected_slopes = 0 @@ -85,7 +84,7 @@ class TestSlantDownLines(ParallelLinesModuleTestBase): """Test the SlantDownLines class.""" shape_name = 'slant_down' - distance_test_cases = [[(20, 50), 1.664101], [(30, 60), 0.554700]] + distance_test_cases = (((20, 50), 1.664101), ((30, 60), 0.554700)) expected_line_count = 5 expected_slopes = -1.5 @@ -94,7 +93,7 @@ class TestSlantUpLines(ParallelLinesModuleTestBase): """Test the SlantUpLines class.""" shape_name = 'slant_up' - distance_test_cases = [[(20, 50), 1.664101], [(30, 60), 1.109400]] + distance_test_cases = (((20, 50), 1.664101), ((30, 60), 1.109400)) expected_line_count = 5 expected_slopes = 1.5 @@ -103,7 +102,7 @@ class TestVerticalLines(ParallelLinesModuleTestBase): """Test the VerticalLines class.""" shape_name = 'v_lines' - distance_test_cases = [[(35, 60), 5.0], [(30, 60), 0.0]] + distance_test_cases = (((35, 60), 5.0), ((30, 60), 0.0)) expected_line_count = 5 expected_slopes = np.inf @@ -112,7 +111,7 @@ class TestWideLines(ParallelLinesModuleTestBase): """Test the WideLines class.""" shape_name = 'wide_lines' - distance_test_cases = [[(26, 50), 0], [(30, 60), 4.0]] + distance_test_cases = (((26, 50), 0), ((30, 60), 4.0)) expected_line_count = 2 expected_slopes = np.inf @@ -121,14 +120,14 @@ class TestXLines(LinesModuleTestBase): """Test the XLines class.""" shape_name = 'x' - distance_test_cases = [ - [(8, 83), 0], # edge of X line - [(20, 65), 0], # middle of X (intersection point) - [(19, 64), 0.277350], # off the X - [(10, 20), 27.073973], # off the X - ] + distance_test_cases = ( + ((8, 83), 0), # edge of X line + ((20, 65), 0), # middle of X (intersection point) + ((19, 64), 0.277350), # off the X + ((10, 20), 27.073973), # off the X + ) expected_line_count = 2 - expected_slopes = [-1.5, 1.5] + expected_slopes = (-1.5, 1.5) def test_lines_form_an_x(self, shape): """Test that the lines form an X.""" diff --git a/tests/shapes/test_points.py b/tests/shapes/test_points.py index 6d409464..61a109ff 100644 --- a/tests/shapes/test_points.py +++ b/tests/shapes/test_points.py @@ -1,6 +1,5 @@ """Test points module.""" -from collections.abc import Iterable from numbers import Number import numpy as np @@ -13,7 +12,7 @@ class PointsModuleTestBase: """Base for testing point-based shapes.""" shape_name: str - distance_test_cases: Iterable[tuple[Iterable[Number], float]] + distance_test_cases: tuple[tuple[tuple[Number], float]] @pytest.fixture(scope='class') def shape(self, shape_factory): @@ -33,7 +32,7 @@ class TestDotsGrid(PointsModuleTestBase): """Test the DotsGrid class.""" shape_name = 'dots' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 3.640055]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 3.640055)) expected_point_count = 9 def test_init(self, shape): @@ -72,21 +71,21 @@ class TestHeart(PointsModuleTestBase): """Test the Heart class.""" shape_name = 'heart' - distance_test_cases = [ - [(19.89946048, 54.82281916), 0.0], - [(10.84680454, 70.18556376), 0.0], - [(29.9971295, 67.66402445), 0.0], - [(27.38657942, 62.417184), 0.0], - [(20, 50), 4.567369], - [(10, 80), 8.564365], - ] + distance_test_cases = ( + ((19.89946048, 54.82281916), 0.0), + ((10.84680454, 70.18556376), 0.0), + ((29.9971295, 67.66402445), 0.0), + ((27.38657942, 62.417184), 0.0), + ((20, 50), 4.567369), + ((10, 80), 8.564365), + ) class TestScatter(PointsModuleTestBase): """Test the Scatter class.""" shape_name = 'scatter' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 0.0], [(-500, -150), 0.0]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 0.0), ((-500, -150), 0.0)) class ParabolaTestBase(PointsModuleTestBase): @@ -108,7 +107,7 @@ class TestDownParabola(ParabolaTestBase): """Test the DownParabola class.""" shape_name = 'down_parab' - distance_test_cases = [[(20, 50), 7.929688], [(30, 60), 3.455534]] + distance_test_cases = (((20, 50), 7.929688), ((30, 60), 3.455534)) positive_quadratic_term = False x_index = 0 y_index = 1 @@ -118,7 +117,7 @@ class TestLeftParabola(ParabolaTestBase): """Test the LeftParabola class.""" shape_name = 'left_parab' - distance_test_cases = [[(50, 20), 46.31798], [(10, 77), 0.0]] + distance_test_cases = (((50, 20), 46.31798), ((10, 77), 0.0)) positive_quadratic_term = False x_index = 1 y_index = 0 @@ -128,7 +127,7 @@ class TestRightParabola(ParabolaTestBase): """Test the RightParabola class.""" shape_name = 'right_parab' - distance_test_cases = [[(50, 20), 38.58756], [(10, 77), 7.740692]] + distance_test_cases = (((50, 20), 38.58756), ((10, 77), 7.740692)) positive_quadratic_term = True x_index = 1 y_index = 0 @@ -138,7 +137,7 @@ class TestUpParabola(ParabolaTestBase): """Test the UpParabola class.""" shape_name = 'up_parab' - distance_test_cases = [[(0, 0), 53.774155], [(30, 60), 5.2576809]] + distance_test_cases = (((0, 0), 53.774155), ((30, 60), 5.2576809)) positive_quadratic_term = True x_index = 0 y_index = 1 @@ -148,28 +147,28 @@ class TestClub(PointsModuleTestBase): """Test the Club class.""" shape_name = 'club' - distance_test_cases = [ - [(19.639387, 73.783711), 0.0], # top lobe - [(12.730310, 60.295844), 0.0], # bottom left lobe - [(27.630301, 60.920443), 0.0], # bottom right lobe - [(20.304761, 55.933333), 0.0], # top of stem - [(18.8, 57.076666), 0.0], # left part of stem - [(20.933333, 57.823333), 0.0], # right part of stem - [(0, 0), 58.717591], - [(20, 50), 5.941155], - [(10, 80), 10.288055], - ] + distance_test_cases = ( + ((19.639387, 73.783711), 0.0), # top lobe + ((12.730310, 60.295844), 0.0), # bottom left lobe + ((27.630301, 60.920443), 0.0), # bottom right lobe + ((20.304761, 55.933333), 0.0), # top of stem + ((18.8, 57.076666), 0.0), # left part of stem + ((20.933333, 57.823333), 0.0), # right part of stem + ((0, 0), 58.717591), + ((20, 50), 5.941155), + ((10, 80), 10.288055), + ) class TestSpade(PointsModuleTestBase): """Test the Spade class.""" shape_name = 'spade' - distance_test_cases = [ - [(19.97189615, 75.43271708), 0], - [(23.75, 55), 0], - [(11.42685318, 59.11304904), 0], - [(20, 75), 0.2037185], - [(0, 0), 57.350348], - [(10, 80), 10.968080], - ] + distance_test_cases = ( + ((19.97189615, 75.43271708), 0), + ((23.75, 55), 0), + ((11.42685318, 59.11304904), 0), + ((20, 75), 0.2037185), + ((0, 0), 57.350348), + ((10, 80), 10.968080), + ) diff --git a/tests/shapes/test_polygons.py b/tests/shapes/test_polygons.py index c97838c9..99a6c538 100644 --- a/tests/shapes/test_polygons.py +++ b/tests/shapes/test_polygons.py @@ -1,6 +1,5 @@ """Test polygons module.""" -from collections.abc import Iterable from numbers import Number import numpy as np @@ -13,7 +12,7 @@ class PolygonsModuleTestBase: """Base for testing polygon shapes.""" shape_name: str - distance_test_cases: Iterable[tuple[Iterable[Number], float]] + distance_test_cases: tuple[tuple[tuple[Number], float]] expected_line_count: int @pytest.fixture(scope='class') @@ -52,7 +51,7 @@ class TestDiamond(PolygonsModuleTestBase): """Test the Diamond class.""" shape_name = 'diamond' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 2.773501]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.773501)) expected_line_count = 4 def test_slopes(self, slopes): @@ -64,7 +63,7 @@ class TestRectangle(PolygonsModuleTestBase): """Test the Rectangle class.""" shape_name = 'rectangle' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 2.0]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.0)) expected_line_count = 4 def test_slopes(self, slopes): @@ -76,5 +75,5 @@ class TestStar(PolygonsModuleTestBase): """Test the Star class.""" shape_name = 'star' - distance_test_cases = [[(20, 50), 5.856516], [(30, 60), 3.709127]] + distance_test_cases = (((20, 50), 5.856516), ((30, 60), 3.709127)) expected_line_count = 10 diff --git a/tests/test_morpher.py b/tests/test_morpher.py index 01ad373b..b740d386 100644 --- a/tests/test_morpher.py +++ b/tests/test_morpher.py @@ -276,6 +276,6 @@ def test_freeze_animation_frames( # check that the images are indeed the same if write_images and freeze_for: assert len(image_hashes.keys()) == 1 - assert list(image_hashes.values())[0] == freeze_for + assert next(iter(image_hashes.values())) == freeze_for else: assert not image_hashes