From bee048e5eac8208ea74c51743d1564cc95adb483 Mon Sep 17 00:00:00 2001 From: Brian Jones Date: Sat, 19 Jul 2025 01:15:01 +0100 Subject: [PATCH 1/2] 11 project format (#446) * Migrates to pyproject.toml Switches the build system from setup.py to pyproject.toml. This change standardizes the project's build configuration, improves dependency management, and prepares the project for future enhancements. Removes unused requirements file. * Updates minimum Python version Corrects the minimum Python version specified in pyproject.toml. The project now requires Python 3.10 or higher. * Uses installable package for dev dependencies Updates the installation of development dependencies in the linting workflow to use the installable package. This simplifies dependency management and aligns with standard Python packaging practices. * Adds pyupgrade as a dev dependency Adds `pyupgrade` to the development dependencies. This enables developers to automatically upgrade Python syntax to more modern versions, improving code maintainability and readability. --- .github/workflows/lint_python.yml | 2 +- pyproject.toml | 34 +++++++++++++++++++++++++------ requirements-dev.txt | 9 -------- setup.py | 14 ------------- 4 files changed, 29 insertions(+), 30 deletions(-) delete mode 100644 requirements-dev.txt delete mode 100644 setup.py diff --git a/.github/workflows/lint_python.yml b/.github/workflows/lint_python.yml index 19d6c078..288a94b0 100644 --- a/.github/workflows/lint_python.yml +++ b/.github/workflows/lint_python.yml @@ -11,7 +11,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-dev.txt + pip install .[dev] - name: Lint with flake8 run: flake8 ./patterns --count --show-source --statistics continue-on-error: true diff --git a/pyproject.toml b/pyproject.toml index 57f6fbe7..dfac5da9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,46 @@ [build-system] -requires = ["setuptools>=42", "wheel"] +requires = ["setuptools >= 77.0.3"] build-backend = "setuptools.build_meta" [project] -name = "patterns" +name = "python-patterns" description = "A collection of design patterns and idioms in Python." version = "0.1.0" readme = "README.md" -requires-python = ">=3.9" -license = {text = "MIT"} +requires-python = ">=3.10" classifiers = [ - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] +dependencies= [ +] + +maintainers=[ + { name="faif" } +] + +[project.urls] +Homepage = "https://github.com/faif/python-patterns" +Repository = "https://github.com/faif/python-patterns" +"Bug Tracker" = "https://github.com/faif/python-patterns/issues" +Contributors = "https://github.com/faif/python-patterns/graphs/contributors" [project.optional-dependencies] -dev = ["pytest", "pytest-cov", "pytest-randomly", "flake8", "mypy", "coverage"] +dev = [ + "mypy", + "pipx>=1.7.1", + "pyupgrade", + "pytest>=6.2.0", + "pytest-cov>=2.11.0", + "pytest-randomly>=3.1.0", + "black>=25.1.0", + "build>=1.2.2", + "isort>=5.7.0", + "flake8>=7.1.0", + "tox>=4.25.0" +] [tool.setuptools] packages = ["patterns"] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 4aaa81f2..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -mypy -pyupgrade -pytest>=6.2.0 -pytest-cov>=2.11.0 -pytest-randomly>=3.1.0 -black>=25.1.0 -isort>=5.7.0 -flake8>=7.1.0 -tox>=4.25.0 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 72bc2b46..00000000 --- a/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -from setuptools import find_packages, setup - -setup( - name="patterns", - packages=find_packages(), - description="A collection of design patterns and idioms in Python.", - classifiers=[ - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - ], -) From 3c0725a9e667c76641d6c5899346fdda940b5bc1 Mon Sep 17 00:00:00 2001 From: M Date: Fri, 18 Jul 2025 19:16:39 -0500 Subject: [PATCH 2/2] Servant pattern (#413) * Add docstring for Servant behavioral design pattern * Implement Servant class * Add docstest examples * Add testing for Servant class * Use fixtures for circle and rectangle --- patterns/behavioral/servant.py | 127 +++++++++++++++++++++++++++++++ tests/behavioral/test_servant.py | 37 +++++++++ 2 files changed, 164 insertions(+) create mode 100644 patterns/behavioral/servant.py create mode 100644 tests/behavioral/test_servant.py diff --git a/patterns/behavioral/servant.py b/patterns/behavioral/servant.py new file mode 100644 index 00000000..de939a60 --- /dev/null +++ b/patterns/behavioral/servant.py @@ -0,0 +1,127 @@ +""" +Implementation of the Servant design pattern. + +The Servant design pattern is a behavioral pattern used to offer functionality +to a group of classes without requiring them to inherit from a base class. + +This pattern involves creating a Servant class that provides certain services +or functionalities. These services are used by other classes which do not need +to be related through a common parent class. It is particularly useful in +scenarios where adding the desired functionality through inheritance is impractical +or would lead to a rigid class hierarchy. + +This pattern is characterized by the following: + +- A Servant class that provides specific services or actions. +- Client classes that need these services, but do not derive from the Servant class. +- The use of the Servant class by the client classes to perform actions on their behalf. + +References: +- https://en.wikipedia.org/wiki/Servant_(design_pattern) +""" +import math + +class Position: + """Representation of a 2D position with x and y coordinates.""" + + def __init__(self, x, y): + self.x = x + self.y = y + +class Circle: + """Representation of a circle defined by a radius and a position.""" + + def __init__(self, radius, position: Position): + self.radius = radius + self.position = position + +class Rectangle: + """Representation of a rectangle defined by width, height, and a position.""" + + def __init__(self, width, height, position: Position): + self.width = width + self.height = height + self.position = position + + +class GeometryTools: + """ + Servant class providing geometry-related services, including area and + perimeter calculations and position updates. + """ + + @staticmethod + def calculate_area(shape): + """ + Calculate the area of a given shape. + + Args: + shape: The geometric shape whose area is to be calculated. + + Returns: + The area of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return math.pi * shape.radius ** 2 + elif isinstance(shape, Rectangle): + return shape.width * shape.height + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def calculate_perimeter(shape): + """ + Calculate the perimeter of a given shape. + + Args: + shape: The geometric shape whose perimeter is to be calculated. + + Returns: + The perimeter of the shape. + + Raises: + ValueError: If the shape type is unsupported. + """ + if isinstance(shape, Circle): + return 2 * math.pi * shape.radius + elif isinstance(shape, Rectangle): + return 2 * (shape.width + shape.height) + else: + raise ValueError("Unsupported shape type") + + @staticmethod + def move_to(shape, new_position: Position): + """ + Move a given shape to a new position. + + Args: + shape: The geometric shape to be moved. + new_position: The new position to move the shape to. + """ + shape.position = new_position + print(f"Moved to ({shape.position.x}, {shape.position.y})") + + +def main(): + """ + >>> servant = GeometryTools() + >>> circle = Circle(5, Position(0, 0)) + >>> rectangle = Rectangle(3, 4, Position(0, 0)) + >>> servant.calculate_area(circle) + 78.53981633974483 + >>> servant.calculate_perimeter(rectangle) + 14 + >>> servant.move_to(circle, Position(3, 4)) + Moved to (3, 4) + >>> servant.move_to(rectangle, Position(5, 6)) + Moved to (5, 6) + """ + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/tests/behavioral/test_servant.py b/tests/behavioral/test_servant.py new file mode 100644 index 00000000..e5edb70d --- /dev/null +++ b/tests/behavioral/test_servant.py @@ -0,0 +1,37 @@ +from patterns.behavioral.servant import GeometryTools, Circle, Rectangle, Position +import pytest +import math + + +@pytest.fixture +def circle(): + return Circle(3, Position(0, 0)) + +@pytest.fixture +def rectangle(): + return Rectangle(4, 5, Position(0, 0)) + + +def test_calculate_area(circle, rectangle): + assert GeometryTools.calculate_area(circle) == math.pi * 3 ** 2 + assert GeometryTools.calculate_area(rectangle) == 4 * 5 + + with pytest.raises(ValueError): + GeometryTools.calculate_area("invalid shape") + +def test_calculate_perimeter(circle, rectangle): + assert GeometryTools.calculate_perimeter(circle) == 2 * math.pi * 3 + assert GeometryTools.calculate_perimeter(rectangle) == 2 * (4 + 5) + + with pytest.raises(ValueError): + GeometryTools.calculate_perimeter("invalid shape") + + +def test_move_to(circle, rectangle): + new_position = Position(1, 1) + GeometryTools.move_to(circle, new_position) + assert circle.position == new_position + + new_position = Position(1, 1) + GeometryTools.move_to(rectangle, new_position) + assert rectangle.position == new_position