{path: tocEntry}
*/
+ @ExposedGet(name = "_files")
+ public final PyObject files;
- public PyZipImporter(PyType type) {
- super(TYPE);
+ /**
+ * Construct a PyZipImporter
for the given path, which may include sub-directories
+ * within the ZIP file, for example path/to/archive.zip/a/sub/directory
. Because of
+ * the equivalence between ZIP files and sub-directories in Python import (see
+ * PEP 273), a
+ * PyZipImporter
operates using the platform-specific file separator ("\" on
+ * Windows) at all public interfaces. On Windows, a path like
+ * path\to\archive.zip\a\sub\directory
will normally be supplied. However, we
+ * follow CPython in tolerating either "/" or the platform-specific file separator in the
+ * archivePath
, or indeed any mixture of the two.
+ *
+ * @param archivePath path to archive and optionally a sub-directory within it
+ */
+ public PyZipImporter(String archivePath) {
+ this(TYPE, archivePath);
}
- public PyZipImporter(String archivePath, String prefix, PyObject files) {
- this(TYPE);
- this.archive = archivePath;
- this.prefix = prefix;
- this.files = files;
- }
+ /**
+ * Equivalent to {@link PyZipImporter#PyZipImporter(String)} when sub-class required.
+ *
+ * @param type of sub-class
+ * @param archivePath path to archive and optionally a sub-directory within it
+ */
+ public PyZipImporter(PyType type, String archivePath) {
+ super(type);
- @ExposedNew
- final static PyObject zipimporter_new(PyNewWrapper new_, boolean init, PyType subtype,
- PyObject[] args, String[] keywords) {
- ArgParser ap = new ArgParser("zipimporter", args, keywords, "archivepath");
- String archivePath = ap.getString(0);
- Path archive = Paths.get(archivePath);
- while (archive != null) {
+ if (archivePath == null || archivePath.length() == 0) {
+ throw ZipImportModule.ZipImportError("archive path is empty");
+ }
+ archivePath = toPlatformSeparator(archivePath);
+
+ /*
+ * archivepath may be e.g. "path/to/archive.zip/a/sub/directory", meaning we must look for
+ * modules in the lib directory inside the ZIP file path/to/archive.zip (provided that it
+ * exists). We must separate the archive (ZIP file) proper from the starting path within it,
+ * which is known as the "prefix".
+ */
+ Path fullPath = Paths.get(archivePath), archive = fullPath;
+ int prefixEnd = archive.getNameCount();
+ int prefixStart = prefixEnd;
+
+ // Strip elements from end of path until empty, a file or a directory
+ for (archive = fullPath; prefixStart > 1; archive = archive.getParent(), prefixStart--) {
if (Files.isRegularFile(archive)) {
break;
+ } else if (Files.isDirectory(archive)) {
+ // Stripping names got us to a directory: no ZIP file here
+ archive = null;
+ break;
}
- archive = archive.getParent();
}
- if (archive == null) {
- throw Py.ImportError(String.format("cannot handle %s", archivePath));
+
+ if (archive == null || archive.getNameCount() == 0) {
+ throw ZipImportModule.ZipImportError(String.format("not a Zip file: %s", archivePath));
}
- String filename = archive.toAbsolutePath().toString();
- PyObject files = ZipImportModule._zip_directory_cache.__finditem__(filename);
+
+ // Look up, or add if necessary, an entry in the cache for the files.
+ this.archive = archive.toString();
+ PyObject files = ZipImportModule._zip_directory_cache.__finditem__(this.archive);
if (files == null) {
- files = readDirectory(filename);
- ZipImportModule._zip_directory_cache.__setitem__(filename, files);
+ /*
+ * This is new. Make a cache entry that enumerates the files in the ZIP. This is also
+ * where we throw if we can't read it as a ZIP.
+ */
+ files = readDirectory(archive);
+ ZipImportModule._zip_directory_cache.__setitem__(this.archive, files);
}
- String prefix = "";
- if (!filename.equals(archivePath)) {
- prefix = archivePath.substring(filename.length() + 1);
- if (!prefix.endsWith(File.separator)) {
- prefix += File.separator;
- }
+ this.files = files;
+
+ // The prefix is that part of the original archivePath that is not in archive.
+ if (prefixStart < prefixEnd) {
+ // There was a prefix
+ Path prefix = fullPath.subpath(prefixStart, prefixEnd);
+ this.prefix = prefix.toString() + File.separator;
+ } else {
+ this.prefix = "";
}
- return new PyZipImporter(filename, prefix, files);
}
- @ExposedMethod
+ /**
+ * __new__ method equivalent to {@link PyZipImporter#PyZipImporter(String)}.
+ */
+ @ExposedNew
+ final static PyObject zipimporter_new(PyNewWrapper new_, boolean init, PyType subtype,
+ PyObject[] args, String[] keywords) {
+ ArgParser ap = new ArgParser("zipimporter", args, keywords, "archivepath");
+ String archivePath = ap.getString(0);
+ // XXX Should FS-decode args[0] here. CPython uses PyUnicode_FSDecoder, during __init__
+ return new PyZipImporter(archivePath);
+ }
+
+ @Override
+ public String toString() {
+ Path archivePath = Paths.get(archive, prefix);
+ return String.format("exec_module
in CPython zipimporter
. */
+ @Deprecated // @ExposedMethod
public final PyObject zipimporter_exec_module(PyObject module) {
PyModule mod = (PyModule) module;
String fullname = mod.__findattr__("__name__").asString();
@@ -98,9 +167,14 @@ public final PyObject zipimporter_exec_module(PyObject module) {
}
/**
- * CPython zipimport module is very outdated, it's not yet compliant with PEP-451, the specs are checking the old behaviour
- * this method and a few others that are deprecated a simply implemented to satisfy the test suite
- * @param fullname
+ * Load the module specified by fullname
, |a fully qualified (dotted) module name.
+ * Return the imported module, or raise ZipImportError
if it wasn’t found.
+ *
+ * CPython zipimport module is very outdated, it's not yet compliant with PEP-451, the specs are
+ * checking the old behaviour this method and a few others that are deprecated a simply
+ * implemented to satisfy the test suite
+ *
+ * @param fullname fully qualified (dotted) module name.
* @return a python module
*/
@Deprecated
@@ -110,12 +184,12 @@ public final PyObject zipimporter_load_module(String fullname) {
PyModule mod = imp.addModule(fullname);
imp.createFromCode(fullname, (PyCode) zipimporter_get_code(fullname));
String folder = archive + File.separator + prefix;
- if (entry._package) {
+ if (entry.isPackage) {
PyList pkgPath = new PyList();
pkgPath.append(new PyUnicode(folder + entry.dir(fullname)));
mod.__setattr__("__path__", pkgPath);
}
- if (entry.binary) {
+ if (entry.isBinary) {
mod.__setattr__("__cached__", new PyUnicode(folder + entry.path(fullname)));
}
mod.__setattr__("__file__", new PyUnicode(folder + entry.sourcePath(fullname)));
@@ -123,19 +197,34 @@ public final PyObject zipimporter_load_module(String fullname) {
});
}
-
+ /**
+ * Return True
if the module specified by fullname
is a package. Raise
+ * ZipImportError
if the module couldn't be found.
+ *
+ * @param fullname fully qualified (dotted) module name.
+ * @return Python True
if a package or False
+ */
@ExposedMethod
public final PyObject zipimporter_is_package(String fullname) {
- return getEntry(fullname, (entry, input) -> Py.newBoolean(entry._package));
+ return getEntry(fullname, (entry, input) -> Py.newBoolean(entry.isPackage));
}
+ /**
+ * Return the source code for the specified module. Raise ZipImportError
if the
+ * module couldn't be found, and return None if the archive does contain the module, but has no
+ * source for it.
+ *
+ * @param fullname fully qualified (dotted) module name.
+ * @return the source as a str
+ */
@ExposedMethod
public final PyObject zipimporter_get_source(String fullname) {
return getEntry(fullname, (entry, inputStream) -> {
try {
- if (entry.binary) {
+ if (entry.isBinary) {
return Py.None;
}
+ // FIXME: encoding? Is there a utility the compiler uses?
return new PyUnicode(FileUtil.readBytes(inputStream));
} catch (IOException e) {
throw ZipImportModule.ZipImportError(e.getMessage());
@@ -143,71 +232,114 @@ public final PyObject zipimporter_get_source(String fullname) {
});
}
+ /**
+ * Given a path to a file within this archive, retrieve the contents of that file as bytes. By
+ * "a path to a file within this archive" we mean either that the path begins with the archive
+ * name (and the rest of it identifies a file within the archive), or that it identifies a file
+ * within the archive (i.e. is relative to this archive). Note that even when the zipimporter
+ * was constructed with a sub-directory as target, a path not beginning with the archive path is
+ * interpreted relative to the base archive, not to the sub-directory:
+ *
+ *
+ * >>> zf = zi(archive+"/foo") + * >>> zf.get_data(archive + "/foo/one.py") + * b"attr = 'portion1 foo one'\n" + * >>> zf.get_data("foo/one.py") + * b"attr = 'portion1 foo one'\n" + * >>> zf.get_data("one.py") + * Traceback (most recent call last): + * File "+ * + * Note also that even where the platform file path separator differs from '/' (i.e. on + * Windows), either that or '/' is acceptable in this context. + * + * @param path to the file within the archive + * @return the contents + */ @ExposedMethod - public final PyObject zipimporter_get_data(String filename) { - ZipFile zipFile = null; - if (filename.startsWith(archive)) { - filename = filename.substring(archive.length() + 1); + public final PyObject zipimporter_get_data(String path) { + // XXX Possibly filename should be an object and if byte-like FS-decoded. + // The path may begin with the archive name as stored, in which case discard it. + if (toPlatformSeparator(path).startsWith(archive)) { + path = path.substring(archive.length() + 1); } - try { - zipFile = new ZipFile(new File(archive)); - ZipEntry zipEntry = zipFile.getEntry(prefix + filename); - if (zipEntry != null) { - return new PyBytes(FileUtil.readBytes(zipFile.getInputStream(zipEntry))); - } - throw ZipImportModule.ZipImportError(filename); - } catch (IOException e) { - throw ZipImportModule.ZipImportError(e.getMessage()); - } finally { - if (zipFile != null) { - try { - zipFile.close(); - } catch (IOException e) { - } + try (ZipFile zipFile = new ZipFile(new File(archive))) { + // path is now definitely relative to the archive but may not use / as separator. + ZipEntry zipEntry = zipFile.getEntry(fromPlatformSeparator(path)); + if (zipEntry == null) { + throw new FileNotFoundException(path); } + return new PyBytes(FileUtil.readBytes(zipFile.getInputStream(zipEntry))); + } catch (IOException ioe) { + throw Py.IOError(ioe); } } -// @ExposedMethod -// public final PyObject zipimporter_get_data(String fullname) { -// throw ZipImportModule.ZipImportError(fullname); -// } - + /** + * Return the value", line 1, in + * OSError: [Errno 0] Error: 'one.py' + *
__file__
would be set to if the specified module were imported.
+ * Raise ZipImportError
if the module couldn't be found.
+ *
+ * @param fullname fully qualified (dotted) module name.
+ * @return file name as Python str
+ */
@ExposedMethod
public final PyObject zipimporter_get_filename(String fullname) {
return getEntry(fullname, (entry, inputStream) -> {
+ // FIXME: this is the name in theory, but what if it isn't present in the ZIP?
return new PyUnicode(archive + File.separator + prefix + entry.sourcePath(fullname));
});
}
+ /**
+ * Return the code
object for the specified module. Raise
+ * ZipImportError
if the module couldn't be found.
+ *
+ * @param fullname fully qualified (dotted) module name.
+ * @return corresponding code object
+ */
@ExposedMethod
public final PyObject zipimporter_get_code(String fullname) {
try {
long mtime = Files.getLastModifiedTime(new File(archive).toPath()).toMillis();
return getEntry(fullname, (entry, inputStream) -> {
byte[] codeBytes;
- if (entry.binary) {
+ if (entry.isBinary) {
try {
codeBytes = imp.readCode(fullname, inputStream, false, mtime);
} catch (IOException ioe) {
- throw Py.ImportError(ioe.getMessage() + "[path=" + entry.path(fullname) + "]");
+ throw Py.ImportError(
+ ioe.getMessage() + "[path=" + entry.path(fullname) + "]");
}
} else {
try {
byte[] bytes = FileUtil.readBytes(inputStream);
- codeBytes = imp.compileSource(fullname, new ByteArrayInputStream(bytes), entry.path(fullname));
+ codeBytes = imp.compileSource(fullname, new ByteArrayInputStream(bytes),
+ entry.path(fullname));
} catch (IOException e) {
throw ZipImportModule.ZipImportError(e.getMessage());
}
}
- return BytecodeLoader.makeCode(fullname + Version.PY_CACHE_TAG, codeBytes, entry.path(fullname));
+ return BytecodeLoader.makeCode(fullname + Version.PY_CACHE_TAG, codeBytes,
+ entry.path(fullname));
});
} catch (IOException e) {
throw ZipImportModule.ZipImportError(e.getMessage());
}
}
- @ExposedMethod
+ /**
+ * Find a spec
for the specified module within the the archive. If a
+ * spec
cannot be found, None
is returned. When passed in,
+ * target
is a module object that the finder may use to make a decision about what
+ * ModuleSpec
to return.
+ *
+ * Disabled: find_spec
is not implemented in zipimport.zipimporter
in
+ * CPython 3.5 as far as we can tell, and by not having it exposed, we should get fall-back
+ * behaviour depending on find_module
.
+ */
+ @Deprecated // @ExposedMethod
final PyObject zipimporter_find_spec(PyObject[] args, String[] keywords) {
ArgParser ap = new ArgParser("find_spec", args, keywords, "fullname", "path", "target");
String fullname = ap.getString(0);
@@ -217,13 +349,13 @@ final PyObject zipimporter_find_spec(PyObject[] args, String[] keywords) {
PyObject spec = moduleSpec.__call__(new PyUnicode(fullname), this);
return getEntry(fullname, (entry, inputStream) -> {
String folder = archive + File.separatorChar + prefix;
- if (entry._package) {
+ if (entry.isPackage) {
PyList pkgpath = new PyList();
pkgpath.add(new PyUnicode(folder + entry.dir(fullname)));
spec.__setattr__("submodule_search_locations", pkgpath);
spec.__setattr__("is_package", Py.True);
}
- if (entry.binary) {
+ if (entry.isBinary) {
spec.__setattr__("cached", new PyUnicode(folder + entry.path(fullname)));
}
spec.__setattr__("origin", new PyUnicode(folder + entry.sourcePath(fullname)));
@@ -232,16 +364,56 @@ final PyObject zipimporter_find_spec(PyObject[] args, String[] keywords) {
});
}
+ /**
+ * Return path with every `\`character replaced by {@link File#pathSeparatorChar}, if that's
+ * different.
+ */
+ private static String toPlatformSeparator(String path) {
+ // Cunningly avoid making a new String if possible.
+ if (File.separatorChar != '/' && path.contains("/")) {
+ return path.replace('/', File.separatorChar);
+ } else {
+ return path;
+ }
+ }
+
+ /**
+ * Return path with every {@link File#pathSeparatorChar} character replaced by `\`, if that's
+ * different. We need this because Python treats paths into zip files as equivalent to paths in
+ * the file system, hence localises the separator to the platform, while zip files themselves
+ * use '/' consistently internally.
+ */
+ private static String fromPlatformSeparator(String path) {
+ // Cunningly avoid making a new String if possible.
+ if (File.separatorChar != '/' && path.contains(File.separator)) {
+ return path.replace(File.separatorChar, '/');
+ } else {
+ return path;
+ }
+ }
+
+ /**
+ * Try to find a ZipEntry in this {@link #archive} corresponding to the given fully-qualified
+ * (dotted) module name, amongst the four search possibilities in order. For the first entry
+ * with a name that fits, open the content as a stream of bytes, and return the result of
+ * applying a supplied function to the ModuleEntry type constant and that stream.
+ *
+ * @param fullname fully qualified (dotted) module name.
+ * @param func to perform on first match
+ * @return return from application of func
+ */
private ModuleEntry(true, true)
, ModuleEntry(true, false)
,
+ * ModuleEntry(false, true)
, ModuleEntry(false, false)
. This is the
+ * order in which we try to identify something to load corresponding to a module name.
+ */
private ModuleEntry[] entries() {
boolean[] options = {true, false};
ModuleEntry[] res = new ModuleEntry[4];
@@ -264,35 +441,27 @@ private ModuleEntry[] entries() {
res[i++] = new ModuleEntry(pack, bin);
}
}
-
return res;
}
- private static PyObject readDirectory(String archive) {
- PySystemState sys = Py.getSystemState();
- File file = new File(sys.getPath(archive));
- if (!file.canRead()) {
- throw ZipImportModule.ZipImportError(String.format("can't open Zip file: '%s'", archive));
- }
+ /**
+ * Create a dictionary of the "files" within the archive at the given Path
.
+ */
+ private static PyObject readDirectory(Path archive) {
- ZipFile zipFile;
- try {
- zipFile = new ZipFile(file);
- } catch (IOException ioe) {
- throw ZipImportModule.ZipImportError(String.format("can't read Zip file: '%s'", archive));
+ if (!Files.isReadable(archive)) {
+ throw ZipImportModule
+ .ZipImportError(String.format("can't open Zip file: '%s'", archive));
}
- PyObject files = new PyDictionary();
- try {
+ try (ZipFile zipFile = new ZipFile(archive.toFile())) {
+ PyObject files = new PyDictionary();
readZipFile(zipFile, files);
- } finally {
- try {
- zipFile.close();
- } catch (IOException ioe) {
- throw Py.IOError(ioe);
- }
+ return files;
+ } catch (IOException ioe) {
+ throw ZipImportModule
+ .ZipImportError(String.format("can't read Zip file: '%s'", archive));
}
- return files;
}
/**
@@ -300,6 +469,7 @@ private static PyObject readDirectory(String archive) {
*
* A tocEntry is a tuple:
*
+ *
* (__file__, # value to use for __file__, available for all files
* compress, # compression kind; 0 for uncompressed
* data_size, # size of compressed data on disk
@@ -309,53 +479,72 @@ private static PyObject readDirectory(String archive) {
* date, # mod data of file (in dos format)
* crc, # crc checksum of the data
* )
+ *
*
- * Directories can be recognized by the trailing SEP in the name, data_size and
- * file_offset are 0.
+ * Directories can be recognized by the trailing os.sep
in the name,
+ * data_size
and file_offset
are 0.
*
* @param zipFile ZipFile to read
* @param files a dict-like PyObject
*/
private static void readZipFile(ZipFile zipFile, PyObject files) {
- for (Enumeration extends ZipEntry> zipEntries = zipFile.entries();
- zipEntries.hasMoreElements();) {
+ // Iterate over the entries and build an informational tuple for each
+ final String zipNameAndSep = zipFile.getName() + File.separator;
+ for (Enumeration extends ZipEntry> zipEntries = zipFile.entries(); zipEntries
+ .hasMoreElements();) {
+ // Oh for Java 9 and Enumeration.asIterator()
ZipEntry zipEntry = zipEntries.nextElement();
- String name = zipEntry.getName().replace('/', File.separatorChar);
+ String name = toPlatformSeparator(zipEntry.getName());
+ PyObject file = new PyUnicode(zipNameAndSep + name);
- PyObject file = new PyUnicode(zipFile.getName() + File.separator + name);
PyObject compress = new PyLong(zipEntry.getMethod());
PyObject data_size = new PyLong(zipEntry.getCompressedSize());
PyObject file_size = new PyLong(zipEntry.getSize());
- // file_offset is a CPython optimization; it's used to seek directly to the
- // file when reading it later. Jython doesn't do this nor is the offset
- // available
+ /*
+ * file_offset is a CPython optimization; it's used to seek directly to the file when
+ * reading it later. Jython doesn't do this nor is the offset available
+ */
PyObject file_offset = new PyLong(-1);
PyObject time = new PyLong(zipEntry.getTime());
PyObject date = new PyLong(zipEntry.getTime());
PyObject crc = new PyLong(zipEntry.getCrc());
- PyTuple entry = new PyTuple(file, compress, data_size, file_size, file_offset,
- time, date, crc);
+ PyTuple entry =
+ new PyTuple(file, compress, data_size, file_size, file_offset, time, date, crc);
+
files.__setitem__(new PyUnicode(name), entry);
}
}
+ /**
+ * A class having 4 possible values representing package-or-not and binary-or-not, which are the
+ * 4 ways to find the form of a module we can load.
+ */
class ModuleEntry {
- private boolean _package;
- private boolean binary;
- ModuleEntry(boolean pack, boolean bin) {
- _package = pack;
- binary = bin;
+ private boolean isPackage;
+ private boolean isBinary;
+
+ ModuleEntry(boolean isPackage, boolean isBinary) {
+ this.isPackage = isPackage;
+ this.isBinary = isBinary;
}
+ /**
+ * Given a simple or fully-qualified (dotted) module name, take the last element as the
+ * name for the corresponding binary or source, and calculate what file we're looking for,
+ * according to the settings of isPackage
or isBinary
.
+ *
+ * @param fullname fully qualified (dotted) module name (e.g. "path.to.mymodule")
+ * @return one of the four strings mymodule(/__init__|).(py|class)
+ */
String path(String name) {
- StringBuilder res = new StringBuilder();
- res.append(name.substring(name.lastIndexOf('.') + 1));
- if (_package) {
- res.append(File.separatorChar + "__init__");
+ // Start with the last element of the module
+ StringBuilder res = new StringBuilder(name.substring(name.lastIndexOf('.') + 1));
+ if (isPackage) {
+ res.append("/__init__");
}
- if (binary) {
+ if (isBinary) {
res.append(".class");
} else {
res.append(".py");
@@ -363,12 +552,25 @@ String path(String name) {
return res.toString();
}
+ /**
+ * Directory of package or "" if not this.isPackage
.
+ *
+ * @param fullname fully qualified (dotted) module name (e.g. "path.to.mymodule")
+ * @return "mymodule"
or ""
+ */
String dir(String name) {
return path(name).replaceFirst("/__init__\\.(py|class)$", "");
}
+ /**
+ * Return the same as {@link #path(String)} but as if isBinary==false
,
+ * so we have the (expected) file name attribute of the module.
+ *
+ * @return "module/__init__.py"
or "module.py"
.
+ */
String sourcePath(String name) {
- return new ModuleEntry(_package, false).path(name);
+ String file = new ModuleEntry(isPackage, false).path(name);
+ return toPlatformSeparator(file);
}
}
}
diff --git a/src/org/python/modules/zipimport/PyZipImporterDerived.java b/src/org/python/modules/zipimport/PyZipImporterDerived.java
index 1ac0b76fb..928283ff7 100644
--- a/src/org/python/modules/zipimport/PyZipImporterDerived.java
+++ b/src/org/python/modules/zipimport/PyZipImporterDerived.java
@@ -73,8 +73,8 @@ public void delDict() {
dict=new PyStringMap();
}
- public PyZipImporterDerived(PyType subtype) {
- super(subtype);
+ public PyZipImporterDerived(PyType subtype,String archivePath) {
+ super(subtype,archivePath);
slots=new PyObject[subtype.getNumSlots()];
dict=subtype.instDict();
if (subtype.needsFinalizer()) {
diff --git a/src/org/python/modules/zipimport/ZipImportModule.java b/src/org/python/modules/zipimport/ZipImportModule.java
index bb01411ea..db9b7218c 100644
--- a/src/org/python/modules/zipimport/ZipImportModule.java
+++ b/src/org/python/modules/zipimport/ZipImportModule.java
@@ -1,20 +1,18 @@
-/* Copyright (c) 2007 Jython Developers */
+/* Copyright (c) 2017 Jython Developers */
package org.python.modules.zipimport;
import org.python.core.BuiltinDocs;
-import org.python.core.ClassDictInit;
import org.python.core.Py;
import org.python.core.PyDictionary;
import org.python.core.PyException;
import org.python.core.PyObject;
-import org.python.core.PyBytes;
import org.python.core.PyStringMap;
import org.python.expose.ExposedModule;
import org.python.expose.ModuleInit;
/**
- * This module adds the ability to import Python modules (*.py,
- * *$py.class) and packages from ZIP-format archives.
+ * This module adds the ability to import Python modules (*.py
,
+ * *$py.class
) and packages from ZIP-format archives.
*
* @author Philip Jenvey
*/
@@ -22,11 +20,13 @@
public class ZipImportModule {
public static PyObject ZipImportError;
+
public static PyException ZipImportError(String message) {
return new PyException(ZipImportError, message);
}
- // FIXME this cache should be per PySystemState, but at the very least it should also be weakly referenced!
+ // FIXME this cache should be per PySystemState, but at the very least it should also be weakly
+ // referenced!
// FIXME could also do this via a loading cache instead
public static PyDictionary _zip_directory_cache = new PyDictionary();
@@ -37,15 +37,15 @@ public static void init(PyObject dict) {
dict.__setitem__("ZipImportError", ZipImportError);
}
- /**
- * Initialize the ZipImportError exception during startup
- *
- */
+ /** Initialize the ZipImportError exception during startup */
public static void initClassExceptions(PyObject exceptions) {
PyObject ImportError = exceptions.__finditem__("ImportError");
- ZipImportError = Py.makeClass("zipimport.ZipImportError", ImportError,
- new PyStringMap() {{
- __setitem__("__module__", Py.newString("zipimport"));
- }});
+
+ ZipImportError = Py.makeClass("zipimport.ZipImportError", ImportError, new PyStringMap() {
+
+ {
+ __setitem__("__module__", Py.newString("zipimport"));
+ }
+ });
}
}
diff --git a/src/templates/zipimporter.derived b/src/templates/zipimporter.derived
index 405e14148..55862bff9 100644
--- a/src/templates/zipimporter.derived
+++ b/src/templates/zipimporter.derived
@@ -1,4 +1,4 @@
base_class: PyZipImporter
want_dict: true
-ctr:
+ctr: String archivePath
incl: object
diff --git a/tests/java/org/python/modules/zipimport/ZipImportTest.java b/tests/java/org/python/modules/zipimport/ZipImportTest.java
new file mode 100644
index 000000000..3265c7481
--- /dev/null
+++ b/tests/java/org/python/modules/zipimport/ZipImportTest.java
@@ -0,0 +1,610 @@
+/* Copyright (c) 2017 Jython Developers */
+package org.python.modules.zipimport;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.python.core.PyBytes;
+import org.python.core.PyDictionary;
+import org.python.core.PyObject;
+import org.python.core.PyTuple;
+import org.python.core.PyUnicode;
+
+/**
+ * Java level tests for {@link PyZipImporter}. If this module does not meet its expected behaviours,
+ * Jython pretty much won't start, so a test independent of the interpreter seems a good idea.
+ *
+ * Python exceptions are not handled correctly in this test because the Python classes involved are
+ * not initialised. So where a PyException might have been raised, a Java NullPointerException tends
+ * to result.
+ */
+public class ZipImportTest {
+
+ /** Shorthand for platform-specific file separator character */
+ static final char SEP = File.separatorChar;
+
+ /** In the standard library is a ZIP file at this (Unix-style) location: */
+ static final String ARCHIVE = platform("tmpdir_ZipImportTest/sub/test.zip");
+ static final Path ARCHIVE_PATH = Paths.get(ARCHIVE);
+
+ /** The file structure in the zip. Separators are '/', irrespective of platform. */
+ // @formatter:off
+ static final String[] STRUCTURE = {
+ // Used in testZipimporter_get_data
+ "sub/dir/foo/", // FOO
+ "sub/dir/foo/x/one.py", // ONE
+ "sub/dir/foo/two.py", // TWO two is a module in sub/dir/foo
+ // A library whose path is "...test.zip"
+ "b/__init__.py", // b is a package
+ "b/three.py", // three is a module in b
+ "b/c/",
+ "b/c/__init__.py", // b.c is a package
+ "b/c/__init__.pyc", // should be preferred to __init__.py
+ "b/c/four.py",
+ "b/c/four.pyc", // should be preferred to four.py
+ "b/c/five.pyc",
+ "b/d/__init__.pyc", // b.d is a package (even though no __init__.py)
+ // No "b/d/__init__.py",
+ // A library whose path is "...test.zip/lib/a"
+ "lib/a/b/__init__.py", // b is a package
+ "lib/a/b/three.py", // three is a module in b
+ "lib/a/b/c/",
+ "lib/a/b/c/__init__.py", // b.c is a package
+ "lib/a/b/c/__init__.pyc", // should be preferred to __init__.py
+ "lib/a/b/c/four.py",
+ "lib/a/b/c/four.pyc", // should be preferred to four.py
+ "lib/a/b/c/five.pyc",
+ "lib/a/b/d/__init__.pyc", // b.d is a package (even though no __init__.py)
+ // No "lib/a/b/d/__init__.py",
+ };
+
+ static final int FOO = 0, ONE = 1, TWO = 2; // Where they are in STRUCTURE
+ // @formatter:on
+
+ /** (Relative) path of directory "foo" using platform separator. */
+ static final String FOOKEY = platform(STRUCTURE[FOO]);
+ /** (Relative) path of file "one.py" using platform separator. */
+ static final String ONEKEY = platform(STRUCTURE[ONE]);
+
+ static final Charset UTF8 = Charset.forName("UTF-8");
+
+ /** A time later than any source file, used on .class
entries. */
+ static final long LATER = Instant.parse("2038-01-01T00:00:00Z").toEpochMilli();
+
+ /** Map from file name to the binary contents in the zip entry. */
+ private static final Map.py
*/
+ static final byte[] EMPTY_MODULE = {0x16, 0xd, 0xd, 0xa, // magic number: CPython pythonrun.c
+ -128, 23, -24, 127, 0, 0, 0, 0, -29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 64,
+ 0, 0, 0, 115, 4, 0, 0, 0, 100, 0, 0, 83, 41, 1, 78, -87, 0, 114, 1, 0, 0, 0, 114, 1, 0,
+ 0, 0, 114, 1, 0, 0, 0, -6, 7, 60, 101, 109, 112, 116, 121, 62, -38, 8, 60, 109, 111,
+ 100, 117, 108, 101, 62, 1, 0, 0, 0, 115, 0, 0, 0, 0};
+
+ /** Empty .class
(don't know what to write and don't care in these tests). */
+ static final byte[] EMPTY_CLASS = {1, 2, 3, 4};
+
+ /** Number of zip entries created. */
+ static int zipEntryCount = 0;
+
+ @BeforeClass
+ public static void createTestZip() throws IOException {
+
+ // Ensure directories exist to the archive location
+ if (ARCHIVE_PATH.getNameCount() > 1) {
+ Files.createDirectories(ARCHIVE_PATH.getParent());
+ }
+
+ // Create (or overwrite) the raw archive file itself
+ OutputStream out = new BufferedOutputStream(Files.newOutputStream(ARCHIVE_PATH));
+
+ // Wrap it so there are two inputs: one for ZipEntry objects and one for text to encode
+ try (ZipOutputStream zip = new ZipOutputStream(out)) {
+
+ // Make an entry for each path mentioned in STRUCTURE
+ for (int i = 0; i < STRUCTURE.length; i++) {
+
+ // The structure table gives us names for the entries
+ String name = STRUCTURE[i];
+
+ if (name.endsWith("/")) {
+ // Directory entry: no content
+ zip.putNextEntry(new ZipEntry(name));
+ zip.closeEntry();
+
+ } else if (name.endsWith(".pyc")) {
+ // Reduce to module name
+ createCompiledZipEntry(zip, name.substring(0, name.length() - 4));
+ zipEntryCount += 1;
+
+ } else if (name.endsWith(".py")) {
+ // Content is Python source
+ createTestZipEntry(zip, name, String.format("# %s\n", name));
+
+ } else {
+ // Any other kind of file (just in case)
+ createTestZipEntry(zip, name, String.format("Contents of %s\n", name));
+ }
+
+ zipEntryCount += 1;
+ }
+ }
+ }
+
+ /**
+ * Helper to {@link #createTestZip()} creating two entries with binary content: one .
+ *
+ * @param zip file to write
+ * @param name of relative zip entry (a '/'-path)
+ * @throws IOException
+ */
+ private static void createCompiledZipEntry(ZipOutputStream zip, String name)
+ throws IOException {
+ // Ensure time stamp on the binary is a long time in the future.
+ createTestZipEntry(zip, name + ".class", LATER, EMPTY_CLASS);
+ // But also make a valid .pyc file (for behavioural comparison with CPython)
+ createTestZipEntry(zip, name + ".pyc", LATER, EMPTY_MODULE);
+ }
+
+ /**
+ * Helper to {@link #createTestZip()} creating one entry with binary content.
+ *
+ * @param zip file to write
+ * @param name of relative zip entry (a '/'-path)
+ * @param time in milliseconds since epoch for timestamp (use current time if <0)
+ * @param content to write in the entry
+ * @throws IOException
+ */
+ private static void createTestZipEntry(ZipOutputStream zip, String name, long time,
+ byte[] content) throws IOException {
+ ZipEntry entry = new ZipEntry(name);
+ if (time >= 0L) {
+ entry.setTime(time);
+ }
+ zip.putNextEntry(entry);
+ zip.write(content);
+ BINARY.put(name, content);
+ zip.closeEntry();
+ }
+
+ /**
+ * Helper to {@link #createTestZip()} creating one entry with text content.
+ *
+ * @param zip file to write
+ * @param name of relative zip entry (a '/'-path)
+ * @param content to write in the entry
+ * @throws IOException
+ */
+ private static void createTestZipEntry(ZipOutputStream zip, String name, String content)
+ throws IOException {
+ ByteBuffer buf = UTF8.encode(content);
+ byte[] bytes = new byte[buf.remaining()];
+ buf.get(bytes);
+ long time = System.currentTimeMillis();
+ createTestZipEntry(zip, name, time, bytes);
+ }
+
+ @AfterClass
+ public static void deleteTestZip() {
+ // Useful to look at from Python!
+ // try {
+ // Files.deleteIfExists(ARCHIVE_PATH);
+ // } catch (IOException e) {
+ // Meh!
+ // }
+ }
+
+ @Before
+ public void setUp() throws Exception {}
+
+ @After
+ public void tearDown() throws Exception {
+ // Empty the cache for a clean start next time
+ PyDictionary cache = ZipImportModule._zip_directory_cache;
+ for (PyObject item : cache.asIterable()) {
+ cache.__delitem__(item);
+ }
+ }
+
+ /** Swap '/' for platform file name separator if different. */
+ private static String platform(String path) {
+ return (SEP == '/') ? path : path.replace('/', SEP);
+ }
+
+ /** Swap platform file name separator for '/' if different. */
+ private static String unplatform(String path) {
+ return (SEP == '/') ? path : path.replace(SEP, '/');
+ }
+
+ /**
+ * Test method for {@link PyZipImporter#PyZipImporter(java.lang.String)} where the archive path
+ * is relative to the working directory.
+ */
+ @Test
+ public void testPyZipImporterRelative() {
+ testPyZipImporterHelper(ARCHIVE);
+ }
+
+ /**
+ * Test method for {@link PyZipImporter#PyZipImporter(java.lang.String)}. where the archive path
+ * is absolute.
+ */
+ @Test
+ public void testPyZipImporterAbsolute() {
+ // Make the zip file location absolute
+ Path path = Paths.get(ARCHIVE);
+ testPyZipImporterHelper(path.toAbsolutePath().toString());
+ }
+
+ /**
+ * Test the constructor using a given path as the path to the base archive (just the zip).
+ *
+ * @param archive path to zip (which may be absolute or relative)
+ */
+ private void testPyZipImporterHelper(String archive) {
+
+ // Test where the archive path is just the zip file location
+ PyZipImporter za = testPyZipImporterHelper(archive, archive, "");
+
+ // Test where the archive path is extended with a sub-directory "foo" within the zip
+ String fooPath = Paths.get(archive, FOOKEY).toString();
+ PyZipImporter zf = testPyZipImporterHelper(fooPath, archive, FOOKEY);
+ assertEquals("cache entry not re-used", za.files, zf.files);
+
+ // If the platform is not Unix-like, check that a Unix-like path has the same result
+ if (SEP != '/') {
+ // Turn platform-specific archive path to Unix-like
+ String archiveAlt = unplatform(archive);
+ // Expected result still uses platform-specific separators (in API though not in ZIP).
+ PyZipImporter za2 = testPyZipImporterHelper(archiveAlt, archive, "");
+ assertEquals("cache entry not re-used", za.files, za2.files);
+ String fooPathAlt = unplatform(fooPath);
+ PyZipImporter zf2 = testPyZipImporterHelper(fooPathAlt, archive, FOOKEY);
+ assertEquals("cache entry not re-used", za.files, zf2.files);
+ }
+
+ // Check that an extra SEP on the end does not upset the constructor
+ PyZipImporter zax = testPyZipImporterHelper(archive + SEP, archive, "");
+ assertEquals("cache entry not re-used", za.files, zax.files);
+ PyZipImporter zfx = testPyZipImporterHelper(fooPath + SEP, archive, FOOKEY);
+ assertEquals("cache entry not re-used", za.files, zfx.files);
+
+ }
+
+ /**
+ * Helper to test construction of a zipimporter
.
+ *
+ * @param archivePath argument to zipimporter
constructor
+ * @param archive expected base archive name
+ * @param prefix expected prefix (sub-directory within ZIP)
+ * @return the zipimporter we created for further tests
+ */
+ private PyZipImporter testPyZipImporterHelper(String archivePath, String archive,
+ String prefix) {
+
+ PyZipImporter z = new PyZipImporter(archivePath);
+ assertEquals(archive, z.archive);
+ assertEquals(prefix, z.prefix);
+
+ // The files attribute should enumerate the files and directories.
+ Map
+ * >>> from zipimport import zipimporter as zi
+ * >>> archive = r'tmpdir_ZipImportTest\sub\test.zip'
+ * >>> z = zi(archive)
+ * >>> for n in sorted(z._files.keys()): print(n, z._files[n][5:7]) # (time, date)
+ * ...
+ * b\__init__.py (44545, 19185)
+ * b\c\ (44545, 19185)
+ * b\c\__init__.class (0, 29729)
+ * b\c\__init__.py (44545, 19185)
+ * b\c\__init__.pyc (0, 29729)
+ * b\c\five.class (0, 29729)
+ * b\c\five.pyc (0, 29729)
+ * b\c\four.class (0, 29729)
+ * b\c\four.py (44545, 19185)
+ * b\c\four.pyc (0, 29729)
+ * b\d\__init__.class (0, 29729)
+ * b\d\__init__.pyc (0, 29729)
+ * b\three.py (44545, 19185)
+ *
CPython behaviour is:
+ * >>> z.get_filename('b')
+ * 'tmpdir_ZipImportTest\\sub\\test.zip\\b\\__init__.py'
+ * >>> zb = zi(archive + '/b')
+ * >>> zb.get_filename('c')
+ * 'tmpdir_ZipImportTest\\sub\\test.zip\\b\\c\\__init__.py'
+ * >>> zb.get_filename('d')
+ * 'tmpdir_ZipImportTest\\sub\\test.zip\\b\\d\\__init__.pyc'
+ * >>> zc = zi(archive + '/b/c')
+ * >>> zc.get_filename('four')
+ * 'tmpdir_ZipImportTest\\sub\\test.zip\\b\\c\\four.py'
+ * >>> zc.get_filename('five')
+ * 'tmpdir_ZipImportTest\\sub\\test.zip\\b\\c\\five.pyc'
+ *
That is, we report the .py
file name if there is one and the
+ * .pyc
file name if the .py
is missing (but in Jython, the
+ * .class
file should be expected).
+ */
+ @Test
+ public void testZipimporter_get_filename() {
+ // Test with each of the entries that is not just a folder
+ for (String entry : STRUCTURE) {
+ if (entry.endsWith(".py")) {
+ // Parse the entry string into the parts we need and test
+ EntrySplit split = new EntrySplit(entry);
+ check_get_filename(split);
+ } else if (entry.endsWith(".class")) {
+ // In this case, if a .py exists, we should be given that instead
+ String pyRelativePath = entry.substring(entry.length() - 6) + ".py";
+ if (BINARY.containsKey(pyRelativePath)) {
+ entry = pyRelativePath;
+ }
+ // Parse the entry string into the parts we need and test
+ EntrySplit split = new EntrySplit(entry);
+ check_get_filename(split);
+ }
+ }
+ }
+
+ /**
+ * When we ask for the module indicated by each the {@link #STRUCTURE} entry, we should get as a
+ * file name a string consistent with the entry, allowing for the several ways to satisfy the
+ * request, and for the use of the platform {@link File#separatorChar}.
+ *
+ * @param split derived from a non-directory entry in {@link #STRUCTURE}.
+ */
+ private static void check_get_filename(EntrySplit split) {
+ // Make a PyZipImporter for this entry (parent of module or package)
+ PyZipImporter z = new PyZipImporter(split.archivePath.toString());
+
+ // The file path in the split tells us What we should have, except for the extension.
+ String expected = split.filePath.toString();
+
+ // Only the last element ought to be used.
+ String target = "ignore.me." + split.name;
+ PyObject filenameObject = z.zipimporter_get_filename(target);
+
+ if (filenameObject instanceof PyUnicode) {
+ // Compare with expected value and actual, but only up to the dot
+ String filename = ((PyUnicode) filenameObject).getString();
+ int dot = dotIndex(filename, SEP);
+ assertThat("base file path", filename.substring(0, dot),
+ equalTo(expected.substring(0, dotIndex(expected, SEP))));
+ assertThat("file path for " + target, filename, equalTo(expected));
+ } else {
+ fail("get_filename() result not a str object");
+ }
+ }
+
+ /**
+ * Test method for {@link PyZipImporter#zipimporter_load_module(java.lang.String)}.
+ */
+ // @Test
+ public void testZipimporter_load_module() {
+ fail("Not yet implemented");
+ }
+
+ /**
+ * Test method for {@link PyZipImporter#zipimporter_is_package(java.lang.String)}.
+ */
+ // @Test
+ public void testZipimporter_is_package() {
+ fail("Not yet implemented");
+ }
+
+ /**
+ * Test method for {@link PyZipImporter#zipimporter_get_source(java.lang.String)}.
+ */
+ // @Test
+ public void testZipimporter_get_source() {
+ fail("Not yet implemented");
+ }
+
+ /**
+ * Test method for {@link PyZipImporter#zipimporter_get_code(java.lang.String)}.
+ */
+ // @Test
+ public void testZipimporter_get_code() {
+ fail("Not yet implemented");
+ }
+
+ /**
+ * Test method for
+ * {@link PyZipImporter#zipimporter_find_spec(org.python.core.PyObject[], java.lang.String[])}.
+ *
+ * Disabled test. find_spec
is not implemented in
+ * zipimport.zipimporter
in CPython 3.5 as far as we can tell, and by not having it
+ * exposed, we should get fall-back behaviour depending on find_module
.
+ */
+ // @Test
+ public void testZipimporter_find_spec() {
+ // fail("Not yet implemented");
+ }
+
+}