[Jython-checkins] jython: Attribute access is import *only* for Java packages and classes (fixes #2654).

jeff.allen jython-checkins at python.org
Thu Mar 7 18:10:45 EST 2019


https://hg.python.org/jython/rev/22c19e5a77ad
changeset:   8221:22c19e5a77ad
user:        Jeff Allen <ja.py at farowl.co.uk>
date:        Thu Mar 07 09:02:07 2019 +0000
summary:
  Attribute access is import *only* for Java packages and classes (fixes #2654).

Refactors PyModule.impAttr and PyModule.__findattr_ex__ so that the latter
only looks for Java packages and classes, rather than invoking a full impAttr,
which would import Python modules.

An addition to test_import_jy tests exercises a range of import patterns,
each time in a clean subprocess, covering combinations of Python and Java
in pure and mixed hierarchy.

files:
  Lib/test/test_import_jy.py        |  224 +++++++++++++++++-
  Lib/test/test_import_jy.zip       |  Bin 
  src/org/python/core/PyModule.java |  132 +++++++---
  src/org/python/core/PyObject.java |   10 +-
  src/org/python/core/imp.java      |   31 +-
  5 files changed, 333 insertions(+), 64 deletions(-)


diff --git a/Lib/test/test_import_jy.py b/Lib/test/test_import_jy.py
--- a/Lib/test/test_import_jy.py
+++ b/Lib/test/test_import_jy.py
@@ -11,8 +11,10 @@
 import tempfile
 import unittest
 import subprocess
+import zipfile
 from test import test_support
 from test_chdir import read, safe_mktemp, COMPILED_SUFFIX
+from doctest import script_from_examples
 
 class MislabeledImportTestCase(unittest.TestCase):
 
@@ -233,11 +235,225 @@
         self.assertEqual(cm.exception.reason, "ordinal not in range(128)")
 
 
+class MixedImportTestCase(unittest.TestCase):
+    #
+    # This test case depends on material in a file structure unpacked
+    # from an associated ZIP archive. The test depends on Python source
+    # and Java class files. The archive also contains the Java source
+    # from which the class files may be regenerated if necessary.
+    #
+    # To regenerate the class files, explode the archive in a
+    # convenient spot on the file system and compile them with javac at
+    # the lowest supported code standard (currently Java 7), e.g. (posh)
+    #   PS jython-trunk> cd mylib
+    #   PS mylib> javac  $(get-childitem -Recurse -Name -Include "*.java")
+    # or the equivalent Unix command using find.
+
+    ZIP = test_support.findfile("test_import_jy.zip")
+
+    @classmethod
+    def setUpClass(cls):
+        td = tempfile.mkdtemp()
+        cls.source = os.path.join(td, "test.py")
+        cls.setpath = "import sys; sys.modules[0] = r'" + td + "'"
+        zip = zipfile.ZipFile(cls.ZIP, 'r')
+        zip.extractall(td)
+        cls.tmpdir = td
+
+    @classmethod
+    def tearDownClass(cls):
+        td = cls.tmpdir
+        if td and os.path.isdir(td):
+            test_support.rmtree(td)
+
+    def make_prog(self, *script):
+        "Write a program to test.py"
+        with open(self.source, "wt") as f:
+            print >> f, MixedImportTestCase.setpath
+            for line in script:
+                print >> f, line
+            print >> f, "raise SystemExit"
+
+    def run_prog(self):
+        # Feed lines to interpreter and capture output
+        process = subprocess.Popen([sys.executable, "-S", MixedImportTestCase.source],
+                                   stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        output, err = process.communicate()
+        retcode = process.poll()
+        if retcode:
+            raise subprocess.CalledProcessError(retcode, sys.executable, output=err)
+        return output
+
+    def module_regex(self, module):
+        "Partial filename from module"
+        sep = "\\" + os.path.sep
+        return sep + module.replace('.', sep)
+
+    def check_package(self, line, module):
+        target = "Executed: .*" +  self.module_regex(module) + "\\" + os.path.sep \
+                    + r"__init__(\.py|\$py\.class|\.pyc)"
+        self.assertRegexpMatches(line, target)
+
+    def check_module(self, line, module):
+        "Check output from loading a module"
+        target = "Executed: .*" + self.module_regex(module) + r"(\.py|\$py\.class|\.pyc)"
+        self.assertRegexpMatches(line, target)
+
+    def test_import_to_program(self):
+        # A Python module in a Python program
+        self.make_prog("import a.b.c.m", "print repr(a.b.c.m)")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "a")
+        self.check_package(out[1], "a.b")
+        self.check_package(out[2], "a.b.c")
+        self.check_module(out[3], "a.b.c.m")
+        self.assertRegexpMatches(out[4], r"\<module 'a\.b\.c\.m' from .*\>")
+
+    def test_import_to_program_no_magic(self):
+        # A Python module in a Python program (issue 2654)
+        self.make_prog("import a.b, a.b.c", "print repr(a.b.m3)")
+        try:
+            out = self.run_prog()
+            self.fail("reference to a.b.m3 did not raise exception")
+        except subprocess.CalledProcessError as e:
+            self.assertRegexpMatches(e.output, r"AttributeError: .* has no attribute 'm3'")
+
+    def test_import_relative_implicit(self):
+        # A Python module by implicit relative import (no dots)
+        self.make_prog("import a.b.m3")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "a")
+        self.check_package(out[1], "a.b")
+        self.check_module(out[2], "a.b.m3")
+        self.check_package(out[3], "a.b.c")
+        self.check_module(out[4], "a.b.c.m")
+        self.assertRegexpMatches(out[5], r"\<module 'a\.b\.c\.m' from .*\>")
+
+    def test_import_absolute_implicit(self):
+        # A built-in module by absolute import (but relative must be tried first)
+        self.make_prog("import a.b.m4")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "a")
+        self.check_package(out[1], "a.b")
+        self.check_module(out[2], "a.b.m4")
+        self.assertRegexpMatches(out[3], r"\<module 'sys' \(built-in\)\>")
+
+    def test_import_from_module(self):
+        # A Python module by from-import
+        self.make_prog("import a.b.m5")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "a")
+        self.check_package(out[1], "a.b")
+        self.check_module(out[2], "a.b.m5")
+        self.check_package(out[3], "a.b.c")
+        self.check_module(out[4], "a.b.c.m")
+        self.assertRegexpMatches(out[5], r"\<module 'a\.b\.c\.m' from .*\>")
+        self.assertRegexpMatches(out[6], r"1 2")
+
+    def test_import_from_relative_module(self):
+        # A Python module by relative from-import
+        self.make_prog("import a.b.m6")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "a")
+        self.check_package(out[1], "a.b")
+        self.check_module(out[2], "a.b.m6")
+        self.check_package(out[3], "a.b.c")
+        self.check_module(out[4], "a.b.c.m")
+        self.assertRegexpMatches(out[5], r"\<module 'a\.b\.c\.m' from .*\>")
+        self.assertRegexpMatches(out[6], r"1 2")
+
+    def check_java_package(self, line, module):
+        target = r"\<java package " + module + r" 0x[0-9a-f]+\>"
+        self.assertRegexpMatches(line, target)
+
+    def test_import_java_java(self):
+        # A Java class in a Java package by from-import
+        self.make_prog("from jpkg.j import K", "print repr(K)")
+        out = self.run_prog().splitlines()
+        self.assertRegexpMatches(out[0], r"\<type 'jpkg.j.K'\>")
+
+    def test_import_java_java_magic(self):
+        # A Java class in a Java package
+        # with implicit sub-module and class import
+        self.make_prog(
+            "import jpkg",
+            "print repr(jpkg)",
+            "print repr(jpkg.j)",
+            "print repr(jpkg.j.K)",
+            "print repr(jpkg.L)")
+        out = self.run_prog().splitlines()
+        self.check_java_package(out[0], "jpkg")
+        self.check_java_package(out[1], "jpkg.j")
+        self.assertRegexpMatches(out[2], r"\<type 'jpkg.j.K'\>")
+        self.assertRegexpMatches(out[3], r"\<type 'jpkg.L'\>")
+
+    def test_import_java_python(self):
+        # A Java class in a Python package by from-import
+        self.make_prog("from mix.b import K1", "print repr(K1)")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "mix")
+        self.check_package(out[1], "mix.b")
+        self.assertRegexpMatches(out[2], r"\<type 'mix.b.K1'\>")
+
+    def test_import_java_python_magic(self):
+        # A Java class in a Python package
+        # with implicit sub-module and class import
+        self.make_prog(
+            "import mix",
+            "print repr(mix.b)",
+            "print repr(mix.b.K1)",
+            "import mix.b",
+            "print repr(mix.b)",
+            "print repr(mix.b.K1)",
+            "print repr(mix.J1)")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "mix")
+        self.check_java_package(out[1], "mix.b")
+        self.assertRegexpMatches(out[2], r"\<type 'mix.b.K1'\>")
+        self.check_package(out[3], "mix.b")
+        self.assertRegexpMatches(out[4], r"\<module 'mix\.b' from .*\>")
+        self.assertRegexpMatches(out[5], r"\<type 'mix.b.K1'\>")
+        self.assertRegexpMatches(out[6], r"\<type 'mix.J1'\>")
+
+    def test_import_javapkg_python(self):
+        # A Java package in a Python package
+        self.make_prog("import mix.j", "print repr(mix.j)", "print repr(mix.j.K2)")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "mix")
+        self.check_java_package(out[1], "mix.j")
+        self.assertRegexpMatches(out[2], r"\<type 'mix.j.K2'\>")
+
+    def test_import_java_from_javapkg(self):
+        # A Java class in a Java package in a Python package
+        self.make_prog("from mix.j import K2", "print repr(K2)")
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "mix")
+        self.assertRegexpMatches(out[1], r"\<type 'mix.j.K2'\>")
+
+    def test_import_javapkg_magic(self):
+        # A Java class in a Java package in a Python package
+        # with implicit sub-module and class import
+        self.make_prog(
+            "import mix",
+            "print repr(mix.J1)",
+            "print repr(mix.j)",
+            "print repr(mix.j.K2)",
+            )
+        out = self.run_prog().splitlines()
+        self.check_package(out[0], "mix")
+        self.assertRegexpMatches(out[1], r"\<type 'mix.J1'\>")
+        self.check_java_package(out[2], "mix.j")
+        self.assertRegexpMatches(out[3], r"\<type 'mix.j.K2'\>")
+
+
 def test_main():
-    test_support.run_unittest(MislabeledImportTestCase,
-                              OverrideBuiltinsImportTestCase,
-                              ImpTestCase,
-                              UnicodeNamesTestCase)
+    test_support.run_unittest(
+            MislabeledImportTestCase,
+            OverrideBuiltinsImportTestCase,
+            ImpTestCase,
+            UnicodeNamesTestCase,
+            MixedImportTestCase
+    )
 
 if __name__ == '__main__':
     test_main()
diff --git a/Lib/test/test_import_jy.zip b/Lib/test/test_import_jy.zip
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b76f432078589f7dfcdd14269b75179f32a8af4e
GIT binary patch
[stripped]
diff --git a/src/org/python/core/PyModule.java b/src/org/python/core/PyModule.java
--- a/src/org/python/core/PyModule.java
+++ b/src/org/python/core/PyModule.java
@@ -91,54 +91,100 @@
     /**
      * {@inheritDoc}
      * <p>
-     * Overridden in {@code PyModule} to search for the named attribute as a
-     * module in {@code sys.modules} (using the key {@code ".".join(self.__name__, name)}) and on the
-     * {@code self.__path__}.
+     * Overridden in {@code PyModule} to search for a sub-module of this module (using the key
+     * {@code ".".join(self.__name__, name)}) in {@code sys.modules}, on the {@code self.__path__},
+     * and as a Java package with the same name. The named sub-module becomes an attribute of this
+     * module (in {@code __dict__}).
      */
     @Override
     protected PyObject impAttr(String name) {
-        // Get hold of the module dictionary and __name__, and __path__ if it has one.
-        if (__dict__ == null || name.length() == 0) {
-            return null;
-        }
-        PyObject path = __dict__.__finditem__("__path__");
-        if (path == null) {
-            path = new PyList();
+
+        // Some of our look-up needs the full name, deduced from __name__ and name.
+        String fullName = getFullName(name);
+
+        if (fullName != null) {
+            // Maybe the attribute is a Python sub-module
+            PyObject attr = findSubModule(name, fullName);
+
+            // Or is a Java package
+            if (attr == null) {
+                // Still looking: maybe it's a Java package?
+                attr = PySystemState.packageManager.lookupName(fullName);
+            }
+
+            // Add as an attribute the thing we found (if not still null)
+            return addedSubModule(name, fullName, attr);
         }
-        PyObject pyName = __dict__.__finditem__("__name__");
-        if (pyName == null) {
-            return null;
-        }
+        return null;
+    }
 
-        // Maybe the module we're looking for is in sys.modules
-        String fullName = (pyName.__str__().toString() + '.' + name).intern();
-        PyObject modules = Py.getSystemState().modules;
-        PyObject attr = modules.__finditem__(fullName);
-
-        // If not, look along the module's __path__
-        if (path instanceof PyList) {
+    /**
+     * Find Python sub-module within this object, within {@code sys.modules} or along this module's
+     * {@code __path__}.
+     *
+     * @param name simple name of sub package
+     * @param fullName of sub package
+     * @return module found or {@code null}
+     */
+    private PyObject findSubModule(String name, String fullName) {
+        PyObject attr =  null;
+        if (fullName != null) {
+            // The module may already have been loaded in sys.modules
+            attr = Py.getSystemState().modules.__finditem__(fullName);
+            // Or it may be found as a Python module along this module's __path__
             if (attr == null) {
-                attr = imp.find_module(name, fullName, (PyList)path);
+                PyObject path = __dict__.__finditem__("__path__");
+                if (path == null) {
+                    attr = imp.find_module(name, fullName, new PyList());
+                } else if (path instanceof PyList) {
+                    attr = imp.find_module(name, fullName, (PyList) path);
+                } else if (path != Py.None) {
+                    throw Py.TypeError("__path__ must be list or None");
+                }
             }
-        } else if (path != Py.None) {
-            throw Py.TypeError("__path__ must be list or None");
         }
-
-        if (attr == null) {
-            // Still looking: maybe it's a Java package?
-            attr = PySystemState.packageManager.lookupName(fullName);
-        }
+        return attr;
+    }
 
+    /**
+     * Add the given attribute to {@code __dict__}, if it is not {@code null} allowing
+     * {@code sys.modules[fullName]} to override.
+     *
+     * @param name of attribute to add
+     * @param fullName by which to check in {@code sys.modules}
+     * @param attr attribute to add (if not overridden)
+     * @return attribute value actually added (may be from {@code sys.modules}) or {@code null}
+     */
+    private PyObject addedSubModule(String name, String fullName, PyObject attr) {
         if (attr != null) {
-            // Allow a package component to change its own meaning
-            PyObject found = modules.__finditem__(fullName);
-            if (found != null) {
-                attr = found;
+            if (fullName != null) {
+                // If a module by the full name exists in sys.modules, that takes precedence.
+                PyObject entry = Py.getSystemState().modules.__finditem__(fullName);
+                if (entry != null) {
+                    attr = entry;
+                }
             }
+            // Enter this as an attribute of this module.
             __dict__.__setitem__(name, attr);
-            return attr;
         }
+        return attr;
+    }
 
+    /**
+     * Construct (and intern) the full name of a possible sub-module of this one, using the
+     * {@code __name__} attribute and a simple sub-module name. Return {@code null} if any of these
+     * requirements is missing.
+     *
+     * @param name simple name of (possible) sub-module
+     * @return interned full name or {@code null}
+     */
+    private String getFullName(String name) {
+        if (__dict__ != null) {
+            PyObject pyName = __dict__.__finditem__("__name__");
+            if (pyName != null && name != null && name.length() > 0) {
+                return (pyName.__str__().toString() + '.' + name).intern();
+            }
+        }
         return null;
     }
 
@@ -146,16 +192,24 @@
      * {@inheritDoc}
      * <p>
      * Overridden in {@code PyModule} so that if the base-class {@code __findattr_ex__} is
-     * unsuccessful, it will to search for the named attribute as a module via
-     * {@link #impAttr(String)}.
+     * unsuccessful, it will to search for the named attribute as a Java sub-package. This is
+     * responsible for the automagical import of Java (but not Python) packages when referred to as
+     * attributes.
      */
     @Override
     public PyObject __findattr_ex__(String name) {
+        // Find the attribute in the dictionary
         PyObject attr = super.__findattr_ex__(name);
-        if (attr != null) {
-            return attr;
+        if (attr == null) {
+            // The attribute may be a Java sub-package to auto-import.
+            String fullName = getFullName(name);
+            if (fullName != null) {
+                attr = PySystemState.packageManager.lookupName(fullName);
+                // Any entry in sys.modules to takes precedence.
+                attr = addedSubModule(name, fullName, attr);
+            }
         }
-        return impAttr(name);
+        return attr;
     }
 
     @Override
diff --git a/src/org/python/core/PyObject.java b/src/org/python/core/PyObject.java
--- a/src/org/python/core/PyObject.java
+++ b/src/org/python/core/PyObject.java
@@ -1028,13 +1028,11 @@
     }
 
     /**
-     * This is a variant of {@link #__findattr__(String)} used by the module import logic to find a
-     * sub-module amongst the attributes of an object representing a package. The default behaviour
-     * is to delegate to {@code __findattr__}, but in particular cases it becomes a hook for
-     * specialised search behaviour.
+     * This is a hook called during the import mechanism when the target module is (or may be)
+     * a sub-module of this object.
      *
-     * @param name the name to lookup in this namespace <b>must be an interned string</b>.
-     * @return the value corresponding to name or null if name is not found
+     * @param name relative to this object <b>must be an interned string</b>.
+     * @return corresponding value (a module or package) or {@code null} if not found
      */
     protected PyObject impAttr(String name) {
         return __findattr__(name);
diff --git a/src/org/python/core/imp.java b/src/org/python/core/imp.java
--- a/src/org/python/core/imp.java
+++ b/src/org/python/core/imp.java
@@ -669,12 +669,12 @@
     }
 
     /**
-     * Try to load a module from {@code sys.meta_path}, as a built-in module, or from either the the
-     * {@code __path__} of the enclosing package or {@code sys.path} if the module is being sought
-     * at the top level.
+     * Try to load a Python module from {@code sys.meta_path}, as a built-in module, or from either
+     * the {@code __path__} of the enclosing package or {@code sys.path} if the module is being
+     * sought at the top level.
      *
      * @param name simple name of the module.
-     * @param moduleName fully-qualified (dotted) name of the module (ending in {@code name}.
+     * @param moduleName fully-qualified (dotted) name of the module (ending in {@code name}).
      * @param path {@code __path__} of the enclosing package (or {@code null} if top level).
      * @return the module if we can load it (or {@code null} if we can't).
      */
@@ -966,12 +966,12 @@
             return null;
         }
 
-        PyObject tmp = dict.__finditem__("__package__"); // XXX: why is this not guaranteed set?
+        PyObject tmp = dict.__finditem__("__package__");
         if (tmp != null && tmp != Py.None) {
             if (!Py.isInstance(tmp, PyString.TYPE)) {
                 throw Py.ValueError("__package__ set to non-string");
             }
-            modname = ((PyString)tmp).getString();
+            modname = ((PyString) tmp).getString();
         } else {
             // __package__ not set, so figure it out and set it.
 
@@ -1015,12 +1015,14 @@
         if (Py.getSystemState().modules.__finditem__(modname) == null) {
             if (orig_level < 1) {
                 if (modname.length() > 0) {
-                    Py.warning(Py.RuntimeWarning, String.format("Parent module '%.200s' not found "
-                            + "while handling absolute import", modname));
+                    Py.warning(Py.RuntimeWarning, String.format(
+                            "Parent module '%.200s' not found " + "while handling absolute import",
+                            modname));
                 }
             } else {
-                throw Py.SystemError(String.format("Parent module '%.200s' not loaded, "
-                        + "cannot perform relative import", modname));
+                throw Py.SystemError(String.format(
+                        "Parent module '%.200s' not loaded, " + "cannot perform relative import",
+                        modname));
             }
         }
         return modname.intern();
@@ -1221,7 +1223,7 @@
      * @param top if true, return the top module in the name, otherwise the last
      * @param modDict the __dict__ of the importing module (used to navigate a relative import)
      * @param fromlist list of names being imported
-     * @param level 0=absolute, n>0=relative levels to go up, -1=try relative then absolute.
+     * @param level 0=absolute, n>0=relative levels to go up - 1, -1=try relative then absolute.
      * @return an imported module (Java or Python)
      */
     private static PyObject import_module_level(String name, boolean top, PyObject modDict,
@@ -1266,7 +1268,7 @@
         PyObject topMod = import_next(pkgMod, parentName, firstName, name, fromlist);
 
         if (topMod == Py.None || topMod == null) {
-            // The first attempt failed. This time
+            // The first attempt failed.
             parentName = new StringBuilder("");
             // could throw ImportError
             if (level > 0) {
@@ -1328,14 +1330,13 @@
 
     /**
      * Ensure that the items mentioned in the from-list of an import are actually present, even if
-     * they are modules we have not imported yet.     *
+     * they are modules we have not imported yet.
      *
      * @param mod module we are importing from
      * @param fromlist tuple of names to import
      * @param name of module we are importing from (as given, may be relative)
-     * @param recursive if true, when the from-list includes "*", do not import __all__
+     * @param recursive true, when this method calls itself
      */
-    // XXX: effect of the recursive argument is hard to fathom.
     private static void ensureFromList(PyObject mod, PyObject fromlist, String name,
             boolean recursive) {
         // This can happen with imports like "from . import foo"

-- 
Repository URL: https://hg.python.org/jython


More information about the Jython-checkins mailing list