venvpool 16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- venvpool/__init__.py +868 -0
- venvpool/motivate.py +5 -0
- venvpool-16.dist-info/METADATA +45 -0
- venvpool-16.dist-info/RECORD +7 -0
- venvpool-16.dist-info/WHEEL +5 -0
- venvpool-16.dist-info/entry_points.txt +2 -0
- venvpool-16.dist-info/top_level.txt +1 -0
venvpool/__init__.py
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import os, sys
|
|
2
|
+
|
|
3
|
+
class MainModule:
|
|
4
|
+
|
|
5
|
+
letter = None
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
args = sys.argv
|
|
9
|
+
if 1 < len(args):
|
|
10
|
+
a = args[1]
|
|
11
|
+
if 1 < len(a) and '-' == a[0]:
|
|
12
|
+
self.letter = a[1]
|
|
13
|
+
|
|
14
|
+
def shortcut(self, cls):
|
|
15
|
+
if self.letter == cls.letter:
|
|
16
|
+
sys.exit(cls.main())
|
|
17
|
+
|
|
18
|
+
def end(self):
|
|
19
|
+
Activate.main()
|
|
20
|
+
|
|
21
|
+
if ('__main__' == __name__): # Hide from scriptregex.
|
|
22
|
+
_module = MainModule
|
|
23
|
+
else:
|
|
24
|
+
class _module:
|
|
25
|
+
def shortcut(self, cls):
|
|
26
|
+
pass
|
|
27
|
+
def end(self):
|
|
28
|
+
pass
|
|
29
|
+
_module = _module()
|
|
30
|
+
propersubcommands = []
|
|
31
|
+
|
|
32
|
+
def _shortcut(cls):
|
|
33
|
+
_module.shortcut(cls)
|
|
34
|
+
propersubcommands.append(cls)
|
|
35
|
+
return cls
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
m = MainModule()
|
|
39
|
+
for cls in propersubcommands:
|
|
40
|
+
m.shortcut(cls)
|
|
41
|
+
m.end()
|
|
42
|
+
|
|
43
|
+
def _decompress(paths):
|
|
44
|
+
i = iter(paths)
|
|
45
|
+
prefix = next(i)
|
|
46
|
+
try:
|
|
47
|
+
while True:
|
|
48
|
+
yield prefix + next(i)
|
|
49
|
+
except StopIteration:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@_shortcut
|
|
53
|
+
class Execute:
|
|
54
|
+
|
|
55
|
+
help = 'augment interpreter environment and exec module for internal use only'
|
|
56
|
+
letter = 'X'
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def main(cls):
|
|
60
|
+
import runpy # A few thousandths.
|
|
61
|
+
assert '-X' == sys.argv.pop(1)
|
|
62
|
+
sys.path[0] = bindir = os.path.dirname(sys.executable)
|
|
63
|
+
try:
|
|
64
|
+
envpath = os.environ['PATH']
|
|
65
|
+
except KeyError:
|
|
66
|
+
envpath = bindir
|
|
67
|
+
else:
|
|
68
|
+
envpath = bindir + os.pathsep + envpath
|
|
69
|
+
os.environ['PATH'] = envpath
|
|
70
|
+
sys.path[slice(*[cls._insertionpoint(sys.path)] * 2)] = _decompress(sys.argv.pop(1).split(os.pathsep))
|
|
71
|
+
exec(sys.argv.pop(1))
|
|
72
|
+
runpy.run_module(sys.argv.pop(1), run_name = '__main__', alter_sys = True)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _insertionpoint(v, suffix = os.sep + 'site-packages'):
|
|
76
|
+
i = n = len(v)
|
|
77
|
+
while not v[i - 1].endswith(suffix):
|
|
78
|
+
i -= 1
|
|
79
|
+
if not i:
|
|
80
|
+
return n
|
|
81
|
+
while i and v[i - 1].endswith(suffix):
|
|
82
|
+
i -= 1
|
|
83
|
+
return i
|
|
84
|
+
|
|
85
|
+
from collections import OrderedDict
|
|
86
|
+
from contextlib import contextmanager
|
|
87
|
+
from random import shuffle # XXX: Expensive?
|
|
88
|
+
from stat import S_IXUSR, S_IXGRP, S_IXOTH
|
|
89
|
+
from tempfile import mkdtemp, mkstemp
|
|
90
|
+
import errno, logging, operator, re, shutil # XXX: Still expensive?
|
|
91
|
+
|
|
92
|
+
class subprocess:
|
|
93
|
+
|
|
94
|
+
def __getattr__(self, name):
|
|
95
|
+
import subprocess # Up to a hundredth.
|
|
96
|
+
return getattr(subprocess, name)
|
|
97
|
+
|
|
98
|
+
log = logging.getLogger(__name__)
|
|
99
|
+
chainrelpath = os.path.join('venvpool', 'chain.py')
|
|
100
|
+
dotpy = '.py'
|
|
101
|
+
'Python source file extension including dot.'
|
|
102
|
+
executablebits = S_IXUSR | S_IXGRP | S_IXOTH
|
|
103
|
+
oserrors = {code: type(name, (OSError,), {}) for code, name in errno.errorcode.items()}
|
|
104
|
+
pooldir = os.path.join(os.environ.get('XDG_CACHE_HOME') or os.path.join(os.path.expanduser('~'), '.cache'), 'venvpool')
|
|
105
|
+
scriptregex, = (r"^if\s+(?:__name__\s*==\s*{main}|{main}\s*==\s*__name__)\s*:\s*$".format(**locals()) for main in ['''(?:'__main__'|"__main__")'''])
|
|
106
|
+
try:
|
|
107
|
+
set_inheritable = os.set_inheritable
|
|
108
|
+
except AttributeError:
|
|
109
|
+
from fcntl import fcntl, FD_CLOEXEC, F_GETFD, F_SETFD
|
|
110
|
+
def set_inheritable(h, inherit):
|
|
111
|
+
assert inherit
|
|
112
|
+
fcntl(h, F_SETFD, fcntl(h, F_GETFD) & ~FD_CLOEXEC)
|
|
113
|
+
subprocess = subprocess()
|
|
114
|
+
userbin = os.path.join(os.path.expanduser('~'), '.local', 'bin')
|
|
115
|
+
|
|
116
|
+
def _osop(f, *args, **kwargs):
|
|
117
|
+
try:
|
|
118
|
+
return f(*args, **kwargs)
|
|
119
|
+
except OSError as e:
|
|
120
|
+
raise oserrors[e.errno](*e.args)
|
|
121
|
+
|
|
122
|
+
@contextmanager
|
|
123
|
+
def TemporaryDirectory():
|
|
124
|
+
tempdir = mkdtemp()
|
|
125
|
+
try:
|
|
126
|
+
yield tempdir
|
|
127
|
+
finally:
|
|
128
|
+
shutil.rmtree(tempdir)
|
|
129
|
+
|
|
130
|
+
@contextmanager
|
|
131
|
+
def _onerror(f):
|
|
132
|
+
try:
|
|
133
|
+
yield
|
|
134
|
+
except:
|
|
135
|
+
f()
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
class Pip:
|
|
139
|
+
|
|
140
|
+
envpatch = dict(PYTHON_KEYRING_BACKEND = 'keyring.backends.null.Keyring')
|
|
141
|
+
|
|
142
|
+
def __init__(self, pippath):
|
|
143
|
+
self.pippath = pippath
|
|
144
|
+
|
|
145
|
+
def pipinstall(self, command):
|
|
146
|
+
subprocess.check_call([self.pippath, 'install'] + command, env = dict(os.environ, **self.envpatch), stdout = sys.stderr)
|
|
147
|
+
|
|
148
|
+
def listorempty(d, xform = lambda p: p):
|
|
149
|
+
try:
|
|
150
|
+
names = _osop(os.listdir, d)
|
|
151
|
+
except oserrors[errno.ENOENT]:
|
|
152
|
+
return []
|
|
153
|
+
return [xform(os.path.join(d, n)) for n in sorted(names)]
|
|
154
|
+
|
|
155
|
+
class LockStateException(Exception): pass
|
|
156
|
+
|
|
157
|
+
class ReadLock:
|
|
158
|
+
|
|
159
|
+
def __init__(self, handle):
|
|
160
|
+
self.handle = handle
|
|
161
|
+
|
|
162
|
+
def unlock(self):
|
|
163
|
+
try:
|
|
164
|
+
_osop(os.close, self.handle)
|
|
165
|
+
except oserrors[errno.EBADF]:
|
|
166
|
+
raise LockStateException
|
|
167
|
+
|
|
168
|
+
def _idempotentunlink(path):
|
|
169
|
+
try:
|
|
170
|
+
_osop(os.remove, path)
|
|
171
|
+
return True
|
|
172
|
+
except oserrors[errno.ENOENT]:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
def _chunkify(n, v):
|
|
176
|
+
i = iter(v)
|
|
177
|
+
while True:
|
|
178
|
+
chunk = []
|
|
179
|
+
for _ in range(n):
|
|
180
|
+
try:
|
|
181
|
+
x = next(i)
|
|
182
|
+
except StopIteration:
|
|
183
|
+
if chunk:
|
|
184
|
+
yield chunk
|
|
185
|
+
return
|
|
186
|
+
chunk.append(x)
|
|
187
|
+
yield chunk
|
|
188
|
+
|
|
189
|
+
if '/' == os.sep:
|
|
190
|
+
def _swept(readlocks):
|
|
191
|
+
for chunk in _chunkify(1000, readlocks):
|
|
192
|
+
# Check stderr instead of returncode for errors:
|
|
193
|
+
stdout, stderr = subprocess.Popen(['lsof', '-t'] + chunk, stdout = subprocess.PIPE, stderr = subprocess.PIPE).communicate()
|
|
194
|
+
if not stderr and not stdout:
|
|
195
|
+
for readlock in chunk:
|
|
196
|
+
if _idempotentunlink(readlock):
|
|
197
|
+
yield readlock
|
|
198
|
+
else:
|
|
199
|
+
def _swept(readlocks): # TODO: Untested!
|
|
200
|
+
for readlock in readlocks:
|
|
201
|
+
try:
|
|
202
|
+
if _idempotentunlink(readlock):
|
|
203
|
+
yield readlock
|
|
204
|
+
except oserrors[errno.EACCES]:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
def _compress(paths, splitchar = os.sep):
|
|
208
|
+
if not paths:
|
|
209
|
+
yield '-' # Avoid empty word in command line.
|
|
210
|
+
return
|
|
211
|
+
firstdiff = -1
|
|
212
|
+
for firstdiff, chars in enumerate(zip(*paths)):
|
|
213
|
+
if 1 != len(set(chars)):
|
|
214
|
+
break
|
|
215
|
+
else:
|
|
216
|
+
firstdiff += 1
|
|
217
|
+
while firstdiff and paths[0][firstdiff - 1] != splitchar:
|
|
218
|
+
firstdiff -= 1
|
|
219
|
+
yield paths[0][:firstdiff]
|
|
220
|
+
for p in paths:
|
|
221
|
+
yield p[firstdiff:]
|
|
222
|
+
|
|
223
|
+
class SharedDir(object):
|
|
224
|
+
|
|
225
|
+
def __init__(self, dirpath):
|
|
226
|
+
self.readlocks = os.path.join(dirpath, 'readlocks')
|
|
227
|
+
|
|
228
|
+
def _sweep(self):
|
|
229
|
+
paths = list(_compress(list(_swept(listorempty(self.readlocks)))))
|
|
230
|
+
n = len(paths)
|
|
231
|
+
if 2 == n:
|
|
232
|
+
log.debug("Swept: %s%s", *paths)
|
|
233
|
+
elif 2 < n:
|
|
234
|
+
log.debug("Swept: %s{%s}", paths[0], ','.join(paths[1:]))
|
|
235
|
+
|
|
236
|
+
def trywritelock(self):
|
|
237
|
+
self._sweep()
|
|
238
|
+
try:
|
|
239
|
+
_osop(os.rmdir, self.readlocks)
|
|
240
|
+
return True
|
|
241
|
+
except (oserrors[errno.ENOENT], oserrors[errno.ENOTEMPTY]):
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
def createortrywritelock(self):
|
|
245
|
+
try:
|
|
246
|
+
_osop(os.mkdir, os.path.dirname(self.readlocks))
|
|
247
|
+
return True
|
|
248
|
+
except oserrors[errno.EEXIST]:
|
|
249
|
+
return self.trywritelock()
|
|
250
|
+
|
|
251
|
+
def writeunlock(self):
|
|
252
|
+
try:
|
|
253
|
+
_osop(os.mkdir, self.readlocks)
|
|
254
|
+
except oserrors[errno.EEXIST]:
|
|
255
|
+
raise LockStateException
|
|
256
|
+
|
|
257
|
+
def tryreadlock(self):
|
|
258
|
+
try:
|
|
259
|
+
h = _osop(mkstemp, dir = self.readlocks, prefix = 'lock')[0]
|
|
260
|
+
set_inheritable(h, True)
|
|
261
|
+
return ReadLock(h)
|
|
262
|
+
except oserrors[errno.ENOENT]:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
def safe_name(name):
|
|
266
|
+
return re.sub('[^A-Za-z0-9.]+', '-', name)
|
|
267
|
+
|
|
268
|
+
def to_filename(name):
|
|
269
|
+
return name.replace('-', '_')
|
|
270
|
+
|
|
271
|
+
class Venv(SharedDir):
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def _safewhich(name):
|
|
275
|
+
poolprefix = pooldir + os.sep
|
|
276
|
+
for bindir in os.environ['PATH'].split(os.pathsep):
|
|
277
|
+
if bindir.startswith(poolprefix) or not os.path.isabs(bindir): # XXX: Also exclude other venvs?
|
|
278
|
+
log.debug("Ignore bin directory: %s", bindir)
|
|
279
|
+
else:
|
|
280
|
+
path = os.path.join(bindir, name)
|
|
281
|
+
if os.path.exists(path):
|
|
282
|
+
return path
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def site_packages(self):
|
|
286
|
+
libpath = os.path.join(self.venvpath, 'lib')
|
|
287
|
+
pyname, = (n for n in os.listdir(libpath) if n.startswith('python'))
|
|
288
|
+
return os.path.join(libpath, pyname, 'site-packages')
|
|
289
|
+
|
|
290
|
+
def __init__(self, venvpath):
|
|
291
|
+
super(Venv, self).__init__(venvpath)
|
|
292
|
+
self.venvpath = venvpath
|
|
293
|
+
|
|
294
|
+
def create(self, pyversion):
|
|
295
|
+
def isolated(*command):
|
|
296
|
+
subprocess.check_call(command, cwd = tempdir, stdout = sys.stderr)
|
|
297
|
+
executable = self._safewhich("python%s" % pyversion)
|
|
298
|
+
absvenvpath = os.path.abspath(self.venvpath) # XXX: Safe when venvpath has symlinks?
|
|
299
|
+
with TemporaryDirectory() as tempdir:
|
|
300
|
+
if pyversion < 3:
|
|
301
|
+
isolated('virtualenv', '-p', executable, absvenvpath)
|
|
302
|
+
else:
|
|
303
|
+
isolated(executable, '-m', 'venv', absvenvpath)
|
|
304
|
+
isolated(os.path.join(absvenvpath, 'bin', 'pip'), 'install', '--upgrade', 'pip', 'setuptools', 'wheel')
|
|
305
|
+
chainpath = os.path.join(self.site_packages, chainrelpath)
|
|
306
|
+
os.mkdir(os.path.dirname(chainpath))
|
|
307
|
+
with open(chainpath, 'w') as f:
|
|
308
|
+
f.write('''import os, sys
|
|
309
|
+
v = [os.path.join(os.path.dirname(sys.executable), sys.argv[1])] + sys.argv[2:]
|
|
310
|
+
os.execv(v[0], v)
|
|
311
|
+
''')
|
|
312
|
+
|
|
313
|
+
def delete(self, label = 'transient'):
|
|
314
|
+
log.debug("Delete %s venv: %s", label, self.venvpath)
|
|
315
|
+
shutil.rmtree(self.venvpath)
|
|
316
|
+
|
|
317
|
+
def programpath(self, name):
|
|
318
|
+
return os.path.join(self.venvpath, 'bin', name)
|
|
319
|
+
|
|
320
|
+
def install(self, args):
|
|
321
|
+
log.debug("Install: %s", ' '.join(args))
|
|
322
|
+
if args:
|
|
323
|
+
Pip(self.programpath('pip')).pipinstall(args)
|
|
324
|
+
|
|
325
|
+
def compatible(self, installdeps):
|
|
326
|
+
for r in installdeps.pypireqs:
|
|
327
|
+
version = self._reqversionornone(r.namepart)
|
|
328
|
+
if version is None or not r.acceptversion(version):
|
|
329
|
+
return
|
|
330
|
+
log.debug("Found compatible venv: %s", self.venvpath)
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
def _reqversionornone(self, name):
|
|
334
|
+
patterns = [re.compile(format % nameregex) for nameregex in [re.escape(to_filename(safe_name(name)).lower())] for format in [
|
|
335
|
+
"^%s-(.+)[.]dist-info$",
|
|
336
|
+
"^%s-([^-]+).*[.]egg-info$"]]
|
|
337
|
+
for lowername in (n.lower() for n in os.listdir(self.site_packages)):
|
|
338
|
+
for p in patterns:
|
|
339
|
+
m = p.search(lowername)
|
|
340
|
+
if m is not None:
|
|
341
|
+
return m.group(1)
|
|
342
|
+
|
|
343
|
+
def run(self, mode, localreqs, task, scriptargs, **kwargs):
|
|
344
|
+
try:
|
|
345
|
+
patch = task.patch
|
|
346
|
+
module = task.module
|
|
347
|
+
except AttributeError:
|
|
348
|
+
patch = '#'
|
|
349
|
+
module = task
|
|
350
|
+
argv = [os.path.join(self.venvpath, 'bin', 'python'), _stripc(__file__), '-X', os.pathsep.join(_compress(localreqs)), patch, module] + scriptargs
|
|
351
|
+
if 'call' == mode:
|
|
352
|
+
return subprocess.call(argv, **kwargs)
|
|
353
|
+
if 'check_call' == mode:
|
|
354
|
+
return subprocess.check_call(argv, **kwargs)
|
|
355
|
+
if 'check_output' == mode:
|
|
356
|
+
return subprocess.check_output(argv, **kwargs)
|
|
357
|
+
if 'exec' == mode:
|
|
358
|
+
os.execv(argv[0], argv, **kwargs)
|
|
359
|
+
raise ValueError(mode)
|
|
360
|
+
|
|
361
|
+
class Task:
|
|
362
|
+
|
|
363
|
+
def __init__(self, patch, module):
|
|
364
|
+
self.patch = patch
|
|
365
|
+
self.module = module
|
|
366
|
+
|
|
367
|
+
def _stripc(path):
|
|
368
|
+
return path[:-1] if 'c' == path[-1] else path
|
|
369
|
+
|
|
370
|
+
class Pool:
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def versiondir(self):
|
|
374
|
+
return os.path.join(pooldir, str(self.pyversion))
|
|
375
|
+
|
|
376
|
+
def __init__(self, pyversion):
|
|
377
|
+
self.readonlyortransient = {
|
|
378
|
+
False: self.readonly,
|
|
379
|
+
True: self._transient,
|
|
380
|
+
}
|
|
381
|
+
self.readonlyorreadwrite = {
|
|
382
|
+
False: self.readonly,
|
|
383
|
+
True: self.readwrite,
|
|
384
|
+
}
|
|
385
|
+
self.pyversion = pyversion
|
|
386
|
+
|
|
387
|
+
def _newvenv(self, installdeps):
|
|
388
|
+
log.info('Create new venv.')
|
|
389
|
+
try:
|
|
390
|
+
_osop(os.makedirs, self.versiondir)
|
|
391
|
+
except oserrors[errno.EEXIST]:
|
|
392
|
+
pass
|
|
393
|
+
venv = Venv(mkdtemp(dir = self.versiondir, prefix = 'venv'))
|
|
394
|
+
with _onerror(venv.delete):
|
|
395
|
+
venv.create(self.pyversion)
|
|
396
|
+
installdeps.invoke(venv)
|
|
397
|
+
assert venv.compatible(installdeps) # Bug if not.
|
|
398
|
+
return venv
|
|
399
|
+
|
|
400
|
+
def _lockcompatiblevenv(self, trylock, installdeps):
|
|
401
|
+
venvs = listorempty(self.versiondir, Venv)
|
|
402
|
+
shuffle(venvs)
|
|
403
|
+
for venv in venvs:
|
|
404
|
+
lock = trylock(venv)
|
|
405
|
+
if lock is not None:
|
|
406
|
+
with _onerror(lock.unlock):
|
|
407
|
+
if venv.compatible(installdeps): # TODO: Upgrade venv if it has a subset.
|
|
408
|
+
return venv, lock
|
|
409
|
+
lock.unlock()
|
|
410
|
+
|
|
411
|
+
@contextmanager
|
|
412
|
+
def _transient(self, installdeps):
|
|
413
|
+
venv = self._newvenv(installdeps)
|
|
414
|
+
try:
|
|
415
|
+
yield venv
|
|
416
|
+
finally:
|
|
417
|
+
venv.delete()
|
|
418
|
+
|
|
419
|
+
@contextmanager
|
|
420
|
+
def readonly(self, installdeps):
|
|
421
|
+
while True:
|
|
422
|
+
t = self._lockcompatiblevenv(Venv.tryreadlock, installdeps)
|
|
423
|
+
if t is not None:
|
|
424
|
+
venv, readlock = t
|
|
425
|
+
break
|
|
426
|
+
venv = self._newvenv(installdeps)
|
|
427
|
+
# XXX: Would it be possible to atomically convert write lock to read lock?
|
|
428
|
+
venv.writeunlock()
|
|
429
|
+
readlock = venv.tryreadlock()
|
|
430
|
+
if readlock is not None:
|
|
431
|
+
break
|
|
432
|
+
try:
|
|
433
|
+
yield venv
|
|
434
|
+
finally:
|
|
435
|
+
readlock.unlock()
|
|
436
|
+
|
|
437
|
+
@contextmanager
|
|
438
|
+
def readwrite(self, installdeps):
|
|
439
|
+
def trywritelock(venv):
|
|
440
|
+
if venv.trywritelock():
|
|
441
|
+
class WriteLock:
|
|
442
|
+
def unlock(self):
|
|
443
|
+
venv.writeunlock()
|
|
444
|
+
return WriteLock()
|
|
445
|
+
t = self._lockcompatiblevenv(trywritelock, installdeps)
|
|
446
|
+
if t is None:
|
|
447
|
+
venv = self._newvenv(installdeps)
|
|
448
|
+
else:
|
|
449
|
+
venv = t[0]
|
|
450
|
+
with _onerror(venv.writeunlock):
|
|
451
|
+
for dirpath, dirnames, filenames in os.walk(venv.venvpath):
|
|
452
|
+
for name in filenames:
|
|
453
|
+
p = os.path.join(dirpath, name)
|
|
454
|
+
if 1 != os.stat(p).st_nlink:
|
|
455
|
+
h, q = mkstemp(dir = dirpath)
|
|
456
|
+
os.close(h)
|
|
457
|
+
shutil.copy2(p, q)
|
|
458
|
+
os.remove(p) # Cross-platform.
|
|
459
|
+
os.rename(q, p)
|
|
460
|
+
try:
|
|
461
|
+
yield venv
|
|
462
|
+
finally:
|
|
463
|
+
venv.writeunlock()
|
|
464
|
+
|
|
465
|
+
class FastReq:
|
|
466
|
+
|
|
467
|
+
class Version:
|
|
468
|
+
|
|
469
|
+
def __init__(self, operator, splitversion):
|
|
470
|
+
self.operator = operator
|
|
471
|
+
self.splitversion = splitversion
|
|
472
|
+
|
|
473
|
+
def accept(self, splitversion):
|
|
474
|
+
def pad(v):
|
|
475
|
+
return v + [0] * (n - len(v))
|
|
476
|
+
versions = [splitversion, self.splitversion]
|
|
477
|
+
n = max(map(len, versions))
|
|
478
|
+
return self.operator(*map(pad, versions))
|
|
479
|
+
|
|
480
|
+
class DevSegment:
|
|
481
|
+
|
|
482
|
+
def __init__(self, n):
|
|
483
|
+
self.n = n
|
|
484
|
+
|
|
485
|
+
@classmethod
|
|
486
|
+
def _splitversion(cls, versionstr, devprefix = 'dev'):
|
|
487
|
+
return [cls.DevSegment(int(k[len(devprefix):])) if k.startswith(devprefix) else int(k) for k in versionstr.split('.')]
|
|
488
|
+
|
|
489
|
+
s = r'\s*'
|
|
490
|
+
nameregex = '[A-Za-z0-9._-]+' # Slightly more lenient than PEP 508.
|
|
491
|
+
extras = r"\[{s}(?:{nameregex}{s}(?:,{s}{nameregex}{s})*)?]".format(**locals())
|
|
492
|
+
version = "(<|<=|!=|==|>=|>){s}([0-9.]+(?:[.]dev[0-9]+)?)".format(**locals()) # Subset of features.
|
|
493
|
+
versionregex = "^{s}{version}{s}$".format(**locals())
|
|
494
|
+
getregex = "^{s}({nameregex}){s}({extras}{s})?({version}{s}(?:,{s}{version}{s})*)?$".format(**locals())
|
|
495
|
+
skipregex = "^{s}(?:#|$)".format(**locals())
|
|
496
|
+
del s, extras, version
|
|
497
|
+
operators = {
|
|
498
|
+
'<': operator.lt,
|
|
499
|
+
'<=': operator.le,
|
|
500
|
+
'!=': operator.ne,
|
|
501
|
+
'==': operator.eq,
|
|
502
|
+
'>=': operator.ge,
|
|
503
|
+
'>': operator.gt,
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
@classmethod
|
|
507
|
+
def parselines(cls, lines):
|
|
508
|
+
def g():
|
|
509
|
+
for line in lines:
|
|
510
|
+
if re.search(cls.skipregex, line) is not None:
|
|
511
|
+
continue
|
|
512
|
+
namepart, extras, versionspec = re.search(cls.getregex, line).groups()[:3]
|
|
513
|
+
extras = () if extras is None else tuple(sorted(set(re.findall(cls.nameregex, extras)))) # TODO LATER: Normalisation.
|
|
514
|
+
versions = []
|
|
515
|
+
reqstrversions = []
|
|
516
|
+
if versionspec is not None:
|
|
517
|
+
for onestr in versionspec.split(','):
|
|
518
|
+
operatorstr, versionstr = re.search(cls.versionregex, onestr).groups()
|
|
519
|
+
versions.append(cls.Version(cls.operators[operatorstr], cls._splitversion(versionstr)))
|
|
520
|
+
reqstrversions.append(operatorstr + versionstr)
|
|
521
|
+
yield cls(namepart, extras, versions, namepart + ("[%s]" % ','.join(extras) if extras else '') + ','.join(sorted(reqstrversions)))
|
|
522
|
+
return list(g())
|
|
523
|
+
|
|
524
|
+
def __init__(self, namepart, extras, versions, reqstr):
|
|
525
|
+
self.namepart = namepart
|
|
526
|
+
self.extras = extras
|
|
527
|
+
self.versions = versions
|
|
528
|
+
self.reqstr = reqstr
|
|
529
|
+
|
|
530
|
+
def acceptversion(self, versionstr):
|
|
531
|
+
splitversion = self._splitversion(versionstr)
|
|
532
|
+
return all(v.accept(splitversion) for v in self.versions)
|
|
533
|
+
|
|
534
|
+
class ParsedRequires:
|
|
535
|
+
|
|
536
|
+
parselines = staticmethod(FastReq.parselines)
|
|
537
|
+
|
|
538
|
+
def __init__(self, requires):
|
|
539
|
+
self.pypireqs = self.parselines(requires)
|
|
540
|
+
|
|
541
|
+
def invoke(self, venv):
|
|
542
|
+
venv.install([r.reqstr for r in self.pypireqs])
|
|
543
|
+
|
|
544
|
+
def poplocalreqs(self, workspace):
|
|
545
|
+
projectdirs = {}
|
|
546
|
+
for projectdir in (os.path.join(workspace, n) for n in os.listdir(workspace)):
|
|
547
|
+
if os.path.isdir(projectdir):
|
|
548
|
+
eggdirnames = [n for n in os.listdir(projectdir) if n.endswith('.egg-info')]
|
|
549
|
+
if 1 == len(eggdirnames):
|
|
550
|
+
with open(os.path.join(projectdir, eggdirnames[0], 'PKG-INFO'), encoding = 'utf-8') as f:
|
|
551
|
+
try:
|
|
552
|
+
parser
|
|
553
|
+
except NameError:
|
|
554
|
+
from email.parser import Parser
|
|
555
|
+
parser = Parser()
|
|
556
|
+
name = parser.parse(f)['Name']
|
|
557
|
+
if name is not None:
|
|
558
|
+
projectdirs[name] = projectdir
|
|
559
|
+
local = OrderedDict()
|
|
560
|
+
reqs = list(self.pypireqs)
|
|
561
|
+
del self.pypireqs[:]
|
|
562
|
+
while reqs:
|
|
563
|
+
nextreqs = []
|
|
564
|
+
for req in reqs:
|
|
565
|
+
projectdir = projectdirs.get(req.namepart)
|
|
566
|
+
if projectdir is None:
|
|
567
|
+
self.pypireqs.append(req)
|
|
568
|
+
elif projectdir not in local:
|
|
569
|
+
requirementslines = _getrequirementslinesornone(projectdir, req.extras)
|
|
570
|
+
if requirementslines is None:
|
|
571
|
+
raise NoRequirementsFoundException("%s[%s]" % (projectdir, ','.join(req.extras)))
|
|
572
|
+
nextreqs.extend(self.parselines(requirementslines))
|
|
573
|
+
local[projectdir] = None
|
|
574
|
+
reqs = nextreqs
|
|
575
|
+
return list(local)
|
|
576
|
+
|
|
577
|
+
def _getrequirementslinesornone(projectdir, extras):
|
|
578
|
+
def linesornone(acceptnull, *names):
|
|
579
|
+
path = os.path.join(projectdir, *names)
|
|
580
|
+
if os.path.exists(path):
|
|
581
|
+
log.debug("Found requirements: %s", path)
|
|
582
|
+
with open(path) as f:
|
|
583
|
+
return f.read().splitlines()
|
|
584
|
+
if acceptnull:
|
|
585
|
+
log.debug("Null requirements: %s", path)
|
|
586
|
+
return []
|
|
587
|
+
if extras:
|
|
588
|
+
log.warning("Ignore extras %s in: %s", ','.join(extras), projectdir) # TODO: Find extra requirements as well.
|
|
589
|
+
v = linesornone(False, 'requirements.txt')
|
|
590
|
+
if v is not None:
|
|
591
|
+
return v
|
|
592
|
+
names = [name for name in os.listdir(projectdir) if name.endswith('.egg-info')]
|
|
593
|
+
if names:
|
|
594
|
+
name, = names # XXX: Could there legitimately be multiple?
|
|
595
|
+
return linesornone(True, name, 'requires.txt')
|
|
596
|
+
|
|
597
|
+
def initlogging():
|
|
598
|
+
'Initialise the logging module to send debug (and higher levels) to stderr.'
|
|
599
|
+
logging.basicConfig(format = "%(asctime)s %(levelname)s %(message)s", level = logging.DEBUG)
|
|
600
|
+
|
|
601
|
+
class ParserCommand:
|
|
602
|
+
|
|
603
|
+
@classmethod
|
|
604
|
+
def main(cls):
|
|
605
|
+
from argparse import ArgumentParser # Two or three hundredths.
|
|
606
|
+
initlogging()
|
|
607
|
+
parser = ArgumentParser(description = cls.help)
|
|
608
|
+
if cls.letter is not None:
|
|
609
|
+
parser.add_argument('-' + cls.letter, action = 'store_true', help = 'select this subcommand')
|
|
610
|
+
cls.initparser(parser)
|
|
611
|
+
parser.add_argument('-v', action = 'store_true', help = 'show debug logging')
|
|
612
|
+
args = parser.parse_args()
|
|
613
|
+
if cls.letter is not None:
|
|
614
|
+
assert getattr(args, cls.letter)
|
|
615
|
+
if not args.v:
|
|
616
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
617
|
+
cls.mainimpl(args)
|
|
618
|
+
|
|
619
|
+
class NoRequirementsFoundException(Exception): pass
|
|
620
|
+
|
|
621
|
+
def _findrequirements(projectdir):
|
|
622
|
+
while True:
|
|
623
|
+
requirementslines = _getrequirementslinesornone(projectdir, ())
|
|
624
|
+
if requirementslines is not None:
|
|
625
|
+
return projectdir, requirementslines
|
|
626
|
+
parent = os.path.dirname(projectdir)
|
|
627
|
+
if parent == projectdir:
|
|
628
|
+
raise NoRequirementsFoundException
|
|
629
|
+
projectdir = parent
|
|
630
|
+
|
|
631
|
+
@_shortcut
|
|
632
|
+
class Launch(ParserCommand):
|
|
633
|
+
|
|
634
|
+
help = 'launch a script with the interpreter and requirements it needs'
|
|
635
|
+
letter = 'L'
|
|
636
|
+
|
|
637
|
+
@staticmethod
|
|
638
|
+
def initparser(parser):
|
|
639
|
+
parser.add_argument('--req', help = 'use the given requirement specifier only')
|
|
640
|
+
parser.add_argument('scriptpath', help = 'should be preceded by a -- arg')
|
|
641
|
+
parser.add_argument('scriptarg', nargs = '*', help = 'arguments for scriptpath')
|
|
642
|
+
|
|
643
|
+
@classmethod
|
|
644
|
+
def mainimpl(cls, args):
|
|
645
|
+
try:
|
|
646
|
+
dd = sys.argv.index('--')
|
|
647
|
+
except ValueError:
|
|
648
|
+
scriptpath = args.scriptpath
|
|
649
|
+
scriptargs = args.scriptarg
|
|
650
|
+
else:
|
|
651
|
+
scriptpath = sys.argv[dd + 1]
|
|
652
|
+
scriptargs = sys.argv[dd + 2:]
|
|
653
|
+
assert scriptpath.endswith(dotpy)
|
|
654
|
+
reqstr = args.req
|
|
655
|
+
if reqstr is None:
|
|
656
|
+
scriptpath = os.path.abspath(scriptpath) # XXX: Is abspath safe when scriptpath has symlinks?
|
|
657
|
+
projectdir, requirementslines = _findrequirements(os.path.dirname(scriptpath))
|
|
658
|
+
installdeps = ParsedRequires(requirementslines)
|
|
659
|
+
localreqs = installdeps.poplocalreqs(os.path.normpath(os.path.join(projectdir, '..')))
|
|
660
|
+
localreqs.insert(0, projectdir)
|
|
661
|
+
module = os.path.relpath(scriptpath[:-len(dotpy)], projectdir).replace(os.sep, '.')
|
|
662
|
+
else:
|
|
663
|
+
installdeps = ParsedRequires([reqstr])
|
|
664
|
+
localreqs = []
|
|
665
|
+
module = scriptpath[:-len(dotpy)].replace(os.sep, '.')
|
|
666
|
+
with Pool(sys.version_info.major).readonly(installdeps) as venv: # TODO: Likely to be major 2 when launching manually.
|
|
667
|
+
venv.run('exec', localreqs, module, scriptargs)
|
|
668
|
+
|
|
669
|
+
class Activate(ParserCommand):
|
|
670
|
+
|
|
671
|
+
help = 'create and maintain wrapper scripts'
|
|
672
|
+
letter = None
|
|
673
|
+
|
|
674
|
+
@staticmethod
|
|
675
|
+
def initparser(parser):
|
|
676
|
+
group = parser.add_mutually_exclusive_group()
|
|
677
|
+
for cls in propersubcommands:
|
|
678
|
+
group.add_argument('-' + cls.letter, action = 'store_true', help = 'subcommand to ' + cls.help)
|
|
679
|
+
parser.add_argument('--bin', help = 'custom scripts directory', default = userbin)
|
|
680
|
+
parser.add_argument('-f', action = 'store_true', help = 'overwrite existing scripts')
|
|
681
|
+
parser.add_argument('projectdir', nargs = '*', default = ['.'], help = 'projects to search for runnable modules')
|
|
682
|
+
|
|
683
|
+
@classmethod
|
|
684
|
+
def mainimpl(cls, args):
|
|
685
|
+
for projectdir in args.projectdir:
|
|
686
|
+
try:
|
|
687
|
+
cls._scan(_findrequirements(os.path.realpath(projectdir))[0], 3, args.f, args.bin) # XXX: Always 3?
|
|
688
|
+
except NoRequirementsFoundException:
|
|
689
|
+
log.exception("Skip: %s", projectdir)
|
|
690
|
+
|
|
691
|
+
@staticmethod
|
|
692
|
+
def _srcpaths(rootdir):
|
|
693
|
+
for dirpath, dirnames, filenames in os.walk(rootdir):
|
|
694
|
+
for name in filenames:
|
|
695
|
+
if name.endswith(dotpy):
|
|
696
|
+
path = os.path.join(dirpath, name)
|
|
697
|
+
with open(path) as f:
|
|
698
|
+
if re.search(scriptregex, f.read(), re.MULTILINE) is not None:
|
|
699
|
+
yield path
|
|
700
|
+
|
|
701
|
+
@classmethod
|
|
702
|
+
def _scan(cls, projectdir, pyversion, force, bindir):
|
|
703
|
+
for srcpath in cls._srcpaths(projectdir):
|
|
704
|
+
if not checkpath(projectdir, srcpath):
|
|
705
|
+
log.debug("Not a project source file: %s", srcpath)
|
|
706
|
+
continue
|
|
707
|
+
command = commandornone(srcpath)
|
|
708
|
+
if command is None:
|
|
709
|
+
log.debug("Bad source name: %s", srcpath)
|
|
710
|
+
continue
|
|
711
|
+
cls.install(force, command, pyversion, None, bindir, srcpath)
|
|
712
|
+
|
|
713
|
+
@staticmethod
|
|
714
|
+
def install(force, command, pyversion, reqstrornone, bindir, *words):
|
|
715
|
+
def allwords():
|
|
716
|
+
if reqstrornone is not None:
|
|
717
|
+
yield '--req'
|
|
718
|
+
yield reqstrornone
|
|
719
|
+
yield '--'
|
|
720
|
+
for w in words:
|
|
721
|
+
yield w
|
|
722
|
+
def identical():
|
|
723
|
+
mode = os.stat(scriptpath).st_mode
|
|
724
|
+
if mode | executablebits == mode:
|
|
725
|
+
with open(scriptpath) as f:
|
|
726
|
+
return f.read() == text
|
|
727
|
+
wordsrepr = ', '.join(map(repr, allwords()))
|
|
728
|
+
venvpoolpath = _stripc(os.path.realpath(__file__))
|
|
729
|
+
text = """#!/usr/bin/env python{pyversion}
|
|
730
|
+
import sys
|
|
731
|
+
sys.argv[1:1] = '-L', {wordsrepr}
|
|
732
|
+
__file__ = {venvpoolpath!r}
|
|
733
|
+
with open(__file__) as f: venvpoolsrc = f.read()
|
|
734
|
+
del sys, f
|
|
735
|
+
exec(venvpoolsrc)
|
|
736
|
+
""".format(**locals())
|
|
737
|
+
scriptpath = os.path.join(bindir, command) # TODO: Warn if shadowed.
|
|
738
|
+
if os.path.exists(scriptpath):
|
|
739
|
+
if identical():
|
|
740
|
+
log.debug("Identical: %s", scriptpath)
|
|
741
|
+
return
|
|
742
|
+
if not force:
|
|
743
|
+
log.info("Exists: %s", scriptpath)
|
|
744
|
+
return
|
|
745
|
+
log.info("Overwrite: %s", scriptpath)
|
|
746
|
+
else:
|
|
747
|
+
log.info("Create: %s", scriptpath)
|
|
748
|
+
with open(scriptpath, 'w') as f:
|
|
749
|
+
f.write(text)
|
|
750
|
+
os.chmod(scriptpath, os.stat(scriptpath).st_mode | executablebits)
|
|
751
|
+
|
|
752
|
+
def checkpath(projectdir, path):
|
|
753
|
+
while True:
|
|
754
|
+
path = os.path.dirname(path)
|
|
755
|
+
if path == projectdir:
|
|
756
|
+
return True
|
|
757
|
+
if not os.path.exists(os.path.join(path, '__init__.py')): # XXX: What about namespace packages?
|
|
758
|
+
break
|
|
759
|
+
|
|
760
|
+
def commandornone(srcpath):
|
|
761
|
+
name = os.path.basename(srcpath)
|
|
762
|
+
name = os.path.basename(os.path.dirname(srcpath)) if '__init__.py' == name else name[:-len(dotpy)]
|
|
763
|
+
if '-' not in name:
|
|
764
|
+
return name.replace('_', '-')
|
|
765
|
+
|
|
766
|
+
@_shortcut
|
|
767
|
+
class Compact(ParserCommand):
|
|
768
|
+
|
|
769
|
+
help = 'compact the pool of venvs'
|
|
770
|
+
letter = 'C'
|
|
771
|
+
|
|
772
|
+
@staticmethod
|
|
773
|
+
def initparser(parser):
|
|
774
|
+
pass
|
|
775
|
+
|
|
776
|
+
@classmethod
|
|
777
|
+
def mainimpl(cls, args): # XXX: Combine venvs with orthogonal dependencies?
|
|
778
|
+
venvtofreeze = {}
|
|
779
|
+
try:
|
|
780
|
+
for versiondir in listorempty(pooldir):
|
|
781
|
+
for venv in listorempty(versiondir, Venv):
|
|
782
|
+
if venv.trywritelock():
|
|
783
|
+
venvtofreeze[venv] = set(subprocess.check_output([venv.programpath('pip'), 'freeze'], universal_newlines = True).splitlines())
|
|
784
|
+
else:
|
|
785
|
+
log.debug("Busy: %s", venv.venvpath)
|
|
786
|
+
log.debug('Find redundant venvs.')
|
|
787
|
+
while True:
|
|
788
|
+
venv = cls._redundantvenv(venvtofreeze)
|
|
789
|
+
if venv is None:
|
|
790
|
+
break
|
|
791
|
+
venv.delete('redundant')
|
|
792
|
+
venvtofreeze.pop(venv)
|
|
793
|
+
cls._compactvenvs([l.venvpath for l in venvtofreeze])
|
|
794
|
+
finally:
|
|
795
|
+
for l in venvtofreeze:
|
|
796
|
+
l.writeunlock()
|
|
797
|
+
|
|
798
|
+
@staticmethod
|
|
799
|
+
def _redundantvenv(venvtofreeze):
|
|
800
|
+
for venv, freeze in venvtofreeze.items():
|
|
801
|
+
for othervenv, otherfreeze in venvtofreeze.items():
|
|
802
|
+
if venv != othervenv and os.path.dirname(venv.venvpath) == os.path.dirname(othervenv.venvpath) and freeze <= otherfreeze:
|
|
803
|
+
return venv
|
|
804
|
+
|
|
805
|
+
@staticmethod
|
|
806
|
+
def _compactvenvs(venvpaths):
|
|
807
|
+
log.info("Compact %s venvs.", len(venvpaths))
|
|
808
|
+
if venvpaths:
|
|
809
|
+
subprocess.check_call(['jdupes', '-Lrq'] + venvpaths)
|
|
810
|
+
log.info('Compaction complete.')
|
|
811
|
+
|
|
812
|
+
@_shortcut
|
|
813
|
+
class Unlock(ParserCommand):
|
|
814
|
+
|
|
815
|
+
help = 'release write locks on reboot'
|
|
816
|
+
letter = 'U'
|
|
817
|
+
|
|
818
|
+
@staticmethod
|
|
819
|
+
def initparser(parser):
|
|
820
|
+
pass
|
|
821
|
+
|
|
822
|
+
@classmethod
|
|
823
|
+
def mainimpl(cls, args):
|
|
824
|
+
for versiondir in listorempty(pooldir):
|
|
825
|
+
for venv in listorempty(versiondir, Venv):
|
|
826
|
+
try:
|
|
827
|
+
venv.writeunlock()
|
|
828
|
+
except LockStateException:
|
|
829
|
+
log.debug("Was not write locked: %s", venv.venvpath)
|
|
830
|
+
else:
|
|
831
|
+
log.warning("Released write lock: %s", venv.venvpath)
|
|
832
|
+
|
|
833
|
+
@_shortcut
|
|
834
|
+
class ConsoleScripts(ParserCommand):
|
|
835
|
+
|
|
836
|
+
help = 'activate all console scripts of the given requirement specifier'
|
|
837
|
+
letter = 'S'
|
|
838
|
+
|
|
839
|
+
@staticmethod
|
|
840
|
+
def initparser(parser):
|
|
841
|
+
parser.add_argument('-f', action = 'store_true', help = 'overwrite existing scripts')
|
|
842
|
+
parser.add_argument('spec', help = 'a requirement specifier')
|
|
843
|
+
|
|
844
|
+
@classmethod
|
|
845
|
+
def mainimpl(cls, args):
|
|
846
|
+
from inspect import getsource # About two hundredths.
|
|
847
|
+
spec, = FastReq.parselines([args.spec])
|
|
848
|
+
pyversion = 3
|
|
849
|
+
with TemporaryDirectory() as tempdir:
|
|
850
|
+
with open(os.path.join(tempdir, 'list.py'), 'w') as f:
|
|
851
|
+
f.write("class C:\n%sC.%s(%r)\n" % (getsource(cls._commands), cls._commands.__name__, spec.namepart))
|
|
852
|
+
with Pool(pyversion).readonly(ParsedRequires(['importlib-metadata', spec.reqstr])) as venv:
|
|
853
|
+
names = set(venv.run('check_output', [tempdir], 'list', [], universal_newlines = True).splitlines())
|
|
854
|
+
for name in names:
|
|
855
|
+
Activate.install(args.f, name, pyversion, spec.reqstr, userbin, chainrelpath, name)
|
|
856
|
+
|
|
857
|
+
@staticmethod
|
|
858
|
+
def _commands(distname):
|
|
859
|
+
from importlib_metadata import distribution, files
|
|
860
|
+
for ep in distribution(distname).entry_points:
|
|
861
|
+
if 'console_scripts' == ep.group:
|
|
862
|
+
print(ep.name)
|
|
863
|
+
for p in files(distname):
|
|
864
|
+
t = p.parts
|
|
865
|
+
if 5 == len(t) and ('..', '..', '..', 'bin') == t[:4]:
|
|
866
|
+
print(t[4])
|
|
867
|
+
|
|
868
|
+
_module.end()
|
venvpool/motivate.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: venvpool
|
|
3
|
+
Version: 16
|
|
4
|
+
Summary: Run your Python scripts using an automated pool of virtual environments to satisfy their requirements
|
|
5
|
+
Home-page: https://pypi.org/project/venvpool/
|
|
6
|
+
Author: foyono
|
|
7
|
+
Author-email: shrovis@foyono.com
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# venvpool
|
|
11
|
+
Run your Python scripts using an automated pool of virtual environments to satisfy their requirements
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
### motivate
|
|
16
|
+
Create and maintain wrapper scripts in ~/.local/bin for all runnable modules in the given projects, or the current project if none given.
|
|
17
|
+
|
|
18
|
+
### motivate -S
|
|
19
|
+
Create/maintain wrappers for all console_scripts of the given requirement specifier.
|
|
20
|
+
|
|
21
|
+
### motivate -C
|
|
22
|
+
Compact the pool of venvs.
|
|
23
|
+
|
|
24
|
+
## API
|
|
25
|
+
|
|
26
|
+
<a id="venvpool"></a>
|
|
27
|
+
|
|
28
|
+
### venvpool
|
|
29
|
+
|
|
30
|
+
<a id="venvpool.dotpy"></a>
|
|
31
|
+
|
|
32
|
+
###### dotpy
|
|
33
|
+
|
|
34
|
+
Python source file extension including dot.
|
|
35
|
+
|
|
36
|
+
<a id="venvpool.initlogging"></a>
|
|
37
|
+
|
|
38
|
+
###### initlogging
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
def initlogging()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Initialise the logging module to send debug (and higher levels) to stderr.
|
|
45
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
venvpool/__init__.py,sha256=W9H13HoAjpSpvgU_vvgl09NVhjJ53KTZvrnsbsB2bCU,30489
|
|
2
|
+
venvpool/motivate.py,sha256=ZLLQhnb1fC6-grrW7dNXeoFAnlpyB6tuNoEH6Z20_5s,198
|
|
3
|
+
venvpool-16.dist-info/METADATA,sha256=YDKhsmrw69cL0OdRrGq84h-tK3xOtPPRRpGGyrk0Dyc,1017
|
|
4
|
+
venvpool-16.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
5
|
+
venvpool-16.dist-info/entry_points.txt,sha256=dYx6DJExGkRlQyppF811suVhhV8oUBEVxmquF6luKno,52
|
|
6
|
+
venvpool-16.dist-info/top_level.txt,sha256=WRvBxvEyUmyq-Dqbja9QG1fJUgv2CaVFuyGLwYaCMbs,9
|
|
7
|
+
venvpool-16.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
venvpool
|