Skip to content

Selenium2Library new architecture - #770

Merged
aaltat merged 16 commits into
robotframework:masterfrom
aaltat:new_architecture
May 3, 2017
Merged

aaltat merged 16 commits into
robotframework:masterfrom
aaltat:new_architecture

Conversation

@aaltat

@aaltat aaltat commented Mar 7, 2017

Copy link
Copy Markdown
Contributor

This PR removes the library multi inheritance madness. This change result cleaner code base, which eases the maintenance and hopefully eases accepting the new enhancement to the library.

The PR also removes the meta classes and the automatic keyword decoration in the keywordgroup object. Also the PR introduces a new context object(which is the library itself). The context object holds information from the different WebDriver instances and allows methods to register and fetch the different WebDriver instances.

@aaltat

aaltat commented Mar 7, 2017

Copy link
Copy Markdown
Contributor Author

This PR is not yet ready to merged, it has two unit test to be fixed and half of the acceptance test do fail. But I want to store the code also somewhere else than in my laptop and I want other also to take a look and give their input on the matter.

Comment thread src/Selenium2Library/base.py Outdated

def _log(self, message, level='INFO', html=False):
level = level.upper()
if (level == 'INFO'):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parens aren't needed! Python ain't Java! =)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy paste from the old library... Will fix it.

@@ -1,46 +1,58 @@
from .keywordgroup import KeywordGroup
from Selenium2Library.robotlibcore import keyword

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to use from .robotlibrarycore import keyword here. Makes it easier to change the library name if/when we decide to do it.

Comment thread src/Selenium2Library/__init__.py Outdated
print 'run failure here'
raise e

def current_browser(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a property and possibly named simply browser. Makes usage from library components nicer when you can say context.browser.something() and not context.current_browser().something(). If the old architecture has this method, we should leave it for backwards compatibility and consider deprecating it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A hundred times yes. Please leave _current_browser() for a release cycle as it will break a whole lot of outside code. I suggest emitting a warning every time it is invoked. Or perhaps only once.
There are many aspects to Selenium S2L does not expose so a lot of people depend on this private method. In the mean time you can have _current_browser() call browser or current_browser().

I also agree it would be better suited as an attribute rather than a method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you from reminding this one. We will create a _current_browser() for backwards compatibility. And most likely we must also create _cache.register() for the same reason. Now it is cache.register()


from .keywordgroup import KeywordGroup
from Selenium2Library.base import Base
from Selenium2Library.robotlibcore import keyword

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already commented once, but I think from .module import something is better than from Selenium2Library.module import something. The former has less noise and require no changes if/when we change the library name.

@pekkaklarck pekkaklarck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great in general!

Comment thread src/Selenium2Library/keywords/alert.py Outdated
class AlertKeywords(Base):

__ACCEPT_ALERT = 'accept'
__DISMISS_ALERT = 'dismiss'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can nowadays be normal constants named just like ACCEPT_ALERT.

Comment thread src/Selenium2Library/keywords/cookie.py Outdated
def delete_all_cookies(self):
"""Deletes all cookies."""
self._current_browser().delete_all_cookies()
self.ctx.current_browser().delete_all_cookies()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good example where ctx.browser.delete_all_cookies() would be cleaner than the current ctx.current_browser().delete_all_cookies().

trg_elem = self._element_find(target, True, True)
ActionChains(self._current_browser()).drag_and_drop(src_elem, trg_elem).perform()
src_elem = self.element_find(source, True, True)
trg_elem = self.element_find(target, True, True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General comments not related to the architecture change:

  • element_find() clearly needs default values so that True, True doesn't need to be repeated.
  • In general Boolean arguments should be given in named format like something=True to make it easier to understand the calling code.
  • This module is rather big. Should consider splitting it into smaller pieces. Element finding logic could perhaps be moved into context.

Because the above aren't really related to the architecture change, it's probably better not to do it as part of this PR. Could perhaps just add some TODOs.

def __init__(self, ctx):
Base.__init__(self)
self.ctx = ctx
self.element_find = ElementKeywords(ctx).element_find

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but moving the element finding logic into the context or into the base class would probably be cleaner.

@pekkaklarck

Copy link
Copy Markdown
Member

One missing change is removing the decorator module dependency from setup.py. =)

Comment thread src/Selenium2Library/keywords/cookie.py Outdated


class CookieKeywords(KeywordGroup):
class CookieKeywords(object):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my own understanding:
Why does this class inherit from object, but many other keyword classes inherit from Base?
I see that it isn't necessary since there is no logging for these keywords. I just want to understand the principal behind the choice.
Is it simplicity over uniformity?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'd say all lib components should extend same base class. Consistency FTW!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, consistency rules in this case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, glad to help.

@aaltat

aaltat commented Mar 7, 2017

Copy link
Copy Markdown
Contributor Author

I think I will fix the unit test next. The failing unit test needs some thinking because the things that they are also testing are not anymore there. Perhaps I need refactor those parts out to a separate unit test for the browser cache.

@aaltat

aaltat commented Mar 16, 2017

Copy link
Copy Markdown
Contributor Author

Only 4 acceptance test fails. Not far to go.

@aaltat

aaltat commented Mar 28, 2017

Copy link
Copy Markdown
Contributor Author

Run on failure fixed. Not totally sure is the this https://github.com/robotframework/Selenium2Library/pull/770/files#diff-d9fdca8cec320726d945c028908fd190R213 best approach, but works nicely. Three more failures to be fixed and then I should refactor the elementfinder.py

@aaltat

aaltat commented Mar 28, 2017

Copy link
Copy Markdown
Contributor Author

@aaltat

aaltat commented Mar 30, 2017

Copy link
Copy Markdown
Contributor Author

All acceptance test now pass. Now it is easier to refactor elementfinder.py

@aaltat

aaltat commented Apr 1, 2017

Copy link
Copy Markdown
Contributor Author

This PR is ready for review, it would be good if someone would have time read the code and make sure that changed are good ones. And I know that this PR is quite huge one and not in average sized PR.

I also think that the elementfinder.py refactoring should be done in a separate PR, because this already is a huge PR.

@pekkaklarck pekkaklarck left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very good in general!

The only serious issue I noticed is how Base has instance attributes that I doubt should be per instance. See the comment in the class itself for details.

As a style issue I really don't like the idea to split function calls that cause > 78 char lines like

result = some_function_call(
    "All args here", "but some would fit prev line"
)

There are places where this enhances readability, but in the above case we just get one extra line and confusing single ). Just splitting the offending arguments into a separate line is, in my opinion, a lot better general guideline:

result = some_function_call("Those args that fit are here",
                            "remaining on second line")

Splitting all arguments to own line gets especially confusing in nested function calls and with if/for constructs.

Comment thread src/Selenium2Library/__init__.py Outdated
from .keywords import *
import warnings

from .utils import BrowserCache

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alphabetical ordering of imports is recommended by PEP-8. This particular import should be combined with the other from .utils import line a little lower.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done and checked imports in all changed files.

Comment thread src/Selenium2Library/__init__.py Outdated
try:
return DynamicCore.run_keyword(self, name, args, kwargs)
except Exception:
RunOnFailureKeywords(self)._run_on_failure()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using _private method is ugly. Is this still work-in-progress? In that case adding TODO would be OK at this phase.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread src/Selenium2Library/base.py Outdated
self.element_finder = ElementFinder()
self._speed_in_secs = 0.0
self._timeout_in_secs = 5.0
self._implicit_wait_in_secs = 5.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the plan that different instances can have different values for these times? If not, then they shouldn't be instance attributes. This is important to check!

The _secs postfix sounds a little unnecessary too, but I guess it could help separating these from possible millisecond values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No all instances must have the same value, that was the way it worked before the refactoring. Those timers where in https://github.com/robotframework/Selenium2Library/blob/master/src/Selenium2Library/keywords/browsermanagement.py#L45 but I moved them to Base. Other place that I was thinking to put them was the library __init__.py but can not anymore recall why I did actually put the timers here. Changed to not the instance specific but perhaps __init__ would be better place after all.

from .keywordgroup import KeywordGroup

from Selenium2Library.base import Base
from Selenium2Library.robotlibcore import keyword

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use ..base instead of Selenium2Library.base? Robot uses the latter but the former would make the library name change easier. This isn't a big deal in general and definitely not in the context of this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets decide this one when we decide should we rename the library.

'Closing browser with session id {}'.format(
self.browsers.current.session_id
)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit verbose coding style, especially for something like debugging. Instead of using 5 lines I'd write it on 2, for example, like this:

self.debug('Closing browser with session id {}'
           .format(self.browsers.current.session_id))

As an unrelated note, I'd prefer proper sentences with . at the end. Again, not part of this PR.



def _make_ff(self , remote , desired_capabilites , profile_dir):
def _make_ff(self, remote, desired_capabilites, profile_dir):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for removing unnecessary and PEP-8 incompatible whitespace. Separate commit in master would have been better, though. I do this all the time too and I wouldn't revert these changes.

self._info("Verifying element '%s' does not contain text '%s'."
self.info(
"Verifying element '%s' does not contain text '%s'."
% (locator, expected))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation. Why not to have info("Verifying ... like earlier?

actual_locator_count = len(self._element_find(locator, False, False))
actual_locator_count = len(self.element_find(
locator, first_only=False, required=False)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really hard to read. I'd use

actual_locator_count = len(self.element_find(locator, first_only=False,
                                             required=False))

or

elements = self.element_find(locator, first_only=False, required=False)
actual_locator_count = len(elements)

instead. Shortening the local actual_locator_count variable to just count would probably be a good idea too. Everything might be nicely < 80 chars after that.

# Public, attributes
self.page_not_contains_element(
locator, message=message, loglevel=loglevel
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the last time I mention this, but I don't think it is generally a good idea to move all arguments into separate row if having them in the same row as the method call makes the line > 80. It works nicely in some cases, especially if arguments themselves are long, but in general I consider it a lot better to split just the offending arguments to own row:

self.page_not_contains_element(locator, message=message, 
                               loglevel=loglevel)

if not element:
element = self.element_find(
locator, required=False, tag='file upload'
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good example of the thing I promised not to mention again. =)

@aaltat

aaltat commented Apr 25, 2017

Copy link
Copy Markdown
Contributor Author

Í agree on the syntax comments but did not have time to fix those today. Hopefully tomorrow or next day.

I am not totally satisfied on the solution for the timers, in Base, perhaps they should be moved to library __init__?

@pekkaklarck

pekkaklarck commented Apr 25, 2017 •

Copy link
Copy Markdown
Member

Having config values in the library itself, i.e. in the context object, sounds good to me. If there are lot of them, we could create a separate class that has them all that the library then uses internally.

@aaltat

aaltat commented Apr 26, 2017

Copy link
Copy Markdown
Contributor Author

There are not many parameters which would be needed globally. There are those three timers and is the run on failure already running, so 4 in total.

@pekkaklarck

Copy link
Copy Markdown
Member

Do we still need a flag to tell is run-on-failure running?

@aaltat

aaltat commented Apr 26, 2017

Copy link
Copy Markdown
Contributor Author

I think we need. Because run on failure can run any keyword and therefore it does this BuiltIn().run_keyword(self.ctx._run_on_failure_keyword). If the run on failure keyword is Selenium2Library​keyword and it fails too, it causes recursive loop.

There might be other solutions to solve the recursive loop problem, but this is the most backwards compatible way.

@aaltat

aaltat commented Apr 26, 2017

Copy link
Copy Markdown
Contributor Author

Fixed those too verbose coding styles, which I did spot. Also did little bit drive by refactoring and adjusted some spacing issues in the code which I have not touched.

@pekkaklarck

Copy link
Copy Markdown
Member

Forgot that run-on-failure is implemented using BuiltIn.run_keyword. You are right that it often ends up calling S2L keywords recursively.

@aaltat

aaltat commented Apr 28, 2017

Copy link
Copy Markdown
Contributor Author

It was pretty late when I made the last commit and therefore I might have missed some styling problems. I need to reread the changes one more time so that all is good to go. I will try to do it today or tomorrow. Is there something else that we would need to consider?

@pekkaklarck

Copy link
Copy Markdown
Member

Haven't looked at this PR since my full review, but I think this is good to be merged in general. We can fine tune the code also after that.

Just need to decide is there so big need for new minor release with better Firefox support that this should wait. Creating a branch for the minor release afterwards is possible too, though.

@aaltat

aaltat commented May 2, 2017

Copy link
Copy Markdown
Contributor Author

That is very good question. I am starting to think that doing Python 3 is more important because one can use the Create Webdriver keyword to get Firefox working. And because workaround exist, putting effort to release the Python 3 support feels more important.

@pekkaklarck

Copy link
Copy Markdown
Member

I trust your judgement on this.

@aaltat

aaltat commented May 3, 2017

Copy link
Copy Markdown
Contributor Author

There is always possibly to make the branch and release from there. Merged to master.

@aaltat
aaltat merged commit 798fb31 into robotframework:master May 3, 2017
@vairamsvsjdo

Copy link
Copy Markdown

@aaltat Thanks for this drive to version 3 support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants