Exposing a Python Class to COM/VBA - python

I have a simple python class that I am trying to make com-accessible (e.g., to VBA):
class Foo(object):
_reg_progid_ = 'Foo.Application'
_reg_clsid_ = '{602462c5-e750-4d1c-879b-a0465bebb526}'
_public_methods_ = ['say_hello']
def __init__(self):
pass
def say_hello(self, name):
return f'Hello, {name}'
if __name__=='__main__':
print("Registering COM server")
import win32com.server.register
win32com.server.register.UseCommandLine(Foo)
Several examples indicate this is a pretty standard approach (see here and here).
From python, this appears to be com-accessible. No errors raise, and the output appears as expected:
from comtypes.client import CreateObject
f = CreateObject('Foo.Application')
f.say_hello('David')
When trying to instantiate a Foo from VBA, however, there is an error (Run-time error -2147024770 (8007007e) Automation error The specified module could not be found).
Dim f As Object
Set f = CreateObject("Foo.Application")
I am actually able to resolve this error using the method described in this answer (and this one), specifically doing:
_reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER
And then localserver.serve('{602462c5-e750-4d1c-879b-a0465bebb526}') in the name guard function.
However, in some past applications development work (a long time ago using python 2.7) I know we did not do this part -- instead we used Innosetup to compile an installer from a foo.exe and foo.dll (derived from foo.py probably from py2exe or similar) and other dependencies.
I'm happy (for now) with the solution, but I guess my question is whether this is necessary (as several examples don't do these things) or if there's something else I'm missing (e.g., the installer that I used in a past life actually handled this bit behind-the-scenes with the DLL instead of a .py file?)?
Additional information: OS is 64-bit Windows, running Excel 2013 (32-bit) and python 3.7.4 (32-bit).

Related

Modifying/debugging frozen builtins like importlib/_boostrap_external.py

Short version: How do I debug frozen libs? Can I tell Python not to use them (and use sourcefiles) or re-freeze them some how?
I'm trying to learn more about the inner functionality of pythons import mechanisms. And by going down that rabbit hole, I want to debug /usr/lib/python37/importlib/_bootstrap_external.py.
Using python -m trace --trace myscript.py gave me indications of where in _bootstrap_external.py I am landing, but not values. So I turned to pdb and it gave me a bit of information but appears to be skipping _frozen objects. (?).
So I added a few print('moo') where I saw my script ending up in _boostrap_external.py but no prints are ever made, because Python internally uses <class '_frozen_importlib_external.PathFinder'>
That lead me in a desperate attempt to try and renaming/removing /usr/lib/python37/importlib/__pycache__ in a hope that Python would re-compile my changes. But no such luck, Python still uses a frozen version.
So I modified /usr/lib/python37/__init__.py where it imports the _bootstrap module, so I changed this:
try:
import _frozen_importlib_external as _bootstrap_external
except ImportError:
from . import _bootstrap_external
_bootstrap_external._setup(_bootstrap)
_bootstrap._bootstrap_external = _bootstrap_external
else:
_bootstrap_external.__name__ = 'importlib._bootstrap_external'
_bootstrap_external.__package__ = 'importlib'
try:
_bootstrap_external.__file__ = __file__.replace('__init__.py', '_bootstrap_external.py')
except NameError:
# __file__ is not guaranteed to be defined, e.g. if this code gets
# frozen by a tool like cx_Freeze.
pass
sys.modules['importlib._bootstrap_external'] = _bootstrap_external
to this:
from . import _bootstrap_external
_bootstrap_external._setup(_bootstrap)
_bootstrap._bootstrap_external = _bootstrap_external
_bootstrap_external.__name__ = 'importlib._bootstrap_external'
_bootstrap_external.__package__ = 'importlib'
try:
_bootstrap_external.__file__ = __file__.replace('__init__.py', '_bootstrap_external.py')
except NameError:
# __file__ is not guaranteed to be defined, e.g. if this code gets
# frozen by a tool like cx_Freeze.
pass
sys.modules['importlib._bootstrap_external'] = _bootstrap_external
And that worked, ish. Obviously the problem will become more and more complex, and there has to be a more generic solution. But I continued my journey and found that _bootstrap.py is indeed loaded with my print('moo') changes. But when /usr/lib/python37/importlib/_bootstrap.py later calls the function _find_spec, and it does:
for finder in meta_path:
with _ImportLockContext():
find_spec = finder.find_spec
spec = find_spec(name, path, target)
find_spec will refer to the frozen version again. (Code snippet shortened to the relevant code in _bootstrap.py)
So my question finally ends up in, how do I debug these frozen files? or how do I make it so Python ignores frozen libraries (I can take the performance impact when debugging). Every attempt to find information on this (searching, docs, irc, etc) ends up on "why?" or "don't do this" or it points me to py2exe and how to freeze my libraries. I just want to be able to debug and understand more of how the inner mechanics work by trying things and looking at the variables.

How to access the Custom Protocol URL used to invoke the MACOS App which comprises of a single python file (from inside the python code)

I have written a small python file which I am packaging as a .app and installing on macos (latest version). The app is intended to be invoked using a custom protocol similar to "abc://efg/ijk/lmn". The python file employs pyobjc package and intends to use it to implement the business logic. I have option of using, only the Python language to implement my business logic because of legacy reasons.
I have to access the invoking custom URL "abc://efg/ijk/lmn" from inside the python code and parse the values. The "efg" "ijk" and "lmn" in the custom URL will vary and will be used to take some decisions further down the flow.
I have tried multiple things from whatever I could find from the internet but i am unable to access the custom url from with in the python code. The value of sys.argv come as below
sys.argv = ['/Applications/XXXXXApp.app/Contents/MacOS/XXXXXApp', '-psn_0_4490312']
But on windows sys.argv[0] is populated with the custom url.
Will appreciate any directions.
Below code is what I have tried among many other variations of it.
`
from Cocoa import NSObject
mylogger = open(os.path.expanduser("~/Desktop/somefile.txt"), 'w+')
class apple_event_handler(NSObject):
def applicationWillFinishLaunching_(self, notification):
mylogger.write("Will finish ")
def applicationDidFinishLaunching_(self, notification):
mylogger.write("Did Finish")
def handleAppleEvent_withReplyEvent_(self, event, reply_event):
theURL = event.descriptorForKeyword_(fourCharToInt(b'----'))
mylogger.write("********* Handler Invoked !!! *********")
mylogger.write("********* the URL = " + str(theURL))
mylogger.write(*self.args)
aem = NSAppleEventManager.sharedAppleEventManager()
aeh = apple_event_handler.alloc().init()
aem.setEventHandler_andSelector_forEventClass_andEventID_(aeh,
"handleAppleEvent:withReplyEvent:", 1, 1)
`

Using my Python program

I'm really new to python and I have made the following program:
class AddressBook:
def __init__(self):
self.b = {}
def insert(self,name, phone):
self.b[name]=phone
print "I am confused"
def get(self,name):
return self.b[name]
def has_name(self,name):
return self.b.has_key(name)
def list(self):
for n,p in self.b.iteritems():
print n,p
def delete(self, name):
del self.b[name]
def orderedList(self):
orderedkeys = self.b.keys()
orderedkeys.sort()
for n in orderedkeys:
print n, self.b[n]
I now want to compile it test it out in terminal to see if it all works.
I went to the directory and compiled it with
python address.py
Now I want to add things to the list, print the contents of the list, delete them (pretty much play around with my program) but I don't know how...
After compiling, how do I manually test (play around) with my python program?
Thanks in advance.
Python is an interpreted language, and .py files do not require direct compilation. There are a few ways to run Python code, but for "playing around" you can simply activate the Python interpreter and import the class.
In a command prompt:
> python
In Python:
>>> from address import AddressBook
>>> a = Addressbook()
>>> a.insert("Jenny", "867-5309")
>>> a.get("Jenny")
'867-5309'
The python script is not compiled. At least not in ways as other languages, like Fortran and C. From this answer:
Python has a compiler! You just don't notice it because it runs automatically. You can tell it's there, though: look at the .pyc (or .pyo if you have the optimizer turned on) files that are generated for modules that you import.
Also, it does not compile to the native machine's code. Instead, it compiles to a byte code that is used by a virtual machine. The virtual machine is itself a compiled program. This is very similar to how Java works; so similar, in fact, that there is a Python variant (Jython) that compiles to the Java Virtual Machine's byte code instead! There's also IronPython, which compiles to Microsoft's CLR (used by .NET). (The normal Python byte code compiler is sometimes called CPython to disambiguate it from these alternatives.)
You have two ways to test it out:
type python -i address.py in the terminal. This will run the script and enter the python shell.
You enter the python shell and then type from address.py import AddressBook.
On both ways, you can
play around with your code.

How to have win32com code completion in IPython?

Via
import win32com.client
wordapp = win32com.client.gencache.EnsureDispatch('Word.Application')
I can get a Word Application object documented e.g. here. However, ipython's autocompletion is not aware of that API, is there any way to add that?
Quick solution
Perhaps the simplest way to achieve code completion in IPython (tested with 6.2.1, see the answer below for a snippet that works with 7.1) and Jupyter is to run the following snippet:
from IPython.utils.generics import complete_object
import win32com.client
#complete_object.when_type(win32com.client.DispatchBaseClass)
def complete_dispatch_base_class(obj, prev_completions):
try:
ole_props = set(obj._prop_map_get_).union(set(obj._prop_map_put_))
return list(ole_props) + prev_completions
except AttributeError:
pass
Short story long
With some more details being outlined in this guide, win32com ships with a script, makepy.py for generating Python types corresponding to the type library of a given COM object.
In the case of Word 2016, we would proceed as follows:
C:\Users\username\AppData\Local\Continuum\Anaconda3\pkgs\pywin32-221-py36h9c10281_0\Lib\site-packages\win32com\client>python makepy.py -i "Microsoft Word 16.0 Object Library"
Microsoft Word 16.0 Object Library
{00020905-0000-0000-C000-000000000046}, lcid=0, major=8, minor=7
>>> # Use these commands in Python code to auto generate .py support
>>> from win32com.client import gencache
>>> gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 7)
The location of makepy.py will of course depend on your Python distribution. The script combrowse.py, available in the same directory, can be used to find the names of available type libraries.
With that in place, win32com.client will automatically make use of the generated types, rather than the raw IPyDispatch, and at this point, auto-completion is available in e.g. IPython or Jupyter, given that the COM object of interest actually publishes its available properties and methods (which is not a requirement).
Now, in your case, by invoking EnsureDispatch instead of Dispatch, the makepy part of the process is performed automatically, so you really should be able to obtain code completion in IPython for the published methods:
Note, though, that while this does give code completion for methods, the same will not be true for properties. It is possible to inspect those using the _prop_map_get_ attribute. For example, wordapp.Selection.Range.Font._prop_map_get_ gives all properties available on fonts.
If using IPython is not a strong requirement, note also that the PythonWin shell (located around \pkgs\pywin32\Lib\site-packages\pythonwin\Pythonwin.exe) has built-in code completion support for both properties and methods.
This, by itself, suggests that the same is achievable in IPython.
Concretely, the logic for auto-completion, which in turn relies on _prop_map_get_, can be found in scintilla.view.CScintillaView._AutoComplete. On the other hand, code completion in IPython 6.2.1 is handled by core.completer.IPCompleter. The API for adding custom code completers is provided by IPython.utils.generics.complete_object, as illustrated in the first solution above. One gotcha is that with complete_object being based on simplegeneric, only one completer may be provided for any given type. Luckily, all types generated by makepy will inherit from win32com.client.DispatchBaseClass.
If this turns out to ever be an issue, one can also circumvent complete_object entirely and simply manually patch IPython by adding the following five lines to core.completer.Completion.attr_matches:
try:
ole_props = set(obj._prop_map_get_).union(set(obj._prop_map_put_))
words += list(ole_props)
except AttributeError:
pass
Conversely, IPython bases its code-completion on __dir__, so one could also patch gencache, which is where the code generation ultimately happens, to include something to like
def __dir__(self):
return list(set(self._prop_map_get_).union(set(self._prop_map_put_)))
to each generated DispatchBaseClass.
fuglede's answer is great, just want to update it for the newest versions of IPython (7.1+).
Since IPython.utils.generics has changes from using simplegeneric to using functools, the #complete_object.when_type method should be changed to #complete_object.register. So his initial code should changed to:
from IPython.utils.generics import complete_object
import win32com.client
#complete_object.register(win32com.client.DispatchBaseClass)
def complete_dispatch_base_class(obj, prev_completions):
try:
ole_props = set(obj._prop_map_get_).union(set(obj._prop_map_put_))
return list(ole_props) + prev_completions
except AttributeError:
pass

Give python "platform" library fake platform information?

At the beginning of the script, I use platform.system and platform.release to determine which OS and version the script is running on (so it knows it's data is in Application Support on Mac, home on unix-like and non-mac unix, appdata on windows <= XP, and appdata/roaming on windows >= Vista). I'd like to test my series of ifs, elifs, and elses what determine the os and release, but I only have access to Mac 10.6.7, some unknown release of Linux, and Windows 7. Is there a way to feed platform fake system and release information so I can be sure XP, Solaris, etc, would handle the script properly without having an installation?
Maybe something like
>>> platform.system = lambda: "whatever"
>>> platform.system()
'whatever'
you could create your initialization functions to take those variables as parameters so it is easy to spoof them in testing
You probably want to explore mocking platform for your testing. Alternatively, you could directly monkey patch platform, or even mess with sys.modules directly to override the default platform module, but mock is already designed to be self contained and also has the benefit of pretty clearly showing in your code what is and is not test instrumentation, so you don't accidentally get test functionality released in your production code.
For me, directly patching platform.system with pytest-mock did not work. A simple trick though is to abstract the retrieval of this information in a utility function of your own (which you can successfully patch with pytest-mock this time).
Hence, in your implementation module/class:
def _get_system() -> str:
return platform.system().lower()
def _is_windows() -> bool:
return _get_system() == 'windows'
And in your test, here using pytest + pytest-mock:
def test_win_no_pad_code():
with patch('module_name._get_system', MagicMock(return_value='windows')):
assert module_name._is_windows()
This is one way of doing using module, but you can do the equivalent using a class instead.

Categories