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

Warn on unused arguments #238

Merged
merged 26 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d080cb0
add more detail to error message
gtfierro Apr 28, 2023
f9d4eb1
Raise warning on partial template eval
gtfierro May 8, 2023
f522766
Add tests for raising warnings when missing params on Template.evaluate
gtfierro May 8, 2023
226d2a8
add a pytest marker for integration tests
gtfierro May 8, 2023
7b22ff1
fix behavior around inlining dependencies with optional args; add tests
gtfierro May 8, 2023
b04fbd4
add fixtures for tests
gtfierro May 8, 2023
815318e
Update buildingmotif/dataclasses/template.py
gtfierro May 8, 2023
bda8ec5
fix another edge case in inlining dependencies with optional args; ad…
gtfierro May 8, 2023
c506b46
Merge remote-tracking branch 'origin/develop' into gtf-warn-unused-is…
gtfierro May 9, 2023
74f57bc
Fix 223P dependencies in the templates
gtfierro May 10, 2023
013df99
Split template dep mgmt into two phases
gtfierro May 10, 2023
aed7c20
add flag to force adding optional_args when calling fill()
gtfierro May 10, 2023
21e4186
Merge branch 'gtf-warn-unused-issue-203' of github.com:NREL/BuildingM…
gtfierro May 10, 2023
c176778
don't treat SHACL warnings as violations
gtfierro May 10, 2023
d3c91d6
adjust notebook to require optional args for 223P; add test to be sure
gtfierro May 10, 2023
258f26b
Update buildingmotif/database/table_connection.py
gtfierro May 11, 2023
59e4e5a
Update buildingmotif/database/table_connection.py
gtfierro May 11, 2023
a431f32
Update buildingmotif/utils.py
gtfierro May 11, 2023
bc695c0
Update buildingmotif/database/table_connection.py
gtfierro May 11, 2023
5d78621
Update buildingmotif/database/table_connection.py
gtfierro May 11, 2023
83ff24e
Update buildingmotif/utils.py
gtfierro May 11, 2023
eb2bdc5
Update buildingmotif/utils.py
gtfierro May 11, 2023
d0c23b4
update method names + docstrings
gtfierro May 11, 2023
1799072
Update buildingmotif/dataclasses/template.py
MatthewSteen May 12, 2023
4cb6ee3
Update buildingmotif/utils.py
MatthewSteen May 12, 2023
f470b88
add check-dependencies to tests
gtfierro May 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion buildingmotif/database/table_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def add_template_dependency(
f"In {templ.name} the values of the bindings to {dep.name} must correspond to the "
"parameters in the dependant template."
f"Dependency {dep.name} refers to params {set(args.values()).difference(params)} "
f"that do not appear in template {templ.name}"
f"that do not appear in template {templ.name} ({params})"
gtfierro marked this conversation as resolved.
Show resolved Hide resolved
)
# do the same for the dependency
graph = self.bm.graph_connection.get_graph(dep.body_id)
Expand Down
15 changes: 7 additions & 8 deletions buildingmotif/dataclasses/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
from buildingmotif.database.tables import DBLibrary, DBTemplate
from buildingmotif.dataclasses.shape_collection import ShapeCollection
from buildingmotif.dataclasses.template import Template
from buildingmotif.namespaces import XSD
from buildingmotif.schemas import validate_libraries_yaml
from buildingmotif.template_compilation import compile_template_spec
from buildingmotif.utils import get_ontology_files, get_template_parts_from_shape
from buildingmotif.utils import (
get_ontology_files,
get_template_parts_from_shape,
skip_uri,
)

if TYPE_CHECKING:
from buildingmotif import BuildingMOTIF
Expand Down Expand Up @@ -386,12 +389,8 @@ def _resolve_template_dependencies(
if dep["template"] in template_id_lookup:
dependee = Template.load(template_id_lookup[dep["template"]])
template.add_dependency(dependee, dep["args"])
# Now that we have all the templates, we can populate the dependencies.
# IGNORES missing XSD imports --- there is really no reason to import the XSD
# ontology because the handling is baked into the software processing the RDF
# graph. Thus, XSD URIs will always yield import warnings. This is noisy, so we
# suppress them.
elif not dep["template"].startswith(XSD):
# check documentation for skip_uri for what URIs get skipped
elif not skip_uri(dep["template"]):
logging.warn(
f"Warning: could not find dependee {dep['template']}"
)
Expand Down
53 changes: 42 additions & 11 deletions buildingmotif/dataclasses/template.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from collections import Counter
from dataclasses import dataclass
from itertools import chain
Expand Down Expand Up @@ -122,7 +123,7 @@ def remove_dependency(self, dependency: "Template") -> None:
@property
def all_parameters(self) -> Set[str]:
"""The set of all parameters used in this template *including* its
dependencies.
dependencies. Includes optional parameters.

:return: set of parameters *with* dependencies
:rtype: Set[str]
Expand All @@ -138,7 +139,7 @@ def all_parameters(self) -> Set[str]:
@property
def parameters(self) -> Set[str]:
"""The set of all parameters used in this template *excluding* its
dependencies.
dependencies. Includes optional parameters.

:return: set of parameters *without* dependencies
:rtype: Set[str]
Expand All @@ -150,7 +151,8 @@ def parameters(self) -> Set[str]:

@property
def dependency_parameters(self) -> Set[str]:
"""The set of all parameters used in this demplate's dependencies.
"""The set of all parameters used in this template's dependencies, including
optional parameters.

:return: set of parameters used in dependencies
:rtype: Set[str]
Expand Down Expand Up @@ -230,7 +232,8 @@ def inline_dependencies(self) -> "Template":
if not self.get_dependencies():
return templ

# start with this template's parameters; if there is
# start with this template's parameters; this recurses into each
# dependency to inline dependencies
for dep in self.get_dependencies():
# get the inlined version of the dependency
deptempl = dep.template.inline_dependencies()
Expand All @@ -256,15 +259,22 @@ def inline_dependencies(self) -> "Template":
replace_nodes(
deptempl.body, {PARAM[k]: PARAM[v] for k, v in rename_params.items()}
)
# be sure to rename optional arguments too
unused_optional_args = set(deptempl.optional_args) - set(dep.args.keys())
dep_optional_args = [
rename_params.get(arg, arg) for arg in unused_optional_args
]

# append into the dependant body
# figure out which of deptempl's parameters are encoded as 'optional' by the
# parent (depending) template
deptempl_opt_args = deptempl.parameters.intersection(templ.optional_args)
# if the 'name' of the deptempl is optional, then all the arguments inside deptempl
# become optional
if rename_params["name"] in deptempl_opt_args:
# mark all of deptempl's parameters as optional
templ.optional_args += deptempl.parameters
else:
# otherwise, only add the parameters that are explicitly
# marked as optional *and* appear in this dependency
templ.optional_args += deptempl_opt_args

# append the inlined template into the parent's body
templ.body += deptempl.body
templ.optional_args += dep_optional_args

return templ

Expand All @@ -273,6 +283,7 @@ def evaluate(
bindings: Dict[str, Node],
namespaces: Optional[Dict[str, rdflib.Namespace]] = None,
require_optional_args: bool = False,
warn_unused: bool = True,
) -> Union["Template", rdflib.Graph]:
"""Evaluate the template with the provided bindings.

Expand All @@ -292,13 +303,29 @@ def evaluate(
:param require_optional_args: whether to require all optional arguments
to be bound, defaults to False
:type require_optional_args: bool
:param warn_unused: if True, print a warning if there were any parameters left
unbound during evaluation. If 'require_optional_args' is True,
then this will consider optional parameters when producing the warning;
otherwise, optional paramters will be ignored; defaults to True
:type warn_unused: bool
:return: either a template or a graph, depending on whether all
parameters were provided
:rtype: Union[Template, rdflib.Graph]
"""
templ = self.in_memory_copy()
# put all of the parameter names into the PARAM namespace so they can be
# directly subsituted in the template body
uri_bindings: Dict[Node, Node] = {PARAM[k]: v for k, v in bindings.items()}
# replace the param:<name> URIs in the template body with the bindings
# provided in the call to evaluate()
replace_nodes(templ.body, uri_bindings)
leftover_params = (
templ.parameters.difference(bindings.keys())
if not require_optional_args
else (templ.parameters.union(self.optional_args)).difference(
bindings.keys()
)
)
# true if all parameters are now bound or only optional args are unbound
if len(templ.parameters) == 0 or (
not require_optional_args and templ.parameters == set(self.optional_args)
Expand All @@ -313,6 +340,10 @@ def evaluate(
for arg in unbound_optional_args:
remove_triples_with_node(templ.body, PARAM[arg])
return templ.body
if len(leftover_params) > 0 and warn_unused:
warnings.warn(
gtfierro marked this conversation as resolved.
Show resolved Hide resolved
f"Parameters \"{', '.join(leftover_params)}\" were not provided during evaluation"
)
return templ

def fill(self, ns: rdflib.Namespace) -> Tuple[Dict[str, Node], rdflib.Graph]:
Expand Down
32 changes: 31 additions & 1 deletion buildingmotif/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from rdflib.paths import ZeroOrOne
from rdflib.term import Node

from buildingmotif.namespaces import OWL, PARAM, RDF, SH, bind_prefixes
from buildingmotif.namespaces import OWL, PARAM, RDF, SH, XSD, bind_prefixes

if TYPE_CHECKING:
from buildingmotif.dataclasses import Template
Expand All @@ -29,6 +29,14 @@ def _gensym(prefix: str = "p") -> URIRef:
return PARAM[f"{prefix}{_gensym_counter}"]


def _param_name(param: URIRef) -> str:
"""
Returns the name of the param w/o the prefix
gtfierro marked this conversation as resolved.
Show resolved Hide resolved
"""
assert param.startswith(PARAM)
return param[len(PARAM) :]


def copy_graph(g: Graph, preserve_blank_nodes: bool = True) -> Graph:
"""
Copy a graph. Creates new blank nodes so that these remain unique to each Graph
Expand Down Expand Up @@ -483,3 +491,25 @@ def rewrite_shape_graph(g: Graph) -> Graph:
# make sure to handle sh:node *after* sh:and
_inline_sh_node(sg)
return sg


def skip_uri(uri: URIRef) -> bool:
"""
Returns true if the URI should be skipped during processing
MatthewSteen marked this conversation as resolved.
Show resolved Hide resolved
because it is axiomatic or not expected to be imported.

Skips URIs in the XSD, SHACL namespaces
gtfierro marked this conversation as resolved.
Show resolved Hide resolved

:return: True if the URI should be skipped; false otherwise
gtfierro marked this conversation as resolved.
Show resolved Hide resolved
:rtype: bool
"""
# Now that we have all the templates, we can populate the dependencies.
# IGNORES missing XSD imports --- there is really no reason to import the XSD
# ontology because the handling is baked into the software processing the RDF
# graph. Thus, XSD URIs will always yield import warnings. This is noisy, so we
# suppress them.
skip_ns = [XSD, SH]
for ns in skip_ns:
if uri.startswith(ns):
return True
return False
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,6 @@ plugins = "sqlalchemy.ext.mypy.plugin"

[tool.pytest.ini_options]
log_cli_level = "WARNING"
markers = [
"integration: mark a test as an integration test"
]
46 changes: 46 additions & 0 deletions tests/unit/fixtures/inline-dep-test/templates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
A:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:Entity ;
brick:hasPoint P:b, P:c, P:d .
optional: ['d']
dependencies:
- template: B
args: {"name": "b"}
- template: C
args: {"name": "c"}
- template: D
args: {"name": "d"}

B:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:B ;
brick:hasPart P:bp .

C:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:C ;
brick:hasPart P:cp .

D:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:D ;
brick:hasPart P:dp .

A-alt:
body: >
@prefix P: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
P:name a brick:Entity ;
brick:hasPoint P:b .
optional: ['b-bp']
dependencies:
- template: B
args: {"name": "b"}
4 changes: 3 additions & 1 deletion tests/unit/fixtures/templates/1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,7 @@ outside-air-damper:
@prefix bmparam: <urn:___param___#> .
@prefix brick: <https://brickschema.org/schema/Brick#> .
bmparam:name a brick:Outside_Air_Damper ;
brick:hasPoint bmparam:pos .
brick:hasPoint bmparam:pos, bmparam:sen .
bmparam:pos a brick:Damper_Position_Command .
bmparam:sen a brick:Damper_Position_Sensor .
optional: ["sen"]
41 changes: 37 additions & 4 deletions tests/unit/test_template_api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from rdflib import Graph, Namespace

from buildingmotif import BuildingMOTIF
Expand All @@ -17,9 +18,10 @@ def test_template_evaluate(bm: BuildingMOTIF):
zone = lib.get_template_by_name("zone")
assert zone.parameters == {"name", "cav"}

partial = zone.evaluate({"name": BLDG["zone1"]})
assert isinstance(partial, Template)
assert partial.parameters == {"cav"}
with pytest.warns():
partial = zone.evaluate({"name": BLDG["zone1"]})
assert isinstance(partial, Template)
assert partial.parameters == {"cav"}

graph = partial.evaluate({"cav": BLDG["cav1"]})
assert isinstance(graph, Graph)
Expand Down Expand Up @@ -109,9 +111,33 @@ def test_template_inline_dependencies(bm: BuildingMOTIF):
"sf-ss",
"sf-st",
"oad-pos",
"oad-sen",
}
assert inlined.parameters == preserved_params

# test optional 'name' param on dependency; this should
# preserve optionality of all params in the dependency
lib = Library.load(directory="tests/unit/fixtures/inline-dep-test")
templ = lib.get_template_by_name("A")
assert templ.parameters == {"name", "b", "c", "d"}
assert set(templ.optional_args) == {"d"}
assert len(templ.get_dependencies()) == 3
inlined = templ.inline_dependencies()
assert len(inlined.get_dependencies()) == 0
assert inlined.parameters == {"name", "b", "c", "b-bp", "c-cp", "d", "d-dp"}
assert set(inlined.optional_args) == {"d", "d-dp"}

# test optional non-'name' parameter on dependency; this should
# only make that single parameter optional
templ = lib.get_template_by_name("A-alt")
assert templ.parameters == {"name", "b"}
assert set(templ.optional_args) == {"b-bp"}
assert len(templ.get_dependencies()) == 1
inlined = templ.inline_dependencies()
assert len(inlined.get_dependencies()) == 0
assert inlined.parameters == {"name", "b", "b-bp"}
assert set(inlined.optional_args) == {"b-bp"}


def test_template_evaluate_with_optional(bm: BuildingMOTIF):
"""
Expand All @@ -131,6 +157,13 @@ def test_template_evaluate_with_optional(bm: BuildingMOTIF):
assert isinstance(t, Template)
assert t.parameters == {"occ"}

with pytest.warns():
t = templ.evaluate(
{"name": BLDG["vav"], "zone": BLDG["zone1"]}, require_optional_args=True
)
assert isinstance(t, Template)
assert t.parameters == {"occ"}


def test_template_matching(bm: BuildingMOTIF):
EX = Namespace("urn:ex/")
Expand All @@ -151,7 +184,7 @@ def test_template_matching(bm: BuildingMOTIF):

remaining_template = matcher.remaining_template(mapping)
assert isinstance(remaining_template, Template)
assert remaining_template.parameters == {"pos"}
assert remaining_template.parameters == {"sen", "pos"}


def test_template_matcher_with_graph_target(bm: BuildingMOTIF):
Expand Down
Loading