Best Practice for Python Library Split Into Multiple Files

I have a robot framework library that is getting quite large and which uses various other classes. I would like to split it up into multiple python files. I was wondering if there is a best practice for how to do this.

For example if I had a single file MyLibrary.py like this:

# MyLibrary.py

class BaseWidget:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Button(BaseWidget):
    def __init__(self, x, y, label):
        super().__init__(x, y)
        self.label = label

    def click(self):
        print(f"Clicked {self.label}")

class MyLibrary:
    def __init__(self):
        self.my_button = Button(10, 20, "My Button")

    def click_my_button(self):
        self.my_button.click()

with a simple test robot file:

# test.robot

*** Settings ***
Library    MyLibrary

*** Test Cases ***
Test
    Click My Button

My thoughts would be to create a structure like so:

β”œβ”€β”€ MyLibrary
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ BaseWidget.py
β”‚   β”œβ”€β”€ Button.py
└── test.robot

with files in MyLibrary split up as follows:

# MyLibrary/__init__.py

from .Button import Button

class MyLibrary:
    def __init__(self):
        self.my_button = Button(10, 20, "My Button")

    def click_my_button(self):
        self.my_button.click()
# MyLibrary/BaseWidget.py

class BaseWidget:
    def __init__(self, x, y):
        self.x = x
        self.y = y
# MyLibrary/Button.py

from .BaseWidget import BaseWidget

class Button(BaseWidget):
    def __init__(self, x, y, label):
        super().__init__(x, y)
        self.label = label

    def click(self):
        print(f"Clicked {self.label}")

I’ve put this here: GitHub - kimfaint/robot-library-multifile: An example of using a Robot Framework library that is spit into multiple python files

This approach should allow me to build my RF library without ending up with a massive MyLibrary.py file. But is this the best approach?

did you know this: GitHub - robotframework/PythonLibCore: Tools to ease creating larger test libraries for Robot Framework using Python

3 Likes

Selenium library is a popular example how to split a huge library using pythonlibcore , as @daniel mentioned.

There has been a talk an demo of pythonlibcore at Robocon 2021, maybe that helps: https://youtu.be/_19SBMEObX8?si=PElLOv-tIPJNiUhO

And probably check out source code of Selenium library.

Regards,
Markus

1 Like

Thankyou @daniel and @Noordsestern for your replies. I watched the presentation from Tatu and now reading the Robot Framework User Guide sections on Dynamic and Hybrid API libraries makes more sense. I’m now unsure if I will stick with a Static API implementation or use Dynamic or Hybrid. It’s good to be aware of these options.

The library I am going to build is for testing a legacy Windows desktop application. Due to the very legacy nature of the application (written in Smalltalk) I need to use a combination of UIA (I’ve chose the uiautomation python module) for the elements that are accessible and OpenCV/Tesseract OCR on screenshots to locate the elements that are not. I would also like to implement it using a Page Object Model approach so that the robot test cases can be very high level without needing to know anything about how a particular element of a window is located and controlled.

For example I’d like the tests to flow something like this:

*** Test Cases ***
Add Employee
    Start Application
    Login As    ${username}    ${password}
    Active Window Should Be    Main Menu
    Open Menu Path    Operations > Employees > Add
    Active Window Should Be    Add Employee
    Set EditBox    Username    Jim Jones
    Set EditBox    Email       jjones@email.com
    Set ListBox    Employee Type    Driver
    Click    Add
    Active Window Should Be    Main Menu

The keywords like Set EditBox and Set ListBox do not mention which window to use, that is implied by the preceeding Active Window Should Be keyword. So each Set EditBox keyword is directed to the currently active window, whatever that happens to be. For one type of window it will use the uiautomation module to access the control directly using a locator, for a different window it might need to take a screenshot of the window and locate coordinates to click on by pattern matching a stored image or looking for a text label with OCR.

Initially I was thinking I could do the POM using the same approach as robotframework-pageobjectlibrary, which is for selenium and works by having a different robot library for each page and using the robot frameworks built-in Set library search order keyword to switch the library search order as the window focus changes. Initially this did seem like a novel approach, but because that occurs at runtime, it breaks things like linting and autocomplete when editing the .robot file in my editor, which I didn’t like.

So perhaps I could use the Dynamic API and use the run_keyword() method to direct certain Set and Get keywords to the class instance representing the currently active screen. Maybe something like this (incomplete example):

# LegacyApplication/__init__.py

import uia

from .mainmenu import MainMenu
from .dialogs import LoginDialog, AddEmployeesDialog

class LegacyApplication():
    def __init__(self):
        self.windows = {
            'Login': LoginDialog(parent=self),
            'Main Menu':  MainMenu(parent=self),
            'Add Employees': AddEmployeesDialog(parent=self),
        }
        self.active_window = 'Login'

        def run_keyword(self, name, args, kwargs):
            if name == 'Active Window Should Be':
                if uia.foreground_window.title == name:
                    if name in self.windows:
                        self.active_window = name
                    else:
                        raise AssertionError(f"{name} is active but not a known window")
                else:
                    raise AssertionError(f"{name} is not the active window")
            elif name in ['Set EditBox', 'Set ListBox']:
                self.active_window.run_keyword(name, args, kwargs)

But as you suggest, I need to read the PythonLibCore docs some more and look at the code for Selenium and Browser libraries first for inspiration :open_book::eyes:…