Skip to content

Instantly share code, notes, and snippets.

@justengel
Created June 29, 2020 16:07
Show Gist options
  • Save justengel/1de720bf0dcaf1e5e4957ee64306ce57 to your computer and use it in GitHub Desktop.
Save justengel/1de720bf0dcaf1e5e4957ee64306ce57 to your computer and use it in GitHub Desktop.
Change a library version and upload it to pypi.
"""
Change a library version and upload it to pypi.
If you are updating the version, this script assumes a __meta__.py file exists in the package you want to upload.
Warning:
This has not been tested on all platforms!
Requirements:
* shell_proc>=1.1.1
* wheel
* twine
"""
import argparse
import contextlib
import os
from shell_proc import Shell
__all__ = ['is_git', 'is_hg', 'PackageUpdater']
def is_git(directory=None):
"""Return if the given directory is a git repository."""
if directory is None:
directory = '.git'
else:
directory = os.path.join(directory, '.git')
return os.path.isdir(directory)
def is_hg(directory=None):
"""Return if the given directory is a git repository."""
if directory is None:
directory = '.hg'
else:
directory = os.path.join(directory, '.hg')
return os.path.isdir(directory)
class PackageUpdater(Shell):
is_git = staticmethod(is_git)
is_hg = staticmethod(is_hg)
@contextlib.contextmanager
def change_dir(self, directory):
"""Temporarily change the directory"""
# Change directory if needed
current_dir = None
dir_changed = False
if directory is not None:
directory = os.path.abspath(str(directory)).replace('\\', '/')
if self.is_windows():
current_dir = self('echo %CD%').stdout.strip()
else:
current_dir = self('echo $PWD').stdout.strip()
current_dir = os.path.abspath(current_dir).replace('\\', '/')
dir_changed = current_dir != directory
if dir_changed:
self('cd "{}"'.format(directory))
os.chdir(directory)
yield
if dir_changed:
self('cd "{}"'.format(current_dir))
os.chdir(current_dir)
@staticmethod
def get_meta(filename):
"""Return the metadata dictionary from the given filename."""
with open(filename, 'r') as f:
meta = {}
exec(compile(f.read(), filename, 'exec'), meta)
return meta
@classmethod
def get_version(cls, name):
"""Return the version number in the __meta__.py file for the given package name."""
filename = os.path.join(name, '__meta__.py')
meta = cls.get_meta(filename)
return meta['version']
@staticmethod
def change_version(name, version):
"""Change the version number in the __meta__.py file for the given package name."""
filename = os.path.join(name, '__meta__.py')
# Read file
with open(filename, 'r') as f:
lines = f.readlines()
# Change version
for i in range(len(lines)):
line = lines[i]
if line.startswith('version'):
lines[i] = "version = '{}'".format(version)
# Write file
with open(filename, 'w') as f:
f.writelines(lines)
def tag_exists(self, tag):
"""Return if the tag exists in git or hg."""
# Change directory if needed
if is_git():
result = self('git tag -l "{}"'.format(tag)).stdout.strip()
return result == tag # The stdout will be the matching tag or blank if not found
elif is_hg():
result = self('hg log -r "{}"'.format(tag))
return result.exit_code == 0 # If 0 True. Will give exit code of 255 if not found
return False
def commit_tag(self, tag, push=True):
"""Commit the tag."""
if self.tag_exists(tag):
raise ValueError('The tag "{}" already exists!'.format(tag))
if is_git():
result = self('git tag "{}"'.format(tag)).stdout.strip()
if push:
result = self('git push --tags')
return result
elif is_hg():
result = self('hg tag "{}"'.format(tag))
if push:
result = self('hg push')
return result
def update_version(self, name, major=False, minor=False, micro=False):
"""Update the version number in the __meta__.py file."""
# Read and change the version.
version = self.get_version(name)
vsplit = version.split('.')
major_changed = minor_changed = micro_changed = True
# Update by adding 1
if isinstance(major, bool):
if major:
major = int(vsplit[0]) + 1
else:
major = int(vsplit[0])
major_changed = False
if isinstance(minor, bool):
if minor:
minor = int(vsplit[1] + 1)
else:
minor = int(vsplit[1])
minor_changed = False
if isinstance(micro, bool):
if micro:
micro = int(vsplit[2] + 1)
else:
micro = int(vsplit[2])
micro_changed = False
version = '.'.join([str(major), str(minor), str(micro)] + vsplit[3:])
if major_changed or minor_changed or micro_changed:
self.change_version(name, version)
return version
else:
return False
def upload(self, name=None, major=False, minor=False, micro=False, tag=True, push=True,
activate_venv=True, directory=None):
"""Upload a package to pypi.
Args:
name (str)[None]: Name of the package to upload. Must have a __meta__.py file.
This is only used if update_version is True
major (bool/int)[False]: If True add 1 to the version. If an integer is given set the version.
minor (bool/int)[False]: If True add 1 to the version. If an integer is given set the version.
micro (bool/int)[False]: If True add 1 to the version. If an integer is given set the version.
tag (bool)[True]: Create a tag after changing the version.
push (bool)[True]: Push the tag after changing the version.
activate_venv (bool)[True]: If True try activating a virtual environment before creating the distribution.
directory (str)[None]: top level directory where the library's setup.py file exists.
"""
sh = self
with sh.change_dir(directory):
version = sh.update_version(name, major=major, minor=minor, micro=micro)
if version is not False and tag:
# Create and Push a new Tag
tag = 'v' + version
sh.commit_tag(tag, push=push)
# Activate virtual environment.
if activate_venv:
if sh.is_windows():
sh('call .\\venv\\Scripts\\activate.bat')
else:
sh('source ./venv/bin/activate')
# Create distribution
sh('python setup.py sdist')
sh('python setup.py sdist bdist_wheel')
return sh('twine upload ./dist/*')
def as_version(value):
if isinstance(value, str):
if value == 'True' or value == 'true' or value == 'TRUE':
return True
elif value == 'False' or value == 'false' or value == 'FALSE':
return False
else:
try:
return int(value)
except (ValueError, TypeError, Exception):
return False
return value
def main(name=None, major=False, minor=False, micro=False, tag=True, push=True, activate_venv=True, directory=None):
P = argparse.ArgumentParser(description='Upload the package to pypi')
P.add_argument('--name', '-n', type=str, default=name, help='Name of the package if you are updating the version.')
P.add_argument('--major', default=major, help='Major version to change to. If True increment by 1.')
P.add_argument('--minor', default=minor, help='Minor version to change to. If True increment by 1.')
P.add_argument('--micro', default=micro, help='Micro version to change to. If True increment by 1.')
P.add_argument('--tag', type=bool, default=tag,
help='Create a tag if the version changes (Tag will be vMajor.Minor.Micro).')
P.add_argument('--push', type=bool, default=push, help='Push the tag if the version changes.')
P.add_argument('--activate_venv', '-a', type=bool, default=activate_venv,
help='Try activating a virtual environment before creating the distribution.')
P.add_argument('--directory', '-d', type=str, default=directory,
help='Directory to change to where the library\'s setup.py file exists.')
ARGS = P.parse_args()
name = ARGS.name
major = as_version(ARGS.major)
minor = as_version(ARGS.minor)
micro = as_version(ARGS.micro)
tag = ARGS.tag
push = ARGS.push
activate_venv = ARGS.activate_venv
directory = ARGS.directory
with PackageUpdater() as sh:
sh.upload(name=name, major=major, minor=minor, micro=micro, tag=tag, push=push, activate_venv=activate_venv,
directory=directory)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment