Mocking output values in pcbnew python API

I am having trouble keeping up with pcbnew python API changes.

Some time ago the footprint flip method pcbnew.MODULE.Flip changed signature, but I was able to find a solution. The solution goes like this. If I find that I am running new python API, I remap current method to a new name and then I bind a mocked method to the old name. It’s a hack, but it works so my plugins are up till now 5.1.x and 5.99 compatible

if pcbnew.MODULE.Flip.__doc__ == "Flip(MODULE self, wxPoint aCentre, bool aFlipLeftRight)":
    # remap the current method to new name
    pcbnew.MODULE.Flip_new = pcbnew.MODULE.Flip

    # create new method with the same signature as in V5.1.x versions
    def old_flip(self, aCentre):
        """ monkeypatched method to have the same signature as in V5.1.x versions """
        pcbnew.MODULE.Flip_new(self, aCentre, False)

    from types import MethodType
    pcbnew.MODULE.Flip = MethodType(old_flip, pcbnew.MODULE)

But now I have a problem as pcbnew.MODULE.GetPath changed return type. Previously it returned a string, but now it returns a pcbnew.KIID object. This object has AsString method, which should work for me. My thinking is that sooner or later I’ll have to update my plugins for this, so I might as well do it now. I can go through the code and swap every call of pcbnew.MODULE.GetPath() with pcbnew.MODULE.GetPath().AsString() so the plugin should work with 5.99 branch.

But in order for same code to also work in 5.1.x branch I’d have to monkeypatch pcbnew so that pcbnew.MODULE.GetPath would return an object (does not matter what type) which would also have AsString method, where invoking this method would actually invoke original pcbnew.MODULE.GetPath. How do I do this?

Invoking magic spirits @HiGreg, @qu1ck to help me with the issue

Does the KIID have a __str__ method? If so, then calling that (or str(OBJECT)) on either KIID or string should result in a string.

Alternatively, create a new function kiid_or_string_tostring() that checks the type (be sure to be compatible with both Python2 and Python3, as windows is still on Python2 due to a wxPython issue.

Edit: clarified __str__ and fixed removal of underscores.

Thanks for the prompt response.

KIID has a __str__method, but I don’t seem to get anywhere with it. Output from pcbnew python console

import pcbnew
board = pcbnew.GetBoard()
mod = board.FindModuleByReference('U101')
path = mod.GetPath()
path.__str__()
"<pcbnew.KIID_PATH; proxy of <Swig Object of type 'KIID_PATH *' at 0x000000000bb8e2d0> >"
str(path)
"<pcbnew.KIID_PATH; proxy of <Swig Object of type 'KIID_PATH *' at 0x000000000bb8e2d0> >"
path.AsString()
u'/00000000-0000-0000-0000-00005e4b26bc'

Anyhow I don’t know if my intentions got through correctly.
With 5.99 I’ve got pcbnew.MODULE.GetPath().AsString() and I’m fine with it.

But I’d also like to run the same code on 5.1.x. In order to do this, I’d like to monkeypatch the pcbnew.MODULE.GetPath() method to return an object, which would have AsString() method. Then I would also be able to use the same code in 5.1.x. So when V6 comes out, I’d just get rid of the monkeypatched code.

The spirits said: thou shall not monkeypatch public api library!
Really, don’t touch global pcbnew namespace, that’s a sure way to make your plugin incompatible with others as well as potentially breaking the API itself.

Just do as HiGreg said: instead of forcing new api to look like old api try using old one and if that fails use new one. In some cases you can check for existence of method before hand but try/catch although ugly works every time.

Regarding KIID looks like __str__ is autogenerated and probably should be overriden in swig to call AsString() but that should be done in kicad not with monkeypatching.

1 Like
def getPathString(module):

    # Three different ways
    
    # Does attribute lookup twice
    path = module.GetPath()
    if hasattr(path,'AsString'):
        return path.AsString()
    else:
        return path
        

    # calls __str__ in all cases
    return getattr(module, 'AsString', module.__str__)()

    # Python try/except is reportedly very fast. Attribute lookup once.
    try:
        return path.AsString()
    except AttributeError:
        return path
1 Like

The Pythonic way is try/except. From this question on Stack overflow: The official Python Documentation mentions EAFP: Easier to ask for forgiveness than permission and Rob Knight notes that catching errors rather than avoiding them, can result in cleaner, easier to read code.

2 Likes

Thanks for the etiquette lesson. I’ll try to avoid monkeypatching public api library. So for the pcbnew.MODULE.GetPath() in 5.99 I’ll raise an issue and see if the overload the SWIG so that it returns a string.

My only reasoning for this is that if I want to support both branches (5.1.x and 5.99) I’ll have shitload of if hasattr or try/except blocks on multiple places within the code. Currently this is really no-go for me. So monkeypatching seemed as an obvious DRY solution. I’ll think about how will I proceed, but I might drop the support for 5.99.

Again, thanks for the input from everybody.

For anybody stumbling on it, this is doable as shown:

if not hasattr(pcbnew, 'KIID'):
    # define new class which has AsString method
    class UuidPath():
        def __init__(self, parent):
            self.parent = parent
        def AsString(self):
            return pcbnew.MODULE.GetPath_new(self.parent)
    # bind it to pcbnew
    pcbnew.MODULE.UuidPath = UuidPath

    # remap the GetPath method as the original name will be overwritten
    pcbnew.MODULE.GetPath_new = pcbnew.MODULE.GetPath

    def old_path(self):
        a = pcbnew.MODULE.UuidPath(self)
        return a

    pcbnew.MODULE.GetPath = old_path

But as qu1ck pointed out this is modifying python API (overriding pcbnew.MODULE.GetPath method).

Either I’ll have to find a way to patch the python API only when runing my code and there restoring the API back or I’ll have to find another solution. Otherwise I am just waiting for trouble to come looking for me.

But why?

Isn’t this just so much simpler?

def getPath(module):
  path = module.GetPath()
  if hasattr(path, 'AsString'):
    path = path.AsString()
  return path

Note that this will be your private method, not shoehorned into pcbnew module.
And if/when swig for KIID’s __str__() gets fixed it will be oneliner:

def getPath(module):
  return str(module.GetPath())
1 Like

Aaarght! Words fail me.

You know when you start down the wrong path and you just keep going and going and you don’t even realize there is a better way nor do you think is there a better way. This is/was one of those times.

Thanks for pulling out from a labyrinth that I’ve built myself.

Setting the problem aside for a year or two usually helps :slight_smile:

1 Like