Repository: iterative/dvc

Issue:
status: locked callback stages 
https://github.com/iterative/dvc/issues/895#issuecomment-442749438

Need to check if the callback stage is locked https://github.com/iterative/dvc/blob/0.21.2/dvc/stage.py#L163 and if it is locked, not report it as changed, unless outputs are changed.


Discussion:

Resolving patch:
diff --git a/dvc/stage.py b/dvc/stage.py
--- a/dvc/stage.py
+++ b/dvc/stage.py
@@ -152,6 +152,23 @@ def is_import(self):
                len(self.deps) == 1 and \
                len(self.outs) == 1
 
+    def _changed_deps(self, print_info, log):
+        if self.is_callback:
+            msg = "Dvc file '{}' is a 'callback' stage (has a command and " \
+                  "no dependencies) and thus always considered as changed."
+            self.project.logger.warn(msg.format(self.relpath))
+            return True
+
+        for dep in self.deps:
+            if not dep.changed():
+                continue
+            if print_info:
+                msg = "Dependency '{}' of '{}' changed."
+                log(msg.format(dep, self.relpath))
+            return True
+
+        return False
+
     def changed(self, print_info=False):
         ret = False
 
@@ -160,20 +177,8 @@ def changed(self, print_info=False):
         else:
             log = self.project.logger.debug
 
-        if self.is_callback:
-            msg = "Dvc file '{}' is a 'callback' stage (has a command and " \
-                  "no dependencies) and thus always considered as changed."
-            self.project.logger.warn(msg.format(self.relpath))
-            ret = True
-
         if not self.locked:
-            for dep in self.deps:
-                if not dep.changed():
-                    continue
-                if print_info:
-                    msg = "Dependency '{}' of '{}' changed."
-                    log(msg.format(dep, self.relpath))
-                ret = True
+            ret = self._changed_deps(print_info, log)
 
         for out in self.outs:
             if not out.changed():


Test patch:
diff --git a/tests/test_repro.py b/tests/test_repro.py
--- a/tests/test_repro.py
+++ b/tests/test_repro.py
@@ -343,6 +343,34 @@ def test_non_existing(self):
         self.assertNotEqual(ret, 0)
 
 
+class TestReproLockedCallback(TestDvc):
+    def test(self):
+        file1 = 'file1'
+        file1_stage = file1 + '.dvc'
+        # NOTE: purposefully not specifying dependencies
+        # to create a callbacs stage.
+        stage = self.dvc.run(fname=file1_stage,
+                             outs=[file1],
+                             cmd='python {} {} {}'.format(self.CODE,
+                                                          self.FOO,
+                                                          file1))
+        self.assertTrue(stage is not None)
+        self.assertEqual(stage.relpath, file1_stage)
+
+        stages = self.dvc.reproduce(file1_stage)
+        self.assertEqual(len(stages), 1)
+
+        self.dvc.lock_stage(file1_stage)
+
+        stages = self.dvc.reproduce(file1_stage)
+        self.assertEqual(len(stages), 0)
+
+        self.dvc.lock_stage(file1_stage, unlock=True)
+
+        stages = self.dvc.reproduce(file1_stage)
+        self.assertEqual(len(stages), 1)
+
+
 class TestReproPhony(TestReproChangedData):
     def test(self):
         stage = self.dvc.run(deps=[self.file1])