Do we have something similar to Java’s .jar-files in the standard library? E.g. a way to run Python code from some kind of compressed file without manually unpack it first?
I’m asking out of curiosity, but also because it would open new possibility for Python programmers to distribute their programs. One thing I found useful using Jars is that it gives a convenient way to group code and all assets it needs into one file that guarantees nothing necessary to run a program would be lost.
There’s zipapp from the standard library. See especially the section on Creating Standalone Applications with zipapp . There are some caveats, like not being able to ship C extensions.
It all looks promising, but there is one thing I don’t know yet: how can I load assets from my .pyz archive? By “assets” I mean things like pictures, texts, or other similar data.
You can use importlib.resources, same as you would in an ordinary package. Inside the zipapp, calling importlib.resources.files(__package__) returns a zipfile.Path that you can use to navigate and load your package resources. For example, if you have a text file foo.txt at the root of your package, you can load it with
print("MAIN:", f"{__package__=}")
if __package__ == '':
import an_module
else:
from . import an_module
When I run normally:
$ python -m myapp
MAIN: __package__='myapp'
__package__='myapp'
fl=PosixPath('<CUT>/myapp') <class 'pathlib.PosixPath'>
asset="The contents of MyApp's test asset."
<class 'str'>
but if I pack it and try to run:
$ python -m zipapp myapp -o packed.pyz -p "/usr/bin/env python3" -c
$ ./packed.pyz
MAIN: __package__=''
__package__=''
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "<CUT>/packed.pyz/__main__.py", line 5, in <module>
File "<CUT>/packed.pyz/an_module.py", line 6, in <module>
File "/usr/lib/python3.12/importlib/resources/_common.py", line 46, in wrapper
return func(anchor)
^^^^^^^^^^^^
File "/usr/lib/python3.12/importlib/resources/_common.py", line 56, in files
return from_package(resolve(anchor))
^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/functools.py", line 909, in wrapper
return dispatch(args[0].__class__)(*args, **kw)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/importlib/resources/_common.py", line 82, in _
return importlib.import_module(cand)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1384, in _gcd_import
File "<frozen importlib._bootstrap>", line 1298, in _sanity_check
ValueError: Empty module name
Actually it doesn’t. When I use the function and execute my app as:
$ python -m myapp
then the function returns an pathlib.PosixPath object.
But when run as:
$ ./packed.pyz
then the type returned from the function is: importlib.resources._adapters.CompatibilityFiles.SpecPath which turns into importlib.resources._adapters.CompatibilityFiles.OrphanPath when I apply joinpath("test_asset.txt") on it.