summaryrefslogtreecommitdiff
path: root/mercurial_extension_utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'mercurial_extension_utils.py')
-rw-r--r--mercurial_extension_utils.py633
1 files changed, 498 insertions, 135 deletions
diff --git a/mercurial_extension_utils.py b/mercurial_extension_utils.py
index 62e916c..82b9f51 100644
--- a/mercurial_extension_utils.py
+++ b/mercurial_extension_utils.py
@@ -30,30 +30,176 @@
#
# See README.txt for more details.
-"""Utility functions useful during Mercurial extension writing
+"""
+Utility functions useful during Mercurial extension writing
+
+Mostly related to configuration processing, path matching and similar
+activities. I extracted this module once I noticed a couple of my
+extensions need the same or similar functions.
-Mostly related to configuration processing, path matching and
-similar activities. I extracted this module once I noticed a couple
-of my extensions need the same or similar functions.
+Part of this module is about wrapping some incompatibilities between
+various Mercurial versions.
Note: file-related functions defined here use / as path separator,
-even on Windows. Backslashes in params should usually work too, but
+even on Windows. Backslashes in params should usually work too, but
returned paths are always /-separated.
-Documentation examples in this module use Unix paths, see
-file mercurial_extension_utils_win.py for windows doctests.
+Documentation examples in this module use Unix paths and Python3
+syntax, see tests/*doctests.py for versions with Python2 and/or
+Windows examples.
+
+On Python3 we mostly follow Mercurial's own idea of using binary
+strings, wrapped as bytestr where useful.
"""
+from __future__ import print_function # For doctests
from mercurial.i18n import _
-
import re
import os
import sys
+import types
from collections import deque
# pylint: disable=line-too-long,invalid-name
###########################################################################
+# Tiny py2/py3 compatibility layer (used internally)
+###########################################################################
+
+# We mostly defer to Mercurial's own compatibility layer - if we are
+# on py3, it exists (elsewhere no chances for working hg, versions
+# without meecurial.pycompat don't install on py3), if we are on py2
+# it may exist or not depending on Mercurial version. It it doesn't or
+# is incomplete, we fix missing parts.
+
+# Trick to avoid separate file craetion
+_compat_name = 'mercurial_extension_utils.pycompat'
+pycompat = types.ModuleType(_compat_name)
+# pycompat = imp.new_module(_compat_name)
+sys.modules[_compat_name] = pycompat
+
+pycompat.identity = lambda a: a
+
+try:
+ from mercurial import pycompat as _pycompat
+
+ for func in [ # py2 / py3
+ 'ispy3', # False / True
+ 'bytestr', # str / bytes subclass with some glue to make it more py2str-like
+ # and smart constructor which accepts bytestr, bytes and str
+ 'unicode', # unicode / str
+ 'bytechr', # chr / chr(x).encode() (more efficient equiv)
+ 'maybebytestr', # identity / upgrade bytes to bytestr, on nonbytes identity
+ 'sysbytes', # identity / x.encode(utf-8)
+ 'sysstr', # identity / make it native str, safely (convert bytes to str, leave str)
+ 'strkwargs', # identity / if any key is bytes, make it str
+ 'byteskwargs', # identity / if nay key is str, make it bytes
+ ]:
+ if hasattr(_pycompat, func):
+ setattr(pycompat, func, getattr(_pycompat, func))
+except ImportError:
+ pass
+
+if not hasattr(pycompat, 'ispy3'):
+ pycompat.ispy3 = (sys.version_info[0] >= 3)
+if not pycompat.ispy3:
+ # Only on py2 we can be on old mercurial where pycompat doesn't exist,
+ # or has less functions. Here we fix missing bits (that's mostly copy
+ # and paste from pycompat in modern mercurial).
+ if not hasattr(pycompat, 'bytechr'):
+ pycompat.bytechr = chr
+ if not hasattr(pycompat, 'bytestr'):
+ pycompat.bytestr = str
+ if not hasattr(pycompat, 'maybebytestr'):
+ pycompat.maybebytestr = pycompat.identity
+ if not hasattr(pycompat, 'iterbytestr'):
+ pycompat.iterbytestr = iter
+ if not hasattr(pycompat, 'strkwargs'):
+ pycompat.strkwargs = pycompat.identity
+ if not hasattr(pycompat, 'byteskwargs'):
+ pycompat.byteskwargs = pycompat.identity
+ if not hasattr(pycompat, 'sysbytes'):
+ pycompat.sysbytes = pycompat.identity
+ if not hasattr(pycompat, 'sysstr'):
+ pycompat.sysstr = pycompat.identity
+ if not hasattr(pycompat, 'unicode'):
+ pycompat.unicode = unicode
+
+if pycompat.ispy3:
+
+ def dict_iteritems(dic):
+ return dic.items()
+
+ if sys.version_info < (3, 7):
+
+ def re_escape(txt):
+ # Pythons 3.5 and 3.6 fail on bytestrings. Let's fix it similarly to 3.7 fix
+ if isinstance(txt, str):
+ return re.escape(txt)
+ else:
+ v = str(txt, 'latin1')
+ return re.escape(v).encode('latin1')
+
+ else:
+
+ def re_escape(txt):
+ return re.escape(txt)
+
+else:
+
+ def dict_iteritems(dic):
+ return dic.iteritems()
+
+ def re_escape(txt):
+ return re.escape(txt)
+
+
+###########################################################################
+# Logging support
+###########################################################################
+
+if pycompat.ispy3:
+ def _log_normalize(args):
+ return tuple(pycompat.bytestr(arg) for arg in args)
+else:
+ _log_normalize = pycompat.identity
+
+
+def ui_string(message, *args):
+ """
+ Idiomatically equivalent to::
+
+ return _(message) % args
+
+ but handles idiosyncracies of mercurial.ui logging under py3 where
+ bytestring and byteargs are expected, and None's and normal strings
+ cause problems.
+
+ Typical use (replace debug with any other method):
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> ui.debug(ui_string("Simple text"))
+ >>> ui.debug(ui_string(b"Simple binary"))
+ >>> ui.debug(ui_string("This is %s and %s and %s", b'bin', u'txt', None))
+ >>> ui.debug(ui_string(b"This is %s and %s and %s", b'bin', u'txt', None))
+
+ This works because we reasonably create binary strings, as ui expects:
+
+ >>> ui_string("Simple text")
+ b'Simple text'
+ >>> ui_string(b"Simple binary")
+ b'Simple binary'
+ >>> ui_string("This is %s and %s and %s", b'bin', u'txt', None)
+ b'This is bin and txt and None'
+ >>> ui_string(b"This is %s and %s and %s", b'bin', u'txt', None)
+ b'This is bin and txt and None'
+ """
+ # _ seems to handle normalization, so we don't have to.
+ return _(message) % _log_normalize(args)
+
+
+
+###########################################################################
# Directory matching in various shapes
###########################################################################
@@ -74,11 +220,27 @@ def normalize_path(path):
'/some/where'
>>> normalize_path("../../../some/where")
'/home/lordvader/some/where'
+
+ In case of python3, result is also forced to bytestr if not in such
+ form yet:
+
+ >>> type(normalize_path("~/src"))
+ <class 'mercurial.pycompat.bytestr'>
+ >>> type(normalize_path(b"~/src"))
+ <class 'mercurial.pycompat.bytestr'>
+
+ This way bytes input is also properly handled:
+
+ >>> normalize_path(b"~/src")
+ '/home/lordvader/src'
+ >>> normalize_path(b"/some/where/")
+ '/some/where'
"""
- reply = os.path.abspath(os.path.expanduser(path))
+ reply = pycompat.bytestr(
+ os.path.abspath(os.path.expanduser(path)))
if os.name == 'nt':
- reply = reply.replace('\\', '/')
- return reply.rstrip('/')
+ reply = reply.replace(b'\\', b'/')
+ return pycompat.bytestr(reply.rstrip(b'/'))
def belongs_to_tree(child, parent):
@@ -114,6 +276,23 @@ def belongs_to_tree(child, parent):
Note: even on Windows, / is used as path separator (both on input,
and on output).
+ On Python3 both kinds of strings are handled as long as arguments are compatible
+ and result is forced to bytestr:
+
+ >>> x = belongs_to_tree(b"/tmp/sub/dir", b"/tmp")
+ >>> x
+ '/tmp'
+ >>> type(x)
+ <class 'mercurial.pycompat.bytestr'>
+ >>> x = belongs_to_tree(b"/usr/sub", b"/tmp")
+ >>> x
+
+ >>> x = belongs_to_tree(b"/home/lordvader/devel/webapps", b"~lordvader/devel")
+ >>> x
+ '/home/lordvader/devel'
+ >>> type(x)
+ <class 'mercurial.pycompat.bytestr'>
+
:param child: tested directory (preferably absolute path)
:param parent: tested parent (will be tilda-expanded, so things
like ~/work are OK)
@@ -126,11 +305,11 @@ def belongs_to_tree(child, parent):
# pfx = normalize_path(os.path.commonprefix([child, parent]))
# return pfx == true_parent and true_parent or None
if os.name != 'nt':
- matches = child == parent or child.startswith(parent + '/')
+ matches = child == parent or child.startswith(parent + b'/')
else:
lower_child = child.lower()
lower_parent = parent.lower()
- matches = lower_child == lower_parent or lower_child.startswith(lower_parent + '/')
+ matches = lower_child == lower_parent or lower_child.startswith(lower_parent + b'/')
return matches and parent or None
@@ -157,12 +336,23 @@ def belongs_to_tree_group(child, parents):
Note: even on Windows, / is used as path separator (both on input,
and on output).
+ On Py3 both kinds of strings are handled as long as arguments are compatible:
+
+ >>> x = belongs_to_tree_group(b"/tmp/sub/dir", [b"/bin", b"/tmp"])
+ >>> x
+ '/tmp'
+ >>> type(x)
+ <class 'mercurial.pycompat.bytestr'>
+
+ >>> belongs_to_tree_group(b"/home/lordvader/src/apps", [b"~/src", b"/home/lordvader"])
+ '/home/lordvader/src'
+
:param child: tested directory (preferably absolute path)
:param parents: tested parents (list or tuple of directories to
test, will be tilda-expanded)
"""
child = normalize_path(child)
- longest_parent = ''
+ longest_parent = pycompat.bytestr(b'')
for parent in parents:
canon_path = belongs_to_tree(child, parent)
if canon_path:
@@ -230,17 +420,46 @@ class DirectoryPattern(object):
{}
>>> pat.search('/home/lordvader/dev/acme/subdir')
>>> pat.search('/home/lordvader/dev')
+
+ On Py3 binary strings can be used as well (and results are wrapped
+ as bytestr)::
+
+ >>> pat = DirectoryPattern(b'~/src/{suffix}')
+ >>> pat.is_valid()
+ True
+ >>> pat.search(b"/opt/repos/abcd")
+ >>> x = pat.search(b"~/src/repos/in/tree")
+ >>> x
+ {'suffix': 'repos/in/tree'}
+ >>> type(x['suffix'])
+ <class 'mercurial.pycompat.bytestr'>
+ >>> [type(t) for t in x.keys()]
+ [<class 'str'>]
+
+ >>> pat.search(b"/home/lordvader/src/repos/here" if os.system != 'nt' else b"c:/users/lordvader/src/repos/here")
+ {'suffix': 'repos/here'}
+ >>> pat.search(b"/home/lordvader/src")
+
+ >>> pat = DirectoryPattern(b'~lordvader/devel/(item)')
+ >>> pat.search(b"/opt/repos/abcd")
+ >>> pat.search(b"~/devel/libxuza")
+ {'item': 'libxuza'}
+ >>> pat.search(b"~/devel/libs/libxuza")
+ >>> pat.search(b"/home/lordvader/devel/webapp")
+ {'item': 'webapp'}
+ >>> pat.search(b"/home/lordvader/devel")
+
"""
# Regexps used during pattern parsing
- _re_pattern_lead = re.compile(r' ^ ([^{}()]*) ([({]) (.*) $', re.VERBOSE)
- _re_closure = {'{': re.compile(r'^ ([a-zA-Z_]+) [}] (.*) $', re.VERBOSE),
- '(': re.compile(r'^ ([a-zA-Z_]+) [)] (.*) $', re.VERBOSE)}
+ _re_pattern_lead = re.compile(b' ^ ([^{}()]*) ([({]) (.*) $', re.VERBOSE)
+ _re_closure = {b'{': re.compile(b'^ ([a-zA-Z_]+) [}] (.*) $', re.VERBOSE),
+ b'(': re.compile(b'^ ([a-zA-Z_]+) [)] (.*) $', re.VERBOSE)}
# (text inside (braces) or {braces} is restricted as it is used within regexp
# Regexp snippets used to match escaped parts
- _re_match_snippet = {'{': r'.+',
- '(': r'[^/\\]+'}
+ _re_match_snippet = {b'{': b'.+',
+ b'(': b'[^/\\\\]+'}
def __init__(self, pattern_text, ui=None):
"""Parses given pattern. Doesn't raise, in case of invalid patterns
@@ -253,7 +472,7 @@ class DirectoryPattern(object):
self._pattern_re = None # Will stay such if we fail somewhere here
# Convert pattern to regexp
- rgxp_text = '^'
+ rgxp_text = b'^'
while text:
match = self._re_pattern_lead.search(text)
if match:
@@ -261,22 +480,25 @@ class DirectoryPattern(object):
match = self._re_closure[open_char].search(text)
if not match:
if ui:
- ui.warn(_("Invalid directory pattern: %s") % pattern_text)
+ ui.warn(ui_string("Invalid directory pattern: %s\n",
+ pattern_text))
return
group_name, text = match.group(1), match.group(2)
- rgxp_text += re.escape(prefix)
- rgxp_text += '(?P<' + group_name + '>' + self._re_match_snippet[open_char] + ')'
+ rgxp_text += re_escape(prefix)
+ rgxp_text += b'(?P<' + group_name + b'>' + self._re_match_snippet[open_char] + b')'
else:
- rgxp_text += re.escape(text)
- text = ''
- rgxp_text += '$'
+ rgxp_text += re_escape(text)
+ text = b''
+ rgxp_text += b'$'
if ui:
- ui.debug(_("Pattern %s translated into regexp %s\n") % (pattern_text, rgxp_text))
+ ui.debug(ui_string("meu: Pattern %s translated into regexp %s\n",
+ pattern_text, rgxp_text))
try:
self._pattern_re = re.compile(rgxp_text, os.name == 'nt' and re.IGNORECASE or 0)
except: # pylint:disable=bare-except
if ui:
- ui.warn(_("Invalid directory pattern: %s") % pattern_text)
+ ui.warn(ui_string("Invalid directory pattern: %s\n",
+ pattern_text))
def is_valid(self):
"""Can be used to check whether object was properly constructed"""
@@ -291,14 +513,16 @@ class DirectoryPattern(object):
:param tested_path: path to check, will be tilda-expanded and
converted to abspath before comparison
:return: Dictionary mapping all ``{brace}`` and ``(paren)`` parts to matched
- items
+ items (on py3 represented as bytestr).
"""
if not self._pattern_re:
return
exp_tested_path = normalize_path(tested_path)
match = self._pattern_re.search(exp_tested_path)
if match:
- return match.groupdict()
+ return dict((key, pycompat.maybebytestr(value))
+ for key, value in dict_iteritems(match.groupdict()))
+ # return match.groupdict()
else:
return None
@@ -360,8 +584,8 @@ class TextFiller(object):
The same parameter can be used in various substitutions:
>>> tf = TextFiller(r'http://go.to/{item:/=-}, G:{item:/=\}, name: {item}')
- >>> print tf.fill(item='so/me/thing')
- http://go.to/so-me-thing, G:so\me\thing, name: so/me/thing
+ >>> print(tf.fill(item='so/me/thing'))
+ b'http://go.to/so-me-thing, G:so\\me\\thing, name: so/me/thing'
Errors are handled by returning None (and warning if ui is given), both
in case of missing key:
@@ -380,23 +604,40 @@ class TextFiller(object):
>>> tf.is_valid()
False
>>> tf.fill(some='prefix', fill='suffix')
+
+ On Py3 binary strings are in fact used and preferred:
+
+ >>> tf = TextFiller(b'{some}/text/to/{fill}')
+ >>> tf.fill(some=b'prefix', fill=b'suffix')
+ 'prefix/text/to/suffix'
+ >>> tf.fill(some=b'/ab/c/d', fill=b'x')
+ '/ab/c/d/text/to/x'
+
+ >>> tf = TextFiller(b'{some}/text/to/{some}')
+ >>> tf.is_valid()
+ True
+ >>> tf.fill(some=b'val')
+ 'val/text/to/val'
+ >>> tf.fill(some=b'ab/c/d', fill=b'x')
+ 'ab/c/d/text/to/ab/c/d'
+
'''
# Regexps used during parsing
- _re_pattern_lead = re.compile(r' ^ ([^{}]*) [{] (.*) $', re.VERBOSE)
- _re_pattern_cont = re.compile(r'''
+ _re_pattern_lead = re.compile(b' ^ ([^{}]*) [{] (.*) $', re.VERBOSE)
+ _re_pattern_cont = re.compile(b'''
^ ([a-zA-Z][a-zA-Z0-9_]*) # name (leading _ disallowed on purpose)
((?: : [^{}:=]+ = [^{}:=]* )*) # :sth=else substitutions
[}]
(.*) $ ''', re.VERBOSE)
- _re_sub = re.compile(r'^ : ([^{}:=]+) = ([^{}:=]*) (.*) $', re.VERBOSE)
+ _re_sub = re.compile(b'^ : ([^{}:=]+) = ([^{}:=]*) (.*) $', re.VERBOSE)
def __init__(self, fill_text, ui=None):
def percent_escape(val):
"""Escape %-s in given text by doubling them."""
- return val.replace('%', '%%')
+ return pycompat.bytestr(val.replace(b'%', b'%%'))
- text = self.fill_text = fill_text
+ text = self.fill_text = pycompat.bytestr(fill_text)
# Replacement text. That's just percent 'some %(abc)s text' (we use % not '{}' to
# leave chances of working on python 2.5). Empty value means I am broken
self._replacement = None
@@ -406,40 +647,43 @@ class TextFiller(object):
# [(from, to), (from, to), ...] list of substitutions to make
self._substitutions = []
- replacement = ''
+ replacement = pycompat.bytestr('')
synth_idx = 0
while text:
match = self._re_pattern_lead.search(text)
if match:
replacement += percent_escape(match.group(1))
- text = match.group(2)
+ text = pycompat.bytestr(match.group(2))
match = self._re_pattern_cont.search(text)
if not match:
if ui:
- ui.warn(_("Bad replacement pattern: %s") % fill_text)
+ ui.warn(ui_string("Bad replacement pattern: %s\n",
+ fill_text))
return
- name, substs, text = match.group(1), match.group(2), match.group(3)
+ name, substs, text = pycompat.bytestr(match.group(1)), pycompat.bytestr(match.group(2)), pycompat.bytestr(match.group(3))
if substs:
fixups = []
while substs:
match = self._re_sub.search(substs)
if not match:
if ui:
- ui.warn(_("Bad replacement pattern: %s") % fill_text)
+ ui.warn(ui_string("Bad replacement pattern: %s\n",
+ fill_text))
return
- src, dest, substs = match.group(1), match.group(2), match.group(3)
+ src, dest, substs = pycompat.bytestr(match.group(1)), pycompat.bytestr(match.group(2)), pycompat.bytestr(match.group(3))
fixups.append((src, dest))
synth_idx += 1
- synth = "_" + str(synth_idx)
+ synth = b"_" + pycompat.bytestr(str(synth_idx))
self._substitutions.append((synth, name, fixups))
name = synth
- replacement += '%(' + name + ')s'
+ replacement += b'%(' + name + b')s'
else:
replacement += percent_escape(text)
- text = ''
+ text = b''
# Final save
if ui:
- ui.debug(_("Replacement %s turned into expression %s") % (fill_text, replacement))
+ ui.debug(ui_string("meu: Replacement %s turned into expression %s\n",
+ fill_text, replacement))
self._replacement = replacement
def is_valid(self):
@@ -451,39 +695,92 @@ class TextFiller(object):
returns None"""
if not self._replacement:
return None
+
+ # TODO: maybe byteskwargs?
+ bstr_args = dict((pycompat.bytestr(key), pycompat.bytestr(value))
+ for key, value in dict_iteritems(kwargs))
+
try:
for made_field, src_field, fixups in self._substitutions:
- value = kwargs[src_field]
+ value = bstr_args[src_field]
for src, dest in fixups:
value = value.replace(src, dest)
- kwargs[made_field] = value
- return self._replacement % kwargs
+ bstr_args[made_field] = value
+ return pycompat.bytestr(self._replacement % bstr_args)
except: # pylint:disable=bare-except
return None
+
###########################################################################
# Config support
###########################################################################
+def setconfig_item(ui, section, name, value):
+ """
+ Mostly equivalent to ```ui.setconfig(section, name, value)```, but
+ under Py3 avoids errors raised if any of the params is unicode.
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_item(ui, b"s1", b'a', b'va')
+ >>> setconfig_item(ui, b"s1", b'b', 7)
+ >>> setconfig_item(ui, "s2", 'a', 'v2a')
+ >>> setconfig_item(ui, "s2", 'b', 8)
+
+ >>> ui.config(b"s1", b'a')
+ b'va'
+ >>> ui.config(b"s1", b'b')
+ 7
+ >>> x = ui.config(b"s2", b'a')
+ >>> x
+ 'v2a'
+ >>> type(x)
+ <class 'mercurial.pycompat.bytestr'>
+ >>> ui.config(b"s2", b'b')
+ 8
+ """
+ if isinstance(value, pycompat.unicode):
+ value = pycompat.bytestr(value)
+ ui.setconfig(pycompat.bytestr(section),
+ pycompat.bytestr(name),
+ value)
+
def setconfig_dict(ui, section, items):
"""
Set's many configuration items with one call. Defined mostly
to make some code (including doctests below) a bit more readable.
+ Note that binary strings are mostly used, in sync with Mercurial 5.0
+ decision to use those as section names and keys in ui.config…
+
>>> import mercurial.ui; ui = mercurial.ui.ui()
- >>> setconfig_dict(ui, "sect1", {'a': 7, 'bbb': 'xxx', 'c': '-'})
- >>> setconfig_dict(ui, "sect2", {'v': 'vvv'})
- >>> ui.config("sect1", 'a')
+ >>> setconfig_dict(ui, b"sect1", {b'a': 7, b'bbb': 'xxx', b'c': b'-'})
+ >>> setconfig_dict(ui, b"sect2", {b'v': 'vvv'})
+ >>> ui.config(b"sect1", b'a')
7
- >>> ui.config("sect2", 'v')
+ >>> x = ui.config(b"sect2", b'v')
+ >>> x
'vvv'
+ >>> type(x)
+ <class 'mercurial.pycompat.bytestr'>
+
+ … but we support some reasonable conversions:
+
+ >>> setconfig_dict(ui, "sect11", {'aa': 7, 'bbbb': 'xxx', 'cc': '-'})
+ >>> setconfig_dict(ui, "sect22", {'vv': 'vvv'})
+ >>> ui.config(b"sect11", b'aa')
+ 7
+ >>> x = ui.config(b"sect22", b'vv')
+ >>> x, type(x)
+ ('vvv', <class 'mercurial.pycompat.bytestr'>)
:param section: configuration section tofill
:param items: dictionary of items to set
"""
- for key, value in items.iteritems():
- ui.setconfig(section, key, value)
+ section = pycompat.bytestr(section)
+
+ for key, value in dict_iteritems(items):
+ setconfig_item(ui, section, key, value)
def setconfig_list(ui, section, items):
@@ -493,19 +790,35 @@ def setconfig_list(ui, section, items):
setconfig_dict, this guarantees ordering.
>>> import mercurial.ui; ui = mercurial.ui.ui()
- >>> setconfig_list(ui, "sect1",
+ >>> setconfig_list(ui, b"sect1",
+ ... [(b'a', 7), (b'bbb', b'xxx'), (b'c', b'-'), (b'a', 8)])
+ >>> setconfig_list(ui, b"sect2", [(b'v', b'vvv')])
+ >>> ui.config(b"sect1", b'a')
+ 8
+ >>> ui.config(b"sect2", b'v')
+ b'vvv'
+
+ We also support normal strings to some degree:
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_list(ui, "sect11",
... [('a', 7), ('bbb', 'xxx'), ('c', '-'), ('a', 8)])
- >>> setconfig_list(ui, "sect2", [('v', 'vvv')])
- >>> ui.config("sect1", 'a')
+ >>> setconfig_list(ui, "sect22", [('v', 'vvv')])
+ >>> ui.config(b"sect11", b'a')
8
- >>> ui.config("sect2", 'v')
+ >>> x = ui.config(b"sect22", b'v')
+ >>> x
'vvv'
+ >>> type(x)
+ <class 'mercurial.pycompat.bytestr'>
:param section: configuration section tofill
:param items: dictionary of items to set
"""
+ section = pycompat.bytestr(section)
+
for key, value in items:
- ui.setconfig(section, key, value)
+ setconfig_item(ui, section, key, value)
def rgxp_config_items(ui, section, rgxp):
@@ -526,10 +839,10 @@ def rgxp_config_items(ui, section, rgxp):
... ])
>>>
>>> for name, value in rgxp_config_items(
- ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
- ... print name, value
- some ala, ma kota
- other 4
+ ... ui, "foo", re.compile(b'^pfx-(\\w+)-sfx$')):
+ ... print(name, value)
+ b'some' b'ala, ma kota'
+ b'other' 4
:param ui: mercurial ui, used to access config
:param section: config section name
@@ -537,10 +850,10 @@ def rgxp_config_items(ui, section, rgxp):
:return: yields pairs (group-match, value) for all matching items
'''
- for key, value in ui.configitems(section):
- match = rgxp.search(key)
+ for key, value in ui.configitems(pycompat.bytestr(section)):
+ match = rgxp.search(pycompat.bytestr(key))
if match:
- yield match.group(1), value
+ yield pycompat.maybebytestr(match.group(1)), pycompat.maybebytestr(value)
def rgxp_configlist_items(ui, section, rgxp):
@@ -561,22 +874,24 @@ def rgxp_configlist_items(ui, section, rgxp):
... ])
>>>
>>> for name, value in rgxp_configlist_items(
- ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
- ... print name, value
- some ['ala', 'ma', 'kota']
- other ['sth']
+ ... ui, "foo", re.compile(b'^pfx-(\\w+)-sfx$')):
+ ... print(name, value)
+ b'some' [b'ala', b'ma', b'kota']
+ b'other' [b'sth']
:param ui: mercurial ui, used to access config
:param section: config section name
- :param rgxp: tested regexp, should contain single (group)
+ :param rgxp: tested regexp, should contain single (group) and be binary-string based
:return: yields pairs (group-match, value-as-list) for all
matching items
'''
+ section = pycompat.bytestr(section)
for key, _unneeded_value in ui.configitems(section):
+ key = pycompat.bytestr(key)
match = rgxp.search(key)
if match:
- yield match.group(1), ui.configlist(section, key)
+ yield pycompat.maybebytestr(match.group(1)), ui.configlist(section, key)
def rgxp_configbool_items(ui, section, rgxp):
@@ -597,10 +912,10 @@ def rgxp_configbool_items(ui, section, rgxp):
... })
>>>
>>> for name, value in rgxp_configbool_items(
- ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
- ... print name, value
- some True
- other False
+ ... ui, "foo", re.compile(b'^pfx-(\\w+)-sfx$')):
+ ... print(name, value)
+ b'some' True
+ b'other' False
:param ui: mercurial ui, used to access config
:param section: config section name
@@ -609,10 +924,12 @@ def rgxp_configbool_items(ui, section, rgxp):
:return: yields pairs (group-match, value-as-list) for all
matching items
'''
+ section = pycompat.bytestr(section)
for key, _unneeded_value in ui.configitems(section):
+ key = pycompat.bytestr(key)
match = rgxp.search(key)
if match:
- yield match.group(1), ui.configbool(section, key)
+ yield pycompat.maybebytestr(match.group(1)), ui.configbool(section, key)
def suffix_config_items(ui, section, suffix):
@@ -633,9 +950,22 @@ def suffix_config_items(ui, section, suffix):
>>>
>>> for name, value in suffix_config_items(
... ui, "foo", 'item'):
- ... print name, value
- some ala, ma kota
- other 4
+ ... print(name, value)
+ ... print(type(name), type(value))
+ b'some' b'ala, ma kota'
+ <class 'mercurial.pycompat.bytestr'> <class 'mercurial.pycompat.bytestr'>
+ b'other' 4
+ <class 'mercurial.pycompat.bytestr'> <class 'int'>
+ >>>
+ >>> for name, value in suffix_config_items(
+ ... ui, b"foo", b'item'):
+ ... print(name, value)
+ ... print(type(name), type(value))
+ b'some' b'ala, ma kota'
+ <class 'mercurial.pycompat.bytestr'> <class 'mercurial.pycompat.bytestr'>
+ b'other' 4
+ <class 'mercurial.pycompat.bytestr'> <class 'int'>
+
:param ui: mercurial ui, used to access config
:param section: config section name
@@ -643,7 +973,8 @@ def suffix_config_items(ui, section, suffix):
:return: yields pairs (prefix, value) for all matching items, values are lists
"""
- rgxp = re.compile(r'^(\w+)\.' + re.escape(suffix))
+ esc_suffix = pycompat.bytestr(re_escape(suffix))
+ rgxp = re.compile(b'^(\\w+)\\.' + esc_suffix)
for key, value in rgxp_config_items(ui, section, rgxp):
yield key, value
@@ -654,22 +985,30 @@ def suffix_configlist_items(ui, section, suffix):
ui.configlist, so returned as lists.
>>> import mercurial.ui; ui = mercurial.ui.ui()
- >>> setconfig_list(ui, "foo", [
- ... ("some.item", "ala, ma kota"),
- ... ("some.nonitem", "bela nie"),
- ... ("x", "yes"),
- ... ("other.item", "kazimira"),
+ >>> setconfig_list(ui, b"foo", [
+ ... (b"some.item", b"ala, ma kota"),
+ ... (b"some.nonitem", b"bela nie"),
+ ... (b"x", b"yes"),
+ ... (b"other.item", b"kazimira"),
... ])
- >>> setconfig_dict(ui, "notfoo", {
- ... "some.item": "bad",
- ... "also.item": "too",
+ >>> setconfig_dict(ui, b"notfoo", {
+ ... b"some.item": "bad",
+ ... b"also.item": "too",
... })
>>>
>>> for name, value in suffix_configlist_items(
+ ... ui, b"foo", b"item"):
+ ... print(name, value)
+ b'some' [b'ala', b'ma', b'kota']
+ b'other' [b'kazimira']
+
+ Attempts to handle native strings:
+
+ >>> for name, value in suffix_configlist_items(
... ui, "foo", "item"):
- ... print name, value
- some ['ala', 'ma', 'kota']
- other ['kazimira']
+ ... print(name, value)
+ b'some' [b'ala', b'ma', b'kota']
+ b'other' [b'kazimira']
:param ui: mercurial ui, used to access config
:param section: config section name
@@ -678,7 +1017,8 @@ def suffix_configlist_items(ui, section, suffix):
:return: yields pairs (group-match, value-as-list) for all
matching items, values are boolean
"""
- rgxp = re.compile(r'^(\w+)\.' + re.escape(suffix))
+ esc_suffix = pycompat.bytestr(re_escape(suffix))
+ rgxp = re.compile(b'^(\\w+)\\.' + esc_suffix)
for key, value in rgxp_configlist_items(ui, section, rgxp):
yield key, value
@@ -706,22 +1046,22 @@ def suffix_configbool_items(ui, section, suffix):
>>>
>>> for name, value in suffix_configbool_items(
... ui, "foo", "item"):
- ... print name, str(value)
- true True
- false False
- one True
- zero False
- yes True
- no False
+ ... print(name, str(value))
+ b'true' True
+ b'false' False
+ b'one' True
+ b'zero' False
+ b'yes' True
+ b'no' False
>>>
- >>> ui.setconfig("foo", "text.item", "something")
- >>> for name, value in suffix_configbool_items(
- ... ui, "foo", "item"):
- ... print name, str(value)
- Traceback (most recent call last):
- File "/usr/lib/python2.7/dist-packages/mercurial/ui.py", line 237, in configbool
- % (section, name, v))
- ConfigError: foo.text.item is not a boolean ('something')
+ >>> ui.setconfig(b"foo", b"text.item", b"something")
+ >>> try:
+ ... for name, value in suffix_configbool_items(ui, "foo", "item"):
+ ... x = (name, str(value))
+ ... print("Strange, no error")
+ ... except Exception as err:
+ ... print(repr(err)[:58]) # :58 augments py36-py37 diff
+ ConfigError(b"foo.text.item is not a boolean ('something')
:param ui: mercurial ui, used to access config
:param section: config section name
@@ -730,7 +1070,9 @@ def suffix_configbool_items(ui, section, suffix):
:return: yields pairs (group-match, value) for all
matching items
"""
- rgxp = re.compile(r'^(\w+)\.' + re.escape(suffix))
+ section = pycompat.bytestr(section)
+ esc_suffix = pycompat.bytestr(re_escape(suffix))
+ rgxp = re.compile(b'^(\\w+)\\.' + esc_suffix)
for key, value in rgxp_configbool_items(ui, section, rgxp):
yield key, value
@@ -754,7 +1096,7 @@ def monkeypatch_method(cls, fname=None):
... return "Patched: " + meth.orig(self, arg)
>>>
>>> obj = SomeClass()
- >>> print obj.meth("some param")
+ >>> print(obj.meth("some param"))
Patched: Original: some param
It is also possible to use different name
@@ -768,7 +1110,7 @@ def monkeypatch_method(cls, fname=None):
... return "Patched: " + another_meth.orig(self, arg)
>>>
>>> obj = SomeClass()
- >>> print obj.meth("some param")
+ >>> print(obj.meth("some param"))
Patched: Original: some param
:param cls: Class being modified
@@ -794,19 +1136,19 @@ def monkeypatch_function(module, fname=None):
>>> import random
>>> @monkeypatch_function(random)
... def seed(x=None):
- ... print "Forcing random to seed with 0 instead of", x
+ ... print("Forcing random to seed with 0 instead of", x)
... return seed.orig(0)
>>>
>>> random.seed()
Forcing random to seed with 0 instead of None
>>> random.randint(0, 10)
- 9
+ 6
>>> import random
>>> @monkeypatch_function(random, 'choice')
... def choice_first(sequence):
... return sequence[0]
- >>> for x in range(0, 4): print random.choice("abcdefgh")
+ >>> for x in range(0, 4): print(random.choice("abcdefgh"))
a
a
a
@@ -847,7 +1189,7 @@ def find_repositories_below(path, check_inside=False):
pending = deque([normalize_path(path)])
while pending:
checked = pending.popleft()
- if os.path.isdir(checked + '/.hg'):
+ if os.path.isdir(checked + b'/.hg'):
yield checked
if not check_inside:
continue
@@ -857,7 +1199,7 @@ def find_repositories_below(path, check_inside=False):
# Things like permission errors (say, on lost+found)
# Let's ignorre this, better to process whatever we can
names = []
- paths = [checked + '/' + name
+ paths = [pycompat.bytestr(checked + b'/' + pycompat.bytestr(name))
for name in names if name != '.hg']
dir_paths = [item
for item in paths if os.path.isdir(item)]
@@ -872,6 +1214,8 @@ def command(cmdtable):
"""
Compatibility layer for mercurial.cmdtutil.command.
+ For Mercurials >= 3.8 it's registrar.command
+
For Mercurials >= 3.1 it's just synonym for cmdutil.command.
For Mercurials <= 3.0 it returns upward compatible function
@@ -906,9 +1250,9 @@ def command(cmdtable):
>>> # Syntax changed in hg3.8, trying to accomodate
>>> commands.norepo if hasattr(commands, 'norepo') else ' othercmd' # doctest: +ELLIPSIS
'... othercmd'
- >>> othercmd.__dict__['norepo'] if othercmd.__dict__ else True
+ >>> othercmd.__dict__['norepo'] if othercmd.__dict__ else True
True
- >>> mycmd.__dict__['norepo'] if mycmd.__dict__ else False
+ >>> mycmd.__dict__['norepo'] if mycmd.__dict__ else False
False
"""
@@ -970,9 +1314,9 @@ def direct_import(module_name, blocked_modules=None):
Allows to block some modules from demandimport machinery,
so they are not accidentally misloaded:
- >>> k = direct_import("anydbm", ["dbhash", "gdbm", "dbm", "bsddb.db"])
+ >>> k = direct_import("dbm", ["dbm.gnu", "dbm.ndbm", "dbm.dumb"])
>>> k.__name__
- 'anydbm'
+ 'dbm'
:param module_name: name of imported module
:param blocked_modules: names of modules to be blocked from demandimport
@@ -1040,9 +1384,8 @@ def direct_import_ext(module_name, blocked_modules=None):
def disable_logging(module_name):
"""
- Shut up warning about initialized logging which happens
- if some imported module logs (mercurial does not setup logging
- machinery)
+ Shut up warning about initialized logging which happens if some
+ imported module logs (mercurial does not setup logging machinery)
>>> disable_logging("keyring")
@@ -1104,29 +1447,49 @@ def enable_hook(ui, hook_name, hook_function):
not lambda, not local function embedded inside another
function).
"""
+ hook_name = pycompat.bytestr(hook_name)
+
# Detecting function name, and checking whether it seems publically
# importable and callable from global module level
if hook_function.__class__.__name__ == 'function' \
and not hook_function.__name__.startswith('<') \
and not hook_function.__module__.startswith('__'):
- hook_function_name = '{module}.{name}'.format(
- module=hook_function.__module__, name=hook_function.__name__)
- hook_activator = 'python:' + hook_function_name
+ hook_function_name = pycompat.bytestr('{module}.{name}'.format(
+ module=hook_function.__module__, name=hook_function.__name__))
+ hook_activator = pycompat.bytestr(b'python:' + hook_function_name)
for key, value in ui.configitems("hooks"):
if key == hook_name:
if value == hook_activator:
- ui.debug(_("Hook already statically installed, skipping %s: %s\n") % (
- hook_name, hook_function_name))
+ ui.debug(ui_string("meu: Hook already statically installed, skipping %s: %s\n",
+ hook_name, hook_function_name))
return
if value == hook_function:
- ui.debug(_("Hook already dynamically installed, skipping %s: %s\n") % (
- hook_name, hook_function_name))
+ ui.debug(ui_string("meu: Hook already dynamically installed, skipping %s: %s\n",
+ hook_name, hook_function_name))
return
- ui.debug(_("Enabling dynamic hook %s: %s.%s\n") % (
- hook_name, hook_function.__module__, hook_function.__name__))
+ ui.debug(ui_string("meu: Enabling dynamic hook %s: %s\n",
+ hook_name, hook_function_name))
# Standard way of hook enabling
- ui.setconfig("hooks", hook_name, hook_function)
+ setconfig_item(ui, b"hooks", hook_name, hook_function)
+
+
+###########################################################################
+# Manual test support
+###########################################################################
+
+if __name__ == "__main__":
+ import doctest
+ # doctest.testmod()
+ # doctest.run_docstring_examples(setconfig_dict, globals())
+ # doctest.run_docstring_examples(setconfig_list, globals())
+ # doctest.run_docstring_examples(rgxp_config_items, globals())
+ # doctest.run_docstring_examples(suffix_configlist_items, globals())
+ # doctest.run_docstring_examples(normalize_path, globals())
+ doctest.run_docstring_examples(rgxp_configbool_items, globals())
+ doctest.run_docstring_examples(rgxp_configlist_items, globals())
+ doctest.run_docstring_examples(suffix_config_items, globals())
+ doctest.run_docstring_examples(suffix_configbool_items, globals())