Repository: pypa/pip

Issue:
Allow customized gone_in sentence for deprecation messages
**What's the problem this feature will solve?**
The current `pip._internal.utils.deprecation:deprecated` function, in at least one case (#10128), leads to an awkward and confusing deprecation message. I would like to make the message more customizable so that awkward wording is reduced.

**Describe the solution you'd like**
See https://github.com/pypa/pip/pull/10129#issuecomment-875980520. Namely, I would like to introduce a new kwarg `gone_in_message` with default value `DEPRECATION_GONE_IN_MESSAGE = "pip {} will remove support for this functionality."`. I would also like to be able to set `gone_in_message=""` to omit the automatic generation of such a message.

This would allow me to achieve my desired wording:

> pip currently copies the source tree into a temporary directory before building it. pip 21.3 will build packages in-place within the original source tree ("in-tree build"). This new behavior can be tested with the --use-feature=in-tree-build flag. Future versions of pip will remove support for out-of-tree builds, and for this testing flag. You can find discussion regarding this at {link}.

**Alternative Solutions**
I have tried to reword things to work with the builtin message phrasing, but it forced me to use awkward sentences like "Regarding...". See #10129 for further discussion.


Discussion:
(I have prepared a commit in order to make a PR, but my current wifi is blocking port 22 so I can't push yet...)
FWIW you can push via HTTPS (port 443). I use this often when I need to use public wifi 🙂 
Part of the point of the whole `deprecated` function is to allow the author to avoid thinking about the specific wording -- so that it's standardised and we don't spend time on iterating that when doing deprecations.

My preference is that we update the default wording we use for gone_in, to something like "In pip {version}, this behaviour change will be enforced." or something broader like that, instead of making this configurable.

In other words, I'm a -1 on adding an argument for making this an argument and making this possible. I'm a +1 to changing this to help us avoid weird sentences in the future.

From https://github.com/pypa/pip/pull/10129#issuecomment-873635608:

> Some part of me wants to change it to something like:
> 
> > DEPRECATION: pip copied a local directory before performing a build. This behaviour will change to stop copying and performing a build "in-tree".
> > pip 21.3 will enforce this behaviour change. You can use `--use-feature=in-tree-build` to test the upcoming behaviour.
> > Discussion regarding this change is at {link}.
> 
> This would, however, be a two-fold change -- partly here and partly in the `deprecated` function.

What bothers me about https://github.com/pypa/pip/issues/10165#issuecomment-883235805 is that we are suggesting a flag which is already planned for deprecation. But based on the consensus from https://github.com/pypa/pip/pull/10129#issuecomment-878069346 it seems that my concern is misguided.

So @pradyunsg, is your idea that you would like to add a new optional "preview" sentence like the following?

> You can temporarily use `--use-feature={preview_flag}` to test the upcoming behavior.
> So @pradyunsg, is your idea that you would like to add a new optional "preview" sentence like the following?

That + changing the "remove this functionality" language to "enforce this change" (specific wording is in the quoted comments).
Here is my suggested wording for #10129, sentence by sentence, in the proper order, and according to whether the behavior change is in the past or future. I will adjust #10167 accordingly. Please request any desired adjustments.

Reason sentence (future or past):
> DEPRECATION: pip copied the source tree into a temporary directory before building it. This is changing so that packages are built in-place within the original source tree ("in-tree build").

Version sentence (future):
> pip 21.3 will enforce this behavior change.

Version sentence (past):
> This behavior change has been enforced since pip 21.3.

Preview sentence (future):
> You can temporarily use the flag --use-feature=in-tree-build to test the upcoming behavior.

Preview sentence (past):
[empty]

Link sentence (future or past):
> Discussion can be found at {link}.


I don't think we need any "past" phrasing. Once the default changes, we don't need to tell the users about it -- they'll either notice the change and deal with it; or be unaffected by the change.

The deprecated helper actually raises an exception, if you run it on a version that's equal or higher than `gone_in`.
I believe we need some sort of "past" phrasing in order to fix #10223. In any case, as you suggest, we should adjust the message (telling the users or not) based on whether the change is already implemented.

As you point out, the logic was already implemented for whether the message gets raised as a warning or an exception. Now #10167 adjusts the message in the appropriate way.

Now it is possible to do

```python
deprecated(
    reason='pip copied the source tree into a temporary directory before building it. This is changing so that packages are built in-place within the original source tree ("in-tree build").',
    replacement=None,
    gone_in="21.3",
    feature_flag="in-tree-build",
    issue=7555,
)
```

When the change is upcoming (future), then the following warning occurs:
```text
<stdin>:1: PipDeprecationWarning: DEPRECATION: pip copied the source tree into a temporary
directory before building it. This is changing so that packages are built in-place within
the original source tree ("in-tree build"). pip 21.3 will enforce this behavior change.
You can temporarily use the flag --use-feature=in-tree-build to test the upcoming behavior.
Discussion can be found at https://github.com/pypa/pip/issues/7555.
```

When the change has already taken place, the following exception occurs:
```text
pip._internal.utils.deprecation.PipDeprecationWarning: DEPRECATION: pip copied the source
tree into a temporary directory before building it. This is changing so that packages are
built in-place within the original source tree ("in-tree build"). This behavior change has
been enforced since pip 21.3. Discussion can be found at
https://github.com/pypa/pip/issues/7555.
```

Resolving patch:
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -11,6 +11,15 @@
 from pip import __version__ as current_version
 
 DEPRECATION_MSG_PREFIX = "DEPRECATION: "
+DEPRECATION_MESSAGE = DEPRECATION_MSG_PREFIX + "{reason}"
+GONE_IN_MESSAGE_FUTURE = "pip {gone_in} will enforce this behavior change."
+GONE_IN_MESSAGE_PAST = "This behavior change has been enforced since pip {gone_in}."
+REPLACEMENT_MESSAGE = "A possible replacement is {replacement}."
+FEATURE_FLAG_MESSAGE = (
+    "You can temporarily use the flag --use-feature={feature_flag} "
+    "to test the upcoming behavior."
+)
+ISSUE_MESSAGE = "Discussion can be found at https://github.com/pypa/pip/issues/{issue}."
 
 
 class PipDeprecationWarning(Warning):
@@ -56,20 +65,24 @@ def deprecated(
     reason: str,
     replacement: Optional[str],
     gone_in: Optional[str],
+    feature_flag: Optional[str] = None,
     issue: Optional[int] = None,
 ) -> None:
     """Helper to deprecate existing functionality.
 
     reason:
         Textual reason shown to the user about why this functionality has
-        been deprecated.
+        been deprecated. Should be a complete sentence.
     replacement:
         Textual suggestion shown to the user about what alternative
         functionality they can use.
     gone_in:
         The version of pip does this functionality should get removed in.
-        Raises errors if pip's current version is greater than or equal to
+        Raises an error if pip's current version is greater than or equal to
         this.
+    feature_flag:
+        Command-line flag of the form --use-feature={feature_flag} for testing
+        upcoming functionality.
     issue:
         Issue number on the tracker that would serve as a useful place for
         users to find related discussion and provide feedback.
@@ -77,28 +90,38 @@ def deprecated(
     Always pass replacement, gone_in and issue as keyword arguments for clarity
     at the call site.
     """
-
+    # Determine whether or not the feature is already gone in this version.
+    is_gone = gone_in is not None and parse(current_version) >= parse(gone_in)
+    # Allow variable substitutions within the "reason" variable.
+    formatted_reason = reason.format(gone_in=gone_in)
     # Construct a nice message.
     #   This is eagerly formatted as we want it to get logged as if someone
     #   typed this entire message out.
+    formatted_deprecation_message = DEPRECATION_MESSAGE.format(reason=formatted_reason)
+    gone_in_message = GONE_IN_MESSAGE_PAST if is_gone else GONE_IN_MESSAGE_FUTURE
+    formatted_gone_in_message = (
+        gone_in_message.format(gone_in=gone_in) if gone_in else None
+    )
+    formatted_replacement_message = (
+        REPLACEMENT_MESSAGE.format(replacement=replacement) if replacement else None
+    )
+    formatted_feature_flag_message = (
+        None
+        if is_gone or not feature_flag
+        else FEATURE_FLAG_MESSAGE.format(feature_flag=feature_flag)
+    )
+    formatted_issue_message = ISSUE_MESSAGE.format(issue=issue) if issue else None
     sentences = [
-        (reason, DEPRECATION_MSG_PREFIX + "{}"),
-        (gone_in, "pip {} will remove support for this functionality."),
-        (replacement, "A possible replacement is {}."),
-        (
-            issue,
-            (
-                "You can find discussion regarding this at "
-                "https://github.com/pypa/pip/issues/{}."
-            ),
-        ),
+        formatted_deprecation_message,
+        formatted_gone_in_message,
+        formatted_replacement_message,
+        formatted_feature_flag_message,
+        formatted_issue_message,
     ]
-    message = " ".join(
-        template.format(val) for val, template in sentences if val is not None
-    )
+    message = " ".join(sentence for sentence in sentences if sentence)
 
-    # Raise as an error if it has to be removed.
-    if gone_in is not None and parse(current_version) >= parse(gone_in):
+    # Raise as an error if the functionality is gone.
+    if is_gone:
         raise PipDeprecationWarning(message)
-
-    warnings.warn(message, category=PipDeprecationWarning, stacklevel=2)
+    else:
+        warnings.warn(message, category=PipDeprecationWarning, stacklevel=2)


Test patch:
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -934,9 +934,9 @@ def test_deprecated_message_reads_well():
 
     assert message == (
         "DEPRECATION: Stop doing this! "
-        "pip 1.0 will remove support for this functionality. "
+        "This behavior change has been enforced since pip 1.0. "
         "A possible replacement is to be nicer. "
-        "You can find discussion regarding this at "
+        "Discussion can be found at "
         "https://github.com/pypa/pip/issues/100000."
     )