Repository: mlflow/mlflow

Issue:
Reintroduces the parent_run_id argument to MlflowClient.create_run
## What changes are proposed in this pull request?
 
Reintroduces the `parent_run_id` argument to `MlflowClient.create_run`. In https://github.com/mlflow/mlflow/pull/978 we removed this argument, which broke some third-party MLflow integrations. We ultimately do want to make the change in #978 with MLflow 1.0 (i.e., remove the `parent_run_id` argument) - this PR just aims to defer making that change till the 1.0 release.
 
## How is this patch tested?
 
Existing unit tests verify that codepaths that call `create_run` and pass parent run ID as a tag still work
Added new unit tests verifying that passing parent_run_id explicitly to `create_run` works.
 
## Release Notes
 
### Is this a user-facing change? 

[ ] No. Add the `rn/none` label, then you can skip the rest of this section.
[x] Yes. Give a description of this change to be included in the release notes for MLflow users.
 
[Python API] Reintroduces the parent_run_id argument to MlflowClient.create_run() to avoid making a breaking change to the create_run API. We will ultimately make this breaking change & remove the parent_run_id argument in MLflow 1.0.
 
### What component(s) does this PR affect?
 
- [ ] UI
- [ ] CLI 
- [ ] API 
- [ ] REST-API 
- [ ] Examples 
- [ ] Docs
- [x] Tracking
- [ ] Projects 
- [ ] Artifacts 
- [ ] Models 
- [ ] Scoring 
- [ ] Serving
- [ ] R
- [ ] Java
- [x] Python

### Please add one label to the PR so it can be classified correctly in the release notes. Options:
 
* `rn/breaking-change` - The PR will be mentioned in the "Breaking Changes" section
* `rn/none` - No description will be included. The PR will be mentioned just by the PR number in the "Small Bugfixes and Documentation Updates" section
* `rn/feature` - A new user-facing feature worth mentioning in the release notes
* `rn/bug-fix` - A user-facing bug fix worth mentioning in the release notes
* `rn/documentation` - A user-facing documentation change worth mentioning in the release notes



Discussion:
# [Codecov](https://codecov.io/gh/mlflow/mlflow/pull/1137?src=pr&el=h1) Report
> Merging [#1137](https://codecov.io/gh/mlflow/mlflow/pull/1137?src=pr&el=desc) into [master](https://codecov.io/gh/mlflow/mlflow/commit/dbd1bcffce4b7e2c76ec3578e187601576e3f26d?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/mlflow/mlflow/pull/1137/graphs/tree.svg?width=650&token=WLpEIkLv2r&height=150&src=pr)](https://codecov.io/gh/mlflow/mlflow/pull/1137?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #1137   +/-   ##
=======================================
  Coverage   70.57%   70.57%           
=======================================
  Files          23       23           
  Lines         999      999           
=======================================
  Hits          705      705           
  Misses        294      294
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/mlflow/mlflow/pull/1137?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mlflow/mlflow/pull/1137?src=pr&el=footer). Last update [dbd1bcf...bd16de5](https://codecov.io/gh/mlflow/mlflow/pull/1137?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Resolving patch:
diff --git a/mlflow/tracking/client.py b/mlflow/tracking/client.py
--- a/mlflow/tracking/client.py
+++ b/mlflow/tracking/client.py
@@ -52,7 +52,8 @@ def get_run(self, run_id):
         _validate_run_id(run_id)
         return self.store.get_run(run_id)
 
-    def create_run(self, experiment_id, user_id=None, run_name=None, start_time=None, tags=None):
+    def create_run(self, experiment_id, user_id=None, run_name=None, start_time=None,
+                   parent_run_id=None, tags=None):
         """
         Create a :py:class:`mlflow.entities.Run` object that can be associated with
         metrics, parameters, artifacts, etc.
@@ -62,6 +63,8 @@ def create_run(self, experiment_id, user_id=None, run_name=None, start_time=None
 
         :param user_id: If not provided, use the current user as a default.
         :param start_time: If not provided, use the current timestamp.
+        :param parent_run_id Optional parent run ID - takes precedence over parent run ID included
+                             in the `tags` argument.
         :param tags: A dictionary of key-value pairs that are converted into
                      :py:class:`mlflow.entities.RunTag` objects.
         :return: :py:class:`mlflow.entities.Run` that was created.
@@ -72,7 +75,8 @@ def create_run(self, experiment_id, user_id=None, run_name=None, start_time=None
         # Extract run attributes from tags
         # This logic is temporary; by the 1.0 release, this information will only be stored in tags
         # and will not be available as attributes of the run
-        parent_run_id = tags.get(MLFLOW_PARENT_RUN_ID)
+        final_parent_run_id =\
+            tags.get(MLFLOW_PARENT_RUN_ID) if parent_run_id is None else parent_run_id
         source_name = tags.get(MLFLOW_SOURCE_NAME, "Python Application")
         source_version = tags.get(MLFLOW_GIT_COMMIT)
         entry_point_name = tags.get(MLFLOW_PROJECT_ENTRY_POINT)
@@ -90,7 +94,7 @@ def create_run(self, experiment_id, user_id=None, run_name=None, start_time=None
             start_time=start_time or int(time.time() * 1000),
             tags=[RunTag(key, value) for (key, value) in iteritems(tags)],
             # The below arguments remain set for backwards compatability:
-            parent_run_id=parent_run_id,
+            parent_run_id=final_parent_run_id,
             source_type=source_type,
             source_name=source_name,
             entry_point_name=entry_point_name,


Test patch:
diff --git a/tests/tracking/test_client.py b/tests/tracking/test_client.py
--- a/tests/tracking/test_client.py
+++ b/tests/tracking/test_client.py
@@ -67,7 +67,7 @@ def test_client_create_run_overrides(mock_store):
         "other-key": "other-value"
     }
 
-    MlflowClient().create_run(experiment_id, user_id, run_name, start_time, tags)
+    MlflowClient().create_run(experiment_id, user_id, run_name, start_time, None, tags)
 
     mock_store.create_run.assert_called_once_with(
         experiment_id=experiment_id,
@@ -81,6 +81,21 @@ def test_client_create_run_overrides(mock_store):
         entry_point_name=tags[MLFLOW_PROJECT_ENTRY_POINT],
         source_version=tags[MLFLOW_GIT_COMMIT]
     )
+    mock_store.reset_mock()
+    parent_run_id = "mock-parent-run-id"
+    MlflowClient().create_run(experiment_id, user_id, run_name, start_time, parent_run_id, tags)
+    mock_store.create_run.assert_called_once_with(
+        experiment_id=experiment_id,
+        user_id=user_id,
+        run_name=run_name,
+        start_time=start_time,
+        tags=[RunTag(key, value) for key, value in tags.items()],
+        parent_run_id=parent_run_id,
+        source_type=SourceType.JOB,
+        source_name=tags[MLFLOW_SOURCE_NAME],
+        entry_point_name=tags[MLFLOW_PROJECT_ENTRY_POINT],
+        source_version=tags[MLFLOW_GIT_COMMIT]
+    )
 
 
 def test_client_search_runs(mock_store, mock_search_filter):
diff --git a/tests/tracking/test_rest_tracking.py b/tests/tracking/test_rest_tracking.py
--- a/tests/tracking/test_rest_tracking.py
+++ b/tests/tracking/test_rest_tracking.py
@@ -164,7 +164,8 @@ def test_rename_experiment_cli(mlflow_client, cli_env):
     assert mlflow_client.get_experiment(experiment_id).name == good_experiment_name
 
 
-def test_create_run_all_args(mlflow_client):
+@pytest.mark.parametrize("parent_run_id_kwarg", [None, "my-parent-id"])
+def test_create_run_all_args(mlflow_client, parent_run_id_kwarg):
     source_name = "Hello"
     entry_point = "entry"
     source_version = "abc"
@@ -180,9 +181,11 @@ def test_create_run_all_args(mlflow_client):
             MLFLOW_PARENT_RUN_ID: "7",
             "my": "tag",
             "other": "tag",
-        }
+        },
+        "parent_run_id": parent_run_id_kwarg
     }
-    experiment_id = mlflow_client.create_experiment('Run A Lot')
+    experiment_id = mlflow_client.create_experiment('Run A Lot (parent_run_id=%s)'
+                                                    % (parent_run_id_kwarg))
     created_run = mlflow_client.create_run(experiment_id, **create_run_kwargs)
     run_id = created_run.info.run_uuid
     print("Run id=%s" % run_id)
@@ -198,9 +201,9 @@ def test_create_run_all_args(mlflow_client):
     assert run.info.source_version == source_version
     actual_tags = {t.key: t.value for t in run.data.tags}
     for tag in create_run_kwargs["tags"]:
-        assert tag in actual_tags
-    assert actual_tags.get(MLFLOW_RUN_NAME) == create_run_kwargs["run_name"]
-
+        assert tag in run.data.tags
+    assert run.data.tags.get(MLFLOW_RUN_NAME) == create_run_kwargs["run_name"]
+    assert run.data.tags.get(MLFLOW_PARENT_RUN_ID) == parent_run_id_kwarg or "7"
     assert mlflow_client.list_run_infos(experiment_id) == [run.info]