diff --git a/README.mkd b/README.mkd index 63d5cc6..a557db0 100644 --- a/README.mkd +++ b/README.mkd @@ -291,10 +291,11 @@ the official module can be used by clyngor if it is available. - 0.4.0 (todo) - see [further ideas](#Further-ideas) - 0.3.24 - - when using clingo module, the models contains only the output atoms, not everything - - access to the answer set number with `.with_answer_number` - - parsing and string reproduction of nested atoms such as `a((a("g(2,3)",(2)),))` is now correctly handled and tested - - fix the `models.command` output when clingo module is used + - `#show 3.` and `#show "hello !".` are [now handled](eb2002dc906d9fc32efd50738fc2c67100e1cd2) + - when using clingo module, the models contains only the output atoms, [not everything](31375774c437403e8a05f5fe8d0346caba0f43e4) (thank you Arnaud) + - [access to](cc60217975de123a5ef0d083fb10971e0d89c03e) the answer set number with `.with_answer_number` + - [parsing and string reproduction](c0c090c34a7028ba34c49815f0197c67c76e7bfb) of nested atoms such as `a((a("g(2,3)",(2)),))` is now correctly handled and tested + - [fix](1840c36e3f57c926a565fef7352cd1b083194e58) the `models.command` output when clingo module is used - 0.3.20 - fix #7 - improve testing cover, fix warning in recent versions of pytest diff --git a/clyngor/answers.py b/clyngor/answers.py index a2b5d88..348bc71 100644 --- a/clyngor/answers.py +++ b/clyngor/answers.py @@ -11,7 +11,7 @@ def naive_parsing_of_answer_set(answer_set:str, *, discard_quotes:bool=False, parse_int:bool=True, parse_args:bool=True) -> [(str, tuple)]: """Yield (pred, args), naively parsed from given answer set encoded as clingo output string""" # print('NAIVE_PARSING_OF_ANSWER_SET:', answer_set, f'\t discard_quotes={discard_quotes}, parse_int={parse_int}, parse_args={parse_args}') - REG_ANSWER_SET = re.compile(r'([a-z][a-zA-Z0-9_]*)(\([^)]+\))?') + REG_ANSWER_SET = re.compile(r'([a-z][a-zA-Z0-9_]*|[0-9]+|"[^"]*")(\([^)]+\))?') for match in REG_ANSWER_SET.finditer(answer_set): pred, args = match.groups() @@ -27,6 +27,7 @@ def naive_parsing_of_answer_set(answer_set:str, *, discard_quotes:bool=False, pa ) if parse_args else args elif args: # args should not be parsed args = args + pred = int(pred) if pred.isnumeric() else pred # handle yield pred, args or () # print('\t>', pred, args) @@ -234,7 +235,7 @@ def _parse_answer(self, answer_set:str) -> iter: elif isinstance(answer_set, str): # the good ol' split # print('THE GOOD OLD SPLIT:', f"discard_quotes={self._discard_quotes} collapse_atoms={self._collapse_atoms}") yield from self.__finish_parsing(naive_parsing_of_answer_set(answer_set, discard_quotes=self._discard_quotes and not self._collapse_atoms, parse_int=self._parse_int, parse_args=True or self._collapse_args or self._first_arg_only)) - elif isinstance(answer_set, (set, tuple)) and all(isinstance(atom, tuple) for atom in answer_set): # already parsed + elif isinstance(answer_set, (set, tuple)) and all(isinstance(atom, (str, int, tuple)) for atom in answer_set): # already parsed # print('FROM SET OR TUPLE') yield from self.__finish_parsing(answer_set) else: # unknown format @@ -316,11 +317,12 @@ def __init__(self, solver, statistics:callable=(lambda: {})): self._statistics = lambda s=solver: s.statistics assert callable(self._statistics) + def __compute_answers(self): kwargs = {'yield_': True, 'async': True} # compat with 3.7 with self._solver.solve(**kwargs) as models: for model in models: - answer_set = set((a.name, utils.clingo_value_to_python(a.arguments)) + answer_set = set(utils.clingo_symbol_as_python_value(a) for a in model.symbols(shown=True)) yield answer_set, model.cost, model.optimality_proven, model.number diff --git a/clyngor/parsing.py b/clyngor/parsing.py index 0b36abb..8c6be42 100644 --- a/clyngor/parsing.py +++ b/clyngor/parsing.py @@ -36,9 +36,9 @@ def visit_args(self, node, children): def visit_single_value(self, node, children): return self.visit_subterm(node, children) def visit_one_uplet(self, node, children): - return self.visit_term(node, ('', children)) + return self.visit_atom(node, ('', children)) def visit_n_uplet(self, node, children): - return self.visit_term(node, ('', children)) + return self.visit_atom(node, ('', children)) def visit_text(self, node, children): text = tuple(children)[0] if children else '' @@ -54,7 +54,7 @@ def visit_subterm(self, node, children): else: return (predicate, tuple(args[0])) if args else predicate - def visit_term(self, node, children): + def visit_atom(self, node, children): predicate, *args = children if self.first_arg_only and args: @@ -81,7 +81,8 @@ def single_value(): return '(', subterm, ')' def one_uplet(): return '(', subterm, ',', ')' def n_uplet(): return '(', subterm, ap.OneOrMore(',', subterm), ')' # NB: litteral outputed by #show are not handled. - def term(): return ident, ap.Optional("(", args, ")") + def atom(): return ident, ap.Optional("(", args, ")") + def term(): return [litteral, atom] def terms(): return ap.ZeroOrMore(term) return terms diff --git a/clyngor/test/test_parsing.py b/clyngor/test/test_parsing.py index 4919e25..2c64c3d 100644 --- a/clyngor/test/test_parsing.py +++ b/clyngor/test/test_parsing.py @@ -93,6 +93,17 @@ def test_string(): assert args == ('","',) +def test_single_litteral(): + """Show that string with comma in it is handled correctly""" + parsed = Parser().parse_clasp_output(OUTCLASP_SINGLE_LITERAL.splitlines()) + type, answer_number = next(parsed) + assert type == 'answer_number' + type, model = next(parsed) + assert next(parsed, None) is None, "there is only one model" + assert type == 'answer', "the model is an answer" + assert len(model) == 2, "only 2 atom in it" + assert model == {3, '"hello !"'} + def test_complex_atoms(): parsed = Parser().parse_clasp_output(OUTCLASP_COMPLEX_ATOMS.splitlines()) type, answer_number = next(parsed) @@ -263,6 +274,19 @@ def test_multithread_with_progression(): CPU Time : 0.000s """ +OUTCLASP_SINGLE_LITERAL = """clasp version 3.2.0 +Reading from test.lp +Solving... +Answer: 1 +3 "hello !" +SATISFIABLE + +Models : 1 +Calls : 1 +Time : 0.001s (Solving: 0.00s 1st Model: 0.00s Unsat: 0.00s) +CPU Time : 0.001s +""" + OUTCLASP_STRING = """clasp version 3.2.0 Reading from test.lp Solving... diff --git a/clyngor/test/test_solving.py b/clyngor/test/test_solving.py index 7e58c78..d70836d 100644 --- a/clyngor/test/test_solving.py +++ b/clyngor/test/test_solving.py @@ -34,4 +34,15 @@ def test_constants(asp_code_with_constants): assert next(iter(answer)) == (2,) +def test_restricted_and_literal_outputs(): + asp_code = 'a. link(a). #show link/1. #show 3. #show "hello !".' + answers = tuple(solve([], inline=asp_code).by_predicate) + assert len(answers) == 1 + answer = answers[0] + print(answers) + assert len(answer) == 3 + assert set(answer) == {'link', 3, '"hello !"'} + assert answer == {'link': {('a',)}, 3: {()}, '"hello !"': {()}} + + # TODO: test solving.command diff --git a/clyngor/utils.py b/clyngor/utils.py index 43adc67..c7fc572 100644 --- a/clyngor/utils.py +++ b/clyngor/utils.py @@ -84,7 +84,9 @@ def is_quoted(arg:str) -> bool: def clingo_value_to_python(value:object) -> int or str or tuple: """Convert a clingo.Symbol object to the python equivalent""" - if isinstance(value, (int, str)): + if str(type(value)) == 'clingo.Symbol': + return clingo_symbol_as_python_value(value) + elif isinstance(value, (int, str)): return value elif isinstance(value, (tuple, list)): return tuple(map(clingo_value_to_python, value)) @@ -112,6 +114,20 @@ def clingo_value_to_python(value:object) -> int or str or tuple: "".format(value, type(value))) +def clingo_symbol_as_python_value(term) -> object: + "Convert a clingo.Symbol object to the python equivalent" + if str(term.type) == 'Function': + assert term.name is not None + return (term.name, clingo_value_to_python(term.arguments)) + elif str(term.type) == 'String': + assert term.name is None + return ('"' + term.string + '"', ()) + elif str(term.type) == 'Number': + assert term.name is None + return (term.number, ()) + raise TypeError("Can't handle clingo.Symbol like {} of type {}." + "".format(value, type(value))) + def python_value_to_asp(val:str or int or list or tuple, *, args_of_predicate:bool=False) -> str or tuple: """Convert given python value in an ASP format""" if isinstance(val, (str, int)):