deprecate: introduce ui.deprecate

Summary:
Adds a simple ui.deprecate() function for deprecating code paths. It
has several levels of deprecation and can optionally automatically escalate
through those levels across a specified time frame.

The levels are:
- Log - just logs the usage to scuba
- Warn - same as Log but also prints a warning to the user
- Slow - same as Warn but also sleeps for 2 seconds
- OptIn - throws an exception, but hints that the exception can be bypassed by
  setting 'deprecated.bypass-XXXX=True'
- Block - throws an exception that cannot be bypassed

I also call it from a few locations that are expected to be deprecated already.

Reviewed By: markbt

Differential Revision: D21890496

fbshipit-source-id: faddb301888ef75cc71d46ffb7374f3fe3d044bd
This commit is contained in:
Durham Goode 2020-06-08 16:18:49 -07:00 committed by Facebook GitHub Bot
parent e28bde4df3
commit 1d65457032
7 changed files with 157 additions and 1 deletions

View File

@ -125,8 +125,11 @@ class dirstate(object):
opener.makedirs("treestate")
self._mapcls = treestate.treestatemap
elif istreedirstate:
ui.deprecate("treedirstate", "treedirstate is replaced by treestate")
self._mapcls = treedirstate.treedirstatemap
else:
if "eden" not in repo.requirements:
ui.deprecate("dirstatemap", "dirstatemap is replaced by treestate")
self._mapcls = dirstatemap
self._fs = filesystem.physicalfilesystem(root, self)

View File

@ -151,6 +151,10 @@ class Abort(Hint, Context, Component, Exception):
exitcode = 255
class DeprecatedError(Abort):
__bytes__ = _tobytes
class UncommitedChangesAbort(Abort):
"""Raised if there are uncommited changs and the command requires a clean
working copy

View File

@ -934,30 +934,35 @@ def allsuccessors(obsstore, nodes, ignoreflags=0):
def marker(repo, data):
movemsg = "obsolete.marker moved to obsutil.marker"
repo.ui.deprecwarn(movemsg, "4.3")
repo.ui.deprecate("obsolete.marker", movemsg)
return obsutil.marker(repo, data)
def getmarkers(repo, nodes=None, exclusive=False):
movemsg = "obsolete.getmarkers moved to obsutil.getmarkers"
repo.ui.deprecwarn(movemsg, "4.3")
repo.ui.deprecate("obsolete.getmarkers", movemsg)
return obsutil.getmarkers(repo, nodes=nodes, exclusive=exclusive)
def exclusivemarkers(repo, nodes):
movemsg = "obsolete.exclusivemarkers moved to obsutil.exclusivemarkers"
repo.ui.deprecwarn(movemsg, "4.3")
repo.ui.deprecate("obsolete.exclusivemarkers", movemsg)
return obsutil.exclusivemarkers(repo, nodes)
def foreground(repo, nodes):
movemsg = "obsolete.foreground moved to obsutil.foreground"
repo.ui.deprecwarn(movemsg, "4.3")
repo.ui.deprecate("obsolete.foreground", movemsg)
return obsutil.foreground(repo, nodes)
def successorssets(repo, initialnode, cache=None):
movemsg = "obsolete.successorssets moved to obsutil.successorssets"
repo.ui.deprecwarn(movemsg, "4.3")
repo.ui.deprecate("obsolete.successorssets", movemsg)
return obsutil.successorssets(repo, initialnode, cache=cache)
@ -1049,6 +1054,7 @@ def _computeobsoleteset(repo):
def _computeunstableset(repo):
msg = "'unstable' volatile set is deprecated, " "use 'orphan'"
repo.ui.deprecwarn(msg, "4.4")
repo.ui.deprecate("unstable-obsolete-set", msg)
return _computeorphanset(repo)
@ -1088,6 +1094,7 @@ def _computeextinctset(repo):
def _computebumpedset(repo):
msg = "'bumped' volatile set is deprecated, " "use 'phasedivergent'"
repo.ui.deprecwarn(msg, "4.4")
repo.ui.deprecate("obsolete.bumped", msg)
return _computephasedivergentset(repo)
@ -1121,6 +1128,7 @@ def _computephasedivergentset(repo):
def _computedivergentset(repo):
msg = "'divergent' volatile set is deprecated, " "use 'contentdivergent'"
repo.ui.deprecwarn(msg, "4.4")
repo.ui.deprecate("obsolete.divergent", msg)
return _computecontentdivergentset(repo)

View File

@ -681,6 +681,9 @@ def branch(repo, subset, x):
def bumped(repo, subset, x):
msg = "'bumped()' is deprecated, " "use 'phasedivergent()'"
repo.ui.deprecwarn(msg, "4.4")
repo.ui.deprecate(
"bumped-revset", "bumped() has been replaced with phasedivergent()"
)
return phasedivergent(repo, subset, x)
@ -929,6 +932,9 @@ def destination(repo, subset, x):
def divergent(repo, subset, x):
msg = "'divergent()' is deprecated, " "use 'contentdivergent()'"
repo.ui.deprecwarn(msg, "4.4")
repo.ui.deprecate(
"divergent-revset", "divergent() has been replaced with contentdivergent()"
)
return contentdivergent(repo, subset, x)
@ -2320,6 +2326,7 @@ def _substringmatcher(pattern, casesensitive=True):
def unstable(repo, subset, x):
msg = "'unstable()' is deprecated, " "use 'orphan()'"
repo.ui.deprecwarn(msg, "4.4")
repo.ui.deprecate("unstable-revset", "unstable() has been replaced with orphan()")
return orphan(repo, subset, x)

View File

@ -981,6 +981,9 @@ def showtroubles(repo, **args):
"""
msg = "'troubles' is deprecated, " "use 'instabilities'"
repo.ui.deprecwarn(msg, "4.4")
repo.ui.deprecate(
"troubles-template", "troubles has been replaced with instabilities"
)
return showinstabilities(repo=repo, **args)

View File

@ -26,7 +26,7 @@ import sys
import tempfile
import time
import traceback
from enum import Enum
from enum import IntEnum
from typing import Any, Dict, List, Optional, Tuple, Union
import bindings
@ -156,6 +156,19 @@ _unset = uiconfig._unset
_reqexithandlers = []
class deprecationlevel(IntEnum):
# Logs usage of the deprecated code path
Log = 0
# Prints a warning on usage of the deprecated code path
Warn = 1
# Inserts a 2 second sleep to the deprecated code path
Slow = 2
# Throws an exception, but a config can be used to opt in to the deprecated feature
Optin = 3
# Throws a non-bypassable exception
Block = 4
class ui(object):
def __init__(self, src=None):
"""Create a fresh new ui object if no src given
@ -1503,6 +1516,77 @@ class ui(object):
self._logsample(service, *origmsg, **opts)
def deprecate(
self, name, message, maxlevel=deprecationlevel.Log, startstr=None, endstr=None
):
"""marks a code path as deprecated
The default behavior is to simply log the usage of the deprecated path,
but `maxlevel` can be used to specify stricter deprecation strategies.
If `start` and `end` are provided, the deprecation level will be slowly
increased over the course of the `start` and `end` time, reaching the
specified `maxlevel` at the end time.
"""
level = maxlevel
if startstr is not None and endstr is not None:
now = time.time()
start = util.parsedate(startstr)[0]
end = util.parsedate(endstr)[0]
# Linearly interpolate to get the current level
percent = float(now - start) / float(end - start)
level = max(0, min(int(percent * maxlevel), maxlevel))
self.log(
"deprecated",
message,
feature=name,
level=int(level),
version=util.version(),
)
bypassed = self.configbool("deprecated", "bypass-%s" % name)
if level == deprecationlevel.Block:
raise error.DeprecatedError(
_("feature '%s' is disabled: %s") % (name, message)
)
elif level == deprecationlevel.Optin and not bypassed:
hint = (
_(
"set config `deprecated.bypass-%s=True` to temporarily bypass this block"
)
% name
)
if endstr is not None and maxlevel == deprecationlevel.Block:
hint = _(
"set config `deprecated.bypass-%s=True` to bypass this block, but note the feature will be completely disabled on %s"
) % (name, endstr)
raise error.DeprecatedError(
_("feature '%s' is disabled: %s") % (name, message), hint=hint
)
elif level >= deprecationlevel.Slow and not bypassed:
self.warn(
_(
"warning: sleeping for 2 seconds because feature '%s' is deprecated: %s\n"
)
% (name, message)
)
self.warn(
_(
"note: the feature will be completely disabled soon, so please migrate off\n"
)
)
time.sleep(2)
elif level >= deprecationlevel.Warn:
self.warn(_("warning: feature '%s' is deprecated: %s\n") % (name, message))
self.warn(
_(
"note: the feature will be completely disabled soon, so please migrate off\n"
)
)
else:
self.develwarn(_("feature '%s' is deprecated: %s\n") % (name, message))
def _computesamplingfilters(self):
filtermap = {}
for k in self.configitems("sampling"):

View File

@ -0,0 +1,47 @@
#chg-compatible
$ configure modern
$ newext deprecatecmd <<EOF
> from edenscm.mercurial import registrar
> cmdtable = {}
> command = registrar.command(cmdtable)
> @command('testdeprecate', [], 'hg testdeprecate')
> def testdeprecate(ui, repo, level):
> ui.deprecate("test-feature", "blah blah message", int(level))
> EOF
$ hg init client
$ cd client
$ hg testdeprecate 0
devel-warn: feature 'test-feature' is deprecated: blah blah message
at: $TESTTMP/deprecatecmd.py:6 (testdeprecate)
$ hg blackbox | grep deprecated
* [legacy][deprecated] blah blah message (glob)
* [legacy][develwarn] devel-warn: feature 'test-feature' is deprecated: blah blah message (glob)
$ hg testdeprecate 1
warning: feature 'test-feature' is deprecated: blah blah message
note: the feature will be completely disabled sooned, so please migrate off
$ hg testdeprecate 2
warning: sleeping for 2 seconds because feature 'test-feature' is deprecated: blah blah message
note: the feature will be completely disabled sooned, so please migrate off
$ hg testdeprecate 3
abort: feature 'test-feature' is disabled: blah blah message
(set config `deprecated.bypass-test-feature=True` to temporarily bypass this block)
[255]
$ hg testdeprecate 3 --config deprecated.bypass-test-feature=True
warning: feature 'test-feature' is deprecated: blah blah message
note: the feature will be completely disabled sooned, so please migrate off
$ hg testdeprecate 4
abort: feature 'test-feature' is disabled: blah blah message
[255]
$ hg testdeprecate 4 --config deprecated.bypass-test-feature=True
abort: feature 'test-feature' is disabled: blah blah message
[255]