-
Notifications
You must be signed in to change notification settings - Fork 136
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
base: master
Are you sure you want to change the base?
Changes from 10 commits
9f12faf
9943044
84348af
05873c5
e1f3ea6
e89d6a7
0719d5b
2845610
901c879
db22b6c
b74475e
6fe3fb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,7 +36,9 @@ | |
"boundprocs", | ||
"vartype", | ||
"permission", | ||
"deferred", | ||
"generic", | ||
"attribs", | ||
] | ||
|
||
# Mapping between entity name and its type | ||
|
@@ -59,18 +61,15 @@ def obj2dict(intObj): | |
""" | ||
if hasattr(intObj, "external_url"): | ||
return None | ||
if isinstance(intObj, str): | ||
return intObj | ||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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] | ||
|
@@ -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: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
ident = ident or f"{root[0].get_dir()}~~{root[0].ident}" | ||
self.ident = f"{ident}~~{self.__class__.__name__}" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I thought the |
||
{%- endif -%} | ||
{% endmacro %} | ||
|
||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 %} | ||
|
||
|
||
|
@@ -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 %} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 %} | ||
|
||
|
@@ -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 %} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
project: base-project | ||
search: false | ||
externalize: true |
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 |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
project: plugin-fks | ||
search: false | ||
graph: true | ||
external: local = ../base/doc | ||
remote = https://example.com |
There was a problem hiding this comment.
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.