Merge pull request #290 from zalando/feature/pyinstaller

Binary build with PyInstaller
This commit is contained in:
Alexander Kukushkin
2016-09-05 14:52:13 +02:00
committed by GitHub
6 changed files with 124 additions and 21 deletions
+42 -6
View File
@@ -1,12 +1,48 @@
data/*
*.pyc
*.egg/
*.egg-info/
*.py[cod]
# vi(m) swap files:
*.sw?
# C extensions
*.so
# Packages
.cache/
*.egg
*.eggs
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.eggs/
build/
.tox
nosetests.xml
coverage.xml
htmlcov
junit.xml
features/output
dummy
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
pgpass
scm-source.json
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
set -e
pip install --ignore-installed setuptools==19.2 pyinstaller
pyinstaller --clean --onefile patroni.spec
+39
View File
@@ -0,0 +1,39 @@
# -*- mode: python -*-
block_cipher = None
def hiddenimports():
import sys
sys.path.insert(0, '.')
try:
import patroni.dcs
return patroni.dcs.dcs_modules()
finally:
sys.path.pop(0)
a = Analysis(['patroni/__main__.py'],
pathex=[],
binaries=None,
datas=None,
hiddenimports=hiddenimports(),
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='patroni',
debug=False,
strip=False,
upx=True,
console=True)
+1 -1
View File
@@ -65,7 +65,7 @@ class Config(object):
print('Usage: {0} config.yml'.format(sys.argv[0]))
print('\tPatroni may also read the configuration from the {0} environment variable'.
format(self.PATRONI_CONFIG_VARIABLE))
exit(1)
sys.exit(1)
self.__effective_configuration = self._build_effective_configuration({}, self._local_configuration)
self._data_dir = self.__effective_configuration['postgresql']['data_dir']
+30 -14
View File
@@ -6,6 +6,7 @@ import json
import os
import pkgutil
import six
import sys
from collections import namedtuple
from patroni.exceptions import PatroniException
@@ -31,22 +32,37 @@ def parse_connection_string(value):
return conn_url, api_url
def dcs_modules():
"""Get names of DCS modules, depending on execution environment. If being packaged with PyInstaller,
modules aren't discoverable dynamically by scanning source directory because `FrozenImporter` doesn't
implement `iter_modules` method. But it is still possible to find all potential DCS modules by
iterating through `toc`, which contains list of all "frozen" resources."""
dcs_dirname = os.path.dirname(__file__)
module_prefix = __package__ + '.'
if getattr(sys, 'frozen', False):
importer = pkgutil.get_importer(dcs_dirname)
return [module for module in list(importer.toc) if module.startswith(module_prefix) and module.count('.') == 2]
else:
return [module_prefix + name for _, name, is_pkg in pkgutil.iter_modules([dcs_dirname]) if not is_pkg]
def get_dcs(config):
available_implementations = set()
for _, module_name, is_pkg in pkgutil.iter_modules([os.path.dirname(__file__)]):
if not is_pkg:
module = importlib.import_module(__package__ + '.' + module_name)
for name in filter(lambda name: not name.startswith('__'), dir(module)): # iterate through module content
value = getattr(module, name)
name = name.lower()
# try to find implementation of AbstractDCS interface, class name must match with module_name
if inspect.isclass(value) and issubclass(value, AbstractDCS) and name == module_name:
available_implementations.add(name)
if name in config: # which has configuration section in the config file
# propagate some parameters
config[name].update({p: config[p] for p in ('namespace', 'name', 'scope',
'loop_wait', 'ttl', 'retry_timeout') if p in config})
return value(config[name])
for module_name in dcs_modules():
module = importlib.import_module(module_name)
for name in filter(lambda name: not name.startswith('__'), dir(module)): # iterate through module content
value = getattr(module, name)
name = name.lower()
# try to find implementation of AbstractDCS interface, class name must match with module_name
if inspect.isclass(value) and issubclass(value, AbstractDCS) and __package__ + '.' + name == module_name:
available_implementations.add(name)
if name in config: # which has configuration section in the config file
# propagate some parameters
config[name].update({p: config[p] for p in ('namespace', 'name', 'scope',
'loop_wait', 'ttl', 'retry_timeout') if p in config})
return value(config[name])
raise PatroniException("""Can not find suitable configuration of distributed configuration store
Available implementations: """ + ', '.join(available_implementations))
+7
View File
@@ -14,6 +14,11 @@ from test_etcd import SleepException, etcd_read, etcd_write
from test_postgresql import Postgresql, psycopg2_connect
class MockFrozenImporter(object):
toc = set(['patroni.dcs.etcd'])
@patch('time.sleep', Mock())
@patch('subprocess.call', Mock(return_value=0))
@patch('psycopg2.connect', psycopg2_connect)
@@ -27,6 +32,8 @@ from test_postgresql import Postgresql, psycopg2_connect
@patch.object(etcd.Client, 'read', etcd_read)
class TestPatroni(unittest.TestCase):
@patch('pkgutil.get_importer', Mock(return_value=MockFrozenImporter()))
@patch('sys.frozen', Mock(return_value=True), create=True)
@patch.object(etcd.Client, 'read', etcd_read)
def setUp(self):
RestApiServer._BaseServer__is_shut_down = Mock()