[py] Allow `Service` as `command_executor` in `Remote.__init__`
User description
🔗 Related Issues
Closes #14464 Ping @cgoldberg
💥 What does this PR do?
Adds Remote(service) as an alternative for Remote(service.service_url). It makes sure the Service doesn't get garbage collected.
Example:
from selenium import webdriver
from selenium.webdriver.chrome import service
def get_driver():
- global webdriver_service # This is necessary to avoid dropping the `Service`
-
webdriver_service = service.Service("operadriver")
webdriver_service.start()
options = webdriver.ChromeOptions()
options.add_experimental_option('w3c', True)
- driver = webdriver.Remote(webdriver_service.service_url, options=options)
+ driver = webdriver.Remote(webdriver_service, options=options)
return driver
driver = get_driver()
driver.get("https://example.com")
print("Success!")
🔧 Implementation Notes
Added a test that confirms the time at which the Service is dropped, and that any arguments are passed through correctly.
💡 Additional Considerations
Ideally, all code currently using Remote(service.service_url) should change to Remote(service). This could in theory be warned for by Selenium, but that would involve making service_url some kind of str wrapper.
🔄 Types of changes
- New feature (non-breaking change which adds functionality and tests!)
PR Type
Enhancement
Description
-
Add
Remote.from_serviceclass method for better service lifecycle management -
Prevent service garbage collection by maintaining reference in Remote instance
-
Include comprehensive test for service lifecycle and argument passing
Diagram Walkthrough
flowchart LR
Service["Service Instance"] -- "from_service()" --> Remote["Remote WebDriver"]
Remote -- "maintains reference" --> Service
Service -- "prevents GC" --> Lifecycle["Service Lifecycle"]
File Walkthrough
| Relevant files | |||
|---|---|---|---|
| Enhancement |
| ||
| Tests |
|
PR Reviewer Guide 🔍
Here are some key observations to aid the review process:
|
🎫 Ticket compliance analysis ❌ 5678 - Partially compliant Compliant requirements:
Non-compliant requirements:
Requires further human verification:
1234 - Not compliant Non-compliant requirements:
|
| ⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪ |
| 🧪 PR contains tests |
| 🔒 No security concerns identified |
⚡ Recommended focus areas for reviewAPI Design
_service reference on Remote without a typed attribute may leak memory if not cleared; confirm lifecycle and document the new API. Consider explicit weakref or cleanup on quit. |
PR Code Suggestions ✨
Explore these optional code suggestions:
| Category | Suggestion | Impact |
| High-level |
Stop service on driver.quit()Remote.from_service retains the Service but WebDriver.quit() doesn’t stop it, Examples:py/selenium/webdriver/remote/webdriver.py [283-288]
Solution Walkthrough:Before:
After:
Suggestion importance[1-10]: 9__ Why: This suggestion correctly identifies a critical resource leak flaw in the PR, where the service process isn't terminated on | High |
| Learned best practice |
Validate
| Low |
✅
_service attribute | Low | |
| ||
Thanks.. I re-opened the original issue.
hmm... what if we added an additional kwarg to Service, that would control if the service gets stopped when garbage collected?
The default would be True, but you could create a service like:
service.Service("operadriver", auto_stop=False)
Then inside __del__ on the Service class, we don't stop the service when it's created with this flag:
def __del__(self) -> None:
try:
if self.auto_stop:
self.stop()
except Exception:
pass
Would that solve the original problem?
Would that solve the original problem?
Then the Service would have to be manually stopped, right? That seems like a bad idea.
Besides that, my solution solves the issue implicitly, without the user having to be aware of stopping, quitting, etc. .from_service can be used anywhere even when the Service stays in scope. Your idea would not have solved my original problem, which came from a code snippet (copied from webdriver_manager's README to be specific) being copied into a function. Using .from_service in that code snippet (which I will make sure to PR if this is merged :)) would have prevented this; but there would be no reason for the code snippet to use auto_stop=False.
That seems like a bad idea.
Small clarification; I don't mean this is a bad idea always, but having to use this to avoid GC is unnecessary IMO. I think my solution results in much cleaner user code, and I would still prefer to see a solution that (as I said) can be applied everywhere; but if that isn't possible that's the next best solution.
Yes, my suggestion would require you to explicitly stop the service.
Your solution adds a classmethod to the base class of all webdrivers that directly instantiates the Service class (an abstract base class). This wouldn't work for any webdriver except Remote.
There might be a nice implicit solution to this, but the current code in your PR isn't it.
Your solution adds a classmethod to the base class of all webdrivers that directly instantiates the Service class (an abstract base class). This wouldn't work for any webdriver except Remote.
It does not??
This is selenium.webdriver.remote.webdriver.WebDriver, which is re-exported as selenium.webdriver.Remote.
It also does not instantiate a Service. It accepts one as an argument.
It does not??
It does.
selenium.webdriver.remote.webdriver.Webdriver is the base class for all webdrivers. Look at the code for any other webdriver class.
You're right, apologies. I expected that to be BaseWebdriver... Sorry, the class hierarchy in Selenium is complexer than I thought.
But that means there is currently no isolated subclass for a Remote, because it is also used a superclass, right? Could a solution be to create a subclass of Remote that is to be used by users, where we replace selenium.webdriver.remote.webdriver.WebDriver with NewRemote as selenium.webdriver.Remote?
Then selenium.webdriver.remote.webdriver.WebDriver can also be made an ABC and all users should instead use the NewRemote.
I mean, yea we could reorganize the class hierarchy to make this work. It's just not a trivial change and I don't want to break any existing code relying on the current API. I'm not sure it's worth it just to address a somewhat uncommon use case that we have never really had complaints about before.
Yep I get it! I was under the impression that Remote was on the same level as Chrome or Firefox (both in class hierarchy and in uses) but that's not true. It's more of an internal class.
I was on the verge of closing this but one more idea came to me. What if selenium.webdriver.remote.webdriver.WebDriver.__init__'s command_executor argument just takes a Union[str, RemoteConnection, Service]? (previously Union[str, RemoteConnection])
This:
- Won't break any existing code; there is no overlap between the three types
- Won't have any impact on subclasses, since they override
__init__and don't get expect acommand_executorat all - Results in clean user code (
Remote(service, options=options); previouslyRemote(service.service_url, options=options))
Implemented, the documentation might need to be improved. I don't know what the standards for types and such are.
Rebased. Most previous comments can be ignored, as I pivoted in the implementation.