Skip to content

Commit be97cc6

Browse files
committed
support --freeze-reqs close #22
1 parent 9e131ce commit be97cc6

File tree

7 files changed

+177
-5
lines changed

7 files changed

+177
-5
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ So, what could `zipapps` be?
2222
2. a `single-file virtual environment`(portable site-packages)
2323
3. a `dependences installer`
2424
4. a `set of import-path`
25+
5. a `pip freezing toolkit`
2526

2627
> PS: The pyz extension can be modified to any character you want, such as `.py`.
2728
@@ -201,7 +202,13 @@ Details:
201202
1. A file path needed and `-` means stdout.
202203
24. `--load-config`
203204
1. Load zipapps build args from a JSON file.
204-
25. all the other (or `unknown`) args will be used by `pip install`
205+
25. `--freeze-reqs`
206+
1. Freeze package versions of pip args with venv, output to the given file path.
207+
1. `-` equals to `stdout`
208+
2. logs will be redirect to `stderr`
209+
2. It tasks time for: init venv + pip install + pip freeze
210+
1. work folder is `tempfile.TemporaryDirectory`, prefix='zipapps_'
211+
26. all the other (or `unknown`) args will be used by `pip install`
205212
1. such as `-r requirements.txt`
206213
2. such as `bottle aiohttp`
207214
3. the `pip_args` arg of `zipapps.create_app`

changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
- 2022.04.27
66
- handle PermissionError for chmod
77
- support `--dump-config` and `--load-config` #24 fixed
8+
- support `--freeze-reqs` close #22
9+
- Freeze package versions of pip args with venv, output to the given file path.
10+
- `-` equals to `stdout`
11+
- logs will be redirect to `stderr`
12+
- It tasks time for: init venv + pip install + pip freeze
13+
- the work folder is `tempfile.TemporaryDirectory`, prefix='zipapps_'
814
- support clear self pyz after running fix #21
915
- refactor environment variables template and interval variables(with string.Template) #23
1016
- change TEMP/HOME/SELF prefixes with $TEMP/$HOME/$SELF

test_utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ def _clean_paths():
3939

4040
def test_create_app_function():
4141

42+
# test --freeze-reqs
43+
_clean_paths()
44+
output = subprocess.check_output(
45+
[sys.executable, '-m', 'zipapps', '--freeze-reqs', '-', 'six==1.15.0'])
46+
assert output.strip() == b'six==1.15.0', output
47+
4248
# test `--dump-config` and `--load-config`
4349
_clean_paths()
4450
output, _ = subprocess.Popen(
@@ -232,7 +238,7 @@ def test_create_app_function():
232238
# files not be set by includes arg
233239
assert b'Traceback' in stderr_output, 'test includes failed'
234240
app_path = create_app(
235-
includes='./zipapps/_entry_point.py.template,./zipapps/main.py')
241+
includes='./zipapps/entry_point.py.template,./zipapps/main.py')
236242
_, stderr_output = subprocess.Popen(
237243
[sys.executable, str(app_path), '-c', 'import main'],
238244
stderr=subprocess.PIPE,

zipapps/__main__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,13 @@ def main():
250250
dest='load_config',
251251
help='Load zipapps build args from a JSON file.',
252252
)
253+
parser.add_argument(
254+
'--freeze-reqs',
255+
default='',
256+
dest='freeze',
257+
help='Freeze package versions of pip args with venv,'
258+
' output to the given file path.',
259+
)
253260
if len(sys.argv) == 1:
254261
return parser.print_help()
255262
args, pip_args = parser.parse_known_args()
@@ -258,6 +265,11 @@ def main():
258265
for path in args.activate.split(','):
259266
activate(path)
260267
return
268+
if args.freeze:
269+
from .freezing import FreezeTool
270+
with FreezeTool(args.freeze, pip_args) as ft:
271+
ft.run()
272+
return
261273
if args.load_config:
262274
import json
263275
with open(args.load_config, 'r') as f:
File renamed without changes.

zipapps/freezing.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import os
2+
import re
3+
import subprocess
4+
import sys
5+
import tempfile
6+
import time
7+
import venv
8+
from pathlib import Path
9+
10+
11+
def ttime(timestamp=None,
12+
tzone=int(-time.timezone / 3600),
13+
fail="",
14+
fmt="%Y-%m-%d %H:%M:%S"):
15+
fix_tz = tzone * 3600
16+
if timestamp is None:
17+
timestamp = time.time()
18+
else:
19+
timestamp = float(timestamp)
20+
if 1e12 <= timestamp < 1e13:
21+
# Compatible timestamp with 13-digit milliseconds
22+
timestamp = timestamp / 1000
23+
try:
24+
timestamp = time.time() if timestamp is None else timestamp
25+
return time.strftime(fmt, time.gmtime(timestamp + fix_tz))
26+
except Exception:
27+
return fail
28+
29+
30+
class FreezeTool(object):
31+
VENV_NAME = 'zipapps_venv'
32+
# not stable
33+
FASTER_PREPARE_PIP = False
34+
35+
def __init__(self, output: str, pip_args: list):
36+
if not pip_args:
37+
raise RuntimeError('pip args is null')
38+
self.temp_dir: tempfile.TemporaryDirectory = None
39+
self.pip_args = pip_args
40+
self.output_path = output
41+
42+
def log(self, msg, flush=False):
43+
_msg = f'{ttime()} | {msg}'
44+
print(_msg, file=sys.stderr, flush=flush)
45+
46+
def run(self):
47+
self.log(
48+
'All the logs will be redirected to stderr to ensure the output is stdout.'
49+
)
50+
self.temp_dir = tempfile.TemporaryDirectory(prefix='zipapps_')
51+
self.temp_path = Path(self.temp_dir.name)
52+
self.log(
53+
f'Start mkdir temp folder: {self.temp_path.absolute()}, exist={self.temp_path.is_dir()}'
54+
)
55+
self.install_env()
56+
output = self.install_packages()
57+
self.freeze_requirements(output)
58+
return output
59+
60+
def install_env(self):
61+
venv_path = self.temp_path / self.VENV_NAME
62+
self.log(f'Initial venv with pip: {venv_path.absolute()}')
63+
if self.FASTER_PREPARE_PIP:
64+
venv.create(env_dir=venv_path, with_pip=False)
65+
import shutil
66+
67+
import pip
68+
pip_dir = Path(pip.__file__).parent
69+
if os.name == 'nt':
70+
target = venv_path / 'Lib' / 'site-packages' / 'pip'
71+
else:
72+
pyv = 'python%d.%d' % sys.version_info[:2]
73+
target = venv_path / 'lib' / pyv / 'site-packages' / 'pip'
74+
shutil.copytree(pip_dir, target)
75+
else:
76+
venv.create(env_dir=venv_path, with_pip=True)
77+
if not venv_path.is_dir():
78+
raise FileNotFoundError(str(venv_path))
79+
80+
def install_packages(self):
81+
if os.name == 'nt':
82+
python_path = self.temp_path / self.VENV_NAME / 'Scripts' / 'python.exe'
83+
else:
84+
python_path = self.temp_path / self.VENV_NAME / 'bin' / 'python'
85+
args = [
86+
str(python_path.absolute()),
87+
'-m',
88+
'pip',
89+
'install',
90+
] + self.pip_args
91+
self.log(f'Install packages in venv: {args}\n{"-" * 30}')
92+
with subprocess.Popen(args, stdout=subprocess.PIPE) as proc:
93+
for line in proc.stdout:
94+
try:
95+
line = line.decode()
96+
except ValueError:
97+
line = line.decode('utf-8', 'ignore')
98+
print(line.rstrip(), file=sys.stderr, flush=True)
99+
args = [str(python_path.absolute()), '-m', 'pip', 'freeze']
100+
print("-" * 30, file=sys.stderr)
101+
self.log(f'Freeze packages in venv: {args}')
102+
output = subprocess.check_output(args)
103+
try:
104+
result = output.decode('utf-8')
105+
except ValueError:
106+
result = output.decode()
107+
result = re.sub('(\n|\r)+', '\n', result).strip()
108+
return result
109+
110+
def freeze_requirements(self, output):
111+
print(output, flush=True)
112+
if self.output_path != '-':
113+
with open(self.output_path, 'w', encoding='utf-8') as f:
114+
print(output, file=f, flush=True)
115+
116+
def remove_env(self):
117+
if self.temp_dir and self.temp_path.is_dir():
118+
self.temp_dir.cleanup()
119+
self.log(
120+
f'Delete temp folder: {self.temp_path.absolute()}, exist={self.temp_path.is_dir()}'
121+
)
122+
123+
def __del__(self):
124+
self.remove_env()
125+
126+
def __enter__(self):
127+
return self
128+
129+
def __exit__(self, *e):
130+
self.remove_env()
131+
132+
133+
def test():
134+
with FreezeTool('-', ['six==1.15.0']) as ft:
135+
result = ft.run()
136+
# print(result)
137+
assert result == 'six==1.15.0'
138+
139+
140+
if __name__ == "__main__":
141+
test()

zipapps/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def __init__(
9090
9191
:param includes: The given paths will be copied to `cache_path` while packaging, which can be used while running. The path strings will be splited by ",". such as `my_package_dir,my_module.py,my_config.json`, often used for libs not from `pypi` or some special config files, defaults to ''
9292
:type includes: str, optional
93-
:param cache_path: if not set, will use TemporaryDirectory, defaults to None
93+
:param cache_path: if not set, will use TemporaryDirectory, prefix='zipapps_', defaults to None
9494
:type cache_path: str, optional
9595
:param main: The entry point function of the application, the `valid format` is: `package.module:function` `package.module` `module:function` `package`, defaults to ''
9696
:type main: str, optional
@@ -188,7 +188,7 @@ def ensure_args(self):
188188
if self.cache_path:
189189
self._cache_path = Path(self.cache_path)
190190
else:
191-
self._tmp_dir = tempfile.TemporaryDirectory()
191+
self._tmp_dir = tempfile.TemporaryDirectory(prefix='zipapps_')
192192
self._cache_path = Path(self._tmp_dir.name)
193193
if not self.unzip_path:
194194
if self.lazy_install:
@@ -341,7 +341,7 @@ def prepare_active_zipapps(self):
341341
}
342342
for k, v in self.ENV_ALIAS.items():
343343
kwargs[f'{k}_env'] = repr(v)
344-
code = get_data('zipapps', '_entry_point.py.template').decode('u8')
344+
code = get_data('zipapps', 'entry_point.py.template').decode('u8')
345345
(self._cache_path / '__main__.py').write_text(code.format(**kwargs))
346346

347347
code = get_data('zipapps', 'ensure_zipapps.py.template').decode('u8')

0 commit comments

Comments
 (0)