Skip to content

Commit

Permalink
Merge pull request #677 from haraldkl/external-type-bound
Browse files Browse the repository at this point in the history
Deal with external type bound procedures without local overwrite
  • Loading branch information
ZedThree authored Jan 23, 2025
2 parents b174e39 + 6fe3fb2 commit f420a03
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 65 deletions.
16 changes: 8 additions & 8 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
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
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 Expand Up @@ -173,7 +172,8 @@ def load_external_modules(project):
urlopen(urljoin(url, "modules.json")).read().decode("utf8")
)
else:
url = pathlib.Path(url).resolve()
if not pathlib.Path(url).is_absolute():
url = project.settings.directory.joinpath(url).resolve()
extModules = modules_from_local(url)
except (URLError, json.JSONDecodeError) as error:
extModules = []
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)

ident = ident or f"{root[0].get_dir()}~~{root[0].ident}"
self.ident = f"{ident}~~{self.__class__.__name__}"
Expand Down
9 changes: 5 additions & 4 deletions ford/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class ProjectSettings:
creation_date: str = "%Y-%m-%dT%H:%M:%S.%f%z"
css: Optional[Path] = None
dbg: bool = True
directory: Path = Path.cwd()
display: List[str] = field(default_factory=lambda: ["public", "protected"])
doc_license: str = ""
docmark: str = "!"
Expand Down Expand Up @@ -256,14 +257,14 @@ def from_markdown_metadata(cls, meta: Dict[str, Any], parent: Optional[str] = No
def normalise_paths(self, directory=None):
if directory is None:
directory = Path.cwd()
directory = Path(directory).absolute()
self.directory = Path(directory).absolute()
field_types = get_type_hints(self)

if self.favicon == FAVICON_PATH:
self.favicon = Path(__file__).parent / FAVICON_PATH

if self.md_base_dir == Path("."):
self.md_base_dir = directory
self.md_base_dir = self.directory

for key, value in asdict(self).items():
if value is None:
Expand All @@ -273,10 +274,10 @@ def normalise_paths(self, directory=None):

if is_same_type(default_type, List[Path]):
value = getattr(self, key)
setattr(self, key, [normalise_path(directory, v) for v in value])
setattr(self, key, [normalise_path(self.directory, v) for v in value])

if is_same_type(default_type, Path):
setattr(self, key, normalise_path(directory, value))
setattr(self, key, normalise_path(self.directory, value))

if self.relative:
self.project_url = self.output_dir
Expand Down
17 changes: 9 additions & 8 deletions ford/sourceform.py
Original file line number Diff line number Diff line change
Expand Up @@ -2065,14 +2065,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:
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 @@ -169,8 +169,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 -%}
{%- endif -%}
{% endmacro %}

Expand Down Expand Up @@ -329,8 +331,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 %}
{% endmacro %}


Expand All @@ -343,48 +349,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 %}
{% 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 @@ -571,7 +579,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 %}
<td>{{ tb | meta('summary') | relurl(page_url) }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
Expand Down
85 changes: 85 additions & 0 deletions test/test_projects/test_676.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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"

# Generate the individual projects from their common parent
# directory, to check that local path definitions are
# relative to the project directory, irrespective of the
# working directory.
os.chdir(path)

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

# Second project uses JSON from first to link to external modules
with monkeymodule.context() as m:
m.setattr(sys, "argv", ["ford", "plugin/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
Loading

0 comments on commit f420a03

Please sign in to comment.