assignment-codeval 0.0.30__py3-none-any.whl → 0.0.32__py3-none-any.whl
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.
- assignment_codeval/ai_benchmark.py +12 -3
- assignment_codeval/convertMD2Html.py +192 -180
- assignment_codeval/create_assignment.py +18 -13
- assignment_codeval/evaluate.py +53 -59
- assignment_codeval/export_tests.py +2 -2
- assignment_codeval/file_utils.py +15 -10
- assignment_codeval/github_connect.py +63 -10
- assignment_codeval/recent_comments.py +6 -2
- assignment_codeval/submissions.py +298 -37
- assignment_codeval/test_template.html +76 -9
- assignment_codeval-0.0.32.dist-info/METADATA +242 -0
- assignment_codeval-0.0.32.dist-info/RECORD +21 -0
- {assignment_codeval-0.0.30.dist-info → assignment_codeval-0.0.32.dist-info}/WHEEL +1 -1
- assignment_codeval-0.0.30.dist-info/METADATA +0 -351
- assignment_codeval-0.0.30.dist-info/RECORD +0 -21
- {assignment_codeval-0.0.30.dist-info → assignment_codeval-0.0.32.dist-info}/entry_points.txt +0 -0
- {assignment_codeval-0.0.30.dist-info → assignment_codeval-0.0.32.dist-info}/top_level.txt +0 -0
|
@@ -75,8 +75,10 @@ def extract_assignment_from_codeval(codeval_path: str) -> tuple[str, str, str]:
|
|
|
75
75
|
with open(codeval_path, "r", encoding="utf-8") as f:
|
|
76
76
|
content = f.read()
|
|
77
77
|
|
|
78
|
-
# Extract content between CRT_HW START and CRT_HW END
|
|
79
|
-
|
|
78
|
+
# Extract content between ASSIGNMENT START / CRT_HW START and ASSIGNMENT END / CRT_HW END.
|
|
79
|
+
# The title after the marker may contain spaces (e.g. "CRT_HW START Place Boats")
|
|
80
|
+
# or be absent entirely, so match the rest of the marker line, not a single token.
|
|
81
|
+
match = re.search(r"(?:ASSIGNMENT START|CRT_HW START)[^\n]*\n(.*?)(?:ASSIGNMENT END|CRT_HW END)", content, re.DOTALL)
|
|
80
82
|
if match:
|
|
81
83
|
description = match.group(1).strip()
|
|
82
84
|
else:
|
|
@@ -223,10 +225,17 @@ def call_openai(model_id: str, prompt: str, api_key: str) -> Optional[str]:
|
|
|
223
225
|
try:
|
|
224
226
|
client = openai.OpenAI(api_key=api_key)
|
|
225
227
|
|
|
228
|
+
# Reasoning models (o1/o3/o4 families, gpt-5) reject `max_tokens` and
|
|
229
|
+
# require `max_completion_tokens` instead.
|
|
230
|
+
if model_id.startswith(("o1", "o3", "o4", "gpt-5")):
|
|
231
|
+
token_kwargs = {"max_completion_tokens": 8192}
|
|
232
|
+
else:
|
|
233
|
+
token_kwargs = {"max_tokens": 8192}
|
|
234
|
+
|
|
226
235
|
response = client.chat.completions.create(
|
|
227
236
|
model=model_id,
|
|
228
237
|
messages=[{"role": "user", "content": prompt}],
|
|
229
|
-
|
|
238
|
+
**token_kwargs,
|
|
230
239
|
)
|
|
231
240
|
|
|
232
241
|
return response.choices[0].message.content
|
|
@@ -1,180 +1,192 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import zipfile
|
|
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
|
-
if
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
elif line.startswith('
|
|
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
|
-
elif
|
|
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
|
-
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import zipfile
|
|
4
|
+
from html import escape as html_escape
|
|
5
|
+
|
|
6
|
+
import markdown
|
|
7
|
+
|
|
8
|
+
from assignment_codeval.commons import info, get_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_ANSI_STYLES = {
|
|
12
|
+
'1': 'font-weight:bold',
|
|
13
|
+
'4': 'text-decoration:underline',
|
|
14
|
+
'30': 'color:black', '31': 'color:red', '32': 'color:green',
|
|
15
|
+
'33': 'color:#a50', '34': 'color:blue', '35': 'color:magenta',
|
|
16
|
+
'36': 'color:cyan', '37': 'color:white',
|
|
17
|
+
'40': 'background-color:black', '41': 'background-color:red',
|
|
18
|
+
'42': 'background-color:green', '43': 'background-color:yellow',
|
|
19
|
+
'44': 'background-color:blue', '45': 'background-color:magenta',
|
|
20
|
+
'46': 'background-color:cyan', '47': 'background-color:white',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ansi_to_html(text):
|
|
25
|
+
"""Convert ANSI escape codes in text to HTML span tags.
|
|
26
|
+
|
|
27
|
+
The text parts are HTML-escaped so characters like < and & render
|
|
28
|
+
literally instead of being parsed as markup.
|
|
29
|
+
"""
|
|
30
|
+
parts = re.split(r'\x1b\[([\d;]*)m', text)
|
|
31
|
+
result = []
|
|
32
|
+
open_spans = 0
|
|
33
|
+
for i, part in enumerate(parts):
|
|
34
|
+
if i % 2 == 0:
|
|
35
|
+
result.append(html_escape(part, quote=False))
|
|
36
|
+
else:
|
|
37
|
+
codes = part.split(';') if part else ['0']
|
|
38
|
+
if '0' in codes:
|
|
39
|
+
result.append('</span>' * open_spans)
|
|
40
|
+
open_spans = 0
|
|
41
|
+
else:
|
|
42
|
+
styles = [_ANSI_STYLES[c] for c in codes if c in _ANSI_STYLES]
|
|
43
|
+
if styles:
|
|
44
|
+
result.append(f'<span style="{";".join(styles)}">')
|
|
45
|
+
open_spans += 1
|
|
46
|
+
result.append('</span>' * open_spans)
|
|
47
|
+
return ''.join(result)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _read_file_content(filename, spec_dir, zip_archives=None):
|
|
51
|
+
"""Read the content of a file referenced by an IF/OF/EF tag.
|
|
52
|
+
|
|
53
|
+
Searches local spec_dir first, then zip archives. Returns content as string.
|
|
54
|
+
"""
|
|
55
|
+
if spec_dir:
|
|
56
|
+
filepath = os.path.join(spec_dir, filename)
|
|
57
|
+
try:
|
|
58
|
+
with open(filepath, 'r') as f:
|
|
59
|
+
return f.read()
|
|
60
|
+
except (FileNotFoundError, OSError):
|
|
61
|
+
pass
|
|
62
|
+
if zip_archives:
|
|
63
|
+
for zf in zip_archives:
|
|
64
|
+
try:
|
|
65
|
+
return zf.read(filename).decode('utf-8')
|
|
66
|
+
except KeyError:
|
|
67
|
+
continue
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def sampleTestCases(listOfTC, numOfTC, spec_dir=None, zip_archives=None):
|
|
72
|
+
counter = 0
|
|
73
|
+
samples = "<pre><code>"
|
|
74
|
+
esc = lambda s: html_escape(s, quote=False)
|
|
75
|
+
for line in listOfTC:
|
|
76
|
+
if line.startswith('T ', 0, 2):
|
|
77
|
+
counter = counter + 1
|
|
78
|
+
if counter > numOfTC:
|
|
79
|
+
break
|
|
80
|
+
samples = samples + "\n" + "Command to RUN: " + esc(line[2:])
|
|
81
|
+
elif line.startswith('IF '):
|
|
82
|
+
filename = line[3:].strip()
|
|
83
|
+
content = _read_file_content(filename, spec_dir, zip_archives)
|
|
84
|
+
if content is not None:
|
|
85
|
+
samples = samples + "<span style=\"color:green\">" + ansi_to_html(content) + "</span>"
|
|
86
|
+
else:
|
|
87
|
+
samples = samples + "<span style=\"color:green\">Input from file: " + esc(filename) + "\n</span>"
|
|
88
|
+
elif line.startswith('IB '):
|
|
89
|
+
samples = samples + "<span style=\"color:green\">" + esc(line[3:]) + "</span>"
|
|
90
|
+
elif line.startswith('I '):
|
|
91
|
+
samples = samples + "<span style=\"color:green\">" + esc(line[2:]) + "</span>"
|
|
92
|
+
elif line.startswith('OF '):
|
|
93
|
+
filename = line[3:].strip()
|
|
94
|
+
content = _read_file_content(filename, spec_dir, zip_archives)
|
|
95
|
+
if content is not None:
|
|
96
|
+
samples = samples + "<span style=\"color:blue\">" + ansi_to_html(content) + "</span>"
|
|
97
|
+
else:
|
|
98
|
+
samples = samples + "<span style=\"color:blue\">Expected output from file: " + esc(filename) + "\n</span>"
|
|
99
|
+
elif line.startswith('OB '):
|
|
100
|
+
samples = samples + "<span style=\"color:blue\">" + esc(line[3:]) + "</span>"
|
|
101
|
+
elif line.startswith('O '):
|
|
102
|
+
samples = samples + "<span style=\"color:blue\">" + esc(line[2:]) + "</span>"
|
|
103
|
+
elif line.startswith('X '):
|
|
104
|
+
samples = samples + "Expected Exit Code: " + esc(line[2:])
|
|
105
|
+
elif line.startswith('EF '):
|
|
106
|
+
filename = line[3:].strip()
|
|
107
|
+
content = _read_file_content(filename, spec_dir, zip_archives)
|
|
108
|
+
if content is not None:
|
|
109
|
+
samples = samples + "<span style=\"color:Tomato\">" + ansi_to_html(content) + "</span>"
|
|
110
|
+
else:
|
|
111
|
+
samples = samples + "<span style=\"color:Tomato\">Expected error from file: " + esc(filename) + "\n</span>"
|
|
112
|
+
elif line.startswith('EB '):
|
|
113
|
+
samples = samples + "<span style=\"color:Tomato\">" + esc(line[3:]) + "</span>"
|
|
114
|
+
elif line.startswith('E '):
|
|
115
|
+
samples = samples + "<span style=\"color:Tomato\">" + esc(line[2:]) + "</span>"
|
|
116
|
+
else:
|
|
117
|
+
continue
|
|
118
|
+
samples = samples + "</code></pre>"
|
|
119
|
+
return samples
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def mdToHtml(file_name, files_resolver=None):
|
|
123
|
+
with open(file_name, 'r') as f:
|
|
124
|
+
text = ""
|
|
125
|
+
examples = []
|
|
126
|
+
assignment = ""
|
|
127
|
+
compile_command = ""
|
|
128
|
+
past_crt_hw = False
|
|
129
|
+
numOfSampleTC = 1
|
|
130
|
+
zip_paths = []
|
|
131
|
+
for line in f:
|
|
132
|
+
if 'ASSIGNMENT START' in line:
|
|
133
|
+
assignment_name = line[len('ASSIGNMENT START'):].strip()
|
|
134
|
+
elif 'CRT_HW START' in line:
|
|
135
|
+
assignment_name = line[len('CRT_HW START'):].strip()
|
|
136
|
+
elif 'ASSIGNMENT END' in line or 'CRT_HW END' in line:
|
|
137
|
+
assignment = text
|
|
138
|
+
past_crt_hw = True
|
|
139
|
+
elif line.startswith(('T ', 'I ', 'IB ', 'IF ', 'O ', 'OB ', 'OF ',
|
|
140
|
+
'EF ', 'X ', 'E ', 'EB ')):
|
|
141
|
+
examples.append(line)
|
|
142
|
+
elif line.startswith('HT '):
|
|
143
|
+
break
|
|
144
|
+
else:
|
|
145
|
+
if past_crt_hw and line.startswith('C '):
|
|
146
|
+
compile_command = line[2:].strip()
|
|
147
|
+
if past_crt_hw and line.startswith('Z '):
|
|
148
|
+
zip_paths.append(line[2:].strip())
|
|
149
|
+
exmpls_match = re.search(r'EXMPLS\s+([0-9]+)', line)
|
|
150
|
+
if exmpls_match:
|
|
151
|
+
numOfSampleTC = int(exmpls_match.group(1))
|
|
152
|
+
text = text + line
|
|
153
|
+
spec_dir = os.path.dirname(os.path.abspath(file_name))
|
|
154
|
+
|
|
155
|
+
# Open zip archives for IF/OF/EF file resolution
|
|
156
|
+
zip_archives = []
|
|
157
|
+
try:
|
|
158
|
+
for zp in zip_paths:
|
|
159
|
+
full_path = os.path.join(spec_dir, zp)
|
|
160
|
+
if os.path.exists(full_path):
|
|
161
|
+
zip_archives.append(zipfile.ZipFile(full_path, 'r'))
|
|
162
|
+
samples = sampleTestCases(examples, numOfSampleTC, spec_dir,
|
|
163
|
+
zip_archives)
|
|
164
|
+
finally:
|
|
165
|
+
for zf in zip_archives:
|
|
166
|
+
zf.close()
|
|
167
|
+
|
|
168
|
+
# Use a function replacement so backslash sequences in the sample data
|
|
169
|
+
# (e.g. "\d" or "\1" in expected output) are inserted literally rather
|
|
170
|
+
# than interpreted as regex escapes.
|
|
171
|
+
assignment = re.sub('EXMPLS [0-9]+', lambda m: samples, assignment)
|
|
172
|
+
if compile_command:
|
|
173
|
+
assignment = assignment.replace('COMPILE', compile_command)
|
|
174
|
+
# Handle FILE macros in two passes:
|
|
175
|
+
# 1. FILE inside markdown link syntax: [text](FILE[name]) -> [text](url)
|
|
176
|
+
assignment = re.sub(r'\]\(FILE\[([^]]+)]\)',
|
|
177
|
+
lambda m: f']({files_resolver(m.group(1))})' if files_resolver else f'](FILE[{m.group(1)}])',
|
|
178
|
+
assignment)
|
|
179
|
+
# 2. Standalone FILE macros: FILE[name] -> [name](url)
|
|
180
|
+
assignment = re.sub(r'FILE\[([^]]+)]',
|
|
181
|
+
lambda m: f'[{m.group(1)}]({files_resolver(m.group(1))})' if files_resolver else f'FILE[{m.group(1)}]',
|
|
182
|
+
assignment)
|
|
183
|
+
html = markdown.markdown(assignment,
|
|
184
|
+
extensions=['mdx_better_lists', 'extra'],
|
|
185
|
+
extension_configs={'mdx_better_lists': {'split_paragraph_lists': True}})
|
|
186
|
+
|
|
187
|
+
if get_config().dry_run:
|
|
188
|
+
html_file_name = file_name + '.html'
|
|
189
|
+
with open(html_file_name, 'w') as f:
|
|
190
|
+
f.write(html)
|
|
191
|
+
info(f'HTML preview created in the path : {html_file_name}')
|
|
192
|
+
return assignment_name, html
|
|
@@ -38,12 +38,14 @@ def get_assignment_subfolder(course, codeval_folder, assignment_name):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def extract_assignment_name(specname):
|
|
41
|
-
"""Extract the assignment name from the CRT_HW START line of a codeval spec file."""
|
|
41
|
+
"""Extract the assignment name from the ASSIGNMENT START (or CRT_HW START) line of a codeval spec file."""
|
|
42
42
|
with open(specname, 'r') as f:
|
|
43
43
|
for line in f:
|
|
44
|
+
if 'ASSIGNMENT START' in line:
|
|
45
|
+
return line[len('ASSIGNMENT START'):].strip()
|
|
44
46
|
if 'CRT_HW START' in line:
|
|
45
|
-
return line[
|
|
46
|
-
error(f"could not find
|
|
47
|
+
return line[len('CRT_HW START'):].strip()
|
|
48
|
+
error(f"could not find ASSIGNMENT START in {specname}")
|
|
47
49
|
exit(2)
|
|
48
50
|
|
|
49
51
|
|
|
@@ -53,9 +55,9 @@ def extract_file_macros(specname):
|
|
|
53
55
|
in_assignment = False
|
|
54
56
|
with open(specname, 'r') as f:
|
|
55
57
|
for line in f:
|
|
56
|
-
if 'CRT_HW START' in line:
|
|
58
|
+
if 'ASSIGNMENT START' in line or 'CRT_HW START' in line:
|
|
57
59
|
in_assignment = True
|
|
58
|
-
elif 'CRT_HW END' in line:
|
|
60
|
+
elif 'ASSIGNMENT END' in line or 'CRT_HW END' in line:
|
|
59
61
|
in_assignment = False
|
|
60
62
|
elif in_assignment:
|
|
61
63
|
# Find all FILE[filename] patterns in this line
|
|
@@ -93,7 +95,7 @@ def files_resolver(filename):
|
|
|
93
95
|
if not matches:
|
|
94
96
|
warn(f"could not find {filename} in {z}")
|
|
95
97
|
elif len(matches) > 1:
|
|
96
|
-
warn("found multiple matches for {filename} in {z}: {matches}")
|
|
98
|
+
warn(f"found multiple matches for {filename} in {z}: {matches}")
|
|
97
99
|
if matches:
|
|
98
100
|
zfname = matches[0]
|
|
99
101
|
with TemporaryDirectory("ce") as tmpdir:
|
|
@@ -113,7 +115,9 @@ def files_resolver(filename):
|
|
|
113
115
|
@click.option("--verbose/--no-verbose", default=False, show_default=True, help="Verbose actions")
|
|
114
116
|
@click.option("--group_name", default="Assignments", show_default=True,
|
|
115
117
|
help="Group name in which assignments needs to be created.")
|
|
116
|
-
|
|
118
|
+
@click.option("--active/--inactive", default=True, show_default=True,
|
|
119
|
+
help="Only look at active courses.")
|
|
120
|
+
def create_assignment(dryrun, verbose, course_name, group_name, specname, extra, active):
|
|
117
121
|
"""
|
|
118
122
|
Create the assignment in the given course.
|
|
119
123
|
"""
|
|
@@ -126,7 +130,7 @@ def create_assignment(dryrun, verbose, course_name, group_name, specname, extra)
|
|
|
126
130
|
(canvas, user) = connect_to_canvas()
|
|
127
131
|
|
|
128
132
|
try:
|
|
129
|
-
course = get_course(canvas, course_name)
|
|
133
|
+
course = get_course(canvas, course_name, is_active=active)
|
|
130
134
|
except Exception as e:
|
|
131
135
|
errorWithException(f'get_course api failed with following error : {e}')
|
|
132
136
|
else:
|
|
@@ -150,16 +154,17 @@ def create_assignment(dryrun, verbose, course_name, group_name, specname, extra)
|
|
|
150
154
|
canvas_folder = get_assignment_subfolder(course, codeval_folder, assign_name_early)
|
|
151
155
|
if extra:
|
|
152
156
|
upload_assignment_files(canvas_folder, extra)
|
|
153
|
-
# find zipfiles in spec
|
|
157
|
+
# find zipfiles in spec; resolve them relative to the spec directory so the
|
|
158
|
+
# command works when run from any working directory
|
|
159
|
+
spec_dir = os.path.dirname(os.path.abspath(specname))
|
|
154
160
|
with open(specname) as f:
|
|
155
161
|
for line in f:
|
|
156
162
|
if line.startswith("Z "):
|
|
157
|
-
|
|
158
|
-
if
|
|
159
|
-
zip_files.append(
|
|
163
|
+
zip_name = os.path.join(spec_dir, line[2:].strip())
|
|
164
|
+
if zip_name not in zip_files:
|
|
165
|
+
zip_files.append(zip_name)
|
|
160
166
|
|
|
161
167
|
# Extract FILE macros and upload local files that exist
|
|
162
|
-
spec_dir = os.path.dirname(os.path.abspath(specname))
|
|
163
168
|
file_macros = extract_file_macros(specname)
|
|
164
169
|
for filename in file_macros:
|
|
165
170
|
if filename not in file_dict:
|