diff --git a/Lib/runpy.py b/Lib/runpy.py index 4e8d563a8a8..673b1a86cd1 100755 --- a/Lib/runpy.py +++ b/Lib/runpy.py @@ -23,19 +23,20 @@ __all__ = [ def _run_code(code, run_globals, init_globals=None, mod_name=None, mod_fname=None, - mod_loader=None): + mod_loader=None, pkg_name=None): """Helper for _run_module_code""" if init_globals is not None: run_globals.update(init_globals) run_globals.update(__name__ = mod_name, __file__ = mod_fname, - __loader__ = mod_loader) + __loader__ = mod_loader, + __package__ = pkg_name) exec code in run_globals return run_globals def _run_module_code(code, init_globals=None, mod_name=None, mod_fname=None, - mod_loader=None): + mod_loader=None, pkg_name=None): """Helper for run_module""" # Set up the top level namespace dictionary temp_module = imp.new_module(mod_name) @@ -49,7 +50,8 @@ def _run_module_code(code, init_globals=None, sys.modules[mod_name] = temp_module try: _run_code(code, mod_globals, init_globals, - mod_name, mod_fname, mod_loader) + mod_name, mod_fname, + mod_loader, pkg_name) finally: sys.argv[0] = saved_argv0 if restore_module: @@ -95,11 +97,12 @@ def _run_module_as_main(mod_name, set_argv0=True): __loader__ """ loader, code, fname = _get_module_details(mod_name) + pkg_name = mod_name.rpartition('.')[0] main_globals = sys.modules["__main__"].__dict__ if set_argv0: sys.argv[0] = fname return _run_code(code, main_globals, None, - "__main__", fname, loader) + "__main__", fname, loader, pkg_name) def run_module(mod_name, init_globals=None, run_name=None, alter_sys=False): @@ -110,13 +113,14 @@ def run_module(mod_name, init_globals=None, loader, code, fname = _get_module_details(mod_name) if run_name is None: run_name = mod_name + pkg_name = mod_name.rpartition('.')[0] if alter_sys: return _run_module_code(code, init_globals, run_name, - fname, loader) + fname, loader, pkg_name) else: # Leave the sys module alone - return _run_code(code, {}, init_globals, - run_name, fname, loader) + return _run_code(code, {}, init_globals, run_name, + fname, loader, pkg_name) if __name__ == "__main__": diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 3a8e8f190f4..9d783565504 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -35,15 +35,15 @@ def temp_dir(): finally: shutil.rmtree(dirname) -test_source = ("""\ +test_source = """\ # Script may be run with optimisation enabled, so don't rely on assert # statements being executed def assertEqual(lhs, rhs): if lhs != rhs: - raise AssertionError("%r != %r" % (lhs, rhs)) + raise AssertionError('%r != %r' % (lhs, rhs)) def assertIdentical(lhs, rhs): if lhs is not rhs: - raise AssertionError("%r is not %r" % (lhs, rhs)) + raise AssertionError('%r is not %r' % (lhs, rhs)) # Check basic code execution result = ['Top level assignment'] def f(): @@ -53,17 +53,18 @@ assertEqual(result, ['Top level assignment', 'Lower level reference']) # Check population of magic variables assertEqual(__name__, '__main__') print '__file__==%r' % __file__ +print '__package__==%r' % __package__ # Check the sys module import sys assertIdentical(globals(), sys.modules[__name__].__dict__) print 'sys.argv[0]==%r' % sys.argv[0] -""") +""" -def _make_test_script(script_dir, script_basename): - script_filename = script_basename+os.extsep+"py" +def _make_test_script(script_dir, script_basename, source=test_source): + script_filename = script_basename+os.extsep+'py' script_name = os.path.join(script_dir, script_filename) - script_file = open(script_name, "w") - script_file.write(test_source) + script_file = open(script_name, 'w') + script_file.write(source) script_file.close() return script_name @@ -76,71 +77,108 @@ def _compile_test_script(script_name): return compiled_name def _make_test_zip(zip_dir, zip_basename, script_name): - zip_filename = zip_basename+os.extsep+"zip" + zip_filename = zip_basename+os.extsep+'zip' zip_name = os.path.join(zip_dir, zip_filename) zip_file = zipfile.ZipFile(zip_name, 'w') zip_file.write(script_name, os.path.basename(script_name)) zip_file.close() # if verbose: # zip_file = zipfile.ZipFile(zip_name, 'r') - # print "Contents of %r:" % zip_name + # print 'Contents of %r:' % zip_name # zip_file.printdir() # zip_file.close() return zip_name +def _make_test_pkg(pkg_dir): + os.mkdir(pkg_dir) + _make_test_script(pkg_dir, '__init__', '') + +# There's no easy way to pass the script directory in to get +# -m to work (avoiding that is the whole point of making +# directories and zipfiles executable!) +# So we fake it for testing purposes with a custom launch script +launch_source = """\ +import sys, os.path, runpy +sys.path[0:0] = os.path.dirname(__file__) +runpy._run_module_as_main(%r) +""" + +def _make_launch_script(script_dir, script_basename, module_name): + return _make_test_script(script_dir, script_basename, + launch_source % module_name) + class CmdLineTest(unittest.TestCase): - def _check_script(self, script_name, expected_file, expected_argv0): - exit_code, data = _run_python(script_name) + def _check_script(self, script_name, expected_file, + expected_argv0, expected_package, + *cmd_line_switches): + run_args = cmd_line_switches + (script_name,) + exit_code, data = _run_python(*run_args) if verbose: - print "Output from test script %r:" % script_name + print 'Output from test script %r:' % script_name print data self.assertEqual(exit_code, 0) printed_file = '__file__==%r' % expected_file printed_argv0 = 'sys.argv[0]==%r' % expected_argv0 + printed_package = '__package__==%r' % expected_package + if verbose: + print 'Expected output:' + print printed_file + print printed_package + print printed_argv0 self.assert_(printed_file in data) + self.assert_(printed_package in data) self.assert_(printed_argv0 in data) def test_basic_script(self): with temp_dir() as script_dir: - script_name = _make_test_script(script_dir, "script") - self._check_script(script_name, script_name, script_name) + script_name = _make_test_script(script_dir, 'script') + self._check_script(script_name, script_name, script_name, None) def test_script_compiled(self): with temp_dir() as script_dir: - script_name = _make_test_script(script_dir, "script") + script_name = _make_test_script(script_dir, 'script') compiled_name = _compile_test_script(script_name) os.remove(script_name) - self._check_script(compiled_name, compiled_name, compiled_name) + self._check_script(compiled_name, compiled_name, compiled_name, None) def test_directory(self): with temp_dir() as script_dir: - script_name = _make_test_script(script_dir, "__main__") - self._check_script(script_dir, script_name, script_dir) + script_name = _make_test_script(script_dir, '__main__') + self._check_script(script_dir, script_name, script_dir, '') def test_directory_compiled(self): with temp_dir() as script_dir: - script_name = _make_test_script(script_dir, "__main__") + script_name = _make_test_script(script_dir, '__main__') compiled_name = _compile_test_script(script_name) os.remove(script_name) - self._check_script(script_dir, compiled_name, script_dir) + self._check_script(script_dir, compiled_name, script_dir, '') def test_zipfile(self): with temp_dir() as script_dir: - script_name = _make_test_script(script_dir, "__main__") - zip_name = _make_test_zip(script_dir, "test_zip", script_name) - self._check_script(zip_name, None, zip_name) + script_name = _make_test_script(script_dir, '__main__') + zip_name = _make_test_zip(script_dir, 'test_zip', script_name) + self._check_script(zip_name, None, zip_name, '') def test_zipfile_compiled(self): with temp_dir() as script_dir: - script_name = _make_test_script(script_dir, "__main__") + script_name = _make_test_script(script_dir, '__main__') compiled_name = _compile_test_script(script_name) - zip_name = _make_test_zip(script_dir, "test_zip", compiled_name) - self._check_script(zip_name, None, zip_name) + zip_name = _make_test_zip(script_dir, 'test_zip', compiled_name) + self._check_script(zip_name, None, zip_name, '') + + def test_module_in_package(self): + with temp_dir() as script_dir: + pkg_dir = os.path.join(script_dir, 'test_pkg') + _make_test_pkg(pkg_dir) + script_name = _make_test_script(pkg_dir, 'script') + launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg.script') + self._check_script(launch_name, script_name, + script_name, 'test_pkg') def test_main(): test.test_support.run_unittest(CmdLineTest) test.test_support.reap_children() -if __name__ == "__main__": +if __name__ == '__main__': test_main() diff --git a/Lib/test/test_pkg.py b/Lib/test/test_pkg.py index 7a4c01ba4ae..28f994316ee 100644 --- a/Lib/test/test_pkg.py +++ b/Lib/test/test_pkg.py @@ -188,11 +188,13 @@ class Test(unittest.TestCase): import t5 self.assertEqual(fixdir(dir(t5)), ['__doc__', '__file__', '__name__', - '__path__', 'foo', 'string', 't5']) + '__package__', '__path__', 'foo', 'string', 't5']) self.assertEqual(fixdir(dir(t5.foo)), - ['__doc__', '__file__', '__name__', 'string']) + ['__doc__', '__file__', '__name__', '__package__', + 'string']) self.assertEqual(fixdir(dir(t5.string)), - ['__doc__', '__file__', '__name__', 'spam']) + ['__doc__', '__file__', '__name__','__package__', + 'spam']) def test_6(self): hier = [ @@ -208,14 +210,14 @@ class Test(unittest.TestCase): import t6 self.assertEqual(fixdir(dir(t6)), ['__all__', '__doc__', '__file__', - '__name__', '__path__']) + '__name__', '__package__', '__path__']) s = """ import t6 from t6 import * self.assertEqual(fixdir(dir(t6)), ['__all__', '__doc__', '__file__', - '__name__', '__path__', 'eggs', - 'ham', 'spam']) + '__name__', '__package__', '__path__', + 'eggs', 'ham', 'spam']) self.assertEqual(dir(), ['eggs', 'ham', 'self', 'spam', 't6']) """ self.run_code(s) @@ -241,17 +243,19 @@ class Test(unittest.TestCase): t7, sub, subsub = None, None, None import t7 as tas self.assertEqual(fixdir(dir(tas)), - ['__doc__', '__file__', '__name__', '__path__']) + ['__doc__', '__file__', '__name__', + '__package__', '__path__']) self.failIf(t7) from t7 import sub as subpar self.assertEqual(fixdir(dir(subpar)), - ['__doc__', '__file__', '__name__', '__path__']) + ['__doc__', '__file__', '__name__', + '__package__', '__path__']) self.failIf(t7) self.failIf(sub) from t7.sub import subsub as subsubsub self.assertEqual(fixdir(dir(subsubsub)), - ['__doc__', '__file__', '__name__', '__path__', - 'spam']) + ['__doc__', '__file__', '__name__', + '__package__', '__path__', 'spam']) self.failIf(t7) self.failIf(sub) self.failIf(subsub) diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py index dfefee982f2..165cb63df5c 100644 --- a/Lib/test/test_runpy.py +++ b/Lib/test/test_runpy.py @@ -5,7 +5,12 @@ import os.path import sys import tempfile from test.test_support import verbose, run_unittest, forget -from runpy import _run_code, _run_module_code, _run_module_as_main, run_module +from runpy import _run_code, _run_module_code, run_module + +# Note: This module can't safely test _run_module_as_main as it +# runs its tests in the current process, which would mess with the +# real __main__ module (usually test.regrtest) +# See test_cmd_line_script for a test that executes that code path # Set up the test code and expected results @@ -36,6 +41,7 @@ class RunModuleCodeTest(unittest.TestCase): self.failUnless(d["__name__"] is None) self.failUnless(d["__file__"] is None) self.failUnless(d["__loader__"] is None) + self.failUnless(d["__package__"] is None) self.failUnless(d["run_argv0"] is saved_argv0) self.failUnless("run_name" not in d) self.failUnless(sys.argv[0] is saved_argv0) @@ -45,13 +51,15 @@ class RunModuleCodeTest(unittest.TestCase): name = "" file = "Some other nonsense" loader = "Now you're just being silly" + package = '' # Treat as a top level module d1 = dict(initial=initial) saved_argv0 = sys.argv[0] d2 = _run_module_code(self.test_source, d1, name, file, - loader) + loader, + package) self.failUnless("result" not in d1) self.failUnless(d2["initial"] is initial) self.failUnless(d2["result"] == self.expected_result) @@ -62,6 +70,7 @@ class RunModuleCodeTest(unittest.TestCase): self.failUnless(d2["__file__"] is file) self.failUnless(d2["run_argv0"] is file) self.failUnless(d2["__loader__"] is loader) + self.failUnless(d2["__package__"] is package) self.failUnless(sys.argv[0] is saved_argv0) self.failUnless(name not in sys.modules) @@ -164,7 +173,7 @@ class RunModuleTest(unittest.TestCase): self._del_pkg(pkg_dir, depth, mod_name) if verbose: print "Module executed successfully" - def _add_relative_modules(self, base_dir, depth): + def _add_relative_modules(self, base_dir, source, depth): if depth <= 1: raise ValueError("Relative module test needs depth > 1") pkg_name = "__runpy_pkg__" @@ -190,7 +199,7 @@ class RunModuleTest(unittest.TestCase): if verbose: print " Added nephew module:", nephew_fname def _check_relative_imports(self, depth, run_name=None): - contents = """\ + contents = r"""\ from __future__ import absolute_import from . import sibling from ..uncle.cousin import nephew @@ -198,16 +207,21 @@ from ..uncle.cousin import nephew pkg_dir, mod_fname, mod_name = ( self._make_pkg(contents, depth)) try: - self._add_relative_modules(pkg_dir, depth) + self._add_relative_modules(pkg_dir, contents, depth) + pkg_name = mod_name.rpartition('.')[0] if verbose: print "Running from source:", mod_name - d1 = run_module(mod_name) # Read from source + d1 = run_module(mod_name, run_name=run_name) # Read from source + self.failUnless("__package__" in d1) + self.failUnless(d1["__package__"] == pkg_name) self.failUnless("sibling" in d1) self.failUnless("nephew" in d1) del d1 # Ensure __loader__ entry doesn't keep file open __import__(mod_name) os.remove(mod_fname) if verbose: print "Running from compiled:", mod_name - d2 = run_module(mod_name) # Read from bytecode + d2 = run_module(mod_name, run_name=run_name) # Read from bytecode + self.failUnless("__package__" in d2) + self.failUnless(d2["__package__"] == pkg_name) self.failUnless("sibling" in d2) self.failUnless("nephew" in d2) del d2 # Ensure __loader__ entry doesn't keep file open @@ -225,6 +239,11 @@ from ..uncle.cousin import nephew if verbose: print "Testing relative imports at depth:", depth self._check_relative_imports(depth) + def test_main_relative_import(self): + for depth in range(2, 5): + if verbose: print "Testing main relative imports at depth:", depth + self._check_relative_imports(depth, "__main__") + def test_main(): run_unittest(RunModuleCodeTest) diff --git a/Misc/NEWS b/Misc/NEWS index c32e3f93e2d..d61851ce2e2 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -12,6 +12,10 @@ What's New in Python 2.6 alpha 1? Core and builtins ----------------- +- PEP 366: Allow explicit relative imports when executing modules + inside packages with the -m switch via a new module level + __package__ attribute. + - Issue #1534: Added ``PyFloat_GetMax()``, ``PyFloat_GetMin()`` and ``PyFloat_GetInfo()`` to the float API. diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 95857957ce0..5e3c293f7ca 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -30,6 +30,8 @@ PyModule_New(const char *name) goto fail; if (PyDict_SetItemString(m->md_dict, "__doc__", Py_None) != 0) goto fail; + if (PyDict_SetItemString(m->md_dict, "__package__", Py_None) != 0) + goto fail; Py_DECREF(nameobj); PyObject_GC_Track(m); return (PyObject *)m; diff --git a/Python/import.c b/Python/import.c index 59a51bc2338..bf2799d5540 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2106,7 +2106,8 @@ get_parent(PyObject *globals, char *buf, Py_ssize_t *p_buflen, int level) { static PyObject *namestr = NULL; static PyObject *pathstr = NULL; - PyObject *modname, *modpath, *modules, *parent; + static PyObject *pkgstr = NULL; + PyObject *pkgname, *modname, *modpath, *modules, *parent; if (globals == NULL || !PyDict_Check(globals) || !level) return Py_None; @@ -2121,44 +2122,103 @@ get_parent(PyObject *globals, char *buf, Py_ssize_t *p_buflen, int level) if (pathstr == NULL) return NULL; } + if (pkgstr == NULL) { + pkgstr = PyString_InternFromString("__package__"); + if (pkgstr == NULL) + return NULL; + } *buf = '\0'; *p_buflen = 0; - modname = PyDict_GetItem(globals, namestr); - if (modname == NULL || !PyString_Check(modname)) - return Py_None; + pkgname = PyDict_GetItem(globals, pkgstr); - modpath = PyDict_GetItem(globals, pathstr); - if (modpath != NULL) { - Py_ssize_t len = PyString_GET_SIZE(modname); + if ((pkgname != NULL) && (pkgname != Py_None)) { + /* __package__ is set, so use it */ + Py_ssize_t len; + if (!PyString_Check(pkgname)) { + PyErr_SetString(PyExc_ValueError, + "__package__ set to non-string"); + return NULL; + } + len = PyString_GET_SIZE(pkgname); + if (len == 0) { + if (level > 0) { + PyErr_SetString(PyExc_ValueError, + "Attempted relative import in non-package"); + return NULL; + } + return Py_None; + } if (len > MAXPATHLEN) { PyErr_SetString(PyExc_ValueError, - "Module name too long"); + "Package name too long"); return NULL; } - strcpy(buf, PyString_AS_STRING(modname)); - } - else { - char *start = PyString_AS_STRING(modname); - char *lastdot = strrchr(start, '.'); - size_t len; - if (lastdot == NULL && level > 0) { - PyErr_SetString(PyExc_ValueError, - "Attempted relative import in non-package"); - return NULL; - } - if (lastdot == NULL) + strcpy(buf, PyString_AS_STRING(pkgname)); + } else { + /* __package__ not set, so figure it out and set it */ + modname = PyDict_GetItem(globals, namestr); + if (modname == NULL || !PyString_Check(modname)) return Py_None; - len = lastdot - start; - if (len >= MAXPATHLEN) { - PyErr_SetString(PyExc_ValueError, - "Module name too long"); - return NULL; + + modpath = PyDict_GetItem(globals, pathstr); + if (modpath != NULL) { + /* __path__ is set, so modname is already the package name */ + Py_ssize_t len = PyString_GET_SIZE(modname); + int error; + if (len > MAXPATHLEN) { + PyErr_SetString(PyExc_ValueError, + "Module name too long"); + return NULL; + } + strcpy(buf, PyString_AS_STRING(modname)); + error = PyDict_SetItem(globals, pkgstr, modname); + if (error) { + PyErr_SetString(PyExc_ValueError, + "Could not set __package__"); + return NULL; + } + } else { + /* Normal module, so work out the package name if any */ + char *start = PyString_AS_STRING(modname); + char *lastdot = strrchr(start, '.'); + size_t len; + int error; + if (lastdot == NULL && level > 0) { + PyErr_SetString(PyExc_ValueError, + "Attempted relative import in non-package"); + return NULL; + } + if (lastdot == NULL) { + error = PyDict_SetItem(globals, pkgstr, Py_None); + if (error) { + PyErr_SetString(PyExc_ValueError, + "Could not set __package__"); + return NULL; + } + return Py_None; + } + len = lastdot - start; + if (len >= MAXPATHLEN) { + PyErr_SetString(PyExc_ValueError, + "Module name too long"); + return NULL; + } + strncpy(buf, start, len); + buf[len] = '\0'; + pkgname = PyString_FromString(buf); + if (pkgname == NULL) { + return NULL; + } + error = PyDict_SetItem(globals, pkgstr, pkgname); + Py_DECREF(pkgname); + if (error) { + PyErr_SetString(PyExc_ValueError, + "Could not set __package__"); + return NULL; + } } - strncpy(buf, start, len); - buf[len] = '\0'; } - while (--level > 0) { char *dot = strrchr(buf, '.'); if (dot == NULL) {