diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9355f26b..8a37642b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -13,6 +13,6 @@
"mode": "auto"
}
],
- "python.linting.pylintEnabled": true,
- "python.linting.enabled": true
+ "python.linting.enabled": true,
+ "python.linting.mypyEnabled": true
}
\ No newline at end of file
diff --git a/README.md b/README.md
index f4c08ffc..5d24f39f 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,10 @@ This readme file is used mostly to document project dependencies
requirements_dev
- coverage
+ - pylint
+ - mypy
+ - django-stubs[compatible-mypy]
+ - djangorestframework-stubs[compatible-mypy]
diff --git a/rsconcept/RunLint.ps1 b/rsconcept/RunLint.ps1
index eb253d39..c47debec 100644
--- a/rsconcept/RunLint.ps1
+++ b/rsconcept/RunLint.ps1
@@ -2,5 +2,7 @@
Set-Location $PSScriptRoot\backend
$pylint = "$PSScriptRoot\backend\venv\Scripts\pylint.exe"
+$mypy = "$PSScriptRoot\backend\venv\Scripts\mypy.exe"
-& $pylint cctext project apps
\ No newline at end of file
+& $pylint cctext project apps
+& $mypy cctext project apps
\ No newline at end of file
diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py
index e0712e0d..40a5aded 100644
--- a/rsconcept/backend/apps/rsform/models.py
+++ b/rsconcept/backend/apps/rsform/models.py
@@ -1,14 +1,18 @@
''' Models: RSForms for conceptual schemas. '''
import json
import pyconcept
-from django.db import models, transaction
+from django.db import transaction
+from django.db.models import (
+ CASCADE, SET_NULL, ForeignKey, Model, PositiveIntegerField, QuerySet,
+ TextChoices, TextField, BooleanField, CharField, DateTimeField, JSONField
+)
from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from django.urls import reverse
from apps.users.models import User
-class CstType(models.TextChoices):
+class CstType(TextChoices):
''' Type of constituenta '''
BASE = 'basic'
CONSTANT = 'constant'
@@ -20,7 +24,7 @@ class CstType(models.TextChoices):
THEOREM = 'theorem'
-class Syntax(models.TextChoices):
+class Syntax(TextChoices):
''' Syntax types '''
UNDEF = 'undefined'
ASCII = 'ascii'
@@ -31,35 +35,35 @@ def _empty_forms():
return []
-class RSForm(models.Model):
+class RSForm(Model):
''' RSForm is a math form of capturing conceptual schema '''
- owner = models.ForeignKey(
+ owner: ForeignKey = ForeignKey(
verbose_name='Владелец',
to=User,
- on_delete=models.SET_NULL,
+ on_delete=SET_NULL,
null=True
)
- title = models.TextField(
+ title: TextField = TextField(
verbose_name='Название'
)
- alias = models.CharField(
+ alias: CharField = CharField(
verbose_name='Шифр',
max_length=255,
blank=True
)
- comment = models.TextField(
+ comment: TextField = TextField(
verbose_name='Комментарий',
blank=True
)
- is_common = models.BooleanField(
+ is_common: BooleanField = BooleanField(
verbose_name='Общая',
default=False
)
- time_create = models.DateTimeField(
+ time_create: DateTimeField = DateTimeField(
verbose_name='Дата создания',
auto_now_add=True
)
- time_update = models.DateTimeField(
+ time_update: DateTimeField = DateTimeField(
verbose_name='Дата изменения',
auto_now=True
)
@@ -69,7 +73,7 @@ class RSForm(models.Model):
verbose_name = 'Схема'
verbose_name_plural = 'Схемы'
- def constituents(self) -> models.QuerySet:
+ def constituents(self) -> QuerySet:
''' Get QuerySet containing all constituents of current RSForm '''
return Constituenta.objects.filter(schema=self)
@@ -162,7 +166,7 @@ class RSForm(models.Model):
else:
cst = Constituenta.create_from_trs(cst_data, self, order)
cst.save()
- uid = cst.id
+ uid = cst.pk
loaded_ids.add(uid)
order += 1
for prev_cst in prev_constituents:
@@ -186,10 +190,10 @@ class RSForm(models.Model):
schema._create_items_from_trs(data['items'])
return schema
- def to_trs(self) -> str:
+ def to_trs(self) -> dict:
''' Generate JSON string containing all data from RSForm '''
result = self._prepare_json_rsform()
- items: list['Constituenta'] = self.constituents().order_by('order')
+ items = self.constituents().order_by('order')
for cst in items:
result['items'].append(cst.to_trs())
return result
@@ -200,7 +204,7 @@ class RSForm(models.Model):
def get_absolute_url(self):
return reverse('rsform-detail', kwargs={'pk': self.pk})
- def _prepare_json_rsform(self: 'Constituenta') -> dict:
+ def _prepare_json_rsform(self: 'RSForm') -> dict:
return {
'type': 'rsform',
'title': self.title,
@@ -211,10 +215,10 @@ class RSForm(models.Model):
@transaction.atomic
def _update_from_core(self) -> dict:
- checked = json.loads(pyconcept.check_schema(json.dumps(self.to_trs())))
+ checked: dict = json.loads(pyconcept.check_schema(json.dumps(self.to_trs())))
update_list = self.constituents().only('id', 'order')
if len(checked['items']) != update_list.count():
- raise ValidationError
+ raise ValidationError('Invalid constituents count')
order = 1
for cst in checked['items']:
cst_id = cst['entityUID']
@@ -235,59 +239,59 @@ class RSForm(models.Model):
order += 1
-class Constituenta(models.Model):
+class Constituenta(Model):
''' Constituenta is the base unit for every conceptual schema '''
- schema = models.ForeignKey(
+ schema: ForeignKey = ForeignKey(
verbose_name='Концептуальная схема',
to=RSForm,
- on_delete=models.CASCADE
+ on_delete=CASCADE
)
- order = models.PositiveIntegerField(
+ order: PositiveIntegerField = PositiveIntegerField(
verbose_name='Позиция',
validators=[MinValueValidator(1)],
default=-1,
)
- alias = models.CharField(
+ alias: CharField = CharField(
verbose_name='Имя',
max_length=8,
default='undefined'
)
- cst_type = models.CharField(
+ cst_type: CharField = CharField(
verbose_name='Тип',
max_length=10,
choices=CstType.choices,
default=CstType.BASE
)
- convention = models.TextField(
+ convention: TextField = TextField(
verbose_name='Комментарий/Конвенция',
default='',
blank=True
)
- term_raw = models.TextField(
+ term_raw: TextField = TextField(
verbose_name='Термин (с отсылками)',
default='',
blank=True
)
- term_resolved = models.TextField(
+ term_resolved: TextField = TextField(
verbose_name='Термин',
default='',
blank=True
)
- term_forms = models.JSONField(
+ term_forms: JSONField = JSONField(
verbose_name='Словоформы',
default=_empty_forms
)
- definition_formal = models.TextField(
+ definition_formal: TextField = TextField(
verbose_name='Родоструктурное определение',
default='',
blank=True
)
- definition_raw = models.TextField(
+ definition_raw: TextField = TextField(
verbose_name='Текстовое определние (с отсылками)',
default='',
blank=True
)
- definition_resolved = models.TextField(
+ definition_resolved: TextField = TextField(
verbose_name='Текстовое определние',
default='',
blank=True
@@ -342,9 +346,9 @@ class Constituenta(models.Model):
self.term_resolved = ''
self.term_forms = []
- def to_trs(self) -> str:
+ def to_trs(self) -> dict:
return {
- 'entityUID': self.id,
+ 'entityUID': self.pk,
'type': 'constituenta',
'cstType': self.cst_type,
'alias': self.alias,
diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py
index a45e7746..11b16fac 100644
--- a/rsconcept/backend/apps/rsform/serializers.py
+++ b/rsconcept/backend/apps/rsform/serializers.py
@@ -73,7 +73,7 @@ class RSFormDetailsSerlializer(serializers.BaseSerializer):
trs = pyconcept.check_schema(json.dumps(instance.to_trs()))
trs = trs.replace('entityUID', 'id')
result = json.loads(trs)
- result['id'] = instance.id
+ result['id'] = instance.pk
result['time_update'] = instance.time_update
result['time_create'] = instance.time_create
result['is_common'] = instance.is_common
@@ -101,7 +101,7 @@ class ConstituentaSerializer(serializers.ModelSerializer):
if 'definition_raw' in validated_data:
validated_data['definition_resolved'] = validated_data['definition_raw']
- result = super().update(instance, validated_data)
+ result: Constituenta = super().update(instance, validated_data)
instance.schema.save()
return result
diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py
index 6041f0b1..301074d3 100644
--- a/rsconcept/backend/apps/rsform/tests/t_views.py
+++ b/rsconcept/backend/apps/rsform/tests/t_views.py
@@ -16,7 +16,7 @@ from apps.rsform.views import (
def _response_contains(response, schema: RSForm) -> bool:
- return any(x for x in response.data if x['id'] == schema.id)
+ return any(x for x in response.data if x['id'] == schema.pk)
class TestConstituentaAPI(APITestCase):
@@ -25,8 +25,8 @@ class TestConstituentaAPI(APITestCase):
self.user = User.objects.create(username='UserTest')
self.client = APIClient()
self.client.force_authenticate(user=self.user)
- self.rsform_owned: RSForm = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
- self.rsform_unowned: RSForm = RSForm.objects.create(title='Test2', alias='T2')
+ self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
+ self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create(
alias='X1', schema=self.rsform_owned, order=1, convention='Test')
self.cst2 = Constituenta.objects.create(
@@ -87,8 +87,8 @@ class TestRSFormViewset(APITestCase):
self.user = User.objects.create(username='UserTest')
self.client = APIClient()
self.client.force_authenticate(user=self.user)
- self.rsform_owned: RSForm = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
- self.rsform_unowned: RSForm = RSForm.objects.create(title='Test2', alias='T2')
+ self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
+ self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2')
def test_create_anonymous(self):
self.client.logout()
@@ -131,7 +131,7 @@ class TestRSFormViewset(APITestCase):
def test_contents(self):
schema = RSForm.objects.create(title='Title1')
- schema.insert_last(alias='X1', type=CstType.BASE)
+ schema.insert_last(alias='X1', insert_type=CstType.BASE)
response = self.client.get(f'/api/rsforms/{schema.id}/contents/')
self.assertEqual(response.status_code, 200)
@@ -418,9 +418,9 @@ class TestLibraryAPI(APITestCase):
self.user = User.objects.create(username='UserTest')
self.client = APIClient()
self.client.force_authenticate(user=self.user)
- self.rsform_owned: RSForm = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
- self.rsform_unowned: RSForm = RSForm.objects.create(title='Test2', alias='T2')
- self.rsform_common: RSForm = RSForm.objects.create(title='Test3', alias='T3', is_common=True)
+ self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
+ self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2')
+ self.rsform_common = RSForm.objects.create(title='Test3', alias='T3', is_common=True)
def test_retrieve_common(self):
self.client.logout()
diff --git a/rsconcept/backend/apps/rsform/utils.py b/rsconcept/backend/apps/rsform/utils.py
index b987923f..8e372cdc 100644
--- a/rsconcept/backend/apps/rsform/utils.py
+++ b/rsconcept/backend/apps/rsform/utils.py
@@ -21,7 +21,8 @@ def read_trs(file) -> dict:
''' Read JSON from TRS file '''
with ZipFile(file, 'r') as archive:
json_data = archive.read('document.json')
- return json.loads(json_data)
+ result: dict = json.loads(json_data)
+ return result
def write_trs(json_data: dict) -> bytes:
diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py
index 97255456..b7b9a4b6 100644
--- a/rsconcept/backend/apps/rsform/views.py
+++ b/rsconcept/backend/apps/rsform/views.py
@@ -53,7 +53,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
ordering = '-time_update'
def _get_schema(self) -> models.RSForm:
- return self.get_object()
+ return self.get_object() # type: ignore
def perform_create(self, serializer):
if not self.request.user.is_anonymous and 'owner' not in self.request.POST:
@@ -114,7 +114,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request, pk):
''' Endpoint: Move multiple constituents. '''
- schema: models.RSForm = self._get_schema()
+ schema = self._get_schema()
serializer = serializers.CstMoveSerlializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
diff --git a/rsconcept/backend/cctext/__init__.py b/rsconcept/backend/cctext/__init__.py
index dc23dee3..7c60c16a 100644
--- a/rsconcept/backend/cctext/__init__.py
+++ b/rsconcept/backend/cctext/__init__.py
@@ -1,4 +1,5 @@
''' Concept core text processing library. '''
+# pylint: skip-file
from .syntax import RuSyntax, Capitalization
from .rumodel import Morphology, SemanticRole, WordTag, morpho
from .ruparser import PhraseParser, WordToken, Collation
diff --git a/rsconcept/backend/cctext/rumodel.py b/rsconcept/backend/cctext/rumodel.py
index cd8cae38..aad798db 100644
--- a/rsconcept/backend/cctext/rumodel.py
+++ b/rsconcept/backend/cctext/rumodel.py
@@ -1,6 +1,7 @@
''' Russian language models. '''
from __future__ import annotations
from enum import Enum, unique
+from typing import Iterable
from pymorphy2 import MorphAnalyzer
from pymorphy2.tagset import OpencorporaTag as WordTag
@@ -59,14 +60,14 @@ class Morphology:
return pos in ['ADJF', 'ADJS', 'PRTF', 'PRTS']
@property
- def effective_pos(self) -> str:
+ def effective_POS(self) -> str:
''' Access part of speech. Pronouns are considered as nouns '''
- pos = self.tag.POS
+ pos: str = self.tag.POS
if pos and self.tag.POS == 'NPRO':
return 'NOUN'
return pos
- def complete_tags(self, tags: frozenset[str]) -> set[str]:
+ def complete_tags(self, tags: Iterable[str]) -> set[str]:
''' Add missing tags before inflection. '''
result = set(tags)
pos = self.tag.POS
@@ -111,6 +112,7 @@ class Morphology:
if count == 0:
return ''
elif count == 1:
- return next(iter(grammemes))
+ result: str = next(iter(grammemes))
+ return result
else:
return ','.join(grammemes)
diff --git a/rsconcept/backend/cctext/ruparser.py b/rsconcept/backend/cctext/ruparser.py
index f9603989..7a8acd00 100644
--- a/rsconcept/backend/cctext/ruparser.py
+++ b/rsconcept/backend/cctext/ruparser.py
@@ -1,5 +1,6 @@
''' Parsing russian language using pymorphy2 and natasha libraries. '''
from __future__ import annotations
+from typing import Iterable, Optional
from razdel.substring import Substring as Segment
from pymorphy2.analyzer import Parse as WordForm
@@ -11,17 +12,16 @@ INDEX_NONE = -1
NO_COORDINATION = -1
WORD_NONE = -1
+Tags = Iterable[str]
+
class WordToken:
- ''' Minimal text token. '''
+ ''' Atomic text token. '''
def __init__(self, segment: Segment, forms: list[WordForm], main_form: int = 0):
self.segment: Segment = segment
self.forms: list[WordForm] = forms
self.main: int = main_form
- def __del__(self):
- pass
-
def get_morpho(self) -> Morphology:
''' Return morphology for current token. '''
return Morphology(self.get_form().tag)
@@ -30,7 +30,7 @@ class WordToken:
''' Access main form. '''
return self.forms[self.main]
- def inflect(self, inflection_tags: set[str]):
+ def inflect(self, inflection_tags: set[str]) -> Optional[WordForm]:
''' Apply inflection to segment text. Does not modify forms '''
inflected = self.get_form().inflect(inflection_tags)
if not inflected:
@@ -43,21 +43,20 @@ class Collation:
''' Parsed data for input coordinated text. '''
def __init__(self, text: str):
self.text = text
- self.words = []
- self.coordination = []
+ self.words: list[WordToken] = []
+ self.coordination: list[int] = []
self.main_word: int = WORD_NONE
- def __del__(self):
- pass
+ def is_valid(self) -> bool:
+ ''' Check if data is parsed correctly '''
+ return self.main_word != WORD_NONE
def get_form(self) -> WordForm:
- ''' Access main form. '''
+ ''' Access WordForm. '''
return self.words[self.main_word].get_form()
def get_morpho(self) -> Morphology:
''' Access parsed main mrophology. '''
- if self.main_word == WORD_NONE:
- return None
return self.words[self.main_word].get_morpho()
def add_word(self, segment, forms: list, main_form: int, need_coordination: bool = True):
@@ -65,28 +64,29 @@ class Collation:
self.words.append(WordToken(segment, forms, main_form))
self.coordination.append(NO_COORDINATION if not need_coordination else 0)
- def inflect(self, target_tags: frozenset[str]) -> str:
+ def inflect(self, target_tags: Tags) -> str:
''' Inflect text to match required tags. '''
- origin = self.get_morpho()
- if not origin or origin.tag.grammemes.issuperset(target_tags):
- return self.text
- if not self._apply_inflection(origin, target_tags):
- return self.text
- new_text = self._generate_text()
- return new_text
+ if self.is_valid():
+ origin = self.get_morpho()
+ if not origin.tag.grammemes.issuperset(target_tags):
+ if self._apply_inflection(origin, target_tags):
+ return self._generate_text()
+ return self.text
def inflect_like(self, base_model: Collation) -> str:
''' Create inflection to substitute base_model form. '''
- morph = base_model.get_morpho()
- if morph.effective_pos is None:
- return self.text
- tags = set()
- tags.add(morph.effective_pos)
- tags = morph.complete_tags(tags)
- return self.inflect(tags)
+ if self.is_valid():
+ morph = base_model.get_morpho()
+ if morph.effective_POS:
+ tags = set()
+ tags.add(morph.effective_POS)
+ tags = morph.complete_tags(tags)
+ return self.inflect(tags)
+ return self.text
def inflect_dependant(self, master_model: Collation) -> str:
''' Create inflection to coordinate with master_model form. '''
+ assert self.is_valid()
morph = master_model.get_morpho()
tags = morph.coordination_tags()
tags = self.get_morpho().complete_tags(tags)
@@ -94,12 +94,12 @@ class Collation:
def normal_form(self) -> str:
''' Generate normal form. '''
- main_form = self.get_form()
- if not main_form:
- return self.text
- new_morpho = Morphology(main_form.normalized.tag)
- new_tags = new_morpho.complete_tags(frozenset())
- return self.inflect(new_tags)
+ if self.is_valid():
+ main_form = self.get_form()
+ new_morpho = Morphology(main_form.normalized.tag)
+ new_tags = new_morpho.complete_tags(frozenset())
+ return self.inflect(new_tags)
+ return self.text
def _iterate_coordinated(self):
words_count = len(self.words)
@@ -108,21 +108,20 @@ class Collation:
yield self.words[current_word]
current_word += self.coordination[current_word]
- def _inflect_main_word(self, origin: Morphology, target_tags: frozenset[str]) -> Morphology:
+ def _inflect_main_word(self, origin: Morphology, target_tags: Tags) -> Optional[Morphology]:
full_tags = origin.complete_tags(target_tags)
inflected = self.words[self.main_word].inflect(full_tags)
if not inflected:
return None
return Morphology(inflected.tag)
- def _apply_inflection(self, origin: Morphology, target_tags: frozenset[str]) -> bool:
+ def _apply_inflection(self, origin: Morphology, target_tags: Tags) -> bool:
new_moprho = self._inflect_main_word(origin, target_tags)
if not new_moprho:
return False
inflection_tags = new_moprho.coordination_tags()
if len(inflection_tags) == 0:
return True
-
for word in self._iterate_coordinated():
word.inflect(inflection_tags)
return True
@@ -155,13 +154,17 @@ class PhraseParser:
_MAIN_WAIT_LIMIT = 10 # count words untill fixing main
_MAIN_MAX_FOLLOWERS = 3 # count words after main as coordination candidates
- def parse(self, text: str, require_index: int = INDEX_NONE, require_tags: frozenset[str] = None) -> Collation:
- ''' Determine morpho tags for input text.
- ::returns:: Morphology of a text or None if no suitable form is available '''
- if text == '':
- return None
+ def parse(self, text: str,
+ require_index: int = INDEX_NONE,
+ require_tags: Optional[Tags] = None) -> Optional[Collation]:
+ '''
+ Determine morpho tags for input text.
+ ::returns:: Morphology of a text or None if no suitable form is available
+ '''
segments = list(RuSyntax.tokenize(text))
- if len(segments) == 1:
+ if len(segments) == 0:
+ return None
+ elif len(segments) == 1:
return self._parse_single(segments[0], require_index, require_tags)
else:
return self._parse_multiword(text, segments, require_index, require_tags)
@@ -169,9 +172,9 @@ class PhraseParser:
def normalize(self, text: str):
''' Get normal form for target text. '''
processed = self.parse(text)
- if not processed:
- return text
- return processed.normal_form()
+ if processed:
+ return processed.normal_form()
+ return text
def find_substr(self, text: str, sub: str) -> tuple[int, int]:
''' Search for substring position in text regardless of morphology. '''
@@ -234,7 +237,7 @@ class PhraseParser:
return dependant_normal
return dependant_model.inflect_dependant(master_model)
- def _parse_single(self, segment, require_index: int, require_tags: frozenset[str]) -> Collation:
+ def _parse_single(self, segment, require_index: int, require_tags: Optional[Tags]) -> Optional[Collation]:
forms = list(self._filtered_parse(segment.text))
parse_index = INDEX_NONE
if len(forms) == 0 or require_index >= len(forms):
@@ -266,9 +269,10 @@ class PhraseParser:
result.main_word = 0
return result
- def _parse_multiword(self, text: str, segments: list, require_index: int, require_tags: frozenset[str]):
+ def _parse_multiword(self, text: str, segments: list, require_index: int,
+ require_tags: Optional[Tags]) -> Optional[Collation]:
result = Collation(text)
- priority_main = self._PRIORITY_NONE
+ priority_main: float = self._PRIORITY_NONE
segment_index = 0
main_wait = 0
word_index = 0
@@ -295,20 +299,20 @@ class PhraseParser:
output: Collation,
segment: Segment,
require_index: int,
- require_tags: frozenset[str]) -> float:
+ require_tags: Optional[Tags]) -> Optional[float]:
''' Return priority for this can be a new main word '''
forms = list(self._filtered_parse(segment.text))
if len(forms) == 0:
return None
- main_index = INDEX_NONE
- segment_score = self._PRIORITY_NONE
+ main_index: int = INDEX_NONE
+ segment_score: float = self._PRIORITY_NONE
needs_coordination = False
- local_sum = 0
- score_sum = 0
+ local_sum: float = 0
+ score_sum: float = 0
if require_index != INDEX_NONE:
form = forms[require_index]
if not require_tags or form.tag.grammemes.issuperset(require_tags):
- (local_max, segment_score) = PhraseParser._get_priority_for(form.tag)
+ (local_max, segment_score) = PhraseParser._get_priorities_for(form.tag)
main_index = require_index
needs_coordination = Morphology.is_dependable(form.tag.POS)
else:
@@ -316,7 +320,7 @@ class PhraseParser:
for (index, form) in enumerate(forms):
if require_tags and not form.tag.grammemes.issuperset(require_tags):
continue
- (local_priority, global_priority) = PhraseParser._get_priority_for(form.tag)
+ (local_priority, global_priority) = PhraseParser._get_priorities_for(form.tag)
needs_coordination = needs_coordination or Morphology.is_dependable(form.tag.POS)
local_sum += global_priority * form.score
score_sum += form.score
@@ -414,7 +418,8 @@ class PhraseParser:
yield form
@staticmethod
- def _parse_word(text: str, require_index: int = INDEX_NONE, require_tags: frozenset[str] = None) -> Morphology:
+ def _parse_word(text: str, require_index: int = INDEX_NONE,
+ require_tags: Optional[Tags] = None) -> Optional[Morphology]:
parsed_variants = morpho.parse(text)
if not parsed_variants or require_index >= len(parsed_variants):
return None
@@ -432,7 +437,7 @@ class PhraseParser:
return None
@staticmethod
- def _get_priority_for(tag: WordTag) -> tuple[float, float]:
+ def _get_priorities_for(tag: WordTag) -> tuple[float, float]:
''' Return pair of local and global priorities. '''
if tag.POS in ['VERB', 'INFN']:
return (9, 10)
@@ -447,7 +452,9 @@ class PhraseParser:
return (0, 0)
@staticmethod
- def _choose_context_etalon(target: Morphology, before: Collation, after: Collation) -> Collation:
+ def _choose_context_etalon(target: Morphology,
+ before: Optional[Collation],
+ after: Optional[Collation]) -> Optional[Collation]:
if not before or not before.get_morpho().can_coordinate:
return after
if not after or not after.get_morpho().can_coordinate:
@@ -473,7 +480,7 @@ class PhraseParser:
return before
@staticmethod
- def _combine_morpho(target: Morphology, etalon: WordTag) -> str:
+ def _combine_morpho(target: Morphology, etalon: WordTag) -> frozenset[str]:
part_of_speech = target.tag.POS
number = etalon.number
if number == 'plur':
diff --git a/rsconcept/backend/cctext/syntax.py b/rsconcept/backend/cctext/syntax.py
index ff3c5eb9..b3bb593d 100644
--- a/rsconcept/backend/cctext/syntax.py
+++ b/rsconcept/backend/cctext/syntax.py
@@ -68,7 +68,11 @@ class RuSyntax:
''' Test if text is a single word. '''
try:
gen = tokenize(text)
- return next(gen) == '' or next(gen) == ''
+ if next(gen) == '':
+ return True
+ if next(gen) == '':
+ return True
+ return False
except StopIteration:
return True
diff --git a/rsconcept/backend/cctext/tests/testRuParser.py b/rsconcept/backend/cctext/tests/testRuParser.py
index 2df9a7d0..0a98013b 100644
--- a/rsconcept/backend/cctext/tests/testRuParser.py
+++ b/rsconcept/backend/cctext/tests/testRuParser.py
@@ -1,6 +1,7 @@
''' Test russian language parsing. '''
import unittest
+from typing import Iterable, Optional
from cctext import PhraseParser
parser = PhraseParser()
@@ -9,16 +10,20 @@ parser = PhraseParser()
class TestRuParser(unittest.TestCase):
''' Test class for russian parsing. '''
- def _assert_parse(self, text: str, expected: list[str], require_index: int = -1, require_tags: list[str] = None):
+ def _assert_parse(self, text: str, expected: list[str],
+ require_index: int = -1,
+ require_tags: Optional[Iterable[str]] = None):
phrase = parser.parse(text, require_index, require_tags)
- self.assertEqual(phrase.get_morpho().tag.grammemes, set(expected))
+ self.assertIsNotNone(phrase)
+ if phrase:
+ self.assertEqual(phrase.get_morpho().tag.grammemes, set(expected))
def _assert_inflect(self, text: str, tags: list[str], expected: str):
model = parser.parse(text)
if not model:
result = text
else:
- result = model.inflect(set(tags))
+ result = model.inflect(frozenset(tags))
self.assertEqual(result, expected)
def test_parse_word(self):
diff --git a/rsconcept/backend/mypy.ini b/rsconcept/backend/mypy.ini
new file mode 100644
index 00000000..98caa4a0
--- /dev/null
+++ b/rsconcept/backend/mypy.ini
@@ -0,0 +1,23 @@
+# Global options:
+
+[mypy]
+warn_return_any = True
+warn_unused_configs = True
+
+plugins = mypy_drf_plugin.main, mypy_django_plugin.main
+
+# Per-module options:
+[mypy.plugins.django-stubs]
+django_settings_module = "project.settings"
+
+[mypy-django_filters.*]
+ignore_missing_imports = True
+
+[mypy-pyconcept.*]
+ignore_missing_imports = True
+
+[mypy-razdel.*]
+ignore_missing_imports = True
+
+[mypy-pymorphy2.*]
+ignore_missing_imports = True
\ No newline at end of file
diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py
index d9c7b91e..d1991044 100644
--- a/rsconcept/backend/project/settings.py
+++ b/rsconcept/backend/project/settings.py
@@ -123,7 +123,7 @@ DATABASES = {
# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
-AUTH_PASSWORD_VALIDATORS = [
+AUTH_PASSWORD_VALIDATORS: list[str] = [
# NOTE: Password validators disabled
# {
# 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
diff --git a/rsconcept/backend/requirements_dev.txt b/rsconcept/backend/requirements_dev.txt
index 3d2c5107..7a1cfcb7 100644
--- a/rsconcept/backend/requirements_dev.txt
+++ b/rsconcept/backend/requirements_dev.txt
@@ -9,5 +9,8 @@ pymorphy2-dicts-ru
pymorphy2-dicts-uk
razdel
+mypy
pylint
-coverage
\ No newline at end of file
+coverage
+django-stubs[compatible-mypy]
+djangorestframework-stubs[compatible-mypy]
\ No newline at end of file