Skip to content

Commit ab6e4f8

Browse files
titusfortnerdiemol
andauthored
[py] improve driver logging (#12103)
[py] implement log_output() for flexibility and consistency of driver logging Co-authored-by: Diego Molina <diemol@users.noreply.github.com>
1 parent 25dbacb commit ab6e4f8

File tree

10 files changed

+245
-19
lines changed

10 files changed

+245
-19
lines changed

py/selenium/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323
WaitExcTypes = typing.Iterable[typing.Type[Exception]]
2424

2525
# Service Types
26-
SubprocessStdAlias = typing.Union[int, typing.IO[typing.Any]]
26+
SubprocessStdAlias = typing.Union[int, str, typing.IO[typing.Any]]

py/selenium/webdriver/chrome/service.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# under the License.
1717
import typing
1818

19+
from selenium.types import SubprocessStdAlias
1920
from selenium.webdriver.chromium import service
2021

2122
DEFAULT_EXECUTABLE_PATH = "chromedriver"
@@ -38,6 +39,7 @@ def __init__(
3839
port: int = 0,
3940
service_args: typing.Optional[typing.List[str]] = None,
4041
log_path: typing.Optional[str] = None,
42+
log_output: SubprocessStdAlias = None,
4143
env: typing.Optional[typing.Mapping[str, str]] = None,
4244
**kwargs,
4345
) -> None:
@@ -46,6 +48,7 @@ def __init__(
4648
port=port,
4749
service_args=service_args,
4850
log_path=log_path,
51+
log_output=log_output,
4952
env=env,
5053
start_error_message="Please see https://chromedriver.chromium.org/home",
5154
**kwargs,

py/selenium/webdriver/chromium/service.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
import typing
18+
import warnings
1819

20+
from selenium.common import InvalidArgumentException
21+
from selenium.types import SubprocessStdAlias
1922
from selenium.webdriver.common import service
2023

2124

@@ -38,18 +41,30 @@ def __init__(
3841
port: int = 0,
3942
service_args: typing.Optional[typing.List[str]] = None,
4043
log_path: typing.Optional[str] = None,
44+
log_output: SubprocessStdAlias = None,
4145
env: typing.Optional[typing.Mapping[str, str]] = None,
4246
start_error_message: typing.Optional[str] = None,
4347
**kwargs,
4448
) -> None:
4549
self.service_args = service_args or []
46-
if log_path:
47-
self.service_args.append(f"--log-path={log_path}")
50+
self.log_output = log_output
51+
if log_path is not None:
52+
warnings.warn("log_path has been deprecated, please use log_output", DeprecationWarning, stacklevel=2)
53+
self.log_output = log_path
54+
55+
if "--append-log" in self.service_args or "--readable-timestamp" in self.service_args:
56+
if isinstance(self.log_output, str):
57+
self.service_args.append(f"--log-path={self.log_output}")
58+
self.log_output = None
59+
else:
60+
msg = "Appending logs and readable timestamps require log output to be a string representing file path"
61+
raise InvalidArgumentException(msg)
4862

4963
super().__init__(
5064
executable=executable_path,
5165
port=port,
5266
env=env,
67+
log_output=self.log_output,
5368
start_error_message=start_error_message,
5469
**kwargs,
5570
)

py/selenium/webdriver/common/service.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import os
2020
import subprocess
2121
import typing
22+
import warnings
2223
from abc import ABC
2324
from abc import abstractmethod
2425
from platform import system
@@ -51,14 +52,27 @@ def __init__(
5152
self,
5253
executable: str,
5354
port: int = 0,
54-
log_file: SubprocessStdAlias = DEVNULL,
55+
log_file: SubprocessStdAlias = None,
56+
log_output: SubprocessStdAlias = None,
5557
env: typing.Optional[typing.Mapping[typing.Any, typing.Any]] = None,
5658
start_error_message: typing.Optional[str] = None,
5759
**kwargs,
5860
) -> None:
61+
if isinstance(log_output, str):
62+
self.log_output = open(log_output, "a+", encoding="utf-8")
63+
elif log_output is subprocess.STDOUT:
64+
self.log_output = None
65+
elif log_output is None or log_output is subprocess.DEVNULL:
66+
self.log_output = open(os.devnull, "wb")
67+
else:
68+
self.log_output = log_output
69+
70+
if log_file is not None:
71+
warnings.warn("log_file has been deprecated, please use log_output", DeprecationWarning, stacklevel=2)
72+
self.log_output = open(log_file, "a+", encoding="utf-8")
73+
5974
self._path = executable
6075
self.port = port or utils.free_port()
61-
self.log_file = open(os.devnull, "wb") if not log_file == DEVNULL else log_file
6276
self.start_error_message = start_error_message or ""
6377
# Default value for every python subprocess: subprocess.Popen(..., creationflags=0)
6478
self.popen_kw = kwargs.pop("popen_kw", {})
@@ -129,10 +143,10 @@ def send_remote_shutdown_command(self) -> None:
129143

130144
def stop(self) -> None:
131145
"""Stops the service."""
132-
if self.log_file != PIPE and not (self.log_file == DEVNULL):
146+
if self.log_output != PIPE and not (self.log_output == DEVNULL):
133147
try:
134148
# Todo: Be explicit in what we are catching here.
135-
if hasattr(self.log_file, "close"):
149+
if hasattr(self.log_output, "close"):
136150
self.log_file.close() # type: ignore
137151
except Exception:
138152
pass
@@ -195,8 +209,8 @@ def _start_process(self, path: str) -> None:
195209
cmd,
196210
env=self.env,
197211
close_fds=close_file_descriptors,
198-
stdout=self.log_file,
199-
stderr=self.log_file,
212+
stdout=self.log_output,
213+
stderr=self.log_output,
200214
stdin=PIPE,
201215
creationflags=self.creation_flags,
202216
**self.popen_kw,

py/selenium/webdriver/edge/service.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import typing
1818
import warnings
1919

20+
from selenium.types import SubprocessStdAlias
2021
from selenium.webdriver.chromium import service
2122

2223
DEFAULT_EXECUTABLE_PATH = "msedgedriver"
@@ -41,6 +42,7 @@ def __init__(
4142
port: int = 0,
4243
verbose: bool = False,
4344
log_path: typing.Optional[str] = None,
45+
log_output: SubprocessStdAlias = None,
4446
service_args: typing.Optional[typing.List[str]] = None,
4547
env: typing.Optional[typing.Mapping[str, str]] = None,
4648
**kwargs,
@@ -60,6 +62,7 @@ def __init__(
6062
port=port,
6163
service_args=service_args,
6264
log_path=log_path,
65+
log_output=log_output,
6366
env=env,
6467
start_error_message="Please download from https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/",
6568
**kwargs,

py/selenium/webdriver/firefox/service.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
import typing
18+
import warnings
1819
from typing import List
1920

21+
from selenium.types import SubprocessStdAlias
2022
from selenium.webdriver.common import service
2123
from selenium.webdriver.common import utils
2224

@@ -41,17 +43,27 @@ def __init__(
4143
port: int = 0,
4244
service_args: typing.Optional[typing.List[str]] = None,
4345
log_path: typing.Optional[str] = None,
46+
log_output: SubprocessStdAlias = None,
4447
env: typing.Optional[typing.Mapping[str, str]] = None,
4548
**kwargs,
4649
) -> None:
47-
# Todo: This is vastly inconsistent, requires a follow up to standardise.
48-
file = log_path or "geckodriver.log"
49-
log_file = open(file, "a+", encoding="utf-8")
5050
self.service_args = service_args or []
51+
if log_path is not None:
52+
warnings.warn("log_path has been deprecated, please use log_output", DeprecationWarning, stacklevel=2)
53+
log_output = open(log_path, "a+", encoding="utf-8")
54+
55+
if log_path is None and log_output is None:
56+
warnings.warn(
57+
"Firefox will soon stop logging to geckodriver.log by default; Specify desired logs with log_output",
58+
DeprecationWarning,
59+
stacklevel=2,
60+
)
61+
log_output = open("geckodriver.log", "a+", encoding="utf-8")
62+
5163
super().__init__(
5264
executable=executable_path,
5365
port=port,
54-
log_file=log_file,
66+
log_output=log_output,
5567
env=env,
5668
**kwargs,
5769
)

py/selenium/webdriver/ie/service.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
import typing
18+
import warnings
1819
from typing import List
1920

21+
from selenium.types import SubprocessStdAlias
2022
from selenium.webdriver.common import service
2123

2224
DEFAULT_EXECUTABLE_PATH = "IEDriverServer.exe"
@@ -31,6 +33,7 @@ def __init__(
3133
port: int = 0,
3234
host: typing.Optional[str] = None,
3335
log_level: typing.Optional[str] = None,
36+
log_output: SubprocessStdAlias = None,
3437
log_file: typing.Optional[str] = None,
3538
**kwargs,
3639
) -> None:
@@ -51,11 +54,13 @@ def __init__(
5154
if log_level:
5255
self.service_args.append(f"--log-level={log_level}")
5356
if log_file:
57+
warnings.warn("log_file has been deprecated, please use log_output", DeprecationWarning, stacklevel=2)
5458
self.service_args.append(f"--log-file={log_file}")
5559

5660
super().__init__(
5761
executable_path,
5862
port=port,
63+
log_output=log_output,
5964
start_error_message="Please download from https://www.selenium.dev/downloads/ and read up at https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver",
6065
**kwargs,
6166
)

py/selenium/webdriver/safari/service.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
# under the License.
1717

1818
import os
19-
import subprocess
2019
import typing
20+
import warnings
2121

2222
from selenium.webdriver.common import service
2323

@@ -30,7 +30,7 @@ class Service(service.Service):
3030
3131
:param executable_path: install path of the safaridriver executable, defaults to `/usr/bin/safaridriver`.
3232
:param port: Port for the service to run on, defaults to 0 where the operating system will decide.
33-
:param quiet: Suppress driver stdout & stderr, redirects to os.devnull if enabled.
33+
:param quiet: (Deprecated) Suppress driver stdout & stderr, redirects to os.devnull if enabled.
3434
:param service_args: (Optional) List of args to be passed to the subprocess when launching the executable.
3535
:param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`.
3636
"""
@@ -39,21 +39,21 @@ def __init__(
3939
self,
4040
executable_path: str = DEFAULT_EXECUTABLE_PATH,
4141
port: int = 0,
42-
quiet: bool = False,
42+
quiet: bool = None,
4343
service_args: typing.Optional[typing.List[str]] = None,
4444
env: typing.Optional[typing.Mapping[str, str]] = None,
4545
reuse_service=False,
4646
**kwargs,
4747
) -> None:
4848
self._check_executable(executable_path)
4949
self.service_args = service_args or []
50-
self.quiet = quiet
50+
if quiet is not None:
51+
warnings.warn("quiet is no longer needed to supress output", DeprecationWarning, stacklevel=2)
52+
5153
self._reuse_service = reuse_service
52-
log_file = subprocess.PIPE if not self.quiet else open(os.devnull, "w", encoding="utf-8")
5354
super().__init__(
5455
executable=executable_path,
5556
port=port,
56-
log_file=log_file, # type: ignore
5757
env=env,
5858
**kwargs,
5959
)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Licensed to the Software Freedom Conservancy (SFC) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The SFC licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
import os
18+
import subprocess
19+
import time
20+
21+
import pytest
22+
23+
from selenium.webdriver import Chrome
24+
from selenium.webdriver.chrome.service import Service
25+
26+
27+
def test_log_path_deprecated() -> None:
28+
log_path = "chromedriver.log"
29+
msg = "log_path has been deprecated, please use log_output"
30+
31+
with pytest.warns(match=msg, expected_warning=DeprecationWarning):
32+
Service(log_path=log_path)
33+
34+
35+
def test_uses_chromedriver_logging() -> None:
36+
log_file = "chromedriver.log"
37+
service_args = ["--append-log"]
38+
39+
service = Service(log_output=log_file, service_args=service_args)
40+
try:
41+
driver1 = Chrome(service=service)
42+
with open(log_file, "r") as fp:
43+
lines = len(fp.readlines())
44+
driver2 = Chrome(service=service)
45+
with open(log_file, "r") as fp:
46+
assert len(fp.readlines()) >= 2 * lines
47+
finally:
48+
driver1.quit()
49+
driver2.quit()
50+
os.remove(log_file)
51+
52+
53+
def test_log_output_as_filename() -> None:
54+
log_file = "chromedriver.log"
55+
service = Service(log_output=log_file)
56+
try:
57+
driver = Chrome(service=service)
58+
with open(log_file, "r") as fp:
59+
assert "Starting ChromeDriver" in fp.readline()
60+
finally:
61+
driver.quit()
62+
os.remove(log_file)
63+
64+
65+
def test_log_output_as_file() -> None:
66+
log_name = "chromedriver.log"
67+
log_file = open(log_name, "w", encoding="utf-8")
68+
service = Service(log_output=log_file)
69+
try:
70+
driver = Chrome(service=service)
71+
time.sleep(1)
72+
with open(log_name, "r") as fp:
73+
assert "Starting ChromeDriver" in fp.readline()
74+
finally:
75+
driver.quit()
76+
log_file.close()
77+
os.remove(log_name)
78+
79+
80+
def test_log_output_as_stdout(capfd) -> None:
81+
service = Service(log_output=subprocess.STDOUT)
82+
driver = Chrome(service=service)
83+
84+
out, err = capfd.readouterr()
85+
assert "Starting ChromeDriver" in out
86+
driver.quit()
87+
88+
89+
def test_log_output_null_default(capfd) -> None:
90+
driver = Chrome()
91+
92+
out, err = capfd.readouterr()
93+
assert "Starting ChromeDriver" not in out
94+
driver.quit()

0 commit comments

Comments
 (0)