File: test_cython.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 (131 lines) | stat: -rw-r--r-- 4,898 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
"""
Tests for profiling Cython code.
"""
import math
import os
import subprocess
import sys
from importlib import reload, import_module
from importlib.util import find_spec
from io import StringIO
from operator import methodcaller
from pathlib import Path
from types import ModuleType
from typing import Generator, Tuple
from uuid import uuid4

import pytest

from line_profiler._line_profiler import (
    CANNOT_LINE_TRACE_CYTHON, find_cython_source_file)
from line_profiler.line_profiler import get_code_block, LineProfiler


def propose_name(prefix: str) -> Generator[str, None, None]:
    while True:
        yield '_'.join([prefix, *str(uuid4()).split('-')])


def _install_cython_example(
        tmp_path_factory: pytest.TempPathFactory,
        editable: bool) -> Generator[Tuple[Path, str], None, None]:
    """
    Install the example Cython module in a name-clash-free manner.
    """
    source = Path(__file__).parent / 'cython_example'
    assert source.is_dir()
    module = next(name for name in propose_name('cython_example')
                  if not find_spec(name))
    replace = methodcaller('replace', 'cython_example', module)
    pip = [sys.executable, '-m', 'pip']
    tmp_path = tmp_path_factory.mktemp('cython_example')
    # Replace all references to `cython_example` with the actual module
    # name and write to the tempdir
    for prefix, _, files in os.walk(source):
        dir_in = Path(prefix)
        for fname in files:
            _, ext = os.path.splitext(fname)
            if ext not in {'.py', '.pyx', '.pxd', '.toml'}:
                continue
            dir_out = tmp_path.joinpath(*(
                replace(part) for part in dir_in.relative_to(source).parts))
            dir_out.mkdir(exist_ok=True)
            file_in = dir_in / fname
            file_out = dir_out / replace(fname)
            file_out.write_text(replace(file_in.read_text()))
    # There should only be one Cython source file
    cython_source, = tmp_path.glob('*.pyx')
    pip_install = pip + ['install', '--verbose']
    if editable:
        pip_install += ['--editable', str(tmp_path)]
    else:
        pip_install.append(str(tmp_path))
    try:
        subprocess.run(pip_install).check_returncode()
        subprocess.run(pip + ['list']).check_returncode()
        yield cython_source, module
    finally:
        pip_uninstall = pip + ['uninstall', '--verbose', '--yes', module]
        subprocess.run(pip_uninstall).check_returncode()


@pytest.fixture(scope='module')
def cython_example(
        tmp_path_factory: pytest.TempPathFactory) -> Tuple[Path, ModuleType]:
    """
    Install the example Cython module, yield the path to the Cython
    source file and the corresponding module, uninstall it at teardown.
    """
    # With editable installs, we need to refresh `sys.meta_path` before
    # the installed module is available
    for path, mod_name in _install_cython_example(tmp_path_factory, True):
        reload(import_module('site'))
        yield (path, import_module(mod_name))


def test_recover_cython_source(cython_example: Tuple[Path, str]) -> None:
    """
    Check that Cython sources are correctly located by
    `line_profiler._line_profiler.find_cython_source_file()` and
    `line_profiler.line_profiler.get_code_block()`.
    """
    expected_source, module = cython_example
    for func in module.cos, module.sin:
        source = find_cython_source_file(func)
        assert source
        assert expected_source.samefile(source)
        source_lines = get_code_block(source, func.__code__.co_firstlineno)
        for line, prefix in [(source_lines[0], '# Start: '),
                             (source_lines[-1], '# End: ')]:
            assert line.rstrip('\n').endswith(prefix + func.__name__)


@pytest.mark.skipif(
    CANNOT_LINE_TRACE_CYTHON,
    reason='Cannot line-trace Cython code in version '
    + '.'.join(str(v) for v in sys.version_info[:3]))
def test_profile_cython_source(cython_example: Tuple[Path, str]) -> None:
    """
    Check that calls to Cython functions (built with the appropriate
    compile-time options) can be profiled.
    """
    prof_cos = LineProfiler()
    prof_sin = LineProfiler()

    cos = prof_cos(cython_example[1].cos)
    sin = prof_sin(cython_example[1].sin)
    assert pytest.approx(cos(.125, 10)) == math.cos(.125)
    assert pytest.approx(sin(2.5, 3)) == 2.5 - 2.5 ** 3 / 6 + 2.5 ** 5 / 120

    for prof, func, expected_nhits in [
            (prof_cos, 'cos', 10), (prof_sin, 'sin', 3)]:
        with StringIO() as fobj:
            prof.print_stats(fobj)
            result = fobj.getvalue()
            print(result)
        assert ('Function: ' + func) in result
        nhits = 0
        for line in result.splitlines():
            if all(chunk in line for chunk in ('result', '+', 'last_term')):
                nhits += int(line.split()[1])
        assert nhits == expected_nhits