[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