diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 6959f26dc..000000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -exclude = - __pycache__, -ignore = E501 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f7d2b9c6a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.approved.txt text eol=lf \ No newline at end of file diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 93b6c0bbe..4045e3ba0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,10 +9,10 @@ jobs: continue-on-error: true strategy: matrix: - python-version: [3.9.23, 3.13.5, 3.14.0-rc.3] # pypy-3.9 - # python-version: [{earliest: 3.9}, {latest: 3.13.0}] # pypy-3.9 - rf-version: [6.1.1, 7.3.2] - selenium-version: [4.28.1, 4.29.0, 4.30.0, 4.31.0, 4.32.0, 4.33.0, 4.34.2] + python-version: [3.10.16, 3.13.5, 3.14.4, pypy-3.10] + # python-version: [{earliest: 3.10}, {latest: 3.14.0-rc.3}, {pypy: pypy-3.10}] + rf-version: [6.1.1, 7.4.2] + selenium-version: [4.29.0, 4.30.0, 4.31.0, 4.32.0, 4.33.0, 4.34.2, 4.43.0] browser: [chrome] # firefox, chrome, headlesschrome, edge steps: @@ -44,12 +44,12 @@ jobs: export DISPLAY=:99.0 Xvfb -ac :99 -screen 0 1280x1024x16 > /dev/null 2>&1 & - name: Install dependencies - if: matrix.python-version != 'pypy-3.7' + if: matrix.python-version != 'pypy-3.10' run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt - name: Install dependencies for pypy - if: matrix.python-version == 'pypy-3.9' + if: matrix.python-version == 'pypy-3.10' run: | python -m pip install --upgrade pip pip install -r requirements.txt @@ -68,7 +68,7 @@ jobs: echo "WEBDRIVERPATH=$($SELENIUM_MANAGER_EXE --browser chrome --debug | awk '/INFO[[:space:]]Driver path:/ {print $NF;exit}')" >> "$GITHUB_ENV" echo "$WEBDRIVERPATH" - name: Generate stub file for ${{ matrix.python-version }} - if: matrix.python-version != 'pypy-3.9' + if: matrix.python-version != 'pypy-3.10' run: | invoke gen-stub @@ -84,12 +84,12 @@ jobs: # xvfb-run --auto-servernum python atest/run.py --zip ${{ matrix.browser }} - name: Run tests with latest python and latest robot framework - if: matrix.python-version == '3.13.0' && matrix.rf-version == '7.2.2' + if: matrix.python-version == '3.14.4' && matrix.rf-version == '7.4.1' run: | xvfb-run --auto-servernum python atest/run.py --zip ${{ matrix.browser }} # - name: Run tests with Selenium Grid - # if: matrix.python-version == '3.11' && matrix.rf-version == '3.2.2' && matrix.python-version != 'pypy-3.9' + # if: matrix.python-version == '3.11' && matrix.rf-version == '3.2.2' && matrix.python-version != 'pypy-3.10' # run: | # wget --no-verbose --output-document=./selenium-server-standalone.jar http://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar # sudo chmod u+x ./selenium-server-standalone.jar diff --git a/.github/workflows/Select.yml b/.github/workflows/Select.yml index dcb89c7e4..9becf8bc0 100644 --- a/.github/workflows/Select.yml +++ b/.github/workflows/Select.yml @@ -10,9 +10,9 @@ jobs: matrix: config: - description: latest - python-version: 3.13.10 - rf-version: 7.4.1 - selenium-version: 4.39.0 + python-version: 3.14.4 + rf-version: 7.4.2 + selenium-version: 4.43.0 browser: chrome - description: previous python-version: 3.12.12 @@ -24,19 +24,23 @@ jobs: rf-version: 6.1.1 selenium-version: 4.37.0 browser: chrome - + - description: latest + python-version: 3.14.4 + rf-version: 7.4.1 + selenium-version: 4.39.0 + browser: firefox steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Configuration Description run: | echo "${{ matrix.config.description }} configuration" echo "Testing with RF v${{ matrix.config.rf-version }}, Selenium v${{ matrix.config.selenium-version}}, Python v${{ matrix.config.python-version }} under ${{ matrix.config.browser }}" - name: Set up Python ${{ matrix.config.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.config.python-version }} - name: Setup ${{ matrix.config.browser }} browser - uses: browser-actions/setup-chrome@v1 + uses: browser-actions/setup-chrome@v2 with: chrome-version: latest install-dependencies: true @@ -74,6 +78,42 @@ jobs: - uses: actions/upload-artifact@v4 if: failure() with: - name: sl_$${{ matrix.config.python-version }}_$${{ matrix.config.rf-version }}_$${{ matrix.config.selenium-version }}_$${{ matrix.config.browser }} + name: sl_${{ matrix.config.python-version }}_${{ matrix.config.rf-version }}_${{ matrix.config.selenium-version }}_${{ matrix.config.browser }} path: atest/zip_results - overwrite: true \ No newline at end of file + overwrite: true + ruff: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14.4" + + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Ruff format check + id: format + continue-on-error: true + run: | + python -m invoke formatter --check + + - name: Ruff lint + id: lint + continue-on-error: true + run: | + python -m invoke lint + + - name: Fail if any Ruff step failed + if: always() + run: | + echo "format outcome: ${{ steps.format.outcome }}" + echo "lint outcome: ${{ steps.lint.outcome }}" + if [ "${{ steps.format.outcome }}" != "success" ] || [ "${{ steps.lint.outcome }}" != "success" ]; then + exit 1 + fi \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ffb9d6ff8..677200817 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -118,21 +118,23 @@ needed in internal code. When docstrings are added, they should follow `PEP-257`_. See `Documentation`_ section below for more details about documentation syntax, generating docs, etc. -The code should be formatted with `Black`_ and errors found by `flake8`_ -should be fixed. Black and flake8 can be run by using -command:: +The code should be formatted and linted with `Ruff`_. See Development commands below for more details. - inv lint +Development commands +~~~~~~~~~~~~~~~~~~~~ -By default flake8 ignores line length error E501, but it does not ignore -warning W503. In practice Black formats list access like this:: +Use `invoke`_ tasks for common local checks and test runs:: - list[1 : 2] + inv formatter --check # Check formatting with Ruff + inv formatter # Format source files with Ruff + inv lint # Run Ruff lint checks + inv lint --fix # Apply safe Ruff lint fixes + inv utest # Run unit tests + inv atest # Run acceptance tests (headlesschrome) -But flake8 will display an warning about it. This should be manually -fixed to look like:: - - list[1:2] +Run these before opening a pull request so local results are close to CI. +Use the project virtual environment and pinned dependencies from +``requirements-dev.txt`` for consistent results across local runs and CI. Documentation ------------- @@ -160,7 +162,7 @@ individual keywords. Keyword documentation can be easily created using `invoke`_ task:: - inv keyword_documentation + inv kw-docs Resulting docs should be verified before the code is committed. @@ -245,5 +247,4 @@ the same code as your changes. In that case you should .. _utest/README.rst: https://github.com/robotframework/SeleniumLibrary/blob/master/utest/README.rst .. _sync your fork: https://help.github.com/articles/syncing-a-fork/ .. _resolve conflicts: https://help.github.com/articles/resolving-a-merge-conflict-from-the-command-line -.. _Black: https://github.com/psf/black -.. _flake8: https://github.com/PyCQA/flake8 \ No newline at end of file +.. _Ruff: https://github.com/astral-sh/ruff \ No newline at end of file diff --git a/README.rst b/README.rst index 1f4346024..2ec34f134 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ SeleniumLibrary_ is a web testing library for `Robot Framework`_ that utilizes the Selenium_ tool internally. The project is hosted on GitHub_ and downloads can be found from PyPI_. -SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.13. +SeleniumLibrary currently works with Selenium 4. It supports Python 3.10 through 3.13. In addition to the normal Python_ interpreter, it works also with PyPy_. diff --git a/atest/acceptance/1-plugin/OpenBrowserExample.py b/atest/acceptance/1-plugin/OpenBrowserExample.py index 2cb006f24..ab04db3bf 100644 --- a/atest/acceptance/1-plugin/OpenBrowserExample.py +++ b/atest/acceptance/1-plugin/OpenBrowserExample.py @@ -121,7 +121,12 @@ def create_driver( ) def create_seleniumwire( - self, desired_capabilities, remote_url, options=None, service_log_path=None, service=None, + self, + desired_capabilities, + remote_url, + options=None, + service_log_path=None, + service=None, ): logger.info(self.extra_dictionary) return webdriver.Chrome() diff --git a/atest/acceptance/2-event_firing_webdriver/event_firing_webdriver.robot b/atest/acceptance/2-event_firing_webdriver/event_firing_webdriver.robot index 5e9a07e6d..d4877f042 100644 --- a/atest/acceptance/2-event_firing_webdriver/event_firing_webdriver.robot +++ b/atest/acceptance/2-event_firing_webdriver/event_firing_webdriver.robot @@ -10,7 +10,7 @@ ${event_firing_or_none} ${NONE} *** Test Cases *** Open Browser To Start Page - [Tags] NoGrid + [Tags] NoGrid SKIP_ON_WINDOWS [Documentation] ... LOG 1:30 DEBUG Wrapping driver to event_firing_webdriver. ... LOG 1:32 INFO Got driver also from SeleniumLibrary. diff --git a/atest/acceptance/create_webdriver.robot b/atest/acceptance/create_webdriver.robot index 9f3796ddf..d87c80086 100644 --- a/atest/acceptance/create_webdriver.robot +++ b/atest/acceptance/create_webdriver.robot @@ -8,7 +8,7 @@ Create Webdriver Creates Functioning WebDriver [Documentation] ... LOG 1:1 INFO REGEXP: Creating an instance of the \\w+ WebDriver. ... LOG 1:18 DEBUG REGEXP: Created \\w+ WebDriver instance with session id (\\w|-)+. - [Tags] Known Issue Internet Explorer Known Issue Safari + [Tags] Known Issue Internet Explorer Known Issue Safari SKIP_ON_WINDOWS [Setup] Set Driver Variables Create Webdriver ${DRIVER_NAME} kwargs=${KWARGS} Go To ${FRONT_PAGE} diff --git a/atest/acceptance/entry_point.robot b/atest/acceptance/entry_point.robot index a92c3740a..631d1a553 100644 --- a/atest/acceptance/entry_point.robot +++ b/atest/acceptance/entry_point.robot @@ -7,7 +7,7 @@ Entry Point Version ${process} = Run Process ... python -m SeleniumLibrary.entry --version ... shell=True - ... cwd=${EXECDIR}/src + ... cwd=${EXECDIR}${/}src Log ${process.stdout} Log ${process.stderr} Should Be Equal As Integers ${process.rc} 0 @@ -17,18 +17,18 @@ Entry Point Version Entry Point Translation ${process} = Run Process - ... python -m SeleniumLibrary.entry translation ${OUTPUT_DIR}/translation.json + ... python -m SeleniumLibrary.entry translation ${OUTPUT_DIR}${/}translation.json ... shell=True - ... cwd=${EXECDIR}/src + ... cwd=${EXECDIR}${/}src Log ${process.stdout} Log ${process.stderr} Should Be Equal As Integers ${process.rc} 0 Should Be Empty ${process.stderr} - Should Be Equal ${process.stdout} Translation file created in ${OUTPUT_DIR}/translation.json + Should Be Equal ${process.stdout} Translation file created in ${OUTPUT_DIR}${/}translation.json ${process} = Run Process - ... python -m SeleniumLibrary.entry translation --compare ${OUTPUT_DIR}/translation.json + ... python -m SeleniumLibrary.entry translation --compare ${OUTPUT_DIR}${/}translation.json ... shell=True - ... cwd=${EXECDIR}/src + ... cwd=${EXECDIR}${/}src Log ${process.stdout} Log ${process.stderr} Should Be Equal As Integers ${process.rc} 0 diff --git a/atest/acceptance/keywords/mouse.robot b/atest/acceptance/keywords/mouse.robot index 5aff5c109..734eb2d01 100644 --- a/atest/acceptance/keywords/mouse.robot +++ b/atest/acceptance/keywords/mouse.robot @@ -15,7 +15,7 @@ Mouse Over ... Mouse Over not_there Mouse Over Error - [Tags] Known Issue Safari + [Tags] Known Issue Safari Known Issue Firefox Mouse Over el_for_mouseover Sleep 0.1secs Textfield Value Should Be el_for_mouseover mouseover el_for_mouseover diff --git a/atest/acceptance/keywords/page_load_timeout.robot b/atest/acceptance/keywords/page_load_timeout.robot index 100bfe2c7..d701f1c0c 100644 --- a/atest/acceptance/keywords/page_load_timeout.robot +++ b/atest/acceptance/keywords/page_load_timeout.robot @@ -6,6 +6,7 @@ Test Teardown Close Browser And Reset Page Load Timeout *** Test Cases *** Should Open Browser With Default Page Load Timeout + [Tags] SKIP_ON_WINDOWS [Documentation] Verify that 'Open Browser' changes the page load timeout. ... LOG 1.1.1:26 DEBUG REGEXP: POST http://localhost:\\d{2,5}/session/[a-f0-9-]+/timeouts {['\\\"]pageLoad['\\\"]: 300000} ... LOG 1.1.1:28 DEBUG STARTS: Remote response: status=200 diff --git a/atest/acceptance/keywords/textfields.robot b/atest/acceptance/keywords/textfields.robot index 6c3a41a3e..b8f225e1a 100644 --- a/atest/acceptance/keywords/textfields.robot +++ b/atest/acceptance/keywords/textfields.robot @@ -81,5 +81,10 @@ Attempt Clear Element Text On Non-Editable Field Open Browser To Start Page Disabling Chrome Leaked Password Detection [Arguments] ${alias}=${None} - Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... options=add_experimental_option("prefs", {"profile.password_manager_leak_detection": False}) alias=${alias} \ No newline at end of file + ${browser}= Evaluate "${BROWSER}".replace(" ", "").lower() + IF "${browser}" in ["chrome", "googlechrome", "gc", "headlesschrome"] + Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + ... options=add_experimental_option("prefs", {"profile.password_manager_leak_detection": False}) alias=${alias} + ELSE + Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} alias=${alias} + END \ No newline at end of file diff --git a/atest/acceptance/multiple_browsers_options.robot b/atest/acceptance/multiple_browsers_options.robot index 2349c9195..81c9c94ae 100644 --- a/atest/acceptance/multiple_browsers_options.robot +++ b/atest/acceptance/multiple_browsers_options.robot @@ -8,6 +8,7 @@ Documentation Creating test which would work on all browser is not possible. *** Test Cases *** Chrome Browser With Selenium Options As String + [Tags] SKIP_ON_WINDOWS [Documentation] ... LOG 1:13 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* ... LOG 1:13 DEBUG REGEXP: .*args['\\\"]: \\\[['\\\"]--disable-dev-shm-usage['\\\"].* @@ -15,6 +16,7 @@ Chrome Browser With Selenium Options As String ... desired_capabilities=${DESIRED_CAPABILITIES} options=add_argument("--disable-dev-shm-usage") Chrome Browser With Selenium Options As String With Attribute As True + [Tags] SKIP_ON_WINDOWS [Documentation] ... LOG 1:13 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* ... LOG 1:13 DEBUG REGEXP: .*args['\\\"]: \\\[['\\\"]--disable-dev-shm-usage['\\\"].* @@ -23,7 +25,7 @@ Chrome Browser With Selenium Options As String With Attribute As True ... desired_capabilities=${DESIRED_CAPABILITIES} options=add_argument ( "--disable-dev-shm-usage" ) ; add_argument ( "--headless=new" ) Chrome Browser With Selenium Options With Complex Object - [Tags] NoGrid + [Tags] NoGrid SKIP_ON_WINDOWS [Documentation] ... LOG 1:13 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* ... LOG 1:13 DEBUG REGEXP: .*['\\\"]mobileEmulation['\\\"]: {['\\\"]deviceName['\\\"]: ['\\\"]Galaxy S5['\\\"].* @@ -32,6 +34,7 @@ Chrome Browser With Selenium Options With Complex Object ... desired_capabilities=${DESIRED_CAPABILITIES} options=add_argument ( "--disable-dev-shm-usage" ) ; add_experimental_option( "mobileEmulation" , { 'deviceName' : 'Galaxy S5'}) Chrome Browser With Selenium Options Object + [Tags] SKIP_ON_WINDOWS [Documentation] ... LOG 2:13 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* ... LOG 2:13 DEBUG REGEXP: .*args['\\\"]: \\\[['\\\"]--disable-dev-shm-usage['\\\"].* @@ -46,6 +49,7 @@ Chrome Browser With Selenium Options Invalid Method Chrome Browser With Selenium Options Argument With Semicolon + [Tags] SKIP_ON_WINDOWS [Documentation] ... LOG 1:13 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* ... LOG 1:13 DEBUG REGEXP: .*\\\[['\\\"]has;semicolon['\\\"].* diff --git a/atest/acceptance/multiple_browsers_service.robot b/atest/acceptance/multiple_browsers_service.robot index e47c917a0..2f98d99ec 100644 --- a/atest/acceptance/multiple_browsers_service.robot +++ b/atest/acceptance/multiple_browsers_service.robot @@ -2,6 +2,7 @@ Suite Teardown Close All Browsers Library ../resources/testlibs/get_driver_path.py Resource resource.robot +Test Tags SKIP_ON_WINDOWS # Force Tags Known Issue Firefox Known Issue Safari Known Issue Internet Explorer Documentation Creating test which would work on all browser is not possible. ... These tests are for Chrome only. @@ -11,11 +12,13 @@ Chrome Browser With Chrome Service As String [Documentation] ... LOG 2:3 DEBUG STARTS: Started executable: ... LOG 2:4 DEBUG GLOB: POST*/session* + [Tags] Known Issue Firefox Known Issue Safari Known Issue Internet Explorer ${driver_path}= Get Driver Path Chrome Open Browser ${FRONT PAGE} Chrome remote_url=${REMOTE_URL} ... service=executable_path='${driver_path}' Chrome Browser With Chrome Service As String With service_args As List + [Tags] Known Issue Firefox Known Issue Safari Known Issue Internet Explorer Open Browser ${FRONT PAGE} Chrome remote_url=${REMOTE_URL} ... service=service_args=['--append-log', '--readable-timestamp']; log_output='${OUTPUT_DIR}/chromedriverlog.txt' File Should Exist ${OUTPUT_DIR}/chromedriverlog.txt @@ -23,6 +26,7 @@ Chrome Browser With Chrome Service As String With service_args As List # ... service=service_args=['--append-log', '--readable-timestamp'] Firefox Browser With Firefox Service As String + [Tags] Known Issue Chrome Known Issue Safari Known Issue Internet Explorer [Documentation] ... LOG 2:3 DEBUG STARTS: Started executable: ... LOG 2:4 DEBUG GLOB: POST*/session* diff --git a/atest/acceptance/open_and_close.robot b/atest/acceptance/open_and_close.robot index 6a0d7a75e..187edad7e 100644 --- a/atest/acceptance/open_and_close.robot +++ b/atest/acceptance/open_and_close.robot @@ -15,6 +15,7 @@ Close Browser Does Nothing When No Browser Is Opened Close Browser Browser Open With Not Well-Formed URL Should Close + [Tags] SKIP_ON_WINDOWS [Documentation] Verify after incomplete 'Open Browser' browser closes ... LOG 1.1:34 DEBUG STARTS: Opened browser with session id ... LOG 1.1:34 DEBUG REGEXP: .*but failed to open url.* diff --git a/atest/resources/testlibs/get_driver_path.py b/atest/resources/testlibs/get_driver_path.py index 95c11e924..19d838e0e 100644 --- a/atest/resources/testlibs/get_driver_path.py +++ b/atest/resources/testlibs/get_driver_path.py @@ -18,6 +18,7 @@ def _import_options(self, browser): return options.Options """ + from selenium import webdriver from selenium.webdriver.common import driver_finder import importlib @@ -30,12 +31,12 @@ def get_driver_path(browser): options = importlib.import_module(f"selenium.webdriver.{browser}.options") args = inspect.signature(driver_finder.DriverFinder.__init__).parameters.keys() - if ('service' in args) and ('options' in args): + if ("service" in args) and ("options" in args): # Selenium V4.20.0 or greater finder = driver_finder.DriverFinder(service.Service(), options.Options()) return finder.get_driver_path() else: # Selenium v4.19.0 and prior finder = driver_finder.DriverFinder() - func = getattr(finder, 'get_path') + func = getattr(finder, "get_path") return finder.get_path(service.Service(), options.Options()) diff --git a/atest/run.py b/atest/run.py index d386ea7db..637e86547 100755 --- a/atest/run.py +++ b/atest/run.py @@ -181,7 +181,7 @@ def _grid_status(status=False, role="hub"): @contextmanager -def http_server(interpreter, port:int): +def http_server(interpreter, port: int): serverlog = open(os.path.join(RESULTS_DIR, "serverlog.txt"), "w") interpreter = "python" if not interpreter else interpreter process = subprocess.Popen( @@ -211,11 +211,18 @@ def execute_tests(interpreter, browser, rf_options, grid, event_firing, port): if platform.system() == "Darwin": runner.append("--exclude") runner.append("SKIP_ON_MAC") - + if platform.system() == "Windows": + runner.append("--exclude") + runner.append("SKIP_ON_WINDOWS") options.extend([opt.format(browser=browser) for opt in ROBOT_OPTIONS]) if rf_options: options += rf_options - options += ["--exclude", f"known issue {browser.replace('headless', '')}", "--exclude", "triage"] + options += [ + "--exclude", + f"known issue {browser.replace('headless', '')}", + "--exclude", + "triage", + ] command = runner if grid: command += [ @@ -259,7 +266,7 @@ def process_output(browser): return exit.code -def create_zip(browser = None): +def create_zip(browser=None): if os.path.exists(ZIP_DIR): shutil.rmtree(ZIP_DIR) os.mkdir(ZIP_DIR) diff --git a/docs/index.html b/docs/index.html index 128edf2fa..15053c378 100644 --- a/docs/index.html +++ b/docs/index.html @@ -29,7 +29,7 @@
SeleniumLibrary is a web testing library for Robot Framework that utilizes the Selenium tool internally. The project is hosted on GitHub and downloads can be found from PyPI.
-SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.13. +
SeleniumLibrary currently works with Selenium 4. It supports Python 3.10 through 3.13. In addition to the normal Python interpreter, it works also with PyPy.
SeleniumLibrary is based on the "old SeleniumLibrary" that was forked to
diff --git a/pyproject.toml b/pyproject.toml
index dd1b9775e..6a8f881ef 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,20 +1,51 @@
-[tool.black]
-target-version = ['py36']
-exclude = '''
-/(
- | \.git
- | \.venv
- | _build
- | dist
- | generated
- | src/SeleniumLibrary/__init__\.pyi
-)/
-'''
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+exclude = [
+ "src/SeleniumLibrary/__init__.pyi",
+]
+
+[tool.ruff.lint]
+select = [
+ "E",
+ "F",
+ "W",
+ "C90",
+ "I",
+ "N",
+ "B",
+ "PYI",
+ "PL",
+ "UP",
+ "A",
+ "C4",
+ "DTZ",
+ "ISC",
+ "ICN",
+ "INP",
+ "PIE",
+ "T20",
+ "PYI",
+ "PT",
+ "RSE",
+ "RET",
+ "SIM",
+ "RUF"
+]
+ignore = [
+ "E501", # line too long
+ "N803", # argument name should be lowercase
+ "N812", # lowercase imported as non lowercase
+ "N999", # Invalid module name: 'SeleniumLibrary'
+ "PLR0913", # too many arguments
+ "PLR2004", # Magic value used in comparison
+ "DTZ006", # No timezone specified
+ "PTH", # Use Path instead of os.path -> maybe soon
+ "N818", # exception naming convention
+]
-[tool.isort]
-profile = "black"
-src_paths="."
-skip_glob = ["src/SeleniumLibrary/__init__.pyi"]
+[tool.ruff.format]
+quote-style = "double"
[tool.pytest.ini_options]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 90b82779d..71bc8bb10 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -15,8 +15,7 @@ pytest-mockito == 0.0.4
pytest-approvaltests == 0.2.4
requests == 2.33.1
robotframework-pabot == 5.2.2
-black == 26.3.1
-flake8 == 6.1.0
+ruff == 0.15.12
# Requirements needed when generating releases. See BUILD.rst for details.
rellu == 0.7
diff --git a/setup.py b/setup.py
index f24a0b8a9..9652afb5d 100755
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,7 @@
keywords = 'robotframework testing testautomation selenium webdriver web',
platforms = 'any',
classifiers = CLASSIFIERS,
- python_requires = '>=3.8',
+ python_requires = '>=3.10',
install_requires = REQUIREMENTS,
package_dir = {'': 'src'},
packages = find_packages('src'),
diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py
index f371f3b1d..bdddc45e7 100644
--- a/src/SeleniumLibrary/__init__.py
+++ b/src/SeleniumLibrary/__init__.py
@@ -13,19 +13,17 @@
# 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.
-from collections import namedtuple
-from datetime import timedelta
import importlib
+import pkgutil
+from datetime import timedelta
from inspect import getdoc, isclass
from pathlib import Path
-import pkgutil
-from typing import Optional, List, Union
+from typing import NamedTuple
from robot.api import logger
from robot.errors import DataError
from robot.libraries.BuiltIn import BuiltIn
from robot.utils.importer import Importer
-
from robotlibcore import DynamicCore
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
@@ -49,10 +47,14 @@
WebDriverCache,
WindowKeywords,
)
-from SeleniumLibrary.keywords.screenshot import EMBED, BASE64
+from SeleniumLibrary.keywords.screenshot import BASE64, EMBED
from SeleniumLibrary.locators import ElementFinder
-from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay
-
+from SeleniumLibrary.utils import (
+ LibraryListener,
+ _convert_delay,
+ _convert_timeout,
+ is_truthy,
+)
__version__ = "6.8.0"
@@ -597,12 +599,12 @@ def __init__(
timeout=timedelta(seconds=5),
implicit_wait=timedelta(seconds=0),
run_on_failure="Capture Page Screenshot",
- screenshot_root_directory: Optional[str] = None,
- plugins: Optional[str] = None,
- event_firing_webdriver: Optional[str] = None,
+ screenshot_root_directory: str | None = None,
+ plugins: str | None = None,
+ event_firing_webdriver: str | None = None,
page_load_timeout=timedelta(minutes=5),
action_chain_delay=timedelta(seconds=0.25),
- language: Optional[str] = None,
+ language: str | None = None,
):
"""SeleniumLibrary can be imported with several optional arguments.
@@ -689,10 +691,13 @@ def get_keyword_documentation(self, name: str) -> str:
return self._get_intro_documentation()
return DynamicCore.get_keyword_documentation(self, name)
+ class Doc(NamedTuple):
+ doc: str
+ name: str
+
def _parse_plugin_doc(self):
- Doc = namedtuple("Doc", "doc, name")
for plugin in self._plugins:
- yield Doc(
+ yield self.Doc(
doc=getdoc(plugin) or "No plugin documentation found.",
name=plugin.__class__.__name__,
)
@@ -751,7 +756,7 @@ def driver(self) -> WebDriver:
return self._drivers.current
def find_element(
- self, locator: str, parent: Optional[WebElement] = None
+ self, locator: str, parent: WebElement | None = None
) -> WebElement:
"""Find element matching `locator`.
@@ -769,7 +774,7 @@ def find_element(
def find_elements(
self, locator: str, parent: WebElement = None
- ) -> List[WebElement]:
+ ) -> list[WebElement]:
"""Find all elements matching `locator`.
:param locator: Locator to use when searching the element.
@@ -807,7 +812,7 @@ def _parse_listener(self, event_firing_webdriver):
listener_module = self._string_to_modules(event_firing_webdriver)
listener_count = len(listener_module)
if listener_count > 1:
- message = f"Is is possible import only one listener but there was {listener_count} listeners."
+ message = f"It is possible to import only one listener but there were {listener_count} listeners."
raise ValueError(message)
listener_module = listener_module[0]
importer = Importer("test library")
@@ -817,12 +822,15 @@ def _parse_listener(self, event_firing_webdriver):
raise DataError(message)
return listener
+ class Module(NamedTuple):
+ module: str
+ args: list
+ kw_args: dict
+
def _string_to_modules(self, modules):
- Module = namedtuple("Module", "module, args, kw_args")
parsed_modules = []
for module in modules.split(","):
- module = module.strip()
- module_and_args = module.split(";")
+ module_and_args = module.strip().split(";")
module_name = module_and_args.pop(0)
kw_args = {}
args = []
@@ -832,8 +840,9 @@ def _string_to_modules(self, modules):
kw_args[key] = value
else:
args.append(argument)
- module = Module(module=module_name, args=args, kw_args=kw_args)
- parsed_modules.append(module)
+ parsed_modules.append(
+ self.Module(module=module_name, args=args, kw_args=kw_args)
+ )
return parsed_modules
def _store_plugin_keywords(self, plugin):
@@ -849,7 +858,7 @@ def _resolve_screenshot_root_directory(self):
self.screenshot_root_directory = BASE64
@staticmethod
- def _get_translation(language: Union[str, None]) -> Union[Path, None]:
+ def _get_translation(language: str | None) -> Path | None:
if not language:
return None
discovered_plugins = {
diff --git a/src/SeleniumLibrary/base/context.py b/src/SeleniumLibrary/base/context.py
index e55aecdc4..d8fbddffc 100644
--- a/src/SeleniumLibrary/base/context.py
+++ b/src/SeleniumLibrary/base/context.py
@@ -13,7 +13,7 @@
# 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.
-from typing import Any, Optional, List
+from typing import Any
from selenium.webdriver.remote.webelement import WebElement
@@ -56,7 +56,7 @@ def event_firing_webdriver(self, event_firing_webdriver: Any):
def find_element(
self,
locator: str,
- tag: Optional[str] = None,
+ tag: str | None = None,
required: bool = True,
parent: WebElement = None,
) -> WebElement:
@@ -82,8 +82,8 @@ def find_element(
return self.element_finder.find(locator, tag, True, required, parent)
def find_elements(
- self, locator: str, tag: Optional[str] = None, parent: WebElement = None
- ) -> List[WebElement]:
+ self, locator: str, tag: str | None = None, parent: WebElement = None
+ ) -> list[WebElement]:
"""Find all elements matching `locator`.
:param locator: Locator to use when searching the element.
@@ -103,7 +103,7 @@ def is_text_present(self, text: str):
locator = f"xpath://*[contains(., {escape_xpath_value(text)})]"
return self.find_element(locator, required=False) is not None
- def is_element_enabled(self, locator: str, tag: Optional[str] = None) -> bool:
+ def is_element_enabled(self, locator: str, tag: str | None = None) -> bool:
element = self.find_element(locator, tag)
return element.is_enabled() and element.get_attribute("readonly") is None
diff --git a/src/SeleniumLibrary/base/librarycomponent.py b/src/SeleniumLibrary/base/librarycomponent.py
index d7174f4de..ae24d57c8 100644
--- a/src/SeleniumLibrary/base/librarycomponent.py
+++ b/src/SeleniumLibrary/base/librarycomponent.py
@@ -16,14 +16,14 @@
import os
from datetime import timedelta
-from typing import Optional, Union
-from SeleniumLibrary.utils import is_noney
from robot.api import logger
from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError
-from .context import ContextAware
+from SeleniumLibrary.utils import is_noney
+
from ..utils import _convert_timeout
+from .context import ContextAware
class LibraryComponent(ContextAware):
@@ -46,8 +46,8 @@ def log_source(self, loglevel: str = "INFO"):
def assert_page_contains(
self,
locator: str,
- tag: Optional[str] = None,
- message: Optional[str] = None,
+ tag: str | None = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
tag_message = tag or "element"
@@ -63,8 +63,8 @@ def assert_page_contains(
def assert_page_not_contains(
self,
locator: str,
- tag: Optional[str] = None,
- message: Optional[str] = None,
+ tag: str | None = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
tag_message = tag or "element"
@@ -75,7 +75,7 @@ def assert_page_not_contains(
raise AssertionError(message)
logger.info(f"Current page does not contain {tag_message} '{locator}'.")
- def get_timeout(self, timeout: Union[str, int, timedelta, None] = None) -> float:
+ def get_timeout(self, timeout: str | int | timedelta | None = None) -> float:
if timeout is None:
return self.ctx.timeout
return _convert_timeout(timeout)
diff --git a/src/SeleniumLibrary/entry/__main__.py b/src/SeleniumLibrary/entry/__main__.py
index 47e049233..61c96462e 100644
--- a/src/SeleniumLibrary/entry/__main__.py
+++ b/src/SeleniumLibrary/entry/__main__.py
@@ -16,13 +16,12 @@
import json
from pathlib import Path
-from typing import Optional
+
import click
from .get_versions import get_version
from .translation import compare_translation, get_library_translation
-
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
VERSION = get_version()
@@ -39,7 +38,6 @@ def cli():
See each command argument help for more details what (optional) arguments that command supports.
"""
- pass
@cli.command()
@@ -63,7 +61,7 @@ def cli():
)
def translation(
filename: Path,
- plugins: Optional[str] = None,
+ plugins: str | None = None,
compare: bool = False,
):
"""Default translation file from library keywords.
@@ -88,17 +86,17 @@ def translation(
lib_translation = get_library_translation(plugins)
if compare:
if table := compare_translation(filename, lib_translation):
- print(
+ click.echo(
"Found differences between translation and library, see below for details."
)
for line in table:
- print(line)
+ click.echo(line)
else:
- print("Translation is valid, no updated needed.")
+ click.echo("Translation is valid, no updated needed.")
else:
with filename.open("w") as file:
json.dump(lib_translation, file, indent=4)
- print(f"Translation file created in {filename.absolute()}")
+ click.echo(f"Translation file created in {filename.absolute()}")
if __name__ == "__main__":
diff --git a/src/SeleniumLibrary/entry/get_versions.py b/src/SeleniumLibrary/entry/get_versions.py
index 51e68da7a..4ad9a7ce5 100644
--- a/src/SeleniumLibrary/entry/get_versions.py
+++ b/src/SeleniumLibrary/entry/get_versions.py
@@ -14,10 +14,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from pathlib import Path
import re
import subprocess
import sys
+from pathlib import Path
from selenium import __version__
diff --git a/src/SeleniumLibrary/entry/translation.py b/src/SeleniumLibrary/entry/translation.py
index 3c98dddaa..0cfdb0e7b 100644
--- a/src/SeleniumLibrary/entry/translation.py
+++ b/src/SeleniumLibrary/entry/translation.py
@@ -18,7 +18,6 @@
import inspect
import json
from pathlib import Path
-from typing import List, Optional
KEYWORD_NAME = "Keyword name"
DOC_CHANGED = "Documentation update needed"
@@ -33,8 +32,8 @@
)
-def get_library_translation(plugins: Optional[str] = None) -> dict:
- from SeleniumLibrary import SeleniumLibrary
+def get_library_translation(plugins: str | None = None) -> dict:
+ from SeleniumLibrary import SeleniumLibrary # noqa: PLC0415
selib = SeleniumLibrary(plugins=plugins)
translation = {}
@@ -65,7 +64,7 @@ def _max_kw_name_length(project_translation: dict) -> int:
return max_lenght
-def _get_heading(max_kw_length: int) -> List[str]:
+def _get_heading(max_kw_length: int) -> list[str]:
heading = f"| {KEYWORD_NAME} "
next_line = f"| {'-' * len(KEYWORD_NAME)}"
if (padding := max_kw_length - len(KEYWORD_NAME)) > 0:
diff --git a/src/SeleniumLibrary/errors.py b/src/SeleniumLibrary/errors.py
index 5dd4310d1..636a13a87 100644
--- a/src/SeleniumLibrary/errors.py
+++ b/src/SeleniumLibrary/errors.py
@@ -40,4 +40,4 @@ class PluginError(SeleniumLibraryException):
class UnkownExpectedCondition(SeleniumLibraryException):
- pass
\ No newline at end of file
+ pass
diff --git a/src/SeleniumLibrary/keywords/alert.py b/src/SeleniumLibrary/keywords/alert.py
index 406946d0f..9484360e1 100644
--- a/src/SeleniumLibrary/keywords/alert.py
+++ b/src/SeleniumLibrary/keywords/alert.py
@@ -14,13 +14,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import timedelta
-from typing import Optional
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
-from SeleniumLibrary.base import keyword, LibraryComponent
+from SeleniumLibrary.base import LibraryComponent, keyword
from SeleniumLibrary.utils import secs_to_timestr
@@ -32,7 +31,7 @@ class AlertKeywords(LibraryComponent):
@keyword
def input_text_into_alert(
- self, text: str, action: str = ACCEPT, timeout: Optional[timedelta] = None
+ self, text: str, action: str = ACCEPT, timeout: timedelta | None = None
):
"""Types the given ``text`` into an input field in an alert.
@@ -53,7 +52,7 @@ def alert_should_be_present(
self,
text: str = "",
action: str = ACCEPT,
- timeout: Optional[timedelta] = None,
+ timeout: timedelta | None = None,
):
"""Verifies that an alert is present and by default, accepts it.
@@ -72,12 +71,12 @@ def alert_should_be_present(
message = self.handle_alert(action, timeout)
if text and text != message:
raise AssertionError(
- f"Alert message should have been '{text}' but it " f"was '{message}'."
+ f"Alert message should have been '{text}' but it was '{message}'."
)
@keyword
def alert_should_not_be_present(
- self, action: str = ACCEPT, timeout: Optional[timedelta] = None
+ self, action: str = ACCEPT, timeout: timedelta | None = None
):
"""Verifies that no alert is present.
@@ -101,7 +100,7 @@ def alert_should_not_be_present(
raise AssertionError(f"Alert with message '{text}' present.")
@keyword
- def handle_alert(self, action: str = ACCEPT, timeout: Optional[timedelta] = None):
+ def handle_alert(self, action: str = ACCEPT, timeout: timedelta | None = None):
"""Handles the current alert and returns its message.
By default, the alert is accepted, but this can be controlled
@@ -146,7 +145,11 @@ def _wait_alert(self, timeout=None):
wait = WebDriverWait(self.driver, timeout)
try:
return wait.until(EC.alert_is_present())
- except TimeoutException:
- raise AssertionError(f"Alert not found in {secs_to_timestr(timeout)}.")
+ except TimeoutException as original_exception:
+ raise AssertionError(
+ f"Alert not found in {secs_to_timestr(timeout)}."
+ ) from original_exception
except WebDriverException as err:
- raise AssertionError(f"An exception occurred waiting for alert: {err}")
+ raise AssertionError(
+ f"An exception occurred waiting for alert: {err}"
+ ) from err
diff --git a/src/SeleniumLibrary/keywords/browsermanagement.py b/src/SeleniumLibrary/keywords/browsermanagement.py
index 042d12d50..102695317 100644
--- a/src/SeleniumLibrary/keywords/browsermanagement.py
+++ b/src/SeleniumLibrary/keywords/browsermanagement.py
@@ -17,15 +17,20 @@
import time
import types
from datetime import timedelta
-from typing import Optional, Union, Any, List
+from typing import Any
from selenium import webdriver
from selenium.webdriver import FirefoxProfile
from selenium.webdriver.support.event_firing_webdriver import EventFiringWebDriver
-from SeleniumLibrary.base import keyword, LibraryComponent
+from SeleniumLibrary.base import LibraryComponent, keyword
from SeleniumLibrary.locators import WindowManager
-from SeleniumLibrary.utils import timestr_to_secs, secs_to_timestr, _convert_timeout, _convert_delay
+from SeleniumLibrary.utils import (
+ _convert_delay,
+ _convert_timeout,
+ secs_to_timestr,
+ timestr_to_secs,
+)
from .webdrivertools import WebDriverCreator
@@ -59,15 +64,15 @@ def close_browser(self):
@keyword
def open_browser(
self,
- url: Optional[str] = None,
+ url: str | None = None,
browser: str = "firefox",
- alias: Optional[str] = None,
- remote_url: Union[bool, str] = False,
- desired_capabilities: Union[dict, None, str] = None,
- ff_profile_dir: Union[FirefoxProfile, str, None] = None,
+ alias: str | None = None,
+ remote_url: bool | str = False,
+ desired_capabilities: dict | None | str = None,
+ ff_profile_dir: FirefoxProfile | str | None = None,
options: Any = None,
- service_log_path: Optional[str] = None,
- executable_path: Optional[str] = None,
+ service_log_path: str | None = None,
+ executable_path: str | None = None,
service: Any = None,
) -> str:
"""Opens a new browser instance to the optional ``url``.
@@ -212,11 +217,17 @@ def open_browser(
self.go_to(url)
return index
if desired_capabilities:
- self.warn("desired_capabilities has been deprecated and removed. Please use options to configure browsers as per documentation.")
+ self.warn(
+ "desired_capabilities has been deprecated and removed. Please use options to configure browsers as per documentation."
+ )
if service_log_path:
- self.warn("service_log_path is being deprecated. Please use service to configure log_output or equivalent service attribute.")
+ self.warn(
+ "service_log_path is being deprecated. Please use service to configure log_output or equivalent service attribute."
+ )
if executable_path:
- self.warn("executable_path is being deprecated. Please use service to configure the driver's executable_path as per documentation.")
+ self.warn(
+ "executable_path is being deprecated. Please use service to configure the driver's executable_path as per documentation."
+ )
return self._make_new_browser(
url,
browser,
@@ -275,7 +286,11 @@ def _make_new_browser(
@keyword
def create_webdriver(
- self, driver_name: str, alias: Optional[str] = None, kwargs: Optional[dict] = None, **init_kwargs
+ self,
+ driver_name: str,
+ alias: str | None = None,
+ kwargs: dict | None = None,
+ **init_kwargs,
) -> str:
"""Creates an instance of Selenium WebDriver.
@@ -314,8 +329,10 @@ def create_webdriver(
driver_name = driver_name.strip()
try:
creation_func = getattr(webdriver, driver_name)
- except AttributeError:
- raise RuntimeError(f"'{driver_name}' is not a valid WebDriver name.")
+ except AttributeError as original_exception:
+ raise RuntimeError(
+ f"'{driver_name}' is not a valid WebDriver name."
+ ) from original_exception
self.info(f"Creating an instance of the {driver_name} WebDriver.")
driver = creation_func(**init_kwargs)
self.debug(
@@ -359,16 +376,16 @@ def switch_browser(self, index_or_alias: str):
"""
try:
self.drivers.switch(index_or_alias)
- except RuntimeError:
+ except RuntimeError as original_exception:
raise RuntimeError(
f"No browser with index or alias '{index_or_alias}' found."
- )
+ ) from original_exception
self.debug(
f"Switched to browser with Selenium session id {self.driver.session_id}."
)
@keyword
- def get_browser_ids(self) -> List[str]:
+ def get_browser_ids(self) -> list[str]:
"""Returns index of all active browser as list.
Example:
@@ -385,7 +402,7 @@ def get_browser_ids(self) -> List[str]:
return self.drivers.active_driver_ids
@keyword
- def get_browser_aliases(self) -> List[str]:
+ def get_browser_aliases(self) -> list[str]:
"""Returns aliases of all active browser that has an alias as NormalizedDict.
The dictionary contains the aliases as keys and the index as value.
This can be accessed as dictionary ``${aliases.key}`` or as list ``@{aliases}[0]``.
@@ -429,7 +446,7 @@ def get_location(self) -> str:
return self.driver.current_url
@keyword
- def location_should_be(self, url: str, message: Optional[str] = None):
+ def location_should_be(self, url: str, message: str | None = None):
"""Verifies that the current URL is exactly ``url``.
The ``url`` argument contains the exact url that should exist in browser.
@@ -442,12 +459,12 @@ def location_should_be(self, url: str, message: Optional[str] = None):
actual = self.get_location()
if actual != url:
if message is None:
- message = f"Location should have been '{url}' but " f"was '{actual}'."
+ message = f"Location should have been '{url}' but was '{actual}'."
raise AssertionError(message)
self.info(f"Current location is '{url}'.")
@keyword
- def location_should_contain(self, expected: str, message: Optional[str] = None):
+ def location_should_contain(self, expected: str, message: str | None = None):
"""Verifies that the current URL contains ``expected``.
The ``expected`` argument contains the expected value in url.
@@ -494,7 +511,7 @@ def log_title(self) -> str:
return title
@keyword
- def title_should_be(self, title: str, message: Optional[str] = None):
+ def title_should_be(self, title: str, message: str | None = None):
"""Verifies that the current page title equals ``title``.
The ``message`` argument can be used to override the default error
@@ -654,8 +671,7 @@ def set_action_chain_delay(self, value: timedelta) -> str:
@keyword
def get_action_chain_delay(self):
- """Gets the currently stored value for chain_delay_value in timestr format.
- """
+ """Gets the currently stored value for chain_delay_value in timestr format."""
return timestr_to_secs(f"{self.ctx.action_chain_delay} milliseconds")
@keyword
diff --git a/src/SeleniumLibrary/keywords/cookie.py b/src/SeleniumLibrary/keywords/cookie.py
index c2c49e8c7..32af0f861 100644
--- a/src/SeleniumLibrary/keywords/cookie.py
+++ b/src/SeleniumLibrary/keywords/cookie.py
@@ -14,7 +14,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import datetime
-from typing import Union, Optional
from robot.libraries.DateTime import convert_date
from robot.utils import DotDict
@@ -45,7 +44,7 @@ def __init__(
self.extra = extra
def __str__(self):
- items = "name value path domain secure httpOnly expiry".split()
+ items = ["name", "value", "path", "domain", "secure", "httpOnly", "expiry"]
string = "\n".join(f"{item}={getattr(self, item)}" for item in items)
if self.extra:
string = f"{string}\nextra={self.extra}\n"
@@ -67,7 +66,7 @@ def delete_cookie(self, name):
self.driver.delete_cookie(name)
@keyword
- def get_cookies(self, as_dict: bool = False) -> Union[str, dict]:
+ def get_cookies(self, as_dict: bool = False) -> str | dict:
"""Returns all cookies of the current page.
If ``as_dict`` argument evaluates as false, see `Boolean arguments`
@@ -87,11 +86,10 @@ def get_cookies(self, as_dict: bool = False) -> Union[str, dict]:
for cookie in self.driver.get_cookies():
pairs.append(f"{cookie['name']}={cookie['value']}")
return "; ".join(pairs)
- else:
- pairs = DotDict()
- for cookie in self.driver.get_cookies():
- pairs[cookie["name"]] = cookie["value"]
- return pairs
+ pairs = DotDict()
+ for cookie in self.driver.get_cookies():
+ pairs[cookie["name"]] = cookie["value"]
+ return pairs
@keyword
def get_cookie(self, name: str) -> CookieInformation:
@@ -144,10 +142,10 @@ def add_cookie(
self,
name: str,
value: str,
- path: Optional[str] = None,
- domain: Optional[str] = None,
- secure: Optional[bool] = None,
- expiry: Optional[str] = None,
+ path: str | None = None,
+ domain: str | None = None,
+ secure: bool | None = None,
+ expiry: str | None = None,
):
"""Adds a cookie to your current session.
diff --git a/src/SeleniumLibrary/keywords/element.py b/src/SeleniumLibrary/keywords/element.py
index c61076286..c74bb1d5a 100644
--- a/src/SeleniumLibrary/keywords/element.py
+++ b/src/SeleniumLibrary/keywords/element.py
@@ -14,18 +14,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from collections import namedtuple
-from typing import List, Optional, Tuple, Union
+from typing import NamedTuple
-from SeleniumLibrary.utils import is_noney
-from robot.utils import plural_or_not, is_truthy
+from robot.utils import is_truthy, plural_or_not
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webelement import WebElement
from SeleniumLibrary.base import LibraryComponent, keyword
from SeleniumLibrary.errors import ElementNotFound
-from SeleniumLibrary.utils.types import type_converter, Locator
+from SeleniumLibrary.utils import is_noney
+from SeleniumLibrary.utils.types import Locator, type_converter
class ElementKeywords(LibraryComponent):
@@ -39,7 +38,7 @@ def get_webelement(self, locator: Locator) -> WebElement:
return self.find_element(locator)
@keyword(name="Get WebElements")
- def get_webelements(self, locator: Locator) -> List[WebElement]:
+ def get_webelements(self, locator: Locator) -> list[WebElement]:
"""Returns a list of WebElement objects matching the ``locator``.
See the `Locating elements` section for details about the locator
@@ -55,8 +54,8 @@ def get_webelements(self, locator: Locator) -> List[WebElement]:
def element_should_contain(
self,
locator: Locator,
- expected: Union[None, str],
- message: Optional[str] = None,
+ expected: None | str,
+ message: str | None = None,
ignore_case: bool = False,
):
"""Verifies that element ``locator`` contains text ``expected``.
@@ -93,8 +92,8 @@ def element_should_contain(
def element_should_not_contain(
self,
locator: Locator,
- expected: Union[None, str],
- message: Optional[str] = None,
+ expected: None | str,
+ message: str | None = None,
ignore_case: bool = False,
):
"""Verifies that element ``locator`` does not contain text ``expected``.
@@ -151,9 +150,9 @@ def page_should_contain(self, text: str, loglevel: str = "TRACE"):
def page_should_contain_element(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
- limit: Optional[int] = None,
+ limit: int | None = None,
):
"""Verifies that element ``locator`` is found on the current page.
@@ -186,14 +185,14 @@ def page_should_contain_element(
count = len(self.find_elements(locator))
if count == limit:
self.info(f"Current page contains {count} element(s).")
- else:
- if message is None:
- message = (
- f'Page should have contained "{limit}" element(s), '
- f'but it did contain "{count}" element(s).'
- )
- self.ctx.log_source(loglevel)
- raise AssertionError(message)
+ return None
+ if message is None:
+ message = (
+ f'Page should have contained "{limit}" element(s), '
+ f'but it did contain "{count}" element(s).'
+ )
+ self.ctx.log_source(loglevel)
+ raise AssertionError(message)
@keyword
def page_should_not_contain(self, text: str, loglevel: str = "TRACE"):
@@ -211,7 +210,7 @@ def page_should_not_contain(self, text: str, loglevel: str = "TRACE"):
def page_should_not_contain_element(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies that element ``locator`` is not found on the current page.
@@ -225,7 +224,7 @@ def page_should_not_contain_element(
self.assert_page_not_contains(locator, message=message, loglevel=loglevel)
@keyword
- def assign_id_to_element(self, locator: Locator, id: str):
+ def assign_id_to_element(self, locator: Locator, id: str): # noqa: A002
"""Assigns a temporary ``id`` to the element specified by ``locator``.
This is mainly useful if the locator is complicated and/or slow XPath
@@ -287,9 +286,7 @@ def element_should_be_focused(self, locator: Locator):
raise AssertionError(f"Element '{locator}' does not have focus.")
@keyword
- def element_should_be_visible(
- self, locator: Locator, message: Optional[str] = None
- ):
+ def element_should_be_visible(self, locator: Locator, message: str | None = None):
"""Verifies that the element identified by ``locator`` is visible.
Herein, visible means that the element is logically visible, not
@@ -311,7 +308,7 @@ def element_should_be_visible(
@keyword
def element_should_not_be_visible(
- self, locator: Locator, message: Optional[str] = None
+ self, locator: Locator, message: str | None = None
):
"""Verifies that the element identified by ``locator`` is NOT visible.
@@ -332,8 +329,8 @@ def element_should_not_be_visible(
def element_text_should_be(
self,
locator: Locator,
- expected: Union[None, str],
- message: Optional[str] = None,
+ expected: None | str,
+ message: str | None = None,
ignore_case: bool = False,
):
"""Verifies that element ``locator`` contains exact the text ``expected``.
@@ -368,8 +365,8 @@ def element_text_should_be(
def element_text_should_not_be(
self,
locator: Locator,
- not_expected: Union[None, str],
- message: Optional[str] = None,
+ not_expected: None | str,
+ message: str | None = None,
ignore_case: bool = False,
):
"""Verifies that element ``locator`` does not contain exact the text ``not_expected``.
@@ -399,9 +396,7 @@ def element_text_should_not_be(
raise AssertionError(message)
@keyword
- def get_element_attribute(
- self, locator: Locator, attribute: str
- ) -> str:
+ def get_element_attribute(self, locator: Locator, attribute: str) -> str:
"""Returns the value of ``attribute`` from the element ``locator``.
See the `Locating elements` section for details about the locator
@@ -417,9 +412,7 @@ def get_element_attribute(
return self.find_element(locator).get_attribute(attribute)
@keyword
- def get_dom_attribute(
- self, locator: Locator, attribute: str
- ) -> str:
+ def get_dom_attribute(self, locator: Locator, attribute: str) -> str:
"""Returns the value of ``attribute`` from the element ``locator``. `Get DOM Attribute` keyword
only returns attributes declared within the element's HTML markup. If the requested attribute
is not there, the keyword returns ${None}.
@@ -435,7 +428,9 @@ def get_dom_attribute(
@keyword
def get_property(
- self, locator: Locator, property: str
+ self,
+ locator: Locator,
+ property: str, # noqa: A002
) -> str:
"""Returns the value of ``property`` from the element ``locator``.
@@ -453,8 +448,8 @@ def element_attribute_value_should_be(
self,
locator: Locator,
attribute: str,
- expected: Union[None, str],
- message: Optional[str] = None,
+ expected: None | str,
+ message: str | None = None,
):
"""Verifies element identified by ``locator`` contains expected attribute value.
@@ -494,7 +489,7 @@ def get_horizontal_position(self, locator: Locator) -> int:
return self.find_element(locator).location["x"]
@keyword
- def get_element_size(self, locator: Locator) -> Tuple[int, int]:
+ def get_element_size(self, locator: Locator) -> tuple[int, int]:
"""Returns width and height of the element identified by ``locator``.
See the `Locating elements` section for details about the locator
@@ -582,9 +577,7 @@ def get_vertical_position(self, locator: Locator) -> int:
return self.find_element(locator).location["y"]
@keyword
- def click_button(
- self, locator: Locator, modifier: Union[bool, str] = False
- ):
+ def click_button(self, locator: Locator, modifier: bool | str = False):
"""Clicks the button identified by ``locator``.
See the `Locating elements` section for details about the locator
@@ -606,9 +599,7 @@ def click_button(
self._click_with_modifier(locator, ["button", "input"], modifier)
@keyword
- def click_image(
- self, locator: Locator, modifier: Union[bool, str] = False
- ):
+ def click_image(self, locator: Locator, modifier: bool | str = False):
"""Clicks an image identified by ``locator``.
See the `Locating elements` section for details about the locator
@@ -631,9 +622,7 @@ def click_image(
self._click_with_modifier(locator, ["image", "input"], modifier)
@keyword
- def click_link(
- self, locator: Locator, modifier: Union[bool, str] = False
- ):
+ def click_link(self, locator: Locator, modifier: bool | str = False):
"""Clicks a link identified by ``locator``.
See the `Locating elements` section for details about the locator
@@ -655,7 +644,7 @@ def click_link(
def click_element(
self,
locator: Locator,
- modifier: Union[bool, str] = False,
+ modifier: bool | str = False,
action_chain: bool = False,
):
"""Click the element identified by ``locator``.
@@ -775,12 +764,12 @@ def scroll_element_into_view(self, locator: Locator):
New in SeleniumLibrary 3.2.0
"""
element = self.find_element(locator)
- ActionChains(self.driver, duration=self.ctx.action_chain_delay).move_to_element(element).perform()
+ ActionChains(self.driver, duration=self.ctx.action_chain_delay).move_to_element(
+ element
+ ).perform()
@keyword
- def drag_and_drop(
- self, locator: Locator, target: Locator
- ):
+ def drag_and_drop(self, locator: Locator, target: Locator):
"""Drags the element identified by ``locator`` into the ``target`` element.
The ``locator`` argument is the locator of the dragged element
@@ -796,9 +785,7 @@ def drag_and_drop(
action.drag_and_drop(element, target).perform()
@keyword
- def drag_and_drop_by_offset(
- self, locator: Locator, xoffset: int, yoffset: int
- ):
+ def drag_and_drop_by_offset(self, locator: Locator, xoffset: int, yoffset: int):
"""Drags the element identified with ``locator`` by ``xoffset/yoffset``.
See the `Locating elements` section for details about the locator
@@ -870,7 +857,9 @@ def mouse_up(self, locator: Locator):
"""
self.info(f"Simulating Mouse Up on element '{locator}'.")
element = self.find_element(locator)
- ActionChains(self.driver, duration=self.ctx.action_chain_delay).release(element).perform()
+ ActionChains(self.driver, duration=self.ctx.action_chain_delay).release(
+ element
+ ).perform()
@keyword
def open_context_menu(self, locator: Locator):
@@ -989,7 +978,9 @@ def press_keys(self, locator: Locator | None = None, *keys: str):
if not is_noney(locator):
self.info(f"Sending key(s) {keys} to {locator} element.")
element = self.find_element(locator)
- ActionChains(self.driver, duration=self.ctx.action_chain_delay).click(element).perform()
+ ActionChains(self.driver, duration=self.ctx.action_chain_delay).click(
+ element
+ ).perform()
else:
self.info(f"Sending key(s) {keys} to page.")
element = None
@@ -1025,7 +1016,7 @@ def _special_key_up(self, actions, parsed_key):
actions.key_up(key.converted)
@keyword
- def get_all_links(self) -> List[str]:
+ def get_all_links(self) -> list[str]:
"""Returns a list containing ids of all links found in current page.
If a link has no id, an empty string will be in the list instead.
@@ -1049,7 +1040,7 @@ def mouse_down_on_link(self, locator: Locator):
def page_should_contain_link(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies link identified by ``locator`` is found from current page.
@@ -1067,7 +1058,7 @@ def page_should_contain_link(
def page_should_not_contain_link(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies link identified by ``locator`` is not found from current page.
@@ -1097,7 +1088,7 @@ def mouse_down_on_image(self, locator: Locator):
def page_should_contain_image(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies image identified by ``locator`` is found from current page.
@@ -1115,7 +1106,7 @@ def page_should_contain_image(
def page_should_not_contain_image(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies image identified by ``locator`` is not found from current page.
@@ -1172,7 +1163,7 @@ def remove_location_strategy(self, strategy_name: str):
self.element_finder.unregister(strategy_name)
def _map_ascii_key_code_to_key(self, key_code):
- map = {
+ key_map = {
0: Keys.NULL,
8: Keys.BACK_SPACE,
9: Keys.TAB,
@@ -1191,7 +1182,7 @@ def _map_ascii_key_code_to_key(self, key_code):
61: Keys.EQUALS,
127: Keys.DELETE,
}
- key = map.get(key_code)
+ key = key_map.get(key_code)
if key is None:
key = chr(key_code)
return key
@@ -1199,10 +1190,10 @@ def _map_ascii_key_code_to_key(self, key_code):
def _map_named_key_code_to_special_key(self, key_name):
try:
return getattr(Keys, key_name)
- except AttributeError:
+ except AttributeError as original_exception:
message = f"Unknown key named '{key_name}'."
self.debug(message)
- raise ValueError(message)
+ raise ValueError(message) from original_exception
def _page_contains(self, text):
self.driver.switch_to.default_content()
@@ -1225,12 +1216,13 @@ def parse_modifier(self, modifier):
modifiers = modifier.split("+")
keys = []
for item in modifiers:
- item = item.strip()
- item = self._parse_aliases(item)
- if hasattr(Keys, item):
- keys.append(getattr(Keys, item))
+ modifier = self._parse_aliases(item.strip())
+ if hasattr(Keys, modifier):
+ keys.append(getattr(Keys, modifier))
else:
- raise ValueError(f"'{item}' modifier does not match to Selenium Keys")
+ raise ValueError(
+ f"'{modifier}' modifier does not match to Selenium Keys"
+ )
return keys
def _parse_keys(self, *keys):
@@ -1263,24 +1255,30 @@ def _separate_key(self, key):
list_keys.append(one_key)
return list_keys
+ class KeysRecord(NamedTuple):
+ converted: object
+ original: str
+ special: bool
+
def _convert_special_keys(self, keys):
- KeysRecord = namedtuple("KeysRecord", "converted, original special")
converted_keys = []
for key in keys:
- key = self._parse_aliases(key)
- if self._selenium_keys_has_attr(key):
- converted_keys.append(KeysRecord(getattr(Keys, key), key, True))
+ resolved_key = self._parse_aliases(key)
+ if self._selenium_keys_has_attr(resolved_key):
+ converted_keys.append(
+ self.KeysRecord(getattr(Keys, resolved_key), resolved_key, True)
+ )
else:
- converted_keys.append(KeysRecord(key, key, False))
+ converted_keys.append(
+ self.KeysRecord(resolved_key, resolved_key, False)
+ )
return converted_keys
def _selenium_keys_has_attr(self, key):
return hasattr(Keys, key)
@keyword("Get CSS Property Value")
- def get_css_property_value(
- self, locator: Locator, css_property: str
- ) -> str:
+ def get_css_property_value(self, locator: Locator, css_property: str) -> str:
"""Returns the computed value of ``css_property`` from the element ``locator``.
See the `Locating elements` section for details about the locator syntax.
@@ -1293,4 +1291,4 @@ def get_css_property_value(
| ${color}= | `Get CSS Property Value` | css:button.submit | background-color |
| ${size}= | `Get CSS Property Value` | id:username | font-size |
"""
- return self.find_element(locator).value_of_css_property(css_property)
\ No newline at end of file
+ return self.find_element(locator).value_of_css_property(css_property)
diff --git a/src/SeleniumLibrary/keywords/expectedconditions.py b/src/SeleniumLibrary/keywords/expectedconditions.py
index c0272ae75..4635ce54f 100644
--- a/src/SeleniumLibrary/keywords/expectedconditions.py
+++ b/src/SeleniumLibrary/keywords/expectedconditions.py
@@ -11,17 +11,18 @@
# 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 string
-from typing import Optional
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.wait import WebDriverWait
from SeleniumLibrary.base import LibraryComponent, keyword
from SeleniumLibrary.errors import UnkownExpectedCondition
-from selenium.webdriver.support.wait import WebDriverWait
-from selenium.webdriver.support import expected_conditions as EC
+
class ExpectedConditionKeywords(LibraryComponent):
@keyword
- def wait_for_expected_condition(self, condition: string, *args, timeout: Optional[float]=10):
+ def wait_for_expected_condition(
+ self, condition: str, *args, timeout: float | None = 10
+ ):
"""Waits until ``condition`` is true or ``timeout`` expires.
The condition must be one of selenium's expected condition which
@@ -51,13 +52,15 @@ def wait_for_expected_condition(self, condition: string, *args, timeout: Optiona
condition = self._parse_condition(condition)
wait = WebDriverWait(self.driver, timeout, 0.1)
try:
- c = getattr(EC, condition)
- except:
- # ToDo: provide hints as to what is avaialbel or find closet match
- raise UnkownExpectedCondition(f"{condition} is an unknown expected condition")
- result = wait.until(c(*args), message="Expected Condition not met within set timeout of " + str(timeout))
- return result
-
- def _parse_condition(self, condition: string):
- parsed = condition.replace(' ','_').lower()
- return parsed
\ No newline at end of file
+ condition_func = getattr(EC, condition)
+ except AttributeError as original_exception:
+ raise UnkownExpectedCondition(
+ f"{condition} is an unknown expected condition"
+ ) from original_exception
+ return wait.until(
+ condition_func(*args),
+ message=f"Expected Condition not met within set timeout of {timeout}s",
+ )
+
+ def _parse_condition(self, condition: str):
+ return condition.replace(" ", "_").lower()
diff --git a/src/SeleniumLibrary/keywords/formelement.py b/src/SeleniumLibrary/keywords/formelement.py
index 43816e04f..cbf774190 100644
--- a/src/SeleniumLibrary/keywords/formelement.py
+++ b/src/SeleniumLibrary/keywords/formelement.py
@@ -15,7 +15,6 @@
# limitations under the License.
import os
-from typing import Optional
from robot.libraries.BuiltIn import BuiltIn
@@ -70,7 +69,7 @@ def checkbox_should_not_be_selected(self, locator: Locator):
def page_should_contain_checkbox(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies checkbox ``locator`` is found from the current page.
@@ -87,7 +86,7 @@ def page_should_contain_checkbox(
def page_should_not_contain_checkbox(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies checkbox ``locator`` is not found from the current page.
@@ -132,7 +131,7 @@ def unselect_checkbox(self, locator: Locator):
def page_should_contain_radio_button(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies radio button ``locator`` is found from current page.
@@ -150,7 +149,7 @@ def page_should_contain_radio_button(
def page_should_not_contain_radio_button(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies radio button ``locator`` is not found from current page.
@@ -239,9 +238,7 @@ def choose_file(self, locator: Locator, file_path: str):
self.ctx._running_keyword = None
@keyword
- def input_password(
- self, locator: Locator, password: str, clear: bool = True
- ):
+ def input_password(self, locator: Locator, password: str, clear: bool = True):
"""Types the given password into the text field identified by ``locator``.
See the `Locating elements` section for details about the locator
@@ -269,9 +266,7 @@ def input_password(
self._input_text_into_text_field(locator, password, clear, disable_log=True)
@keyword
- def input_text(
- self, locator: Locator, text: str, clear: bool = True
- ):
+ def input_text(self, locator: Locator, text: str, clear: bool = True):
"""Types the given ``text`` into the text field identified by ``locator``.
When ``clear`` is true, the input element is cleared before
@@ -300,7 +295,7 @@ def input_text(
def page_should_contain_textfield(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies text field ``locator`` is found from current page.
@@ -317,7 +312,7 @@ def page_should_contain_textfield(
def page_should_not_contain_textfield(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies text field ``locator`` is not found from current page.
@@ -335,7 +330,7 @@ def textfield_should_contain(
self,
locator: Locator,
expected: str,
- message: Optional[str] = None,
+ message: str | None = None,
):
"""Verifies text field ``locator`` contains text ``expected``.
@@ -359,7 +354,7 @@ def textfield_value_should_be(
self,
locator: Locator,
expected: str,
- message: Optional[str] = None,
+ message: str | None = None,
):
"""Verifies text field ``locator`` has exactly text ``expected``.
@@ -383,7 +378,7 @@ def textarea_should_contain(
self,
locator: Locator,
expected: str,
- message: Optional[str] = None,
+ message: str | None = None,
):
"""Verifies text area ``locator`` contains text ``expected``.
@@ -407,7 +402,7 @@ def textarea_value_should_be(
self,
locator: Locator,
expected: str,
- message: Optional[str] = None,
+ message: str | None = None,
):
"""Verifies text area ``locator`` has exactly text ``expected``.
@@ -430,7 +425,7 @@ def textarea_value_should_be(
def page_should_contain_button(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies button ``locator`` is found from current page.
@@ -451,7 +446,7 @@ def page_should_contain_button(
def page_should_not_contain_button(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies button ``locator`` is not found from current page.
@@ -488,11 +483,10 @@ def _get_radio_button_with_value(self, group_name, value):
self.debug(f"Radio group locator: {xpath}")
try:
return self.find_element(xpath)
- except ElementNotFound:
+ except ElementNotFound as original_exception:
raise ElementNotFound(
- f"No radio button with name '{group_name}' "
- f"and value '{value}' found."
- )
+ f"No radio button with name '{group_name}' and value '{value}' found."
+ ) from original_exception
def _get_value_from_radio_buttons(self, elements):
for element in elements:
diff --git a/src/SeleniumLibrary/keywords/javascript.py b/src/SeleniumLibrary/keywords/javascript.py
index 9c2bb1c90..7166ad573 100644
--- a/src/SeleniumLibrary/keywords/javascript.py
+++ b/src/SeleniumLibrary/keywords/javascript.py
@@ -15,17 +15,14 @@
# limitations under the License.
import os
-from collections import namedtuple
-from typing import Any, Union
+from typing import Any, NamedTuple
from robot.utils import plural_or_not, seq2str
-from selenium.webdriver.remote.webelement import WebElement
from SeleniumLibrary.base import LibraryComponent, keyword
class JavaScriptKeywords(LibraryComponent):
-
js_marker = "JAVASCRIPT"
arg_marker = "ARGUMENTS"
@@ -130,11 +127,9 @@ def _separate_code_and_args(self, code):
return code[index.js + 1 :], []
if self.js_marker not in code:
return code[0 : index.arg], code[index.arg + 1 :]
- else:
- if index.js == 0:
- return code[index.js + 1 : index.arg], code[index.arg + 1 :]
- else:
- return code[index.js + 1 :], code[index.arg + 1 : index.js]
+ if index.js == 0:
+ return code[index.js + 1 : index.arg], code[index.arg + 1 :]
+ return code[index.js + 1 :], code[index.arg + 1 : index.js]
def _check_marker_error(self, code):
if not code:
@@ -151,17 +146,14 @@ def _check_marker_error(self, code):
if message:
raise ValueError(message)
+ class Index(NamedTuple):
+ js: int
+ arg: int
+
def _get_marker_index(self, code):
- Index = namedtuple("Index", "js arg")
- if self.js_marker in code:
- js = code.index(self.js_marker)
- else:
- js = -1
- if self.arg_marker in code:
- arg = code.index(self.arg_marker)
- else:
- arg = -1
- return Index(js=js, arg=arg)
+ js = code.index(self.js_marker) if self.js_marker in code else -1
+ arg = code.index(self.arg_marker) if self.arg_marker in code else -1
+ return self.Index(js=js, arg=arg)
def _read_javascript_from_file(self, path):
self.info(
diff --git a/src/SeleniumLibrary/keywords/runonfailure.py b/src/SeleniumLibrary/keywords/runonfailure.py
index 4d23c5ade..78832055b 100644
--- a/src/SeleniumLibrary/keywords/runonfailure.py
+++ b/src/SeleniumLibrary/keywords/runonfailure.py
@@ -13,14 +13,13 @@
# 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.
-from typing import Optional
from SeleniumLibrary.base import LibraryComponent, keyword
class RunOnFailureKeywords(LibraryComponent):
@keyword
- def register_keyword_to_run_on_failure(self, keyword: Optional[str]) -> str:
+ def register_keyword_to_run_on_failure(self, keyword: str | None) -> str:
"""Sets the keyword to execute, when a SeleniumLibrary keyword fails.
``keyword`` is the name of a keyword that will be executed if a
@@ -64,10 +63,6 @@ def register_keyword_to_run_on_failure(self, keyword: Optional[str]) -> str:
def resolve_keyword(name):
if name is None:
return None
- if (
- isinstance(name, str)
- and name.upper() == "NOTHING"
- or name.upper() == "NONE"
- ):
+ if isinstance(name, str) and name.upper() in ("NOTHING", "NONE"):
return None
return name
diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py
index 6963b4e28..58854edff 100644
--- a/src/SeleniumLibrary/keywords/screenshot.py
+++ b/src/SeleniumLibrary/keywords/screenshot.py
@@ -15,11 +15,10 @@
# limitations under the License.
import os
-from typing import Optional, Union
from base64 import b64decode
from robot.utils import get_link_path
-from selenium.webdriver.common.print_page_options import PrintOptions, Orientation
+from selenium.webdriver.common.print_page_options import Orientation, PrintOptions
from SeleniumLibrary.base import LibraryComponent, keyword
from SeleniumLibrary.utils.path_formatter import _format_path
@@ -35,7 +34,7 @@
class ScreenshotKeywords(LibraryComponent):
@keyword
- def set_screenshot_directory(self, path: Union[None, str]) -> str:
+ def set_screenshot_directory(self, path: None | str) -> str:
"""Sets the directory for captured screenshots.
``path`` argument specifies the absolute path to a directory where
@@ -72,7 +71,9 @@ def set_screenshot_directory(self, path: Union[None, str]) -> str:
return previous
@keyword
- def capture_page_screenshot(self, filename: str = DEFAULT_FILENAME_PAGE) -> str:
+ def capture_page_screenshot(
+ self, filename: str = DEFAULT_FILENAME_PAGE
+ ) -> str | None:
"""Takes a screenshot of the current page and embeds it into a log file.
``filename`` argument specifies the name of the file to write the
@@ -123,7 +124,7 @@ def capture_page_screenshot(self, filename: str = DEFAULT_FILENAME_PAGE) -> str:
"""
if not self.drivers.current:
self.info("Cannot capture screenshot because no browser is open.")
- return
+ return None
is_embedded, method = self._decide_embedded(filename)
if is_embedded:
return self._capture_page_screen_to_log(method)
@@ -149,7 +150,7 @@ def capture_element_screenshot(
self,
locator: Locator,
filename: str = DEFAULT_FILENAME_ELEMENT,
- ) -> str:
+ ) -> str | None:
"""Captures a screenshot from the element identified by ``locator`` and embeds it into log file.
See `Capture Page Screenshot` for details about ``filename`` argument.
@@ -179,7 +180,7 @@ def capture_element_screenshot(
self.info(
"Cannot capture screenshot from element because no browser is open."
)
- return
+ return None
element = self.find_element(locator, required=True)
is_embedded, method = self._decide_embedded(filename)
if is_embedded:
@@ -263,24 +264,25 @@ def _embed_to_log_as_file(self, path, width):
f'',
html=True,
)
-
+
@keyword
- def print_page_as_pdf(self,
- filename: str = DEFAULT_FILENAME_PDF,
- background: Optional[bool] = None,
- margin_bottom: Optional[float] = None,
- margin_left: Optional[float] = None,
- margin_right: Optional[float] = None,
- margin_top: Optional[float] = None,
- orientation: Optional[Orientation] = None,
- page_height: Optional[float] = None,
- page_ranges: Optional[list] = None,
- page_width: Optional[float] = None,
- scale: Optional[float] = None,
- shrink_to_fit: Optional[bool] = None,
- # path_to_file=None,
- ):
- """ Print the current page as a PDF
+ def print_page_as_pdf( # noqa: PLR0912 C901
+ self,
+ filename: str = DEFAULT_FILENAME_PDF,
+ background: bool | None = None,
+ margin_bottom: float | None = None,
+ margin_left: float | None = None,
+ margin_right: float | None = None,
+ margin_top: float | None = None,
+ orientation: Orientation | None = None,
+ page_height: float | None = None,
+ page_ranges: list | None = None,
+ page_width: float | None = None,
+ scale: float | None = None,
+ shrink_to_fit: bool | None = None,
+ # path_to_file=None,
+ ):
+ """Print the current page as a PDF
``page_ranges`` defaults to `['-']` or "all" pages. ``page_ranges`` takes a list of
strings indicating the ranges.
@@ -304,11 +306,11 @@ def print_page_as_pdf(self,
"""
if page_ranges is None:
- page_ranges = ['-']
+ page_ranges = ["-"]
print_options = PrintOptions()
if background is not None:
- print_options.background = background
+ print_options.background = background
if margin_bottom is not None:
print_options.margin_bottom = margin_bottom
if margin_left is not None:
@@ -332,7 +334,7 @@ def print_page_as_pdf(self,
if not self.drivers.current:
self.info("Cannot print page to pdf because no browser is open.")
- return
+ return None
return self._print_page_as_pdf_to_file(filename, print_options)
def _print_page_as_pdf_to_file(self, filename, options):
@@ -340,13 +342,13 @@ def _print_page_as_pdf_to_file(self, filename, options):
self._create_directory(path)
pdfdata = self.driver.print_page(options)
if not pdfdata:
- raise RuntimeError(f"Failed to print page.")
+ raise RuntimeError("Failed to print page.")
self._save_pdf_to_file(pdfdata, path)
return path
def _save_pdf_to_file(self, pdfbase64, path):
pdfdata = b64decode(pdfbase64)
- with open(path, mode='wb') as pdf:
+ with open(path, mode="wb") as pdf:
pdf.write(pdfdata)
def _get_pdf_path(self, filename):
diff --git a/src/SeleniumLibrary/keywords/selectelement.py b/src/SeleniumLibrary/keywords/selectelement.py
index 68290f1fd..24b45978f 100644
--- a/src/SeleniumLibrary/keywords/selectelement.py
+++ b/src/SeleniumLibrary/keywords/selectelement.py
@@ -13,9 +13,7 @@
# 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.
-from typing import List, Optional, Union
-from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import Select
from SeleniumLibrary.base import LibraryComponent, keyword
@@ -25,9 +23,7 @@
class SelectElementKeywords(LibraryComponent):
@keyword
- def get_list_items(
- self, locator: Locator, values: bool = False
- ) -> List[str]:
+ def get_list_items(self, locator: Locator, values: bool = False) -> list[str]:
"""Returns all labels or values of selection list ``locator``.
See the `Locating elements` section for details about the locator
@@ -46,8 +42,7 @@ def get_list_items(
options = self._get_options(locator)
if is_truthy(values):
return self._get_values(options)
- else:
- return self._get_labels(options)
+ return self._get_labels(options)
@keyword
def get_selected_list_label(self, locator: Locator) -> str:
@@ -63,7 +58,7 @@ def get_selected_list_label(self, locator: Locator) -> str:
return select.first_selected_option.text
@keyword
- def get_selected_list_labels(self, locator: Locator) -> List[str]:
+ def get_selected_list_labels(self, locator: Locator) -> list[str]:
"""Returns labels of selected options from selection list ``locator``.
Starting from SeleniumLibrary 3.0, returns an empty list if there
@@ -89,7 +84,7 @@ def get_selected_list_value(self, locator: Locator) -> str:
return select.first_selected_option.get_attribute("value")
@keyword
- def get_selected_list_values(self, locator: Locator) -> List[str]:
+ def get_selected_list_values(self, locator: Locator) -> list[str]:
"""Returns values of selected options from selection list ``locator``.
Starting from SeleniumLibrary 3.0, returns an empty list if there
@@ -136,7 +131,9 @@ def list_selection_should_be(self, locator: Locator, *expected: str):
)
def _format_selection(self, labels, values):
- return " | ".join(f"{label} ({value})" for label, value in zip(labels, values))
+ return " | ".join(
+ f"{label} ({value})" for label, value in zip(labels, values, strict=True)
+ )
@keyword
def list_should_have_no_selections(self, locator: Locator):
@@ -160,7 +157,7 @@ def list_should_have_no_selections(self, locator: Locator):
def page_should_contain_list(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies selection list ``locator`` is found from current page.
@@ -177,7 +174,7 @@ def page_should_contain_list(
def page_should_not_contain_list(
self,
locator: Locator,
- message: Optional[str] = None,
+ message: str | None = None,
loglevel: str = "TRACE",
):
"""Verifies selection list ``locator`` is not found from current page.
@@ -293,9 +290,7 @@ def unselect_all_from_list(self, locator: Locator):
select.deselect_all()
@keyword
- def unselect_from_list_by_index(
- self, locator: Locator, *indexes: str
- ):
+ def unselect_from_list_by_index(self, locator: Locator, *indexes: str):
"""Unselects options from selection list ``locator`` by ``indexes``.
Indexes of list options start from 0. This keyword works only with
@@ -320,9 +315,7 @@ def unselect_from_list_by_index(
select.deselect_by_index(int(index))
@keyword
- def unselect_from_list_by_value(
- self, locator: Locator, *values: str
- ):
+ def unselect_from_list_by_value(self, locator: Locator, *values: str):
"""Unselects options from selection list ``locator`` by ``values``.
This keyword works only with multi-selection lists.
@@ -345,9 +338,7 @@ def unselect_from_list_by_value(
select.deselect_by_value(value)
@keyword
- def unselect_from_list_by_label(
- self, locator: Locator, *labels: str
- ):
+ def unselect_from_list_by_label(self, locator: Locator, *labels: str):
"""Unselects options from selection list ``locator`` by ``labels``.
This keyword works only with multi-selection lists.
diff --git a/src/SeleniumLibrary/keywords/waiting.py b/src/SeleniumLibrary/keywords/waiting.py
index b8bb9473e..2d6cfe81e 100644
--- a/src/SeleniumLibrary/keywords/waiting.py
+++ b/src/SeleniumLibrary/keywords/waiting.py
@@ -16,7 +16,6 @@
import time
from datetime import timedelta
-from typing import Optional
from selenium.common.exceptions import StaleElementReferenceException
@@ -31,8 +30,8 @@ class WaitingKeywords(LibraryComponent):
def wait_for_condition(
self,
condition: str,
- timeout: Optional[timedelta] = None,
- error: Optional[str] = None,
+ timeout: timedelta | None = None,
+ error: str | None = None,
):
"""Waits until ``condition`` is true or ``timeout`` expires.
@@ -66,8 +65,8 @@ def wait_for_condition(
def wait_until_location_is(
self,
expected: str,
- timeout: Optional[timedelta] = None,
- message: Optional[str] = None,
+ timeout: timedelta | None = None,
+ message: str | None = None,
):
"""Waits until the current URL is ``expected``.
@@ -95,8 +94,8 @@ def wait_until_location_is(
def wait_until_location_is_not(
self,
location: str,
- timeout: Optional[timedelta] = None,
- message: Optional[str] = None,
+ timeout: timedelta | None = None,
+ message: str | None = None,
):
"""Waits until the current URL is not ``location``.
@@ -123,8 +122,8 @@ def wait_until_location_is_not(
def wait_until_location_contains(
self,
expected: str,
- timeout: Optional[timedelta] = None,
- message: Optional[str] = None,
+ timeout: timedelta | None = None,
+ message: str | None = None,
):
"""Waits until the current URL contains ``expected``.
@@ -151,8 +150,8 @@ def wait_until_location_contains(
def wait_until_location_does_not_contain(
self,
location: str,
- timeout: Optional[timedelta] = None,
- message: Optional[str] = None,
+ timeout: timedelta | None = None,
+ message: str | None = None,
):
"""Waits until the current URL does not contains ``location``.
@@ -179,8 +178,8 @@ def wait_until_location_does_not_contain(
def wait_until_page_contains(
self,
text: str,
- timeout: Optional[timedelta] = None,
- error: Optional[str] = None,
+ timeout: timedelta | None = None,
+ error: str | None = None,
):
"""Waits until ``text`` appears on the current page.
@@ -201,8 +200,8 @@ def wait_until_page_contains(
def wait_until_page_does_not_contain(
self,
text: str,
- timeout: Optional[timedelta] = None,
- error: Optional[str] = None,
+ timeout: timedelta | None = None,
+ error: str | None = None,
):
"""Waits until ``text`` disappears from the current page.
@@ -223,9 +222,9 @@ def wait_until_page_does_not_contain(
def wait_until_page_contains_element(
self,
locator: Locator,
- timeout: Optional[timedelta] = None,
- error: Optional[str] = None,
- limit: Optional[int] = None,
+ timeout: timedelta | None = None,
+ error: str | None = None,
+ limit: int | None = None,
):
"""Waits until the element ``locator`` appears on the current page.
@@ -244,12 +243,13 @@ def wait_until_page_contains_element(
``limit`` is new in SeleniumLibrary 4.4
"""
if limit is None:
- return self._wait_until(
+ self._wait_until(
lambda: self.find_element(locator, required=False) is not None,
f"Element '{locator}' did not appear in