[Curiosity] Python's equivalent of Java's .jar-file

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.

2 Likes

So there it is! :smiley: I wonder why I haven’t discovered it before. Thanks!

Python can import from zip files: zipimport — Import modules from Zip archives — Python 3.12.3 documentation

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

resource_root = importlib.resources.files(__package__)

foo_content = resource_root.joinpath("foot.txt").read_text()

I feel like general awareness of these techniques is not nearly high enough.

OK, I actually tried it myself but I get an error.

an_module.py

import importlib.resources as imp_res

print(f"{__package__=}")
fl = imp_res.files(__package__)
print(f"{fl=}\t{type(fl)}")
#print(dir(fl))

asset = fl.joinpath("test_asset.txt").read_text()
print(f"{asset=}\n\t{type(asset)}")

__main__.py

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

I’m confused :confused:

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.

PS: I’m using Python 3.12.3