From 06edbaabe0633b1887c698f0e828d9a07c9f9c01 Mon Sep 17 00:00:00 2001 From: Praveen Mudalgeri Date: Mon, 28 Jul 2025 10:32:53 +0530 Subject: [PATCH 1/2] Remove pluggable-libs submodule and cleanup (#433) --- patterns/behavioral/memento.py | 3 +++ patterns/behavioral/servant.py | 6 ++++- patterns/other/blackboard.py | 5 ++++ patterns/structural/mvc.py | 28 +++++++++++++++------- tests/behavioral/test_publish_subscribe.py | 14 ++++++----- tests/behavioral/test_servant.py | 4 +++- tests/structural/test_bridge.py | 14 ++++++----- tests/test_hsm.py | 17 +++++++------ 8 files changed, 60 insertions(+), 31 deletions(-) diff --git a/patterns/behavioral/memento.py b/patterns/behavioral/memento.py index c1bc7f0b..1714e8de 100644 --- a/patterns/behavioral/memento.py +++ b/patterns/behavioral/memento.py @@ -47,6 +47,7 @@ def Transactional(method): :param method: The function to be decorated. """ + def transaction(obj, *args, **kwargs): state = memento(obj) try: @@ -54,8 +55,10 @@ def transaction(obj, *args, **kwargs): except Exception as e: state() raise e + return transaction + class NumObj: def __init__(self, value): self.value = value diff --git a/patterns/behavioral/servant.py b/patterns/behavioral/servant.py index de939a60..776c4126 100644 --- a/patterns/behavioral/servant.py +++ b/patterns/behavioral/servant.py @@ -19,8 +19,10 @@ References: - https://en.wikipedia.org/wiki/Servant_(design_pattern) """ + import math + class Position: """Representation of a 2D position with x and y coordinates.""" @@ -28,6 +30,7 @@ def __init__(self, x, y): self.x = x self.y = y + class Circle: """Representation of a circle defined by a radius and a position.""" @@ -35,6 +38,7 @@ 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.""" @@ -65,7 +69,7 @@ def calculate_area(shape): ValueError: If the shape type is unsupported. """ if isinstance(shape, Circle): - return math.pi * shape.radius ** 2 + return math.pi * shape.radius**2 elif isinstance(shape, Rectangle): return shape.width * shape.height else: diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard.py index 58fbdb98..0269a3e7 100644 --- a/patterns/other/blackboard.py +++ b/patterns/other/blackboard.py @@ -15,6 +15,7 @@ class AbstractExpert(ABC): """Abstract class for experts in the blackboard system.""" + @abstractmethod def __init__(self, blackboard) -> None: self.blackboard = blackboard @@ -31,6 +32,7 @@ def contribute(self) -> None: class Blackboard: """The blackboard system that holds the common state.""" + def __init__(self) -> None: self.experts: list = [] self.common_state = { @@ -46,6 +48,7 @@ def add_expert(self, expert: AbstractExpert) -> None: class Controller: """The controller that manages the blackboard system.""" + def __init__(self, blackboard: Blackboard) -> None: self.blackboard = blackboard @@ -63,6 +66,7 @@ def run_loop(self): class Student(AbstractExpert): """Concrete class for a student expert.""" + def __init__(self, blackboard) -> None: super().__init__(blackboard) @@ -79,6 +83,7 @@ def contribute(self) -> None: class Scientist(AbstractExpert): """Concrete class for a scientist expert.""" + def __init__(self, blackboard) -> None: super().__init__(blackboard) diff --git a/patterns/structural/mvc.py b/patterns/structural/mvc.py index 24b0017a..5726b089 100644 --- a/patterns/structural/mvc.py +++ b/patterns/structural/mvc.py @@ -11,6 +11,7 @@ class Model(ABC): """The Model is the data layer of the application.""" + @abstractmethod def __iter__(self) -> Any: pass @@ -29,6 +30,7 @@ def item_type(self) -> str: class ProductModel(Model): """The Model is the data layer of the application.""" + class Price(float): """A polymorphic way to pass a float with a particular __str__ functionality.""" @@ -56,12 +58,15 @@ def get(self, product: str) -> dict: class View(ABC): """The View is the presentation layer of the application.""" + @abstractmethod def show_item_list(self, item_type: str, item_list: list) -> None: pass @abstractmethod - def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None: + def show_item_information( + self, item_type: str, item_name: str, item_info: dict + ) -> None: """Will look for item information by iterating over key,value pairs yielded by item_info.items()""" pass @@ -73,6 +78,7 @@ def item_not_found(self, item_type: str, item_name: str) -> None: class ConsoleView(View): """The View is the presentation layer of the application.""" + def show_item_list(self, item_type: str, item_list: list) -> None: print(item_type.upper() + " LIST:") for item in item_list: @@ -84,7 +90,9 @@ def capitalizer(string: str) -> str: """Capitalizes the first letter of a string and lowercases the rest.""" return string[0].upper() + string[1:].lower() - def show_item_information(self, item_type: str, item_name: str, item_info: dict) -> None: + def show_item_information( + self, item_type: str, item_name: str, item_info: dict + ) -> None: """Will look for item information by iterating over key,value pairs""" print(item_type.upper() + " INFORMATION:") printout = "Name: %s" % item_name @@ -99,6 +107,7 @@ def item_not_found(self, item_type: str, item_name: str) -> None: class Controller: """The Controller is the intermediary between the Model and the View.""" + def __init__(self, model_class: Model, view_class: View) -> None: self.model: Model = model_class self.view: View = view_class @@ -124,15 +133,17 @@ def show_item_information(self, item_name: str) -> None: class Router: """The Router is the entry point of the application.""" + def __init__(self): self.routes = {} def register( - self, - path: str, - controller_class: type[Controller], - model_class: type[Model], - view_class: type[View]) -> None: + self, + path: str, + controller_class: type[Controller], + model_class: type[Model], + view_class: type[View], + ) -> None: model_instance: Model = model_class() view_instance: View = view_class() self.routes[path] = controller_class(model_instance, view_instance) @@ -184,7 +195,7 @@ def main(): controller: Controller = router.resolve(argv[1]) action: str = str(argv[2]) if len(argv) > 2 else "" - args: str = ' '.join(map(str, argv[3:])) if len(argv) > 3 else "" + args: str = " ".join(map(str, argv[3:])) if len(argv) > 3 else "" if hasattr(controller, action): command = getattr(controller, action) @@ -201,4 +212,5 @@ def main(): print(f"Command {action} not found in the controller.") import doctest + doctest.testmod() diff --git a/tests/behavioral/test_publish_subscribe.py b/tests/behavioral/test_publish_subscribe.py index c153da5b..8bb7130c 100644 --- a/tests/behavioral/test_publish_subscribe.py +++ b/tests/behavioral/test_publish_subscribe.py @@ -48,9 +48,10 @@ def test_provider_shall_update_affected_subscribers_with_published_subscription( sub2 = Subscriber("sub 2 name", pro) sub2.subscribe("sub 2 msg 1") sub2.subscribe("sub 2 msg 2") - with patch.object(sub1, "run") as mock_subscriber1_run, patch.object( - sub2, "run" - ) as mock_subscriber2_run: + with ( + patch.object(sub1, "run") as mock_subscriber1_run, + patch.object(sub2, "run") as mock_subscriber2_run, + ): pro.update() cls.assertEqual(mock_subscriber1_run.call_count, 0) cls.assertEqual(mock_subscriber2_run.call_count, 0) @@ -58,9 +59,10 @@ def test_provider_shall_update_affected_subscribers_with_published_subscription( pub.publish("sub 1 msg 2") pub.publish("sub 2 msg 1") pub.publish("sub 2 msg 2") - with patch.object(sub1, "run") as mock_subscriber1_run, patch.object( - sub2, "run" - ) as mock_subscriber2_run: + with ( + patch.object(sub1, "run") as mock_subscriber1_run, + patch.object(sub2, "run") as mock_subscriber2_run, + ): pro.update() expected_sub1_calls = [call("sub 1 msg 1"), call("sub 1 msg 2")] mock_subscriber1_run.assert_has_calls(expected_sub1_calls) diff --git a/tests/behavioral/test_servant.py b/tests/behavioral/test_servant.py index e5edb70d..dd487171 100644 --- a/tests/behavioral/test_servant.py +++ b/tests/behavioral/test_servant.py @@ -7,18 +7,20 @@ 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(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) diff --git a/tests/structural/test_bridge.py b/tests/structural/test_bridge.py index 7fa8a278..6665f327 100644 --- a/tests/structural/test_bridge.py +++ b/tests/structural/test_bridge.py @@ -8,9 +8,10 @@ class BridgeTest(unittest.TestCase): def test_bridge_shall_draw_with_concrete_api_implementation(cls): ci1 = DrawingAPI1() ci2 = DrawingAPI2() - with patch.object(ci1, "draw_circle") as mock_ci1_draw_circle, patch.object( - ci2, "draw_circle" - ) as mock_ci2_draw_circle: + with ( + patch.object(ci1, "draw_circle") as mock_ci1_draw_circle, + patch.object(ci2, "draw_circle") as mock_ci2_draw_circle, + ): sh1 = CircleShape(1, 2, 3, ci1) sh1.draw() cls.assertEqual(mock_ci1_draw_circle.call_count, 1) @@ -33,9 +34,10 @@ def test_bridge_shall_scale_both_api_circles_with_own_implementation(cls): sh2.scale(SCALE_FACTOR) cls.assertEqual(sh1._radius, EXPECTED_CIRCLE1_RADIUS) cls.assertEqual(sh2._radius, EXPECTED_CIRCLE2_RADIUS) - with patch.object(sh1, "scale") as mock_sh1_scale_circle, patch.object( - sh2, "scale" - ) as mock_sh2_scale_circle: + with ( + patch.object(sh1, "scale") as mock_sh1_scale_circle, + patch.object(sh2, "scale") as mock_sh2_scale_circle, + ): sh1.scale(2) sh2.scale(2) cls.assertEqual(mock_sh1_scale_circle.call_count, 1) diff --git a/tests/test_hsm.py b/tests/test_hsm.py index f42323a9..5b49fb97 100644 --- a/tests/test_hsm.py +++ b/tests/test_hsm.py @@ -58,15 +58,14 @@ def test_given_standby_on_message_switchover_shall_set_active(cls): cls.assertEqual(isinstance(cls.hsm._current_state, Active), True) def test_given_standby_on_message_switchover_shall_call_hsm_methods(cls): - with patch.object( - cls.hsm, "_perform_switchover" - ) as mock_perform_switchover, patch.object( - cls.hsm, "_check_mate_status" - ) as mock_check_mate_status, patch.object( - cls.hsm, "_send_switchover_response" - ) as mock_send_switchover_response, patch.object( - cls.hsm, "_next_state" - ) as mock_next_state: + with ( + patch.object(cls.hsm, "_perform_switchover") as mock_perform_switchover, + patch.object(cls.hsm, "_check_mate_status") as mock_check_mate_status, + patch.object( + cls.hsm, "_send_switchover_response" + ) as mock_send_switchover_response, + patch.object(cls.hsm, "_next_state") as mock_next_state, + ): cls.hsm.on_message("switchover") cls.assertEqual(mock_perform_switchover.call_count, 1) cls.assertEqual(mock_check_mate_status.call_count, 1) From 7ec9916dd991e566bcd6864bcd7f61d9b5e4c491 Mon Sep 17 00:00:00 2001 From: Praveen Mudalgeri Date: Sat, 2 Aug 2025 23:46:48 +0530 Subject: [PATCH 2/2] Fix GitHub Actions output format error for multiline file lists --- .github/workflows/lint_pr.yml | 62 ++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/workflows/lint_pr.yml b/.github/workflows/lint_pr.yml index f18e5c2e..ca1eaddf 100644 --- a/.github/workflows/lint_pr.yml +++ b/.github/workflows/lint_pr.yml @@ -9,8 +9,8 @@ jobs: steps: - uses: actions/checkout@v3 with: - fetch-depth: 0 # To get all history for git diff commands - + fetch-depth: 0 # To get all history for git diff commands + - name: Get changed Python files id: changed-files run: | @@ -31,7 +31,7 @@ jobs: CHANGED_FILES="$CHANGED_FILES $SETUP_PY_CHANGED" fi fi - + # Check if any Python files were changed and set the output accordingly if [ -z "$CHANGED_FILES" ]; then echo "No Python files changed" @@ -40,9 +40,11 @@ jobs: else echo "Changed Python files: $CHANGED_FILES" echo "has_python_changes=true" >> $GITHUB_OUTPUT - echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT + # Use proper delimiter formatting for GitHub Actions + FILES_SINGLE_LINE=$(echo "$CHANGED_FILES" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') + echo "files=$FILES_SINGLE_LINE" >> $GITHUB_OUTPUT fi - + - name: PR information if: ${{ github.event_name == 'pull_request' }} run: | @@ -68,27 +70,27 @@ jobs: echo "No Python files were changed. Skipping linting." exit 0 fi - + - uses: actions/checkout@v3 with: fetch-depth: 0 - + - uses: actions/setup-python@v4 with: python-version: 3.12 - + - uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip- - + - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt - + # Flake8 linting - name: Lint with flake8 if: ${{ matrix.tool == 'flake8' }} @@ -96,7 +98,7 @@ jobs: run: | echo "Linting files: ${{ needs.check_changes.outputs.files }}" flake8 ${{ needs.check_changes.outputs.files }} --count --show-source --statistics - + # Format checking with isort and black - name: Format check if: ${{ matrix.tool == 'format' }} @@ -106,7 +108,7 @@ jobs: isort --profile black --check ${{ needs.check_changes.outputs.files }} echo "Checking format with black for: ${{ needs.check_changes.outputs.files }}" black --check ${{ needs.check_changes.outputs.files }} - + # Type checking with mypy - name: Type check with mypy if: ${{ matrix.tool == 'mypy' }} @@ -114,7 +116,7 @@ jobs: run: | echo "Type checking: ${{ needs.check_changes.outputs.files }}" mypy --ignore-missing-imports ${{ needs.check_changes.outputs.files }} - + # Run tests with pytest - name: Run tests with pytest if: ${{ matrix.tool == 'pytest' }} @@ -122,11 +124,11 @@ jobs: run: | echo "Running pytest discovery..." python -m pytest --collect-only -v - + # First run any test files that correspond to changed files echo "Running tests for changed files..." changed_files="${{ needs.check_changes.outputs.files }}" - + # Extract module paths from changed files modules=() for file in $changed_files; do @@ -137,13 +139,13 @@ jobs: modules+=("$module_path") fi done - + # Run tests for each module for module in "${modules[@]}"; do echo "Testing module: $module" python -m pytest -xvs tests/ -k "$module" || true done - + # Then run doctests on the changed files echo "Running doctests for changed files..." for file in $changed_files; do @@ -152,13 +154,13 @@ jobs: python -m pytest --doctest-modules -v $file || true fi done - + # Check Python version compatibility - name: Check Python version compatibility if: ${{ matrix.tool == 'pyupgrade' }} id: pyupgrade run: pyupgrade --py312-plus ${{ needs.check_changes.outputs.files }} - + # Run tox - name: Run tox if: ${{ matrix.tool == 'tox' }} @@ -166,12 +168,12 @@ jobs: run: | echo "Running tox integration for changed files..." changed_files="${{ needs.check_changes.outputs.files }}" - + # Create a temporary tox configuration that extends the original one echo "[tox]" > tox_pr.ini echo "envlist = py312" >> tox_pr.ini echo "skip_missing_interpreters = true" >> tox_pr.ini - + echo "[testenv]" >> tox_pr.ini echo "setenv =" >> tox_pr.ini echo " COVERAGE_FILE = .coverage.{envname}" >> tox_pr.ini @@ -182,11 +184,11 @@ jobs: echo " coverage" >> tox_pr.ini echo " python" >> tox_pr.ini echo "commands =" >> tox_pr.ini - + # Check if we have any implementation files that changed pattern_files=0 test_files=0 - + for file in $changed_files; do if [[ $file == patterns/* ]]; then pattern_files=1 @@ -194,12 +196,12 @@ jobs: test_files=1 fi done - + # Only run targeted tests, no baseline echo " # Run specific tests for changed files" >> tox_pr.ini - + has_tests=false - + # Add coverage-focused test commands for file in $changed_files; do if [[ $file == *.py ]]; then @@ -246,18 +248,18 @@ jobs: fi fi done - + # If we didn't find any specific tests to run, mention it if [ "$has_tests" = false ]; then echo " python -c \"print('No specific tests found for changed files. Consider adding tests.')\"" >> tox_pr.ini # Add a minimal test to avoid failure, but ensure it generates coverage data echo " coverage run -m pytest -xvs --cov=patterns --cov-append -k \"not integration\" --no-header" >> tox_pr.ini fi - + # Add coverage report command echo " coverage combine" >> tox_pr.ini echo " coverage report -m" >> tox_pr.ini - + # Run tox with the custom configuration echo "Running tox with custom PR configuration..." echo "======================== TOX CONFIG ========================" @@ -272,7 +274,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - + - name: Summarize results run: | echo "## Pull Request Lint Results" >> $GITHUB_STEP_SUMMARY