Repository: huggingface/huggingface_hub
Pull request #847 (2022-04-20 20:49:20)

Push to hub mixins that do not leverage `git`
This draft PR proposes a refactor of both mixins, `ModelHubMixin` and `push_to_hub_keras`, so that they do not leverage `git` under the hood. It is the continuation of https://github.com/huggingface/huggingface_hub/pull/321.

This PR was done to present how it would behave. It was not done with backward-compatibility in mind, but instead by thinking of the best API, therefore with lean signatures. I recommend reading the files themselves rather than the diff as they are drastically simpler:




If we choose to go this way, a standard backward compatibility approach would be implemented.

<table>
<tr>
<td>PyTorch</td>
<td>Keras</td>
</tr>
<tr>
<td>
Relevant code: <a href='https://github.com/LysandreJik/huggingface_hub/blob/6389eaa0d519e3121ba0a55cf6bacb19d520464b/src/huggingface_hub/hub_mixin.py#L218-L286'>ModelHubMixin</a>
</td>
<td>
Relevant code: <a href='https://github.com/LysandreJik/huggingface_hub/blob/6389eaa0d519e3121ba0a55cf6bacb19d520464b/src/huggingface_hub/keras_mixin.py#L271-L369'>Keras</a>
</td>
</tr>
<tr>
<td>

```py
def push_to_hub(
    self,
    repo_id: str,
    *,
    commit_message: Optional[str] = "Add model",
    private: Optional[bool] = None,
    api_endpoint: Optional[str] = None,
    token: Optional[str] = None,
    branch: Optional[str] = None,
    config: Optional[dict] = None,
) -> str:
```

</td>
<td>

```py
def push_to_hub_keras(
    model,
    repo_id: str,
    *,
    log_dir: Optional[str] = None,
    commit_message: Optional[str] = "Add model",
    private: Optional[bool] = None,
    api_endpoint: Optional[str] = None,
    token: Optional[str] = True,
    branch: Optional[str] = None,
    config: Optional[dict] = None,
    include_optimizer: Optional[bool] = False,
    task_name: Optional[str] = None,
    plot_model: Optional[bool] = True,
    **model_save_kwargs,
):
```

</td>
</tr>
<tr>
<td>

```py
import os.path
from typing import List

import torch

from huggingface_hub import ModelHubMixin
from torch import nn


class MyModel(nn.Module, ModelHubMixin):
    def __init__(self):
        super().__init__()

        self.ln_1 = nn.Linear(10, 100)
        self.ln_2 = nn.Linear(100, 10)

    def __call__(self, *args, **kwargs):
        return self.ln_1(self.ln_2(*args, **kwargs))

    def _save_pretrained(self, save_directory) -> List[str]:
        path = os.path.join(save_directory, 'pytorch_model.bin')
        torch.save(self, path)

        return [path]


MyModel().push_to_hub(
    'Jikiwa/my-new-model', 
    commit_message="Adding a config.", 
    config={'parameter': 2}
)
``` 

</td>
<td>

```py
import tensorflow as tf

from huggingface_hub import push_to_hub_keras


class MyModel(tf.keras.Model):
    def __init__(self, **kwargs):
        super().__init__()
        self.l1 = tf.keras.layers.Dense(2, activation="relu")
        dummy_batch_size = input_dim = 2
        self.dummy_inputs = tf.ones([dummy_batch_size, input_dim])

    def call(self, x):
        return self.l1(x)


model = MyModel()
model(tf.ones([2, 2]))
push_to_hub_keras(
    model, 
    'Jikiwa/my-new-keras-model', 
    commit_message="Committing model number 2!"
)
```

</td>
</tr>
</table>

cc @osanseviero @NielsRogge @nateraw @merveenoyan 

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_847). All of your documentation changes will be reflected on that endpoint.
adrinjalali: Merging https://github.com/huggingface/huggingface_hub/pull/849 should reduce the diff here.
merveenoyan: I love the refactor with HF API! I can open a PR for tests if you want @LysandreJik 
LysandreJik: Thank you for the proposal @merveenoyan, I'll likely proceed to a backward compatibility implementation + test update next week. I'll let you know if I can use assistance on any of it!
LysandreJik: This is on hold as another, more robust mechanism to upload files is in the making on the backend side. 
fcakyon: > This is on hold as another, more robust mechanism to upload files is in the making on the backend side.

🤔 waiting for the new upload mechanism to integrate push to hub functionality to my packages!
LysandreJik: Glad to hear it @fcakyon, we will let you know.
Wauplin: @SBrandeis @julien-c @nateraw Work on `push_to_hub` feature for mixins (using HTTP endpoints) is now ready for review. 

Changes have first been reviewed by @LysandreJik in https://github.com/LysandreJik/huggingface_hub/pull/8 before merging into his branch (after some feedback/changes, see discussions). Main changes compared to before is handling backward compatibility with deprecated arguments and some tests and docstring.
LysandreJik: Pinging @NielsRogge as he wants to play with it and to help advertise it after this is merged!
fcakyon: I was using PytorchModelMixin class for my upcoming open source package, is this PR going to affect the API? If yes, how should I migrate to the upcoming API?
Wauplin: > I was using PytorchModelMixin class for my upcoming open source package, is this PR going to affect the API? If yes, how should I migrate to the upcoming API?

Hi @fcakyon, once PR is merged and `huggingface_hub` v0.9 is released you should be able to push to hub using the non-git mixins the same way as before but using different arguments:

```py
model.push_to_hub(repo_id="mymodel", commit_message="model-1")
```

**Note:** previous git-based implementation still works and is expected to be supported until v0.12. Using deprecated arguments will prompt you some `FutureWarnings`.

---
Full list of arguments in new version will be:
```py
def push_to_hub(
    self,
    repo_id: str,
    *,
    commit_message: Optional[str] = "Add model",
    private: Optional[bool] = None,
    api_endpoint: Optional[str] = None,
    token: Optional[str] = None,
    branch: Optional[str] = None,
    create_pr: Optional[bool] = None,
    config: Optional[dict] = None,
)
 ```
fcakyon: @Wauplin thanks a lot! So by setting `repo_id=organization_name/model_name` I will be able to create a fresh model or update an existing model in the hub, right?
Wauplin: > @Wauplin thanks a lot! So by setting `repo_id=organization_name/model_name` I will be able to create a fresh model or update an existing model in the hub, right?

Exactly 👍 
codecov[bot]: # [Codecov](https://codecov.io/gh/huggingface/huggingface_hub/pull/847?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) Report
> Merging [#847](https://codecov.io/gh/huggingface/huggingface_hub/pull/847?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) (e820b70) into [main](https://codecov.io/gh/huggingface/huggingface_hub/commit/880408a148dc79825c995de136f5c2a8eaa31741?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) (880408a) will **decrease** coverage by `51.24%`.
> The diff coverage is `30.43%`.

> :exclamation: Current head e820b70 differs from pull request most recent head 44d5941. Consider uploading reports for the commit 44d5941 to get more accurate results

```diff
@@             Coverage Diff             @@
##             main     #847       +/-   ##
===========================================
- Coverage   80.69%   29.45%   -51.25%     
===========================================
  Files          29       29               
  Lines        3316     3378       +62     
===========================================
- Hits         2676      995     -1681     
- Misses        640     2383     +1743     
```


| [Impacted Files](https://codecov.io/gh/huggingface/huggingface_hub/pull/847?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) | Coverage Δ | |
|---|---|---|
| [src/huggingface\_hub/utils/endpoint\_helpers.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi91dGlscy9lbmRwb2ludF9oZWxwZXJzLnB5) | `44.08% <ø> (-53.77%)` | :arrow_down: |
| [src/huggingface\_hub/hf\_api.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9oZl9hcGkucHk=) | `27.65% <20.83%> (-58.97%)` | :arrow_down: |
| [src/huggingface\_hub/hub\_mixin.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9odWJfbWl4aW4ucHk=) | `27.77% <25.00%> (-58.43%)` | :arrow_down: |
| [src/huggingface\_hub/keras\_mixin.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9rZXJhc19taXhpbi5weQ==) | `19.64% <32.00%> (-71.46%)` | :arrow_down: |
| [src/huggingface\_hub/utils/\_deprecation.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi91dGlscy9fZGVwcmVjYXRpb24ucHk=) | `65.11% <47.36%> (-12.67%)` | :arrow_down: |
| [src/huggingface\_hub/snapshot\_download.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9zbmFwc2hvdF9kb3dubG9hZC5weQ==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [src/huggingface\_hub/\_snapshot\_download.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9fc25hcHNob3RfZG93bmxvYWQucHk=) | `17.74% <0.00%> (-74.20%)` | :arrow_down: |
| [src/huggingface\_hub/repocard.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9yZXBvY2FyZC5weQ==) | `23.86% <0.00%> (-71.60%)` | :arrow_down: |
| [src/huggingface\_hub/repository.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9yZXBvc2l0b3J5LnB5) | `13.59% <0.00%> (-66.61%)` | :arrow_down: |
| ... and [16 more](https://codecov.io/gh/huggingface/huggingface_hub/pull/847/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface)

LysandreJik: Big effort! Let's merge this, thanks for the help and all the work improving & wrapping it up @Wauplin!

Patch:
diff --git a/.gitignore b/.gitignore
index 28c08a67..4b6a96a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,6 +109,7 @@ venv/
 ENV/
 env.bak/
 venv.bak/
+.venv*
 
 # Spyder project settings
 .spyderproject
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b8c216eb..33cb858b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -258,13 +258,14 @@ repository they can be run with the following:
 
 ```bash
 $ HUGGINGFACE_CO_STAGING=1 python -m pytest -sv ./tests
+```
 
-In fact, that's how `make test` is implemented (sans the `pip install` line)!
+In fact, that's how `make test` is implemented (without the `pip install` line)!
 
 You can specify a smaller set of tests in order to test only the feature
 you're working on.
 
-For example, the following will only run the tests hel in the `test_repository.py` file:
+For example, the following will only run the tests in the `test_repository.py` file:
 
 ```bash
 $ HUGGINGFACE_CO_STAGING=1 python -m pytest -sv ./tests/test_repository.py
diff --git a/docs/source/how-to-manage.mdx b/docs/source/how-to-manage.mdx
index 7800fa37..86106b93 100644
--- a/docs/source/how-to-manage.mdx
+++ b/docs/source/how-to-manage.mdx
@@ -1,6 +1,6 @@
 # Create and manage a repository
 
-A repository is a space for you to store your model or dataset files. This guide will show you how to:
+A repository is a place where you can store your model or dataset files. This guide will show you how to:
 
 * Create and delete a repository.
 * Adjust repository visibility.
diff --git a/setup.cfg b/setup.cfg
index 6e107fef..cff24923 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -48,6 +48,7 @@ multi_line_output = 3
 use_parentheses = True
 
 [flake8]
+exclude = .git,__pycache__,old,build,dist,.venv*
 ignore = E203, E501, E741, W503
 max-line-length = 88
 
diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py
index 3e26e4b8..dec2f424 100644
--- a/src/huggingface_hub/hf_api.py
+++ b/src/huggingface_hub/hf_api.py
@@ -73,9 +73,8 @@ if sys.version_info >= (3, 8):
 else:
     from typing_extensions import Literal, TypedDict
 
-
-REGEX_DISCUSSION_URL = re.compile(r".*/discussions/(\d+)$")
 USERNAME_PLACEHOLDER = "hf_user"
+_REGEX_DISCUSSION_URL = re.compile(r".*/discussions/(\d+)$")
 
 logger = logging.get_logger(__name__)
 
@@ -1519,7 +1518,7 @@ class HfApi:
         )
         return [f.rfilename for f in repo_info.siblings]
 
-    @_deprecate_positional_args
+    @_deprecate_positional_args(version="0.12")
     def create_repo(
         self,
         repo_id: str = None,
@@ -2006,6 +2005,11 @@ class HfApi:
                 " `CommitOperationDelete`"
             )
 
+        logger.debug(
+            f"About to commit to the hub: {len(additions)} addition(s) and"
+            f" {len(deletions)} deletion(s)."
+        )
+
         for addition in additions:
             addition.validate()
 
@@ -2189,20 +2193,14 @@ class HfApi:
             create_pr=create_pr,
             parent_commit=parent_commit,
         )
-        if pr_url is not None:
-            re_match = re.match(REGEX_DISCUSSION_URL, pr_url)
-            if re_match is None:
-                raise RuntimeError(
-                    "Unexpected response from the hub, expected a Pull Request URL but"
-                    f" got: '{pr_url}'"
-                )
-            revision = quote(f"refs/pr/{re_match[1]}", safe="")
 
+        if pr_url is not None:
+            revision = quote(_parse_revision_from_pr_url(pr_url), safe="")
         if repo_type in REPO_TYPES_URL_PREFIXES:
             repo_id = REPO_TYPES_URL_PREFIXES[repo_type] + repo_id
         revision = revision if revision is not None else DEFAULT_REVISION
+        # Similar to `hf_hub_url` but it's "blob" instead of "resolve"
         return f"{self.endpoint}/{repo_id}/blob/{revision}/{path_in_repo}"
-        # ^ Similar to `hf_hub_url` but it's "blob" instead of "resolve"
 
     def upload_folder(
         self,
@@ -2313,25 +2311,8 @@ class HfApi:
             if commit_message is not None
             else f"Upload {path_in_repo} with huggingface_hub"
         )
-        folder_path = os.path.normpath(os.path.expanduser(folder_path))
-        if not os.path.isdir(folder_path):
-            raise ValueError(f"Provided path: '{folder_path}' is not a directory")
-
-        files_to_add: List[CommitOperationAdd] = []
-        for dirpath, _, filenames in os.walk(folder_path):
-            for filename in filenames:
-                abs_path = os.path.join(dirpath, filename)
-                rel_path = os.path.relpath(abs_path, folder_path)
-                files_to_add.append(
-                    CommitOperationAdd(
-                        path_or_fileobj=abs_path,
-                        path_in_repo=os.path.normpath(
-                            os.path.join(path_in_repo, rel_path)
-                        ).replace(os.sep, "/"),
-                    )
-                )
 
-        logger.debug(f"About to upload / commit {len(files_to_add)} files to the Hub")
+        files_to_add = _prepare_upload_folder_commit(folder_path, path_in_repo)
 
         pr_url = self.create_commit(
             repo_type=repo_type,
@@ -2346,20 +2327,12 @@ class HfApi:
         )
 
         if pr_url is not None:
-            re_match = re.match(REGEX_DISCUSSION_URL, pr_url)
-            if re_match is None:
-                raise RuntimeError(
-                    "Unexpected response from the hub, expected a Pull Request URL but"
-                    f" got: '{pr_url}'"
-                )
-            revision = quote(f"refs/pr/{re_match[1]}", safe="")
-
+            revision = quote(_parse_revision_from_pr_url(pr_url), safe="")
         if repo_type in REPO_TYPES_URL_PREFIXES:
             repo_id = REPO_TYPES_URL_PREFIXES[repo_type] + repo_id
-
         revision = revision if revision is not None else DEFAULT_REVISION
+        # Similar to `hf_hub_url` but it's "tree" instead of "resolve"
         return f"{self.endpoint}/{repo_id}/tree/{revision}/{path_in_repo}"
-        # ^ Similar to `hf_hub_url` but it's "tree" instead of "resolve"
 
     def delete_file(
         self,
@@ -3231,8 +3204,8 @@ class HfFolder:
         """
         Get token or None if not existent.
 
-        Note that a token can be also provided using the `HUGGING_FACE_HUB_TOKEN`
-        environment variable.
+        Note that a token can be also provided using the
+        `HUGGING_FACE_HUB_TOKEN` environment variable.
 
         Returns:
             `str` or `None`: The token, `None` if it doesn't exist.
@@ -3258,6 +3231,48 @@ class HfFolder:
             pass
 
 
+def _prepare_upload_folder_commit(
+    folder_path: str, path_in_repo: str
+) -> List[CommitOperationAdd]:
+    """Generate the list of Add operations for a commit to upload a folder."""
+    folder_path = os.path.normpath(os.path.expanduser(folder_path))
+    if not os.path.isdir(folder_path):
+        raise ValueError(f"Provided path: '{folder_path}' is not a directory")
+
+    files_to_add: List[CommitOperationAdd] = []
+    for dirpath, _, filenames in os.walk(folder_path):
+        for filename in filenames:
+            abs_path = os.path.join(dirpath, filename)
+            rel_path = os.path.relpath(abs_path, folder_path)
+            files_to_add.append(
+                CommitOperationAdd(
+                    path_or_fileobj=abs_path,
+                    path_in_repo=os.path.normpath(
+                        os.path.join(path_in_repo, rel_path)
+                    ).replace(os.sep, "/"),
+                )
+            )
+    return files_to_add
+
+
+def _parse_revision_from_pr_url(pr_url: str) -> str:
+    """Safely parse revision number from a PR url.
+
+    Example:
+    ```py
+    >>> _parse_revision_from_pr_url("https://huggingface.co/bigscience/bloom/discussions/2")
+    "refs/pr/2"
+    ```
+    """
+    re_match = re.match(_REGEX_DISCUSSION_URL, pr_url)
+    if re_match is None:
+        raise RuntimeError(
+            "Unexpected response from the hub, expected a Pull Request URL but"
+            f" got: '{pr_url}'"
+        )
+    return f"refs/pr/{re_match[1]}"
+
+
 api = HfApi()
 
 set_access_token = api.set_access_token
@@ -3301,3 +3316,5 @@ comment_discussion = api.comment_discussion
 edit_discussion_comment = api.edit_discussion_comment
 rename_discussion = api.rename_discussion
 merge_pull_request = api.merge_pull_request
+
+_validate_or_retrieve_token = api._validate_or_retrieve_token
diff --git a/src/huggingface_hub/hub_mixin.py b/src/huggingface_hub/hub_mixin.py
index 6985d447..b207fdbc 100644
--- a/src/huggingface_hub/hub_mixin.py
+++ b/src/huggingface_hub/hub_mixin.py
@@ -1,21 +1,23 @@
 import json
 import os
+import tempfile
 from pathlib import Path
 from typing import Dict, Optional, Union
 
 import requests
+from huggingface_hub import hf_api
 
 from .constants import CONFIG_NAME, PYTORCH_WEIGHTS_NAME
 from .file_download import hf_hub_download, is_torch_available
 from .hf_api import HfApi, HfFolder
 from .repository import Repository
 from .utils import logging
+from .utils._deprecation import _deprecate_arguments, _deprecate_positional_args
 
 
 if is_torch_available():
     import torch
 
-
 logger = logging.get_logger(__name__)
 
 
@@ -37,34 +39,59 @@ class ModelHubMixin:
         """
         Save weights in local directory.
 
-                Parameters:
-                    save_directory (`str`):
-                        Specify directory in which you want to save weights.
-                    config (`dict`, *optional*):
-                        specify config (must be dict) in case you want to save
-                        it.
-                    push_to_hub (`bool`, *optional*, defaults to `False`):
-                        Set it to `True` in case you want to push your weights
-                        to huggingface_hub
-                    kwargs (`Dict`, *optional*):
-                        kwargs will be passed to `push_to_hub`
+        Parameters:
+            save_directory (`str`):
+                Specify directory in which you want to save weights.
+            config (`dict`, *optional*):
+                Specify config (must be dict) in case you want to save
+                it.
+            push_to_hub (`bool`, *optional*, defaults to `False`):
+                Whether or not to push your model to the Hugging Face model hub after
+                saving it. You can specify the repository you want to push to with
+                `repo_id` (will default to the name of `save_directory` in your
+                namespace).
+            kwargs:
+                Additional key word arguments passed along to the
+                [`~utils.PushToHubMixin.push_to_hub`] method.
         """
-
         os.makedirs(save_directory, exist_ok=True)
 
+        # saving model weights/files
+        self._save_pretrained(save_directory)
+
         # saving config
         if isinstance(config, dict):
             path = os.path.join(save_directory, CONFIG_NAME)
             with open(path, "w") as f:
                 json.dump(config, f)
 
-        # saving model weights/files
-        self._save_pretrained(save_directory)
-
         if push_to_hub:
-            return self.push_to_hub(save_directory, **kwargs)
-
-    def _save_pretrained(self, save_directory):
+            kwargs = kwargs.copy()  # soft-copy to avoid mutating input
+            if config is not None:  # kwarg for `push_to_hub`
+                kwargs["config"] = config
+
+            if (
+                # If a deprecated argument is passed, we have to use the deprecated
+                # version of `push_to_hub`.
+                # TODO: remove this possibility in v0.12
+                kwargs.get("repo_url") is not None
+                or kwargs.get("repo_path_or_name") is not None
+                or kwargs.get("organization") is not None
+                or kwargs.get("use_auth_token") is not None
+                or kwargs.get("git_user") is not None
+                or kwargs.get("git_email") is not None
+                or kwargs.get("skip_lfs_files") is not None
+            ):
+                if kwargs.get("repo_path_or_name") is None:
+                    # Repo name defaults to `save_directory` name
+                    kwargs["repo_path_or_name"] = save_directory
+            elif kwargs.get("repo_id") is None:
+                # Repo name defaults to `save_directory` name
+                kwargs["repo_id"] = Path(save_directory).name
+
+            return self.push_to_hub(**kwargs)
+
+    def _save_pretrained(self, save_directory: str):
         """
         Overwrite this method in subclass to define how to save your model.
         """
@@ -208,8 +235,23 @@ class ModelHubMixin:
         pretrained"""
         raise NotImplementedError
 
+    @_deprecate_positional_args(version="0.12")
+    @_deprecate_arguments(
+        version="0.12",
+        deprecated_args={
+            "repo_url",
+            "repo_path_or_name",
+            "organization",
+            "use_auth_token",
+            "git_user",
+            "git_email",
+            "skip_lfs_files",
+        },
+    )
     def push_to_hub(
         self,
+        # NOTE: deprecated signature that will change in 0.12
+        *,
         repo_path_or_name: Optional[str] = None,
         repo_url: Optional[str] = None,
         commit_message: Optional[str] = "Add model",
@@ -221,55 +263,81 @@ class ModelHubMixin:
         git_email: Optional[str] = None,
         config: Optional[dict] = None,
         skip_lfs_files: bool = False,
+        # NOTE: New arguments since 0.9
+        repo_id: Optional[str] = None,  # optional only until 0.12
+        token: Optional[str] = None,
+        branch: Optional[str] = None,
+        create_pr: Optional[bool] = None,
+        # TODO (release 0.12): signature must be the following
+        # repo_id: str,
+        # *,
+        # commit_message: Optional[str] = "Add model",
+        # private: Optional[bool] = None,
+        # api_endpoint: Optional[str] = None,
+        # token: Optional[str] = None,
+        # branch: Optional[str] = None,
+        # create_pr: Optional[bool] = None,
+        # config: Optional[dict] = None,
     ) -> str:
         """
-        Upload model checkpoint or tokenizer files to the Hub while
-        synchronizing a local clone of the repo in `repo_path_or_name`.
+        Upload model checkpoint to the Hub.
 
         Parameters:
-            repo_path_or_name (`str`, *optional*):
-                Can either be a repository name for your model or tokenizer in
-                the Hub or a path to a local folder (in which case the
-                repository will have the name of that local folder). If not
-                specified, will default to the name given by `repo_url` and a
-                local directory with that name will be created.
-            repo_url (`str`, *optional*):
-                Specify this in case you want to push to an existing repository
-                in the hub. If unspecified, a new repository will be created in
-                your namespace (unless you specify an `organization`) with
-                `repo_name`.
+            repo_id (`str`, *optional*):
+                Repository name to which push.
             commit_message (`str`, *optional*):
-                Message to commit while pushing. Will default to `"add config"`,
-                `"add tokenizer"` or `"add model"` depending on the type of the
-                class.
-            organization (`str`, *optional*):
-                Organization in which you want to push your model or tokenizer
-                (you must be a member of this organization).
+                Message to commit while pushing.
             private (`bool`, *optional*):
                 Whether the repository created should be private.
             api_endpoint (`str`, *optional*):
                 The API endpoint to use when pushing the model to the hub.
-            use_auth_token (`bool` or `str`, *optional*):
+            token (`str`, *optional*):
                 The token to use as HTTP bearer authorization for remote files.
-                If `True`, will use the token generated when running
-                `transformers-cli login` (stored in `~/.huggingface`). Will
-                default to `True` if `repo_url` is not specified.
-            git_user (`str`, *optional*):
-                will override the `git config user.name` for committing and
-                pushing files to the hub.
-            git_email (`str`, *optional*):
-                will override the `git config user.email` for committing and
-                pushing files to the hub.
+                If not set, will use the token set when logging in with
+                `transformers-cli login` (stored in `~/.huggingface`).
+            branch (`str`, *optional*):
+                The git branch on which to push the model. This defaults to
+                the default branch as specified in your repository, which
+                defaults to `"main"`.
+            create_pr (`boolean`, *optional*):
+                Whether or not to create a Pull Request from `branch` with that commit.
+                Defaults to `False`.
             config (`dict`, *optional*):
                 Configuration object to be saved alongside the model weights.
-            skip_lfs_files (`bool`, *optional*, defaults to `False`):
-                Whether to skip git-LFS files or not.
-
 
         Returns:
             The url of the commit of your model in the given repository.
         """
+        # If the repo id is set, it means we use the new version using HTTP endpoint
+        # (introduced in v0.9).
+        if repo_id is not None:
+            token, _ = hf_api._validate_or_retrieve_token(token)
+            api = HfApi(endpoint=api_endpoint)
+
+            api.create_repo(
+                repo_id=repo_id,
+                repo_type="model",
+                token=token,
+                private=private,
+                exist_ok=True,
+            )
+
+            # Push the files to the repo in a single commit
+            with tempfile.TemporaryDirectory() as tmp:
+                saved_path = Path(tmp) / repo_id
+                self.save_pretrained(saved_path, config=config)
+                return api.upload_folder(
+                    repo_id=repo_id,
+                    repo_type="model",
+                    token=token,
+                    folder_path=saved_path,
+                    commit_message=commit_message,
+                    revision=branch,
+                    create_pr=create_pr,
+                )
 
+        # If the repo id is None, it means we use the deprecated version using Git
+        # TODO: remove code between here and `return repo.git_push()` in release 0.12
         if repo_path_or_name is None and repo_url is None:
             raise ValueError(
                 "You need to specify a `repo_path_or_name` or a `repo_url`."
@@ -351,7 +419,7 @@ class PyTorchModelHubMixin(ModelHubMixin):
         ...     "mymodel", push_to_hub=False
         >>> )  # Saving model weights in the directory
         >>> model.push_to_hub(
-        ...     "mymodel", "model-1"
+        ...     repo_id="mymodel", commit_message="model-1"
         >>> )  # Pushing model-weights to hf-hub
 
         >>> # Downloading weights from hf-hub & model will be initialized from those weights
diff --git a/src/huggingface_hub/keras_mixin.py b/src/huggingface_hub/keras_mixin.py
index 0a5e7ba8..f3199cdb 100644
--- a/src/huggingface_hub/keras_mixin.py
+++ b/src/huggingface_hub/keras_mixin.py
@@ -1,13 +1,20 @@
 import collections.abc as collections
 import json
 import os
+import tempfile
 import warnings
 from pathlib import Path
 from shutil import copytree, rmtree
 from typing import Any, Dict, Optional, Union
+from urllib.parse import quote
 
 import yaml
-from huggingface_hub import ModelHubMixin, snapshot_download
+from huggingface_hub import (
+    CommitOperationDelete,
+    ModelHubMixin,
+    hf_api,
+    snapshot_download,
+)
 from huggingface_hub.file_download import (
     get_tf_version,
     is_graphviz_available,
@@ -15,10 +22,16 @@ from huggingface_hub.file_download import (
     is_tf_available,
 )
 
-from .constants import CONFIG_NAME
-from .hf_api import HfApi, HfFolder
+from .constants import CONFIG_NAME, DEFAULT_REVISION
+from .hf_api import (
+    HfApi,
+    HfFolder,
+    _parse_revision_from_pr_url,
+    _prepare_upload_folder_commit,
+)
 from .repository import Repository
 from .utils import logging
+from .utils._deprecation import _deprecate_arguments, _deprecate_positional_args
 
 
 logger = logging.get_logger(__name__)
@@ -281,8 +294,22 @@ def from_pretrained_keras(*args, **kwargs):
     return KerasModelHubMixin.from_pretrained(*args, **kwargs)
 
 
+@_deprecate_positional_args(version="0.12")
+@_deprecate_arguments(
+    version="0.12",
+    deprecated_args={
+        "repo_path_or_name",
+        "repo_url",
+        "organization",
+        "use_auth_token",
+        "git_user",
+        "git_email",
+    },
+)
 def push_to_hub_keras(
+    # NOTE: deprecated signature that will change in 0.12
     model,
+    *,
     repo_path_or_name: Optional[str] = None,
     repo_url: Optional[str] = None,
     log_dir: Optional[str] = None,
@@ -297,7 +324,28 @@ def push_to_hub_keras(
     include_optimizer: Optional[bool] = False,
     tags: Optional[Union[list, str]] = None,
     plot_model: Optional[bool] = True,
+    # NOTE: New arguments since 0.9
+    token: Optional[str] = True,
+    repo_id: Optional[str] = None,  # optional only until 0.12
+    branch: Optional[str] = None,
+    create_pr: Optional[bool] = None,
     **model_save_kwargs,
+    # TODO (release 0.12): signature must be the following
+    # model,
+    # repo_id: str,
+    # *,
+    # commit_message: Optional[str] = "Add model",
+    # private: Optional[bool] = None,
+    # api_endpoint: Optional[str] = None,
+    # token: Optional[str] = True,
+    # branch: Optional[str] = None,
+    # create_pr: Optional[bool] = None,
+    # config: Optional[dict] = None,
+    # log_dir: Optional[str] = None,
+    # include_optimizer: Optional[bool] = False,
+    # tags: Optional[Union[list, str]] = None,
+    # plot_model: Optional[bool] = True,
+    # **model_save_kwargs,
 ):
     """
     Upload model checkpoint or tokenizer files to the Hub while synchronizing a
@@ -308,41 +356,31 @@ def push_to_hub_keras(
             The [Keras
             model](`https://www.tensorflow.org/api_docs/python/tf/keras/Model`)
             you'd like to push to the Hub. The model must be compiled and built.
-        repo_path_or_name (`str`, *optional*):
-            Can either be a repository name for your model or tokenizer in the
-            Hub or a path to a local folder (in which case the repository will
-            have the name of that local folder). If not specified, will default
-            to the name given by `repo_url` and a local directory with that name
-            will be created.
-        repo_url (`str`, *optional*):
-            Specify this in case you want to push to an existing repository in
-            the Hub. If unspecified, a new repository will be created in your
-            namespace (unless you specify an `organization`) with `repo_name`.
-        log_dir (`str`, *optional*):
-            TensorBoard logging directory to be pushed. The Hub automatically
-            hosts and displays a TensorBoard instance if log files are included
-            in the repository.
+        repo_id (`str`):
+            Repository name to which push
         commit_message (`str`, *optional*, defaults to "Add message"):
             Message to commit while pushing.
-        organization (`str`, *optional*):
-            Organization in which you want to push your model or tokenizer (you
-            must be a member of this organization).
         private (`bool`, *optional*):
             Whether the repository created should be private.
         api_endpoint (`str`, *optional*):
             The API endpoint to use when pushing the model to the hub.
-        use_auth_token (`bool` or `str`, *optional*, defaults to `True`):
+        token (`str`, *optional*):
             The token to use as HTTP bearer authorization for remote files. If
-            `True`, will use the token generated when running `transformers-cli
-            login` (stored in `~/.huggingface`). Will default to `True`.
-        git_user (`str`, *optional*):
-            will override the `git config user.name` for committing and pushing
-            files to the Hub.
-        git_email (`str`, *optional*):
-            will override the `git config user.email` for committing and pushing
-            files to the Hub.
+            not set, will use the token set when logging in with
+            `huggingface-cli login` (stored in `~/.huggingface`).
+        branch (`str`, *optional*):
+            The git branch on which to push the model. This defaults to
+            the default branch as specified in your repository, which
+            defaults to `"main"`.
+        create_pr (`boolean`, *optional*):
+            Whether or not to create a Pull Request from `branch` with that commit.
+            Defaults to `False`.
         config (`dict`, *optional*):
             Configuration object to be saved alongside the model weights.
+        log_dir (`str`, *optional*):
+            TensorBoard logging directory to be pushed. The Hub automatically
+            hosts and displays a TensorBoard instance if log files are included
+            in the repository.
         include_optimizer (`bool`, *optional*, defaults to `False`):
             Whether or not to include optimizer during serialization.
         tags (Union[`list`, `str`], *optional*):
@@ -358,7 +396,69 @@ def push_to_hub_keras(
     Returns:
         The url of the commit of your model in the given repository.
     """
+    if repo_id is not None:
+        token, _ = hf_api._validate_or_retrieve_token(token)
+        api = HfApi(endpoint=api_endpoint)
+
+        api.create_repo(
+            repo_id=repo_id,
+            repo_type="model",
+            token=token,
+            private=private,
+            exist_ok=True,
+        )
+
+        # Push the files to the repo in a single commit
+        with tempfile.TemporaryDirectory() as tmp:
+            saved_path = Path(tmp) / repo_id
+            save_pretrained_keras(
+                model,
+                saved_path,
+                config=config,
+                include_optimizer=include_optimizer,
+                tags=tags,
+                plot_model=plot_model,
+                **model_save_kwargs,
+            )
+
+            # If log dir is provided, delete old logs + add new ones
+            operations = []
+            if log_dir is not None:
+                # Delete previous log files from Hub
+                operations += [
+                    CommitOperationDelete(path_in_repo=file)
+                    for file in api.list_repo_files(repo_id=repo_id, token=token)
+                    if file.startswith("logs/")
+                ]
+
+                # Copy new log files
+                copytree(log_dir, saved_path / "logs")
+
+            # NOTE: `_prepare_upload_folder_commit` and `create_commit` calls are
+            #       duplicate code from `upload_folder`. We are not directly using
+            #       `upload_folder` since we want to add delete operations to the
+            #       commit as well.
+            operations += _prepare_upload_folder_commit(saved_path, path_in_repo="")
+            pr_url = api.create_commit(
+                repo_type="model",
+                repo_id=repo_id,
+                operations=operations,
+                commit_message=commit_message,
+                token=token,
+                revision=branch,
+                create_pr=create_pr,
+            )
+            revision = branch
+            if revision is None:
+                revision = (
+                    quote(_parse_revision_from_pr_url(pr_url), safe="")
+                    if pr_url is not None
+                    else DEFAULT_REVISION
+                )
+            return f"{api.endpoint}/{repo_id}/tree/{revision}/"
 
+    # Repo id is None means we use the deprecated version using Git
+    # TODO: remove code between here and `return repo.git_push()` in release 0.12
     if repo_path_or_name is None and repo_url is None:
         raise ValueError("You need to specify a `repo_path_or_name` or a `repo_url`.")
 
diff --git a/src/huggingface_hub/utils/_deprecation.py b/src/huggingface_hub/utils/_deprecation.py
index cf4bce1c..5958432c 100644
--- a/src/huggingface_hub/utils/_deprecation.py
+++ b/src/huggingface_hub/utils/_deprecation.py
@@ -1,17 +1,16 @@
 import warnings
 from functools import wraps
 from inspect import Parameter, signature
+from typing import Set
 
 
-def _deprecate_positional_args(func=None, *, version="0.8"):
+def _deprecate_positional_args(*, version: str):
     """Decorator for methods that issues warnings for positional arguments.
     Using the keyword-only argument syntax in pep 3102, arguments after the
     * will issue a warning when passed as a positional argument.
 
     Args:
-        func (``Callable``, `optional`):
-            Function to check arguments on.
-        version (``Callable``, `optional`, defaults to ``"0.8"``):
+        version (`str`):
             The version when positional arguments will result in error.
     """
 
@@ -37,9 +36,9 @@ def _deprecate_positional_args(func=None, *, version="0.8"):
             ]
             args_msg = ", ".join(args_msg)
             warnings.warn(
-                f"Pass {args_msg} as keyword args. From version "
-                f"{version} passing these as positional arguments "
-                "will result in an error",
+                f"Deprecated positional argument(s) used in '{f.__name__}': pass"
+                f" {args_msg} as keyword args. From version {version} passing these as"
+                " positional arguments will result in an error,",
                 FutureWarning,
             )
             kwargs.update(zip(sig.parameters, args))
@@ -47,6 +46,45 @@ def _deprecate_positional_args(func=None, *, version="0.8"):
 
         return inner_f
 
-    if func is not None:
-        return _inner_deprecate_positional_args(func)
+    return _inner_deprecate_positional_args
+
+
+def _deprecate_arguments(*, version: str, deprecated_args: Set[str]):
+    """Decorator to issue warnings when using deprecated arguments.
+
+    TODO: could be useful to be able to set a custom error message.
+
+    Args:
+        version (`str`):
+            The version when deprecated arguments will result in error.
+        deprecated_args (`List[str]`):
+            List of the arguments to be deprecated.
+    """
+
+    def _inner_deprecate_positional_args(f):
+        sig = signature(f)
+
+        @wraps(f)
+        def inner_f(*args, **kwargs):
+            # Check for used deprecated arguments
+            used_deprecated_args = []
+            for _, parameter in zip(args, sig.parameters.values()):
+                if parameter.name in deprecated_args:
+                    used_deprecated_args.append(parameter.name)
+            for kwarg_name in kwargs:
+                if kwarg_name in deprecated_args:
+                    used_deprecated_args.append(kwarg_name)
+
+            # Warn and proceed
+            if len(used_deprecated_args) > 0:
+                warnings.warn(
+                    f"Deprecated argument(s) used in '{f.__name__}':"
+                    f" {', '.join(used_deprecated_args)}. Will not be supported from"
+                    f" version '{version}'.",
+                    FutureWarning,
+                )
+            return f(*args, **kwargs)
+
+        return inner_f
+
     return _inner_deprecate_positional_args
diff --git a/src/huggingface_hub/utils/endpoint_helpers.py b/src/huggingface_hub/utils/endpoint_helpers.py
index cc97f047..bdb7ab90 100644
--- a/src/huggingface_hub/utils/endpoint_helpers.py
+++ b/src/huggingface_hub/utils/endpoint_helpers.py
@@ -11,9 +11,8 @@
 # limitations under the License.
 """
 Helpful utility functions and classes in relation to exploring API endpoints
-with the aim for a user-friendly interface
+with the aim for a user-friendly interface.
 """
-
 import math
 import re
 from dataclasses import dataclass


Test patch:
diff --git a/tests/test_hubmixin.py b/tests/test_hubmixin.py
index b09cbf34..28b4f3ce 100644
--- a/tests/test_hubmixin.py
+++ b/tests/test_hubmixin.py
@@ -3,15 +3,16 @@ import os
 import shutil
 import tempfile
 import unittest
+from unittest.mock import Mock
 
-from huggingface_hub import HfApi
+from huggingface_hub import HfApi, hf_hub_download
 from huggingface_hub.file_download import is_torch_available
 from huggingface_hub.hub_mixin import PyTorchModelHubMixin
 from huggingface_hub.repository import Repository
 from huggingface_hub.utils import logging
 
 from .testing_constants import ENDPOINT_STAGING, TOKEN, USER
-from .testing_utils import repo_name, set_write_permission_and_retry
+from .testing_utils import expect_deprecation, repo_name, set_write_permission_and_retry
 
 
 logger = logging.get_logger(__name__)
@@ -91,6 +92,57 @@ class HubMixingTest(HubMixingCommonTest):
         self.assertTrue("pytorch_model.bin" in files)
         self.assertEqual(len(files), 2)
 
+    def test_save_pretrained_with_push_to_hub(self):
+        REPO_NAME = repo_name("save")
+        save_directory = f"{WORKING_REPO_DIR}/{REPO_NAME}"
+        config = {"hello": "world"}
+        mocked_model = DummyModel()
+        mocked_model.push_to_hub = Mock()
+        mocked_model._save_pretrained = Mock()  # disable _save_pretrained to speed-up
+
+        # Not pushed to hub
+        mocked_model.save_pretrained(save_directory)
+        mocked_model.push_to_hub.assert_not_called()
+
+        # Push to hub with repo_id
+        mocked_model.save_pretrained(
+            save_directory, push_to_hub=True, repo_id="CustomID", config=config
+        )
+        mocked_model.push_to_hub.assert_called_with(repo_id="CustomID", config=config)
+
+        # Push to hub with default repo_id (based on dir name)
+        mocked_model.save_pretrained(save_directory, push_to_hub=True, config=config)
+        mocked_model.push_to_hub.assert_called_with(repo_id=REPO_NAME, config=config)
+
+        # Push to hub with deprecated kwargs (git-based)
+        mocked_model.save_pretrained(
+            save_directory,
+            push_to_hub=True,
+            config=config,
+            repo_path_or_name="custom_repo_name",
+            git_email="myemail",
+            git_user="gituser",
+        )
+        mocked_model.push_to_hub.assert_called_with(
+            repo_path_or_name="custom_repo_name",
+            config=config,
+            git_email="myemail",
+            git_user="gituser",
+        )
+
+        # Push to hub with deprecated kwargs + use default repo_name + no config
+        mocked_model.save_pretrained(
+            save_directory,
+            push_to_hub=True,
+            git_email="myemail",
+            git_user="gituser",
+        )
+        mocked_model.push_to_hub.assert_called_with(
+            repo_path_or_name=save_directory,
+            git_email="myemail",
+            git_user="gituser",
+        )
+
     def test_rel_path_from_pretrained(self):
         model = DummyModel()
         model.save_pretrained(
@@ -114,31 +166,59 @@ class HubMixingTest(HubMixingCommonTest):
         model = DummyModel.from_pretrained(f"{WORKING_REPO_DIR}/{REPO_NAME}")
         self.assertDictEqual(model.config, {"num": 10, "act": "gelu_fast"})
 
-    def test_push_to_hub(self):
-        REPO_NAME = repo_name("PUSH_TO_HUB")
-        model = DummyModel()
-        model.push_to_hub(
-            repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
+    def test_push_to_hub_via_http_basic(self):
+        REPO_NAME = repo_name("PUSH_TO_HUB_via_http")
+        repo_id = f"{USER}/{REPO_NAME}"
+
+        DummyModel().push_to_hub(
+            repo_id=repo_id,
             api_endpoint=ENDPOINT_STAGING,
-            use_auth_token=self._token,
-            git_user="ci",
-            git_email="ci@dummy.com",
+            token=self._token,
             config={"num": 7, "act": "gelu_fast"},
         )
 
-        model_info = self._api.model_info(f"{USER}/{REPO_NAME}", token=self._token)
-        self.assertEqual(model_info.modelId, f"{USER}/{REPO_NAME}")
+        # Test model id exists
+        model_info = self._api.model_info(repo_id, token=self._token)
+        self.assertEqual(model_info.modelId, repo_id)
 
-        self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
+        # Test config has been pushed to hub
+        tmp_config_path = hf_hub_download(
+            repo_id=repo_id, filename="config.json", use_auth_token=self._token
+        )
+        with open(tmp_config_path) as f:
+            self.assertEqual(json.load(f), {"num": 7, "act": "gelu_fast"})
+
+        # Delete tmp file and repo
+        os.remove(tmp_config_path)
+        self._api.delete_repo(repo_id=repo_id, token=self._token)
+
+    @expect_deprecation("push_to_hub")
+    def test_push_to_hub_via_git_deprecated(self):
+        # TODO: remove in 0.12 when git method will be removed
+        REPO_NAME = repo_name("PUSH_TO_HUB_via_git")
+        repo_id = f"{USER}/{REPO_NAME}"
+
+        DummyModel().push_to_hub(
+            repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
+            api_endpoint=ENDPOINT_STAGING,
+            use_auth_token=self._token,
+        )
+
+        model_info = self._api.model_info(repo_id, token=self._token)
+        self.assertEqual(model_info.modelId, repo_id)
+        self._api.delete_repo(repo_id=repo_id, token=self._token)
 
-    def test_auto_lfs(self):
-        REPO_NAME = repo_name("PUSH_TO_HUB_LFS")
+    @expect_deprecation("push_to_hub")
+    def test_push_to_hub_via_git_use_lfs_by_default(self):
+        # TODO: remove in 0.12 when git method will be removed
+        REPO_NAME = repo_name("PUSH_TO_HUB_with_lfs_file")
         with tempfile.TemporaryDirectory() as tmpdirname:
             os.makedirs(f"{tmpdirname}/{WORKING_REPO_DIR}/{REPO_NAME}")
             self._repo_url = self._api.create_repo(repo_id=REPO_NAME, token=self._token)
             Repository(
                 local_dir=f"{tmpdirname}/{WORKING_REPO_DIR}/{REPO_NAME}",
                 clone_from=self._repo_url,
+                use_auth_token=self._token,
             )
 
             model = DummyModel()
@@ -154,11 +234,9 @@ class HubMixingTest(HubMixingCommonTest):
                 use_auth_token=self._token,
                 git_user="ci",
                 git_email="ci@dummy.com",
-                config={"num": 7, "act": "gelu_fast"},
             )
-            model_info = self._api.model_info(f"{USER}/{REPO_NAME}", token=self._token)
 
-            self.assertTrue(
-                "large_file.txt" in [f.rfilename for f in model_info.siblings]
-            )
-            self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
+        model_info = self._api.model_info(f"{USER}/{REPO_NAME}", token=self._token)
+
+        self.assertTrue("large_file.txt" in [f.rfilename for f in model_info.siblings])
+        self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
diff --git a/tests/test_keras_integration.py b/tests/test_keras_integration.py
index e72cc71e..badc8f3f 100644
--- a/tests/test_keras_integration.py
+++ b/tests/test_keras_integration.py
@@ -1,12 +1,13 @@
 import json
 import os
+import re
 import shutil
 import tempfile
 import unittest
 
 import pytest
 
-from huggingface_hub import HfApi
+from huggingface_hub import HfApi, hf_hub_download
 from huggingface_hub.file_download import (
     is_graphviz_available,
     is_pydot_available,
@@ -18,10 +19,16 @@ from huggingface_hub.keras_mixin import (
     push_to_hub_keras,
     save_pretrained_keras,
 )
+from huggingface_hub.repository import Repository
 from huggingface_hub.utils import logging
 
 from .testing_constants import ENDPOINT_STAGING, TOKEN, USER
-from .testing_utils import repo_name, retry_endpoint, set_write_permission_and_retry
+from .testing_utils import (
+    expect_deprecation,
+    repo_name,
+    retry_endpoint,
+    set_write_permission_and_retry,
+)
 
 
 logger = logging.get_logger(__name__)
@@ -31,6 +38,10 @@ WORKING_REPO_DIR = os.path.join(
     os.path.dirname(os.path.abspath(__file__)), WORKING_REPO_SUBDIR
 )
 
+PUSH_TO_HUB_KERAS_WARNING_REGEX = re.escape(
+    "Deprecated argument(s) used in 'push_to_hub_keras':"
+)
+
 if is_tf_available():
     import tensorflow as tf
 
@@ -146,10 +157,43 @@ class HubMixingTestKeras(unittest.TestCase):
         self.assertDictEqual(model.config, {"num": 10, "act": "gelu_fast"})
 
     @retry_endpoint
-    def test_push_to_hub(self):
-        REPO_NAME = repo_name("PUSH_TO_HUB")
+    def test_push_to_hub_keras_mixin_via_http_basic(self):
+        REPO_NAME = repo_name("PUSH_TO_HUB_KERAS_via_http")
+        repo_id = f"{USER}/{REPO_NAME}"
+
+        model = DummyModel()
+        model(model.dummy_inputs)
+
+        model.push_to_hub(
+            repo_id=repo_id,
+            api_endpoint=ENDPOINT_STAGING,
+            token=self._token,
+            config={"num": 7, "act": "gelu_fast"},
+        )
+
+        # Test model id exists
+        model_info = self._api.model_info(repo_id, token=self._token)
+        self.assertEqual(model_info.modelId, repo_id)
+
+        # Test config has been pushed to hub
+        tmp_config_path = hf_hub_download(
+            repo_id=repo_id, filename="config.json", use_auth_token=self._token
+        )
+        with open(tmp_config_path) as f:
+            self.assertEqual(json.load(f), {"num": 7, "act": "gelu_fast"})
+
+        # Delete tmp file and repo
+        os.remove(tmp_config_path)
+        self._api.delete_repo(repo_id=repo_id, token=self._token)
+
+    @retry_endpoint
+    @expect_deprecation("push_to_hub")
+    def test_push_to_hub_keras_mixin_via_git_deprecated(self):
+        REPO_NAME = repo_name("PUSH_TO_HUB_KERAS_via_git")
+        repo_id = f"{USER}/{REPO_NAME}"
         model = DummyModel()
         model(model.dummy_inputs)
+
         model.push_to_hub(
             repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
             api_endpoint=ENDPOINT_STAGING,
@@ -159,12 +203,9 @@ class HubMixingTestKeras(unittest.TestCase):
             config={"num": 7, "act": "gelu_fast"},
         )
 
-        model_info = self._api.model_info(
-            f"{USER}/{REPO_NAME}",
-        )
-        self.assertEqual(model_info.modelId, f"{USER}/{REPO_NAME}")
-
-        self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
+        model_info = self._api.model_info(repo_id)
+        self.assertEqual(model_info.modelId, repo_id)
+        self._api.delete_repo(repo_id=repo_id, token=self._token)
 
 
 @require_tf
@@ -361,53 +402,46 @@ class HubKerasSequentialTest(HubMixingTestKeras):
         self.assertTrue(new_model.config == {"num": 10, "act": "gelu_fast"})
 
     @retry_endpoint
-    def test_push_to_hub(self):
+    def test_push_to_hub_keras_sequential_via_http_basic(self):
         REPO_NAME = repo_name("PUSH_TO_HUB")
+        repo_id = f"{USER}/{REPO_NAME}"
         model = self.model_init()
-        model.build((None, 2))
-        push_to_hub_keras(
-            model,
-            repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
-            api_endpoint=ENDPOINT_STAGING,
-            use_auth_token=self._token,
-            git_user="ci",
-            git_email="ci@dummy.com",
-            config={"num": 7, "act": "gelu_fast"},
-            include_optimizer=False,
-        )
+        model = self.model_fit(model)
 
-        model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(
-            f"{USER}/{REPO_NAME}",
+        push_to_hub_keras(
+            model, repo_id=repo_id, token=self._token, api_endpoint=ENDPOINT_STAGING
         )
-        self.assertEqual(model_info.modelId, f"{USER}/{REPO_NAME}")
-
-        self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
+        model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(repo_id)
+        self.assertEqual(model_info.modelId, repo_id)
+        self.assertTrue("README.md" in [f.rfilename for f in model_info.siblings])
+        self.assertTrue("model.png" in [f.rfilename for f in model_info.siblings])
+        self._api.delete_repo(repo_id=repo_id, token=self._token)
 
     @retry_endpoint
-    def test_push_to_hub_model_card_build(self):
+    def test_push_to_hub_keras_sequential_via_http_plot_false(self):
         REPO_NAME = repo_name("PUSH_TO_HUB")
+        repo_id = f"{USER}/{REPO_NAME}"
         model = self.model_init()
-        model.build((None, 2))
+        model = self.model_fit(model)
+
         push_to_hub_keras(
             model,
-            repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
+            repo_id=repo_id,
+            token=self._token,
             api_endpoint=ENDPOINT_STAGING,
-            use_auth_token=self._token,
-            git_user="ci",
-            git_email="ci@dummy.com",
-        )
-        model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(
-            f"{USER}/{REPO_NAME}",
+            plot_model=False,
         )
-        self.assertTrue("README.md" in [f.rfilename for f in model_info.siblings])
-        self.assertTrue("model.png" in [f.rfilename for f in model_info.siblings])
-        self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
+        model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(repo_id)
+        self.assertFalse("model.png" in [f.rfilename for f in model_info.siblings])
+        self._api.delete_repo(repo_id=repo_id, token=self._token)
 
     @retry_endpoint
-    def test_push_to_hub_model_card_plot_false(self):
-        REPO_NAME = repo_name("PUSH_TO_HUB")
+    @expect_deprecation("push_to_hub_keras")
+    def test_push_to_hub_keras_sequential_via_git_deprecated(self):
+        REPO_NAME = repo_name("PUSH_TO_HUB_KERAS_sequential_via_git")
         model = self.model_init()
-        model = self.model_fit(model)
+        model.build((None, 2))
+
         push_to_hub_keras(
             model,
             repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
@@ -415,17 +449,21 @@ class HubKerasSequentialTest(HubMixingTestKeras):
             use_auth_token=self._token,
             git_user="ci",
             git_email="ci@dummy.com",
-            plot_model=False,
-        )
-        model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(
-            f"{USER}/{REPO_NAME}",
+            config={"num": 7, "act": "gelu_fast"},
+            include_optimizer=False,
         )
-        self.assertFalse("model.png" in [f.rfilename for f in model_info.siblings])
+
+        model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(f"{USER}/{REPO_NAME}")
+        self.assertEqual(model_info.modelId, f"{USER}/{REPO_NAME}")
+        self.assertTrue("README.md" in [f.rfilename for f in model_info.siblings])
+        self.assertTrue("model.png" in [f.rfilename for f in model_info.siblings])
         self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
 
     @retry_endpoint
-    def test_override_tensorboard(self):
-        REPO_NAME = repo_name("PUSH_TO_HUB")
+    def test_push_to_hub_keras_via_http_override_tensorboard(self):
+        """Test log directory is overwritten when pushing a keras model a 2nd time."""
+        REPO_NAME = repo_name("PUSH_TO_HUB_KERAS_via_http_override_tensorboard")
+        repo_id = f"{USER}/{REPO_NAME}"
         with tempfile.TemporaryDirectory() as tmpdirname:
             os.makedirs(f"{tmpdirname}/tb_log_dir")
             with open(f"{tmpdirname}/tb_log_dir/tensorboard.txt", "w") as fp:
@@ -434,29 +472,24 @@ class HubKerasSequentialTest(HubMixingTestKeras):
             model.build((None, 2))
             push_to_hub_keras(
                 model,
-                repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
+                repo_id=repo_id,
                 log_dir=f"{tmpdirname}/tb_log_dir",
                 api_endpoint=ENDPOINT_STAGING,
-                use_auth_token=self._token,
-                git_user="ci",
-                git_email="ci@dummy.com",
+                token=self._token,
             )
+
             os.makedirs(f"{tmpdirname}/tb_log_dir2")
             with open(f"{tmpdirname}/tb_log_dir2/override.txt", "w") as fp:
                 fp.write("Keras FTW")
             push_to_hub_keras(
                 model,
-                repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
+                repo_id=repo_id,
                 log_dir=f"{tmpdirname}/tb_log_dir2",
                 api_endpoint=ENDPOINT_STAGING,
-                use_auth_token=self._token,
-                git_user="ci",
-                git_email="ci@dummy.com",
+                token=self._token,
             )
 
-            model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(
-                f"{USER}/{REPO_NAME}",
-            )
+            model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(repo_id)
             self.assertTrue(
                 "logs/override.txt" in [f.rfilename for f in model_info.siblings]
             )
@@ -464,32 +497,37 @@ class HubKerasSequentialTest(HubMixingTestKeras):
                 "logs/tensorboard.txt" in [f.rfilename for f in model_info.siblings]
             )
 
-            self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
+            self._api.delete_repo(repo_id=repo_id, token=self._token)
 
     @retry_endpoint
-    def test_push_to_hub_model_kwargs(self):
-        REPO_NAME = repo_name("PUSH_TO_HUB")
+    def test_push_to_hub_keras_via_http_with_model_kwargs(self):
+        REPO_NAME = repo_name("PUSH_TO_HUB_KERAS_via_http_with_model_kwargs")
+        repo_id = f"{USER}/{REPO_NAME}"
+
         model = self.model_init()
         model = self.model_fit(model)
         push_to_hub_keras(
             model,
-            repo_path_or_name=f"{WORKING_REPO_DIR}/{REPO_NAME}",
+            repo_id=repo_id,
             api_endpoint=ENDPOINT_STAGING,
-            use_auth_token=self._token,
-            git_user="ci",
-            git_email="ci@dummy.com",
-            config={"num": 7, "act": "gelu_fast"},
+            token=self._token,
             include_optimizer=True,
             save_traces=False,
         )
 
-        model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(
-            f"{USER}/{REPO_NAME}",
-        )
-        self.assertEqual(model_info.modelId, f"{USER}/{REPO_NAME}")
+        model_info = HfApi(endpoint=ENDPOINT_STAGING).model_info(repo_id)
+        self.assertEqual(model_info.modelId, repo_id)
 
-        from_pretrained_keras(f"{WORKING_REPO_DIR}/{REPO_NAME}")
-        self.assertRaises(ValueError, msg="Exception encountered when calling layer*")
+        with tempfile.TemporaryDirectory() as tmpdirname:
+            Repository(
+                local_dir=tmpdirname,
+                clone_from=ENDPOINT_STAGING + "/" + repo_id,
+                use_auth_token=self._token,
+            )
+            from_pretrained_keras(tmpdirname)
+            self.assertRaises(
+                ValueError, msg="Exception encountered when calling layer*"
+            )
 
         self._api.delete_repo(repo_id=f"{REPO_NAME}", token=self._token)
 
diff --git a/tests/test_utils_deprecation.py b/tests/test_utils_deprecation.py
new file mode 100644
index 00000000..0f74acd7
--- /dev/null
+++ b/tests/test_utils_deprecation.py
@@ -0,0 +1,70 @@
+import unittest
+import warnings
+
+import pytest
+
+from huggingface_hub.utils._deprecation import (
+    _deprecate_arguments,
+    _deprecate_positional_args,
+)
+
+
+class TestDeprecationUtils(unittest.TestCase):
+    def test_deprecate_positional_args(self):
+        """Test warnings are triggered when using deprecated positional args."""
+
+        @_deprecate_positional_args(version="xxx")
+        def dummy_position_deprecated(a, *, b="b", c="c"):
+            pass
+
+        with warnings.catch_warnings():
+            # Assert no warnings when used correctly.
+            # Taken from https://docs.pytest.org/en/latest/how-to/capture-warnings.html#additional-use-cases-of-warnings-in-tests
+            warnings.simplefilter("error")
+            dummy_position_deprecated(a="A", b="B", c="C")
+            dummy_position_deprecated("A", b="B", c="C")
+
+        with pytest.warns(FutureWarning):
+            dummy_position_deprecated("A", "B", c="C")
+
+        with pytest.warns(FutureWarning):
+            dummy_position_deprecated("A", "B", "C")
+
+    def test_deprecate_arguments(self):
+        """Test warnings are triggered when using deprecated arguments."""
+
+        @_deprecate_arguments(version="xxx", deprecated_args={"c"})
+        def dummy_c_deprecated(a, b="b", c="c"):
+            pass
+
+        @_deprecate_arguments(version="xxx", deprecated_args={"b", "c"})
+        def dummy_b_c_deprecated(a, b="b", c="c"):
+            pass
+
+        with warnings.catch_warnings():
+            # Assert no warnings when used correctly.
+            # Taken from https://docs.pytest.org/en/latest/how-to/capture-warnings.html#additional-use-cases-of-warnings-in-tests
+            warnings.simplefilter("error")
+            dummy_c_deprecated("A")
+            dummy_c_deprecated("A", "B")
+            dummy_c_deprecated("A", b="B")
+
+            dummy_b_c_deprecated("A")
+
+        with pytest.warns(FutureWarning):
+            dummy_c_deprecated("A", "B", "C")
+
+        with pytest.warns(FutureWarning):
+            dummy_c_deprecated("A", c="C")
+
+        with pytest.warns(FutureWarning):
+            dummy_c_deprecated("A", b="B", c="C")
+
+        with pytest.warns(FutureWarning):
+            dummy_b_c_deprecated("A", b="B")
+
+        with pytest.warns(FutureWarning):
+            dummy_b_c_deprecated("A", c="C")
+
+        with pytest.warns(FutureWarning):
+            dummy_b_c_deprecated("A", b="B", c="C")
diff --git a/tests/testing_utils.py b/tests/testing_utils.py
index 58188c89..7d306d3b 100644
--- a/tests/testing_utils.py
+++ b/tests/testing_utils.py
@@ -6,9 +6,12 @@ import uuid
 from contextlib import contextmanager
 from distutils.util import strtobool
 from enum import Enum
-from typing import Optional
+from functools import wraps
+from typing import Callable, Optional
 from unittest.mock import patch
 
+import pytest
+
 from huggingface_hub.utils import logging
 from requests.exceptions import HTTPError
 from tests.testing_constants import ENDPOINT_PRODUCTION, ENDPOINT_PRODUCTION_URL_SCHEME
@@ -232,3 +235,57 @@ def retry_endpoint(function, number_of_tries: int = 3, wait_time: int = 5):
             return function(*args, **kwargs)
 
     return decorator
+
+
+def expect_deprecation(function_name: str):
+    """
+    Decorator to flag tests that we expect to use deprecated arguments.
+
+    Parameters:
+        function_name (`str`):
+            Name of the function that we expect to use in a deprecated way.
+
+    NOTE: if a test is expected to warns FutureWarnings but is not, the test will fail.
+
+    Context: over time, some arguments/methods become deprecated. In order to track
+             deprecation in tests, we run pytest with flag `-Werror::FutureWarning`.
+             In order to keep old tests during the deprecation phase (before removing
+             the feature completely) without changing them internally, we can flag
+             them with this decorator.
+    See full discussion in https://github.com/huggingface/huggingface_hub/pull/952.
+
+    This decorator works hand-in-hand with the `_deprecate_arguments` and
+    `_deprecate_positional_args` decorators.
+
+    Example
+    ```py
+    # in src/hub_mixins.py
+    from .utils._deprecation import _deprecate_arguments
+
+    @_deprecate_arguments(version="0.12", deprecated_args={"repo_url"})
+    def push_to_hub(...):
+        (...)
+
+    # in tests/test_something.py
+    from .testing_utils import expect_deprecation
+
+    class SomethingTest(unittest.TestCase):
+        (...)
+
+        @expect_deprecation("push_to_hub"):
+        def test_push_to_hub_git_version(self):
+            (...)
+            push_to_hub(repo_url="something") <- Should warn with FutureWarnings
+            (...)
+    ```
+    """
+
+    def _inner_decorator(test_function: Callable) -> Callable:
+        @wraps(test_function)
+        def _inner_test_function(*args, **kwargs):
+            with pytest.warns(FutureWarning, match=f".*'{function_name}'.*"):
+                return test_function(*args, **kwargs)
+
+        return _inner_test_function
+
+    return _inner_decorator