Improved PythonEvaluator when dealing with concept class
This commit is contained in:
@@ -490,6 +490,16 @@ class InfiniteRecursionResolved:
|
|||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_concept(*concepts):
|
||||||
|
if hasattr(concepts, "__iter__"):
|
||||||
|
for concept in concepts:
|
||||||
|
if not isinstance(concept, Concept):
|
||||||
|
raise TypeError(f"'{concept}' must be a concept")
|
||||||
|
else:
|
||||||
|
if not isinstance(concepts, Concept):
|
||||||
|
raise TypeError(f"'{concepts}' must be a concept")
|
||||||
|
|
||||||
|
|
||||||
# ################################
|
# ################################
|
||||||
#
|
#
|
||||||
# Class created for tests purpose
|
# Class created for tests purpose
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from core.builtin_concepts import BuiltinConcepts, ErrorConcept, ReturnValueConc
|
|||||||
from core.concept import Concept, ConceptParts, PROPERTIES_FOR_NEW
|
from core.concept import Concept, ConceptParts, PROPERTIES_FOR_NEW
|
||||||
from core.sheerka.ExecutionContext import ExecutionContext
|
from core.sheerka.ExecutionContext import ExecutionContext
|
||||||
from core.sheerka_logger import console_handler
|
from core.sheerka_logger import console_handler
|
||||||
|
from core.tokenizer import Token, TokenKind
|
||||||
from printer.SheerkaPrinter import SheerkaPrinter
|
from printer.SheerkaPrinter import SheerkaPrinter
|
||||||
from sdp.sheerkaDataProvider import SheerkaDataProvider, Event
|
from sdp.sheerkaDataProvider import SheerkaDataProvider, Event
|
||||||
|
|
||||||
@@ -489,6 +490,37 @@ class Sheerka(Concept):
|
|||||||
metadata = [(index_name, key), ("id", concept_id)] if concept_id else (index_name, key)
|
metadata = [(index_name, key), ("id", concept_id)] if concept_id else (index_name, key)
|
||||||
return self._get_unknown(metadata)
|
return self._get_unknown(metadata)
|
||||||
|
|
||||||
|
def resolve(self, concept):
|
||||||
|
if concept is None:
|
||||||
|
return concept
|
||||||
|
|
||||||
|
# if the entry is a concept token, use its values.
|
||||||
|
if isinstance(concept, Token):
|
||||||
|
if concept.type != TokenKind.CONCEPT:
|
||||||
|
return None
|
||||||
|
concept = concept.value
|
||||||
|
|
||||||
|
# if the entry is a tuple
|
||||||
|
# concept[0] is the name
|
||||||
|
# concept[1] is the id
|
||||||
|
if isinstance(concept, tuple):
|
||||||
|
if concept[1]:
|
||||||
|
if self.is_known(found := self.get_by_id(concept[1])):
|
||||||
|
return found
|
||||||
|
elif concept[0]:
|
||||||
|
return found if self.is_known(found := self.get_by_name(concept[0])) else None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# otherwise search in db
|
||||||
|
if isinstance(concept, str):
|
||||||
|
if self.is_known(found := self.get_by_id(concept)):
|
||||||
|
return found
|
||||||
|
if self.is_known(found := self.get_by_name(concept)):
|
||||||
|
return found
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def has_id(self, concept_id):
|
def has_id(self, concept_id):
|
||||||
"""
|
"""
|
||||||
Returns True if a concept with this id exists in cache
|
Returns True if a concept with this id exists in cache
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
|||||||
from cache.Cache import Cache
|
from cache.Cache import Cache
|
||||||
from cache.ListCache import ListCache
|
from cache.ListCache import ListCache
|
||||||
from core.builtin_concepts import BuiltinConcepts
|
from core.builtin_concepts import BuiltinConcepts
|
||||||
|
from core.concept import ensure_concept
|
||||||
from core.sheerka.services.sheerka_service import ServiceObj, BaseService
|
from core.sheerka.services.sheerka_service import ServiceObj, BaseService
|
||||||
|
|
||||||
|
|
||||||
@@ -105,6 +106,7 @@ class SheerkaComparisonManager(BaseService):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
context.log(f"Setting concept {concept_a} is greater than {concept_b}", who=self.NAME)
|
context.log(f"Setting concept {concept_a} is greater than {concept_b}", who=self.NAME)
|
||||||
|
ensure_concept(concept_a, concept_b)
|
||||||
|
|
||||||
event_digest = context.event.get_digest()
|
event_digest = context.event.get_digest()
|
||||||
comparison_obj = ComparisonObj(event_digest, prop_name, concept_a.id, concept_b.id, ">", comparison_context)
|
comparison_obj = ComparisonObj(event_digest, prop_name, concept_a.id, concept_b.id, ">", comparison_context)
|
||||||
@@ -121,6 +123,7 @@ class SheerkaComparisonManager(BaseService):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
context.log(f"Setting concept {concept_a} is less than {concept_b}", who=self.NAME)
|
context.log(f"Setting concept {concept_a} is less than {concept_b}", who=self.NAME)
|
||||||
|
ensure_concept(concept_a, concept_b)
|
||||||
|
|
||||||
event_digest = context.event.get_digest()
|
event_digest = context.event.get_digest()
|
||||||
comparison_obj = ComparisonObj(event_digest, prop_name, concept_a.id, concept_b.id, "<", comparison_context)
|
comparison_obj = ComparisonObj(event_digest, prop_name, concept_a.id, concept_b.id, "<", comparison_context)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import core.utils
|
import core.utils
|
||||||
from core.builtin_concepts import BuiltinConcepts, ErrorConcept
|
from core.builtin_concepts import BuiltinConcepts, ErrorConcept
|
||||||
from core.concept import Concept
|
from core.concept import Concept, DEFINITION_TYPE_DEF, ensure_concept
|
||||||
from core.sheerka.services.sheerka_service import BaseService
|
from core.sheerka.services.sheerka_service import BaseService
|
||||||
from sdp.sheerkaDataProvider import SheerkaDataProviderDuplicateKeyError
|
from sdp.sheerkaDataProvider import SheerkaDataProviderDuplicateKeyError
|
||||||
|
|
||||||
@@ -30,6 +30,8 @@ class SheerkaCreateNewConcept(BaseService):
|
|||||||
:return: digest of the new concept
|
:return: digest of the new concept
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
ensure_concept(concept)
|
||||||
|
|
||||||
sheerka = self.sheerka
|
sheerka = self.sheerka
|
||||||
|
|
||||||
concept.init_key()
|
concept.init_key()
|
||||||
@@ -49,24 +51,29 @@ class SheerkaCreateNewConcept(BaseService):
|
|||||||
# set id before saving in db
|
# set id before saving in db
|
||||||
sheerka.set_id_if_needed(concept, False)
|
sheerka.set_id_if_needed(concept, False)
|
||||||
|
|
||||||
# update the dictionary of concepts by first key
|
# compute new concepts_by_first_keyword
|
||||||
init_ret_value = self.bnp.get_concepts_by_first_keyword(context, [concept], True)
|
init_ret_value = self.bnp.get_concepts_by_first_keyword(context, [concept], True)
|
||||||
if not init_ret_value.status:
|
if not init_ret_value.status:
|
||||||
return sheerka.ret(self.NAME, False, ErrorConcept(init_ret_value.value))
|
return sheerka.ret(self.NAME, False, ErrorConcept(init_ret_value.value))
|
||||||
concepts_by_first_keyword = init_ret_value.body
|
concepts_by_first_keyword = init_ret_value.body
|
||||||
|
|
||||||
# update resolved dictionary
|
# computes resolved concepts_by_first_keyword
|
||||||
init_ret_value = self.bnp.resolve_concepts_by_first_keyword(context, concepts_by_first_keyword)
|
init_ret_value = self.bnp.resolve_concepts_by_first_keyword(context, concepts_by_first_keyword)
|
||||||
if not init_ret_value.status:
|
if not init_ret_value.status:
|
||||||
return sheerka.ret(self.NAME, False, ErrorConcept(init_ret_value.value))
|
return sheerka.ret(self.NAME, False, ErrorConcept(init_ret_value.value))
|
||||||
resolved_concepts_by_first_keyword = init_ret_value.body
|
resolved_concepts_by_first_keyword = init_ret_value.body
|
||||||
|
|
||||||
concept.freeze_definition_hash()
|
# if everything is fine
|
||||||
|
|
||||||
|
concept.freeze_definition_hash()
|
||||||
cache_manager.add_concept(concept)
|
cache_manager.add_concept(concept)
|
||||||
cache_manager.put(sheerka.CONCEPTS_BY_FIRST_KEYWORD_ENTRY, False, concepts_by_first_keyword)
|
cache_manager.put(sheerka.CONCEPTS_BY_FIRST_KEYWORD_ENTRY, False, concepts_by_first_keyword)
|
||||||
cache_manager.put(sheerka.RESOLVED_CONCEPTS_BY_FIRST_KEYWORD_ENTRY, False, resolved_concepts_by_first_keyword)
|
cache_manager.put(sheerka.RESOLVED_CONCEPTS_BY_FIRST_KEYWORD_ENTRY, False, resolved_concepts_by_first_keyword)
|
||||||
|
|
||||||
|
if concept.metadata.definition_type == DEFINITION_TYPE_DEF and concept.metadata.definition != concept.name:
|
||||||
|
# allow search by definition when definition relevant
|
||||||
|
cache_manager.put(self.sheerka.CONCEPTS_BY_NAME_ENTRY, concept.metadata.definition, concept)
|
||||||
|
|
||||||
if concept.bnf and init_bnf_ret_value is not None and init_bnf_ret_value.status:
|
if concept.bnf and init_bnf_ret_value is not None and init_bnf_ret_value.status:
|
||||||
sheerka.cache_manager.clear(sheerka.CONCEPTS_GRAMMARS_ENTRY)
|
sheerka.cache_manager.clear(sheerka.CONCEPTS_GRAMMARS_ENTRY)
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class SheerkaModifyConcept(BaseService):
|
|||||||
# TODO : update concept by first keyword
|
# TODO : update concept by first keyword
|
||||||
# TODO : update resolved by first keyword
|
# TODO : update resolved by first keyword
|
||||||
# TODO : update concepts grammars
|
# TODO : update concepts grammars
|
||||||
|
# TODO : update when definition_type = DEFINITION_TYPE_DEF
|
||||||
|
|
||||||
ret = self.sheerka.ret(self.NAME, True, self.sheerka.new(BuiltinConcepts.NEW_CONCEPT, body=concept))
|
ret = self.sheerka.ret(self.NAME, True, self.sheerka.new(BuiltinConcepts.NEW_CONCEPT, body=concept))
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import core.builtin_helpers
|
|||||||
from cache.SetCache import SetCache
|
from cache.SetCache import SetCache
|
||||||
from core.ast.nodes import python_to_concept
|
from core.ast.nodes import python_to_concept
|
||||||
from core.builtin_concepts import BuiltinConcepts
|
from core.builtin_concepts import BuiltinConcepts
|
||||||
from core.concept import Concept, ConceptParts
|
from core.concept import Concept, ConceptParts, ensure_concept
|
||||||
from core.sheerka.services.sheerka_service import BaseService
|
from core.sheerka.services.sheerka_service import BaseService
|
||||||
|
|
||||||
GROUP_PREFIX = 'All_'
|
GROUP_PREFIX = 'All_'
|
||||||
@@ -36,6 +36,7 @@ class SheerkaSetsManager(BaseService):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
context.log(f"Setting concept {concept} is a {concept_set}", who=self.NAME)
|
context.log(f"Setting concept {concept} is a {concept_set}", who=self.NAME)
|
||||||
|
ensure_concept(concept, concept_set)
|
||||||
|
|
||||||
if BuiltinConcepts.ISA in concept.metadata.props and concept_set in concept.metadata.props[BuiltinConcepts.ISA]:
|
if BuiltinConcepts.ISA in concept.metadata.props and concept_set in concept.metadata.props[BuiltinConcepts.ISA]:
|
||||||
return self.sheerka.ret(
|
return self.sheerka.ret(
|
||||||
@@ -61,9 +62,7 @@ class SheerkaSetsManager(BaseService):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
context.log(f"Adding concept {concept} to set {concept_set}", who=self.NAME)
|
context.log(f"Adding concept {concept} to set {concept_set}", who=self.NAME)
|
||||||
|
ensure_concept(concept, concept_set)
|
||||||
assert concept.id
|
|
||||||
assert concept_set.id
|
|
||||||
|
|
||||||
set_elements = self.sheerka.cache_manager.get(self.CONCEPTS_GROUPS_ENTRY, concept_set.id)
|
set_elements = self.sheerka.cache_manager.get(self.CONCEPTS_GROUPS_ENTRY, concept_set.id)
|
||||||
if set_elements and concept.id in set_elements:
|
if set_elements and concept.id in set_elements:
|
||||||
@@ -79,6 +78,7 @@ class SheerkaSetsManager(BaseService):
|
|||||||
"""Adding multiple concepts at the same time"""
|
"""Adding multiple concepts at the same time"""
|
||||||
|
|
||||||
context.log(f"Adding concepts {concepts} to set {concept_set}", who=self.NAME)
|
context.log(f"Adding concepts {concepts} to set {concept_set}", who=self.NAME)
|
||||||
|
ensure_concept(concept_set)
|
||||||
already_in_set = []
|
already_in_set = []
|
||||||
for concept in concepts:
|
for concept in concepts:
|
||||||
res = self.add_concept_to_set(context, concept, concept_set)
|
res = self.add_concept_to_set(context, concept, concept_set)
|
||||||
@@ -103,6 +103,8 @@ class SheerkaSetsManager(BaseService):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
ensure_concept(concept)
|
||||||
|
|
||||||
def _get_set_elements(sub_concept):
|
def _get_set_elements(sub_concept):
|
||||||
if not self.isaset(context, sub_concept):
|
if not self.isaset(context, sub_concept):
|
||||||
return self.sheerka.new(BuiltinConcepts.NOT_A_SET, body=concept)
|
return self.sheerka.new(BuiltinConcepts.NOT_A_SET, body=concept)
|
||||||
@@ -151,8 +153,9 @@ class SheerkaSetsManager(BaseService):
|
|||||||
if isinstance(a, BuiltinConcepts): # common KSI error ;-)
|
if isinstance(a, BuiltinConcepts): # common KSI error ;-)
|
||||||
raise SyntaxError("Remember that the first parameter of isinstance MUST be a concept")
|
raise SyntaxError("Remember that the first parameter of isinstance MUST be a concept")
|
||||||
|
|
||||||
if not (isinstance(a, Concept) and isinstance(b, Concept)):
|
ensure_concept(a, b)
|
||||||
return False
|
# if not (isinstance(a, Concept) and isinstance(b, Concept)):
|
||||||
|
# return False
|
||||||
|
|
||||||
# TODO, first check the 'isa' property of a
|
# TODO, first check the 'isa' property of a
|
||||||
if not (a.id and b.id):
|
if not (a.id and b.id):
|
||||||
@@ -163,6 +166,7 @@ class SheerkaSetsManager(BaseService):
|
|||||||
|
|
||||||
def isa(self, a, b):
|
def isa(self, a, b):
|
||||||
|
|
||||||
|
ensure_concept(a, b)
|
||||||
if BuiltinConcepts.ISA not in a.metadata.props:
|
if BuiltinConcepts.ISA not in a.metadata.props:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
+4
-7
@@ -363,7 +363,7 @@ def unstr_concept(concept_repr):
|
|||||||
return key if key != "" else None, id if id != "" else None
|
return key if key != "" else None, id if id != "" else None
|
||||||
|
|
||||||
|
|
||||||
def encode_concept(t, use_concept=False):
|
def encode_concept(t):
|
||||||
"""
|
"""
|
||||||
Given a tuple of concept id, concept id
|
Given a tuple of concept id, concept id
|
||||||
Create a valid Python identifier that can be parsed back
|
Create a valid Python identifier that can be parsed back
|
||||||
@@ -371,15 +371,13 @@ def encode_concept(t, use_concept=False):
|
|||||||
>>> assert encode_concept(("key", "id")) == "__C__KEY_key__ID_id__C__"
|
>>> assert encode_concept(("key", "id")) == "__C__KEY_key__ID_id__C__"
|
||||||
>>> assert encode_concept((None, "id")) == "__C__KEY_00None00__ID_id__C__"
|
>>> assert encode_concept((None, "id")) == "__C__KEY_00None00__ID_id__C__"
|
||||||
>>> assert encode_concept(("key", None)) == "__C__KEY_key__ID_00None00__C__"
|
>>> assert encode_concept(("key", None)) == "__C__KEY_key__ID_00None00__C__"
|
||||||
>>> assert encode_concept(("key", "id"), True) == "__C__USE_CONCEPT__KEY_key__ID_id__C__"
|
|
||||||
|
|
||||||
:param t:
|
:param t:
|
||||||
:param use_concept:
|
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key, id_ = (t[0], t[1]) if isinstance(t, tuple) else (t.key, t.id)
|
key, id_ = (t[0], t[1]) if isinstance(t, tuple) else (t.key, t.id)
|
||||||
prefix = "__C__USE_CONCEPT" if use_concept else "__C"
|
prefix = "__C"
|
||||||
sanitized_key = "".join(c if c.isalnum() else "0" for c in key) if key else "00None00"
|
sanitized_key = "".join(c if c.isalnum() else "0" for c in key) if key else "00None00"
|
||||||
return prefix + f"__KEY_{sanitized_key}__ID_{id_ or '00None00'}__C__"
|
return prefix + f"__KEY_{sanitized_key}__ID_{id_ or '00None00'}__C__"
|
||||||
|
|
||||||
@@ -393,15 +391,14 @@ def decode_concept(text):
|
|||||||
:param text:
|
:param text:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
use_concept = text.startswith("__C__USE_CONCEPT")
|
|
||||||
m = decode_regex.search(text)
|
m = decode_regex.search(text)
|
||||||
lookup = {"00None00": None}
|
lookup = {"00None00": None}
|
||||||
if m:
|
if m:
|
||||||
key = lookup.get(m.group(1), m.group(1))
|
key = lookup.get(m.group(1), m.group(1))
|
||||||
id_ = lookup.get(m.group(2), m.group(2))
|
id_ = lookup.get(m.group(2), m.group(2))
|
||||||
return key, id_, use_concept
|
return key, id_
|
||||||
|
|
||||||
return None, None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def tokens_index(tokens, sub_tokens, skip=0):
|
def tokens_index(tokens, sub_tokens, skip=0):
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ class PythonEvaluator(OneReturnValueEvaluator):
|
|||||||
|
|
||||||
# Do not evaluate if the ast refers to a concept (leave it to ConceptEvaluator)
|
# Do not evaluate if the ast refers to a concept (leave it to ConceptEvaluator)
|
||||||
if isinstance(node.ast_, ast.Expression) and isinstance(node.ast_.body, ast.Name):
|
if isinstance(node.ast_, ast.Expression) and isinstance(node.ast_.body, ast.Name):
|
||||||
c = context.sheerka.get_by_key(node.ast_.body.id)
|
c = context.sheerka.resolve(node.ast_.body.id)
|
||||||
if not context.sheerka.isinstance(c, BuiltinConcepts.UNKNOWN_CONCEPT):
|
if c is not None:
|
||||||
context.log("It's a simple concept. Not for me.", self.name)
|
context.log("It's a simple concept. Not for me.", self.name)
|
||||||
not_for_me = context.sheerka.new(BuiltinConcepts.NOT_FOR_ME, body=node)
|
not_for_me = context.sheerka.new(BuiltinConcepts.NOT_FOR_ME, body=node)
|
||||||
return sheerka.ret(self.name, False, not_for_me, parents=[return_value])
|
return sheerka.ret(self.name, False, not_for_me, parents=[return_value])
|
||||||
@@ -91,54 +91,11 @@ class PythonEvaluator(OneReturnValueEvaluator):
|
|||||||
"BuiltinConcepts": core.builtin_concepts.BuiltinConcepts,
|
"BuiltinConcepts": core.builtin_concepts.BuiltinConcepts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# has to tbe the first, to allow override
|
||||||
method_from_sheerka = self.update_globals_with_sheerka_methods(my_locals, context)
|
method_from_sheerka = self.update_globals_with_sheerka_methods(my_locals, context)
|
||||||
|
|
||||||
if context.obj:
|
self.update_globals_with_context(my_locals, context)
|
||||||
context.log(f"Concept '{context.obj}' is in context. Adding it and its properties to locals.", self.name)
|
self.update_globals_with_node(my_locals, context, node)
|
||||||
|
|
||||||
for prop_name in context.obj.variables():
|
|
||||||
prop_value = context.obj.get_value(prop_name)
|
|
||||||
if isinstance(prop_value, Concept):
|
|
||||||
my_locals[prop_name] = context.sheerka.objvalue(prop_value)
|
|
||||||
else:
|
|
||||||
my_locals[prop_name] = prop_value
|
|
||||||
|
|
||||||
my_locals["self"] = context.obj.body
|
|
||||||
|
|
||||||
node_concept = core.ast.nodes.python_to_concept(node.ast_)
|
|
||||||
unreferenced_names_visitor = UnreferencedNamesVisitor(context.sheerka)
|
|
||||||
unreferenced_names_visitor.visit(node_concept)
|
|
||||||
|
|
||||||
for name in unreferenced_names_visitor.names:
|
|
||||||
context.log(f"Resolving '{name}'.", self.name)
|
|
||||||
|
|
||||||
if name in node.concepts:
|
|
||||||
context.log(f"Using value from node.", self.name)
|
|
||||||
concept = node.concepts[name]
|
|
||||||
return_concept = False
|
|
||||||
|
|
||||||
else:
|
|
||||||
c_key, c_id, return_concept = self.resolve_name(name)
|
|
||||||
|
|
||||||
if c_key in my_locals:
|
|
||||||
context.log(f"Using value from property.", self.name)
|
|
||||||
continue
|
|
||||||
|
|
||||||
context.log(f"Instantiating new concept with {c_key=}, {c_id=}.", self.name)
|
|
||||||
new = context.sheerka.new
|
|
||||||
concept = new((None, c_id)) if c_id else new(c_key)
|
|
||||||
if context.sheerka.isinstance(concept, BuiltinConcepts.UNKNOWN_CONCEPT):
|
|
||||||
context.log(f"({c_key=}, {c_id=}) is not a concept. Skipping.", self.name)
|
|
||||||
continue
|
|
||||||
|
|
||||||
context.log(f"Evaluating '{concept}'", self.name)
|
|
||||||
with context.push(self.name, desc=f"Evaluating '{concept}'", obj=concept) as sub_context:
|
|
||||||
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
|
|
||||||
evaluated = context.sheerka.evaluate_concept(sub_context, concept)
|
|
||||||
sub_context.add_values(return_values=evaluated)
|
|
||||||
|
|
||||||
if evaluated.key == concept.key:
|
|
||||||
my_locals[name] = evaluated if return_concept else context.sheerka.objvalue(evaluated)
|
|
||||||
|
|
||||||
if self.locals: # when exta values are given. Add them
|
if self.locals: # when exta values are given. Add them
|
||||||
my_locals.update(self.locals)
|
my_locals.update(self.locals)
|
||||||
@@ -163,6 +120,77 @@ class PythonEvaluator(OneReturnValueEvaluator):
|
|||||||
|
|
||||||
return methods_from_sheerka # to allow access using prefix "sheerka."
|
return methods_from_sheerka # to allow access using prefix "sheerka."
|
||||||
|
|
||||||
|
def update_globals_with_context(self, my_locals, context):
|
||||||
|
if context.obj:
|
||||||
|
context.log(f"Concept '{context.obj}' is in context. Adding it and its properties to locals.", self.name)
|
||||||
|
|
||||||
|
for prop_name in context.obj.variables():
|
||||||
|
prop_value = context.obj.get_value(prop_name)
|
||||||
|
if isinstance(prop_value, Concept):
|
||||||
|
my_locals[prop_name] = context.sheerka.objvalue(prop_value)
|
||||||
|
else:
|
||||||
|
my_locals[prop_name] = prop_value
|
||||||
|
|
||||||
|
my_locals["self"] = context.obj.body
|
||||||
|
|
||||||
|
def update_globals_with_node(self, my_locals, context, node):
|
||||||
|
node_concept = core.ast.nodes.python_to_concept(node.ast_)
|
||||||
|
unreferenced_names_visitor = UnreferencedNamesVisitor(context.sheerka)
|
||||||
|
unreferenced_names_visitor.visit(node_concept)
|
||||||
|
|
||||||
|
for name in unreferenced_names_visitor.names:
|
||||||
|
context.log(f"Resolving '{name}'.", self.name)
|
||||||
|
|
||||||
|
if name in node.concepts:
|
||||||
|
context.log(f"Using value from node.", self.name)
|
||||||
|
concept = self.resolve_concept(context, node.concepts[name])
|
||||||
|
|
||||||
|
elif name in my_locals:
|
||||||
|
context.log(f"Using value from property.", self.name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
context.log(f"Instantiating new concept with {name}.", self.name)
|
||||||
|
concept = self.resolve_concept(context, name)
|
||||||
|
|
||||||
|
if concept is None:
|
||||||
|
context.log(f"Concept '{name}' is not found or cannot be instantiated. Skipping.", self.name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if concept.metadata.is_evaluated:
|
||||||
|
context.log(f"Concept {name} is already evaluated.", self.name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
context.log(f"Evaluating '{concept}'", self.name)
|
||||||
|
with context.push(self.name, desc=f"Evaluating '{concept}'", obj=concept) as sub_context:
|
||||||
|
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
|
||||||
|
evaluated = context.sheerka.evaluate_concept(sub_context, concept)
|
||||||
|
sub_context.add_values(return_values=evaluated)
|
||||||
|
|
||||||
|
if evaluated.key != concept.key:
|
||||||
|
context.log(f"Error while evaluating '{name}'. Skipping.", self.name)
|
||||||
|
continue
|
||||||
|
concept = evaluated
|
||||||
|
|
||||||
|
my_locals[name] = context.sheerka.objvalue(concept)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_concept(context, concept_hint):
|
||||||
|
if isinstance(concept_hint, Concept):
|
||||||
|
return concept_hint
|
||||||
|
|
||||||
|
concept = context.sheerka.resolve(concept_hint)
|
||||||
|
if concept is None:
|
||||||
|
return None
|
||||||
|
new_instance = context.sheerka.new_from_template(concept, concept.key)
|
||||||
|
if isinstance(concept_hint, tuple):
|
||||||
|
# It's means that it comes from PythonParser which have found a concept token (c:xxx:)
|
||||||
|
# So a concept was explicitly required, not its value
|
||||||
|
# We mark the concept as already evaluated, so it's body will not be evaluated
|
||||||
|
new_instance.metadata.is_evaluated = True
|
||||||
|
|
||||||
|
return new_instance
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_name(to_resolve):
|
def resolve_name(to_resolve):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -141,15 +141,22 @@ class BaseParser:
|
|||||||
body=self.error_sink if self.has_error else tree,
|
body=self.error_sink if self.has_error else tree,
|
||||||
try_parsed=try_parse)
|
try_parsed=try_parse)
|
||||||
|
|
||||||
def get_input_as_text(self, parser_input, custom_switcher=None):
|
def get_input_as_text(self, parser_input, custom_switcher=None, tracker=None):
|
||||||
|
"""
|
||||||
|
Recreate back the source code from parser_input
|
||||||
|
:param parser_input: list of Tokens
|
||||||
|
:param custom_switcher: map of [TokenKind, overridden values]
|
||||||
|
:param tracker: keep track of the value overridden by custom_switcher
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if isinstance(parser_input, list):
|
if isinstance(parser_input, list):
|
||||||
return self.get_text_from_tokens(parser_input, custom_switcher)
|
return self.get_text_from_tokens(parser_input, custom_switcher, tracker)
|
||||||
|
|
||||||
if isinstance(parser_input, ParserResultConcept):
|
if isinstance(parser_input, ParserResultConcept):
|
||||||
parser_input = parser_input.source
|
parser_input = parser_input.source
|
||||||
|
|
||||||
if "c:" in parser_input:
|
if "c:" in parser_input:
|
||||||
return self.get_text_from_tokens(list(Tokenizer(parser_input)), custom_switcher)
|
return self.get_text_from_tokens(list(Tokenizer(parser_input)), custom_switcher, tracker)
|
||||||
|
|
||||||
return parser_input
|
return parser_input
|
||||||
|
|
||||||
@@ -194,7 +201,14 @@ class BaseParser:
|
|||||||
return lst
|
return lst
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_text_from_tokens(tokens, custom_switcher=None):
|
def get_text_from_tokens(tokens, custom_switcher=None, tracker=None):
|
||||||
|
"""
|
||||||
|
Create the source code, from the list of token
|
||||||
|
:param tokens: list of tokens
|
||||||
|
:param custom_switcher: to override the behaviour (the return value) of some token
|
||||||
|
:param tracker: keep track of the original token value when custom switched
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if tokens is None:
|
if tokens is None:
|
||||||
return ""
|
return ""
|
||||||
res = ""
|
res = ""
|
||||||
@@ -213,6 +227,8 @@ class BaseParser:
|
|||||||
for token in tokens:
|
for token in tokens:
|
||||||
value = switcher.get(token.type, lambda t: t.value)(token)
|
value = switcher.get(token.type, lambda t: t.value)(token)
|
||||||
res += value
|
res += value
|
||||||
|
if tracker is not None and token.type in custom_switcher:
|
||||||
|
tracker[value] = token.value
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -72,11 +72,12 @@ class PythonParser(BaseParser):
|
|||||||
tree = None
|
tree = None
|
||||||
|
|
||||||
python_switcher = {
|
python_switcher = {
|
||||||
TokenKind.CONCEPT: lambda t: core.utils.encode_concept(t.value, True)
|
TokenKind.CONCEPT: lambda t: core.utils.encode_concept(t.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
source = self.get_input_as_text(parser_input, python_switcher)
|
tracker = {}
|
||||||
|
source = self.get_input_as_text(parser_input, python_switcher, tracker)
|
||||||
source = source.strip()
|
source = source.strip()
|
||||||
parser_input = parser_input if isinstance(parser_input, str) else source
|
parser_input = parser_input if isinstance(parser_input, str) else source
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ class PythonParser(BaseParser):
|
|||||||
BuiltinConcepts.PARSER_RESULT,
|
BuiltinConcepts.PARSER_RESULT,
|
||||||
parser=self,
|
parser=self,
|
||||||
source=parser_input,
|
source=parser_input,
|
||||||
body=PythonNode(parser_input, tree),
|
body=PythonNode(parser_input, tree, tracker),
|
||||||
try_parsed=None))
|
try_parsed=None))
|
||||||
|
|
||||||
self.log_result(context, parser_input, ret)
|
self.log_result(context, parser_input, ret)
|
||||||
|
|||||||
@@ -122,3 +122,18 @@ class BaseTest:
|
|||||||
concept.bnf = expression or StrMatch(name)
|
concept.bnf = expression or StrMatch(name)
|
||||||
concept.metadata.definition_type = DEFINITION_TYPE_BNF
|
concept.metadata.definition_type = DEFINITION_TYPE_BNF
|
||||||
return concept
|
return concept
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def def_concept(name, definition, variables=None, **kwargs):
|
||||||
|
concept = Concept(name=name, definition=definition, definition_type=DEFINITION_TYPE_DEF)
|
||||||
|
if variables:
|
||||||
|
for v in variables:
|
||||||
|
concept.def_var(v)
|
||||||
|
|
||||||
|
if kwargs:
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
if k in ("body", "pre", "post", "where"):
|
||||||
|
setattr(concept.metadata, k, v)
|
||||||
|
else:
|
||||||
|
concept.metadata.variables[k] = v
|
||||||
|
return concept
|
||||||
|
|||||||
@@ -139,6 +139,23 @@ class TestSheerkaCreateNewConcept(TestUsingMemoryBasedSheerka):
|
|||||||
|
|
||||||
assert res.status
|
assert res.status
|
||||||
|
|
||||||
|
def test_i_can_get_by_name_when_created_with_def_definition(self):
|
||||||
|
sheerka = self.get_sheerka(cache_only=False)
|
||||||
|
context = self.get_context(sheerka)
|
||||||
|
concept = self.def_concept("plus", "a plus b", ["a", "b"])
|
||||||
|
|
||||||
|
res = sheerka.create_new_concept(context, concept)
|
||||||
|
|
||||||
|
assert res.status
|
||||||
|
assert sheerka.get_by_name(concept.name) == concept
|
||||||
|
assert sheerka.get_by_name(concept.metadata.definition) == concept
|
||||||
|
|
||||||
|
concept = Concept(name="foo", definition="foo", definition_type=DEFINITION_TYPE_DEF)
|
||||||
|
res = sheerka.create_new_concept(context, concept)
|
||||||
|
|
||||||
|
assert res.status
|
||||||
|
assert sheerka.get_by_name(concept.name) == concept # it's not a list, ie the entry is not duplicated
|
||||||
|
|
||||||
|
|
||||||
class TestSheerkaCreateNewConceptFileBased(TestUsingFileBasedSheerka):
|
class TestSheerkaCreateNewConceptFileBased(TestUsingFileBasedSheerka):
|
||||||
def test_i_can_add_several_concepts(self):
|
def test_i_can_add_several_concepts(self):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import pytest
|
|||||||
from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept, UserInputConcept
|
from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept, UserInputConcept
|
||||||
from core.concept import Concept, PROPERTIES_TO_SERIALIZE, ConceptParts
|
from core.concept import Concept, PROPERTIES_TO_SERIALIZE, ConceptParts
|
||||||
from core.sheerka.Sheerka import Sheerka, BASE_NODE_PARSER_CLASS
|
from core.sheerka.Sheerka import Sheerka, BASE_NODE_PARSER_CLASS
|
||||||
|
from core.tokenizer import Token, TokenKind, Tokenizer
|
||||||
|
|
||||||
from tests.TestUsingFileBasedSheerka import TestUsingFileBasedSheerka
|
from tests.TestUsingFileBasedSheerka import TestUsingFileBasedSheerka
|
||||||
from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka
|
from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka
|
||||||
@@ -274,6 +275,55 @@ class TestSheerkaUsingMemoryBasedSheerka(TestUsingMemoryBasedSheerka):
|
|||||||
sheerka = self.get_sheerka()
|
sheerka = self.get_sheerka()
|
||||||
assert not sheerka.is_success(sheerka.new(BuiltinConcepts.TOO_MANY_SUCCESS))
|
assert not sheerka.is_success(sheerka.new(BuiltinConcepts.TOO_MANY_SUCCESS))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("concept, expected", [
|
||||||
|
(None, None),
|
||||||
|
("foo", ["foo", "foo2"]),
|
||||||
|
("bar", "bar"),
|
||||||
|
("1001", "foo"), # by id take precedence over by name
|
||||||
|
("plus", "plus"),
|
||||||
|
("a mult b", "mult"),
|
||||||
|
("unknown", None),
|
||||||
|
|
||||||
|
# by tuple
|
||||||
|
((None, None), None),
|
||||||
|
(("foo", None), ["foo", "foo2"]),
|
||||||
|
(("foo", "1002"), "foo2"),
|
||||||
|
((None, "1001"), "foo"),
|
||||||
|
(("plus", None), "plus"),
|
||||||
|
(("1001", None), "1001"),
|
||||||
|
(("unknown", None), None),
|
||||||
|
((None, "unknown"), None),
|
||||||
|
#
|
||||||
|
# by token
|
||||||
|
(Token(TokenKind.CONCEPT, (None, None), 0, 0, 0), None),
|
||||||
|
(Token(TokenKind.CONCEPT, ("foo", None), 0, 0, 0), ["foo", "foo2"]),
|
||||||
|
|
||||||
|
])
|
||||||
|
def test_i_can_resolve_concept(self, concept, expected):
|
||||||
|
sheerka, context, *concepts = self.init_concepts(
|
||||||
|
"foo",
|
||||||
|
Concept("foo", body="another one"),
|
||||||
|
"bar",
|
||||||
|
self.def_concept("plus", "a plus b", ["a", "b"]),
|
||||||
|
Concept("a mult b").def_var("a").def_var("b"),
|
||||||
|
Concept("1001"),
|
||||||
|
)
|
||||||
|
|
||||||
|
cmap = {k: concepts[i] for i, k in enumerate(["foo", "foo2", "bar", "plus", "mult", "1001"])}
|
||||||
|
cmap[None] = None
|
||||||
|
|
||||||
|
if isinstance(expected, list):
|
||||||
|
assert sheerka.resolve(concept) == [cmap[e] for e in expected]
|
||||||
|
else:
|
||||||
|
assert sheerka.resolve(concept) == cmap[expected]
|
||||||
|
|
||||||
|
def test_i_can_resolve_when_searching_by_definition(self):
|
||||||
|
sheerka, context, plus = self.init_concepts(
|
||||||
|
self.def_concept("plus", "a plus b", ["a", "b"]),
|
||||||
|
create_new=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sheerka.resolve("a plus b") == plus
|
||||||
|
|
||||||
class TestSheerkaUsingFileBasedSheerka(TestUsingFileBasedSheerka):
|
class TestSheerkaUsingFileBasedSheerka(TestUsingFileBasedSheerka):
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ def test_encode_concept_key_id():
|
|||||||
assert core.utils.encode_concept(("key", "id")) == "__C__KEY_key__ID_id__C__"
|
assert core.utils.encode_concept(("key", "id")) == "__C__KEY_key__ID_id__C__"
|
||||||
assert core.utils.encode_concept((None, "id")) == "__C__KEY_00None00__ID_id__C__"
|
assert core.utils.encode_concept((None, "id")) == "__C__KEY_00None00__ID_id__C__"
|
||||||
assert core.utils.encode_concept(("key", None)) == "__C__KEY_key__ID_00None00__C__"
|
assert core.utils.encode_concept(("key", None)) == "__C__KEY_key__ID_00None00__C__"
|
||||||
assert core.utils.encode_concept(("key", "id"), True) == "__C__USE_CONCEPT__KEY_key__ID_id__C__"
|
|
||||||
assert core.utils.encode_concept(("k + y", "id")) == "__C__KEY_k000y__ID_id__C__"
|
assert core.utils.encode_concept(("k + y", "id")) == "__C__KEY_k000y__ID_id__C__"
|
||||||
|
|
||||||
concept = Concept("foo").init_key()
|
concept = Concept("foo").init_key()
|
||||||
@@ -209,7 +208,6 @@ def test_encode_concept_key_id():
|
|||||||
|
|
||||||
|
|
||||||
def test_decode_concept_key_id():
|
def test_decode_concept_key_id():
|
||||||
assert core.utils.decode_concept("__C__KEY_key__ID_id__C__") == ("key", "id", False)
|
assert core.utils.decode_concept("__C__KEY_key__ID_id__C__") == ("key", "id")
|
||||||
assert core.utils.decode_concept("__C__KEY_00None00__ID_id__C__") == (None, "id", False)
|
assert core.utils.decode_concept("__C__KEY_00None00__ID_id__C__") == (None, "id")
|
||||||
assert core.utils.decode_concept("__C__KEY_key__ID_00None00__C__") == ("key", None, False)
|
assert core.utils.decode_concept("__C__KEY_key__ID_00None00__C__") == ("key", None)
|
||||||
assert core.utils.decode_concept("__C__USE_CONCEPT__KEY_key__ID_id__C__") == ("key", "id", True)
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.builtin_concepts import ReturnValueConcept, ParserResultConcept, BuiltinConcepts
|
from core.builtin_concepts import ReturnValueConcept, ParserResultConcept, BuiltinConcepts
|
||||||
from core.concept import Concept
|
from core.concept import Concept, DEFINITION_TYPE_DEF
|
||||||
from evaluators.PythonEvaluator import PythonEvaluator
|
from evaluators.PythonEvaluator import PythonEvaluator
|
||||||
from parsers.PythonParser import PythonNode, PythonParser
|
from parsers.PythonParser import PythonNode, PythonParser
|
||||||
from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka
|
from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka
|
||||||
|
|
||||||
|
|
||||||
def get_context_name(context):
|
def get_concept_name(concept):
|
||||||
return context.name
|
return concept.name
|
||||||
|
|
||||||
|
|
||||||
class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
|
class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
|
||||||
@@ -118,23 +118,38 @@ class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
|
|||||||
context = self.get_context()
|
context = self.get_context()
|
||||||
context.sheerka.add_in_cache(Concept("foo", body="2"))
|
context.sheerka.add_in_cache(Concept("foo", body="2"))
|
||||||
|
|
||||||
parsed = PythonParser().parse(context, "get_context_name(c:foo:)")
|
parsed = PythonParser().parse(context, "get_concept_name(c:foo:)")
|
||||||
python_evaluator = PythonEvaluator()
|
python_evaluator = PythonEvaluator()
|
||||||
python_evaluator.locals["get_context_name"] = get_context_name
|
python_evaluator.locals["get_concept_name"] = get_concept_name
|
||||||
evaluated = python_evaluator.eval(context, parsed)
|
evaluated = python_evaluator.eval(context, parsed)
|
||||||
|
|
||||||
assert evaluated.status
|
assert evaluated.status
|
||||||
assert evaluated.value == "foo"
|
assert evaluated.value == "foo"
|
||||||
|
|
||||||
# sanity, does not work otherwise
|
# sanity, does not work otherwise
|
||||||
parsed = PythonParser().parse(context, "get_context_name(foo)")
|
parsed = PythonParser().parse(context, "get_concept_name(foo)")
|
||||||
python_evaluator = PythonEvaluator()
|
python_evaluator = PythonEvaluator()
|
||||||
python_evaluator.locals["get_context_name"] = get_context_name
|
python_evaluator.locals["get_concept_name"] = get_concept_name
|
||||||
evaluated = python_evaluator.eval(context, parsed)
|
evaluated = python_evaluator.eval(context, parsed)
|
||||||
|
|
||||||
assert not evaluated.status
|
assert not evaluated.status
|
||||||
assert evaluated.body.body.args[0] == "'int' object has no attribute 'name'"
|
assert evaluated.body.body.args[0] == "'int' object has no attribute 'name'"
|
||||||
|
|
||||||
|
def test_i_can_call_function_with_complex_concepts(self):
|
||||||
|
sheerka, context, plus, mult = self.init_concepts(
|
||||||
|
self.def_concept("plus", "a plus b", ["a", "b"]),
|
||||||
|
self.def_concept("mult", "a mult b", ["a", "b"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
parsed = PythonParser().parse(context, "is_greater_than(BuiltinConcepts.PRECEDENCE, mult, plus)")
|
||||||
|
python_evaluator = PythonEvaluator()
|
||||||
|
|
||||||
|
evaluated = python_evaluator.eval(context, parsed)
|
||||||
|
|
||||||
|
assert evaluated.status
|
||||||
|
assert sheerka.get_concepts_weights(BuiltinConcepts.PRECEDENCE) == {'1001': 1, '1002': 2}
|
||||||
|
|
||||||
|
|
||||||
# @pytest.mark.parametrize("text, concept_key, concept_id, use_concept", [
|
# @pytest.mark.parametrize("text, concept_key, concept_id, use_concept", [
|
||||||
# ("__C__key__C__", "key", None, False),
|
# ("__C__key__C__", "key", None, False),
|
||||||
# ("__C__key__id__C__", "key", "id", False),
|
# ("__C__key__id__C__", "key", "id", False),
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import ast
|
import ast
|
||||||
|
|
||||||
|
import core.utils
|
||||||
import pytest
|
import pytest
|
||||||
from core.builtin_concepts import ParserResultConcept, NotForMeConcept
|
from core.builtin_concepts import ParserResultConcept, NotForMeConcept
|
||||||
from core.tokenizer import Tokenizer, LexerError
|
from core.tokenizer import Tokenizer, LexerError
|
||||||
from parsers.PythonParser import PythonNode, PythonParser, PythonErrorNode
|
from parsers.PythonParser import PythonNode, PythonParser, PythonErrorNode
|
||||||
import core.utils
|
|
||||||
|
|
||||||
from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka
|
from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka
|
||||||
|
|
||||||
@@ -72,12 +73,16 @@ class TestPythonParser(TestUsingMemoryBasedSheerka):
|
|||||||
assert res.value.get_value("reason")[0].text == error_text
|
assert res.value.get_value("reason")[0].text == error_text
|
||||||
|
|
||||||
def test_i_can_parse_a_concept(self):
|
def test_i_can_parse_a_concept(self):
|
||||||
text = "c:name|key: + 1"
|
text = "c:name|id: + 1"
|
||||||
|
|
||||||
parser = PythonParser()
|
parser = PythonParser()
|
||||||
res = parser.parse(self.get_context(), text)
|
res = parser.parse(self.get_context(), text)
|
||||||
|
encoded = core.utils.encode_concept(("name", "id"))
|
||||||
|
|
||||||
assert res
|
assert res
|
||||||
assert res.value.value == PythonNode(
|
assert res.value.value == PythonNode(
|
||||||
"c:name|key: + 1",
|
"c:name|id: + 1",
|
||||||
ast.parse(core.utils.encode_concept(("name", "key"), True) + "+1", mode="eval"))
|
ast.parse(encoded + "+1", mode="eval"))
|
||||||
|
assert res.value.value.concepts == {
|
||||||
|
encoded: ("name", "id")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user