diff --git a/README.md b/README.md index 3da07c5..05a784b 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,17 @@ -AutoUpgrade -=========== -[![PyPI](https://img.shields.io/pypi/v/autoupgrade-ng.svg)] -(https://pypi.python.org/pypi/autoupgrade-ng) -[![GitHub issues](https://img.shields.io/github/issues/vuolter/autoupgrade.svg)] -(https://github.com/vuolter/autoupgrade/issues) -[![PyPI](https://img.shields.io/pypi/dm/autoupgrade-ng.svg)] -(https://pypi.python.org/pypi/autoupgrade-ng) -[![PyPI](https://img.shields.io/pypi/l/autoupgrade-ng.svg)] -(https://pypi.python.org/pypi/autoupgrade-ng) -[![PyPI](https://img.shields.io/pypi/format/autoupgrade-ng.svg)] -(https://pypi.python.org/pypi/autoupgrade-ng) -[![PyPI](https://img.shields.io/pypi/pyversions/autoupgrade-ng.svg)] -(https://pypi.python.org/pypi/autoupgrade-ng) -[![PyPI](https://img.shields.io/pypi/status/autoupgrade-ng.svg)] -(https://pypi.python.org/pypi/autoupgrade-ng) -[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/WalterPurcaro.svg?style=social)] -(https://twitter.com/intent/tweet?text=Wow:&url=%5Bobject%20Object%5D) - -Automatic upgrade of PyPI packages. - - -Table of contents ------------------ +# AutoUpgrade +[![PyPI](https://img.shields.io/pypi/v/autoupgrade-ng.svg)](https://pypi.python.org/pypi/autoupgrade-ng) +[![GitHub issues](https://img.shields.io/github/issues/vuolter/autoupgrade.svg)](https://github.com/vuolter/autoupgrade/issues) +[![PyPI](https://img.shields.io/pypi/dm/autoupgrade-ng.svg)](https://pypi.python.org/pypi/autoupgrade-ng) +[![PyPI](https://img.shields.io/pypi/l/autoupgrade-ng.svg)](https://pypi.python.org/pypi/autoupgrade-ng) +[![PyPI](https://img.shields.io/pypi/format/autoupgrade-ng.svg)](https://pypi.python.org/pypi/autoupgrade-ng) +[![PyPI](https://img.shields.io/pypi/pyversions/autoupgrade-ng.svg)](https://pypi.python.org/pypi/autoupgrade-ng) +[![PyPI](https://img.shields.io/pypi/status/autoupgrade-ng.svg)](https://pypi.python.org/pypi/autoupgrade-ng) +[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/WalterPurcaro.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=%5Bobject%20Object%5D) + +Automatic upgrade of PyPI packages or GitHub repos. + + +## Table of contents - [Quick Start](#quick-start) - [Installation](#installation) @@ -29,8 +19,7 @@ Table of contents - [Licensing](#licensing) -Quick Start ------------ +## Quick Start from autoupgrade import Package Package().smartupgrade() @@ -50,8 +39,7 @@ Old methods are still supported; you can accomplish the same task calling: AutoUpgrade('pip').upgrade_if_needed() -Installation ------------- +## Installation pip install autoupgrade-ng @@ -60,83 +48,117 @@ sure you have already removed the old [AutoUpgrade package] (https://pypi.python.org/pypi/autoupgrade) before install this** to avoid an installation conflict. +The GitHub features are only accessible if the +[PyGithub package](https://github.com/PyGithub/PyGithub) is installed. -Usage ------ + pip install pygithub -### Classes - class Package(__builtin__.object) +## API -**Decription**: Basic package class, holds one package. +### Classes - class AutoUpgrade(__builtin__.object) +#### class autoupgrade.abc.ABCPackage(object) -**Decription**: Legacy class refering to `Package` one. +Abstract class that defines the structure of an `autoupgrade` package. +#### class autoupgrade.pypi.PyPIPackage(autoupgrade.abc.ABCPackage) -### Methods +Basic package class for PyPI, holds one package. __init__(self, pkg, index=None, verbose=False) -**Decription**: None. - **Arguments**: + - `pkg` (str) name of package. - `index` (str) alternative index, if not given default from *pip* will be used. -Include full index url _(e.g. https://example.com/simple)_. +Include full index url *(e.g. https://example.com/simple)*. +- `verbose` (bool) print verbose statements. **Return**: None. - check(self) +#### class autoupgrade.github.GitHubPackage(autoupgrade.abc.ABCPackage) -**Decription**: Check if `pkg` has a later version. +Basic package class for GitHub, holds one repository. -**Arguments**: None. + __init__(self, pkg, user, repo=None, authenticate=(), verbose=False) -**Return**: True if later version exists, else False. +**Arguments**: - restart(self) +- `pkg` (str) name of package. +- `user` (str) name of the GitHub user/organization that the repo belongs to. +- `repo` (None, str) if the repo doesn't match the `pkg` name. +- `authenticate` (tuple) login credentials to login to GitHub (see +[PyGithum package](https://github.com/PyGithub/PyGithub)) this likely will just be +`(, )` or `()` +- `verbose` (bool) print verbose statements. -**Decription**: Restart application with same args as it was started. +**Return**: None. -**Arguments**: None. +#### class autoupgrade.package.Package(object) + +Basic class used to bundle PyPIPackage and GitHubPackage. + + __init__(self, *args, **kwargs) + +**Arguments**: provide arguments for `PyPIPackage` **or** `GitHubPackage` and the +applicable package will be created. Alternatively access the class method +corresponding to the package type you wish to initialize +(e.g. `Package.pypi(*args, **kwargs)` versus `Package.github(*args, **kwargs)`. **Return**: None. - upgrade(self, dependencies=False, prerelease=False, force=False) +#### class autoupgrde.AutoUpgrade(autoupgrade.package.Package) -**Decription**: Upgrade the package unconditionaly. +**(Deprecated, see Package)** -**Arguments**: -- `dependencies` update dependencies if True _(see `pip --no-deps`)_. -- `prerelease` update to pre-release and development versions. -- `force` reinstall all packages even if they are already up-to-date. -**Return**: None. +### Methods for PyPIPackage & GitHubPackage smartupgrade(self, restart=True, dependencies=False, prerelease=False) -**Decription**: Upgrade the package if there is a later version available. +Upgrade the package if there is a newer version available. **Arguments**: + - `restart` restart app if True. -- `dependencies` update dependencies if True _(see `pip --no-deps`)_. +- `dependencies` update dependencies if True *(see `pip --no-deps`)*. - `prerelease` update to pre-release and development versions. **Return**: None. upgrade_if_needed(self, restart=True, dependencies=False, prerelease=False) -**Decription**: Legacy method refering to `smartupgrade` one. +**(Deprecated, see smartupgrade)** + + upgrade(self, dependencies=False, prerelease=False, force=False) + +Upgrade the package unconditionally. -**Arguments**: Same as `smartupgrade`. +**Arguments**: + +- `dependencies` update dependencies if True *(see `pip --no-deps`)*. +- `prerelease` update to pre-release and development versions. +- `force` reinstall all packages even if they are already up-to-date. + + check(self) -**Return**: Same as `smartupgrade`. +Check if `pkg` has a newer version. + +**Arguments**: None. + +**Return**: True if a newer version exists, else False. + + restart(self) + +Restart application with same args as it was started. + +**Arguments**: None. + +**Return**: None. -Licensing ---------- +## Licensing Please refer to the included [LICENSE](/LICENSE.md) for the extended license. diff --git a/autoupgrade/__init__.py b/autoupgrade/__init__.py index 35af386..513ad85 100644 --- a/autoupgrade/__init__.py +++ b/autoupgrade/__init__.py @@ -1,17 +1,9 @@ # -*- coding: utf-8 -*- -from .exceptions import NoVersionsError, PIPError, PkgNotFoundError -from .package import Package -from .utils import normalize_version +from .package import (Package, AutoUpgrade) -# NOTE: Legacy class -class AutoUpgrade(Package): - - def upgrade(self, *args, **kwargs): - try: - Package.upgrade(self, *args, **kwargs) - except PIPError: - return False - else: - return True +__all__ = [ + "Package", + "AutoUpgrade", +] diff --git a/autoupgrade/abc.py b/autoupgrade/abc.py new file mode 100644 index 0000000..4471c12 --- /dev/null +++ b/autoupgrade/abc.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +import sys +import os +from abc import (ABCMeta, abstractmethod) +import pkg_resources + +from .utils import ver_to_tuple + + +class ABCPackage: + # py2 backwards compatible + __metaclass__ = ABCMeta + + # deprecated in favor of smartupgrade + def upgrade_if_needed(self, *args, **kwargs): + return self.smartupgrade(*args, **kwargs) + + def smartupgrade(self, restart=True, dependencies=False, prerelease=False): + """ + Upgrade the package if there is a later version available. + Args: + restart: restart app if True + dependencies: update package dependencies if True (see pip --no-deps) + prerelease: update to pre-release and development versions + """ + if not self.check(): + if self.verbose: + print("Package {} already up-to-date!".format(self.pkg)) + return + if self.verbose: + print("Upgrading {} ... (v{} -> v{})".format( + self.pkg, + ".".join(map(str, self._get_current())), + ".".join(map(str, self._get_newest_version())))) + self.upgrade(dependencies, prerelease, force=False) + if restart: + self.restart() + + @abstractmethod + def upgrade(self, dependencies=False, prerelease=False, force=False): + pass + + def restart(self): + """ + Restart application with same args as it was started. + Does **not** return + """ + if self.verbose: + print("Restarting {} {} ...".format(sys.executable, sys.argv)) + os.execl(sys.executable, *([sys.executable] + sys.argv)) + + def check(self): + """ + Check if pkg has a later version + Returns true if later version exists + """ + current = self._get_current() + highest = self._get_newest_version() + outdated = highest > current + if self.verbose: + if outdated: + if current == (-1,): + print("{} is not installed!".format(self.pkg)) + else: + print("{} current version: {}".format(self.pkg, current)) + print("{} latest version: {}".format(self.pkg, highest)) + else: + print("{} is up-to-date!".format(self.pkg)) + return + + def _get_current(self): + try: + current = pkg_resources.get_distribution(self.pkg).version + current = ver_to_tuple(current) + except pkg_resources.DistributionNotFound: + current = (-1,) + return current + + @abstractmethod + def _get_newest_version(self): + pass + + def __str__(self): + return self.pkg + + def __repr__(self): + return "{}({})".format(self.__class__.__name__, str(self)) diff --git a/autoupgrade/github.py b/autoupgrade/github.py new file mode 100644 index 0000000..8fe9c1f --- /dev/null +++ b/autoupgrade/github.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +import pip +import os + +from .exceptions import PIPError +from .abc import ABCPackage +from .utils import ver_to_tuple + +try: + import github + + # only create the GitHubPackage if the github module is available + class GitHubPackage(ABCPackage): + __slots__ = ['pkg', 'repository', 'verbose'] + + def __init__(self, pkg, user, repo=None, authenticate=(), + verbose=False): + """ + Args: + repo (str): GitHub repo + user (str): GitHub user/organization + verbose (bool): display verbose messages + """ + if repo is None: + repo = pkg + + self.pkg = pkg + gh = github.Github(*authenticate) + usr = gh.get_user(user) + self.repository = usr.get_repo(repo) + self.verbose = verbose + + def upgrade(self, dependencies=False, prerelease=False, force=False): + """ + Upgrade the package unconditionaly + Args: + dependencies: update package dependencies if True (see pip --no-deps) + prerelease: update to pre-release and development versions + force: reinstall all packages even if they are already up-to-date + Returns True if pip was sucessful + """ + pip_args = ['install'] + + found = self._get_current() != (-1) + if found: + pip_args.append("--upgrade") + + if force: + pip_args.append("--force-reinstall") + + if not dependencies: + pip_args.append("--no-deps") + + if prerelease: + pip_args.append("--pre") + + proxy = os.environ.get('http_proxy') + if proxy: + pip_args.extend(['--proxy', proxy]) + + pip_args.append(self._get_latest_release().raw_data["zipball_url"]) + + try: + ecode = pip.main(args=pip_args) + except TypeError: + # pip changed in 0.6.0 from initial_args to args, this is for + # backwards compatibility can be removed when pip 0.5 is no longer + # in use at all (approx. year 2025) + ecode = pip.main(initial_args=pip_args) + + if ecode != 0: + raise PIPError(ecode) + + def _get_latest_release(self): + return next(iter(self.repository.get_releases())) + + def _get_newest_version(self): + return ver_to_tuple(self._get_latest_release().tag_name) +except ImportError: + pass diff --git a/autoupgrade/package.py b/autoupgrade/package.py index 0a2472b..8708ca0 100644 --- a/autoupgrade/package.py +++ b/autoupgrade/package.py @@ -1,142 +1,64 @@ # -*- coding: utf-8 -*- -import os -import re -import sys - -import pip -import pkg_resources - -from .exceptions import NoVersionsError, PIPError, PkgNotFoundError -from .utils import ver_to_tuple +from .exceptions import PIPError +from .pypi import PyPIPackage try: - from urllib.request import urlopen -except Exception: - from urllib import urlopen + from .github import GitHubPackage +except ImportError: + GitHubPackage = None class Package(object): - """ - AutoUpgrade class, holds one package. - """ - - __slots__ = ['__index', 'index', 'pkg', 'verbose'] - - def __init__(self, pkg, index=None, verbose=False): - """ - Args: - pkg (str): name of package - index (str): alternative index, if not given default for *pip* will be used. Include - full index url, e.g. https://example.com/simple - """ - self.pkg = pkg - self.verbose = verbose - if index: - self.index = index.rstrip('/') - self.__index = True - else: - self.index = "https://pypi.python.org/simple" - self.__index = False - - def upgrade_if_needed(self, *args, **kwargs): - return self.smartupgrade(*args, **kwargs) - - def smartupgrade(self, restart=True, dependencies=False, prerelease=False): - """ - Upgrade the package if there is a later version available. - Args: - restart: restart app if True - dependencies: update package dependencies if True (see pip --no-deps) - prerelease: update to pre-release and development versions - """ - if not self.check(): - if self.verbose: - print("Package {} already up-to-date!".format(self.pkg)) - return - if self.verbose: - print("Upgrading {} ...".format(self.pkg)) - self.upgrade(dependencies, prerelease, force=False) - if restart: - self.restart() - - def upgrade(self, dependencies=False, prerelease=False, force=False): - """ - Upgrade the package unconditionaly - Args: - dependencies: update package dependencies if True (see pip --no-deps) - prerelease: update to pre-release and development versions - force: reinstall all packages even if they are already up-to-date - Returns True if pip was sucessful - """ - pip_args = ['install', self.pkg] - - found = self._get_current() != (-1) - if found: - pip_args.append("--upgrade") - - if force: - pip_args.append( - "--force-reinstall" if found else "--ignore-installed") - - if not dependencies: - pip_args.append("--no-deps") - - if prerelease: - pip_args.append("--pre") - - proxy = os.environ.get('http_proxy') - if proxy: - pip_args.extend(['--proxy', proxy]) - - if self.__index: - pip_args.extend(['-i', self.index]) - + __preferred = [PyPIPackage] + + def __new__(cls, *args, **kwargs): + # to keep track of the first error (if there was one) + first_error = None + + # loop over the preferred order of the package handlers, for each one + # try to initialize, if it fails to initialize then continue to the + # next preferred package handler + for pkg_type in cls.__preferred: + try: + # return a successful package handler match + return pkg_type(*args, **kwargs) + except TypeError as e: + # only remember the very first error thrown as that is the most + # preferred package handler + if first_error is None: + first_error = e + + # raise an error if there was no preferred handler match + if first_error is None: + err = ("This {} has no preferred package handlers " + "(__preferred == {})").format( + cls.__name__, + cls.__preferred) + raise TypeError(err) + raise first_error + + @classmethod + def pypi(cls, *args, **kwargs): + return PyPIPackage(*args, **kwargs) + + +# only add the GitHubPackage if it was imported properly +if GitHubPackage is not None: + class Package(Package): + __preferred = [PyPIPackage, GitHubPackage] + + @classmethod + def github(cls, *args, **kwargs): + return GitHubPackage(*args, **kwargs) + + +# deprecated in favor of Package +class AutoUpgrade(Package): + def upgrade(self, *args, **kwargs): try: - ecode = pip.main(args=pip_args) - except TypeError: - # pip changed in 0.6.0 from initial_args to args, this is for backwards compatibility - # can be removed when pip 0.5 is no longer in use at all (approx. - # year 2025) - ecode = pip.main(initial_args=pip_args) - - if ecode != 0: - raise PIPError(ecode) - - def restart(self): - """ - Restart application with same args as it was started. - Does **not** return - """ - if self.verbose: - print("Restarting {} {} ...".format(sys.executable, sys.argv)) - os.execl(sys.executable, *([sys.executable] + sys.argv)) - - def check(self): - """ - Check if pkg has a later version - Returns true if later version exists - """ - current = self._get_current() - highest = self._get_highest_version() - return highest > current - - def _get_current(self): - try: - current = ver_to_tuple( - pkg_resources.get_distribution(self.pkg).version) - except pkg_resources.DistributionNotFound: - current = (-1,) - return current - - def _get_highest_version(self): - url = "{}/{}/".format(self.index, self.pkg) - html = urlopen(url) - if html.getcode() != 200: - raise PkgNotFoundError - pattr = r'>{}-(.+?)<'.format(self.pkg) - versions = map(ver_to_tuple, - re.findall(pattr, html.read(), flags=re.I)) - if not versions: - raise NoVersionsError - return max(versions) + Package.upgrade(self, *args, **kwargs) + except PIPError: + return False + else: + return True diff --git a/autoupgrade/pypi.py b/autoupgrade/pypi.py new file mode 100644 index 0000000..0304711 --- /dev/null +++ b/autoupgrade/pypi.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +import os +import re +import pip + +from .exceptions import NoVersionsError, PIPError, PkgNotFoundError +from .utils import ver_to_tuple +from .abc import ABCPackage + +try: + from urllib.request import urlopen +except Exception: + from urllib import urlopen + + +class PyPIPackage(ABCPackage): + __slots__ = ['pkg', 'has_custom_index', 'index', 'verbose'] + + def __init__(self, pkg, index=None, verbose=False): + """ + Args: + pkg (str): name of package + index (str): alternative index, if not given default for *pip* will be used. Include + full index url, e.g. https://example.com/simple + verbose (bool): display verbose messages + """ + self.pkg = pkg + self.verbose = verbose + if index: + self.index = index.rstrip('/') + self.has_custom_index = True + else: + self.index = "https://pypi.python.org/simple" + self.has_custom_index = False + + def upgrade(self, dependencies=False, prerelease=False, force=False): + """ + Upgrade the package unconditionaly + Args: + dependencies: update package dependencies if True (see pip --no-deps) + prerelease: update to pre-release and development versions + force: reinstall all packages even if they are already up-to-date + Returns True if pip was sucessful + """ + pip_args = ['install', self.pkg] + + found = self._get_current() != (-1) + if found: + pip_args.append("--upgrade") + + if force: + pip_args.append("--force-reinstall") + + if not dependencies: + pip_args.append("--no-deps") + + if prerelease: + pip_args.append("--pre") + + proxy = os.environ.get('http_proxy') + if proxy: + pip_args.extend(['--proxy', proxy]) + + if self.has_custom_index: + pip_args.extend(['-i', self.index]) + + try: + ecode = pip.main(args=pip_args) + except TypeError: + # pip changed in 0.6.0 from initial_args to args, this is for + # backwards compatibility can be removed when pip 0.5 is no longer + # in use at all (approx. year 2025) + ecode = pip.main(initial_args=pip_args) + + if ecode != 0: + raise PIPError(ecode) + + def _get_newest_version(self): + url = "{}/{}/".format(self.index, self.pkg) + html = urlopen(url) + if html.getcode() != 200: + raise PkgNotFoundError + pattr = re.compile(r'>{}-(.+?)<'.format(self.pkg), flags=re.I) + versions = map(ver_to_tuple, pattr.findall(str(html.read()))) + if not versions: + raise NoVersionsError + return max(versions) diff --git a/autoupgrade/utils.py b/autoupgrade/utils.py index c71e717..eabcd15 100644 --- a/autoupgrade/utils.py +++ b/autoupgrade/utils.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- -import re +from re import compile as r + + +RE_DIGIT = r("(\d+)") +RE_NONDIGIT = r(r'\D+') def normalize_version(version): @@ -15,7 +19,7 @@ def normalize_version(version): try: rv.append(int(x)) except ValueError: - for y in re.split("([0-9]+)", x): + for y in RE_DIGIT.split(x): if y == '': continue try: @@ -29,4 +33,4 @@ def ver_to_tuple(value): """ Convert version like string to a tuple of integers. """ - return tuple(int(_f) for _f in re.split(r'\D+', value) if _f) + return tuple(int(_f) for _f in RE_NONDIGIT.split(value) if _f) diff --git a/tests/test_autoupgrade.py b/tests/test_autoupgrade.py index 1711142..ce97cfa 100644 --- a/tests/test_autoupgrade.py +++ b/tests/test_autoupgrade.py @@ -2,7 +2,8 @@ from unittest import TestCase -from autoupgrade import Package, ver_to_tuple +from autoupgrade import Package +from autoupgrade.utils import ver_to_tuple class TestFunctions(TestCase): @@ -27,12 +28,23 @@ def test_ver_to_tuple(self): ver_to_tuple('1.2.3'), ver_to_tuple('1.2.3')) - def test_upgrade_default(self): - inst = Package("pip", verbose=True) + def test_pypi_upgrade_default(self): + inst = Package.pypi( + pkg="pip", + verbose=True) inst.smartupgrade(restart=False) - def test_upgrade_index(self): - inst = Package("pip", - "https://pypi.python.org/simple", - verbose=True) + def test_pypi_upgrade_index(self): + inst = Package.pypi( + pkg="pip", + index="https://pypi.python.org/simple", + verbose=True) + inst.smartupgrade(restart=False) + + def test_github_upgrade_default(self): + inst = Package.github( + pkg="autoupgrade", + user="vuolter", + repo="autoupgrade-ng", + verbose=True) inst.smartupgrade(restart=False)