forked from QuantConnect/Lean
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun_syntax_check.py
More file actions
159 lines (132 loc) · 7.09 KB
/
run_syntax_check.py
File metadata and controls
159 lines (132 loc) · 7.09 KB
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
import os
import sys
import time
import tempfile
import re
from pathlib import Path
from subprocess import run
from multiprocessing import Pool, Lock, freeze_support
target_files = []
lock = None
start_time = time.time()
# to resolve imports of other algorithms
expanded_envs = os.environ.copy()
expanded_envs["MYPYPATH"] = os.path.join(os.getcwd(), "./Algorithm.Python")
def init_pool(l):
global lock
lock = l
def log(message: str):
print(message)
with open('log.txt', "a") as file:
file.write(message)
def sync_log(message: str):
with lock:
log(message)
for subdir, dirs, files_in_folder in os.walk("./Algorithm.Python"):
for file_name in files_in_folder:
file_path = subdir + os.sep + file_name
if file_path.endswith(".py"):
target_files.append(file_path)
target_files.sort()
def adjust_file_contents(target_file: str):
try:
file = Path(target_file)
file_content = file.read_text(encoding='utf-8')
adjusted_import = 'from AlgorithmImports import *;from datetime import date, time, datetime, timedelta;import pandas as pd;import numpy as np;import math;import json;import os;'
tmp_file = tempfile.NamedTemporaryFile(prefix=f"{file.name}_", delete=False)
Path(tmp_file.name).write_text("# mypy: disable-error-code=\"no-redef\"\n" + file_content.replace("from AlgorithmImports import *", adjusted_import), encoding='utf-8')
return tmp_file
except:
import traceback
sync_log(f"{target_file} failed An exception occurred: {traceback.format_exc()}")
return None
specific_order_attributes = ['limit_price', 'trigger_price', 'trigger_touched', 'stop_price', 'stop_triggered', 'trailing_amount', 'trailing_as_percentage']
specific_ibase_data_attributes = ['is_fill_forward', 'volume', 'open', 'high', 'low', 'close', 'bid', 'bid_size', 'ask', 'ask_size', 'last_bid_size', 'last_ask_size', 'bid_price', 'ask_price', 'last_price', 'period', 'tick_type', 'quantity', 'exchange_code', 'exchange', 'sale_condition', 'parsed_sale_condition', 'suspicious', 'update']
specific_indicator_attributes = ['is_ready', 'samples', 'name', 'current', 'update', 'reset', 'updated']
def should_ignore(line: str, prev_line_ignored: bool) -> bool:
result = any(to_ignore in line for to_ignore in (
# this (None and object) is just noise the variable was initialized with None or mypy might not be able to resolve base class in some cases
'None',
'"object"',
'Name "datetime" is not defined',
'Name "np" is not defined',
'Name "pd" is not defined',
'Name "math" is not defined',
'Name "time" is not defined',
'Name "json" is not defined',
'Name "timedelta" is not defined',
'be derived from BaseException',
'Argument 1 of "update" is incompatible with supertype "IndicatorBase"; supertype defines the argument type as "IBaseData"',
'Module has no attribute "JsonConvert"',
'Too many arguments for "update" of "IndicatorBase"',
'Signature of "update" incompatible with supertype "IndicatorBase"',
'has incompatible type "Symbol"; expected "str"',
# This methods take an indicator and consolidator which might be instances of custom
# indicator/consolidator Python classes that don't inherit from PythonIndicator or IDataConsolidator
'No overload variant of "register_indicator" of "QCAlgorithm" matches argument types',
'No overload variant of "warm_up_indicator" of "QCAlgorithm" matches argument types'
))
if result or ('note: ' in line and prev_line_ignored):
return True
# Ignore accessing specific order types properties
order_attributes_match = re.search(r'error: "Order" has no attribute "([^"]+)"', line)
if order_attributes_match and order_attributes_match.group(1) in specific_order_attributes:
return True
# Ignore accessing specific properties of common data types derived from IBaseData, like Tick, TradeBar and QoteBar
base_data_attributes_match = re.search(r'error: "IBaseData" has no attribute "([^"]+)"', line)
if base_data_attributes_match and base_data_attributes_match.group(1) in specific_ibase_data_attributes:
return True
# Ignore accessing indicator properties. Useful for instance when adding indicators of different types
# to a list and then iterating over them, the common type will be IIndicatorWarmUpPeriodProvider
indicator_attributes_match = re.search(r'error: "IIndicatorWarmUpPeriodProvider" has no attribute "([^"]+)"', line)
if indicator_attributes_match and indicator_attributes_match.group(1) in specific_indicator_attributes:
return True
# Ignore accessing specific properties of some models, just to reduce noise in regression algorithms asserting internal stuff.
# We don't expect users to be accessing properties of models like this in most cases
if re.search('error: "(IBuyingPowerModel)|(IBenchmark)|(IMarginInterestRateModel)" has no attribute "([^"]+)"', line):
return True
# In some cases Python developers use the same variable and redefine it, this is not a problem in Python but mypy doesn't like it
if re.search(r'error: Incompatible types in assignment \(expression has type "([^"]+)", variable has type "([^"]+)"\)', line):
return True
return False
def run_syntax_check(target_file: str):
tmp_file = adjust_file_contents(target_file)
if not tmp_file:
return False
try:
algorithm_result = run([sys.executable, "-m", "mypy", "--skip-cache-mtime-checks", "--skip-version-check", "--show-error-codes",
"--no-error-summary", "--no-color-output", "--ignore-missing-imports", "--check-untyped-defs", tmp_file.name], capture_output=True, text=True, env=expanded_envs)
output = ''
if algorithm_result.stderr:
output += algorithm_result.stderr
if algorithm_result.stdout:
output += algorithm_result.stdout
filtered_output = ''
prev_line_ignored = False
for line in output.splitlines():
ignored = not line.startswith(tmp_file.name) or should_ignore(line, prev_line_ignored)
if not ignored:
filtered_output += f"{line}\n"
prev_line_ignored = ignored
if filtered_output:
sync_log(filtered_output)
return False
return True
except:
import traceback
sync_log(f"{target_file} failed An exception occurred: {traceback.format_exc()}")
finally:
tmp_file.close()
os.unlink(tmp_file.name)
return False
if __name__ == '__main__':
freeze_support()
with Pool(12, initializer=init_pool, initargs=(Lock(),)) as pool:
if len(sys.argv) > 1:
target_files = [target for target in target_files if sys.argv[1] in target]
result = pool.map(run_syntax_check, target_files)
log(f"ALGOS: {target_files}")
log(str(result))
success_rate = round((sum(result) / len(result)) * 100, 1)
log(f"SUCCESS RATE {success_rate}% took {time.time() - start_time}s")
exit(0 if success_rate >= 98.6 else 1)