From 368f8fbed150d8803f69ad592c39dd503f9aae18 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 22:34:37 +0000 Subject: [PATCH 1/2] ci: only run the release workflow on the upstream repository The Release workflow triggers on every push to master. On a fork that means semantic-release rewrites the version in pyproject.toml, commits the change to the default branch, tags it and then tries to publish the result to PyPI under the upstream package name. Gate the job on the repository name so forks keep a clean master. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PjZRpyoEfSCLjTx5ctzQmJ --- .github/workflows/publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8df3ca7f..2119bfad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,6 +11,10 @@ permissions: jobs: release: + # Releases are cut from the upstream repository only. Without this guard the + # workflow also runs on forks, where semantic-release rewrites the version in + # pyproject.toml, commits to the default branch and attempts a PyPI upload. + if: github.repository == 'appium/python-client' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 From 0edc68dc9ce42416e66e94e30f27455dc5b35741 Mon Sep 17 00:00:00 2001 From: Dor-bl <59066376+Dor-bl@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:44:35 +0000 Subject: [PATCH 2/2] refactor: reduce parameter complexity in swipe action helper Support both tuple coordinate pairs and individual coordinate arguments in `ActionHelpers.swipe`. Extracted point extraction logic into helper methods `_extract_start_point` and `_extract_end_point_and_duration` to reduce function complexity and parameter count. Added `@overload` type hints and unit tests. --- appium/webdriver/extensions/action_helpers.py | 89 +++++++++++-- test/unit/webdriver/action_helpers_test.py | 122 ++++++++++++++++++ 2 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 test/unit/webdriver/action_helpers_test.py diff --git a/appium/webdriver/extensions/action_helpers.py b/appium/webdriver/extensions/action_helpers.py index be32ca9f..a65d3865 100644 --- a/appium/webdriver/extensions/action_helpers.py +++ b/appium/webdriver/extensions/action_helpers.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast, overload from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.actions import interaction @@ -132,35 +132,108 @@ def tap(self, positions: list[tuple[int, int]], duration: int | None = None) -> actions.perform() return self - def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 0) -> Self: + @overload + def swipe( + self, + start_x: tuple[int, int] | list[int], + start_y: tuple[int, int] | list[int], + duration: int = 0, + ) -> Self: ... + + @overload + def swipe( + self, + start_x: int, + start_y: int, + end_x: int, + end_y: int, + duration: int = 0, + ) -> Self: ... + + def swipe( + self, + start_x: tuple[int, int] | list[int] | int | None = None, + start_y: tuple[int, int] | list[int] | int | None = None, + *args: Any, + duration: int = 0, + **kwargs: Any, + ) -> Self: """Swipe from one point to another point, for an optional duration. Args: - start_x: x-coordinate at which to start - start_y: y-coordinate at which to start - end_x: x-coordinate at which to stop - end_y: y-coordinate at which to stop + start_x: x-coordinate at which to start, or (x, y) tuple/list for start point + start_y: y-coordinate at which to start, or (x, y) tuple/list for end point + *args: Positionally passed end_x, end_y, and optional duration duration: defines the swipe speed as time taken to swipe from point a to point b, in ms. + **kwargs: Keyword arguments such as start_point, end_point, end_x, end_y Usage: + driver.swipe((100, 100), (100, 400)) driver.swipe(100, 100, 100, 400) + driver.swipe(start_x=100, start_y=100, end_x=100, end_y=400, duration=500) Returns: Union['WebDriver', 'ActionHelpers']: Self instance """ + sx, sy = self._extract_start_point(start_x, start_y, **kwargs) + ex, ey, duration = self._extract_end_point_and_duration(start_x, start_y, args, duration, **kwargs) + touch_input = PointerInput(interaction.POINTER_TOUCH, 'touch') actions = ActionChains(cast('WebDriver', self)) actions.w3c_actions = ActionBuilder(self, mouse=touch_input) - actions.w3c_actions.pointer_action.move_to_location(start_x, start_y) + actions.w3c_actions.pointer_action.move_to_location(sx, sy) actions.w3c_actions.pointer_action.pointer_down() if duration > 0: actions.w3c_actions = ActionBuilder(self, mouse=touch_input, duration=duration) - actions.w3c_actions.pointer_action.move_to_location(end_x, end_y) + actions.w3c_actions.pointer_action.move_to_location(ex, ey) actions.w3c_actions.pointer_action.release() actions.perform() return self + def _extract_start_point( + self, + start_x: tuple[int, int] | list[int] | int | None, + start_y: tuple[int, int] | list[int] | int | None, + **kwargs: Any, + ) -> tuple[int, int]: + if 'start_point' in kwargs: + sp = kwargs['start_point'] + return sp[0], sp[1] + if isinstance(start_x, (tuple, list)): + return start_x[0], start_x[1] + if start_x is not None: + if start_y is None or isinstance(start_y, (tuple, list)): + raise ValueError('start_y must be an integer when start_x is an integer.') + return int(start_x), int(start_y) + raise ValueError('Start coordinates or start point must be provided.') + + def _extract_end_point_and_duration( + self, + start_x: tuple[int, int] | list[int] | int | None, + start_y: tuple[int, int] | list[int] | int | None, + args: tuple[Any, ...], + duration: int, + **kwargs: Any, + ) -> tuple[int, int, int]: + if 'end_point' in kwargs: + ep = kwargs['end_point'] + return ep[0], ep[1], duration + if isinstance(start_y, (tuple, list)) and isinstance(start_x, (tuple, list)): + d = int(args[0]) if args and duration == 0 else duration + return start_y[0], start_y[1], d + if len(args) > 1: + end_x_val, end_y_val, *rest = args + d = int(rest[0]) if rest and duration == 0 else duration + return int(end_x_val), int(end_y_val), d + if 'end_x' in kwargs and 'end_y' in kwargs: + return int(kwargs['end_x']), int(kwargs['end_y']), duration + if args and isinstance(args[0], (tuple, list)): + ep, *rest = args + d = int(rest[0]) if rest and duration == 0 else duration + return ep[0], ep[1], d + raise ValueError('End coordinates or end point must be provided.') + def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> Self: """Flick from one point to another point. diff --git a/test/unit/webdriver/action_helpers_test.py b/test/unit/webdriver/action_helpers_test.py new file mode 100644 index 00000000..8e86c887 --- /dev/null +++ b/test/unit/webdriver/action_helpers_test.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import httpretty +import pytest + +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestActionHelpers: + @httpretty.activate + def test_swipe_with_tuples(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/actions'), + body=json.dumps({'value': None}), + ) + + driver.swipe((100, 200), (300, 400)) + + req_body = get_httpretty_request_body(httpretty.last_request()) + actions = req_body['actions'][0]['actions'] + assert actions[0]['type'] == 'pointerMove' + assert actions[0]['x'] == 100 + assert actions[0]['y'] == 200 + assert actions[1]['type'] == 'pointerDown' + assert actions[2]['type'] == 'pointerMove' + assert actions[2]['x'] == 300 + assert actions[2]['y'] == 400 + assert actions[3]['type'] == 'pointerUp' + + @httpretty.activate + def test_swipe_with_tuples_and_duration(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/actions'), + body=json.dumps({'value': None}), + ) + + driver.swipe((100, 200), (300, 400), 500) + + req_body = get_httpretty_request_body(httpretty.last_request()) + actions = req_body['actions'][0]['actions'] + assert actions[0]['type'] == 'pointerMove' + assert actions[0]['x'] == 100 + assert actions[0]['y'] == 200 + assert actions[1]['type'] == 'pointerDown' + assert actions[2]['type'] == 'pointerMove' + assert actions[2]['x'] == 300 + assert actions[2]['y'] == 400 + assert actions[2]['duration'] == 500 + assert actions[3]['type'] == 'pointerUp' + + @httpretty.activate + def test_swipe_with_integers(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/actions'), + body=json.dumps({'value': None}), + ) + + driver.swipe(100, 200, 300, 400) + + req_body = get_httpretty_request_body(httpretty.last_request()) + actions = req_body['actions'][0]['actions'] + assert actions[0]['type'] == 'pointerMove' + assert actions[0]['x'] == 100 + assert actions[0]['y'] == 200 + assert actions[1]['type'] == 'pointerDown' + assert actions[2]['type'] == 'pointerMove' + assert actions[2]['x'] == 300 + assert actions[2]['y'] == 400 + assert actions[3]['type'] == 'pointerUp' + + @httpretty.activate + def test_swipe_with_integers_and_duration(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/actions'), + body=json.dumps({'value': None}), + ) + + driver.swipe(100, 200, 300, 400, duration=800) + + req_body = get_httpretty_request_body(httpretty.last_request()) + actions = req_body['actions'][0]['actions'] + assert actions[0]['type'] == 'pointerMove' + assert actions[0]['x'] == 100 + assert actions[0]['y'] == 200 + assert actions[1]['type'] == 'pointerDown' + assert actions[2]['type'] == 'pointerMove' + assert actions[2]['x'] == 300 + assert actions[2]['y'] == 400 + assert actions[2]['duration'] == 800 + assert actions[3]['type'] == 'pointerUp' + + @httpretty.activate + def test_swipe_invalid_args(self): + driver = android_w3c_driver() + + with pytest.raises(ValueError): + driver.swipe(100, 200) + + with pytest.raises(ValueError): + driver.swipe((100, 200), 300)