Skip to content

Commit 7d23a21

Browse files
committed
Migrated to Python 3 (only)
commit f69c25a Author: sourcesimian <[email protected]> Date: Mon Dec 20 20:21:10 2021 +0200 Migrate to Python 3 commit 9d5040b Author: sourcesimian <[email protected]> Date: Sat Jun 9 14:13:25 2018 +0200 : Added unittests for bake and dictfilesystem commit 20caca1 Author: sourcesimian <[email protected]> Date: Fri Jun 8 23:58:59 2018 +0200 : Python 3 compatibility
1 parent b50b555 commit 7d23a21

22 files changed

+634
-271
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
*.pyc
22
.idea/
3-
.cache/
3+
.cache*/
44
reports/
55
dist/
66
pybake.egg-info/
77
virtualenv/
8+
venv/
9+
.pytest_cache/

Makefile

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11

2+
23
develop:
3-
virtualenv --no-site-packages --python /usr/bin/python2.7 virtualenv
4-
{ \
5-
. ./virtualenv/bin/activate; \
6-
curl https://bootstrap.pypa.io/get-pip.py | python; \
7-
pip install pytest flake8; \
8-
}
4+
python3 -m venv venv
95

106

117
clean:
12-
git clean -dfx
8+
git clean -dfxn | grep 'Would remove' | awk '{print $3}' | grep -v -e '^.idea' -e '^.cache' | xargs rm -rf
139

1410

1511
check:
@@ -20,11 +16,11 @@ test:
2016
pytest ./tests/ -vvv --junitxml=./reports/unittest-results.xml
2117

2218

23-
to_pypi_test:
24-
python setup.py register -r pypitest
25-
python setup.py sdist upload -r pypitest
19+
to_pypi_test: test
20+
python -m build
21+
twine upload -r testpypi dist/*
2622

2723

28-
to_pypi_live:
29-
python setup.py register -r pypi
30-
python setup.py sdist upload -r pypi
24+
to_pypi_live: test
25+
python -m build
26+
twine upload dist/*

README.md

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,116 @@
1-
# PyBake
1+
PyBake <!-- omit in toc -->
2+
===
23

3-
Build single file Python scripts with builtin frozen file system
4+
***Create single file standalone Python scripts with builtin frozen file system***
5+
6+
- [Purpose](#purpose)
7+
- [Usage](#usage)
8+
- [Bake Script](#bake-script)
9+
- [Advanced](#advanced)
10+
- [Tracebacks](#tracebacks)
11+
- [Inspection Server](#inspection-server)
12+
- [Content](#content)
13+
- [Frozen File Systems](#frozen-file-systems)
14+
- [License](#license)
15+
16+
# Purpose
17+
PyBake can bundle an entire Python project including modules and data files into a single Python file, to provide a "standalone executable" style utility. PyBake supports pure Python modules only, thus PyBakes are multi-platform.
18+
19+
PyBakes just run and don't need an installation so are a great way of rapidly distributing tooling or utility scripts, yet the development can still be done using a formal module hierarchy.
20+
21+
The intention of PyBake is to be lightweight and to remain pure Python. There are several other "standalone executable" Python projects, with differing options and capabilities:
22+
* [PyInstaller](https://www.pyinstaller.org/)
23+
* [py2exe](https://www.py2exe.org/)
24+
* [cx_Freeze](https://marcelotduarte.github.io/cx_Freeze)
25+
* [py2app](https://github.com/ronaldoussoren/py2app/blob/master/README.rst)
26+
27+
28+
29+
30+
# Usage
31+
The current usage idiom is to create a bake script that is run when you wish to produce a release of your utility.
32+
## Bake Script
33+
This bake script includes an optional header to provide version information and setup instructions. The footer is used to invoke the desired entry point. The footer can be
34+
omitted should you wish your PyBake to simply act as a module that can be imported into other scripts, e.g.:
35+
```
36+
#!/usr/bin/env python3
37+
from pybake import PyBake
38+
39+
HEADER = '''\
40+
################################################################################
41+
## my-util - Helper for ... ##
42+
## Setup: Save this entire script to a file called "my-util.py". ##
43+
## Make it executable, e.g.: `chmod +x ./my-util.py`. ##
44+
## Run `./my-util.py --help` ##
45+
## Version: 0.1 ##
46+
################################################################################
47+
'''
48+
49+
FOOTER = '''\
50+
if __name__ == '__main__': ##
51+
from my_util.main import main ##
52+
exit(main()) ##
53+
'''
54+
55+
pb = PyBake(HEADER, FOOTER)
56+
import my_util
57+
pb.add_module(my_util)
58+
59+
with open('./data.json', 'rt') as fh:
60+
pb.add_file(('my_util', 'data.json'), fh)
61+
62+
pb.write_bake('my-util.py')
63+
```
64+
65+
Then just run `my-util.py`.
66+
67+
# Advanced
68+
## Tracebacks
69+
A PyBake maintains a full representation of source paths and line numbers in Tracebacks. So it is easy to track down the source of any unhandled exceptions. For example if there was a `KeyError` in `main.py` of your utility, the Traceback would appear as follows. The path points to the PyBake path, and then the path from the frozen filesystem is suffixed.
70+
```
71+
Traceback (most recent call last):
72+
File "/home/user/./my-util.py", line 132, in <module>
73+
exit(main()) ##
74+
File "/home/user/./my-util.py/my_util/main.py", line 3, in main
75+
KeyError: 'Oops!'
76+
```
77+
78+
## Inspection Server
79+
Sometimes you may want to inspect the contents of your PyBake. This can be done by running your PyBake with the `--pybake-server` argument. It will run a small HTTP server that serves on `localhost:8080`. Open your web browser to http://localhost:8080 from where you can browse the contents. An additional argument will be interpreted as a port.
80+
81+
This feature is also a good way to calm the minds of your more suspicious colleagues, who think you're up to something nefarious because you've "obfuscated" your code. And then after lunch they happily `apt-get` install some binary on their machine without first reading the machine code ¯\_(ツ)_
82+
83+
## Content
84+
The content of a PyBake is pure Python. However, if you glance at the source you might think that it is some form of executable. However, taking a closer look at the head will show, e.g.:
85+
```
86+
#!/usr/bin/env python3
87+
################################################################################
88+
## my-util - Helper for ... ##
89+
## ...
90+
################################################################################
91+
_='''eJzVPGuT2kiSf6XDGxv2xPV6QTQ+MxHzAWSkRtB4gEYIeRwdSAIkkISmxUtszH+/zCqp9CrRjHf
92+
```
93+
Then many of lines of "garbage", e.g.:
94+
```
95+
MzsXtdQdhkOqRle/Myqqv77wg2r8cbg5JtI5/DdmvOIHv+B++fAz2ztFfxx/j9cFZb1ZH//DhfZRYq93
96+
```
97+
And the tail will show. e.g.:
98+
```
99+
...
100+
WgtFLYPu09hG9EDwjGF8MG9ei/fHH9/8BdRTTUQ=='''
101+
import binascii, json, zlib ##
102+
_ = json.loads(zlib.decompress(binascii.a2b_base64(_))); exec(_[0], globals())##
103+
if __name__ == '__main__': ##
104+
from my_util.main import main ##
105+
exit(main()) ##
106+
###########################################################################END##
107+
```
108+
As you can see a PyBake is simply a very short Python script with a giant zlib compressed Base 64 encoded data structure assigned to a string called `-`.
109+
110+
## Frozen File Systems
111+
As mentioned PyBake is pure Python and serves as a demonstration of the awesome builtin capabilities of the Python standard libraries. If you are interested to understand more, start reading the source in [launcher.py](./pybake/launcher.py) and follow the `DictFileSystem`.
112+
113+
114+
# License
115+
116+
In the spirit of the Hackers of the [Tech Model Railroad Club](https://en.wikipedia.org/wiki/Tech_Model_Railroad_Club) from the [Massachusetts Institute of Technology](https://en.wikipedia.org/wiki/Massachusetts_Institute_of_Technology), who gave us all so very much to play with. The license is [MIT](LICENSE).

pybake/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
3+
__all__ = ['PyBake']
4+
5+
from pybake.pybake import PyBake

pybake/abstractimporter.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import imp
1+
2+
3+
import types
24
import os
35
import sys
46

@@ -21,7 +23,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
2123
self.uninstall()
2224

2325
def install(self):
24-
sys.meta_path.append(self)
26+
sys.meta_path.insert(0, self)
2527

2628
def uninstall(self):
2729
try:
@@ -64,7 +66,7 @@ def find_module(self, fullname, path=None):
6466
def load_module(self, fullname):
6567
full_path = self._full_path(fullname)
6668

67-
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
69+
mod = sys.modules.setdefault(fullname, types.ModuleType(fullname))
6870
mod.__file__ = full_path
6971
mod.__loader__ = self
7072

@@ -76,18 +78,16 @@ def load_module(self, fullname):
7678
mod.__package__ = '.'.join(fullname.split('.')[:-1])
7779
source = self._read_file(full_path)
7880
try:
79-
exec compile(source, full_path, 'exec') in mod.__dict__
81+
exec(compile(source, full_path, 'exec'), mod.__dict__)
8082
except ImportError:
8183
exc_info = sys.exc_info()
82-
raise exc_info[0], "%s, while importing '%s'" % (exc_info[1], fullname), exc_info[2]
83-
# raise exc_info[0] from ex
84+
exc_info1 = ImportError("%s, while importing '%s'" % (exc_info[1], fullname))
85+
reraise(exc_info[0], exc_info1, exc_info[2])
8486
except Exception:
85-
orig_exc_type, exc, tb = sys.exc_info()
86-
exc_info = (ImportError, exc, tb)
87-
raise exc_info[0], "%s: %s, while importing '%s'" % (orig_exc_type.__name__,
88-
exc_info[1],
89-
fullname), exc_info[2]
90-
# raise exc_info[0] from ex
87+
exc_info = sys.exc_info()
88+
exc_info1 = ImportError("%s: %s, while importing '%s'" % (exc_info[0].__name__,
89+
exc_info[1], fullname))
90+
reraise(ImportError, exc_info1, exc_info[2])
9191
self._add_module(fullname)
9292
return mod
9393

@@ -96,13 +96,28 @@ def get_source(self, fullname):
9696
return self._read_file(full_path)
9797

9898
def is_package(self, fullname):
99-
print '!!!! is_package',
99+
path = self._full_path(fullname)
100+
if path:
101+
return path.endswith('__init__.py')
102+
return False
100103

101104
def get_code(self, fullname):
102-
print '!!!! get_code',
105+
print('!!!! get_code')
103106

104107
def get_data(self, path):
105-
print '!!!! get_data',
108+
print('!!!! get_data')
106109

107110
def get_filename(self, fullname):
108-
print '!!!! get_filename',
111+
return self._full_path(fullname)
112+
113+
114+
def reraise(tp, value, tb=None):
115+
try:
116+
if value is None:
117+
value = tp()
118+
if value.__traceback__ is not tb:
119+
raise value.with_traceback(tb)
120+
raise value
121+
finally:
122+
value = None
123+
tb = None

pybake/blobserver.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ def run(self, address='8080'):
1919
port = address
2020
port = int(port)
2121

22-
from SimpleHTTPServer import SimpleHTTPRequestHandler
23-
from BaseHTTPServer import HTTPServer
22+
from http.server import SimpleHTTPRequestHandler, HTTPServer
2423

2524
class Server(HTTPServer):
2625
def __init__(self, *args, **kwargs):

pybake/dictfilesystem.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class DictFileSystem(object):
88

99
def __init__(self, base_dir=None, dict_tree=None):
1010
self._base_dir = self._normalise_path(base_dir or '/pybake/root')
11-
self._dict_tree = dict_tree or {}
11+
self._dict_tree = {} if dict_tree is None else dict_tree
1212
try:
1313
self._base_stat = os.stat(self._base_dir)
1414
except OSError:
@@ -17,7 +17,7 @@ def __init__(self, base_dir=None, dict_tree=None):
1717
def tpath(self, path):
1818
path = self._normalise_path(path)
1919
try:
20-
if isinstance(path, basestring) and path.startswith(self._base_dir):
20+
if isinstance(path, str) and path.startswith(self._base_dir):
2121
subpath = path[len(self._base_dir):]
2222
# if not subpath:
2323
# return None
@@ -52,7 +52,7 @@ def _get_node(self, tpath):
5252
raise IOError(errno.ENOENT, "No such file or directory", '%s/%s' % (self._base_dir, os.sep.join(tpath)))
5353

5454
def listdir(self, tpath):
55-
return self._get_node(tpath).keys()
55+
return sorted(self._get_node(tpath).keys())
5656

5757
def read(self, tpath):
5858
type, content = self._get_node(tpath)
@@ -105,7 +105,9 @@ def stat(self, tpath):
105105

106106
def get_dict_tree(self):
107107
def encode(type, content):
108-
return ('base64', binascii.b2a_base64(content))
108+
if isinstance(content, str):
109+
content = content.encode('utf-8')
110+
return ('base64', binascii.b2a_base64(content).decode('utf-8'))
109111
# try:
110112
# content.decode('ascii')
111113
# return ('raw', content)
@@ -121,7 +123,8 @@ def walk(src):
121123
dst[key] = encode(*src[key])
122124
return dst
123125

124-
return walk(self._dict_tree)
126+
tree = walk(self._dict_tree)
127+
return tree
125128

126129
@staticmethod
127130
def _normalise_path(filename):

pybake/dictfilesystembuilder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def add_module(self, module, base=()):
1616

1717
if module.__package__ is not None:
1818
package = tuple(module.__package__.split('.'))
19+
else:
20+
package = tuple(module.__name__.split('.'))
1921
elif os.path.exists(module):
2022
if os.path.isfile(module):
2123
basedir = os.path.dirname(module)

pybake/dictfilesysteminterceptor.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
2+
13
import errno
24
import os
3-
from StringIO import StringIO
5+
from io import BytesIO
46

5-
from pybake.filesysteminterceptor import FileSystemInterceptor
7+
from pybake.filesysteminterceptor import FileSystemInterceptor, BUILTINS_OPEN
68

79

810
class DictFileSystemInterceptor(FileSystemInterceptor):
911
def __init__(self, reader):
1012
intercept_list = [
11-
'__builtin__.open',
13+
BUILTINS_OPEN,
1214
'os.path.isfile',
1315
'os.path.isdir',
1416
'os.path.exists',
@@ -21,10 +23,13 @@ def __init__(self, reader):
2123
self._reader = reader
2224
self._fileno = FileNo()
2325

24-
def _builtin_open(self, path, mode='r'):
26+
def _builtins_open(self, *args, **kwargs):
27+
return self._builtin_open(*args, **kwargs)
28+
29+
def _builtin_open(self, path, mode='r', *args, **kwargs):
2530
tpath = self._reader.tpath(path)
2631
if tpath is None:
27-
return self._oldhooks['__builtin__.open'](path, mode)
32+
return self._oldhooks[BUILTINS_OPEN](path, mode, *args, **kwargs)
2833
if 'w' in mode:
2934
raise IOError(errno.EROFS, 'Read-only file system', path)
3035
content = self._reader.read(tpath)
@@ -119,11 +124,11 @@ def close_path(self, path):
119124
pass
120125

121126

122-
class FrozenFile(StringIO):
127+
class FrozenFile(BytesIO):
123128
def __init__(self, fileno, path, content):
124129
self._fileno = fileno
125130
self._path = path
126-
StringIO.__init__(self, content)
131+
BytesIO.__init__(self, content)
127132

128133
def __enter__(self):
129134
return self

0 commit comments

Comments
 (0)