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

Raise error for groups that decorate non-operation functions #544

Merged
merged 34 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0445141
Raise error for groups that decorate non-operation functions
kidrahahjo Jun 19, 2021
d57f051
Update changelog
kidrahahjo Jun 19, 2021
0a8399c
Update error message.
bdice Jun 19, 2021
42e7442
Update changelog.txt
bdice Jun 19, 2021
ba6e2af
Merge remote-tracking branch 'origin/master' into fix/group-decorator…
kidrahahjo Jun 23, 2021
dadc341
Merge branch 'master' into fix/group-decorator-error
kidrahahjo Jun 23, 2021
67a2674
Update changelog version
kidrahahjo Jun 24, 2021
a0dcd34
Update with Bradley's changes
kidrahahjo Jun 24, 2021
aff635e
Merge branch 'master' into fix/group-decorator-error
kidrahahjo Jul 8, 2021
6f6cc89
Merge branch 'master' into fix/group-decorator-error
bdice Jul 30, 2021
792bd81
resolve merge conflicts
kidrahahjo Aug 12, 2021
4a4bc0b
resolve merge conflicts
kidrahahjo Aug 12, 2021
9cd8065
Merge branch 'master' into fix/group-decorator-error
bdice Aug 19, 2021
39ab112
Update changelog.
bdice Aug 19, 2021
63e757e
Use FlowProjectDefinitionError.
bdice Aug 19, 2021
80233f2
Use mock_project for creating FlowProject instances.
bdice Aug 19, 2021
4296546
Add test for multiple lambda functions as operations.
bdice Aug 19, 2021
9aa4a27
Add tests for anonymous functions in groups.
bdice Aug 19, 2021
ee92b19
Use sets instead of lists for operation._flow_groups and group._opera…
bdice Aug 19, 2021
8b14160
Make FlowGroupEntry._operations private.
bdice Aug 19, 2021
6636943
Move error for assigning non-operation to group to definition.
b-butler Sep 17, 2021
df3a754
Update tests to use new decorator ordering.
b-butler Sep 17, 2021
8017e85
Merge branch 'master' into fix/group-decorator-error
bdice Oct 17, 2021
d03fb0f
Move changelog entry.
bdice Oct 17, 2021
06b66e5
Reorder group decorators.
bdice Oct 17, 2021
fe84df8
Merge branch 'master' into fix/group-decorator-error
bdice Nov 8, 2021
6b37a8f
Use correct number of fields in tuple.
bdice Nov 8, 2021
5e67984
Merge remote-tracking branch 'origin/master' into fix/group-decorator…
vyasr Nov 17, 2021
83a3d83
Merge branch 'master' into fix/group-decorator-error
vyasr Nov 18, 2021
884377b
Merge branch 'fix/group-decorator-error' of github.com:glotzerlab/sig…
vyasr Nov 18, 2021
ec32ebb
Fix bug in status generation script that was regenerating without force.
vyasr Nov 18, 2021
ccc6daa
Update reference data.
vyasr Nov 18, 2021
7e6623f
Address PR reviews.
vyasr Nov 19, 2021
774c799
Update flow/project.py
vyasr Nov 19, 2021
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
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Fixed

- Scripts are now generated correctly when project path contains spaces and special characters (#572).
- XSEDE Expanse template has been fixed to remove leading spaces (#575, #576).
- Error raised when a ``FlowGroup`` decorates functions that are not ``FlowProject`` operations (#538, #544).

Changed
+++++++
Expand Down
49 changes: 30 additions & 19 deletions flow/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,8 @@ class FlowGroupEntry:
----------
name : str
The name of the :class:`FlowGroup` to be created.
project : flow.FlowProject
The project the group is associated with.
options : str
The :meth:`FlowProject.run` options to pass when submitting the group.
These will be included in all submissions. Submissions use run
Expand All @@ -649,8 +651,9 @@ class FlowGroupEntry:
aggregator object associated with the :class:`FlowGroup` (Default value = None).
"""

def __init__(self, name, options="", group_aggregator=None):
def __init__(self, name, project, options="", group_aggregator=None):
self.name = name
self._project = project
self.options = options
# We register aggregators associated with operation functions in
# `_register_groups` and we do not set the aggregator explicitly.
Expand All @@ -675,14 +678,19 @@ def __call__(self, func):
The decorated function.

"""
if hasattr(func, "_flow_groups"):
if self.name in func._flow_groups:
raise FlowProjectDefinitionError(
f"Cannot reregister operation '{func}' with the group '{self.name}'."
)
func._flow_groups.append(self.name)
else:
func._flow_groups = [self.name]
if not any(
func == op_func for _, op_func in self._project._OPERATION_FUNCTIONS
):
raise FlowProjectDefinitionError(
f"Cannot add function '{func}' to group without making the function an "
f"operation. Add @MyProjectClass.operation below group decorator."
)

if self.name in func._flow_groups[self._project]:
raise FlowProjectDefinitionError(
f"Cannot reregister operation '{func}' with the group '{self.name}'."
)
func._flow_groups[self._project].add(self.name)
return func

def _set_directives(self, func, directives):
Expand Down Expand Up @@ -1455,11 +1463,12 @@ def __call__(self, func, name=None):
# delay setting the aggregator because we do not restrict the decorator
# placement in terms of `@FlowGroupEntry`, `@aggregator`, or
# `@operation`.
self._parent_class._GROUPS.append(FlowGroupEntry(name=name, options=""))
if hasattr(func, "_flow_groups"):
func._flow_groups.append(name)
else:
func._flow_groups = [name]
self._parent_class._GROUPS.append(
FlowGroupEntry(name=name, project=self._parent_class, options="")
)
if not hasattr(func, "_flow_groups"):
func._flow_groups = {}
func._flow_groups[self._parent_class] = {name}
return func

def with_directives(self, directives, name=None):
Expand Down Expand Up @@ -2989,7 +2998,7 @@ def _op_submission_summary(counter):
num_omitted_operations = len(op_counter) - len(context["op_counter"])
if num_omitted_operations > 0:
context["op_counter"].append(
(f"[{num_omitted_operations} more operations omitted]", "")
(f"[{num_omitted_operations} more operations omitted]", "", "")
)

# We have to make a deep copy of the template environment if we're
Expand Down Expand Up @@ -4299,7 +4308,7 @@ def foo(job):
)
cls._GROUP_NAMES.add(name)
group_entry = FlowGroupEntry(
name=name, options=options, group_aggregator=group_aggregator
name=name, project=cls, options=options, group_aggregator=group_aggregator
)
cls._GROUPS.append(group_entry)
return group_entry
Expand Down Expand Up @@ -4353,9 +4362,11 @@ def _register_groups(self):
else:
func = operation._op_func

if hasattr(func, "_flow_groups"):
op_directives = getattr(func, "_flow_group_operation_directives", {})
for group_name in func._flow_groups:
op_directives = getattr(func, "_flow_group_operation_directives", {})
for cls in self.__class__.__mro__:
# Need to use `get` since we don't know which class in the
# hierarchy this function was registered to.
for group_name in func._flow_groups.get(cls, []):
directives = op_directives.get(group_name)
self._groups[group_name].add_operation(
operation_name, operation, directives
Expand Down
4 changes: 2 additions & 2 deletions tests/define_aggregate_test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ def agg_op1_custom(*jobs):
set_all_job_docs(jobs, "sum_custom", sum_custom)


@_AggregateTestProject.operation
@group1
@_AggregateTestProject.operation
@aggregator.groupsof(30)
def agg_op2(*jobs):
set_all_job_docs(jobs, "op2", True)


@_AggregateTestProject.operation
@group1
@_AggregateTestProject.operation
@aggregator()
def agg_op3(*jobs):
set_all_job_docs(jobs, "op3", True)
Expand Down
6 changes: 3 additions & 3 deletions tests/define_directives_test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ class _DirectivesTestProject(FlowProject):
group = _DirectivesTestProject.make_group(name="walltimegroup")


@_DirectivesTestProject.operation.with_directives({"walltime": 1.0})
@group
@_DirectivesTestProject.operation.with_directives({"walltime": 1.0})
def op_walltime(job):
pass


@_DirectivesTestProject.operation.with_directives({"walltime": None})
@group
@_DirectivesTestProject.operation.with_directives({"walltime": None})
def op_walltime_2(job):
pass


@group
@_DirectivesTestProject.operation.with_directives(
{"walltime": datetime.timedelta(hours=2)}
)
@group
def op_walltime_3(job):
pass

Expand Down
5 changes: 3 additions & 2 deletions tests/define_status_test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,22 @@ def b_is_even(job):

@flow.cmd
@group1
@_TestProject.operation
@_TestProject.pre(b_is_even)
@_TestProject.post.isfile("world.txt")
def op1(job):
return 'echo "hello" > {job.ws}/world.txt'


@_TestProject.operation
@group1
@_TestProject.operation
@_TestProject.post.true("test")
def op2(job):
job.document.test = os.getpid()


@_TestProject.operation
@group2
@_TestProject.operation
@_TestProject.post.true("test2")
def op3(job):
job.document.test2 = True
Expand Down
8 changes: 4 additions & 4 deletions tests/define_template_test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ class TestProject(flow.FlowProject):
group1 = TestProject.make_group(name="group1")


@TestProject.operation
@group1
@TestProject.operation
def serial_op(job):
pass


@TestProject.operation.with_directives({"np": TestProject.np})
@group1
@TestProject.operation.with_directives({"np": TestProject.np})
def parallel_op(job):
pass

Expand Down Expand Up @@ -56,14 +56,14 @@ def mpi_gpu_op(job):
pass


@TestProject.operation.with_directives({"memory": TestProject.memory})
@group1
@TestProject.operation.with_directives({"memory": TestProject.memory})
def memory_op(job):
pass


@TestProject.operation.with_directives({"walltime": TestProject.walltime})
@group1
@TestProject.operation.with_directives({"walltime": TestProject.walltime})
def walltime_op(job):
pass

Expand Down
8 changes: 4 additions & 4 deletions tests/define_test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def b_is_even(job):
return False


@group1
@_TestProject.operation.with_directives(
{
# Explicitly set a "bad" directive that is unused by the template.
Expand All @@ -46,7 +47,6 @@ def b_is_even(job):
)
@flow.cmd
@_TestProject.pre(b_is_even)
@group1
@_TestProject.post.isfile("world.txt")
def op1(job):
return 'echo "hello" > {job.ws}/world.txt'
Expand All @@ -56,15 +56,15 @@ def _need_to_fork(job):
return job.doc.get("fork")


@_TestProject.operation.with_directives({"fork": _need_to_fork})
@group1
@_TestProject.operation.with_directives({"fork": _need_to_fork})
@_TestProject.post.true("test")
def op2(job):
job.document.test = os.getpid()


@_TestProject.operation.with_directives({"ngpu": 1, "omp_num_threads": 1})
@group2.with_directives(dict(omp_num_threads=4))
@_TestProject.operation.with_directives({"ngpu": 1, "omp_num_threads": 1})
@_TestProject.post.true("test3_true")
@_TestProject.post.false("test3_false")
@_TestProject.post.not_(lambda job: job.doc.test3_false)
Expand All @@ -80,8 +80,8 @@ class _DynamicTestProject(_TestProject):
group3 = _DynamicTestProject.make_group(name="group3")


@_DynamicTestProject.operation
@group3
@_DynamicTestProject.operation
@_DynamicTestProject.pre.after(op1)
@_DynamicTestProject.post(lambda job: job.sp.get("dynamic", False))
def op4(job):
Expand Down
1 change: 1 addition & 0 deletions tests/generate_status_reference_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def main(args):
f"Archive '{ARCHIVE_PATH}' already exists, exiting. "
"Use `-f/--force` to overwrite."
)
return

with signac.TemporaryProject(name=PROJECT_NAME) as p, signac.TemporaryProject(
name=STATUS_OPTIONS_PROJECT_NAME
Expand Down
Binary file modified tests/status_reference_data.tar.gz
Binary file not shown.
65 changes: 56 additions & 9 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ def baz(job):
pass

with suspend_logging():
a = A.get_project(root=self._tmp_dir.name)
b = B.get_project(root=self._tmp_dir.name)
c = C.get_project(root=self._tmp_dir.name)
a = self.mock_project(A)
b = self.mock_project(B)
c = self.mock_project(C)

assert len(a.operations) == 2
assert len(b.operations) == 3
Expand All @@ -288,7 +288,23 @@ class A(FlowProject):
def op1(job):
pass

return
def test_repeat_anonymous_operation_definition(self):
class A(FlowProject):
pass

A.operation(lambda job: print("Hello", job))

assert len(self.mock_project(A).operations) == 1

anonymous_func = lambda job: print("Hi", job) # noqa: E731

with pytest.raises(FlowProjectDefinitionError):
# Only one anonymous operation is allowed, or else the name
# "<lambda>" conflicts between the operations.
A.operation(anonymous_func)

A.operation(anonymous_func, name="hi_operation")
assert len(self.mock_project(A).operations) == 2

def test_repeat_operation_name(self):
class A(FlowProject):
Expand Down Expand Up @@ -347,10 +363,10 @@ def op1(job):

# Should raise no error
with suspend_logging():
A.get_project(root=self._tmp_dir.name)
self.mock_project(A)

with pytest.raises(FlowProjectDefinitionError):
B.get_project(root=self._tmp_dir.name)
self.mock_project(B)

def test_label_definition(self):
class A(FlowProject):
Expand All @@ -371,9 +387,9 @@ def label1(job):
def label2(job):
pass

a = A.get_project(root=self._tmp_dir.name)
b = B.get_project(root=self._tmp_dir.name)
c = C.get_project(root=self._tmp_dir.name)
a = self.mock_project(A)
b = self.mock_project(B)
c = self.mock_project(C)

assert len(a._label_functions) == 1
assert len(b._label_functions) == 2
Expand Down Expand Up @@ -1772,6 +1788,37 @@ def bar(job):
with pytest.raises(FlowProjectDefinitionError):
B.make_group("bar")

def test_group_operation_without_operation_definition(self):
"""Test that groups can only be applied to operations."""

class A(FlowProject):
pass

group = A.make_group("foo")

with pytest.raises(FlowProjectDefinitionError):

@group
def test_op(job):
pass

# Make test_op into an operation, then group addition should succeed.
@group
@A.operation
def test_op_2(job):
pass

def test_group_operation_without_operation_definition_anonymous(self):
"""Test that groups cannot be applied to anonymous functions."""

class A(FlowProject):
pass

group = A.make_group("foo")

with pytest.raises(FlowProjectDefinitionError):
group(lambda job: print(job))

def test_repeat_group_definition(self):
"""Test that groups cannot be registered if a group with that name exists."""

Expand Down