Repository: iterative/dvc

Issue:
Implement `--no-exec` option for `import-url` command
`dvc import-url` creates new `.dvc` file, just as `dvc run`. Sometimes files which would be imported are already present locally and it's quite inconvenient that they should be downloaded again in order to create a pipeline step.

Because of that it would be great to add `--no-exec` option: we create pipeline step, then use `dvc commit` to update its md5 with already downloaded file.

Discussion:

Patch:
diff --git a/dvc/command/imp_url.py b/dvc/command/imp_url.py
index 18591891b..aaed48a34 100644
--- a/dvc/command/imp_url.py
+++ b/dvc/command/imp_url.py
@@ -12,7 +12,10 @@ class CmdImportUrl(CmdBase):
     def run(self):
         try:
             self.repo.imp_url(
-                self.args.url, out=self.args.out, fname=self.args.file
+                self.args.url,
+                out=self.args.out,
+                fname=self.args.file,
+                no_exec=self.args.no_exec,
             )
         except DvcException:
             logger.exception(
@@ -66,4 +69,10 @@ def add_parser(subparsers, parent_parser):
         metavar="<filename>",
         choices=completion.Optional.DIR,
     )
+    import_parser.add_argument(
+        "--no-exec",
+        action="store_true",
+        default=False,
+        help="Only create stage file without actually download it.",
+    )
     import_parser.set_defaults(func=CmdImportUrl)
diff --git a/dvc/repo/imp_url.py b/dvc/repo/imp_url.py
index b6aabfefc..c20e57e65 100644
--- a/dvc/repo/imp_url.py
+++ b/dvc/repo/imp_url.py
@@ -10,7 +10,9 @@ from . import locked
 
 @locked
 @scm_context
-def imp_url(self, url, out=None, fname=None, erepo=None, frozen=True):
+def imp_url(
+    self, url, out=None, fname=None, erepo=None, frozen=True, no_exec=False
+):
     from dvc.dvcfile import Dvcfile
     from dvc.stage import Stage, create_stage
 
@@ -46,7 +48,10 @@ def imp_url(self, url, out=None, fname=None, erepo=None, frozen=True):
     except OutputDuplicationError as exc:
         raise OutputDuplicationError(exc.output, set(exc.stages) - {stage})
 
-    stage.run()
+    if no_exec:
+        stage.ignore_outs()
+    else:
+        stage.run()
 
     stage.frozen = frozen
 


Test patch:
diff --git a/tests/func/test_import_url.py b/tests/func/test_import_url.py
index 52e7edd2f..c5b8db8f9 100644
--- a/tests/func/test_import_url.py
+++ b/tests/func/test_import_url.py
@@ -103,3 +103,12 @@ def test_import_stage_accompanies_target(tmp_dir, dvc, erepo_dir):
 def test_import_url_nonexistent(dvc, erepo_dir):
     with pytest.raises(DependencyDoesNotExistError):
         dvc.imp_url(os.fspath(erepo_dir / "non-existent"))
+
+
+def test_import_url_with_no_exec(tmp_dir, dvc, erepo_dir):
+    tmp_dir.gen({"data_dir": {"file": "file content"}})
+    src = os.path.join("data_dir", "file")
+
+    dvc.imp_url(src, ".", no_exec=True)
+    dst = tmp_dir / "file"
+    assert not dst.exists()
diff --git a/tests/unit/command/test_imp_url.py b/tests/unit/command/test_imp_url.py
index 8e04d1268..6b5b5f784 100644
--- a/tests/unit/command/test_imp_url.py
+++ b/tests/unit/command/test_imp_url.py
@@ -14,7 +14,7 @@ def test_import_url(mocker):
 
     assert cmd.run() == 0
 
-    m.assert_called_once_with("src", out="out", fname="file")
+    m.assert_called_once_with("src", out="out", fname="file", no_exec=False)
 
 
 def test_failed_import_url(mocker, caplog):
@@ -31,3 +31,16 @@ def test_failed_import_url(mocker, caplog):
             "adding it with `dvc add`."
         )
         assert expected_error in caplog.text
+
+
+def test_import_url_no_exec(mocker):
+    cli_args = parse_args(
+        ["import-url", "--no-exec", "src", "out", "--file", "file"]
+    )
+
+    cmd = cli_args.func(cli_args)
+    m = mocker.patch.object(cmd.repo, "imp_url", autospec=True)
+
+    assert cmd.run() == 0
+
+    m.assert_called_once_with("src", out="out", fname="file", no_exec=True)