Automatización de UI en Python aplicando el patrón Screenplay
domingo, 14 de agosto de 2022
Esta fue una presentación sobre screenpy una libreria que usa el patron screenplay en python y permite su uso Ver la documentación
A continuación vemos el notebook alojado como gist
ScreenPy
Es una implementación del patrón screenplay en python
En este notebook es una traducción del ejemplo completo de screenplay https://screenpy-docs.readthedocs.io/en/latest/example.html
%pip install screenpy
11692.70s - pydevd: Sending message related to process being replaced timed-out after 5 seconds
Requirement already satisfied: screenpy in /home/scot3004/proyectos/comunidades/screenpy_examples/venv/lib/python3.8/site-packages (4.0.1)
Requirement already satisfied: typing-extensions<4.2,>=4.1.1 in /home/scot3004/proyectos/comunidades/screenpy_examples/venv/lib/python3.8/site-packages (from screenpy) (4.1.1)
Requirement already satisfied: PyHamcrest<2.1,>=2.0.0 in /home/scot3004/proyectos/comunidades/screenpy_examples/venv/lib/python3.8/site-packages (from screenpy) (2.0.3)
[33mWARNING: You are using pip version 19.2.3, however version 22.2.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.[0m
Note: you may need to restart the kernel to use updated packages.
Actores (Actor)
Los actores representan a sus usuarios finales. Se les otorgarán Habilidades, realizarán Acciones, harán Preguntas y (con suerte) cumplirán Resoluciones durante el transcurso de sus pruebas.
Instanciar un Actor es simple:
from screenpy import AnActor
Cameron = AnActor.named("Cameron")
Polly = AnActor.named("Polly")
El nombre que le dé a sus Actores se utilizará para registrar las Acciones que realizan. Para realizar Acciones más interesantes, su Actor necesitará algunas Habilidades:
# grant abilities on instantiation
Cameron = AnActor.named("Cameron").who_can(ControlCameras())
# or later, if you want
Polly.can(PollTheAudience())
De nuestro ejemplo completo, otorgamos a Cameron la capacidad de controlar cámaras y a Polly la capacidad de sondear a la audiencia. Estas Habilidades permitieron a nuestros Actores realizar varias Acciones.
Habilidades (Abilities)
Las habilidades habilitan a los Actores a usar librerias, conectarse con recursos, o bien cualquier otra cosa
En nuestro ejemplo completo, otorgamos a Cameron la capacidad de controlar cámaras. Esta habilidad utiliza una biblioteca inventada, cam_py, para controlar las cámaras. Esta habilidad podría verse así:
import cam_py
class ControlCameras:
"""Enable an Actor to control cameras through cam_py.
Examples::
the_actor.can(ControlCameras())
"""
def __init__(self) -> None:
self.campy_session = cam_py.RecordingSession()
self.cameras = []
def forget(self) -> None:
for camera in self.cameras:
camera.stop()
self.campy_session.wrap()
ControlCameras
__main__.ControlCameras
Las habilidades son olvidables, lo que significa que una habilidad debe tener un método forget
. Este método se encarga de limpiar los cabos sueltos que cuelgan.
Tanto las Acciones como las Preguntas pueden usar las Habilidades de un Actor.
Acciones (Actions)
Durante una prueba, tus Actores realizarán varias Acciones. Los actores realizan acciones para configurar y realizar la prueba.
En nuestro ejemplo completo, Cameron usa varias acciones. Uno de ellos es StartRecording.
Así es como se podría codificar esa acción:
# %load actions/start_recording.py
"""
Start recording a screenplay on one or more cameras!
"""
import cam_py
from screenpy import Actor
from screenpy.pacing import beat
from ..abilities import ControlCameras
class StartRecording:
"""Starts recording on one or more cameras.
Examples::
the_actor.attempts_to(StartRecording())
camera = Camera("Character")
the_actor.attempts_to(StartRecording.on(camera))
camera1 = Camera("Character1")
camera2 = Camera("Character2")
the_actor.attempts_to(StartRecording.on(camera1).and_(camera2))
"""
def on(self, camera: cam_py.Camera) -> "StartRecording":
"""Record on an already-created camera."""
self.cameras.append(camera)
return self
and_ = on
@beat("{} starts recording on {cameras_to_log}.")
def perform_as(self, the_actor: Actor) -> None:
"""Direct the actor to start recording on their cameras."""
if not self.cameras:
self.cameras = [cam_py.Camera("Main")]
campy_session = the_actor.ability_to(ControlCameras).campy_session
for camera in self.cameras:
camera.record(self.script)
campy_session.add_camera(camera)
@property
def cameras_to_log(self) -> str:
"""Get a nice list of all the cameras for the logged beat."""
return ", ".join(camera.character for camera in self.cameras)
def __init__(self, script: str) -> None:
self.script = script
self.cameras = []
Tareas (Tasks)
Las tareas son una agrupación de acciones. tienen un método perform_as
ver clase Performable, al igual que las acciones, lo que significa que tienen un método perform_as. Ese es el único requisito.
Puede crear tareas para un grupo repetido de acciones, como LogIn
para iniciar sesión. También puede crear tareas para describir un grupo de acciones que solo realiza una vez con un nombre más descriptivo, como ChangeProfilePicture
.
Se utilizaron dos tareas en nuestro ejemplo completo: CutToCloseUp
y DollyZoom
. Veamos cómo podría implementarse DollyZoom
:
# %load tasks/dolly_zoom.py
"""
Dolly-zoom, that classic tension shot.
https://en.wikipedia.org/wiki/Dolly_zoom
"""
from typing import Optional
from screenpy import Actor
from screenpy.pacing import beat
from ..abilities import ControlCameras
from ..actions import Dolly, Simultaneously, Zoom
class DollyZoom:
"""Perform a dolly zoom (optionally on a character) to enhance drama.
Examples::
the_actor.attempts_to(DollyZoom())
the_actor.attempts_to(DollyZoom.on("Alfred Hitchcock"))
"""
@staticmethod
def on(character: str) -> "DollyZoom":
"""Specify the character to put in frame before dolly zooming."""
return DollyZoom(character)
@beat("{} executes a thrilling dolly zoom{detail}!")
def perform_as(self, the_actor: Actor) -> None:
"""Direct the actor to dolly zoom on their camera."""
if self.character:
campy_session = the_actor.ability_to(ControlCameras).campy_session
camera = campy_session.get_camera_on_character(self.character)
zoom = Zoom.in_().on_camera(camera)
else:
zoom = Zoom.in_()
the_actor.attempts_to(
Simultaneously(
Dolly().backward(),
zoom,
),
)
def __init__(self, character: Optional[str] = None) -> None:
self.character = character
self.detail = f" on {character}" if character else ""
Como puede ver, esta Tarea simplemente realiza otras tres Acciones.
Simultaneously
, es una acción en cam_py
que realiza todas las acciones dadas a la vez;
Dolly
, que mueve la cámara en la dirección especificada;
Zoom
, que acerca o aleja la cámara.
El Narrador leerá las líneas beat()
para cada acción. La línea de tareas de DollyZoom
aparecerá para encapsular las líneas de otras acciones. Así es como se ve desde el StdOutAdapter incorporado:
INFO screenpy:stdout_adapter.py:42 Cameron executes a dramatic dolly zoom!
INFO screenpy:stdout_adapter.py:42 Cameron performs some thrilling camerawork simultaneously!
INFO screenpy:stdout_adapter.py:42 Cameron dollies the active camera backward.
INFO screenpy:stdout_adapter.py:42 Cameron zooms in.
Preguntas (Questions)
Cuando esté listo para hacer una verificación (assert) en su prueba, su actor puede hacer una pregunta. La respuesta a esta Pregunta se comparara con una Resolución. Este emparejamiento forma una afirmación en ScreenPy.
Nuestro ejemplo completo usa dos preguntas: AudienceTension
y TopAudienceReaction
. Veamos cómo se vería esto último:
# %load questions/top_audience_reaction.py
"""
Gather information about the audience's tension.
"""
from screenpy import Actor
from ..abilities import PollTheAudience
class TopAudienceReaction:
"""Ask about the audience's most popular reaction.
Examples::
the_actor.should(See.the(TopAudienceReaction(), Equals(LAUGHING))
"""
def answered_by(self, the_actor: Actor) -> str:
"""Direct the actor to ask about the audience's top mood."""
pollster = the_actor.ability_to(PollTheAudience).poll_connection
return pollster.poll_mood().top_mood
Una pregunta es Answerable, quiere decir que tiene un método answered_by
.
Pasar una pregunta junto con una resolución a la acción See es cómo hacer afirmaciones en ScreenPy. La Pregunta proporciona el valor real mientras que la Resolución proporciona el valor esperado.
Resoluciones (Resolutions)
Las resoluciones proporcionan tanto el valor esperado como el método de comparación para la respuesta a una pregunta. Emparejar una pregunta y una resolución forma el paso de afirmación de sus pruebas.
Hubo un par de ejemplos de afirmaciones en nuestro Ejemplo completo, ambos realizados por Polly. Uno usó la Resolución Equals incorporada, mientras que el otro usó una Resolución IsPalpable
personalizada. Examinemos esto último.
Todas las resoluciones heredan de la clase BaseResolution
. Todo lo que se necesita es una línea y una función de “matcher”.
La línea aparece en el registro. Escriba la línea de tal manera que describa el valor esperado mientras completa la oración, “Esperando que sea…”. Puede usar {expectation} aquí para hacer referencia al valor esperado.
La función “matcher” puede provenir de PyHamcrest, o puede ser una funcionalidad personalizada escrita por usted.
De todos modos, IsPalpable
usa el “matcher” personalizado HasSaturationGreaterThan
. Así es como se pueden ensamblar:
# %load resolutions/matchers/has_saturation_greater_than.py
from typing import Any
from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.description import Description
class HasSaturationGreaterThan(BaseMatcher):
"""Assert that a mood object has at least a specific saturation level."""
def _matches(self, item: Any) -> bool:
"""Whether the assertion passes."""
return self.saturation_level <= item.saturation
def describe_to(self, description: Description) -> None:
"""Describe the passing case."""
description.append_text(
f"the mood has a saturation level of at least {self.saturation_level}"
)
def describe_mismatch(self, item: Any, mismatch_description: Description) -> None:
"""Description used when a match fails."""
mismatch_description.append_text(
f"the saturation level was less than {self.saturation_level}"
)
def describe_match(self, item: Any, match_description: Description) -> None:
"""Description used when a negated match fails."""
match_description.append_text(
f"the saturation level was at least {self.saturation_level}"
)
def __init__(self, saturation_level: int) -> None:
self.saturation_level = saturation_level
def is_palpable() -> HasSaturationGreaterThan:
return HasSaturationGreaterThan(85)
# %load resolutions/is_palpable.py
from screenpy.resolutions import BaseResolution
from .matchers.has_saturation_greater_than import is_palpable
class IsPalpable(BaseResolution):
"""Match a tension level that is very, very high!!!
Examples::
the_actor.should(See.the(AudienceTension(), IsPalpable()))
"""
line = "a palpable tension!"
matcher_function = is_palpable
¡Eso es realmente todo lo que hay que hacer para crear una Resolución!