Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 76 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,22 @@ 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 subclass (or
otherwise) this argument is from. Used for documenting
sub transformation options.

:returns: The docstring represented by this ArgumentData.
'''
if cls_name:
cls_name = f"(Option used for {cls_name}) "
rstr = ":param "
if function:
# If the argument is in function's parameter list and has a type
Expand All @@ -103,13 +111,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 +179,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 +209,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 +245,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 +272,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 +335,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 +443,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