Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Actionchain speedup #55

Merged
merged 7 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to also link to ScreenPy's settings here?

========

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"""
Comment on lines +7 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it nice how easy this is?



# 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