diff --git a/CHANGELOG.md b/CHANGELOG.md index b92bb3d..6ca1094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,15 @@ ### Removed * `penman.lexer` is now non-public ([#77]) +* `penman.interface` is removed from the public API but remains + temporarily for backward compatibility ([#78]) ### Changed * Make `parse()`, `format()`, `interpret()`, and `configure()` available at the top-level module ([#75]) +* Make `iterparse()`, `iterdecode()`, `parse_triples()`, and + `format_triples()` available at the top-level module ([#78]) * Move the implementations of `parse()` and `format()` to separate modules from PENMANCodec ([#76]) * Make `penman.tree.Tree` available at the top-level module @@ -692,3 +696,4 @@ First release with very basic functionality. [#75]: https://github.com/goodmami/penman/issues/75 [#76]: https://github.com/goodmami/penman/issues/76 [#77]: https://github.com/goodmami/penman/issues/77 +[#78]: https://github.com/goodmami/penman/issues/78 diff --git a/docs/api/penman.interface.rst b/docs/api/penman.interface.rst deleted file mode 100644 index c17d83f..0000000 --- a/docs/api/penman.interface.rst +++ /dev/null @@ -1,19 +0,0 @@ - -penman.interface -================ - -.. automodule:: penman.interface - -Graph-reading Functions ------------------------ - -.. autofunction:: decode -.. autofunction:: loads -.. autofunction:: load - -Graph-writing Functions ------------------------ - -.. autofunction:: encode -.. autofunction:: dumps -.. autofunction:: dump diff --git a/docs/api/penman.rst b/docs/api/penman.rst index e372f7e..3e554aa 100644 --- a/docs/api/penman.rst +++ b/docs/api/penman.rst @@ -35,7 +35,6 @@ Other ''''' - :doc:`penman.exceptions` -- Exception classes -- :doc:`penman.interface` -- Functional interface to a codec - :doc:`penman.transform` -- Graph and tree transformation functions @@ -74,30 +73,54 @@ Classes Module Functions ---------------- +Trees +''''' + .. autofunction:: parse +.. autofunction:: iterparse + +.. autofunction:: format + .. function:: interpret(t, model=None) + Interpret a graph from the :class:`Tree` *t*. + Alias of :func:`penman.layout.interpret` +Graphs +'''''' + .. autofunction:: decode -.. autofunction:: loads +.. autofunction:: iterdecode -.. autofunction:: load - -.. autofunction:: format +.. autofunction:: encode .. function:: configure(g, top=None, model=None, strict=False) + Configure a tree from the :class:`Graph` *g*. + Alias of :func:`penman.layout.configure` -.. autofunction:: encode +Corpus Files +'''''''''''' + +.. autofunction:: loads + +.. autofunction:: load .. autofunction:: dumps .. autofunction:: dump +Triple Conjunctions +''''''''''''''''''' + +.. autofunction:: parse_triples + +.. autofunction:: format_triples + Exceptions ---------- diff --git a/docs/basic.rst b/docs/basic.rst index 1a8ad08..e5a4e7a 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -15,11 +15,10 @@ And here's an example of its library usage: .. code-block:: python - >>> from penman import PENMANCodec - >>> codec = PENMANCodec() - >>> g = codec.decode('(s / sleep-01 :polarity - :ARG0 (i / i))') + >>> import penman + >>> g = penman.decode('(s / sleep-01 :polarity - :ARG0 (i / i))') >>> g.triples.remove(('s', ':polarity', '-')) - >>> print(PENMANCodec().encode(g)) + >>> print(penman.encode(g)) (s / sleep-01 :ARG0 (i / i)) @@ -97,20 +96,19 @@ For example: .. code-block:: python - >>> from penman import PENMANCodec - >>> codec = PENMANCodec() - >>> g = codec.decode('(b / bark-01 :ARG0 (d / dog))') + >>> import penman + >>> g = penman.decode('(b / bark-01 :ARG0 (d / dog))') >>> g.instances() [Instance(source='b', role=':instance', target='bark-01'), Instance(source='d', role=':instance', target='dog')] >>> g.edges() [Edge(source='b', role=':ARG0', target='d')] >>> sorted(g.variables()) ['b', 'd'] - >>> print(codec.encode(g, top='d')) + >>> print(penman.encode(g, top='d')) (d / dog :ARG0-of (b / bark-01)) >>> g.triples.append(('b', ':polarity', '-')) - >>> print(codec.encode(g)) + >>> print(penman.encode(g)) (b / bark-01 :ARG0 (d / dog) :polarity -) diff --git a/docs/index.rst b/docs/index.rst index d6c1539..32ea55f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,7 +48,6 @@ models if they need more control. api/penman.epigraph api/penman.exceptions api/penman.graph - api/penman.interface api/penman.layout api/penman.model api/penman.models diff --git a/penman/__init__.py b/penman/__init__.py index 2eca250..2f85734 100644 --- a/penman/__init__.py +++ b/penman/__init__.py @@ -16,10 +16,14 @@ 'Graph', 'PENMANCodec', 'parse', + 'iterparse', + 'parse_triples', 'format', + 'format_triples', 'interpret', 'configure', 'decode', + 'iterdecode', 'encode', 'load', 'loads', @@ -46,16 +50,24 @@ interpret, configure, ) -from penman._parse import parse -from penman._format import format -from penman.codec import PENMANCodec -from penman.interface import ( - decode, - encode, - load, - loads, - dump, - dumps, +from penman._parse import ( + parse, + iterparse, + parse_triples, +) +from penman._format import ( + format, + format_triples, +) +from penman.codec import ( + PENMANCodec, + _decode as decode, + _iterdecode as iterdecode, + _encode as encode, + _load as load, + _loads as loads, + _dump as dump, + _dumps as dumps, ) logging.basicConfig() diff --git a/penman/_format.py b/penman/_format.py index a314083..a5c9a4a 100644 --- a/penman/_format.py +++ b/penman/_format.py @@ -10,6 +10,19 @@ def format(tree: Tree, compact: bool = False) -> str: """ Format *tree* into a PENMAN string. + + Args: + tree: a Tree object + indent: how to indent formatted strings + compact: if ``True``, put initial attributes on the first line + Returns: + the PENMAN-serialized string of the Tree *t* + Example: + >>> import penman + >>> penman.format(('b', [('/', 'bark-01'), + ... (':ARG0', ('d', [('/', 'dog')]))])) + (b / bark-01 + :ARG0 (d / dog)) """ if not isinstance(tree, Tree): tree = Tree(tree) diff --git a/penman/codec.py b/penman/codec.py index e6cbb29..a0656dd 100644 --- a/penman/codec.py +++ b/penman/codec.py @@ -4,7 +4,8 @@ Serialization of PENMAN graphs. """ -from typing import Union, Iterable, Iterator, List +from typing import Union, Iterable, Iterator, List, IO +from pathlib import Path from penman.types import ( Variable, @@ -25,6 +26,11 @@ from penman import layout +# "Utility" types; not Penman-specific + +FileOrFilename = Union[str, Path, IO[str]] + + class PENMANCodec(object): """ An encoder/decoder for PENMAN-serialized graphs. @@ -63,7 +69,7 @@ def iterdecode(self, The :class:`~penman.graph.Graph` objects described in *lines*. """ - for tree in self.iterparse(lines): + for tree in iterparse(lines): yield layout.interpret(tree, self.model) def iterparse(self, lines: Union[Iterable[str], str]) -> Iterator[Tree]: @@ -155,3 +161,173 @@ def format_triples(self, """ return format_triples(triples, indent=indent) + + +# The following are for the top-level API. They are renamed when they +# are imported into __init__.py. They are named with the leading +# underscore here so they are not included as part of penman.codec's +# public API. + +def _decode(s: str, + model: Model = None) -> Graph: + """ + Deserialize PENMAN-serialized *s* into its Graph object + + Args: + s: a string containing a single PENMAN-serialized graph + model: the model used for interpreting the graph + Returns: + the Graph object described by *s* + Example: + >>> import penman + >>> penman.decode('(b / bark-01 :ARG0 (d / dog))') + + + """ + codec = PENMANCodec(model=model) + return codec.decode(s) + + +def _iterdecode(lines: Union[Iterable[str], str], + model: Model = None) -> Iterator[Graph]: + """ + Yield graphs parsed from *lines*. + + Args: + lines: a string or open file with PENMAN-serialized graphs + model: the model used for interpreting the graph + Returns: + The :class:`~penman.graph.Graph` objects described in + *lines*. + Example: + >>> import penman + >>> for g in penman.iterdecode('(a / alpha) (b / beta)'): + ... print(repr(g)) + + + """ + codec = PENMANCodec(model=model) + yield from codec.iterdecode(lines) + + +def _encode(g: Graph, + top: Variable = None, + model: Model = None, + indent: Union[int, bool] = -1, + compact: bool = False) -> str: + """ + Serialize the graph *g* from *top* to PENMAN notation. + + Args: + g: the Graph object + top: if given, the node to use as the top in serialization + model: the model used for interpreting the graph + indent: how to indent formatted strings + compact: if ``True``, put initial attributes on the first line + Returns: + the PENMAN-serialized string of the Graph *g* + Example: + >>> import penman + >>> from penman.graph import Graph + >>> penman.encode(Graph([('h', 'instance', 'hi')])) + '(h / hi)' + + """ + codec = PENMANCodec(model=model) + return codec.encode(g, + top=top, + indent=indent, + compact=compact) + + +def _load(source: FileOrFilename, + model: Model = None) -> List[Graph]: + """ + Deserialize a list of PENMAN-encoded graphs from *source*. + + Args: + source: a filename or file-like object to read from + model: the model used for interpreting the graph + Returns: + a list of Graph objects + """ + codec = PENMANCodec(model=model) + if isinstance(source, (str, Path)): + with open(source) as fh: + return list(codec.iterdecode(fh)) + else: + assert hasattr(source, 'read') + return list(codec.iterdecode(source)) + + +def _loads(string: str, + model: Model = None) -> List[Graph]: + """ + Deserialize a list of PENMAN-encoded graphs from *string*. + + Args: + string: a string containing graph data + model: the model used for interpreting the graph + Returns: + a list of Graph objects + """ + codec = PENMANCodec(model=model) + return list(codec.iterdecode(string)) + + +def _dump(graphs: Iterable[Graph], + file: FileOrFilename, + model: Model = None, + indent: Union[int, bool] = -1, + compact: bool = False) -> None: + """ + Serialize each graph in *graphs* to PENMAN and write to *file*. + + Args: + graphs: an iterable of Graph objects + file: a filename or file-like object to write to + model: the model used for interpreting the graph + indent: how to indent formatted strings + compact: if ``True``, put initial attributes on the first line + """ + codec = PENMANCodec(model=model) + if isinstance(file, (str, Path)): + with open(file, 'w') as fh: + _dump_stream(fh, graphs, codec, indent, compact) + else: + assert hasattr(file, 'write') + _dump_stream(file, graphs, codec, indent, compact) + + +def _dump_stream(fh, gs, codec, indent, compact): + """Helper method for dump() for incremental printing.""" + ss = (codec.encode(g, indent=indent, compact=compact) + for g in gs) + try: + print(next(ss), file=fh) + except StopIteration: + return + for s in ss: + print(file=fh) + print(s, file=fh) + + +def _dumps(graphs: Iterable[Graph], + model: Model = None, + indent: Union[int, bool] = -1, + compact: bool = False) -> str: + """ + Serialize each graph in *graphs* to the PENMAN format. + + Args: + graphs: an iterable of Graph objects + model: the model used for interpreting the graph + indent: how to indent formatted strings + compact: if ``True``, put initial attributes on the first line + Returns: + the string of serialized graphs + """ + codec = PENMANCodec(model=model) + strings = [codec.encode(g, indent=indent, compact=compact) + for g in graphs] + return '\n\n'.join(strings) diff --git a/penman/interface.py b/penman/interface.py index 6b00ddc..338337c 100644 --- a/penman/interface.py +++ b/penman/interface.py @@ -1,155 +1,24 @@ """ Functions for basic reading and writing of PENMAN graphs. -""" - -from typing import Union, Iterable, List -from pathlib import Path - -from penman.codec import PENMANCodec -from penman.model import Model -from penman.graph import Graph -from penman.types import (Variable, file_or_filename) - - -def decode(s: str, - model: Model = None) -> Graph: - """ - Deserialize PENMAN-serialized *s* into its Graph object - - Args: - s: a string containing a single PENMAN-serialized graph - model: the model used for interpreting the graph - Returns: - the Graph object described by *s* - Example: - >>> from penman.interface import decode - >>> decode('(b / bark-01 :ARG0 (d / dog))') - - - """ - codec = PENMANCodec(model=model) - return codec.decode(s) - - -def encode(g: Graph, - top: Variable = None, - model: Model = None, - indent: Union[int, bool] = -1, - compact: bool = False) -> str: - """ - Serialize the graph *g* from *top* to PENMAN notation. - - Args: - g: the Graph object - top: if given, the node to use as the top in serialization - model: the model used for interpreting the graph - indent: how to indent formatted strings - compact: if ``True``, put initial attributes on the first line - Returns: - the PENMAN-serialized string of the Graph *g* - Example: - >>> from penman.interface import encode - >>> from penman.graph import Graph - >>> encode(Graph([('h', 'instance', 'hi')])) - '(h / hi)' - - """ - codec = PENMANCodec(model=model) - return codec.encode(g, - top=top, - indent=indent, - compact=compact) - -def load(source: file_or_filename, - model: Model = None) -> List[Graph]: - """ - Deserialize a list of PENMAN-encoded graphs from *source*. - - Args: - source: a filename or file-like object to read from - model: the model used for interpreting the graph - Returns: - a list of Graph objects - """ - codec = PENMANCodec(model=model) - if isinstance(source, (str, Path)): - with open(source) as fh: - return list(codec.iterdecode(fh)) - else: - assert hasattr(source, 'read') - return list(codec.iterdecode(source)) - - -def loads(string: str, - model: Model = None) -> List[Graph]: - """ - Deserialize a list of PENMAN-encoded graphs from *string*. - - Args: - string: a string containing graph data - model: the model used for interpreting the graph - Returns: - a list of Graph objects - """ - codec = PENMANCodec(model=model) - return list(codec.iterdecode(string)) - - -def dump(graphs: Iterable[Graph], - file: file_or_filename, - model: Model = None, - indent: Union[int, bool] = -1, - compact: bool = False) -> None: - """ - Serialize each graph in *graphs* to PENMAN and write to *file*. - - Args: - graphs: an iterable of Graph objects - file: a filename or file-like object to write to - model: the model used for interpreting the graph - indent: how to indent formatted strings - compact: if ``True``, put initial attributes on the first line - """ - codec = PENMANCodec(model=model) - if isinstance(file, (str, Path)): - with open(file, 'w') as fh: - _dump(fh, graphs, codec, indent, compact) - else: - assert hasattr(file, 'write') - _dump(file, graphs, codec, indent, compact) - - -def _dump(fh, gs, codec, indent, compact): - """Helper method for dump() for incremental printing.""" - ss = (codec.encode(g, indent=indent, compact=compact) - for g in gs) - try: - print(next(ss), file=fh) - except StopIteration: - return - for s in ss: - print(file=fh) - print(s, file=fh) - - -def dumps(graphs: Iterable[Graph], - model: Model = None, - indent: Union[int, bool] = -1, - compact: bool = False) -> str: - """ - Serialize each graph in *graphs* to the PENMAN format. +NOTE: This module is now deprecated and will be removed in a future +version. Its functions are now available in the penman module. +""" - Args: - graphs: an iterable of Graph objects - model: the model used for interpreting the graph - indent: how to indent formatted strings - compact: if ``True``, put initial attributes on the first line - Returns: - the string of serialized graphs - """ - codec = PENMANCodec(model=model) - strings = [codec.encode(g, indent=indent, compact=compact) - for g in graphs] - return '\n\n'.join(strings) +import warnings + +from penman.codec import ( # noqa: F401 + _decode as decode, + _iterdecode as iterdecode, + _encode as encode, + _load as load, + _loads as loads, + _dump as dump, + _dumps as dumps, +) + +warnings.warn( + 'The penman.interface module is deprecated. Use the functions from ' + 'the penman module directly, e.g., penman.decode().', + DeprecationWarning) diff --git a/penman/types.py b/penman/types.py index ba7ab95..bb913b4 100644 --- a/penman/types.py +++ b/penman/types.py @@ -3,8 +3,7 @@ Basic types used by various Penman modules. """ -from typing import (Union, Iterable, Tuple, List, IO, Any) -from pathlib import Path +from typing import (Union, Iterable, Tuple, List, Any) Variable = str @@ -19,8 +18,3 @@ Target = Union[Variable, Constant] BasicTriple = Tuple[Variable, Role, Target] Triples = Iterable[BasicTriple] - - -# "Utility" types; not Penman-specific - -file_or_filename = Union[str, Path, IO[str]] diff --git a/tests/test_codec.py b/tests/test_codec.py index c354eb9..576ab23 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -15,121 +15,13 @@ class TestPENMANCodec(object): def test_parse(self): - assert codec.parse('()') == ( - None, []) - assert codec.parse('(a)') == ( - 'a', []) - assert codec.parse('(a / )') == ( - 'a', [('/', None)]) - assert codec.parse('(a / alpha)') == ( - 'a', [('/', 'alpha')]) - assert codec.parse('(a : b)') == ( - 'a', [(':', 'b')]) - assert codec.parse('(a : ())') == ( - 'a', [(':', (None, []))]) - assert codec.parse('(a : (b))') == ( - 'a', [(':', ('b', []))]) - assert codec.parse('(a / alpha :ARG (b / beta))') == ( - 'a', [('/', 'alpha'), - (':ARG', ('b', [('/', 'beta')]))]) - assert codec.parse('(a :ARG-of b)') == ( - 'a', [(':ARG-of', 'b')]) - assert codec.parse('(a :ARG~1 b~2)') == ( - 'a', [(':ARG~1', 'b~2')]) - # https://github.com/goodmami/penman/issues/50 - assert codec.parse('(a :ARG "str~ing")') == ( - 'a', [(':ARG', '"str~ing"')]) - assert codec.parse('(a :ARG "str~ing"~1)') == ( - 'a', [(':ARG', '"str~ing"~1')]) + assert codec.parse('(a / alpha)') == ('a', [('/', 'alpha')]) + + def test_parse_triples(self): + assert codec.parse_triples('role(a, b)') == [('a', 'role', 'b')] def test_format(self): - assert codec.format( - (None, []) - ) == '()' - assert codec.format( - ('a', []) - ) == '(a)' - assert codec.format( - ('a', [('/', None)]) - ) == '(a /)' - assert codec.format( - ('a', [('/', '')]) - ) == '(a /)' - assert codec.format( - ('a', [('/', 'alpha')]) - ) == '(a / alpha)' - assert codec.format( - ('a', [('', 'b')]) - ) == '(a : b)' - assert codec.format( - ('a', [(':', 'b')]) - ) == '(a : b)' - assert codec.format( - ('a', [(':', (None, []))]) - ) == '(a : ())' - assert codec.format( - ('a', [('', ('b', []))]) - ) == '(a : (b))' - assert codec.format( - ('a', [('/', 'alpha'), - ('ARG', ('b', [('/', 'beta')]))]), - indent=None - ) == '(a / alpha :ARG (b / beta))' - assert codec.format( - ('a', [('ARG-of', 'b')]) - ) == '(a :ARG-of b)' - assert codec.format( - ('a', [(':ARG-of', 'b')]) - ) == '(a :ARG-of b)' - assert codec.format( - ('a', [('ARG~1', 'b~2')]) - ) == '(a :ARG~1 b~2)' - - def test_format_with_parameters(self): - # no indent - assert codec.format( - ('a', [('/', 'alpha'), ('ARG', ('b', [('/', 'beta')]))]), - indent=None - ) == '(a / alpha :ARG (b / beta))' - # default (adaptive) indent - assert codec.format( - ('a', [('/', 'alpha'), ('ARG', ('b', [('/', 'beta')]))]), - indent=-1 - ) == ('(a / alpha\n' - ' :ARG (b / beta))') - # fixed indent - assert codec.format( - ('a', [('/', 'alpha'), ('ARG', ('b', [('/', 'beta')]))]), - indent=6 - ) == ('(a / alpha\n' - ' :ARG (b / beta))') - # default compactness of attributes - assert codec.format( - ('a', [('/', 'alpha'), - ('polarity', '-'), - ('ARG', ('b', [('/', 'beta')]))]), - compact=False - ) == ('(a / alpha\n' - ' :polarity -\n' - ' :ARG (b / beta))') - # compact of attributes - assert codec.format( - ('a', [('/', 'alpha'), - ('polarity', '-'), - ('ARG', ('b', [('/', 'beta')]))]), - compact=True - ) == ('(a / alpha :polarity -\n' - ' :ARG (b / beta))') - # compact of attributes (only initial) - assert codec.format( - ('a', [('/', 'alpha'), - ('polarity', '-'), - ('ARG', ('b', [('/', 'beta')])), - ('mode', 'expressive')]), - compact=True - ) == ('(a / alpha :polarity -\n' - ' :ARG (b / beta)\n' - ' :mode expressive)') + assert codec.format(('a', [('/', 'alpha')])) == '(a / alpha)' def test_decode(self, x1): # unlabeled single node @@ -408,39 +300,3 @@ def test_encode_issue_67(self): '(h / have-org-role-91\n' ' :ARG0 (a / activist)\n' ' :ARG2 a)') - - def test_parse_triples(self): - assert codec.parse_triples('role(a,b)') == [ - ('a', 'role', 'b')] - assert codec.parse_triples('role(a, b)') == [ - ('a', 'role', 'b')] - assert codec.parse_triples('role(a ,b)') == [ - ('a', 'role', 'b')] - assert codec.parse_triples('role(a , b)') == [ - ('a', 'role', 'b')] - assert codec.parse_triples('role(a,)') == [ - ('a', 'role', None)] - assert codec.parse_triples('role(a ,)') == [ - ('a', 'role', None)] - assert codec.parse_triples('role(a,b)^role(b,c)') == [ - ('a', 'role', 'b'), ('b', 'role', 'c')] - assert codec.parse_triples('role(a, b) ^role(b, c)') == [ - ('a', 'role', 'b'), ('b', 'role', 'c')] - assert codec.parse_triples('role(a, b) ^ role(b, c)') == [ - ('a', 'role', 'b'), ('b', 'role', 'c')] - with pytest.raises(penman.DecodeError): - decode('role') - with pytest.raises(penman.DecodeError): - decode('role(') - with pytest.raises(penman.DecodeError): - decode('role(a') - with pytest.raises(penman.DecodeError): - decode('role()') - with pytest.raises(penman.DecodeError): - decode('role(a,') - with pytest.raises(penman.DecodeError): - decode('role(a ^') - with pytest.raises(penman.DecodeError): - decode('role(a b') - with pytest.raises(penman.DecodeError): - decode('role(a b)') diff --git a/tests/test_interface.py b/tests/test_interface.py deleted file mode 100644 index 464e6e8..0000000 --- a/tests/test_interface.py +++ /dev/null @@ -1,37 +0,0 @@ - -from penman.tree import Tree -from penman.interface import ( - decode, - loads, - load, - encode, - dumps, - dump, -) - - -def test_decode(): - assert decode('(a / alpha)').triples == [('a', ':instance', 'alpha')] - - -def test_loads(): - gs = loads('(a / alpha)(b / beta)') - assert len(gs) == 2 - assert gs[0].triples == [('a', ':instance', 'alpha')] - assert gs[1].triples == [('b', ':instance', 'beta')] - - -def test_load(): - pass - - -def test_encode(): - assert encode(decode('(a / alpha)')) == '(a / alpha)' - - -def test_dumps(): - assert dumps(loads('(a / alpha)(b / beta)')) == '(a / alpha)\n\n(b / beta)' - - -def test_dump(): - pass diff --git a/tests/test_penman.py b/tests/test_penman.py new file mode 100644 index 0000000..5704110 --- /dev/null +++ b/tests/test_penman.py @@ -0,0 +1,223 @@ + +import pytest + +from penman import ( + parse, + parse_triples, + interpret, + decode, + loads, + load, + format, + format_triples, + configure, + encode, + dumps, + dump, + DecodeError, +) + + +def test_decode(): + assert decode('(a / alpha)').triples == [('a', ':instance', 'alpha')] + + +def test_loads(): + gs = loads('(a / alpha)(b / beta)') + assert len(gs) == 2 + assert gs[0].triples == [('a', ':instance', 'alpha')] + assert gs[1].triples == [('b', ':instance', 'beta')] + + +def test_load(tmp_path): + f = tmp_path / 'test_load1' + f.write_text('(a / alpha)(b / beta)') + gs = load(f) + assert len(gs) == 2 + assert gs[0].triples == [('a', ':instance', 'alpha')] + assert gs[1].triples == [('b', ':instance', 'beta')] + + with f.open() as fh: + assert load(fh) == gs + + +def test_encode(): + assert encode(decode('(a / alpha)')) == '(a / alpha)' + + +def test_dumps(): + assert dumps(loads('(a / alpha)(b / beta)')) == '(a / alpha)\n\n(b / beta)' + + +def test_dump(tmp_path): + gs = loads('(a / alpha)(b / beta)') + f1 = tmp_path / 'test_dump1' + f2 = tmp_path / 'test_dump2' + dump(gs, f1) + with f2.open('w') as fh: + dump(gs, fh) + assert f1.read_text() == f2.read_text() + + +def test_parse(): + assert parse('()') == (None, []) + assert parse('(a)') == ('a', []) + assert parse('(a / )') == ('a', [('/', None)]) + assert parse('(a / alpha)') == ('a', [('/', 'alpha')]) + assert parse('(a : b)') == ('a', [(':', 'b')]) + assert parse('(a : ())') == ('a', [(':', (None, []))]) + assert parse('(a : (b))') == ('a', [(':', ('b', []))]) + assert parse('(a / alpha :ARG (b / beta))') == ( + 'a', [('/', 'alpha'), + (':ARG', ('b', [('/', 'beta')]))]) + assert parse('(a :ARG-of b)') == ('a', [(':ARG-of', 'b')]) + assert parse('(a :ARG~1 b~2)') == ('a', [(':ARG~1', 'b~2')]) + # https://github.com/goodmami/penman/issues/50 + assert parse('(a :ARG "str~ing")') == ('a', [(':ARG', '"str~ing"')]) + assert parse('(a :ARG "str~ing"~1)') == ('a', [(':ARG', '"str~ing"~1')]) + + +def test_format(): + assert format((None, [])) == '()' + assert format(('a', [])) == '(a)' + assert format(('a', [('/', None)])) == '(a /)' + assert format(('a', [('/', '')])) == '(a /)' + assert format(('a', [('/', 'alpha')])) == '(a / alpha)' + assert format(('a', [('', 'b')])) == '(a : b)' + assert format(('a', [(':', 'b')])) == '(a : b)' + assert format(('a', [(':', (None, []))])) == '(a : ())' + assert format(('a', [('', ('b', []))])) == '(a : (b))' + assert format( + ('a', [('/', 'alpha'), + ('ARG', ('b', [('/', 'beta')]))]), + indent=None + ) == '(a / alpha :ARG (b / beta))' + assert format(('a', [('ARG-of', 'b')])) == '(a :ARG-of b)' + assert format(('a', [(':ARG-of', 'b')])) == '(a :ARG-of b)' + assert format(('a', [('ARG~1', 'b~2')])) == '(a :ARG~1 b~2)' + + +def test_parse_triples(): + assert parse_triples('role(a,b)') == [ + ('a', ':role', 'b')] + assert parse_triples('role(a, b)') == [ + ('a', ':role', 'b')] + assert parse_triples('role(a ,b)') == [ + ('a', ':role', 'b')] + assert parse_triples('role(a , b)') == [ + ('a', ':role', 'b')] + assert parse_triples('role(a,)') == [ + ('a', ':role', None)] + assert parse_triples('role(a ,)') == [ + ('a', ':role', None)] + assert parse_triples('role(a,b)^role(b,c)') == [ + ('a', ':role', 'b'), ('b', ':role', 'c')] + assert parse_triples('role(a, b) ^role(b, c)') == [ + ('a', ':role', 'b'), ('b', ':role', 'c')] + assert parse_triples('role(a, b) ^ role(b, c)') == [ + ('a', ':role', 'b'), ('b', ':role', 'c')] + with pytest.raises(DecodeError): + decode('role') + with pytest.raises(DecodeError): + decode('role(') + with pytest.raises(DecodeError): + decode('role(a') + with pytest.raises(DecodeError): + decode('role()') + with pytest.raises(DecodeError): + decode('role(a,') + with pytest.raises(DecodeError): + decode('role(a ^') + with pytest.raises(DecodeError): + decode('role(a b') + with pytest.raises(DecodeError): + decode('role(a b)') + + +def test_format_triples(): + triples = format_triples([ + ('a', ':instance', 'alpha'), + ('a', ':ARG0', 'b'), + ('b', ':instance', 'beta'), + ('g', ':ARG0', 'a'), + ('g', ':instance', 'gamma'), + ('g', ':ARG1', 'b'), + ]) + assert triples == ( + 'instance(a, alpha) ^\n' + 'ARG0(a, b) ^\n' + 'instance(b, beta) ^\n' + 'ARG0(g, a) ^\n' + 'instance(g, gamma) ^\n' + 'ARG1(g, b)' + ) + + +def test_format_with_parameters(): + # no indent + assert format( + ('a', [('/', 'alpha'), ('ARG', ('b', [('/', 'beta')]))]), + indent=None + ) == '(a / alpha :ARG (b / beta))' + # default (adaptive) indent + assert format( + ('a', [('/', 'alpha'), ('ARG', ('b', [('/', 'beta')]))]), + indent=-1 + ) == ('(a / alpha\n' + ' :ARG (b / beta))') + # fixed indent + assert format( + ('a', [('/', 'alpha'), ('ARG', ('b', [('/', 'beta')]))]), + indent=6 + ) == ('(a / alpha\n' + ' :ARG (b / beta))') + # default compactness of attributes + assert format( + ('a', [('/', 'alpha'), + ('polarity', '-'), + ('ARG', ('b', [('/', 'beta')]))]), + compact=False + ) == ('(a / alpha\n' + ' :polarity -\n' + ' :ARG (b / beta))') + # compact of attributes + assert format( + ('a', [('/', 'alpha'), + ('polarity', '-'), + ('ARG', ('b', [('/', 'beta')]))]), + compact=True + ) == ('(a / alpha :polarity -\n' + ' :ARG (b / beta))') + # compact of attributes (only initial) + assert format( + ('a', [('/', 'alpha'), + ('polarity', '-'), + ('ARG', ('b', [('/', 'beta')])), + ('mode', 'expressive')]), + compact=True + ) == ('(a / alpha :polarity -\n' + ' :ARG (b / beta)\n' + ' :mode expressive)') + + +def test_interpret(): + t = parse('(a / alpha :ARG0 (b / beta) :ARG0-of (g / gamma :ARG1 b))') + g = interpret(t) + assert g.triples == [ + ('a', ':instance', 'alpha'), + ('a', ':ARG0', 'b'), + ('b', ':instance', 'beta'), + ('g', ':ARG0', 'a'), + ('g', ':instance', 'gamma'), + ('g', ':ARG1', 'b'), + ] + + +def test_configure(): + g = decode('(a / alpha :ARG0 (b / beta) :ARG0-of (g / gamma :ARG1 b))') + t = configure(g) + assert t.node == ( + 'a', [('/', 'alpha'), + (':ARG0', ('b', [('/', 'beta')])), + (':ARG0-of', ('g', [('/', 'gamma'), + (':ARG1', 'b')]))])