Repository: wandb/wandb
Pull request: Improve `wandb.init(settings=)` to handle `Settings` object similarly to `dict` parameter
Fixes #3505

Description
-----------
Treat a `wandb.Settings` object passed to `wandb.init()` similar to how a passed `dict` is treated. Will now detect settings that differ from defaults and apply them using `Source.INIT`, as this is probably that the user would expect when doing `wandb.init(settings=wandb.Settings(...))`.

Also deprecated `config_include_keys` and `config_exclude_keys` `kwargs` in `wandb.init(...)` bc saw a todo in the code next to what I was touching and couldn't ignore it :)

Testing
-------
Hopefully fixed a couple tests + improved robustness in `tests/test_redir.py` that would flake out at times.


Problem statement:
[CLI]: _disable_stats doesn't work
### Describe the bug

<!--- Description of the issue below  -->

`_disable_stats` doesn't work. `wandb.init(settings=wandb.Settings(_disable_stats=True))` It still sends stats to WANDB, which in turn leads to BSOD due to incompatibility with the old PYNVML dependency in the vendor folder in very specific scenarios.

<!--- A minimal code snippet between the quotes below  -->
```
wandb.init(settings=wandb.Settings(_disable_stats=True))
```

<!--- A full traceback of the exception in the quotes below -->
None


### Additional Files

_No response_

### Environment

WandB version:
0.12.14

OS:
Windows 10

Python version:
 3.9.10

### Additional Context

_No response_

Hints from issue discussion:
Hey @CosmicHazel, many thanks for reporting this! 
This should be fixed in https://github.com/wandb/client/pull/3510, but for now, instead of passing a `wandb.Settings` object to `init()`, please try passing a simple dict, as in `wandb.init(settings=dict(_disable_stats=True))`. 
Also, could you please elaborate on the dependency incompatibilities that you are observing?

Commit message: rm unnecessary sleep statements in updated tests

Patch to review:
diff --git a/tests/conftest.py b/tests/conftest.py
index d22ae681a6e..76480503ae4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -225,9 +225,11 @@ def test_settings(test_dir, mocker, live_mock_server):
     wandb_dir = os.path.join(test_dir, "wandb")
     mkdir_exists_ok(wandb_dir)
     # root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+    time_stamp: float = time.time()
+    datetime_now: datetime.datetime = datetime.datetime.fromtimestamp(time_stamp)
     settings = wandb.Settings(
-        _start_datetime=datetime.datetime.now(),
-        _start_time=time.time(),
+        _start_datetime=datetime_now,
+        _start_time=time_stamp,
         api_key=DUMMY_API_KEY,
         base_url=live_mock_server.base_url,
         console="off",
diff --git a/tests/test_redir.py b/tests/test_redir.py
index 842e9c7b83d..8dea99ec2cb 100644
--- a/tests/test_redir.py
+++ b/tests/test_redir.py
@@ -1,10 +1,10 @@
 """redirect tests"""
 
-import copy
 import os
 import re
 import sys
 import time
+from unittest import mock
 
 import numpy as np
 import pytest
@@ -12,6 +12,8 @@
 
 import wandb
 from wandb.cli import cli
+import wandb.sdk.lib.redirect
+import wandb.util
 
 
 impls = [wandb.wandb_sdk.lib.redirect.StreamWrapper]
@@ -218,11 +220,10 @@ def test_print_torch_model(cls, capfd):
 @pytest.mark.parametrize("console", console_modes)
 def test_run_with_console_redirect(test_settings, capfd, console):
     with capfd.disabled():
-        local_settings = copy.copy(test_settings)
-        local_settings.update(
+        test_settings.update(
             console=console, source=wandb.sdk.wandb_settings.Source.INIT
         )
-        run = wandb.init(settings=local_settings)
+        run = wandb.init(settings=test_settings)
 
         print(np.random.randint(64, size=(40, 40, 40, 40)))
 
@@ -236,16 +237,16 @@ def test_run_with_console_redirect(test_settings, capfd, console):
 
 
 @pytest.mark.parametrize("console", console_modes)
+@pytest.mark.timeout(300)
 def test_offline_compression(test_settings, capfd, runner, console):
     with capfd.disabled():
-        local_settings = copy.copy(test_settings)
-        local_settings.update(
+        test_settings.update(
             mode="offline",
             console=console,
-            source=wandb.sdk.wandb_settings.Source.INIT,
         )
 
-        run = wandb.init(settings=local_settings)
+        run = wandb.init(settings=test_settings)
+        run_dir, run_id = run.dir, run.id
 
         for _ in tqdm.tqdm(range(100), ncols=139, ascii=" 123456789#"):
             time.sleep(0.05)
@@ -259,10 +260,10 @@ def test_offline_compression(test_settings, capfd, runner, console):
         print("\x1b[A\r\x1b[J\x1b[A\r\x1b[1J")
 
         time.sleep(2)
-
         run.finish()
+
         binary_log_file = (
-            os.path.join(os.path.dirname(run.dir), "run-" + run.id) + ".wandb"
+            os.path.join(os.path.dirname(run_dir), "run-" + run_id) + ".wandb"
         )
         binary_log = runner.invoke(
             cli.sync, ["--view", "--verbose", binary_log_file]
@@ -283,47 +284,51 @@ def test_offline_compression(test_settings, capfd, runner, console):
 
 @pytest.mark.parametrize("console", console_modes)
 @pytest.mark.parametrize("numpy", [True, False])
-@pytest.mark.timeout(120)
+@pytest.mark.timeout(300)
 def test_very_long_output(test_settings, capfd, runner, console, numpy):
     # https://wandb.atlassian.net/browse/WB-5437
-    local_settings = copy.copy(test_settings)
-    local_settings.update(
+    test_settings.update(
         mode="offline",
         console=console,
-        source=wandb.sdk.wandb_settings.Source.INIT,
+        run_id=wandb.util.generate_id(),
     )
 
     with capfd.disabled():
-        if not numpy:
-            wandb.wandb_sdk.lib.redirect.np = wandb.wandb_sdk.lib.redirect._Numpy()
-        try:
-            run = wandb.init(settings=local_settings)
+        with mock.patch.object(
+            wandb.wandb_sdk.lib.redirect,
+            "np",
+            wandb.sdk.lib.redirect._Numpy() if not numpy else np,
+        ):
+            run = wandb.init(settings=test_settings)
+            run_dir, run_id = run.dir, run.id
             print("LOG" * 1000000)
             print("\x1b[31m\x1b[40m\x1b[1mHello\x01\x1b[22m\x1b[39m" * 100)
             print("===finish===")
             time.sleep(3)
             run.finish()
+
             binary_log_file = (
-                os.path.join(os.path.dirname(run.dir), "run-" + run.id) + ".wandb"
+                os.path.join(os.path.dirname(run_dir), "run-" + run_id) + ".wandb"
             )
             binary_log = runner.invoke(
                 cli.sync, ["--view", "--verbose", binary_log_file]
             ).stdout
+
             assert "\\033[31m\\033[40m\\033[1mHello" in binary_log
             assert binary_log.count("LOG") == 1000000
             assert "===finish===" in binary_log
-        finally:
-            wandb.wandb_sdk.lib.redirect.np = np
 
 
 @pytest.mark.parametrize("console", console_modes)
 def test_no_numpy(test_settings, capfd, runner, console):
-    local_settings = copy.copy(test_settings)
-    local_settings.update(console=console, source=wandb.sdk.wandb_settings.Source.INIT)
+    test_settings.update(console=console, source=wandb.sdk.wandb_settings.Source.INIT)
     with capfd.disabled():
-        wandb.wandb_sdk.lib.redirect.np = wandb.wandb_sdk.lib.redirect._Numpy()
-        try:
-            run = wandb.init(settings=local_settings)
+        with mock.patch.object(
+            wandb.wandb_sdk.lib.redirect,
+            "np",
+            wandb.sdk.lib.redirect._Numpy(),
+        ):
+            run = wandb.init(settings=test_settings)
             print("\x1b[31m\x1b[40m\x1b[1mHello\x01\x1b[22m\x1b[39m")
             run.finish()
             binary_log_file = (
@@ -332,16 +337,13 @@ def test_no_numpy(test_settings, capfd, runner, console):
             binary_log = runner.invoke(
                 cli.sync, ["--view", "--verbose", binary_log_file]
             ).stdout
-        finally:
-            wandb.wandb_sdk.lib.redirect.np = np
 
 
 @pytest.mark.parametrize("console", console_modes)
 def test_memory_leak2(test_settings, capfd, runner, console):
-    local_settings = copy.copy(test_settings)
-    local_settings.update(console=console, source=wandb.sdk.wandb_settings.Source.INIT)
+    test_settings.update(console=console, source=wandb.sdk.wandb_settings.Source.INIT)
     with capfd.disabled():
-        run = wandb.init(settings=local_settings)
+        run = wandb.init(settings=test_settings)
         for i in range(1000):
             print("ABCDEFGH")
         time.sleep(3)
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
index 31e4b624bb3..f224a431886 100644
--- a/tests/utils/__init__.py
+++ b/tests/utils/__init__.py
@@ -18,6 +18,7 @@
     mock_sagemaker,
     mock_k8s,
     assert_deep_lists_equal,
+    retry,
 )
 
 __all__ = [
@@ -41,4 +42,5 @@
     "matplotlib_with_image",
     "matplotlib_without_image",
     "InjectRequests",
+    "retry",
 ]
diff --git a/tests/utils/utils.py b/tests/utils/utils.py
index d95b1ab48cf..14c6442fccd 100644
--- a/tests/utils/utils.py
+++ b/tests/utils/utils.py
@@ -1,8 +1,10 @@
+import functools
 import io
 import os
 import shutil
 import socket
-from typing import Union
+import time
+from typing import Any, Callable, Union
 
 _mock_module = None
 
@@ -165,3 +167,26 @@ def magic(path, *args, **kwargs):
     env["KUBERNETES_PORT_443_TCP_PORT"] = "123"
     env["HOSTNAME"] = "test"
     return env
+
+
+def retry(retries: int = 3, delay: int = 1) -> Callable:
+    """
+    A simple retry decorator
+    """
+
+    def decorator(func: Callable) -> Callable:
+        @functools.wraps(func)
+        def wrapper(*args: Any, **kwargs: Any):
+            for i in range(retries):
+                try:
+                    return func(*args, **kwargs)
+                except Exception:
+                    print(
+                        f"`{func.__name__}` call failed, retrying in {delay} seconds..."
+                    )
+                    time.sleep(delay)
+            return func(*args, **kwargs)
+
+        return wrapper
+
+    return decorator
diff --git a/tests/wandb_integration_test.py b/tests/wandb_integration_test.py
index c9b53869dea..e0e3a210816 100644
--- a/tests/wandb_integration_test.py
+++ b/tests/wandb_integration_test.py
@@ -155,6 +155,7 @@ def test_include_exclude_config_keys(runner, live_mock_server, test_settings):
         assert "bar" not in run.config
         run.finish()
 
+        test_settings._start_run()  # update timestamp
         run = wandb.init(
             reinit=True,
             resume=True,
@@ -167,6 +168,7 @@ def test_include_exclude_config_keys(runner, live_mock_server, test_settings):
         assert "baz" not in run.config
         run.finish()
 
+        test_settings._start_run()  # update timestamp
         with pytest.raises(wandb.errors.UsageError):
             wandb.init(
                 reinit=True,
diff --git a/tests/wandb_run_test.py b/tests/wandb_run_test.py
index 28d8469bceb..9066b0fd510 100644
--- a/tests/wandb_run_test.py
+++ b/tests/wandb_run_test.py
@@ -317,15 +317,21 @@ def test_use_artifact_offline(live_mock_server, test_settings):
     run.finish()
 
 
-def test_run_urls(test_settings):
+@pytest.mark.skip(
+    reason=(
+        "mock server enforces entity=mock_server_entity and project=test,"
+        "this should only work on a live server, or mock_server would need customization."
+    )
+)
+def test_run_urls(live_mock_server, test_settings):
     base_url = "https://my.cool.site.com"
     entity = "me"
-    project = "test"
-    test_settings.update(dict(base_url=base_url, entity=entity, project=project))
+    project = "lol"
+    test_settings.update(base_url=base_url, entity=entity, project=project)
     run = wandb.init(settings=test_settings)
     assert run.get_project_url() == f"{base_url}/{entity}/{project}"
     assert run.get_url() == f"{base_url}/{entity}/{project}/runs/{run.id}"
-    run.finish
+    run.finish()
 
 
 def test_use_artifact(live_mock_server, test_settings):
@@ -647,7 +653,10 @@ def test_wandb_artifact_config_update(
 
 
 def test_deprecated_feature_telemetry(live_mock_server, test_settings, parse_ctx):
-    run = wandb.init(settings=test_settings)
+    run = wandb.init(
+        config_include_keys=("lol",),
+        settings=test_settings,
+    )
     # use deprecated features
     deprecated_features = [
         run.mode,
@@ -658,11 +667,13 @@ def test_deprecated_feature_telemetry(live_mock_server, test_settings, parse_ctx
     telemetry = ctx_util.telemetry
     # TelemetryRecord field 10 is Deprecated,
     # whose fields 2-4 correspond to deprecated wandb.run features
+    # fields 7 & 8 are deprecated wandb.init kwargs
     telemetry_deprecated = telemetry.get("10", [])
     assert (
         (2 in telemetry_deprecated)
         and (3 in telemetry_deprecated)
         and (4 in telemetry_deprecated)
+        and (7 in telemetry_deprecated)
     )
     run.finish()
 
@@ -720,10 +731,23 @@ def test_settings_unexpected_args_telemetry(
         run.finish()
 
 
-def test_attach_same_process(test_settings):
+def test_attach_same_process(live_mock_server, test_settings):
     with mock.patch.dict("os.environ", WANDB_REQUIRE_SERVICE="True"):
         with pytest.raises(RuntimeError) as excinfo:
             run = wandb.init(settings=test_settings)
             new_run = pickle.loads(pickle.dumps(run))
             new_run.log({"a": 2})
     assert "attach in the same process is not supported" in str(excinfo.value)
+
+
+def test_init_with_settings(live_mock_server, test_settings):
+    # test that when calling `wandb.init(settings=wandb.Settings(...))`,
+    # the settings are passed with Source.INIT as the source
+    test_settings.update(_disable_stats=True)
+    run = wandb.init(settings=test_settings)
+    assert run.settings._disable_stats
+    assert (
+        run.settings.__dict__["_disable_stats"].source
+        == wandb_sdk.wandb_settings.Source.INIT
+    )
+    run.finish()
diff --git a/tests/wandb_settings_test.py b/tests/wandb_settings_test.py
index bdc6e2c1e6f..4c22bff3b2b 100644
--- a/tests/wandb_settings_test.py
+++ b/tests/wandb_settings_test.py
@@ -995,3 +995,13 @@ def test_local_api_key_validation():
 
     # ensure that base_url is applied first without causing an error in api_key validation
     wandb.Settings()._apply_settings(s)
+
+
+def test_run_urls():
+    base_url = "https://my.cool.site.com"
+    entity = "me"
+    project = "lol"
+    run_id = "123"
+    s = Settings(base_url=base_url, entity=entity, project=project, run_id=run_id)
+    assert s.project_url == f"{base_url}/{entity}/{project}"
+    assert s.run_url == f"{base_url}/{entity}/{project}/runs/{run_id}"
diff --git a/wandb/proto/wandb_deprecated.py b/wandb/proto/wandb_deprecated.py
index 60867660b8f..2fb43f97400 100644
--- a/wandb/proto/wandb_deprecated.py
+++ b/wandb/proto/wandb_deprecated.py
@@ -16,7 +16,9 @@
     "run__save_no_args",
     "run__join",
     "plots",
-    "run__log_sync"
+    "run__log_sync",
+    "init__config_include_keys",
+    "init__config_exclude_keys"
 ]
 
 
@@ -27,3 +29,5 @@ class Deprecated:
     run__join: DEPRECATED_FEATURES = "run__join"
     plots: DEPRECATED_FEATURES = "plots"
     run__log_sync: DEPRECATED_FEATURES = "run__log_sync"
+    init__config_include_keys: DEPRECATED_FEATURES = "init__config_include_keys"
+    init__config_exclude_keys: DEPRECATED_FEATURES = "init__config_exclude_keys"
diff --git a/wandb/proto/wandb_telemetry.proto b/wandb/proto/wandb_telemetry.proto
index e2cea13a016..dc7c5daecd7 100644
--- a/wandb/proto/wandb_telemetry.proto
+++ b/wandb/proto/wandb_telemetry.proto
@@ -147,6 +147,8 @@ message Deprecated {
   bool run__join = 4; // wandb.run.join() called
   bool plots = 5; // wandb.plots.* called
   bool run__log_sync = 6; // wandb.run.log(sync=...) called
+  bool init__config_include_keys = 7 ; // wandb.init(config_include_keys=...) called
+  bool init__config_exclude_keys = 8 ; // wandb.init(config_exclude_keys=...) called
 }
 
 message Issues {
diff --git a/wandb/proto/wandb_telemetry_pb2.py b/wandb/proto/wandb_telemetry_pb2.py
index 857f583f008..e94ed35e45b 100644
--- a/wandb/proto/wandb_telemetry_pb2.py
+++ b/wandb/proto/wandb_telemetry_pb2.py
@@ -20,7 +20,7 @@
   syntax='proto3',
   serialized_options=None,
   create_key=_descriptor._internal_create_key,
-  serialized_pb=b'\n!wandb/proto/wandb_telemetry.proto\x12\x0ewandb_internal\x1a\x1cwandb/proto/wandb_base.proto\"\xb3\x03\n\x0fTelemetryRecord\x12-\n\x0cimports_init\x18\x01 \x01(\x0b\x32\x17.wandb_internal.Imports\x12/\n\x0eimports_finish\x18\x02 \x01(\x0b\x32\x17.wandb_internal.Imports\x12(\n\x07\x66\x65\x61ture\x18\x03 \x01(\x0b\x32\x17.wandb_internal.Feature\x12\x16\n\x0epython_version\x18\x04 \x01(\t\x12\x13\n\x0b\x63li_version\x18\x05 \x01(\t\x12\x1b\n\x13huggingface_version\x18\x06 \x01(\t\x12 \n\x03\x65nv\x18\x08 \x01(\x0b\x32\x13.wandb_internal.Env\x12%\n\x05label\x18\t \x01(\x0b\x32\x16.wandb_internal.Labels\x12.\n\ndeprecated\x18\n \x01(\x0b\x32\x1a.wandb_internal.Deprecated\x12&\n\x06issues\x18\x0b \x01(\x0b\x32\x16.wandb_internal.Issues\x12+\n\x05_info\x18\xc8\x01 \x01(\x0b\x32\x1b.wandb_internal._RecordInfo\"\x11\n\x0fTelemetryResult\"\xcc\x06\n\x07Imports\x12\r\n\x05torch\x18\x01 \x01(\x08\x12\r\n\x05keras\x18\x02 \x01(\x08\x12\x12\n\ntensorflow\x18\x03 \x01(\x08\x12\x0e\n\x06\x66\x61stai\x18\x04 \x01(\x08\x12\x0f\n\x07sklearn\x18\x05 \x01(\x08\x12\x0f\n\x07xgboost\x18\x06 \x01(\x08\x12\x10\n\x08\x63\x61tboost\x18\x07 \x01(\x08\x12\x10\n\x08lightgbm\x18\x08 \x01(\x08\x12\x19\n\x11pytorch_lightning\x18\t \x01(\x08\x12\x16\n\x0epytorch_ignite\x18\n \x01(\x08\x12 \n\x18transformers_huggingface\x18\x0b \x01(\x08\x12\x0b\n\x03jax\x18\x0c \x01(\x08\x12\x10\n\x08metaflow\x18\r \x01(\x08\x12\x10\n\x08\x61llennlp\x18\x0e \x01(\x08\x12\x11\n\tautogluon\x18\x0f \x01(\x08\x12\x11\n\tautokeras\x18\x10 \x01(\x08\x12\x10\n\x08\x63\x61talyst\x18\x12 \x01(\x08\x12\x10\n\x08\x64\x65\x65pchem\x18\x15 \x01(\x08\x12\x0f\n\x07\x64\x65\x65pctr\x18\x16 \x01(\x08\x12\x0f\n\x07pycaret\x18\x1c \x01(\x08\x12\x14\n\x0cpytorchvideo\x18\x1d \x01(\x08\x12\x0b\n\x03ray\x18\x1e \x01(\x08\x12\x1a\n\x12simpletransformers\x18\x1f \x01(\x08\x12\x0e\n\x06skorch\x18  \x01(\x08\x12\r\n\x05spacy\x18! \x01(\x08\x12\r\n\x05\x66lash\x18\" \x01(\x08\x12\x0e\n\x06optuna\x18# \x01(\x08\x12\x0f\n\x07recbole\x18$ \x01(\x08\x12\x0c\n\x04mmcv\x18% \x01(\x08\x12\r\n\x05mmdet\x18& \x01(\x08\x12\x11\n\ttorchdrug\x18\' \x01(\x08\x12\x11\n\ttorchtext\x18( \x01(\x08\x12\x13\n\x0btorchvision\x18) \x01(\x08\x12\r\n\x05\x65legy\x18* \x01(\x08\x12\x12\n\ndetectron2\x18+ \x01(\x08\x12\r\n\x05\x66lair\x18, \x01(\x08\x12\x0c\n\x04\x66lax\x18- \x01(\x08\x12\x0c\n\x04syft\x18. \x01(\x08\x12\x0b\n\x03TTS\x18/ \x01(\x08\x12\r\n\x05monai\x18\x30 \x01(\x08\x12\x17\n\x0fhuggingface_hub\x18\x31 \x01(\x08\x12\r\n\x05hydra\x18\x32 \x01(\x08\x12\x10\n\x08\x64\x61tasets\x18\x33 \x01(\x08\x12\x0e\n\x06sacred\x18\x34 \x01(\x08\x12\x0e\n\x06joblib\x18\x35 \x01(\x08\x12\x0c\n\x04\x64\x61sk\x18\x36 \x01(\x08\x12\x0f\n\x07\x61syncio\x18\x37 \x01(\x08\"\xae\x06\n\x07\x46\x65\x61ture\x12\r\n\x05watch\x18\x01 \x01(\x08\x12\x0e\n\x06\x66inish\x18\x02 \x01(\x08\x12\x0c\n\x04save\x18\x03 \x01(\x08\x12\x0f\n\x07offline\x18\x04 \x01(\x08\x12\x0f\n\x07resumed\x18\x05 \x01(\x08\x12\x0c\n\x04grpc\x18\x06 \x01(\x08\x12\x0e\n\x06metric\x18\x07 \x01(\x08\x12\r\n\x05keras\x18\x08 \x01(\x08\x12\x11\n\tsagemaker\x18\t \x01(\x08\x12\x1c\n\x14\x61rtifact_incremental\x18\n \x01(\x08\x12\x10\n\x08metaflow\x18\x0b \x01(\x08\x12\x0f\n\x07prodigy\x18\x0c \x01(\x08\x12\x15\n\rset_init_name\x18\r \x01(\x08\x12\x13\n\x0bset_init_id\x18\x0e \x01(\x08\x12\x15\n\rset_init_tags\x18\x0f \x01(\x08\x12\x17\n\x0fset_init_config\x18\x10 \x01(\x08\x12\x14\n\x0cset_run_name\x18\x11 \x01(\x08\x12\x14\n\x0cset_run_tags\x18\x12 \x01(\x08\x12\x17\n\x0fset_config_item\x18\x13 \x01(\x08\x12\x0e\n\x06launch\x18\x14 \x01(\x08\x12\x1c\n\x14torch_profiler_trace\x18\x15 \x01(\x08\x12\x0b\n\x03sb3\x18\x16 \x01(\x08\x12\x0f\n\x07service\x18\x17 \x01(\x08\x12\x17\n\x0finit_return_run\x18\x18 \x01(\x08\x12\x1f\n\x17lightgbm_wandb_callback\x18\x19 \x01(\x08\x12\x1c\n\x14lightgbm_log_summary\x18\x1a \x01(\x08\x12\x1f\n\x17\x63\x61tboost_wandb_callback\x18\x1b \x01(\x08\x12\x1c\n\x14\x63\x61tboost_log_summary\x18\x1c \x01(\x08\x12\x17\n\x0ftensorboard_log\x18\x1d \x01(\x08\x12\x16\n\x0e\x65stimator_hook\x18\x1e \x01(\x08\x12\x1e\n\x16xgboost_wandb_callback\x18\x1f \x01(\x08\x12\"\n\x1axgboost_old_wandb_callback\x18  \x01(\x08\x12\x0e\n\x06\x61ttach\x18! \x01(\x08\x12\x19\n\x11tensorboard_patch\x18\" \x01(\x08\x12\x18\n\x10tensorboard_sync\x18# \x01(\x08\x12\x15\n\rkfp_wandb_log\x18$ \x01(\x08\"\xb2\x01\n\x03\x45nv\x12\x0f\n\x07jupyter\x18\x01 \x01(\x08\x12\x0e\n\x06kaggle\x18\x02 \x01(\x08\x12\x0f\n\x07windows\x18\x03 \x01(\x08\x12\x0e\n\x06m1_gpu\x18\x04 \x01(\x08\x12\x13\n\x0bstart_spawn\x18\x05 \x01(\x08\x12\x12\n\nstart_fork\x18\x06 \x01(\x08\x12\x18\n\x10start_forkserver\x18\x07 \x01(\x08\x12\x14\n\x0cstart_thread\x18\x08 \x01(\x08\x12\x10\n\x08maybe_mp\x18\t \x01(\x08\"H\n\x06Labels\x12\x13\n\x0b\x63ode_string\x18\x01 \x01(\t\x12\x13\n\x0brepo_string\x18\x02 \x01(\t\x12\x14\n\x0c\x63ode_version\x18\x03 \x01(\t\"\x96\x01\n\nDeprecated\x12!\n\x19keras_callback__data_type\x18\x01 \x01(\x08\x12\x11\n\trun__mode\x18\x02 \x01(\x08\x12\x19\n\x11run__save_no_args\x18\x03 \x01(\x08\x12\x11\n\trun__join\x18\x04 \x01(\x08\x12\r\n\x05plots\x18\x05 \x01(\x08\x12\x15\n\rrun__log_sync\x18\x06 \x01(\x08\"|\n\x06Issues\x12%\n\x1dsettings__validation_warnings\x18\x01 \x01(\x08\x12!\n\x19settings__unexpected_args\x18\x02 \x01(\x08\x12(\n settings__preprocessing_warnings\x18\x03 \x01(\x08\x62\x06proto3'
+  serialized_pb=b'\n!wandb/proto/wandb_telemetry.proto\x12\x0ewandb_internal\x1a\x1cwandb/proto/wandb_base.proto\"\xb3\x03\n\x0fTelemetryRecord\x12-\n\x0cimports_init\x18\x01 \x01(\x0b\x32\x17.wandb_internal.Imports\x12/\n\x0eimports_finish\x18\x02 \x01(\x0b\x32\x17.wandb_internal.Imports\x12(\n\x07\x66\x65\x61ture\x18\x03 \x01(\x0b\x32\x17.wandb_internal.Feature\x12\x16\n\x0epython_version\x18\x04 \x01(\t\x12\x13\n\x0b\x63li_version\x18\x05 \x01(\t\x12\x1b\n\x13huggingface_version\x18\x06 \x01(\t\x12 \n\x03\x65nv\x18\x08 \x01(\x0b\x32\x13.wandb_internal.Env\x12%\n\x05label\x18\t \x01(\x0b\x32\x16.wandb_internal.Labels\x12.\n\ndeprecated\x18\n \x01(\x0b\x32\x1a.wandb_internal.Deprecated\x12&\n\x06issues\x18\x0b \x01(\x0b\x32\x16.wandb_internal.Issues\x12+\n\x05_info\x18\xc8\x01 \x01(\x0b\x32\x1b.wandb_internal._RecordInfo\"\x11\n\x0fTelemetryResult\"\xcc\x06\n\x07Imports\x12\r\n\x05torch\x18\x01 \x01(\x08\x12\r\n\x05keras\x18\x02 \x01(\x08\x12\x12\n\ntensorflow\x18\x03 \x01(\x08\x12\x0e\n\x06\x66\x61stai\x18\x04 \x01(\x08\x12\x0f\n\x07sklearn\x18\x05 \x01(\x08\x12\x0f\n\x07xgboost\x18\x06 \x01(\x08\x12\x10\n\x08\x63\x61tboost\x18\x07 \x01(\x08\x12\x10\n\x08lightgbm\x18\x08 \x01(\x08\x12\x19\n\x11pytorch_lightning\x18\t \x01(\x08\x12\x16\n\x0epytorch_ignite\x18\n \x01(\x08\x12 \n\x18transformers_huggingface\x18\x0b \x01(\x08\x12\x0b\n\x03jax\x18\x0c \x01(\x08\x12\x10\n\x08metaflow\x18\r \x01(\x08\x12\x10\n\x08\x61llennlp\x18\x0e \x01(\x08\x12\x11\n\tautogluon\x18\x0f \x01(\x08\x12\x11\n\tautokeras\x18\x10 \x01(\x08\x12\x10\n\x08\x63\x61talyst\x18\x12 \x01(\x08\x12\x10\n\x08\x64\x65\x65pchem\x18\x15 \x01(\x08\x12\x0f\n\x07\x64\x65\x65pctr\x18\x16 \x01(\x08\x12\x0f\n\x07pycaret\x18\x1c \x01(\x08\x12\x14\n\x0cpytorchvideo\x18\x1d \x01(\x08\x12\x0b\n\x03ray\x18\x1e \x01(\x08\x12\x1a\n\x12simpletransformers\x18\x1f \x01(\x08\x12\x0e\n\x06skorch\x18  \x01(\x08\x12\r\n\x05spacy\x18! \x01(\x08\x12\r\n\x05\x66lash\x18\" \x01(\x08\x12\x0e\n\x06optuna\x18# \x01(\x08\x12\x0f\n\x07recbole\x18$ \x01(\x08\x12\x0c\n\x04mmcv\x18% \x01(\x08\x12\r\n\x05mmdet\x18& \x01(\x08\x12\x11\n\ttorchdrug\x18\' \x01(\x08\x12\x11\n\ttorchtext\x18( \x01(\x08\x12\x13\n\x0btorchvision\x18) \x01(\x08\x12\r\n\x05\x65legy\x18* \x01(\x08\x12\x12\n\ndetectron2\x18+ \x01(\x08\x12\r\n\x05\x66lair\x18, \x01(\x08\x12\x0c\n\x04\x66lax\x18- \x01(\x08\x12\x0c\n\x04syft\x18. \x01(\x08\x12\x0b\n\x03TTS\x18/ \x01(\x08\x12\r\n\x05monai\x18\x30 \x01(\x08\x12\x17\n\x0fhuggingface_hub\x18\x31 \x01(\x08\x12\r\n\x05hydra\x18\x32 \x01(\x08\x12\x10\n\x08\x64\x61tasets\x18\x33 \x01(\x08\x12\x0e\n\x06sacred\x18\x34 \x01(\x08\x12\x0e\n\x06joblib\x18\x35 \x01(\x08\x12\x0c\n\x04\x64\x61sk\x18\x36 \x01(\x08\x12\x0f\n\x07\x61syncio\x18\x37 \x01(\x08\"\xae\x06\n\x07\x46\x65\x61ture\x12\r\n\x05watch\x18\x01 \x01(\x08\x12\x0e\n\x06\x66inish\x18\x02 \x01(\x08\x12\x0c\n\x04save\x18\x03 \x01(\x08\x12\x0f\n\x07offline\x18\x04 \x01(\x08\x12\x0f\n\x07resumed\x18\x05 \x01(\x08\x12\x0c\n\x04grpc\x18\x06 \x01(\x08\x12\x0e\n\x06metric\x18\x07 \x01(\x08\x12\r\n\x05keras\x18\x08 \x01(\x08\x12\x11\n\tsagemaker\x18\t \x01(\x08\x12\x1c\n\x14\x61rtifact_incremental\x18\n \x01(\x08\x12\x10\n\x08metaflow\x18\x0b \x01(\x08\x12\x0f\n\x07prodigy\x18\x0c \x01(\x08\x12\x15\n\rset_init_name\x18\r \x01(\x08\x12\x13\n\x0bset_init_id\x18\x0e \x01(\x08\x12\x15\n\rset_init_tags\x18\x0f \x01(\x08\x12\x17\n\x0fset_init_config\x18\x10 \x01(\x08\x12\x14\n\x0cset_run_name\x18\x11 \x01(\x08\x12\x14\n\x0cset_run_tags\x18\x12 \x01(\x08\x12\x17\n\x0fset_config_item\x18\x13 \x01(\x08\x12\x0e\n\x06launch\x18\x14 \x01(\x08\x12\x1c\n\x14torch_profiler_trace\x18\x15 \x01(\x08\x12\x0b\n\x03sb3\x18\x16 \x01(\x08\x12\x0f\n\x07service\x18\x17 \x01(\x08\x12\x17\n\x0finit_return_run\x18\x18 \x01(\x08\x12\x1f\n\x17lightgbm_wandb_callback\x18\x19 \x01(\x08\x12\x1c\n\x14lightgbm_log_summary\x18\x1a \x01(\x08\x12\x1f\n\x17\x63\x61tboost_wandb_callback\x18\x1b \x01(\x08\x12\x1c\n\x14\x63\x61tboost_log_summary\x18\x1c \x01(\x08\x12\x17\n\x0ftensorboard_log\x18\x1d \x01(\x08\x12\x16\n\x0e\x65stimator_hook\x18\x1e \x01(\x08\x12\x1e\n\x16xgboost_wandb_callback\x18\x1f \x01(\x08\x12\"\n\x1axgboost_old_wandb_callback\x18  \x01(\x08\x12\x0e\n\x06\x61ttach\x18! \x01(\x08\x12\x19\n\x11tensorboard_patch\x18\" \x01(\x08\x12\x18\n\x10tensorboard_sync\x18# \x01(\x08\x12\x15\n\rkfp_wandb_log\x18$ \x01(\x08\"\xb2\x01\n\x03\x45nv\x12\x0f\n\x07jupyter\x18\x01 \x01(\x08\x12\x0e\n\x06kaggle\x18\x02 \x01(\x08\x12\x0f\n\x07windows\x18\x03 \x01(\x08\x12\x0e\n\x06m1_gpu\x18\x04 \x01(\x08\x12\x13\n\x0bstart_spawn\x18\x05 \x01(\x08\x12\x12\n\nstart_fork\x18\x06 \x01(\x08\x12\x18\n\x10start_forkserver\x18\x07 \x01(\x08\x12\x14\n\x0cstart_thread\x18\x08 \x01(\x08\x12\x10\n\x08maybe_mp\x18\t \x01(\x08\"H\n\x06Labels\x12\x13\n\x0b\x63ode_string\x18\x01 \x01(\t\x12\x13\n\x0brepo_string\x18\x02 \x01(\t\x12\x14\n\x0c\x63ode_version\x18\x03 \x01(\t\"\xdc\x01\n\nDeprecated\x12!\n\x19keras_callback__data_type\x18\x01 \x01(\x08\x12\x11\n\trun__mode\x18\x02 \x01(\x08\x12\x19\n\x11run__save_no_args\x18\x03 \x01(\x08\x12\x11\n\trun__join\x18\x04 \x01(\x08\x12\r\n\x05plots\x18\x05 \x01(\x08\x12\x15\n\rrun__log_sync\x18\x06 \x01(\x08\x12!\n\x19init__config_include_keys\x18\x07 \x01(\x08\x12!\n\x19init__config_exclude_keys\x18\x08 \x01(\x08\"|\n\x06Issues\x12%\n\x1dsettings__validation_warnings\x18\x01 \x01(\x08\x12!\n\x19settings__unexpected_args\x18\x02 \x01(\x08\x12(\n settings__preprocessing_warnings\x18\x03 \x01(\x08\x62\x06proto3'
   ,
   dependencies=[wandb_dot_proto_dot_wandb__base__pb2.DESCRIPTOR,])
 
@@ -969,6 +969,20 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='init__config_include_keys', full_name='wandb_internal.Deprecated.init__config_include_keys', index=6,
+      number=7, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='init__config_exclude_keys', full_name='wandb_internal.Deprecated.init__config_exclude_keys', index=7,
+      number=8, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
   ],
   extensions=[
   ],
@@ -982,7 +996,7 @@
   oneofs=[
   ],
   serialized_start=2460,
-  serialized_end=2610,
+  serialized_end=2680,
 )
 
 
@@ -1027,8 +1041,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=2612,
-  serialized_end=2736,
+  serialized_start=2682,
+  serialized_end=2806,
 )
 
 _TELEMETRYRECORD.fields_by_name['imports_init'].message_type = _IMPORTS
diff --git a/wandb/proto/wandb_telemetry_pb2.pyi b/wandb/proto/wandb_telemetry_pb2.pyi
index 7d76915dcfc..9917f5f7d08 100644
--- a/wandb/proto/wandb_telemetry_pb2.pyi
+++ b/wandb/proto/wandb_telemetry_pb2.pyi
@@ -406,12 +406,16 @@ class Deprecated(google.protobuf.message.Message):
     RUN__JOIN_FIELD_NUMBER: builtins.int
     PLOTS_FIELD_NUMBER: builtins.int
     RUN__LOG_SYNC_FIELD_NUMBER: builtins.int
+    INIT__CONFIG_INCLUDE_KEYS_FIELD_NUMBER: builtins.int
+    INIT__CONFIG_EXCLUDE_KEYS_FIELD_NUMBER: builtins.int
     keras_callback__data_type: builtins.bool = ...
     run__mode: builtins.bool = ...
     run__save_no_args: builtins.bool = ...
     run__join: builtins.bool = ...
     plots: builtins.bool = ...
     run__log_sync: builtins.bool = ...
+    init__config_include_keys: builtins.bool = ...
+    init__config_exclude_keys: builtins.bool = ...
 
     def __init__(self,
         *,
@@ -421,8 +425,10 @@ class Deprecated(google.protobuf.message.Message):
         run__join : builtins.bool = ...,
         plots : builtins.bool = ...,
         run__log_sync : builtins.bool = ...,
+        init__config_include_keys : builtins.bool = ...,
+        init__config_exclude_keys : builtins.bool = ...,
         ) -> None: ...
-    def ClearField(self, field_name: typing_extensions.Literal[u"keras_callback__data_type",b"keras_callback__data_type",u"plots",b"plots",u"run__join",b"run__join",u"run__log_sync",b"run__log_sync",u"run__mode",b"run__mode",u"run__save_no_args",b"run__save_no_args"]) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal[u"init__config_exclude_keys",b"init__config_exclude_keys",u"init__config_include_keys",b"init__config_include_keys",u"keras_callback__data_type",b"keras_callback__data_type",u"plots",b"plots",u"run__join",b"run__join",u"run__log_sync",b"run__log_sync",u"run__mode",b"run__mode",u"run__save_no_args",b"run__save_no_args"]) -> None: ...
 global___Deprecated = Deprecated
 
 class Issues(google.protobuf.message.Message):
diff --git a/wandb/sdk/internal/internal_api.py b/wandb/sdk/internal/internal_api.py
index 251d6b9683c..2f81503ea10 100644
--- a/wandb/sdk/internal/internal_api.py
+++ b/wandb/sdk/internal/internal_api.py
@@ -9,7 +9,7 @@
 from io import BytesIO
 import json
 import os
-from pkg_resources import parse_version  # type: ignore
+from pkg_resources import parse_version
 import re
 import requests
 import logging
diff --git a/wandb/sdk/lib/retry.py b/wandb/sdk/lib/retry.py
index f389a107659..fd64c312d65 100644
--- a/wandb/sdk/lib/retry.py
+++ b/wandb/sdk/lib/retry.py
@@ -88,7 +88,7 @@ def __call__(self, *args, **kwargs):
 
         sleep_base = kwargs.pop("retry_sleep_base", 1)
 
-        # an extra function to allow performing more logic on the filtered exceptiosn
+        # an extra function to allow performing more logic on the filtered exception
         check_retry_fn = kwargs.pop("check_retry_fn", self._check_retry_fn)
 
         sleep = sleep_base
diff --git a/wandb/sdk/wandb_init.py b/wandb/sdk/wandb_init.py
index 492b6ad8559..198e569ae53 100644
--- a/wandb/sdk/wandb_init.py
+++ b/wandb/sdk/wandb_init.py
@@ -8,13 +8,11 @@
 [guide and FAQs](https://docs.wandb.ai/guides/track/launch).
 """
 import copy
-import datetime
 import logging
 import os
 import platform
 import sys
 import tempfile
-import time
 import traceback
 from typing import Any, Dict, Optional, Sequence, Union
 
@@ -30,6 +28,7 @@
 from .backend.backend import Backend
 from .lib import filesystem, ipython, module, reporting, telemetry
 from .lib import RunDisabled, SummaryDisabled
+from .lib.deprecate import deprecate, Deprecated
 from .lib.printer import get_printer
 from .lib.proto_util import message_to_dict
 from .lib.wburls import wburls
@@ -92,6 +91,8 @@ def __init__(self):
 
         self._init_telemetry_obj = telemetry.TelemetryRecord()
 
+        self.deprecated_features_used: Dict[str, str] = dict()
+
     def setup(self, kwargs) -> None:  # noqa: C901
         """Completes setup for `wandb.init()`.
 
@@ -136,16 +137,8 @@ def setup(self, kwargs) -> None:  # noqa: C901
         # Start with settings from wandb library singleton
         settings: Settings = self._wl.settings.copy()
         settings_param = kwargs.pop("settings", None)
-        if settings_param is not None:
-            if isinstance(settings_param, Settings):
-                # todo: check the logic here. this _only_ comes up in tests?
-                # update settings with settings_param using whatever
-                # source each parameter has there
-                settings._apply_settings(settings_param, _logger=logger)
-            elif isinstance(settings_param, dict):
-                # if it is a mapping, update the settings with it
-                # explicitly using Source.INIT
-                settings.update(settings_param, source=Source.INIT)
+        if settings_param is not None and isinstance(settings_param, (Settings, dict)):
+            settings.update(settings_param, source=Source.INIT)
 
         self._reporter = reporting.setup_reporter(settings=settings)
 
@@ -180,13 +173,17 @@ def setup(self, kwargs) -> None:  # noqa: C901
         config_include_keys = kwargs.pop("config_include_keys", None)
         config_exclude_keys = kwargs.pop("config_exclude_keys", None)
 
-        # todo: deprecate config_include_keys and config_exclude_keys
-        # if config_include_keys or config_exclude_keys:
-        #     wandb.termwarn(
-        #       "config_include_keys and config_exclude_keys are deprecated:"
-        #       " use config=wandb.helper.parse_config(config_object, include=('key',))"
-        #       " or config=wandb.helper.parse_config(config_object, exclude=('key',))"
-        #     )
+        deprecated_kwargs = {
+            "config_include_keys": (
+                "Use `config=wandb.helper.parse_config(config_object, include=('key',))` instead."
+            ),
+            "config_exclude_keys": (
+                "Use `config=wandb.helper.parse_config(config_object, exclude=('key',))` instead."
+            ),
+        }
+        for deprecated_kwarg, msg in deprecated_kwargs.items():
+            if locals().get(deprecated_kwarg):
+                self.deprecated_features_used[deprecated_kwarg] = msg
 
         init_config = parse_config(
             init_config, include=config_include_keys, exclude=config_exclude_keys
@@ -261,14 +258,8 @@ def setup(self, kwargs) -> None:  # noqa: C901
             settings.update({"save_code": False}, source=Source.INIT)
 
         # TODO(jhr): should this be moved? probably.
-        time_stamp: float = time.time()
-        settings.update(
-            {
-                "_start_time": time_stamp,
-                "_start_datetime": datetime.datetime.fromtimestamp(time_stamp),
-            },
-            source=Source.INIT,
-        )
+        # if settings._start_time is None:
+        settings._start_run(source=Source.INIT)
 
         if not settings._noop:
             self._log_setup(settings)
@@ -289,8 +280,8 @@ def teardown(self):
     def _enable_logging(self, log_fname, run_id=None):
         """Enables logging to the global debug log.
 
-        This adds a run_id to the log, in case of muliple processes on the same machine.
-        Currently there is no way to disable logging after it's enabled.
+        This adds a run_id to the log, in case of multiple processes on the same machine.
+        Currently, there is no way to disable logging after it's enabled.
         """
         handler = logging.FileHandler(log_fname)
         handler.setLevel(logging.INFO)
@@ -580,7 +571,7 @@ def init(self) -> Union[Run, RunDisabled, None]:  # noqa: C901
 
             tel.env.maybe_mp = _maybe_mp_process(backend)
 
-            # fixme: detected issues with settings
+            # todo: detected issues with settings.
             if self.settings.__dict__["_Settings__preprocessing_warnings"]:
                 tel.issues.settings__preprocessing_warnings = True
             if self.settings.__dict__["_Settings__validation_warnings"]:
@@ -594,6 +585,14 @@ def init(self) -> Union[Run, RunDisabled, None]:  # noqa: C901
             else:
                 run._label_probe_main()
 
+        for deprecated_feature, msg in self.deprecated_features_used.items():
+            warning_message = f"`{deprecated_feature}` is deprecated. {msg}"
+            deprecate(
+                field_name=getattr(Deprecated, "init__" + deprecated_feature),
+                warning_message=warning_message,
+                run=run,
+            )
+
         logger.info("updated telemetry")
 
         run._set_library(self._wl)
diff --git a/wandb/sdk/wandb_run.py b/wandb/sdk/wandb_run.py
index f380aba973b..14bef52c660 100644
--- a/wandb/sdk/wandb_run.py
+++ b/wandb/sdk/wandb_run.py
@@ -258,7 +258,7 @@ def wrapper(self: Type["Run"], *args: Any, **kwargs: Any) -> Any:
 
 
 class Run:
-    """A unit of computation logged by wandb. Typically this is an ML experiment.
+    """A unit of computation logged by wandb. Typically, this is an ML experiment.
 
     Create a run with `wandb.init()`:
     <!--yeadoc-test:run-object-basic-->
@@ -309,7 +309,7 @@ class Run:
     to `wandb.init`. For more details on distributed training with W&B, check out
     [our guide](https://docs.wandb.ai/guides/track/advanced/distributed-training).
 
-    Currently there is a parallel `Run` object in the `wandb.Api`. Eventually these
+    Currently, there is a parallel `Run` object in the `wandb.Api`. Eventually these
     two objects will be merged.
 
     Attributes:
@@ -408,7 +408,7 @@ def _init(
         self._step = 0
         self._torch_history: Optional["wandb.wandb_torch.TorchHistory"] = None
 
-        self._start_time = time.time()
+        self._start_time = self._settings._start_time or time.time()
 
         _datatypes_set_callback(self._datatypes_callback)
 
diff --git a/wandb/sdk/wandb_settings.py b/wandb/sdk/wandb_settings.py
index fec0b459f2a..b4384921681 100644
--- a/wandb/sdk/wandb_settings.py
+++ b/wandb/sdk/wandb_settings.py
@@ -18,6 +18,7 @@
     Callable,
     Dict,
     FrozenSet,
+    ItemsView,
     Iterable,
     Mapping,
     no_type_check,
@@ -81,7 +82,7 @@ def _get_wandb_dir(root_dir: str) -> str:
     return os.path.expanduser(path)
 
 
-# fixme: should either return bool or error out. fix once confident.
+# todo: should either return bool or error out. fix once confident.
 def _str_as_bool(val: Union[str, bool]) -> bool:
     """
     Parse a string as a bool.
@@ -94,7 +95,7 @@ def _str_as_bool(val: Union[str, bool]) -> bool:
     except (AttributeError, ValueError):
         pass
 
-    # fixme: remove this and only raise error once we are confident.
+    # todo: remove this and only raise error once we are confident.
     wandb.termwarn(
         f"Could not parse value {val} as a bool. ",
         repeat=False,
@@ -156,7 +157,7 @@ def _get_program_relpath_from_gitrepo(
 @enum.unique
 class Source(enum.IntEnum):
     OVERRIDE: int = 0
-    BASE: int = 1  # fixme: audit this
+    BASE: int = 1  # todo: audit this
     ORG: int = 2
     ENTITY: int = 3
     PROJECT: int = 4
@@ -192,7 +193,7 @@ class Property:
           E.g. if `is_policy` is True, the smallest `Source` value takes precedence.
     """
 
-    # fixme: this is a temporary measure to bypass validation of the settings
+    # todo: this is a temporary measure to bypass validation of the settings
     #  whose validation was not previously enforced to make sure we don't brake anything.
     __strict_validate_settings = {
         "project",
@@ -234,7 +235,7 @@ def __init__(  # pylint: disable=unused-argument
         self._is_policy = is_policy
         self._source = source
 
-        # fixme: this is a temporary measure to collect stats on failed preprocessing and validation
+        # todo: this is a temporary measure to collect stats on failed preprocessing and validation
         self.__failed_preprocessing: bool = False
         self.__failed_validation: bool = False
 
@@ -282,14 +283,14 @@ def _preprocess(self, value: Any) -> Any:
         return value
 
     def _validate(self, value: Any) -> Any:
-        self.__failed_validation = False  # fixme: this is a temporary measure
+        self.__failed_validation = False  # todo: this is a temporary measure
         if value is not None and self._validator is not None:
             _validator = (
                 [self._validator] if callable(self._validator) else self._validator
             )
             for v in _validator:
                 if not v(value):
-                    # fixme: this is a temporary measure to bypass validation of certain settings.
+                    # todo: this is a temporary measure to bypass validation of certain settings.
                     #  remove this once we are confident
                     if self.name in self.__strict_validate_settings:
                         raise ValueError(
@@ -636,9 +637,11 @@ def _default_props(self) -> Dict[str, Dict[str, Any]]:
             system_sample_seconds={"value": 2},
             timespec={
                 "hook": (
-                    lambda _: datetime.strftime(self._start_datetime, "%Y%m%d_%H%M%S")
-                    if self._start_time and self._start_datetime
-                    else None
+                    lambda _: (
+                        datetime.strftime(self._start_datetime, "%Y%m%d_%H%M%S")
+                        if self._start_datetime
+                        else None
+                    )
                 ),
                 "auto_hook": True,
             },
@@ -817,7 +820,7 @@ def _validate_base_url(value: Optional[str]) -> bool:
             r"\.?"  # may have a trailing dot
         )
         # host_re = "(" + hostname_re + domain_re + tld_re + "|localhost)"
-        # fixme?: allow hostname to be just a hostname (no tld)?
+        # todo?: allow hostname to be just a hostname (no tld)?
         host_re = "(" + hostname_re + domain_re + f"({tld_re})?" + "|localhost)"
 
         regex = re.compile(
@@ -917,11 +920,16 @@ def _run_url(self) -> str:
         query = self._get_url_query_string()
         return f"{project_url}/runs/{quote(self.run_id)}{query}"
 
-    def _start_run(self) -> None:
+    def _start_run(self, source: int = Source.BASE) -> None:
         time_stamp: float = time.time()
         datetime_now: datetime = datetime.fromtimestamp(time_stamp)
         object.__setattr__(self, "_Settings_start_datetime", datetime_now)
         object.__setattr__(self, "_Settings_start_time", time_stamp)
+        self.update(
+            _start_datetime=datetime_now,
+            _start_time=time_stamp,
+            source=source,
+        )
 
     def _sweep_url(self) -> str:
         project_url = self._project_url_base()
@@ -935,7 +943,7 @@ def __init__(self, **kwargs: Any) -> None:
         self.__frozen: bool = False
         self.__initialized: bool = False
 
-        # fixme: this is collect telemetry on validation errors and unexpected args
+        # todo: this is collect telemetry on validation errors and unexpected args
         # values are stored as strings to avoid potential json serialization errors down the line
         self.__preprocessing_warnings: Dict[str, str] = dict()
         self.__validation_warnings: Dict[str, str] = dict()
@@ -985,7 +993,7 @@ def __init__(self, **kwargs: Any) -> None:
                     ),
                 )
 
-            # fixme: this is to collect stats on preprocessing and validation errors
+            # todo: this is to collect stats on preprocessing and validation errors
             if self.__dict__[prop].__dict__["_Property__failed_preprocessing"]:
                 self.__preprocessing_warnings[prop] = str(self.__dict__[prop]._value)
             if self.__dict__[prop].__dict__["_Property__failed_validation"]:
@@ -996,7 +1004,7 @@ def __init__(self, **kwargs: Any) -> None:
         # allow only explicitly defined arguments
         if unexpected_arguments:
 
-            # fixme: remove this and raise error instead once we are confident
+            # todo: remove this and raise error instead once we are confident
             self.__unexpected_args.update(unexpected_arguments)
             wandb.termwarn(
                 f"Ignoring unexpected arguments: {unexpected_arguments}. "
@@ -1023,7 +1031,7 @@ def __init__(self, **kwargs: Any) -> None:
         # done with init, use self.update() to update attributes from now on
         self.__initialized = True
 
-        # fixme? freeze settings to prevent accidental changes
+        # todo? freeze settings to prevent accidental changes
         # self.freeze()
 
     def __str__(self) -> str:
@@ -1049,7 +1057,7 @@ def __copy__(self) -> "Settings":
         """
         Ensure that a copy of the settings object is a truly deep copy
 
-        Note that the copied object will not be frozen  fixme? why is this needed?
+        Note that the copied object will not be frozen  todo? why is this needed?
         """
         # get attributes that are instances of the Property class:
         attributes = {k: v for k, v in self.__dict__.items() if isinstance(v, Property)}
@@ -1099,13 +1107,37 @@ def __getitem__(self, name: str) -> Any:
 
     def update(
         self,
-        settings: Optional[Dict[str, Any]] = None,
+        settings: Optional[Union[Dict[str, Any], "Settings"]] = None,
         source: int = Source.OVERRIDE,
         **kwargs: Any,
     ) -> None:
         """Update individual settings using the Property.update() method."""
         if "_Settings__frozen" in self.__dict__ and self.__frozen:
             raise TypeError("Settings object is frozen")
+
+        if isinstance(settings, Settings):
+            # If a Settings object is passed, detect the settings that differ
+            # from defaults, collect them into a dict, and apply them using `source`.
+            # This comes up in `wandb.init(settings=wandb.Settings(...))` and
+            # seems like the behavior that the user would expect when calling init that way.
+            defaults = Settings()
+            settings_dict = dict()
+            for k, v in settings.__dict__.items():
+                if isinstance(v, Property):
+                    if v._value != defaults.__dict__[k]._value:
+                        settings_dict[k] = v._value
+            # todo: store warnings from the passed Settings object, if any,
+            #  to collect telemetry on validation errors and unexpected args.
+            #  remove this once strict checking is enforced.
+            for attr in (
+                "_Settings__unexpected_args",
+                "_Settings__preprocessing_warnings",
+                "_Settings__validation_warnings",
+            ):
+                getattr(self, attr).update(getattr(settings, attr))
+            # replace with the generated dict
+            settings = settings_dict
+
         # add kwargs to settings
         settings = settings or dict()
         # explicit kwargs take precedence over settings
@@ -1121,7 +1153,7 @@ def update(
         for key, value in settings.items():
             self.__dict__[key].update(value, source)
 
-            # fixme: this is to collect stats on preprocessing and validation errors
+            # todo: this is to collect stats on preprocessing and validation errors
             if self.__dict__[key].__dict__["_Property__failed_preprocessing"]:
                 self.__preprocessing_warnings[key] = str(self.__dict__[key]._value)
             else:
@@ -1132,6 +1164,12 @@ def update(
             else:
                 self.__validation_warnings.pop(key, None)
 
+    def items(self) -> ItemsView[str, Any]:
+        return self.make_static().items()
+
+    def get(self, key: str, default: Any = None) -> Any:
+        return self.make_static().get(key, default)
+
     def freeze(self) -> None:
         object.__setattr__(self, "_Settings__frozen", True)
 
@@ -1166,7 +1204,7 @@ def _apply_settings(
             # note that only the same/higher priority settings are propagated
             self.update({k: v._value}, source=v.source)
 
-        # fixme: this is to pass on info on unexpected args in settings
+        # todo: this is to pass on info on unexpected args in settings
         if settings.__dict__["_Settings__unexpected_args"]:
             self.__dict__["_Settings__unexpected_args"].update(
                 settings.__dict__["_Settings__unexpected_args"]


Code review comments:
diff_hunk: @@ -236,16 +237,16 @@ def test_run_with_console_redirect(test_settings, capfd, console):
 
 
 @pytest.mark.parametrize("console", console_modes)
+@pytest.mark.timeout(300)
path: tests/test_redir.py
text: @user1:
why do you need to add a timeout?

@author:
oops, this one's not necessary, thanks! rm'ed