SeleniumLibrary keywords via Python

Hi @aaltat Tatu,

A feature i would really like to see, is the possibility to call SeleniumLibrary keyword from python including code completion.

I think this is nothing the language server can help with because we are talking about pure python.

Use case: when we work with our customers we prefer to offer higher level keywords as library. These keywords are directly related to the elements on the page and all identifiers and so on are in python. Users do not see anything about Selenium, so they can concentrate on their job. Specifying test cases


I don’t know how to solf this, but it would be nice to get the library instance from Robot Framework BuiltIn and be able to get the keywords completed


Maybe you can help with an example if you think it is possible.

Thx

Calling keywords from the instance should be already be possible, but the IDE support is more complex. And there is also a limitation in the SeleniumLibrary side too.

First problem is that when one calls get_library_instance("SeleniumLibrary") Python does not know what the method returns. That can be solved by example giving type hints. But SeleniumLibrary instance doesn’t know what methods are available as keywords. Most likely this could be solved by implementing __dir__ in the SeleniumLibrary or in the PythonLibCore side.

I was thinking about a static wrapper class.
A class that has each keyword as static function and calls the real SeleniumLibrary keyword if a keyword is called.

Could be generated from libdoc.xml.

Sounds complicated, implementing Python __dir__ should be just few lines of code.

1 Like

Wouldn’t implementing a wapping static class around the dynamic nature of SeleniumLibrary sort of be a step backwards? Plus, wouldn’t a static wrapper then require constant maintenance to keep it in synchronization with the SeleniumLibrary itself?

1 Like

Normally yes.
ThatÂŽs why i would plan to generate this wrapper code from dynamically generated LibDoc xml output.
So each keyword had its documentation and all arguments.

When you install a new SeleniumLibrary the wrapper code can just be regenerated.

The biggest advantage of the dynamic library api (aka dynamic lib core) is, that you can organize your keyword implementations in multiple ‘sub classes’. This advantage would still be there.

1 Like

I did look into the matter. SeleniumLibrary, or more accurately PythonLibCore, already has __dir__ method, but example PyCharm is not able to use __dir__ (for some unknown reason) to retrieve the attributes from class. One possible solution is to generate sub file which would provide automatic completion to IDE’s like PyCharm. Mypy has a stubgen tool that allows generation stub files. With some scripting, it looks pretty good.

@René would you like to try it out? Copy the __init__.pyi file to the same folder where you have SeleniumLibrary init installed. Then in your IDE, do:

from robot.libraries.BuiltIn import BuiltIn

from SeleniumLibrary import SeleniumLibrary

sl: SeleniumLibrary = BuiltIn().get_library_instance('SeleniumLibrary')
sl.open_browser()  # open_browser should work with IDE automatic completion.

Note requires Python 3.6 or greater.

Is that makes also you happy, I could think how to distribute the __init__.pyi file and polish the script.

2 Likes

i will try it out tomorrow.
Look promising !!! :heart_eyes:
Thanks Pal!!

Hello @aaltat I followed up on your suggestions and I must say it looks good! I just did a basic test like you described
nothing elaborate to test it out. But nice work!!_2020May06105648

Works well!!!

i maybe were a little stupid, but after a few minutes i relized to install mypy and download this stuff instead of installing with pip from git and then run the script and then it works.

Only one thing should be done.
Arguments that are optional should be show as such.

Like with Open Browser i would expect to see the default values.

But really nice!

How would you distribute it?
Put the Pyi files into the wheel?

I did not install mypy. How was it relevant or needed?

As i said, maybe just because i was too stupid. Where did you get the pyi files?

Did you just download the file from tatus github?
:man_facepalming:

Correct, I just used his. Ah, so you generated your own .pyi file yourself? I thought he said he also used a script that would require some polishing up before it would be ready for “production” usage?

@aaltat

I do not really understand what the pyi file is, but for me i looks like it is just a list of functionnames.
There is no real functionality behind, right?
You can not Go To Definition etc. So:Wouldn’t it be the same effort to just call the get_keyword_names and get_keyword_arguments of the Hybrid/DynamicCore, and write this file with default values by your own, without this Strange url: Optional[Any] = ... stuff?

This happens when i write the original method header into the pyi file.

def open_browser(self, url=None, browser='firefox', alias=None, remote_url=False, desired_capabilities=None, ff_profile_dir=None, options=None, service_log_path=None, executable_path=None): ...

image

much better than this generated thing:

def open_browser(self, url: Optional[Any] = ..., browser: str = ..., alias: Optional[Any] = ..., remote_url: bool = ..., desired_capabilities: Optional[Any] = ..., ff_profile_dir: Optional[Any] = ..., options: Optional[Any] = ..., service_log_path: Optional[Any] = ..., executable_path: Optional[Any] = ...): ...

image

1 Like

Stub files, the .pyi extension, are meant for type checking and what you see is coming out of the MyPy implementation. But I understand your point, missing the default values is problem. Also when there is not default value available in the argument, the MyPy implementation doesn’t know what is the argument type. Then MyPy adds that Any value, as the pep says.

The later part can be improved by adding typing hints to the keyword and then the stub file will look more realistic. Also I did not try will MyPy create better stub file for default values if typing hints are done properly. But let’s see what can be done with the stub files, but at least it’s better than nothing.

Thats right.

but again. Wouldn®t it “easy” to just ask the PythonLibCore for its keywords, arguments and Documentation and just write this “text-file” with a small script, than relying on a third party library that does it not perfect?

Could be just a small script that ships with PythonLibCore.

@aaltat
This is far from ready, but this superstupid script just lists the keyword names and arguments and print it in form of a method definition:

from SeleniumLibrary import SeleniumLibrary

keywords = SeleniumLibrary().get_keyword_names()
for keyword in keywords:
    args = SeleniumLibrary().get_keyword_arguments(keyword)
    args_str = ""
    for arg in args:
        if isinstance(arg, tuple):
            arg_str = f"{arg[0]}='{arg[1]}'"
        else:
            arg_str = str(arg)
        args_str = f'{args_str}, {arg_str}'
    print(f'    def {keyword}(self{args_str}): ...')

This is the result. I was first a little bit confused about the Get WebElements but i saw why


    def Get WebElement(self, locator): ...
    def Get WebElements(self, locator): ...
    def add_cookie(self, name, value, path='None', domain='None', secure='None', expiry='None'): ...
    def add_location_strategy(self, strategy_name, strategy_keyword, persist='False'): ...
    def alert_should_be_present(self, text='', action='ACCEPT', timeout='None'): ...
    def alert_should_not_be_present(self, action='ACCEPT', timeout='0'): ...
    def assign_id_to_element(self, locator, id): ...
    def capture_element_screenshot(self, locator, filename='selenium-element-screenshot-{index}.png'): ...
    def capture_page_screenshot(self, filename='selenium-screenshot-{index}.png'): ...
    def checkbox_should_be_selected(self, locator): ...
    def checkbox_should_not_be_selected(self, locator): ...
    def choose_file(self, locator, file_path): ...
    def clear_element_text(self, locator): ...
...
...
    def mouse_out(self, locator): ...
    def mouse_over(self, locator): ...
    def mouse_up(self, locator): ...
    def open_browser(self, url='None', browser='firefox', alias='None', remote_url='False', desired_capabilities='None', ff_profile_dir='None', options='None', service_log_path='None', executable_path='None'): ...
    def open_context_menu(self, locator): ...
    def page_should_contain(self, text, loglevel='TRACE'): ...
    def page_should_contain_button(self, locator, message='None', loglevel='TRACE'): ...

I like your idea, it’s worth of exploring. Also would like to how adding proper typing hints change the picture.

One question about HybridCore here:

1    def add_library_components(self, library_components):
2        for component in library_components:
3            for name, func in self.__get_members(component):
4                if callable(func) and hasattr(func, 'robot_name'):
5                    kw = getattr(component, name)
6                    kw_name = func.robot_name or name 
7                    self.keywords[kw_name] = kw
8                    self.attributes[name] = self.attributes[kw_name] = kw

If a keyword has a decorator and assigns a fixed name to the method, the self.keywords key is the func.robot:name. Like “Get WebElements”

In self.keywords only the robot_name is stored, but not the method_name which could now be very helpful. Would you thing that this could be added?

It is stored in the attributes:

self.attributes[name] = self.attributes[kw_name] = kw
1 Like