diff --git a/product/runtime/docs/sphinx/changelog.rst b/product/runtime/docs/sphinx/changelog.rst index 4159cf55e3..52b1dfed0b 100644 --- a/product/runtime/docs/sphinx/changelog.rst +++ b/product/runtime/docs/sphinx/changelog.rst @@ -25,7 +25,7 @@ Bugfixes - .pth files are now processed after `sys.path` is fully initialized. (`#1338 `__) -- The importer now recognizes libraries with an SOABI suffix such as +- The importer now recognizes modules with an SOABI suffix such as `.cpython-313-aarch64-linux-android.so`. (`#1370 `__) diff --git a/product/runtime/docs/sphinx/changes/1383.feature.rst b/product/runtime/docs/sphinx/changes/1383.feature.rst new file mode 100644 index 0000000000..ebe19f3a77 --- /dev/null +++ b/product/runtime/docs/sphinx/changes/1383.feature.rst @@ -0,0 +1 @@ +Improved compatibility with various ways of bundling non-Python libraries in a wheel. diff --git a/product/runtime/docs/sphinx/changes/1431.bugfix.rst b/product/runtime/docs/sphinx/changes/1431.bugfix.rst new file mode 100644 index 0000000000..1abf3e7eec --- /dev/null +++ b/product/runtime/docs/sphinx/changes/1431.bugfix.rst @@ -0,0 +1 @@ +The importer now matches the standard behavior of preferring an .so file over a .py file of the same name. diff --git a/product/runtime/docs/sphinx/changes/892.feature.rst b/product/runtime/docs/sphinx/changes/892.feature.rst new file mode 100644 index 0000000000..ebe19f3a77 --- /dev/null +++ b/product/runtime/docs/sphinx/changes/892.feature.rst @@ -0,0 +1 @@ +Improved compatibility with various ways of bundling non-Python libraries in a wheel. diff --git a/product/runtime/src/main/python/java/android/importer.py b/product/runtime/src/main/python/java/android/importer.py index e2abc36acc..1addaf8fd8 100644 --- a/product/runtime/src/main/python/java/android/importer.py +++ b/product/runtime/src/main/python/java/android/importer.py @@ -82,11 +82,12 @@ def hook(path): assert isinstance(finder, AssetFinder), ("Finder for '{}' is {}" .format(entry, type(finder).__name__)) - # Extract data files from the root directory. This includes .pth files, which will be - # read by addsitedir below. + # Extract necessary files from the root directory. This includes .pth files, + # which will be read by addsitedir below. finder.extract_dir("", recursive=False) - # Extract data files from top-level directories which aren't Python packages. + # Extract necessary files from top-level directories which aren't Python + # packages or dist-info directories. for name in finder.listdir(""): if finder.isdir(name) and \ not is_dist_info(name) and \ @@ -135,14 +136,17 @@ def initialize_ctypes(): def find_library_override(name): filename = "lib{}.so".format(name) - # First look in the requirements. The return value will probably be passed to - # CDLL_init_override below, but the caller may load the library using another - # API (e.g. soundfile uses ffi.dlopen), so make sure its dependencies are - # extracted and pre-loaded. + # First look in the requirements. try: - return reqs_finder.extract_lib(filename) + filename = reqs_finder.find_lib(filename) except FileNotFoundError: pass + else: + # The return value will probably be passed to CDLL_init_override below, but + # the caller may load the library in another way (e.g. soundfile uses + # ffi.dlopen), so make sure its dependencies are pre-loaded. + load_needed(filename) + return filename # For system libraries I can't see any easy way of finding the absolute library # filename, but we can at least support the case where the user passes the return value @@ -159,15 +163,12 @@ def CDLL_init_override(self, name, *args, **kwargs): if name: # CDLL(None) is equivalent to dlopen(NULL). if "/" not in name: try: - name = reqs_finder.extract_lib(name) + name = reqs_finder.find_lib(name) except FileNotFoundError: pass - else: - # Some packages (e.g. llvmlite) use CDLL to load libraries from their own - # directories. - finder = get_importer(dirname(name)) - if isinstance(finder, AssetFinder): - name = finder.extract_so(name) + + if exists(name): + load_needed(name) CDLL_init_original(self, name, *args, **kwargs) @@ -521,16 +522,12 @@ def iter_modules(self, prefix=""): if mod_base_name and (mod_base_name != "__init__"): yield prefix + mod_base_name, False - # If this method raises FileNotFoundError, then maybe it's a system library, or one of the - # libraries loaded by AndroidPlatform.loadNativeLibs. If the library is truly missing, - # we'll get an exception when we load the file that needs it. - def extract_lib(self, filename): - return self.extract_so(f"chaquopy/lib/{filename}") - - def extract_so(self, path): - path = self.extract_if_changed(self.zip_path(path)) - load_needed(self, path) - return path + def find_lib(self, filename): + zip_path = f"chaquopy/lib/{filename}" + if self.exists(zip_path): + return join(self.extract_root, zip_path) + else: + raise FileNotFoundError(zip_path) def extract_dir(self, zip_dir, recursive=True): dotted_dir = zip_dir.replace("/", ".") @@ -542,9 +539,9 @@ def extract_dir(self, zip_dir, recursive=True): if self.isdir(zip_path): if recursive: self.extract_dir(zip_path) - elif (extract_package and filename.endswith(".py") - or not (any(filename.endswith(suffix) for suffix in LOADERS) or - re.search(r"^lib.*\.so\.", filename))): # e.g. libgfortran + # For performance, we don't extract any Python modules unless their + # containing package is listed in extract_packages. + elif extract_package or not filename.endswith(PYTHON_SUFFIXES): self.extract_if_changed(zip_path) def extract_if_changed(self, zip_path): @@ -715,34 +712,46 @@ def get_code(self, fullname): class ExtensionAssetLoader(AssetLoader, machinery.ExtensionFileLoader): def create_module(self, spec): - self.finder.extract_so(self.path) + self.finder.extract_if_changed(self.finder.zip_path(self.path)) + load_needed(self.path) return super().create_module(spec) needed_lock = RLock() needed_loaded = {} -# CDLL will cause a recursive call back to extract_so, so there's no need for any additional -# recursion here. If we return to executables in the future, we can implement a separate -# recursive extraction on top of get_needed. -def load_needed(finder, path): +# Load any libraries in chaquopy/lib which are needed by the .so file at the given path. +# +# RTLD_GLOBAL and RTLD_LOCAL behave a bit differently on Android compared to Linux: +# +# * Regardless of the mode, dlopening a library is sufficient to make it available to +# other libraries which use its SONAME in a DT_NEEDED entry. +# +# * Regardless of the mode, the library's symbols are NOT implicitly available to other +# libraries which don't list it in DT_NEEDED. But this may change in the future, so +# it's safer for us to use the default of RTLD_LOCAL, which should avoid conflicts +# between multiple libraries defining the same symbol. +# +# * RTLD_GLOBAL makes the library's symbols available to explicit searches of other +# libraries using dlsym, but that probably isn't relevant to us. +# +# Sources: +# * https://github.com/android/ndk/issues/1244#issuecomment-620310397 +# * https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md +def load_needed(path): with needed_lock: for soname in get_needed(path): if soname not in needed_loaded: try: - # Before API level 23, the only dlopen mode was RTLD_GLOBAL, and - # RTLD_LOCAL was ignored. From API level 23, RTLD_LOCAL is available - # and used by default, just like in Linux - # (https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md). - # - # We use RTLD_GLOBAL to make the library's symbols available to - # subsequently-loaded libraries, but this may not actually work - - # see #728. + # Whether the library is closed when the CDLL object is garbage + # collected is not documented, so keep a reference for safety. # - # It doesn't look like the library is closed when the CDLL object is garbage - # collected, but this isn't documented, so keep a reference for safety. - needed_loaded[soname] = ctypes.CDLL(soname, ctypes.RTLD_GLOBAL) - except FileNotFoundError: + # CDLL will recursively call load_needed if necessary. + needed_loaded[soname] = ctypes.CDLL(soname) + except OSError: + # It's not in chaquopy/lib, but maybe it can be found in some other + # way, such as DT_RUNPATH. If it's truly missing, we'll get an + # exception when we load the file that needs it. needed_loaded[soname] = None @@ -759,12 +768,20 @@ def get_needed(path): # Suffixes are in order of preference. LOADERS = { + # .so files should come first, to match the standard finder. For example, pyzmq + # 27.1.0 depends on this (see zmq/backend/cython/_zmq.py). + **{suffix: ExtensionAssetLoader for suffix in _imp.extension_suffixes()}, + # .pyc should be preferred over .py, because it'll load faster. ".pyc": SourcelessAssetLoader, ".py": SourceAssetLoader, } -for suffix in _imp.extension_suffixes(): - LOADERS[suffix] = ExtensionAssetLoader + +# If a filename ends with .so, without any .cpython or .abi3 marker, then we can't +# distinguish it from a non-Python library, so we must eagerly extract it. +PYTHON_SUFFIXES = tuple( + suffix for suffix in LOADERS if suffix != ".so" +) class AssetZipFile(ZipFile): diff --git a/product/runtime/src/test/python/chaquopy/test/android/test_import.py b/product/runtime/src/test/python/chaquopy/test/android/test_import.py index 054c5153e6..3de6efa2cb 100644 --- a/product/runtime/src/test/python/chaquopy/test/android/test_import.py +++ b/product/runtime/src/test/python/chaquopy/test/android/test_import.py @@ -8,7 +8,7 @@ import io import marshal import os -from os.path import dirname, exists, join, realpath, relpath, splitext +from os.path import basename, dirname, exists, join, realpath, relpath from pathlib import Path, PosixPath import pkgutil import re @@ -20,6 +20,7 @@ from warnings import catch_warnings, filterwarnings import zipfile +from java._vendor.elftools.common.exceptions import ELFError from java.android import importer from ..test_utils import FilterWarningsCase @@ -31,30 +32,32 @@ # REQS_COMMON_ZIP and REQS_ABI_ZIP are now both extracted into the same directory, but we # maintain the distinction in the tests in case that changes again in the future. APP_ZIP = "app" -REQS_COMMON_ZIP = "requirements-common" -multi_abi = len([name for name in context.getAssets().list("chaquopy") - if name.startswith("requirements")]) > 2 -REQS_ABI_ZIP = f"requirements-{ABI}" if multi_abi else REQS_COMMON_ZIP +REQS_COMMON_ZIP = REQS_ABI_ZIP = "requirements" +STDLIB_ZIP = f"stdlib-{ABI}" def asset_path(zip_name, *paths): - return join(realpath(context.getFilesDir().toString()), "chaquopy/AssetFinder", - zip_name.partition("-")[0], *paths) + return join( + realpath(context.getFilesDir().toString()), + "chaquopy/AssetFinder", + zip_name, + *paths, + ) def resource_path(zip_name, package, filename): return asset_path(zip_name, package.replace(".", "/"), filename) -def importable(filename): - return splitext(filename)[1] in [".py", ".pyc", ".so"] +def extracted(filename): + return not filename.endswith((".py", ".pyc", ".abi3.so", add_soabi(".so"))) -def add_soabi(version_info, abi, filename): - soabi = f"cpython-{version_info[0]}{version_info[1]}" - if version_info >= (3, 13): +def add_soabi(filename): + soabi = f"cpython-{sys.version_info[0]}{sys.version_info[1]}" + if sys.version_info >= (3, 13): soabi += "-" + { "arm64-v8a": "aarch64", "x86_64": "x86_64", - }[abi] + "-linux-android" + }[ABI] + "-linux-android" return filename.replace(".so", f".{soabi}.so") @@ -87,13 +90,7 @@ def test_bootstrap(self): stdlib_bootstrap_expected -= {"_datetime.so", "_opcode.so"} for subdir, entries in [ - ( - ABI, - [ - add_soabi(sys.version_info, ABI, filename) - for filename in stdlib_bootstrap_expected - ], - ), + (ABI, [add_soabi(filename) for filename in stdlib_bootstrap_expected]), (f"{ABI}/java", ["chaquopy.so"]), ]: with self.subTest(subdir=subdir): @@ -110,6 +107,24 @@ def test_bootstrap(self): with self.subTest(filename=filename): self.assertIn(re.sub(r"\..*", "", filename), sys.modules) + # The only other extracted stdlib modules should be those used by the unit + # tests. This should pass on repeated runs, but the files may require manual + # cleanup if anything else is imported while debugging. + # + # These bounds don't have to be particularly tight, we just need to verify that + # it's extracting something expected, but not everything. + stdlib_extracted_expected = { + "_asyncio.so", "_blake2.so", "_contextvars.so", "_csv.so", "_hashlib.so", + "_heapq.so", "_json.so", "_multiprocessing.so", "_pickle.so", + "_posixsubprocess.so", "_queue.so", "_socket.so", "_sqlite3.so", "_ssl.so", + "array.so", "fcntl.so", "pyexpat.so", "select.so", "unicodedata.so", + } + actual = set(os.listdir(asset_path(STDLIB_ZIP))) + self.assertIn(add_soabi("_posixsubprocess.so"), actual) # subprocess < test_stdlib + self.assertLessEqual( + actual, {add_soabi(filename) for filename in stdlib_extracted_expected} + ) + def test_init(self): self.check_py("murmurhash", REQS_COMMON_ZIP, "murmurhash/__init__.py", "get_include", is_package=True) @@ -172,10 +187,23 @@ def write_pyc_header(self, filename, header): pyc_file.write(header) def test_so(self): + # murmurhash's module has no SOABI suffix, so it should be extracted when its + # containing package is imported. + import murmurhash filename = asset_path(REQS_ABI_ZIP, "murmurhash/mrmr.so") - mod = self.check_module("murmurhash.mrmr", filename, filename) + self.check_module("murmurhash.mrmr", filename, cache_filename=None) + self.check_extract_if_changed(murmurhash, filename) + + # stdlib modules have SOABI suffixes, so they should be extracted individually + # on demand. + filename = asset_path(STDLIB_ZIP, add_soabi("fcntl.so")) + mod = self.check_module("fcntl", filename, cache_filename=filename) self.check_extract_if_changed(mod, filename) + def test_loader_priority(self): + with self.assertRaisesRegex(ELFError, r"Magic number does not match"): + import loader_priority # noqa: F401 + def test_ctypes(self): def assertHasSymbol(dll, name): self.assertIsNotNone(getattr(dll, name)) @@ -183,29 +211,31 @@ def assertNotHasSymbol(dll, name): with self.assertRaises(AttributeError): getattr(dll, name) - # Library extraction from calling CDLL with a path outside of chaquopy/lib. - from murmurhash import mrmr - os.remove(mrmr.__file__) - ctypes.CDLL(mrmr.__file__) - self.assertPredicate(exists, mrmr.__file__) - - # Library extraction caused by find_library. + # find_library should find libraries inside chaquopy/lib. LIBCXX_FILENAME = asset_path(REQS_ABI_ZIP, "chaquopy/lib/libc++_shared.so") - os.remove(LIBCXX_FILENAME) self.assertEqual(find_library("c++_shared"), LIBCXX_FILENAME) self.assertPredicate(exists, LIBCXX_FILENAME) - libcxx = ctypes.CDLL(LIBCXX_FILENAME) - assertHasSymbol(libcxx, "_ZSt9terminatev") # std::terminate() - assertNotHasSymbol(libcxx, "nonexistent") - # Calling CDLL with a basename in chaquopy/lib can also cause an extraction. - os.remove(LIBCXX_FILENAME) - ctypes.CDLL("libc++_shared.so") - self.assertPredicate(exists, LIBCXX_FILENAME) + # CDLL should accept both absolute paths and basenames inside chaquopy/lib. + for path in [LIBCXX_FILENAME, basename(LIBCXX_FILENAME)]: + with self.subTest(path): + libcxx = ctypes.CDLL(path) + assertHasSymbol(libcxx, "_ZSt9terminatev") # std::terminate() + assertNotHasSymbol(libcxx, "nonexistent") + + # CDLL should accept absolute paths outside of chaquopy/lib. + from murmurhash import mrmr + mrmr_dll = ctypes.CDLL(mrmr.__file__) + assertHasSymbol(mrmr_dll, "PyInit_mrmr") # CDLL with nonexistent filenames, both relative and absolute. for name in ["invalid.so", f"{dirname(mrmr.__file__)}/invalid.so"]: - with self.assertRaisesRegex(OSError, "invalid.so"): + with ( + self.subTest(name), + self.assertRaisesRegex( + OSError, rf'dlopen failed: library "{name}" not found' + ), + ): ctypes.CDLL(name) # System libraries. @@ -234,8 +264,15 @@ def test_non_package_data(self): with self.subTest(dir_name=dir_name): extracted_dir = asset_path(APP_ZIP, dir_name) self.assertCountEqual( - ["non_package_data.txt"] + (["test.pth"] if not dir_name else []), - [entry.name for entry in os.scandir(extracted_dir) if entry.is_file()]) + [entry.name for entry in os.scandir(extracted_dir) if entry.is_file()], + [ + # Should extract everything except .py and .pyc files. + "libnon_package_data.so.1", + "non_package_data.so", + "non_package_data.txt", + *(["test.pth"] if not dir_name else []), + ] + ) with open(join(extracted_dir, "non_package_data.txt")) as f: self.assertPredicate(str.startswith, f.read(), f"# Text file in {dir_description}") @@ -244,6 +281,15 @@ def test_non_package_data(self): # package is never imported, so it should never be extracted at all. self.assertNotPredicate(exists, asset_path(APP_ZIP, "never_imported")) + # The chaquopy directory is also treated as non-package data. + for dir_name, expected in [ + ("chaquopy", ["lib"]), + ("chaquopy/lib", ["libc++_shared.so"]), + ]: + self.assertCountEqual( + os.listdir(asset_path(REQS_ABI_ZIP, dir_name)), expected + ) + def test_package_data(self): # App ZIP pkg = "android1" @@ -277,8 +323,7 @@ def check_data(self, zip_name, package, filename, start): data = pkgutil.get_data(package, filename) self.assertTrue(data.startswith(start)) - if importable(filename): - # Importable files are not extracted. + if not extracted(filename): self.assertNotPredicate(exists, cache_filename) else: self.check_extract_if_changed(mod, cache_filename) @@ -304,11 +349,13 @@ def check_extract_if_changed(self, mod, cache_filename): self.assertEqual(original_mtime, os.stat(cache_filename).st_mtime) def test_extract_packages(self): - self.check_extract_packages("ep_alpha", []) - self.check_extract_packages("ep_bravo", [ + self.check_extract_packages("ep_alpha", []) # Not extracted + self.check_extract_packages("ep_bravo", [ # Top-level package extracted "__init__.py", "mod.py", "one/__init__.py", "two/__init__.py" ]) - self.check_extract_packages("ep_charlie", ["one/__init__.py"]) + self.check_extract_packages("ep_charlie", [ # Second-level package extracted + "one/__init__.py" + ]) # If a module has both a .py and a .pyc file, the .pyc file should be used because # it'll load faster. @@ -328,7 +375,8 @@ def check_extract_packages(self, package, files): if not files: self.assertNotPredicate(exists, cache_dir) else: - self.assertCountEqual(files, + pyc_files = [file + "c" for file in files if file.endswith(".py")] + self.assertCountEqual(files + pyc_files, [relpath(join(dirpath, name), cache_dir) for dirpath, _, filenames in os.walk(cache_dir) for name in filenames]) @@ -336,6 +384,13 @@ def check_extract_packages(self, package, files): with open(f"{cache_dir}/{path}") as file: self.assertEqual(f"# This file is {package}/{path}\n", file.read()) + def test_top_level(self): + actual = set(os.listdir(asset_path(REQS_COMMON_ZIP))) + self.assertGreaterEqual(actual, {"chaquopy"}) + self.assertLessEqual( + actual, {"chaquopy", "ep_bravo", "ep_charlie", "murmurhash"} + ) + def clean_reload(self, mod): sys.modules.pop(mod.__name__, None) submod_names = [name for name in sys.modules if name.startswith(mod.__name__ + ".")] @@ -649,8 +704,8 @@ def test_path_subdir(self): # Make sure the standard library importer implements the new loader API # (https://stackoverflow.com/questions/63574951). def test_zipimport(self): - for mod_name in ["zipfile", # Imported during bootstrap - "wave"]: # Imported after bootstrap + for mod_name in ["zipfile", # Imported during bootstrap (in importer.py) + "subprocess"]: # Imported after bootstrap (in test_stdlib.py) with self.subTest(mod_name=mod_name): old_mod = import_module(mod_name) spec = importlib.util.find_spec(mod_name) @@ -794,7 +849,7 @@ def check_pr_resource(self, zip_name, package, filename, start): abs_filename = pr.resource_filename(package, filename) self.assertEqual(abs_filename, resource_path(zip_name, package, filename)) - if importable(filename): + if not extracted(filename): # pkg_resources has a mechanism for extracting resources to temporary # files, but we don't currently support it. self.assertNotPredicate(exists, abs_filename) @@ -900,7 +955,7 @@ def check_resource_file(self, file, abs_filename, data, binary): self.assertIsInstance(file, io.TextIOWrapper) buffer = file.buffer - if importable(abs_filename): + if not extracted(abs_filename): self.assertIsInstance(buffer, io.BytesIO) else: self.assertIsInstance(buffer, io.BufferedReader) @@ -910,7 +965,7 @@ def check_resource_file(self, file, abs_filename, data, binary): def check_resource_path(self, path, abs_filename, data, binary): self.assertIs(type(path), PosixPath) - if importable(abs_filename): + if not extracted(abs_filename): # Non-extracted files are copied to the temporary directory. self.assertEqual(dirname(path), join(str(context.getCacheDir()), "chaquopy/tmp")) @@ -1017,7 +1072,7 @@ def check_resource_new(self, zip_name, package, filename, start): # We should get the same result when passing the whole filename at once. self.assertEqual(resources.files(package) / filename, path) - if importable(filename): + if not extracted(filename): self.assertNotIsInstance(path, Path) else: self.assertIs(type(path), PosixPath) diff --git a/product/runtime/src/test/python/loader_priority/__init__.py b/product/runtime/src/test/python/loader_priority/__init__.py new file mode 100644 index 0000000000..4a0879c11f --- /dev/null +++ b/product/runtime/src/test/python/loader_priority/__init__.py @@ -0,0 +1 @@ +raise Exception("loader_priority.py was imported") diff --git a/product/runtime/src/test/python/loader_priority/__init__.so b/product/runtime/src/test/python/loader_priority/__init__.so new file mode 100644 index 0000000000..810bfdc047 --- /dev/null +++ b/product/runtime/src/test/python/loader_priority/__init__.so @@ -0,0 +1,2 @@ +This is obviously an invalid .so file, but it still proves that the finder prefers .so +files over .py files with the same name. diff --git a/release/performance.md b/release/performance.md index bf603eeabc..2e35d65492 100644 --- a/release/performance.md +++ b/release/performance.md @@ -3,8 +3,8 @@ Use the pkgtest app as follows (process for older versions varied as noted in the table): -* Chaquopy release build – temporarily edit the top-level build.gradle file to set the - version. +* Chaquopy release build – temporarily edit the pkgtest top-level build.gradle file to + set the version. * App debug build * scipy and matplotlib * abiFilters set to arm64-v8a and x86_64 @@ -32,13 +32,14 @@ Sizes are as reported in Settings: ``` First run Second run Startup Test Startup Test App MB Data MB -Samsung J2 + 8.0.0 3.46 13.81 3.20 7.73 76.7 59.2 8.0.1 3.57 13.80 3.38 7.79 76.9 59.2 9.0.0 3.56 14.25 3.34 7.70 77.3 59.2 9.1.0 3.49 14.29 3.30 7.59 78.5 59.3 -Nexus 4 +* Moved from Samsung J2 to Nexus 4. + 9.1.0 3.52 17.49 3.48 8.91 78.8 59.3 10.0.1 3.82 19.50 3.61 9.04 76.8 59.4 @@ -57,19 +58,25 @@ Nexus 4 14.0.2 3.40 18.97 3.06 11.31 76.1 62.7 +* Moved from Nexus 4 to Pixel 7. * Changed abiFilters from armeabi-v7a and x86 to arm64-v8a and x86_64. * Startup times now use the time from the "Displayed com.application.id: +123ms" message. Previously we measured from the first to the last message mentioning the application ID, which is roughly the same. -Pixel 7 - 14.0.2 0.64 1.51 0.62 0.89 78.7 67.7 - 15.0.1 0.61 1.54 0.55 0.93 78.7 68.0 + 14.0.2 0.64 1.51 0.62 0.89 78.7 67.7 + +* Updated Matplotlib from 3.1.2 to 3.6.0. + + 15.0.1 0.61 1.54 0.55 0.93 78.7 68.0 * Re-testing 15.0.1 now shows an App size of 84.4 MB, and even the APK is bigger than 80, so most of the increase here is probably caused by new versions of the AGP or the app's Java dependencies. - 16.0.0 0.63 1.50 0.60 0.91 84.9 68.3 - 16.1.0 0.61 1.46 0.59 0.86 85.1 68.5 + 16.0.0 0.63 1.50 0.60 0.91 84.9 68.3 + 16.1.0 0.61 1.46 0.59 0.86 85.1 68.5 + +* Updated Python from 3.8 to 3.10, NumPy from 1.19.5 to 1.26.2, and SciPy from + 1.4.1 to 1.8.1. ``` diff --git a/server/pypi/packages/pyzmq/test.py b/server/pypi/packages/pyzmq/test.py index 9b5b51c312..12322b05a5 100644 --- a/server/pypi/packages/pyzmq/test.py +++ b/server/pypi/packages/pyzmq/test.py @@ -1,11 +1,5 @@ -import importlib -from os.path import basename, isfile import unittest -try: - from android.os import Build -except ImportError: - Build = None ADDRESS = "tcp://127.0.0.1" TIMEOUT = 500 @@ -30,30 +24,3 @@ def test_basic(self): server.close() client.close() - - # Several packages have modules with the filename utils.so. Make sure the importer - # handles that correctly. - @unittest.skipUnless(Build, "Android only") - def test_importer(self): - import zmq - zmq_mod = zmq.backend.cython.utils - - OTHER_MODULES = ["cytoolz.utils", "h5py.utils"] # Alphabetical order. - for name in OTHER_MODULES: - try: - other_mod = importlib.import_module(name) - break - except ImportError: - pass - else: - self.fail(f"requires at least one of {OTHER_MODULES}") - - self.assertNotEqual(zmq_mod, other_mod) - self.assertNotEqual(zmq_mod.__file__, other_mod.__file__) - self.assertNotEqual(dir(zmq_mod), dir(other_mod)) - - for mod in [zmq_mod, other_mod]: - with self.subTest(mod): - file = mod.__file__ - self.assertEqual("utils.so", basename(file)) - self.assertTrue(isfile(file)) diff --git a/server/pypi/pkgtest/app/build.gradle b/server/pypi/pkgtest/app/build.gradle index 920e797134..2825947aa7 100644 --- a/server/pypi/pkgtest/app/build.gradle +++ b/server/pypi/pkgtest/app/build.gradle @@ -20,7 +20,6 @@ ext.TEST_PACKAGES = [ "photutils": ["scipy"], "pycurl": ["certifi"], "pyzbar": ["pillow"], - "pyzmq": ["cytoolz"], "qutip": ["setuptools"], "ruamel-yaml-clib": ["ruamel-yaml"], "shapely": ["numpy"],