Add endpoint for constituenta substitution

This commit is contained in:
IRBorisov 2024-01-15 23:37:14 +03:00
parent dfdbd4b17c
commit 1aa80c3dce
14 changed files with 339 additions and 96 deletions

View File

@ -47,6 +47,7 @@
"clsx",
"codemirror",
"Constituenta",
"corsheaders",
"csrftoken",
"cstlist",
"csttype",
@ -67,6 +68,7 @@
"GRND",
"impr",
"inan",
"incapsulation",
"indc",
"INFN",
"Infr",
@ -84,6 +86,7 @@
"NUMR",
"Opencorpora",
"perfectivity",
"PNCT",
"ponomarev",
"PRCL",
"PRTF",

View File

@ -0,0 +1,29 @@
''' Utility: Text messages. '''
# pylint: skip-file
def constituentaNotOwned(title: str):
return f'Конституента не принадлежит схеме: {title}'
def constituentaNotExists():
return 'Конституента не существует'
def renameTrivial(name: str):
return f'Имя должно отличаться от текущего: {name}'
def substituteTrivial(name: str):
return f'Отождествление конституенты с собой не корректно: {name}'
def renameTaken(name: str):
return f'Имя уже используется: {name}'
def pyconceptFailure():
return 'Invalid data response from pyconcept'
def libraryTypeUnexpected():
return 'Attempting to use invalid adaptor for non-RSForm item'
def exteorFileVersionNotSupported():
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
def positionNegative():
return 'Invalid position: should be positive integer'

View File

@ -15,10 +15,11 @@ from apps.users.models import User
from cctext import Resolver, Entity, extract_entities, split_grams, TermForm
from .graph import Graph
from .utils import apply_pattern
from . import messages as msg
_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)')
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line
class LibraryItemType(TextChoices):
@ -125,7 +126,7 @@ class LibraryItem(Model):
def subscribers(self) -> list[User]:
''' Get all subscribers for this item . '''
return [s.user for s in Subscription.objects.filter(item=self.pk)]
return [subscription.user for subscription in Subscription.objects.filter(item=self.pk)]
@transaction.atomic
def save(self, *args, **kwargs):
@ -272,7 +273,7 @@ class RSForm:
''' RSForm is a math form of capturing conceptual schema. '''
def __init__(self, item: LibraryItem):
if item.item_type != LibraryItemType.RSFORM:
raise ValueError('Attempting to use invalid adaptor for non-RSForm item')
raise ValueError(msg.libraryTypeUnexpected())
self.item = item
@staticmethod
@ -330,11 +331,12 @@ class RSForm:
@transaction.atomic
def insert_at(self, position: int, alias: str, insert_type: CstType) -> 'Constituenta':
''' Insert new constituenta at given position. All following constituents order is shifted by 1 position '''
''' Insert new constituenta at given position.
All following constituents order is shifted by 1 position. '''
if position <= 0:
raise ValidationError('Invalid position: should be positive integer')
raise ValidationError(msg.positionNegative())
if self.constituents().filter(alias=alias).exists():
raise ValidationError(f'Alias taken {alias}')
raise ValidationError(msg.renameTaken(alias))
currentSize = self.constituents().count()
position = max(1, min(position, currentSize + 1))
update_list = \
@ -357,9 +359,9 @@ class RSForm:
@transaction.atomic
def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta':
''' Insert new constituenta at last position '''
''' Insert new constituenta at last position. '''
if self.constituents().filter(alias=alias).exists():
raise ValidationError(f'Alias taken {alias}')
raise ValidationError(msg.renameTaken(alias))
position = 1
if self.constituents().exists():
position += self.constituents().count()
@ -398,7 +400,7 @@ class RSForm:
@transaction.atomic
def delete_cst(self, listCst):
''' Delete multiple constituents. Do not check if listCst are from this schema '''
''' Delete multiple constituents. Do not check if listCst are from this schema. '''
for cst in listCst:
cst.delete()
self._reset_order()
@ -426,8 +428,26 @@ class RSForm:
cst.refresh_from_db()
return cst
@transaction.atomic
def substitute(
self,
original: 'Constituenta',
substitution: 'Constituenta',
transfer_term: bool
):
''' Execute constituenta substitution. '''
assert original.pk != substitution.pk
mapping = { original.alias: substitution.alias }
self.apply_mapping(mapping)
if transfer_term:
substitution.term_raw = original.term_raw
substitution.term_forms = original.term_forms
substitution.save()
original.delete()
self.on_term_change([substitution.alias])
def reset_aliases(self):
''' Recreate all aliases based on cst order. '''
''' Recreate all aliases based on constituents order. '''
mapping = self._create_reset_mapping()
self.apply_mapping(mapping, change_aliases=True)
@ -508,7 +528,10 @@ class RSForm:
def _term_graph(self) -> Graph:
result = Graph()
cst_list = self.constituents().only('order', 'alias', 'term_raw').order_by('order')
cst_list = \
self.constituents() \
.only('order', 'alias', 'term_raw') \
.order_by('order')
for cst in cst_list:
result.add_node(cst.alias)
for cst in cst_list:
@ -519,7 +542,10 @@ class RSForm:
def _definition_graph(self) -> Graph:
result = Graph()
cst_list = self.constituents().only('order', 'alias', 'definition_raw').order_by('order')
cst_list = \
self.constituents() \
.only('order', 'alias', 'definition_raw') \
.order_by('order')
for cst in cst_list:
result.add_node(cst.alias)
for cst in cst_list:

View File

@ -9,6 +9,7 @@ from cctext import Resolver, Reference, ReferenceType, EntityReference, Syntacti
from .utils import fix_old_references
from .models import Constituenta, LibraryItem, RSForm
from . import messages as msg
_CST_TYPE = 'constituenta'
_TRS_TYPE = 'rsform'
@ -16,6 +17,8 @@ _TRS_VERSION_MIN = 16
_TRS_VERSION = 16
_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022'
ConstituentaID = serializers.IntegerField
NodeID = serializers.IntegerField
class FileSerializer(serializers.Serializer):
''' Serializer: File input. '''
@ -99,7 +102,7 @@ class NodeDataSerializer(serializers.Serializer):
class ASTNodeSerializer(serializers.Serializer):
''' Serializer: Syntax tree node. '''
uid = serializers.IntegerField()
uid = NodeID()
parent = serializers.IntegerField() # type: ignore
typeID = serializers.IntegerField()
start = serializers.IntegerField()
@ -148,7 +151,7 @@ class ConstituentaSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved')
def update(self, instance: Constituenta, validated_data) -> Constituenta:
data = validated_data # Note: create alias for better code readability
data = validated_data # Note: use alias for better code readability
schema = RSForm(instance.schema)
definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None
term: Optional[str] = data['term_raw'] if 'term_raw' in data else None
@ -175,7 +178,10 @@ class CstCreateSerializer(serializers.ModelSerializer):
class Meta:
''' serializer metadata. '''
model = Constituenta
fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after', 'term_forms'
fields = \
'alias', 'cst_type', 'convention', \
'term_raw', 'definition_raw', 'definition_formal', \
'insert_after', 'term_forms'
class CstRenameSerializer(serializers.ModelSerializer):
@ -191,15 +197,15 @@ class CstRenameSerializer(serializers.ModelSerializer):
new_alias = self.initial_data['alias']
if old_cst.schema != schema.item:
raise serializers.ValidationError({
'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}'
'id': msg.constituentaNotOwned(schema.item.title)
})
if old_cst.alias == new_alias:
raise serializers.ValidationError({
'alias': f'Имя конституенты должно отличаться от текущего: {new_alias}'
'alias': msg.renameTrivial(new_alias)
})
if schema.constituents().filter(alias=new_alias).exists():
raise serializers.ValidationError({
'alias': f'Конституента с таким именем уже существует: {new_alias}'
'alias': msg.renameTaken(new_alias)
})
self.instance = old_cst
attrs['schema'] = schema.item
@ -207,6 +213,34 @@ class CstRenameSerializer(serializers.ModelSerializer):
return attrs
class CstSubstituteSerializer(serializers.Serializer):
''' Serializer: Constituenta substitution. '''
original = ConstituentaID()
substitution = ConstituentaID()
transfer_term = serializers.BooleanField(required=False, default=False)
def validate(self, attrs):
schema = cast(RSForm, self.context['schema'])
original_cst = Constituenta.objects.get(pk=self.initial_data['original'])
substitution_cst = Constituenta.objects.get(pk=self.initial_data['substitution'])
if original_cst.alias == substitution_cst.alias:
raise serializers.ValidationError({
'alias': msg.substituteTrivial(original_cst.alias)
})
if original_cst.schema != schema.item:
raise serializers.ValidationError({
'original': msg.constituentaNotOwned(schema.item.title)
})
if substitution_cst.schema != schema.item:
raise serializers.ValidationError({
'substitution': msg.constituentaNotOwned(schema.item.title)
})
attrs['original'] = original_cst
attrs['substitution'] = substitution_cst
attrs['transfer_term'] = self.initial_data['transfer_term']
return attrs
class CstListSerializer(serializers.Serializer):
''' Serializer: List of constituents from one origin. '''
items = serializers.ListField(
@ -220,12 +254,13 @@ class CstListSerializer(serializers.Serializer):
try:
cst = Constituenta.objects.get(pk=item)
except Constituenta.DoesNotExist as exception:
raise serializers.ValidationError(
{f"{item}": 'Конституента не существует'}
) from exception
raise serializers.ValidationError({
f'{item}': msg.constituentaNotExists
}) from exception
if cst.schema != schema.item:
raise serializers.ValidationError(
{'items': f'Конституенты должны относиться к данной схеме: {item}'})
raise serializers.ValidationError({
f'{item}': msg.constituentaNotOwned(schema.item.title)
})
cstList.append(cst)
attrs['constituents'] = cstList
return attrs
@ -310,7 +345,7 @@ class PyConceptAdapter:
Warning! Does not include texts. '''
self._produce_response()
if self._checked_data is None:
raise ValueError('Invalid data response from pyconcept')
raise ValueError(msg.pyconceptFailure())
return self._checked_data
def _prepare_request(self) -> dict:
@ -481,7 +516,7 @@ class RSFormTRSSerializer(serializers.Serializer):
or self.initial_data['version'] < _TRS_VERSION_MIN \
or self.initial_data['version'] > _TRS_VERSION:
raise serializers.ValidationError({
'version': 'Некорректная версия файла Экстеор. Сохраните файл в новой версии'
'version': msg.exteorFileVersionNotSupported()
})
return attrs

View File

@ -242,19 +242,44 @@ class TestRSForm(TestCase):
self.assertEqual(cst2.term_resolved, 'слон')
self.assertEqual(cst2.definition_resolved, 'слонам слоны')
def test_delete_cst(self):
def test_apply_mapping(self):
schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE)
x2 = schema.insert_last('X11', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM)
d1.definition_formal = 'X1 = X11 = X2'
d1.definition_raw = '@{X11|sing}'
d1.convention = 'X1'
d1.term_raw = '@{X1|plur}'
d1.save()
schema.apply_mapping({x1.alias: 'X3', x2.alias: 'X4'})
d1.refresh_from_db()
self.assertEqual(d1.definition_formal, 'X3 = X4 = X2', msg='Map IDs in expression')
self.assertEqual(d1.definition_raw, '@{X4|sing}', msg='Map IDs in definition')
self.assertEqual(d1.convention, 'X3', msg='Map IDs in convention')
self.assertEqual(d1.term_raw, '@{X3|plur}', msg='Map IDs in term')
self.assertEqual(d1.term_resolved, '', msg='Do not run resolve on mapping')
self.assertEqual(d1.definition_resolved, '', msg='Do not run resolve on mapping')
def test_substitute(self):
schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM)
d2 = schema.insert_last('D2', CstType.TERM)
schema.delete_cst([x2, d1])
x1.refresh_from_db()
d2.refresh_from_db()
schema.item.refresh_from_db()
d1.definition_formal = x1.alias
d1.save()
x1.term_raw = 'Test'
x1.save()
x2.term_raw = 'Test2'
x2.save()
schema.substitute(x1, x2, True)
x2.refresh_from_db()
d1.refresh_from_db()
self.assertEqual(schema.constituents().count(), 2)
self.assertEqual(x1.order, 1)
self.assertEqual(d2.order, 2)
self.assertEqual(x2.term_raw, 'Test')
self.assertEqual(d1.definition_formal, x2.alias)
def test_move_cst(self):
schema = RSForm.create(title='Test')

View File

@ -36,16 +36,30 @@ class TestConstituentaAPI(APITestCase):
self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create(
alias='X1', schema=self.rsform_owned.item, order=1, convention='Test',
term_raw='Test1', term_resolved='Test1R',
alias='X1',
schema=self.rsform_owned.item,
order=1,
convention='Test',
term_raw='Test1',
term_resolved='Test1R',
term_forms=[{'text':'form1', 'tags':'sing,datv'}])
self.cst2 = Constituenta.objects.create(
alias='X2', schema=self.rsform_unowned.item, order=1, convention='Test1',
term_raw='Test2', term_resolved='Test2R')
alias='X2',
schema=self.rsform_unowned.item,
order=1,
convention='Test1',
term_raw='Test2',
term_resolved='Test2R'
)
self.cst3 = Constituenta.objects.create(
alias='X3', schema=self.rsform_owned.item, order=2,
term_raw='Test3', term_resolved='Test3',
definition_raw='Test1', definition_resolved='Test2')
alias='X3',
schema=self.rsform_owned.item,
order=2,
term_raw='Test3',
term_resolved='Test3',
definition_raw='Test1',
definition_resolved='Test2'
)
def test_retrieve(self):
response = self.client.get(f'/api/constituents/{self.cst1.id}')
@ -421,8 +435,18 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.status_code, 403)
item = self.owned.item
Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
Constituenta.objects.create(
schema=item,
alias='X1',
cst_type='basic',
order=1
)
x2 = Constituenta.objects.create(
schema=item,
alias='X2',
cst_type='basic',
order=2
)
response = self.client.post(
f'/api/rsforms/{item.id}/cst-create',
data=data, format='json'
@ -452,21 +476,29 @@ class TestRSFormViewset(APITestCase):
def test_rename_constituenta(self):
cst1 = Constituenta.objects.create(
alias='X1', schema=self.owned.item, order=1, convention='Test',
term_raw='Test1', term_resolved='Test1',
alias='X1',
schema=self.owned.item,
order=1,
convention='Test',
term_raw='Test1',
term_resolved='Test1',
term_forms=[{'text':'form1', 'tags':'sing,datv'}]
)
cst2 = Constituenta.objects.create(
alias='X2', schema=self.unowned.item, order=1, convention='Test1',
term_raw='Test2', term_resolved='Test2'
alias='X2',
schema=self.unowned.item,
order=1
)
cst3 = Constituenta.objects.create(
alias='X3', schema=self.owned.item, order=2,
term_raw='Test3', term_resolved='Test3',
definition_raw='Test1', definition_resolved='Test2'
alias='X3',
schema=self.owned.item, order=2,
term_raw='Test3',
term_resolved='Test3',
definition_raw='Test1',
definition_resolved='Test2'
)
data = {'alias': 'D2', 'cst_type': 'term', 'id': cst2.pk}
data = {'id': cst2.pk, 'alias': 'D2', 'cst_type': 'term'}
response = self.client.patch(
f'/api/rsforms/{self.unowned.item.id}/cst-rename',
data=data, format='json'
@ -479,14 +511,14 @@ class TestRSFormViewset(APITestCase):
)
self.assertEqual(response.status_code, 400)
data = {'alias': cst1.alias, 'cst_type': 'term', 'id': cst1.pk}
data = {'id': cst1.pk, 'alias': cst1.alias, 'cst_type': 'term'}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
data = {'alias': cst3.alias, 'id': cst1.pk}
data = {'id': cst1.pk, 'alias': cst3.alias}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename',
data=data, format='json'
@ -520,6 +552,74 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(cst1.alias, 'D2')
self.assertEqual(cst1.cst_type, CstType.TERM)
def test_substitute_constituenta(self):
x1 = Constituenta.objects.create(
alias='X1',
schema=self.owned.item,
order=1,
term_raw='Test1',
term_resolved='Test1',
term_forms=[{'text':'form1', 'tags':'sing,datv'}]
)
x2 = Constituenta.objects.create(
alias='X2',
schema=self.owned.item,
order=2,
term_raw='Test2'
)
unowned = Constituenta.objects.create(
alias='X2',
schema=self.unowned.item,
order=1
)
data = {'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True}
response = self.client.patch(
f'/api/rsforms/{self.unowned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 403)
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
data = {'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
data = {'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
d1 = Constituenta.objects.create(
alias='D1',
schema=self.owned.item,
order=3,
term_raw='@{X2|sing,datv}',
definition_formal='X1'
)
data = {'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 200)
d1.refresh_from_db()
x2.refresh_from_db()
self.assertEqual(x2.term_raw, 'Test1')
self.assertEqual(d1.term_resolved, 'form1')
self.assertEqual(d1.definition_formal, 'X2')
def test_create_constituenta_data(self):
data = {
'alias': 'X3',

View File

@ -195,7 +195,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def get_permissions(self):
''' Determine permission class. '''
if self.action in ['load_trs', 'cst_create', 'cst_delete_multiple',
'reset_aliases', 'cst_rename']:
'reset_aliases', 'cst_rename', 'cst_substitute']:
permission_classes = [utils.ObjectOwnerOrAdmin]
else:
permission_classes = [permissions.AllowAny]
@ -256,6 +256,30 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
}
)
@extend_schema(
summary='substitute constituenta',
tags=['Constituenta'],
request=s.CstSubstituteSerializer,
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
)
@transaction.atomic
@action(detail=True, methods=['patch'], url_path='cst-substitute')
def cst_substitute(self, request, pk):
''' Substitute occurrences of constituenta with another one. '''
schema = self._get_schema()
serializer = s.CstSubstituteSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
schema.substitute(
original=serializer.validated_data['original'],
substitution=serializer.validated_data['substitution'],
transfer_term=serializer.validated_data['transfer_term']
)
schema.item.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
)
@extend_schema(
summary='delete constituents',
tags=['Constituenta'],

View File

@ -0,0 +1,8 @@
''' Utility: Text messages. '''
# pylint: skip-file
def passwordAuthFailed():
return 'Неизвестное сочетание имени пользователя и пароля'
def passwordsNotMatch():
return 'Введенные пароли не совпадают'

View File

@ -5,6 +5,7 @@ from rest_framework import serializers
from apps.rsform.models import Subscription
from . import models
from . import messages as msg
class NonFieldErrorSerializer(serializers.Serializer):
@ -36,17 +37,13 @@ class LoginSerializer(serializers.Serializer):
password=password
)
if not user:
msg = 'Неправильное сочетание имени пользователя и пароля.'
raise serializers.ValidationError(msg, code='authorization')
raise serializers.ValidationError(
msg.passwordAuthFailed(),
code='authorization'
)
attrs['user'] = user
return attrs
def create(self, validated_data):
raise NotImplementedError('unexpected `create()` call')
def update(self, instance, validated_data):
raise NotImplementedError('unexpected `update()` call')
class AuthSerializer(serializers.Serializer):
''' Serializer: Authorization data. '''
@ -108,12 +105,6 @@ class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
def create(self, validated_data):
raise NotImplementedError('unexpected `create()` call')
def update(self, instance, validated_data):
raise NotImplementedError('unexpected `update()` call')
class SignupSerializer(serializers.ModelSerializer):
''' Serializer: Create user profile. '''
@ -136,7 +127,9 @@ class SignupSerializer(serializers.ModelSerializer):
def validate(self, attrs):
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "Введенные пароли не совпадают"})
raise serializers.ValidationError({
'password': msg.passwordsNotMatch()
})
return attrs
def create(self, validated_data):

View File

@ -1,7 +1,7 @@
'''
Concept API Python functions.
::guarantee:: doesnt raise exceptions and returns workable outputs
::guarantee:: doesn't raise exceptions and returns workable outputs
'''
from cctext.rumodel import Morphology
from .syntax import RuSyntax
@ -56,9 +56,9 @@ def inflect(text: str, target_grams: str) -> str:
return model.inflect(target_set)
def inflect_context(target: str, cntxt_before: str = '', cntxt_after: str = '') -> str:
def inflect_context(target: str, before: str = '', after: str = '') -> str:
''' Inflect text in accordance to context before and after. '''
return parser.inflect_context(target, cntxt_before, cntxt_after)
return parser.inflect_context(target, before, after)
def inflect_substitute(substitute_normal: str, original: str) -> str:

View File

@ -25,11 +25,11 @@ class EntityReference:
class SyntacticReference:
''' Reference to syntactic dependcy on EntityReference. '''
''' Reference to syntactic dependency on EntityReference. '''
def __init__(self, referal_offset: int, text: str):
def __init__(self, referral_offset: int, text: str):
self.nominal = text
self.offset = referal_offset
self.offset = referral_offset
def get_type(self) -> ReferenceType:
return ReferenceType.syntactic

View File

@ -34,31 +34,31 @@ def resolve_entity(ref: EntityReference, context: TermContext) -> str:
return resolved
def resolve_syntactic(ref: SyntacticReference, index: int, allrefs: list['ResolvedReference']) -> str:
def resolve_syntactic(ref: SyntacticReference, index: int, references: list['ResolvedReference']) -> str:
''' Resolve syntactic reference. '''
offset = ref.offset
mainref: Optional['ResolvedReference'] = None
master: Optional['ResolvedReference'] = None
if offset > 0:
index += 1
while index < len(allrefs):
if isinstance(allrefs[index].ref, EntityReference):
while index < len(references):
if isinstance(references[index].ref, EntityReference):
if offset == 1:
mainref = allrefs[index]
master = references[index]
else:
offset -= 1
index += 1
else:
index -= 1
while index >= 0:
if isinstance(allrefs[index].ref, EntityReference):
if isinstance(references[index].ref, EntityReference):
if offset == -1:
mainref = allrefs[index]
master = references[index]
else:
offset += 1
index -= 1
if mainref is None:
if master is None:
return f'!Некорректное смещение: {ref.offset}!'
return inflect_dependant(ref.nominal, mainref.resolved)
return inflect_dependant(ref.nominal, master.resolved)
@dataclass

View File

@ -149,7 +149,7 @@ class PhraseParser:
_SINGLE_SCORE_SEARCH = 0.2
_PRIORITY_NONE = NO_COORDINATION
_MAIN_WAIT_LIMIT = 10 # count words untill fixing main
_MAIN_WAIT_LIMIT = 10 # count words until fixing main
_MAIN_MAX_FOLLOWERS = 3 # count words after main as coordination candidates
def parse(self, text: str,
@ -194,7 +194,7 @@ class PhraseParser:
return (start, token.stop)
return (0, 0)
def inflect_context(self, text: str, cntxt_before: str = '', cntxt_after: str = '') -> str:
def inflect_context(self, text: str, before: str = '', after: str = '') -> str:
''' Inflect text in accordance to context before and after. '''
target = self.parse(text)
if not target:
@ -203,8 +203,8 @@ class PhraseParser:
if not target_morpho or not target_morpho.can_coordinate:
return text
model_after = self.parse(cntxt_after)
model_before = self.parse(cntxt_before)
model_after = self.parse(after)
model_before = self.parse(before)
etalon = PhraseParser._choose_context_etalon(target_morpho, model_before, model_after)
if not etalon:
return text

View File

@ -40,14 +40,14 @@ class TestResolver(unittest.TestCase):
def test_resolve_syntactic(self):
ref = ResolvedReference(ref=EntityReference('X1', 'sing,datv'), resolved='человеку')
allrefs = [ref, ref, ref, ref]
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-1), 0, allrefs), '!Некорректное смещение: -1!')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=1), 3, allrefs), '!Некорректное смещение: 1!')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=1), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=2), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=3), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-1), 3, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-2), 3, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-3), 3, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 0, allrefs), '!Некорректное смещение: -1!')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 3, allrefs), '!Некорректное смещение: 1!')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=2), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=3), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 3, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-2), 3, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-3), 3, allrefs), 'умному')
def test_resolve_invalid(self):
self.assertEqual(self.resolver.resolve(''), '')