1"""
2Helper module that will enable lazy imports of Cocoa wrapper items.
3
4This should improve startup times and memory usage, at the cost
5of not being able to use 'from Cocoa import *'
6"""
7__all__ = ('ObjCLazyModule',)
8
9import sys
10import re
11import struct
12
13from objc import lookUpClass, getClassList, nosuchclass_error, loadBundle
14import objc
15ModuleType = type(sys)
16
17def _loadBundle(frameworkName, frameworkIdentifier, frameworkPath):
18    if frameworkIdentifier is None:
19        bundle = loadBundle(
20            frameworkName,
21            {},
22            bundle_path=frameworkPath,
23            scan_classes=False)
24
25    else:
26        try:
27            bundle = loadBundle(
28                frameworkName,
29                {},
30                bundle_identifier=frameworkIdentifier,
31                scan_classes=False)
32
33        except ImportError:
34            bundle = loadBundle(
35                frameworkName,
36                {},
37                bundle_path=frameworkPath,
38                scan_classes=False)
39
40    return bundle
41
42class GetAttrMap (object):
43    __slots__ = ('_container',)
44    def __init__(self, container):
45        self._container = container
46
47    def __getitem__(self, key):
48        try:
49            return getattr(self._container, key)
50        except AttributeError:
51            raise KeyError(key)
52
53class ObjCLazyModule (ModuleType):
54
55    # Define slots for all attributes, that way they don't end up it __dict__.
56    __slots__ = (
57                '_ObjCLazyModule__bundle', '_ObjCLazyModule__enummap', '_ObjCLazyModule__funcmap',
58                '_ObjCLazyModule__parents', '_ObjCLazyModule__varmap', '_ObjCLazyModule__inlinelist',
59                '_ObjCLazyModule__aliases',
60            )
61
62    def __init__(self, name, frameworkIdentifier, frameworkPath, metadict, inline_list=None, initialdict={}, parents=()):
63        super(ObjCLazyModule, self).__init__(name)
64
65        if frameworkIdentifier is not None or frameworkPath is not None:
66            self.__bundle = self.__dict__['__bundle__'] = _loadBundle(name, frameworkIdentifier, frameworkPath)
67
68        pfx = name + '.'
69        for nm in sys.modules:
70            if nm.startswith(pfx):
71                rest = nm[len(pfx):]
72                if '.' in rest: continue
73                if sys.modules[nm] is not None:
74                    self.__dict__[rest] = sys.modules[nm]
75
76        self.__dict__.update(initialdict)
77        self.__dict__.update(metadict.get('misc', {}))
78        self.__parents = parents
79        self.__varmap = metadict.get('constants')
80        self.__varmap_dct = metadict.get('constants_dict', {})
81        self.__enummap = metadict.get('enums')
82        self.__funcmap = metadict.get('functions')
83        self.__aliases = metadict.get('aliases')
84        self.__inlinelist = inline_list
85
86        self.__expressions = metadict.get('expressions')
87        self.__expressions_mapping = GetAttrMap(self)
88
89        self.__load_cftypes(metadict.get('cftypes'))
90
91        if metadict.get('protocols') is not None:
92            self.__dict__['protocols'] = ModuleType('%s.protocols'%(name,))
93            self.__dict__['protocols'].__dict__.update(
94                    metadict['protocols'])
95
96            for p in objc.protocolsForProcess():
97                setattr(self.__dict__['protocols'], p.__name__, p)
98
99
100    def __dir__(self):
101        return self.__all__
102
103    def __getattr__(self, name):
104        if name == "__all__":
105            # Load everything immediately
106            value = self.__calc_all()
107            self.__dict__[name] = value
108            return value
109
110        # First try parent module, as we had done
111        # 'from parents import *'
112        for p in self.__parents:
113            try:
114                value = getattr(p, name)
115            except AttributeError:
116                pass
117
118            else:
119                self.__dict__[name] = value
120                return value
121
122        # Check if the name is a constant from
123        # the metadata files
124        try:
125            value = self.__get_constant(name)
126        except AttributeError:
127            pass
128        else:
129            self.__dict__[name] = value
130            return value
131
132        # Then check if the name is class
133        try:
134            value = lookUpClass(name)
135        except nosuchclass_error:
136            pass
137
138        else:
139            self.__dict__[name] = value
140            return value
141
142        # Finally give up and raise AttributeError
143        raise AttributeError(name)
144
145    def __calc_all(self):
146        all = set()
147
148        # Ensure that all dynamic entries get loaded
149        if self.__varmap_dct:
150            for nm in self.__varmap_dct:
151                try:
152                    getattr(self, nm)
153                except AttributeError:
154                    pass
155
156        if self.__varmap:
157            for nm in re.findall(r"\$([A-Z0-9a-z_]*)(?:@[^$]*)?(?=\$)", self.__varmap):
158                try:
159                    getattr(self, nm)
160                except AttributeError:
161                    pass
162
163        if self.__enummap:
164            for nm in re.findall(r"\$([A-Z0-9a-z_]*)@[^$]*(?=\$)", self.__enummap):
165                try:
166                    getattr(self, nm)
167                except AttributeError:
168                    pass
169
170        if self.__funcmap:
171            for nm in self.__funcmap:
172                try:
173                    getattr(self, nm)
174                except AttributeError:
175                    pass
176
177        if self.__expressions:
178            for nm in self.__expressions:
179                try:
180                    getattr(self, nm)
181                except AttributeError:
182                    pass
183
184        if self.__aliases:
185            for nm in self.__aliases:
186                try:
187                    getattr(self, nm)
188                except AttributeError:
189                    pass
190
191        # Add all names that are already in our __dict__
192        all.update(self.__dict__)
193
194        # Merge __all__of parents ('from parent import *')
195        for p in self.__parents:
196            all.update(getattr(p, '__all__', ()))
197
198        # Add all class names
199        all.update(cls.__name__ for cls in getClassList())
200
201
202        return [ v for v in all if not v.startswith('_') ]
203
204        return list(all)
205
206    def __get_constant(self, name):
207        # FIXME: Loading variables and functions requires too much
208        # code at the moment, the objc API can be adjusted for
209        # this later on.
210        if self.__varmap_dct:
211            if name in self.__varmap_dct:
212                tp = self.__varmap_dct[name]
213                return objc._loadConstant(name, tp, False)
214
215        if self.__varmap:
216            m = re.search(r"\$%s(@[^$]*)?\$"%(name,), self.__varmap)
217            if m is not None:
218                tp = m.group(1)
219                if tp is None:
220                    tp = '@'
221                else:
222                    tp = tp[1:]
223
224                d = {}
225                if tp.startswith('='):
226                    tp = tp[1:]
227                    magic = True
228                else:
229                    magic = False
230
231                #try:
232                return objc._loadConstant(name, tp, magic)
233                #except Exception as exc:
234                #    print "LOAD %r %r %r -> raise %s"%(name, tp, magic, exc)
235                #    raise
236
237        if self.__enummap:
238            m = re.search(r"\$%s@([^$]*)\$"%(name,), self.__enummap)
239            if m is not None:
240                val = m.group(1)
241
242                if val.startswith("'"):
243                    if isinstance(val, bytes):
244                        # Python 2.x
245                        val, = struct.unpack('>l', val[1:-1])
246                    else:
247                        # Python 3.x
248                        val, = struct.unpack('>l', val[1:-1].encode('latin1'))
249
250                elif '.' in val:
251                    val = float(val)
252                else:
253                    val = int(val)
254
255                return val
256
257        if self.__funcmap:
258            if name in self.__funcmap:
259                info = self.__funcmap[name]
260
261                func_list = [ (name,) + info ]
262
263                d = {}
264                objc.loadBundleFunctions(self.__bundle, d, func_list)
265                if name in d:
266                    return d[name]
267
268                if self.__inlinelist is not None:
269                    try:
270                        objc.loadFunctionList(
271                            self.__inlinelist, d, func_list, skip_undefined=False)
272                    except objc.error:
273                        pass
274
275                    else:
276                        if name in d:
277                            return d[name]
278
279        if self.__expressions:
280            if name in self.__expressions:
281                info = self.__expressions[name]
282                try:
283                    return eval(info, {}, self.__expressions_mapping)
284                except NameError:
285                    pass
286
287        if self.__aliases:
288            if name in self.__aliases:
289                alias = self.__aliases[name]
290                if alias == 'ULONG_MAX':
291                    return (sys.maxsize * 2) + 1
292                elif alias == 'LONG_MAX':
293                    return sys.maxsize
294                elif alias == 'LONG_MIN':
295                    return -sys.maxsize-1
296
297                return getattr(self, alias)
298
299        raise AttributeError(name)
300
301    def __load_cftypes(self, cftypes):
302        if not cftypes: return
303
304        for name, type, gettypeid_func, tollfree in cftypes:
305            if tollfree:
306                for nm in tollfree.split(','):
307                    try:
308                        objc.lookUpClass(nm)
309                    except objc.error:
310                        pass
311                    else:
312                        tollfree = nm
313                        break
314                try:
315                    v = objc.registerCFSignature(name, type, None, tollfree)
316                    if v is not None:
317                        self.__dict__[name] = v
318                        continue
319                except objc.nosuchclass_error:
320                    pass
321
322            try:
323                func = getattr(self, gettypeid_func)
324            except AttributeError:
325                # GetTypeID function not found, this is either
326                # a CFType that isn't present on the current
327                # platform, or a CFType without a public GetTypeID
328                # function. Proxy using the generic CFType
329                if tollfree is None:
330                    v = objc.registerCFSignature(name, type, None, 'NSCFType')
331                    if v is not None:
332                        self.__dict__[name] = v
333                continue
334
335            v = objc.registerCFSignature(name, type, func())
336            if v is not None:
337                self.__dict__[name] = v
338