Skip to content

Commit

Permalink
Actionchain speedup (ScreenPyHQ#55)
Browse files Browse the repository at this point in the history
* adding the ability to adjust the duration of ActionChains
* adding test for settings
* fixing namespace test
* using keyword for argument
* adding documentation for settings
* adding pydantic reqs
* docs section correction
  • Loading branch information
bandophahita authored Mar 8, 2024
1 parent 4b7ea86 commit e9c2006
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 205 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ to :class:`~screenpy_selenium.abilities.BrowseTheWeb`!
targets
cookbook
deprecations
settings
55 changes: 55 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
========
Settings
========

To configure ScreenPy Selenium,
we provide some settings
through `Pydantic's settings management <https://docs.pydantic.dev/usage/settings/>`__.

Settings can be configured through these ways:

* In your test configuration file (like conftest.py).
* Using environment variables.
* In the ``[tool.screenpy.selenium]`` section in your ``pyproject.toml``.

The above options are in order of precedence;
that is,
setting the values directly in your configuration file will override environment variables,
any environment variables will override any ``pyproject.toml`` settings,
and any ``pyproject.toml`` settings will override the defaults.

To demonstrate,
here is how we can change the default actionchain duration
used by things like :class:`screenpy_selenium.actions.Chain`::

# in your conftest.py
from screenpy_selenium import settings

settings.CHAIN_DURATION = 50

.. code-block:: bash
$ # environment variables in your shell
$ SCREENPY_SELENIUM_CHAIN_DURATION=50 pytest
.. code-block:: toml
# in your pyproject.toml file
[tool.screenpy.selenium]
CHAIN_DURATION = 50
The environment variable approach
works particularly well with `python-dotenv <https://pypi.org/project/python-dotenv/>`__!



Default Settings
----------------

These are the default settings included in ScreenPy Selenium.

ScreenPy Selenium Default Settings
++++++++++++++++++++++++++++++++++

.. autopydantic_settings:: screenpy_selenium.configuration.ScreenPySeleniumSettings

421 changes: 222 additions & 199 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ python = "^3.8"
screenpy = ">=4.0.2"
screenpy_pyotp = ">=4.0.0"
selenium = ">=4.1.0"
pydantic = "*"
pydantic-settings = "*"
importlib_metadata = {version = "*", python = "3.8.*"}

# convenience packages for development
Expand All @@ -186,8 +188,11 @@ sphinx = {version = "*", optional = true}
sphinx-rtd-theme = {version = "*", optional = true}
tox = {version = "*", optional = true}

autodoc-pydantic = {version = "*", optional = true}

[tool.poetry.extras]
dev = [
"autodoc-pydantic",
"black",
"coverage",
"cruft",
Expand All @@ -206,6 +211,7 @@ test = [
"pytest-mock",
]
docs = [
"autodoc-pydantic",
"sphinx",
"sphinx-rtd-theme",
]
6 changes: 4 additions & 2 deletions screenpy_selenium/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@
from . import abilities, actions, questions, resolutions
from .abilities import * # noqa: F403
from .actions import * # noqa: F403
from .configuration import settings
from .exceptions import BrowsingError, TargetingError
from .protocols import Chainable
from .questions import * # noqa: F403
from .resolutions import * # noqa: F403
from .target import Target

__all__ = [
"Target",
"TargetingError",
"BrowsingError",
"Chainable",
"settings",
"Target",
"TargetingError",
]

__all__ += abilities.__all__ + actions.__all__ + questions.__all__ + resolutions.__all__
3 changes: 2 additions & 1 deletion screenpy_selenium/actions/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from selenium.webdriver.common.action_chains import ActionChains

from ..abilities import BrowseTheWeb
from ..configuration import settings
from ..protocols import Chainable

if TYPE_CHECKING:
Expand Down Expand Up @@ -41,7 +42,7 @@ def describe(self) -> str:
def perform_as(self, the_actor: Actor) -> None:
"""Choreograph the Actions and direct the Actor to perform the chain."""
browser = the_actor.ability_to(BrowseTheWeb).browser
the_chain = ActionChains(browser)
the_chain = ActionChains(browser, duration=settings.CHAIN_DURATION)

for action in self.actions:
if not isinstance(action, Chainable):
Expand Down
3 changes: 2 additions & 1 deletion screenpy_selenium/actions/double_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from selenium.webdriver.common.action_chains import ActionChains

from ..abilities import BrowseTheWeb
from ..configuration import settings

if TYPE_CHECKING:
from screenpy.actor import Actor
Expand Down Expand Up @@ -69,7 +70,7 @@ def describe(self) -> str:
def perform_as(self, the_actor: Actor) -> None:
"""Direct the Actor to double-click on the element."""
browser = the_actor.ability_to(BrowseTheWeb).browser
the_chain = ActionChains(browser) # type: ignore[arg-type]
the_chain = ActionChains(browser, duration=settings.CHAIN_DURATION)
self._add_action_to_chain(the_actor, the_chain)
the_chain.perform()

Expand Down
3 changes: 2 additions & 1 deletion screenpy_selenium/actions/move_mouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from selenium.webdriver.common.action_chains import ActionChains

from ..abilities import BrowseTheWeb
from ..configuration import settings

if TYPE_CHECKING:
from screenpy.actor import Actor
Expand Down Expand Up @@ -118,7 +119,7 @@ def describe(self) -> str:
def perform_as(self, the_actor: Actor) -> None:
"""Direct the Actor to move the mouse."""
browser = the_actor.ability_to(BrowseTheWeb).browser
the_chain = ActionChains(browser) # type: ignore[arg-type]
the_chain = ActionChains(browser, duration=settings.CHAIN_DURATION)
self._add_action_to_chain(the_actor, the_chain)
the_chain.perform()

Expand Down
3 changes: 2 additions & 1 deletion screenpy_selenium/actions/right_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from selenium.webdriver.common.action_chains import ActionChains

from ..abilities import BrowseTheWeb
from ..configuration import settings

if TYPE_CHECKING:
from screenpy import Actor
Expand Down Expand Up @@ -73,7 +74,7 @@ def describe(self) -> str:
def perform_as(self, the_actor: Actor) -> None:
"""Direct the Actor to right-click on the element."""
browser = the_actor.ability_to(BrowseTheWeb).browser
the_chain = ActionChains(browser) # type: ignore[arg-type]
the_chain = ActionChains(browser, duration=settings.CHAIN_DURATION)
self._add_action_to_chain(the_actor, the_chain)
the_chain.perform()

Expand Down
24 changes: 24 additions & 0 deletions screenpy_selenium/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Define settings for the StdOutAdapter."""

from pydantic_settings import SettingsConfigDict
from screenpy.configuration import ScreenPySettings


class ScreenPySeleniumSettings(ScreenPySettings):
"""Settings for the ScreenPySelenium.
To change these settings using environment variables, use the prefix
``SCREENPY_SELENIUM_``, like so::
SCREENPY_SELENIUM_CHAIN_DURATION=50 # sets the actionchain duration to 50ms
"""

_tool_path = "screenpy.selenium"
model_config = SettingsConfigDict(env_prefix="SCREENPY_SELENIUM_")

CHAIN_DURATION: int = 10
"""Default duration of ActionChains in milleseconds"""


# initialized instance
settings = ScreenPySeleniumSettings()
1 change: 1 addition & 0 deletions tests/test_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def test_screenpy_selenium() -> None:
"Visits",
"Wait",
"Waits",
"settings",
)

assert sorted(screenpy_selenium.__all__) == sorted(expected)
Expand Down
44 changes: 44 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os
from unittest import mock

from screenpy_selenium import settings as screenpy_selenium_settings
from screenpy_selenium.configuration import ScreenPySeleniumSettings


class TestSettings:
def test_pyproject_overwrites_initial(self) -> None:
mock_open = mock.mock_open(
read_data=b"[tool.screenpy.selenium]\nCHAIN_DURATION = 500"
)

with mock.patch("pathlib.Path.open", mock_open):
settings = ScreenPySeleniumSettings()

assert settings.CHAIN_DURATION == 500

def test_env_overwrites_pyproject(self) -> None:
mock_open = mock.mock_open(
read_data=b"[tool.screenpy.selenium]\nCHAIN_DURATION = 500"
)
mock_env = {"SCREENPY_SELENIUM_CHAIN_DURATION": "1337"}

with mock.patch("pathlib.Path.open", mock_open): # noqa: SIM117
with mock.patch.dict(os.environ, mock_env):
settings = ScreenPySeleniumSettings()

assert settings.CHAIN_DURATION == 1337

def test_init_overwrites_env(self) -> None:
mock_env = {"SCREENPY_SELENIUM_CHAIN_DURATION": "1337"}

with mock.patch.dict(os.environ, mock_env):
settings = ScreenPySeleniumSettings(CHAIN_DURATION=9001)

assert settings.CHAIN_DURATION == 9001

def test_can_be_changed_at_runtime(self) -> None:
try:
screenpy_selenium_settings.CHAIN_DURATION = 100
except TypeError as exc:
msg = "ScreenPySeleniumSettings could not be changed at runtime."
raise AssertionError(msg) from exc

0 comments on commit e9c2006

Please sign in to comment.