Transfering project

This commit is contained in:
IRBorisov 2024-04-10 20:54:12 +03:00
parent 5831bf4c9e
commit cc94f805b8
33 changed files with 5367 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
~$*
.vs
.pytest_cache
__pycache__
build/
whl/
venv/
output/
*egg-info
nosetests.xml

635
.pylintrc Normal file
View File

@ -0,0 +1,635 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=pyconcept
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\\' represents the directory delimiter on Windows systems,
# it can't be used as an escape character.
ignore-paths=t_.*,.*migrations.*
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=t_.*?py
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.9
# Discover python modules and packages in the file system subtree.
recursive=no
# Add paths to the list of the source roots. Supports globbing patterns. The
# source root is an absolute path or a path relative to the current working
# directory used to determine a package namespace for modules located under the
# source root.
source-roots=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type alias names. If left empty, type
# alias names will be checked with the set naming style.
#typealias-rgx=
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
asyncSetUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=no
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=too-many-public-methods,
invalid-name,
no-else-break,
no-else-continue,
no-else-return,
no-member,
too-many-ancestors,
too-many-return-statements,
too-many-locals,
too-many-instance-attributes,
too-few-public-methods,
unused-argument,
missing-function-docstring,
attribute-defined-outside-init,
ungrouped-imports,
abstract-method
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
max-line-length=120
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. No available dictionaries : You need to install
# both the python package and the system dependency for enchant to work..
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io

22
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Build",
"type": "PowerShell",
"request": "launch",
"script": "${workspaceFolder}/scripts/Build.ps1",
"args": []
},
{
"name": "Lint",
"type": "PowerShell",
"request": "launch",
"script": "${workspaceFolder}/scripts/RunLint.ps1",
"args": []
}
]
}

40
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,40 @@
{
"search.exclude": {
".mypy_cache/": true,
".pytest_cache/": true
},
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"python.analysis.typeCheckingMode": "off",
"python.analysis.ignore": ["**/tests/**", "**/node_modules/**", "**/venv/**"],
"cSpell.words": [
"ADJF",
"ADJS",
"ADVB",
"Grammemes",
"GRND",
"INFN",
"INTJ",
"multiword",
"NPRO",
"NUMR",
"Opencorpora",
"PNCT",
"PRCL",
"PRTF",
"PRTS",
"pymorphy",
"razdel",
"rumodel",
"tagset",
"unknwn"
],
"cSpell.language": "en,ru",
"cSpell.ignorePaths": ["node_modules/**", "*.json"]
}

1
README.md Normal file
View File

@ -0,0 +1 @@
Long description goes here

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.1.0

16
cctext/__init__.py Normal file
View File

@ -0,0 +1,16 @@
''' Concept core text processing library. '''
# pylint: skip-file
from .syntax import RuSyntax, Capitalization
from .rumodel import Morphology, SemanticRole, WordTag, morpho, split_grams, combine_grams
from .ruparser import PhraseParser, WordToken, Collation
from .reference import EntityReference, ReferenceType, SyntacticReference, parse_reference
from .context import TermForm, Entity, TermContext
from .resolver import Reference, Position, Resolver, ResolvedReference, resolve_entity, resolve_syntactic, extract_entities
from .conceptapi import (
parse, normalize,
generate_lexeme, inflect, inflect_context, inflect_substitute, inflect_dependant,
match_all_morpho, find_substr
)
# TODO: implement Part of speech transition for VERB <-> NOUN

90
cctext/conceptapi.py Normal file
View File

@ -0,0 +1,90 @@
'''
Concept API Python functions.
::guarantee:: doesn't raise exceptions and returns workable outputs
'''
from cctext.rumodel import Morphology
from .syntax import RuSyntax
from .ruparser import PhraseParser
from .rumodel import split_grams
parser = PhraseParser()
def parse(text: str, require_grams: str = '') -> str:
''' Determine morpho tags for input text.
::returns:: string of comma separated grammar tags or empty string '''
model = parser.parse(text, require_grams=split_grams(require_grams))
if model is None:
return ''
result = model.get_morpho().to_text()
return result if result != 'UNKN' else ''
# def parse_variants(text: str, require_grams: str = '') -> list[tuple[str, str]]:
# ''' Get all variants of a parse.
# ::returns:: string of comma separated grammar tags or empty string '''
def generate_lexeme(text_normal: str) -> list[tuple[str, str]]:
''' Get all inflected forms belonging to same Lexeme. '''
model = parser.parse(text_normal)
if not model:
return []
result = []
for form in model.get_form().lexeme:
result.append((model.inflect(form.tag.grammemes), Morphology(form.tag).to_text()))
return result
def normalize(text: str) -> str:
''' Generate normal form.
::returns:: normal form of input text or text itself if no parse is available '''
model = parser.parse(text)
if model is None:
return text
return model.normal_form()
def inflect(text: str, target_grams: str) -> str:
''' Inflect text to match required tags.
::returns:: infected text or initial text if infection failed '''
target_set = split_grams(target_grams)
model = parser.parse(text)
if model is None:
return text
return model.inflect(target_set)
def inflect_context(target: str, before: str = '', after: str = '') -> str:
''' Inflect text in accordance to context before and after. '''
return parser.inflect_context(target, before, after)
def inflect_substitute(substitute_normal: str, original: str) -> str:
''' Inflect substitute to match original form. '''
return parser.inflect_substitute(substitute_normal, original)
def inflect_dependant(dependant_normal: str, master: str) -> str:
''' Inflect dependant to coordinate with master text. '''
return parser.inflect_dependant(dependant_normal, master)
def match_all_morpho(text: str, filter_grams: str) -> list[list[int]]:
''' Search for all words corresponding to tags. '''
target_set = split_grams(filter_grams)
if len(target_set) == 0:
return []
result = []
for elem in RuSyntax.tokenize(text):
model = parser.parse(elem.text, require_grams=target_set)
if model:
result.append([elem.start, elem.stop])
return result
def find_substr(text: str, sub: str) -> tuple[int, int]:
''' Search for substring position in text regardless of morphology. '''
return parser.find_substr(text, sub)

84
cctext/context.py Normal file
View File

@ -0,0 +1,84 @@
''' Term context for reference resolution. '''
from typing import Iterable, Optional, TypedDict
from .ruparser import PhraseParser
from .rumodel import WordTag
parser = PhraseParser()
class TermForm(TypedDict):
''' Represents term in a specific form. '''
text: str
grams: Iterable[str]
def _match_grams(query: Iterable[str], test: Iterable[str]) -> bool:
''' Check if grams from test fit query. '''
for gram in test:
if not gram in query:
if not gram in WordTag.PARTS_OF_SPEECH:
return False
for pos in WordTag.PARTS_OF_SPEECH:
if pos in query:
return False
return True
def _search_form(query: Iterable[str], data: Iterable[TermForm]) -> Optional[str]:
for form in data:
if _match_grams(query, form['grams']):
return form['text']
return None
class Entity:
''' Represents text entity. '''
def __init__(self, alias: str, nominal: str, manual_forms: Optional[Iterable[TermForm]]=None):
if manual_forms is None:
self.manual = []
else:
self.manual = list(manual_forms)
self.alias = alias
self._nominal = nominal
self._cached: list[TermForm] = []
def get_nominal(self) -> str:
''' Getter for _nominal. '''
return self._nominal
def set_nominal(self, new_text: str):
''' Setter for _nominal.
Note: clears manual and cached forms. '''
if self._nominal == new_text:
return
self._nominal = new_text
self.manual = []
self._cached = []
def get_form(self, grams: Iterable[str]) -> str:
''' Get specific term form. '''
if all(False for _ in grams):
return self._nominal
text = _search_form(grams, self.manual)
if text is not None:
return text
text = _search_form(grams, self._cached)
if text is not None:
return text
model = parser.parse(self._nominal)
if model is None:
text = self._nominal
else:
try:
text = model.inflect(grams)
except ValueError as error:
text = f'!{error}!'.replace('Unknown grammeme', 'Неизвестная граммема')
self._cached.append({'text': text, 'grams': grams})
return text
# Represents term context for resolving entity references.
TermContext = dict[str, Entity]

60
cctext/reference.py Normal file
View File

@ -0,0 +1,60 @@
''' Text reference API. '''
from enum import Enum, unique
from typing import Optional, Union
@unique
class ReferenceType(Enum):
''' Text reference types. '''
entity = 'entity'
syntactic = 'syntax'
class EntityReference:
''' Reference to entity. '''
def __init__(self, identifier: str, form: str):
self.entity = identifier
self.form = form
def get_type(self) -> ReferenceType:
return ReferenceType.entity
def to_text(self) -> str:
return f'@{{{self.entity}|{self.form}}}'
class SyntacticReference:
''' Reference to syntactic dependency on EntityReference. '''
def __init__(self, referral_offset: int, text: str):
self.nominal = text
self.offset = referral_offset
def get_type(self) -> ReferenceType:
return ReferenceType.syntactic
def to_text(self) -> str:
return f'@{{{self.offset}|{self.nominal}}}'
Reference = Union[EntityReference, SyntacticReference]
def parse_reference(text: str) -> Optional[Reference]:
if len(text) < 4 or text[-1] != '}' or text[0:2] != '@{':
return None
blocks: list[str] = [block.strip() for block in text[2:-1].split('|')]
if len(blocks) != 2 or blocks[0] == '' or blocks[0][0] in '0':
return None
if blocks[0][0] in '-123456789':
if blocks[1] == '':
return None
try:
offset = int(blocks[0])
return SyntacticReference(offset, blocks[1])
except ValueError:
return None
else:
form = blocks[1].replace(' ', '')
return EntityReference(blocks[0], form)

140
cctext/resolver.py Normal file
View File

@ -0,0 +1,140 @@
''' Reference resolution API. '''
import re
from typing import cast, Optional
from dataclasses import dataclass
from .rumodel import split_grams
from .conceptapi import inflect_dependant
from .context import TermContext
from .reference import EntityReference, SyntacticReference, parse_reference, Reference
_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-][^\}\|\{]*?)\|([^\}\|\{]*?)}')
def extract_entities(text: str) -> list[str]:
''' Extract list of entities that are referenced. '''
result: list[str] = []
for segment in re.finditer(_REF_ENTITY_PATTERN, text):
entity = segment.group(1)
if entity not in result:
result.append(entity)
return result
def resolve_entity(ref: EntityReference, context: TermContext) -> str:
''' Resolve entity reference. '''
alias = ref.entity
if alias not in context:
return f'!Неизвестная сущность: {alias}!'
grams = split_grams(ref.form)
resolved = context[alias].get_form(grams)
if resolved == '':
return f'!Отсутствует термин: {alias}!'
else:
return resolved
def resolve_syntactic(ref: SyntacticReference, index: int, references: list['ResolvedReference']) -> str:
''' Resolve syntactic reference. '''
offset = ref.offset
master: Optional['ResolvedReference'] = None
if offset > 0:
index += 1
while index < len(references):
if isinstance(references[index].ref, EntityReference):
if offset == 1:
master = references[index]
else:
offset -= 1
index += 1
else:
index -= 1
while index >= 0:
if isinstance(references[index].ref, EntityReference):
if offset == -1:
master = references[index]
else:
offset += 1
index -= 1
if master is None:
return f'!Некорректное смещение: {ref.offset}!'
return inflect_dependant(ref.nominal, master.resolved)
@dataclass
class Position:
''' 0-indexed contiguous segment position in text. '''
start: int = 0
finish: int = 0
def __hash__(self) -> int:
return hash((self.start, self.finish))
@dataclass
class ResolvedReference:
''' Resolved reference data '''
ref: Reference
resolved: str = ''
pos_input: Position = Position()
pos_output: Position = Position()
def __hash__(self) -> int:
return hash((self.resolved, self.pos_input, self.pos_output, self.ref.to_text()))
class Resolver:
''' Text reference resolver '''
REFERENCE_PATTERN = re.compile(r'@{[^\}\{]*?}')
def __init__(self, context: TermContext):
self.context = context
self.refs = cast(list[ResolvedReference], [])
self.input = ''
self.output = ''
def resolve(self, text: str) -> str:
''' Resolve references in input text.
Note: data on references positions is accessed through class attributes '''
self._reset(text)
self._parse_refs()
if len(self.refs) == 0:
self.output = self.input
return self.output
else:
self._resolve_refs()
self._combine_output()
return self.output
def _reset(self, input_text: str):
self.refs = cast(list[ResolvedReference], [])
self.input = input_text
self.output = ''
def _parse_refs(self):
for segment in re.finditer(Resolver.REFERENCE_PATTERN, self.input):
parse = parse_reference(segment[0])
if parse is not None:
ref_info = ResolvedReference(ref=parse,
resolved='',
pos_input=Position(segment.start(0), segment.end(0)))
self.refs.append(ref_info)
def _resolve_refs(self):
for ref in self.refs:
if isinstance(ref.ref, EntityReference):
ref.resolved = resolve_entity(ref.ref, self.context)
for (index, ref) in enumerate(self.refs):
if isinstance(ref.ref, SyntacticReference):
ref.resolved = resolve_syntactic(ref.ref, index, self.refs)
def _combine_output(self):
pos_in = 0
for ref in self.refs:
self.output += self.input[pos_in : ref.pos_input.start]
self.output += ref.resolved
ref.pos_output = Position(len(self.output) - len(ref.resolved), len(self.output))
pos_in = ref.pos_input.finish
self.output += self.input[pos_in : len(self.input)]

118
cctext/rumodel.py Normal file
View File

@ -0,0 +1,118 @@
''' Russian language models. '''
from __future__ import annotations
from enum import Enum, unique
from typing import Iterable, Optional
from pymorphy3 import MorphAnalyzer
from pymorphy3.tagset import OpencorporaTag as WordTag
# ''' Morphology parser. '''
morpho = MorphAnalyzer()
Grammemes = Iterable[str]
def split_grams(text: str) -> list[str]:
''' Split grammemes string into set of items. '''
return [tag.strip() for tag in filter(None, text.split(','))]
def combine_grams(tags: Iterable[str]) -> str:
''' Combine grammemes into string. '''
return ','.join(tags)
@unique
class SemanticRole(Enum):
''' Enumerating semantic types for different parse patterns. '''
unknwn = 0
term = 1
action = 2
definition = 3
@staticmethod
def from_POS(pos: Optional[str]) -> SemanticRole:
''' Production method: types from part of speech. '''
if pos in ['NOUN', 'NPRO']:
return SemanticRole.term
elif pos in ['VERB', 'INFN', 'PRTF', 'PRTS']:
return SemanticRole.action
elif pos in ['ADJF', 'ADJS']:
return SemanticRole.definition
return SemanticRole.unknwn
class Morphology:
''' Wrapper for OpencorporaTag expanding functionality for multiword.
Full morphology tags see http://opencorpora.org/dict.php?act=gram
'''
def __init__(self, tag: WordTag, semantic=SemanticRole.unknwn):
self.tag = tag
self.semantic = semantic if semantic != SemanticRole.unknwn else SemanticRole.from_POS(tag.POS)
_TAGS_IMMUTABLE = frozenset(['INFN', 'ADVB', 'COMP', 'PNCT', 'PREP', 'CONJ', 'PRCL', 'INTJ'])
_TAGS_NO_TENSE = frozenset(['NOUN', 'NPRO', 'ADJF', 'ADJS'])
_TAGS_NO_CASE = frozenset(['GRND', 'VERB', 'ADJS', 'PRTS'])
_TAGS_NO_NUMBER = frozenset(['GRND'])
_TAGS_NO_GENDER = frozenset(['GRND', 'NOUN', 'NPRO', 'plur'])
_TAGS_NO_PERSON = frozenset(['GRND', 'NOUN', 'ADJF', 'ADJS', 'PRTF', 'PRTS', 'past'])
@property
def can_coordinate(self) -> bool:
''' Check if coordination can change text. '''
return self.tag.POS in ['NOUN', 'NPRO', 'NUMR', 'ADJF', 'ADJS', 'PRTF', 'PRTS']
@staticmethod
def is_dependable(pos: str):
''' Check if this morphology can be dependant. '''
return pos in ['ADJF', 'ADJS', 'PRTF', 'PRTS']
@property
def effective_POS(self) -> Optional[str]:
''' Access part of speech. Pronouns are considered as nouns '''
pos: Optional[str] = self.tag.POS
if pos and self.tag.POS == 'NPRO':
return 'NOUN'
return pos
def complete_grams(self, grams: Iterable[str]) -> set[str]:
''' Add missing tags before inflection. '''
result = set(grams)
pos = self.tag.POS
if pos and result.isdisjoint(WordTag.PARTS_OF_SPEECH):
result.add(pos if pos != 'INFN' or len(result) == 0 else 'VERB')
if not result.isdisjoint(self._TAGS_IMMUTABLE):
return result
if self.tag.case and result.isdisjoint(WordTag.CASES) and result.isdisjoint(self._TAGS_NO_CASE):
result.add(self.tag.case)
if self.tag.tense and result.isdisjoint(WordTag.TENSES) and result.isdisjoint(self._TAGS_NO_TENSE):
if (self.tag.tense != 'past' or result.isdisjoint(WordTag.PERSONS)) \
and (self.tag.tense != 'pres' or result.isdisjoint(WordTag.GENDERS)):
result.add(self.tag.tense)
if self.tag.number and result.isdisjoint(WordTag.NUMBERS) and result.isdisjoint(self._TAGS_NO_NUMBER):
if self.tag.number != 'plur' or result.isdisjoint(WordTag.GENDERS):
result.add(self.tag.number)
if self.tag.gender and result.isdisjoint(WordTag.GENDERS) and result.isdisjoint(self._TAGS_NO_GENDER):
if 'PRTF' in result or 'pres' not in result:
result.add(self.tag.gender)
if self.tag.person and result.isdisjoint(WordTag.PERSONS) and result.isdisjoint(self._TAGS_NO_PERSON):
result.add(self.tag.person)
if 'plur' in result and not result.isdisjoint(WordTag.GENDERS):
result = result.difference(WordTag.GENDERS)
return result
def coordination_grams(self) -> set[str]:
''' Return set of grammemes for inflection to keep coordination . '''
result = set()
if self.tag.case:
result.add(self.tag.case)
if self.tag:
number = self.tag.number
result.add(number)
if self.tag.gender and 'plur' not in result:
result.add(self.tag.gender)
return result
def to_text(self) -> str:
''' Produce string of all grammemes. '''
return combine_grams(self.tag.grammemes)

486
cctext/ruparser.py Normal file
View File

@ -0,0 +1,486 @@
''' Parsing russian language using pymorphy3 library. '''
from __future__ import annotations
from typing import Optional
from razdel.substring import Substring as Segment
from pymorphy3.analyzer import Parse as WordParse
from .syntax import RuSyntax, Capitalization
from .rumodel import SemanticRole, Morphology, WordTag, morpho, Grammemes
INDEX_NONE = -1
NO_COORDINATION = -1
WORD_NONE = -1
class WordToken:
''' Atomic text token. '''
def __init__(self, segment: Segment, parse: list[WordParse], main_parse: int = 0):
self.segment: Segment = segment
self.forms: list[WordParse] = parse
self.main: int = main_parse
def get_morpho(self) -> Morphology:
''' Return morphology for current token. '''
return Morphology(self.get_parse().tag)
def get_parse(self) -> WordParse:
''' Access main form. '''
return self.forms[self.main]
def inflect(self, inflection_grams: set[str]) -> Optional[WordParse]:
''' Apply inflection to segment text. Does not modify forms '''
inflected = self.get_parse().inflect(inflection_grams)
if not inflected:
return None
self.segment.text = Capitalization.from_text(self.segment.text).apply_to(inflected.word)
return inflected
class Collation:
''' Parsed data for input coordinated text. '''
def __init__(self, text: str):
self.text = text
self.words: list[WordToken] = []
self.coordination: list[int] = []
self.main_word: int = WORD_NONE
def is_valid(self) -> bool:
''' Check if data is parsed correctly '''
return self.main_word != WORD_NONE
def get_form(self) -> WordParse:
''' Access WordParse. '''
return self.words[self.main_word].get_parse()
def get_morpho(self) -> Morphology:
''' Access parsed main morphology. '''
return self.words[self.main_word].get_morpho()
def add_word(self, segment, forms: list, main_form: int, need_coordination: bool = True):
''' Add word information. '''
self.words.append(WordToken(segment, forms, main_form))
self.coordination.append(NO_COORDINATION if not need_coordination else 0)
def inflect(self, target_grams: Grammemes) -> str:
''' Inflect text to match required tags. '''
if self.is_valid():
origin = self.get_morpho()
if not origin.tag.grammemes.issuperset(target_grams):
if self._apply_inflection(origin, target_grams):
return self._generate_text()
return self.text
def inflect_like(self, base_model: Collation) -> str:
''' Create inflection to substitute base_model form. '''
if self.is_valid():
morph = base_model.get_morpho()
if morph.effective_POS:
tags = set()
tags.add(morph.effective_POS)
tags = morph.complete_grams(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_grams()
tags = self.get_morpho().complete_grams(tags)
return self.inflect(tags)
def normal_form(self) -> str:
''' Generate normal form. '''
if self.is_valid():
main_form = self.get_form()
new_morpho = Morphology(main_form.normalized.tag)
new_grams = new_morpho.complete_grams(frozenset())
return self.inflect(new_grams)
return self.text
def _iterate_coordinated(self):
words_count = len(self.words)
current_word = self.coordination[words_count]
while current_word != words_count:
yield self.words[current_word]
current_word += self.coordination[current_word]
def _inflect_main_word(self, origin: Morphology, target_grams: Grammemes) -> Optional[Morphology]:
full_grams = origin.complete_grams(target_grams)
inflected = self.words[self.main_word].inflect(full_grams)
if not inflected:
return None
return Morphology(inflected.tag)
def _apply_inflection(self, origin: Morphology, target_grams: Grammemes) -> bool:
new_moprho = self._inflect_main_word(origin, target_grams)
if not new_moprho:
return False
inflection_grams = new_moprho.coordination_grams()
if len(inflection_grams) == 0:
return True
for word in self._iterate_coordinated():
word.inflect(inflection_grams)
return True
def _generate_text(self) -> str:
current_pos = 0
result = ''
for token in self.words:
if token.segment.start > current_pos:
result += self.text[current_pos: token.segment.start]
result += token.segment.text
current_pos = token.segment.stop
if current_pos + 1 < len(self.text):
result += self.text[current_pos:]
return result
class PhraseParser:
''' Russian grammar parser. '''
def __init__(self):
pass
def __del__(self):
pass
_FILTER_SCORE = 0.005
_SINGLE_SCORE_SEARCH = 0.2
_PRIORITY_NONE = NO_COORDINATION
_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,
require_index: int = INDEX_NONE,
require_grams: Optional[Grammemes] = 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) == 0:
return None
elif len(segments) == 1:
return self._parse_single(segments[0], require_index, require_grams)
else:
return self._parse_multiword(text, segments, require_index, require_grams)
def normalize(self, text: str):
''' Get normal form for target text. '''
processed = self.parse(text)
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. '''
if not text or not sub:
return (0, 0)
query = [self.normalize(elem.text) for elem in RuSyntax.tokenize(sub)]
query_len = len(query)
start = 0
current_index = 0
for token in RuSyntax.tokenize(text):
text_word = self.normalize(token.text)
if text_word != query[current_index]:
current_index = 0
else:
if current_index == 0:
start = token.start
current_index += 1
if current_index == query_len:
return (start, token.stop)
return (0, 0)
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:
return text
target_morpho = target.get_morpho()
if not target_morpho or not target_morpho.can_coordinate:
return text
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
etalon_moprho = etalon.get_morpho()
if not etalon_moprho.can_coordinate:
return text
new_form = PhraseParser._combine_morpho(target_morpho, etalon_moprho.tag)
return target.inflect(new_form)
def inflect_substitute(self, substitute_normal: str, original: str) -> str:
''' Inflect substitute to match original form. '''
original_model = self.parse(original)
if not original_model:
return substitute_normal
substitute_model = self.parse(substitute_normal)
if not substitute_model:
return substitute_normal
return substitute_model.inflect_like(original_model)
def inflect_dependant(self, dependant_normal: str, master: str) -> str:
''' Inflect dependant to coordinate with master text. '''
master_model = self.parse(master)
if not master_model:
return dependant_normal
dependant_model = self.parse(dependant_normal)
if not dependant_model:
return dependant_normal
return dependant_model.inflect_dependant(master_model)
def _parse_single(self, segment, require_index: int, require_grams: Optional[Grammemes]) -> Optional[Collation]:
forms = list(self._filtered_parse(segment.text))
parse_index = INDEX_NONE
if len(forms) == 0 or require_index >= len(forms):
return None
if require_index != INDEX_NONE:
tags = forms[require_index].tag
if require_grams and not tags.grammemes.issuperset(require_grams):
return None
parse_index = require_index
else:
current_score = 0
for (index, form) in enumerate(forms):
if not require_grams or form.tag.grammemes.issuperset(require_grams):
if form.tag.case == 'nomn':
parse_index = index
break
elif parse_index == INDEX_NONE:
current_score = form.score
parse_index = index
elif form.score / current_score < self._SINGLE_SCORE_SEARCH:
break
if parse_index == INDEX_NONE:
return None
result = Collation(segment.text)
result.add_word(segment, [forms[parse_index]], main_form=0, need_coordination=False)
result.coordination.append(len(result.words))
result.main_word = 0
return result
def _parse_multiword(self, text: str, segments: list, require_index: int,
require_grams: Optional[Grammemes]) -> Optional[Collation]:
result = Collation(text)
priority_main: float = self._PRIORITY_NONE
segment_index = 0
main_wait = 0
word_index = 0
for segment in segments:
if main_wait > PhraseParser._MAIN_WAIT_LIMIT:
break
segment_index += 1
priority = self._parse_segment(result, segment, require_index, require_grams)
if priority is None:
continue # skip non-parsable entities
main_wait += 1
if priority > priority_main:
result.main_word = word_index
priority_main = priority
word_index += 1
if result.main_word == INDEX_NONE:
return None
self._finalize_coordination(result)
if segment_index < len(segments):
pass # finish to parse segments after main if needed
return result
def _parse_segment(self,
output: Collation,
segment: Segment,
require_index: int,
require_grams: Optional[Grammemes]) -> 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: int = INDEX_NONE
segment_score: float = self._PRIORITY_NONE
needs_coordination = False
local_sum: float = 0
score_sum: float = 0
if require_index != INDEX_NONE:
form = forms[require_index]
if not require_grams or form.tag.grammemes.issuperset(require_grams):
(local_max, segment_score) = PhraseParser._get_priorities_for(form.tag)
main_index = require_index
needs_coordination = Morphology.is_dependable(form.tag.POS)
else:
local_max = self._PRIORITY_NONE
for (index, form) in enumerate(forms):
if require_grams and not form.tag.grammemes.issuperset(require_grams):
continue
(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
if local_priority > local_max:
local_max = local_priority
# segment_score = global_priority
main_index = index
if score_sum == 0:
return None
segment_score = local_sum / score_sum
output.add_word(segment, forms, main_index, needs_coordination)
return segment_score
# Alternative: return segment_score
# penalty_suspicious = 0 if local_max == 0 else (1 - local_sum / local_max) * self._PRIORITY_PENALTY
# return segment_score - penalty_suspicious
@classmethod
def _finalize_coordination(cls, target: Collation):
main_morpho: Morphology = target.get_morpho()
main_coordinate = main_morpho.can_coordinate
target.coordination[target.main_word] = NO_COORDINATION
first_change = INDEX_NONE
current_len = 0
for (index, word) in enumerate(target.words):
if target.coordination[index] == NO_COORDINATION or index - target.main_word > cls._MAIN_MAX_FOLLOWERS:
needs_change = False
if index != target.main_word:
word.main = INDEX_NONE
else:
word.main = PhraseParser._find_coordination(word.forms, main_morpho.tag, index < target.main_word)
needs_change = word.main != INDEX_NONE
if not needs_change or not main_coordinate:
target.coordination[index] = NO_COORDINATION
current_len += 1
if needs_change and main_coordinate:
target.coordination[index] = current_len
current_len = 0
if first_change == INDEX_NONE:
first_change = index
if first_change == INDEX_NONE:
target.coordination.append(len(target.words))
return
previous_reference = first_change
current_word = len(target.words)
target.coordination.append(current_len + 1)
while target.coordination[current_word] != INDEX_NONE:
previous_word = current_word - target.coordination[current_word]
target.coordination[current_word] = previous_reference
previous_reference = current_word - previous_word
current_word = previous_word
if previous_reference == 0 or current_word < 0:
break
@staticmethod
def _find_coordination(forms: list, main_tag: WordTag, before_main: bool) -> int:
for (index, form) in enumerate(forms):
pos = form.tag.POS
case = form.tag.case
if pos not in ['ADJF', 'ADJS', 'PRTF', 'PRTS']:
continue
if SemanticRole.from_POS(pos) == SemanticRole.term and case == 'gent':
if before_main:
continue
else:
return INDEX_NONE
if case == main_tag.case:
return index
elif main_tag.case in ['accs', 'gent'] and case in ['accs', 'gent']:
return index
return INDEX_NONE
@staticmethod
def _filtered_parse(text: str):
capital = Capitalization.from_text(text)
score_filter = PhraseParser._filter_score(morpho.parse(text))
yield from PhraseParser._filter_capital(score_filter, capital)
@staticmethod
def _filter_score(generator):
for form in generator:
if form.score < PhraseParser._FILTER_SCORE:
break
yield form
@staticmethod
def _filter_capital(generator, capital: Capitalization):
if capital in [Capitalization.upper_case, Capitalization.mixed]:
for form in generator:
if 'Abbr' not in form.tag.grammemes:
continue
yield form
else:
yield from generator
@staticmethod
def _parse_word(text: str, require_index: int = INDEX_NONE,
require_grams: Optional[Grammemes] = None) -> Optional[Morphology]:
parsed_variants = morpho.parse(text)
if not parsed_variants or require_index >= len(parsed_variants):
return None
if require_index != INDEX_NONE:
tags = parsed_variants[require_index].tag
if not require_grams or tags.grammemes.issuperset(require_grams):
return Morphology(tags)
else:
return None
else:
for variant in parsed_variants:
tags = variant.tag
if not require_grams or tags.grammemes.issuperset(require_grams):
return Morphology(tags)
return None
@staticmethod
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)
if tag.POS in ['NOUN', 'NPRO']:
return (10, 9) if 'nomn' in tag.grammemes and 'Fixd' not in tag.grammemes else (8, 8)
if tag.POS in ['PRTF', 'PRTS']:
return (6, 6)
if tag.POS in ['ADJF', 'ADJS']:
return (5, 5)
if tag.POS == 'ADVB':
return (7, 4)
return (0, 0)
@staticmethod
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:
return before
before_semantic = before.get_morpho().semantic
after_semantic = after.get_morpho().semantic
if target.semantic == SemanticRole.definition:
if after_semantic == SemanticRole.term:
return after
if before_semantic == SemanticRole.term:
return before
if before_semantic == SemanticRole.definition:
return before
return after
if target.semantic == SemanticRole.term:
if before_semantic == SemanticRole.definition:
return before
if after_semantic == SemanticRole.definition:
return after
return before
@staticmethod
def _combine_morpho(target: Morphology, etalon: WordTag) -> frozenset[str]:
part_of_speech = target.tag.POS
number = etalon.number
if number == 'plur':
return frozenset([part_of_speech, number, etalon.case])
else:
gender = etalon.gender if target.semantic != SemanticRole.term else target.tag.gender
return frozenset([part_of_speech, number, gender, etalon.case])

87
cctext/syntax.py Normal file
View File

@ -0,0 +1,87 @@
''' Russian language syntax incapsulation. '''
from __future__ import annotations
from enum import Enum, unique
from razdel import tokenize
@unique
class Capitalization(Enum):
''' Enumerating capitalization types. '''
unknwn = 0
lower_case = 1
upper_case = 2
first_capital = 3
mixed = 4
@staticmethod
def from_text(text: str) -> Capitalization:
''' Fabric method to identify capitalization in text. '''
if len(text) == 0:
return Capitalization.unknwn
first_capital = Capitalization._is_capital(text[0])
has_mid_capital = False
has_lower = not first_capital
for symbol in text[1:]:
if Capitalization._is_capital(symbol):
if has_lower:
return Capitalization.mixed
has_mid_capital = True
else:
if has_mid_capital:
return Capitalization.mixed
else:
has_lower = True
if has_mid_capital:
return Capitalization.upper_case
elif first_capital:
return Capitalization.first_capital
else:
return Capitalization.lower_case
def apply_to(self, text: str) -> str:
''' Apply capitalization to text. '''
if not text or self in [Capitalization.unknwn, Capitalization.mixed]:
return text
elif self == Capitalization.lower_case:
return text.lower()
elif self == Capitalization.upper_case:
return text.upper()
else:
return text[0].upper() + text[1:]
@staticmethod
def _is_capital(symbol: str) -> bool:
return 'А' <= symbol <= 'Я' or 'A' <= symbol <= 'Z'
class RuSyntax:
''' Russian language syntax parser. '''
def __init__(self):
pass
def __del__(self):
pass
@staticmethod
def is_single_word(text: str) -> bool:
''' Test if text is a single word. '''
try:
gen = tokenize(text)
if next(gen) == '':
return True
if next(gen) == '':
return True
return False
except StopIteration:
return True
@staticmethod
def tokenize(text: str):
''' Split text into words. Returns list[(start, stop, text)]. '''
return tokenize(text)
@staticmethod
def split_words(text: str) -> list[str]:
''' Split text into words. '''
return [elem.text for elem in tokenize(text)]

12
mypy.ini Normal file
View File

@ -0,0 +1,12 @@
# Global options:
[mypy]
warn_return_any = True
warn_unused_configs = True
# Per-module options:
[mypy-razdel.*]
ignore_missing_imports = True
[mypy-pymorphy3.*]
ignore_missing_imports = True

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

BIN
requirements-build.txt Normal file

Binary file not shown.

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
pymorphy3
pymorphy3-dicts-ru
razdel

27
scripts/Build.ps1 Normal file
View File

@ -0,0 +1,27 @@
Set-Location $PSScriptRoot\..
$packageName = 'cctext'
$output = 'output'
$python = '.\venv\Scripts\python.exe'
if (-not (Test-Path -Path $python -PathType Leaf)) {
& 'python' -m venv .\venv
& $python -m pip install -r requirements.txt
& $python -m pip install -r requirements-build.txt
}
if (Test-Path -Path $output\$packageName) {
Remove-Item $output\$packageName -Recurse -Force
}
& $python -m build --outdir=$output\$packageName
$wheel = Get-Childitem -Path $output\$packageName\*.whl -Name
if (-not $wheel) {
Write-Error "No wheel generated for $packageName"
Exit 1
}
& $python -m pip install -I $output\$packageName\$wheel
& $python -m unittest
$exitcode = $LASTEXITCODE
& $python -m pip uninstall -y $packageName
Exit $exitcode

10
scripts/RunLint.ps1 Normal file
View File

@ -0,0 +1,10 @@
# Run lint
function RunLinters() {
$pylint = "$PSScriptRoot\..\venv\Scripts\pylint.exe"
$mypy = "$PSScriptRoot\..\venv\Scripts\mypy.exe"
& $pylint cctext
& $mypy cctext
}
RunLinters

10
scripts/Test.ps1 Normal file
View File

@ -0,0 +1,10 @@
# Run lint
function RunLinters() {
$pylint = "$PSScriptRoot\..\venv\Scripts\pylint.exe"
$mypy = "$PSScriptRoot\..\venv\Scripts\mypy.exe"
& $pylint cctext
& $mypy cctext
}
RunLinters

18
setup.cfg Normal file
View File

@ -0,0 +1,18 @@
[metadata]
name = cctext
version = file: VERSION
author = CIHT CONCEPT, IRBorisov
author_email = iborisov@acconcept.ru
description = Concept text processing library
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT
classifiers =
Programming Language :: Python :: 3
[options]
packages = find:
install_requires =
razdel
pymorphy3
pymorphy3-dicts-ru

5
setup.py Normal file
View File

@ -0,0 +1,5 @@
''' CCText package installer '''
from setuptools import setup
if __name__ == "__main__":
setup()

8
tests/__init__.py Normal file
View File

@ -0,0 +1,8 @@
''' Tests. '''
from .t_reference import *
from .t_ruparser import *
from .t_syntax import *
from .t_conceptapi import *
from .t_rumodel import *
from .t_context import *
from .t_resolver import *

1819
tests/data/functions.txt Normal file

File diff suppressed because it is too large Load Diff

866
tests/data/thesaurus.txt Normal file
View File

@ -0,0 +1,866 @@
тектологические состояния
первые здания
доска
структуры
тектологические состояния
тектологические переходы
уникомплексные тектологические переходы
поликомплексные переходы
процессы
разрушаемые оружием структуры
создаваемые оружием структуры
неразрушаемые оружием структуры
несоздаваемые оружием структуры
формы движения
объекты
систенты
субъекты
системные классы
системные субъекты
системное сознание
системно познаные объекты
обсубъект
системно непознанные объекты
оценки
шкала оценок
альтернативы
критерии
сеть альтернатив
предъявление
субъект актуализации
основание
актуализированная альтернатива
цепи актуализаций
Сечения актуализации
комплексы
квазиконфликтанты
консистенции
квазиконфликтантные комплексы
коструктурный слой
коструктурное окружение
квазиконфликты
шкалы оценок квазиконфликтанта
ущемленная сторона конфликтного перехода
кооперативный переход
конфликты
консистенты
цепи нагнетания напряженности
состояние напряженности
конфликтное состояние
структурный дифференциал
конфликтоген
основание конфликта
конфликтанты
Уникомплексный переход
структурный дифференциал перехода
рабочий процесс оружия
приемы
натуральные числа
складские операции
группа организационных единиц
полные описания издений
разбиения
номера договорных поставок
номера приходных ордеров поставок
дни приходных ордеров поставок
дни
выбранное число
организационные единицы
изделия
операции
группа организаций
предприятия
организация
склад
отрезки времени
прошлые состояния склада
подразделения
поступления партий изделий
отпуск партий изделий
типы изделий
виды изделий
договорные поставки
поставки
начальные отрезки натуральных чисел
абсолютные нумерации разбиений
относительные нумерации разбиений
календари
солнечные календари
григорианские календари
квартальные григорианские календари
календарные годы
календарные кварталы
отрезки нумерации складских операций
поступление
отпуск
классы изделий
текущее состояние склада
спецификация
единицы учета, заданные спецификациями изделий
Единицы учета, заданные множествами изделий
единицы учета
прием партий изделий
отпуск изделий
характеристики
единицы измерения
числовые индексы
полные описания изделий
полные описания
самое мелкое разбиение
самое крупное разбиение
виды операций
Складские операции, выраженные в единицах учета
Поступления изделий, выраженные в единицах учета
Отпуск изделий, выраженный в единицах учета
високосные годы
невисокосные годы
годы
месяцы
кварталы
номера фондовых извещений
номера договоров
фондовые извещения
Нумерация складских операций
фонды
договора
дата
номер приходного ордера
поставки по техпомощи
Тезаурус кафедры КАиП:
Н-арное отношение
Н-местное отношение
R-интерпретация
Абельность
Абсолютное дополнение
Абстрагирование
Абстрактная система
Абстрактный процесс
Абстракция
Автоматизированная система проектирования систем организационного управления
Адаптивная система
Адекватность
Аксиома
Аксиома связности
Аксиома структуры
Аксиоматизация
Аксиоматика теории
Аксиоматическая теория
Аксиомы группы
Аксиомы синтеза
Актуальность
Алгебраическая структура
Алгебра конструктов
Альтернатива
Альтернативные методы
Анализ
Аналитическая теория
Аналогия
Аспект
Аспектная теория
АСП СОУ
АТ
Атрибут
Атрибутивная форма
Атрибутивное мышление
База данных
Базис множеств
Базисное множество
Библиотека моделей
Бинарная операция
Бинарное отношение
Богданов
Большая проекция
Боулдинг
Булеан
Бурбаки
Вершина
Взаимно дополнительные теории
Видообразование
Включение
Власть
Внешность предметной области
Внутренние термы ядра теории
Возможность
Восхождение
Вспомогательные базисные множества
Вторичная функция
Вторичный метод
Вход
Вход процесса
Выбор
Выразимость
Выразительная сила
Выразительные средства теории
Высказывание
Выход
Выход процесса
ГДМ
Генезис
Гипертеория
Гипотеза
Гипотетико-дедуктивные требования
Гипотетико-дедуктивный метод
Главная функция
Глубина проникновения
Гносеология
Гомоморфизм
ГП РС
Границы сети процессов
Граф
Граф термов
Группа
Двухместное отношение
Дебуленизация
Дедуктивная теория
Декартово произведение
Декомпозиция
Дескриптивное использование теории
Дескриптивный подход
Диаграмма Венна
Динамическая система
Дихотомия
Доказательство
Дуга
Желаемая система
Жизненный цикл
Жизненный цикл интереса
Жизненный цикл угрозы
Зависимое множество аксиом
Задача
Закон композиции
Знаковая система
Идентификация
Идеолог
Идея
Идея абстрактного выбора
Иерархическая структура
Измельчение графа термов
Изменение
Изоморфизм
Изофункциональные методы
Ильковский
Имплицитная форма
Интегрированные объекты
Интенсионализация
Интересы
Интерпретант
Интерпретатор
Интерпретация
Интерпретированная теория
Информационная система
Инцесс
Инцессант
Искусственная система
Исследовательский подход
Истинное подмножество
Исток графа термов
Исходная сущность
Исходное отношение
Исходное понятие
Исходное утверждение
КА
КАК-ориентированная система
КАНС
Карта понятий
Картезиан
Каталог методов
Категорическая теория
Качественная проблема
Качественные характеристики теории
Квазиаксиома
Классификация
Классификация множества объектов
Классы концептуальных схем
Классы систем
Класс эквивалентности
Количественная проблема
Комплексное управление развитием систем
Композиция
Композиция-2
Конвенция
Конкрест
Конкретант
Конкретизация
Конкретор
Конкреция
Конституента
Конструкт
Конструкт-конструктные конструкции
Конструкт-содержантные конструкции
Конструкция
Концепт
Концептуализация
Концептуализм
Концептуальная конструкция
Концептуальная модель
Концептуальная схема
Концептуальное мышление
Концептуальное проектирование
Концептуальный анализ
Концептуальный анализ и синтез
Концептуальный аппарат
Корректировка целей
Кортеж
КП
Критерий
Критический элемент
Круги Эйлера
КС
КТО-ориентированная система
КУРС
Лингвистическая интерпретация
Логика
Логические средства
Логическое мышление
ЛПР
Максимальное измельчение
МАКС система
Малая проекция
Математическая структура
Математическая теория
Математические средства КП СОУ
Материальная интерпретация
Машинные средства КП СОУ
М-граф
Мегатеория
Мезотеория
Меновая власть
Мероприятия по достижению частных целей
Метатеорема
Метатеория
Метаязык
Метод
Метод КП СОУ
Методология
Механизм
Механизм парирования угрозы
Микротеория
Минимальный род структуры
Множество
Множество сечений
Множество-степень
Модель
Модель желаемого выхода
Морфологическое отношение
Мощность множества
Мультиграф
Мышление
Нагрузка
Надпроцесс
Надсистема
Направления развёртывания
Н-арное отношение
Независимое множество аксиом
Непересекающиеся множества
Непротиворечивая теория
Нереализованные функции
Несовместная теория
Неформальная аксиоматическая теория
Нижнее замыкание вершины графа
Н-местное отношение
Нормативная методология
Нормативная теория
Нормативное мышление
Нормативный подход
Область значений
Область определения
Область отправления
Область прибытия
Обратная связь
Обстоятельства
Общая теория систем
Общезначимая формула
Объединение
Объект
Объектная регламентация
Объектные теории
Объект управления
Объём теории
Объяснительная теория
Объясняемая теория
Ограничение
Онтологизация
Онтология
Оператор
Операции КАНС
Операциональная схема синтеза
Операциональное понятие
Операция
Операция малая проекция
Операция повышения размерности
Определяющее свойство множества
Оптимизация
Организационное управление
Организация
Ориентированный путь
Основание классификации
Отвлечённые теории
Открытая система
Относительное дополнение
Отношение
Отношение порядка
Отношение эквивалентности
Отображение
Отождествление
Отрицание множества
ОТСУ
ПВР
Первичная функция
Первичные термины теории
Перепостулирование
Пересекающиеся множества
Пересечение множеств
Петля
ПО
Поддержание
Подмножество
Подсистема
Подтеория
Подфункция
Политика
Полифункциональные методы
Полная согласованная интерпретация
Полная теория
Полный вход
Полный выход
Понятие
Порождающие структуры
Постулирование
Потенциальная связь
Потенциально связанные процессы
Потенциальный критический элемент системы
Предмет
Предметная область
Предметный язык
Предположение
Преобразование
Препятствие
Прескриптивное использование теории
Прескриптивный подход
Приложение теории
Принадлежность
Принцип дополнительности
ПРО
Проблема
Прогнозный сценарий
Проект
Проектирование СОУ
Проектный подход
Проект СОУ
Проекция родовой структуры
Произведение множеств
Производное понятие
Производные понятия n-го ранга
Производные термы
Противоречивая теория
Процесс
Процесс КП конкретной СОУ
Процесс ограничения
Процессор
Процесс с необеспеченными связями
Процесс с ролевыми отношениями
ПРС
ПТ
Пустое множество
Разбиение множества объектов
Разбиение множества объектов по явному основанию
Развёрнутое графовое представление
Развёрнутый род структуры
Развёртывание теории
Развивающаяся система
Развитие
Разнообразие
Ранг
Ранжирование
Расчленённые множества
Редукционизм
Редукция
Реконструкция
Ресурс
Рефлексивное отношение
Решение
Р-интерпретация
Родовая структура
Родовое отношение
Родоструктурная экспликация
Род структуры
Рост
Рутинная операция
СА
Самоорганизующаяся система
САПР
Свободное мышление
Свойство
Свойство икса
Связь
Семантическая сеть
Сеть
Сеть ПРО
Сеть процессов
Сеть процессов с ролевыми отношениями
Сеть частных целей
Сечение
Сильные и слабые формы концептуализации
Симметричное отношение
Синтаксический язык
Синтез
Синтез родов структур
С-интерпретация
Система
Система автоматизированного проектирования
Система восстановления
Система организационного управления
Система поддержания
Система процессов
Система развития
Система стратегического планирования
Система стратегического управления
Система управления базами данных
Система функционирования
Системные классы
Системные объекты
Системный анализ
Слабоструктурированная проблема
Словесная форма
Сложная система
Сложная структура
Событие
Совершенствование
Совместимая теория
Содержание
Содержант
Содержательно-дедуктивное развёртывание
Соединение
Соединение множеств
Соискатель ресурсов
Соответствие
Состояние организации
СОУ
Сравнение
Среда
Средства КП СОУ
Срез графа термов
Срез подграфа термов
Сток графа термов
Стратегические цели
Стратегическое планирование
Стратегическое состояние
Стратегия
Стратификация
Строгое включение
Структ
Структура
Структура, определённая отношением порядка
Ступень
СУБД
Субъект
Субъектная регламентация
Субъект целеполагания
Сумма
Сущность
Схема
Схематизация
Творческая операция
Тектология
Тело теории
Теорема
Теоретико-множественная интерпретация
Теоретико-множественная экспликация
Теоретико-множественные операции
Теоретико-системная экспликация
Теоретико-системное мышление
Теоретическое описание объекта
Теория
Теория множеств
Теория систем
Терм
Терминальная теория
Терм-функция
Термы n-го ранга
Техническая система
Техногенема n-го порядка
Технология КП СОУ
Типизация
Типология
Типы мышления
ТМИ
ТМФ
Толчок Ильковского
Топология
Точка зрения
Транзитивное отношение
Требование проблемы
Требования к проекту СОУ
Треугольник Фреге
ТСК
Угроза
Универсальное множество
Универсум
Упорядоченная n-ка
Упорядоченная пара
Управление
Управление процессом достижения цели
Урманцев
Уровень эксплицитности
Условие проблемы
Фактормножество
Фактор-структура
Фактор-структура n-го ранга
Феноменология
Формализация
Формальная система
Формальная теория
Формально-дедуктивное развёртывание
Формальное мышление
Форма от x
Формы концептуализации
Формы мышления
Функционально-методное отношение
Функциональные свойства
Функционирование
Функция
Целевой выход
Целевой терм
Целенаправленная система
Целенаправленная система с ОС
Целеустремлённая система
Цель
Цикл
ЦНС
Частная цель
Член множества
Членство
Шкала множеств
Шкала ступеней
Эквивалентность
Экспликация
Эксплицитное мышление
Экстенсионализация
Элемент
Элементарная теория
Элементарный объект
Элемент входа
Элемент выхода
Эмпирический материал
Эпистемология
Этапы разработки концептуальной схемы
Этиология
Ядро
Язык-объект
Ярус графа
Тезаурус по экологии:
жизнедеятельность человека
технологический процесс
пространственный объем
естественная экологическая система
ценность природного объекта
ценность природной среды
субъект сохраняемой природной среды
экологически значимый отход
естественный состав природной среды
искусственное изменение природной среды
повреждение природного объекта
восстановление природного объекта
восстановление природной среды
восстановимое изменение природной среды
физически необратимое изменение природной среды
источник воздействия хозяйственной деятельности
благоприятный экологический эффект хозяйственной деятельности
резерв ресурса целевой экологической зоны под декларации
установленный запас ресурса целевой экологической зоны
заявленное воздействие
экологический аудит
благоприятная природная среда жизнедеятельности
благоприятная природная среда хозяйственной деятельности
благоприятная экологическая зона
виды экологической экспертизы
государственное оперативное экологическое управление
государственное экологическое управление
границы применимости методики
деклараций о выбросах, сбросах и физических воздействиях
декларируемые воздействия
декларируемые опосредованные воздействия
документы методического обеспечения экологических отношений
документы оценки воздействия
документы экологического планирования
Единый экологический реестр
законодательство в области экологических отношений
заключение экологического нормоконтроля
зона экологического бедствия
информационное обеспечение ЭО
карта экологического зонирования
муниципальное оперативное экологическое управление
муниципальное экологическое управление
муниципальный экологический контроль
муниципальный экологический мониторинг
научное обеспечение экологических отношений
научный экологический совет
неблагоприятная экологическая зона
неопределенная экологическая зона
неприемлемая экологическая зона
общественное экологическое управление
общественные экологические объединения
общественный экологический контроль
общественный экологический мониторинг
объект оценки
объект экологического нормоконтроля
объект, подлежащий обязательному экологическому страхованию
обязательное экологическое страхование
опасная экологическая зона
организации, осуществляющие научное обеспечение экологических отношений
основания для ответственности в области экологических отношений
приемлемая экологическая зона
программа мероприятий по благоприятному изменению природной среды и снижению риска возникновения негативных экологических последствий
производственное оперативное экологическое управление
производственное экологическое управление
производственный экологический контроль
производственный экологический мониторинг
прокурорский экологический надзор
разрешение на воздействие на природную среду
разрешение на выбросы, сбросы и физические воздействия
разрешенные воздействия
разрешенные опосредованные воздействия
региональный экологический нормоконтроль
региональное оперативное экологическое управление
региональное экологическое нормирование
региональное экологическое управление
региональные экологические советы
региональный экологический контроль
региональный экологический мониторинг
региональный экологический надзор
способы возмещения экологического вреда
стратегическая экологическая оценка
субъект обязательного экологического страхования
схема экологического зонирования
технические и технологические экологические нормативы
территория зонирования
условия применимости норматива
участники экологических отношений
федеральный экологический нормоконтроль
федеральное оперативное экологическое управление
федеральное экологическое нормирование
федеральное экологическое управление
федеральный экологический контроль
федеральный экологический мониторинг
федеральный экологический надзор
первичные нормативы благоприятной природной среды жизнедеятельности
первичные нормативы благоприятной природной среды хозяйственной деятельности
первичные нормативы опасной природной среды жизнедеятельности
первичные нормативы приемлемой природной среды жизнедеятельности
первичные нормативы сохраняемой природной среды
целевая экологическая зона
экологическая плата
экологическая экспертиза
экологический нормоконтроль
экологические нормативы воздействия хозяйственной деятельности
экологические нормативы последствий воздействия
экологические нормативы природной среды
экологическое зонирование по нормативам
экологическое зонирование по целям
экологическое нормирование
экологическое управление
экспертное экологическое заключение
Муниципальный экологический план
Ежегодный отчет о состоянии природной среды на территории Российской Федерации
Ежегодный отчет о состоянии природной среды на территории муниципальных образований
Порядок государственной аккредитации организаций, осуществляющих научное обеспечение экологических отношений
Порядок государственной аккредитации экспертных организаций и экспертов в области экологических отношений
Порядок научного обеспечения экологических отношений
Положение о Научном экологическом совете
Положение об экологическом нормоконтроле
Положение об экологической экспертизе
Методика расчета совокупного опосредованного воздействия на природную среду
Положение об Едином экологическом реестре
Методика оценки воздействия на природную среду
Положение о мерах по благоприятному изменению природной среды и снижению риска возникновения негативных экологических последствий
Перечень видов хозяйственной деятельности, для которых устанавливаются экологические нормативы природной среды хозяйственной деятельности
Порядок утверждения экологических нормативов природной среды хозяйственной деятельности
Порядок утверждения технических и технологических экологических нормативов
Порядок утверждения экологических нормативов хозяйственной деятельности
Методические рекомендации по разработке муниципальных экологических планов
Методические рекомендации по разработке производственных экологических планов
Федеральный экологический план
Порядок разработки федеральных экологических планов
Порядок проведения стратегической экологической оценки
Методика расчета воздействия по опосредованному воздействию
Порядок оформления разрешений на воздействие на природную среду
Методика определения и изменения размера экологической платы
Методика экологического мониторинга
Методика оперативного экологического управления
Методика экологического контроля
Методика оценки экологического ущерба
Правила экологического страхования
Ежегодный отчет о состоянии природной среды на территории субъекта Российской Федерации
Положение о Региональном экологическом совете субъекта Российской Федерации
Порядок создания региональных экологических советов
Порядок проведения публичных слушаний при рассмотрении проектов региональных схем экологического зонирования
Региональный экологический план
Порядок разработки региональных экологических планов
Производственный экологический план
жизнедеятельность
технология
хозяйственная деятельность
производственная инфраструктура
объект хозяйственной деятельности
природный объект
природная среда
природная среда жизнедеятельности
природная среда хозяйственной деятельности
природный объект особого значения
особо охраняемая природная территория
сохраняемая природная среда
действующий объект хозяйственной деятельности
новый объект хозяйственной деятельности
субъект хозяйственной деятельности
работник субъекта хозяйственной деятельности
отход
выброс
сброс
размещение отходов
размещенные отходы
объект размещения отходов
воздействие хозяйственной деятельности
физическое воздействие
мероприятие по благоприятному изменению природной среды
технологически невосстановимое изменение природной среды
опосредующая природная среда
опосредующие свойства природной среды
опосредованное воздействие
территория опосредованного воздействия хозяйственной деятельности
совокупное опосредованное воздействие на природную среду
экологический эффект хозяйственной деятельности
экологическое последствие
экологическое правонарушение
экологический вред
экологический ущерб
наилучшая доступная технология
негативный экологический эффект хозяйственной деятельности
негативное экологическое последствие хозяйственной деятельности
реципиент воздействия
мероприятие по снижению риска возникновения негативных экологических последствий
мера по благоприятному изменению природной среды и снижению риска возникновения негативных экологических последствий
экологическое отношение
субъекты
критерий незначительности воздействия
незначительное воздействие хозяйственной деятельности
критерий декларирования опосредованного воздействия
первичный норматив
экологический норматив
баланс интересов
экологическое зонирование
экологических зон
базовое состояние природной среды
ресурс природной среды
распределяемый ресурс природной ресурс
квота опосредованного воздействия
утраченный экологический ресурс
критерий корректировки программ мероприятий
экологическая экспертная оценка
экологический экспертный расчет
экологический нормоконтроль
управленческое решение
экологический мониторинг
оперативное экологическое управление
экологический контроль
экологический надзор

83
tests/t_conceptapi.py Normal file
View File

@ -0,0 +1,83 @@
''' Unit tests: conceptapi. '''
import unittest
import cctext as cc
class TestConceptAPI(unittest.TestCase):
'''Test class for Concept API.'''
def _assert_tags(self, actual: str, expected: str):
self.assertEqual(set(cc.split_grams(actual)), set(cc.split_grams(expected)))
def test_parse(self):
''' Test parsing. '''
self._assert_tags(cc.parse(''), '')
self._assert_tags(cc.parse('1'), 'NUMB,intg')
self._assert_tags(cc.parse('слон', require_grams='masc'), 'NOUN,anim,masc,sing,nomn')
def test_normalize_word(self):
''' Test normalize for single word. '''
self.assertEqual(cc.normalize(''), '')
self.assertEqual(cc.normalize('первого'), 'первый')
self.assertEqual(cc.normalize('диких людей'), 'дикий человек')
def test_generate_lexeme(self):
''' Test all lexical forms. '''
self.assertEqual(cc.generate_lexeme(''), [])
forms = cc.generate_lexeme('наверное')
self.assertEqual(len(forms), 1)
self.assertEqual(forms[0][0], 'наверное')
self._assert_tags(forms[0][1], 'CONJ,Prnt')
forms = cc.generate_lexeme('молодой человек')
self.assertEqual(len(forms), 19)
self.assertEqual(forms[0][0], 'молодой человек')
self._assert_tags(forms[0][1], 'nomn,masc,sing,anim,NOUN')
def test_inflect(self):
''' Test inflection. '''
self.assertEqual(cc.inflect('', ''), '')
self.assertEqual(cc.inflect('слона', 'nomn'), 'слон')
self.assertEqual(cc.inflect('слона', 'ADJF'), 'слона')
self.assertEqual(cc.inflect('слона', 'nomn,plur'), 'слоны')
self.assertEqual(cc.inflect('слона', 'nomn, plur'), 'слоны')
self.assertEqual(cc.inflect('шкала оценок', 'loct,plur'), 'шкалах оценок')
def test_find_substr(self):
'''Test substring search'''
self.assertEqual(cc.find_substr('', ''), (0, 0))
self.assertEqual(cc.find_substr('сложного красивого слона', 'красивые слоном'), (9, 24))
def test_inflect_context(self):
'''Test contex inflection'''
self.assertEqual(cc.inflect_context('', '', ''), '')
self.assertEqual(cc.inflect_context('красивый', '', 'чашка'), 'красивая')
def test_inflect_substitute(self):
'''Test substitute inflection'''
self.assertEqual(cc.inflect_substitute('', ''), '')
self.assertEqual(cc.inflect_substitute('', 'слон'), '')
self.assertEqual(cc.inflect_substitute('слон', ''), 'слон')
self.assertEqual(cc.inflect_substitute('красивый бантик', 'кошкой'), 'красивым бантиком')
def test_inflect_dependant(self):
''' Test coordination inflection. '''
self.assertEqual(cc.inflect_dependant('', ''), '')
self.assertEqual(cc.inflect_dependant('', 'слон'), '')
self.assertEqual(cc.inflect_dependant('слон', ''), 'слон')
self.assertEqual(cc.inflect_dependant('общий', 'мать'), 'общая')
self.assertEqual(cc.inflect_dependant('синий', 'слонов'), 'синих')
def test_match_all_morpho(self):
''' Test extracting matching morpho. '''
self.assertEqual(cc.match_all_morpho('', ''), [])
self.assertEqual(cc.match_all_morpho('горит город', ''), [])
self.assertEqual(cc.match_all_morpho('горит город', 'NOUN'), [[6, 11]])
self.assertEqual(cc.match_all_morpho('горит город', 'VERB'), [[0, 5]])
self.assertEqual(cc.match_all_morpho('столица страны', 'NOUN'), [[0, 7], [8, 14]])
self.assertEqual(cc.match_all_morpho('столица страны', 'NOUN,sing,nomn'), [[0, 7]])
if __name__ == '__main__':
unittest.main()

32
tests/t_context.py Normal file
View File

@ -0,0 +1,32 @@
''' Unit tests: context. '''
import unittest
from cctext.context import Entity, TermContext
class TestEntity(unittest.TestCase):
'''Test Entity termform access.'''
def setUp(self):
self.alias = 'X1'
self.nominal = 'человек'
self.text1 = 'test1'
self.form1 = ['sing','datv']
self.entity = Entity(self.alias, self.nominal, [{'text': self.text1, 'grams': self.form1}])
def test_attributes(self):
self.assertEqual(self.entity.alias, self.alias)
self.assertEqual(self.entity.get_nominal(), self.nominal)
self.assertEqual(self.entity.manual, [{'text': self.text1, 'grams': self.form1}])
def test_get_form(self):
self.assertEqual(self.entity.get_form([]), self.nominal)
self.assertEqual(self.entity.get_form(self.form1), self.text1)
self.assertEqual(self.entity.get_form(['invalid tags']), '!Неизвестная граммема: invalid tags!')
self.assertEqual(self.entity.get_form(['plur']), 'люди')
def test_set_nominal(self):
new_nomial = 'TEST'
self.assertEqual(self.entity.get_form(['plur']), 'люди')
self.entity.set_nominal(new_nomial)
self.assertEqual(self.entity.get_nominal(), new_nomial)
self.assertEqual(self.entity.get_form(['plur']), new_nomial)
self.assertEqual(self.entity.manual, [])

44
tests/t_reference.py Normal file
View File

@ -0,0 +1,44 @@
''' Unit tests: reference. '''
import unittest
from cctext import EntityReference, ReferenceType, SyntacticReference, parse_reference
class TestReferences(unittest.TestCase):
''' Test class for references. '''
def test_EntityReference(self):
''' Testing EntityRefence basics. '''
ref = EntityReference('X1', 'sing,nomn')
self.assertEqual(ref.get_type(), ReferenceType.entity)
self.assertEqual(ref.to_text(), '@{X1|sing,nomn}')
def test_SyntacticReference(self):
''' Testing SyntacticReference basics. '''
ref = SyntacticReference(-1, 'черный')
self.assertEqual(ref.get_type(), ReferenceType.syntactic)
self.assertEqual(ref.to_text(), '@{-1|черный}')
def test_parse_reference_invalid(self):
''' Testing parsing reference invalid input. '''
self.assertIsNone(parse_reference(''))
self.assertIsNone(parse_reference('X1'))
self.assertIsNone(parse_reference('invalid'))
self.assertIsNone(parse_reference(' '))
self.assertIsNone(parse_reference('@{|}'))
self.assertIsNone(parse_reference('@{|черный}'))
self.assertIsNone(parse_reference('@{ | }'))
self.assertIsNone(parse_reference('@{-1| }'))
self.assertIsNone(parse_reference('@{1| }'))
self.assertIsNone(parse_reference('@{0|черный}'))
def test_parse_reference(self):
''' Testing parsing reference text. '''
ref = parse_reference('@{1| черный }')
self.assertIsNotNone(ref)
self.assertEqual(ref.to_text(), '@{1|черный}')
self.assertEqual(ref.get_type(), ReferenceType.syntactic)
ref = parse_reference('@{X1 | VERB, past, sing}')
self.assertIsNotNone(ref)
self.assertEqual(ref.to_text(), '@{X1|VERB,past,sing}')
self.assertEqual(ref.get_type(), ReferenceType.entity)

107
tests/t_resolver.py Normal file
View File

@ -0,0 +1,107 @@
''' Unit tests: resolver. '''
import unittest
from typing import cast
from cctext import (
EntityReference, TermContext, Entity, SyntacticReference,
Resolver, ResolvedReference, Position, TermForm,
resolve_entity, resolve_syntactic, extract_entities
)
class TestUtils(unittest.TestCase):
''' Test utility methods. '''
def test_extract_entities(self):
self.assertEqual(extract_entities(''), [])
self.assertEqual(extract_entities('@{-1|черны}'), [])
self.assertEqual(extract_entities('@{X1|nomn}'), ['X1'])
self.assertEqual(extract_entities('@{X1|datv}'), ['X1'])
self.assertEqual(extract_entities('@{X1|datv} @{X1|datv} @{X2|datv}'), ['X1', 'X2'])
self.assertEqual(extract_entities('@{X1} | @{X1} | @{X2}'), [])
class TestResolver(unittest.TestCase):
'''Test reference Resolver.'''
def setUp(self):
self.context = cast(TermContext, {})
self.context['X1'] = Entity('X1', 'человек')
self.context['X2'] = Entity('X2', '')
self.resolver = Resolver(self.context)
def test_resolve_entity(self):
self.assertEqual(resolve_entity(EntityReference('X1', ''), self.context), 'человек')
self.assertEqual(resolve_entity(EntityReference('X1', 'plur'), self.context), 'люди')
self.assertEqual(resolve_entity(EntityReference('X2', ''), self.context), '!Отсутствует термин: X2!')
self.assertEqual(resolve_entity(EntityReference('X1', 'invalid'), self.context), '!Неизвестная граммема: invalid!')
self.assertEqual(resolve_entity(EntityReference('X123', 'plur'), self.context), '!Неизвестная сущность: X123!')
def test_resolve_syntactic(self):
ref = ResolvedReference(ref=EntityReference('X1', 'sing,datv'), resolved='человеку')
refs_list = [ref, ref, ref, ref]
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 0, refs_list), '!Некорректное смещение: -1!')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 3, refs_list), '!Некорректное смещение: 1!')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 0, refs_list), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=2), 0, refs_list), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=3), 0, refs_list), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 3, refs_list), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-2), 3, refs_list), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-3), 3, refs_list), 'умному')
def test_resolve_invalid(self):
self.assertEqual(self.resolver.resolve(''), '')
self.assertEqual(len(self.resolver.refs), 0)
self.assertEqual(self.resolver.resolve('simple text'), 'simple text')
self.assertEqual(len(self.resolver.refs), 0)
self.assertEqual(self.resolver.resolve('simple @{unparsable ref} text'), 'simple @{unparsable ref} text')
self.assertEqual(len(self.resolver.refs), 0)
def test_resolve_single(self):
self.assertEqual(self.resolver.resolve('просто @{-1|умный} текст'), 'просто !Некорректное смещение: -1! текст')
self.assertEqual(len(self.resolver.refs), 1)
self.assertEqual(self.resolver.refs[0].pos_input, Position(7, 18))
self.assertEqual(self.resolver.refs[0].pos_output, Position(7, 34))
self.assertEqual(self.resolver.resolve('просто @{X123|sing,nomn} текст'), 'просто !Неизвестная сущность: X123! текст')
self.assertEqual(len(self.resolver.refs), 1)
self.assertEqual(self.resolver.refs[0].pos_input, Position(7, 24))
self.assertEqual(self.resolver.refs[0].pos_output, Position(7, 35))
self.assertEqual(self.resolver.resolve('@{X1|sing,nomn}'), 'человек')
self.assertEqual(len(self.resolver.refs), 1)
self.assertEqual(self.resolver.refs[0].pos_input, Position(0, 15))
self.assertEqual(self.resolver.refs[0].pos_output, Position(0, 7))
self.assertEqual(self.resolver.resolve('просто @{X1|sing,nomn} текст'), 'просто человек текст')
self.assertEqual(len(self.resolver.refs), 1)
self.assertEqual(self.resolver.refs[0].pos_input, Position(7, 22))
self.assertEqual(self.resolver.refs[0].pos_output, Position(7, 14))
def test_resolve_multiple(self):
input = '@{X1|sing,datv} @{-1|умный} @{X1|plur} завидуют'
self.assertEqual(self.resolver.resolve(input), 'человеку умному люди завидуют')
self.assertEqual(len(self.resolver.refs), 3)
self.assertEqual(self.resolver.refs[0].pos_input, Position(0, 15))
self.assertEqual(self.resolver.refs[0].pos_output, Position(0, 8))
self.assertEqual(self.resolver.refs[1].pos_input, Position(16, 27))
self.assertEqual(self.resolver.refs[1].pos_output, Position(9, 15))
self.assertEqual(self.resolver.refs[2].pos_input, Position(28, 38))
self.assertEqual(self.resolver.refs[2].pos_output, Position(16, 20))
def test_resolve_manual_forms(self):
self.context['X1'] = Entity(
alias='X1',
nominal='человек',
manual_forms=[
TermForm(text='тест1', grams='NOUN,sing'.split(',')),
TermForm(text='тест2', grams='NOUN,datv,plur'.split(','))
]
)
self.assertEqual(self.resolver.resolve('@{X1|NOUN,sing,nomn}'), 'тест1', 'Match subset')
self.assertEqual(self.resolver.resolve('@{X1|NOUN,sing}'), 'тест1', 'Match full')
self.assertEqual(self.resolver.resolve('@{X1|NOUN,datv,plur}'), 'тест2')
self.assertEqual(self.resolver.resolve('@{X1|NOUN,plur,datv}'), 'тест2', 'Match any order')
self.assertEqual(self.resolver.resolve('@{X1|datv,plur}'), 'тест2', 'Match missing POS')
self.assertEqual(self.resolver.resolve('@{X1|NOUN,datv,sing}'), 'тест1')
self.assertEqual(self.resolver.resolve('@{X1|VERB,datv,plur}'), 'человек')

18
tests/t_rumodel.py Normal file
View File

@ -0,0 +1,18 @@
''' Unit tests: rumodel. '''
import unittest
from cctext import split_grams, combine_grams
class TestTags(unittest.TestCase):
'''Test tags manipulation.'''
def test_split_tags(self):
self.assertEqual(split_grams(''), [])
self.assertEqual(split_grams('NOUN'), ['NOUN'])
self.assertEqual(split_grams('NOUN,plur,sing'), ['NOUN','plur','sing'])
def test_combine_tags(self):
self.assertEqual(combine_grams([]), '')
self.assertEqual(combine_grams(['NOUN']), 'NOUN')
self.assertEqual(combine_grams(['NOUN','plur','sing']), 'NOUN,plur,sing')

446
tests/t_ruparser.py Normal file
View File

@ -0,0 +1,446 @@
''' Unit tests: ruparser. '''
import unittest
from typing import Iterable, Optional
from cctext import PhraseParser
parser = PhraseParser()
class TestRuParser(unittest.TestCase):
''' Test class for russian parsing. '''
def _assert_parse(self, text: str, expected: Iterable[str],
require_index: int = -1,
require_grams: Optional[Iterable[str]] = None):
phrase = parser.parse(text, require_index, require_grams)
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(frozenset(tags))
self.assertEqual(result, expected)
def test_parse_word(self):
''' Test parse for single word. '''
self._assert_parse('1', ['NUMB', 'intg'])
self._assert_parse('пять', ['NUMR', 'nomn'])
self._assert_parse('трёх', ['NUMR', 'gent'])
self._assert_parse('трех', ['NUMR', 'gent'])
self._assert_parse('круча', ['NOUN', 'femn', 'sing', 'nomn', 'inan'])
self._assert_parse('круть', ['NOUN', 'femn', 'sing', 'nomn', 'inan', 'Sgtm', 'Geox'])
self._assert_parse('ПВО', ['NOUN', 'femn', 'sing', 'nomn', 'inan', 'Sgtm', 'Abbr', 'Fixd'])
self._assert_parse('СМИ', ['NOUN', 'plur', 'nomn', 'inan', 'Pltm', 'Abbr', 'Fixd', 'GNdr'])
self._assert_parse('ему', ['NPRO', 'masc', 'sing', 'datv', '3per', 'Anph'])
self._assert_parse('крутит', ['VERB', 'sing', '3per', 'pres', 'impf', 'tran', 'indc'])
self._assert_parse('смеркалось', ['VERB', 'neut', 'sing', 'Impe', 'past', 'impf', 'intr', 'indc'])
self._assert_parse('крутить', ['INFN', 'impf', 'tran'])
self._assert_parse('крученый', ['ADJF', 'masc', 'sing', 'nomn'])
self._assert_parse('крут', ['ADJS', 'masc', 'sing', 'Qual'])
self._assert_parse('крутящего', ['PRTF', 'masc', 'sing', 'gent', 'pres', 'impf', 'tran', 'actv'])
self._assert_parse('откручен', ['PRTS', 'masc', 'sing', 'past', 'perf', 'pssv'])
self._assert_parse('крутя', ['GRND', 'pres', 'impf', 'tran'])
self._assert_parse('круто', ['ADVB'])
self._assert_parse('круче', ['COMP', 'Qual'])
self._assert_parse(',', ['PNCT'])
self._assert_parse('32-', ['intg', 'NUMB'])
self._assert_parse('слон', ['NOUN', 'anim', 'masc', 'sing', 'nomn'], require_index=0)
self._assert_parse('слон', ['NOUN', 'anim', 'masc', 'sing', 'nomn'], require_grams=['masc'])
self._assert_parse('прямой', ['ADJF', 'gent', 'sing', 'femn', 'Qual'], require_index=0)
self._assert_parse('прямой', ['ADJF', 'datv', 'Qual', 'sing', 'femn'], require_index=1)
self._assert_parse('прямой', ['NOUN', 'sing', 'inan', 'femn', 'gent'], require_grams=['NOUN'])
self._assert_parse('консистенции', ['NOUN', 'inan', 'femn', 'plur', 'nomn'])
self._assert_parse('тест', ['NOUN', 'sing', 'masc', 'inan', 'nomn'])
self._assert_parse('петля', ['NOUN', 'inan', 'femn', 'sing', 'nomn'])
self._assert_parse('Слон', ['NOUN', 'anim', 'masc', 'sing', 'nomn'])
self._assert_parse('СМИ', ['NOUN', 'Pltm', 'GNdr', 'Fixd', 'inan', 'Abbr', 'plur', 'nomn'])
self.assertEqual(parser.parse('КАиП'), None)
self.assertEqual(parser.parse('СЛОН'), None)
self.assertEqual(parser.parse(''), None)
self.assertEqual(parser.parse('слон', require_grams=set(['femn'])), None)
self.assertEqual(parser.parse('32', require_grams=set(['NOUN'])), None)
self.assertEqual(parser.parse('32-', require_grams=set(['NOUN'])), None)
self.assertEqual(parser.parse('слон', require_index=42), None)
def test_parse_text(self):
''' Test parse for multiword sequences. '''
self._assert_parse(', ,', ['PNCT'])
self._assert_parse('слон,', ['NOUN', 'anim', 'masc', 'sing', 'nomn'])
self._assert_parse('синий слон', ['NOUN', 'anim', 'masc', 'sing', 'nomn'])
self._assert_parse('слон синий', ['NOUN', 'anim', 'masc', 'sing', 'nomn'])
self._assert_parse('тихий Дон', ['NOUN', 'anim', 'masc', 'sing', 'nomn'])
self._assert_parse('слон, лежащий на траве', ['NOUN', 'anim', 'masc', 'sing', 'nomn'])
self._assert_parse('лежащий на траве слон', ['NOUN', 'anim', 'masc', 'sing', 'nomn'])
self._assert_parse('города улиц', ['NOUN', 'nomn', 'plur', 'masc', 'inan'])
self._assert_parse('первый дом улиц города', ['NOUN', 'inan', 'masc', 'nomn', 'sing'])
self._assert_parse('быстро едет', ['VERB', 'intr', 'impf', '3per', 'sing', 'indc', 'pres'])
self._assert_parse('летучий 1-2-пептид', ['NOUN', 'masc', 'nomn', 'sing', 'inan'])
self._assert_parse('прямой угол', ['NOUN', 'masc', 'nomn', 'inan', 'sing'])
self._assert_parse('прямого угла', ['NOUN', 'sing', 'inan', 'masc', 'gent'])
self._assert_parse('угла прямой', ['NOUN', 'sing', 'inan', 'masc', 'gent'])
self._assert_parse('бесконечной прямой', ['NOUN', 'gent', 'femn', 'sing', 'inan'])
self._assert_parse('складские операции', ['NOUN', 'femn', 'plur', 'inan', 'nomn'])
self._assert_parse('незначительному воздействию хозяйственной деятельности',
['NOUN', 'datv', 'sing', 'neut', 'inan'])
self._assert_parse('варить овсянку', ['INFN', 'tran', 'impf'])
self._assert_parse('варить рис', ['INFN', 'tran', 'impf'])
self._assert_parse('нарочито сложный', ['ADJF', 'sing', 'masc', 'nomn', 'Qual'])
self._assert_parse('части программы', ['NOUN', 'femn', 'plur', 'nomn', 'inan'])
self._assert_parse('летучий 1-2-фторметил', ['NOUN', 'nomn', 'sing', 'masc', 'inan'])
self._assert_parse('отрезок времени', ['NOUN', 'nomn', 'sing', 'masc', 'inan'])
self._assert_parse('портки сушить', ['INFN', 'impf', 'tran'])
self._assert_parse('портки вешай', ['VERB', 'tran', 'impr', 'impf', 'sing', 'excl'])
self._assert_parse('Анализирует состояние организации.',
['VERB', 'sing', 'tran', 'pres', '3per', 'impf', 'indc'])
self._assert_parse('Во взаимодействии с подразделениями Генеральной прокуратуры формирует перечень показателей',
['VERB', 'sing', 'tran', 'pres', '3per', 'impf', 'indc'])
def test_parse_coordination(self):
''' Test parse coordination info. '''
self.assertEqual(parser.parse('говорить').coordination, [-1, 1])
self.assertEqual(parser.parse('ворчливая карга').coordination, [2, -1, 0])
self.assertEqual(parser.parse('страна, объятая пожаром').coordination, [-1, -1, 2, -1, 2])
self.assertEqual(parser.parse('тихо говорил').coordination, [-1, -1, 2])
def test_normalize_word(self):
''' Test normalize for single word. '''
self.assertEqual(parser.normalize(''), '')
self.assertEqual(parser.normalize('123'), '123')
self.assertEqual(parser.normalize('test'), 'test')
self.assertEqual(parser.normalize('первого'), 'первый')
self.assertEqual(parser.normalize('слону'), 'слон')
self.assertEqual(parser.normalize('слонам'), 'слон')
self.assertEqual(parser.normalize('обеспечил'), 'обеспечить')
self.assertEqual(parser.normalize('сильную'), 'сильный')
self.assertEqual(parser.normalize('бежавший'), 'бежать')
self.assertEqual(parser.normalize('1 2 3'), '1 2 3')
def test_normalize_text(self):
''' Test normalize for multiword collation. '''
self.assertEqual(parser.normalize('синего слона'), 'синий слон')
self.assertEqual(parser.normalize('тихо говоривший'), 'тихо говорить')
self.assertEqual(parser.normalize('канавой квартала'), 'канава квартала')
def test_inflect_word(self):
''' Test inflection for single word. '''
self._assert_inflect('', [], '')
self._assert_inflect('invalid', [], 'invalid')
self._assert_inflect('invalid', ['nomn'], 'invalid')
self._assert_inflect('', ['nomn'], '')
self._assert_inflect('123', ['nomn'], '123')
self._assert_inflect('слона', [], 'слона')
self._assert_inflect('слона', ['ADJF'], 'слона')
self._assert_inflect('слона', ['nomn'], 'слон')
self._assert_inflect('объектоид', ['datv'], 'объектоиду')
self._assert_inflect('терм-функция', ['datv'], 'терм-функции')
self._assert_inflect('Слона', ['nomn'], 'Слон')
self._assert_inflect('СМИ', ['datv'], 'СМИ')
self._assert_inflect('КАНС', ['datv'], 'КАНС')
self._assert_inflect('КАиП', ['datv'], 'КАиП')
self._assert_inflect('АТ', ['datv'], 'АТ')
self._assert_inflect('А-проекция', ['datv'], 'А-проекции')
def test_inflect_noun(self):
''' Test inflection for single noun. '''
self._assert_inflect('книга', ['nomn'], 'книга')
self._assert_inflect('книга', ['gent'], 'книги')
self._assert_inflect('книга', ['datv'], 'книге')
self._assert_inflect('книга', ['accs'], 'книгу')
self._assert_inflect('книга', ['ablt'], 'книгой')
self._assert_inflect('книга', ['loct'], 'книге')
self._assert_inflect('люди', ['loct'], 'людях')
self._assert_inflect('книга', ['plur'], 'книги')
self._assert_inflect('люди', ['sing'], 'человек')
self._assert_inflect('человек', ['plur'], 'люди')
self._assert_inflect('человек', ['plur', 'loct'], 'людях')
self._assert_inflect('человеку', ['masc'], 'человеку')
self._assert_inflect('человеку', ['neut'], 'человеку')
self._assert_inflect('человека', ['femn'], 'человека')
self._assert_inflect('человека', ['past'], 'человека')
def test_inflect_npro(self):
''' Test inflection for single pronoun. '''
self._assert_inflect('меня', ['nomn'], 'я')
self._assert_inflect('я', ['gent'], 'меня')
self._assert_inflect('я', ['datv'], 'мне')
self._assert_inflect('я', ['accs'], 'меня')
self._assert_inflect('я', ['ablt'], 'мной')
self._assert_inflect('я', ['loct'], 'мне')
self._assert_inflect('я', ['ADJF'], 'я')
self._assert_inflect('я', ['NOUN'], 'я')
self._assert_inflect('я', ['2per'], 'я')
self._assert_inflect('я', ['past'], 'я')
def test_inflect_numr(self):
''' Test inflection for single numeric. '''
self._assert_inflect('трёх', ['nomn'], 'три')
self._assert_inflect('три', ['gent'], 'трёх')
self._assert_inflect('три', ['datv'], 'трём')
self._assert_inflect('три', ['accs', 'inan'], 'три')
self._assert_inflect('три', ['accs', 'anim'], 'трёх')
self._assert_inflect('три', ['ablt'], 'тремя')
self._assert_inflect('три', ['loct'], 'трёх')
def test_inflect_adjf(self):
''' Test inflection for single adjectif. '''
self._assert_inflect('хороший', ['nomn'], 'хороший')
self._assert_inflect('хороший', ['gent'], 'хорошего')
self._assert_inflect('хороший', ['datv'], 'хорошему')
self._assert_inflect('хороший', ['accs'], 'хорошего')
self._assert_inflect('хороший', ['ablt'], 'хорошим')
self._assert_inflect('хороший', ['loct'], 'хорошем')
self._assert_inflect('хороший', ['plur'], 'хорошие')
self._assert_inflect('хорошие', ['sing'], 'хороший')
self._assert_inflect('хорошие', ['sing', 'datv'], 'хорошему')
self._assert_inflect('хорошие', ['plur', 'masc', 'datv'], 'хорошим')
self._assert_inflect('хорошая', ['masc'], 'хороший')
self._assert_inflect('перепончатокрылое', ['masc'], 'перепончатокрылый')
self._assert_inflect('хороший', ['neut'], 'хорошее')
self._assert_inflect('хорошая', ['neut'], 'хорошее')
self._assert_inflect('хороший', ['femn'], 'хорошая')
self._assert_inflect('перепончатокрылое', ['femn'], 'перепончатокрылая')
self._assert_inflect('хороший', ['masc', 'femn'], 'хороший')
self._assert_inflect('хороший', ['plur', 'femn'], 'хорошие')
self._assert_inflect('хороший', ['past'], 'хороший')
def test_inflect_prtf(self):
''' Test inflection for single participle. '''
self._assert_inflect('бегущего', ['nomn'], 'бегущий')
self._assert_inflect('бегущий', ['gent'], 'бегущего')
self._assert_inflect('бегущий', ['datv'], 'бегущему')
self._assert_inflect('бегущий', ['accs'], 'бегущего')
self._assert_inflect('бегущий', ['ablt'], 'бегущим')
self._assert_inflect('бегущий', ['loct'], 'бегущем')
self._assert_inflect('бегущая', ['loct'], 'бегущей')
self._assert_inflect('бежавшая', ['loct'], 'бежавшей')
self._assert_inflect('бегущий', ['plur'], 'бегущие')
self._assert_inflect('бегущие', ['sing'], 'бегущий')
self._assert_inflect('бегущие', ['sing', 'datv'], 'бегущему')
self._assert_inflect('бегущий', ['femn'], 'бегущая')
self._assert_inflect('бегущий', ['neut'], 'бегущее')
self._assert_inflect('бегущая', ['masc'], 'бегущий')
self._assert_inflect('бегущий', ['past'], 'бежавший')
self._assert_inflect('бежавших', ['pres'], 'бегущих')
self._assert_inflect('бегущий', ['masc', 'femn'], 'бегущий')
self._assert_inflect('бегущий', ['plur', 'femn'], 'бегущие')
def test_inflect_verb(self):
''' Test inflection for single verb. '''
self._assert_inflect('говорить', ['1per'], 'говорю')
self._assert_inflect('говорить', ['2per'], 'говоришь')
self._assert_inflect('говорить', ['2per', 'plur'], 'говорите')
self._assert_inflect('говорить', ['3per'], 'говорит')
self._assert_inflect('говорите', ['1per'], 'говорим')
self._assert_inflect('говорите', ['3per'], 'говорят')
self._assert_inflect('говорит', ['plur'], 'говорят')
self._assert_inflect('говорят', ['sing'], 'говорит')
self._assert_inflect('говорит', ['past'], 'говорил')
self._assert_inflect('говорил', ['pres'], 'говорю')
self._assert_inflect('говорили', ['sing'], 'говорил')
self._assert_inflect('говорил', ['plur'], 'говорили')
self._assert_inflect('говорила', ['masc'], 'говорил')
self._assert_inflect('говорили', ['masc'], 'говорил')
self._assert_inflect('говорил', ['neut'], 'говорило')
self._assert_inflect('говорил', ['femn'], 'говорила')
self._assert_inflect('говорить', ['datv'], 'говорить')
def test_inflect_text_nominal(self):
''' Test inflection for multiword text in nominal form. '''
self._assert_inflect('синий короткий', ['accs', 'sing', 'femn'], 'синюю короткую')
self._assert_inflect('красивые слоны', ['accs', 'sing'], 'красивого слона')
self._assert_inflect('вход процесса', ['loct', 'plur'], 'входах процесса')
self._assert_inflect('нарочито сложный тест', ['datv', 'sing'], 'нарочито сложному тесту')
self._assert_inflect('первый дом улиц города', ['loct', 'plur'], 'первых домах улиц города')
self._assert_inflect('шкала оценок', ['loct', 'plur'], 'шкалах оценок')
self._assert_inflect('складские операции', ['sing', 'datv'], 'складской операции')
self._assert_inflect('стороны конфликтного перехода', ['loct', 'sing'], 'стороне конфликтного перехода')
self._assert_inflect('уникомплексные тектологические переходы', ['loct', 'sing'],
'уникомплексном тектологическом переходе')
self._assert_inflect('слабый НИР', ['datv', 'sing'], 'слабому НИР')
self._assert_inflect('слабый НИР', ['accs', 'plur'], 'слабых НИР')
self._assert_inflect('летучий 1-2-бутан', ['ablt', 'sing'], 'летучим 1-2-бутаном')
self._assert_inflect('летучий 1-2-фторметил', ['ablt', 'sing'], 'летучим 1-2-фторметилом')
self._assert_inflect('красивые процессы', ['accs', 'sing'], 'красивого процесс')
self._assert_inflect('красивые процессы', ['gent', 'sing'], 'красивого процесса')
self._assert_inflect('части программы', ['ablt', 'sing'], 'части программой')
self._assert_inflect('первые здания', ['ablt', 'sing'], 'первым зданием')
self._assert_inflect('прямой слон', ['ablt', 'sing'], 'прямым слоном')
self._assert_inflect('тихо говорить', ['past', 'masc'], 'тихо говорил')
self._assert_inflect('быть готовым', ['past', 'masc'], 'был готовым')
self._assert_inflect('уметь готовить', ['pres', '2per'], 'умеешь готовить')
self._assert_inflect('готовить рис', ['pres', '1per'], 'готовлю рис')
# self._assert_inflect('десять миллионов', ['datv'], 'десяти миллионам')
# self._assert_inflect('десять апельсинов', ['datv'], 'десяти апельсинов')
# self._assert_inflect('два миллиона', ['datv'], 'двум миллионам')
self._assert_inflect('техногенема n-го порядка', ['datv'], 'техногенеме n-го порядка')
self._assert_inflect('Положение об органе АБВ', ['datv'], 'Положению об органе АБВ')
def test_inflect_text_cross(self):
''' Test inflection for multiword text in multiple forms. '''
self._assert_inflect('слона кота', ['nomn'], 'слон кота')
self._assert_inflect('готовкой риса', ['nomn'], 'готовка риса')
# self._assert_inflect('реципиенту воздействия', ['nomn'], 'реципиент воздействия')
def test_inflect_complex_mainword(self):
''' Test inflection of mainword conmprised of multiple words. '''
# Do not parse complex main words
self._assert_inflect('слона и кота', ['nomn'], 'слон и кота')
self._assert_inflect('сказал и поехал', ['INFN'], 'сказать и поехал')
def test_inflect_word_pos(self):
''' Test inflection for word changing pars of speech. '''
self._assert_inflect('обеспечит', ['INFN'], 'обеспечить')
self._assert_inflect('обеспечить', ['VERB', '1per'], 'обеспечу')
# self._assert_inflect('обеспечить', ['NOUN', 'sing','nomn'], 'обеспечение')
# self._assert_inflect('обеспечить', ['NOUN', 'plur','datv'], 'обеспечениям')
# self._assert_inflect('синеть', ['NOUN'], 'синь')
# self._assert_inflect('готовить', ['NOUN', 'sing'], 'готовка')
# self._assert_inflect('обеспечить', ['ADJF'], '???')
# self._assert_inflect('обеспечить', ['ADJS'], '???')
# self._assert_inflect('синеть', ['ADJF'], 'синий')
# self._assert_inflect('готовить', ['ADJF', 'sing', 'femn'], 'готовая')
self._assert_inflect('обеспечить', ['PRTF', 'plur', 'past'], 'обеспечившие')
self._assert_inflect('обеспечить', ['PRTS', 'plur'], 'обеспечены')
self._assert_inflect('обеспечить', ['GRND', 'past'], 'обеспечив')
# self._assert_inflect('обеспечить', ['ADVB'], 'обеспечённо')
# self._assert_inflect('обеспечить', ['COMP'], 'обеспеченнее')
# self._assert_inflect('обеспечение', ['INFN'], 'обеспечить')
# self._assert_inflect('обеспечение', ['VERB','1per'], 'обеспечу')
self._assert_inflect('обеспечение', ['NOUN', 'plur', 'nomn'], 'обеспечения')
self._assert_inflect('обеспечение', ['NOUN', 'plur', 'datv'], 'обеспечениям')
# self._assert_inflect('синь', ['ADJF'], 'синий')
# self._assert_inflect('обеспечение', ['PRTF', 'plur', 'past'], 'обеспечившие')
# self._assert_inflect('обеспечение', ['PRTS', 'plur'], 'обеспечены')
# self._assert_inflect('обеспечение', ['GRND', 'past'], 'обеспечив')
# self._assert_inflect('обеспечение', ['ADVB'], 'обеспечённо')
# self._assert_inflect('обеспечение', ['COMP'], 'обеспеченнее')
# self._assert_inflect('синий', ['INFN'], 'синеть')
# self._assert_inflect('синий', ['VERB','1per'], 'синею')
# self._assert_inflect('синий', ['NOUN', 'plur','nomn'], 'синьки')
# self._assert_inflect('синий', ['NOUN', 'plur','datv'], 'синькам')
self._assert_inflect('синий', ['ADJS'], 'синь')
self._assert_inflect('хороший', ['ADJS'], 'хорош')
# self._assert_inflect('синий', ['PRTF', 'plur', 'past'], 'синевшие')
# self._assert_inflect('синий', ['PRTS', 'plur'], '??')
# self._assert_inflect('синий', ['GRND', 'past'], 'синев')
# self._assert_inflect('хороший', ['ADVB'], 'хорошо')
self._assert_inflect('синий', ['COMP'], 'синее')
self._assert_inflect('обеспечащий', ['INFN'], 'обеспечить')
self._assert_inflect('обеспечивающий', ['INFN'], 'обеспечивать')
self._assert_inflect('бегущий', ['INFN'], 'бежать')
self._assert_inflect('бегущий', ['VERB'], 'бегу')
self._assert_inflect('бежавшего', ['VERB'], 'бежал')
# self._assert_inflect('обеспечащий', ['NOUN', 'plur','datv'], 'обеспечениям')
# self._assert_inflect('синеющий', ['NOUN'], 'синь')
# self._assert_inflect('готовящий', ['NOUN', 'sing'], 'готовка')
# self._assert_inflect('синеющий', ['ADJF'], 'синий')
self._assert_inflect('обеспечащий', ['PRTF', 'plur', 'past'], 'обеспечившие')
self._assert_inflect('обеспечащий', ['PRTS', 'plur'], 'обеспечимы')
self._assert_inflect('обеспечащий', ['GRND', 'past'], 'обеспечив')
# self._assert_inflect('обеспечащий', ['ADVB'], 'обеспечённо')
# self._assert_inflect('обеспечащий', ['COMP'], 'обеспеченнее')
def test_inflect_text_pos(self):
''' Test inflection for multiword text changing parts of speech. '''
# self._assert_inflect('готовить еду', ['NOUN', 'sing'], 'готовка еды')
# self._assert_inflect('обеспечение безопасности', ['INFN'], 'обеспечить безопасность')
# self._assert_inflect('сильный удар по мячу', ['INFN'], 'сильно ударить по мячу')
self._assert_inflect('сильно обиженный', ['INFN'], 'сильно обидеть')
# self._assert_inflect('сильно обиженный', ['NOUN'], 'сильная обида')
# self._assert_inflect('надежно обеспечить', ['NOUN'], 'надежное обеспечение')
def test_inflect_invalid_text(self):
''' Test inflection for multiword not coordinated text. '''
self._assert_inflect('синими слоны', ['nomn', 'sing'], 'синими слон')
def test_inflect_context(self):
''' Test content inflection. '''
self.assertEqual(parser.inflect_context('', '', ''), '')
self.assertEqual(parser.inflect_context('', 'красивый', ''), '')
self.assertEqual(parser.inflect_context('', '', 'в'), '')
self.assertEqual(parser.inflect_context('слон', '', ''), 'слон')
self.assertEqual(parser.inflect_context('красивый', '', 'чашка'), 'красивая')
self.assertEqual(parser.inflect_context('красивый', '', 'черного'), 'красивого')
self.assertEqual(parser.inflect_context('слон', '', 'черного'), 'слона')
self.assertEqual(parser.inflect_context('слоны', 'сильный', 'черную'), 'слон')
self.assertEqual(parser.inflect_context('город', 'огня', ''), 'города')
# self.assertEqual(parser.inflect_context('улица', 'дом', ''), 'улицы')
self.assertEqual(parser.inflect_context('большой город', 'стильного', 'необъятной страны'), 'большого города')
self.assertEqual(parser.inflect_context('город', '', ', расположенного неподалеку'), 'города')
def test_inflect_substitute(self):
''' Test substitute inflection. '''
self.assertEqual(parser.inflect_substitute('', ''), '')
self.assertEqual(parser.inflect_substitute('123', '123'), '123')
self.assertEqual(parser.inflect_substitute('', 'слон'), '')
self.assertEqual(parser.inflect_substitute('слон', ''), 'слон')
self.assertEqual(parser.inflect_substitute('слон', 'слон'), 'слон')
self.assertEqual(parser.inflect_substitute('слон', 'слоны'), 'слоны')
self.assertEqual(parser.inflect_substitute('слон', 'кошкой'), 'слоном')
self.assertEqual(parser.inflect_substitute('синий слон', 'стильного чайника'), 'синего слона')
self.assertEqual(parser.inflect_substitute('варить клюкву', 'осуществляет'), 'варит клюкву')
def test_inflect_dependant(self):
''' Test coordination inflection. '''
self.assertEqual(parser.inflect_dependant('', ''), '')
self.assertEqual(parser.inflect_dependant('', 'слон'), '')
self.assertEqual(parser.inflect_dependant('слон', ''), 'слон')
self.assertEqual(parser.inflect_dependant('общий', 'мать'), 'общая')
self.assertEqual(parser.inflect_dependant('синий', 'слонов'), 'синих')
self.assertEqual(parser.inflect_dependant('белый длинный', 'столами'), 'белыми длинными')
def test_find_substr(self):
''' Test substring search. '''
self.assertEqual(parser.find_substr('', ''), (0, 0))
self.assertEqual(parser.find_substr('слон', ''), (0, 0))
self.assertEqual(parser.find_substr('', 'слон'), (0, 0))
self.assertEqual(parser.find_substr('слон', 'слон'), (0, 4))
self.assertEqual(parser.find_substr('сложного слона', 'слон'), (9, 14))
self.assertEqual(parser.find_substr('сложного слона', 'слоном'), (9, 14))
self.assertEqual(parser.find_substr('сложного красивого слона', 'красивые слоном'), (9, 24))
self.assertEqual(parser.find_substr('человек', 'люди'), (0, 7))
if __name__ == '__main__':
unittest.main()

62
tests/t_syntax.py Normal file
View File

@ -0,0 +1,62 @@
''' Unit tests: syntax. '''
import unittest
from cctext import RuSyntax, Capitalization
class TestRusSyntax(unittest.TestCase):
''' Test class for russian syntax. '''
def test_capitalization(self):
''' Testing capitalization. '''
self.assertEqual(Capitalization.from_text(''), Capitalization.unknwn)
self.assertEqual(Capitalization.from_text('Альфа'), Capitalization.first_capital)
self.assertEqual(Capitalization.from_text('АЛЬФА'), Capitalization.upper_case)
self.assertEqual(Capitalization.from_text('альфа'), Capitalization.lower_case)
self.assertEqual(Capitalization.from_text('альФа'), Capitalization.mixed)
self.assertEqual(Capitalization.from_text('альфА'), Capitalization.mixed)
self.assertEqual(Capitalization.from_text('КАиП'), Capitalization.mixed)
self.assertEqual(Capitalization.upper_case.apply_to('альфа'), 'АЛЬФА')
self.assertEqual(Capitalization.lower_case.apply_to('АльФа'), 'альфа')
self.assertEqual(Capitalization.first_capital.apply_to('альфа'), 'Альфа')
self.assertEqual(Capitalization.first_capital.apply_to('АльФа'), 'АльФа')
self.assertEqual(Capitalization.unknwn.apply_to('АльФа'), 'АльФа')
self.assertEqual(Capitalization.mixed.apply_to('АльФа'), 'АльФа')
def test_is_single_word(self):
''' Testing single word identification. '''
self.assertTrue(RuSyntax.is_single_word(''))
self.assertTrue(RuSyntax.is_single_word('word'))
self.assertTrue(RuSyntax.is_single_word('слово'))
self.assertTrue(RuSyntax.is_single_word(' word '), 'Whitespace doesnt count')
self.assertTrue(RuSyntax.is_single_word('1001'), 'Numbers are words')
self.assertTrue(RuSyntax.is_single_word('кое-как'), 'Hyphen doesnt break work')
self.assertTrue(RuSyntax.is_single_word('1-2-метилбутан'), 'Complex words')
self.assertFalse(RuSyntax.is_single_word('one two'))
self.assertFalse(RuSyntax.is_single_word('синий слон'))
def test_tokenize(self):
''' Testing tokenization. '''
self.assertEqual(list(RuSyntax.tokenize('')), [])
self.assertEqual(list(RuSyntax.tokenize(' ')), [])
self.assertEqual(self._list_tokenize('test'), [(0, 4, 'test')])
self.assertEqual(self._list_tokenize(' test '), [(1, 5, 'test')])
self.assertEqual(self._list_tokenize('синий слон'), [(0, 5, 'синий'), (6, 10, 'слон')])
def test_split_words(self):
''' Testing splitting text into words. '''
self.assertEqual([], list(RuSyntax.split_words('')))
self.assertEqual([], list(RuSyntax.split_words(' ')))
self.assertEqual(RuSyntax.split_words('test'), ['test'])
self.assertEqual(RuSyntax.split_words(' test '), ['test'])
self.assertEqual(RuSyntax.split_words('синий слон'), ['синий', 'слон'])
self.assertEqual(RuSyntax.split_words('синий, большой слон'), ['синий', ',', 'большой', 'слон'])
@staticmethod
def _list_tokenize(text: str):
return [(token.start, token.stop, token.text) for token in RuSyntax.tokenize(text)]
if __name__ == '__main__':
unittest.main()