From 3a2177a968877c28dca442d460af2b0d95f1da81 Mon Sep 17 00:00:00 2001 From: Brandon Butler Date: Tue, 22 Nov 2022 14:04:20 -0700 Subject: [PATCH] Refactor/remove deprecated code (#686) * refactor!: Remove deprecated code. * fix: Update directives decoration deprecation * refactor: Remove directives decorator * refactor!: Remove FlowProject.operation.with_directives * fix: Remove remaining references to removed code * refactor!: Remove formatting cmd operations * fix/test: Update/fix lingering errors from deprecation removals * refactor: Use obj.attr = value over setattr Co-authored-by: Bradley Dice Co-authored-by: Bradley Dice --- doc/api.rst | 20 --- flow/__init__.py | 1 - flow/directives.py | 10 +- flow/environments/incite.py | 2 +- flow/operations.py | 209 ------------------------- flow/project.py | 146 +++++------------ tests/define_aggregate_test_project.py | 2 +- tests/define_status_test_project.py | 2 +- tests/define_test_project.py | 2 +- tests/interactive_template_test.py | 22 ++- tests/test_project.py | 172 ++------------------ 11 files changed, 66 insertions(+), 522 deletions(-) delete mode 100644 flow/operations.py diff --git a/doc/api.rst b/doc/api.rst index 28de535ce..139e406a7 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -22,7 +22,6 @@ The FlowProject FlowProject.main FlowProject.make_group FlowProject.operation - FlowProject.operation.with_directives FlowProject.operation_hooks FlowProject.operation_hooks.on_exception FlowProject.operation_hooks.on_exit @@ -58,8 +57,6 @@ The FlowProject .. automethod:: flow.FlowProject.operation(func, name=None) -.. automethod:: flow.FlowProject.operation.with_directives(directives, name=None) - .. automethod:: flow.FlowProject.operation_hooks(hook_func, trigger) .. automethod:: flow.FlowProject.operation_hooks.on_exception @@ -134,23 +131,6 @@ Labels :members: :special-members: __call__ -@flow.cmd ---------- - -.. autofunction:: cmd - -@flow.with_job --------------- - -.. autofunction:: with_job - -@flow.directives ----------------- - -.. autoclass:: directives - :members: - :special-members: __call__ - flow.init() ----------- diff --git a/flow/__init__.py b/flow/__init__.py index 10715554d..1b047f619 100644 --- a/flow/__init__.py +++ b/flow/__init__.py @@ -11,7 +11,6 @@ from . import environment, environments, errors, hooks, scheduling, testing from .aggregates import aggregator, get_aggregate_id from .environment import get_environment -from .operations import cmd, directives, with_job from .project import FlowProject, IgnoreConditions, classlabel, label, staticlabel from .template import init diff --git a/flow/directives.py b/flow/directives.py index 936d904ba..fbdcce25e 100644 --- a/flow/directives.py +++ b/flow/directives.py @@ -507,7 +507,7 @@ def _GET_EXECUTABLE(): .. code-block:: python - @Project.operation.with_directives({"memory": "4g"}) + @Project.operation(directives={"memory": "4g"}) def op(job): pass @@ -517,7 +517,7 @@ def op(job): .. code-block:: python - @Project.operation.with_directives({"memory": "512m"}) + @Project.operation(directives={"memory": "512m"}) def op(job): pass @@ -527,11 +527,11 @@ def op(job): .. code-block:: python - @Project.operation.with_directives({"memory": "4"}) + @Project.operation(directives={"memory": "4"}) def op1(job): pass - @Project.operation.with_directives({"memory": 4}) + @Project.operation(directives={"memory": 4}) def op2(job): pass """ @@ -601,7 +601,7 @@ def op2(job): .. code-block:: python - @Project.operation.with_directives({"walltime": 24}) + @Project.operation(directives={"walltime": 24}) def op(job): # This operation takes 1 day to run pass diff --git a/flow/environments/incite.py b/flow/environments/incite.py index 1c86f5b0c..0fe168b6d 100644 --- a/flow/environments/incite.py +++ b/flow/environments/incite.py @@ -19,7 +19,7 @@ class SummitEnvironment(DefaultLSFEnvironment): Example:: - @Project.operation.with_directives({ + @Project.operation(directives={ "nranks": 3, # 3 MPI ranks per operation "ngpu": 3, # 3 GPUs "np": 3, # 3 CPU cores diff --git a/flow/operations.py b/flow/operations.py deleted file mode 100644 index 3052aa0da..000000000 --- a/flow/operations.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright (c) 2018 The Regents of the University of Michigan -# All rights reserved. -# This software is licensed under the BSD 3-Clause License. -"""Defines operation decorators and a simple command line interface ``run``. - -This module implements the run() function, which when called equips a regular -Python module with a command line interface. This interface can be used to -execute functions defined within the same module that operate on a signac data -space. - -See also: :class:`~.FlowProject`. -""" -import inspect -import logging -import warnings -from functools import wraps -from textwrap import indent - -from .aggregates import aggregator -from .directives import _document_directive -from .environment import ComputeEnvironment -from .errors import FlowProjectDefinitionError - -logger = logging.getLogger(__name__) - - -def cmd(func): - """Indicate that ``func`` returns a shell command with this decorator. - - If this function is an operation function defined by :class:`~.FlowProject`, it will - be interpreted to return a shell command, instead of executing the function itself. - - For example: - - .. code-block:: python - - @FlowProject.operation - @flow.cmd - def hello(job): - return "echo {job.id}" - - .. note:: - The final shell command generated for :meth:`~.FlowProject.run` or - :meth:`~.FlowProject.submit` still respects directives and will prepend e.g. MPI or OpenMP - prefixes to the shell command provided here. - """ - warnings.warn( - "@flow.cmd has been deprecated as of 0.22.0 and will be removed in " - "0.23.0. Use @FlowProject.operation(cmd=True) instead.", - FutureWarning, - ) - if getattr(func, "_flow_with_job", False): - raise FlowProjectDefinitionError( - "The @flow.cmd decorator must appear below the @flow.with_job decorator." - ) - if hasattr(func, "_flow_cmd"): - raise FlowProjectDefinitionError( - f"Cannot specify that {func} is a command operation twice." - ) - setattr(func, "_flow_cmd", True) - return func - - -def with_job(func): - """Use ``arg`` as a context manager for ``func(arg)`` with this decorator. - - This decorator can only be used for operations that accept a single job as a parameter. - - If this function is an operation function defined by :class:`~.FlowProject`, it will - be the same as using ``with job:``. - - For example: - - .. code-block:: python - - @FlowProject.operation - @flow.with_job - def hello(job): - print("hello {}".format(job)) - - Is equivalent to: - - .. code-block:: python - - @FlowProject.operation - def hello(job): - with job: - print("hello {}".format(job)) - - This also works with the `@cmd` decorator: - - .. code-block:: python - - @FlowProject.operation - @with_job - @cmd - def hello(job): - return "echo 'hello {}'".format(job) - - Is equivalent to: - - .. code-block:: python - - @FlowProject.operation - @cmd - def hello_cmd(job): - return 'trap "cd `pwd`" EXIT && cd {} && echo "hello {job}"'.format(job.ws) - """ - warnings.warn( - "@flow.with_job has been deprecated as of 0.22.0 and will be removed in " - "0.23.0. Use @FlowProject.operation(with_job=True) instead.", - FutureWarning, - ) - base_aggregator = aggregator.groupsof(1) - if hasattr(func, "_flow_with_job"): - raise FlowProjectDefinitionError(f"Cannot specify with_job for {func} twice.") - - if getattr(func, "_flow_aggregate", base_aggregator) != base_aggregator: - raise FlowProjectDefinitionError( - "The @with_job decorator cannot be used with aggregation." - ) - - @wraps(func) - def decorated(*jobs): - with jobs[0] as job: - if getattr(func, "_flow_cmd", False): - return f'trap "cd $(pwd)" EXIT && cd {job.ws} && {func(job)}' - else: - return func(job) - - setattr(decorated, "_flow_with_job", True) - return decorated - - -class directives: - """Decorator for operation functions to provide additional execution directives. - - Directives can for example be used to provide information about required resources - such as the number of processes required for execution of parallelized operations. - For more information, read about :ref:`signac-docs:cluster_submission_directives`. - - .. deprecated:: 0.15 - This decorator is deprecated and will be removed in 1.0. - Use :class:`FlowProject.operation.with_directives` instead. - - """ - - def __init__(self, **kwargs): - self.kwargs = kwargs - - @classmethod - def copy_from(cls, func): - """Copy directives from another operation.""" - return cls(**getattr(func, "_flow_directives", {})) - - def __call__(self, func): - """Add directives to the function. - - This call operator allows the class to be used as a decorator. - - Parameters - ---------- - func : callable - The function to decorate. - - Returns - ------- - callable - The decorated function. - - """ - directives = getattr(func, "_flow_directives", {}) - directives.update(self.kwargs) - setattr(func, "_flow_directives", directives) - return func - - -# Remove when @flow.directives is removed -_directives_to_document = ( - ComputeEnvironment._get_default_directives()._directive_definitions.values() -) -directives.__doc__ += indent( - "\n**Supported Directives:**\n\n" - + "\n\n".join( - _document_directive(directive) for directive in _directives_to_document - ), - " ", -) - - -def _get_operations(include_private=False): - """Yield the name of all functions that qualify as an operation function. - - The module is inspected and all functions that have only one argument - is yielded. Unless the 'include_private' argument is True, all private - functions, that means the name starts with one or more '_' characters - are ignored. - """ - module = inspect.getmodule(inspect.currentframe().f_back.f_back) - for name, obj in inspect.getmembers(module): - if not include_private and name.startswith("_"): - continue - if inspect.isfunction(obj): - signature = inspect.getfullargspec(obj) - if len(signature.args) == 1: - yield name - - -__all__ = ["cmd", "directives", "with_job"] diff --git a/flow/project.py b/flow/project.py index 0155def1c..b55e04a33 100644 --- a/flow/project.py +++ b/flow/project.py @@ -21,7 +21,6 @@ import textwrap import threading import time -import warnings from collections import Counter, defaultdict from copy import deepcopy from enum import IntFlag @@ -58,8 +57,6 @@ ) from .hooks import _Hooks from .labels import _is_label_func, classlabel, label, staticlabel -from .operations import cmd as _cmd -from .operations import with_job as _with_job from .render_status import _render_status from .scheduling.base import ClusterJob, JobStatus from .util import config as flow_config @@ -580,49 +577,7 @@ def __str__(self): def __call__(self, *jobs): """Return the command formatted with the supplied job(s).""" - cmd = self._cmd(*jobs) if callable(self._cmd) else self._cmd - # The logic below will be removed after version 0.23.0. This is only to temporary fix an - # issue in supporting the formatting of cmd operation in the interim. - format_arguments = {} - if not callable(self._cmd): - format_arguments["jobs"] = jobs - if len(jobs) == 1: - format_arguments["job"] = jobs[0] - formatted_cmd = cmd.format(**format_arguments) - else: - argspec = inspect.getfullargspec(self._cmd) - signature = inspect.signature(self._cmd) - args = { - k: v for k, v in signature.parameters.items() if k != argspec.varargs - } - - # get all named positional/keyword arguments with individual names. - for i, arg_name in enumerate(args): - try: - format_arguments[arg_name] = jobs[i] - except IndexError: - format_arguments[arg_name] = args[arg_name].default - - # capture any remaining variable positional arguments. - if argspec.varargs: - format_arguments[argspec.varargs] = jobs[len(args) :] - - # Capture old behavior which assumes job or jobs in the format string. We need to test - # the truthiness of the key versus the inclusion because in the case of with_job the - # above logic results in format_arguments["jobs"] = (). - if not any(format_arguments.get(key, False) for key in ("jobs", "job")): - if match := re.search("{.*(jobs?).*}", cmd): - # Saves in key jobs or job based on regex match. - format_arguments[match.group(1)] = jobs - formatted_cmd = cmd.format(**format_arguments) - if formatted_cmd != cmd: - _deprecated_warning( - deprecation="Returning format strings in a cmd operation", - alternative="Users should format the command string.", - deprecated_in="0.22.0", - removed_in="0.23.0", - ) - return formatted_cmd + return self._cmd(*jobs) if callable(self._cmd) else self._cmd class FlowOperation(BaseFlowOperation): @@ -772,7 +727,7 @@ def with_directives(self, directives): The directives specified in this decorator are only applied when executing the operation through the :class:`FlowGroup`. To apply directives to an individual operation executed outside of the - group, see :meth:`.FlowProject.operation.with_directives`. + group, see :meth:`.FlowProject.operation`. Parameters ---------- @@ -809,12 +764,12 @@ class FlowGroup: group = FlowProject.make_group(name='example_group') @group.with_directives({"nranks": 4}) - @FlowProject.operation.with_directives({"nranks": 2, "executable": "python3"}) + @FlowProject.operation({"nranks": 2, "executable": "python3"}) def op1(job): pass @group - @FlowProject.operation.with_directives({"nranks": 2, "executable": "python3"}) + @FlowProject.operation({"nranks": 2, "executable": "python3"}) def op2(job): pass @@ -1464,11 +1419,11 @@ class OperationRegister: def hello(job): print('Hello', job) - Directives can also be specified by using :meth:`FlowProject.operation.with_directives`. + Directives can also be specified by using :meth:`FlowProject.operation`. .. code-block:: python - @FlowProject.operation.with_directives({"nranks": 4}) + @FlowProject.operation({"nranks": 4}) def mpi_hello(job): print("hello") @@ -1547,16 +1502,10 @@ def _internal_call( "A condition function cannot be used as an operation." ) - # Handle cmd and with_job options. Use the deprecated decorators internally until - # the decorators are removed. These must be done first for now as with_job actually - # wraps the original function meaning that any other labels we apply will be masked - # if we do this later or not even captured it not added to _OPERATION_FUNCTIONS. - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=FutureWarning) - if cmd: - _cmd(func) - if with_job: - func = _with_job(func) + if cmd: + setattr(func, "_flow_cmd", True) + if with_job: + func = self._with_job(func) # Store directives if directives is not None: @@ -1608,56 +1557,37 @@ def _internal_call( func._flow_groups[self._parent_class] = {name} return func - def with_directives(self, directives, name=None): - """Decorate a function to make it an operation with additional execution directives. - - Directives can be used to provide information about required - resources such as the number of processors required for - execution of parallelized operations. For more information, see - :ref:`signac-docs:cluster_submission_directives`. To apply - directives to an operation that is part of a group, use - :meth:`.FlowGroupEntry.with_directives`. - - Parameters - ---------- - directives : dict - Directives to use for resource requests and execution. - name : str - The operation name. Uses the name of the function if None - (Default value = None). - - Returns - ------- - function - A decorator which registers the function with the provided - name and directives as an operation of the - :class:`~.FlowProject` subclass. - """ - _deprecated_warning( - deprecation="@FlowProject.operation.with_directives", - alternative="Use @FlowProject.operation(directives={...}) instead.", - deprecated_in="0.22.0", - removed_in="0.23.0", - ) - - def add_operation_with_directives(function): - function._flow_directives = directives - return self(function, name) + def _with_job(self, func): + base_aggregator = aggregator.groupsof(1) + if getattr(func, "_flow_aggregate", base_aggregator) != base_aggregator: + raise FlowProjectDefinitionError( + "The with_job keyword argument cannot be used with aggregation." + ) - return add_operation_with_directives + @functools.wraps(func) + def decorated(*jobs): + with jobs[0] as job: + if getattr(func, "_flow_cmd", False): + return ( + f'trap "cd $(pwd)" EXIT && cd {job.ws} && {func(job)}' + ) + else: + return func(job) - _directives_to_document = ( - ComputeEnvironment._get_default_directives()._directive_definitions.values() - ) - with_directives.__doc__ += textwrap.indent( - "\n\n**Supported Directives:**\n\n" - + "\n\n".join( - _document_directive(directive) - for directive in _directives_to_document - ), - " " * 16, - ) + decorated._flow_with_job = True + decorated._flow_cmd = getattr(func, "_flow_cmd", False) + return decorated + _directives_to_document = ( + ComputeEnvironment._get_default_directives()._directive_definitions.values() + ) + OperationRegister.__doc__ += textwrap.indent( + "\n\n**Supported Directives:**\n\n" + + "\n\n".join( + _document_directive(directive) for directive in _directives_to_document + ), + " " * 16, + ) return OperationRegister() @staticmethod diff --git a/tests/define_aggregate_test_project.py b/tests/define_aggregate_test_project.py index 563632c89..718568cc4 100644 --- a/tests/define_aggregate_test_project.py +++ b/tests/define_aggregate_test_project.py @@ -72,7 +72,7 @@ def agg_op3(*jobs): cmd=True, aggregator=aggregator(sort_by="i", select=lambda job: job.sp.i <= 2) ) def agg_op4(*jobs): - return "echo '{jobs[0].sp.i} and {jobs[1].sp.i}'" + return f"echo '{jobs[0].sp.i} and {jobs[1].sp.i}'" if __name__ == "__main__": diff --git a/tests/define_status_test_project.py b/tests/define_status_test_project.py index abbc246f5..6cb57ff54 100644 --- a/tests/define_status_test_project.py +++ b/tests/define_status_test_project.py @@ -39,7 +39,7 @@ def b_is_even(job): @_TestProject.pre(b_is_even) @_TestProject.post.isfile("world.txt") def op1(job): - return 'echo "hello" > {job.ws}/world.txt' + return f'echo "hello" > {job.ws}/world.txt' @group1 diff --git a/tests/define_test_project.py b/tests/define_test_project.py index f7ea5b51b..8fbb64fca 100644 --- a/tests/define_test_project.py +++ b/tests/define_test_project.py @@ -48,7 +48,7 @@ def b_is_even(job): @_TestProject.pre(b_is_even) @_TestProject.post.isfile("world.txt") def op1(job): - return 'echo "hello" > {job.ws}/world.txt' + return f'echo "hello" > {job.ws}/world.txt' def _need_to_fork(job): diff --git a/tests/interactive_template_test.py b/tests/interactive_template_test.py index 80201d307..66b715ded 100755 --- a/tests/interactive_template_test.py +++ b/tests/interactive_template_test.py @@ -9,25 +9,23 @@ import jinja2 import signac -import flow - def add_operation(Project, name): click.echo("Specify directives for the next operation.") - nranks = click.prompt("nranks", default=0) - omp_num_threads = click.prompt("omp_num_threads", default=0) - np = click.prompt("np", default=max(1, nranks) * max(1, omp_num_threads)) - ngpu = click.prompt("ngpus", default=0) - - @flow.directives(np=np) - @flow.directives(nranks=nranks) - @flow.directives(omp_num_threads=omp_num_threads) - @flow.directives(ngpu=ngpu) + directives = {} + directives["nranks"] = click.prompt("nranks", default=0) + directives["omp_num_threads"] = click.prompt("omp_num_threads", default=0) + directives["np"] = click.prompt( + "np", + default=max(1, directives["nranks"]) * max(1, directives["omp_num_threads"]), + ) + directives["ngpu"] = click.prompt("ngpus", default=0) + def op(job): pass op.__name__ = name - Project.operation(op) + Project.operation(op, directives=directives) return click.prompt("Do you want to add more operations?", default=False) diff --git a/tests/test_project.py b/tests/test_project.py index 657626b30..044147874 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -22,15 +22,7 @@ from deprecation import fail_if_not_removed import flow -from flow import ( - FlowProject, - aggregator, - cmd, - directives, - get_aggregate_id, - init, - with_job, -) +from flow import FlowProject, aggregator, get_aggregate_id, init from flow.environment import ComputeEnvironment from flow.errors import ( DirectivesError, @@ -351,7 +343,7 @@ def op2(job): assert len(B._collect_postconditions()[op2]) == 2 assert len(C._collect_postconditions()[op2]) == 3 - def test_with_job_decorator(self): + def test_with_job_argument(self): class A(FlowProject): pass @@ -365,13 +357,13 @@ def test_context(job): project.run() assert os.getcwd() == starting_dir - def test_cmd_operation_argument_as_command(self): + def test_cmd_operation_argument(self): class A(FlowProject): pass @A.operation(with_job=True, cmd=True) def test_cmd(joba, jobb="test"): - return "echo '{joba} {jobb}' > output.txt" + return f"echo '{joba} {jobb}' > output.txt" project = self.mock_project(A) with setup_project_subprocess_execution(project): @@ -382,117 +374,6 @@ def test_cmd(joba, jobb="test"): lines = f.readlines() assert f"{job.id} test\n" == lines[0] - def test_cmd_operation_argument_as_command_invalid(self): - class A(FlowProject): - pass - - @A.operation(with_job=True, cmd=True) - def test_cmd(job): - return "echo '{jobabc}' > output.txt" - - project = self.mock_project(A) - with setup_project_subprocess_execution(project): - with pytest.raises(KeyError): - project.run() - for job in project: - assert not os.path.isfile(job.fn("output.txt")) - - def test_cmd_argument_deprecated_jobs_argument(self): - class A(FlowProject): - pass - - @A.operation(cmd=True, with_job=True) - def test_cmd(job123): - return "echo '{jobs}' > output.txt" - - project = self.mock_project(A) - with setup_project_subprocess_execution(project): - with pytest.warns(FutureWarning): - project.run() - for job in project: - assert os.path.isfile(job.fn("output.txt")) - - def test_cmd_argument_deprecated_job_argument(self): - class A(FlowProject): - pass - - @A.operation(with_job=True, cmd=True) - def test_cmd(job123): - return "echo '{job}' > output.txt" - - project = self.mock_project(A) - with setup_project_subprocess_execution(project): - with pytest.warns(FutureWarning): - project.run() - for job in project: - assert os.path.isfile(job.fn("output.txt")) - - @pytest.mark.filterwarnings("ignore:@flow.cmd") - def test_cmd_decorator_with_cmd_argument(self): - class A(FlowProject): - pass - - with pytest.raises(FlowProjectDefinitionError): - - @A.operation(cmd=True) - @cmd - def op1(job): - pass - - with pytest.raises(FlowProjectDefinitionError): - - @cmd - @A.operation(cmd=True) - def op2(job): - pass - - @pytest.mark.filterwarnings("ignore:@flow.with_job") - def test_with_job_decorator_with_with_job_argument(self): - class A(FlowProject): - pass - - with pytest.raises(FlowProjectDefinitionError): - - @A.operation(with_job=True) - @with_job - def op1(job): - pass - - with pytest.raises(FlowProjectDefinitionError): - - @with_job - @A.operation(with_job=True) - def op2(job): - pass - - @pytest.mark.filterwarnings("ignore:@flow.with_job") - @pytest.mark.filterwarnings("ignore:@flow.cmd") - def test_cmd_with_job_invalid_ordering(self): - class A(FlowProject): - pass - - with pytest.raises(FlowProjectDefinitionError): - - @A.operation(cmd=True) - @with_job - def op1(job): - pass - - with pytest.raises(FlowProjectDefinitionError): - - @cmd - @A.operation(with_job=True) - def op4(job): - pass - - with pytest.raises(FlowProjectDefinitionError): - - @A.operation - @cmd - @with_job - def op5(job): - pass - def test_with_job_works_with_cmd(self): class A(FlowProject): pass @@ -772,39 +653,6 @@ def b(job): not callable(value) for value in submit_job_operation.directives.values() ) - @pytest.mark.filterwarnings("ignore:@FlowProject.operation.with_directives") - def test_operation_with_directives(self): - class A(FlowProject): - pass - - @A.operation.with_directives({"executable": "python3", "nranks": 4}) - def test_context(job): - return "exit 1" - - project = self.mock_project(A) - for job in project: - for next_op in project._next_operations([(job,)]): - assert next_op.directives["executable"] == "python3" - assert next_op.directives["nranks"] == 4 - break - - def test_old_directives_decorator(self): - class A(FlowProject): - pass - - # TODO: Add deprecation warning context manager in v0.15 - @A.operation - @directives(executable=lambda job: f"mpirun -np {job.doc.np} python") - def test_context(job): - return "exit 1" - - project = self.mock_project(A) - for job in project: - job.doc.np = 3 - for next_op in project._next_operations([(job,)]): - assert "mpirun -np 3 python" in next_op.cmd - break - def test_copy_conditions(self): class A(FlowProject): pass @@ -1888,8 +1736,7 @@ def op2(job): pass @group - @A.operation - @flow.directives(nranks=2) + @A.operation(directives={"nranks": 2}) def op3(job): pass @@ -1920,8 +1767,7 @@ def op2(job): pass @group - @A.operation - @flow.directives(nranks=2) + @A.operation(directives={"nranks": 2}) def op3(job): pass @@ -2518,7 +2364,7 @@ def test_fail(self, project, job, operation_name): class TestHooksCmd(TestHooksBase): - # Tests hook decorators for a job operation with the @cmd decorator + # Tests hook decorators for a job operation with the cmd keyword argument error_message = 42 @pytest.fixture(params=["base_cmd"]) @@ -2551,7 +2397,7 @@ def operation_name(self, request): class TestHooksInstallCmd(TestHooksCmd, TestHooksInstallSetUp): - # Tests project-wide hooks on job operations with the @cmd decorator. + # Tests project-wide hooks on job operations with the cmd keyword argument. # Job operations are with or without operation level hooks # Check job document for keys from installed, project-wide hooks @@ -2576,7 +2422,7 @@ def operation_name(self, request): class TestHooksInstallCmdWithDecorators(TestHooksCmd, TestHooksInstallSetUp): # Tests if project-wide hooks interfere with operation level hooks - # in job operations with the @cmd decorator + # in job operations with the cmd keyword argument @pytest.fixture() def operation_name(self): return "base_cmd"