Skip to content
Merged
3 changes: 3 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
27) PR #3438 for #3435. Add support for sub_transformations in the kwargs
documentation functionality.

26) PR #3443 for #3243. Unifies NEMO parallelisation scripts to reduce the
amount of code duplication between all the parallelisation scripts, and
adds a README for the NEMO scripts directory.
Expand Down
83 changes: 75 additions & 8 deletions src/psyclone/docstring_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
Marcin Kurczewski - https://github.com/rr-/docstring_parser.
'''

from __future__ import annotations

from collections import OrderedDict
from dataclasses import dataclass
import inspect
Expand All @@ -85,16 +87,21 @@ class ArgumentData():
desc: str
inline_type: bool

def gen_docstring(self, function: Union[None, Callable[..., Any]] = None)\
-> str:
def gen_docstring(self, function: Union[None, Callable[..., Any]] = None,
cls_name: str = "") -> str:
'''
:param function: The function who the generated docstring will be for.
Default option is None. If no function is supplied, there
can be no type annotation and so the type information is
included inline in the :param or in a separate :type
entry.
:param cls_name: The cls_name to use for saying which argument is this
from. Used for documenting sub transformation options.

:returns: The docstring represented by this ArgumentData.
'''
if cls_name:
cls_name = f"(Option provided for {cls_name}) "
rstr = ":param "
if function:
# If the argument is in function's parameter list and has a type
Expand All @@ -103,13 +110,13 @@ def gen_docstring(self, function: Union[None, Callable[..., Any]] = None)\
val = signature.parameters.get(self.name)
if (val is not None and val.annotation is not
inspect.Parameter.empty):
rstr += f"{self.name}: {self.desc}"
rstr += f"{self.name}: {cls_name}{self.desc}"
return rstr

if self.inline_type:
rstr += f"{self.datatype} {self.name}: {self.desc}"
rstr += f"{self.datatype} {self.name}: {cls_name}{self.desc}"
else:
rstr += f"{self.name}: {self.desc}{os.linesep}"
rstr += f"{self.name}: {cls_name}{self.desc}{os.linesep}"
rstr += f":type {self.name}: {self.datatype}"

return rstr
Expand Down Expand Up @@ -171,6 +178,7 @@ class DocstringData():
arguments: OrderedDict
raises: list
returns: ReturnsData
sub_arguments: OrderedDict[str, OrderedDict]

def add_data(self, docstring_element:
Union[ArgumentData, RaisesData, ReturnsData, str]) -> None:
Expand Down Expand Up @@ -200,7 +208,32 @@ def add_data(self, docstring_element:
f"'{docstring_element}'."
)

def merge(self, other_data, replace_desc: bool = False,
def add_subarguments(self, cls_name: str, other_data: "DocstringData",
replace_args: bool = False):
'''Add the other DocstringData's arguments as sub arguments to be
referenced via cls_name.

:param cls_name: The class name to use for the sub arguments.
:param other_data: The DocstringData object whose arguments to add
as sub arguments for this object.
:param replace_args: whether to replace duplicate sub arguments if
found.
'''
# If the class isn't already in the sub arguments list, we just
# add a copy of the other data's argument dicts to the sub_arguments.
if cls_name not in self.sub_arguments:
self.sub_arguments[cls_name] = other_data.arguments.copy()
return
# Otherwise we need to add them manually.
for arg in other_data.arguments:
# If the arg is already present and we're not overwriting then
# skip.
if (arg in self.sub_arguments[cls_name].keys() and
not replace_args):
continue
self.sub_arguments[cls_name][arg] = other_data.arguments[arg]

def merge(self, other_data: "DocstringData", replace_desc: bool = False,
replace_args: bool = False, replace_returns: bool = False):
'''
Merges the other_data DocstringData object into this one.
Expand All @@ -211,7 +244,6 @@ def merge(self, other_data, replace_desc: bool = False,
always.

:param other_data: the DocstringData object to merge into this object.
:type other_data: :py:class:`psyclone.docstring_parser.DocstringData`
:param replace_desc: whether to replace the desc with that of
other_data.
:param replace_args: whether to replace duplicate arguments with that
Expand Down Expand Up @@ -239,6 +271,23 @@ def merge(self, other_data, replace_desc: bool = False,
and other_data.returns is not None):
self.returns = other_data.returns

# Merge the sub_arguments.
for subarg in other_data.sub_arguments:
# If the sub argument isn't in our own sub arguments, make a copy
# of the sub argument.
if subarg not in self.sub_arguments.keys():
self.sub_arguments[subarg] = \
other_data.sub_arguments[subarg].copy()
else:
# Otherwise we merge the sub arguments in the same fashion we
# merge the arguments.
for arg in other_data.sub_arguments[subarg]:
if (arg in self.sub_arguments[subarg].keys()
and not replace_args):
continue
self.sub_arguments[subarg][arg] = \
other_data.sub_arguments[subarg][arg]

def gen_docstring(
self, indentation: str = " ",
function: Union[None, Callable[..., Any]] = None
Expand Down Expand Up @@ -285,6 +334,24 @@ def gen_docstring(
else:
argstring = indentation + argstring
argstrings.append(argstring)
for cls in self.sub_arguments:
for arg in self.sub_arguments[cls]:
argstring = self.sub_arguments[cls][arg].gen_docstring(
cls_name=cls
)
if os.linesep in argstring:
lines = argstring.split(os.linesep)
argstring = indentation + lines[0] + os.linesep
for line in lines[1:]:
if ":type" not in line:
argstring += indentation*2 + line + os.linesep
else:
argstring += indentation + line + os.linesep
# Remove the last newline character
argstring = argstring[:-1]
else:
argstring = indentation + argstring
argstrings.append(argstring)

raisestrings = []
for element in self.raises:
Expand Down Expand Up @@ -375,7 +442,7 @@ def create_from_object(cls, obj: Any):
rtype = None
docstring_data = DocstringData(
desc="", arguments=OrderedDict(), raises=[],
returns=None
returns=None, sub_arguments=OrderedDict()
)

docstring_data.add_data(desc_chunk)
Expand Down
115 changes: 104 additions & 11 deletions src/psyclone/tests/docstring_parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,21 @@ def test_docstringdata_base():
arguments = OrderedDict()
returns = ReturnsData(desc="desc", datatype="datatype")
raises = []
subargs = OrderedDict()

docdata = DocstringData(desc="desc", arguments=arguments, raises=raises,
returns=returns)
returns=returns, sub_arguments=subargs)
assert docdata.desc == "desc"
assert docdata.arguments is arguments
assert docdata.raises is raises
assert docdata.returns is returns
assert docdata.sub_arguments is subargs


def test_docstringdata_add_data():
'Test the add_data function of the DocstringData dataclass.'''
docdata = DocstringData(desc=None, arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())

docdata.add_data("desc")
assert docdata.desc == "desc"
Expand Down Expand Up @@ -124,10 +126,10 @@ def test_docstringdata_add_data():
def test_docstringdata_merge():
'''Test the merge function of the DocstringData dataclass.'''
docdata = DocstringData(desc=None, arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())

docdata2 = DocstringData(desc=None, arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())
adata = ArgumentData(name="name", datatype="datatype",
desc="desc", inline_type=True)
docdata2.add_data(adata)
Expand All @@ -142,7 +144,7 @@ def test_docstringdata_merge():

# Check we don't overwrite arguments without replace set.
docdata3 = DocstringData(desc=None, arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())
adata3 = ArgumentData(name="name", datatype="datatype",
desc="desc", inline_type=True)
docdata3.add_data(adata3)
Expand All @@ -153,44 +155,71 @@ def test_docstringdata_merge():

# Merge a description.
docdata4 = DocstringData(desc="desc", arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())
docdata.merge(docdata4)
assert docdata.desc == "desc"

# Don't overwrite without param
docdata5 = DocstringData(desc="desc2", arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())
docdata.merge(docdata5)
assert docdata.desc == "desc"
docdata.merge(docdata5, replace_desc=True)
assert docdata.desc == "desc2"

# Merge raises
docdata6 = DocstringData(desc="desc", arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())
rdata = RaisesData(desc="desc2", exception="Error")
docdata6.add_data(rdata)
docdata.merge(docdata6)
assert docdata.raises[0] is rdata

# Merge returns
docdata7 = DocstringData(desc="desc", arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())
rdata = ReturnsData(desc="desc", datatype="datatype")
docdata7.add_data(rdata)
docdata.merge(docdata7)
assert docdata.returns is rdata

# Don't overwrite without param
docdata8 = DocstringData(desc="desc", arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())
rdata2 = ReturnsData(desc="desc2", datatype="datatype")
docdata8.add_data(rdata2)
docdata.merge(docdata8)
assert docdata.returns is not rdata2
docdata.merge(docdata8, replace_returns=True)
assert docdata.returns is rdata2

# Test merging of sub arguments.
subargs1 = {"Subclass1": {"opt1": "Test opt1"}}
subargs2 = {"Subclass2": {"opt2": "Test opt2"}}
docdata = DocstringData(desc="desc", arguments=OrderedDict(), raises=[],
returns=None, sub_arguments=subargs1)
docdata2 = DocstringData(desc="desc", arguments=OrderedDict(), raises=[],
returns=None, sub_arguments=subargs2)

docdata.merge(docdata2)
assert docdata.sub_arguments["Subclass2"] == {"opt2": "Test opt2"}
# Modify subargs2[Subclass2] to show its not modifying our docdata.
subargs2["Subclass2"]["opt3"] = "Test opt3"
assert docdata.sub_arguments["Subclass2"] == {"opt2": "Test opt2"}

subargs3 = {"Subclass1": {"opt1": "Not test opt1",
"opt3": "Test opt 3"}}
docdata3 = DocstringData(desc="desc", arguments=OrderedDict(), raises=[],
returns=None, sub_arguments=subargs3)
# Check the merging without replace_args doesn't change things.
docdata.merge(docdata3)
assert docdata.sub_arguments["Subclass1"]["opt1"] == "Test opt1"
assert docdata.sub_arguments["Subclass1"]["opt3"] == "Test opt 3"

# Check that merging with replace_args does change things.
docdata.merge(docdata3, replace_args=True)
assert docdata.sub_arguments["Subclass1"]["opt1"] == "Not test opt1"


def dummy_function(typed_arg: int, untyped_arg):
'''Dummy function for testing functionality.'''
Expand Down Expand Up @@ -316,7 +345,7 @@ def test_DocstringData_gen_docstring_():

# Check we get nothing for an empty DocstringData
docdata = DocstringData(desc=None, arguments=OrderedDict(), raises=[],
returns=None)
returns=None, sub_arguments=OrderedDict())
output = docdata.gen_docstring()
assert output == ""

Expand Down Expand Up @@ -594,3 +623,67 @@ def test_function(param: DocstringData):
assert isinstance(data, ArgumentData)
assert (data.datatype ==
"<class 'psyclone.docstring_parser.DocstringData'>")


def test_subarguments():
'''
Test that we can add sub arguments to a docstring data as expected.
'''

def docstringobj(arg1: int):
'''
description.

:param arg1: my param
'''

def subobject(arg1: int, arg2: int):
'''
subobject description

:param arg1: sub param
:param arg2: arg2
'''

def subobject2(arg3, arg2: int):
'''
subobject2 description

:param arg2: subobj2 arg2
:param arg3: arg3
:type arg3: int
'''

doc1 = DocstringData.create_from_object(docstringobj)

# Should have no sub arguments
assert doc1.sub_arguments == {}

doc2 = DocstringData.create_from_object(subobject)
assert "arg1" in doc2.arguments
assert "arg2" in doc2.arguments
assert len(doc2.arguments) == 2

# Add doc2 as subarguments to doc1
doc1.add_subarguments("subobject", doc2, replace_args=False)

# Should only have arg2 in the sub arguments
assert len(doc1.sub_arguments["subobject"]) == 2
assert doc1.sub_arguments["subobject"]["arg1"].desc == "sub param"
assert doc1.sub_arguments["subobject"]["arg2"].desc == "arg2"

doc3 = DocstringData.create_from_object(subobject2)
# Add doc3 also as subobject to doc1 without replace_args
doc1.add_subarguments("subobject", doc3, replace_args=False)
assert len(doc1.sub_arguments["subobject"]) == 3
assert doc1.sub_arguments["subobject"]["arg1"].desc == "sub param"
assert doc1.sub_arguments["subobject"]["arg2"].desc == "arg2"
assert doc1.sub_arguments["subobject"]["arg3"].desc == "arg3"

# Add doc 3 as a subojbject with replace_args
# Add doc3 also as subobject to doc1 without replace_args
doc1.add_subarguments("subobject", doc3, replace_args=True)
assert len(doc1.sub_arguments["subobject"]) == 3
assert doc1.sub_arguments["subobject"]["arg1"].desc == "sub param"
assert doc1.sub_arguments["subobject"]["arg2"].desc == "subobj2 arg2"
assert doc1.sub_arguments["subobject"]["arg3"].desc == "arg3"
Loading
Loading