Giter Site home page Giter Site logo

robotframework / pythonlibcore Goto Github PK

View Code? Open in Web Editor NEW
58.0 15.0 20.0 228 KB

Tools to ease creating larger test libraries for Robot Framework using Python

License: Apache License 2.0

Python 89.59% RobotFramework 10.41%
robotframework pyhton library test-automation

pythonlibcore's Introduction

Python Library Core

Tools to ease creating larger test libraries for Robot Framework using Python. The Robot Framework hybrid and dynamic library API gives more flexibility for library than the static library API, but they also sets requirements for libraries which needs to be implemented in the library side. PythonLibCore eases the problem by providing simpler interface and handling all the requirements towards the Robot Framework library APIs.

Code is stable and is already used by SeleniumLibrary and Browser library. Project supports two latest version of Robot Framework.

Version Actions Status License

Usage

There are two ways to use PythonLibCore, either by HybridCore or by using DynamicCore. HybridCore provides support for the hybrid library API and DynamicCore provides support for dynamic library API. Consult the Robot Framework User Guide, for choosing the correct API for library.

Regardless which library API is chosen, both have similar requirements.

  1. Library must inherit either the HybridCore or DynamicCore.
  2. Library keywords must be decorated with Robot Framework @keyword decorator.
  3. Provide a list of class instances implementing keywords to library_components argument in the HybridCore or DynamicCore __init__.

It is also possible implement keywords in the library main class, by marking method with @keyword as keywords. It is not required pass main library instance in the library_components argument.

All keyword, also keywords implemented in the classes outside of the main library are available in the library instance as methods. This automatically publish library keywords in as methods in the Python public API.

The example in below demonstrates how the PythonLibCore can be used with a library.

Example

"""Main library."""

from robotlibcore import DynamicCore

from mystuff import Library1, Library2


class MyLibrary(DynamicCore):
    """General library documentation."""

    def __init__(self):
        libraries = [Library1(), Library2()]
        DynamicCore.__init__(self, libraries)

    @keyword
    def keyword_in_main(self):
        pass
"""Library components."""

from robotlibcore import keyword


class Library1(object):

    @keyword
    def example(self):
        """Keyword documentation."""
        pass

    @keyword
    def another_example(self, arg1, arg2='default'):
        pass

    def not_keyword(self):
        pass


class Library2(object):

    @keyword('Custom name')
    def this_name_is_not_used(self):
        pass

    @keyword(tags=['tag', 'another'])
    def tags(self):
        pass

Plugin API

It is possible to create plugin API to a library by using PythonLibCore. This allows extending library with external Python classes. Plugins can be imported during library import time, example by defining argumet in library [__init__]{.title-ref} which allows defining the plugins. It is possible to define multiple plugins, by seperating plugins with with comma. Also it is possible to provide arguments to plugin by seperating arguments with semicolon.

from robot.api.deco import keyword  # noqa F401

from robotlibcore import DynamicCore, PluginParser

from mystuff import Library1, Library2


class PluginLib(DynamicCore):

    def __init__(self, plugins):
        plugin_parser = PluginParser()
        libraries = [Library1(), Library2()]
        parsed_plugins = plugin_parser.parse_plugins(plugins)
        libraries.extend(parsed_plugins)
        DynamicCore.__init__(self, libraries)

When plugin class can look like this:

class MyPlugi:

    @keyword
    def plugin_keyword(self):
        return 123

Then Library can be imported in Robot Framework side like this:

Library    ${CURDIR}/PluginLib.py    plugins=${CURDIR}/MyPlugin.py

Translation

PLC supports translation of keywords names and documentation, but arguments names, tags and types can not be currently translated. Translation is provided as a file containing Json and as a Path object. Translation is provided in translation argument in the HybridCore or DynamicCore __init__. Providing translation file is optional, also it is not mandatory to provide translation to all keyword.

The keys of json are the methods names, not the keyword names, which implements keyword. Value of key is json object which contains two keys: name and doc. name key contains the keyword translated name and doc contains keyword translated documentation. Providing doc and name is optional, example translation json file can only provide translations only to keyword names or only to documentatin. But it is always recomended to provide translation to both name and doc.

Library class documentation and instance documetation has special keys, __init__ key will replace instance documentation and __intro__ will replace libary class documentation.

Example

If there is library like this:

from pathlib import Path

from robotlibcore import DynamicCore, keyword

class SmallLibrary(DynamicCore):
    """Library documentation."""

    def __init__(self, translation: Path):
        """__init__ documentation."""
        DynamicCore.__init__(self, [], translation.absolute())

    @keyword(tags=["tag1", "tag2"])
    def normal_keyword(self, arg: int, other: str) -> str:
        """I have doc

        Multiple lines.
        Other line.
        """
        data = f"{arg} {other}"
        print(data)
        return data

    def not_keyword(self, data: str) -> str:
        print(data)
        return data

    @keyword(name="This Is New Name", tags=["tag1", "tag2"])
    def name_changed(self, some: int, other: int) -> int:
        """This one too"""
        print(f"{some} {type(some)}, {other} {type(other)}")
        return some + other

And when there is translation file like:

{
    "normal_keyword": {
        "name": "other_name",
        "doc": "This is new doc"
    },
    "name_changed": {
        "name": "name_changed_again",
        "doc": "This is also replaced.\n\nnew line."
    },
    "__init__": {
        "name": "__init__",
        "doc": "Replaces init docs with this one."
    },
    "__intro__": {
        "name": "__intro__",
        "doc": "New __intro__ documentation is here."
    },
}

Then normal_keyword is translated to other_name. Also this keyword documentions is translted to This is new doc. The keyword is name_changed is translted to name_changed_again keyword and keyword documentation is translted to This is also replaced.\n\nnew line.. The library class documentation is translated to Replaces init docs with this one. and class documentation is translted to New __intro__ documentation is here.

pythonlibcore's People

Contributors

aaltat avatar adongy avatar bollwyvl avatar dependabot[bot] avatar eeter avatar hugovk avatar mkorpela avatar pekkaklarck avatar rasjani avatar snooz82 avatar yo-ga avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pythonlibcore's Issues

Documentation

Motivation for this tool and its usage needs to be documented better.

Fix typing hints for None and bool types

PythonLibCore adds type hints for None and bool types when they are used as argument defaults. This causes problems for libdoc and those type hints should not be returned.

When library is using @keyword decorator to give new name for the keyword, then the method can not be called with the method name programmatically

Example if I have this code:

from robotlibcore import keyword
from robotlibcore import DynamicCore


class KwClass(object):

    @keyword('My Keyword')
    def my_kw(self):
        print 'My Kw'


class MyLibrary(DynamicCore):

    def __init__(self):
        DynamicCore.__init__(self, [KwClass()])


if __name__ == '__main__':
    x = MyLibrary()
    print x.keywords
    x.my_kw()

Then I can not call keyword with the method name my_kw and the above code raises:

{'My Keyword': <bound method KwClass.my_kw of <__main__.KwClass object at 0x059728D0>>}
Traceback (most recent call last):
  File "foo.py", line 21, in <module>
    x.my_kw()
  File "D:\workspace\SeleniumLibrary\src\SeleniumLibrary\base\robotlibcore.py", line 80, in __getattr__
    .format(type(self).__name__, name))
AttributeError: 'MyLibrary' object has no attribute 'my_kw'

Decorator resolves as wron file path

Reported in MarketSquare/robotframework-browser#2382

Original issue says
Describe the bug

In the documentation generated with libdoc (in JSON or libspec format), the source file for the keyword New Context is Browser/utils/deprecated.py.

To Reproduce
Steps to reproduce the behavior:

Execute libdoc Browser browser.json
Search for "name": "New Context" and then for "source":
Expected behavior

The source of the keyword New Context is

OS: Windows 10

The keyword New Context has the following definition

@keyword(tags=("Setter", "BrowserControl"))
@attribute_warning(
    old_args=("videosPath", "videoSize"), new_args=("recordVideo", "recordVideo")
)
def new_context(
    self,
    ...
)

Apparently, libdoc expects that the line after the keyword decorator is a function definition.

Problem with Python 3 and when extending SeleniumLibrary with custom library.

When I have this test case:

*** Settings ***
Library           ./InheritSeleniumLibrary.py

*** Test Cases ***
Use InheritSeleniumLibrary Open Browser Keyword
    Open Browser     https://www.google.fi/
    ${capabilities} =    Get Browser Desired Capabilities
    Log    ${capabilities}
    [Teardown]    Close Browser

And when the InheritSeleniumLibrary.py is defined like this:

from robot.api import logger
from SeleniumLibrary import SeleniumLibrary
from SeleniumLibrary.base import keyword
from SeleniumLibrary.keywords import BrowserManagementKeywords


class InheritSeleniumLibrary(SeleniumLibrary):

    @keyword
    def get_browser_desired_capabilities(self):
        logger.info('Getting currently open browser desired capabilities')
        return self.driver.desired_capabilities

Then when I run the test with command: python2 -m robot.run --loglevel trace inheritance.robot test works OK, but when I run the test with command: python3 -m robot.run --loglevel trace inheritance.robot then the test fails with exception:

22:00:49.234    FAIL    AttributeError: 'InheritSeleniumLibrary' object has no attribute 'driver'   
22:00:49.234    DEBUG   Traceback (most recent call last):
  File "/usr/local/lib/python3.6/dist-packages/SeleniumLibrary/__init__.py", line 344, in run_keyword
    return DynamicCore.run_keyword(self, name, args, kwargs)
  File "/usr/local/lib/python3.6/dist-packages/SeleniumLibrary/base/robotlibcore.py", line 97, in run_keyword
    return self.keywords[name](*args, **kwargs)
  File "/home/godtdd/workspace/SeleniumLibrary/docs/extending/examples/inheritance/InheritSeleniumLibrary.py", line 18, in get_browser_desired_capabilities
    return self.driver.desired_capabilities
  File "/usr/local/lib/python3.6/dist-packages/SeleniumLibrary/base/robotlibcore.py", line 80, in __getattr__
    .format(type(self).__name__, name))

Here are the test suite and the library test_suite.zip

Enhance get_keyword_arguments to support new format in Rf 3.2

Robot Framework 3.2 will (most likely) add support for returning get_keyword_arguments method call in new format, see more details in robotframework/robotframework#3514. In RF 3.1 it is returned like: ['mandatory', 'optional=default', '**kws'] but in RF 3.2 this would be supported: [('x', 1), ('y', None), ('z', True)]. This would ease the conversion of default types in RF side. Add support for new return type in RF 3.2 but do not loose support for RF 3,1 which does not support new format.

Packaging

Needs setup.py etc. Package should also be uploaded to PyPI.

Dynamically added keywords not discovered by VSCode's Python IntelliSense

As the title describes, when adopting this DynamicCore pattern with dynamically registering methods at class instantiation time based on the registered ContextAware derivates, everything works just fine within Robot Framework space (mainly due to the generated & packaged libspecs), but with VSCode while editing Python code:

Screenshot 2022-09-15 at 15 07 24

Robot example:

  • selenium_lib.open_browser from the pure RF Selenium library is detected due to the related *.pyi module interface, where the downside is the absence of the docstring.

Screenshot 2022-09-15 at 15 07 10

  • Without such interface, like in the pdf_lib.open_pdf case, it is even worse, as the keyword method isn't even detected.
  1. Is this a known limitation? And no matter the answer, is there any plan on supporting VSCode detecting such dynamically added methods when working in Python?
  2. Why this black-magic pattern when we could have just inherited from all the bases (library components) in the final library class? (so method resolution is done at class creation time rather than instantiation time)

Remember, this has nothing to do with rpaframework or even Robocorp. It is purely about crafting a library by:

  1. Inheriting from DynamicCore on the final library class and registering under its __init__ the library components. (the pattern used in RF)
  2. Editing robot code with VSCode in Python while having Microsoft's official Python extension enabled.
  3. Not being able to auto-complete code nor seeing any of the methods or their definitions/docstrings.
    a. Exception when generating with mypy a stub file describing the full interface of the final library instance. (like observed with Selenium)

Related to: robocorp/rpaframework#621

Common base class

Currently both DynamicCore and StaticCore extend HybridCore. This is a bit strange inheritance hierarchy in general, but it's especially stupid to check does a library extend any of these by using isinstance(library, HybridCore). It would be better to have a common base class named LibraryCore or RobotLibraryCore that all concrete lib cores extend. It would allow using isinstance(library, RobotLibraryCore).

Until this common base class is implemented, it's probably best to use isinstance(library, (HybridCore, DynamicCore, StaticCore)) with any generic code. That's both more explicit than just using isinstance(library, HybridCore) and also works if and when other cores don't anymore extend HybridCore.

Default None type changes to unicode string "None"

args += ['{}={}'.format(name, value) for name, value in defaults]

Greetings!

I have experienced the following issue with DynamicCore:
Env: Robot Framework 3.0.4 (Python 2.7.15 on win32)

@keyword
def example(self, p1, p2=None, p3=False):
    import logging
    logging.debug(p2)
    logging.debug(type(p2))
    logging.debug(p3)
    logging.debug(type(p3))

OK:
Example | foobar |
20180920 11:40:19.188 : DEBUG : None
20180920 11:40:19.189 : DEBUG : <type 'NoneType'>
20180920 11:40:19.189 : DEBUG : False
20180920 11:40:19.189 : DEBUG : <type 'bool'>

OK:
Example | foobar | p2=${None}
20180920 11:42:47.513 : DEBUG : None
20180920 11:42:47.514 : DEBUG : <type 'NoneType'>
20180920 11:42:47.514 : DEBUG : False
20180920 11:42:47.515 : DEBUG : <type 'bool'>

NOK:
Example | foobar | p3=${True}
20180920 11:43:23.717 : DEBUG : None
20180920 11:43:23.717 : DEBUG : <type 'unicode'>

20180920 11:43:23.718 : DEBUG : True
20180920 11:43:23.718 : DEBUG : <type 'bool'>

As you can see, the default value is not correct for the p2 in the last case.

Without the dynamic core, e.g: Built-In libraries are not affected:
Library | OperatingSystem
List Directory | ${CURDIR} | absolute=${True}
20180920 11:54:03.198 : DEBUG : None
20180920 11:54:03.198 : DEBUG : <type 'NoneType'>
20180920 11:54:03.198 : DEBUG : True
20180920 11:54:03.198 : DEBUG : <type 'bool'>

Cheers from Budapest,
Rábel, Sándor

Why it does not work?

I tried it with the example code, and it does not work.

mystuff.py

"""Library components."""

from robotlibcore import keyword


class Library1(object):

    @keyword
    def example(self):
        """Keyword documentation."""
        pass

    @keyword
    def another_example(self, arg1, arg2='default'):
        pass

    def not_keyword(self):
        pass


class Library2(object):

    @keyword('Custom name')
    def this_name_is_not_used(self):
        pass

    @keyword(tags=['tag', 'another'])
    def tags(self):
        pass

mainlib.py:

"""Main library."""

from robotlibcore import DynamicCore
from robotlibcore import keyword

from mystuff import Library1, Library2


class MyLibrary(DynamicCore):
    """General library documentation."""

    def __init__(self):
        libraries = [Library1(), Library2()]
        DynamicCore.__init__(self, libraries)

    @keyword
    def keyword_in_main(self):
        print('ok')

my case file:

*** Settings ***
Library    mainlib



*** Test Cases ***

MyUTETest

    [Tags]    4G_TDD    QC_100186    CLOUD_RF_AIL_B1_RRM5_99_AZNA

    example
    keyword_in_main

But I got the following error:

C:\work\ta>pybot -b debug.log --loglevel DEBUG mycase.robot
==============================================================================
Mycase
==============================================================================
MyUTETest                                                             | FAIL |
No keyword with name 'example' found.
------------------------------------------------------------------------------
Mycase                                                                | FAIL |
1 critical test, 0 passed, 1 failed
1 test total, 0 passed, 1 failed
==============================================================================

With decorators containing arguments, argument specifucation is not correctly resolved.

If library is using decorator which takes in arguments, the arguments are not correctly resolved. Example if there is this decorator:

def _my_deco(old_args: Tuple[str, str], new_args: Tuple[str, str]):
    def actual_decorator(method):
        @wraps(method)
        def wrapper(*args, **kwargs):
            for index, old_arg in enumerate(old_args):
                logger.warn(
                    f"{old_arg} has deprecated, use {new_args[index]}",
                )
            return method(*args, **kwargs)

        return wrapper

    return actual_decorator

Then then in that vase arguments are not correctly resolved.

Support list in plugin import

If there are many plugins to import, plugins must be separated with comma, example: "foo.py,bar.py". But supporting list would be good too, example [”foo.py", "bar.py"], would be useful too.

Installing PythonLibCore

Do we have this tool in pip repo?

How to use it in my sources?

My next task: overwrite Selenium2Libraries keyword "Open Browser" - add some logic and call your original "Open Browser" with another arguments
Example: I want change default Firefox browser to Chrome

Can I do it using this tool?

`DynamicCore` doesn't handle named only arguments properly

For example, arg in this example isn't handled properly:

class Example(DynamicCore):
    def __init__(self):
        super().__init__([])

    @keyword
    def kw(self, *, arg):
        print(arg)

Libdoc reports arg as a normal argument, not as named-only as it should. Luckily execution like

Kw    arg=value

works as expected so named-only arguments aren't totally broken. Invalid usage like

Kw    value

isn't handled properly, though.

inv task set-version does not work nor does it indicate any errors.

When using inv set-version 0.0.0 - version file does not get updated and there's no indication that errors happened.

This is because Rellu uses single quotes by default in its VERSION_PATTERN and i'd assume that before using black, default string quote was ' instead of what it's now ".

param:Optional[x] = None type hint behaves differently than Robot Framework

Please add the following "hacky" Optional removal to

return hints

 def _remove_optional_none_type_hints(self, type_hints, defaults):
    # If argument has None as a default, typing.get_type_hints adds
    # optional None to the information it returns. We don't want that.
    for arg in defaults:
        if defaults[arg] is None and arg in type_hints:
            type_ = type_hints[arg]
            if self._is_union(type_):
                try:
                    types = type_.__args__
                except AttributeError:
                    # Python 3.5.2's typing uses __union_params__ instead
                    # of __args__. This block can likely be safely removed
                    # when Python 3.5 support is dropped
                    types = type_.__union_params__
                if len(types) == 2 and types[1] is type(None):
                    type_hints[arg] = types[0]

https://github.com/robotframework/robotframework/blob/5dc9affd4aee31703a58ad105ed49579b1ca7067/src/robot/running/arguments/argumentparser.py#L101

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.