pythonwindowsvirtualenvpython-3.4pywin32

Using PythonService.exe to host python service while using virtualenv


I've got a Windows 7 environment where I need to develop a Python Windows Service using Python 3.4. I'm using pywin32's win32service module to setup the service and most of the hooks seem to be working ok.

The problem is when I attempt to run the service from source code (using python service.py install followed by python service.py start). This uses PythonService.exe to host service.py - but I'm using a venv virtual environment and the script can't find it's modules (error message discovered with python service.py debug).

Pywin32 is installed in the virtualenv and in looking at the source code of PythonService.exe, it dynamically links in Python34.dll, imports my service.py and invokes it.

How can I get PythonService.exe to use my virtualenv when running my service.py?


Solution

  • It appears this used to work correctly with the virtualenv module before virtual environments were added to Python 3.3. There's anecdotal evidence (see this answer: https://stackoverflow.com/a/12424980/1055722) that Python's site.py used to look upward from the executable file until it found a directory that would satisfy imports. It would then use that for sys.prefix and this was sufficient for PythonService.exe to find the virtualenv it was inside of and use it.

    If that was the behavior, it appears that site.py no longer does that with the introduction of the venv module. Instead, it looks one level up for a pyvenv.cfg file and configures for a virtual environment in that case only. This of course doesn't work for PythonService.exe which is buried down in the pywin32 module under site-packages.

    To work around it, I adapted the activate_this.py code that comes with the original virtualenv module (see this answer: https://stackoverflow.com/a/33637378/1055722). It is used to bootstrap an interpreter embedded in an executable (which is the case with PythonService.exe) into using a virtualenv. Unfortunately, venv does not include this.

    Here's what worked for me. Note, this assumes the virtual environment is named my-venv and is located one level above the source code location.

    import os
    import sys
    
    if sys.executable.endswith("PythonService.exe"):
    
        # Change current working directory from PythonService.exe location to something better.
        service_directory = os.path.dirname(__file__)
        source_directory = os.path.abspath(os.path.join(service_directory, ".."))
        os.chdir(source_directory)
        sys.path.append(".")
    
        # Adapted from virtualenv's activate_this.py
        # Manually activate a virtual environment inside an already initialized interpreter.
        old_os_path = os.environ['PATH']
        venv_base = os.path.abspath(os.path.join(source_directory, "..", "my-venv"))
        os.environ['PATH'] = os.path.join(venv_base, "Scripts") + os.pathsep + old_os_path
        site_packages = os.path.join(venv_base, 'Lib', 'site-packages')
        prev_sys_path = list(sys.path)
        import site
        site.addsitedir(site_packages)
        sys.real_prefix = sys.prefix
        sys.prefix = venv_base
    
        new_sys_path = []
        for item in list(sys.path):
            if item not in prev_sys_path:
                new_sys_path.append(item)
                sys.path.remove(item)
        sys.path[:0] = new_sys_path
    

    One other factor in my troubles - there is a new pypi wheel for pywin32 that is provided by the Twisted folks that makes it easier to install with pip. The PythonService.exe in that package was acting oddly (couldn't find a pywin32 dll when invoked) compared to the one you get when installing the official win32 exe package into the virtual env using easy_install.