diff options
Diffstat (limited to 'mercurial_extension_utils.py')
-rw-r--r-- | mercurial_extension_utils.py | 633 |
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()) |