Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 138 additions & 20 deletions pyrelease.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,29 @@
* push

"""

from __future__ import print_function
import io
import os
import re
import ast
import imp
import tempfile
import yaml

# try:
# from ConfigParser import ConfigParser
# except ImportError:
# from configparser import ConfigParser

from configparser import ConfigParser

from email.utils import getaddresses
import subprocess
from fnmatch import fnmatchcase, fnmatch
from shutil import copyfile

SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))


class PyPiRc:
"""
Expand All @@ -43,6 +52,7 @@ def __init__(self):
self.author = parser.get('simple_setup', 'author', fallback=None)
self.author_email = parser.get('simple_setup', 'author_email', fallback=None)


class GitConfig:
"""
~/.gitconfig
Expand All @@ -58,6 +68,7 @@ def __init__(self):
self.author = parser.get('user', 'name', fallback=None)
self.author_email = parser.get('user', 'email', fallback=None)


class HgRc:
"""
~/.hgrc
Expand All @@ -68,7 +79,10 @@ def __init__(self):
parser = ConfigParser()
parser.read(os.path.expanduser('~/.hgrc'))
username = parser.get('ui', 'username', fallback=None)
name_email = getaddresses([username])
try:
name_email = getaddresses([username])
except TypeError:
name_email = None
self.author = None
self.author_email = None
if name_email:
Expand Down Expand Up @@ -96,6 +110,7 @@ def read(*parts):
except IOError:
return ''


def version_from_file(fname):
"""
"""
Expand All @@ -117,6 +132,7 @@ def version_from_git():
if lines:
return lines[-1].decode('utf-8')


# find('*.py', 'some/path/')
# def find(pattern, path):
# result = []
Expand Down Expand Up @@ -156,7 +172,6 @@ def package_py(what_to_package):
return apath



def dependencies_ast(apy):
"""Try to find 3rd party dependencies in the past in .py file.

Expand All @@ -179,7 +194,6 @@ def dependencies_ast(apy):
return deps



class ModuleDocs:
"""For getting the description and long_description out of the module docstring
"""
Expand Down Expand Up @@ -231,7 +245,6 @@ def name(self):
if os.path.isdir(self.what_to_package):
return os.path.split(os.path.abspath(self.what_to_package))[-1]


@property
def description(self):
return self.module_docs.description
Expand All @@ -251,7 +264,6 @@ def version(self):
ver = '0.0.0'
return ver


@property
def url(self):
# from .git/config
Expand Down Expand Up @@ -279,8 +291,6 @@ def is_script(self):
def is_single_file(self):
return package_py(self.what_to_package)



def __str__(self):
"""
"""
Expand Down Expand Up @@ -350,21 +360,20 @@ class FillFiles:
def __init__(self, package_info):
self.package_info = package_info
self.tmpdir = tempfile.mkdtemp()
print (self.tmpdir)
print(self.tmpdir)
# repo = 'https://github.com/pypa/sampleproject/'
# subprocess.check_output = 'git clone %s %s' % (repo, self.tmpdir)

def fill_files(self):
setup_py_data = self.setup_py()
manifest_in_data = self.manifest_in()

with open(os.path.join(self.tmpdir, 'setup.py'), 'w') as afile:
afile.write(setup_py_data)
with open(os.path.join(self.tmpdir, 'MANIFEST.in'), 'w') as afile:
afile.write(manifest_in_data)

self.copy_files()


def copy_files(self):
"""Copies our package files into the new output folder.
"""
Expand All @@ -375,7 +384,6 @@ def copy_files(self):
else:
raise NotImplementedError('only single files supported')


def manifest_in(self):
"""Fill in a MANIFEST.in
"""
Expand Down Expand Up @@ -405,7 +413,6 @@ def setup_py(self):
py_modules = ''
packages = "packages=find_packages(exclude=['contrib', 'docs', 'tests']),"


install_requires = "install_requires=%s," % repr(self.package_info.install_requires)

return SETUP_PY.format(name=self.package_info.name,
Expand All @@ -422,23 +429,134 @@ def setup_py(self):
py_modules=py_modules)


def fill_files(package_info):
filler = FillFiles(package_info)
filler.fill_files()
return filler.tmpdir


class UploadPyPi:
"""Builds pypackage project and uploads to PyPi.

Commands for build are intended to be stored in a config file
to allow for easy customization. Config format is `yaml`. If
no config is found the default commands should be used.
Errors during build will set the self.errors switch to True
indicating there was an error.
"""
"""
# twine upload dist/*.tar.gz

command_file = os.path.join(SCRIPT_PATH, "pybuilder.yml")
default_commands = {
"builds": [
"python setup.py sdist",
"python setup.py bdist_wheel --universal",
]
}

def __init__(self, test=False):
self.errors = None
self.config = self._read_command_config()

# Set True to upload to PyPi test server.
self.use_test_server = test

def build(self):
# python setup.py build sdist bdist_wheel
pass
"""Build out project distros.

Build commands are pulled from config file to
allow for easy customization.
"""
for cmd in self.config["builds"]:
try:
subprocess.call(cmd, shell=True)
except Exception as e:
print("Error processing", str(cmd))
print(e)
self.errors = True
return
else:
self.errors = False

# If using test server then project needs to register first.
# Not necessary for normal PyPi server
if self.use_test_server:
register_test_site = "python setup.py register -r https://testpypi.python.org/pypi", # FOR PYPI TEST SITE
subprocess.call(register_test_site, shell=True)

def upload(self):
"""Uploads package to PyPi using twine.
The advantage to using Twine is your package is uploaded

over HTTPS which prevents your private info from appearing
in the request header.
"""
if self.use_test_server:
response = subprocess.call("twine upload dist/* -r testpypi", shell=True) # TEST SERVER
else:
response = subprocess.call("twine upload dist/*", shell=True)

# TODO: This needs to be better..
if response == 127:
print("Twine not installed.. Cancelled.")
self.errors = True

def _read_command_config(self):
"""Get yaml config file with build commands.

If no config file is found the defaults are used
"""
if not os.path.isfile(self.command_file):
return self.default_commands
with open(self.command_file, 'r') as f:
return yaml.load(f.read())

@property
def success(self):
"""Get build and deploy status.

If self.errors is None, then no builds have been
attempted yet. Otherwise returns False if an error
was detected, True if build was a success.
"""
if self.errors is None:
return False
return not self.errors


def build_and_upload_to_pypi(path, test=False):
"""Helper function for `UploadPyPi` class.

Raises BuildError Exception if errors occur during build.
Raises DeployError Exception if error occurs during upload

TODO: Need better twine update status.

"""
class BuildError(Exception):
pass

class DeployError(Exception):
pass
# python setup.py upload

builder = UploadPyPi(test)
os.chdir(path)
builder.build()
if not builder.success:
raise BuildError("Error while building package.")
builder.upload()
if not builder.success:
raise DeployError("Error deploying package to PyPi")


def main():
"""
"""Main entry point for pyrelease.

Gathers info for package, fills out necessary files, builds,
then uploads to PyPi
"""
package_info = GatherInfo('.')
filled_files = FillFiles(package_info)
temp_dir = fill_files(package_info)
build_and_upload_to_pypi(temp_dir, test=True)


if __name__ == '__main__':
Expand Down