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

Merged
merged 12 commits into from
Jan 23, 2025
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
ZedThree marked this conversation as resolved.
Show resolved Hide resolved
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
ZedThree marked this conversation as resolved.
Show resolved Hide resolved
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)
ZedThree marked this conversation as resolved.
Show resolved Hide resolved

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:
ZedThree marked this conversation as resolved.
Show resolved Hide resolved
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 -%}
ZedThree marked this conversation as resolved.
Show resolved Hide resolved
{%- 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 %}
ZedThree marked this conversation as resolved.
Show resolved Hide resolved
{% 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 %}
ZedThree marked this conversation as resolved.
Show resolved Hide resolved
{% 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 %}
ZedThree marked this conversation as resolved.
Show resolved Hide resolved
<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