gtalarico / revitpythonwrapper

Python Wrapper for the Revit API
138 stars 32 forks source link

Suggestion: Sphinx Compatiblity (Import handler class) #3

Closed eirannejad closed 7 years ago

eirannejad commented 7 years ago

@gtalarico I ended up defining the class below in the sphinx conf.py that will handle all dot net imports:

# based on:
# http://blog.dowski.com/2008/07/31/customizing-the-python-import-system/
class DotNetImporter(object):
    domain_modules = ['clr', 'System', 'Autodesk', 'Microsoft']
    found_mods = dict()

    def find_module(self, fullname, path=None):
        if fullname in self.domain_modules:
            return self
        if path:
            for p in path:
                if p in self.domain_modules:
                    self.domain_modules.append(fullname)
                    return self

        return None

    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]

        mod = imp.new_module(fullname)
        mod.__loader__ = self
        sys.modules[fullname] = mod
        mod.__file__ = fullname
        mod.__path__ = [fullname]
        return mod

# add importer to the list
sys.meta_path.append(DotNetImporter())

I'm running sphinx on a mac to create the docs for pyRevit and is working well. I also had to define global __sphinx__ parameter to protect the module level code from execution.

gtalarico commented 7 years ago

Thanks for sharing this @eirannejad . I have been using this: https://github.com/gtalarico/revitpythonwrapper/blob/master/rpw/utils/sphinx_compat.py

Would mind explaining it a bit more? How does it handle the .dot imports? Are you running sphinx on cpyhon or ironpython? The code I shared above works by making sure the code can be loaded (if it were to be executed fully, it would fail), but the way I load the modules, it works well.

fbe-work commented 7 years ago

@eirannejad Excellent!! - I was struggeling with this today, while trying to get our revit helpers into sphinx. Somehow I could not get the mock for "import clr" working properly (sphinx's conf.py accepst it, but not the loaded revit helper scripts..). In your setup is sphinx ran from CPython? (or Ironpython?) Yes a bit of explanation would be very much appreciated and helpful - thank you!!

eirannejad commented 7 years ago

Yes so I'm running sphinx on Cpython 3 on mac OS so I had to fake all dot net imports for the program to work. (This is also the reason behind all the #noinspection directives in the code to exclude all these dot net imports from pyCharm's inspection engine.)

python runs the find_module() method of the class above when it can't find any of the modules being imported and this method basically fakes the import and adds it to sys.modules

I also defined a __sphinx__ parameter in the conf.py file and check its availability in the actual code to protect it when it's being run under sphinx. All module level statements will not run if the script is in documentation mode (that is when __sphinx__ is defined.)

if not __sphinx__:
    # do the module level stuff
fbe-work commented 7 years ago

@eirannejad Thanks a lot for the explanation!! It made me re-watch David Beazley's excellent talk on packages to hopefully understand better: https://www.youtube.com/watch?v=0oTh1CXRaQ0 I pulled your meta_path class into my simple clr dummy module (attached below) which lives in my cpython site-packages. so "import clr", "clr.AddReference("RevitAPI")" and "import Autodesk" and the like get tricked properly, but as soon as an Autodesk class attributes get accessed like "XYZ.Zero" it unfortunately falls apart. Are these caught by your "if not sphinx"?

@gtalarico out of curiosity: Since you run the awesome revitapidocs - do you get a non-dll version or stubs out of processing RevitAPI that could feed cpython with Sphinx (or an IDE for completions)?

import sys
import imp
print("clr fake loaded.")

def AddReference(ref_name):
    print("reference add {0} faked.".format(ref_name))

class DotNetImporter(object):
    domain_modules = ['System', 'Autodesk', 'Microsoft']
    found_mods = dict()

    def find_module(self, fullname, path=None):
        if fullname in self.domain_modules:
            return self
        if path:
            for p in path:
                if p in self.domain_modules:
                    self.domain_modules.append(fullname)
                    return self

        return None

    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]

        mod = imp.new_module(fullname)
        mod.__loader__ = self
        sys.modules[fullname] = mod
        mod.__file__ = fullname
        mod.__path__ = [fullname]
        return mod

# add importer to the list
sys.meta_path.append(DotNetImporter())
eirannejad commented 7 years ago

@hdm-dt-fb So I use this import-wrapper class to manage all the major imports and it catches all import statements in my code. For everything else, especially module level code, I'm protecting the code from execution and giving None or empty values to global variables. pyRevit module has a EXEC_PARAMS.doc_mode property that has the value of __sphinx__ and it's used everywhere is main pyRevit to protect module level code.


if EXEC_PARAMS.doc_mode:
    # if in doc mode, give empty values to globals
    PYREVIT_APP_DIR = PYREVIT_VERSION_APP_DIR = ' '
else:
    # otherwise, define the values
    PYREVIT_APP_DIR = op.join(USER_ROAMING_DIR, PYREVIT_ADDON_NAME)
    PYREVIT_VERSION_APP_DIR = op.join(PYREVIT_APP_DIR, HOST_APP.version)
gtalarico commented 7 years ago

@hdm-df-fb Take look at these https://www.dropbox.com/s/kyxhlq7aop519y3/db_index_2017.1.json?dl=0 https://www.dropbox.com/s/5n6dpu89zeccsyp/db_index.json?dl=0

fbe-work commented 7 years ago

@eirannejad now I got it, that is interesting!! sorry it took me bit - thank you!! I definitively have to (re-)read more pyRevit! @gtalarico thank you so much!!

fbe-work commented 7 years ago

@eirannejad ..one thing I was wondering: I saw "import builtin" in your docs/conf.py - I thought it was renamed to "import builtins" in python3? Or do you use a special module/package there? (at least my python3.6 complained on builtin)

eirannejad commented 7 years ago

Correct that module is renamed to builtins in python 3