chronaxis.configuration.diagnostics
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""为配置错误附上原文位置与有限的上下文;成功计划不保存此处的文档对象。23这里只消费已解析的原生值,不重新读取配置或来源。字段路径采用带引号键的4JSON路径;不存在字段回指最近所属对象,不能给缺少的值捏造行列。5"""6import json7import re89from .models import field_path_text101112def field_path_parts(text):13 """读取内部字段路径。参数: $开头文本。返回: tuple或无法定位时None。异常: 无。1415 引号键用标准JSON解码,不执行表达式;组合诊断如$.signals/derived不能冒充字段。16 """17 if not isinstance(text,str) or not text.startswith('$'):18 return None19 parts=[]20 position=121 while position<len(text):22 if text[position]=='.':23 match=re.match(r'[^.\[\]/]+',text[position+1:])24 if match is None:25 return None26 parts.append(match[0])27 position+=len(match[0])+128 elif text[position:position+2]=='["':29 try:30 key,end=json.JSONDecoder().raw_decode(text,position+1)31 except ValueError:32 return None33 if type(key) is not str or text[end:end+1]!=']':34 return None35 parts.append(key)36 position=end+137 else:38 match=re.match(r'\[(0|[1-9][0-9]*)\]',text[position:])39 if match is None:40 return None41 parts.append(int(match[1]))42 position+=len(match[0])43 return tuple(parts)444546def child_path(path,key):47 """追加一层真实键。参数: 字段路径/字符串键。返回: 无歧义路径。异常: 内部路径不合法。48 """49 return path+field_path_text((key,))[1:]505152def value_preview(value):53 """只显示有限值摘要。参数: JSON原生值。返回: 最多180字符。异常: 无。5455 容器仅报类型/大小,防止错误输出整份配置、内存行或自由元数据。56 """57 if type(value) is dict:58 return f'对象({len(value)}个字段)'59 if type(value) is list:60 return f'数组({len(value)}项)'61 text=json.dumps(value,ensure_ascii=False,allow_nan=False)62 return text if len(text)<=180 else text[:177]+'…'636465def locate_error(error,document):66 """给计划异常补充有限诊断字段。参数: 计划异常/原文档。返回: 同一异常。异常: 无。6768 不保存document、原字典或位置表;只复制源码名、单一位置、对象名和短值。69 """70 # 集合的每项分别通过导入来源表定位,不把首项位置套到整份报告。71 if getattr(error, '_diagnostics', None) is not None:72 for item in error.diagnostics:73 locate_error(item, document)74 first = error.diagnostics[0]75 for name in ('path','source','location','object_name','value_preview','hint'):76 setattr(error, name, getattr(first, name))77 error.refresh_text()78 return error79 # 导入器可能已经把异常定位到另一个文件;外层不得用主文件覆盖真实来源。80 if error.source is not None:return error81 parts=field_path_parts(error.path)82 error.source=document.source83 if parts is None:84 error.refresh_text()85 return error86 error.location=document.location_for(parts)87 current=document.data88 name=None89 labels={'signals':'信号','sources':'来源','rules':'规则','windows':'窗口',90 'calculations':'计算项','sequences':'序列','jobs':'任务','presets':'预设'}91 label=next((labels[part] for part in parts if part in labels),'对象')92 # 从根走向出错字段,最近的命名对象比根dataset更能解释该处声明。93 missing=False94 for part in (*parts,None):95 if type(current) is dict:96 candidate=current.get('name')97 if type(candidate) is str:98 name=candidate[:120]99 if part is None:100 break101 if type(current) is dict and type(part) is str and part in current:102 current=current[part]103 elif type(current) is list and type(part) is int and part<len(current):104 current=current[part]105 else:106 missing=True107 break108 error.object_name=None if name is None else label+' '+name109 error.value_preview='未提供' if missing else value_preview(current)110 origin=document.origin_for(parts)111 if origin is not None:112 error.source=origin.source113 error.path=field_path_text(origin.field_path)114 error.location=origin.location115 error.refresh_text()116 return error117