Skip to content

Commit

Permalink
Merge pull request #451 from Fortran-FOSS-Programmers/fix-bound-proce…
Browse files Browse the repository at this point in the history
…dure-docstrings

Fix type-bound procedure docs not showing full docstring
  • Loading branch information
ZedThree authored Oct 5, 2022
2 parents 2dd1d48 + 4650805 commit 2f603a9
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 17 deletions.
1 change: 0 additions & 1 deletion example/example-project-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ docmark_alt: #
predocmark_alt: <
display: public
protected
private
source: false
graph: true
search: true
Expand Down
37 changes: 37 additions & 0 deletions example/src/ford_example_type.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module ford_example_type_mod
implicit none

private

type, public :: example_type
!! Some type that can say hello.
!!
!! This type's docs demonstrate how documentation is rendered
!! differently for public/private components, and for type-bound
!! procedure bindings and the procedures themselves.
private
integer :: counter = 1
!! Internal counter
character(len=:), allocatable, public :: name
!! Name of this instance
contains
procedure :: say_hello => example_type_say
!! This will document the type-bound procedure *binding*, rather
!! than the procedure itself. The specific procedure docs will be
!! rendered on the `example_type` page.
end type example_type

contains

subroutine example_type_say(self)
!! Prints how many times this instance has said hello
!!
!! This subroutine has more documentation that won't appear in the
!! summary in the procedure list, but should appear in the type
!! page.
class(example_type), intent(in) :: self
write(*, '(a, " has said hello ", i0, " times.")') self%name, self%counter
self%counter = self%counter + 1
end subroutine example_type_say

end module ford_example_type_mod
21 changes: 17 additions & 4 deletions ford/sourceform.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,18 @@ def __init__(
self.VARIABLE_STRING.format(typestr), re.IGNORECASE
)

# This is a little bit confusing, because `permission` here is sort of
# overloaded for "permission for this entity", and "permission for child
# entities". For example, the former doesn't apply to modules or programs,
# while for procedures _only_ the first applies. For things like types, we
# need to keep track of these meanings separately. Also note that
# `child_permission` for types can be different for components and bound
# procedures, but luckily they cannot be mixed in the source, so we don't
# need to actually track `child_permission` separately for them both
child_permission = (
"public" if isinstance(self, FortranType) else self.permission
)

blocklevel = 0
associatelevel = 0
for line in source:
Expand Down Expand Up @@ -709,12 +721,13 @@ def __init__(
if not incontains and type(self) in _can_have_contains:
incontains = True
if isinstance(self, FortranType):
self.permission = "public"
child_permission = "public"
elif incontains:
self.print_error(line, "Multiple CONTAINS statements present")
else:
self.print_error(line, "Unexpected CONTAINS statement")
elif line_lower in ["public", "private", "protected"]:
child_permission = line_lower
if not isinstance(self, FortranType):
self.permission = line_lower
elif line_lower == "sequence":
Expand Down Expand Up @@ -920,7 +933,7 @@ def __init__(
source,
self.BOUNDPROC_RE.match(line),
self,
self.permission,
child_permission,
)
)
else:
Expand All @@ -931,7 +944,7 @@ def __init__(
source,
self.BOUNDPROC_RE.match(pseudo_line),
self,
self.permission,
child_permission,
)
)
else:
Expand Down Expand Up @@ -985,7 +998,7 @@ def __init__(
elif self.VARIABLE_RE.match(line) and blocklevel == 0:
if hasattr(self, "variables"):
self.variables.extend(
line_to_variables(source, line, self.permission, self)
line_to_variables(source, line, child_permission, self)
)
else:
self.print_error(line, "Unexpected variable")
Expand Down
71 changes: 64 additions & 7 deletions ford/templates/macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,21 @@ <h3>Contents</h3>
</table>
{% endmacro %}

{%- macro header(size, small=False) -%}
{%- if small -%}
<h{{ size + 2 }}>
{%- else -%}
<h{{ size }}>
{%- endif -%}
{% endmacro %}

{%- macro close_header(size, small=False) -%}
{%- if small -%}
</h{{ size + 2 }}>
{%- else -%}
</h{{ size }}>
{%- endif -%}
{%- endmacro -%}

{% macro proc_summary(proc,title=True,small=False,proto=False) %}
{% if title %}
Expand Down Expand Up @@ -411,6 +426,48 @@ <h3>Contents</h3>
{% endmacro %}


{% macro binding_summary(proc,title=True,small=False,proto=False) %}
{# Render the documentation for an individual binding of a type-bound procedure #}
{% if title %}
{{ header(3, small) }}
{{ proc_line(proc, proto=proto) }}
{%- if proto -%}
<small>Prototype</small>
{%- endif -%}
{{ close_header(3, small) }}
{% endif %}
{% if proc.parent and proc.visible %}
{{ meta_list(proc.meta) }}
{% endif %}
{{ proc.doc }}

{{ header(4, small) }}
Arguments
{{ close_header(4, small) }}
{% if proc.args|length %}
{{ var_list(proc.args, intent=True, summary=proc.visible, id=False) }}
{% else %}
<em>None</em>
{% endif %}
{% if proc.retvar %}
{% set var = proc.retvar %}
{% set args = 0 %}
{% if var.kind %}{% set args = args + 1 %}{% endif %}
{% if var.strlen %}{% set args = args + 1 %}{% endif %}
{% if var.proto %}{% set args = args + 1 %}{% endif %}
{{ header(4, small) }}
Return Value <small>{{ var.vartype }}{% if args -%}({% if var.kind -%}kind={{ var.kind }}{%- endif %}{% if args > 1 -%},{%- endif %}{% if var.strlen -%}len={{ var.strlen }}{%- endif %}{% if var.proto -%}{% if not var.proto[0].permission or var.proto[0].visible -%}{{ var.proto[0] }}{% else %}{{ var.proto[0].name }}{%- endif %}{{ var.proto[1] }}{%- endif %}){%- endif %}{% if var.attribs|length -%},{%- endif %}
{% for attrib in var.attribs -%}
{{ attrib }}{% if not loop.last or var.dimension -%}, {% endif %}
{%- endfor %}
{{ var.dimension }}
</small>
{{ close_header(4, small) }}
{{ var.doc }}
{% endif %}
{% endmacro %}


{% macro bound_info(tb) %}
<div class="panel panel-default">
<div class="panel-heading codesum"><span class="anchor" id="{{ tb.anchor }}"></span><h3>{% if tb.generic -%}generic,{% else %}procedure{% if tb.proto -%}({% if not tb.protomatch or tb.proto.visible -%}{{ tb.proto }}{% else %}{{ tb.proto.name }}{%- endif %}){%- endif %},{%- endif %} {{ tb.permission }}{% if tb.deferred -%}, deferred{%- endif %}{% if tb.attribs -%}, {% for attrib in tb.attribs -%}{{ attrib }}{% if not loop.last -%}, {% endif %}{%- endfor %}{%- endif %} :: <strong>{{ tb.name }}</strong> {% if tb.generic or (tb.name != tb.bindings[0].name and tb.name != tb.bindings[0]) %} => {% for bind in tb.bindings -%}{% if not bind.parent or bind.visible %}{{ bind }}{% else %}{{ bind.name }}{% endif %}{% if not loop.last -%}, {% endif %}{%- endfor %}{% endif %} {% if tb.binding|length == 1 %}<small>{{ tb.bindings[0].proctype }}</small>{% endif %}
Expand All @@ -429,19 +486,19 @@ <h3>Contents</h3>
<li class="list-group-item">
{% if tb.deferred and tb.protomatch %}
{% if tb.proto.obj == 'interface' %}
{{ proc_summary(tb.proto.procedure,proto=True) }}
{{ binding_summary(tb.proto.procedure,proto=True) }}
{% elif tb.proto.obj == 'procedure' %}
{{ proc_summary(tb.proto,proto=True) }}
{{ binding_summary(tb.proto,proto=True) }}
{% endif %}
{% elif bind.obj == 'boundprocedure' %}
{% if bind.deferred and bind.protomatch %}
{% if bind.proto.obj == 'interface' %}
{{ proc_summary(bind.proto.procedure,proto=True) }}
{{ binding_summary(bind.proto.procedure,proto=True) }}
{% elif bind.proto.obj == 'procedure' %}
{{ proc_summary(bind.proto,proto=True) }}
{{ binding_summary(bind.proto,proto=True) }}
{% endif %}
{% else %}
{{ proc_summary(bind.bindings[0]) }}
{{ binding_summary(bind.bindings[0]) }}
{% endif %}
{% else %}
{% if bind.obj == 'interface' %}
Expand All @@ -457,9 +514,9 @@ <h3>interface
{% endif %}
{{ bind.meta['summary'] }}
{% endif %}
{{ proc_summary(bind.procedure) }}
{{ binding_summary(bind.procedure) }}
{% else %}
{{ proc_summary(bind) }}
{{ binding_summary(bind) }}
{% endif %}
{% endif %}
</li>
Expand Down
22 changes: 17 additions & 5 deletions test/test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,17 @@ def test_source_file_links(example_index):
source_files_list = sorted([f.text for f in source_files_box("li")])

assert source_files_list == sorted(
["ford_test_module.fpp", "ford_test_program.f90"]
["ford_test_module.fpp", "ford_test_program.f90", "ford_example_type.f90"]
)


def test_module_links(example_index):
index, settings = example_index

modules_box = index.find(ANY_TEXT, string="Modules").parent
modules_list = [f.text for f in modules_box("li")]
modules_list = sorted([f.text for f in modules_box("li")])

assert modules_list == ["test_module"]
assert modules_list == sorted(["test_module", "ford_example_type_mod"])


def test_procedures_links(example_index):
Expand All @@ -147,9 +147,21 @@ def test_types_links(example_index):
index, settings = example_index

types_box = index.find(ANY_TEXT, string="Derived Types").parent
types_list = [f.text for f in types_box("li")]
types_list = sorted([f.text for f in types_box("li")])

assert types_list == ["bar", "foo"]
assert types_list == sorted(["bar", "foo", "example_type"])


def test_types_type_bound_procedure(example_project):
path, _ = example_project
index = read_html(path / "type/example_type.html")

bound_procedures_section = index.find("h2", string="Type-Bound Procedures").parent

assert "This will document" in bound_procedures_section.text, "Binding docstring"
assert (
"has more documentation" in bound_procedures_section.text
), "Full procedure docstring"


def test_graph_submodule(example_project):
Expand Down
80 changes: 80 additions & 0 deletions test/test_sourceform.py
Original file line number Diff line number Diff line change
Expand Up @@ -1171,3 +1171,83 @@ def test_routine_iterator(parse_fortran_file):
"sub3",
"sub4",
]


def test_type_component_permissions(parse_fortran_file):
data = """\
module default_access
private
type :: type_default
complex :: component_public
complex, private :: component_private
contains
procedure :: sub_public
procedure, private :: sub_private
end type type_default
type, public :: type_public
public
complex :: component_public
complex, private :: component_private
contains
public
procedure :: sub_public
procedure, private :: sub_private
end type type_public
type :: type_private
private
character(len=1), public :: string_public
character(len=1) :: string_private
contains
private
procedure, public :: sub_public
procedure :: sub_private
end type type_private
type :: type_public_private
public
character(len=1) :: string_public
character(len=1), private :: string_private
contains
private
procedure, public :: sub_public
procedure :: sub_private
end type type_public_private
type :: type_private_public
private
character(len=1), public :: string_public
character(len=1) :: string_private
contains
public
procedure :: sub_public
procedure, private :: sub_private
end type type_private_public
public :: type_default, type_private_public, type_public_private
contains
subroutine sub_public
end subroutine sub_public
subroutine sub_private
end subroutine sub_private
end module default_access
"""

fortran_file = parse_fortran_file(data)
fortran_file.modules[0].correlate(None)

for ftype in fortran_file.modules[0].types:
assert (
ftype.variables[0].permission == "public"
), f"{ftype.name}::{ftype.variables[0].name}"
assert (
ftype.variables[1].permission == "private"
), f"{ftype.name}::{ftype.variables[1].name}"
assert (
ftype.boundprocs[0].permission == "public"
), f"{ftype.name}::{ftype.boundprocs[0].name}"
assert (
ftype.boundprocs[1].permission == "private"
), f"{ftype.name}::{ftype.boundprocs[1].name}"

0 comments on commit 2f603a9

Please sign in to comment.