F: Update pyconcept and implement improved typechecking
Some checks failed
Frontend CI / build (22.x) (push) Waiting to run
Backend CI / build (3.12) (push) Has been cancelled

This commit is contained in:
Ivan 2024-09-22 20:25:36 +03:00
parent 7f88d030d4
commit 35bfc86533
16 changed files with 147 additions and 145 deletions

View File

@ -107,6 +107,7 @@ class LibraryItem(Model):
verbose_name = 'Схема' verbose_name = 'Схема'
verbose_name_plural = 'Схемы' verbose_name_plural = 'Схемы'
# pylint: disable=invalid-str-returned
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.alias}' return f'{self.alias}'

View File

@ -400,7 +400,7 @@ class OperationSchema:
if child_schema.change_cst_type(successor_id, ctype): if child_schema.change_cst_type(successor_id, ctype):
self._cascade_change_cst_type(child_id, successor_id, ctype) self._cascade_change_cst_type(child_id, successor_id, ctype)
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments, too-many-positional-arguments
def _cascade_update_cst( def _cascade_update_cst(
self, self,
operation: int, operation: int,

View File

@ -2,6 +2,7 @@
from .basics import ( from .basics import (
ASTNodeSerializer, ASTNodeSerializer,
ConstituentaCheckSerializer,
ExpressionParseSerializer, ExpressionParseSerializer,
ExpressionSerializer, ExpressionSerializer,
InheritanceDataSerializer, InheritanceDataSerializer,

View File

@ -10,6 +10,13 @@ class ExpressionSerializer(serializers.Serializer):
expression = serializers.CharField() expression = serializers.CharField()
class ConstituentaCheckSerializer(serializers.Serializer):
''' Serializer: RSLang expression. '''
alias = serializers.CharField()
definition_formal = serializers.CharField(allow_blank=True)
cst_type = serializers.CharField()
class WordFormSerializer(serializers.Serializer): class WordFormSerializer(serializers.Serializer):
''' Serializer: inflect request. ''' ''' Serializer: inflect request. '''
text = serializers.CharField() text = serializers.CharField()
@ -85,6 +92,7 @@ class ASTNodeSerializer(serializers.Serializer):
class ExpressionParseSerializer(serializers.Serializer): class ExpressionParseSerializer(serializers.Serializer):
''' Serializer: RSlang expression parse result. ''' ''' Serializer: RSlang expression parse result. '''
parseResult = serializers.BooleanField() parseResult = serializers.BooleanField()
prefixLen = serializers.IntegerField()
syntax = serializers.CharField() syntax = serializers.CharField()
typification = serializers.CharField() typification = serializers.CharField()
valueClass = serializers.CharField() valueClass = serializers.CharField()

View File

@ -113,8 +113,8 @@ class TestRSFormViewset(EndpointTester):
self.executeForbidden(item=self.private_id) self.executeForbidden(item=self.private_id)
@decl_endpoint('/api/rsforms/{item}/check', method='post') @decl_endpoint('/api/rsforms/{item}/check-expression', method='post')
def test_check(self): def test_check_expression(self):
self.owned.insert_new('X1') self.owned.insert_new('X1')
data = {'expression': 'X1=X1'} data = {'expression': 'X1=X1'}
response = self.executeOK(data=data, item=self.owned_id) response = self.executeOK(data=data, item=self.owned_id)
@ -127,6 +127,24 @@ class TestRSFormViewset(EndpointTester):
self.executeOK(data=data, item=self.unowned_id) self.executeOK(data=data, item=self.unowned_id)
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
def test_check_constituenta(self):
self.owned.insert_new('X1')
data = {'definition_formal': 'X1=X1', 'alias': 'A111', 'cst_type': CstType.AXIOM}
response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], 'math')
self.assertEqual(response.data['astText'], '[:==[A111][=[X1][X1]]]')
self.assertEqual(response.data['typification'], 'LOGIC')
self.assertEqual(response.data['valueClass'], 'value')
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
def test_check_constituenta_error(self):
self.owned.insert_new('X1')
data = {'definition_formal': 'X1=X1', 'alias': 'D111', 'cst_type': CstType.TERM}
response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], False)
@decl_endpoint('/api/rsforms/{item}/resolve', method='post') @decl_endpoint('/api/rsforms/{item}/resolve', method='post')
def test_resolve(self): def test_resolve(self):
x1 = self.owned.insert_new( x1 = self.owned.insert_new(

View File

@ -56,7 +56,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
'details', 'details',
'export_trs', 'export_trs',
'resolve', 'resolve',
'check' 'check_expression',
'check_constituenta'
]: ]:
permission_list = [permissions.ItemAnyone] permission_list = [permissions.ItemAnyone]
else: else:
@ -424,8 +425,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
}, },
) )
@action(detail=True, methods=['post'], url_path='check') @action(detail=True, methods=['post'], url_path='check-expression')
def check(self, request: Request, pk) -> HttpResponse: def check_expression(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Check RSLang expression against schema context. ''' ''' Endpoint: Check RSLang expression against schema context. '''
serializer = s.ExpressionSerializer(data=request.data) serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -437,6 +438,31 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
data=json.loads(result) data=json.loads(result)
) )
@extend_schema(
summary='check expression for specific CstType',
tags=['RSForm', 'FormalLanguage'],
request=s.ConstituentaCheckSerializer,
responses={
c.HTTP_200_OK: s.ExpressionParseSerializer,
c.HTTP_404_NOT_FOUND: None
},
)
@action(detail=True, methods=['post'], url_path='check-constituenta')
def check_constituenta(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Check RSLang expression against schema context. '''
serializer = s.ConstituentaCheckSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['definition_formal']
alias = serializer.validated_data['alias']
cst_type = cast(m.CstType, serializer.validated_data['cst_type'])
pySchema = s.PyConceptAdapter(m.RSForm(self.get_object()))
result = pyconcept.check_constituenta(json.dumps(pySchema.data), alias, expression, cst_type)
return Response(
status=c.HTTP_200_OK,
data=json.loads(result)
)
@extend_schema( @extend_schema(
summary='resolve text with references', summary='resolve text with references',
tags=['RSForm', 'NaturalLanguage'], tags=['RSForm', 'NaturalLanguage'],

View File

@ -8,13 +8,12 @@ drf-spectacular-sidecar==2024.7.1
coreapi==2.3.3 coreapi==2.3.3
django-rest-passwordreset==1.4.1 django-rest-passwordreset==1.4.1
cctext==0.1.4 cctext==0.1.4
pyconcept==0.1.6 pyconcept==0.1.8
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
gunicorn==23.0.0 gunicorn==23.0.0
djangorestframework-stubs==3.15.1 djangorestframework-stubs==3.15.1
django-extensions==3.2.3 django-extensions==3.2.3
mypy==1.11.2 mypy==1.11.2
pylint==3.2.7 pylint==3.3.0
coverage==7.6.1 coverage==7.6.1

View File

@ -8,7 +8,7 @@ drf-spectacular-sidecar==2024.7.1
coreapi==2.3.3 coreapi==2.3.3
django-rest-passwordreset==1.4.1 django-rest-passwordreset==1.4.1
cctext==0.1.4 cctext==0.1.4
pyconcept==0.1.6 pyconcept==0.1.8
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
gunicorn==23.0.0 gunicorn==23.0.0

View File

@ -5,6 +5,7 @@
import { ILibraryCreateData, ILibraryItem } from '@/models/library'; import { ILibraryCreateData, ILibraryItem } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss'; import { ICstSubstituteData } from '@/models/oss';
import { import {
ICheckConstituentaData,
IConstituentaList, IConstituentaList,
IConstituentaMeta, IConstituentaMeta,
ICstCreateData, ICstCreateData,
@ -18,7 +19,7 @@ import {
IRSFormUploadData, IRSFormUploadData,
ITargetCst ITargetCst
} from '@/models/rsform'; } from '@/models/rsform';
import { IExpressionParse, IRSExpression } from '@/models/rslang'; import { IExpressionParse } from '@/models/rslang';
import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull } from './apiTransport'; import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull } from './apiTransport';
@ -113,9 +114,12 @@ export function patchMoveConstituenta(schema: string, request: FrontExchange<ICs
}); });
} }
export function postCheckExpression(schema: string, request: FrontExchange<IRSExpression, IExpressionParse>) { export function postCheckConstituenta(
schema: string,
request: FrontExchange<ICheckConstituentaData, IExpressionParse>
) {
AxiosPost({ AxiosPost({
endpoint: `/api/rsforms/${schema}/check`, endpoint: `/api/rsforms/${schema}/check-constituenta`,
request: request request: request
}); });
} }

View File

@ -0,0 +1,40 @@
'use client';
import { useCallback, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { postCheckConstituenta } from '@/backend/rsforms';
import { type ErrorData } from '@/components/info/InfoError';
import { ICheckConstituentaData, IConstituenta, type IRSForm } from '@/models/rsform';
import { IExpressionParse } from '@/models/rslang';
function useCheckConstituenta({ schema }: { schema?: IRSForm }) {
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<ErrorData>(undefined);
const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined);
const resetParse = useCallback(() => setParseData(undefined), []);
function checkConstituenta(expression: string, activeCst: IConstituenta, onSuccess?: DataCallback<IExpressionParse>) {
const data: ICheckConstituentaData = {
definition_formal: expression,
alias: activeCst.alias,
cst_type: activeCst.cst_type
};
setError(undefined);
postCheckConstituenta(String(schema!.id), {
data: data,
showError: true,
setLoading: setProcessing,
onError: setError,
onSuccess: parse => {
setParseData(parse);
if (onSuccess) onSuccess(parse);
}
});
}
return { parseData, checkConstituenta, resetParse, error, setError, processing };
}
export default useCheckConstituenta;

View File

@ -1,100 +0,0 @@
'use client';
import { useCallback, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { postCheckExpression } from '@/backend/rsforms';
import { type ErrorData } from '@/components/info/InfoError';
import { CstType, IConstituenta, type IRSForm } from '@/models/rsform';
import { getDefinitionPrefix } from '@/models/rsformAPI';
import { IArgumentInfo, IExpressionParse } from '@/models/rslang';
import { RSErrorType } from '@/models/rslang';
import { PARAMETER } from '@/utils/constants';
function useCheckExpression({ schema }: { schema?: IRSForm }) {
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<ErrorData>(undefined);
const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined);
const resetParse = useCallback(() => setParseData(undefined), []);
function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback<IExpressionParse>) {
setError(undefined);
postCheckExpression(String(schema!.id), {
data: { expression: expression },
showError: true,
setLoading: setProcessing,
onError: setError,
onSuccess: parse => {
if (activeCst) {
adjustResults(parse, expression.trim() === getDefinitionPrefix(activeCst), activeCst.cst_type);
}
setParseData(parse);
if (onSuccess) onSuccess(parse);
}
});
}
return { parseData, checkExpression, resetParse, error, setError, processing };
}
export default useCheckExpression;
// ===== Internals ========
function checkTypeConsistency(type: CstType, typification: string, args: IArgumentInfo[]): boolean {
switch (type) {
case CstType.BASE:
case CstType.CONSTANT:
case CstType.STRUCTURED:
case CstType.TERM:
return typification !== PARAMETER.logicLabel && args.length === 0;
case CstType.AXIOM:
case CstType.THEOREM:
return typification === PARAMETER.logicLabel && args.length === 0;
case CstType.FUNCTION:
return typification !== PARAMETER.logicLabel && args.length !== 0;
case CstType.PREDICATE:
return typification === PARAMETER.logicLabel && args.length !== 0;
}
}
function adjustResults(parse: IExpressionParse, emptyExpression: boolean, cstType: CstType) {
if (!parse.parseResult && parse.errors.length > 0) {
return;
}
if (cstType === CstType.BASE || cstType === CstType.CONSTANT) {
if (!emptyExpression) {
parse.parseResult = false;
parse.errors.push({
errorType: RSErrorType.globalNonemptyBase,
isCritical: true,
params: [],
position: 0
});
return;
}
} else {
if (emptyExpression) {
parse.parseResult = false;
parse.errors.push({
errorType: RSErrorType.globalEmptyDerived,
isCritical: true,
params: [],
position: 0
});
return;
}
}
if (!checkTypeConsistency(cstType, parse.typification, parse.args)) {
parse.parseResult = false;
parse.errors.push({
errorType: RSErrorType.globalUnexpectedType,
isCritical: true,
params: [],
position: 0
});
}
}

View File

@ -144,6 +144,11 @@ export interface IConstituentaList {
items: ConstituentaID[]; items: ConstituentaID[];
} }
/**
* Represents {@link IConstituenta} data, used for checking expression.
*/
export interface ICheckConstituentaData extends Pick<IConstituentaMeta, 'alias' | 'cst_type' | 'definition_formal'> {}
/** /**
* Represents {@link IConstituenta} data, used in creation process. * Represents {@link IConstituenta} data, used in creation process.
*/ */

View File

@ -7,13 +7,6 @@
*/ */
export type AliasMapping = Record<string, string>; export type AliasMapping = Record<string, string>;
/**
* Represents formal expression.
*/
export interface IRSExpression {
expression: string;
}
/** /**
* Represents syntax type. * Represents syntax type.
*/ */
@ -91,6 +84,7 @@ export interface IArgumentValue extends IArgumentInfo {
*/ */
export interface IExpressionParse { export interface IExpressionParse {
parseResult: boolean; parseResult: boolean;
prefixLen: number;
syntax: Syntax; syntax: Syntax;
typification: string; typification: string;
valueClass: ValueClass; valueClass: ValueClass;
@ -248,16 +242,17 @@ export enum RSErrorType {
typesNotCompatible = 34853, typesNotCompatible = 34853,
orderingNotSupported = 34854, orderingNotSupported = 34854,
// !!!! Добавлены по сравнению с ConceptCore !!!!!
globalNonemptyBase = 34855,
globalUnexpectedType = 34856,
globalEmptyDerived = 34857,
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
globalNoValue = 34880, globalNoValue = 34880,
invalidPropertyUsage = 34881, invalidPropertyUsage = 34881,
globalMissingAST = 34882, globalMissingAST = 34882,
globalFuncNoInterpretation = 34883 globalFuncNoInterpretation = 34883,
cstNonemptyBase = 34912,
cstEmptyDerived = 34913,
cstCallableNoArgs = 34914,
cstNonCallableHasArgs = 34915,
cstExpectedLogical = 34916,
cstExpectedTyped = 34917
} }
/** /**

View File

@ -11,7 +11,7 @@ import { RSTextWrapper } from '@/components/RSInput/textEditing';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import DlgShowAST from '@/dialogs/DlgShowAST'; import DlgShowAST from '@/dialogs/DlgShowAST';
import useCheckExpression from '@/hooks/useCheckExpression'; import useCheckConstituenta from '@/hooks/useCheckConstituenta';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { ConstituentaID, IConstituenta } from '@/models/rsform'; import { ConstituentaID, IConstituenta } from '@/models/rsform';
@ -54,7 +54,7 @@ function EditorRSExpression({
const model = useRSForm(); const model = useRSForm();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
const parser = useCheckExpression({ schema: model.schema }); const parser = useCheckConstituenta({ schema: model.schema });
const { resetParse } = parser; const { resetParse } = parser;
const rsInput = useRef<ReactCodeMirrorRef>(null); const rsInput = useRef<ReactCodeMirrorRef>(null);
@ -74,11 +74,9 @@ function EditorRSExpression({
} }
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) { function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
const prefix = getDefinitionPrefix(activeCst); parser.checkConstituenta(value, activeCst, parse => {
const expression = prefix + value;
parser.checkExpression(expression, activeCst, parse => {
if (parse.errors.length > 0) { if (parse.errors.length > 0) {
onShowError(parse.errors[0]); onShowError(parse.errors[0], parse.prefixLen);
} else { } else {
rsInput.current?.view?.focus(); rsInput.current?.view?.focus();
} }
@ -95,12 +93,11 @@ function EditorRSExpression({
} }
const onShowError = useCallback( const onShowError = useCallback(
(error: IRSErrorDescription) => { (error: IRSErrorDescription, prefixLen: number) => {
if (!rsInput.current) { if (!rsInput.current) {
return; return;
} }
const prefix = getDefinitionPrefix(activeCst); let errorPosition = error.position - prefixLen;
let errorPosition = error.position - prefix.length;
if (errorPosition < 0) errorPosition = 0; if (errorPosition < 0) errorPosition = 0;
rsInput.current?.view?.dispatch({ rsInput.current?.view?.dispatch({
selection: { selection: {
@ -133,6 +130,7 @@ function EditorRSExpression({
toast.error(errors.astFailed); toast.error(errors.astFailed);
} else { } else {
setSyntaxTree(parse.ast); setSyntaxTree(parse.ast);
// TODO: return prefix from parser API instead of prefixLength
setExpression(getDefinitionPrefix(activeCst) + value); setExpression(getDefinitionPrefix(activeCst) + value);
setShowAST(true); setShowAST(true);
} }
@ -199,7 +197,7 @@ function EditorRSExpression({
isOpen={!!parser.parseData && parser.parseData.errors.length > 0} isOpen={!!parser.parseData && parser.parseData.errors.length > 0}
data={parser.parseData} data={parser.parseData}
disabled={disabled} disabled={disabled}
onShowError={onShowError} onShowError={error => onShowError(error, parser.parseData?.prefixLen ?? 0)}
/> />
</div> </div>
); );

View File

@ -34,7 +34,7 @@ export const PARAMETER = {
statSmallThreshold: 3, // characters - threshold for small labels - small font statSmallThreshold: 3, // characters - threshold for small labels - small font
logicLabel: 'LOGIC', logicLabel: 'LOGIC',
exteorVersion: '4.9.4', exteorVersion: '4.9.5',
TOOLTIP_WIDTH: 'max-w-[29rem]' TOOLTIP_WIDTH: 'max-w-[29rem]'
}; };

View File

@ -787,13 +787,20 @@ export function describeRSError(error: IRSErrorDescription): string {
case RSErrorType.globalMissingAST: case RSErrorType.globalMissingAST:
return `Не удалось получить дерево разбора для глобального идентификатора: ${error.params[0]}`; return `Не удалось получить дерево разбора для глобального идентификатора: ${error.params[0]}`;
case RSErrorType.globalFuncNoInterpretation: case RSErrorType.globalFuncNoInterpretation:
return `Функция не интерпретируется для данных аргументов`; return 'Функция не интерпретируется для данных аргументов';
case RSErrorType.globalNonemptyBase:
return `Непустое выражение базисного/константного множества`; case RSErrorType.cstNonemptyBase:
case RSErrorType.globalUnexpectedType: return 'Непустое выражение базисного/константного множества';
return `Типизация выражения не соответствует типу конституенты`; case RSErrorType.cstEmptyDerived:
case RSErrorType.globalEmptyDerived: return 'Пустое выражение для сложного понятия или утверждения';
return `Пустое выражение для выводимого понятия или утверждения`; case RSErrorType.cstCallableNoArgs:
return 'Отсутствуют аргументы для параметризованной конституенты';
case RSErrorType.cstNonCallableHasArgs:
return 'Параметризованное выражение не подходит для данного типа конституенты';
case RSErrorType.cstExpectedLogical:
return 'Данный тип конституенты требует логического выражения';
case RSErrorType.cstExpectedTyped:
return 'Данный тип конституенты требует теоретико-множественного выражения';
} }
return 'UNKNOWN ERROR'; return 'UNKNOWN ERROR';
} }