1 | """ |
---|
2 | Custom Models |
---|
3 | ------------- |
---|
4 | |
---|
5 | This is a place holder for the custom models namespace. When models are |
---|
6 | loaded from a file by :func:`generate.load_kernel_module` they are loaded |
---|
7 | as if they exist in *sasmodels.custom*. This package needs to exist for this |
---|
8 | to occur without error. |
---|
9 | """ |
---|
10 | from __future__ import division, print_function |
---|
11 | |
---|
12 | import sys |
---|
13 | import os |
---|
14 | from os.path import basename, splitext |
---|
15 | |
---|
16 | try: |
---|
17 | # Python 3.5 and up |
---|
18 | from importlib.util import spec_from_file_location, module_from_spec # type: ignore |
---|
19 | def load_module_from_path(fullname, path): |
---|
20 | """load module from *path* as *fullname*""" |
---|
21 | spec = spec_from_file_location(fullname, os.path.expanduser(path)) |
---|
22 | module = module_from_spec(spec) |
---|
23 | spec.loader.exec_module(module) |
---|
24 | return module |
---|
25 | except ImportError: |
---|
26 | # CRUFT: python 2 |
---|
27 | import imp |
---|
28 | def load_module_from_path(fullname, path): |
---|
29 | """load module from *path* as *fullname*""" |
---|
30 | # Clear out old definitions, if any |
---|
31 | if fullname in sys.modules: |
---|
32 | del sys.modules[fullname] |
---|
33 | if path.endswith(".py") and os.path.exists(path) and os.path.exists(path+"c"): |
---|
34 | # remove automatic pyc file before loading a py file |
---|
35 | os.unlink(path+"c") |
---|
36 | module = imp.load_source(fullname, os.path.expanduser(path)) |
---|
37 | return module |
---|
38 | |
---|
39 | _MODULE_CACHE = {} |
---|
40 | _MODULE_DEPENDS = {} |
---|
41 | _MODULE_DEPENDS_STACK = [] |
---|
42 | def load_custom_kernel_module(path): |
---|
43 | """load SAS kernel from *path* as *sasmodels.custom.modelname*""" |
---|
44 | # Pull off the last .ext if it exists; there may be others |
---|
45 | name = basename(splitext(path)[0]) |
---|
46 | path = os.path.expanduser(path) |
---|
47 | |
---|
48 | # reload module if necessary |
---|
49 | if need_reload(path): |
---|
50 | # Push to the next dependency level |
---|
51 | _MODULE_DEPENDS_STACK.append(path) |
---|
52 | _MODULE_DEPENDS[path] = set([path]) |
---|
53 | |
---|
54 | # Load module into the 'sasmodels.custom' name space. |
---|
55 | # If this triggers any submodule loads then they will be added |
---|
56 | # as dependencies below when _MODULE_DEPENDS_STACK is not empty. |
---|
57 | module = load_module_from_path('sasmodels.custom.'+name, path) |
---|
58 | |
---|
59 | # Pop the dependency level |
---|
60 | _MODULE_DEPENDS_STACK.pop() |
---|
61 | |
---|
62 | # TODO: include external C code in the dependencies |
---|
63 | # If we had the model info structure we could do the following: |
---|
64 | # _MODEL_DEPENDS[path].extend(generate.model_sources(info)) |
---|
65 | # but at this point all we have is the module. Don't want to |
---|
66 | # repeat the logic in modelinfo.make_model_info. |
---|
67 | |
---|
68 | # Cache the module with the newest timestamp |
---|
69 | timestamp = max(os.path.getmtime(f) for f in _MODULE_DEPENDS[path]) |
---|
70 | _MODULE_CACHE[path] = module, timestamp |
---|
71 | |
---|
72 | #print("loading", os.path.basename(path), _MODULE_CACHE[path][1], |
---|
73 | # [os.path.basename(p) for p in _MODULE_DEPENDS[path]]) |
---|
74 | |
---|
75 | if _MODULE_DEPENDS_STACK: |
---|
76 | # Add child and all its dependence to the parent module |
---|
77 | working_on = _MODULE_DEPENDS_STACK[-1] |
---|
78 | _MODULE_DEPENDS[working_on].update(_MODULE_DEPENDS[path]) |
---|
79 | |
---|
80 | return _MODULE_CACHE[path][0] |
---|
81 | |
---|
82 | def need_reload(path): |
---|
83 | # TODO: fails if a dependency has a modification time in the future |
---|
84 | # If the newest dependency has a time stamp in the future, then this |
---|
85 | # will be recorded as the cached time. When a second dependency |
---|
86 | # is updated to the current time stamp, it will still be considered |
---|
87 | # older than the current build and the reload will not be triggered. |
---|
88 | # Could instead treat all future times as 0 here and in the code above |
---|
89 | # which records the newest timestamp. This will force a reload when |
---|
90 | # the future time is reached, but other than that should perform |
---|
91 | # correctly. Probably not worth the extra code... |
---|
92 | _, cache_time = _MODULE_CACHE.get(path, (None, -1)) |
---|
93 | depends = _MODULE_DEPENDS.get(path, [path]) |
---|
94 | return any(cache_time < os.path.getmtime(p) for p in depends) |
---|