File: test_explicit_profile.py

package info (click to toggle)
python-line-profiler 5.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,256 kB
  • sloc: python: 8,119; sh: 810; ansic: 297; makefile: 14
file content (956 lines) | stat: -rw-r--r-- 30,626 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
import os
import re
import sys
import tempfile
from contextlib import ExitStack

import pytest
import ubelt as ub


class enter_tmpdir:
    """
    Set up a temporary directory and :cmd:`chdir` into it.
    """
    def __init__(self):
        self.stack = ExitStack()

    def __enter__(self):
        """
        Returns:
            curdir (ubelt.Path)
                Temporary directory :cmd:`chdir`-ed into.

        Side effects:
            ``curdir`` created and :cmd:`chdir`-ed into.
        """
        enter = self.stack.enter_context
        tmpdir = os.path.abspath(enter(tempfile.TemporaryDirectory()))
        enter(ub.ChDir(tmpdir))
        return ub.Path(tmpdir)

    def __exit__(self, *_, **__):
        """
        Side effects:
            * Original working directory restored.
            * Temporary directory created deleted.
        """
        self.stack.close()


class restore_sys_modules:
    """
    Restore :py:attr:`sys.modules` after exiting the context.
    """
    def __enter__(self):
        self.old = sys.modules.copy()

    def __exit__(self, *_, **__):
        sys.modules.clear()
        sys.modules.update(self.old)


def write(path, code=None):
    path.parent.mkdir(exist_ok=True, parents=True)
    if code is None:
        path.touch()
    else:
        path.write_text(ub.codeblock(code))


def test_simple_explicit_nonglobal_usage():
    """
    python -c "from test_explicit_profile import *; test_simple_explicit_nonglobal_usage()"
    """
    from line_profiler import LineProfiler
    profiler = LineProfiler()

    def func(a):
        return a + 1

    profiled_func = profiler(func)

    # Run Once
    profiled_func(1)

    lstats = profiler.get_stats()
    print(f'lstats.timings={lstats.timings}')
    print(f'lstats.unit={lstats.unit}')
    print(f'profiler.code_hash_map={profiler.code_hash_map}')
    profiler.print_stats()


def _demo_explicit_profile_script():
    return ub.codeblock(
        '''
        from line_profiler import profile

        @profile
        def fib(n):
            a, b = 0, 1
            for _ in range(n):
                a, b = b, a + b
            return a
        fib(10)
        ''')


def test_explicit_profile_with_nothing():
    """
    Test that no profiling happens when we dont request it.
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)
        with ub.ChDir(temp_dpath):

            script_fpath = ub.Path('script.py')
            script_fpath.write_text(_demo_explicit_profile_script())

            args = [sys.executable, os.fspath(script_fpath)]
            proc = ub.cmd(args)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        assert not (temp_dpath / 'profile_output.txt').exists()
        assert not (temp_dpath / 'profile_output.lprof').exists()


def test_explicit_profile_with_environ_on():
    """
    Test that explicit profiling is enabled when we specify the LINE_PROFILE
    enviornment variable.
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)
        env = os.environ.copy()
        env['LINE_PROFILE'] = '1'

        with ub.ChDir(temp_dpath):

            script_fpath = ub.Path('script.py')
            script_fpath.write_text(_demo_explicit_profile_script())

            args = [sys.executable, os.fspath(script_fpath)]
            proc = ub.cmd(args, env=env)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        assert (temp_dpath / 'profile_output.txt').exists()
        assert (temp_dpath / 'profile_output.lprof').exists()


def test_explicit_profile_with_environ_off():
    """
    When LINE_PROFILE is falsy, profiling should not run.
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)
        env = os.environ.copy()
        env['LINE_PROFILE'] = '0'

        with ub.ChDir(temp_dpath):

            script_fpath = ub.Path('script.py')
            script_fpath.write_text(_demo_explicit_profile_script())

            args = [sys.executable, os.fspath(script_fpath)]
            proc = ub.cmd(args)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        assert not (temp_dpath / 'profile_output.txt').exists()
        assert not (temp_dpath / 'profile_output.lprof').exists()


def test_explicit_profile_with_cmdline():
    """
    Test that explicit profiling is enabled when we specify the --line-profile
    command line flag.

    xdoctest ~/code/line_profiler/tests/test_explicit_profile.py test_explicit_profile_with_environ
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)
        with ub.ChDir(temp_dpath):

            script_fpath = ub.Path('script.py')
            script_fpath.write_text(_demo_explicit_profile_script())

            args = [sys.executable, os.fspath(script_fpath), '--line-profile']
            print(f'args={args}')
            proc = ub.cmd(args)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        assert (temp_dpath / 'profile_output.txt').exists()
        assert (temp_dpath / 'profile_output.lprof').exists()


@pytest.mark.parametrize('line_profile', [True, False])
def test_explicit_profile_with_kernprof(line_profile: bool):
    """
    Test that explicit profiling works when using kernprof. In this case
    we should get as many output files.
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)
        base_cmd = [sys.executable, '-m', 'kernprof']
        if line_profile:
            base_cmd.append('-l')
            outfile = 'script.py.lprof'
        else:
            outfile = 'script.py.prof'

        with ub.ChDir(temp_dpath):
            script_fpath = ub.Path('script.py')
            script_fpath.write_text(_demo_explicit_profile_script())
            args = base_cmd + [os.fspath(script_fpath)]
            proc = ub.cmd(args)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        assert not (temp_dpath / 'profile_output.txt').exists()
        assert (temp_dpath / outfile).exists()


@pytest.mark.parametrize('package', [True, False])
@pytest.mark.parametrize('builtin', [True, False])
def test_explicit_profile_with_kernprof_m(builtin: bool, package: bool):
    """
    Test that explicit (non-line) profiling works when using
    `kernprof -m` to run packages and/or submodules with relative
    imports.

    Parameters:
        builtin (bool)
            Whether to slip `@profile` into the globals with `--builtin`
            (true) or to require importing it from `line_profiler` in
            the profiled source code (false)

        package (bool)
            Whether to add the code to a package's `__main__.py` and
            `kernprof -m {<package>}` (true), or to add it to a
            submodule and `kernprof -m {<package>}.{<submodule>}`
            (false)
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)

        lib_code = ub.codeblock(
            '''
            @profile
            def func1(a):
                return a + 1

            @profile
            def func2(a):
                return a + 1

            def func3(a):
                return a + 1

            def func4(a):
                return a + 1
            ''').strip()
        if not builtin:
            lib_code = 'from line_profiler import profile\n' + lib_code
        target_code = ub.codeblock(
            '''
            from ._lib import func1, func2, func3, func4

            if __name__ == '__main__':
                func1(1)
                func2(1)
                func3(1)
                func4(1)
            ''').strip()

        if package:
            target_module = 'package'
            target_fname = '__main__.py'
        else:
            target_module = 'package.api'
            target_fname = 'api.py'

        args = ['kernprof', '-v', '-m', target_module]
        if builtin:
            args.insert(2, '--builtin')  # Insert before the `-m` flag

        if 'PYTHONPATH' in os.environ:
            python_path = '{}:{}'.format(os.environ['PYTHONPATH'], os.curdir)
        else:
            python_path = os.curdir
        env = {**os.environ, 'PYTHONPATH': python_path}

        with ub.ChDir(temp_dpath):
            package_dir = ub.Path('package').mkdir()

            lib_fpath = package_dir / '_lib.py'
            lib_fpath.write_text(lib_code)

            target_fpath = package_dir / target_fname
            target_fpath.write_text(target_code)

            (package_dir / '__init__.py').touch()

            proc = ub.cmd(args, env=env)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        # Note: in non-builtin mode, the entire script is profiled
        for func, profiled in [('func1', True), ('func2', True),
                               ('func3', not builtin), ('func4', not builtin)]:
            result = re.search(r'lib\.py:[0-9]+\({}\)'.format(func),
                               proc.stdout)
            assert bool(result) == profiled

        assert not (temp_dpath / 'profile_output.txt').exists()
        assert (temp_dpath / (target_module + '.prof')).exists()


def test_explicit_profile_with_in_code_enable():
    """
    Test that the user can enable the profiler explicitly from within their
    code.

    CommandLine:
        pytest tests/test_explicit_profile.py -s -k test_explicit_profile_with_in_code_enable
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)

        code = ub.codeblock(
            '''
            from line_profiler import profile
            import ubelt as ub
            print('')
            print('')
            print('start test')

            print('profile = {}'.format(ub.urepr(profile, nl=1)))
            print(f'profile._profile={profile._profile}')
            print(f'profile.enabled={profile.enabled}')

            @profile
            def func1(a):
                return a + 1

            profile.enable(output_prefix='custom_output')

            print('profile = {}'.format(ub.urepr(profile, nl=1)))
            print(f'profile._profile={profile._profile}')
            print(f'profile.enabled={profile.enabled}')

            @profile
            def func2(a):
                return a + 1

            print('func2 = {}'.format(ub.urepr(func2, nl=1)))

            profile.disable()

            @profile
            def func3(a):
                return a + 1

            profile.enable()

            @profile
            def func4(a):
                return a + 1

            func1(1)
            func2(1)
            func3(1)
            func4(1)

            profile._profile
            ''')
        with ub.ChDir(temp_dpath):

            script_fpath = ub.Path('script.py')
            script_fpath.write_text(code)

            args = [sys.executable, os.fspath(script_fpath)]
            proc = ub.cmd(args)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        print('Finished running script')

        output_fpath = (temp_dpath / 'custom_output.txt')
        raw_output = output_fpath.read_text()
        print(f'Contents of {output_fpath}')
        print(raw_output)

        assert 'Function: func1' not in raw_output
        assert 'Function: func2' in raw_output
        assert 'Function: func3' not in raw_output
        assert 'Function: func4' in raw_output

        assert output_fpath.exists()
        assert (temp_dpath / 'custom_output.lprof').exists()


def test_explicit_profile_with_duplicate_functions():
    """
    Test profiling duplicate functions with the explicit profiler

    CommandLine:
        pytest -sv tests/test_explicit_profile.py -k test_explicit_profile_with_duplicate_functions
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)

        code = ub.codeblock(
            '''
            from line_profiler import profile

            @profile
            def func1(a):
                return a + 1

            @profile
            def func2(a):
                return a + 1

            @profile
            def func3(a):
                return a + 1

            @profile
            def func4(a):
                return a + 1

            func1(1)
            func2(1)
            func3(1)
            func4(1)
            ''').strip()
        with ub.ChDir(temp_dpath):

            script_fpath = ub.Path('script.py')
            script_fpath.write_text(code)

            args = [sys.executable, os.fspath(script_fpath), '--line-profile']
            proc = ub.cmd(args)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        output_fpath = (temp_dpath / 'profile_output.txt')
        raw_output = output_fpath.read_text()
        print(raw_output)

        assert 'Function: func1' in raw_output
        assert 'Function: func2' in raw_output
        assert 'Function: func3' in raw_output
        assert 'Function: func4' in raw_output

        assert output_fpath.exists()
        assert (temp_dpath / 'profile_output.lprof').exists()


def test_explicit_profile_with_customized_config():
    """
    Test that explicit profiling can be configured with the appropriate
    TOML file.
    """
    with tempfile.TemporaryDirectory() as tmp:
        temp_dpath = ub.Path(tmp)

        env = os.environ.copy()
        env['PROFILE'] = '1'

        with ub.ChDir(temp_dpath):
            script_fpath = ub.Path('script.py')
            script_fpath.write_text(_demo_explicit_profile_script())
            toml = ub.Path('my_config.toml')
            toml.write_text(ub.codeblock('''
        [tool.line_profiler.setup]
        environ_flags = ['PROFILE']

        [tool.line_profiler.write]
        output_prefix = 'my_profiling_results'
        timestamped_text = false

        [tool.line_profiler.show]
        details = true
        summarize = false
            '''))

            env['LINE_PROFILER_RC'] = str(toml)
            args = [sys.executable, os.fspath(script_fpath)]
            proc = ub.cmd(args, env=env)
            print(proc.stdout)
            print(proc.stderr)
            proc.check_returncode()

        # Check the `write` config
        assert set(os.listdir(temp_dpath)) == {'script.py',
                                               'my_config.toml',
                                               'my_profiling_results.lprof',
                                               'my_profiling_results.txt'}
        # Check the `show` config
        assert '- fib' not in proc.stdout  # No summary
        assert 'Function: fib' in proc.stdout  # With details


@pytest.mark.parametrize('reset_enable_count', [True, False])
@pytest.mark.parametrize('wrap_class, wrap_module',
                         [(None, None), (False, True),
                          (True, False), (True, True)])
def test_profiler_add_methods(wrap_class, wrap_module, reset_enable_count):
    """
    Test the `wrap` argument for the
    `LineProfiler.add_class()`, `.add_module()`, and
    `.add_imported_function_or_module()` (added via
    `line_profiler.autoprofile.autoprofile.
    _extend_line_profiler_for_profiling_imports()`) methods.
    """
    script = ub.codeblock('''
        from line_profiler import LineProfiler
        from line_profiler.autoprofile.autoprofile import (
            _extend_line_profiler_for_profiling_imports as upgrade_profiler)

        import my_module_1
        from my_module_2 import Class
        from my_module_3 import func3


        profiler = LineProfiler()
        upgrade_profiler(profiler)
        # This dispatches to `.add_module()`
        profiler.add_imported_function_or_module(my_module_1{})
        # This dispatches to `.add_class()`
        profiler.add_imported_function_or_module(Class{})
        profiler.add_imported_function_or_module(func3)

        if {}:
            for _ in range(profiler.enable_count):
                profiler.disable_by_count()

        # `func1()` should only have timing info if `wrap_module`
        my_module_1.func1()
        # `method2()` should only have timing info if `wrap_class`
        Class.method2()
        # `func3()` is profiled but don't see any timing info because it
        # isn't wrapped and doesn't auto-`.enable()` before being called
        func3()
        profiler.print_stats(details=True, summarize=True)
                          '''.format(
        '' if wrap_module is None else f', wrap={wrap_module}',
        '' if wrap_class is None else f', wrap={wrap_class}',
        reset_enable_count))

    with enter_tmpdir() as curdir:
        write(curdir / 'script.py', script)
        write(curdir / 'my_module_1.py',
              '''
        def func1():
            pass  # Marker: func1
              ''')
        write(curdir / 'my_module_2.py',
              '''
        class Class:
            @classmethod
            def method2(cls):
                pass  # Marker: method2
              ''')
        write(curdir / 'my_module_3.py',
              '''
        def func3():
            pass  # Marker: func3
              ''')
        proc = ub.cmd([sys.executable, str(curdir / 'script.py')])

    # Check that the profiler has seen each of the methods
    raw_output = proc.stdout
    print(script)
    print(raw_output)
    print(proc.stderr)
    proc.check_returncode()
    assert '# Marker: func1' in raw_output
    assert '# Marker: method2' in raw_output
    assert '# Marker: func3' in raw_output

    # Check that the timing info (of the lack thereof) are correct
    for func, has_timing in [('func1', wrap_module), ('method2', wrap_class),
                             ('func3', False)]:
        line, = (line for line in raw_output.splitlines()
                 if line.endswith('Marker: ' + func))
        has_timing = has_timing or not reset_enable_count
        assert line.split()[1] == ('1' if has_timing else 'pass')


def test_profiler_add_class_recursion_guard():
    """
    Test that if we were to add a pair of classes which each of them
    has a reference to the other in its namespace, we don't end up in
    infinite recursion.
    """
    from line_profiler import LineProfiler

    class Class1:
        def method1(self):
            pass

        class ChildClass1:
            def child_method_1(self):
                pass

    class Class2:
        def method2(self):
            pass

        class ChildClass2:
            def child_method_2(self):
                pass

        OtherClass = Class1
        # A duplicate reference shouldn't affect profiling either
        YetAnotherClass = Class1

    # Add self/mutual references
    Class1.ThisClass = Class1
    Class1.OtherClass = Class2

    profile = LineProfiler()
    profile.add_class(Class1)
    assert len(profile.functions) == 4
    assert Class1.method1 in profile.functions
    assert Class2.method2 in profile.functions
    assert Class1.ChildClass1.child_method_1 in profile.functions
    assert Class2.ChildClass2.child_method_2 in profile.functions


def test_profiler_warn_unwrappable():
    """
    Test for warnings when using `LineProfiler.add_*(wrap=True)` with a
    namespace which doesn't allow attribute assignment.
    """
    from line_profiler import LineProfiler

    class ProblamticMeta(type):
        def __init__(cls, *args, **kwargs):
            super(ProblamticMeta, cls).__init__(*args, **kwargs)
            cls._initialized = True

        def __setattr__(cls, attr, value):
            if not getattr(cls, '_initialized', None):
                return super(ProblamticMeta, cls).__setattr__(attr, value)
            raise AttributeError(
                f'cannot set attribute on {type(cls)} instance')

    class ProblematicClass(metaclass=ProblamticMeta):
        def method(self):
            pass

    profile = LineProfiler()
    vanilla_method = ProblematicClass.method

    with pytest.warns(match=r"cannot wrap 1 attribute\(s\) of "
                      r"<class '.*\.ProblematicClass'> \(`\{attr: value\}`\): "
                      r"\{'method': <function .*\.method at 0x.*>\}"):
        # The method is added to the profiler, but we can't assign its
        # wrapper back into the class namespace
        assert profile.add_class(ProblematicClass, wrap=True) == 1

    assert ProblematicClass.method is vanilla_method


@pytest.mark.parametrize(
    ('scoping_policy', 'add_module_targets', 'add_class_targets'),
    [('exact', {}, {'class3_method'}),
     ('children',
      {'class2_method', 'child_class2_method'},
      {'class3_method', 'child_class3_method'}),
     ('descendants',
      {'class2_method', 'child_class2_method',
       'class3_method', 'child_class3_method'},
      {'class3_method', 'child_class3_method'}),
     ('siblings',
      {'class1_method', 'child_class1_method',
       'class2_method', 'child_class2_method',
       'class3_method', 'child_class3_method', 'other_class3_method'},
      {'class3_method', 'child_class3_method', 'other_class3_method'}),
     ('none',
      {'class1_method', 'child_class1_method',
       'class2_method', 'child_class2_method',
       'class3_method', 'child_class3_method', 'other_class3_method'},
      {'child_class1_method',
       'class3_method', 'child_class3_method', 'other_class3_method'})])
def test_profiler_class_scope_matching(monkeypatch,
                                       scoping_policy,
                                       add_module_targets,
                                       add_class_targets):
    """
    Test for the class-scope-matching strategies of the
    `LineProfiler.add_*()` methods.
    """
    with ExitStack() as stack:
        stack.enter_context(restore_sys_modules())
        curdir = stack.enter_context(enter_tmpdir())

        pkg_dir = curdir / 'packages' / 'my_pkg'
        write(pkg_dir / '__init__.py')
        write(pkg_dir / 'submod1.py',
              """
        class Class1:
            def class1_method(self):
                pass

            class ChildClass1:
                def child_class1_method(self):
                    pass
              """)
        write(pkg_dir / 'subpkg2' / '__init__.py',
              """
        from ..submod1 import Class1  # Import from a sibling
        from .submod3 import Class3  # Import descendant from a child


        class Class2:
            def class2_method(self):
                pass

            class ChildClass2:
                def child_class2_method(self):
                    pass

            BorrowedChildClass = Class1.ChildClass1  # Non-sibling class
              """)
        write(pkg_dir / 'subpkg2' / 'submod3.py',
              """
        from ..submod1 import Class1


        class Class3:
            def class3_method(self):
                pass

            class OtherChildClass3:
                def child_class3_method(self):
                    pass

            # Unrelated class
            BorrowedChildClass1 = Class1.ChildClass1

        class OtherClass3:
            def other_class3_method(self):
                pass

        # Sibling class
        Class3.BorrowedChildClass3 = OtherClass3
              """)
        monkeypatch.syspath_prepend(pkg_dir.parent)

        from my_pkg import subpkg2
        from line_profiler import LineProfiler

        policies = {'func': 'none', 'class': scoping_policy,
                    'module': 'exact'}  # Don't descend into submodules
        # Add a module
        profile = LineProfiler()
        profile.add_module(subpkg2, scoping_policy=policies)
        assert len(profile.functions) == len(add_module_targets)
        added = {func.__name__ for func in profile.functions}
        assert added == set(add_module_targets)
        # Add a class
        profile = LineProfiler()
        profile.add_class(subpkg2.Class3, scoping_policy=policies)
        assert len(profile.functions) == len(add_class_targets)
        added = {func.__name__ for func in profile.functions}
        assert added == set(add_class_targets)


@pytest.mark.parametrize(
    ('scoping_policy', 'add_module_targets', 'add_subpackage_targets'),
    [('exact', {'func4'}, {'class_method'}),
     ('children', {'func4'}, {'class_method', 'func2'}),
     ('descendants', {'func4'}, {'class_method', 'func2'}),
     ('siblings', {'func4'}, {'class_method', 'func2', 'func3'}),
     ('none',
      {'func4', 'func5'},
      {'class_method', 'func2', 'func3', 'func4', 'func5'})])
def test_profiler_module_scope_matching(monkeypatch,
                                        scoping_policy,
                                        add_module_targets,
                                        add_subpackage_targets):
    """
    Test for the module-scope-matching strategies of the
    `LineProfiler.add_*()` methods.
    """
    with ExitStack() as stack:
        stack.enter_context(restore_sys_modules())
        curdir = stack.enter_context(enter_tmpdir())

        pkg_dir = curdir / 'packages' / 'my_pkg'
        write(pkg_dir / '__init__.py')
        write(pkg_dir / 'subpkg1' / '__init__.py',
              """
              import my_mod4  # Unrelated
              from .. import submod3  # Sibling
              from . import submod2  # Child


              class Class:
                  @classmethod
                  def class_method(cls):
                      pass

                  # We shouldn't descend into this no matter what
                  import my_mod5 as module
              """)
        write(pkg_dir / 'subpkg1' / 'submod2.py',
              """
              def func2():
                  pass
              """)
        write(pkg_dir / 'submod3.py',
              """
              def func3():
                  pass
              """)
        write(curdir / 'packages' / 'my_mod4.py',
              """
              import my_mod5  # Unrelated


              def func4():
                  pass
              """)
        write(curdir / 'packages' / 'my_mod5.py',
              """
              def func5():
                  pass
              """)
        monkeypatch.syspath_prepend(pkg_dir.parent)

        import my_mod4
        from my_pkg import subpkg1
        from line_profiler import LineProfiler

        policies = {'func': 'none', 'class': 'children',
                    'module': scoping_policy}
        # Add a module
        profile = LineProfiler()
        profile.add_module(my_mod4, scoping_policy=policies)
        assert len(profile.functions) == len(add_module_targets)
        added = {func.__name__ for func in profile.functions}
        assert added == set(add_module_targets)
        # Add a subpackage
        profile = LineProfiler()
        profile.add_module(subpkg1, scoping_policy=policies)
        assert len(profile.functions) == len(add_subpackage_targets)
        added = {func.__name__ for func in profile.functions}
        assert added == set(add_subpackage_targets)
        # Add a class
        profile = LineProfiler()
        profile.add_class(subpkg1.Class, scoping_policy=policies)
        assert [func.__name__ for func in profile.functions] == ['class_method']


@pytest.mark.parametrize(
    ('scoping_policy', 'add_module_targets', 'add_class_targets'),
    [('exact', {'func1'}, {'method'}),
     ('children', {'func1'}, {'method'}),
     ('descendants', {'func1', 'func2'}, {'method', 'child_class_method'}),
     ('siblings',
      {'func1', 'func2', 'func3'},
      {'method', 'child_class_method', 'func1'}),
     ('none',
      {'func1', 'func2', 'func3', 'func4'},
      {'method', 'child_class_method', 'func1', 'another_func4'})])
def test_profiler_func_scope_matching(monkeypatch,
                                      scoping_policy,
                                      add_module_targets,
                                      add_class_targets):
    """
    Test for the class-scope-matching strategies of the
    `LineProfiler.add_*()` methods.
    """
    with ExitStack() as stack:
        stack.enter_context(restore_sys_modules())
        curdir = stack.enter_context(enter_tmpdir())

        pkg_dir = curdir / 'packages' / 'my_pkg'
        write(pkg_dir / '__init__.py')
        write(pkg_dir / 'subpkg1' / '__init__.py',
              """
              from ..submod3 import func3  # Sibling
              from .submod2 import func2  # Descendant
              from my_mod4 import func4  # Unrelated

              def func1():
                  pass

              class Class:
                  def method(self):
                      pass

                  class ChildClass:
                      @classmethod
                      def child_class_method(cls):
                          pass

                  # Descendant
                  descdent_method = ChildClass.child_class_method

                  # Sibling
                  sibling_method = staticmethod(func1)

                  # Unrelated
                  from my_mod4 import another_func4 as imported_method
              """)
        write(pkg_dir / 'subpkg1' / 'submod2.py',
              """
              def func2():
                  pass
              """)
        write(pkg_dir / 'submod3.py',
              """
              def func3():
                  pass
              """)
        write(curdir / 'packages' / 'my_mod4.py',
              """
              def func4():
                  pass


              def another_func4(_):
                  pass
              """)
        monkeypatch.syspath_prepend(pkg_dir.parent)

        from my_pkg import subpkg1
        from line_profiler import LineProfiler

        policies = {'func': scoping_policy,
                    # No descensions
                    'class': 'exact', 'module': 'exact'}
        # Add a module
        profile = LineProfiler()
        profile.add_module(subpkg1, scoping_policy=policies)
        assert len(profile.functions) == len(add_module_targets)
        added = {func.__name__ for func in profile.functions}
        assert added == set(add_module_targets)
        # Add a class
        profile = LineProfiler()
        profile.add_module(subpkg1.Class, scoping_policy=policies)
        assert len(profile.functions) == len(add_class_targets)
        added = {func.__name__ for func in profile.functions}
        assert added == set(add_class_targets)


if __name__ == '__main__':
    ...
    test_simple_explicit_nonglobal_usage()