Skip to content

Commit 0d44daf

Browse files
author
dmy.berezovskyi
committed
added logger decorator that takes from func doc string
1 parent be6966a commit 0d44daf

File tree

10 files changed

+102
-40
lines changed

10 files changed

+102
-40
lines changed

core/driver.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import os
22
from abc import ABC, abstractmethod
3-
43
from selenium import webdriver
54
from selenium.webdriver.chrome.service import Service as ChromeService
65
from selenium.webdriver.remote.remote_connection import RemoteConnection
76
from webdriver_manager.chrome import ChromeDriverManager
8-
97
from core.driver_options import _init_driver_options
108
from utils.error_handler import ErrorHandler, ErrorType
119
from utils.logger import Logger, LogLevel
@@ -31,9 +29,9 @@ def _get_driver_path(driver_type=None):
3129

3230
def _configure_driver(driver, environment):
3331
driver.maximize_window()
34-
driver.implicitly_wait(15)
32+
driver.implicitly_wait(3)
3533
driver.get(Properties.get_base_url(environment))
36-
log.info(f"Local Chrome driver created with session: {driver}")
34+
log.info(f"Configure driver and base url: {Properties.get_base_url(environment)}")
3735

3836

3937
class Driver(ABC):
@@ -43,28 +41,32 @@ def create_driver(self, environment, dr_type):
4341

4442
def get_desired_caps(self, browser="chrome"):
4543
caps = YAMLReader.read_caps(browser)
46-
log.info(f"capabilities for driver {caps}")
44+
log.info(f"Capabilities for driver {caps}")
4745
return caps
4846

4947

5048
class LocalDriver(Driver):
51-
def create_driver(self, environment=None, dr_type=None):
49+
def create_driver(self, environment=None, dr_type="local"):
5250
"""Tries to use ChromeDriverManager to install the latest driver,
5351
and if it fails, it falls back to a locally stored driver in resources."""
52+
driver = None
5453
try:
55-
log.info(f"Run local chrome driver with {_init_driver_options()}")
5654
driver_path = ChromeDriverManager().install()
55+
options = _init_driver_options(dr_type=dr_type)
5756
driver = webdriver.Chrome(
5857
service=ChromeService(executable_path=driver_path),
59-
options=_init_driver_options(dr_type=dr_type))
58+
options=options
59+
)
60+
log.info(
61+
f"Created local Chrome driver with session: {driver.session_id}")
6062
except Exception as e:
61-
log.info(f"Run local driver: {e}")
63+
log.error(
64+
f"Failed to create Chrome driver, falling back to local driver: {e}")
6265
driver = webdriver.Chrome(
6366
service=ChromeService(_get_driver_path(dr_type)),
6467
options=_init_driver_options(dr_type=dr_type),
6568
)
6669
_configure_driver(driver, environment)
67-
log.info(f"Local Chrome driver created with session: {driver}")
6870
return driver
6971

7072

@@ -75,19 +77,23 @@ def create_driver(self, environment=None, dr_type=None):
7577
command_executor=RemoteConnection("your remote URL"),
7678
desired_capabilities={"LT:Options": caps}, # noqa
7779
)
78-
log.info(f"Local Chrome driver created with session: {driver}")
80+
log.info(
81+
f"Remote Chrome driver created with session: {driver.session_id}")
7982
return driver
8083

8184

8285
class FirefoxDriver(Driver):
8386
def create_driver(self, environment=None, dr_type=None):
8487
try:
85-
driver = webdriver.Firefox(options=_init_driver_options(dr_type))
88+
driver = webdriver.Firefox(
89+
options=_init_driver_options(dr_type=dr_type))
90+
log.info(f"Created Firefox driver with session: {driver.session_id}")
8691
except Exception as e:
8792
driver = webdriver.Chrome(
8893
service=ChromeService(_get_driver_path(dr_type)),
89-
options=_init_driver_options(),
94+
options=_init_driver_options(dr_type=dr_type),
9095
)
91-
log.error(f"Run local firefox driver: {e}")
96+
log.error(
97+
f"Failed to create Firefox driver, falling back to Chrome: {e}")
9298
_configure_driver(driver, environment)
9399
return driver

core/driver_factory.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
log = Logger(log_lvl=LogLevel.INFO).get_instance()
66

7-
87
class WebDriverFactory:
98
DRIVER_MAPPING = {
109
"chrome": ChromeRemoteDriver,
@@ -14,12 +13,10 @@ class WebDriverFactory:
1413

1514
@staticmethod
1615
def create_driver(environment=None, driver_type="local"):
17-
log.info(f"Creating driver {driver_type}")
16+
log.info(f"Creating driver of type: {driver_type}")
1817
driver_type = driver_type.lower()
1918
if driver_type in WebDriverFactory.DRIVER_MAPPING:
2019
driver_class = WebDriverFactory.DRIVER_MAPPING[driver_type]
21-
return driver_class().create_driver(environment, driver_type)
20+
return driver_class().create_driver(environment, driver_type) # No .value needed
2221
else:
23-
raise ErrorHandler.raise_error(
24-
ErrorType.ENV_ERROR, environment, driver_type
25-
)
22+
raise ErrorHandler.raise_error(ErrorType.ENV_ERROR, environment, driver_type)

core/driver_options.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1+
import os
12
import platform
2-
33
from selenium import webdriver
4-
54
from utils.logger import Logger, LogLevel
65
from utils.error_handler import ErrorHandler, ErrorType
76

87
log = Logger(log_lvl=LogLevel.INFO).get_instance()
98

109

1110
def _shared_driver_options(options):
12-
# ... (options setup)
13-
# options.add_argument("--headless") # use headless with --no-sandbox
1411
options.add_argument("--start-maximized")
1512
options.add_argument("--disable-dev-shm-usage")
13+
options.page_load_strategy = 'none' # disable waiting for fully load page
1614
if platform.system() == "Linux":
1715
options.add_argument("--no-sandbox")
1816
log.info(f"Driver options {options.arguments}")
@@ -28,9 +26,7 @@ def _init_driver_options(dr_type=None):
2826
options = driver_option_mapping.get(dr_type)
2927

3028
if options is None:
31-
raise ErrorHandler.raise_error(
32-
ErrorType.UNSUPPORTED_DRIVER_TYPE, dr_type
33-
)
29+
raise ErrorHandler.raise_error(ErrorType.UNSUPPORTED_DRIVER_TYPE, dr_type)
3430

3531
_shared_driver_options(options)
3632
log.info(f"Driver options {options.arguments}")

src/locators/locators.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
class General:
55
LOGO = (By.CLASS_NAME, "sj-logo lazy loaded")
6+
7+
8+
class TextFields:
9+
USER_NAME = (By.ID, "userName")

src/pageobjects/base_page.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
TimeoutException, ElementNotVisibleException
99
)
1010

11+
from utils.helpers import timing
1112
from utils.logger import log
1213

1314
# Type alias for locators
@@ -45,18 +46,21 @@ def _get_waiter(self, wait_type: Optional[WaitType] = None) -> WebDriverWait:
4546
WaitType.FLUENT: self._fluent_wait,
4647
}.get(wait_type, self._wait)
4748

49+
@timing
4850
def wait_for(
4951
self,
5052
locator: Locator,
51-
condition: Literal["clickable", "visible", "present"],
53+
condition: Literal["clickable", "visible", "present"] = "visible",
5254
waiter: Optional[WebDriverWait] = None,
5355
) -> WebElement:
56+
"""Wait for an element"""
5457
waiter = waiter or self._wait
5558

5659
conditions = {
57-
"clickable": EC.element_to_be_clickable(*locator),
58-
"visible": EC.visibility_of_element_located(*locator),
59-
"present": EC.presence_of_element_located(*locator),
60+
"clickable": EC.element_to_be_clickable(locator),
61+
# Pass the locator tuple as a single argument
62+
"visible": EC.visibility_of_element_located(locator),
63+
"present": EC.presence_of_element_located(locator),
6064
}
6165

6266
if condition not in conditions:
@@ -83,17 +87,20 @@ def click(
8387
element = self.wait_for(locator, condition=condition, waiter=waiter)
8488
element.click()
8589

90+
# @log()
91+
# @timing
8692
def set(
8793
self, locator: Locator, text: str, wait_type: Optional[WaitType] = None
8894
):
8995
"""
9096
Set text in an input field.
9197
"""
9298
waiter = self._get_waiter(wait_type)
93-
element = self.wait_for(locator, condition="visible", waiter=waiter)
99+
element = self.wait_for(locator, waiter=waiter)
94100
element.clear()
95101
element.send_keys(text)
96102

103+
@log
97104
def get_text(
98105
self, locator: Locator, wait_type: Optional[WaitType] = None
99106
) -> str:

src/pageobjects/text/fill_form.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from locators.locators import TextFields
2+
from pageobjects.base_page import BasePage
3+
from utils.logger import log, Logger
4+
5+
6+
class FillForm(BasePage):
7+
@log()
8+
def enter_username(self, name: str):
9+
"""Enter username"""
10+
self.set(TextFields.USER_NAME, name)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from pageobjects.text.fill_form import FillForm
2+
3+
4+
class TestFillForm:
5+
6+
def test_fill_user(self, make_driver):
7+
fill = FillForm(make_driver)
8+
fill.enter_username("selenium framework")

utils/helpers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,16 @@ def wrapper(*args, **kwargs):
1919
return wrapper
2020

2121
return decorator
22+
23+
24+
def timing(func):
25+
"""Perform a timing function"""
26+
@wraps(func)
27+
def wrapper(*args, **kwargs):
28+
start_time = time.time()
29+
result = func(*args, **kwargs)
30+
end_time = time.time()
31+
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
32+
return result
33+
34+
return wrapper

utils/logger.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,49 @@ def annotate(self, message: str, level: Literal["info", "warn", "debug", "error"
6464
raise ValueError(f"Invalid log level: {level}")
6565

6666

67-
def log(level: Literal["info", "warn", "debug", "error"] = "info") -> Callable:
67+
def log(
68+
data: Optional[str] = None,
69+
level: Literal["info", "warn", "debug", "error"] = "info"
70+
) -> Callable:
6871
"""Decorator to log the current method's execution.
6972
73+
:param data: Custom log message to use if no docstring is provided.
7074
:param level: Level of the logs, e.g., info, warn, debug, error.
7175
"""
7276
logger_instance = Logger() # Get the singleton instance of Logger
7377

7478
def decorator(func: Callable) -> Callable:
7579
def wrapper(self, *args, **kwargs) -> Any:
76-
result = func(self, *args, **kwargs)
77-
method_name = f" Method :: {func.__name__}()"
80+
# Get the method's docstring
7881
method_docs = format_method_doc_str(func.__doc__)
7982

80-
logs = method_docs + method_name if method_docs else f"Executed {func.__name__}()"
83+
# Raise an exception if both the docstring and data are None
84+
if method_docs is None and data is None:
85+
raise ValueError(
86+
f"No documentation available for method :: {func.__name__} and no custom log data provided."
87+
)
88+
89+
# Construct the parameter string for logging
90+
params_str = ', '.join(repr(arg) for arg in args)
91+
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in kwargs.items())
92+
all_params_str = ', '.join(filter(None, [params_str, kwargs_str]))
93+
94+
# Log message with method documentation or custom data
95+
logs = (method_docs + f" Method :: {func.__name__}()" + f" with parameters: {all_params_str}"
96+
if method_docs else
97+
data + f" Method :: {func.__name__}()" + f" with parameters: {all_params_str}")
98+
8199
logger_instance.annotate(logs, level)
82-
return result
100+
101+
# Call the original method, passing *args and **kwargs
102+
return func(self, *args, **kwargs) # <--- Fix: properly passing args and kwargs
83103

84104
return wrapper
85105

86106
return decorator
87107

88108

109+
89110
def format_method_doc_str(doc_str: Optional[str]) -> Optional[str]:
90111
"""Add a dot to the docs string if it doesn't exist."""
91112
if doc_str and not doc_str.endswith('.'):

utils/yaml_reader.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def _flatten_values(
139139

140140

141141
# Example usage
142-
caps = YAMLReader.read_caps("chrome", "caps.yaml")
142+
# caps = YAMLReader.read_caps("chrome", "caps.yaml")
143143
# Example usage simple namespace
144-
simple = YAMLReader.read("data.yaml", to_simple_namespace=True)
145-
print(simple.users.username1)
144+
# simple = YAMLReader.read("data.yaml", to_simple_namespace=True)
145+
# print(simple.users.username1)

0 commit comments

Comments
 (0)