diff --git a/mercurial/parsers.c b/mercurial/parsers.c --- a/mercurial/parsers.c +++ b/mercurial/parsers.c @@ -1941,6 +1941,25 @@ static void module_init(PyObject *mod) dirstate_unset = Py_BuildValue("ciii", 'n', 0, -1, -1); } +static int check_python_version() +{ + PyObject *sys = PyImport_ImportModule("sys"); + PyObject *hexversion = PyObject_GetAttrString(sys, "hexversion"); + long version = PyInt_AsLong(hexversion); + /* sys.hexversion is a 32-bit number by default, so the -1 case + * should only occur in unusual circumstances (e.g. if sys.hexversion + * is manually set to an invalid value). */ + if ((version == -1) || (version >> 16 != PY_VERSION_HEX >> 16)) { + PyErr_Format(PyExc_ImportError, "Python minor version mismatch: " + "The Mercurial extension modules were compiled with Python " + PY_VERSION ", but Mercurial is currently using Python with " + "sys.hexversion=%ld: Python %s\n at: %s", version, + Py_GetVersion(), Py_GetProgramFullPath()); + return -1; + } + return 0; +} + #ifdef IS_PY3K static struct PyModuleDef parsers_module = { PyModuleDef_HEAD_INIT, @@ -1952,6 +1971,8 @@ static struct PyModuleDef parsers_module PyMODINIT_FUNC PyInit_parsers(void) { + if (check_python_version() == -1) + return; PyObject *mod = PyModule_Create(&parsers_module); module_init(mod); return mod; @@ -1959,6 +1980,8 @@ PyMODINIT_FUNC PyInit_parsers(void) #else PyMODINIT_FUNC initparsers(void) { + if (check_python_version() == -1) + return; PyObject *mod = Py_InitModule3("parsers", methods, parsers_doc); module_init(mod); } diff --git a/tests/test-parseindex2.py b/tests/test-parseindex2.py --- a/tests/test-parseindex2.py +++ b/tests/test-parseindex2.py @@ -1,6 +1,8 @@ from mercurial import parsers from mercurial.node import nullid, nullrev import struct +import subprocess +import sys # This unit test compares the return value of the original Python # implementation of parseindex and the new C implementation for @@ -97,7 +99,62 @@ def parse_index2(data, inline): index, chunkcache = parsers.parse_index2(data, inline) return list(index), chunkcache +def importparsers(hexversion): + """Import mercurial.parsers with the given sys.hexversion.""" + # The file parsers.c inspects sys.hexversion to determine the version + # of the currently-running Python interpreter, so we monkey-patch + # sys.hexversion to simulate using different versions. + code = ("import sys; sys.hexversion=%s; " + "import mercurial.parsers" % hexversion) + cmd = "python -c \"%s\"" % code + # We need to do these tests inside a subprocess because parser.c's + # version-checking code happens inside the module init function, and + # when using reload() to reimport an extension module, "The init function + # of extension modules is not called a second time" + # (from http://docs.python.org/2/library/functions.html?#reload). + p = subprocess.Popen(cmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + return p.communicate() # returns stdout, stderr + +def printhexfail(testnumber, hexversion, msg): + try: + hexstring = hex(hexversion) + except TypeError: + hexstring = None + print ("%s) using Python %s and patched sys.hexversion %r (%r): %s" % + (testnumber, sys.version_info, hexversion, hexstring, msg)) + +def testversionokay(testnumber, hexversion): + stdout, stderr = importparsers(hexversion) + if stdout: + printhexfail(testnumber, hexversion, + "Expected no stdout but got: %r" % stdout) + +def testversionfail(testnumber, hexversion): + stdout, stderr = importparsers(hexversion) + if not "ImportError: Python minor version mismatch" in stdout: + printhexfail(testnumber, hexversion, + "Expected stdout to contain %r but got: %r" % + (errstring, stdout)) + +def makehex(major, minor, micro): + return int("%x%02x%02x00" % (major, minor, micro), 16) + +def runversiontests(): + """Test importing parsers using different Python versions.""" + info = sys.version_info + major, minor, micro = info[0], info[1], info[2] + # Test same major-minor versions. + testversionokay(1, makehex(major, minor, micro)) + testversionokay(2, makehex(major, minor, micro + 1)) + # Test different major-minor versions. + testversionfail(3, makehex(major + 1, minor, micro)) + testversionfail(4, makehex(major, minor + 1, micro)) + testversionfail(5, "'foo'") + def runtest() : + runversiontests() + # Check that parse_index2() raises TypeError on bad arguments. try: parse_index2(0, True)