codetac 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ """Lines of Jinja templates, the Python equivalent of source maps.
2
+
3
+ The code Jinja compiles from a template has the template's path as its file
4
+ name, but its lines are those of the generated Python. The module of each
5
+ template keeps `debug_info`, pairs "template line=generated line", the table
6
+ Template.get_corresponding_lineno reads. The functions of the template (the
7
+ body `root`, each `block_*`, macros) are shown at their line of the .html.
8
+
9
+ When the table cannot be read, or gives a line the file does not have, the
10
+ template is a single opaque step, with a diagnostic: never a wrong line.
11
+ """
12
+ import os
13
+
14
+ from .project import project_path
15
+
16
+
17
+ def is_template(globals_):
18
+ """Whether a frame's globals are those of a compiled Jinja template."""
19
+ return '__jinja_template__' in globals_ or ('debug_info' in globals_ and 'environment' in globals_ and 'blocks' in globals_)
20
+
21
+
22
+ def template_file(code, globals_, root):
23
+ """The real path of the template, when it is a file of the project."""
24
+ filename = globals_.get('__file__') or code.co_filename
25
+ if not isinstance(filename, str) or filename.startswith('<'):
26
+ return None
27
+ try:
28
+ return project_path(os.path.abspath(filename), root, python=False)
29
+ except (OSError, ValueError):
30
+ return None
31
+
32
+
33
+ def read_table(text):
34
+ """"1=12&2=17" -> [(1, 12), (2, 17)]; None when it is not a table."""
35
+ if not isinstance(text, str):
36
+ return None
37
+ pairs = []
38
+ if text:
39
+ for item in text.split('&'):
40
+ template_line, _, code_line = item.partition('=')
41
+ if not template_line.isdigit() or not code_line.isdigit():
42
+ return None
43
+ pairs.append((int(template_line), int(code_line)))
44
+ return pairs
45
+
46
+
47
+ def label(code, template):
48
+ if code.co_name == 'root':
49
+ return 'template %s' % template
50
+ if code.co_name.startswith('block_'):
51
+ return 'bloco %s (%s)' % (code.co_name[len('block_'):], template)
52
+ return '%s (%s)' % (code.co_name, template)
53
+
54
+
55
+ def place(code, table, line_count):
56
+ """(line, endLine) in the template for a function of its compiled code, or None."""
57
+ first = code.co_firstlineno
58
+ # Only the lines this code runs itself: a macro defined inside the body of
59
+ # the template has lines of its own.
60
+ own = set(line for _, _, line in code.co_lines() if line is not None)
61
+ inside = [template_line for template_line, code_line in table if code_line in own]
62
+ at_def = [template_line for template_line, code_line in table if code_line == first]
63
+ if at_def:
64
+ line = at_def[0]
65
+ elif code.co_name == 'root':
66
+ # The body of the template starts at its first line; Jinja records no line for it.
67
+ line = 1
68
+ elif inside:
69
+ line = min(inside)
70
+ else:
71
+ return None
72
+ end = max(inside + [line])
73
+ if not 1 <= line <= end <= line_count:
74
+ return None
75
+ return line, end
76
+
77
+
78
+ def _up_to_date(globals_):
79
+ template = globals_.get('__jinja_template__')
80
+ try:
81
+ return bool(getattr(template, 'is_up_to_date', True))
82
+ except Exception:
83
+ return False
84
+
85
+
86
+ class Templates(object):
87
+ """What the capture needs to know about each template file, read once."""
88
+
89
+ def __init__(self, root):
90
+ self.root = root
91
+ self.counts = {} # real path -> number of lines, or None
92
+ self.failed = set() # templates announced as opaque
93
+
94
+ def line_count(self, path):
95
+ if path not in self.counts:
96
+ try:
97
+ with open(path, 'rb') as file:
98
+ data = file.read()
99
+ self.counts[path] = data.count(b'\n') + (0 if data.endswith(b'\n') or not data else 1)
100
+ except OSError:
101
+ self.counts[path] = None
102
+ return self.counts[path]
103
+
104
+ def describe(self, code, globals_):
105
+ """(metadata of the function, limitation event or None), or None when the code is not
106
+ a function of a project template."""
107
+ path = template_file(code, globals_, self.root)
108
+ if path is None:
109
+ return None
110
+ name = globals_.get('name') if isinstance(globals_.get('name'), str) else os.path.basename(path)
111
+ table = read_table(globals_.get('debug_info'))
112
+ count = self.line_count(path)
113
+ # A table with a line the file does not have is not this file's (the
114
+ # template changed on disk, or it is not Jinja's): nothing of it is used.
115
+ # Nor when Jinja itself says the file changed since it was compiled.
116
+ if table is not None and count and all(1 <= line <= count for line, _ in table) and _up_to_date(globals_):
117
+ found = place(code, table, count)
118
+ else:
119
+ found = None
120
+ if found is not None:
121
+ return {'function': label(code, name), 'file': path, 'line': found[0], 'endLine': found[1], 'column': None,
122
+ 'mapped': True, 'async': bool(code.co_flags & 0x0380)}, None
123
+ # Opaque: the body of the template is one step without lines; its other
124
+ # functions are not shown (they would need lines to be told apart).
125
+ if code.co_name != 'root':
126
+ return None
127
+ limitation = None
128
+ if path not in self.failed:
129
+ self.failed.add(path)
130
+ limitation = {'type': 'limitation', 'reason': 'template-lines-unmapped', 'file': path}
131
+ return {'function': 'template %s' % name, 'file': path, 'line': None, 'endLine': None, 'column': None,
132
+ 'mapped': False, 'async': False, 'opaque': True}, limitation