Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deal with external type bound procedures without local overwrite #677

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
13 changes: 6 additions & 7 deletions ford/external_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
"boundprocs",
"vartype",
"permission",
"deferred",
"generic",
"attribs",
]

# Mapping between entity name and its type
Expand All @@ -59,18 +61,15 @@ def obj2dict(intObj):
"""
if hasattr(intObj, "external_url"):
return None
if isinstance(intObj, str):
return intObj
Comment on lines +64 to +65
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the attribs list for type-bound procedures we need to take care of strings in the recursive serialization.

extDict = {
"name": intObj.name,
"external_url": f"./{intObj.get_url()}",
"obj": intObj.obj,
}
if hasattr(intObj, "proctype"):
extDict["proctype"] = intObj.proctype
if hasattr(intObj, "extends"):
if isinstance(intObj.extends, FortranType):
extDict["extends"] = obj2dict(intObj.extends)
else:
extDict["extends"] = intObj.extends
Comment on lines -69 to -73
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to follow the extends further up, after we first hit an external project, so there isn't really a need for this information in the serialized file.

for attrib in ATTRIBUTES:
if not hasattr(intObj, attrib):
continue
Expand Down Expand Up @@ -99,6 +98,8 @@ def dict2obj(project, extDict, url, parent=None, remote: bool = False) -> Fortra
"""
Converts a dictionary to an object and immediately adds it to the project
"""
if isinstance(extDict, str):
return extDict
name = extDict["name"]
if extDict["external_url"]:
extDict["external_url"] = extDict["external_url"].split("/", 1)[-1]
Expand All @@ -119,8 +120,6 @@ def dict2obj(project, extDict, url, parent=None, remote: bool = False) -> Fortra

if obj_type == "interface":
extObj.proctype = extDict["proctype"]
elif obj_type == "type":
extObj.extends = extDict["extends"]

for key in ATTRIBUTES:
if key not in extDict:
Expand Down
8 changes: 5 additions & 3 deletions ford/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,9 +791,11 @@ def __init__(

for r in root:
self.root.append(self.data.get_node(r))
self.max_nesting = max(self.max_nesting, int(r.meta.graph_maxdepth))
self.max_nodes = max(self.max_nodes, int(r.meta.graph_maxnodes))
self.warn = self.warn or (r.settings.warn)
if hasattr(r, "meta"):
self.max_nesting = max(self.max_nesting, int(r.meta.graph_maxdepth))
self.max_nodes = max(self.max_nodes, int(r.meta.graph_maxnodes))
if hasattr(r, "settings"):
self.warn = self.warn or (r.settings.warn)
Comment on lines +794 to +798
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nodes we want to add here, may be from external projects and won't have the meta and settings attribute, hence we need to treat this gracefully.


ident = ident or f"{root[0].get_dir()}~~{root[0].ident}"
self.ident = f"{ident}~~{self.__class__.__name__}"
Expand Down
17 changes: 9 additions & 8 deletions ford/sourceform.py
Original file line number Diff line number Diff line change
Expand Up @@ -2059,14 +2059,15 @@ def correlate(self, project):
self.boundprocs = inherited + self.boundprocs
# Match up generic type-bound procedures to their particular bindings
for proc in self.boundprocs:
for bp in inherited_generic:
if bp.name.lower() == proc.name.lower() and isinstance(
bp, FortranBoundProcedure
):
proc.bindings = bp.bindings + proc.bindings
break
if proc.generic:
proc.correlate(project)
if type(proc) is FortranBoundProcedure:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only continue correlation and bindings lookup for non-external BoundProcedures.

for bp in inherited_generic:
if bp.name.lower() == proc.name.lower() and isinstance(
bp, FortranBoundProcedure
):
proc.bindings = bp.bindings + proc.bindings
break
if proc.generic:
proc.correlate(project)
# Match finalprocs
for fp in self.finalprocs:
fp.correlate(project)
Expand Down
94 changes: 52 additions & 42 deletions ford/templates/macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,10 @@ <h3>Contents</h3>

{% macro deprecated(entity) %}
{# Add 'Deprecated' warning #}
{%- if entity | meta('deprecated') -%}
<span class="badge bg-danger depwarn">Deprecated</span>
{%- if not entity.external_url -%}
{%- if entity | meta('deprecated') -%}
<span class="badge bg-danger depwarn">Deprecated</span>
{%- endif -%}
Comment on lines +170 to +173
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For external projects we do not know the deprecation status and need to skip this here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I thought the meta function took care of attributes not being present?

{%- endif -%}
{% endmacro %}

Expand Down Expand Up @@ -327,8 +329,12 @@ <h3>Contents</h3>
{# Type-bound procedure declaration and bindings #}
{{ tb.full_declaration | relurl(page_url) }} ::
<strong>{% if link_name %}{{ tb | relurl(page_url) }}{% else %}{{ tb.name }}{% endif %}</strong>
{%- if tb.generic or (tb.name != tb.bindings[0].name and tb.name != tb.bindings[0]) %} => {{ tb.bindings | join(", ") }}{% endif %}
{% if tb.binding|length == 1 %}<small>{{ tb.bindings[0].proctype }}</small>{% endif %}
{%- if tb.external_url %}
(external{% if not link_name %}: {{ tb | relurl(page_url) }}{% endif %})
{%- else %}
{%- if tb.generic or (tb.name != tb.bindings[0].name and tb.name != tb.bindings[0]) %} => {{ tb.bindings | join(", ") }}{% endif %}
{% if tb.binding|length == 1 %}<small>{{ tb.bindings[0].proctype }}</small>{% endif %}
{% endif %}
Comment on lines +332 to +337
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is an external procedure, we do not have more info on it, point it out, and link to it if not done already.

{% endmacro %}


Expand All @@ -341,48 +347,50 @@ <h3>
{{ deprecated(tb) }}
</h3>
</div>
{% if tb.doc or meta_list(tb.meta)|trim|length is more_than_one %}
<div class="card-body">
{{ meta_list(tb.meta) }}
{{ docstring(tb) }}
</div>
{% endif %}
<ul class="list-group">
{% for bind in tb.bindings %}
<li class="list-group-item">
{% if tb.deferred and tb.protomatch %}
{% if tb.proto.obj == 'interface' %}
{{ binding_summary(tb.proto.procedure,proto=True) }}
{% elif tb.proto.obj == 'procedure' %}
{{ binding_summary(tb.proto,proto=True) }}
{% endif %}
{% elif bind.obj == 'boundprocedure' %}
{% if bind.deferred and bind.protomatch %}
{% if bind.proto.obj == 'interface' %}
{{ binding_summary(bind.proto.procedure,proto=True) }}
{% elif bind.proto.obj == 'procedure' %}
{{ binding_summary(bind.proto,proto=True) }}
{% if not tb.external_url %}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip these details for external procedures.

{% if tb.doc or meta_list(tb.meta)|trim|length is more_than_one %}
<div class="card-body">
{{ meta_list(tb.meta) }}
{{ docstring(tb) }}
</div>
{% endif %}
<ul class="list-group">
{% for bind in tb.bindings %}
<li class="list-group-item">
{% if tb.deferred and tb.protomatch %}
{% if tb.proto.obj == 'interface' %}
{{ binding_summary(tb.proto.procedure,proto=True) }}
{% elif tb.proto.obj == 'procedure' %}
{{ binding_summary(tb.proto,proto=True) }}
{% endif %}
{% else %}
{{ binding_summary(bind.bindings[0]) }}
{% endif %}
{% else %}
{% if bind.obj == 'interface' %}
<h3>interface {{ deprecated(bind) }}</h3>
{% if bind.doc or (meta_list(bind.meta)|trim and not bind.visible) %}
{% if not bind.visible %}
{{ meta_list(bind.meta) }}
{% elif bind.obj == 'boundprocedure' %}
{% if bind.deferred and bind.protomatch %}
{% if bind.proto.obj == 'interface' %}
{{ binding_summary(bind.proto.procedure,proto=True) }}
{% elif bind.proto.obj == 'procedure' %}
{{ binding_summary(bind.proto,proto=True) }}
{% endif %}
{{ bind | meta('summary') }}
{% else %}
{{ binding_summary(bind.bindings[0]) }}
{% endif %}
{{ binding_summary(bind.procedure) }}
{% else %}
{{ binding_summary(bind) }}
{% if bind.obj == 'interface' %}
<h3>interface {{ deprecated(bind) }}</h3>
{% if bind.doc or (meta_list(bind.meta)|trim and not bind.visible) %}
{% if not bind.visible %}
{{ meta_list(bind.meta) }}
{% endif %}
{{ bind | meta('summary') }}
{% endif %}
{{ binding_summary(bind.procedure) }}
{% else %}
{{ binding_summary(bind) }}
{% endif %}
{% endif %}
{% endif %}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}

Expand Down Expand Up @@ -569,7 +577,9 @@ <h4>Type-Bound Procedures</h4>
{% for tb in dtype.boundprocs %}
<tr>
<td>{{ bound_declaration(tb, link_name=True) }}</td>
<td>{{ tb | meta('summary') | relurl(page_url) }}</td>
{% if not tb.external_url %}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip the summary for external procedures.

<td>{{ tb | meta('summary') | relurl(page_url) }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
Expand Down
81 changes: 81 additions & 0 deletions test/test_projects/test_676.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import shutil
import sys
import os
import pathlib
from urllib.parse import urlparse
import json
from typing import Dict, Any

import ford

from bs4 import BeautifulSoup
import pytest


@pytest.fixture(scope="module")
def monkeymodule(request):
"""pytest won't let us use function-scope fixtures in module-scope
fixtures, so we need to reimplement this with module scope"""
mpatch = pytest.MonkeyPatch()
yield mpatch
mpatch.undo()


@pytest.fixture(scope="module")
def external_project(tmp_path_factory, monkeymodule):
"""Generate the documentation for an "external" project and then
for a "top level" one that uses the first.

A remote external project is simulated through a mocked `urlopen`
which returns `REMOTE_MODULES_JSON`

"""

this_dir = pathlib.Path(__file__).parent
path = tmp_path_factory.getbasetemp() / "issue_676"
shutil.copytree(this_dir / "../../test_data/issue_676", path)

external_project = path / "base"
top_level_project = path / "plugin"

# Run FORD in the two projects
# First project has "externalize: True" and will generate JSON dump
with monkeymodule.context() as m:
os.chdir(external_project)
m.setattr(sys, "argv", ["ford", "doc.md"])
ford.run()

# Second project uses JSON from first to link to external modules
with monkeymodule.context() as m:
os.chdir(top_level_project)
m.setattr(sys, "argv", ["ford", "doc.md"])
ford.run()

# Make sure we're in a directory where relative paths won't
# resolve correctly
os.chdir("/")

return top_level_project, external_project


def test_issue676_project(external_project):
"""Check that we can build external projects and get the links correct"""

top_level_project, _ = external_project

# Read generated HTML
module_dir = top_level_project / "doc/module"
with open(module_dir / "gc_method_fks_h.html", "r") as f:
top_module_html = BeautifulSoup(f.read(), features="html.parser")

# Find links to external modules
uses_box = top_module_html.find(string="Uses").parent.parent.parent
links = {
tag.text: tag.a["href"] for tag in uses_box("li", class_="list-inline-item")
}

assert len(links) == 1
assert "gc_method_h" in links
local_url = urlparse(links["gc_method_h"])
local_path = module_dir / local_url.path
assert local_path.is_file()
3 changes: 3 additions & 0 deletions test_data/issue_676/base/doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
project: base-project
search: false
externalize: true
36 changes: 36 additions & 0 deletions test_data/issue_676/base/src/gc_method_h.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module gc_method_h

use gc_pointers_h, only: pointers
implicit none

private
public :: t_method, method_constructor

type, extends(pointers) :: t_method
contains
procedure :: init, run
end type t_method

abstract interface
function method_constructor(ptr, nwords, words) result(this)
import t_method
class(t_method), pointer :: this
class(*), intent(in) :: ptr
integer, intent(in) :: nwords
character(*), intent(in) :: words(:)
end function method_constructor
end interface

CONTAINS

function init(self) result(ierr)
class(t_method), intent(inout) :: self
integer :: ierr
end function init

function run(self) result(ierr)
class(t_method), intent(inout) :: self
integer :: ierr
end function run

end module gc_method_h
29 changes: 29 additions & 0 deletions test_data/issue_676/base/src/gc_pointers_h.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module gc_pointers_h
implicit none

private
public :: pointers

type :: pointers
contains
procedure :: check
end type pointers

interface pointers
module procedure pointers_constructor
end interface pointers

interface
module function pointers_constructor( gencat )result( this )
class(*), pointer, intent( in ) :: gencat
type( pointers ) :: this
end function pointers_constructor

module subroutine check( self )
class( pointers ), intent( in ) :: self
end subroutine check
end interface

CONTAINS

end module gc_pointers_h
4 changes: 4 additions & 0 deletions test_data/issue_676/plugin/doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
project: plugin-fks
search: false
graph: true
external: local = ../base/doc
Loading
Loading