From a3e6ec66be9f8002205ad448207564c0772584a3 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Tue, 4 Oct 2022 15:26:22 +0100 Subject: [PATCH 1/2] Fix type-bound procedure docs not showing full docstring --- example/example-project-file.md | 1 - example/src/ford_example_type.f90 | 37 ++++++++++++++++ ford/templates/macros.html | 71 ++++++++++++++++++++++++++++--- test/test_example.py | 22 +++++++--- 4 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 example/src/ford_example_type.f90 diff --git a/example/example-project-file.md b/example/example-project-file.md index fac31ad0..fa9d68b3 100644 --- a/example/example-project-file.md +++ b/example/example-project-file.md @@ -17,7 +17,6 @@ docmark_alt: # predocmark_alt: < display: public protected - private source: false graph: true search: true diff --git a/example/src/ford_example_type.f90 b/example/src/ford_example_type.f90 new file mode 100644 index 00000000..8104b4f6 --- /dev/null +++ b/example/src/ford_example_type.f90 @@ -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 diff --git a/ford/templates/macros.html b/ford/templates/macros.html index e2c22996..52a4e68c 100644 --- a/ford/templates/macros.html +++ b/ford/templates/macros.html @@ -376,6 +376,21 @@

Contents

{% endmacro %} +{%- macro header(size, small=False) -%} + {%- if small -%} + + {%- else -%} + + {%- endif -%} +{% endmacro %} + +{%- macro close_header(size, small=False) -%} + {%- if small -%} + + {%- else -%} + + {%- endif -%} +{%- endmacro -%} {% macro proc_summary(proc,title=True,small=False,proto=False) %} {% if title %} @@ -411,6 +426,48 @@

Contents

{% 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 -%} + Prototype + {%- 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 %} + None + {% 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 {{ 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 }} + + {{ close_header(4, small) }} + {{ var.doc }} + {% endif %} +{% endmacro %} + + {% macro bound_info(tb) %}

{% 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 %} :: {{ tb.name }} {% 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 %}{{ tb.bindings[0].proctype }}{% endif %} @@ -429,19 +486,19 @@

Contents

  • {% 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' %} @@ -457,9 +514,9 @@

    interface {% endif %} {{ bind.meta['summary'] }} {% endif %} - {{ proc_summary(bind.procedure) }} + {{ binding_summary(bind.procedure) }} {% else %} - {{ proc_summary(bind) }} + {{ binding_summary(bind) }} {% endif %} {% endif %}

  • diff --git a/test/test_example.py b/test/test_example.py index cd6c26e1..680f4d50 100644 --- a/test/test_example.py +++ b/test/test_example.py @@ -115,7 +115,7 @@ 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"] ) @@ -123,9 +123,9 @@ 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): @@ -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): From 465080540edc1ae4f4b89a5d3d4e42de39d867c3 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Wed, 5 Oct 2022 11:36:02 +0100 Subject: [PATCH 2/2] Fix for type components sometimes getting the wrong permission Type components should default to `public`, regardless of permission of parent type --- ford/sourceform.py | 21 ++++++++--- test/test_sourceform.py | 80 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/ford/sourceform.py b/ford/sourceform.py index c0d41a6b..29dbbb0b 100644 --- a/ford/sourceform.py +++ b/ford/sourceform.py @@ -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: @@ -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": @@ -920,7 +933,7 @@ def __init__( source, self.BOUNDPROC_RE.match(line), self, - self.permission, + child_permission, ) ) else: @@ -931,7 +944,7 @@ def __init__( source, self.BOUNDPROC_RE.match(pseudo_line), self, - self.permission, + child_permission, ) ) else: @@ -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") diff --git a/test/test_sourceform.py b/test/test_sourceform.py index d6903328..24fb025e 100644 --- a/test/test_sourceform.py +++ b/test/test_sourceform.py @@ -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}"