~jameinel/bzr/fix-push2

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_lazy_import.py

  • Committer: Aaron Bentley
  • Date: 2006-09-22 04:52:17 UTC
  • mfrom: (2029 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2078.
  • Revision ID: aaron.bentley@utoronto.ca-20060922045217-4e775bf2fc6d0b3b
Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 by Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Test the lazy_import functionality."""
 
18
 
 
19
import os
 
20
import sys
 
21
 
 
22
from bzrlib import (
 
23
    errors,
 
24
    lazy_import,
 
25
    osutils,
 
26
    )
 
27
from bzrlib.tests import TestCase, TestCaseInTempDir
 
28
 
 
29
 
 
30
class InstrumentedReplacer(lazy_import.ScopeReplacer):
 
31
    """Track what actions are done"""
 
32
 
 
33
    @staticmethod
 
34
    def use_actions(actions):
 
35
        InstrumentedReplacer.actions = actions
 
36
 
 
37
    def _replace(self):
 
38
        InstrumentedReplacer.actions.append('_replace')
 
39
        return lazy_import.ScopeReplacer._replace(self)
 
40
 
 
41
    def __getattribute__(self, attr):
 
42
        InstrumentedReplacer.actions.append(('__getattribute__', attr))
 
43
        return lazy_import.ScopeReplacer.__getattribute__(self, attr)
 
44
 
 
45
    def __call__(self, *args, **kwargs):
 
46
        InstrumentedReplacer.actions.append(('__call__', args, kwargs))
 
47
        return lazy_import.ScopeReplacer.__call__(self, *args, **kwargs)
 
48
 
 
49
 
 
50
class InstrumentedImportReplacer(lazy_import.ImportReplacer):
 
51
 
 
52
    @staticmethod
 
53
    def use_actions(actions):
 
54
        InstrumentedImportReplacer.actions = actions
 
55
 
 
56
    def _import(self, scope, name):
 
57
        InstrumentedImportReplacer.actions.append(('_import', name))
 
58
        return lazy_import.ImportReplacer._import(self, scope, name)
 
59
 
 
60
    def _replace(self):
 
61
        InstrumentedImportReplacer.actions.append('_replace')
 
62
        return lazy_import.ScopeReplacer._replace(self)
 
63
 
 
64
    def __getattribute__(self, attr):
 
65
        InstrumentedImportReplacer.actions.append(('__getattribute__', attr))
 
66
        return lazy_import.ScopeReplacer.__getattribute__(self, attr)
 
67
 
 
68
    def __call__(self, *args, **kwargs):
 
69
        InstrumentedImportReplacer.actions.append(('__call__', args, kwargs))
 
70
        return lazy_import.ScopeReplacer.__call__(self, *args, **kwargs)
 
71
 
 
72
 
 
73
class TestClass(object):
 
74
    """Just a simple test class instrumented for the test cases"""
 
75
 
 
76
    class_member = 'class_member'
 
77
 
 
78
    @staticmethod
 
79
    def use_actions(actions):
 
80
        TestClass.actions = actions
 
81
 
 
82
    def __init__(self):
 
83
        TestClass.actions.append('init')
 
84
 
 
85
    def foo(self, x):
 
86
        TestClass.actions.append(('foo', x))
 
87
        return 'foo'
 
88
 
 
89
 
 
90
class TestScopeReplacer(TestCase):
 
91
    """Test the ability of the replacer to put itself into the correct scope.
 
92
 
 
93
    In these tests we use the global scope, because we cannot replace
 
94
    variables in the local scope. This means that we need to be careful
 
95
    and not have the replacing objects use the same name, or we would
 
96
    get collisions.
 
97
    """
 
98
 
 
99
    def test_object(self):
 
100
        """ScopeReplacer can create an instance in local scope.
 
101
        
 
102
        An object should appear in globals() by constructing a ScopeReplacer,
 
103
        and it will be replaced with the real object upon the first request.
 
104
        """
 
105
        actions = []
 
106
        InstrumentedReplacer.use_actions(actions)
 
107
        TestClass.use_actions(actions)
 
108
 
 
109
        def factory(replacer, scope, name):
 
110
            actions.append('factory')
 
111
            return TestClass()
 
112
 
 
113
        try:
 
114
            test_obj1
 
115
        except NameError:
 
116
            # test_obj1 shouldn't exist yet
 
117
            pass
 
118
        else:
 
119
            self.fail('test_obj1 was not supposed to exist yet')
 
120
 
 
121
        orig_globals = set(globals().keys())
 
122
 
 
123
        InstrumentedReplacer(scope=globals(), name='test_obj1',
 
124
                             factory=factory)
 
125
 
 
126
        new_globals = set(globals().keys())
 
127
 
 
128
        # We can't use isinstance() because that uses test_obj1.__class__
 
129
        # and that goes through __getattribute__ which would activate
 
130
        # the replacement
 
131
        self.assertEqual(InstrumentedReplacer,
 
132
                         object.__getattribute__(test_obj1, '__class__'))
 
133
        self.assertEqual('foo', test_obj1.foo(1))
 
134
        self.assertIsInstance(test_obj1, TestClass)
 
135
        self.assertEqual('foo', test_obj1.foo(2))
 
136
        self.assertEqual([('__getattribute__', 'foo'),
 
137
                          '_replace',
 
138
                          'factory',
 
139
                          'init',
 
140
                          ('foo', 1),
 
141
                          ('foo', 2),
 
142
                         ], actions)
 
143
 
 
144
    def test_replace_side_effects(self):
 
145
        """Creating a new object should only create one entry in globals.
 
146
 
 
147
        And only that entry even after replacement.
 
148
        """
 
149
        try:
 
150
            test_scope1
 
151
        except NameError:
 
152
            # test_scope1 shouldn't exist yet
 
153
            pass
 
154
        else:
 
155
            self.fail('test_scope1 was not supposed to exist yet')
 
156
 
 
157
        # ignore the logged actions
 
158
        TestClass.use_actions([])
 
159
 
 
160
        def factory(replacer, scope, name):
 
161
            return TestClass()
 
162
 
 
163
        orig_globals = set(globals().keys())
 
164
 
 
165
        lazy_import.ScopeReplacer(scope=globals(), name='test_scope1',
 
166
                                  factory=factory)
 
167
 
 
168
        new_globals = set(globals().keys())
 
169
 
 
170
        self.assertEqual(lazy_import.ScopeReplacer,
 
171
                         object.__getattribute__(test_scope1, '__class__'))
 
172
        self.assertEqual('foo', test_scope1.foo(1))
 
173
        self.assertIsInstance(test_scope1, TestClass)
 
174
 
 
175
        final_globals = set(globals().keys())
 
176
 
 
177
        self.assertEqual(set(['test_scope1']), new_globals - orig_globals)
 
178
        self.assertEqual(set(), orig_globals - new_globals)
 
179
        self.assertEqual(set(), final_globals - new_globals)
 
180
        self.assertEqual(set(), new_globals - final_globals)
 
181
 
 
182
    def test_class(self):
 
183
        actions = []
 
184
        InstrumentedReplacer.use_actions(actions)
 
185
        TestClass.use_actions(actions)
 
186
 
 
187
        def factory(replacer, scope, name):
 
188
            actions.append('factory')
 
189
            return TestClass
 
190
 
 
191
        try:
 
192
            test_class1
 
193
        except NameError:
 
194
            # test_class2 shouldn't exist yet
 
195
            pass
 
196
        else:
 
197
            self.fail('test_class1 was not supposed to exist yet')
 
198
 
 
199
        InstrumentedReplacer(scope=globals(), name='test_class1',
 
200
                             factory=factory)
 
201
 
 
202
        self.assertEqual('class_member', test_class1.class_member)
 
203
        self.assertEqual(test_class1, TestClass)
 
204
        self.assertEqual([('__getattribute__', 'class_member'),
 
205
                          '_replace',
 
206
                          'factory',
 
207
                         ], actions)
 
208
 
 
209
    def test_call_class(self):
 
210
        actions = []
 
211
        InstrumentedReplacer.use_actions(actions)
 
212
        TestClass.use_actions(actions)
 
213
 
 
214
        def factory(replacer, scope, name):
 
215
            actions.append('factory')
 
216
            return TestClass
 
217
 
 
218
        try:
 
219
            test_class2
 
220
        except NameError:
 
221
            # test_class2 shouldn't exist yet
 
222
            pass
 
223
        else:
 
224
            self.fail('test_class2 was not supposed to exist yet')
 
225
 
 
226
        InstrumentedReplacer(scope=globals(), name='test_class2',
 
227
                             factory=factory)
 
228
 
 
229
        self.failIf(test_class2 is TestClass)
 
230
        obj = test_class2()
 
231
        self.assertIs(test_class2, TestClass)
 
232
        self.assertIsInstance(obj, TestClass)
 
233
        self.assertEqual('class_member', obj.class_member)
 
234
        self.assertEqual([('__call__', (), {}),
 
235
                          '_replace',
 
236
                          'factory',
 
237
                          'init',
 
238
                         ], actions)
 
239
 
 
240
    def test_call_func(self):
 
241
        actions = []
 
242
        InstrumentedReplacer.use_actions(actions)
 
243
 
 
244
        def func(a, b, c=None):
 
245
            actions.append('func')
 
246
            return (a, b, c)
 
247
 
 
248
        def factory(replacer, scope, name):
 
249
            actions.append('factory')
 
250
            return func
 
251
 
 
252
        try:
 
253
            test_func1
 
254
        except NameError:
 
255
            # test_func1 shouldn't exist yet
 
256
            pass
 
257
        else:
 
258
            self.fail('test_func1 was not supposed to exist yet')
 
259
        InstrumentedReplacer(scope=globals(), name='test_func1',
 
260
                             factory=factory)
 
261
 
 
262
        self.failIf(test_func1 is func)
 
263
        val = test_func1(1, 2, c='3')
 
264
        self.assertIs(test_func1, func)
 
265
 
 
266
        self.assertEqual((1,2,'3'), val)
 
267
        self.assertEqual([('__call__', (1,2), {'c':'3'}),
 
268
                          '_replace',
 
269
                          'factory',
 
270
                          'func',
 
271
                         ], actions)
 
272
 
 
273
    def test_other_variable(self):
 
274
        """Test when a ScopeReplacer is assigned to another variable.
 
275
 
 
276
        This test could be updated if we find a way to trap '=' rather
 
277
        than just giving a belated exception.
 
278
        ScopeReplacer only knows about the variable it was created as,
 
279
        so until the object is replaced, it is illegal to pass it to
 
280
        another variable. (Though discovering this may take a while)
 
281
        """
 
282
        actions = []
 
283
        InstrumentedReplacer.use_actions(actions)
 
284
        TestClass.use_actions(actions)
 
285
 
 
286
        def factory(replacer, scope, name):
 
287
            actions.append('factory')
 
288
            return TestClass()
 
289
 
 
290
        try:
 
291
            test_obj2
 
292
        except NameError:
 
293
            # test_obj2 shouldn't exist yet
 
294
            pass
 
295
        else:
 
296
            self.fail('test_obj2 was not supposed to exist yet')
 
297
 
 
298
        InstrumentedReplacer(scope=globals(), name='test_obj2',
 
299
                             factory=factory)
 
300
 
 
301
        self.assertEqual(InstrumentedReplacer,
 
302
                         object.__getattribute__(test_obj2, '__class__'))
 
303
        # This is technically not allowed, but we don't have a way to
 
304
        # test it until later.
 
305
        test_obj3 = test_obj2
 
306
        self.assertEqual(InstrumentedReplacer,
 
307
                         object.__getattribute__(test_obj2, '__class__'))
 
308
        self.assertEqual(InstrumentedReplacer,
 
309
                         object.__getattribute__(test_obj3, '__class__'))
 
310
        
 
311
        # The first use of the alternate variable causes test_obj2 to
 
312
        # be replaced.
 
313
        self.assertEqual('foo', test_obj3.foo(1))
 
314
        # test_obj2 has been replaced, but the ScopeReplacer has no
 
315
        # idea of test_obj3
 
316
        self.assertEqual(TestClass,
 
317
                         object.__getattribute__(test_obj2, '__class__'))
 
318
        self.assertEqual(InstrumentedReplacer,
 
319
                         object.__getattribute__(test_obj3, '__class__'))
 
320
        # We should be able to access test_obj2 attributes normally
 
321
        self.assertEqual('foo', test_obj2.foo(2))
 
322
        self.assertEqual('foo', test_obj2.foo(3))
 
323
 
 
324
        # However, the next access on test_obj3 should raise an error
 
325
        # because only now are we able to detect the problem.
 
326
        self.assertRaises(errors.IllegalUseOfScopeReplacer,
 
327
                          getattr, test_obj3, 'foo')
 
328
        
 
329
        # However, the 
 
330
        self.assertEqual([('__getattribute__', 'foo'),
 
331
                          '_replace',
 
332
                          'factory',
 
333
                          'init',
 
334
                          ('foo', 1),
 
335
                          ('foo', 2),
 
336
                          ('foo', 3),
 
337
                          ('__getattribute__', 'foo'),
 
338
                          '_replace',
 
339
                         ], actions)
 
340
 
 
341
 
 
342
class ImportReplacerHelper(TestCaseInTempDir):
 
343
    """Test the ability to have a lazily imported module or object"""
 
344
 
 
345
    def setUp(self):
 
346
        TestCaseInTempDir.setUp(self)
 
347
        self.create_modules()
 
348
        base_path = self.test_dir + '/base'
 
349
 
 
350
        self.actions = []
 
351
        InstrumentedImportReplacer.use_actions(self.actions)
 
352
 
 
353
        original_import = __import__
 
354
        def instrumented_import(mod, scope1, scope2, fromlist):
 
355
            self.actions.append(('import', mod, fromlist))
 
356
            return original_import(mod, scope1, scope2, fromlist)
 
357
 
 
358
        def cleanup():
 
359
            if base_path in sys.path:
 
360
                sys.path.remove(base_path)
 
361
            __builtins__['__import__'] = original_import
 
362
        self.addCleanup(cleanup)
 
363
        sys.path.append(base_path)
 
364
        __builtins__['__import__'] = instrumented_import
 
365
 
 
366
    def create_modules(self):
 
367
        """Create some random modules to be imported.
 
368
 
 
369
        Each entry has a random suffix, and the full names are saved
 
370
 
 
371
        These are setup as follows:
 
372
         base/ <= used to ensure not in default search path
 
373
            root-XXX/
 
374
                __init__.py <= This will contain var1, func1
 
375
                mod-XXX.py <= This will contain var2, func2
 
376
                sub-XXX/
 
377
                    __init__.py <= Contains var3, func3
 
378
                    submoda-XXX.py <= contains var4, func4
 
379
                    submodb-XXX.py <= containse var5, func5
 
380
        """
 
381
        rand_suffix = osutils.rand_chars(4)
 
382
        root_name = 'root_' + rand_suffix
 
383
        mod_name = 'mod_' + rand_suffix
 
384
        sub_name = 'sub_' + rand_suffix
 
385
        submoda_name = 'submoda_' + rand_suffix
 
386
        submodb_name = 'submodb_' + rand_suffix
 
387
 
 
388
        os.mkdir('base')
 
389
        root_path = osutils.pathjoin('base', root_name)
 
390
        os.mkdir(root_path)
 
391
        root_init = osutils.pathjoin(root_path, '__init__.py')
 
392
        f = open(osutils.pathjoin(root_path, '__init__.py'), 'wb')
 
393
        try:
 
394
            f.write('var1 = 1\ndef func1(a):\n  return a\n')
 
395
        finally:
 
396
            f.close()
 
397
        mod_path = osutils.pathjoin(root_path, mod_name + '.py')
 
398
        f = open(mod_path, 'wb')
 
399
        try:
 
400
            f.write('var2 = 2\ndef func2(a):\n  return a\n')
 
401
        finally:
 
402
            f.close()
 
403
 
 
404
        sub_path = osutils.pathjoin(root_path, sub_name)
 
405
        os.mkdir(sub_path)
 
406
        f = open(osutils.pathjoin(sub_path, '__init__.py'), 'wb')
 
407
        try:
 
408
            f.write('var3 = 3\ndef func3(a):\n  return a\n')
 
409
        finally:
 
410
            f.close()
 
411
        submoda_path = osutils.pathjoin(sub_path, submoda_name + '.py')
 
412
        f = open(submoda_path, 'wb')
 
413
        try:
 
414
            f.write('var4 = 4\ndef func4(a):\n  return a\n')
 
415
        finally:
 
416
            f.close()
 
417
        submodb_path = osutils.pathjoin(sub_path, submodb_name + '.py')
 
418
        f = open(submodb_path, 'wb')
 
419
        try:
 
420
            f.write('var5 = 5\ndef func5(a):\n  return a\n')
 
421
        finally:
 
422
            f.close()
 
423
        self.root_name = root_name
 
424
        self.mod_name = mod_name
 
425
        self.sub_name = sub_name
 
426
        self.submoda_name = submoda_name
 
427
        self.submodb_name = submodb_name
 
428
 
 
429
 
 
430
class TestImportReplacerHelper(ImportReplacerHelper):
 
431
 
 
432
    def test_basic_import(self):
 
433
        """Test that a real import of these modules works"""
 
434
        sub_mod_path = '.'.join([self.root_name, self.sub_name,
 
435
                                  self.submoda_name])
 
436
        root = __import__(sub_mod_path, globals(), locals(), [])
 
437
        self.assertEqual(1, root.var1)
 
438
        self.assertEqual(3, getattr(root, self.sub_name).var3)
 
439
        self.assertEqual(4, getattr(getattr(root, self.sub_name),
 
440
                                    self.submoda_name).var4)
 
441
 
 
442
        mod_path = '.'.join([self.root_name, self.mod_name])
 
443
        root = __import__(mod_path, globals(), locals(), [])
 
444
        self.assertEqual(2, getattr(root, self.mod_name).var2)
 
445
 
 
446
        self.assertEqual([('import', sub_mod_path, []),
 
447
                          ('import', mod_path, []),
 
448
                         ], self.actions)
 
449
 
 
450
 
 
451
class TestImportReplacer(ImportReplacerHelper):
 
452
 
 
453
    def test_import_root(self):
 
454
        """Test 'import root-XXX as root1'"""
 
455
        try:
 
456
            root1
 
457
        except NameError:
 
458
            # root1 shouldn't exist yet
 
459
            pass
 
460
        else:
 
461
            self.fail('root1 was not supposed to exist yet')
 
462
 
 
463
        # This should replicate 'import root-xxyyzz as root1'
 
464
        InstrumentedImportReplacer(scope=globals(), name='root1',
 
465
                                   module_path=[self.root_name],
 
466
                                   member=None, children={})
 
467
 
 
468
        self.assertEqual(InstrumentedImportReplacer,
 
469
                         object.__getattribute__(root1, '__class__'))
 
470
        self.assertEqual(1, root1.var1)
 
471
        self.assertEqual('x', root1.func1('x'))
 
472
 
 
473
        self.assertEqual([('__getattribute__', 'var1'),
 
474
                          '_replace',
 
475
                          ('_import', 'root1'),
 
476
                          ('import', self.root_name, []),
 
477
                         ], self.actions)
 
478
 
 
479
    def test_import_mod(self):
 
480
        """Test 'import root-XXX.mod-XXX as mod2'"""
 
481
        try:
 
482
            mod1
 
483
        except NameError:
 
484
            # mod1 shouldn't exist yet
 
485
            pass
 
486
        else:
 
487
            self.fail('mod1 was not supposed to exist yet')
 
488
 
 
489
        mod_path = self.root_name + '.' + self.mod_name
 
490
        InstrumentedImportReplacer(scope=globals(), name='mod1',
 
491
                                   module_path=[self.root_name, self.mod_name],
 
492
                                   member=None, children={})
 
493
 
 
494
        self.assertEqual(InstrumentedImportReplacer,
 
495
                         object.__getattribute__(mod1, '__class__'))
 
496
        self.assertEqual(2, mod1.var2)
 
497
        self.assertEqual('y', mod1.func2('y'))
 
498
 
 
499
        self.assertEqual([('__getattribute__', 'var2'),
 
500
                          '_replace',
 
501
                          ('_import', 'mod1'),
 
502
                          ('import', mod_path, []),
 
503
                         ], self.actions)
 
504
 
 
505
    def test_import_mod_from_root(self):
 
506
        """Test 'from root-XXX import mod-XXX as mod2'"""
 
507
        try:
 
508
            mod2
 
509
        except NameError:
 
510
            # mod2 shouldn't exist yet
 
511
            pass
 
512
        else:
 
513
            self.fail('mod2 was not supposed to exist yet')
 
514
 
 
515
        InstrumentedImportReplacer(scope=globals(), name='mod2',
 
516
                                   module_path=[self.root_name],
 
517
                                   member=self.mod_name, children={})
 
518
 
 
519
        self.assertEqual(InstrumentedImportReplacer,
 
520
                         object.__getattribute__(mod2, '__class__'))
 
521
        self.assertEqual(2, mod2.var2)
 
522
        self.assertEqual('y', mod2.func2('y'))
 
523
 
 
524
        self.assertEqual([('__getattribute__', 'var2'),
 
525
                          '_replace',
 
526
                          ('_import', 'mod2'),
 
527
                          ('import', self.root_name, [self.mod_name]),
 
528
                         ], self.actions)
 
529
 
 
530
    def test_import_root_and_mod(self):
 
531
        """Test 'import root-XXX.mod-XXX' remapping both to root3.mod3"""
 
532
        try:
 
533
            root3
 
534
        except NameError:
 
535
            # root3 shouldn't exist yet
 
536
            pass
 
537
        else:
 
538
            self.fail('root3 was not supposed to exist yet')
 
539
 
 
540
        InstrumentedImportReplacer(scope=globals(),
 
541
            name='root3', module_path=[self.root_name], member=None,
 
542
            children={'mod3':([self.root_name, self.mod_name], None, {})})
 
543
 
 
544
        # So 'root3' should be a lazy import
 
545
        # and once it is imported, mod3 should also be lazy until
 
546
        # actually accessed.
 
547
        self.assertEqual(InstrumentedImportReplacer,
 
548
                         object.__getattribute__(root3, '__class__'))
 
549
        self.assertEqual(1, root3.var1)
 
550
 
 
551
        # There is a mod3 member, but it is also lazy
 
552
        self.assertEqual(InstrumentedImportReplacer,
 
553
                         object.__getattribute__(root3.mod3, '__class__'))
 
554
        self.assertEqual(2, root3.mod3.var2)
 
555
 
 
556
        mod_path = self.root_name + '.' + self.mod_name
 
557
        self.assertEqual([('__getattribute__', 'var1'),
 
558
                          '_replace',
 
559
                          ('_import', 'root3'),
 
560
                          ('import', self.root_name, []),
 
561
                          ('__getattribute__', 'var2'),
 
562
                          '_replace',
 
563
                          ('_import', 'mod3'),
 
564
                          ('import', mod_path, []),
 
565
                         ], self.actions)
 
566
 
 
567
    def test_import_root_and_root_mod(self):
 
568
        """Test that 'import root, root.mod' can be done.
 
569
 
 
570
        The second import should re-use the first one, and just add
 
571
        children to be imported.
 
572
        """
 
573
        try:
 
574
            root4
 
575
        except NameError:
 
576
            # root4 shouldn't exist yet
 
577
            pass
 
578
        else:
 
579
            self.fail('root4 was not supposed to exist yet')
 
580
 
 
581
        InstrumentedImportReplacer(scope=globals(),
 
582
            name='root4', module_path=[self.root_name], member=None,
 
583
            children={})
 
584
 
 
585
        # So 'root4' should be a lazy import
 
586
        self.assertEqual(InstrumentedImportReplacer,
 
587
                         object.__getattribute__(root4, '__class__'))
 
588
 
 
589
        # Lets add a new child to be imported on demand
 
590
        # This syntax of using object.__getattribute__ is the correct method
 
591
        # for accessing the _import_replacer_children member
 
592
        children = object.__getattribute__(root4, '_import_replacer_children')
 
593
        children['mod4'] = ([self.root_name, self.mod_name], None, {})
 
594
 
 
595
        # Accessing root4.mod4 should import root, but mod should stay lazy
 
596
        self.assertEqual(InstrumentedImportReplacer,
 
597
                         object.__getattribute__(root4.mod4, '__class__'))
 
598
        self.assertEqual(2, root4.mod4.var2)
 
599
 
 
600
        mod_path = self.root_name + '.' + self.mod_name
 
601
        self.assertEqual([('__getattribute__', 'mod4'),
 
602
                          '_replace',
 
603
                          ('_import', 'root4'),
 
604
                          ('import', self.root_name, []),
 
605
                          ('__getattribute__', 'var2'),
 
606
                          '_replace',
 
607
                          ('_import', 'mod4'),
 
608
                          ('import', mod_path, []),
 
609
                         ], self.actions)
 
610
 
 
611
    def test_import_root_sub_submod(self):
 
612
        """Test import root.mod, root.sub.submoda, root.sub.submodb
 
613
        root should be a lazy import, with multiple children, who also
 
614
        have children to be imported.
 
615
        And when root is imported, the children should be lazy, and
 
616
        reuse the intermediate lazy object.
 
617
        """
 
618
        try:
 
619
            root5
 
620
        except NameError:
 
621
            # root5 shouldn't exist yet
 
622
            pass
 
623
        else:
 
624
            self.fail('root5 was not supposed to exist yet')
 
625
 
 
626
        InstrumentedImportReplacer(scope=globals(),
 
627
            name='root5', module_path=[self.root_name], member=None,
 
628
            children={'mod5':([self.root_name, self.mod_name], None, {}),
 
629
                      'sub5':([self.root_name, self.sub_name], None,
 
630
                            {'submoda5':([self.root_name, self.sub_name,
 
631
                                         self.submoda_name], None, {}),
 
632
                             'submodb5':([self.root_name, self.sub_name,
 
633
                                          self.submodb_name], None, {})
 
634
                            }),
 
635
                     })
 
636
 
 
637
        # So 'root5' should be a lazy import
 
638
        self.assertEqual(InstrumentedImportReplacer,
 
639
                         object.__getattribute__(root5, '__class__'))
 
640
 
 
641
        # Accessing root5.mod5 should import root, but mod should stay lazy
 
642
        self.assertEqual(InstrumentedImportReplacer,
 
643
                         object.__getattribute__(root5.mod5, '__class__'))
 
644
        # root5.sub5 should still be lazy, but not re-import root5
 
645
        self.assertEqual(InstrumentedImportReplacer,
 
646
                         object.__getattribute__(root5.sub5, '__class__'))
 
647
 
 
648
        # Accessing root5.sub5.submoda5 should import sub5, but not either
 
649
        # of the sub objects (they should be available as lazy objects
 
650
        self.assertEqual(InstrumentedImportReplacer,
 
651
                     object.__getattribute__(root5.sub5.submoda5, '__class__'))
 
652
        self.assertEqual(InstrumentedImportReplacer,
 
653
                     object.__getattribute__(root5.sub5.submodb5, '__class__'))
 
654
 
 
655
        # This should import mod5
 
656
        self.assertEqual(2, root5.mod5.var2)
 
657
        # These should import submoda5 and submodb5
 
658
        self.assertEqual(4, root5.sub5.submoda5.var4)
 
659
        self.assertEqual(5, root5.sub5.submodb5.var5)
 
660
 
 
661
        mod_path = self.root_name + '.' + self.mod_name
 
662
        sub_path = self.root_name + '.' + self.sub_name
 
663
        submoda_path = sub_path + '.' + self.submoda_name
 
664
        submodb_path = sub_path + '.' + self.submodb_name
 
665
 
 
666
        self.assertEqual([('__getattribute__', 'mod5'),
 
667
                          '_replace',
 
668
                          ('_import', 'root5'),
 
669
                          ('import', self.root_name, []),
 
670
                          ('__getattribute__', 'submoda5'),
 
671
                          '_replace',
 
672
                          ('_import', 'sub5'),
 
673
                          ('import', sub_path, []),
 
674
                          ('__getattribute__', 'var2'),
 
675
                          '_replace',
 
676
                          ('_import', 'mod5'),
 
677
                          ('import', mod_path, []),
 
678
                          ('__getattribute__', 'var4'),
 
679
                          '_replace',
 
680
                          ('_import', 'submoda5'),
 
681
                          ('import', submoda_path, []),
 
682
                          ('__getattribute__', 'var5'),
 
683
                          '_replace',
 
684
                          ('_import', 'submodb5'),
 
685
                          ('import', submodb_path, []),
 
686
                         ], self.actions)
 
687
 
 
688
 
 
689
class TestConvertImportToMap(TestCase):
 
690
    """Directly test the conversion from import strings to maps"""
 
691
 
 
692
    def check(self, expected, import_strings):
 
693
        proc = lazy_import.ImportProcessor()
 
694
        for import_str in import_strings:
 
695
            proc._convert_import_str(import_str)
 
696
        self.assertEqual(expected, proc.imports,
 
697
                         'Import of %r was not converted correctly'
 
698
                         ' %s != %s' % (import_strings, expected,
 
699
                                        proc.imports))
 
700
 
 
701
    def test_import_one(self):
 
702
        self.check({'one':(['one'], None, {}),
 
703
                   }, ['import one'])
 
704
 
 
705
    def test_import_one_two(self):
 
706
        one_two_map = {'one':(['one'], None,
 
707
                              {'two':(['one', 'two'], None, {}),
 
708
                              }),
 
709
                      }
 
710
        self.check(one_two_map, ['import one.two'])
 
711
        self.check(one_two_map, ['import one, one.two'])
 
712
        self.check(one_two_map, ['import one', 'import one.two'])
 
713
        self.check(one_two_map, ['import one.two', 'import one'])
 
714
 
 
715
    def test_import_one_two_three(self):
 
716
        one_two_three_map = {
 
717
            'one':(['one'], None,
 
718
                   {'two':(['one', 'two'], None,
 
719
                           {'three':(['one', 'two', 'three'], None, {}),
 
720
                           }),
 
721
                   }),
 
722
        }
 
723
        self.check(one_two_three_map, ['import one.two.three'])
 
724
        self.check(one_two_three_map, ['import one, one.two.three'])
 
725
        self.check(one_two_three_map, ['import one',
 
726
                                              'import one.two.three'])
 
727
        self.check(one_two_three_map, ['import one.two.three',
 
728
                                              'import one'])
 
729
 
 
730
    def test_import_one_as_x(self):
 
731
        self.check({'x':(['one'], None, {}),
 
732
                          }, ['import one as x'])
 
733
 
 
734
    def test_import_one_two_as_x(self):
 
735
        self.check({'x':(['one', 'two'], None, {}),
 
736
                   }, ['import one.two as x'])
 
737
 
 
738
    def test_import_mixed(self):
 
739
        mixed = {'x':(['one', 'two'], None, {}),
 
740
                 'one':(['one'], None,
 
741
                       {'two':(['one', 'two'], None, {}),
 
742
                       }),
 
743
                }
 
744
        self.check(mixed, ['import one.two as x, one.two'])
 
745
        self.check(mixed, ['import one.two as x', 'import one.two'])
 
746
        self.check(mixed, ['import one.two', 'import one.two as x'])
 
747
 
 
748
    def test_import_with_as(self):
 
749
        self.check({'fast':(['fast'], None, {})}, ['import fast'])
 
750
 
 
751
 
 
752
class TestFromToMap(TestCase):
 
753
    """Directly test the conversion of 'from foo import bar' syntax"""
 
754
 
 
755
    def check_result(self, expected, from_strings):
 
756
        proc = lazy_import.ImportProcessor()
 
757
        for from_str in from_strings:
 
758
            proc._convert_from_str(from_str)
 
759
        self.assertEqual(expected, proc.imports,
 
760
                         'Import of %r was not converted correctly'
 
761
                         ' %s != %s' % (from_strings, expected, proc.imports))
 
762
 
 
763
    def test_from_one_import_two(self):
 
764
        self.check_result({'two':(['one'], 'two', {})},
 
765
                          ['from one import two'])
 
766
 
 
767
    def test_from_one_import_two_as_three(self):
 
768
        self.check_result({'three':(['one'], 'two', {})},
 
769
                          ['from one import two as three'])
 
770
 
 
771
    def test_from_one_import_two_three(self):
 
772
        two_three_map = {'two':(['one'], 'two', {}),
 
773
                         'three':(['one'], 'three', {}),
 
774
                        }
 
775
        self.check_result(two_three_map,
 
776
                          ['from one import two, three'])
 
777
        self.check_result(two_three_map,
 
778
                          ['from one import two',
 
779
                           'from one import three'])
 
780
 
 
781
    def test_from_one_two_import_three(self):
 
782
        self.check_result({'three':(['one', 'two'], 'three', {})},
 
783
                          ['from one.two import three'])
 
784
 
 
785
 
 
786
class TestCanonicalize(TestCase):
 
787
    """Test that we can canonicalize import texts"""
 
788
 
 
789
    def check(self, expected, text):
 
790
        proc = lazy_import.ImportProcessor()
 
791
        parsed = proc._canonicalize_import_text(text)
 
792
        self.assertEqual(expected, parsed,
 
793
                         'Incorrect parsing of text:\n%s\n%s\n!=\n%s'
 
794
                         % (text, expected, parsed))
 
795
 
 
796
    def test_import_one(self):
 
797
        self.check(['import one'], 'import one')
 
798
        self.check(['import one'], '\nimport one\n\n')
 
799
 
 
800
    def test_import_one_two(self):
 
801
        self.check(['import one, two'], 'import one, two')
 
802
        self.check(['import one, two'], '\nimport one, two\n\n')
 
803
 
 
804
    def test_import_one_as_two_as(self):
 
805
        self.check(['import one as x, two as y'], 'import one as x, two as y')
 
806
        self.check(['import one as x, two as y'],
 
807
                   '\nimport one as x, two as y\n')
 
808
 
 
809
    def test_from_one_import_two(self):
 
810
        self.check(['from one import two'], 'from one import two')
 
811
        self.check(['from one import two'], '\nfrom one import two\n\n')
 
812
        self.check(['from one import two'], '\nfrom one import (two)\n')
 
813
        self.check(['from one import  two '], '\nfrom one import (\n\ttwo\n)\n')
 
814
 
 
815
    def test_multiple(self):
 
816
        self.check(['import one', 'import two, three', 'from one import four'],
 
817
                   'import one\nimport two, three\nfrom one import four')
 
818
        self.check(['import one', 'import two, three', 'from one import four'],
 
819
                   'import one\nimport (two, three)\nfrom one import four')
 
820
        self.check(['import one', 'import two, three', 'from one import four'],
 
821
                   'import one\n'
 
822
                   'import two, three\n'
 
823
                   'from one import four')
 
824
        self.check(['import one',
 
825
                    'import two, three', 'from one import  four, '],
 
826
                   'import one\n'
 
827
                   'import two, three\n'
 
828
                   'from one import (\n'
 
829
                   '    four,\n'
 
830
                   '    )\n'
 
831
                   )
 
832
 
 
833
    def test_missing_trailing(self):
 
834
        proc = lazy_import.ImportProcessor()
 
835
        self.assertRaises(errors.InvalidImportLine,
 
836
                          proc._canonicalize_import_text,
 
837
                          "from foo import (\n  bar\n")
 
838
 
 
839
 
 
840
class TestImportProcessor(TestCase):
 
841
    """Test that ImportProcessor can turn import texts into lazy imports"""
 
842
 
 
843
    def check(self, expected, text):
 
844
        proc = lazy_import.ImportProcessor()
 
845
        proc._build_map(text)
 
846
        self.assertEqual(expected, proc.imports,
 
847
                         'Incorrect processing of:\n%s\n%s\n!=\n%s'
 
848
                         % (text, expected, proc.imports))
 
849
 
 
850
    def test_import_one(self):
 
851
        exp = {'one':(['one'], None, {})}
 
852
        self.check(exp, 'import one')
 
853
        self.check(exp, '\nimport one\n')
 
854
 
 
855
    def test_import_one_two(self):
 
856
        exp = {'one':(['one'], None,
 
857
                      {'two':(['one', 'two'], None, {}),
 
858
                      }),
 
859
              }
 
860
        self.check(exp, 'import one.two')
 
861
        self.check(exp, 'import one, one.two')
 
862
        self.check(exp, 'import one\nimport one.two')
 
863
 
 
864
    def test_import_as(self):
 
865
        exp = {'two':(['one'], None, {})}
 
866
        self.check(exp, 'import one as two')
 
867
 
 
868
    def test_import_many(self):
 
869
        exp = {'one':(['one'], None,
 
870
                      {'two':(['one', 'two'], None,
 
871
                              {'three':(['one', 'two', 'three'], None, {}),
 
872
                              }),
 
873
                       'four':(['one', 'four'], None, {}),
 
874
                      }),
 
875
               'five':(['one', 'five'], None, {}),
 
876
              }
 
877
        self.check(exp, 'import one.two.three, one.four, one.five as five')
 
878
        self.check(exp, 'import one.five as five\n'
 
879
                        'import one\n'
 
880
                        'import one.two.three\n'
 
881
                        'import one.four\n')
 
882
 
 
883
    def test_from_one_import_two(self):
 
884
        exp = {'two':(['one'], 'two', {})}
 
885
        self.check(exp, 'from one import two\n')
 
886
        self.check(exp, 'from one import (\n'
 
887
                        '    two,\n'
 
888
                        '    )\n')
 
889
 
 
890
    def test_from_one_import_two(self):
 
891
        exp = {'two':(['one'], 'two', {})}
 
892
        self.check(exp, 'from one import two\n')
 
893
        self.check(exp, 'from one import (two)\n')
 
894
        self.check(exp, 'from one import (two,)\n')
 
895
        self.check(exp, 'from one import two as two\n')
 
896
        self.check(exp, 'from one import (\n'
 
897
                        '    two,\n'
 
898
                        '    )\n')
 
899
 
 
900
    def test_from_many(self):
 
901
        exp = {'two':(['one'], 'two', {}),
 
902
               'three':(['one', 'two'], 'three', {}),
 
903
               'five':(['one', 'two'], 'four', {}),
 
904
              }
 
905
        self.check(exp, 'from one import two\n'
 
906
                        'from one.two import three, four as five\n')
 
907
        self.check(exp, 'from one import two\n'
 
908
                        'from one.two import (\n'
 
909
                        '    three,\n'
 
910
                        '    four as five,\n'
 
911
                        '    )\n')
 
912
 
 
913
    def test_mixed(self):
 
914
        exp = {'two':(['one'], 'two', {}),
 
915
               'three':(['one', 'two'], 'three', {}),
 
916
               'five':(['one', 'two'], 'four', {}),
 
917
               'one':(['one'], None,
 
918
                      {'two':(['one', 'two'], None, {}),
 
919
                      }),
 
920
              }
 
921
        self.check(exp, 'from one import two\n'
 
922
                        'from one.two import three, four as five\n'
 
923
                        'import one.two')
 
924
        self.check(exp, 'from one import two\n'
 
925
                        'from one.two import (\n'
 
926
                        '    three,\n'
 
927
                        '    four as five,\n'
 
928
                        '    )\n'
 
929
                        'import one\n'
 
930
                        'import one.two\n')
 
931
 
 
932
    def test_incorrect_line(self):
 
933
        proc = lazy_import.ImportProcessor()
 
934
        self.assertRaises(errors.InvalidImportLine,
 
935
                          proc._build_map, 'foo bar baz')
 
936
        self.assertRaises(errors.InvalidImportLine,
 
937
                          proc._build_map, 'improt foo')
 
938
        self.assertRaises(errors.InvalidImportLine,
 
939
                          proc._build_map, 'importfoo')
 
940
        self.assertRaises(errors.InvalidImportLine,
 
941
                          proc._build_map, 'fromimport')
 
942
 
 
943
    def test_name_collision(self):
 
944
        proc = lazy_import.ImportProcessor()
 
945
        proc._build_map('import foo')
 
946
 
 
947
        # All of these would try to create an object with the
 
948
        # same name as an existing object.
 
949
        self.assertRaises(errors.ImportNameCollision,
 
950
                          proc._build_map, 'import bar as foo')
 
951
        self.assertRaises(errors.ImportNameCollision,
 
952
                          proc._build_map, 'from foo import bar as foo')
 
953
        self.assertRaises(errors.ImportNameCollision,
 
954
                          proc._build_map, 'from bar import foo')
 
955
 
 
956
 
 
957
class TestLazyImportProcessor(ImportReplacerHelper):
 
958
 
 
959
    def test_root(self):
 
960
        try:
 
961
            root6
 
962
        except NameError:
 
963
            pass # root6 should not be defined yet
 
964
        else:
 
965
            self.fail('root6 was not supposed to exist yet')
 
966
 
 
967
        text = 'import %s as root6' % (self.root_name,)
 
968
        proc = lazy_import.ImportProcessor(InstrumentedImportReplacer)
 
969
        proc.lazy_import(scope=globals(), text=text)
 
970
 
 
971
        # So 'root6' should be a lazy import
 
972
        self.assertEqual(InstrumentedImportReplacer,
 
973
                         object.__getattribute__(root6, '__class__'))
 
974
 
 
975
        self.assertEqual(1, root6.var1)
 
976
        self.assertEqual('x', root6.func1('x'))
 
977
 
 
978
        self.assertEqual([('__getattribute__', 'var1'),
 
979
                          '_replace',
 
980
                          ('_import', 'root6'),
 
981
                          ('import', self.root_name, []),
 
982
                         ], self.actions)
 
983
 
 
984
    def test_import_deep(self):
 
985
        """Test import root.mod, root.sub.submoda, root.sub.submodb
 
986
        root should be a lazy import, with multiple children, who also
 
987
        have children to be imported.
 
988
        And when root is imported, the children should be lazy, and
 
989
        reuse the intermediate lazy object.
 
990
        """
 
991
        try:
 
992
            submoda7
 
993
        except NameError:
 
994
            pass # submoda7 should not be defined yet
 
995
        else:
 
996
            self.fail('submoda7 was not supposed to exist yet')
 
997
 
 
998
        text = """\
 
999
import %(root_name)s.%(sub_name)s.%(submoda_name)s as submoda7
 
1000
""" % self.__dict__
 
1001
        proc = lazy_import.ImportProcessor(InstrumentedImportReplacer)
 
1002
        proc.lazy_import(scope=globals(), text=text)
 
1003
 
 
1004
        # So 'submoda7' should be a lazy import
 
1005
        self.assertEqual(InstrumentedImportReplacer,
 
1006
                         object.__getattribute__(submoda7, '__class__'))
 
1007
 
 
1008
        # This should import submoda7
 
1009
        self.assertEqual(4, submoda7.var4)
 
1010
 
 
1011
        sub_path = self.root_name + '.' + self.sub_name
 
1012
        submoda_path = sub_path + '.' + self.submoda_name
 
1013
 
 
1014
        self.assertEqual([('__getattribute__', 'var4'),
 
1015
                          '_replace',
 
1016
                          ('_import', 'submoda7'),
 
1017
                          ('import', submoda_path, []),
 
1018
                         ], self.actions)
 
1019
 
 
1020
    def test_lazy_import(self):
 
1021
        """Smoke test that lazy_import() does the right thing"""
 
1022
        try:
 
1023
            root8
 
1024
        except NameError:
 
1025
            pass # root8 should not be defined yet
 
1026
        else:
 
1027
            self.fail('root8 was not supposed to exist yet')
 
1028
        lazy_import.lazy_import(globals(),
 
1029
                                'import %s as root8' % (self.root_name,),
 
1030
                                lazy_import_class=InstrumentedImportReplacer)
 
1031
 
 
1032
        self.assertEqual(InstrumentedImportReplacer,
 
1033
                         object.__getattribute__(root8, '__class__'))
 
1034
 
 
1035
        self.assertEqual(1, root8.var1)
 
1036
        self.assertEqual(1, root8.var1)
 
1037
        self.assertEqual(1, root8.func1(1))
 
1038
 
 
1039
        self.assertEqual([('__getattribute__', 'var1'),
 
1040
                          '_replace',
 
1041
                          ('_import', 'root8'),
 
1042
                          ('import', self.root_name, []),
 
1043
                         ], self.actions)