summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Mathys <eraserix@gmail.com>2019-12-04 20:58:18 +0100
committerAndrej Shadura <andrewsh@debian.org>2020-01-02 11:42:50 +0100
commit68d1a399d546ee54bf067a80011746e3544a287e (patch)
tree9dbc243eac533b538d0e8ae925c534bc821efd83
parentaded9f3d1f6f2691acd71f777f13251ce497c68b (diff)
parent2de78fde17a865b4a7b71b6c99872bde146a842e (diff)
Convert to Python 3
* New upstream release. * Convert package to python3. (Closes: #937011) * Update Standards Version and compat. * Add Upstream-Contact to d/copyright.
-rw-r--r--.hgignore17
-rw-r--r--.hgtags26
-rw-r--r--HISTORY.txt166
-rw-r--r--PKG-INFO3
-rw-r--r--README-TESTING.txt13
-rw-r--r--TODO.txt3
-rw-r--r--debian/changelog9
-rw-r--r--debian/compat1
-rw-r--r--debian/control10
-rw-r--r--debian/copyright1
-rwxr-xr-xdebian/rules2
-rw-r--r--drone.sh14
-rw-r--r--mercurial_extension_utils.egg-info/PKG-INFO3
-rw-r--r--mercurial_extension_utils.egg-info/SOURCES.txt15
-rw-r--r--mercurial_extension_utils.py633
-rw-r--r--mercurial_extension_utils_loader.py11
-rw-r--r--setup.py3
-rw-r--r--tests/manual_find_repositories_below.py6
-rw-r--r--tests/py2_doctests_mercurial_extension_utils.py456
-rw-r--r--tests/py2win_doctests_mercurial_extension_utils.py351
-rw-r--r--tests/test_doctest.py78
-rw-r--r--tests/test_find_repositories_below.py101
-rw-r--r--tox.ini55
23 files changed, 1830 insertions, 147 deletions
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..56c4d3a
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,17 @@
+syntax: regexp
+
+\.pyc$
+\.pyo$
+~$
+^\.\#
+^\.(project|pydevproject)$
+^\.settings/
+^build/
+^dist/
+\.egg-info/
+^README\.html
+^\.tox/
+
+syntax: glob
+
+.pytest_cache
diff --git a/.hgtags b/.hgtags
new file mode 100644
index 0000000..f4c8fa9
--- /dev/null
+++ b/.hgtags
@@ -0,0 +1,26 @@
+57362b22fd15a5ebde9b03d28c12e0dc2a35bf2d 0.6.0
+57362b22fd15a5ebde9b03d28c12e0dc2a35bf2d 0.6.0
+e54bee893d66e23ee68a1b21f695706a9aa2f5c4 0.6.0
+bb58a4178ffc887911168dd23d588efa0be4b015 0.6.1
+3dc85253b779b8b64db5b639c18c8c1cc360f0ea 0.7.0
+78990a2b31c10737d1dbf7b50beee47040ff8f71 0.8.0
+c208ad3d955f9049f92775d31fd0e439ae055dfe 0.8.1
+6b8053838226d0c615f167c05eb4cadf08edc8e1 0.9.0
+9ae993c639d822ccf214cc5b55960eb50f1acdb5 0.10.0
+39089a39a53194f95ab08a3072dbb6defd39706d 0.11.0
+65624c55a18f99861142cc3be5b210ac359f5e02 1.0.0
+b1df1e710f03cdba58a3a4433dfd5d9a547ec993 1.0.1
+27bdda476e4fe97e04ae6595be8112a0bffb6edb 1.1.0
+8942af39a234280b167350c16249d6f8eb343d21 1.1.1
+254d849ee46b39dcb7b1228c70a08bcdcda000fd 1.1.2
+3044deffd6201a9323ecfe594ed898b5766209c8 1.2.0
+7a91a4f5c179dd78decc924d13fa5d99860f98d3 1.3.0
+8759af69e0c8194015b79c0592e0bf5c2d140126 1.3.1
+feef8221d9da41f448d0ff978d6420cf18fffad7 1.3.2
+d04b7635b0b12a74976a88dd6ca4ace7a86b2dc0 1.3.3
+45ba5bd5b1d25287b141f64a0acfbf848100cdf0 1.3.4
+3f3951195f8f223bcfcd6b03ef85182b9e6edc69 1.3.5
+d6c91f955d2c4f06d3c71eabddc4a257a9a59536 1.3.6
+9c2241c231bb47acb902aa790fb36ee2c7a98370 1.3.7
+78671ef929e60d53f458c83c4332cf501f7081cb 1.4.0
+5144b5a15ea46208adb496d4436dbf47e1847f8a 1.5.0
diff --git a/HISTORY.txt b/HISTORY.txt
new file mode 100644
index 0000000..e4083df
--- /dev/null
+++ b/HISTORY.txt
@@ -0,0 +1,166 @@
+1.5.0
+~~~~~~~~~~~~
+
+Polished support for python3 (tested with py 3.5-3.7 and with
+mercurial 5.0-5.2). Seems to work as expected, yet to be verified
+against all extensions.
+
+Added ui_string function, helper to safely format argument
+for ui.debug, ui.status etc.
+
+1.4.0
+~~~~~~~~~~~~
+
+Preliminary support for python3 (with mercurial 5.0).
+Exact APIs are yet to be verified (bstr/str decisions)
+but (adapted) tests pass.
+
+Tested against hg 5.0 and hg 4.9.
+
+1.3.7
+~~~~~~~~~~~~
+
+Tested against hg 4.8 (no changes needed).
+
+1.3.6
+~~~~~~~~~~~~
+
+Fixed problems with hg 4.7 (accomodating changed demandimport APIs).
+
+1.3.5
+~~~~~~~~~~~~
+
+Formally tested with hg 4.5 and 4.6.
+
+Dropping test badges which don't work anymore from docs.
+
+1.3.4
+~~~~~~~~~~~~~
+
+In-advance preparation for cmdutil.commands → registrar.commands
+migration in core Mercurial API (see 46ba2cdda476 in hg-stable, likely
+to be released in 4.3).
+
+1.3.3
+~~~~~~~~~~~~~
+
+Updated links after bitbucket changes.
+
+hg 4.1 and 4.2 added to tested versions.
+
+1.3.2
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+find_repositories_below doesn't fail in case some subdirectory is
+unreadable. Instead, it simply skips it and continues to work
+(realistic use-case: lost+found doesn't crash it anymore, but is
+skipped…)
+
+1.3.1
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Some tests were failing on mercurial 3.8, even more on 4.0
+(actual code worked properly, just tests were faiing).
+
+1.3.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Added enable_hook function (which detects whether hook is already
+installed and withdraws in such a case).
+
+Added inside_tortoisehg function (detecting that „we're running under
+Tortoise”).
+
+1.2.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Added meu.command (compatibility wrapper for cmdutil.command).
+
+
+1.1.2
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Added setconfig_list.
+
+Various test improvements (including tox tests configured
+to check various mercurial versions)
+
+
+1.1.1
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Tests should work on any machine. Started Drone.io autotests.
+Added some requirement.s
+
+1.1.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+New functions: direct_import, direct_import_ext, and disable_logging.
+Mostly taken from mercurial_keyring, but improved:
+- imports handle dotted.modules
+- disable_logging actually works for py2.6
+
+1.0.1
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Test fixes, minor code cleanups.
+
+1.0.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Documentation updates.
+
+0.11.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Works on Windows (and handles normalizing paths to /-separator)
+
+0.10.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+find_repositories_below
+
+0.9.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+monkeypatch_method and monkeypatch_function
+
+0.8.1
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Bugfix: TextFiller was hanging if run on pattern
+not ending with {item}. Effectively mercurial hanged
+while loading path patterns, for example.
+
+0.8.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ``rgxp_configbool_items``
+- ``suffix_configbool_items``
+
+0.7.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- ``setconfig_dict``,
+- ``DirectoryPattern``
+- ``TextFiller``
+
+Actually used to simplify and improve ``mercurial_path_pattern``.
+
+0.6.1
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Extra config support:
+- ``suffix_config_items``,
+- ``suffix_configlist_items``.
+
+Actually used to simplify ``mercurial_dynamic_username``.
+
+0.6.0
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+First public release:
+- ``belongs_to_tree``,
+- ``belongs_to_tree_group``,
+- ``rgxp_config_items``,
+- ``rgxp_configlist_items``
diff --git a/PKG-INFO b/PKG-INFO
index 58695fe..1e0c58c 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: mercurial_extension_utils
-Version: 1.3.6
+Version: 1.5.0
Summary: Mercurial Extension Utils
Home-page: http://bitbucket.org/Mekk/mercurial-extension_utils
Author: Marcin Kasperski
@@ -206,6 +206,7 @@ Classifier: Intended Audience :: Developers
Classifier: License :: DFSG approved
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Version Control
diff --git a/README-TESTING.txt b/README-TESTING.txt
new file mode 100644
index 0000000..56f914a
--- /dev/null
+++ b/README-TESTING.txt
@@ -0,0 +1,13 @@
+Various ways of pre-release testing:
+
+- manual, current install
+
+ python -m unittest discover tests/
+
+- cross-version
+
+ tox
+
+- automatic
+
+ (active on drone.io, script saved in drone.sh)
diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 0000000..7677613
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,3 @@
+
+→ Finishing mercurial_extension_utils_loader
+ and publishing it as install alternative.
diff --git a/debian/changelog b/debian/changelog
index 16f23f5..5c2d309 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
+mercurial-extension-utils (1.5.0-1) UNRELEASED; urgency=medium
+
+ * New upstream release.
+ * Convert package to python3. (Closes: #937011)
+ * Update Standards Version and compat.
+ * Add Upstream-Contact to d/copyright.
+
+ -- Christoph Mathys <eraserix@gmail.com> Wed, 04 Dec 2019 20:58:18 +0100
+
mercurial-extension-utils (1.3.6-1) unstable; urgency=medium
* New upstream release.
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index b4de394..0000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-11
diff --git a/debian/control b/debian/control
index 0eaf564..edf35c7 100644
--- a/debian/control
+++ b/debian/control
@@ -2,16 +2,14 @@ Source: mercurial-extension-utils
Section: python
Priority: optional
Maintainer: Christoph Mathys <eraserix@gmail.com>
-Build-Depends: debhelper (>= 11), python-all (>= 2.6.6-3~),
- python-setuptools, dh-python
-Standards-Version: 4.2.1
+Build-Depends: debhelper-compat (= 12), python3-all,
+ python3-setuptools, dh-python
+Standards-Version: 4.4.1
Homepage: http://pypi.python.org/pypi/mercurial_extension_utils
-X-Python-Version: all
-XB-Python-Version: ${python:Versions}
Package: mercurial-extension-utils
Architecture: all
-Depends: ${python:Depends}, ${misc:Depends}, mercurial
+Depends: ${python3:Depends}, ${misc:Depends}, mercurial
Description: Contains functions for writing Mercurial extensions
Contains functions used by Mercurial extension mercurial-keyring. They are
mostly tiny utilities related to configuration processing or location
diff --git a/debian/copyright b/debian/copyright
index ac58945..4f17080 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,5 +1,6 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: mercurial-extension-utils
+Upstream-Contact: Marcin Kasperski <marcin.kasperski@mekk.waw.pl>
Source: http://pypi.python.org/pypi/mercurial_extension-utils
Files: *
diff --git a/debian/rules b/debian/rules
index 1ef08e4..70bbb53 100755
--- a/debian/rules
+++ b/debian/rules
@@ -4,4 +4,4 @@
export PYBUILD_NAME="mercurial-keyring"
%:
- dh $@ --with python2 --buildsystem=pybuild
+ dh $@ --with python3 --buildsystem=pybuild
diff --git a/drone.sh b/drone.sh
new file mode 100644
index 0000000..af49ce8
--- /dev/null
+++ b/drone.sh
@@ -0,0 +1,14 @@
+# Copy of test recipe used on drone.io
+#
+# Configuration:
+# Language: Python2.7
+# No Database
+# No Environment Variables
+# Work dir: /home/ubuntu/src/bitbucket.org/Mekk/mercurial-extension_utils
+# (can't change)
+
+pip install Mercurial --use-mirrors
+python -m unittest discover tests
+
+pip install tox
+tox -e py27-hg27,py27-hg29,py27-hg32,py27-hg33
diff --git a/mercurial_extension_utils.egg-info/PKG-INFO b/mercurial_extension_utils.egg-info/PKG-INFO
index d33468a..29b8f8d 100644
--- a/mercurial_extension_utils.egg-info/PKG-INFO
+++ b/mercurial_extension_utils.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: mercurial-extension-utils
-Version: 1.3.6
+Version: 1.5.0
Summary: Mercurial Extension Utils
Home-page: http://bitbucket.org/Mekk/mercurial-extension_utils
Author: Marcin Kasperski
@@ -206,6 +206,7 @@ Classifier: Intended Audience :: Developers
Classifier: License :: DFSG approved
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Version Control
diff --git a/mercurial_extension_utils.egg-info/SOURCES.txt b/mercurial_extension_utils.egg-info/SOURCES.txt
index 283bb7a..57c1fdd 100644
--- a/mercurial_extension_utils.egg-info/SOURCES.txt
+++ b/mercurial_extension_utils.egg-info/SOURCES.txt
@@ -1,8 +1,21 @@
+.hgignore
+.hgtags
+HISTORY.txt
+README-TESTING.txt
README.txt
+TODO.txt
+drone.sh
mercurial_extension_utils.py
+mercurial_extension_utils_loader.py
setup.py
+tox.ini
mercurial_extension_utils.egg-info/PKG-INFO
mercurial_extension_utils.egg-info/SOURCES.txt
mercurial_extension_utils.egg-info/dependency_links.txt
mercurial_extension_utils.egg-info/top_level.txt
-mercurial_extension_utils.egg-info/zip-safe \ No newline at end of file
+mercurial_extension_utils.egg-info/zip-safe
+tests/manual_find_repositories_below.py
+tests/py2_doctests_mercurial_extension_utils.py
+tests/py2win_doctests_mercurial_extension_utils.py
+tests/test_doctest.py
+tests/test_find_repositories_below.py \ No newline at end of file
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())
diff --git a/mercurial_extension_utils_loader.py b/mercurial_extension_utils_loader.py
new file mode 100644
index 0000000..54a1894
--- /dev/null
+++ b/mercurial_extension_utils_loader.py
@@ -0,0 +1,11 @@
+
+import os, sys
+
+THE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+sys.path.append(THE_DIR)
+
+# This makes this dir a winner (but later)
+#
+# def extsetup(ui):
+# sys.path.insert(0, THE_DIR)
diff --git a/setup.py b/setup.py
index 6e435da..95b1dc9 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
from setuptools import setup, find_packages
-VERSION = '1.3.6'
+VERSION = '1.5.0'
LONG_DESCRIPTION = open("README.txt").read()
INSTALL_REQUIRES = []
@@ -28,6 +28,7 @@ setup(
'License :: DFSG approved',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 3',
'Operating System :: OS Independent',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Version Control',
diff --git a/tests/manual_find_repositories_below.py b/tests/manual_find_repositories_below.py
new file mode 100644
index 0000000..df2381b
--- /dev/null
+++ b/tests/manual_find_repositories_below.py
@@ -0,0 +1,6 @@
+
+import mercurial_extension_utils as meu
+
+#for repo_path in meu.find_repositories_below("~/DEV_hg/mercurial"):
+for repo_path in meu.find_repositories_below("~/DEV_hg"):
+ print repo_path
diff --git a/tests/py2_doctests_mercurial_extension_utils.py b/tests/py2_doctests_mercurial_extension_utils.py
new file mode 100644
index 0000000..883dcd3
--- /dev/null
+++ b/tests/py2_doctests_mercurial_extension_utils.py
@@ -0,0 +1,456 @@
+# -*- coding: utf-8 -*-
+#
+# mercurial extension utils: Python 2 doctests
+#
+# Copyright (c) 2015-2019 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# See README.txt for more details.
+
+r'''
+This module exists solely to give some examples (and doctest)
+of mercurial_extension_utils styled for Python2 syntax.
+Tests are copied from mercurial_extension_utils.py (before
+switch to py3 syntax in doctests).
+
+ >>> 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")
+ 'Simple text'
+ >>> ui_string(b"Simple binary")
+ 'Simple binary'
+ >>> ui_string("This is %s and %s and %s", b'bin', u'txt', None)
+ u'This is bin and txt and None'
+ >>> ui_string(b"This is %s and %s and %s", b'bin', u'txt', None)
+ u'This is bin and txt and None'
+
+ >>> normalize_path("~/src")
+ '/home/lordvader/src'
+ >>> normalize_path("/some/where")
+ '/some/where'
+ >>> normalize_path("/some/where/")
+ '/some/where'
+ >>> normalize_path("../../../some/where")
+ '/home/lordvader/some/where'
+
+
+ >>> belongs_to_tree("/tmp/sub/dir", "/tmp")
+ '/tmp'
+ >>> belongs_to_tree("/tmp", "/tmp")
+ '/tmp'
+ >>> belongs_to_tree("/tmp/sub", "/tmp/sub/dir/../..")
+ '/tmp'
+
+
+ >>> belongs_to_tree("/usr/sub", "/tmp")
+
+
+ >>> home_work_src = os.path.join(os.environ["HOME"], "work", "src")
+ >>> belongs_to_tree(home_work_src, "~/work")
+ '/home/lordvader/work'
+ >>> belongs_to_tree("/home/lordvader/devel/webapps", "~lordvader/devel")
+ '/home/lordvader/devel'
+
+
+ >>> belongs_to_tree_group("/tmp/sub/dir", ["/bin", "/tmp"])
+ '/tmp'
+ >>> belongs_to_tree_group("/tmp", ["/tmp"])
+ '/tmp'
+ >>> belongs_to_tree_group("/tmp/sub/dir", ["/bin", "~/src"])
+
+
+ >>> belongs_to_tree_group("/tmp/sub/dir", ["/tmp", "/bin", "/tmp", "/tmp/sub"])
+ '/tmp/sub'
+
+
+ >>> belongs_to_tree_group("/home/lordvader/src/apps", ["~/src", "/home/lordvader"])
+ '/home/lordvader/src'
+
+
+ >>> pat = DirectoryPattern('~/src/{suffix}')
+ >>> pat.is_valid()
+ True
+ >>> pat.search("/opt/repos/abcd")
+ >>> pat.search("~/src/repos/in/tree")
+ {'suffix': 'repos/in/tree'}
+ >>> pat.search("/home/lordvader/src/repos/here" if os.system != 'nt' else "c:/users/lordvader/src/repos/here")
+ {'suffix': 'repos/here'}
+ >>> pat.search("/home/lordvader/src")
+
+ >>> pat = DirectoryPattern('~lordvader/devel/(item)')
+ >>> pat.search("/opt/repos/abcd")
+ >>> pat.search("~/devel/libxuza")
+ {'item': 'libxuza'}
+ >>> pat.search("~/devel/libs/libxuza")
+ >>> pat.search("/home/lordvader/devel/webapp")
+ {'item': 'webapp'}
+ >>> pat.search("/home/lordvader/devel")
+
+ >>> from pprint import pprint # Help pass doctests below
+
+ >>> pat = DirectoryPattern('/opt/repos/(group)/{suffix}')
+ >>> pat.search("/opt/repos/abcd")
+ >>> pprint(pat.search("/opt/repos/libs/abcd"))
+ {'group': 'libs', 'suffix': 'abcd'}
+ >>> pprint(pat.search("/opt/repos/apps/mini/webby"))
+ {'group': 'apps', 'suffix': 'mini/webby'}
+
+ >>> pat = DirectoryPattern('/opt/repos/(group/{suffix}')
+ >>> pat.is_valid()
+ False
+ >>> pat.search('/opt/repos/some/where')
+
+ Fixed strings can also be used and work reasonably:
+
+ >>> pat = DirectoryPattern('~/dev/acme')
+ >>> pat.is_valid()
+ True
+ >>> pat.search('/home/lordvader/dev/acme')
+ {}
+ >>> pat.search('/home/lordvader/dev/acme/')
+ {}
+ >>> pat.search('/home/lordvader/dev/acme/subdir')
+ >>> pat.search('/home/lordvader/dev')
+
+
+ >>> tf = TextFiller('{some}/text/to/{fill}')
+ >>> tf.fill(some='prefix', fill='suffix')
+ 'prefix/text/to/suffix'
+ >>> tf.fill(some='/ab/c/d', fill='x')
+ '/ab/c/d/text/to/x'
+
+
+ >>> tf = TextFiller('{some}/text/to/{some}')
+ >>> tf.is_valid()
+ True
+ >>> tf.fill(some='val')
+ 'val/text/to/val'
+ >>> tf.fill(some='ab/c/d', fill='x')
+ 'ab/c/d/text/to/ab/c/d'
+
+
+ >>> tf = TextFiller('{prefix:_=___}/goto/{suffix:/=-}')
+ >>> tf.fill(prefix='some_prefix', suffix='some/long/suffix')
+ 'some___prefix/goto/some-long-suffix'
+
+
+ >>> tf = TextFiller('{prefix:/home/=}/docs/{suffix:.txt=.html}')
+ >>> tf.fill(prefix='/home/joe', suffix='some/document.txt')
+ 'joe/docs/some/document.html'
+
+
+ >>> tf = TextFiller(r'/goto/{item:/=-:\=_}/')
+ >>> tf.fill(item='this/is/slashy')
+ '/goto/this-is-slashy/'
+ >>> tf.fill(item=r'this\is\back')
+ '/goto/this_is_back/'
+ >>> tf.fill(item=r'this/is\mixed')
+ '/goto/this-is_mixed/'
+
+
+ >>> 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
+
+
+ >>> tf = TextFiller('{some}/text/to/{fill}')
+ >>> tf.fill(some='prefix', badfill='suffix')
+
+
+ >>> tf = TextFiller('{some/text/to/{fill}')
+ >>> tf.is_valid()
+ False
+ >>> tf.fill(some='prefix', fill='suffix')
+
+ >>> tf = TextFiller('{some}/text/to/{fill:}')
+ >>> tf.is_valid()
+ False
+ >>> tf.fill(some='prefix', fill='suffix')
+
+
+ >>> 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')
+ 7
+ >>> ui.config("sect2", 'v')
+ 'vvv'
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_list(ui, "sect1",
+ ... [('a', 7), ('bbb', 'xxx'), ('c', '-'), ('a', 8)])
+ >>> setconfig_list(ui, "sect2", [('v', 'vvv')])
+ >>> ui.config("sect1", 'a')
+ 8
+ >>> ui.config("sect2", 'v')
+ 'vvv'
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_list(ui, "foo", [
+ ... ("pfx-some-sfx", "ala, ma kota"),
+ ... ("some.nonitem", "bela nie"),
+ ... ("x", "yes"),
+ ... ("pfx-other-sfx", 4)
+ ... ])
+ >>> setconfig_list(ui, "notfoo", [
+ ... ("pfx-some-sfx", "bad"),
+ ... ("pfx-also-sfx", "too"),
+ ... ])
+ >>>
+ >>> for name, value in rgxp_config_items(
+ ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
+ ... print(name, value)
+ some ala, ma kota
+ other 4
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_list(ui, "foo", [
+ ... ("pfx-some-sfx", "ala, ma kota"),
+ ... ("some.nonitem", "bela nie"),
+ ... ("x", "yes"),
+ ... ("pfx-other-sfx", "sth"),
+ ... ])
+ >>> setconfig_list(ui, "notfoo", [
+ ... ("pfx-some-sfx", "bad"),
+ ... ("pfx-also-sfx", "too"),
+ ... ])
+ >>>
+ >>> for name, value in rgxp_configlist_items(
+ ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
+ ... print(name, value)
+ some ['ala', 'ma', 'kota']
+ other ['sth']
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_list(ui, "foo", [
+ ... ("pfx-some-sfx", "true"),
+ ... ("some.nonitem", "bela nie"),
+ ... ("x", "yes"),
+ ... ("pfx-other-sfx", "false"),
+ ... ])
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "pfx-some-sfx": "1",
+ ... "pfx-also-sfx": "0",
+ ... })
+ >>>
+ >>> for name, value in rgxp_configbool_items(
+ ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
+ ... print(name, value)
+ some True
+ other False
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_list(ui, "foo", [
+ ... ("some.item", "ala, ma kota"),
+ ... ("some.nonitem", "bela nie"),
+ ... ("x", "yes"),
+ ... ("other.item", 4),
+ ... ])
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "some.item": "bad",
+ ... "also.item": "too",
+ ... })
+ >>>
+ >>> for name, value in suffix_config_items(
+ ... ui, "foo", 'item'):
+ ... print(name, value)
+ some ala, ma kota
+ other 4
+
+
+ >>> 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_dict(ui, "notfoo", {
+ ... "some.item": "bad",
+ ... "also.item": "too",
+ ... })
+ >>>
+ >>> for name, value in suffix_configlist_items(
+ ... ui, "foo", "item"):
+ ... print(name, value)
+ some ['ala', 'ma', 'kota']
+ other ['kazimira']
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_list(ui, "foo", [
+ ... ("true.item", "true"),
+ ... ("false.item", "false"),
+ ... ("one.item", "1"),
+ ... ("zero.item", "0"),
+ ... ("yes.item", "yes"),
+ ... ("no.item", "no"),
+ ... ("some.nonitem", "1"),
+ ... ("x", "yes"),
+ ... ])
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "some.item": "0",
+ ... "also.item": "too",
+ ... })
+ >>>
+ >>> 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
+ >>>
+ >>> 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')
+
+
+ >>> class SomeClass(object):
+ ... def meth(self, arg):
+ ... return "Original: " + arg
+ >>>
+ >>> @monkeypatch_method(SomeClass)
+ ... def meth(self, arg):
+ ... return "Patched: " + meth.orig(self, arg)
+ >>>
+ >>> obj = SomeClass()
+ >>> print(obj.meth("some param"))
+ Patched: Original: some param
+
+
+ >>> class SomeClass(object):
+ ... def meth(self, arg):
+ ... return "Original: " + arg
+ >>>
+ >>> @monkeypatch_method(SomeClass, "meth")
+ ... def another_meth(self, arg):
+ ... return "Patched: " + another_meth.orig(self, arg)
+ >>>
+ >>> obj = SomeClass()
+ >>> print(obj.meth("some param"))
+ Patched: Original: some param
+
+
+ >>> import random
+ >>> @monkeypatch_function(random)
+ ... def seed(x=None):
+ ... 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
+
+ >>> import random
+ >>> @monkeypatch_function(random, 'choice')
+ ... def choice_first(sequence):
+ ... return sequence[0]
+ >>> for x in range(0, 4): print(random.choice("abcdefgh"))
+ a
+ a
+ a
+ a
+
+
+ >>> cmdtable = {}
+ >>> cmd = command(cmdtable)
+ >>>
+ >>> @cmd("somecmd", [], "somecmd")
+ ... def mycmd(ui, repo, sth, **opts):
+ ... pass
+ >>>
+ >>> @cmd("othercmd", [
+ ... ('l', 'list', None, 'List widgets'),
+ ... ('p', 'pagesize', 10, 'Page size'),
+ ... ], "othercmd [-l] [-p 20]", norepo=True)
+ ... def othercmd(ui, sth, **opts):
+ ... pass
+ >>>
+ >>> sorted(cmdtable.keys())
+ ['othercmd', 'somecmd']
+ >>> cmdtable['othercmd'] # doctest: +ELLIPSIS
+ (<function othercmd at ...>, [('l', 'list', None, 'List widgets'), ('p', 'pagesize', 10, 'Page size')], 'othercmd [-l] [-p 20]')
+
+
+ >>> from mercurial import commands
+ >>> # 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
+ True
+ >>> mycmd.__dict__['norepo'] if mycmd.__dict__ else False
+ False
+
+
+ >>> re = direct_import("re")
+ >>> re.__name__
+ 're'
+ >>> re.search("^(.)", "Ala").group(1)
+ 'A'
+
+
+ >>> k = direct_import("anydbm", ["dbhash", "gdbm", "dbm", "bsddb.db"])
+ >>> k.__name__
+ 'anydbm'
+
+
+ >>> m1, loaded = direct_import_ext("xml.sax.handler")
+ >>> m1.__name__, loaded
+ ('xml.sax.handler', True)
+
+ >>> m2, loaded = direct_import_ext("xml.sax.handler")
+ >>> m2.__name__, loaded
+ ('xml.sax.handler', False)
+
+ >>> m1 == m2
+ True
+
+
+ >>> disable_logging("keyring")
+
+
+
+
+'''
diff --git a/tests/py2win_doctests_mercurial_extension_utils.py b/tests/py2win_doctests_mercurial_extension_utils.py
new file mode 100644
index 0000000..a9faaf4
--- /dev/null
+++ b/tests/py2win_doctests_mercurial_extension_utils.py
@@ -0,0 +1,351 @@
+# -*- coding: utf-8 -*-
+#
+# mercurial extension utils: Windows doctests
+#
+# Copyright (c) 2015 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# See README.txt for more details.
+
+r'''
+This module exists solely to give some examples (and doctest)
+of mercurial_extension_utils behaviour on Windows. Structure
+mimics that of mercurial_extension_utils.
+
+
+
+ >>> normalize_path("~/src")
+ 'C:/Users/lordvader/src'
+ >>> normalize_path("/some/where")
+ 'c:/some/where'
+ >>> normalize_path("/some/where/")
+ 'c:/some/where'
+ >>> normalize_path("../../../some/where")
+ 'c:/Users/lordvader/some/where'
+ >>> normalize_path(r'C:\Users\Joe\source files')
+ 'C:/Users/Joe/source files'
+
+
+
+ >>> belongs_to_tree("/tmp/sub/dir", "/tmp")
+ 'c:/tmp'
+ >>> belongs_to_tree("/tmp", "/tmp")
+ 'c:/tmp'
+ >>> belongs_to_tree("/tmp/sub", "/tmp/sub/dir/../..")
+ 'c:/tmp'
+
+ >>> belongs_to_tree("/usr/sub", "/tmp")
+
+ >>> home_work_src = os.path.join(os.environ["HOME"], "work", "src")
+ >>> belongs_to_tree(home_work_src, "~/work")
+ 'C:/Users/lordvader/work'
+ >>> belongs_to_tree("/home/lordvader/devel/webapps" if os.name != 'nt' else "c:/users/lordvader/devel/webapps",
+ ... "~lordvader/devel")
+ 'C:/Users/lordvader/devel'
+
+
+
+ >>> belongs_to_tree_group("/tmp/sub/dir", ["/bin", "/tmp"])
+ 'c:/tmp'
+ >>> belongs_to_tree_group("/tmp", ["/tmp"])
+ 'c:/tmp'
+ >>> belongs_to_tree_group("/tmp/sub/dir", ["/bin", "~/src"])
+
+ >>> belongs_to_tree_group("/tmp/sub/dir", ["/tmp", "/bin", "/tmp", "/tmp/sub"])
+ 'c:/tmp/sub'
+
+ >>> belongs_to_tree_group("C:/Users/lordvader/src/apps", ["~/src", "C:/Users/lordvader"])
+ 'C:/Users/lordvader/src'
+
+
+
+ >>> pat = DirectoryPattern('~/src/{suffix}')
+ >>> pat.is_valid()
+ True
+ >>> pat.search("/opt/repos/abcd")
+ >>> pat.search("~/src/repos/in/tree")
+ {'suffix': 'repos/in/tree'}
+ >>> pat.search("c:/users/lordvader/src/repos/here")
+ {'suffix': 'repos/here'}
+ >>> pat.search("C:/Users/lordvader/src/repos/here")
+ {'suffix': 'repos/here'}
+ >>> pat.search("/home/lordvader/src")
+
+ >>> pat = DirectoryPattern('~lordvader/devel/(item)')
+ >>> pat.search("/opt/repos/abcd")
+ >>> pat.search("~/devel/libxuza")
+ {'item': 'libxuza'}
+ >>> pat.search("~/devel/libs/libxuza")
+ >>> pat.search("C:/Users/lordvader/devel/webapp")
+ {'item': 'webapp'}
+ >>> pat.search("/users/lordvader/devel/webapp")
+ {'item': 'webapp'}
+ >>> pat.search("/home/lordvader/devel")
+
+ >>> pat = DirectoryPattern('/opt/repos/(group)/{suffix}')
+ >>> pat.search("/opt/repos/abcd")
+ >>> pat.search("/opt/repos/libs/abcd")
+ {'group': 'libs', 'suffix': 'abcd'}
+ >>> pat.search("/opt/repos/apps/mini/webby")
+ {'group': 'apps', 'suffix': 'mini/webby'}
+
+ >>> pat = DirectoryPattern('/opt/repos/(group/{suffix}')
+ >>> pat.is_valid()
+ False
+ >>> pat.search('/opt/repos/some/where')
+
+
+
+ >>> tf = TextFiller('{some}/text/to/{fill}')
+ >>> tf.fill(some='prefix', fill='suffix')
+ 'prefix/text/to/suffix'
+ >>> tf.fill(some='/ab/c/d', fill='x')
+ '/ab/c/d/text/to/x'
+
+ >>> tf = TextFiller('{some}/text/to/{some}')
+ >>> tf.is_valid()
+ True
+ >>> tf.fill(some='val')
+ 'val/text/to/val'
+ >>> tf.fill(some='ab/c/d', fill='x')
+ 'ab/c/d/text/to/ab/c/d'
+
+ >>> tf = TextFiller('{prefix:_=___}/goto/{suffix:/=-}')
+ >>> tf.fill(prefix='some_prefix', suffix='some/long/suffix')
+ 'some___prefix/goto/some-long-suffix'
+
+ >>> tf = TextFiller('{prefix:/home/=}/docs/{suffix:.txt=.html}')
+ >>> tf.fill(prefix='/home/joe', suffix='some/document.txt')
+ 'joe/docs/some/document.html'
+
+ >>> tf = TextFiller(r'/goto/{item:/=-:\=_}/')
+ >>> tf.fill(item='this/is/slashy')
+ '/goto/this-is-slashy/'
+ >>> tf.fill(item=r'this\is\back')
+ '/goto/this_is_back/'
+ >>> tf.fill(item=r'this/is\mixed')
+ '/goto/this-is_mixed/'
+
+ >>> 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
+
+ >>> tf = TextFiller('{some}/text/to/{fill}')
+ >>> tf.fill(some='prefix', badfill='suffix')
+
+ >>> tf = TextFiller('{some/text/to/{fill}')
+ >>> tf.is_valid()
+ False
+ >>> tf.fill(some='prefix', fill='suffix')
+
+ >>> tf = TextFiller('{some}/text/to/{fill:}')
+ >>> tf.is_valid()
+ False
+ >>> tf.fill(some='prefix', fill='suffix')
+
+
+
+ >>> 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')
+ 7
+ >>> ui.config("sect2", 'v')
+ 'vvv'
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_dict(ui, "foo", {
+ ... "pfx-some-sfx": "ala, ma kota",
+ ... "some.nonitem": "bela nie",
+ ... "x": "yes",
+ ... "pfx-other-sfx": 4})
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "pfx-some-sfx": "bad",
+ ... "pfx-also-sfx": "too",
+ ... })
+ >>>
+ >>> for name, value in rgxp_config_items(
+ ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
+ ... print name, value
+ some ala, ma kota
+ other 4
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_dict(ui, "foo", {
+ ... "pfx-some-sfx": "ala, ma kota",
+ ... "some.nonitem": "bela nie",
+ ... "x": "yes",
+ ... "pfx-other-sfx": "sth"})
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "pfx-some-sfx": "bad",
+ ... "pfx-also-sfx": "too",
+ ... })
+ >>>
+ >>> for name, value in rgxp_configlist_items(
+ ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
+ ... print name, value
+ some ['ala', 'ma', 'kota']
+ other ['sth']
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_dict(ui, "foo", {
+ ... "pfx-some-sfx": "true",
+ ... "some.nonitem": "bela nie",
+ ... "x": "yes",
+ ... "pfx-other-sfx": "false"})
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "pfx-some-sfx": "1",
+ ... "pfx-also-sfx": "0",
+ ... })
+ >>>
+ >>> for name, value in rgxp_configbool_items(
+ ... ui, "foo", re.compile(r'^pfx-(\w+)-sfx$')):
+ ... print name, value
+ some True
+ other False
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_dict(ui, "foo", {
+ ... "some.item": "ala, ma kota",
+ ... "some.nonitem": "bela nie",
+ ... "x": "yes",
+ ... "other.item": 4})
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "some.item": "bad",
+ ... "also.item": "too",
+ ... })
+ >>>
+ >>> for name, value in suffix_config_items(
+ ... ui, "foo", 'item'):
+ ... print name, value
+ some ala, ma kota
+ other 4
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_dict(ui, "foo", {
+ ... "some.item": "ala, ma kota",
+ ... "some.nonitem": "bela nie",
+ ... "x": "yes",
+ ... "other.item": "kazimira"})
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "some.item": "bad",
+ ... "also.item": "too",
+ ... })
+ >>>
+ >>> for name, value in suffix_configlist_items(
+ ... ui, "foo", "item"):
+ ... print name, value
+ some ['ala', 'ma', 'kota']
+ other ['kazimira']
+
+
+ >>> import mercurial.ui; ui = mercurial.ui.ui()
+ >>> setconfig_dict(ui, "foo", {
+ ... "true.item": "true",
+ ... "false.item": "false",
+ ... "one.item": "1",
+ ... "zero.item": "0",
+ ... "yes.item": "yes",
+ ... "no.item": "no",
+ ... "some.nonitem": "1",
+ ... "x": "yes"})
+ >>> setconfig_dict(ui, "notfoo", {
+ ... "some.item": "0",
+ ... "also.item": "too",
+ ... })
+ >>>
+ >>> for name, value in suffix_configbool_items(
+ ... ui, "foo", "item"):
+ ... print name, str(value)
+ zero False
+ yes True
+ one True
+ true True
+ no False
+ false 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')
+
+
+ >>> class SomeClass(object):
+ ... def meth(self, arg):
+ ... return "Original: " + arg
+ >>>
+ >>> @monkeypatch_method(SomeClass)
+ ... def meth(self, arg):
+ ... return "Patched: " + meth.orig(self, arg)
+ >>>
+ >>> obj = SomeClass()
+ >>> print obj.meth("some param")
+ Patched: Original: some param
+
+
+ >>> class SomeClass(object):
+ ... def meth(self, arg):
+ ... return "Original: " + arg
+ >>>
+ >>> @monkeypatch_method(SomeClass, "meth")
+ ... def another_meth(self, arg):
+ ... return "Patched: " + another_meth.orig(self, arg)
+ >>>
+ >>> obj = SomeClass()
+ >>> print obj.meth("some param")
+ Patched: Original: some param
+
+
+ >>> import random
+ >>> @monkeypatch_function(random)
+ ... def seed(x=None):
+ ... 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
+
+ >>> import random
+ >>> @monkeypatch_function(random, 'choice')
+ ... def choice_first(sequence):
+ ... return sequence[0]
+ >>> for x in range(0, 4): print random.choice("abcdefgh")
+ a
+ a
+ a
+ a
+
+'''
diff --git a/tests/test_doctest.py b/tests/test_doctest.py
new file mode 100644
index 0000000..6ef1e09
--- /dev/null
+++ b/tests/test_doctest.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+
+# pylint: disable=missing-docstring,unused-argument,too-many-arguments
+
+import os
+import unittest
+import doctest
+import getpass
+import mercurial_extension_utils
+import sys
+
+USING_PY3 = sys.version_info >= (3, 0, 0)
+
+# IMPORTANT NOTE:
+#
+# As I wanted doctests to be readable, most of them assume
+# specific paths and names (for example some tests assume /home/lordvader
+# as home directory). This is on purpose,
+#
+# >>> normalize_path("~/src")
+# '/home/lordvader/src'
+#
+# is readable and fulfills documentation role well, whatever I could write
+# instead to handle various accounts, would be unreadable mess.
+#
+# To make running tests possible, below we adapt docstrings
+# before executing them.
+
+
+class FixingUpDocTestParser(doctest.DocTestParser): # pylint: disable=no-init
+
+ PATTERN_HOME = '/home/lordvader'
+ PATTERN_NAME = 'lordvader'
+ TRUE_HOME = os.path.expanduser("~")
+ TRUE_NAME = getpass.getuser()
+ REL_TO_HOME = os.path.relpath(TRUE_HOME)
+
+ def get_doctest(self, string, globs, name, filename, lineno):
+ # Replace /home/lordvader with whatever true home is
+ # (and similar)
+ string = string \
+ .replace(self.PATTERN_HOME, self.TRUE_HOME) \
+ .replace(self.PATTERN_NAME, self.TRUE_NAME)
+ # Special fixup for ../../.. pointing at home
+ string = string.replace('"../../..', '"' + self.REL_TO_HOME)
+ return doctest.DocTestParser.get_doctest(
+ self, string, globs, name, filename, lineno)
+
+
+def load_tests(loader, tests, pattern):
+ if os.name != 'nt':
+ if USING_PY3:
+ finder = doctest.DocTestFinder(parser=FixingUpDocTestParser())
+ suite = doctest.DocTestSuite(
+ mercurial_extension_utils,
+ test_finder=finder)
+ else:
+ suite = doctest.DocFileSuite(
+ "py2_doctests_mercurial_extension_utils.py",
+ module_relative=True,
+ globs=mercurial_extension_utils.__dict__,
+ parser=FixingUpDocTestParser())
+ else:
+ if USING_PY3:
+ raise Exception("TODO: py3 tests for Windows")
+ else:
+ suite = doctest.DocFileSuite(
+ "py2win_doctests_mercurial_extension_utils.py",
+ module_relative=True,
+ globs=mercurial_extension_utils.__dict__,
+ parser=FixingUpDocTestParser())
+ tests.addTests(suite)
+ return tests
+
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/tests/test_find_repositories_below.py b/tests/test_find_repositories_below.py
new file mode 100644
index 0000000..ff3c4bf
--- /dev/null
+++ b/tests/test_find_repositories_below.py
@@ -0,0 +1,101 @@
+
+import mercurial_extension_utils as meu
+import os
+import tempfile
+import shutil
+import subprocess
+import unittest
+
+
+class RepoBuffer(object):
+ def __init__(self):
+ self.location = tempfile.mkdtemp()
+ self.setup_repos()
+
+ def __del__(self):
+ shutil.rmtree(self.location)
+
+ def setup_repos(self):
+ self._exec_in_top("hg", "init", "c/c1/c11-repo")
+ self._exec_in_top("hg", "init", "a-repo")
+ self._exec_in_top("hg", "init", "b/b3/b3a-repo")
+ self._exec_in_top("hg", "init", "b/b1-repo")
+ self._exec_in_top("hg", "init", "b/b2-repo")
+ self._exec_in_top("hg", "init", "b/b3/b3b-repo")
+ self._exec_in_top("hg", "init", "a-repo/a1-subrepo")
+ self._exec_in_top("hg", "init", "b/b1-repo/b11-subrepo")
+
+ def _exec_in_top(self, *args):
+ status = subprocess.Popen(args, cwd=self.location).wait()
+ if status:
+ raise subprocess.CalledProcessError(status, args[0])
+
+
+class TestFindRepositories(unittest.TestCase):
+
+ buffer = RepoBuffer()
+
+ def _check_path_and_fix(self, repo_path, where):
+ self.assertTrue(os.path.isdir(repo_path))
+ self.assertTrue(os.path.isabs(repo_path))
+ self.assertTrue(os.path.isdir(os.path.join(repo_path, b".hg")))
+ norm_where = meu.normalize_path(where)
+ fixed_path = meu.pycompat.bytestr(repo_path.replace(norm_where, b"/xxx"))
+ if fixed_path == repo_path:
+ self.fail("Failed to normalize path, repo_path %s, where %s" % (repo_path, norm_where))
+ return fixed_path
+
+ def test_std(self):
+ where = self.buffer.location
+ items = []
+ for repo_path in meu.find_repositories_below(where):
+ items.append(self._check_path_and_fix(repo_path, where))
+ self.assertEqual(items, [meu.pycompat.bytestr(x) for x in [
+ "/xxx/a-repo",
+ "/xxx/b/b1-repo",
+ "/xxx/b/b2-repo",
+ "/xxx/b/b3/b3a-repo",
+ "/xxx/b/b3/b3b-repo",
+ "/xxx/c/c1/c11-repo",
+ ]])
+
+ def test_std_check_inside(self):
+ where = self.buffer.location
+ items = []
+ for repo_path in meu.find_repositories_below(where, check_inside=True):
+ items.append(self._check_path_and_fix(repo_path, where))
+ self.assertEqual(items, [meu.pycompat.bytestr(x) for x in [
+ "/xxx/a-repo",
+ "/xxx/a-repo/a1-subrepo",
+ "/xxx/b/b1-repo",
+ "/xxx/b/b1-repo/b11-subrepo",
+ "/xxx/b/b2-repo",
+ "/xxx/b/b3/b3a-repo",
+ "/xxx/b/b3/b3b-repo",
+ "/xxx/c/c1/c11-repo",
+ ]])
+
+ def test_from_repo(self):
+ where = self.buffer.location
+ items = []
+ for repo_path in meu.find_repositories_below(
+ os.path.join(where, "a-repo")):
+ items.append(self._check_path_and_fix(repo_path, where))
+ self.assertEqual(items, [meu.pycompat.bytestr(x) for x in [
+ "/xxx/a-repo",
+ ]])
+
+ def test_from_repo_check_inside(self):
+ where = self.buffer.location
+ items = []
+ for repo_path in meu.find_repositories_below(
+ os.path.join(where, "a-repo"), check_inside=True):
+ items.append(self._check_path_and_fix(repo_path, where))
+ self.assertEqual(items, [meu.pycompat.bytestr(x) for x in [
+ "/xxx/a-repo",
+ "/xxx/a-repo/a1-subrepo",
+ ]])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..3fc69ff
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,55 @@
+
+[tox]
+minversion = 1.8
+toxworkdir = {homedir}/.tox/work/mercurial/extension_utils
+distshare = {homedir}/.tox/distshare
+envlist = py{35,36,37}-hg{52,51,50},py27-hg{52,51,50,49,48,45,44,41,38,37,33,30,29,27}
+skip_missing_interpreters = true
+;; We don't test python3.5, it's old and causes regexp problems (and some apis
+;; actually fail).
+;;
+;; Installing custom pythons:
+;; sudo add-apt-repository ppa:deadsnakes/ppa
+
+[testenv]
+passenv = HOME
+setenv =
+ HGRCPATH = {toxworkdir}/hgrc
+ py35: HGPYTHON3 = 1
+ py36: HGPYTHON3 = 1
+ py37: HGPYTHON3 = 1
+deps =
+ py26: unittest2
+ hg27: Mercurial>=2.7,<2.8
+ hg28: Mercurial>=2.8,<2.9
+ hg29: Mercurial>=2.9,<3.0
+ hg30: Mercurial>=3.0,<3.1
+ hg31: Mercurial>=3.1,<3.2
+ hg32: Mercurial>=3.2,<3.3
+ hg33: Mercurial>=3.3,<3.4
+ hg34: Mercurial>=3.4,<3.5
+ hg35: Mercurial>=3.5,<3.6
+ hg36: Mercurial>=3.6,<3.7
+ hg37: Mercurial>=3.7,<3.8
+ hg38: Mercurial>=3.8,<3.9
+ hg40: Mercurial>=4.0,<4.1
+ hg41: Mercurial>=4.1,<4.2
+ hg42: Mercurial>=4.2,<4.3
+ hg43: Mercurial>=4.3,<4.4
+ hg44: Mercurial>=4.4,<4.5
+ hg45: Mercurial>=4.5,<4.6
+ hg46: Mercurial>=4.6,<4.7
+ hg47: Mercurial>=4.7,<4.8
+ hg48: Mercurial>=4.8,<4.9
+ hg49: Mercurial>=4.9,<4.10
+ hg50: Mercurial>=5.0,<5.1
+ hg51: Mercurial>=5.1,<5.2
+ hg52: Mercurial>=5.2,<5.3
+commands =
+ py26: unit2 discover tests
+ py27: python -m unittest discover tests
+ py35: python -m unittest discover tests
+ py36: python -m unittest discover tests
+ py37: python -m unittest discover tests
+
+