# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. from __future__ import absolute_import, division, print_function import click import os import datetime from typing import TYPE_CHECKING, Dict, Optional, Callable, Iterable from incremental import Version if TYPE_CHECKING: from typing_extensions import Protocol class _ReadableWritable(Protocol): def read(self): # type: () -> bytes pass def write(self, v): # type: (bytes) -> object pass def __enter__(self): # type: () -> _ReadableWritable pass def __exit__(self, *args, **kwargs): # type: (object, object) -> Optional[bool] pass # FilePath is missing type annotations # https://twistedmatrix.com/trac/ticket/10148 class FilePath(object): def __init__(self, path): # type: (str) -> None self.path = path def child(self, v): # type: (str) -> FilePath pass def isdir(self): # type: () -> bool pass def isfile(self): # type: () -> bool pass def getContent(self): # type: () -> bytes pass def open(self, mode): # type: (str) -> _ReadableWritable pass def walk(self): # type: () -> Iterable[FilePath] pass else: from twisted.python.filepath import FilePath _VERSIONPY_TEMPLATE = '''""" Provides {package} version information. """ # This file is auto-generated! Do not edit! # Use `python -m incremental.update {package}` to change this file. from incremental import Version __version__ = {version_repr} __all__ = ["__version__"] ''' _YEAR_START = 2000 def _findPath(path, package): # type: (str, str) -> FilePath cwd = FilePath(path) src_dir = cwd.child("src").child(package.lower()) current_dir = cwd.child(package.lower()) if src_dir.isdir(): return src_dir elif current_dir.isdir(): return current_dir else: raise ValueError( "Can't find under `./src` or `./`. Check the " "package name is right (note that we expect your " "package name to be lower cased), or pass it using " "'--path'." ) def _existing_version(path): # type: (FilePath) -> Version version_info = {} # type: Dict[str, Version] with path.child("_version.py").open("r") as f: exec(f.read(), version_info) return version_info["__version__"] def _run( package, # type: str path, # type: Optional[str] newversion, # type: Optional[str] patch, # type: bool rc, # type: bool post, # type: bool dev, # type: bool create, # type: bool _date=None, # type: Optional[datetime.date] _getcwd=None, # type: Optional[Callable[[], str]] _print=print, # type: Callable[[object], object] ): # type: (...) -> None if not _getcwd: _getcwd = os.getcwd if not _date: _date = datetime.date.today() if type(package) != str: package = package.encode("utf8") # type: ignore[assignment] _path = FilePath(path) if path else _findPath(_getcwd(), package) if ( newversion and patch or newversion and dev or newversion and rc or newversion and post ): raise ValueError("Only give --newversion") if dev and patch or dev and rc or dev and post: raise ValueError("Only give --dev") if ( create and dev or create and patch or create and rc or create and post or create and newversion ): raise ValueError("Only give --create") if newversion: from pkg_resources import parse_version existing = _existing_version(_path) st_version = parse_version(newversion)._version # type: ignore[attr-defined] release = list(st_version.release) minor = 0 micro = 0 if len(release) == 1: (major,) = release elif len(release) == 2: major, minor = release else: major, minor, micro = release v = Version( package, major, minor, micro, release_candidate=st_version.pre[1] if st_version.pre else None, post=st_version.post[1] if st_version.post else None, dev=st_version.dev[1] if st_version.dev else None, ) elif create: v = Version(package, _date.year - _YEAR_START, _date.month, 0) existing = v elif rc and not patch: existing = _existing_version(_path) if existing.release_candidate: v = Version( package, existing.major, existing.minor, existing.micro, existing.release_candidate + 1, ) else: v = Version(package, _date.year - _YEAR_START, _date.month, 0, 1) elif patch: existing = _existing_version(_path) v = Version( package, existing.major, existing.minor, existing.micro + 1, 1 if rc else None, ) elif post: existing = _existing_version(_path) if existing.post is None: _post = 0 else: _post = existing.post + 1 v = Version(package, existing.major, existing.minor, existing.micro, post=_post) elif dev: existing = _existing_version(_path) if existing.dev is None: _dev = 0 else: _dev = existing.dev + 1 v = Version( package, existing.major, existing.minor, existing.micro, existing.release_candidate, dev=_dev, ) else: existing = _existing_version(_path) if existing.release_candidate: v = Version(package, existing.major, existing.minor, existing.micro) else: raise ValueError("You need to issue a rc before updating the major/minor") NEXT_repr = repr(Version(package, "NEXT", 0, 0)).split("#")[0].replace("'", '"') NEXT_repr_bytes = NEXT_repr.encode("utf8") version_repr = repr(v).split("#")[0].replace("'", '"') version_repr_bytes = version_repr.encode("utf8") existing_version_repr = repr(existing).split("#")[0].replace("'", '"') existing_version_repr_bytes = existing_version_repr.encode("utf8") _print("Updating codebase to %s" % (v.public())) for x in _path.walk(): if not x.isfile(): continue original_content = x.getContent() content = original_content # Replace previous release_candidate calls to the new one if existing.release_candidate: content = content.replace(existing_version_repr_bytes, version_repr_bytes) content = content.replace( (package.encode("utf8") + b" " + existing.public().encode("utf8")), (package.encode("utf8") + b" " + v.public().encode("utf8")), ) # Replace NEXT Version calls with the new one content = content.replace(NEXT_repr_bytes, version_repr_bytes) content = content.replace( NEXT_repr_bytes.replace(b"'", b'"'), version_repr_bytes ) # Replace NEXT with content = content.replace( package.encode("utf8") + b" NEXT", (package.encode("utf8") + b" " + v.public().encode("utf8")), ) if content != original_content: _print("Updating %s" % (x.path,)) with x.open("w") as f: f.write(content) _print("Updating %s/_version.py" % (_path.path)) with _path.child("_version.py").open("w") as f: f.write( ( _VERSIONPY_TEMPLATE.format(package=package, version_repr=version_repr) ).encode("utf8") ) @click.command() @click.argument("package") @click.option("--path", default=None) @click.option("--newversion", default=None) @click.option("--patch", is_flag=True) @click.option("--rc", is_flag=True) @click.option("--post", is_flag=True) @click.option("--dev", is_flag=True) @click.option("--create", is_flag=True) def run( package, # type: str path, # type: Optional[str] newversion, # type: Optional[str] patch, # type: bool rc, # type: bool post, # type: bool dev, # type: bool create, # type: bool ): # type: (...) -> None return _run( package=package, path=path, newversion=newversion, patch=patch, rc=rc, post=post, dev=dev, create=create, ) if __name__ == "__main__": # pragma: no cover run()