UNO: find hierarchy till a selected object

Main problem I’m having with UNO API so far is being able to find path to a particular document object in terms of objects hierarchy. For example: to access the first cell in LO Calc, the path one have to use is (in python):

CurrentComponent.CurrentController.ActiveSheet.Rows.getByIndex(0).getCellByPosition(0, 0)

But to find this hierarchy I had to introspect a lot of different objects, searching for the magic field that might get me one step closer. This is lots of time and effort!

Offhand, I imagine, it’s possible to write in Python a code that iterates over everything in search for a value you passed it (such as a text), and prints the path upon finding.

Overall this, seems, should be very frequent problem, so sure someone already wrote a function or a code to do something like that. Any hints?

The same cell object could be also accessed e.g. using

CurrentComponent.Sheets.getByIndex(0).getCellByPosition(0, 0)

… (given you have first sheet selected) and many others: e.g.,

CurrentComponent.CurrentController.ActiveSheet.getCellByPosition(0, 0)
CurrentComponent.Sheets.getByIndex(0).Columns.getByIndex(0).getCellByPosition(0, 0)
CurrentComponent.Sheets.getByName("Sheet1").getCellRangeByName("A1:B2").getCellByPosition(0, 0)
CurrentComponent.CurrentController.ActiveSheet.getCellRangeByName("A1")

So - which one of the multiple “hierarchies” should be given? And how would any introspection tool be able to inspect infinite set of possible “paths” in the search of this object? Which is actually answering your question: there’s no way to tell you generally. Of course, a tool could give you how it has obtained the selection. Which is likely simply

CurrentComponent.CurrentController.getSelection

@MikeKaganski I’m fine with any way it could’ve obtained it that is not using getSelection(), because generally I can follow any of them, except of course the one with selection because most of the times you won’t have the object you want to work on selected.

I hope you noticed that while introspection tool uses the method you wouldn’t like, the real “path” to the object is unknown to a tool. And there might be infinite number of such ways; taking into account that many properties and methods create loops (when two objects reference each other in one of their members), there’s no way to give you what you want.

FTR: in implementing at least a limited (e.g. by depth) version of a search I stumbled upon limitation in UNO API that may make it harder to implement: not every object is exposed with iterators. E.g. cells aren’t, one have to call very specific function getCellByPosition() with specific arguments. It’s of course possible to handle that case, but makes me wonder if there’s other objects that I can’t get simply by calling iter() on some of their parents.

I wrote some search function, though it’s is far from being satisfactory, because:

  1. This bug doesn’t yet allow to exclude “going in loops” for walking through the hierarchy.
  2. Some objects that ought to have iterators doesn’t have them. Luckily, so far I only stumbled upon one such object: XCellRange, it allows to iterate with getCellByPosition(), but not with iter(myCellRange) in python. Currently, searchLimited() function simply does not descend into these.

Still, I was able to implement something limited, and some minutes ago from me writing this I managed to successfully apply searchLimited() for finding something I couldn’t figure out from searching the internet (which is: “given existing PageFields PivotTable filter, how to modify rows it filters out”). That makes me think, the code is worth putting out at the current state. When you absolutely desperate at finding the hierarchy, it may well save your day :relaxed:


Repo for the code is here. The current version is at the end of this post, and consists of 3 functions:

  • getmembers_uno: basically, modified version of vanilla getmembers() from inspect module. I had to modify it because accessing some properties in UNO results in exceptions.

  • isiter: tests whether given object is iterable

  • searchLimited a depth-first search function with various knobs to alter its behavior. And, oh, believe me, you will want to do it, because occasionally you may stumble upon UNO objects with so many properties that dir() function gonna hang for dozens of minutes, basically, not doing anything useful for you.

    For that reason btw I left a print('TRACE:…') function, so you can see whether it’s progressing, or got stuck and where; then interrupt it, and use ignore() hook to ignore the field where it got stuck (or simply to ignore fields where, you’re sure, not gonna be anything useful).

Here’s modified snip from the session where it helped me:

>>> ignore = lambda s: 'Links' in s or 'Picture' in s or 'MasterPage' in s
>>> searchLimited(pilotTable, 'Publisher', 5, lambda p: isinstance(p, str), ignore)
…[lots of TRACE output snipped]
'FOUND: <TOPLEVEL>.DataPilotFields.<iter 9>.Items.<iter 3>.Name'

What this says is that I can find the Publisher name if I evaluate pilotTable.DataPilotFields.getByIndex(9).Items.getByIndex(3).Name. (and if anyone interested how to use that to filter PivotTable: altering ….getByIndex(3).IsHidden will do it)

Code:

import uno
from inspect import *

# this is a modified version of inspect.getmembers(). It was modified to not fail on
# uno exceptions that sometimes happen during access attempts
# And while on it: ignore uno.ByteSequence by default.
# returns: [(String, a)]
def getmembers_uno(object, predicate=lambda obj: not isinstance(obj, uno.ByteSequence)):
    """Return all members of an object as (name, value) pairs sorted by name.
    Optionally, only return members that satisfy a given predicate."""
    if isclass(object):
        mro = (object,) + getmro(object)
    else:
        mro = ()
    results = []
    processed = set()
    names = dir(object)
    # :dd any DynamicClassAttributes to the list of names if object is a class;
    # this may result in duplicate entries if, for example, a virtual
    # attribute with the same name as a DynamicClassAttribute exists
    try:
        for base in object.__bases__:
            for k, v in base.__dict__.items():
                if isinstance(v, types.DynamicClassAttribute):
                    names.append(k)
    except AttributeError:
        pass
    for key in names:
        # First try to get the value via getattr.  Some descriptors don't
        # like calling their __get__ (see bug #1785), so fall back to
        # looking in the __dict__.
        try:
            value = getattr(object, key)
            # handle the duplicate key
            if key in processed:
                raise AttributeError
        except AttributeError:
            for base in mro:
                if key in base.__dict__:
                    value = base.__dict__[key]
                    break
            else:
                # could be a (currently) missing slot member, or a buggy
                # __dir__; discard and move on
                continue
        except uno.getClass("com.sun.star.uno.RuntimeException"):
            continue # ignore: inspect.RuntimeException: Getting from this property is not supported
        except Exception:
            continue # ignore: everything, we don't care
        if not predicate or predicate(value):
            results.append((key, value))
        processed.add(key)
    results.sort(key=lambda pair: pair[0])
    return results

def isiter(maybe_iterable):
    try:
        iter(maybe_iterable)
        return True
    except TypeError:
        return False

# nLevels: number of levels it allowed to descend
# predicate: types to use, except iterables
# pIgnore: String -> Bool, accepts property name, may be used to drive search.
def searchLimited(unoObject, valToSearch, nLevels,
                predicate, pIgnore, path = '<TOPLEVEL>'):
    def try_property(property_name, val):
        if property_name.startswith('__') or pIgnore(property_name):
            return None # ignore private stuff
        print('TRACE: ' + path + '.' + property_name)
        if val == valToSearch:
            return 'FOUND: ' + path + '.' + property_name
        if nLevels - 1 != 0 and isiter(val):
            index = 0
            for item in val:
                ret = searchLimited(item, valToSearch, nLevels - 1, predicate, pIgnore,
                                    path + '.' + property_name + '.<iter ' + str(index) + '>')
                if (ret != None):
                    return ret
                index += 1
    for (property_name, val) in getmembers_uno(unoObject, lambda p: isiter(p) or predicate(p)):
        ret = try_property(property_name, val)
        if (ret != None):
            return ret
    if nLevels - 1 != 0 and isiter(unoObject):
        index = 0
        for item in unoObject:
            ret = searchLimited(item, valToSearch, nLevels - 1, predicate, pIgnore,
                                path + '.<iter ' + str(index) + '>')
            if (ret != None):
                return ret
            index += 1
    return None

Nice! :+1:t2: