From fc92fb425c19d0a209e078017c237ead8579e384 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 17 Oct 2020 14:43:53 +0200 Subject: [PATCH 01/86] Fix #176: Create semver 3.0.0-dev.1 * Remove targets py27, py34, py35, and pypy from `tox.ini` * Update `README.rst` and remove anything related to Python2 Mention maintenance branch `maint/v2` * `setup.py` - Update Trove classifiers - Require now Python >=3.6.* - Remove Tox and Clean classes, try to make it as simple as possible - Extract metadata directly from source (affects all the __version__, __author__ etc. variables) * `setup.cfg` - Add pycodestyle section - Ignore venv directory * `semver.py` - Change version number to "3.0.0-dev.1" - Remove old code related to Python2 - Adjust Python2 vs. Python3 str/bytes - Add wheel as another test requirement - Add type annotations - Remove data types and return types from docstring * `tox.ini` - Remove py27, py34, and py35 (out of maintenance) - Add docs to default testenv - Remove --universal from bdist_wheel - Add mypy target * test suite: - Split test suite into separate files under tests/ dir - Move conftests.py -> tests * Travis: Remove old versions, integrate new - Remove 2.7, 3.4, 3.5, and pypy - Integrate 3.8, 3.9-dev and nightly builds - Allow nightly to fail - Add mypy test * Add .editorconfig to have a consistent editor setup * Add supported python versions to black config (cherry picked from commit d9394af8824109285f5378c086cf644b6725afce) Co-authored-by: Thomas Laferriere --- .editorconfig | 15 + .travis.yml | 36 +- CHANGELOG.rst | 1 - README.rst | 19 +- changelog.d/pr290.trivial.rst | 1 + docs/_static/css/default.css | 6 - docs/_static/css/semver.css | 20 + docs/_templates/layout.html | 4 + docs/conf.py | 93 ++- docs/requirements.txt | 3 +- docs/usage.rst | 2 +- pyproject.toml | 2 +- semver.py | 399 +++++----- setup.cfg | 17 +- setup.py | 105 +-- test_semver.py | 1176 ---------------------------- test_typeerror-274.py | 102 --- conftest.py => tests/conftest.py | 19 +- tests/test_bump.py | 101 +++ tests/test_compare.py | 303 +++++++ tests/test_deprecated_functions.py | 57 ++ tests/test_docstrings.py | 39 + tests/test_format.py | 65 ++ tests/test_immutable.py | 33 + tests/test_index.py | 95 +++ tests/test_match.py | 56 ++ tests/test_max-min.py | 43 + tests/test_parsing.py | 157 ++++ tests/test_pysemver-cli.py | 127 +++ tests/test_replace.py | 50 ++ tests/test_semver.py | 77 ++ tests/test_subclass.py | 19 + tests/test_typeerror-274.py | 94 +++ tox.ini | 23 +- 34 files changed, 1729 insertions(+), 1630 deletions(-) create mode 100644 .editorconfig create mode 100644 changelog.d/pr290.trivial.rst delete mode 100644 docs/_static/css/default.css create mode 100644 docs/_static/css/semver.css create mode 100644 docs/_templates/layout.html delete mode 100644 test_semver.py delete mode 100644 test_typeerror-274.py rename conftest.py => tests/conftest.py (50%) create mode 100644 tests/test_bump.py create mode 100644 tests/test_compare.py create mode 100644 tests/test_deprecated_functions.py create mode 100644 tests/test_docstrings.py create mode 100644 tests/test_format.py create mode 100644 tests/test_immutable.py create mode 100644 tests/test_index.py create mode 100644 tests/test_match.py create mode 100644 tests/test_max-min.py create mode 100644 tests/test_parsing.py create mode 100644 tests/test_pysemver-cli.py create mode 100644 tests/test_replace.py create mode 100644 tests/test_semver.py create mode 100644 tests/test_subclass.py create mode 100644 tests/test_typeerror-274.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..573ac0f4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org +root = true + +[*] +end_of_line = lf +charset = utf-8 + +[*.py] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[docs/Makefile] +indent_style = tab diff --git a/.travis.yml b/.travis.yml index 54165f6e..665ebd19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,27 +3,24 @@ language: python cache: pip before_install: - sudo apt-get install -y python3-dev + - sudo apt-get install -y python3-dev install: - pip install --upgrade pip setuptools - - pip install virtualenv tox + - pip install virtualenv tox wheel + - tox --version script: tox -v matrix: include: - - python: "2.7" - env: TOXENV=py27 - - - python: "3.4" - env: TOXENV=py34 - python: "3.6" env: TOXENV=checks - - python: "3.5" - env: TOXENV=py35 + - python: "3.8" + dist: xenial + env: TOXENV=mypy - python: "3.6" env: TOXENV=py36 @@ -32,5 +29,22 @@ matrix: dist: xenial env: TOXENV=py37 - - python: "pypy" - env: TOXENV=pypy + - python: "3.8" + dist: xenial + env: TOXENV=py38 + + - python: "3.9-dev" + dist: bionic + env: TOXENV=py39 + + - python: "nightly" + dist: bionic + env: TOXENV=py310 + + - python: "3.8" + dist: xenial + env: TOXENV=mypy + +jobs: + allow_failures: + - python: "nightly" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 10a8d20f..7427340f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,7 +39,6 @@ Features n/a - Bug Fixes --------- diff --git a/README.rst b/README.rst index 0a1fe664..8f777ed3 100644 --- a/README.rst +++ b/README.rst @@ -9,23 +9,22 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. teaser-end -.. warning:: +.. note:: - As anything comes to an end, this project will focus on Python 3.x only. - New features and bugfixes will be integrated into the 3.x.y branch only. + This project works for Python 3.6 and greater only. If you are + looking for a compatible version for Python 2, use the + maintenance branch |MAINT|_. - Major version 3 of semver will contain some incompatible changes: - - * removes support for Python 2.7 and 3.3 - * removes deprecated functions. - - The last version of semver which supports Python 2.7 and 3.4 will be - 2.10.x. However, keep in mind, version 2.10.x is frozen: no new + The last version of semver which supports Python 2.7 to 3.5 will be + 2.11.x. However, keep in mind, version 2.11.x is frozen: no new features nor backports will be integrated. We recommend to upgrade your workflow to Python 3.x to gain support, bugfixes, and new features. +.. |MAINT| replace:: ``maint/v2`` +.. _MAINT: https://github.com/python-semver/python-semver/tree/maint/v2 + The module follows the ``MAJOR.MINOR.PATCH`` style: * ``MAJOR`` version when you make incompatible API changes, diff --git a/changelog.d/pr290.trivial.rst b/changelog.d/pr290.trivial.rst new file mode 100644 index 00000000..9dc914f3 --- /dev/null +++ b/changelog.d/pr290.trivial.rst @@ -0,0 +1 @@ +Add supported Python versions to :command:`black`. diff --git a/docs/_static/css/default.css b/docs/_static/css/default.css deleted file mode 100644 index ed7cf80a..00000000 --- a/docs/_static/css/default.css +++ /dev/null @@ -1,6 +0,0 @@ -/* Customize logo width */ - -.wy-side-nav-search > a img.logo { - width: 6em; - background: white; -} diff --git a/docs/_static/css/semver.css b/docs/_static/css/semver.css new file mode 100644 index 00000000..88b6cba6 --- /dev/null +++ b/docs/_static/css/semver.css @@ -0,0 +1,20 @@ +/* +*/ + +.py.method { + padding-top: 0.25em; + padding-bottom: 1.25em; + border-top: 1px solid #EEE; +} + +.py.function{ + padding-top: 1.25em; +} + +.related.bottom { + margin-top: 1em; +} + +nav#rellinks { + float: left; +} \ No newline at end of file diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000..6bae6eed --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,4 @@ +{# + Import the theme's layout. +#} +{% extends "!layout.html" %} diff --git a/docs/conf.py b/docs/conf.py index a07c94a8..5d7f7eef 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,6 +23,7 @@ from semver import __version__ # noqa: E402 + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -94,38 +95,74 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -# html_theme = 'alabaster' -html_theme = "sphinx_rtd_theme" +html_theme = "alabaster" +templates_path = ["_templates"] -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} +GITHUB_URL = "https://github.com/python-semver/python-semver" + +html_theme_options = { + # -- Basics + #: Text blurb about your project to appear under the logo: + # "description": "Semantic versioning", + #: Makes the sidebar "fixed" or pinned in place: + "fixed_sidebar": True, + #: Relative path to $PROJECT/_static to logo image: + # "logo": "logo.svg", + #: Set to true to insert your site's project name under + #: the logo: + # "logo_name": True, + #: CSS width specifier controller default sidebar width: + "sidebar_width": "25%", + #: CSS width specifier controlling default content/page width: + "page_width": "auto", + #: CSS width specifier controlling default body text width: + "body_max_width": "auto", + # + # -- Service Links and Badges + #: Contains project name and user of GitHub: + "github_user": "python-semver", + "github_repo": "python-semver", + #: whether to link to your GitHub: + "github_button": True, + #: + "github_type": "star", + #: whether to apply a ‘Fork me on Github’ banner + #: in the top right corner of the page: + # "github_banner": True, + # + # -- Non-service sidebar control + #: Dictionary mapping link names to link targets: + "extra_nav_links": { + "PyPI": "https://pypi.org/project/semver/", + "Libraries.io": "https://libraries.io/pypi/semver", + }, + #: Boolean determining whether all TOC entries that + #: are not ancestors of the current page are collapsed: + "sidebar_collapse": True, + # + # -- Header/footer options + #: used to display next and previous links above and + #: below the main page content + "show_relbars": True, + "show_relbar_top": True, + # + # -- Style colors + # "anchor": "", + # "anchor_hover_bg": "", + # "anchor_hover_fg": "", + "narrow_sidebar_fg": "lightgray", + # + # -- Fonts + # "code_font_size": "", + "font_family": "", + "head_font_family": "", + "font_size": "1.25rem", +} -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_css_files = ["css/semver.css"] -html_css_files = ["css/default.css"] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - "**": [ - "about.html", - "navigation.html", - "relations.html", # needs 'show_related': True theme option to display - "searchbox.html", - "donate.html", - ] -} - -html_logo = "logo.svg" +# html_logo = "logo.svg" # -- Options for HTMLHelp output ------------------------------------------ diff --git a/docs/requirements.txt b/docs/requirements.txt index 28467ce6..26233642 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ # requirements file for documentation sphinx -sphinx_rtd_theme sphinx-argparse +# sphinx_rtd_theme +guzzle_sphinx_theme \ No newline at end of file diff --git a/docs/usage.rst b/docs/usage.rst index cda55670..10ca3404 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -455,7 +455,7 @@ To compare two versions depends on your type: >>> v > dict(major=1, unknown=42) Traceback (most recent call last): ... - TypeError: __init__() got an unexpected keyword argument 'unknown' + TypeError: ... got an unexpected keyword argument 'unknown' Other types cannot be compared. diff --git a/pyproject.toml b/pyproject.toml index eca41891..7c4419c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 88 -target-version = ['py37'] +target-version = ['py36', 'py37', 'py38'] include = '\.pyi?$' # diff = true exclude = ''' diff --git a/semver.py b/semver.py index ce8816af..09bca561 100644 --- a/semver.py +++ b/semver.py @@ -1,24 +1,36 @@ -"""Python helper for Semantic Versioning (http://semver.org/)""" +"""Python helper for Semantic Versioning (http://semver.org)""" from __future__ import print_function import argparse import collections -from functools import wraps, partial import inspect import re import sys import warnings +from functools import partial, wraps +from types import FrameType +from typing import ( + Any, + Callable, + Collection, + Dict, + Iterable, + Iterator, + List, + Optional, + SupportsInt, + Tuple, + TypeVar, + Union, + cast, +) - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - - -__version__ = "2.13.0" +__version__ = "3.0.0-dev.1" __author__ = "Kostiantyn Rybnikov" __author_email__ = "k-bx@k-bx.com" __maintainer__ = ["Sebastien Celles", "Tom Schraitle"] __maintainer_email__ = "s.celles@gmail.com" +__description__ = "Python helper for Semantic Versioning (http://semver.org)" #: Our public interface __all__ = ( @@ -53,99 +65,98 @@ "VersionInfo", ) + #: Contains the implemented semver.org version of the spec SEMVER_SPEC_VERSION = "2.0.0" -if not hasattr(__builtins__, "cmp"): - - def cmp(a, b): - """Return negative if ab.""" - return (a > b) - (a < b) - - -if PY3: # pragma: no cover - string_types = str, bytes - text_type = str - binary_type = bytes - - def b(s): - return s.encode("latin-1") - - def u(s): - return s - +# Types +VersionPart = Union[int, Optional[str]] +Comparable = Union["VersionInfo", Dict[str, VersionPart], Collection[VersionPart], str] +Comparator = Callable[["VersionInfo", Comparable], bool] +String = Union[str, bytes] +VersionTuple = Tuple[int, int, int, Optional[str], Optional[str]] +VersionDict = Dict[str, VersionPart] +VersionIterator = Iterator[VersionPart] -else: # pragma: no cover - string_types = unicode, str - text_type = unicode - binary_type = str - def b(s): - return s +def cmp(a, b): + """Return negative if ab.""" + return (a > b) - (a < b) - # Workaround for standalone backslash - def u(s): - return unicode(s.replace(r"\\", r"\\\\"), "unicode_escape") - -def ensure_str(s, encoding="utf-8", errors="strict"): +def ensure_str(s: String, encoding="utf-8", errors="strict") -> str: # Taken from six project """ Coerce *s* to `str`. - For Python 2: - - `unicode` -> encoded to `str` - - `str` -> `str` - - For Python 3: - - `str` -> `str` - - `bytes` -> decoded to `str` + * `str` -> `str` + * `bytes` -> decoded to `str` + + :param s: the string to convert + :type s: str | bytes + :param encoding: the encoding to apply, defaults to "utf-8" + :type encoding: str + :param errors: set a different error handling scheme, + defaults to "strict". + Other possible values are `ignore`, `replace`, and + `xmlcharrefreplace` as well as any other name + registered with :func:`codecs.register_error`. + :type errors: str + :raises TypeError: if ``s`` is not str or bytes type + :return: the converted string + :rtype: str """ - if not isinstance(s, (text_type, binary_type)): - raise TypeError("not expecting type '%s'" % type(s)) - if PY2 and isinstance(s, text_type): - s = s.encode(encoding, errors) - elif PY3 and isinstance(s, binary_type): + if isinstance(s, bytes): s = s.decode(encoding, errors) + elif not isinstance(s, String.__args__): # type: ignore + raise TypeError("not expecting type '%s'" % type(s)) return s -def deprecated(func=None, replace=None, version=None, category=DeprecationWarning): +F = TypeVar("F", bound=Callable) + + +def deprecated( + func: F = None, + replace: str = None, + version: str = None, + category=DeprecationWarning, +) -> Union[Callable[..., F], partial]: """ Decorates a function to output a deprecation warning. - :param func: the function to decorate (or None) - :param str replace: the function to replace (use the full qualified + :param func: the function to decorate + :param replace: the function to replace (use the full qualified name like ``semver.VersionInfo.bump_major``. - :param str version: the first version when this function was deprecated. + :param version: the first version when this function was deprecated. :param category: allow you to specify the deprecation warning class of your choice. By default, it's :class:`DeprecationWarning`, but you can choose :class:`PendingDeprecationWarning` or a custom class. + :return: decorated function which is marked as deprecated """ if func is None: return partial(deprecated, replace=replace, version=version, category=category) @wraps(func) - def wrapper(*args, **kwargs): - msg = ["Function '{m}.{f}' is deprecated."] + def wrapper(*args, **kwargs) -> Callable[..., F]: + msg_list = ["Function '{m}.{f}' is deprecated."] if version: - msg.append("Deprecated since version {v}. ") - msg.append("This function will be removed in semver 3.") + msg_list.append("Deprecated since version {v}. ") + msg_list.append("This function will be removed in semver 3.") if replace: - msg.append("Use {r!r} instead.") + msg_list.append("Use {r!r} instead.") else: - msg.append("Use the respective 'semver.VersionInfo.{r}' instead.") + msg_list.append("Use the respective 'semver.VersionInfo.{r}' instead.") - # hasattr is needed for Python2 compatibility: - f = func.__qualname__ if hasattr(func, "__qualname__") else func.__name__ + f = cast(F, func).__qualname__ r = replace or f - frame = inspect.currentframe().f_back + frame = cast(FrameType, cast(FrameType, inspect.currentframe()).f_back) - msg = " ".join(msg) + msg = " ".join(msg_list) warnings.warn_explicit( msg.format(m=func.__module__, f=f, r=r, v=version), category=category, @@ -156,7 +167,7 @@ def wrapper(*args, **kwargs): # https://docs.python.org/3/library/inspect.html#the-interpreter-stack # better remove the interpreter stack: del frame - return func(*args, **kwargs) + return func(*args, **kwargs) # type: ignore return wrapper @@ -173,7 +184,6 @@ def parse(version): :return: dictionary with the keys 'build', 'major', 'minor', 'patch', and 'prerelease'. The prerelease or build keys can be None if not provided - :rtype: dict >>> ver = semver.parse('3.4.5-pre.2+build.4') >>> ver['major'] @@ -190,12 +200,18 @@ def parse(version): return VersionInfo.parse(version).to_dict() -def comparator(operator): +def comparator(operator: Comparator) -> Comparator: """Wrap a VersionInfo binary op method in a type-check.""" @wraps(operator) - def wrapper(self, other): - comparable_types = (VersionInfo, dict, tuple, list, text_type, binary_type) + def wrapper(self: "VersionInfo", other: Comparable) -> bool: + comparable_types = ( + VersionInfo, + dict, + tuple, + list, + *String.__args__, # type: ignore + ) if not isinstance(other, comparable_types): raise TypeError( "other type %r must be in %r" % (type(other), comparable_types) @@ -205,16 +221,16 @@ def wrapper(self, other): return wrapper -class VersionInfo(object): +class VersionInfo: """ A semver compatible version class. - :param int major: version when you make incompatible API changes. - :param int minor: version when you add functionality in - a backwards-compatible manner. - :param int patch: version when you make backwards-compatible bug fixes. - :param str prerelease: an optional prerelease string - :param str build: an optional build string + :param major: version when you make incompatible API changes. + :param minor: version when you add functionality in + a backwards-compatible manner. + :param patch: version when you make backwards-compatible bug fixes. + :param prerelease: an optional prerelease string + :param build: an optional build string """ __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") @@ -242,17 +258,18 @@ class VersionInfo(object): re.VERBOSE, ) - def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): + def __init__( + self, + major: SupportsInt, + minor: SupportsInt = 0, + patch: SupportsInt = 0, + prerelease: Union[String, int] = None, + build: Union[String, int] = None, + ): # Build a dictionary of the arguments except prerelease and build - version_parts = { - "major": major, - "minor": minor, - "patch": patch, - } + version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)} for name, value in version_parts.items(): - value = int(value) - version_parts[name] = value if value < 0: raise ValueError( "{!r} is negative. A version can only be positive.".format(name) @@ -265,7 +282,7 @@ def __init__(self, major, minor=0, patch=0, prerelease=None, build=None): self._build = None if build is None else str(build) @property - def major(self): + def major(self) -> int: """The major part of a version (read-only).""" return self._major @@ -274,7 +291,7 @@ def major(self, value): raise AttributeError("attribute 'major' is readonly") @property - def minor(self): + def minor(self) -> int: """The minor part of a version (read-only).""" return self._minor @@ -283,7 +300,7 @@ def minor(self, value): raise AttributeError("attribute 'minor' is readonly") @property - def patch(self): + def patch(self) -> int: """The patch part of a version (read-only).""" return self._patch @@ -292,7 +309,7 @@ def patch(self, value): raise AttributeError("attribute 'patch' is readonly") @property - def prerelease(self): + def prerelease(self) -> Optional[str]: """The prerelease part of a version (read-only).""" return self._prerelease @@ -301,7 +318,7 @@ def prerelease(self, value): raise AttributeError("attribute 'prerelease' is readonly") @property - def build(self): + def build(self) -> Optional[str]: """The build part of a version (read-only).""" return self._build @@ -309,7 +326,7 @@ def build(self): def build(self, value): raise AttributeError("attribute 'build' is readonly") - def to_tuple(self): + def to_tuple(self) -> VersionTuple: """ Convert the VersionInfo object to a tuple. @@ -318,14 +335,13 @@ def to_tuple(self): make this function available in the public API. :return: a tuple with all the parts - :rtype: tuple >>> semver.VersionInfo(5, 3, 1).to_tuple() (5, 3, 1, None, None) """ return (self.major, self.minor, self.patch, self.prerelease, self.build) - def to_dict(self): + def to_dict(self) -> VersionDict: """ Convert the VersionInfo object to an OrderedDict. @@ -335,7 +351,6 @@ def to_dict(self): :return: an OrderedDict with the keys in the order ``major``, ``minor``, ``patch``, ``prerelease``, and ``build``. - :rtype: :class:`collections.OrderedDict` >>> semver.VersionInfo(3, 2, 1).to_dict() OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \ @@ -351,31 +366,16 @@ def to_dict(self): ) ) - # For compatibility reasons: - @deprecated(replace="semver.VersionInfo.to_tuple", version="2.10.0") - def _astuple(self): - return self.to_tuple() # pragma: no cover - - _astuple.__doc__ = to_tuple.__doc__ - - @deprecated(replace="semver.VersionInfo.to_dict", version="2.10.0") - def _asdict(self): - return self.to_dict() # pragma: no cover - - _asdict.__doc__ = to_dict.__doc__ - - def __iter__(self): + def __iter__(self) -> VersionIterator: """Implement iter(self).""" - # As long as we support Py2.7, we can't use the "yield from" syntax - for v in self.to_tuple(): - yield v + yield from self.to_tuple() @staticmethod - def _increment_string(string): + def _increment_string(string: str) -> str: """ Look for the last sequence of number(s) in a string and increment. - :param str string: the string to search for. + :param string: the string to search for. :return: the incremented string Source: @@ -388,13 +388,12 @@ def _increment_string(string): string = string[: max(end - len(next_), start)] + next_ + string[end:] return string - def bump_major(self): + def bump_major(self) -> "VersionInfo": """ Raise the major part of the version, return a new object but leave self untouched. :return: new object with the raised major part - :rtype: :class:`VersionInfo` >>> ver = semver.VersionInfo.parse("3.4.5") >>> ver.bump_major() @@ -403,13 +402,12 @@ def bump_major(self): cls = type(self) return cls(self._major + 1) - def bump_minor(self): + def bump_minor(self) -> "VersionInfo": """ Raise the minor part of the version, return a new object but leave self untouched. :return: new object with the raised minor part - :rtype: :class:`VersionInfo` >>> ver = semver.VersionInfo.parse("3.4.5") >>> ver.bump_minor() @@ -418,13 +416,12 @@ def bump_minor(self): cls = type(self) return cls(self._major, self._minor + 1) - def bump_patch(self): + def bump_patch(self) -> "VersionInfo": """ Raise the patch part of the version, return a new object but leave self untouched. :return: new object with the raised patch part - :rtype: :class:`VersionInfo` >>> ver = semver.VersionInfo.parse("3.4.5") >>> ver.bump_patch() @@ -433,14 +430,13 @@ def bump_patch(self): cls = type(self) return cls(self._major, self._minor, self._patch + 1) - def bump_prerelease(self, token="rc"): + def bump_prerelease(self, token: str = "rc") -> "VersionInfo": """ Raise the prerelease part of the version, return a new object but leave self untouched. :param token: defaults to 'rc' :return: new object with the raised prerelease part - :rtype: :class:`VersionInfo` >>> ver = semver.VersionInfo.parse("3.4.5-rc.1") >>> ver.bump_prerelease() @@ -451,14 +447,13 @@ def bump_prerelease(self, token="rc"): prerelease = cls._increment_string(self._prerelease or (token or "rc") + ".0") return cls(self._major, self._minor, self._patch, prerelease) - def bump_build(self, token="build"): + def bump_build(self, token: str = "build") -> "VersionInfo": """ Raise the build part of the version, return a new object but leave self untouched. :param token: defaults to 'build' :return: new object with the raised build part - :rtype: :class:`VersionInfo` >>> ver = semver.VersionInfo.parse("3.4.5-rc.1+build.9") >>> ver.bump_build() @@ -469,15 +464,13 @@ def bump_build(self, token="build"): build = cls._increment_string(self._build or (token or "build") + ".0") return cls(self._major, self._minor, self._patch, self._prerelease, build) - def compare(self, other): + def compare(self, other: Comparable) -> int: """ Compare self with other. - :param other: the second version (can be string, a dict, tuple/list, or - a VersionInfo instance) + :param other: the second version :return: The return value is negative if ver1 < ver2, zero if ver1 == ver2 and strictly positive if ver1 > ver2 - :rtype: int >>> semver.VersionInfo.parse("1.0.0").compare("2.0.0") -1 @@ -489,7 +482,7 @@ def compare(self, other): 0 """ cls = type(self) - if isinstance(other, string_types): + if isinstance(other, String.__args__): # type: ignore other = cls.parse(other) elif isinstance(other, dict): other = cls(**other) @@ -497,9 +490,8 @@ def compare(self, other): other = cls(*other) elif not isinstance(other, cls): raise TypeError( - "Expected str or {} instance, but got {}".format( - cls.__name__, type(other) - ) + f"Expected str, bytes, dict, tuple, list, or {cls.__name__} instance, " + f"but got {type(other)}" ) v1 = self.to_tuple()[:3] @@ -520,7 +512,7 @@ def compare(self, other): return rccmp - def next_version(self, part, prerelease_token="rc"): + def next_version(self, part: str, prerelease_token: str = "rc") -> "VersionInfo": """ Determines next version, preserving natural order. @@ -538,7 +530,6 @@ def next_version(self, part, prerelease_token="rc"): :param part: One of "major", "minor", "patch", or "prerelease" :param prerelease_token: prefix string of prerelease, defaults to 'rc' :return: new object with the appropriate part raised - :rtype: :class:`VersionInfo` """ validparts = { "major", @@ -569,30 +560,32 @@ def next_version(self, part, prerelease_token="rc"): return version.bump_prerelease(prerelease_token) @comparator - def __eq__(self, other): + def __eq__(self, other: Comparable) -> bool: # type: ignore return self.compare(other) == 0 @comparator - def __ne__(self, other): + def __ne__(self, other: Comparable) -> bool: # type: ignore return self.compare(other) != 0 @comparator - def __lt__(self, other): + def __lt__(self, other: Comparable) -> bool: return self.compare(other) < 0 @comparator - def __le__(self, other): + def __le__(self, other: Comparable) -> bool: return self.compare(other) <= 0 @comparator - def __gt__(self, other): + def __gt__(self, other: Comparable) -> bool: return self.compare(other) > 0 @comparator - def __ge__(self, other): + def __ge__(self, other: Comparable) -> bool: return self.compare(other) >= 0 - def __getitem__(self, index): + def __getitem__( + self, index: Union[int, slice] + ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]: """ self.__getitem__(index) <==> self[index] @@ -602,7 +595,7 @@ def __getitem__(self, index): :param Union[int, slice] index: a positive integer indicating the offset or a :func:`slice` object - :raises: IndexError, if index is beyond the range or a part is None + :raises IndexError: if index is beyond the range or a part is None :return: the requested part of the version at position index >>> ver = semver.VersionInfo.parse("3.4.5") @@ -611,6 +604,7 @@ def __getitem__(self, index): """ if isinstance(index, int): index = slice(index, index + 1) + index = cast(slice, index) if ( isinstance(index, slice) @@ -619,19 +613,21 @@ def __getitem__(self, index): ): raise IndexError("Version index cannot be negative") - part = tuple(filter(lambda p: p is not None, self.to_tuple()[index])) + part = tuple( + filter(lambda p: p is not None, cast(Iterable, self.to_tuple()[index])) + ) if len(part) == 1: - part = part[0] + return part[0] elif not part: raise IndexError("Version part undefined") return part - def __repr__(self): + def __repr__(self) -> str: s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) return "%s(%s)" % (type(self).__name__, s) - def __str__(self): + def __str__(self) -> str: """str(self)""" version = "%d.%d.%d" % (self.major, self.minor, self.patch) if self.prerelease: @@ -640,15 +636,14 @@ def __str__(self): version += "+%s" % self.build return version - def __hash__(self): + def __hash__(self) -> int: return hash(self.to_tuple()[:4]) - def finalize_version(self): + def finalize_version(self) -> "VersionInfo": """ Remove any prerelease and build metadata from the version. :return: a new instance with the finalized version string - :rtype: :class:`VersionInfo` >>> str(semver.VersionInfo.parse('1.2.3-rc.5').finalize_version()) '1.2.3' @@ -656,11 +651,11 @@ def finalize_version(self): cls = type(self) return cls(self.major, self.minor, self.patch) - def match(self, match_expr): + def match(self, match_expr: str) -> bool: """ Compare self to match a match expression. - :param str match_expr: operator and version; valid operators are + :param match_expr: operator and version; valid operators are < smaller than > greater than >= greator or equal than @@ -668,7 +663,6 @@ def match(self, match_expr): == equal != not equal :return: True if the expression matches the version, otherwise False - :rtype: bool >>> semver.VersionInfo.parse("2.0.0").match(">=1.0.0") True @@ -704,36 +698,32 @@ def match(self, match_expr): return cmp_res in possibilities @classmethod - def parse(cls, version): + def parse(cls, version: String) -> "VersionInfo": """ Parse version string to a VersionInfo instance. - :param version: version string - :return: a :class:`VersionInfo` instance - :raises: :class:`ValueError` - :rtype: :class:`VersionInfo` - .. versionchanged:: 2.11.0 Changed method from static to classmethod to allow subclasses. + :param version: version string + :return: a :class:`VersionInfo` instance + :raises ValueError: if version is invalid + >>> semver.VersionInfo.parse('3.4.5-pre.2+build.4') VersionInfo(major=3, minor=4, patch=5, \ prerelease='pre.2', build='build.4') """ - match = cls._REGEX.match(ensure_str(version)) + version_str = ensure_str(version) + match = cls._REGEX.match(version_str) if match is None: - raise ValueError("%s is not valid SemVer string" % version) + raise ValueError(f"{version_str} is not valid SemVer string") - version_parts = match.groupdict() + matched_version_parts: Dict[str, Any] = match.groupdict() - version_parts["major"] = int(version_parts["major"]) - version_parts["minor"] = int(version_parts["minor"]) - version_parts["patch"] = int(version_parts["patch"]) + return cls(**matched_version_parts) - return cls(**version_parts) - - def replace(self, **parts): + def replace(self, **parts: Union[int, Optional[str]]) -> "VersionInfo": """ Replace one or more parts of a version and return a new :class:`VersionInfo` object, but leave self untouched @@ -741,16 +731,16 @@ def replace(self, **parts): .. versionadded:: 2.9.0 Added :func:`VersionInfo.replace` - :param dict parts: the parts to be updated. Valid keys are: + :param parts: the parts to be updated. Valid keys are: ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` :return: the new :class:`VersionInfo` object with the changed parts - :raises: :class:`TypeError`, if ``parts`` contains invalid keys + :raises TypeError: if ``parts`` contains invalid keys """ version = self.to_dict() version.update(parts) try: - return VersionInfo(**version) + return VersionInfo(**version) # type: ignore except TypeError: unknownkeys = set(parts) - set(self.to_dict()) error = "replace() got %d unexpected keyword " "argument(s): %s" % ( @@ -760,16 +750,15 @@ def replace(self, **parts): raise TypeError(error) @classmethod - def isvalid(cls, version): + def isvalid(cls, version: str) -> bool: """ Check if the string is a valid semver version. .. versionadded:: 2.9.1 - :param str version: the version string to check + :param version: the version string to check :return: True if the version string is a valid semver version, False otherwise. - :rtype: bool """ try: cls.parse(version) @@ -791,7 +780,6 @@ def parse_version_info(version): :param version: version string :return: a :class:`VersionInfo` instance - :rtype: :class:`VersionInfo` >>> version_info = semver.VersionInfo.parse("3.4.5-pre.2+build.4") >>> version_info.major @@ -808,14 +796,14 @@ def parse_version_info(version): return VersionInfo.parse(version) -def _nat_cmp(a, b): - def convert(text): - return int(text) if re.match("^[0-9]+$", text) else text +def _nat_cmp(a: Optional[str], b: Optional[str]) -> int: + def convert(text: str) -> Union[int, str]: + return int(text) if re.match("^[0-9]+$", text) else text # type: ignore - def split_key(key): + def split_key(key: str) -> List[Union[int, str]]: return [convert(c) for c in key.split(".")] - def cmp_prerelease_tag(a, b): + def cmp_prerelease_tag(a: Union[int, str], b: Union[int, str]) -> int: if isinstance(a, int) and isinstance(b, int): return cmp(a, b) elif isinstance(a, int): @@ -844,7 +832,6 @@ def compare(ver1, ver2): :param ver2: version string 2 :return: The return value is negative if ver1 < ver2, zero if ver1 == ver2 and strictly positive if ver1 > ver2 - :rtype: int >>> semver.compare("1.0.0", "2.0.0") -1 @@ -862,8 +849,8 @@ def match(version, match_expr): """ Compare two versions strings through a comparison. - :param str version: a version string - :param str match_expr: operator and version; valid operators are + :param version: a version string + :param match_expr: operator and version; valid operators are < smaller than > greater than >= greator or equal than @@ -871,7 +858,6 @@ def match(version, match_expr): == equal != not equal :return: True if the expression matches the version, otherwise False - :rtype: bool >>> semver.match("2.0.0", ">=1.0.0") True @@ -890,12 +876,11 @@ def max_ver(ver1, ver2): :param ver1: version string 1 :param ver2: version string 2 :return: the greater version of the two - :rtype: :class:`VersionInfo` >>> semver.max_ver("1.0.0", "2.0.0") '2.0.0' """ - if isinstance(ver1, string_types): + if isinstance(ver1, String.__args__): ver1 = VersionInfo.parse(ver1) elif not isinstance(ver1, VersionInfo): raise TypeError() @@ -914,7 +899,6 @@ def min_ver(ver1, ver2): :param ver1: version string 1 :param ver2: version string 2 :return: the smaller version of the two - :rtype: :class:`VersionInfo` >>> semver.min_ver("1.0.0", "2.0.0") '1.0.0' @@ -935,13 +919,12 @@ def format_version(major, minor, patch, prerelease=None, build=None): .. deprecated:: 2.10.0 Use ``str(VersionInfo(VERSION)`` instead. - :param int major: the required major part of a version - :param int minor: the required minor part of a version - :param int patch: the required patch part of a version - :param str prerelease: the optional prerelease part of a version - :param str build: the optional build part of a version + :param major: the required major part of a version + :param minor: the required minor part of a version + :param patch: the required patch part of a version + :param prerelease: the optional prerelease part of a version + :param build: the optional build part of a version :return: the formatted string - :rtype: str >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') '3.4.5-pre.2+build.4' @@ -959,7 +942,6 @@ def bump_major(version): :param: version string :return: the raised version string - :rtype: str >>> semver.bump_major("3.4.5") '4.0.0' @@ -977,7 +959,6 @@ def bump_minor(version): :param: version string :return: the raised version string - :rtype: str >>> semver.bump_minor("3.4.5") '3.5.0' @@ -995,7 +976,6 @@ def bump_patch(version): :param: version string :return: the raised version string - :rtype: str >>> semver.bump_patch("3.4.5") '3.4.6' @@ -1014,7 +994,6 @@ def bump_prerelease(version, token="rc"): :param version: version string :param token: defaults to 'rc' :return: the raised version string - :rtype: str >>> semver.bump_prerelease('3.4.5', 'dev') '3.4.5-dev.1' @@ -1033,7 +1012,6 @@ def bump_build(version, token="build"): :param version: version string :param token: defaults to 'build' :return: the raised version string - :rtype: str >>> semver.bump_build('3.4.5-rc.1+build.9') '3.4.5-rc.1+build.10' @@ -1054,7 +1032,6 @@ def finalize_version(version): :param version: version string :return: the finalized version string - :rtype: str >>> semver.finalize_version('1.2.3-rc.5') '1.2.3' @@ -1074,12 +1051,11 @@ def replace(version, **parts): .. versionadded:: 2.9.0 Added :func:`replace` - :param str version: the version string to replace - :param dict parts: the parts to be updated. Valid keys are: + :param version: the version string to replace + :param parts: the parts to be updated. Valid keys are: ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` :return: the replaced version string - :raises: TypeError, if ``parts`` contains invalid keys - :rtype: str + :raises TypeError: if ``parts`` contains invalid keys >>> import semver >>> semver.replace("1.2.3", major=2, patch=10) @@ -1089,7 +1065,7 @@ def replace(version, **parts): # ---- CLI -def cmd_bump(args): +def cmd_bump(args: argparse.Namespace) -> str: """ Subcommand: Bumps a version. @@ -1097,7 +1073,6 @@ def cmd_bump(args): can be major, minor, patch, prerelease, or build :param args: The parsed arguments - :type args: :class:`argparse.Namespace` :return: the new, bumped version """ maptable = { @@ -1114,55 +1089,51 @@ def cmd_bump(args): ver = VersionInfo.parse(args.version) # get the respective method and call it - func = getattr(ver, maptable[args.bump]) + func = getattr(ver, maptable[cast(str, args.bump)]) return str(func()) -def cmd_check(args): +def cmd_check(args: argparse.Namespace) -> None: """ Subcommand: Checks if a string is a valid semver version. Synopsis: check :param args: The parsed arguments - :type args: :class:`argparse.Namespace` """ if VersionInfo.isvalid(args.version): return None raise ValueError("Invalid version %r" % args.version) -def cmd_compare(args): +def cmd_compare(args: argparse.Namespace) -> str: """ Subcommand: Compare two versions Synopsis: compare :param args: The parsed arguments - :type args: :class:`argparse.Namespace` """ return str(compare(args.version1, args.version2)) -def cmd_nextver(args): +def cmd_nextver(args: argparse.Namespace) -> str: """ Subcommand: Determines the next version, taking prereleases into account. Synopsis: nextver :param args: The parsed arguments - :type args: :class:`argparse.Namespace` """ version = VersionInfo.parse(args.version) return str(version.next_version(args.part)) -def createparser(): +def createparser() -> argparse.ArgumentParser: """ Create an :class:`argparse.ArgumentParser` instance. :return: parser instance - :rtype: :class:`argparse.ArgumentParser` """ parser = argparse.ArgumentParser(prog=__package__, description=__doc__) @@ -1211,16 +1182,13 @@ def createparser(): return parser -def process(args): +def process(args: argparse.Namespace) -> str: """ Process the input from the CLI. :param args: The parsed arguments - :type args: :class:`argparse.Namespace` :param parser: the parser instance - :type parser: :class:`argparse.ArgumentParser` :return: result of the selected action - :rtype: str """ if not hasattr(args, "func"): args.parser.print_help() @@ -1230,13 +1198,12 @@ def process(args): return args.func(args) -def main(cliargs=None): +def main(cliargs: List[str] = None) -> int: """ Entry point for the application script. :param list cliargs: Arguments to parse or None (=use :class:`sys.argv`) :return: error code - :rtype: int """ try: parser = createparser() diff --git a/setup.cfg b/setup.cfg index 5b2a59b0..5abd4bbb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [tool:pytest] -norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ -testpaths = . docs +norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ +testpaths = tests docs filterwarnings = ignore:Function 'semver.*:DeprecationWarning addopts = @@ -16,6 +16,7 @@ max-line-length = 88 ignore = F821,W503 exclude = .env, + venv, .eggs, .tox, .git, @@ -24,3 +25,15 @@ exclude = dist docs conftest.py + +[pycodestyle] +count = False +# ignore = E226,E302,E41 +max-line-length = 88 +statistics = True +exclude = + .env, + .eggs, + .tox, + .git, + docs diff --git a/setup.py b/setup.py index 746c1436..57ee4b26 100755 --- a/setup.py +++ b/setup.py @@ -1,80 +1,49 @@ -#!/usr/bin/env python -import semver as package -from glob import glob -from os import remove +#!/usr/bin/env python3 +# import semver as package from os.path import dirname, join from setuptools import setup -from setuptools.command.test import test as TestCommand +import re -try: - from setuptools.command.clean import clean as CleanCommand -except ImportError: - from distutils.command.clean import clean as CleanCommand -from shlex import split -from shutil import rmtree +VERSION_MATCH = re.compile(r"__version__ = ['\"]([^'\"]*)['\"]", re.M) -class Tox(TestCommand): - user_options = [("tox-args=", "a", "Arguments to pass to tox")] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.tox_args = None - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - from tox import cmdline - - args = self.tox_args - if args: - args = split(self.tox_args) - errno = cmdline(args=args) - exit(errno) +def read_file(filename): + """ + Read RST file and return content -class Clean(CleanCommand): - def run(self): - CleanCommand.run(self) - delete_in_root = ["build", ".cache", "dist", ".eggs", "*.egg-info", ".tox"] - delete_everywhere = ["__pycache__", "*.pyc"] - for candidate in delete_in_root: - rmtree_glob(candidate) - for visible_dir in glob("[A-Za-z0-9]*"): - for candidate in delete_everywhere: - rmtree_glob(join(visible_dir, candidate)) - rmtree_glob(join(visible_dir, "*", candidate)) + :param filename: the RST file + :return: content of the RST file + """ + with open(join(dirname(__file__), filename)) as f: + return f.read() -def rmtree_glob(file_glob): - for fobj in glob(file_glob): - try: - rmtree(fobj) - print("%s/ removed ..." % fobj) - except OSError: - try: - remove(fobj) - print("%s removed ..." % fobj) - except OSError: - pass +def find_meta(meta): + """ + Extract __*meta*__ from META_FILE. + """ + meta_match = re.search( + r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M + ) + if meta_match: + return meta_match.group(1) + raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) -def read_file(filename): - with open(join(dirname(__file__), filename)) as f: - return f.read() +NAME = "semver" +META_FILE = read_file("semver.py") +# ----------------------------------------------------------------------------- setup( - name=package.__name__, - version=package.__version__, - description=package.__doc__.strip(), + name=NAME, + version=find_meta("version"), + description=find_meta("description").strip(), long_description=read_file("README.rst"), long_description_content_type="text/x-rst", - author=package.__author__, - author_email=package.__author_email__, + author=find_meta("author"), + author_email=find_meta("author_email"), url="https://github.com/python-semver/python-semver", download_url="https://github.com/python-semver/python-semver/downloads", project_urls={ @@ -82,7 +51,7 @@ def read_file(filename): "Releases": "https://github.com/python-semver/python-semver/releases", "Bug Tracker": "https://github.com/python-semver/python-semver/issues", }, - py_modules=[package.__name__], + py_modules=[NAME], include_package_data=True, license="BSD", classifiers=[ @@ -92,17 +61,15 @@ def read_file(filename): "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + # "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", ], - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", - tests_require=["tox", "virtualenv"], - cmdclass={"clean": Clean, "test": Tox}, + python_requires=">=3.6.*", + tests_require=["tox", "virtualenv", "wheel"], entry_points={"console_scripts": ["pysemver = semver:main"]}, ) diff --git a/test_semver.py b/test_semver.py deleted file mode 100644 index bb8cbba7..00000000 --- a/test_semver.py +++ /dev/null @@ -1,1176 +0,0 @@ -from argparse import Namespace -from contextlib import contextmanager -import pytest # noqa - -from semver import ( - VersionInfo, - bump_build, - bump_major, - bump_minor, - bump_patch, - bump_prerelease, - cmd_bump, - cmd_check, - cmd_compare, - compare, - createparser, - deprecated, - finalize_version, - format_version, - main, - match, - max_ver, - min_ver, - parse, - parse_version_info, - process, - replace, - cmd_nextver, -) - -SEMVERFUNCS = [ - compare, - createparser, - bump_build, - bump_major, - bump_minor, - bump_patch, - bump_prerelease, - finalize_version, - format_version, - match, - max_ver, - min_ver, - parse, - process, - replace, -] - - -@contextmanager -def does_not_raise(item): - yield item - - -@pytest.mark.parametrize( - "string,expected", [("rc", "rc"), ("rc.1", "rc.2"), ("2x", "3x")] -) -def test_should_private_increment_string(string, expected): - assert VersionInfo._increment_string(string) == expected - - -@pytest.fixture -def version(): - return VersionInfo( - major=1, minor=2, patch=3, prerelease="alpha.1.2", build="build.11.e0f985a" - ) - - -@pytest.mark.parametrize( - "func", SEMVERFUNCS, ids=[func.__name__ for func in SEMVERFUNCS] -) -def test_fordocstrings(func): - assert func.__doc__, "Need a docstring for function %r" % func.__name - - -@pytest.mark.parametrize( - "ver", - [ - {"major": -1}, - {"major": 1, "minor": -2}, - {"major": 1, "minor": 2, "patch": -3}, - {"major": 1, "minor": -2, "patch": 3}, - ], -) -def test_should_not_allow_negative_numbers(ver): - with pytest.raises(ValueError, match=".* is negative. .*"): - VersionInfo(**ver) - - -@pytest.mark.parametrize( - "version,expected", - [ - # no. 1 - ( - "1.2.3-alpha.1.2+build.11.e0f985a", - { - "major": 1, - "minor": 2, - "patch": 3, - "prerelease": "alpha.1.2", - "build": "build.11.e0f985a", - }, - ), - # no. 2 - ( - "1.2.3-alpha-1+build.11.e0f985a", - { - "major": 1, - "minor": 2, - "patch": 3, - "prerelease": "alpha-1", - "build": "build.11.e0f985a", - }, - ), - ( - "0.1.0-0f", - {"major": 0, "minor": 1, "patch": 0, "prerelease": "0f", "build": None}, - ), - ( - "0.0.0-0foo.1", - {"major": 0, "minor": 0, "patch": 0, "prerelease": "0foo.1", "build": None}, - ), - ( - "0.0.0-0foo.1+build.1", - { - "major": 0, - "minor": 0, - "patch": 0, - "prerelease": "0foo.1", - "build": "build.1", - }, - ), - ], -) -def test_should_parse_version(version, expected): - result = parse(version) - assert result == expected - - -@pytest.mark.parametrize( - "version,expected", - [ - # no. 1 - ( - "1.2.3-rc.0+build.0", - { - "major": 1, - "minor": 2, - "patch": 3, - "prerelease": "rc.0", - "build": "build.0", - }, - ), - # no. 2 - ( - "1.2.3-rc.0.0+build.0", - { - "major": 1, - "minor": 2, - "patch": 3, - "prerelease": "rc.0.0", - "build": "build.0", - }, - ), - ], -) -def test_should_parse_zero_prerelease(version, expected): - result = parse(version) - assert result == expected - - -@pytest.mark.parametrize( - "left,right", - [ - ("1.0.0", "2.0.0"), - ("1.0.0-alpha", "1.0.0-alpha.1"), - ("1.0.0-alpha.1", "1.0.0-alpha.beta"), - ("1.0.0-alpha.beta", "1.0.0-beta"), - ("1.0.0-beta", "1.0.0-beta.2"), - ("1.0.0-beta.2", "1.0.0-beta.11"), - ("1.0.0-beta.11", "1.0.0-rc.1"), - ("1.0.0-rc.1", "1.0.0"), - ], -) -def test_should_get_less(left, right): - assert compare(left, right) == -1 - - -@pytest.mark.parametrize( - "left,right", - [ - ("2.0.0", "1.0.0"), - ("1.0.0-alpha.1", "1.0.0-alpha"), - ("1.0.0-alpha.beta", "1.0.0-alpha.1"), - ("1.0.0-beta", "1.0.0-alpha.beta"), - ("1.0.0-beta.2", "1.0.0-beta"), - ("1.0.0-beta.11", "1.0.0-beta.2"), - ("1.0.0-rc.1", "1.0.0-beta.11"), - ("1.0.0", "1.0.0-rc.1"), - ], -) -def test_should_get_greater(left, right): - assert compare(left, right) == 1 - - -def test_should_match_simple(): - assert match("2.3.7", ">=2.3.6") is True - - -def test_should_no_match_simple(): - assert match("2.3.7", ">=2.3.8") is False - - -@pytest.mark.parametrize( - "left,right,expected", - [ - ("2.3.7", "!=2.3.8", True), - ("2.3.7", "!=2.3.6", True), - ("2.3.7", "!=2.3.7", False), - ], -) -def test_should_match_not_equal(left, right, expected): - assert match(left, right) is expected - - -@pytest.mark.parametrize( - "left,right,expected", - [ - ("2.3.7", "<2.4.0", True), - ("2.3.7", ">2.3.5", True), - ("2.3.7", "<=2.3.9", True), - ("2.3.7", ">=2.3.5", True), - ("2.3.7", "==2.3.7", True), - ("2.3.7", "!=2.3.7", False), - ], -) -def test_should_not_raise_value_error_for_expected_match_expression( - left, right, expected -): - assert match(left, right) is expected - - -@pytest.mark.parametrize( - "left,right", [("2.3.7", "=2.3.7"), ("2.3.7", "~2.3.7"), ("2.3.7", "^2.3.7")] -) -def test_should_raise_value_error_for_unexpected_match_expression(left, right): - with pytest.raises(ValueError): - match(left, right) - - -@pytest.mark.parametrize("version", ["01.2.3", "1.02.3", "1.2.03"]) -def test_should_raise_value_error_for_zero_prefixed_versions(version): - with pytest.raises(ValueError): - parse(version) - - -@pytest.mark.parametrize( - "left,right", [("foo", "bar"), ("1.0", "1.0.0"), ("1.x", "1.0.0")] -) -def test_should_raise_value_error_for_invalid_value(left, right): - with pytest.raises(ValueError): - compare(left, right) - - -@pytest.mark.parametrize( - "left,right", [("1.0.0", ""), ("1.0.0", "!"), ("1.0.0", "1.0.0")] -) -def test_should_raise_value_error_for_invalid_match_expression(left, right): - with pytest.raises(ValueError): - match(left, right) - - -def test_should_follow_specification_comparison(): - """ - produce comparison chain: - 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 - < 1.0.0-rc.1 < 1.0.0-rc.1+build.1 < 1.0.0 < 1.0.0+0.3.7 < 1.3.7+build - < 1.3.7+build.2.b8f12d7 < 1.3.7+build.11.e0f985a - and in backward too. - """ - chain = [ - "1.0.0-alpha", - "1.0.0-alpha.1", - "1.0.0-beta.2", - "1.0.0-beta.11", - "1.0.0-rc.1", - "1.0.0", - "1.3.7+build", - ] - versions = zip(chain[:-1], chain[1:]) - for low_version, high_version in versions: - assert ( - compare(low_version, high_version) == -1 - ), "%s should be lesser than %s" % (low_version, high_version) - assert ( - compare(high_version, low_version) == 1 - ), "%s should be higher than %s" % (high_version, low_version) - - -@pytest.mark.parametrize("left,right", [("1.0.0-beta.2", "1.0.0-beta.11")]) -def test_should_compare_rc_builds(left, right): - assert compare(left, right) == -1 - - -@pytest.mark.parametrize( - "left,right", [("1.0.0-rc.1", "1.0.0"), ("1.0.0-rc.1+build.1", "1.0.0")] -) -def test_should_compare_release_candidate_with_release(left, right): - assert compare(left, right) == -1 - - -@pytest.mark.parametrize( - "left,right", - [ - ("2.0.0", "2.0.0"), - ("1.1.9-rc.1", "1.1.9-rc.1"), - ("1.1.9+build.1", "1.1.9+build.1"), - ("1.1.9-rc.1+build.1", "1.1.9-rc.1+build.1"), - ], -) -def test_should_say_equal_versions_are_equal(left, right): - assert compare(left, right) == 0 - - -@pytest.mark.parametrize( - "left,right,expected", - [("1.1.9-rc.1", "1.1.9-rc.1+build.1", 0), ("1.1.9-rc.1", "1.1.9+build.1", -1)], -) -def test_should_compare_versions_with_build_and_release(left, right, expected): - assert compare(left, right) == expected - - -@pytest.mark.parametrize( - "left,right,expected", - [ - ("1.0.0+build.1", "1.0.0", 0), - ("1.0.0-alpha.1+build.1", "1.0.0-alpha.1", 0), - ("1.0.0+build.1", "1.0.0-alpha.1", 1), - ("1.0.0+build.1", "1.0.0-alpha.1+build.1", 1), - ], -) -def test_should_ignore_builds_on_compare(left, right, expected): - assert compare(left, right) == expected - - -def test_should_correctly_format_version(): - assert format_version(3, 4, 5) == "3.4.5" - assert format_version(3, 4, 5, "rc.1") == "3.4.5-rc.1" - assert format_version(3, 4, 5, prerelease="rc.1") == "3.4.5-rc.1" - assert format_version(3, 4, 5, build="build.4") == "3.4.5+build.4" - assert format_version(3, 4, 5, "rc.1", "build.4") == "3.4.5-rc.1+build.4" - - -def test_should_bump_major(): - assert bump_major("3.4.5") == "4.0.0" - - -def test_should_bump_minor(): - assert bump_minor("3.4.5") == "3.5.0" - - -def test_should_bump_patch(): - assert bump_patch("3.4.5") == "3.4.6" - - -def test_should_versioninfo_bump_major_and_minor(): - v = parse_version_info("3.4.5") - expected = parse_version_info("4.1.0") - assert v.bump_major().bump_minor() == expected - - -def test_should_versioninfo_bump_minor_and_patch(): - v = parse_version_info("3.4.5") - expected = parse_version_info("3.5.1") - assert v.bump_minor().bump_patch() == expected - - -def test_should_versioninfo_bump_patch_and_prerelease(): - v = parse_version_info("3.4.5-rc.1") - expected = parse_version_info("3.4.6-rc.1") - assert v.bump_patch().bump_prerelease() == expected - - -def test_should_versioninfo_bump_patch_and_prerelease_with_token(): - v = parse_version_info("3.4.5-dev.1") - expected = parse_version_info("3.4.6-dev.1") - assert v.bump_patch().bump_prerelease("dev") == expected - - -def test_should_versioninfo_bump_prerelease_and_build(): - v = parse_version_info("3.4.5-rc.1+build.1") - expected = parse_version_info("3.4.5-rc.2+build.2") - assert v.bump_prerelease().bump_build() == expected - - -def test_should_versioninfo_bump_prerelease_and_build_with_token(): - v = parse_version_info("3.4.5-rc.1+b.1") - expected = parse_version_info("3.4.5-rc.2+b.2") - assert v.bump_prerelease().bump_build("b") == expected - - -def test_should_versioninfo_bump_multiple(): - v = parse_version_info("3.4.5-rc.1+build.1") - expected = parse_version_info("3.4.5-rc.2+build.2") - assert v.bump_prerelease().bump_build().bump_build() == expected - expected = parse_version_info("3.4.5-rc.3") - assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == expected - - -def test_should_versioninfo_to_dict(version): - resultdict = version.to_dict() - assert isinstance(resultdict, dict), "Got type from to_dict" - assert list(resultdict.keys()) == ["major", "minor", "patch", "prerelease", "build"] - - -def test_should_versioninfo_to_tuple(version): - result = version.to_tuple() - assert isinstance(result, tuple), "Got type from to_dict" - assert len(result) == 5, "Different length from to_tuple()" - - -def test_should_ignore_extensions_for_bump(): - assert bump_patch("3.4.5-rc1+build4") == "3.4.6" - - -def test_should_get_max(): - assert max_ver("3.4.5", "4.0.2") == "4.0.2" - - -def test_should_get_max_same(): - assert max_ver("3.4.5", "3.4.5") == "3.4.5" - - -def test_should_get_min(): - assert min_ver("3.4.5", "4.0.2") == "3.4.5" - - -def test_should_get_min_same(): - assert min_ver("3.4.5", "3.4.5") == "3.4.5" - - -def test_should_get_more_rc1(): - assert compare("1.0.0-rc1", "1.0.0-rc0") == 1 - - -@pytest.mark.parametrize( - "left,right,expected", - [ - ("1.2.3-rc.2", "1.2.3-rc.10", "1.2.3-rc.2"), - ("1.2.3-rc2", "1.2.3-rc10", "1.2.3-rc10"), - # identifiers with letters or hyphens are compared lexically in ASCII sort - # order. - ("1.2.3-Rc10", "1.2.3-rc10", "1.2.3-Rc10"), - # Numeric identifiers always have lower precedence than non-numeric - # identifiers. - ("1.2.3-2", "1.2.3-rc", "1.2.3-2"), - # A larger set of pre-release fields has a higher precedence than a - # smaller set, if all of the preceding identifiers are equal. - ("1.2.3-rc.2.1", "1.2.3-rc.2", "1.2.3-rc.2"), - # When major, minor, and patch are equal, a pre-release version has lower - # precedence than a normal version. - ("1.2.3", "1.2.3-1", "1.2.3-1"), - ("1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha"), - ], -) -def test_prerelease_order(left, right, expected): - assert min_ver(left, right) == expected - - -@pytest.mark.parametrize( - "version,token,expected", - [ - ("3.4.5-rc.9", None, "3.4.5-rc.10"), - ("3.4.5", None, "3.4.5-rc.1"), - ("3.4.5", "dev", "3.4.5-dev.1"), - ("3.4.5", "", "3.4.5-rc.1"), - ], -) -def test_should_bump_prerelease(version, token, expected): - token = "rc" if not token else token - assert bump_prerelease(version, token) == expected - - -def test_should_ignore_build_on_prerelease_bump(): - assert bump_prerelease("3.4.5-rc.1+build.4") == "3.4.5-rc.2" - - -@pytest.mark.parametrize( - "version,expected", - [ - ("3.4.5-rc.1+build.9", "3.4.5-rc.1+build.10"), - ("3.4.5-rc.1+0009.dev", "3.4.5-rc.1+0010.dev"), - ("3.4.5-rc.1", "3.4.5-rc.1+build.1"), - ("3.4.5", "3.4.5+build.1"), - ], -) -def test_should_bump_build(version, expected): - assert bump_build(version) == expected - - -@pytest.mark.parametrize( - "version,expected", - [ - ("1.2.3", "1.2.3"), - ("1.2.3-rc.5", "1.2.3"), - ("1.2.3+build.2", "1.2.3"), - ("1.2.3-rc.1+build.5", "1.2.3"), - ("1.2.3-alpha", "1.2.3"), - ("1.2.0", "1.2.0"), - ], -) -def test_should_finalize_version(version, expected): - assert finalize_version(version) == expected - - -def test_should_compare_version_info_objects(): - v1 = VersionInfo(major=0, minor=10, patch=4) - v2 = VersionInfo(major=0, minor=10, patch=4, prerelease="beta.1", build=None) - - # use `not` to enforce using comparision operators - assert v1 != v2 - assert v1 > v2 - assert v1 >= v2 - assert not (v1 < v2) - assert not (v1 <= v2) - assert not (v1 == v2) - - v3 = VersionInfo(major=0, minor=10, patch=4) - - assert not (v1 != v3) - assert not (v1 > v3) - assert v1 >= v3 - assert not (v1 < v3) - assert v1 <= v3 - assert v1 == v3 - - v4 = VersionInfo(major=0, minor=10, patch=5) - assert v1 != v4 - assert not (v1 > v4) - assert not (v1 >= v4) - assert v1 < v4 - assert v1 <= v4 - assert not (v1 == v4) - - -def test_should_compare_version_dictionaries(): - v1 = VersionInfo(major=0, minor=10, patch=4) - v2 = dict(major=0, minor=10, patch=4, prerelease="beta.1", build=None) - - assert v1 != v2 - assert v1 > v2 - assert v1 >= v2 - assert not (v1 < v2) - assert not (v1 <= v2) - assert not (v1 == v2) - - v3 = dict(major=0, minor=10, patch=4) - - assert not (v1 != v3) - assert not (v1 > v3) - assert v1 >= v3 - assert not (v1 < v3) - assert v1 <= v3 - assert v1 == v3 - - v4 = dict(major=0, minor=10, patch=5) - assert v1 != v4 - assert not (v1 > v4) - assert not (v1 >= v4) - assert v1 < v4 - assert v1 <= v4 - assert not (v1 == v4) - - -@pytest.mark.parametrize( - "t", # fmt: off - ( - (1, 0, 0), - (1, 0), - (1,), - (1, 0, 0, "pre.2"), - (1, 0, 0, "pre.2", "build.4"), - ), # fmt: on -) -def test_should_compare_version_tuples(t): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") - - assert v0 < t - assert v0 <= t - assert v0 != t - assert not v0 == t - assert v1 > t - assert v1 >= t - # Symmetric - assert t > v0 - assert t >= v0 - assert t < v1 - assert t <= v1 - assert t != v0 - assert not t == v0 - - -@pytest.mark.parametrize( - "lst", # fmt: off - ( - [1, 0, 0], - [1, 0], - [1], - [1, 0, 0, "pre.2"], - [1, 0, 0, "pre.2", "build.4"], - ), # fmt: on -) -def test_should_compare_version_list(lst): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") - - assert v0 < lst - assert v0 <= lst - assert v0 != lst - assert not v0 == lst - assert v1 > lst - assert v1 >= lst - # Symmetric - assert lst > v0 - assert lst >= v0 - assert lst < v1 - assert lst <= v1 - assert lst != v0 - assert not lst == v0 - - -@pytest.mark.parametrize( - "s", # fmt: off - ( - "1.0.0", - # "1.0", - # "1", - "1.0.0-pre.2", - "1.0.0-pre.2+build.4", - ), # fmt: on -) -def test_should_compare_version_string(s): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") - - assert v0 < s - assert v0 <= s - assert v0 != s - assert not v0 == s - assert v1 > s - assert v1 >= s - # Symmetric - assert s > v0 - assert s >= v0 - assert s < v1 - assert s <= v1 - assert s != v0 - assert not s == v0 - - -@pytest.mark.parametrize("s", ("1", "1.0", "1.0.x")) -def test_should_not_allow_to_compare_invalid_versionstring(s): - v = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") - with pytest.raises(ValueError): - v < s - with pytest.raises(ValueError): - s > v - - -def test_should_not_allow_to_compare_version_with_int(): - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") - with pytest.raises(TypeError): - v1 > 1 - with pytest.raises(TypeError): - 1 > v1 - with pytest.raises(TypeError): - v1.compare(1) - - -def test_should_compare_prerelease_with_numbers_and_letters(): - v1 = VersionInfo(major=1, minor=9, patch=1, prerelease="1unms", build=None) - v2 = VersionInfo(major=1, minor=9, patch=1, prerelease=None, build="1asd") - assert v1 < v2 - assert compare("1.9.1-1unms", "1.9.1+1") == -1 - - -def test_parse_version_info_str_hash(): - s_version = "1.2.3-alpha.1.2+build.11.e0f985a" - v = parse_version_info(s_version) - assert v.__str__() == s_version - d = {} - d[v] = "" # to ensure that VersionInfo are hashable - - -def test_equal_versions_have_equal_hashes(): - v1 = parse_version_info("1.2.3-alpha.1.2+build.11.e0f985a") - v2 = parse_version_info("1.2.3-alpha.1.2+build.22.a589f0e") - assert v1 == v2 - assert hash(v1) == hash(v2) - d = {} - d[v1] = 1 - d[v2] = 2 - assert d[v1] == 2 - s = set() - s.add(v1) - assert v2 in s - - -def test_parse_method_for_version_info(): - s_version = "1.2.3-alpha.1.2+build.11.e0f985a" - v = VersionInfo.parse(s_version) - assert str(v) == s_version - - -def test_immutable_major(version): - with pytest.raises(AttributeError, match="attribute 'major' is readonly"): - version.major = 9 - - -def test_immutable_minor(version): - with pytest.raises(AttributeError, match="attribute 'minor' is readonly"): - version.minor = 9 - - -def test_immutable_patch(version): - with pytest.raises(AttributeError, match="attribute 'patch' is readonly"): - version.patch = 9 - - -def test_immutable_prerelease(version): - with pytest.raises(AttributeError, match="attribute 'prerelease' is readonly"): - version.prerelease = "alpha.9.9" - - -def test_immutable_build(version): - with pytest.raises(AttributeError, match="attribute 'build' is readonly"): - version.build = "build.99.e0f985a" - - -def test_immutable_unknown_attribute(version): - # "no new attribute can be set" - with pytest.raises(AttributeError): - version.new_attribute = "forbidden" - - -def test_version_info_should_be_iterable(version): - assert tuple(version) == ( - version.major, - version.minor, - version.patch, - version.prerelease, - version.build, - ) - - -def test_should_compare_prerelease_and_build_with_numbers(): - assert VersionInfo(major=1, minor=9, patch=1, prerelease=1, build=1) < VersionInfo( - major=1, minor=9, patch=1, prerelease=2, build=1 - ) - assert VersionInfo(1, 9, 1, 1, 1) < VersionInfo(1, 9, 1, 2, 1) - assert VersionInfo("2") < VersionInfo(10) - assert VersionInfo("2") < VersionInfo("10") - - -def test_should_be_able_to_use_strings_as_major_minor_patch(): - v = VersionInfo("1", "2", "3") - assert isinstance(v.major, int) - assert isinstance(v.minor, int) - assert isinstance(v.patch, int) - assert v.prerelease is None - assert v.build is None - assert VersionInfo("1", "2", "3") == VersionInfo(1, 2, 3) - - -def test_using_non_numeric_string_as_major_minor_patch_throws(): - with pytest.raises(ValueError): - VersionInfo("a") - with pytest.raises(ValueError): - VersionInfo(1, "a") - with pytest.raises(ValueError): - VersionInfo(1, 2, "a") - - -def test_should_be_able_to_use_integers_as_prerelease_build(): - v = VersionInfo(1, 2, 3, 4, 5) - assert isinstance(v.prerelease, str) - assert isinstance(v.build, str) - assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, "4", "5") - - -@pytest.mark.parametrize( - "version, index, expected", - [ - # Simple positive indices - ("1.2.3-rc.0+build.0", 0, 1), - ("1.2.3-rc.0+build.0", 1, 2), - ("1.2.3-rc.0+build.0", 2, 3), - ("1.2.3-rc.0+build.0", 3, "rc.0"), - ("1.2.3-rc.0+build.0", 4, "build.0"), - ("1.2.3-rc.0", 0, 1), - ("1.2.3-rc.0", 1, 2), - ("1.2.3-rc.0", 2, 3), - ("1.2.3-rc.0", 3, "rc.0"), - ("1.2.3", 0, 1), - ("1.2.3", 1, 2), - ("1.2.3", 2, 3), - # Special cases - ("1.0.2", 1, 0), - ], -) -def test_version_info_should_be_accessed_with_index(version, index, expected): - version_info = VersionInfo.parse(version) - assert version_info[index] == expected - - -@pytest.mark.parametrize( - "version, slice_object, expected", - [ - # Slice indices - ("1.2.3-rc.0+build.0", slice(0, 5), (1, 2, 3, "rc.0", "build.0")), - ("1.2.3-rc.0+build.0", slice(0, 4), (1, 2, 3, "rc.0")), - ("1.2.3-rc.0+build.0", slice(0, 3), (1, 2, 3)), - ("1.2.3-rc.0+build.0", slice(0, 2), (1, 2)), - ("1.2.3-rc.0+build.0", slice(3, 5), ("rc.0", "build.0")), - ("1.2.3-rc.0", slice(0, 4), (1, 2, 3, "rc.0")), - ("1.2.3-rc.0", slice(0, 3), (1, 2, 3)), - ("1.2.3-rc.0", slice(0, 2), (1, 2)), - ("1.2.3", slice(0, 10), (1, 2, 3)), - ("1.2.3", slice(0, 3), (1, 2, 3)), - ("1.2.3", slice(0, 2), (1, 2)), - # Special cases - ("1.2.3-rc.0+build.0", slice(3), (1, 2, 3)), - ("1.2.3-rc.0+build.0", slice(0, 5, 2), (1, 3, "build.0")), - ("1.2.3-rc.0+build.0", slice(None, 5, 2), (1, 3, "build.0")), - ("1.2.3-rc.0+build.0", slice(5, 0, -2), ("build.0", 3)), - ("1.2.0-rc.0+build.0", slice(3), (1, 2, 0)), - ], -) -def test_version_info_should_be_accessed_with_slice_object( - version, slice_object, expected -): - version_info = VersionInfo.parse(version) - assert version_info[slice_object] == expected - - -@pytest.mark.parametrize( - "version, index", - [ - ("1.2.3", 3), - ("1.2.3", slice(3, 4)), - ("1.2.3", 4), - ("1.2.3", slice(4, 5)), - ("1.2.3", 5), - ("1.2.3", slice(5, 6)), - ("1.2.3-rc.0", 5), - ("1.2.3-rc.0", slice(5, 6)), - ("1.2.3-rc.0", 6), - ("1.2.3-rc.0", slice(6, 7)), - ], -) -def test_version_info_should_throw_index_error(version, index): - version_info = VersionInfo.parse(version) - with pytest.raises(IndexError, match=r"Version part undefined"): - version_info[index] - - -@pytest.mark.parametrize( - "version, index", - [ - ("1.2.3", -1), - ("1.2.3", -2), - ("1.2.3", slice(-2, 2)), - ("1.2.3", slice(2, -2)), - ("1.2.3", slice(-2, -2)), - ], -) -def test_version_info_should_throw_index_error_when_negative_index(version, index): - version_info = VersionInfo.parse(version) - with pytest.raises(IndexError, match=r"Version index cannot be negative"): - version_info[index] - - -@pytest.mark.parametrize( - "cli,expected", - [ - (["bump", "major", "1.2.3"], Namespace(bump="major", version="1.2.3")), - (["bump", "minor", "1.2.3"], Namespace(bump="minor", version="1.2.3")), - (["bump", "patch", "1.2.3"], Namespace(bump="patch", version="1.2.3")), - ( - ["bump", "prerelease", "1.2.3"], - Namespace(bump="prerelease", version="1.2.3"), - ), - (["bump", "build", "1.2.3"], Namespace(bump="build", version="1.2.3")), - # --- - (["compare", "1.2.3", "2.1.3"], Namespace(version1="1.2.3", version2="2.1.3")), - # --- - (["check", "1.2.3"], Namespace(version="1.2.3")), - ], -) -def test_should_parse_cli_arguments(cli, expected): - parser = createparser() - assert parser - result = parser.parse_args(cli) - del result.func - assert result == expected - - -@pytest.mark.parametrize( - "func,args,expectation", - [ - # bump subcommand - (cmd_bump, Namespace(bump="major", version="1.2.3"), does_not_raise("2.0.0")), - (cmd_bump, Namespace(bump="minor", version="1.2.3"), does_not_raise("1.3.0")), - (cmd_bump, Namespace(bump="patch", version="1.2.3"), does_not_raise("1.2.4")), - ( - cmd_bump, - Namespace(bump="prerelease", version="1.2.3-rc1"), - does_not_raise("1.2.3-rc2"), - ), - ( - cmd_bump, - Namespace(bump="build", version="1.2.3+build.13"), - does_not_raise("1.2.3+build.14"), - ), - # compare subcommand - ( - cmd_compare, - Namespace(version1="1.2.3", version2="2.1.3"), - does_not_raise("-1"), - ), - ( - cmd_compare, - Namespace(version1="1.2.3", version2="1.2.3"), - does_not_raise("0"), - ), - ( - cmd_compare, - Namespace(version1="2.4.0", version2="2.1.3"), - does_not_raise("1"), - ), - # check subcommand - (cmd_check, Namespace(version="1.2.3"), does_not_raise(None)), - (cmd_check, Namespace(version="1.2"), pytest.raises(ValueError)), - # nextver subcommand - ( - cmd_nextver, - Namespace(version="1.2.3", part="major"), - does_not_raise("2.0.0"), - ), - ( - cmd_nextver, - Namespace(version="1.2", part="major"), - pytest.raises(ValueError), - ), - ( - cmd_nextver, - Namespace(version="1.2.3", part="nope"), - pytest.raises(ValueError), - ), - ], -) -def test_should_process_parsed_cli_arguments(func, args, expectation): - with expectation as expected: - result = func(args) - assert result == expected - - -def test_should_process_print(capsys): - rc = main(["bump", "major", "1.2.3"]) - assert rc == 0 - captured = capsys.readouterr() - assert captured.out.rstrip() == "2.0.0" - - -def test_should_process_raise_error(capsys): - rc = main(["bump", "major", "1.2"]) - assert rc != 0 - captured = capsys.readouterr() - assert captured.err.startswith("ERROR") - - -def test_should_raise_systemexit_when_called_with_empty_arguments(): - with pytest.raises(SystemExit): - main([]) - - -def test_should_raise_systemexit_when_bump_iscalled_with_empty_arguments(): - with pytest.raises(SystemExit): - main(["bump"]) - - -def test_should_process_check_iscalled_with_valid_version(capsys): - result = main(["check", "1.1.1"]) - assert not result - captured = capsys.readouterr() - assert not captured.out - - -@pytest.mark.parametrize( - "version,parts,expected", - [ - ("3.4.5", dict(major=2), "2.4.5"), - ("3.4.5", dict(major="2"), "2.4.5"), - ("3.4.5", dict(major=2, minor=5), "2.5.5"), - ("3.4.5", dict(minor=2), "3.2.5"), - ("3.4.5", dict(major=2, minor=5, patch=10), "2.5.10"), - ("3.4.5", dict(major=2, minor=5, patch=10, prerelease="rc1"), "2.5.10-rc1"), - ( - "3.4.5", - dict(major=2, minor=5, patch=10, prerelease="rc1", build="b1"), - "2.5.10-rc1+b1", - ), - ("3.4.5-alpha.1.2", dict(major=2), "2.4.5-alpha.1.2"), - ("3.4.5-alpha.1.2", dict(build="x1"), "3.4.5-alpha.1.2+x1"), - ("3.4.5+build1", dict(major=2), "2.4.5+build1"), - ], -) -def test_replace_method_replaces_requested_parts(version, parts, expected): - assert replace(version, **parts) == expected - - -def test_replace_raises_TypeError_for_invalid_keyword_arg(): - with pytest.raises(TypeError, match=r"replace\(\).*unknown.*"): - assert replace("1.2.3", unknown="should_raise") - - -@pytest.mark.parametrize( - "version,parts,expected", - [ - ("3.4.5", dict(major=2, minor=5), "2.5.5"), - ("3.4.5", dict(major=2, minor=5, patch=10), "2.5.10"), - ("3.4.5-alpha.1.2", dict(major=2), "2.4.5-alpha.1.2"), - ("3.4.5-alpha.1.2", dict(build="x1"), "3.4.5-alpha.1.2+x1"), - ("3.4.5+build1", dict(major=2), "2.4.5+build1"), - ], -) -def test_should_return_versioninfo_with_replaced_parts(version, parts, expected): - assert VersionInfo.parse(version).replace(**parts) == VersionInfo.parse(expected) - - -def test_replace_raises_ValueError_for_non_numeric_values(): - with pytest.raises(ValueError): - VersionInfo.parse("1.2.3").replace(major="x") - - -def test_should_versioninfo_isvalid(): - assert VersionInfo.isvalid("1.0.0") is True - assert VersionInfo.isvalid("foo") is False - - -@pytest.mark.parametrize( - "func, args, kwargs", - [ - (bump_build, ("1.2.3",), {}), - (bump_major, ("1.2.3",), {}), - (bump_minor, ("1.2.3",), {}), - (bump_patch, ("1.2.3",), {}), - (bump_prerelease, ("1.2.3",), {}), - (compare, ("1.2.1", "1.2.2"), {}), - (format_version, (3, 4, 5), {}), - (finalize_version, ("1.2.3-rc.5",), {}), - (match, ("1.0.0", ">=1.0.0"), {}), - (parse, ("1.2.3",), {}), - (parse_version_info, ("1.2.3",), {}), - (replace, ("1.2.3",), dict(major=2, patch=10)), - (max_ver, ("1.2.3", "1.2.4"), {}), - (min_ver, ("1.2.3", "1.2.4"), {}), - ], -) -def test_should_raise_deprecation_warnings(func, args, kwargs): - with pytest.warns( - DeprecationWarning, match=r"Function 'semver.[_a-zA-Z]+' is deprecated." - ) as record: - func(*args, **kwargs) - if not record: - pytest.fail("Expected a DeprecationWarning for {}".format(func.__name__)) - assert len(record), "Expected one DeprecationWarning record" - - -def test_deprecated_deco_without_argument(): - @deprecated - def mock_func(): - return True - - with pytest.deprecated_call(): - assert mock_func() - - -def test_next_version_with_invalid_parts(): - version = VersionInfo.parse("1.0.1") - with pytest.raises(ValueError): - version.next_version("invalid") - - -@pytest.mark.parametrize( - "version, part, expected", - [ - # major - ("1.0.4-rc.1", "major", "2.0.0"), - ("1.1.0-rc.1", "major", "2.0.0"), - ("1.1.4-rc.1", "major", "2.0.0"), - ("1.2.3", "major", "2.0.0"), - ("1.0.0-rc.1", "major", "1.0.0"), - # minor - ("0.2.0-rc.1", "minor", "0.2.0"), - ("0.2.5-rc.1", "minor", "0.3.0"), - ("1.3.1", "minor", "1.4.0"), - # patch - ("1.3.2", "patch", "1.3.3"), - ("0.1.5-rc.2", "patch", "0.1.5"), - # prerelease - ("0.1.4", "prerelease", "0.1.5-rc.1"), - ("0.1.5-rc.1", "prerelease", "0.1.5-rc.2"), - # special cases - ("0.2.0-rc.1", "patch", "0.2.0"), # same as "minor" - ("1.0.0-rc.1", "patch", "1.0.0"), # same as "major" - ("1.0.0-rc.1", "minor", "1.0.0"), # same as "major" - ], -) -def test_next_version_with_versioninfo(version, part, expected): - ver = VersionInfo.parse(version) - next_version = ver.next_version(part) - assert isinstance(next_version, VersionInfo) - assert str(next_version) == expected - - -@pytest.mark.parametrize( - "version, expected", - [ - ( - VersionInfo(major=1, minor=2, patch=3, prerelease=None, build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease=None, build=None)", - ), - ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build=None)", - ), - ( - VersionInfo(major=1, minor=2, patch=3, prerelease="dev.1", build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease='dev.1', build=None)", - ), - ( - VersionInfo(major=1, minor=2, patch=3, prerelease="dev.1", build="b.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='dev.1', build='b.1')", - ), - ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build="b.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build='b.1')", - ), - ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build="build.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build='build.1')", - ), - ], -) -def test_repr(version, expected): - assert repr(version) == expected - - -def test_subclass_from_versioninfo(): - class SemVerWithVPrefix(VersionInfo): - @classmethod - def parse(cls, version): - if not version[0] in ("v", "V"): - raise ValueError( - "{v!r}: version must start with 'v' or 'V'".format(v=version) - ) - return super(SemVerWithVPrefix, cls).parse(version[1:]) - - def __str__(self): - # Reconstruct the tag. - return "v" + super(SemVerWithVPrefix, self).__str__() - - v = SemVerWithVPrefix.parse("v1.2.3") - assert str(v) == "v1.2.3" diff --git a/test_typeerror-274.py b/test_typeerror-274.py deleted file mode 100644 index 2ed03d61..00000000 --- a/test_typeerror-274.py +++ /dev/null @@ -1,102 +0,0 @@ -import pytest -import sys - -import semver - - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - - -def ensure_binary(s, encoding="utf-8", errors="strict"): - """Coerce **s** to six.binary_type. - - For Python 2: - - `unicode` -> encoded to `str` - - `str` -> `str` - - For Python 3: - - `str` -> encoded to `bytes` - - `bytes` -> `bytes` - """ - if isinstance(s, semver.text_type): - return s.encode(encoding, errors) - elif isinstance(s, semver.binary_type): - return s - else: - raise TypeError("not expecting type '%s'" % type(s)) - - -def test_should_work_with_string_and_unicode(): - result = semver.compare(semver.u("1.1.0"), semver.b("1.2.2")) - assert result == -1 - result = semver.compare(semver.b("1.1.0"), semver.u("1.2.2")) - assert result == -1 - - -class TestEnsure: - # From six project - # grinning face emoji - UNICODE_EMOJI = semver.u("\U0001F600") - BINARY_EMOJI = b"\xf0\x9f\x98\x80" - - def test_ensure_binary_raise_type_error(self): - with pytest.raises(TypeError): - semver.ensure_str(8) - - def test_errors_and_encoding(self): - ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore") - with pytest.raises(UnicodeEncodeError): - ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="strict") - - def test_ensure_binary_raise(self): - converted_unicode = ensure_binary( - self.UNICODE_EMOJI, encoding="utf-8", errors="strict" - ) - converted_binary = ensure_binary( - self.BINARY_EMOJI, encoding="utf-8", errors="strict" - ) - if semver.PY2: - # PY2: unicode -> str - assert converted_unicode == self.BINARY_EMOJI and isinstance( - converted_unicode, str - ) - # PY2: str -> str - assert converted_binary == self.BINARY_EMOJI and isinstance( - converted_binary, str - ) - else: - # PY3: str -> bytes - assert converted_unicode == self.BINARY_EMOJI and isinstance( - converted_unicode, bytes - ) - # PY3: bytes -> bytes - assert converted_binary == self.BINARY_EMOJI and isinstance( - converted_binary, bytes - ) - - def test_ensure_str(self): - converted_unicode = semver.ensure_str( - self.UNICODE_EMOJI, encoding="utf-8", errors="strict" - ) - converted_binary = semver.ensure_str( - self.BINARY_EMOJI, encoding="utf-8", errors="strict" - ) - if PY2: - # PY2: unicode -> str - assert converted_unicode == self.BINARY_EMOJI and isinstance( - converted_unicode, str - ) - # PY2: str -> str - assert converted_binary == self.BINARY_EMOJI and isinstance( - converted_binary, str - ) - else: - # PY3: str -> str - assert converted_unicode == self.UNICODE_EMOJI and isinstance( - converted_unicode, str - ) - # PY3: bytes -> str - assert converted_binary == self.UNICODE_EMOJI and isinstance( - converted_unicode, str - ) diff --git a/conftest.py b/tests/conftest.py similarity index 50% rename from conftest.py rename to tests/conftest.py index e6a1f048..2e935d0b 100644 --- a/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,13 @@ +import sys + import pytest + import semver -import sys sys.path.insert(0, "docs") from coerce import coerce # noqa:E402 -from semverwithvprefix import SemVerWithVPrefix +from semverwithvprefix import SemVerWithVPrefix # noqa:E402 @pytest.fixture(autouse=True) @@ -13,3 +15,16 @@ def add_semver(doctest_namespace): doctest_namespace["semver"] = semver doctest_namespace["coerce"] = coerce doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix + + +@pytest.fixture +def version(): + """ + Creates a version + + :return: a version type + :rtype: VersionInfo + """ + return semver.VersionInfo( + major=1, minor=2, patch=3, prerelease="alpha.1.2", build="build.11.e0f985a" + ) diff --git a/tests/test_bump.py b/tests/test_bump.py new file mode 100644 index 00000000..c28e1905 --- /dev/null +++ b/tests/test_bump.py @@ -0,0 +1,101 @@ +import pytest + +from semver import ( + bump_build, + bump_major, + bump_minor, + bump_patch, + bump_prerelease, + parse_version_info, +) + + +def test_should_bump_major(): + assert bump_major("3.4.5") == "4.0.0" + + +def test_should_bump_minor(): + assert bump_minor("3.4.5") == "3.5.0" + + +def test_should_bump_patch(): + assert bump_patch("3.4.5") == "3.4.6" + + +def test_should_versioninfo_bump_major_and_minor(): + v = parse_version_info("3.4.5") + expected = parse_version_info("4.1.0") + assert v.bump_major().bump_minor() == expected + + +def test_should_versioninfo_bump_minor_and_patch(): + v = parse_version_info("3.4.5") + expected = parse_version_info("3.5.1") + assert v.bump_minor().bump_patch() == expected + + +def test_should_versioninfo_bump_patch_and_prerelease(): + v = parse_version_info("3.4.5-rc.1") + expected = parse_version_info("3.4.6-rc.1") + assert v.bump_patch().bump_prerelease() == expected + + +def test_should_versioninfo_bump_patch_and_prerelease_with_token(): + v = parse_version_info("3.4.5-dev.1") + expected = parse_version_info("3.4.6-dev.1") + assert v.bump_patch().bump_prerelease("dev") == expected + + +def test_should_versioninfo_bump_prerelease_and_build(): + v = parse_version_info("3.4.5-rc.1+build.1") + expected = parse_version_info("3.4.5-rc.2+build.2") + assert v.bump_prerelease().bump_build() == expected + + +def test_should_versioninfo_bump_prerelease_and_build_with_token(): + v = parse_version_info("3.4.5-rc.1+b.1") + expected = parse_version_info("3.4.5-rc.2+b.2") + assert v.bump_prerelease().bump_build("b") == expected + + +def test_should_versioninfo_bump_multiple(): + v = parse_version_info("3.4.5-rc.1+build.1") + expected = parse_version_info("3.4.5-rc.2+build.2") + assert v.bump_prerelease().bump_build().bump_build() == expected + expected = parse_version_info("3.4.5-rc.3") + assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == expected + + +def test_should_ignore_extensions_for_bump(): + assert bump_patch("3.4.5-rc1+build4") == "3.4.6" + + +@pytest.mark.parametrize( + "version,token,expected", + [ + ("3.4.5-rc.9", None, "3.4.5-rc.10"), + ("3.4.5", None, "3.4.5-rc.1"), + ("3.4.5", "dev", "3.4.5-dev.1"), + ("3.4.5", "", "3.4.5-rc.1"), + ], +) +def test_should_bump_prerelease(version, token, expected): + token = "rc" if not token else token + assert bump_prerelease(version, token) == expected + + +def test_should_ignore_build_on_prerelease_bump(): + assert bump_prerelease("3.4.5-rc.1+build.4") == "3.4.5-rc.2" + + +@pytest.mark.parametrize( + "version,expected", + [ + ("3.4.5-rc.1+build.9", "3.4.5-rc.1+build.10"), + ("3.4.5-rc.1+0009.dev", "3.4.5-rc.1+0010.dev"), + ("3.4.5-rc.1", "3.4.5-rc.1+build.1"), + ("3.4.5", "3.4.5+build.1"), + ], +) +def test_should_bump_build(version, expected): + assert bump_build(version) == expected diff --git a/tests/test_compare.py b/tests/test_compare.py new file mode 100644 index 00000000..41caa08d --- /dev/null +++ b/tests/test_compare.py @@ -0,0 +1,303 @@ +import pytest + +from semver import VersionInfo, compare + + +@pytest.mark.parametrize( + "left,right", + [ + ("1.0.0", "2.0.0"), + ("1.0.0-alpha", "1.0.0-alpha.1"), + ("1.0.0-alpha.1", "1.0.0-alpha.beta"), + ("1.0.0-alpha.beta", "1.0.0-beta"), + ("1.0.0-beta", "1.0.0-beta.2"), + ("1.0.0-beta.2", "1.0.0-beta.11"), + ("1.0.0-beta.11", "1.0.0-rc.1"), + ("1.0.0-rc.1", "1.0.0"), + ], +) +def test_should_get_less(left, right): + assert compare(left, right) == -1 + + +@pytest.mark.parametrize( + "left,right", + [ + ("2.0.0", "1.0.0"), + ("1.0.0-alpha.1", "1.0.0-alpha"), + ("1.0.0-alpha.beta", "1.0.0-alpha.1"), + ("1.0.0-beta", "1.0.0-alpha.beta"), + ("1.0.0-beta.2", "1.0.0-beta"), + ("1.0.0-beta.11", "1.0.0-beta.2"), + ("1.0.0-rc.1", "1.0.0-beta.11"), + ("1.0.0", "1.0.0-rc.1"), + ], +) +def test_should_get_greater(left, right): + assert compare(left, right) == 1 + + +@pytest.mark.parametrize( + "left,right", [("foo", "bar"), ("1.0", "1.0.0"), ("1.x", "1.0.0")] +) +def test_should_raise_value_error_for_invalid_value(left, right): + with pytest.raises(ValueError): + compare(left, right) + + +def test_should_follow_specification_comparison(): + """ + produce comparison chain: + 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 + < 1.0.0-rc.1 < 1.0.0-rc.1+build.1 < 1.0.0 < 1.0.0+0.3.7 < 1.3.7+build + < 1.3.7+build.2.b8f12d7 < 1.3.7+build.11.e0f985a + and in backward too. + """ + chain = [ + "1.0.0-alpha", + "1.0.0-alpha.1", + "1.0.0-beta.2", + "1.0.0-beta.11", + "1.0.0-rc.1", + "1.0.0", + "1.3.7+build", + ] + versions = zip(chain[:-1], chain[1:]) + for low_version, high_version in versions: + assert ( + compare(low_version, high_version) == -1 + ), "%s should be lesser than %s" % (low_version, high_version) + assert ( + compare(high_version, low_version) == 1 + ), "%s should be higher than %s" % (high_version, low_version) + + +@pytest.mark.parametrize("left,right", [("1.0.0-beta.2", "1.0.0-beta.11")]) +def test_should_compare_rc_builds(left, right): + assert compare(left, right) == -1 + + +@pytest.mark.parametrize( + "left,right", [("1.0.0-rc.1", "1.0.0"), ("1.0.0-rc.1+build.1", "1.0.0")] +) +def test_should_compare_release_candidate_with_release(left, right): + assert compare(left, right) == -1 + + +@pytest.mark.parametrize( + "left,right", + [ + ("2.0.0", "2.0.0"), + ("1.1.9-rc.1", "1.1.9-rc.1"), + ("1.1.9+build.1", "1.1.9+build.1"), + ("1.1.9-rc.1+build.1", "1.1.9-rc.1+build.1"), + ], +) +def test_should_say_equal_versions_are_equal(left, right): + assert compare(left, right) == 0 + + +@pytest.mark.parametrize( + "left,right,expected", + [("1.1.9-rc.1", "1.1.9-rc.1+build.1", 0), ("1.1.9-rc.1", "1.1.9+build.1", -1)], +) +def test_should_compare_versions_with_build_and_release(left, right, expected): + assert compare(left, right) == expected + + +@pytest.mark.parametrize( + "left,right,expected", + [ + ("1.0.0+build.1", "1.0.0", 0), + ("1.0.0-alpha.1+build.1", "1.0.0-alpha.1", 0), + ("1.0.0+build.1", "1.0.0-alpha.1", 1), + ("1.0.0+build.1", "1.0.0-alpha.1+build.1", 1), + ], +) +def test_should_ignore_builds_on_compare(left, right, expected): + assert compare(left, right) == expected + + +def test_should_get_more_rc1(): + assert compare("1.0.0-rc1", "1.0.0-rc0") == 1 + + +def test_should_compare_prerelease_with_numbers_and_letters(): + v1 = VersionInfo(major=1, minor=9, patch=1, prerelease="1unms", build=None) + v2 = VersionInfo(major=1, minor=9, patch=1, prerelease=None, build="1asd") + assert v1 < v2 + assert compare("1.9.1-1unms", "1.9.1+1") == -1 + + +def test_should_compare_version_info_objects(): + v1 = VersionInfo(major=0, minor=10, patch=4) + v2 = VersionInfo(major=0, minor=10, patch=4, prerelease="beta.1", build=None) + + # use `not` to enforce using comparision operators + assert v1 != v2 + assert v1 > v2 + assert v1 >= v2 + assert not (v1 < v2) + assert not (v1 <= v2) + assert not (v1 == v2) + + v3 = VersionInfo(major=0, minor=10, patch=4) + + assert not (v1 != v3) + assert not (v1 > v3) + assert v1 >= v3 + assert not (v1 < v3) + assert v1 <= v3 + assert v1 == v3 + + v4 = VersionInfo(major=0, minor=10, patch=5) + assert v1 != v4 + assert not (v1 > v4) + assert not (v1 >= v4) + assert v1 < v4 + assert v1 <= v4 + assert not (v1 == v4) + + +def test_should_compare_version_dictionaries(): + v1 = VersionInfo(major=0, minor=10, patch=4) + v2 = dict(major=0, minor=10, patch=4, prerelease="beta.1", build=None) + + assert v1 != v2 + assert v1 > v2 + assert v1 >= v2 + assert not (v1 < v2) + assert not (v1 <= v2) + assert not (v1 == v2) + + v3 = dict(major=0, minor=10, patch=4) + + assert not (v1 != v3) + assert not (v1 > v3) + assert v1 >= v3 + assert not (v1 < v3) + assert v1 <= v3 + assert v1 == v3 + + v4 = dict(major=0, minor=10, patch=5) + assert v1 != v4 + assert not (v1 > v4) + assert not (v1 >= v4) + assert v1 < v4 + assert v1 <= v4 + assert not (v1 == v4) + + +@pytest.mark.parametrize( + "t", # fmt: off + ( + (1, 0, 0), + (1, 0), + (1,), + (1, 0, 0, "pre.2"), + (1, 0, 0, "pre.2", "build.4"), + ), # fmt: on +) +def test_should_compare_version_tuples(t): + v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + + assert v0 < t + assert v0 <= t + assert v0 != t + assert not v0 == t + assert v1 > t + assert v1 >= t + # Symmetric + assert t > v0 + assert t >= v0 + assert t < v1 + assert t <= v1 + assert t != v0 + assert not t == v0 + + +@pytest.mark.parametrize( + "lst", # fmt: off + ( + [1, 0, 0], + [1, 0], + [1], + [1, 0, 0, "pre.2"], + [1, 0, 0, "pre.2", "build.4"], + ), # fmt: on +) +def test_should_compare_version_list(lst): + v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + + assert v0 < lst + assert v0 <= lst + assert v0 != lst + assert not v0 == lst + assert v1 > lst + assert v1 >= lst + # Symmetric + assert lst > v0 + assert lst >= v0 + assert lst < v1 + assert lst <= v1 + assert lst != v0 + assert not lst == v0 + + +@pytest.mark.parametrize( + "s", # fmt: off + ( + "1.0.0", + # "1.0", + # "1", + "1.0.0-pre.2", + "1.0.0-pre.2+build.4", + ), # fmt: on +) +def test_should_compare_version_string(s): + v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + + assert v0 < s + assert v0 <= s + assert v0 != s + assert not v0 == s + assert v1 > s + assert v1 >= s + # Symmetric + assert s > v0 + assert s >= v0 + assert s < v1 + assert s <= v1 + assert s != v0 + assert not s == v0 + + +@pytest.mark.parametrize("s", ("1", "1.0", "1.0.x")) +def test_should_not_allow_to_compare_invalid_versionstring(s): + v = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + with pytest.raises(ValueError): + v < s + with pytest.raises(ValueError): + s > v + + +def test_should_not_allow_to_compare_version_with_int(): + v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + with pytest.raises(TypeError): + v1 > 1 + with pytest.raises(TypeError): + 1 > v1 + with pytest.raises(TypeError): + v1.compare(1) + + +def test_should_compare_prerelease_and_build_with_numbers(): + assert VersionInfo(major=1, minor=9, patch=1, prerelease=1, build=1) < VersionInfo( + major=1, minor=9, patch=1, prerelease=2, build=1 + ) + assert VersionInfo(1, 9, 1, 1, 1) < VersionInfo(1, 9, 1, 2, 1) + assert VersionInfo("2") < VersionInfo(10) + assert VersionInfo("2") < VersionInfo("10") diff --git a/tests/test_deprecated_functions.py b/tests/test_deprecated_functions.py new file mode 100644 index 00000000..8a04e3e9 --- /dev/null +++ b/tests/test_deprecated_functions.py @@ -0,0 +1,57 @@ +import pytest + +from semver import ( + bump_build, + bump_major, + bump_minor, + bump_patch, + bump_prerelease, + compare, + deprecated, + finalize_version, + format_version, + match, + max_ver, + min_ver, + parse, + parse_version_info, + replace, +) + + +@pytest.mark.parametrize( + "func, args, kwargs", + [ + (bump_build, ("1.2.3",), {}), + (bump_major, ("1.2.3",), {}), + (bump_minor, ("1.2.3",), {}), + (bump_patch, ("1.2.3",), {}), + (bump_prerelease, ("1.2.3",), {}), + (compare, ("1.2.1", "1.2.2"), {}), + (format_version, (3, 4, 5), {}), + (finalize_version, ("1.2.3-rc.5",), {}), + (match, ("1.0.0", ">=1.0.0"), {}), + (parse, ("1.2.3",), {}), + (parse_version_info, ("1.2.3",), {}), + (replace, ("1.2.3",), dict(major=2, patch=10)), + (max_ver, ("1.2.3", "1.2.4"), {}), + (min_ver, ("1.2.3", "1.2.4"), {}), + ], +) +def test_should_raise_deprecation_warnings(func, args, kwargs): + with pytest.warns( + DeprecationWarning, match=r"Function 'semver.[_a-zA-Z]+' is deprecated." + ) as record: + func(*args, **kwargs) + if not record: + pytest.fail("Expected a DeprecationWarning for {}".format(func.__name__)) + assert len(record), "Expected one DeprecationWarning record" + + +def test_deprecated_deco_without_argument(): + @deprecated + def mock_func(): + return True + + with pytest.deprecated_call(): + assert mock_func() diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py new file mode 100644 index 00000000..a3ff08b1 --- /dev/null +++ b/tests/test_docstrings.py @@ -0,0 +1,39 @@ +import inspect + +import pytest + +import semver + + +def getallfunctions(module=semver): + def getfunctions(_module): + for _, func in inspect.getmembers(_module, inspect.isfunction): + # Make sure you only investigate functions from our modules: + if not func.__name__.startswith("_") and func.__module__.startswith( + _module.__name__ + ): + yield func + + def getmodules(_module): + for _, m in inspect.getmembers(_module, inspect.ismodule): + if m.__package__.startswith(_module.__package__): + yield m + + for ff in getfunctions(module): + yield ff + # for mm in getmodules(module): + # for ff in getfunctions(mm): + # yield ff + + +SEMVERFUNCS = [func for func in getallfunctions()] + + +@pytest.mark.parametrize( + "func", SEMVERFUNCS, ids=[func.__name__ for func in SEMVERFUNCS] +) +def test_fordocstrings(func): + assert func.__doc__, "Need a docstring for function %r from module %r" % ( + func.__name__, + func.__module__, + ) diff --git a/tests/test_format.py b/tests/test_format.py new file mode 100644 index 00000000..b1c6ad5b --- /dev/null +++ b/tests/test_format.py @@ -0,0 +1,65 @@ +import pytest + +from semver import VersionInfo, finalize_version, format_version + + +@pytest.mark.parametrize( + "version,expected", + [ + ("1.2.3", "1.2.3"), + ("1.2.3-rc.5", "1.2.3"), + ("1.2.3+build.2", "1.2.3"), + ("1.2.3-rc.1+build.5", "1.2.3"), + ("1.2.3-alpha", "1.2.3"), + ("1.2.0", "1.2.0"), + ], +) +def test_should_finalize_version(version, expected): + assert finalize_version(version) == expected + + +def test_should_correctly_format_version(): + assert format_version(3, 4, 5) == "3.4.5" + assert format_version(3, 4, 5, "rc.1") == "3.4.5-rc.1" + assert format_version(3, 4, 5, prerelease="rc.1") == "3.4.5-rc.1" + assert format_version(3, 4, 5, build="build.4") == "3.4.5+build.4" + assert format_version(3, 4, 5, "rc.1", "build.4") == "3.4.5-rc.1+build.4" + + +def test_parse_method_for_version_info(): + s_version = "1.2.3-alpha.1.2+build.11.e0f985a" + v = VersionInfo.parse(s_version) + assert str(v) == s_version + + +@pytest.mark.parametrize( + "version, expected", + [ + ( + VersionInfo(major=1, minor=2, patch=3, prerelease=None, build=None), + "VersionInfo(major=1, minor=2, patch=3, prerelease=None, build=None)", + ), + ( + VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build=None), + "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build=None)", + ), + ( + VersionInfo(major=1, minor=2, patch=3, prerelease="dev.1", build=None), + "VersionInfo(major=1, minor=2, patch=3, prerelease='dev.1', build=None)", + ), + ( + VersionInfo(major=1, minor=2, patch=3, prerelease="dev.1", build="b.1"), + "VersionInfo(major=1, minor=2, patch=3, prerelease='dev.1', build='b.1')", + ), + ( + VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build="b.1"), + "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build='b.1')", + ), + ( + VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build="build.1"), + "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build='build.1')", + ), + ], +) +def test_repr(version, expected): + assert repr(version) == expected diff --git a/tests/test_immutable.py b/tests/test_immutable.py new file mode 100644 index 00000000..ef6aa40e --- /dev/null +++ b/tests/test_immutable.py @@ -0,0 +1,33 @@ +import pytest + + +def test_immutable_major(version): + with pytest.raises(AttributeError, match="attribute 'major' is readonly"): + version.major = 9 + + +def test_immutable_minor(version): + with pytest.raises(AttributeError, match="attribute 'minor' is readonly"): + version.minor = 9 + + +def test_immutable_patch(version): + with pytest.raises(AttributeError, match="attribute 'patch' is readonly"): + version.patch = 9 + + +def test_immutable_prerelease(version): + with pytest.raises(AttributeError, match="attribute 'prerelease' is readonly"): + version.prerelease = "alpha.9.9" + + +def test_immutable_build(version): + with pytest.raises(AttributeError, match="attribute 'build' is readonly"): + version.build = "build.99.e0f985a" + + +def test_immutable_unknown_attribute(version): + with pytest.raises( + AttributeError, match=".* object has no attribute 'new_attribute'" + ): + version.new_attribute = "forbidden" diff --git a/tests/test_index.py b/tests/test_index.py new file mode 100644 index 00000000..d54ea110 --- /dev/null +++ b/tests/test_index.py @@ -0,0 +1,95 @@ +import pytest + +from semver import VersionInfo + + +@pytest.mark.parametrize( + "version, index, expected", + [ + # Simple positive indices + ("1.2.3-rc.0+build.0", 0, 1), + ("1.2.3-rc.0+build.0", 1, 2), + ("1.2.3-rc.0+build.0", 2, 3), + ("1.2.3-rc.0+build.0", 3, "rc.0"), + ("1.2.3-rc.0+build.0", 4, "build.0"), + ("1.2.3-rc.0", 0, 1), + ("1.2.3-rc.0", 1, 2), + ("1.2.3-rc.0", 2, 3), + ("1.2.3-rc.0", 3, "rc.0"), + ("1.2.3", 0, 1), + ("1.2.3", 1, 2), + ("1.2.3", 2, 3), + # Special cases + ("1.0.2", 1, 0), + ], +) +def test_version_info_should_be_accessed_with_index(version, index, expected): + version_info = VersionInfo.parse(version) + assert version_info[index] == expected + + +@pytest.mark.parametrize( + "version, slice_object, expected", + [ + # Slice indices + ("1.2.3-rc.0+build.0", slice(0, 5), (1, 2, 3, "rc.0", "build.0")), + ("1.2.3-rc.0+build.0", slice(0, 4), (1, 2, 3, "rc.0")), + ("1.2.3-rc.0+build.0", slice(0, 3), (1, 2, 3)), + ("1.2.3-rc.0+build.0", slice(0, 2), (1, 2)), + ("1.2.3-rc.0+build.0", slice(3, 5), ("rc.0", "build.0")), + ("1.2.3-rc.0", slice(0, 4), (1, 2, 3, "rc.0")), + ("1.2.3-rc.0", slice(0, 3), (1, 2, 3)), + ("1.2.3-rc.0", slice(0, 2), (1, 2)), + ("1.2.3", slice(0, 10), (1, 2, 3)), + ("1.2.3", slice(0, 3), (1, 2, 3)), + ("1.2.3", slice(0, 2), (1, 2)), + # Special cases + ("1.2.3-rc.0+build.0", slice(3), (1, 2, 3)), + ("1.2.3-rc.0+build.0", slice(0, 5, 2), (1, 3, "build.0")), + ("1.2.3-rc.0+build.0", slice(None, 5, 2), (1, 3, "build.0")), + ("1.2.3-rc.0+build.0", slice(5, 0, -2), ("build.0", 3)), + ("1.2.0-rc.0+build.0", slice(3), (1, 2, 0)), + ], +) +def test_version_info_should_be_accessed_with_slice_object( + version, slice_object, expected +): + version_info = VersionInfo.parse(version) + assert version_info[slice_object] == expected + + +@pytest.mark.parametrize( + "version, index", + [ + ("1.2.3", 3), + ("1.2.3", slice(3, 4)), + ("1.2.3", 4), + ("1.2.3", slice(4, 5)), + ("1.2.3", 5), + ("1.2.3", slice(5, 6)), + ("1.2.3-rc.0", 5), + ("1.2.3-rc.0", slice(5, 6)), + ("1.2.3-rc.0", 6), + ("1.2.3-rc.0", slice(6, 7)), + ], +) +def test_version_info_should_throw_index_error(version, index): + version_info = VersionInfo.parse(version) + with pytest.raises(IndexError, match=r"Version part undefined"): + version_info[index] + + +@pytest.mark.parametrize( + "version, index", + [ + ("1.2.3", -1), + ("1.2.3", -2), + ("1.2.3", slice(-2, 2)), + ("1.2.3", slice(2, -2)), + ("1.2.3", slice(-2, -2)), + ], +) +def test_version_info_should_throw_index_error_when_negative_index(version, index): + version_info = VersionInfo.parse(version) + with pytest.raises(IndexError, match=r"Version index cannot be negative"): + version_info[index] diff --git a/tests/test_match.py b/tests/test_match.py new file mode 100644 index 00000000..b4cc50cc --- /dev/null +++ b/tests/test_match.py @@ -0,0 +1,56 @@ +import pytest + +from semver import match + + +def test_should_match_simple(): + assert match("2.3.7", ">=2.3.6") is True + + +def test_should_no_match_simple(): + assert match("2.3.7", ">=2.3.8") is False + + +@pytest.mark.parametrize( + "left,right,expected", + [ + ("2.3.7", "!=2.3.8", True), + ("2.3.7", "!=2.3.6", True), + ("2.3.7", "!=2.3.7", False), + ], +) +def test_should_match_not_equal(left, right, expected): + assert match(left, right) is expected + + +@pytest.mark.parametrize( + "left,right,expected", + [ + ("2.3.7", "<2.4.0", True), + ("2.3.7", ">2.3.5", True), + ("2.3.7", "<=2.3.9", True), + ("2.3.7", ">=2.3.5", True), + ("2.3.7", "==2.3.7", True), + ("2.3.7", "!=2.3.7", False), + ], +) +def test_should_not_raise_value_error_for_expected_match_expression( + left, right, expected +): + assert match(left, right) is expected + + +@pytest.mark.parametrize( + "left,right", [("2.3.7", "=2.3.7"), ("2.3.7", "~2.3.7"), ("2.3.7", "^2.3.7")] +) +def test_should_raise_value_error_for_unexpected_match_expression(left, right): + with pytest.raises(ValueError): + match(left, right) + + +@pytest.mark.parametrize( + "left,right", [("1.0.0", ""), ("1.0.0", "!"), ("1.0.0", "1.0.0")] +) +def test_should_raise_value_error_for_invalid_match_expression(left, right): + with pytest.raises(ValueError): + match(left, right) diff --git a/tests/test_max-min.py b/tests/test_max-min.py new file mode 100644 index 00000000..d465fe8e --- /dev/null +++ b/tests/test_max-min.py @@ -0,0 +1,43 @@ +import pytest + +from semver import max_ver, min_ver + + +def test_should_get_max(): + assert max_ver("3.4.5", "4.0.2") == "4.0.2" + + +def test_should_get_max_same(): + assert max_ver("3.4.5", "3.4.5") == "3.4.5" + + +def test_should_get_min(): + assert min_ver("3.4.5", "4.0.2") == "3.4.5" + + +def test_should_get_min_same(): + assert min_ver("3.4.5", "3.4.5") == "3.4.5" + + +@pytest.mark.parametrize( + "left,right,expected", + [ + ("1.2.3-rc.2", "1.2.3-rc.10", "1.2.3-rc.2"), + ("1.2.3-rc2", "1.2.3-rc10", "1.2.3-rc10"), + # identifiers with letters or hyphens are compared lexically in ASCII sort + # order. + ("1.2.3-Rc10", "1.2.3-rc10", "1.2.3-Rc10"), + # Numeric identifiers always have lower precedence than non-numeric + # identifiers. + ("1.2.3-2", "1.2.3-rc", "1.2.3-2"), + # A larger set of pre-release fields has a higher precedence than a + # smaller set, if all of the preceding identifiers are equal. + ("1.2.3-rc.2.1", "1.2.3-rc.2", "1.2.3-rc.2"), + # When major, minor, and patch are equal, a pre-release version has lower + # precedence than a normal version. + ("1.2.3", "1.2.3-1", "1.2.3-1"), + ("1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha"), + ], +) +def test_prerelease_order(left, right, expected): + assert min_ver(left, right) == expected diff --git a/tests/test_parsing.py b/tests/test_parsing.py new file mode 100644 index 00000000..c31cca18 --- /dev/null +++ b/tests/test_parsing.py @@ -0,0 +1,157 @@ +import pytest + +from semver import VersionInfo, parse, parse_version_info + + +@pytest.mark.parametrize( + "version,expected", + [ + # no. 1 + ( + "1.2.3-alpha.1.2+build.11.e0f985a", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "alpha.1.2", + "build": "build.11.e0f985a", + }, + ), + # no. 2 + ( + "1.2.3-alpha-1+build.11.e0f985a", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "alpha-1", + "build": "build.11.e0f985a", + }, + ), + ( + "0.1.0-0f", + {"major": 0, "minor": 1, "patch": 0, "prerelease": "0f", "build": None}, + ), + ( + "0.0.0-0foo.1", + {"major": 0, "minor": 0, "patch": 0, "prerelease": "0foo.1", "build": None}, + ), + ( + "0.0.0-0foo.1+build.1", + { + "major": 0, + "minor": 0, + "patch": 0, + "prerelease": "0foo.1", + "build": "build.1", + }, + ), + ], +) +def test_should_parse_version(version, expected): + result = parse(version) + assert result == expected + + +def test_parse_version_info_str_hash(): + s_version = "1.2.3-alpha.1.2+build.11.e0f985a" + v = parse_version_info(s_version) + assert v.__str__() == s_version + d = {} + d[v] = "" # to ensure that VersionInfo are hashable + + +@pytest.mark.parametrize( + "version,expected", + [ + # no. 1 + ( + "1.2.3-rc.0+build.0", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "rc.0", + "build": "build.0", + }, + ), + # no. 2 + ( + "1.2.3-rc.0.0+build.0", + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "rc.0.0", + "build": "build.0", + }, + ), + ], +) +def test_should_parse_zero_prerelease(version, expected): + result = parse(version) + assert result == expected + + +@pytest.mark.parametrize("version", ["01.2.3", "1.02.3", "1.2.03"]) +def test_should_raise_value_error_for_zero_prefixed_versions(version): + with pytest.raises(ValueError): + parse(version) + + +def test_equal_versions_have_equal_hashes(): + v1 = parse_version_info("1.2.3-alpha.1.2+build.11.e0f985a") + v2 = parse_version_info("1.2.3-alpha.1.2+build.22.a589f0e") + assert v1 == v2 + assert hash(v1) == hash(v2) + d = {} + d[v1] = 1 + d[v2] = 2 + assert d[v1] == 2 + s = set() + s.add(v1) + assert v2 in s + + +def test_parse_method_for_version_info(): + s_version = "1.2.3-alpha.1.2+build.11.e0f985a" + v = VersionInfo.parse(s_version) + assert str(v) == s_version + + +def test_next_version_with_invalid_parts(): + version = VersionInfo.parse("1.0.1") + with pytest.raises(ValueError): + version.next_version("invalid") + + +@pytest.mark.parametrize( + "version, part, expected", + [ + # major + ("1.0.4-rc.1", "major", "2.0.0"), + ("1.1.0-rc.1", "major", "2.0.0"), + ("1.1.4-rc.1", "major", "2.0.0"), + ("1.2.3", "major", "2.0.0"), + ("1.0.0-rc.1", "major", "1.0.0"), + # minor + ("0.2.0-rc.1", "minor", "0.2.0"), + ("0.2.5-rc.1", "minor", "0.3.0"), + ("1.3.1", "minor", "1.4.0"), + # patch + ("1.3.2", "patch", "1.3.3"), + ("0.1.5-rc.2", "patch", "0.1.5"), + # prerelease + ("0.1.4", "prerelease", "0.1.5-rc.1"), + ("0.1.5-rc.1", "prerelease", "0.1.5-rc.2"), + # special cases + ("0.2.0-rc.1", "patch", "0.2.0"), # same as "minor" + ("1.0.0-rc.1", "patch", "1.0.0"), # same as "major" + ("1.0.0-rc.1", "minor", "1.0.0"), # same as "major" + ], +) +def test_next_version_with_versioninfo(version, part, expected): + ver = VersionInfo.parse(version) + next_version = ver.next_version(part) + assert isinstance(next_version, VersionInfo) + assert str(next_version) == expected diff --git a/tests/test_pysemver-cli.py b/tests/test_pysemver-cli.py new file mode 100644 index 00000000..1fbeef26 --- /dev/null +++ b/tests/test_pysemver-cli.py @@ -0,0 +1,127 @@ +from argparse import Namespace +from contextlib import contextmanager + +import pytest + +from semver import cmd_bump, cmd_check, cmd_compare, cmd_nextver, createparser, main + + +@contextmanager +def does_not_raise(item): + yield item + + +@pytest.mark.parametrize( + "cli,expected", + [ + (["bump", "major", "1.2.3"], Namespace(bump="major", version="1.2.3")), + (["bump", "minor", "1.2.3"], Namespace(bump="minor", version="1.2.3")), + (["bump", "patch", "1.2.3"], Namespace(bump="patch", version="1.2.3")), + ( + ["bump", "prerelease", "1.2.3"], + Namespace(bump="prerelease", version="1.2.3"), + ), + (["bump", "build", "1.2.3"], Namespace(bump="build", version="1.2.3")), + # --- + (["compare", "1.2.3", "2.1.3"], Namespace(version1="1.2.3", version2="2.1.3")), + # --- + (["check", "1.2.3"], Namespace(version="1.2.3")), + ], +) +def test_should_parse_cli_arguments(cli, expected): + parser = createparser() + assert parser + result = parser.parse_args(cli) + del result.func + assert result == expected + + +@pytest.mark.parametrize( + "func,args,expectation", + [ + # bump subcommand + (cmd_bump, Namespace(bump="major", version="1.2.3"), does_not_raise("2.0.0")), + (cmd_bump, Namespace(bump="minor", version="1.2.3"), does_not_raise("1.3.0")), + (cmd_bump, Namespace(bump="patch", version="1.2.3"), does_not_raise("1.2.4")), + ( + cmd_bump, + Namespace(bump="prerelease", version="1.2.3-rc1"), + does_not_raise("1.2.3-rc2"), + ), + ( + cmd_bump, + Namespace(bump="build", version="1.2.3+build.13"), + does_not_raise("1.2.3+build.14"), + ), + # compare subcommand + ( + cmd_compare, + Namespace(version1="1.2.3", version2="2.1.3"), + does_not_raise("-1"), + ), + ( + cmd_compare, + Namespace(version1="1.2.3", version2="1.2.3"), + does_not_raise("0"), + ), + ( + cmd_compare, + Namespace(version1="2.4.0", version2="2.1.3"), + does_not_raise("1"), + ), + # check subcommand + (cmd_check, Namespace(version="1.2.3"), does_not_raise(None)), + (cmd_check, Namespace(version="1.2"), pytest.raises(ValueError)), + # nextver subcommand + ( + cmd_nextver, + Namespace(version="1.2.3", part="major"), + does_not_raise("2.0.0"), + ), + ( + cmd_nextver, + Namespace(version="1.2", part="major"), + pytest.raises(ValueError), + ), + ( + cmd_nextver, + Namespace(version="1.2.3", part="nope"), + pytest.raises(ValueError), + ), + ], +) +def test_should_process_parsed_cli_arguments(func, args, expectation): + with expectation as expected: + result = func(args) + assert result == expected + + +def test_should_process_print(capsys): + rc = main(["bump", "major", "1.2.3"]) + assert rc == 0 + captured = capsys.readouterr() + assert captured.out.rstrip() == "2.0.0" + + +def test_should_process_raise_error(capsys): + rc = main(["bump", "major", "1.2"]) + assert rc != 0 + captured = capsys.readouterr() + assert captured.err.startswith("ERROR") + + +def test_should_raise_systemexit_when_called_with_empty_arguments(): + with pytest.raises(SystemExit): + main([]) + + +def test_should_raise_systemexit_when_bump_iscalled_with_empty_arguments(): + with pytest.raises(SystemExit): + main(["bump"]) + + +def test_should_process_check_iscalled_with_valid_version(capsys): + result = main(["check", "1.1.1"]) + assert not result + captured = capsys.readouterr() + assert not captured.out diff --git a/tests/test_replace.py b/tests/test_replace.py new file mode 100644 index 00000000..e8e417a7 --- /dev/null +++ b/tests/test_replace.py @@ -0,0 +1,50 @@ +import pytest + +from semver import VersionInfo, replace + + +@pytest.mark.parametrize( + "version,parts,expected", + [ + ("3.4.5", dict(major=2), "2.4.5"), + ("3.4.5", dict(major="2"), "2.4.5"), + ("3.4.5", dict(major=2, minor=5), "2.5.5"), + ("3.4.5", dict(minor=2), "3.2.5"), + ("3.4.5", dict(major=2, minor=5, patch=10), "2.5.10"), + ("3.4.5", dict(major=2, minor=5, patch=10, prerelease="rc1"), "2.5.10-rc1"), + ( + "3.4.5", + dict(major=2, minor=5, patch=10, prerelease="rc1", build="b1"), + "2.5.10-rc1+b1", + ), + ("3.4.5-alpha.1.2", dict(major=2), "2.4.5-alpha.1.2"), + ("3.4.5-alpha.1.2", dict(build="x1"), "3.4.5-alpha.1.2+x1"), + ("3.4.5+build1", dict(major=2), "2.4.5+build1"), + ], +) +def test_replace_method_replaces_requested_parts(version, parts, expected): + assert replace(version, **parts) == expected + + +def test_replace_raises_TypeError_for_invalid_keyword_arg(): + with pytest.raises(TypeError, match=r"replace\(\).*unknown.*"): + assert replace("1.2.3", unknown="should_raise") + + +@pytest.mark.parametrize( + "version,parts,expected", + [ + ("3.4.5", dict(major=2, minor=5), "2.5.5"), + ("3.4.5", dict(major=2, minor=5, patch=10), "2.5.10"), + ("3.4.5-alpha.1.2", dict(major=2), "2.4.5-alpha.1.2"), + ("3.4.5-alpha.1.2", dict(build="x1"), "3.4.5-alpha.1.2+x1"), + ("3.4.5+build1", dict(major=2), "2.4.5+build1"), + ], +) +def test_should_return_versioninfo_with_replaced_parts(version, parts, expected): + assert VersionInfo.parse(version).replace(**parts) == VersionInfo.parse(expected) + + +def test_replace_raises_ValueError_for_non_numeric_values(): + with pytest.raises(ValueError): + VersionInfo.parse("1.2.3").replace(major="x") diff --git a/tests/test_semver.py b/tests/test_semver.py new file mode 100644 index 00000000..630ebbce --- /dev/null +++ b/tests/test_semver.py @@ -0,0 +1,77 @@ +import pytest # noqa + +from semver import VersionInfo + + +@pytest.mark.parametrize( + "string,expected", [("rc", "rc"), ("rc.1", "rc.2"), ("2x", "3x")] +) +def test_should_private_increment_string(string, expected): + assert VersionInfo._increment_string(string) == expected + + +@pytest.mark.parametrize( + "ver", + [ + {"major": -1}, + {"major": 1, "minor": -2}, + {"major": 1, "minor": 2, "patch": -3}, + {"major": 1, "minor": -2, "patch": 3}, + ], +) +def test_should_not_allow_negative_numbers(ver): + with pytest.raises(ValueError, match=".* is negative. .*"): + VersionInfo(**ver) + + +def test_should_versioninfo_to_dict(version): + resultdict = version.to_dict() + assert isinstance(resultdict, dict), "Got type from to_dict" + assert list(resultdict.keys()) == ["major", "minor", "patch", "prerelease", "build"] + + +def test_should_versioninfo_to_tuple(version): + result = version.to_tuple() + assert isinstance(result, tuple), "Got type from to_dict" + assert len(result) == 5, "Different length from to_tuple()" + + +def test_version_info_should_be_iterable(version): + assert tuple(version) == ( + version.major, + version.minor, + version.patch, + version.prerelease, + version.build, + ) + + +def test_should_be_able_to_use_strings_as_major_minor_patch(): + v = VersionInfo("1", "2", "3") + assert isinstance(v.major, int) + assert isinstance(v.minor, int) + assert isinstance(v.patch, int) + assert v.prerelease is None + assert v.build is None + assert VersionInfo("1", "2", "3") == VersionInfo(1, 2, 3) + + +def test_using_non_numeric_string_as_major_minor_patch_throws(): + with pytest.raises(ValueError): + VersionInfo("a") + with pytest.raises(ValueError): + VersionInfo(1, "a") + with pytest.raises(ValueError): + VersionInfo(1, 2, "a") + + +def test_should_be_able_to_use_integers_as_prerelease_build(): + v = VersionInfo(1, 2, 3, 4, 5) + assert isinstance(v.prerelease, str) + assert isinstance(v.build, str) + assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, "4", "5") + + +def test_should_versioninfo_isvalid(): + assert VersionInfo.isvalid("1.0.0") is True + assert VersionInfo.isvalid("foo") is False diff --git a/tests/test_subclass.py b/tests/test_subclass.py new file mode 100644 index 00000000..afd10b4a --- /dev/null +++ b/tests/test_subclass.py @@ -0,0 +1,19 @@ +from semver import VersionInfo + + +def test_subclass_from_versioninfo(): + class SemVerWithVPrefix(VersionInfo): + @classmethod + def parse(cls, version): + if not version[0] in ("v", "V"): + raise ValueError( + "{v!r}: version must start with 'v' or 'V'".format(v=version) + ) + return super().parse(version[1:]) + + def __str__(self): + # Reconstruct the tag. + return "v" + super().__str__() + + v = SemVerWithVPrefix.parse("v1.2.3") + assert str(v) == "v1.2.3" diff --git a/tests/test_typeerror-274.py b/tests/test_typeerror-274.py new file mode 100644 index 00000000..a0375d0d --- /dev/null +++ b/tests/test_typeerror-274.py @@ -0,0 +1,94 @@ +import sys + +import pytest + +import semver + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + + +def ensure_binary(s, encoding="utf-8", errors="strict"): + """ + Coerce ``s`` to bytes. + + * `str` -> encoded to `bytes` + * `bytes` -> `bytes` + + :param s: the string to convert + :type s: str | bytes + :param encoding: the encoding to apply, defaults to "utf-8" + :type encoding: str + :param errors: set a different error handling scheme; + other possible values are `ignore`, `replace`, and + `xmlcharrefreplace` as well as any other name + registered with :func:`codecs.register_error`. + Defaults to "strict". + :type errors: str + :raises TypeError: if ``s`` is not str or bytes type + :return: the converted string + :rtype: str + """ + if isinstance(s, str): + return s.encode(encoding, errors) + elif isinstance(s, bytes): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + +def test_should_work_with_string_and_unicode(): + result = semver.compare("1.1.0", b"1.2.2") + assert result == -1 + result = semver.compare(b"1.1.0", "1.2.2") + assert result == -1 + + +class TestEnsure: + # From six project + # grinning face emoji + UNICODE_EMOJI = "\U0001F600" + BINARY_EMOJI = b"\xf0\x9f\x98\x80" + + def test_ensure_binary_raise_type_error(self): + with pytest.raises(TypeError): + semver.ensure_str(8) + + def test_errors_and_encoding(self): + ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore") + with pytest.raises(UnicodeEncodeError): + ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="strict") + + def test_ensure_binary_raise(self): + converted_unicode = ensure_binary( + self.UNICODE_EMOJI, encoding="utf-8", errors="strict" + ) + converted_binary = ensure_binary( + self.BINARY_EMOJI, encoding="utf-8", errors="strict" + ) + + # PY3: str -> bytes + assert converted_unicode == self.BINARY_EMOJI and isinstance( + converted_unicode, bytes + ) + # PY3: bytes -> bytes + assert converted_binary == self.BINARY_EMOJI and isinstance( + converted_binary, bytes + ) + + def test_ensure_str(self): + converted_unicode = semver.ensure_str( + self.UNICODE_EMOJI, encoding="utf-8", errors="strict" + ) + converted_binary = semver.ensure_str( + self.BINARY_EMOJI, encoding="utf-8", errors="strict" + ) + + # PY3: str -> str + assert converted_unicode == self.UNICODE_EMOJI and isinstance( + converted_unicode, str + ) + # PY3: bytes -> str + assert converted_binary == self.UNICODE_EMOJI and isinstance( + converted_unicode, str + ) diff --git a/tox.ini b/tox.ini index 833c9655..cdbf6208 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,13 @@ [tox] envlist = flake8 - py{27,34,35,36,37} - pypy + py{36,37,38,39,310} + docs + mypy + [testenv] -description = Run test suite +description = Run test suite for {basepython} whitelist_externals = make commands = pytest {posargs:} deps = @@ -14,34 +16,47 @@ deps = setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 + [testenv:black] description = Check for formatting changes basepython = python3 deps = black commands = black --check {posargs:.} + [testenv:flake8] description = Check code style basepython = python3 deps = flake8 commands = flake8 {posargs:} + +[testenv:mypy] +description = Check code style +basepython = python3 +deps = mypy +commands = mypy {posargs:--ignore-missing-imports .} + + [testenv:docstrings] description = Check for PEP257 compatible docstrings basepython = python3 deps = docformatter commands = docformatter --check {posargs:--pre-summary-newline semver.py} + [testenv:checks] description = Run code style checks basepython = python3 deps = {[testenv:black]deps} {[testenv:flake8]deps} + {[testenv:mypy]deps} {[testenv:docstrings]deps} commands = {[testenv:black]commands} {[testenv:flake8]commands} + {[testenv:mypy]commands} {[testenv:docstrings]commands} [testenv:docs] @@ -66,5 +81,5 @@ deps = wheel twine commands = - python3 setup.py sdist bdist_wheel --universal + python3 setup.py sdist bdist_wheel twine check dist/* From fe64de25a4ac1d50a3a2a62a6ab79aee82b6183b Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 19 Oct 2020 11:36:21 +0200 Subject: [PATCH 02/86] Rework .gitignore file Completely revamp file. Mostly update from gh://github/gitignore/ and integrate: * Python * Global/Kate * Global/Vim * Global/VisualStudio * Global/JetBrains --- .gitignore | 251 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 219 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 2ef76af8..ffddda1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,31 @@ -# Patch/Diff Files -*.patch -*.diff - +# Python .gitignore file from gh://github/gitignore/Python.gitignore +# # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -.pytest_cache/ *$py.class +# C extensions +*.so + # Distribution / packaging -.cache -.emacs-project -.installed.cfg -.idea/ -*.egg -*.egg-info/ -.eggs/ .Python -.tmp/ build/ develop-eggs/ dist/ downloads/ eggs/ +.eggs/ lib/ lib64/ parts/ sdist/ var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg MANIFEST # PyInstaller @@ -36,43 +34,232 @@ MANIFEST *.manifest *.spec -# Environment -env*/ -venv*/ -.env* -.venv* - # Installer logs pip-log.txt pip-delete-this-directory.txt -# Spyder project settings -.spyderproject -.spyproject - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache +nosetests.xml coverage.xml -*,cover +*.cover +*.py,cover .hypothesis/ .pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation -doc/_build/ docs/_build/ # PyBuilder +.pybuilder/ target/ -# Backup files -*~ +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +#/-- Python + +#--- Kate from gh://github/gitignore/Global/Kate.gitignore *.kate-swp +.swp.* +#/--- Kate + +#--- Vim from gh://github/gitignore/Global/Vim.gitignore +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ +#/--- Vim + +#--- VisualStudioCode from gh://github/gitignore/Global/VisualStudioCode.gitignore +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +#/--- VisualStudio + +#--- JetBrains from gh://github/gitignore/Global/JetBrains.gitignore +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +#/--- JetBrains + +# -------- + + + +# Patch/Diff Files +*.patch +*.diff From 24bcdfd18531840f436ffb3a93a43459b543a202 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 18 Oct 2020 14:43:03 +0200 Subject: [PATCH 03/86] Improve semver documentation * Remove Python2 * Use Roboto font family Sans: Roboto (for text) "Serif": Roboto Slab (for headings) Monospace: Roboto Mono (for code) * Improve CSS navigation * Create new logo and move it to docs/_static * Add new section about how to get the version of semver * Use a more general term (2.x.y) instead of specific versions * Number sections * Rework deps for doc build * Rework release procedure * Add sphinx-autodoc-typehints dependency for type hints --- README.rst | 2 +- docs/_static/css/custom.css | 74 ++++++++++++++++++++++ docs/_static/css/semver.css | 20 ------ docs/{ => _static}/logo.svg | 121 ++++++++++++++---------------------- docs/conf.py | 12 ++-- docs/index.rst | 1 + docs/install.rst | 14 +---- docs/requirements.txt | 3 +- docs/usage.rst | 13 +++- release-procedure.md | 69 ++++++++++++++------ 10 files changed, 195 insertions(+), 134 deletions(-) create mode 100644 docs/_static/css/custom.css delete mode 100644 docs/_static/css/semver.css rename docs/{ => _static}/logo.svg (56%) diff --git a/README.rst b/README.rst index 8f777ed3..fa001046 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. maintenance branch |MAINT|_. The last version of semver which supports Python 2.7 to 3.5 will be - 2.11.x. However, keep in mind, version 2.11.x is frozen: no new + 2.x.y However, keep in mind, the major 2 release is frozen: no new features nor backports will be integrated. We recommend to upgrade your workflow to Python 3.x to gain support, diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 00000000..33ff51f1 --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,74 @@ +/* +https://github.com/bitprophet/alabaster +*/ + +/* Roboto (Sans), Roboto Slab ("serif"), Roboto Mono*/ +@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,400;0,600;1,400&family=Roboto+Slab:wght@700&family=Roboto:ital@0;1&display=swap'); + +.logo { + font-family: "Roboto Slab"; +} + +img.logo { + width: 80%; +} + +div.document { + margin-top: 0pt; +} + +div.related.top { + margin-top: -1em; +} + +div.related.top nav { + margin-bottom: 0.5em; + margin-top: 0.5em; +} + +.section h1 { + font-weight: 700; +} + +.py.method { + padding-top: 0.25em; + padding-bottom: 1.25em; + border-top: 1px solid #EEE; +} + +.py.function{ + padding-top: 1.25em; +} + +.related.bottom { + margin-top: 1em; +} + +body { + font-weight: 400; +} + +nav#rellinks { + float: left; + width: 100%; +} + +nav#rellinks li:first-child { + float: left; + text-align: left; + width: 50%; +} + +nav#rellinks li:last-child { + float: right; + text-align: right; + width: 50%; +} + +nav#rellinks li+li:before { + content: ""; +} + +div.related.top nav::after { + float: none; +} diff --git a/docs/_static/css/semver.css b/docs/_static/css/semver.css deleted file mode 100644 index 88b6cba6..00000000 --- a/docs/_static/css/semver.css +++ /dev/null @@ -1,20 +0,0 @@ -/* -*/ - -.py.method { - padding-top: 0.25em; - padding-bottom: 1.25em; - border-top: 1px solid #EEE; -} - -.py.function{ - padding-top: 1.25em; -} - -.related.bottom { - margin-top: 1em; -} - -nav#rellinks { - float: left; -} \ No newline at end of file diff --git a/docs/logo.svg b/docs/_static/logo.svg similarity index 56% rename from docs/logo.svg rename to docs/_static/logo.svg index b2853465..1be72ee6 100644 --- a/docs/logo.svg +++ b/docs/_static/logo.svg @@ -1,4 +1,4 @@ - + diff --git a/docs/conf.py b/docs/conf.py index 5d7f7eef..3653ecce 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,6 +35,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.extlinks", @@ -107,7 +108,7 @@ #: Makes the sidebar "fixed" or pinned in place: "fixed_sidebar": True, #: Relative path to $PROJECT/_static to logo image: - # "logo": "logo.svg", + "logo": "logo.svg", #: Set to true to insert your site's project name under #: the logo: # "logo_name": True, @@ -154,13 +155,14 @@ # # -- Fonts # "code_font_size": "", - "font_family": "", - "head_font_family": "", - "font_size": "1.25rem", + "font_family": "'Roboto',sans-serif", + "head_font_family": "'Roboto Slab',serif", + "code_font_family": "'Roboto Mono',monospace", + "font_size": "1.20rem", } html_static_path = ["_static"] -html_css_files = ["css/semver.css"] +html_css_files = ["css/custom.css"] # html_logo = "logo.svg" diff --git a/docs/index.rst b/docs/index.rst index 4cc5a966..c9ba7626 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,7 @@ Semver |version| -- Semantic Versioning .. toctree:: :maxdepth: 2 :caption: Contents + :numbered: readme install diff --git a/docs/install.rst b/docs/install.rst index 7086fc5d..b603703c 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -5,7 +5,7 @@ Release Policy -------------- As semver uses `Semantic Versioning`_, breaking changes are only introduced in major -releases (incremented X in "X.Y.Z"). +releases (incremented ``X`` in "X.Y.Z"). For users who want to stay with major 2 releases only, add the following version restriction:: @@ -13,7 +13,7 @@ restriction:: semver>=2,<3 This line avoids surprises. You will get any updates within the major 2 release like -2.9.1, 2.10.0, or above. However, you will never get an update for semver 3.0.0. +2.11.0 or above. However, you will never get an update for semver 3.0.0. Keep in mind, as this line avoids any major version updates, you also will never get new exciting features or bug fixes. @@ -24,14 +24,6 @@ file that lists your dependencies. Pip --- -For Python 2: - -.. code-block:: bash - - pip install semver - -For Python 3: - .. code-block:: bash pip3 install semver @@ -41,7 +33,7 @@ with an URL and its version: .. parsed-literal:: - pip3 install git+https://github.com/python-semver/python-semver.git@2.10.0 + pip3 install git+https://github.com/python-semver/python-semver.git@2.11.0 Linux Distributions diff --git a/docs/requirements.txt b/docs/requirements.txt index 26233642..ee76828b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,4 @@ # requirements file for documentation sphinx sphinx-argparse -# sphinx_rtd_theme -guzzle_sphinx_theme \ No newline at end of file +sphinx-autodoc-typehints diff --git a/docs/usage.rst b/docs/usage.rst index 10ca3404..4e2b6f92 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -8,7 +8,7 @@ Each type can be converted into the other, if the minimum requirements are met. -Knowing the Implemented semver.org Version +Getting the Implemented semver.org Version ------------------------------------------ The semver.org page is the authoritative specification of how semantic @@ -20,6 +20,15 @@ use the following constant:: '2.0.0' +Getting the Version of semver +----------------------------- + +To know the version of semver itself, use the following construct:: + + >>> semver.__version__ + '3.0.0-dev.1' + + Creating a Version ------------------ @@ -37,7 +46,7 @@ creating a version: .. warning:: **Deprecation Warning** - Module level functions are marked as *deprecated* in version 2.10.0 now. + Module level functions are marked as *deprecated* in version 2.x.y now. These functions will be removed in semver 3. For details, see the sections :ref:`sec_replace_deprecated_functions` and :ref:`sec_display_deprecation_warnings`. diff --git a/release-procedure.md b/release-procedure.md index f2571be9..db9ed1b5 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -1,52 +1,81 @@ # Release Procedure -1. Verify that issues about new release are closed https://github.com/python-semver/python-semver/issues and verify that no pull requests that should be included in this release haven't been left out https://github.com/python-semver/python-semver/pulls +The following procedures gives a short overview of what steps are needed to +create a new release. -1. Verify that continuous integration for latest build was passing https://travis-ci.com/python-semver/python-semver +## Prepare the Release -1. Verify that `__version__` in [semver.py](https://github.com/python-semver/python-semver/blob/master/semver.py) have been updated and follow https://semver.org/ +1. Verify that issues about new release are closed https://github.com/python-semver/python-semver/issues. -1. Verify that [CHANGELOG](https://github.com/python-semver/python-semver/blob/master/CHANGELOG.rst) have been updated. No WIP should be present in CHANGELOG during release! +1. Verify that no pull requests that should be included in this release haven't been left out https://github.com/python-semver/python-semver/pulls. + +1. Verify that continuous integration for latest build was passing https://travis-ci.com/python-semver/python-semver. + +1. Create a new branch `release/VERSION`. 1. If one or several supported Python versions have been removed or added, verify that the 3 following files have been updated: * [setup.py](https://github.com/python-semver/python-semver/blob/master/setup.py) * [tox.ini](https://github.com/python-semver/python-semver/blob/master/tox.ini) * [.travis.yml](https://github.com/python-semver/python-semver/blob/master/.travis.yml) -1. Verify that doc reflecting new changes have been updated and are available at https://python-semver.readthedocs.io/en/latest/ If necessary, trigger doc build at https://readthedocs.org/projects/python-semver/ +1. Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/python-semver/python-semver/blob/master/CONTRIBUTORS). + +1. Verify that `__version__` in [semver.py](https://github.com/python-semver/python-semver/blob/master/semver.py) have been updated and follow https://semver.org. + +1. Show the new draft [CHANGELOG](https://github.com/python-semver/python-semver/blob/master/CHANGELOG.rst) entry for the latest release with: + + $ tox -e changelog + + Check the output. If you are not happy, update the files in the + `changelog.d/` directory. + If everything is okay, build the new `CHANGELOG` with: -1. Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/python-semver/python-semver/blob/master/CONTRIBUTORS) + $ tox -e changelog -- build + +1. Build the documentation and check the output: + + $ tox -e docs + + +## Create the New Release 1. Ensure that long description (ie [README.rst](https://github.com/python-semver/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` -1. Upload it to TestPyPI first: +1. Upload the wheel and source to TestPyPI first: ```bash - git clean -xfd - python setup.py sdist bdist_wheel --universal - twine upload --repository-url https://test.pypi.org/legacy/ dist/* + $ git clean -xfd + $ rm dist/* + $ python3 setup.py sdist bdist_wheel + $ twine upload --repository-url https://test.pypi.org/legacy/ dist/* ``` If you have a `~/.pypirc` with a `testpyi` section, the upload can be simplified: - twine upload --repository testpyi dist/* + $ twine upload --repository testpyi dist/* + +1. Check if everything is okay with the wheel. -1. Upload to PyPI +1. Upload to PyPI: ```bash - git clean -xfd - python setup.py register sdist bdist_wheel --universal - twine upload dist/* + $ git clean -xfd + $ python setup.py register sdist bdist_wheel + $ twine upload dist/* ``` -1. Go to https://pypi.org/project/semver/ to verify that new version is online and page is rendered correctly +1. Go to https://pypi.org/project/semver/ to verify that new version is online and the page is rendered correctly. -1. Tag commit and push to github using command line interface +1. Tag commit and push to GitHub using command line interface: ```bash - git tag -a x.x.x -m 'Version x.x.x' - git push python-semver master --tags + $ git tag -a x.x.x -m 'Version x.x.x' + $ git push python-semver master --tags ``` -or using GitHub web interface available at https://github.com/python-semver/python-semver/releases +1. In [GitHub Release page](https://github.com/python-semver/python-semver/release) + document the new release. + Usually it's enough to take it from a commit message or the tag description. + +You're done! Celebrate! From cbd4335fd45fd9d961f1547aad42b9f223805f64 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 19 Oct 2020 11:13:58 +0200 Subject: [PATCH 04/86] Configure and add Towncrier files * Add `changelog.d/.gitignore` to keep this directory * Create `changelog.d/README.rst` with some descriptions * Add `changelog.d/_template.rst` as Towncrier template * Add `[tool.towncrier]` section in pyproject.toml * Add "changelog" target into `tox.ini`. Use it like "tox -e changes -- CMD" whereas CMD is a towncrier command. The default "tox -e changes" calls towncrier to create a draft of the changelog file and output it to stdout. * Update documentation and add include a new section "Changelog" included from `changelog.d/README.rst` * Update changelog.d directory and add new files * Add news file in changelog.d Co-authored-by: Thomas Laferriere --- CHANGELOG.rst | 393 ++++------------------------ changelog.d/.gitignore | 1 + changelog.d/213.improvement.rst | 1 + changelog.d/234.deprecation.rst | 2 + changelog.d/270.feature.rst | 13 + changelog.d/276.feature.rst | 1 + changelog.d/291.bugfix.rst | 2 + changelog.d/README.rst | 76 ++++++ changelog.d/_template.rst | 42 +++ changelog.d/pr290.deprecation.rst | 10 + changelog.d/pr290.doc.rst | 7 + changelog.d/pr290.feature.rst | 11 + docs/changelog-2.7.9-and-before.rst | 353 +++++++++++++++++++++++++ docs/development.rst | 41 +-- docs/index.rst | 15 +- pyproject.toml | 59 +++++ tox.ini | 11 + 17 files changed, 682 insertions(+), 356 deletions(-) create mode 100644 changelog.d/.gitignore create mode 100644 changelog.d/213.improvement.rst create mode 100644 changelog.d/234.deprecation.rst create mode 100644 changelog.d/270.feature.rst create mode 100644 changelog.d/276.feature.rst create mode 100644 changelog.d/291.bugfix.rst create mode 100644 changelog.d/README.rst create mode 100644 changelog.d/_template.rst create mode 100644 changelog.d/pr290.deprecation.rst create mode 100644 changelog.d/pr290.doc.rst create mode 100644 changelog.d/pr290.feature.rst create mode 100644 docs/changelog-2.7.9-and-before.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7427340f..0455560c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,9 +2,21 @@ Change Log ########## +Changes for the upcoming release can be found in +the `"changelog.d" directory `_ +in our repository. + +.. + Do *NOT* add changelog entries here! + + This changelog is managed by towncrier and is compiled at release time. + + See https://python-semver.rtd.io/en/latest/development.html#changelog + for details. + +.. towncrier release notes start + -All notable changes to this code base will be documented in this file, -in every released version. Version 2.13.0 ============== @@ -12,6 +24,7 @@ Version 2.13.0 :Released: 2020-10-20 :Maintainer: Tom Schraitle + Features -------- @@ -28,34 +41,36 @@ Bug Fixes is ignored. +Additions +--------- + +n/a + + +Deprecations +------------ + +n/a + + +---- + + Version 2.12.0 ============== :Released: 2020-10-19 :Maintainer: Tom Schraitle -Features --------- - -n/a Bug Fixes --------- * :gh:`291` (:pr:`292`): Disallow negative numbers of - major, minor, and patch for ``semver.VersionInfo`` + ``major``, ``minor``, and ``patch`` for :class:`semver.VersionInfo` -Additions ---------- - -n/a - - -Deprecations ------------- - -n/a +---- Version 2.11.0 @@ -64,30 +79,16 @@ Version 2.11.0 :Released: 2020-10-17 :Maintainer: Tom Schraitle -Features --------- - -n/a - Bug Fixes --------- -* :gh:`276` (:pr:`277`): VersionInfo.parse should be a class method +* :gh:`276` (:pr:`277`): ``VersionInfo.parse`` should be a class method Also add authors and update changelog in :gh:`286` * :gh:`274` (:pr:`275`): Py2 vs. Py3 incompatibility TypeError -Additions ---------- - -n/a - - -Deprecations ------------- - -n/a +---- Version 2.10.2 @@ -109,11 +110,6 @@ Bug Fixes * :pr:`263`: Doc: Add missing "install" subcommand for openSUSE -Additions ---------- - -n/a - Deprecations ------------ @@ -122,6 +118,9 @@ Deprecations * :func:`semver.min_ver` +---- + + Version 2.10.1 ============== @@ -138,6 +137,7 @@ Features * :pr:`256`: Made docstrings consistent + Bug Fixes --------- @@ -145,6 +145,10 @@ Bug Fixes to always return a ``VersionInfo`` instance. +---- + + + Version 2.10.0 ============== @@ -158,6 +162,11 @@ Features Allows to access a version like ``version[1]``. * :pr:`235`: Improved documentation and shift focus on ``semver.VersionInfo`` instead of advertising the old and deprecated module-level functions. +* :pr:`230`: Add version information in some functions: + + * Use ``.. versionadded::`` RST directive in docstrings to + make it more visible when something was added + * Minor wording fix in docstrings (versions -> version strings) Bug Fixes @@ -192,6 +201,8 @@ Deprecations These deprecated functions will be removed in semver 3. +---- + Version 2.9.1 ============= @@ -218,6 +229,8 @@ Bug Fixes * :gh:`192` (:pr:`193`): Fixed "pysemver" and "pysemver bump" when called without arguments +---- + Version 2.9.0 ============= :Released: 2019-10-30 @@ -261,6 +274,8 @@ Removals * :gh:`148` (:pr:`149`): Removed and replaced ``python setup.py test`` +---- + Version 2.8.2 ============= :Released: 2019-05-19 @@ -268,6 +283,7 @@ Version 2.8.2 Skipped, not released. +---- Version 2.8.1 ============= @@ -289,6 +305,8 @@ Bug Fixes * :gh:`96` (:pr:`97`): Made VersionInfo immutable +---- + Version 2.8.0 ============= :Released: 2018-05-16 @@ -313,305 +331,6 @@ Removals * :gh:`76` (:pr:`80`): Removed Python 2.6 compatibility -Version 2.7.9 -============= - -:Released: 2017-09-23 -:Maintainer: Kostiantyn Rybnikov - - -Additions ---------- - -* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. - - -Version 2.7.8 -============= - -:Released: 2017-08-25 -:Maintainer: Kostiantyn Rybnikov - -* :gh:`62`: Support custom default names for pre and build - - -Version 2.7.7 -============= - -:Released: 2017-05-25 -:Maintainer: Kostiantyn Rybnikov - -* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects -* :pr:`56`: Added support for Python 3.6 - - -Version 2.7.2 -============= - -:Released: 2016-11-08 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Added :func:`semver.parse_version_info` to parse a version string to a - version info tuple. - -Bug Fixes ---------- - -* :gh:`37`: Removed trailing zeros from prelease doesn't allow to - parse 0 pre-release version - -* Refine parsing to conform more strictly to SemVer 2.0.0. - - SemVer 2.0.0 specification §9 forbids leading zero on identifiers in - the prerelease version. - - -Version 2.6.0 -============= - -:Released: 2016-06-08 -:Maintainer: Kostiantyn Rybnikov - -Removals --------- - -* Remove comparison of build component. - - SemVer 2.0.0 specification recommends that build component is - ignored in comparisons. - - -Version 2.5.0 -============= - -:Released: 2016-05-25 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Support matching 'not equal' with “!=”. - -Changes -------- - -* Made separate builds for tests on Travis CI. - - -Version 2.4.2 -============= - -:Released: 2016-05-16 -:Maintainer: Kostiantyn Rybnikov - -Changes -------- - -* Migrated README document to reStructuredText format. - -* Used Setuptools for distribution management. - -* Migrated test cases to Py.test. - -* Added configuration for Tox test runner. - - -Version 2.4.1 -============= - -:Released: 2016-03-04 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* :gh:`23`: Compared build component of a version. - - -Version 2.4.0 -============= - -:Released: 2016-02-12 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* :gh:`21`: Compared alphanumeric components correctly. - - -Version 2.3.1 -============= - -:Released: 2016-01-30 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Declared granted license name in distribution metadata. - - -Version 2.3.0 -============= - -:Released: 2016-01-29 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Added functions to increment prerelease and build components in a - version. - - -Version 2.2.1 -============= - -:Released: 2015-08-04 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Corrected comparison when any component includes zero. - - -Version 2.2.0 -============= - -:Released: 2015-06-21 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Add functions to determined minimum and maximum version. - -* Add code examples for recently-added functions. - - -Version 2.1.2 -============= - -:Released: 2015-05-23 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Restored current README document to distribution manifest. - - -Version 2.1.1 -============= - -:Released: 2015-05-23 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Removed absent document from distribution manifest. - - -Version 2.1.0 -============= - -:Released: 2015-05-22 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Documented installation instructions. - -* Documented project home page. - -* Added function to format a version string from components. - -* Added functions to increment specific components in a version. - -Changes -------- - -* Migrated README document to Markdown format. - -Bug Fixes ---------- - -* Corrected code examples in README document. - - -Version 2.0.2 -============= - -:Released: 2015-04-14 -:Maintainer: Konstantine Rybnikov - -Additions ---------- - -* Added configuration for Travis continuous integration. - -* Explicitly declared supported Python versions. - - -Version 2.0.1 -============= - -:Released: 2014-09-24 -:Maintainer: Konstantine Rybnikov - -Bug Fixes ---------- - -* :gh:`9`: Fixed comparison of equal version strings. - - -Version 2.0.0 -============= - -:Released: 2014-05-24 -:Maintainer: Konstantine Rybnikov - -Additions ---------- - -* Grant license in this code base under BSD 3-clause license terms. - -Changes -------- - -* Update parser to SemVer standard 2.0.0. - -* Ignore build component for comparison. - - -Version 0.0.2 -============= - -:Released: 2012-05-10 -:Maintainer: Konstantine Rybnikov - -Changes -------- - -* Use standard library Distutils for distribution management. - - -Version 0.0.1 -============= - -:Released: 2012-04-28 -:Maintainer: Konstantine Rybnikov - -* Initial release. - - .. Local variables: coding: utf-8 diff --git a/changelog.d/.gitignore b/changelog.d/.gitignore new file mode 100644 index 00000000..f935021a --- /dev/null +++ b/changelog.d/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/changelog.d/213.improvement.rst b/changelog.d/213.improvement.rst new file mode 100644 index 00000000..dcedc695 --- /dev/null +++ b/changelog.d/213.improvement.rst @@ -0,0 +1 @@ +Add typing information \ No newline at end of file diff --git a/changelog.d/234.deprecation.rst b/changelog.d/234.deprecation.rst new file mode 100644 index 00000000..bf0c0301 --- /dev/null +++ b/changelog.d/234.deprecation.rst @@ -0,0 +1,2 @@ +In :file:`setup.py` simplified file and remove +``Tox`` and ``Clean`` classes diff --git a/changelog.d/270.feature.rst b/changelog.d/270.feature.rst new file mode 100644 index 00000000..79a6ee4a --- /dev/null +++ b/changelog.d/270.feature.rst @@ -0,0 +1,13 @@ +Configure Towncrier (:pr:`273`:) + +* Add :file:`changelog.d/.gitignore` to keep this directory +* Create :file:`changelog.d/README.rst` with some descriptions +* Add :file:`changelog.d/_template.rst` as Towncrier template +* Add ``[tool.towncrier]`` section in :file:`pyproject.toml` +* Add "changelog" target into :file:`tox.ini`. Use it like + :command:`tox -e changelog -- CMD` whereas ``CMD`` is a + Towncrier command. The default :command:`tox -e changelog` + calls Towncrier to create a draft of the changelog file + and output it to stdout. +* Update documentation and add include a new section + "Changelog" included from :file:`changelog.d/README.rst`. diff --git a/changelog.d/276.feature.rst b/changelog.d/276.feature.rst new file mode 100644 index 00000000..9fc4680b --- /dev/null +++ b/changelog.d/276.feature.rst @@ -0,0 +1 @@ +Document how to create a sublass from :class:`VersionInfo` class diff --git a/changelog.d/291.bugfix.rst b/changelog.d/291.bugfix.rst new file mode 100644 index 00000000..74ee4d87 --- /dev/null +++ b/changelog.d/291.bugfix.rst @@ -0,0 +1,2 @@ +Disallow negative numbers in VersionInfo arguments +for ``major``, ``minor``, and ``patch``. \ No newline at end of file diff --git a/changelog.d/README.rst b/changelog.d/README.rst new file mode 100644 index 00000000..6c478204 --- /dev/null +++ b/changelog.d/README.rst @@ -0,0 +1,76 @@ +The ``changelog.d`` Directory +============================= + +.. This file is also included into the documentation + +.. -text-begin- + +A "Changelog" is a record of all notable changes made to a project. Such +a changelog, in our case the :file:`CHANGELOG.rst`, is read by our *users*. +Therefor, any description should be aimed to users instead of describing +internal changes which are only relevant to developers. + +To avoid merge conflicts, we use the `Towncrier`_ package to manage our changelog. + +The directory :file:`changelog.d` contains "newsfragments" which are short +ReST-formatted files. +On release, those news fragments are compiled into our :file:`CHANGELOG.rst`. + +You don't need to install ``towncrier`` yourself, use the :command:`tox` command +to call the tool. + +We recommend to follow the steps to make a smooth integration of your changes: + +#. After you have created a new pull request (PR), add a new file into the + directory :file:`changelog.d`. Each filename follows the syntax:: + + ..rst + + where ```` is the GitHub issue number. + In case you have no issue but a pull request, prefix your number with ``pr``. + ```` is one of: + + * ``bugfix``: fixes a reported bug. + * ``deprecation``: informs about deprecation warnings + * ``doc``: improves documentation. + * ``feature``: adds new user facing features. + * ``removal``: removes obsolete or deprecated features. + * ``trivial``: fixes a small typo or internal change that might be noteworthy. + + For example: ``123.feature.rst``, ``pr233.removal.rst``, ``456.bugfix.rst`` etc. + +#. Create the new file with the command:: + + tox -e changelog -- create 123.feature.rst + + The file is created int the :file:`changelog.d/` directory. + +#. Open the file and describe your changes in RST format. + + * Wrap symbols like modules, functions, or classes into double backticks + so they are rendered in a ``monospace font``. + * Prefer simple past tense or constructions with "now". + +#. Check your changes with:: + + tox -e changelog -- check + +#. Optionally, build a draft version of the changelog file with the command:: + + tox -e changelog + +#. Commit all your changes and push it. + + +This finishes your steps. + +On release, the maintainer compiles a new :file:`CHANGELOG.rst` file by running:: + + tox -e changelog -- build + +This will remove all newsfragments inside the :file:`changelog.d` directory, +making it ready for the next release. + + + +.. _Towncrier: https://pypi.org/project/towncrier diff --git a/changelog.d/_template.rst b/changelog.d/_template.rst new file mode 100644 index 00000000..982ad41a --- /dev/null +++ b/changelog.d/_template.rst @@ -0,0 +1,42 @@ +{% for section, _ in sections.items() %} +{% set underline = underlines[0] %}{% if section %}{{section}} +{{ underline * section|length }}{% set underline = underlines[1] %} + +{% endif %} + +:Released: {{ versiondata.date }} +:Maintainer: + + +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section] %} +{{ definitions[category]['name'] }} +{{ underline * definitions[category]['name']|length }} + +{% if definitions[category]['showcontent'] %} +{% for text, values in sections[section][category].items() %} +{%- for value in values %} +{% if value.startswith("pr") %} +* :pr:`{{ value[2:] }}`{% else %} +* :gh:`{{ value[1:] }}`{% endif %}{%- endfor -%}: {{ text }} + +{% endfor %} + +{% else %} +- {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% if sections[section][category]|length == 0 %} +No significant changes. + +{% else %} +{% endif %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} +{% endfor %} +---- diff --git a/changelog.d/pr290.deprecation.rst b/changelog.d/pr290.deprecation.rst new file mode 100644 index 00000000..1067d5f2 --- /dev/null +++ b/changelog.d/pr290.deprecation.rst @@ -0,0 +1,10 @@ +For semver 3.0.0-alpha0: + +* Remove anything related to Python2 +* In :file:`tox.ini` and :file:`.travis.yml` + Remove targets py27, py34, py35, and pypy. + Add py38, py39, and nightly (allow to fail) +* In :file:`setup.py` simplified file and remove + ``Tox`` and ``Clean`` classes +* Remove old Python versions (2.7, 3.4, 3.5, and pypy) + from Travis \ No newline at end of file diff --git a/changelog.d/pr290.doc.rst b/changelog.d/pr290.doc.rst new file mode 100644 index 00000000..fa420ac9 --- /dev/null +++ b/changelog.d/pr290.doc.rst @@ -0,0 +1,7 @@ +Several improvements in the documentation: + +* New layout to distinguish from the semver2 development line. +* Create new logo. +* Remove any occurances of Python2. +* Describe changelog process with Towncrier. +* Update the release process. diff --git a/changelog.d/pr290.feature.rst b/changelog.d/pr290.feature.rst new file mode 100644 index 00000000..f2a937a8 --- /dev/null +++ b/changelog.d/pr290.feature.rst @@ -0,0 +1,11 @@ +Create semver 3.0.0-alpha0 + +* Update :file:`README.rst`, mention maintenance + branch ``maint/v2``. +* Remove old code mainly used for Python2 compatibility, + adjusted code to support Python3 features. +* Split test suite into separate files under :file:`tests/` + directory +* Adjust and update :file:`setup.py`. Requires Python >=3.6.* + Extract metadata directly from source (affects all the ``__version__``, + ``__author__`` etc. variables) \ No newline at end of file diff --git a/docs/changelog-2.7.9-and-before.rst b/docs/changelog-2.7.9-and-before.rst new file mode 100644 index 00000000..f7acc1e1 --- /dev/null +++ b/docs/changelog-2.7.9-and-before.rst @@ -0,0 +1,353 @@ +################ +Older Change Log +################ + +This changelog contains older entries from +2.7.9 and before. + +Version 2.7.9 +============= + +:Released: 2017-09-23 +:Maintainer: Kostiantyn Rybnikov + + +Additions +--------- + +* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. + + +---- + +Version 2.7.8 +============= + +:Released: 2017-08-25 +:Maintainer: Kostiantyn Rybnikov + +* :gh:`62`: Support custom default names for pre and build + + +---- + +Version 2.7.7 +============= + +:Released: 2017-05-25 +:Maintainer: Kostiantyn Rybnikov + +* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects +* :pr:`56`: Added support for Python 3.6 + + +---- + +Version 2.7.2 +============= + +:Released: 2016-11-08 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Added :func:`semver.parse_version_info` to parse a version string to a + version info tuple. + +Bug Fixes +--------- + +* :gh:`37`: Removed trailing zeros from prelease doesn't allow to + parse 0 pre-release version + +* Refine parsing to conform more strictly to SemVer 2.0.0. + + SemVer 2.0.0 specification §9 forbids leading zero on identifiers in + the prerelease version. + + +---- + +Version 2.6.0 +============= + +:Released: 2016-06-08 +:Maintainer: Kostiantyn Rybnikov + +Removals +-------- + +* Remove comparison of build component. + + SemVer 2.0.0 specification recommends that build component is + ignored in comparisons. + + +---- + +Version 2.5.0 +============= + +:Released: 2016-05-25 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Support matching 'not equal' with “!=”. + +Changes +------- + +* Made separate builds for tests on Travis CI. + + +---- + +Version 2.4.2 +============= + +:Released: 2016-05-16 +:Maintainer: Kostiantyn Rybnikov + +Changes +------- + +* Migrated README document to reStructuredText format. + +* Used Setuptools for distribution management. + +* Migrated test cases to Py.test. + +* Added configuration for Tox test runner. + + +---- + +Version 2.4.1 +============= + +:Released: 2016-03-04 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* :gh:`23`: Compared build component of a version. + + +---- + +Version 2.4.0 +============= + +:Released: 2016-02-12 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* :gh:`21`: Compared alphanumeric components correctly. + + +---- + +Version 2.3.1 +============= + +:Released: 2016-01-30 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Declared granted license name in distribution metadata. + + +---- + +Version 2.3.0 +============= + +:Released: 2016-01-29 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Added functions to increment prerelease and build components in a + version. + + +---- + +Version 2.2.1 +============= + +:Released: 2015-08-04 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Corrected comparison when any component includes zero. + + +---- + +Version 2.2.0 +============= + +:Released: 2015-06-21 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Add functions to determined minimum and maximum version. + +* Add code examples for recently-added functions. + + +---- + +Version 2.1.2 +============= + +:Released: 2015-05-23 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Restored current README document to distribution manifest. + + +---- + +Version 2.1.1 +============= + +:Released: 2015-05-23 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Removed absent document from distribution manifest. + + +---- + +Version 2.1.0 +============= + +:Released: 2015-05-22 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Documented installation instructions. + +* Documented project home page. + +* Added function to format a version string from components. + +* Added functions to increment specific components in a version. + +Changes +------- + +* Migrated README document to Markdown format. + +Bug Fixes +--------- + +* Corrected code examples in README document. + + +---- + +Version 2.0.2 +============= + +:Released: 2015-04-14 +:Maintainer: Konstantine Rybnikov + +Additions +--------- + +* Added configuration for Travis continuous integration. + +* Explicitly declared supported Python versions. + + +---- + +Version 2.0.1 +============= + +:Released: 2014-09-24 +:Maintainer: Konstantine Rybnikov + +Bug Fixes +--------- + +* :gh:`9`: Fixed comparison of equal version strings. + + +---- + +Version 2.0.0 +============= + +:Released: 2014-05-24 +:Maintainer: Konstantine Rybnikov + +Additions +--------- + +* Grant license in this code base under BSD 3-clause license terms. + +Changes +------- + +* Update parser to SemVer standard 2.0.0. + +* Ignore build component for comparison. + + +---- + +Version 0.0.2 +============= + +:Released: 2012-05-10 +:Maintainer: Konstantine Rybnikov + +Changes +------- + +* Use standard library Distutils for distribution management. + + +---- + +Version 0.0.1 +============= + +:Released: 2012-04-28 +:Maintainer: Konstantine Rybnikov + +* Initial release. + + +.. + Local variables: + coding: utf-8 + mode: text + mode: rst + End: + vim: fileencoding=utf-8 filetype=rst : diff --git a/docs/development.rst b/docs/development.rst index 3f5e9b6d..049fe1a3 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,3 +1,5 @@ +.. _contributing: + Contributing to semver ====================== @@ -16,8 +18,8 @@ First, take the time to look into our GitHub `issues`_ tracker if this already covered. If not, changes are good that we avoid double work. -Fixing Bugs and Implementing New Features ------------------------------------------ +Prerequisites +------------- Before you make changes to the code, we would highly appreciate if you consider the following general requirements: @@ -27,12 +29,6 @@ consider the following general requirements: * Check if your feature is covered by the Semantic Versioning specification. If not, ask on its GitHub project https://github.com/semver/semver. -* Write test cases if you implement a new feature. - -* Test also for side effects of your new feature and run the complete - test suite. - -* Document the new feature, see :ref:`doc` for details. Modifying the Code @@ -59,20 +55,23 @@ We recommend the following workflow: $ git checkout -b feature/NAME_OF_YOUR_FEATURE -#. Work on your branch. Commit your work. +#. Work on your branch and create a pull request: -#. Write test cases and run the test suite, see :ref:`testsuite` for details. + a. Write test cases and run the complete test suite, see :ref:`testsuite` + for details. -#. Create a `pull request`_. Describe in the pull request what you did - and why. If you have open questions, ask. + b. Write a changelog entry, see section :ref:`changelog`. -#. Wait for feedback. If you receive any comments, address these. + c. If you have implemented a new feature, document it into our + documentation to help our reader. See section :ref:`doc` for + further details. -#. After your pull request got accepted, delete your branch. + d. Create a `pull request`_. Describe in the pull request what you did + and why. If you have open questions, ask. -#. Use the ``clean`` command to remove build and test files and folders:: +#. Wait for feedback. If you receive any comments, address these. - $ python setup.py clean +#. After your pull request got accepted, delete your branch. .. _testsuite: @@ -223,6 +222,15 @@ documentation includes: edge cases. +.. _changelog: + +Adding a Changelog Entry +------------------------ + +.. include:: ../changelog.d/README.rst + :start-after: -text-begin- + + .. _flake8: https://flake8.readthedocs.io .. _issues: https://github.com/python-semver/python-semver/issues .. _pull request: https://github.com/python-semver/python-semver/pulls @@ -230,3 +238,4 @@ documentation includes: .. _Semantic Versioning: https://semver.org .. _Sphinx style: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html .. _tox: https://tox.readthedocs.org/ + diff --git a/docs/index.rst b/docs/index.rst index c9ba7626..0654dabd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,16 +5,25 @@ Semver |version| -- Semantic Versioning .. toctree:: :maxdepth: 2 :caption: Contents - :numbered: readme install usage - pysemver development api - changelog +.. toctree:: + :maxdepth: 2 + :caption: CLI + + pysemver + + +.. toctree:: + :maxdepth: 1 + + changelog + changelog-2.7.9-and-before Indices and Tables ================== diff --git a/pyproject.toml b/pyproject.toml index 7c4419c6..b3ee70a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,12 @@ +[build-system] +requires = [ + # sync with setup.py until we discard non-pep-517/518 + "setuptools>=40.0", + "setuptools-scm", + "wheel", +] +build-backend = "setuptools.build_meta" + [tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38'] @@ -18,3 +27,53 @@ exclude = ''' )/ ) ''' + +[tool.towncrier] +package = "semver" +# package_dir = "src" +filename = "CHANGELOG.rst" +directory = "changelog.d/" +title_format = "Version {version}" +template = "changelog.d/_template.rst" +# issue_format = "`#{issue} `_" +# issue_format = ":gh:`{issue}`" + + # [[tool.towncrier.type]] + # directory = "breaking" + # name = "Breaking Changes" + # showcontent = true + + [[tool.towncrier.type]] + directory = "deprecation" + name = "Deprecations" + showcontent = true + + [[tool.towncrier.type]] + directory = "feature" + name = "Features" + showcontent = true + + # [[tool.towncrier.type]] + # directory = "improvement" + # name = "Improvements" + # showcontent = true + + [[tool.towncrier.type]] + directory = "bugfix" + name = "Bug Fixes" + showcontent = true + + [[tool.towncrier.type]] + directory = "doc" + name = "Improved Documentation" + showcontent = true + + [[tool.towncrier.type]] + directory = "trivial" + name = "Trivial/Internal Changes" + showcontent = true + + [[tool.towncrier.type]] + directory = "removal" + name = "Removals" + showcontent = true diff --git a/tox.ini b/tox.ini index cdbf6208..d253f1f6 100644 --- a/tox.ini +++ b/tox.ini @@ -59,6 +59,7 @@ commands = {[testenv:mypy]commands} {[testenv:docstrings]commands} + [testenv:docs] description = Build HTML documentation basepython = python3 @@ -66,6 +67,7 @@ deps = -r{toxinidir}/docs/requirements.txt skip_install = true commands = make -C docs html + [testenv:man] description = Build the manpage basepython = python3 @@ -83,3 +85,12 @@ deps = commands = python3 setup.py sdist bdist_wheel twine check dist/* + + +[testenv:changelog] +description = Run towncrier to check, build, or create the CHANGELOG.rst +basepython = python3 +deps = + git+https://github.com/twisted/towncrier.git +commands = + towncrier {posargs:build --draft} From ae8a961c6a85711066ef6fc8a32e66b6fb0dca5b Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 26 Oct 2020 13:48:58 +0100 Subject: [PATCH 05/86] Doc: Disable sphinx-argparse --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index ee76828b..dd2f77ca 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ # requirements file for documentation sphinx -sphinx-argparse +# sphinx-argparse sphinx-autodoc-typehints From 8a91525ab08d518ff5af4739953c53df96e62a04 Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Mon, 26 Oct 2020 10:13:25 -0400 Subject: [PATCH 06/86] Remove sphinx from requirements.txt --- docs/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index dd2f77ca..1186b4c3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ # requirements file for documentation -sphinx -# sphinx-argparse +# sphinx +sphinx-argparse sphinx-autodoc-typehints From 1dab57f7c7b8567f512a5dae72da5a8c2e36a77f Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 26 Oct 2020 16:42:23 +0100 Subject: [PATCH 07/86] Create 3.0.0-dev.1 Release * Update CHANGELOG * Make the Quickstart the main page of semver doc. It's a bit friendlier than having a sterile table of contents. --- CHANGELOG.rst | 88 +++++++++++++++++++++++++++++++ README.rst | 5 ++ changelog.d/234.deprecation.rst | 2 - changelog.d/270.feature.rst | 13 ----- changelog.d/276.feature.rst | 1 - changelog.d/291.bugfix.rst | 2 - changelog.d/pr290.deprecation.rst | 10 ---- changelog.d/pr290.doc.rst | 7 --- changelog.d/pr290.feature.rst | 11 ---- changelog.d/pr290.trivial.rst | 1 - docs/index.rst | 7 ++- 11 files changed, 99 insertions(+), 48 deletions(-) delete mode 100644 changelog.d/234.deprecation.rst delete mode 100644 changelog.d/270.feature.rst delete mode 100644 changelog.d/276.feature.rst delete mode 100644 changelog.d/291.bugfix.rst delete mode 100644 changelog.d/pr290.deprecation.rst delete mode 100644 changelog.d/pr290.doc.rst delete mode 100644 changelog.d/pr290.feature.rst delete mode 100644 changelog.d/pr290.trivial.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0455560c..4e529404 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,94 @@ in our repository. .. towncrier release notes start +Version 3.0.0-dev.1 +=================== + +:Released: 2020-10-26 +:Maintainer: Tom Schraitle + + +Deprecations +------------ + +* :pr:`290`: For semver 3.0.0-alpha0: + + * Remove anything related to Python2 + * In :file:`tox.ini` and :file:`.travis.yml` + Remove targets py27, py34, py35, and pypy. + Add py38, py39, and nightly (allow to fail) + * In :file:`setup.py` simplified file and remove + ``Tox`` and ``Clean`` classes + * Remove old Python versions (2.7, 3.4, 3.5, and pypy) + from Travis + +* :gh:`234`: In :file:`setup.py` simplified file and remove + ``Tox`` and ``Clean`` classes + + + +Features +-------- + +* :pr:`290`: Create semver 3.0.0-alpha0 + + * Update :file:`README.rst`, mention maintenance + branch ``maint/v2``. + * Remove old code mainly used for Python2 compatibility, + adjusted code to support Python3 features. + * Split test suite into separate files under :file:`tests/` + directory + * Adjust and update :file:`setup.py`. Requires Python >=3.6.* + Extract metadata directly from source (affects all the ``__version__``, + ``__author__`` etc. variables) + +* :gh:`270`: Configure Towncrier (:pr:`273`:) + + * Add :file:`changelog.d/.gitignore` to keep this directory + * Create :file:`changelog.d/README.rst` with some descriptions + * Add :file:`changelog.d/_template.rst` as Towncrier template + * Add ``[tool.towncrier]`` section in :file:`pyproject.toml` + * Add "changelog" target into :file:`tox.ini`. Use it like + :command:`tox -e changelog -- CMD` whereas ``CMD`` is a + Towncrier command. The default :command:`tox -e changelog` + calls Towncrier to create a draft of the changelog file + and output it to stdout. + * Update documentation and add include a new section + "Changelog" included from :file:`changelog.d/README.rst`. + +* :gh:`276`: Document how to create a sublass from :class:`VersionInfo` class + + + +Bug Fixes +--------- + +* :gh:`291`: Disallow negative numbers in VersionInfo arguments + for ``major``, ``minor``, and ``patch``. + + + +Improved Documentation +---------------------- + +* :pr:`290`: Several improvements in the documentation: + + * New layout to distinguish from the semver2 development line. + * Create new logo. + * Remove any occurances of Python2. + * Describe changelog process with Towncrier. + * Update the release process. + + + +Trivial/Internal Changes +------------------------ + +* :pr:`290`: Add supported Python versions to :command:`black`. + + + +---- Version 2.13.0 diff --git a/README.rst b/README.rst index fa001046..2baef45c 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,8 @@ +.. warning:: + + This is a development version. Do **NOT** use it + in production before the final 3.0.0 is released. + Quickstart ========== diff --git a/changelog.d/234.deprecation.rst b/changelog.d/234.deprecation.rst deleted file mode 100644 index bf0c0301..00000000 --- a/changelog.d/234.deprecation.rst +++ /dev/null @@ -1,2 +0,0 @@ -In :file:`setup.py` simplified file and remove -``Tox`` and ``Clean`` classes diff --git a/changelog.d/270.feature.rst b/changelog.d/270.feature.rst deleted file mode 100644 index 79a6ee4a..00000000 --- a/changelog.d/270.feature.rst +++ /dev/null @@ -1,13 +0,0 @@ -Configure Towncrier (:pr:`273`:) - -* Add :file:`changelog.d/.gitignore` to keep this directory -* Create :file:`changelog.d/README.rst` with some descriptions -* Add :file:`changelog.d/_template.rst` as Towncrier template -* Add ``[tool.towncrier]`` section in :file:`pyproject.toml` -* Add "changelog" target into :file:`tox.ini`. Use it like - :command:`tox -e changelog -- CMD` whereas ``CMD`` is a - Towncrier command. The default :command:`tox -e changelog` - calls Towncrier to create a draft of the changelog file - and output it to stdout. -* Update documentation and add include a new section - "Changelog" included from :file:`changelog.d/README.rst`. diff --git a/changelog.d/276.feature.rst b/changelog.d/276.feature.rst deleted file mode 100644 index 9fc4680b..00000000 --- a/changelog.d/276.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Document how to create a sublass from :class:`VersionInfo` class diff --git a/changelog.d/291.bugfix.rst b/changelog.d/291.bugfix.rst deleted file mode 100644 index 74ee4d87..00000000 --- a/changelog.d/291.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Disallow negative numbers in VersionInfo arguments -for ``major``, ``minor``, and ``patch``. \ No newline at end of file diff --git a/changelog.d/pr290.deprecation.rst b/changelog.d/pr290.deprecation.rst deleted file mode 100644 index 1067d5f2..00000000 --- a/changelog.d/pr290.deprecation.rst +++ /dev/null @@ -1,10 +0,0 @@ -For semver 3.0.0-alpha0: - -* Remove anything related to Python2 -* In :file:`tox.ini` and :file:`.travis.yml` - Remove targets py27, py34, py35, and pypy. - Add py38, py39, and nightly (allow to fail) -* In :file:`setup.py` simplified file and remove - ``Tox`` and ``Clean`` classes -* Remove old Python versions (2.7, 3.4, 3.5, and pypy) - from Travis \ No newline at end of file diff --git a/changelog.d/pr290.doc.rst b/changelog.d/pr290.doc.rst deleted file mode 100644 index fa420ac9..00000000 --- a/changelog.d/pr290.doc.rst +++ /dev/null @@ -1,7 +0,0 @@ -Several improvements in the documentation: - -* New layout to distinguish from the semver2 development line. -* Create new logo. -* Remove any occurances of Python2. -* Describe changelog process with Towncrier. -* Update the release process. diff --git a/changelog.d/pr290.feature.rst b/changelog.d/pr290.feature.rst deleted file mode 100644 index f2a937a8..00000000 --- a/changelog.d/pr290.feature.rst +++ /dev/null @@ -1,11 +0,0 @@ -Create semver 3.0.0-alpha0 - -* Update :file:`README.rst`, mention maintenance - branch ``maint/v2``. -* Remove old code mainly used for Python2 compatibility, - adjusted code to support Python3 features. -* Split test suite into separate files under :file:`tests/` - directory -* Adjust and update :file:`setup.py`. Requires Python >=3.6.* - Extract metadata directly from source (affects all the ``__version__``, - ``__author__`` etc. variables) \ No newline at end of file diff --git a/changelog.d/pr290.trivial.rst b/changelog.d/pr290.trivial.rst deleted file mode 100644 index 9dc914f3..00000000 --- a/changelog.d/pr290.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Add supported Python versions to :command:`black`. diff --git a/docs/index.rst b/docs/index.rst index 0654dabd..aefdc843 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,12 +1,14 @@ Semver |version| -- Semantic Versioning ======================================= +.. include:: readme.rst + .. toctree:: :maxdepth: 2 :caption: Contents + :hidden: - readme install usage development @@ -15,12 +17,15 @@ Semver |version| -- Semantic Versioning .. toctree:: :maxdepth: 2 :caption: CLI + :hidden: pysemver .. toctree:: :maxdepth: 1 + :caption: Changelogs + :hidden: changelog changelog-2.7.9-and-before From b0f854da3424ed73231e4f55bac36c86b2c82987 Mon Sep 17 00:00:00 2001 From: Thomas Laferriere Date: Thu, 22 Oct 2020 22:43:08 -0400 Subject: [PATCH 08/86] Create semver package * Bump the dev part to "dev.2" * Add stubby setup.py file for compatibility with python 3.6 * Deprecate cli functions imported from root * Revert to `pysemver` as console script. * Refactor __main__.py * add type hints for correctness * Rename VersionInfo to Version to close #305 * Refactor and integrate suggestion from @tomschr * Create :file:`src/semver/cli.py` for all CLI methods * Create :file:`src/semver/_deprecated.py` for the ``deprecated`` decorator and other deprecated functions * Create :file:`src/semver/__main__.py` to allow calling the CLI using :command:`python -m semver` * Create :file:`src/semver/_types.py` to hold type aliases * Create :file:`src/semver/version.py` to hold the :class:`VersionInfo` class and its utility functions * Create :file:`src/semver/__about__.py` for all the metadata variables * Adapt infrastructure code to the new project layout. * Replace :file:`setup.py` with :file:`setup.cfg` because the :file:`setup.cfg` is easier to use * Adapt documentation code snippets where needed * Adapt tests * Changed the ``deprecated`` to hardcode the ``semver`` package name in the warning. * Change path for docformatter and run it. * Remove pyi inclusion from black sine we aren't using them * Split up changelog to make more sense. * Add documentation for Version rename * Increase coverage to 100% in non-deprecated parts. * Update changelog.d/169.feature.rst Co-authored-by: Thomas Laferriere Co-authored-by: Tom Schraitle --- README.rst | 20 +- changelog.d/169.deprecation.rst | 1 + changelog.d/169.feature.rst | 10 + changelog.d/169.trivial.rst | 8 + changelog.d/305.doc.rst | 1 + changelog.d/305.feature.rst | 1 + docs/coerce.py | 8 +- docs/conf.py | 2 +- docs/semverwithvprefix.py | 8 +- docs/usage.rst | 228 +++--- pyproject.toml | 1 - semver.py | 1226 ---------------------------- setup.cfg | 59 +- setup.py | 75 +- src/semver/__about__.py | 8 + src/semver/__init__.py | 33 + src/semver/__main__.py | 23 + src/semver/_deprecated.py | 378 +++++++++ src/semver/_types.py | 8 + src/semver/cli.py | 162 ++++ src/semver/version.py | 655 +++++++++++++++ tests/conftest.py | 4 +- tests/test_compare.py | 43 +- tests/test_deprecated_functions.py | 38 +- tests/test_format.py | 28 +- tests/test_index.py | 10 +- tests/test_parsing.py | 12 +- tests/test_pysemver-cli.py | 19 +- tests/test_replace.py | 6 +- tests/test_semver.py | 29 +- tests/test_subclass.py | 4 +- tests/test_typeerror-274.py | 7 +- tox.ini | 5 +- 33 files changed, 1611 insertions(+), 1509 deletions(-) create mode 100644 changelog.d/169.deprecation.rst create mode 100644 changelog.d/169.feature.rst create mode 100644 changelog.d/169.trivial.rst create mode 100644 changelog.d/305.doc.rst create mode 100644 changelog.d/305.feature.rst delete mode 100644 semver.py mode change 100755 => 100644 setup.py create mode 100644 src/semver/__about__.py create mode 100644 src/semver/__init__.py create mode 100644 src/semver/__main__.py create mode 100644 src/semver/_deprecated.py create mode 100644 src/semver/_types.py create mode 100644 src/semver/cli.py create mode 100644 src/semver/version.py diff --git a/README.rst b/README.rst index 2baef45c..03faebab 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,12 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. |MAINT| replace:: ``maint/v2`` .. _MAINT: https://github.com/python-semver/python-semver/tree/maint/v2 +.. note:: + + The :class:`VersionInfo` has been renamed to :class:`Version`. An + alias has been created to preserve compatibility but the use of the old + name has been deprecated. + The module follows the ``MAJOR.MINOR.PATCH`` style: * ``MAJOR`` version when you make incompatible API changes, @@ -45,11 +51,11 @@ To import this library, use: >>> import semver Working with the library is quite straightforward. To turn a version string into the -different parts, use the ``semver.VersionInfo.parse`` function: +different parts, use the ``semver.Version.parse`` function: .. code-block:: python - >>> ver = semver.VersionInfo.parse('1.2.3-pre.2+build.4') + >>> ver = semver.Version.parse('1.2.3-pre.2+build.4') >>> ver.major 1 >>> ver.minor @@ -62,21 +68,21 @@ different parts, use the ``semver.VersionInfo.parse`` function: 'build.4' To raise parts of a version, there are a couple of functions available for -you. The function ``semver.VersionInfo.bump_major`` leaves the original object untouched, but -returns a new ``semver.VersionInfo`` instance with the raised major part: +you. The function ``semver.Version.bump_major`` leaves the original object untouched, but +returns a new ``semver.Version`` instance with the raised major part: .. code-block:: python - >>> ver = semver.VersionInfo.parse("3.4.5") + >>> ver = semver.Version.parse("3.4.5") >>> ver.bump_major() - VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) + Version(major=4, minor=0, patch=0, prerelease=None, build=None) It is allowed to concatenate different "bump functions": .. code-block:: python >>> ver.bump_major().bump_minor() - VersionInfo(major=4, minor=1, patch=0, prerelease=None, build=None) + Version(major=4, minor=1, patch=0, prerelease=None, build=None) To compare two versions, semver provides the ``semver.compare`` function. The return value indicates the relationship between the first and second diff --git a/changelog.d/169.deprecation.rst b/changelog.d/169.deprecation.rst new file mode 100644 index 00000000..9ce5ef6b --- /dev/null +++ b/changelog.d/169.deprecation.rst @@ -0,0 +1 @@ +Deprecate CLI functions not imported from ``semver.cli``. \ No newline at end of file diff --git a/changelog.d/169.feature.rst b/changelog.d/169.feature.rst new file mode 100644 index 00000000..1b762676 --- /dev/null +++ b/changelog.d/169.feature.rst @@ -0,0 +1,10 @@ +Create semver package and split code among different modules in the packages. + +* Remove :file:`semver.py` +* Create :file:`src/semver/__init__.py` +* Create :file:`src/semver/cli.py` for all CLI methods +* Create :file:`src/semver/_deprecated.py` for the ``deprecated`` decorator and other deprecated functions +* Create :file:`src/semver/__main__.py` to allow calling the CLI using :command:`python -m semver` +* Create :file:`src/semver/_types.py` to hold type aliases +* Create :file:`src/semver/version.py` to hold the :class:`Version` class (old name :class:`VersionInfo`) and its utility functions +* Create :file:`src/semver/__about__.py` for all the metadata variables diff --git a/changelog.d/169.trivial.rst b/changelog.d/169.trivial.rst new file mode 100644 index 00000000..536e2b88 --- /dev/null +++ b/changelog.d/169.trivial.rst @@ -0,0 +1,8 @@ +Adapted infrastructure code to the new project layout. + +* Replace :file:`setup.py` with :file:`setup.cfg` because the :file:`setup.cfg` is easier to use +* Adapt documentation code snippets where needed +* Adapt tests +* Changed the ``deprecated`` to hardcode the ``semver`` package name in the warning. + +Increase coverage to 100% for all non-deprecated APIs \ No newline at end of file diff --git a/changelog.d/305.doc.rst b/changelog.d/305.doc.rst new file mode 100644 index 00000000..1ce69247 --- /dev/null +++ b/changelog.d/305.doc.rst @@ -0,0 +1 @@ +Add note about :class:`Version` rename. \ No newline at end of file diff --git a/changelog.d/305.feature.rst b/changelog.d/305.feature.rst new file mode 100644 index 00000000..98ef9665 --- /dev/null +++ b/changelog.d/305.feature.rst @@ -0,0 +1 @@ +Rename :class:`VersionInfo` to :class:`Version` but keep an alias for compatibility \ No newline at end of file diff --git a/docs/coerce.py b/docs/coerce.py index 3e5eb21b..9fe87276 100644 --- a/docs/coerce.py +++ b/docs/coerce.py @@ -17,7 +17,7 @@ def coerce(version): """ - Convert an incomplete version string into a semver-compatible VersionInfo + Convert an incomplete version string into a semver-compatible Version object * Tries to detect a "basic" version string (``major.minor.patch``). @@ -25,10 +25,10 @@ def coerce(version): set to zero to obtain a valid semver version. :param str version: the version string to convert - :return: a tuple with a :class:`VersionInfo` instance (or ``None`` + :return: a tuple with a :class:`Version` instance (or ``None`` if it's not a version) and the rest of the string which doesn't belong to a basic version. - :rtype: tuple(:class:`VersionInfo` | None, str) + :rtype: tuple(:class:`Version` | None, str) """ match = BASEVERSION.search(version) if not match: @@ -37,6 +37,6 @@ def coerce(version): ver = { key: 0 if value is None else value for key, value in match.groupdict().items() } - ver = semver.VersionInfo(**ver) + ver = semver.Version(**ver) rest = match.string[match.end() :] # noqa:E203 return ver, rest diff --git a/docs/conf.py b/docs/conf.py index 3653ecce..71738daa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ import os import sys -sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, os.path.abspath("../src/")) from semver import __version__ # noqa: E402 diff --git a/docs/semverwithvprefix.py b/docs/semverwithvprefix.py index 13298d5f..304ce772 100644 --- a/docs/semverwithvprefix.py +++ b/docs/semverwithvprefix.py @@ -1,15 +1,15 @@ -from semver import VersionInfo +from semver import Version -class SemVerWithVPrefix(VersionInfo): +class SemVerWithVPrefix(Version): """ - A subclass of VersionInfo which allows a "v" prefix + A subclass of Version which allows a "v" prefix """ @classmethod def parse(cls, version): """ - Parse version string to a VersionInfo instance. + Parse version string to a Version instance. :param version: version string with "v" or "V" prefix :type version: str diff --git a/docs/usage.rst b/docs/usage.rst index 4e2b6f92..31363bcf 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,7 +1,7 @@ Using semver ============ -The ``semver`` module can store a version in the :class:`semver.VersionInfo` class. +The ``semver`` module can store a version in the :class:`semver.Version` class. For historical reasons, a version can be also stored as a string or dictionary. Each type can be converted into the other, if the minimum requirements @@ -26,7 +26,7 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.0-dev.1' + '3.0.0-dev.2' Creating a Version @@ -35,7 +35,7 @@ Creating a Version Due to historical reasons, the semver project offers two ways of creating a version: -* through an object oriented approach with the :class:`semver.VersionInfo` +* through an object oriented approach with the :class:`semver.Version` class. This is the preferred method when using semver. * through module level functions and builtin datatypes (usually string @@ -52,30 +52,46 @@ creating a version: :ref:`sec_display_deprecation_warnings`. -A :class:`semver.VersionInfo` instance can be created in different ways: +A :class:`semver.Version` instance can be created in different ways: * From a string (a Unicode string in Python 2):: - >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - >>> semver.VersionInfo.parse(u"5.3.1") - VersionInfo(major=5, minor=3, patch=1, prerelease=None, build=None) + >>> semver.Version.parse("3.4.5-pre.2+build.4") + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + >>> semver.Version.parse(u"5.3.1") + Version(major=5, minor=3, patch=1, prerelease=None, build=None) * From a byte string:: - >>> semver.VersionInfo.parse(b"2.3.4") - VersionInfo(major=2, minor=3, patch=4, prerelease=None, build=None) + >>> semver.Version.parse(b"2.3.4") + Version(major=2, minor=3, patch=4, prerelease=None, build=None) * From individual parts by a dictionary:: >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - >>> semver.VersionInfo(**d) - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + >>> semver.Version(**d) + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') Keep in mind, the ``major``, ``minor``, ``patch`` parts has to + be positive. + + >>> semver.Version(-1) + Traceback (most recent call last): + ... + ValueError: 'major' is negative. A version can only be positive. + + As a minimum requirement, your dictionary needs at least the + be positive. + + >>> semver.Version(-1) + Traceback (most recent call last): + ... + ValueError: 'major' is negative. A version can only be positive. + + As a minimum requirement, your dictionary needs at least the be positive. - >>> semver.VersionInfo(-1) + >>> semver.Version(-1) Traceback (most recent call last): ... ValueError: 'major' is negative. A version can only be positive. @@ -89,20 +105,20 @@ A :class:`semver.VersionInfo` instance can be created in different ways: * From a tuple:: >>> t = (3, 5, 6) - >>> semver.VersionInfo(*t) - VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + >>> semver.Version(*t) + Version(major=3, minor=5, patch=6, prerelease=None, build=None) You can pass either an integer or a string for ``major``, ``minor``, or ``patch``:: - >>> semver.VersionInfo("3", "5", 6) - VersionInfo(major=3, minor=5, patch=6, prerelease=None, build=None) + >>> semver.Version("3", "5", 6) + Version(major=3, minor=5, patch=6, prerelease=None, build=None) The old, deprecated module level functions are still available. If you need them, they return different builtin objects (string and dictionary). Keep in mind, once you have converted a version into a string or dictionary, it's an ordinary builtin object. It's not a special version object like -the :class:`semver.VersionInfo` class anymore. +the :class:`semver.Version` class anymore. Depending on your use case, the following methods are available: @@ -137,13 +153,13 @@ Parsing a Version String * With :func:`semver.parse_version_info`:: >>> semver.parse_version_info("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') -* With :func:`semver.VersionInfo.parse` (basically the same as +* With :func:`semver.Version.parse` (basically the same as :func:`semver.parse_version_info`):: - >>> semver.VersionInfo.parse("3.4.5-pre.2+build.4") - VersionInfo(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + >>> semver.Version.parse("3.4.5-pre.2+build.4") + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') * With :func:`semver.parse`:: @@ -155,13 +171,13 @@ Checking for a Valid Semver Version ----------------------------------- If you need to check a string if it is a valid semver version, use the -classmethod :func:`semver.VersionInfo.isvalid`: +classmethod :func:`semver.Version.isvalid`: .. code-block:: python - >>> semver.VersionInfo.isvalid("1.0.0") + >>> semver.Version.isvalid("1.0.0") True - >>> semver.VersionInfo.isvalid("invalid") + >>> semver.Version.isvalid("invalid") False @@ -170,12 +186,12 @@ classmethod :func:`semver.VersionInfo.isvalid`: Accessing Parts of a Version Through Names ------------------------------------------ -The :class:`semver.VersionInfo` contains attributes to access the different +The :class:`semver.Version` contains attributes to access the different parts of a version: .. code-block:: python - >>> v = semver.VersionInfo.parse("3.4.5-pre.2+build.4") + >>> v = semver.Version.parse("3.4.5-pre.2+build.4") >>> v.major 3 >>> v.minor @@ -197,16 +213,16 @@ If you do, you get an ``AttributeError``:: If you need to replace different parts of a version, refer to section :ref:`sec.replace.parts`. -In case you need the different parts of a version stepwise, iterate over the :class:`semver.VersionInfo` instance:: +In case you need the different parts of a version stepwise, iterate over the :class:`semver.Version` instance:: - >>> for item in semver.VersionInfo.parse("3.4.5-pre.2+build.4"): + >>> for item in semver.Version.parse("3.4.5-pre.2+build.4"): ... print(item) 3 4 5 pre.2 build.4 - >>> list(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) + >>> list(semver.Version.parse("3.4.5-pre.2+build.4")) [3, 4, 5, 'pre.2', 'build.4'] @@ -218,15 +234,15 @@ Accessing Parts Through Index Numbers .. versionadded:: 2.10.0 Another way to access parts of a version is to use an index notation. The underlying -:class:`VersionInfo ` object allows to access its data through -the magic method :func:`__getitem__ `. +:class:`Version ` object allows to access its data through +the magic method :func:`__getitem__ `. For example, the ``major`` part can be accessed by index number 0 (zero). Likewise the other parts: .. code-block:: python - >>> ver = semver.VersionInfo.parse("10.3.2-pre.5+build.10") + >>> ver = semver.Version.parse("10.3.2-pre.5+build.10") >>> ver[0], ver[1], ver[2], ver[3], ver[4] (10, 3, 2, 'pre.5', 'build.10') @@ -249,7 +265,7 @@ Negative numbers or undefined parts raise an :class:`IndexError` exception: .. code-block:: python - >>> ver = semver.VersionInfo.parse("10.3.2") + >>> ver = semver.Version.parse("10.3.2") >>> ver[3] Traceback (most recent call last): ... @@ -265,13 +281,13 @@ Replacing Parts of a Version ---------------------------- If you want to replace different parts of a version, but leave other parts -unmodified, use the function :func:`semver.VersionInfo.replace` or :func:`semver.replace`: +unmodified, use the function :func:`semver.Version.replace` or :func:`semver.replace`: -* From a :class:`semver.VersionInfo` instance:: +* From a :class:`semver.Version` instance:: - >>> version = semver.VersionInfo.parse("1.4.5-pre.1+build.6") + >>> version = semver.Version.parse("1.4.5-pre.1+build.6") >>> version.replace(major=2, minor=2) - VersionInfo(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') + Version(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') * From a version string:: @@ -284,7 +300,7 @@ If you pass invalid keys you get an exception:: Traceback (most recent call last): ... TypeError: replace() got 1 unexpected keyword argument(s): invalidkey - >>> version = semver.VersionInfo.parse("1.4.5-pre.1+build.6") + >>> version = semver.Version.parse("1.4.5-pre.1+build.6") >>> version.replace(invalidkey=2) Traceback (most recent call last): ... @@ -293,28 +309,28 @@ If you pass invalid keys you get an exception:: .. _sec.convert.versions: -Converting a VersionInfo instance into Different Types +Converting a Version instance into Different Types ------------------------------------------------------ -Sometimes it is needed to convert a :class:`semver.VersionInfo` instance into +Sometimes it is needed to convert a :class:`semver.Version` instance into a different type. For example, for displaying or to access all parts. -It is possible to convert a :class:`semver.VersionInfo` instance: +It is possible to convert a :class:`semver.Version` instance: * Into a string with the builtin function :func:`str`:: - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4")) '3.4.5-pre.2+build.4' -* Into a dictionary with :func:`semver.VersionInfo.to_dict`:: +* Into a dictionary with :func:`semver.Version.to_dict`:: - >>> v = semver.VersionInfo(major=3, minor=4, patch=5) + >>> v = semver.Version(major=3, minor=4, patch=5) >>> v.to_dict() OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) -* Into a tuple with :func:`semver.VersionInfo.to_tuple`:: +* Into a tuple with :func:`semver.Version.to_tuple`:: - >>> v = semver.VersionInfo(major=5, minor=4, patch=2) + >>> v = semver.Version(major=5, minor=4, patch=2) >>> v.to_tuple() (5, 4, 2, None, None) @@ -325,27 +341,27 @@ Raising Parts of a Version The ``semver`` module contains the following functions to raise parts of a version: -* :func:`semver.VersionInfo.bump_major`: raises the major part and set all other parts to +* :func:`semver.Version.bump_major`: raises the major part and set all other parts to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_minor`: raises the minor part and sets ``patch`` to zero. +* :func:`semver.Version.bump_minor`: raises the minor part and sets ``patch`` to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_patch`: raises the patch part. Set ``prerelease`` and +* :func:`semver.Version.bump_patch`: raises the patch part. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_prerelease`: raises the prerelease part and set +* :func:`semver.Version.bump_prerelease`: raises the prerelease part and set ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_build`: raises the build part. +* :func:`semver.Version.bump_build`: raises the build part. .. code-block:: python - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_major()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_major()) '4.0.0' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_minor()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_minor()) '3.5.0' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_patch()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_patch()) '3.4.6' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_prerelease()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) '3.4.5-pre.3' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").bump_build()) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_build()) '3.4.5-pre.2+build.5' Likewise the module level functions :func:`semver.bump_major`. @@ -355,23 +371,23 @@ Increasing Parts of a Version Taking into Account Prereleases ------------------------------------------------------------- .. versionadded:: 2.10.0 - Added :func:`semver.VersionInfo.next_version`. + Added :func:`semver.Version.next_version`. If you want to raise your version and take prereleases into account, -the function :func:`semver.VersionInfo.next_version` would perhaps a +the function :func:`semver.Version.next_version` would perhaps a better fit. .. code-block:: python - >>> v = semver.VersionInfo.parse("3.4.5-pre.2+build.4") + >>> v = semver.Version.parse("3.4.5-pre.2+build.4") >>> str(v.next_version(part="prerelease")) '3.4.5-pre.3' - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").next_version(part="patch")) + >>> str(semver.Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) '3.4.5' - >>> str(semver.VersionInfo.parse("3.4.5+build.4").next_version(part="patch")) + >>> str(semver.Version.parse("3.4.5+build.4").next_version(part="patch")) '3.4.5' - >>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease")) + >>> str(semver.Version.parse("0.1.4").next_version("prerelease")) '0.1.5-rc.1' @@ -394,23 +410,23 @@ To compare two versions depends on your type: The return value is negative if ``version1 < version2``, zero if ``version1 == version2`` and strictly positive if ``version1 > version2``. -* **Two** :class:`semver.VersionInfo` **instances** +* **Two** :class:`semver.Version` **instances** Use the specific operator. Currently, the operators ``<``, ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: - >>> v1 = semver.VersionInfo.parse("3.4.5") - >>> v2 = semver.VersionInfo.parse("3.5.1") + >>> v1 = semver.Version.parse("3.4.5") + >>> v2 = semver.Version.parse("3.5.1") >>> v1 < v2 True >>> v1 > v2 False -* **A** :class:`semver.VersionInfo` **type and a** :func:`tuple` **or** :func:`list` +* **A** :class:`semver.Version` **type and a** :func:`tuple` **or** :func:`list` - Use the operator as with two :class:`semver.VersionInfo` types:: + Use the operator as with two :class:`semver.Version` types:: - >>> v = semver.VersionInfo.parse("3.4.5") + >>> v = semver.Version.parse("3.4.5") >>> v > (1, 0) True >>> v < [3, 5] @@ -423,7 +439,7 @@ To compare two versions depends on your type: >>> [3, 5] > v True -* **A** :class:`semver.VersionInfo` **type and a** :func:`str` +* **A** :class:`semver.Version` **type and a** :func:`str` You can use also raw strings to compare:: @@ -446,7 +462,7 @@ To compare two versions depends on your type: ... ValueError: 1.0 is not valid SemVer string -* **A** :class:`semver.VersionInfo` **type and a** :func:`dict` +* **A** :class:`semver.Version` **type and a** :func:`dict` You can also use a dictionary. In contrast to strings, you can have an "incomplete" version (as the other parts are set to zero):: @@ -483,16 +499,16 @@ Version equality means for semver, that major, minor, patch, and prerelease parts are equal in both versions you compare. The build part is ignored. For example:: - >>> v = semver.VersionInfo.parse("1.2.3-rc4+1e4664d") + >>> v = semver.Version.parse("1.2.3-rc4+1e4664d") >>> v == "1.2.3-rc4+dedbeef" True -This also applies when a :class:`semver.VersionInfo` is a member of a set, or a +This also applies when a :class:`semver.Version` is a member of a set, or a dictionary key:: >>> d = {} - >>> v1 = semver.VersionInfo.parse("1.2.3-rc4+1e4664d") - >>> v2 = semver.VersionInfo.parse("1.2.3-rc4+dedbeef") + >>> v1 = semver.Version.parse("1.2.3-rc4+1e4664d") + >>> v2 = semver.Version.parse("1.2.3-rc4+dedbeef") >>> d[v1] = 1 >>> d[v2] 1 @@ -538,34 +554,34 @@ Getting Minimum and Maximum of Multiple Versions The functions :func:`semver.max_ver` and :func:`semver.min_ver` are deprecated in favor of their builtin counterparts :func:`max` and :func:`min`. -Since :class:`semver.VersionInfo` implements :func:`__gt__()` and :func:`__lt__()`, it can be used with builtins requiring +Since :class:`semver.Version` implements :func:`__gt__()` and :func:`__lt__()`, it can be used with builtins requiring .. code-block:: python - >>> max([semver.VersionInfo(0, 1, 0), semver.VersionInfo(0, 2, 0), semver.VersionInfo(0, 1, 3)]) - VersionInfo(major=0, minor=2, patch=0, prerelease=None, build=None) - >>> min([semver.VersionInfo(0, 1, 0), semver.VersionInfo(0, 2, 0), semver.VersionInfo(0, 1, 3)]) - VersionInfo(major=0, minor=1, patch=0, prerelease=None, build=None) + >>> max([semver.Version(0, 1, 0), semver.Version(0, 2, 0), semver.Version(0, 1, 3)]) + Version(major=0, minor=2, patch=0, prerelease=None, build=None) + >>> min([semver.Version(0, 1, 0), semver.Version(0, 2, 0), semver.Version(0, 1, 3)]) + Version(major=0, minor=1, patch=0, prerelease=None, build=None) Incidentally, using :func:`map`, you can get the min or max version of any number of versions of the same type -(convertible to :class:`semver.VersionInfo`). +(convertible to :class:`semver.Version`). For example, here are the maximum and minimum versions of a list of version strings: .. code-block:: python - >>> str(max(map(semver.VersionInfo.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> str(max(map(semver.Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) '2.1.0' - >>> str(min(map(semver.VersionInfo.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> str(min(map(semver.Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) '0.4.99' And the same can be done with tuples: .. code-block:: python - >>> max(map(lambda v: semver.VersionInfo(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + >>> max(map(lambda v: semver.Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() (2, 1, 0, None, None) - >>> min(map(lambda v: semver.VersionInfo(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + >>> min(map(lambda v: semver.Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() (0, 4, 99, None, None) For dictionaries, it is very similar to finding the max version tuple: see :ref:`sec.convert.versions`. @@ -600,7 +616,7 @@ information and returns a tuple with two items: :language: python -The function returns a *tuple*, containing a :class:`VersionInfo` +The function returns a *tuple*, containing a :class:`Version` instance or None as the first element and the rest as the second element. The second element (the rest) can be used to make further adjustments. @@ -609,9 +625,9 @@ For example: .. code-block:: python >>> coerce("v1.2") - (VersionInfo(major=1, minor=2, patch=0, prerelease=None, build=None), '') + (Version(major=1, minor=2, patch=0, prerelease=None, build=None), '') >>> coerce("v2.5.2-bla") - (VersionInfo(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') + (Version(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') .. _sec_replace_deprecated_functions: @@ -622,7 +638,7 @@ Replacing Deprecated Functions .. versionchanged:: 2.10.0 The development team of semver has decided to deprecate certain functions on the module level. The preferred way of using semver is through the - :class:`semver.VersionInfo` class. + :class:`semver.Version` class. The deprecated functions can still be used in version 2.10.0 and above. In version 3 of semver, the deprecated functions will be removed. @@ -633,15 +649,15 @@ them with code which is compatible for future versions: * :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` - Replace them with the respective methods of the :class:`semver.VersionInfo` + Replace them with the respective methods of the :class:`semver.Version` class. For example, the function :func:`semver.bump_major` is replaced by - :func:`semver.VersionInfo.bump_major` and calling the ``str(versionobject)``: + :func:`semver.Version.bump_major` and calling the ``str(versionobject)``: .. code-block:: python >>> s1 = semver.bump_major("3.4.5") - >>> s2 = str(semver.VersionInfo.parse("3.4.5").bump_major()) + >>> s2 = str(semver.Version.parse("3.4.5").bump_major()) >>> s1 == s2 True @@ -649,12 +665,12 @@ them with code which is compatible for future versions: * :func:`semver.finalize_version` - Replace it with :func:`semver.VersionInfo.finalize_version`: + Replace it with :func:`semver.Version.finalize_version`: .. code-block:: python >>> s1 = semver.finalize_version('1.2.3-rc.5') - >>> s2 = str(semver.VersionInfo.parse('1.2.3-rc.5').finalize_version()) + >>> s2 = str(semver.Version.parse('1.2.3-rc.5').finalize_version()) >>> s1 == s2 True @@ -665,7 +681,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') - >>> s2 = str(semver.VersionInfo(5, 4, 3, 'pre.2', 'build.1')) + >>> s2 = str(semver.Version(5, 4, 3, 'pre.2', 'build.1')) >>> s1 == s2 True @@ -676,7 +692,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.max_ver("1.2.3", "1.2.4") - >>> s2 = str(max(map(semver.VersionInfo.parse, ("1.2.3", "1.2.4")))) + >>> s2 = str(max(map(semver.Version.parse, ("1.2.3", "1.2.4")))) >>> s1 == s2 True @@ -687,41 +703,41 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.min_ver("1.2.3", "1.2.4") - >>> s2 = str(min(map(semver.VersionInfo.parse, ("1.2.3", "1.2.4")))) + >>> s2 = str(min(map(semver.Version.parse, ("1.2.3", "1.2.4")))) >>> s1 == s2 True * :func:`semver.parse` - Replace it with :func:`semver.VersionInfo.parse` and - :func:`semver.VersionInfo.to_dict`: + Replace it with :func:`semver.Version.parse` and + :func:`semver.Version.to_dict`: .. code-block:: python >>> v1 = semver.parse("1.2.3") - >>> v2 = semver.VersionInfo.parse("1.2.3").to_dict() + >>> v2 = semver.Version.parse("1.2.3").to_dict() >>> v1 == v2 True * :func:`semver.parse_version_info` - Replace it with :func:`semver.VersionInfo.parse`: + Replace it with :func:`semver.Version.parse`: .. code-block:: python >>> v1 = semver.parse_version_info("3.4.5") - >>> v2 = semver.VersionInfo.parse("3.4.5") + >>> v2 = semver.Version.parse("3.4.5") >>> v1 == v2 True * :func:`semver.replace` - Replace it with :func:`semver.VersionInfo.replace`: + Replace it with :func:`semver.Version.replace`: .. code-block:: python >>> s1 = semver.replace("1.2.3", major=2, patch=10) - >>> s2 = str(semver.VersionInfo.parse('1.2.3').replace(major=2, patch=10)) + >>> s2 = str(semver.Version.parse('1.2.3').replace(major=2, patch=10)) >>> s1 == s2 True @@ -764,12 +780,12 @@ the following methods: .. _sec_creating_subclasses_from_versioninfo: -Creating Subclasses from VersionInfo +Creating Subclasses from Version ------------------------------------ If you do not like creating functions to modify the behavior of semver (as shown in section :ref:`sec_dealing_with_invalid_versions`), you can -also create a subclass of the :class:`VersionInfo` class. +also create a subclass of the :class:`Version` class. For example, if you want to output a "v" prefix before a version, but the other behavior is the same, use the following code: diff --git a/pyproject.toml b/pyproject.toml index b3ee70a3..1b406dac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38'] -include = '\.pyi?$' # diff = true exclude = ''' ( diff --git a/semver.py b/semver.py deleted file mode 100644 index 09bca561..00000000 --- a/semver.py +++ /dev/null @@ -1,1226 +0,0 @@ -"""Python helper for Semantic Versioning (http://semver.org)""" -from __future__ import print_function - -import argparse -import collections -import inspect -import re -import sys -import warnings -from functools import partial, wraps -from types import FrameType -from typing import ( - Any, - Callable, - Collection, - Dict, - Iterable, - Iterator, - List, - Optional, - SupportsInt, - Tuple, - TypeVar, - Union, - cast, -) - -__version__ = "3.0.0-dev.1" -__author__ = "Kostiantyn Rybnikov" -__author_email__ = "k-bx@k-bx.com" -__maintainer__ = ["Sebastien Celles", "Tom Schraitle"] -__maintainer_email__ = "s.celles@gmail.com" -__description__ = "Python helper for Semantic Versioning (http://semver.org)" - -#: Our public interface -__all__ = ( - # - # Module level function: - "bump_build", - "bump_major", - "bump_minor", - "bump_patch", - "bump_prerelease", - "compare", - "deprecated", - "finalize_version", - "format_version", - "match", - "max_ver", - "min_ver", - "parse", - "parse_version_info", - "replace", - # - # CLI interface - "cmd_bump", - "cmd_check", - "cmd_compare", - "createparser", - "main", - "process", - # - # Constants and classes - "SEMVER_SPEC_VERSION", - "VersionInfo", -) - - -#: Contains the implemented semver.org version of the spec -SEMVER_SPEC_VERSION = "2.0.0" - - -# Types -VersionPart = Union[int, Optional[str]] -Comparable = Union["VersionInfo", Dict[str, VersionPart], Collection[VersionPart], str] -Comparator = Callable[["VersionInfo", Comparable], bool] -String = Union[str, bytes] -VersionTuple = Tuple[int, int, int, Optional[str], Optional[str]] -VersionDict = Dict[str, VersionPart] -VersionIterator = Iterator[VersionPart] - - -def cmp(a, b): - """Return negative if ab.""" - return (a > b) - (a < b) - - -def ensure_str(s: String, encoding="utf-8", errors="strict") -> str: - # Taken from six project - """ - Coerce *s* to `str`. - - * `str` -> `str` - * `bytes` -> decoded to `str` - - :param s: the string to convert - :type s: str | bytes - :param encoding: the encoding to apply, defaults to "utf-8" - :type encoding: str - :param errors: set a different error handling scheme, - defaults to "strict". - Other possible values are `ignore`, `replace`, and - `xmlcharrefreplace` as well as any other name - registered with :func:`codecs.register_error`. - :type errors: str - :raises TypeError: if ``s`` is not str or bytes type - :return: the converted string - :rtype: str - """ - if isinstance(s, bytes): - s = s.decode(encoding, errors) - elif not isinstance(s, String.__args__): # type: ignore - raise TypeError("not expecting type '%s'" % type(s)) - return s - - -F = TypeVar("F", bound=Callable) - - -def deprecated( - func: F = None, - replace: str = None, - version: str = None, - category=DeprecationWarning, -) -> Union[Callable[..., F], partial]: - """ - Decorates a function to output a deprecation warning. - - :param func: the function to decorate - :param replace: the function to replace (use the full qualified - name like ``semver.VersionInfo.bump_major``. - :param version: the first version when this function was deprecated. - :param category: allow you to specify the deprecation warning class - of your choice. By default, it's :class:`DeprecationWarning`, but - you can choose :class:`PendingDeprecationWarning` or a custom class. - :return: decorated function which is marked as deprecated - """ - - if func is None: - return partial(deprecated, replace=replace, version=version, category=category) - - @wraps(func) - def wrapper(*args, **kwargs) -> Callable[..., F]: - msg_list = ["Function '{m}.{f}' is deprecated."] - - if version: - msg_list.append("Deprecated since version {v}. ") - msg_list.append("This function will be removed in semver 3.") - if replace: - msg_list.append("Use {r!r} instead.") - else: - msg_list.append("Use the respective 'semver.VersionInfo.{r}' instead.") - - f = cast(F, func).__qualname__ - r = replace or f - - frame = cast(FrameType, cast(FrameType, inspect.currentframe()).f_back) - - msg = " ".join(msg_list) - warnings.warn_explicit( - msg.format(m=func.__module__, f=f, r=r, v=version), - category=category, - filename=inspect.getfile(frame.f_code), - lineno=frame.f_lineno, - ) - # As recommended in the Python documentation - # https://docs.python.org/3/library/inspect.html#the-interpreter-stack - # better remove the interpreter stack: - del frame - return func(*args, **kwargs) # type: ignore - - return wrapper - - -@deprecated(version="2.10.0") -def parse(version): - """ - Parse version to major, minor, patch, pre-release, build parts. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.parse` instead. - - :param version: version string - :return: dictionary with the keys 'build', 'major', 'minor', 'patch', - and 'prerelease'. The prerelease or build keys can be None - if not provided - - >>> ver = semver.parse('3.4.5-pre.2+build.4') - >>> ver['major'] - 3 - >>> ver['minor'] - 4 - >>> ver['patch'] - 5 - >>> ver['prerelease'] - 'pre.2' - >>> ver['build'] - 'build.4' - """ - return VersionInfo.parse(version).to_dict() - - -def comparator(operator: Comparator) -> Comparator: - """Wrap a VersionInfo binary op method in a type-check.""" - - @wraps(operator) - def wrapper(self: "VersionInfo", other: Comparable) -> bool: - comparable_types = ( - VersionInfo, - dict, - tuple, - list, - *String.__args__, # type: ignore - ) - if not isinstance(other, comparable_types): - raise TypeError( - "other type %r must be in %r" % (type(other), comparable_types) - ) - return operator(self, other) - - return wrapper - - -class VersionInfo: - """ - A semver compatible version class. - - :param major: version when you make incompatible API changes. - :param minor: version when you add functionality in - a backwards-compatible manner. - :param patch: version when you make backwards-compatible bug fixes. - :param prerelease: an optional prerelease string - :param build: an optional build string - """ - - __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") - #: Regex for number in a prerelease - _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") - #: Regex for a semver version - _REGEX = re.compile( - r""" - ^ - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - (?:-(?P - (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) - (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* - ))? - (?:\+(?P - [0-9a-zA-Z-]+ - (?:\.[0-9a-zA-Z-]+)* - ))? - $ - """, - re.VERBOSE, - ) - - def __init__( - self, - major: SupportsInt, - minor: SupportsInt = 0, - patch: SupportsInt = 0, - prerelease: Union[String, int] = None, - build: Union[String, int] = None, - ): - # Build a dictionary of the arguments except prerelease and build - version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)} - - for name, value in version_parts.items(): - if value < 0: - raise ValueError( - "{!r} is negative. A version can only be positive.".format(name) - ) - - self._major = version_parts["major"] - self._minor = version_parts["minor"] - self._patch = version_parts["patch"] - self._prerelease = None if prerelease is None else str(prerelease) - self._build = None if build is None else str(build) - - @property - def major(self) -> int: - """The major part of a version (read-only).""" - return self._major - - @major.setter - def major(self, value): - raise AttributeError("attribute 'major' is readonly") - - @property - def minor(self) -> int: - """The minor part of a version (read-only).""" - return self._minor - - @minor.setter - def minor(self, value): - raise AttributeError("attribute 'minor' is readonly") - - @property - def patch(self) -> int: - """The patch part of a version (read-only).""" - return self._patch - - @patch.setter - def patch(self, value): - raise AttributeError("attribute 'patch' is readonly") - - @property - def prerelease(self) -> Optional[str]: - """The prerelease part of a version (read-only).""" - return self._prerelease - - @prerelease.setter - def prerelease(self, value): - raise AttributeError("attribute 'prerelease' is readonly") - - @property - def build(self) -> Optional[str]: - """The build part of a version (read-only).""" - return self._build - - @build.setter - def build(self, value): - raise AttributeError("attribute 'build' is readonly") - - def to_tuple(self) -> VersionTuple: - """ - Convert the VersionInfo object to a tuple. - - .. versionadded:: 2.10.0 - Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to - make this function available in the public API. - - :return: a tuple with all the parts - - >>> semver.VersionInfo(5, 3, 1).to_tuple() - (5, 3, 1, None, None) - """ - return (self.major, self.minor, self.patch, self.prerelease, self.build) - - def to_dict(self) -> VersionDict: - """ - Convert the VersionInfo object to an OrderedDict. - - .. versionadded:: 2.10.0 - Renamed ``VersionInfo._asdict`` to ``VersionInfo.to_dict`` to - make this function available in the public API. - - :return: an OrderedDict with the keys in the order ``major``, ``minor``, - ``patch``, ``prerelease``, and ``build``. - - >>> semver.VersionInfo(3, 2, 1).to_dict() - OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \ -('prerelease', None), ('build', None)]) - """ - return collections.OrderedDict( - ( - ("major", self.major), - ("minor", self.minor), - ("patch", self.patch), - ("prerelease", self.prerelease), - ("build", self.build), - ) - ) - - def __iter__(self) -> VersionIterator: - """Implement iter(self).""" - yield from self.to_tuple() - - @staticmethod - def _increment_string(string: str) -> str: - """ - Look for the last sequence of number(s) in a string and increment. - - :param string: the string to search for. - :return: the incremented string - - Source: - http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 - """ - match = VersionInfo._LAST_NUMBER.search(string) - if match: - next_ = str(int(match.group(1)) + 1) - start, end = match.span(1) - string = string[: max(end - len(next_), start)] + next_ + string[end:] - return string - - def bump_major(self) -> "VersionInfo": - """ - Raise the major part of the version, return a new object but leave self - untouched. - - :return: new object with the raised major part - - >>> ver = semver.VersionInfo.parse("3.4.5") - >>> ver.bump_major() - VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) - """ - cls = type(self) - return cls(self._major + 1) - - def bump_minor(self) -> "VersionInfo": - """ - Raise the minor part of the version, return a new object but leave self - untouched. - - :return: new object with the raised minor part - - >>> ver = semver.VersionInfo.parse("3.4.5") - >>> ver.bump_minor() - VersionInfo(major=3, minor=5, patch=0, prerelease=None, build=None) - """ - cls = type(self) - return cls(self._major, self._minor + 1) - - def bump_patch(self) -> "VersionInfo": - """ - Raise the patch part of the version, return a new object but leave self - untouched. - - :return: new object with the raised patch part - - >>> ver = semver.VersionInfo.parse("3.4.5") - >>> ver.bump_patch() - VersionInfo(major=3, minor=4, patch=6, prerelease=None, build=None) - """ - cls = type(self) - return cls(self._major, self._minor, self._patch + 1) - - def bump_prerelease(self, token: str = "rc") -> "VersionInfo": - """ - Raise the prerelease part of the version, return a new object but leave - self untouched. - - :param token: defaults to 'rc' - :return: new object with the raised prerelease part - - >>> ver = semver.VersionInfo.parse("3.4.5-rc.1") - >>> ver.bump_prerelease() - VersionInfo(major=3, minor=4, patch=5, prerelease='rc.2', \ -build=None) - """ - cls = type(self) - prerelease = cls._increment_string(self._prerelease or (token or "rc") + ".0") - return cls(self._major, self._minor, self._patch, prerelease) - - def bump_build(self, token: str = "build") -> "VersionInfo": - """ - Raise the build part of the version, return a new object but leave self - untouched. - - :param token: defaults to 'build' - :return: new object with the raised build part - - >>> ver = semver.VersionInfo.parse("3.4.5-rc.1+build.9") - >>> ver.bump_build() - VersionInfo(major=3, minor=4, patch=5, prerelease='rc.1', \ -build='build.10') - """ - cls = type(self) - build = cls._increment_string(self._build or (token or "build") + ".0") - return cls(self._major, self._minor, self._patch, self._prerelease, build) - - def compare(self, other: Comparable) -> int: - """ - Compare self with other. - - :param other: the second version - :return: The return value is negative if ver1 < ver2, - zero if ver1 == ver2 and strictly positive if ver1 > ver2 - - >>> semver.VersionInfo.parse("1.0.0").compare("2.0.0") - -1 - >>> semver.VersionInfo.parse("2.0.0").compare("1.0.0") - 1 - >>> semver.VersionInfo.parse("2.0.0").compare("2.0.0") - 0 - >>> semver.VersionInfo.parse("2.0.0").compare(dict(major=2, minor=0, patch=0)) - 0 - """ - cls = type(self) - if isinstance(other, String.__args__): # type: ignore - other = cls.parse(other) - elif isinstance(other, dict): - other = cls(**other) - elif isinstance(other, (tuple, list)): - other = cls(*other) - elif not isinstance(other, cls): - raise TypeError( - f"Expected str, bytes, dict, tuple, list, or {cls.__name__} instance, " - f"but got {type(other)}" - ) - - v1 = self.to_tuple()[:3] - v2 = other.to_tuple()[:3] - x = cmp(v1, v2) - if x: - return x - - rc1, rc2 = self.prerelease, other.prerelease - rccmp = _nat_cmp(rc1, rc2) - - if not rccmp: - return 0 - if not rc1: - return 1 - elif not rc2: - return -1 - - return rccmp - - def next_version(self, part: str, prerelease_token: str = "rc") -> "VersionInfo": - """ - Determines next version, preserving natural order. - - .. versionadded:: 2.10.0 - - This function is taking prereleases into account. - The "major", "minor", and "patch" raises the respective parts like - the ``bump_*`` functions. The real difference is using the - "preprelease" part. It gives you the next patch version of the prerelease, - for example: - - >>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease")) - '0.1.5-rc.1' - - :param part: One of "major", "minor", "patch", or "prerelease" - :param prerelease_token: prefix string of prerelease, defaults to 'rc' - :return: new object with the appropriate part raised - """ - validparts = { - "major", - "minor", - "patch", - "prerelease", - # "build", # currently not used - } - if part not in validparts: - raise ValueError( - "Invalid part. Expected one of {validparts}, but got {part!r}".format( - validparts=validparts, part=part - ) - ) - version = self - if (version.prerelease or version.build) and ( - part == "patch" - or (part == "minor" and version.patch == 0) - or (part == "major" and version.minor == version.patch == 0) - ): - return version.replace(prerelease=None, build=None) - - if part in ("major", "minor", "patch"): - return getattr(version, "bump_" + part)() - - if not version.prerelease: - version = version.bump_patch() - return version.bump_prerelease(prerelease_token) - - @comparator - def __eq__(self, other: Comparable) -> bool: # type: ignore - return self.compare(other) == 0 - - @comparator - def __ne__(self, other: Comparable) -> bool: # type: ignore - return self.compare(other) != 0 - - @comparator - def __lt__(self, other: Comparable) -> bool: - return self.compare(other) < 0 - - @comparator - def __le__(self, other: Comparable) -> bool: - return self.compare(other) <= 0 - - @comparator - def __gt__(self, other: Comparable) -> bool: - return self.compare(other) > 0 - - @comparator - def __ge__(self, other: Comparable) -> bool: - return self.compare(other) >= 0 - - def __getitem__( - self, index: Union[int, slice] - ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]: - """ - self.__getitem__(index) <==> self[index] - - Implement getitem. If the part requested is undefined, or a part of the - range requested is undefined, it will throw an index error. - Negative indices are not supported - - :param Union[int, slice] index: a positive integer indicating the - offset or a :func:`slice` object - :raises IndexError: if index is beyond the range or a part is None - :return: the requested part of the version at position index - - >>> ver = semver.VersionInfo.parse("3.4.5") - >>> ver[0], ver[1], ver[2] - (3, 4, 5) - """ - if isinstance(index, int): - index = slice(index, index + 1) - index = cast(slice, index) - - if ( - isinstance(index, slice) - and (index.start is not None and index.start < 0) - or (index.stop is not None and index.stop < 0) - ): - raise IndexError("Version index cannot be negative") - - part = tuple( - filter(lambda p: p is not None, cast(Iterable, self.to_tuple()[index])) - ) - - if len(part) == 1: - return part[0] - elif not part: - raise IndexError("Version part undefined") - return part - - def __repr__(self) -> str: - s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) - return "%s(%s)" % (type(self).__name__, s) - - def __str__(self) -> str: - """str(self)""" - version = "%d.%d.%d" % (self.major, self.minor, self.patch) - if self.prerelease: - version += "-%s" % self.prerelease - if self.build: - version += "+%s" % self.build - return version - - def __hash__(self) -> int: - return hash(self.to_tuple()[:4]) - - def finalize_version(self) -> "VersionInfo": - """ - Remove any prerelease and build metadata from the version. - - :return: a new instance with the finalized version string - - >>> str(semver.VersionInfo.parse('1.2.3-rc.5').finalize_version()) - '1.2.3' - """ - cls = type(self) - return cls(self.major, self.minor, self.patch) - - def match(self, match_expr: str) -> bool: - """ - Compare self to match a match expression. - - :param match_expr: operator and version; valid operators are - < smaller than - > greater than - >= greator or equal than - <= smaller or equal than - == equal - != not equal - :return: True if the expression matches the version, otherwise False - - >>> semver.VersionInfo.parse("2.0.0").match(">=1.0.0") - True - >>> semver.VersionInfo.parse("1.0.0").match(">1.0.0") - False - """ - prefix = match_expr[:2] - if prefix in (">=", "<=", "==", "!="): - match_version = match_expr[2:] - elif prefix and prefix[0] in (">", "<"): - prefix = prefix[0] - match_version = match_expr[1:] - else: - raise ValueError( - "match_expr parameter should be in format , " - "where is one of " - "['<', '>', '==', '<=', '>=', '!=']. " - "You provided: %r" % match_expr - ) - - possibilities_dict = { - ">": (1,), - "<": (-1,), - "==": (0,), - "!=": (-1, 1), - ">=": (0, 1), - "<=": (-1, 0), - } - - possibilities = possibilities_dict[prefix] - cmp_res = self.compare(match_version) - - return cmp_res in possibilities - - @classmethod - def parse(cls, version: String) -> "VersionInfo": - """ - Parse version string to a VersionInfo instance. - - .. versionchanged:: 2.11.0 - Changed method from static to classmethod to - allow subclasses. - - :param version: version string - :return: a :class:`VersionInfo` instance - :raises ValueError: if version is invalid - - >>> semver.VersionInfo.parse('3.4.5-pre.2+build.4') - VersionInfo(major=3, minor=4, patch=5, \ -prerelease='pre.2', build='build.4') - """ - version_str = ensure_str(version) - match = cls._REGEX.match(version_str) - if match is None: - raise ValueError(f"{version_str} is not valid SemVer string") - - matched_version_parts: Dict[str, Any] = match.groupdict() - - return cls(**matched_version_parts) - - def replace(self, **parts: Union[int, Optional[str]]) -> "VersionInfo": - """ - Replace one or more parts of a version and return a new - :class:`VersionInfo` object, but leave self untouched - - .. versionadded:: 2.9.0 - Added :func:`VersionInfo.replace` - - :param parts: the parts to be updated. Valid keys are: - ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` - :return: the new :class:`VersionInfo` object with the changed - parts - :raises TypeError: if ``parts`` contains invalid keys - """ - version = self.to_dict() - version.update(parts) - try: - return VersionInfo(**version) # type: ignore - except TypeError: - unknownkeys = set(parts) - set(self.to_dict()) - error = "replace() got %d unexpected keyword " "argument(s): %s" % ( - len(unknownkeys), - ", ".join(unknownkeys), - ) - raise TypeError(error) - - @classmethod - def isvalid(cls, version: str) -> bool: - """ - Check if the string is a valid semver version. - - .. versionadded:: 2.9.1 - - :param version: the version string to check - :return: True if the version string is a valid semver version, False - otherwise. - """ - try: - cls.parse(version) - return True - except ValueError: - return False - - -@deprecated(replace="semver.VersionInfo.parse", version="2.10.0") -def parse_version_info(version): - """ - Parse version string to a VersionInfo instance. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.parse` instead. - - .. versionadded:: 2.7.2 - Added :func:`semver.parse_version_info` - - :param version: version string - :return: a :class:`VersionInfo` instance - - >>> version_info = semver.VersionInfo.parse("3.4.5-pre.2+build.4") - >>> version_info.major - 3 - >>> version_info.minor - 4 - >>> version_info.patch - 5 - >>> version_info.prerelease - 'pre.2' - >>> version_info.build - 'build.4' - """ - return VersionInfo.parse(version) - - -def _nat_cmp(a: Optional[str], b: Optional[str]) -> int: - def convert(text: str) -> Union[int, str]: - return int(text) if re.match("^[0-9]+$", text) else text # type: ignore - - def split_key(key: str) -> List[Union[int, str]]: - return [convert(c) for c in key.split(".")] - - def cmp_prerelease_tag(a: Union[int, str], b: Union[int, str]) -> int: - if isinstance(a, int) and isinstance(b, int): - return cmp(a, b) - elif isinstance(a, int): - return -1 - elif isinstance(b, int): - return 1 - else: - return cmp(a, b) - - a, b = a or "", b or "" - a_parts, b_parts = split_key(a), split_key(b) - for sub_a, sub_b in zip(a_parts, b_parts): - cmp_result = cmp_prerelease_tag(sub_a, sub_b) - if cmp_result != 0: - return cmp_result - else: - return cmp(len(a), len(b)) - - -@deprecated(version="2.10.0") -def compare(ver1, ver2): - """ - Compare two versions strings. - - :param ver1: version string 1 - :param ver2: version string 2 - :return: The return value is negative if ver1 < ver2, - zero if ver1 == ver2 and strictly positive if ver1 > ver2 - - >>> semver.compare("1.0.0", "2.0.0") - -1 - >>> semver.compare("2.0.0", "1.0.0") - 1 - >>> semver.compare("2.0.0", "2.0.0") - 0 - """ - v1 = VersionInfo.parse(ver1) - return v1.compare(ver2) - - -@deprecated(version="2.10.0") -def match(version, match_expr): - """ - Compare two versions strings through a comparison. - - :param version: a version string - :param match_expr: operator and version; valid operators are - < smaller than - > greater than - >= greator or equal than - <= smaller or equal than - == equal - != not equal - :return: True if the expression matches the version, otherwise False - - >>> semver.match("2.0.0", ">=1.0.0") - True - >>> semver.match("1.0.0", ">1.0.0") - False - """ - ver = VersionInfo.parse(version) - return ver.match(match_expr) - - -@deprecated(replace="max", version="2.10.2") -def max_ver(ver1, ver2): - """ - Returns the greater version of two versions strings. - - :param ver1: version string 1 - :param ver2: version string 2 - :return: the greater version of the two - - >>> semver.max_ver("1.0.0", "2.0.0") - '2.0.0' - """ - if isinstance(ver1, String.__args__): - ver1 = VersionInfo.parse(ver1) - elif not isinstance(ver1, VersionInfo): - raise TypeError() - cmp_res = ver1.compare(ver2) - if cmp_res >= 0: - return str(ver1) - else: - return ver2 - - -@deprecated(replace="min", version="2.10.2") -def min_ver(ver1, ver2): - """ - Returns the smaller version of two versions strings. - - :param ver1: version string 1 - :param ver2: version string 2 - :return: the smaller version of the two - - >>> semver.min_ver("1.0.0", "2.0.0") - '1.0.0' - """ - ver1 = VersionInfo.parse(ver1) - cmp_res = ver1.compare(ver2) - if cmp_res <= 0: - return str(ver1) - else: - return ver2 - - -@deprecated(replace="str(versionobject)", version="2.10.0") -def format_version(major, minor, patch, prerelease=None, build=None): - """ - Format a version string according to the Semantic Versioning specification. - - .. deprecated:: 2.10.0 - Use ``str(VersionInfo(VERSION)`` instead. - - :param major: the required major part of a version - :param minor: the required minor part of a version - :param patch: the required patch part of a version - :param prerelease: the optional prerelease part of a version - :param build: the optional build part of a version - :return: the formatted string - - >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') - '3.4.5-pre.2+build.4' - """ - return str(VersionInfo(major, minor, patch, prerelease, build)) - - -@deprecated(version="2.10.0") -def bump_major(version): - """ - Raise the major part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_major` instead. - - :param: version string - :return: the raised version string - - >>> semver.bump_major("3.4.5") - '4.0.0' - """ - return str(VersionInfo.parse(version).bump_major()) - - -@deprecated(version="2.10.0") -def bump_minor(version): - """ - Raise the minor part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_minor` instead. - - :param: version string - :return: the raised version string - - >>> semver.bump_minor("3.4.5") - '3.5.0' - """ - return str(VersionInfo.parse(version).bump_minor()) - - -@deprecated(version="2.10.0") -def bump_patch(version): - """ - Raise the patch part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_patch` instead. - - :param: version string - :return: the raised version string - - >>> semver.bump_patch("3.4.5") - '3.4.6' - """ - return str(VersionInfo.parse(version).bump_patch()) - - -@deprecated(version="2.10.0") -def bump_prerelease(version, token="rc"): - """ - Raise the prerelease part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_prerelease` instead. - - :param version: version string - :param token: defaults to 'rc' - :return: the raised version string - - >>> semver.bump_prerelease('3.4.5', 'dev') - '3.4.5-dev.1' - """ - return str(VersionInfo.parse(version).bump_prerelease(token)) - - -@deprecated(version="2.10.0") -def bump_build(version, token="build"): - """ - Raise the build part of the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.bump_build` instead. - - :param version: version string - :param token: defaults to 'build' - :return: the raised version string - - >>> semver.bump_build('3.4.5-rc.1+build.9') - '3.4.5-rc.1+build.10' - """ - return str(VersionInfo.parse(version).bump_build(token)) - - -@deprecated(version="2.10.0") -def finalize_version(version): - """ - Remove any prerelease and build metadata from the version string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.finalize_version` instead. - - .. versionadded:: 2.7.9 - Added :func:`finalize_version` - - :param version: version string - :return: the finalized version string - - >>> semver.finalize_version('1.2.3-rc.5') - '1.2.3' - """ - verinfo = VersionInfo.parse(version) - return str(verinfo.finalize_version()) - - -@deprecated(version="2.10.0") -def replace(version, **parts): - """ - Replace one or more parts of a version and return the new string. - - .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.replace` instead. - - .. versionadded:: 2.9.0 - Added :func:`replace` - - :param version: the version string to replace - :param parts: the parts to be updated. Valid keys are: - ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` - :return: the replaced version string - :raises TypeError: if ``parts`` contains invalid keys - - >>> import semver - >>> semver.replace("1.2.3", major=2, patch=10) - '2.2.10' - """ - return str(VersionInfo.parse(version).replace(**parts)) - - -# ---- CLI -def cmd_bump(args: argparse.Namespace) -> str: - """ - Subcommand: Bumps a version. - - Synopsis: bump - can be major, minor, patch, prerelease, or build - - :param args: The parsed arguments - :return: the new, bumped version - """ - maptable = { - "major": "bump_major", - "minor": "bump_minor", - "patch": "bump_patch", - "prerelease": "bump_prerelease", - "build": "bump_build", - } - if args.bump is None: - # When bump is called without arguments, - # print the help and exit - args.parser.parse_args(["bump", "-h"]) - - ver = VersionInfo.parse(args.version) - # get the respective method and call it - func = getattr(ver, maptable[cast(str, args.bump)]) - return str(func()) - - -def cmd_check(args: argparse.Namespace) -> None: - """ - Subcommand: Checks if a string is a valid semver version. - - Synopsis: check - - :param args: The parsed arguments - """ - if VersionInfo.isvalid(args.version): - return None - raise ValueError("Invalid version %r" % args.version) - - -def cmd_compare(args: argparse.Namespace) -> str: - """ - Subcommand: Compare two versions - - Synopsis: compare - - :param args: The parsed arguments - """ - return str(compare(args.version1, args.version2)) - - -def cmd_nextver(args: argparse.Namespace) -> str: - """ - Subcommand: Determines the next version, taking prereleases into account. - - Synopsis: nextver - - :param args: The parsed arguments - """ - version = VersionInfo.parse(args.version) - return str(version.next_version(args.part)) - - -def createparser() -> argparse.ArgumentParser: - """ - Create an :class:`argparse.ArgumentParser` instance. - - :return: parser instance - """ - parser = argparse.ArgumentParser(prog=__package__, description=__doc__) - - parser.add_argument( - "--version", action="version", version="%(prog)s " + __version__ - ) - - s = parser.add_subparsers() - # create compare subcommand - parser_compare = s.add_parser("compare", help="Compare two versions") - parser_compare.set_defaults(func=cmd_compare) - parser_compare.add_argument("version1", help="First version") - parser_compare.add_argument("version2", help="Second version") - - # create bump subcommand - parser_bump = s.add_parser("bump", help="Bumps a version") - parser_bump.set_defaults(func=cmd_bump) - sb = parser_bump.add_subparsers(title="Bump commands", dest="bump") - - # Create subparsers for the bump subparser: - for p in ( - sb.add_parser("major", help="Bump the major part of the version"), - sb.add_parser("minor", help="Bump the minor part of the version"), - sb.add_parser("patch", help="Bump the patch part of the version"), - sb.add_parser("prerelease", help="Bump the prerelease part of the version"), - sb.add_parser("build", help="Bump the build part of the version"), - ): - p.add_argument("version", help="Version to raise") - - # Create the check subcommand - parser_check = s.add_parser( - "check", help="Checks if a string is a valid semver version" - ) - parser_check.set_defaults(func=cmd_check) - parser_check.add_argument("version", help="Version to check") - - # Create the nextver subcommand - parser_nextver = s.add_parser( - "nextver", help="Determines the next version, taking prereleases into account." - ) - parser_nextver.set_defaults(func=cmd_nextver) - parser_nextver.add_argument("version", help="Version to raise") - parser_nextver.add_argument( - "part", help="One of 'major', 'minor', 'patch', or 'prerelease'" - ) - return parser - - -def process(args: argparse.Namespace) -> str: - """ - Process the input from the CLI. - - :param args: The parsed arguments - :param parser: the parser instance - :return: result of the selected action - """ - if not hasattr(args, "func"): - args.parser.print_help() - raise SystemExit() - - # Call the respective function object: - return args.func(args) - - -def main(cliargs: List[str] = None) -> int: - """ - Entry point for the application script. - - :param list cliargs: Arguments to parse or None (=use :class:`sys.argv`) - :return: error code - """ - try: - parser = createparser() - args = parser.parse_args(args=cliargs) - # Save parser instance: - args.parser = parser - result = process(args) - if result is not None: - print(result) - return 0 - - except (ValueError, TypeError) as err: - print("ERROR", err, file=sys.stderr) - return 2 - - -if __name__ == "__main__": - import doctest - - doctest.testmod() diff --git a/setup.cfg b/setup.cfg index 5abd4bbb..52b5d3e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,46 @@ +[metadata] +name = semver +version = attr: semver.__about__.__version__ +description = attr: semver.__about__.__description__ +long_description = file: README.rst +author = attr: semver.__about__.__author__ +author_email = attr: semver.__about__.__author_email__ +maintainer = attr: semver.__about__.__maintainer__ +maintainer_email = attr: semver.__about__.__maintainer_email__ +url = https://github.com/python-semver/python-semver +download_url = https://github.com/python-semver/python-semver/downloads +project_urls = + Documentation = https://python-semver.rtfd.io + Releases = https://github.com/python-semver/python-semver/releases + Bug Tracker = https://github.com/python-semver/python-semver/issues +classifiers = + Environment :: Web Environment + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Topic :: Software Development :: Libraries :: Python Modules +license = BSD + +[options] +package_dir = + =src +packages = find: +python_requires = >=3.6.* +include_package_data = True + +[options.entry_points] +console_scripts = + pysemver = semver.cli:main + +[options.packages.find] +where = src + [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ testpaths = tests docs @@ -15,13 +58,14 @@ addopts = max-line-length = 88 ignore = F821,W503 exclude = - .env, - venv, - .eggs, - .tox, - .git, - __pycache__, - build, + src/semver/__init__.py + .env + venv + .eggs + .tox + .git + __pycache__ + build dist docs conftest.py @@ -32,6 +76,7 @@ count = False max-line-length = 88 statistics = True exclude = + src/semver/__init__.py .env, .eggs, .tox, diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 57ee4b26..88990ad8 --- a/setup.py +++ b/setup.py @@ -1,75 +1,4 @@ #!/usr/bin/env python3 -# import semver as package -from os.path import dirname, join -from setuptools import setup -import re +import setuptools - -VERSION_MATCH = re.compile(r"__version__ = ['\"]([^'\"]*)['\"]", re.M) - - -def read_file(filename): - """ - Read RST file and return content - - :param filename: the RST file - :return: content of the RST file - """ - with open(join(dirname(__file__), filename)) as f: - return f.read() - - -def find_meta(meta): - """ - Extract __*meta*__ from META_FILE. - """ - meta_match = re.search( - r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M - ) - if meta_match: - return meta_match.group(1) - raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) - - -NAME = "semver" -META_FILE = read_file("semver.py") - - -# ----------------------------------------------------------------------------- -setup( - name=NAME, - version=find_meta("version"), - description=find_meta("description").strip(), - long_description=read_file("README.rst"), - long_description_content_type="text/x-rst", - author=find_meta("author"), - author_email=find_meta("author_email"), - url="https://github.com/python-semver/python-semver", - download_url="https://github.com/python-semver/python-semver/downloads", - project_urls={ - "Documentation": "https://python-semver.rtfd.io", - "Releases": "https://github.com/python-semver/python-semver/releases", - "Bug Tracker": "https://github.com/python-semver/python-semver/issues", - }, - py_modules=[NAME], - include_package_data=True, - license="BSD", - classifiers=[ - # See https://pypi.org/pypi?%3Aaction=list_classifiers - "Environment :: Web Environment", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - # "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - python_requires=">=3.6.*", - tests_require=["tox", "virtualenv", "wheel"], - entry_points={"console_scripts": ["pysemver = semver:main"]}, -) +setuptools.setup() # For compatibility with python 3.6 diff --git a/src/semver/__about__.py b/src/semver/__about__.py new file mode 100644 index 00000000..5e7a3537 --- /dev/null +++ b/src/semver/__about__.py @@ -0,0 +1,8 @@ +__version__ = "3.0.0-dev.2" +__author__ = "Kostiantyn Rybnikov" +__author_email__ = "k-bx@k-bx.com" +__maintainer__ = ["Sebastien Celles", "Tom Schraitle"] +__maintainer_email__ = "s.celles@gmail.com" +__description__ = "Python helper for Semantic Versioning (http://semver.org)" + +SEMVER_SPEC_VERSION = "2.0.0" diff --git a/src/semver/__init__.py b/src/semver/__init__.py new file mode 100644 index 00000000..1a80f6c3 --- /dev/null +++ b/src/semver/__init__.py @@ -0,0 +1,33 @@ +from ._deprecated import ( + bump_build, + bump_major, + bump_minor, + bump_patch, + bump_prerelease, + compare, + finalize_version, + format_version, + match, + max_ver, + min_ver, + parse, + parse_version_info, + replace, + cmd_bump, + cmd_compare, + cmd_nextver, + cmd_check, + createparser, + process, + main, +) +from .version import Version, VersionInfo +from .__about__ import ( + __version__, + __author__, + __maintainer__, + __author_email__, + __description__, + __maintainer_email__, + SEMVER_SPEC_VERSION, +) diff --git a/src/semver/__main__.py b/src/semver/__main__.py new file mode 100644 index 00000000..0e0648a9 --- /dev/null +++ b/src/semver/__main__.py @@ -0,0 +1,23 @@ +""" +Module to support call with :file:`__main__.py`. Used to support the following +call: + +$ python3 -m semver ... +""" +import os.path +import sys +from typing import List + +from semver import cli + + +def main(cliargs: List[str] = None) -> int: + if __package__ == "": + path = os.path.dirname(os.path.dirname(__file__)) + sys.path[0:0] = [path] + + return cli.main(cliargs) + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py new file mode 100644 index 00000000..45c52753 --- /dev/null +++ b/src/semver/_deprecated.py @@ -0,0 +1,378 @@ +import inspect +import warnings +from functools import partial, wraps +from types import FrameType +from typing import Type, Union, Callable, cast + +from . import cli +from .version import Version +from ._types import F, String + + +def deprecated( + func: F = None, + replace: str = None, + version: str = None, + category: Type[Warning] = DeprecationWarning, +) -> Union[Callable[..., F], partial]: + """ + Decorates a function to output a deprecation warning. + + :param func: the function to decorate + :param replace: the function to replace (use the full qualified + name like ``semver.Version.bump_major``. + :param version: the first version when this function was deprecated. + :param category: allow you to specify the deprecation warning class + of your choice. By default, it's :class:`DeprecationWarning`, but + you can choose :class:`PendingDeprecationWarning` or a custom class. + :return: decorated function which is marked as deprecated + """ + + if func is None: + return partial(deprecated, replace=replace, version=version, category=category) + + @wraps(func) + def wrapper(*args, **kwargs) -> Callable[..., F]: + msg_list = ["Function 'semver.{f}' is deprecated."] + + if version: + msg_list.append("Deprecated since version {v}. ") + msg_list.append("This function will be removed in semver 3.") + if replace: + msg_list.append("Use {r!r} instead.") + else: + msg_list.append("Use the respective 'semver.Version.{r}' instead.") + + f = cast(F, func).__qualname__ + r = replace or f + + frame = cast(FrameType, cast(FrameType, inspect.currentframe()).f_back) + + msg = " ".join(msg_list) + warnings.warn_explicit( + msg.format(f=f, r=r, v=version), + category=category, + filename=inspect.getfile(frame.f_code), + lineno=frame.f_lineno, + ) + # As recommended in the Python documentation + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + # better remove the interpreter stack: + del frame + return func(*args, **kwargs) # type: ignore + + return wrapper + + +@deprecated(version="2.10.0") +def parse(version): + """ + Parse version to major, minor, patch, pre-release, build parts. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.parse` instead. + + :param version: version string + :return: dictionary with the keys 'build', 'major', 'minor', 'patch', + and 'prerelease'. The prerelease or build keys can be None + if not provided + :rtype: dict + + >>> ver = semver.parse('3.4.5-pre.2+build.4') + >>> ver['major'] + 3 + >>> ver['minor'] + 4 + >>> ver['patch'] + 5 + >>> ver['prerelease'] + 'pre.2' + >>> ver['build'] + 'build.4' + """ + return Version.parse(version).to_dict() + + +@deprecated(replace="semver.Version.parse", version="2.10.0") +def parse_version_info(version): + """ + Parse version string to a VersionInfo instance. + + .. deprecated:: 2.10.0 + Use :func:`semver.VersionInfo.parse` instead. + .. versionadded:: 2.7.2 + Added :func:`semver.parse_version_info` + :param version: version string + :return: a :class:`VersionInfo` instance + >>> version_info = semver.Version.parse("3.4.5-pre.2+build.4") + >>> version_info.major + 3 + >>> version_info.minor + 4 + >>> version_info.patch + 5 + >>> version_info.prerelease + 'pre.2' + >>> version_info.build + 'build.4' + """ + return Version.parse(version) + + +@deprecated(version="2.10.0") +def compare(ver1, ver2): + """ + Compare two versions strings. + + :param ver1: version string 1 + :param ver2: version string 2 + :return: The return value is negative if ver1 < ver2, + zero if ver1 == ver2 and strictly positive if ver1 > ver2 + :rtype: int + + >>> semver.compare("1.0.0", "2.0.0") + -1 + >>> semver.compare("2.0.0", "1.0.0") + 1 + >>> semver.compare("2.0.0", "2.0.0") + 0 + """ + v1 = Version.parse(ver1) + return v1.compare(ver2) + + +@deprecated(version="2.10.0") +def match(version, match_expr): + """ + Compare two versions strings through a comparison. + + :param str version: a version string + :param str match_expr: operator and version; valid operators are + < smaller than + > greater than + >= greator or equal than + <= smaller or equal than + == equal + != not equal + :return: True if the expression matches the version, otherwise False + :rtype: bool + + >>> semver.match("2.0.0", ">=1.0.0") + True + >>> semver.match("1.0.0", ">1.0.0") + False + """ + ver = Version.parse(version) + return ver.match(match_expr) + + +@deprecated(replace="max", version="2.10.2") +def max_ver(ver1, ver2): + """ + Returns the greater version of two versions strings. + + :param ver1: version string 1 + :param ver2: version string 2 + :return: the greater version of the two + :rtype: :class:`Version` + + >>> semver.max_ver("1.0.0", "2.0.0") + '2.0.0' + """ + if isinstance(ver1, String.__args__): # type: ignore + ver1 = Version.parse(ver1) + elif not isinstance(ver1, Version): + raise TypeError() + cmp_res = ver1.compare(ver2) + if cmp_res >= 0: + return str(ver1) + else: + return ver2 + + +@deprecated(replace="min", version="2.10.2") +def min_ver(ver1, ver2): + """ + Returns the smaller version of two versions strings. + + :param ver1: version string 1 + :param ver2: version string 2 + :return: the smaller version of the two + :rtype: :class:`Version` + + >>> semver.min_ver("1.0.0", "2.0.0") + '1.0.0' + """ + ver1 = Version.parse(ver1) + cmp_res = ver1.compare(ver2) + if cmp_res <= 0: + return str(ver1) + else: + return ver2 + + +@deprecated(replace="str(versionobject)", version="2.10.0") +def format_version(major, minor, patch, prerelease=None, build=None): + """ + Format a version string according to the Semantic Versioning specification. + + .. deprecated:: 2.10.0 + Use ``str(Version(VERSION)`` instead. + + :param int major: the required major part of a version + :param int minor: the required minor part of a version + :param int patch: the required patch part of a version + :param str prerelease: the optional prerelease part of a version + :param str build: the optional build part of a version + :return: the formatted string + :rtype: str + + >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') + '3.4.5-pre.2+build.4' + """ + return str(Version(major, minor, patch, prerelease, build)) + + +@deprecated(version="2.10.0") +def bump_major(version): + """ + Raise the major part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_major` instead. + + :param: version string + :return: the raised version string + :rtype: str + + >>> semver.bump_major("3.4.5") + '4.0.0' + """ + return str(Version.parse(version).bump_major()) + + +@deprecated(version="2.10.0") +def bump_minor(version): + """ + Raise the minor part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_minor` instead. + + :param: version string + :return: the raised version string + :rtype: str + + >>> semver.bump_minor("3.4.5") + '3.5.0' + """ + return str(Version.parse(version).bump_minor()) + + +@deprecated(version="2.10.0") +def bump_patch(version): + """ + Raise the patch part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_patch` instead. + + :param: version string + :return: the raised version string + :rtype: str + + >>> semver.bump_patch("3.4.5") + '3.4.6' + """ + return str(Version.parse(version).bump_patch()) + + +@deprecated(version="2.10.0") +def bump_prerelease(version, token="rc"): + """ + Raise the prerelease part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_prerelease` instead. + + :param version: version string + :param token: defaults to 'rc' + :return: the raised version string + :rtype: str + + >>> semver.bump_prerelease('3.4.5', 'dev') + '3.4.5-dev.1' + """ + return str(Version.parse(version).bump_prerelease(token)) + + +@deprecated(version="2.10.0") +def bump_build(version, token="build"): + """ + Raise the build part of the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.bump_build` instead. + + :param version: version string + :param token: defaults to 'build' + :return: the raised version string + :rtype: str + + >>> semver.bump_build('3.4.5-rc.1+build.9') + '3.4.5-rc.1+build.10' + """ + return str(Version.parse(version).bump_build(token)) + + +@deprecated(version="2.10.0") +def finalize_version(version): + """ + Remove any prerelease and build metadata from the version string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.finalize_version` instead. + + .. versionadded:: 2.7.9 + Added :func:`finalize_version` + + :param version: version string + :return: the finalized version string + :rtype: str + + >>> semver.finalize_version('1.2.3-rc.5') + '1.2.3' + """ + verinfo = Version.parse(version) + return str(verinfo.finalize_version()) + + +@deprecated(version="2.10.0") +def replace(version, **parts): + """ + Replace one or more parts of a version and return the new string. + + .. deprecated:: 2.10.0 + Use :func:`semver.Version.replace` instead. + .. versionadded:: 2.9.0 + Added :func:`replace` + :param version: the version string to replace + :param parts: the parts to be updated. Valid keys are: + ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` + :return: the replaced version string + :raises TypeError: if ``parts`` contains invalid keys + >>> import semver + >>> semver.replace("1.2.3", major=2, patch=10) + '2.2.10' + """ + return str(Version.parse(version).replace(**parts)) + + +# CLI +cmd_bump = deprecated(cli.cmd_bump, "semver.cli.cmd_bump", "3.0.0") +cmd_check = deprecated(cli.cmd_check, "semver.cli.cmd_check", "3.0.0") +cmd_compare = deprecated(cli.cmd_compare, "semver.cli.cmd_compare", "3.0.0") +cmd_nextver = deprecated(cli.cmd_nextver, "semver.cli.cmd_nextver", "3.0.0") +createparser = deprecated(cli.createparser, "semver.cli.createparser", "3.0.0") +process = deprecated(cli.process, "semver.cli.process", "3.0.0") +main = deprecated(cli.main, "semver.cli.main", "3.0.0") diff --git a/src/semver/_types.py b/src/semver/_types.py new file mode 100644 index 00000000..823c7349 --- /dev/null +++ b/src/semver/_types.py @@ -0,0 +1,8 @@ +from typing import Union, Optional, Tuple, Dict, Iterable, Callable, TypeVar + +VersionPart = Union[int, Optional[str]] +VersionTuple = Tuple[int, int, int, Optional[str], Optional[str]] +VersionDict = Dict[str, VersionPart] +VersionIterator = Iterable[VersionPart] +String = Union[str, bytes] +F = TypeVar("F", bound=Callable) diff --git a/src/semver/cli.py b/src/semver/cli.py new file mode 100644 index 00000000..1514979c --- /dev/null +++ b/src/semver/cli.py @@ -0,0 +1,162 @@ +import argparse +import sys +from typing import cast, List + +from .version import Version +from .__about__ import __version__ + + +def cmd_bump(args: argparse.Namespace) -> str: + """ + Subcommand: Bumps a version. + + Synopsis: bump + can be major, minor, patch, prerelease, or build + + :param args: The parsed arguments + :return: the new, bumped version + """ + maptable = { + "major": "bump_major", + "minor": "bump_minor", + "patch": "bump_patch", + "prerelease": "bump_prerelease", + "build": "bump_build", + } + if args.bump is None: + # When bump is called without arguments, + # print the help and exit + args.parser.parse_args(["bump", "-h"]) + + ver = Version.parse(args.version) + # get the respective method and call it + func = getattr(ver, maptable[cast(str, args.bump)]) + return str(func()) + + +def cmd_check(args: argparse.Namespace) -> None: + """ + Subcommand: Checks if a string is a valid semver version. + + Synopsis: check + + :param args: The parsed arguments + """ + if Version.isvalid(args.version): + return None + raise ValueError("Invalid version %r" % args.version) + + +def cmd_compare(args: argparse.Namespace) -> str: + """ + Subcommand: Compare two versions + + Synopsis: compare + + :param args: The parsed arguments + """ + ver1 = Version.parse(args.version1) + return str(ver1.compare(args.version2)) + + +def cmd_nextver(args: argparse.Namespace) -> str: + """ + Subcommand: Determines the next version, taking prereleases into account. + + Synopsis: nextver + + :param args: The parsed arguments + """ + version = Version.parse(args.version) + return str(version.next_version(args.part)) + + +def createparser() -> argparse.ArgumentParser: + """ + Create an :class:`argparse.ArgumentParser` instance. + + :return: parser instance + """ + parser = argparse.ArgumentParser(prog=__package__, description=__doc__) + + parser.add_argument( + "--version", action="version", version="%(prog)s " + __version__ + ) + + s = parser.add_subparsers() + # create compare subcommand + parser_compare = s.add_parser("compare", help="Compare two versions") + parser_compare.set_defaults(func=cmd_compare) + parser_compare.add_argument("version1", help="First version") + parser_compare.add_argument("version2", help="Second version") + + # create bump subcommand + parser_bump = s.add_parser("bump", help="Bumps a version") + parser_bump.set_defaults(func=cmd_bump) + sb = parser_bump.add_subparsers(title="Bump commands", dest="bump") + + # Create subparsers for the bump subparser: + for p in ( + sb.add_parser("major", help="Bump the major part of the version"), + sb.add_parser("minor", help="Bump the minor part of the version"), + sb.add_parser("patch", help="Bump the patch part of the version"), + sb.add_parser("prerelease", help="Bump the prerelease part of the version"), + sb.add_parser("build", help="Bump the build part of the version"), + ): + p.add_argument("version", help="Version to raise") + + # Create the check subcommand + parser_check = s.add_parser( + "check", help="Checks if a string is a valid semver version" + ) + parser_check.set_defaults(func=cmd_check) + parser_check.add_argument("version", help="Version to check") + + # Create the nextver subcommand + parser_nextver = s.add_parser( + "nextver", help="Determines the next version, taking prereleases into account." + ) + parser_nextver.set_defaults(func=cmd_nextver) + parser_nextver.add_argument("version", help="Version to raise") + parser_nextver.add_argument( + "part", help="One of 'major', 'minor', 'patch', or 'prerelease'" + ) + return parser + + +def process(args: argparse.Namespace) -> str: + """ + Process the input from the CLI. + + :param args: The parsed arguments + :param parser: the parser instance + :return: result of the selected action + """ + if not hasattr(args, "func"): + args.parser.print_help() + raise SystemExit() + + # Call the respective function object: + return args.func(args) + + +def main(cliargs: List[str] = None) -> int: + """ + Entry point for the application script. + + :param list cliargs: Arguments to parse or None (=use :class:`sys.argv`) + :return: error code + """ + try: + parser = createparser() + args = parser.parse_args(args=cliargs) + # Save parser instance: + args.parser = parser + result = process(args) + if result is not None: + print(result) + return 0 + + except (ValueError, TypeError) as err: + print("ERROR", err, file=sys.stderr) + return 2 diff --git a/src/semver/version.py b/src/semver/version.py new file mode 100644 index 00000000..93a6d338 --- /dev/null +++ b/src/semver/version.py @@ -0,0 +1,655 @@ +import collections +import re +from functools import wraps +from typing import ( + Any, + Dict, + Iterable, + Optional, + SupportsInt, + Tuple, + Union, + cast, + Callable, + Collection, +) + +from ._types import ( + VersionTuple, + VersionDict, + VersionIterator, + String, + VersionPart, +) + +# These types are required here because of circular imports +Comparable = Union["Version", Dict[str, VersionPart], Collection[VersionPart], str] +Comparator = Callable[["Version", Comparable], bool] + + +def cmp(a, b): # TODO: type hints + """Return negative if ab.""" + return (a > b) - (a < b) + + +def ensure_str(s: String, encoding="utf-8", errors="strict") -> str: + # Taken from six project + """ + Coerce *s* to `str`. + + * `str` -> `str` + * `bytes` -> decoded to `str` + + :param s: the string to convert + :type s: str | bytes + :param encoding: the encoding to apply, defaults to "utf-8" + :type encoding: str + :param errors: set a different error handling scheme, + defaults to "strict". + Other possible values are `ignore`, `replace`, and + `xmlcharrefreplace` as well as any other name + registered with :func:`codecs.register_error`. + :type errors: str + :raises TypeError: if ``s`` is not str or bytes type + :return: the converted string + :rtype: str + """ + if isinstance(s, bytes): + s = s.decode(encoding, errors) + elif not isinstance(s, String.__args__): # type: ignore + raise TypeError("not expecting type '%s'" % type(s)) + return s + + +def comparator(operator: Comparator) -> Comparator: + """Wrap a Version binary op method in a type-check.""" + + @wraps(operator) + def wrapper(self: "Version", other: Comparable) -> bool: + comparable_types = ( + Version, + dict, + tuple, + list, + *String.__args__, # type: ignore + ) + if not isinstance(other, comparable_types): + raise TypeError( + "other type %r must be in %r" % (type(other), comparable_types) + ) + return operator(self, other) + + return wrapper + + +def _nat_cmp(a, b): # TODO: type hints + def convert(text): + return int(text) if re.match("^[0-9]+$", text) else text + + def split_key(key): + return [convert(c) for c in key.split(".")] + + def cmp_prerelease_tag(a, b): + if isinstance(a, int) and isinstance(b, int): + return cmp(a, b) + elif isinstance(a, int): + return -1 + elif isinstance(b, int): + return 1 + else: + return cmp(a, b) + + a, b = a or "", b or "" + a_parts, b_parts = split_key(a), split_key(b) + for sub_a, sub_b in zip(a_parts, b_parts): + cmp_result = cmp_prerelease_tag(sub_a, sub_b) + if cmp_result != 0: + return cmp_result + else: + return cmp(len(a), len(b)) + + +class Version: + """ + A semver compatible version class. + + :param major: version when you make incompatible API changes. + :param minor: version when you add functionality in + a backwards-compatible manner. + :param patch: version when you make backwards-compatible bug fixes. + :param prerelease: an optional prerelease string + :param build: an optional build string + """ + + __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") + #: Regex for number in a prerelease + _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") + #: Regex for a semver version + _REGEX = re.compile( + r""" + ^ + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + \. + (?P0|[1-9]\d*) + (?:-(?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* + ))? + (?:\+(?P + [0-9a-zA-Z-]+ + (?:\.[0-9a-zA-Z-]+)* + ))? + $ + """, + re.VERBOSE, + ) + + def __init__( + self, + major: SupportsInt, + minor: SupportsInt = 0, + patch: SupportsInt = 0, + prerelease: Union[String, int] = None, + build: Union[String, int] = None, + ): + # Build a dictionary of the arguments except prerelease and build + version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)} + + for name, value in version_parts.items(): + if value < 0: + raise ValueError( + "{!r} is negative. A version can only be positive.".format(name) + ) + + self._major = version_parts["major"] + self._minor = version_parts["minor"] + self._patch = version_parts["patch"] + self._prerelease = None if prerelease is None else str(prerelease) + self._build = None if build is None else str(build) + + @property + def major(self) -> int: + """The major part of a version (read-only).""" + return self._major + + @major.setter + def major(self, value): + raise AttributeError("attribute 'major' is readonly") + + @property + def minor(self) -> int: + """The minor part of a version (read-only).""" + return self._minor + + @minor.setter + def minor(self, value): + raise AttributeError("attribute 'minor' is readonly") + + @property + def patch(self) -> int: + """The patch part of a version (read-only).""" + return self._patch + + @patch.setter + def patch(self, value): + raise AttributeError("attribute 'patch' is readonly") + + @property + def prerelease(self) -> Optional[str]: + """The prerelease part of a version (read-only).""" + return self._prerelease + + @prerelease.setter + def prerelease(self, value): + raise AttributeError("attribute 'prerelease' is readonly") + + @property + def build(self) -> Optional[str]: + """The build part of a version (read-only).""" + return self._build + + @build.setter + def build(self, value): + raise AttributeError("attribute 'build' is readonly") + + def to_tuple(self) -> VersionTuple: + """ + Convert the VersionInfo object to a tuple. + + .. versionadded:: 2.10.0 + Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to + make this function available in the public API. + + :return: a tuple with all the parts + + >>> semver.Version(5, 3, 1).to_tuple() + (5, 3, 1, None, None) + """ + return (self.major, self.minor, self.patch, self.prerelease, self.build) + + def to_dict(self) -> VersionDict: + """ + Convert the VersionInfo object to an OrderedDict. + + .. versionadded:: 2.10.0 + Renamed ``VersionInfo._asdict`` to ``VersionInfo.to_dict`` to + make this function available in the public API. + + :return: an OrderedDict with the keys in the order ``major``, ``minor``, + ``patch``, ``prerelease``, and ``build``. + + >>> semver.Version(3, 2, 1).to_dict() + OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \ +('prerelease', None), ('build', None)]) + """ + return collections.OrderedDict( + ( + ("major", self.major), + ("minor", self.minor), + ("patch", self.patch), + ("prerelease", self.prerelease), + ("build", self.build), + ) + ) + + def __iter__(self) -> VersionIterator: + """Implement iter(self).""" + yield from self.to_tuple() + + @staticmethod + def _increment_string(string: str) -> str: + """ + Look for the last sequence of number(s) in a string and increment. + + :param string: the string to search for. + :return: the incremented string + + Source: + http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 + """ + match = Version._LAST_NUMBER.search(string) + if match: + next_ = str(int(match.group(1)) + 1) + start, end = match.span(1) + string = string[: max(end - len(next_), start)] + next_ + string[end:] + return string + + def bump_major(self) -> "Version": + """ + Raise the major part of the version, return a new object but leave self + untouched. + + :return: new object with the raised major part + + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_major() + Version(major=4, minor=0, patch=0, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major + 1) + + def bump_minor(self) -> "Version": + """ + Raise the minor part of the version, return a new object but leave self + untouched. + + :return: new object with the raised minor part + + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_minor() + Version(major=3, minor=5, patch=0, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major, self._minor + 1) + + def bump_patch(self) -> "Version": + """ + Raise the patch part of the version, return a new object but leave self + untouched. + + :return: new object with the raised patch part + + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_patch() + Version(major=3, minor=4, patch=6, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major, self._minor, self._patch + 1) + + def bump_prerelease(self, token: str = "rc") -> "Version": + """ + Raise the prerelease part of the version, return a new object but leave + self untouched. + + :param token: defaults to 'rc' + :return: new object with the raised prerelease part + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_prerelease() + Version(major=3, minor=4, patch=5, prerelease='rc.2', \ +build=None) + """ + cls = type(self) + prerelease = cls._increment_string(self._prerelease or (token or "rc") + ".0") + return cls(self._major, self._minor, self._patch, prerelease) + + def bump_build(self, token: str = "build") -> "Version": + """ + Raise the build part of the version, return a new object but leave self + untouched. + + :param token: defaults to 'build' + :return: new object with the raised build part + + >>> ver = semver.parse("3.4.5-rc.1+build.9") + >>> ver.bump_build() + Version(major=3, minor=4, patch=5, prerelease='rc.1', \ +build='build.10') + """ + cls = type(self) + build = cls._increment_string(self._build or (token or "build") + ".0") + return cls(self._major, self._minor, self._patch, self._prerelease, build) + + def compare(self, other: Comparable) -> int: + """ + Compare self with other. + + :param other: the second version + :return: The return value is negative if ver1 < ver2, + zero if ver1 == ver2 and strictly positive if ver1 > ver2 + + + >>> semver.compare("2.0.0") + -1 + >>> semver.compare("1.0.0") + 1 + >>> semver.compare("2.0.0") + 0 + >>> semver.compare(dict(major=2, minor=0, patch=0)) + 0 + """ + cls = type(self) + if isinstance(other, String.__args__): # type: ignore + other = cls.parse(other) + elif isinstance(other, dict): + other = cls(**other) + elif isinstance(other, (tuple, list)): + other = cls(*other) + elif not isinstance(other, cls): + raise TypeError( + f"Expected str, bytes, dict, tuple, list, or {cls.__name__} instance, " + f"but got {type(other)}" + ) + + v1 = self.to_tuple()[:3] + v2 = other.to_tuple()[:3] + x = cmp(v1, v2) + if x: + return x + + rc1, rc2 = self.prerelease, other.prerelease + rccmp = _nat_cmp(rc1, rc2) + + if not rccmp: + return 0 + if not rc1: + return 1 + elif not rc2: + return -1 + + return rccmp + + def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": + """ + Determines next version, preserving natural order. + + .. versionadded:: 2.10.0 + + This function is taking prereleases into account. + The "major", "minor", and "patch" raises the respective parts like + the ``bump_*`` functions. The real difference is using the + "preprelease" part. It gives you the next patch version of the + prerelease, for example: + + >>> str(semver.parse("0.1.4").next_version("prerelease")) + '0.1.5-rc.1' + + :param part: One of "major", "minor", "patch", or "prerelease" + :param prerelease_token: prefix string of prerelease, defaults to 'rc' + :return: new object with the appropriate part raised + """ + validparts = { + "major", + "minor", + "patch", + "prerelease", + # "build", # currently not used + } + if part not in validparts: + raise ValueError( + "Invalid part. Expected one of {validparts}, but got {part!r}".format( + validparts=validparts, part=part + ) + ) + version = self + if (version.prerelease or version.build) and ( + part == "patch" + or (part == "minor" and version.patch == 0) + or (part == "major" and version.minor == version.patch == 0) + ): + return version.replace(prerelease=None, build=None) + + if part in ("major", "minor", "patch"): + return getattr(version, "bump_" + part)() + + if not version.prerelease: + version = version.bump_patch() + return version.bump_prerelease(prerelease_token) + + @comparator + def __eq__(self, other: Comparable) -> bool: # type: ignore + return self.compare(other) == 0 + + @comparator + def __ne__(self, other: Comparable) -> bool: # type: ignore + return self.compare(other) != 0 + + @comparator + def __lt__(self, other: Comparable) -> bool: + return self.compare(other) < 0 + + @comparator + def __le__(self, other: Comparable) -> bool: + return self.compare(other) <= 0 + + @comparator + def __gt__(self, other: Comparable) -> bool: + return self.compare(other) > 0 + + @comparator + def __ge__(self, other: Comparable) -> bool: + return self.compare(other) >= 0 + + def __getitem__( + self, index: Union[int, slice] + ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]: + """ + self.__getitem__(index) <==> self[index] Implement getitem. If the part + requested is undefined, or a part of the range requested is undefined, + it will throw an index error. Negative indices are not supported. + + :param Union[int, slice] index: a positive integer indicating the + offset or a :func:`slice` object + :raises IndexError: if index is beyond the range or a part is None + :return: the requested part of the version at position index + >>> ver = semver.Version.parse("3.4.5") + >>> ver[0], ver[1], ver[2] + (3, 4, 5) + """ + if isinstance(index, int): + index = slice(index, index + 1) + index = cast(slice, index) + + if ( + isinstance(index, slice) + and (index.start is not None and index.start < 0) + or (index.stop is not None and index.stop < 0) + ): + raise IndexError("Version index cannot be negative") + + part = tuple( + filter(lambda p: p is not None, cast(Iterable, self.to_tuple()[index])) + ) + + if len(part) == 1: + return part[0] + elif not part: + raise IndexError("Version part undefined") + return part + + def __repr__(self) -> str: + s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) + return "%s(%s)" % (type(self).__name__, s) + + def __str__(self) -> str: + """str(self)""" + version = "%d.%d.%d" % (self.major, self.minor, self.patch) + if self.prerelease: + version += "-%s" % self.prerelease + if self.build: + version += "+%s" % self.build + return version + + def __hash__(self) -> int: + return hash(self.to_tuple()[:4]) + + def finalize_version(self) -> "Version": + """ + Remove any prerelease and build metadata from the version. + :return: a new instance with the finalized version string + >>> str(semver.Version.parse('1.2.3-rc.5').finalize_version()) + '1.2.3' + """ + cls = type(self) + return cls(self.major, self.minor, self.patch) + + def match(self, match_expr: str) -> bool: + """ + Compare self to match a match expression. + + :param match_expr: operator and version; valid operators are + < smaller than + > greater than + >= greator or equal than + <= smaller or equal than + == equal + != not equal + :return: True if the expression matches the version, otherwise False + >>> semver.Version.parse("2.0.0").match(">=1.0.0") + True + >>> semver.Version.parse("1.0.0").match(">1.0.0") + False + """ + prefix = match_expr[:2] + if prefix in (">=", "<=", "==", "!="): + match_version = match_expr[2:] + elif prefix and prefix[0] in (">", "<"): + prefix = prefix[0] + match_version = match_expr[1:] + else: + raise ValueError( + "match_expr parameter should be in format , " + "where is one of " + "['<', '>', '==', '<=', '>=', '!=']. " + "You provided: %r" % match_expr + ) + + possibilities_dict = { + ">": (1,), + "<": (-1,), + "==": (0,), + "!=": (-1, 1), + ">=": (0, 1), + "<=": (-1, 0), + } + + possibilities = possibilities_dict[prefix] + cmp_res = self.compare(match_version) + + return cmp_res in possibilities + + @classmethod + def parse(cls, version: String) -> "Version": + """ + Parse version string to a VersionInfo instance. + + .. versionchanged:: 2.11.0 + Changed method from static to classmethod to + allow subclasses. + :param version: version string + :return: a :class:`VersionInfo` instance + :raises ValueError: if version is invalid + >>> semver.Version.parse('3.4.5-pre.2+build.4') + VersionInfo(major=3, minor=4, patch=5, \ +prerelease='pre.2', build='build.4') + """ + version_str = ensure_str(version) + match = cls._REGEX.match(version_str) + if match is None: + raise ValueError(f"{version_str} is not valid SemVer string") + + matched_version_parts: Dict[str, Any] = match.groupdict() + + return cls(**matched_version_parts) + + def replace(self, **parts: Union[int, Optional[str]]) -> "Version": + """ + Replace one or more parts of a version and return a new + :class:`Version` object, but leave self untouched + + .. versionadded:: 2.9.0 + Added :func:`Version.replace` + + :param parts: the parts to be updated. Valid keys are: + ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` + :return: the new :class:`Version` object with the changed + parts + :raises TypeError: if ``parts`` contains invalid keys + """ + version = self.to_dict() + version.update(parts) + try: + return Version(**version) # type: ignore + except TypeError: + unknownkeys = set(parts) - set(self.to_dict()) + error = "replace() got %d unexpected keyword " "argument(s): %s" % ( + len(unknownkeys), + ", ".join(unknownkeys), + ) + raise TypeError(error) + + @classmethod + def isvalid(cls, version: str) -> bool: + """ + Check if the string is a valid semver version. + + .. versionadded:: 2.9.1 + + :param version: the version string to check + :return: True if the version string is a valid semver version, False + otherwise. + """ + try: + cls.parse(version) + return True + except ValueError: + return False + + +# Keep the VersionInfo name for compatibility +VersionInfo = Version diff --git a/tests/conftest.py b/tests/conftest.py index 2e935d0b..153edf0c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,8 +23,8 @@ def version(): Creates a version :return: a version type - :rtype: VersionInfo + :rtype: Version """ - return semver.VersionInfo( + return semver.Version( major=1, minor=2, patch=3, prerelease="alpha.1.2", build="build.11.e0f985a" ) diff --git a/tests/test_compare.py b/tests/test_compare.py index 41caa08d..1c99f450 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -1,6 +1,7 @@ import pytest -from semver import VersionInfo, compare +import semver +from semver import Version, compare @pytest.mark.parametrize( @@ -123,15 +124,15 @@ def test_should_get_more_rc1(): def test_should_compare_prerelease_with_numbers_and_letters(): - v1 = VersionInfo(major=1, minor=9, patch=1, prerelease="1unms", build=None) - v2 = VersionInfo(major=1, minor=9, patch=1, prerelease=None, build="1asd") + v1 = Version(major=1, minor=9, patch=1, prerelease="1unms", build=None) + v2 = Version(major=1, minor=9, patch=1, prerelease=None, build="1asd") assert v1 < v2 assert compare("1.9.1-1unms", "1.9.1+1") == -1 def test_should_compare_version_info_objects(): - v1 = VersionInfo(major=0, minor=10, patch=4) - v2 = VersionInfo(major=0, minor=10, patch=4, prerelease="beta.1", build=None) + v1 = Version(major=0, minor=10, patch=4) + v2 = Version(major=0, minor=10, patch=4, prerelease="beta.1", build=None) # use `not` to enforce using comparision operators assert v1 != v2 @@ -141,7 +142,7 @@ def test_should_compare_version_info_objects(): assert not (v1 <= v2) assert not (v1 == v2) - v3 = VersionInfo(major=0, minor=10, patch=4) + v3 = Version(major=0, minor=10, patch=4) assert not (v1 != v3) assert not (v1 > v3) @@ -150,7 +151,7 @@ def test_should_compare_version_info_objects(): assert v1 <= v3 assert v1 == v3 - v4 = VersionInfo(major=0, minor=10, patch=5) + v4 = Version(major=0, minor=10, patch=5) assert v1 != v4 assert not (v1 > v4) assert not (v1 >= v4) @@ -160,7 +161,7 @@ def test_should_compare_version_info_objects(): def test_should_compare_version_dictionaries(): - v1 = VersionInfo(major=0, minor=10, patch=4) + v1 = Version(major=0, minor=10, patch=4) v2 = dict(major=0, minor=10, patch=4, prerelease="beta.1", build=None) assert v1 != v2 @@ -199,8 +200,8 @@ def test_should_compare_version_dictionaries(): ), # fmt: on ) def test_should_compare_version_tuples(t): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v0 = Version(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") assert v0 < t assert v0 <= t @@ -228,8 +229,8 @@ def test_should_compare_version_tuples(t): ), # fmt: on ) def test_should_compare_version_list(lst): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v0 = Version(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") assert v0 < lst assert v0 <= lst @@ -257,8 +258,8 @@ def test_should_compare_version_list(lst): ), # fmt: on ) def test_should_compare_version_string(s): - v0 = VersionInfo(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v0 = Version(major=0, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") assert v0 < s assert v0 <= s @@ -277,7 +278,7 @@ def test_should_compare_version_string(s): @pytest.mark.parametrize("s", ("1", "1.0", "1.0.x")) def test_should_not_allow_to_compare_invalid_versionstring(s): - v = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") with pytest.raises(ValueError): v < s with pytest.raises(ValueError): @@ -285,19 +286,19 @@ def test_should_not_allow_to_compare_invalid_versionstring(s): def test_should_not_allow_to_compare_version_with_int(): - v1 = VersionInfo(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") + v1 = Version(major=3, minor=4, patch=5, prerelease="pre.2", build="build.4") with pytest.raises(TypeError): v1 > 1 with pytest.raises(TypeError): 1 > v1 with pytest.raises(TypeError): - v1.compare(1) + semver.compare(1) def test_should_compare_prerelease_and_build_with_numbers(): - assert VersionInfo(major=1, minor=9, patch=1, prerelease=1, build=1) < VersionInfo( + assert Version(major=1, minor=9, patch=1, prerelease=1, build=1) < Version( major=1, minor=9, patch=1, prerelease=2, build=1 ) - assert VersionInfo(1, 9, 1, 1, 1) < VersionInfo(1, 9, 1, 2, 1) - assert VersionInfo("2") < VersionInfo(10) - assert VersionInfo("2") < VersionInfo("10") + assert Version(1, 9, 1, 1, 1) < Version(1, 9, 1, 2, 1) + assert Version("2") < Version(10) + assert Version("2") < Version("10") diff --git a/tests/test_deprecated_functions.py b/tests/test_deprecated_functions.py index 8a04e3e9..0b5123cc 100644 --- a/tests/test_deprecated_functions.py +++ b/tests/test_deprecated_functions.py @@ -1,22 +1,31 @@ +from argparse import Namespace + import pytest from semver import ( - bump_build, + parse, + parse_version_info, + compare, + match, + max_ver, + min_ver, + format_version, bump_major, bump_minor, bump_patch, bump_prerelease, - compare, - deprecated, + bump_build, finalize_version, - format_version, - match, - max_ver, - min_ver, - parse, - parse_version_info, replace, + cmd_bump, + cmd_compare, + cmd_check, + cmd_nextver, + createparser, + process, + main, ) +from semver._deprecated import deprecated @pytest.mark.parametrize( @@ -36,6 +45,17 @@ (replace, ("1.2.3",), dict(major=2, patch=10)), (max_ver, ("1.2.3", "1.2.4"), {}), (min_ver, ("1.2.3", "1.2.4"), {}), + (cmd_bump, (Namespace(bump="major", version="1.2.3"),), {}), + (cmd_compare, (Namespace(version1="1.2.3", version2="2.1.3"),), {}), + (cmd_check, (Namespace(version="1.2.3"),), {}), + (cmd_nextver, (Namespace(version="1.2.3", part="major"),), {}), + (createparser, (), {}), + ( + process, + (Namespace(func=cmd_compare, version1="1.2.3", version2="2.1.3"),), + {}, + ), + (main, (["bump", "major", "1.2.3"],), {}), ], ) def test_should_raise_deprecation_warnings(func, args, kwargs): diff --git a/tests/test_format.py b/tests/test_format.py index b1c6ad5b..73ff3122 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,6 +1,6 @@ import pytest -from semver import VersionInfo, finalize_version, format_version +from semver import Version, finalize_version, format_version @pytest.mark.parametrize( @@ -28,7 +28,7 @@ def test_should_correctly_format_version(): def test_parse_method_for_version_info(): s_version = "1.2.3-alpha.1.2+build.11.e0f985a" - v = VersionInfo.parse(s_version) + v = Version.parse(s_version) assert str(v) == s_version @@ -36,28 +36,28 @@ def test_parse_method_for_version_info(): "version, expected", [ ( - VersionInfo(major=1, minor=2, patch=3, prerelease=None, build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease=None, build=None)", + Version(major=1, minor=2, patch=3, prerelease=None, build=None), + "Version(major=1, minor=2, patch=3, prerelease=None, build=None)", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build=None)", + Version(major=1, minor=2, patch=3, prerelease="r.1", build=None), + "Version(major=1, minor=2, patch=3, prerelease='r.1', build=None)", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="dev.1", build=None), - "VersionInfo(major=1, minor=2, patch=3, prerelease='dev.1', build=None)", + Version(major=1, minor=2, patch=3, prerelease="dev.1", build=None), + "Version(major=1, minor=2, patch=3, prerelease='dev.1', build=None)", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="dev.1", build="b.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='dev.1', build='b.1')", + Version(major=1, minor=2, patch=3, prerelease="dev.1", build="b.1"), + "Version(major=1, minor=2, patch=3, prerelease='dev.1', build='b.1')", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build="b.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build='b.1')", + Version(major=1, minor=2, patch=3, prerelease="r.1", build="b.1"), + "Version(major=1, minor=2, patch=3, prerelease='r.1', build='b.1')", ), ( - VersionInfo(major=1, minor=2, patch=3, prerelease="r.1", build="build.1"), - "VersionInfo(major=1, minor=2, patch=3, prerelease='r.1', build='build.1')", + Version(major=1, minor=2, patch=3, prerelease="r.1", build="build.1"), + "Version(major=1, minor=2, patch=3, prerelease='r.1', build='build.1')", ), ], ) diff --git a/tests/test_index.py b/tests/test_index.py index d54ea110..79e45025 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,6 +1,6 @@ import pytest -from semver import VersionInfo +from semver import Version @pytest.mark.parametrize( @@ -24,7 +24,7 @@ ], ) def test_version_info_should_be_accessed_with_index(version, index, expected): - version_info = VersionInfo.parse(version) + version_info = Version.parse(version) assert version_info[index] == expected @@ -54,7 +54,7 @@ def test_version_info_should_be_accessed_with_index(version, index, expected): def test_version_info_should_be_accessed_with_slice_object( version, slice_object, expected ): - version_info = VersionInfo.parse(version) + version_info = Version.parse(version) assert version_info[slice_object] == expected @@ -74,7 +74,7 @@ def test_version_info_should_be_accessed_with_slice_object( ], ) def test_version_info_should_throw_index_error(version, index): - version_info = VersionInfo.parse(version) + version_info = Version.parse(version) with pytest.raises(IndexError, match=r"Version part undefined"): version_info[index] @@ -90,6 +90,6 @@ def test_version_info_should_throw_index_error(version, index): ], ) def test_version_info_should_throw_index_error_when_negative_index(version, index): - version_info = VersionInfo.parse(version) + version_info = Version.parse(version) with pytest.raises(IndexError, match=r"Version index cannot be negative"): version_info[index] diff --git a/tests/test_parsing.py b/tests/test_parsing.py index c31cca18..25c55c74 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1,6 +1,6 @@ import pytest -from semver import VersionInfo, parse, parse_version_info +from semver import Version, parse, parse_version_info @pytest.mark.parametrize( @@ -58,7 +58,7 @@ def test_parse_version_info_str_hash(): v = parse_version_info(s_version) assert v.__str__() == s_version d = {} - d[v] = "" # to ensure that VersionInfo are hashable + d[v] = "" # to ensure that Version are hashable @pytest.mark.parametrize( @@ -115,12 +115,12 @@ def test_equal_versions_have_equal_hashes(): def test_parse_method_for_version_info(): s_version = "1.2.3-alpha.1.2+build.11.e0f985a" - v = VersionInfo.parse(s_version) + v = Version.parse(s_version) assert str(v) == s_version def test_next_version_with_invalid_parts(): - version = VersionInfo.parse("1.0.1") + version = Version.parse("1.0.1") with pytest.raises(ValueError): version.next_version("invalid") @@ -151,7 +151,7 @@ def test_next_version_with_invalid_parts(): ], ) def test_next_version_with_versioninfo(version, part, expected): - ver = VersionInfo.parse(version) + ver = Version.parse(version) next_version = ver.next_version(part) - assert isinstance(next_version, VersionInfo) + assert isinstance(next_version, Version) assert str(next_version) == expected diff --git a/tests/test_pysemver-cli.py b/tests/test_pysemver-cli.py index 1fbeef26..e783a0b4 100644 --- a/tests/test_pysemver-cli.py +++ b/tests/test_pysemver-cli.py @@ -1,9 +1,18 @@ from argparse import Namespace from contextlib import contextmanager +from unittest.mock import patch import pytest -from semver import cmd_bump, cmd_check, cmd_compare, cmd_nextver, createparser, main +from semver import ( + cmd_bump, + cmd_check, + cmd_compare, + cmd_nextver, + createparser, + main, + __main__, +) @contextmanager @@ -125,3 +134,11 @@ def test_should_process_check_iscalled_with_valid_version(capsys): assert not result captured = capsys.readouterr() assert not captured.out + + +@pytest.mark.parametrize("package_name", ["", "semver"]) +def test_main_file_should_call_cli_main(package_name): + with patch("semver.__main__.cli.main") as mocked_main: + with patch("semver.__main__.__package__", package_name): + __main__.main() + mocked_main.assert_called_once() diff --git a/tests/test_replace.py b/tests/test_replace.py index e8e417a7..f223eddb 100644 --- a/tests/test_replace.py +++ b/tests/test_replace.py @@ -1,6 +1,6 @@ import pytest -from semver import VersionInfo, replace +from semver import Version, replace @pytest.mark.parametrize( @@ -42,9 +42,9 @@ def test_replace_raises_TypeError_for_invalid_keyword_arg(): ], ) def test_should_return_versioninfo_with_replaced_parts(version, parts, expected): - assert VersionInfo.parse(version).replace(**parts) == VersionInfo.parse(expected) + assert Version.parse(version).replace(**parts) == Version.parse(expected) def test_replace_raises_ValueError_for_non_numeric_values(): with pytest.raises(ValueError): - VersionInfo.parse("1.2.3").replace(major="x") + Version.parse("1.2.3").replace(major="x") diff --git a/tests/test_semver.py b/tests/test_semver.py index 630ebbce..b15bfeaf 100644 --- a/tests/test_semver.py +++ b/tests/test_semver.py @@ -1,13 +1,13 @@ import pytest # noqa -from semver import VersionInfo +from semver import Version @pytest.mark.parametrize( "string,expected", [("rc", "rc"), ("rc.1", "rc.2"), ("2x", "3x")] ) def test_should_private_increment_string(string, expected): - assert VersionInfo._increment_string(string) == expected + assert Version._increment_string(string) == expected @pytest.mark.parametrize( @@ -21,7 +21,7 @@ def test_should_private_increment_string(string, expected): ) def test_should_not_allow_negative_numbers(ver): with pytest.raises(ValueError, match=".* is negative. .*"): - VersionInfo(**ver) + Version(**ver) def test_should_versioninfo_to_dict(version): @@ -47,31 +47,36 @@ def test_version_info_should_be_iterable(version): def test_should_be_able_to_use_strings_as_major_minor_patch(): - v = VersionInfo("1", "2", "3") + v = Version("1", "2", "3") assert isinstance(v.major, int) assert isinstance(v.minor, int) assert isinstance(v.patch, int) assert v.prerelease is None assert v.build is None - assert VersionInfo("1", "2", "3") == VersionInfo(1, 2, 3) + assert Version("1", "2", "3") == Version(1, 2, 3) def test_using_non_numeric_string_as_major_minor_patch_throws(): with pytest.raises(ValueError): - VersionInfo("a") + Version("a") with pytest.raises(ValueError): - VersionInfo(1, "a") + Version(1, "a") with pytest.raises(ValueError): - VersionInfo(1, 2, "a") + Version(1, 2, "a") def test_should_be_able_to_use_integers_as_prerelease_build(): - v = VersionInfo(1, 2, 3, 4, 5) + v = Version(1, 2, 3, 4, 5) assert isinstance(v.prerelease, str) assert isinstance(v.build, str) - assert VersionInfo(1, 2, 3, 4, 5) == VersionInfo(1, 2, 3, "4", "5") + assert Version(1, 2, 3, 4, 5) == Version(1, 2, 3, "4", "5") def test_should_versioninfo_isvalid(): - assert VersionInfo.isvalid("1.0.0") is True - assert VersionInfo.isvalid("foo") is False + assert Version.isvalid("1.0.0") is True + assert Version.isvalid("foo") is False + + +def test_versioninfo_compare_should_raise_when_passed_invalid_value(): + with pytest.raises(TypeError): + Version(1, 2, 3).compare(4) diff --git a/tests/test_subclass.py b/tests/test_subclass.py index afd10b4a..cbf9d271 100644 --- a/tests/test_subclass.py +++ b/tests/test_subclass.py @@ -1,8 +1,8 @@ -from semver import VersionInfo +from semver import Version def test_subclass_from_versioninfo(): - class SemVerWithVPrefix(VersionInfo): + class SemVerWithVPrefix(Version): @classmethod def parse(cls, version): if not version[0] in ("v", "V"): diff --git a/tests/test_typeerror-274.py b/tests/test_typeerror-274.py index a0375d0d..61480bcf 100644 --- a/tests/test_typeerror-274.py +++ b/tests/test_typeerror-274.py @@ -3,6 +3,7 @@ import pytest import semver +import semver.version PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 @@ -52,7 +53,7 @@ class TestEnsure: def test_ensure_binary_raise_type_error(self): with pytest.raises(TypeError): - semver.ensure_str(8) + semver.version.ensure_str(8) def test_errors_and_encoding(self): ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore") @@ -77,10 +78,10 @@ def test_ensure_binary_raise(self): ) def test_ensure_str(self): - converted_unicode = semver.ensure_str( + converted_unicode = semver.version.ensure_str( self.UNICODE_EMOJI, encoding="utf-8", errors="strict" ) - converted_binary = semver.ensure_str( + converted_binary = semver.version.ensure_str( self.BINARY_EMOJI, encoding="utf-8", errors="strict" ) diff --git a/tox.ini b/tox.ini index d253f1f6..851327cd 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = py{36,37,38,39,310} docs mypy +isolated_build = True [testenv] @@ -35,14 +36,14 @@ commands = flake8 {posargs:} description = Check code style basepython = python3 deps = mypy -commands = mypy {posargs:--ignore-missing-imports .} +commands = mypy {posargs:--ignore-missing-imports --check-untyped-defs src} [testenv:docstrings] description = Check for PEP257 compatible docstrings basepython = python3 deps = docformatter -commands = docformatter --check {posargs:--pre-summary-newline semver.py} +commands = docformatter --check {posargs:--pre-summary-newline -r src} [testenv:checks] From 2123413ef7f18099f587ddae02b387f489629614 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 31 Oct 2020 17:28:17 +0100 Subject: [PATCH 09/86] Support PEP-561 py.typed Acoording to the mentioned PEP: "Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing." Add package_data to setup.cfg to include this marker in dist and whl file. --- setup.cfg | 9 +++++++++ src/semver/py.typed | 0 2 files changed, 9 insertions(+) create mode 100644 src/semver/py.typed diff --git a/setup.cfg b/setup.cfg index 52b5d3e5..873be36d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,14 @@ +# +# Metadata for setup.py +# +# See https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html + [metadata] name = semver version = attr: semver.__about__.__version__ description = attr: semver.__about__.__description__ long_description = file: README.rst +long_description_content_type = text/x-rst author = attr: semver.__about__.__author__ author_email = attr: semver.__about__.__author_email__ maintainer = attr: semver.__about__.__maintainer__ @@ -41,6 +47,9 @@ console_scripts = [options.packages.find] where = src +[options.package_data] +semver = py.typed + [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ testpaths = tests docs diff --git a/src/semver/py.typed b/src/semver/py.typed new file mode 100644 index 00000000..e69de29b From d1cf906549ea17848f1d375b417a57629eed647e Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 1 Nov 2020 16:12:28 +0100 Subject: [PATCH 10/86] add changelog.d/304.trivial.rst --- changelog.d/304.trivial.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 changelog.d/304.trivial.rst diff --git a/changelog.d/304.trivial.rst b/changelog.d/304.trivial.rst new file mode 100644 index 00000000..fe0d012a --- /dev/null +++ b/changelog.d/304.trivial.rst @@ -0,0 +1,10 @@ +Support PEP-561 :file:`py.typed`. + +According to the mentioned PEP: + + "Package maintainers who wish to support type checking + of their code MUST add a marker file named :file:`py.typed` + to their package supporting typing." + +Add package_data to :file:`setup.cfg` to include this marker in dist +and whl file. From edfbbe991c04cabff888117edc616f54c8ffaf2a Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Fri, 30 Oct 2020 16:20:38 +0100 Subject: [PATCH 11/86] Distinguish between changlog for version 2 and 3 Split changelog entries into semver 3 (new) and semver 2 (old). --- CHANGELOG.rst | 317 ------------- docs/changelog-2.7.9-and-before.rst | 353 --------------- docs/changelog-semver2.rst | 670 ++++++++++++++++++++++++++++ docs/index.rst | 2 +- 4 files changed, 671 insertions(+), 671 deletions(-) delete mode 100644 docs/changelog-2.7.9-and-before.rst create mode 100644 docs/changelog-semver2.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e529404..00bc7813 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -102,323 +102,6 @@ Trivial/Internal Changes * :pr:`290`: Add supported Python versions to :command:`black`. - ----- - - -Version 2.13.0 -============== - -:Released: 2020-10-20 -:Maintainer: Tom Schraitle - - -Features --------- - -* :pr:`287`: Document how to create subclass from ``VersionInfo`` - - -Bug Fixes ---------- - -* :pr:`283`: Ensure equal versions have equal hashes. - Version equality means for semver, that ``major``, - ``minor``, ``patch``, and ``prerelease`` parts are - equal in both versions you compare. The ``build`` part - is ignored. - - -Additions ---------- - -n/a - - -Deprecations ------------- - -n/a - - ----- - - -Version 2.12.0 -============== - -:Released: 2020-10-19 -:Maintainer: Tom Schraitle - - -Bug Fixes ---------- - -* :gh:`291` (:pr:`292`): Disallow negative numbers of - ``major``, ``minor``, and ``patch`` for :class:`semver.VersionInfo` - - ----- - - -Version 2.11.0 -============== - -:Released: 2020-10-17 -:Maintainer: Tom Schraitle - - -Bug Fixes ---------- - -* :gh:`276` (:pr:`277`): ``VersionInfo.parse`` should be a class method - Also add authors and update changelog in :gh:`286` -* :gh:`274` (:pr:`275`): Py2 vs. Py3 incompatibility TypeError - - ----- - - -Version 2.10.2 -============== - -:Released: 2020-06-15 -:Maintainer: Tom Schraitle - -Features --------- - -:gh:`268`: Increase coverage - - -Bug Fixes ---------- - -* :gh:`260` (:pr:`261`): Fixed ``__getitem__`` returning None on wrong parts -* :pr:`263`: Doc: Add missing "install" subcommand for openSUSE - - -Deprecations ------------- - -* :gh:`160` (:pr:`264`): - * :func:`semver.max_ver` - * :func:`semver.min_ver` - - ----- - - -Version 2.10.1 -============== - -:Released: 2020-05-13 -:Maintainer: Tom Schraitle - - -Features --------- - -* :pr:`249`: Added release policy and version restriction in documentation to - help our users which would like to stay on the major 2 release. -* :pr:`250`: Simplified installation semver on openSUSE with ``obs://``. -* :pr:`256`: Made docstrings consistent - - - -Bug Fixes ---------- - -* :gh:`251` (:pr:`254`): Fixed return type of ``semver.VersionInfo.next_version`` - to always return a ``VersionInfo`` instance. - - ----- - - - -Version 2.10.0 -============== - -:Released: 2020-05-05 -:Maintainer: Tom Schraitle - -Features --------- - -* :pr:`138`: Added ``__getitem__`` magic method to ``semver.VersionInfo`` class. - Allows to access a version like ``version[1]``. -* :pr:`235`: Improved documentation and shift focus on ``semver.VersionInfo`` instead of advertising - the old and deprecated module-level functions. -* :pr:`230`: Add version information in some functions: - - * Use ``.. versionadded::`` RST directive in docstrings to - make it more visible when something was added - * Minor wording fix in docstrings (versions -> version strings) - - -Bug Fixes ---------- - -* :gh:`224` (:pr:`226`): In ``setup.py``, replaced in class ``clean``, - ``super(CleanCommand, self).run()`` with ``CleanCommand.run(self)`` -* :gh:`244` (:pr:`245`): Allow comparison with ``VersionInfo``, tuple/list, dict, and string. - - -Additions ---------- - -* :pr:`228`: Added better doctest integration - - -Deprecations ------------- -* :gh:`225` (:pr:`229`): Output a DeprecationWarning for the following functions: - - - ``semver.parse`` - - ``semver.parse_version_info`` - - ``semver.format_version`` - - ``semver.bump_{major,minor,patch,prerelease,build}`` - - ``semver.finalize_version`` - - ``semver.replace`` - - ``semver.VersionInfo._asdict`` (use the new, public available - function ``semver.VersionInfo.to_dict()``) - - ``semver.VersionInfo._astuple`` (use the new, public available - function ``semver.VersionInfo.to_tuple()``) - - These deprecated functions will be removed in semver 3. - - ----- - - -Version 2.9.1 -============= -:Released: 2020-02-16 -:Maintainer: Tom Schraitle - -Features --------- - -* :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) -* :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub -* :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation -* :gh:`191` (:pr:`194`): Created manpage for pysemver -* :gh:`196` (:pr:`197`): Added distribution specific installation instructions -* :gh:`201` (:pr:`202`): Reformatted source code with black -* :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid` - and extend :command:`pysemver` with :command:`check` subcommand -* :gh:`210` (:pr:`215`): Document how to deal with invalid versions -* :pr:`212`: Improve docstrings according to PEP257 - -Bug Fixes ---------- - -* :gh:`192` (:pr:`193`): Fixed "pysemver" and "pysemver bump" when called without arguments - - ----- - -Version 2.9.0 -============= -:Released: 2019-10-30 -:Maintainer: Sébastien Celles - -Features --------- - -* :gh:`59` (:pr:`164`): Implemented a command line interface -* :gh:`85` (:pr:`147`, :pr:`154`): Improved contribution section -* :gh:`104` (:pr:`125`): Added iterator to :func:`semver.VersionInfo` -* :gh:`112`, :gh:`113`: Added Python 3.7 support -* :pr:`120`: Improved test_immutable function with properties -* :pr:`125`: Created :file:`setup.cfg` for pytest and tox -* :gh:`126` (:pr:`127`): Added target for documentation in :file:`tox.ini` -* :gh:`142` (:pr:`143`): Improved usage section -* :gh:`144` (:pr:`156`): Added :func:`semver.replace` and :func:`semver.VersionInfo.replace` - functions -* :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` -* :pr:`157`: Introduce :file:`conftest.py` to improve doctests -* :pr:`165`: Improved code coverage -* :pr:`166`: Reworked :file:`.gitignore` file -* :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` - -Bug Fixes ---------- - -* :gh:`102`: Fixed comparison between VersionInfo and tuple -* :gh:`103`: Disallow comparison between VersionInfo and string (and int) -* :gh:`121` (:pr:`122`): Use python3 instead of python3.4 in :file:`tox.ini` -* :pr:`123`: Improved :func:`__repr__` and derive class name from :func:`type` -* :gh:`128` (:pr:`129`): Fixed wrong datatypes in docstring for :func:`semver.format_version` -* :gh:`135` (:pr:`140`): Converted prerelease and build to string -* :gh:`136` (:pr:`151`): Added testsuite to tarball -* :gh:`154` (:pr:`155`): Improved README description - -Removals --------- - -* :gh:`111` (:pr:`110`): Dropped Python 3.3 -* :gh:`148` (:pr:`149`): Removed and replaced ``python setup.py test`` - - ----- - -Version 2.8.2 -============= -:Released: 2019-05-19 -:Maintainer: Sébastien Celles - -Skipped, not released. - ----- - -Version 2.8.1 -============= -:Released: 2018-07-09 -:Maintainer: Sébastien Celles - -Features --------- - -* :gh:`40` (:pr:`88`): Added a static parse method to VersionInfo -* :gh:`77` (:pr:`47`): Converted multiple tests into pytest.mark.parametrize -* :gh:`87`, :gh:`94` (:pr:`93`): Removed named tuple inheritance. -* :gh:`89` (:pr:`90`): Added doctests. - -Bug Fixes ---------- - -* :gh:`98` (:pr:`99`): Set prerelease and build to None by default -* :gh:`96` (:pr:`97`): Made VersionInfo immutable - - ----- - -Version 2.8.0 -============= -:Released: 2018-05-16 -:Maintainer: Sébastien Celles - - -Changes -------- - -* :gh:`82` (:pr:`83`): Renamed :file:`test.py` to :file:`test_semver.py` so - py.test can autodiscover test file - -Additions ---------- - -* :gh:`79` (:pr:`81`, :pr:`84`): Defined and improve a release procedure file -* :gh:`72`, :gh:`73` (:pr:`75`): Implemented :func:`__str__` and :func:`__hash__` - -Removals --------- - -* :gh:`76` (:pr:`80`): Removed Python 2.6 compatibility - - .. Local variables: coding: utf-8 diff --git a/docs/changelog-2.7.9-and-before.rst b/docs/changelog-2.7.9-and-before.rst deleted file mode 100644 index f7acc1e1..00000000 --- a/docs/changelog-2.7.9-and-before.rst +++ /dev/null @@ -1,353 +0,0 @@ -################ -Older Change Log -################ - -This changelog contains older entries from -2.7.9 and before. - -Version 2.7.9 -============= - -:Released: 2017-09-23 -:Maintainer: Kostiantyn Rybnikov - - -Additions ---------- - -* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. - - ----- - -Version 2.7.8 -============= - -:Released: 2017-08-25 -:Maintainer: Kostiantyn Rybnikov - -* :gh:`62`: Support custom default names for pre and build - - ----- - -Version 2.7.7 -============= - -:Released: 2017-05-25 -:Maintainer: Kostiantyn Rybnikov - -* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects -* :pr:`56`: Added support for Python 3.6 - - ----- - -Version 2.7.2 -============= - -:Released: 2016-11-08 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Added :func:`semver.parse_version_info` to parse a version string to a - version info tuple. - -Bug Fixes ---------- - -* :gh:`37`: Removed trailing zeros from prelease doesn't allow to - parse 0 pre-release version - -* Refine parsing to conform more strictly to SemVer 2.0.0. - - SemVer 2.0.0 specification §9 forbids leading zero on identifiers in - the prerelease version. - - ----- - -Version 2.6.0 -============= - -:Released: 2016-06-08 -:Maintainer: Kostiantyn Rybnikov - -Removals --------- - -* Remove comparison of build component. - - SemVer 2.0.0 specification recommends that build component is - ignored in comparisons. - - ----- - -Version 2.5.0 -============= - -:Released: 2016-05-25 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Support matching 'not equal' with “!=”. - -Changes -------- - -* Made separate builds for tests on Travis CI. - - ----- - -Version 2.4.2 -============= - -:Released: 2016-05-16 -:Maintainer: Kostiantyn Rybnikov - -Changes -------- - -* Migrated README document to reStructuredText format. - -* Used Setuptools for distribution management. - -* Migrated test cases to Py.test. - -* Added configuration for Tox test runner. - - ----- - -Version 2.4.1 -============= - -:Released: 2016-03-04 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* :gh:`23`: Compared build component of a version. - - ----- - -Version 2.4.0 -============= - -:Released: 2016-02-12 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* :gh:`21`: Compared alphanumeric components correctly. - - ----- - -Version 2.3.1 -============= - -:Released: 2016-01-30 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Declared granted license name in distribution metadata. - - ----- - -Version 2.3.0 -============= - -:Released: 2016-01-29 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Added functions to increment prerelease and build components in a - version. - - ----- - -Version 2.2.1 -============= - -:Released: 2015-08-04 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Corrected comparison when any component includes zero. - - ----- - -Version 2.2.0 -============= - -:Released: 2015-06-21 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Add functions to determined minimum and maximum version. - -* Add code examples for recently-added functions. - - ----- - -Version 2.1.2 -============= - -:Released: 2015-05-23 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Restored current README document to distribution manifest. - - ----- - -Version 2.1.1 -============= - -:Released: 2015-05-23 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Removed absent document from distribution manifest. - - ----- - -Version 2.1.0 -============= - -:Released: 2015-05-22 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Documented installation instructions. - -* Documented project home page. - -* Added function to format a version string from components. - -* Added functions to increment specific components in a version. - -Changes -------- - -* Migrated README document to Markdown format. - -Bug Fixes ---------- - -* Corrected code examples in README document. - - ----- - -Version 2.0.2 -============= - -:Released: 2015-04-14 -:Maintainer: Konstantine Rybnikov - -Additions ---------- - -* Added configuration for Travis continuous integration. - -* Explicitly declared supported Python versions. - - ----- - -Version 2.0.1 -============= - -:Released: 2014-09-24 -:Maintainer: Konstantine Rybnikov - -Bug Fixes ---------- - -* :gh:`9`: Fixed comparison of equal version strings. - - ----- - -Version 2.0.0 -============= - -:Released: 2014-05-24 -:Maintainer: Konstantine Rybnikov - -Additions ---------- - -* Grant license in this code base under BSD 3-clause license terms. - -Changes -------- - -* Update parser to SemVer standard 2.0.0. - -* Ignore build component for comparison. - - ----- - -Version 0.0.2 -============= - -:Released: 2012-05-10 -:Maintainer: Konstantine Rybnikov - -Changes -------- - -* Use standard library Distutils for distribution management. - - ----- - -Version 0.0.1 -============= - -:Released: 2012-04-28 -:Maintainer: Konstantine Rybnikov - -* Initial release. - - -.. - Local variables: - coding: utf-8 - mode: text - mode: rst - End: - vim: fileencoding=utf-8 filetype=rst : diff --git a/docs/changelog-semver2.rst b/docs/changelog-semver2.rst new file mode 100644 index 00000000..dca94413 --- /dev/null +++ b/docs/changelog-semver2.rst @@ -0,0 +1,670 @@ +################## +Change Log semver2 +################## + +This changelog contains older entries for semver2. + +---- + + +Version 2.13.0 +============== + +:Released: 2020-10-20 +:Maintainer: Tom Schraitle + + +Features +-------- + +* :pr:`287`: Document how to create subclass from ``VersionInfo`` + + +Bug Fixes +--------- + +* :pr:`283`: Ensure equal versions have equal hashes. + Version equality means for semver, that ``major``, + ``minor``, ``patch``, and ``prerelease`` parts are + equal in both versions you compare. The ``build`` part + is ignored. + + +Additions +--------- + +n/a + + +Deprecations +------------ + +n/a + + +---- + + +Version 2.12.0 +============== + +:Released: 2020-10-19 +:Maintainer: Tom Schraitle + + +Bug Fixes +--------- + +* :gh:`291` (:pr:`292`): Disallow negative numbers of + ``major``, ``minor``, and ``patch`` for :class:`semver.VersionInfo` + + +---- + + +Version 2.11.0 +============== + +:Released: 2020-10-17 +:Maintainer: Tom Schraitle + + +Bug Fixes +--------- + +* :gh:`276` (:pr:`277`): ``VersionInfo.parse`` should be a class method + Also add authors and update changelog in :gh:`286` +* :gh:`274` (:pr:`275`): Py2 vs. Py3 incompatibility TypeError + + +---- + + +Version 2.10.2 +============== + +:Released: 2020-06-15 +:Maintainer: Tom Schraitle + +Features +-------- + +:gh:`268`: Increase coverage + + +Bug Fixes +--------- + +* :gh:`260` (:pr:`261`): Fixed ``__getitem__`` returning None on wrong parts +* :pr:`263`: Doc: Add missing "install" subcommand for openSUSE + + +Deprecations +------------ + +* :gh:`160` (:pr:`264`): + * :func:`semver.max_ver` + * :func:`semver.min_ver` + + +---- + + +Version 2.10.1 +============== + +:Released: 2020-05-13 +:Maintainer: Tom Schraitle + + +Features +-------- + +* :pr:`249`: Added release policy and version restriction in documentation to + help our users which would like to stay on the major 2 release. +* :pr:`250`: Simplified installation semver on openSUSE with ``obs://``. +* :pr:`256`: Made docstrings consistent + + + +Bug Fixes +--------- + +* :gh:`251` (:pr:`254`): Fixed return type of ``semver.VersionInfo.next_version`` + to always return a ``VersionInfo`` instance. + + +---- + + + +Version 2.10.0 +============== + +:Released: 2020-05-05 +:Maintainer: Tom Schraitle + +Features +-------- + +* :pr:`138`: Added ``__getitem__`` magic method to ``semver.VersionInfo`` class. + Allows to access a version like ``version[1]``. +* :pr:`235`: Improved documentation and shift focus on ``semver.VersionInfo`` instead of advertising + the old and deprecated module-level functions. +* :pr:`230`: Add version information in some functions: + + * Use ``.. versionadded::`` RST directive in docstrings to + make it more visible when something was added + * Minor wording fix in docstrings (versions -> version strings) + + +Bug Fixes +--------- + +* :gh:`224` (:pr:`226`): In ``setup.py``, replaced in class ``clean``, + ``super(CleanCommand, self).run()`` with ``CleanCommand.run(self)`` +* :gh:`244` (:pr:`245`): Allow comparison with ``VersionInfo``, tuple/list, dict, and string. + + +Additions +--------- + +* :pr:`228`: Added better doctest integration + + +Deprecations +------------ +* :gh:`225` (:pr:`229`): Output a DeprecationWarning for the following functions: + + - ``semver.parse`` + - ``semver.parse_version_info`` + - ``semver.format_version`` + - ``semver.bump_{major,minor,patch,prerelease,build}`` + - ``semver.finalize_version`` + - ``semver.replace`` + - ``semver.VersionInfo._asdict`` (use the new, public available + function ``semver.VersionInfo.to_dict()``) + - ``semver.VersionInfo._astuple`` (use the new, public available + function ``semver.VersionInfo.to_tuple()``) + + These deprecated functions will be removed in semver 3. + + +---- + + +Version 2.9.1 +============= +:Released: 2020-02-16 +:Maintainer: Tom Schraitle + +Features +-------- + +* :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) +* :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub +* :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation +* :gh:`191` (:pr:`194`): Created manpage for pysemver +* :gh:`196` (:pr:`197`): Added distribution specific installation instructions +* :gh:`201` (:pr:`202`): Reformatted source code with black +* :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid` + and extend :command:`pysemver` with :command:`check` subcommand +* :gh:`210` (:pr:`215`): Document how to deal with invalid versions +* :pr:`212`: Improve docstrings according to PEP257 + +Bug Fixes +--------- + +* :gh:`192` (:pr:`193`): Fixed "pysemver" and "pysemver bump" when called without arguments + + +---- + +Version 2.9.0 +============= +:Released: 2019-10-30 +:Maintainer: Sébastien Celles + +Features +-------- + +* :gh:`59` (:pr:`164`): Implemented a command line interface +* :gh:`85` (:pr:`147`, :pr:`154`): Improved contribution section +* :gh:`104` (:pr:`125`): Added iterator to :func:`semver.VersionInfo` +* :gh:`112`, :gh:`113`: Added Python 3.7 support +* :pr:`120`: Improved test_immutable function with properties +* :pr:`125`: Created :file:`setup.cfg` for pytest and tox +* :gh:`126` (:pr:`127`): Added target for documentation in :file:`tox.ini` +* :gh:`142` (:pr:`143`): Improved usage section +* :gh:`144` (:pr:`156`): Added :func:`semver.replace` and :func:`semver.VersionInfo.replace` + functions +* :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` +* :pr:`157`: Introduce :file:`conftest.py` to improve doctests +* :pr:`165`: Improved code coverage +* :pr:`166`: Reworked :file:`.gitignore` file +* :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` + +Bug Fixes +--------- + +* :gh:`102`: Fixed comparison between VersionInfo and tuple +* :gh:`103`: Disallow comparison between VersionInfo and string (and int) +* :gh:`121` (:pr:`122`): Use python3 instead of python3.4 in :file:`tox.ini` +* :pr:`123`: Improved :func:`__repr__` and derive class name from :func:`type` +* :gh:`128` (:pr:`129`): Fixed wrong datatypes in docstring for :func:`semver.format_version` +* :gh:`135` (:pr:`140`): Converted prerelease and build to string +* :gh:`136` (:pr:`151`): Added testsuite to tarball +* :gh:`154` (:pr:`155`): Improved README description + +Removals +-------- + +* :gh:`111` (:pr:`110`): Dropped Python 3.3 +* :gh:`148` (:pr:`149`): Removed and replaced ``python setup.py test`` + + +---- + +Version 2.8.2 +============= +:Released: 2019-05-19 +:Maintainer: Sébastien Celles + +Skipped, not released. + +---- + +Version 2.8.1 +============= +:Released: 2018-07-09 +:Maintainer: Sébastien Celles + +Features +-------- + +* :gh:`40` (:pr:`88`): Added a static parse method to VersionInfo +* :gh:`77` (:pr:`47`): Converted multiple tests into pytest.mark.parametrize +* :gh:`87`, :gh:`94` (:pr:`93`): Removed named tuple inheritance. +* :gh:`89` (:pr:`90`): Added doctests. + +Bug Fixes +--------- + +* :gh:`98` (:pr:`99`): Set prerelease and build to None by default +* :gh:`96` (:pr:`97`): Made VersionInfo immutable + + +---- + +Version 2.8.0 +============= +:Released: 2018-05-16 +:Maintainer: Sébastien Celles + + +Changes +------- + +* :gh:`82` (:pr:`83`): Renamed :file:`test.py` to :file:`test_semver.py` so + py.test can autodiscover test file + +Additions +--------- + +* :gh:`79` (:pr:`81`, :pr:`84`): Defined and improve a release procedure file +* :gh:`72`, :gh:`73` (:pr:`75`): Implemented :func:`__str__` and :func:`__hash__` + +Removals +-------- + +* :gh:`76` (:pr:`80`): Removed Python 2.6 compatibility + +---- + + +Version 2.7.9 +============= + +:Released: 2017-09-23 +:Maintainer: Kostiantyn Rybnikov + + +Additions +--------- + +* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. + + +---- + +Version 2.7.8 +============= + +:Released: 2017-08-25 +:Maintainer: Kostiantyn Rybnikov + +* :gh:`62`: Support custom default names for pre and build + + +---- + +Version 2.7.7 +============= + +:Released: 2017-05-25 +:Maintainer: Kostiantyn Rybnikov + +* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects +* :pr:`56`: Added support for Python 3.6 + + +---- + +Version 2.7.2 +============= + +:Released: 2016-11-08 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Added :func:`semver.parse_version_info` to parse a version string to a + version info tuple. + +Bug Fixes +--------- + +* :gh:`37`: Removed trailing zeros from prelease doesn't allow to + parse 0 pre-release version + +* Refine parsing to conform more strictly to SemVer 2.0.0. + + SemVer 2.0.0 specification §9 forbids leading zero on identifiers in + the prerelease version. + + +---- + +Version 2.6.0 +============= + +:Released: 2016-06-08 +:Maintainer: Kostiantyn Rybnikov + +Removals +-------- + +* Remove comparison of build component. + + SemVer 2.0.0 specification recommends that build component is + ignored in comparisons. + + +---- + +Version 2.5.0 +============= + +:Released: 2016-05-25 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Support matching 'not equal' with “!=”. + +Changes +------- + +* Made separate builds for tests on Travis CI. + + +---- + +Version 2.4.2 +============= + +:Released: 2016-05-16 +:Maintainer: Kostiantyn Rybnikov + +Changes +------- + +* Migrated README document to reStructuredText format. + +* Used Setuptools for distribution management. + +* Migrated test cases to Py.test. + +* Added configuration for Tox test runner. + + +---- + +Version 2.4.1 +============= + +:Released: 2016-03-04 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* :gh:`23`: Compared build component of a version. + + +---- + +Version 2.4.0 +============= + +:Released: 2016-02-12 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* :gh:`21`: Compared alphanumeric components correctly. + + +---- + +Version 2.3.1 +============= + +:Released: 2016-01-30 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Declared granted license name in distribution metadata. + + +---- + +Version 2.3.0 +============= + +:Released: 2016-01-29 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Added functions to increment prerelease and build components in a + version. + + +---- + +Version 2.2.1 +============= + +:Released: 2015-08-04 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Corrected comparison when any component includes zero. + + +---- + +Version 2.2.0 +============= + +:Released: 2015-06-21 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Add functions to determined minimum and maximum version. + +* Add code examples for recently-added functions. + + +---- + +Version 2.1.2 +============= + +:Released: 2015-05-23 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Restored current README document to distribution manifest. + + +---- + +Version 2.1.1 +============= + +:Released: 2015-05-23 +:Maintainer: Kostiantyn Rybnikov + +Bug Fixes +--------- + +* Removed absent document from distribution manifest. + + +---- + +Version 2.1.0 +============= + +:Released: 2015-05-22 +:Maintainer: Kostiantyn Rybnikov + +Additions +--------- + +* Documented installation instructions. + +* Documented project home page. + +* Added function to format a version string from components. + +* Added functions to increment specific components in a version. + +Changes +------- + +* Migrated README document to Markdown format. + +Bug Fixes +--------- + +* Corrected code examples in README document. + + +---- + +Version 2.0.2 +============= + +:Released: 2015-04-14 +:Maintainer: Konstantine Rybnikov + +Additions +--------- + +* Added configuration for Travis continuous integration. + +* Explicitly declared supported Python versions. + + +---- + +Version 2.0.1 +============= + +:Released: 2014-09-24 +:Maintainer: Konstantine Rybnikov + +Bug Fixes +--------- + +* :gh:`9`: Fixed comparison of equal version strings. + + +---- + +Version 2.0.0 +============= + +:Released: 2014-05-24 +:Maintainer: Konstantine Rybnikov + +Additions +--------- + +* Grant license in this code base under BSD 3-clause license terms. + +Changes +------- + +* Update parser to SemVer standard 2.0.0. + +* Ignore build component for comparison. + + +---- + +Version 0.0.2 +============= + +:Released: 2012-05-10 +:Maintainer: Konstantine Rybnikov + +Changes +------- + +* Use standard library Distutils for distribution management. + + +---- + +Version 0.0.1 +============= + +:Released: 2012-04-28 +:Maintainer: Konstantine Rybnikov + +* Initial release. + + +.. + Local variables: + coding: utf-8 + mode: text + mode: rst + End: + vim: fileencoding=utf-8 filetype=rst : diff --git a/docs/index.rst b/docs/index.rst index aefdc843..3152071e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,7 +28,7 @@ Semver |version| -- Semantic Versioning :hidden: changelog - changelog-2.7.9-and-before + changelog-semver2 Indices and Tables ================== From bb2cb90b4722c79a4b1c284bc1ec7192aa7bf411 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Fri, 30 Oct 2020 14:42:53 +0100 Subject: [PATCH 12/86] Document migration from semver2 to semver3 Create a separate file to describe how to migrate to new semver3. --- README.rst | 5 ----- docs/index.rst | 1 + docs/migratetosemver3.rst | 42 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 docs/migratetosemver3.rst diff --git a/README.rst b/README.rst index 03faebab..d4f29819 100644 --- a/README.rst +++ b/README.rst @@ -30,11 +30,6 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. |MAINT| replace:: ``maint/v2`` .. _MAINT: https://github.com/python-semver/python-semver/tree/maint/v2 -.. note:: - - The :class:`VersionInfo` has been renamed to :class:`Version`. An - alias has been created to preserve compatibility but the use of the old - name has been deprecated. The module follows the ``MAJOR.MINOR.PATCH`` style: diff --git a/docs/index.rst b/docs/index.rst index 3152071e..405d9e27 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Semver |version| -- Semantic Versioning install usage + migratetosemver3 development api diff --git a/docs/migratetosemver3.rst b/docs/migratetosemver3.rst new file mode 100644 index 00000000..d6d90954 --- /dev/null +++ b/docs/migratetosemver3.rst @@ -0,0 +1,42 @@ +.. _semver2-to-3: + +Migrating from semver2 to semver3 +================================= + +This chapter describes the visible differences for +users and how your code stays compatible for semver3. + +Although the development team tries to make the transition +to semver3 as smooth as possible, at some point change +is inevitable. + +For a more detailed overview of all the changes, refer +to our :ref:`changelog`. + + +Use Version instead of VersionInfo +---------------------------------- + +The :class:`VersionInfo` has been renamed to :class:`Version` +to have a more succinct name. +An alias has been created to preserve compatibility but +using old name has been deprecated. + +If you still need the old version, use this line: + +.. code-block:: python + + from semver.version import Version as VersionInfo + + + +Use semver.cli instead of semver +-------------------------------- + +All functions related to CLI parsing are moved to :mod:`semver.cli`. +If you are such functions, like :func:`semver.cmd_bump `, +import it from :mod:`semver.cli` in the future: + +.. code-block:: python + + from semver.cli import cmd_bump \ No newline at end of file From 7d00884712af389f8b58b1d78ef9206565db4005 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Fri, 30 Oct 2020 15:13:12 +0100 Subject: [PATCH 13/86] Improve API documentation * Use sphinx-apidoc to build API documentation * Amend tox.ini and call sphinx-apidoc * Remove old autosummary; it turned out it was difficult to configure and returned warning messages which were hard to fix. * Add semver version in footer * Add semver.__about__ to API doc * Unorthodox solution with sed * Remove obsolete config variables in docs/config.py * Add docs/_api/semver.__about__.rst as a placeholder * Add changelog.d/304.doc.rst (An old attempt was to use autosummary from https://stackoverflow.com/a/62613202; however, that didn't work quite well.) Co-authored-by: Tom Schraitle --- .gitignore | 3 ++- changelog.d/304.doc.rst | 5 ++++ docs/_api/semver.__about__.rst | 5 ++++ docs/_static/css/custom.css | 5 ++++ docs/_templates/layout.html | 45 ++++++++++++++++++++++++++++++++++ docs/api.rst | 10 +++++--- docs/coerce.py | 8 +++--- docs/conf.py | 37 ++++++++++++++++++++++++---- docs/readme.rst | 4 ++- docs/usage.rst | 2 +- src/semver/__about__.py | 21 ++++++++++++++++ src/semver/__init__.py | 6 +++++ src/semver/__main__.py | 9 +++++-- src/semver/_deprecated.py | 9 +++++++ src/semver/cli.py | 2 ++ src/semver/version.py | 9 +++++-- tests/conftest.py | 1 + tox.ini | 20 ++++++++++++--- 18 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 changelog.d/304.doc.rst create mode 100644 docs/_api/semver.__about__.rst diff --git a/.gitignore b/.gitignore index ffddda1c..d26b5515 100644 --- a/.gitignore +++ b/.gitignore @@ -259,7 +259,8 @@ fabric.properties # -------- - # Patch/Diff Files *.patch *.diff +docs/_api +!docs/_api/semver.__about__.rst diff --git a/changelog.d/304.doc.rst b/changelog.d/304.doc.rst new file mode 100644 index 00000000..3fa09bc6 --- /dev/null +++ b/changelog.d/304.doc.rst @@ -0,0 +1,5 @@ +Several improvements in documentation: + +* Reorganize API documentation. +* Add migration chapter from semver2 to semver3. +* Distinguish between changlog for version 2 and 3 diff --git a/docs/_api/semver.__about__.rst b/docs/_api/semver.__about__.rst new file mode 100644 index 00000000..22395ebd --- /dev/null +++ b/docs/_api/semver.__about__.rst @@ -0,0 +1,5 @@ +semver.\_\_about\_\_ module +=========================== + +.. automodule:: semver.__about__ + :members: diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 33ff51f1..002e6b2f 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -26,6 +26,11 @@ div.related.top nav { margin-top: 0.5em; } +.sphinxsidebarwrapper .caption { + margin-top: 1em; + margin-bottom: -0.75em; +} + .section h1 { font-weight: 700; } diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 6bae6eed..7a114d41 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -2,3 +2,48 @@ Import the theme's layout. #} {% extends "!layout.html" %} + +{%- block footer %} + + +{% if theme_github_banner|lower != 'false' %} + + Fork me on GitHub + +{% endif %} +{% if theme_analytics_id %} + +{% endif %} +{%- endblock %} \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst index 0003fefc..9d884601 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,8 +1,10 @@ .. _api: +### API -=== +### -.. automodule:: semver - :members: - :undoc-members: +.. toctree:: + :maxdepth: 4 + + _api/semver \ No newline at end of file diff --git a/docs/coerce.py b/docs/coerce.py index 9fe87276..7da20315 100644 --- a/docs/coerce.py +++ b/docs/coerce.py @@ -1,5 +1,7 @@ import re -import semver +from semver import Version +from typing import Optional, Tuple + BASEVERSION = re.compile( r"""[vV]? @@ -15,7 +17,7 @@ ) -def coerce(version): +def coerce(version: str) -> Tuple[Version, Optional[str]]: """ Convert an incomplete version string into a semver-compatible Version object @@ -37,6 +39,6 @@ def coerce(version): ver = { key: 0 if value is None else value for key, value in match.groupdict().items() } - ver = semver.Version(**ver) + ver = Version(**ver) rest = match.string[match.end() :] # noqa:E203 return ver, rest diff --git a/docs/conf.py b/docs/conf.py index 71738daa..f5e04b19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,12 +16,35 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # +import codecs import os +import re import sys sys.path.insert(0, os.path.abspath("../src/")) +# from semver import __version__ # noqa: E402 -from semver import __version__ # noqa: E402 + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + here = os.path.abspath(os.path.dirname(__file__)) + with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: + return f.read() + + +def find_version(*file_paths): + """ + Build a path from *file_paths* and search for a ``__version__`` + string inside. + """ + version_file = read(*file_paths) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") # -- General configuration ------------------------------------------------ @@ -35,12 +58,16 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.autosummary", "sphinx_autodoc_typehints", "sphinx.ext.intersphinx", - "sphinx.ext.napoleon", "sphinx.ext.extlinks", ] +autoclass_content = "class" +autodoc_default_options = {} + + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -62,16 +89,16 @@ # built documents. # # The short X.Y version. -version = __version__ +release = find_version("../src/semver/__about__.py") # The full version, including alpha/beta/rc tags. -release = version +version = release # .rsplit(u".", 1)[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/docs/readme.rst b/docs/readme.rst index 0aa732a1..034e9ee6 100644 --- a/docs/readme.rst +++ b/docs/readme.rst @@ -1,2 +1,4 @@ -.. include:: ../README.rst +If you are searching for how to stay compatible +with semver3, refer to :ref:`semver2-to-3`. +.. include:: ../README.rst diff --git a/docs/usage.rst b/docs/usage.rst index 31363bcf..94a115a8 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -89,7 +89,7 @@ A :class:`semver.Version` instance can be created in different ways: ValueError: 'major' is negative. A version can only be positive. As a minimum requirement, your dictionary needs at least the - be positive. + be positive. >>> semver.Version(-1) Traceback (most recent call last): diff --git a/src/semver/__about__.py b/src/semver/__about__.py index 5e7a3537..aa293425 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -1,8 +1,29 @@ +""" +Metadata about semver. + +Contains information about semver's version, the implemented version +of the semver specifictation, author, maintainers, and description. + +.. autodata:: __version__ +""" + +#: Semver version __version__ = "3.0.0-dev.2" + +#: Original semver author __author__ = "Kostiantyn Rybnikov" + +#: Author's email address __author_email__ = "k-bx@k-bx.com" + +#: Current maintainer __maintainer__ = ["Sebastien Celles", "Tom Schraitle"] + +#: Maintainer's email address __maintainer_email__ = "s.celles@gmail.com" + +#: Short description about semver __description__ = "Python helper for Semantic Versioning (http://semver.org)" +#: Supported semver specification SEMVER_SPEC_VERSION = "2.0.0" diff --git a/src/semver/__init__.py b/src/semver/__init__.py index 1a80f6c3..c6726f2e 100644 --- a/src/semver/__init__.py +++ b/src/semver/__init__.py @@ -1,3 +1,9 @@ +""" +semver package major release 3. + +A Python module for semantic versioning. Simplifies comparing versions. +""" + from ._deprecated import ( bump_build, bump_major, diff --git a/src/semver/__main__.py b/src/semver/__main__.py index 0e0648a9..7fde54d7 100644 --- a/src/semver/__main__.py +++ b/src/semver/__main__.py @@ -1,8 +1,13 @@ """ Module to support call with :file:`__main__.py`. Used to support the following -call: +call:: + + $ python3 -m semver ... + +This makes it also possible to "run" a wheel like in this command:: + + $ python3 semver-3*-py3-none-any.whl/semver -h -$ python3 -m semver ... """ import os.path import sys diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py index 45c52753..545a2438 100644 --- a/src/semver/_deprecated.py +++ b/src/semver/_deprecated.py @@ -1,3 +1,8 @@ +""" +Contains all deprecated functions. + +.. autofunction: deprecated +""" import inspect import warnings from functools import partial, wraps @@ -102,8 +107,10 @@ def parse_version_info(version): Use :func:`semver.VersionInfo.parse` instead. .. versionadded:: 2.7.2 Added :func:`semver.parse_version_info` + :param version: version string :return: a :class:`VersionInfo` instance + >>> version_info = semver.Version.parse("3.4.5-pre.2+build.4") >>> version_info.major 3 @@ -356,11 +363,13 @@ def replace(version, **parts): Use :func:`semver.Version.replace` instead. .. versionadded:: 2.9.0 Added :func:`replace` + :param version: the version string to replace :param parts: the parts to be updated. Valid keys are: ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` :return: the replaced version string :raises TypeError: if ``parts`` contains invalid keys + >>> import semver >>> semver.replace("1.2.3", major=2, patch=10) '2.2.10' diff --git a/src/semver/cli.py b/src/semver/cli.py index 1514979c..ca400373 100644 --- a/src/semver/cli.py +++ b/src/semver/cli.py @@ -1,3 +1,5 @@ +"""CLI parsing for :command:`pysemver` command.""" + import argparse import sys from typing import cast, List diff --git a/src/semver/version.py b/src/semver/version.py index 93a6d338..64353011 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -1,3 +1,5 @@ +"""Version handling.""" + import collections import re from functools import wraps @@ -414,7 +416,7 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": The "major", "minor", and "patch" raises the respective parts like the ``bump_*`` functions. The real difference is using the "preprelease" part. It gives you the next patch version of the - prerelease, for example: + prerelease, for example: >>> str(semver.parse("0.1.4").next_version("prerelease")) '0.1.5-rc.1' @@ -550,6 +552,7 @@ def match(self, match_expr: str) -> bool: == equal != not equal :return: True if the expression matches the version, otherwise False + >>> semver.Version.parse("2.0.0").match(">=1.0.0") True >>> semver.Version.parse("1.0.0").match(">1.0.0") @@ -591,9 +594,11 @@ def parse(cls, version: String) -> "Version": .. versionchanged:: 2.11.0 Changed method from static to classmethod to allow subclasses. + :param version: version string :return: a :class:`VersionInfo` instance :raises ValueError: if version is invalid + >>> semver.Version.parse('3.4.5-pre.2+build.4') VersionInfo(major=3, minor=4, patch=5, \ prerelease='pre.2', build='build.4') @@ -651,5 +656,5 @@ def isvalid(cls, version: str) -> bool: return False -# Keep the VersionInfo name for compatibility +#: Keep the VersionInfo name for compatibility VersionInfo = Version diff --git a/tests/conftest.py b/tests/conftest.py index 153edf0c..f7f927cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ @pytest.fixture(autouse=True) def add_semver(doctest_namespace): + doctest_namespace["Version"] = semver.Version doctest_namespace["semver"] = semver doctest_namespace["coerce"] = coerce doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix diff --git a/tox.ini b/tox.ini index 851327cd..73fbfc58 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ isolated_build = True [testenv] description = Run test suite for {basepython} -whitelist_externals = make +allowlist_externals = make commands = pytest {posargs:} deps = pytest @@ -17,7 +17,6 @@ deps = setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 - [testenv:black] description = Check for formatting changes basepython = python3 @@ -66,8 +65,21 @@ description = Build HTML documentation basepython = python3 deps = -r{toxinidir}/docs/requirements.txt skip_install = true -commands = make -C docs html - +allowlist_externals = + make + rm + echo + sed +commands_pre = + sphinx-apidoc --module-first -f --separate -H semver -o docs/_api src/semver src/semver/_types.py src/semver/_deprecated.py + # we don't need this, it just add another level and it's all in docs/api.rst + - rm docs/_api/modules.rst + # Include the semver.__about__ module before semver.cli: + sed -i '/semver\.cli/i\ \ \ semver.__about__' docs/_api/semver.rst +commands = + make -C docs html +commands_post = + echo "Find the HTML documentation at {toxinidir}/docs/_build/html/index.html" [testenv:man] description = Build the manpage From 8b22c6f55307632bf65b161ade40878c957e8292 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 1 Nov 2020 16:33:44 +0100 Subject: [PATCH 14/86] Create 3.0.0-dev.2 * Build changelog from news files --- CHANGELOG.rst | 73 +++++++++++++++++++++++++++++++++ changelog.d/169.deprecation.rst | 1 - changelog.d/169.feature.rst | 10 ----- changelog.d/169.trivial.rst | 8 ---- changelog.d/304.doc.rst | 5 --- changelog.d/304.trivial.rst | 10 ----- changelog.d/305.doc.rst | 1 - changelog.d/305.feature.rst | 1 - 8 files changed, 73 insertions(+), 36 deletions(-) delete mode 100644 changelog.d/169.deprecation.rst delete mode 100644 changelog.d/169.feature.rst delete mode 100644 changelog.d/169.trivial.rst delete mode 100644 changelog.d/304.doc.rst delete mode 100644 changelog.d/304.trivial.rst delete mode 100644 changelog.d/305.doc.rst delete mode 100644 changelog.d/305.feature.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 00bc7813..072ceb86 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,79 @@ in our repository. .. towncrier release notes start +Version 3.0.0-dev.2 +=================== + +:Released: 2020-11-01 +:Maintainer: Tom Schraitle + + +Deprecations +------------ + +* :gh:`169`: Deprecate CLI functions not imported from ``semver.cli``. + + + +Features +-------- + +* :gh:`169`: Create semver package and split code among different modules in the packages. + + * Remove :file:`semver.py` + * Create :file:`src/semver/__init__.py` + * Create :file:`src/semver/cli.py` for all CLI methods + * Create :file:`src/semver/_deprecated.py` for the ``deprecated`` decorator and other deprecated functions + * Create :file:`src/semver/__main__.py` to allow calling the CLI using :command:`python -m semver` + * Create :file:`src/semver/_types.py` to hold type aliases + * Create :file:`src/semver/version.py` to hold the :class:`Version` class (old name :class:`VersionInfo`) and its utility functions + * Create :file:`src/semver/__about__.py` for all the metadata variables + +* :gh:`305`: Rename :class:`VersionInfo` to :class:`Version` but keep an alias for compatibility + + + +Improved Documentation +---------------------- + +* :gh:`304`: Several improvements in documentation: + + * Reorganize API documentation. + * Add migration chapter from semver2 to semver3. + * Distinguish between changlog for version 2 and 3 + +* :gh:`305`: Add note about :class:`Version` rename. + + + +Trivial/Internal Changes +------------------------ + +* :gh:`169`: Adapted infrastructure code to the new project layout. + + * Replace :file:`setup.py` with :file:`setup.cfg` because the :file:`setup.cfg` is easier to use + * Adapt documentation code snippets where needed + * Adapt tests + * Changed the ``deprecated`` to hardcode the ``semver`` package name in the warning. + + Increase coverage to 100% for all non-deprecated APIs + +* :gh:`304`: Support PEP-561 :file:`py.typed`. + + According to the mentioned PEP: + + "Package maintainers who wish to support type checking + of their code MUST add a marker file named :file:`py.typed` + to their package supporting typing." + + Add package_data to :file:`setup.cfg` to include this marker in dist + and whl file. + + + +---- + + Version 3.0.0-dev.1 =================== diff --git a/changelog.d/169.deprecation.rst b/changelog.d/169.deprecation.rst deleted file mode 100644 index 9ce5ef6b..00000000 --- a/changelog.d/169.deprecation.rst +++ /dev/null @@ -1 +0,0 @@ -Deprecate CLI functions not imported from ``semver.cli``. \ No newline at end of file diff --git a/changelog.d/169.feature.rst b/changelog.d/169.feature.rst deleted file mode 100644 index 1b762676..00000000 --- a/changelog.d/169.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -Create semver package and split code among different modules in the packages. - -* Remove :file:`semver.py` -* Create :file:`src/semver/__init__.py` -* Create :file:`src/semver/cli.py` for all CLI methods -* Create :file:`src/semver/_deprecated.py` for the ``deprecated`` decorator and other deprecated functions -* Create :file:`src/semver/__main__.py` to allow calling the CLI using :command:`python -m semver` -* Create :file:`src/semver/_types.py` to hold type aliases -* Create :file:`src/semver/version.py` to hold the :class:`Version` class (old name :class:`VersionInfo`) and its utility functions -* Create :file:`src/semver/__about__.py` for all the metadata variables diff --git a/changelog.d/169.trivial.rst b/changelog.d/169.trivial.rst deleted file mode 100644 index 536e2b88..00000000 --- a/changelog.d/169.trivial.rst +++ /dev/null @@ -1,8 +0,0 @@ -Adapted infrastructure code to the new project layout. - -* Replace :file:`setup.py` with :file:`setup.cfg` because the :file:`setup.cfg` is easier to use -* Adapt documentation code snippets where needed -* Adapt tests -* Changed the ``deprecated`` to hardcode the ``semver`` package name in the warning. - -Increase coverage to 100% for all non-deprecated APIs \ No newline at end of file diff --git a/changelog.d/304.doc.rst b/changelog.d/304.doc.rst deleted file mode 100644 index 3fa09bc6..00000000 --- a/changelog.d/304.doc.rst +++ /dev/null @@ -1,5 +0,0 @@ -Several improvements in documentation: - -* Reorganize API documentation. -* Add migration chapter from semver2 to semver3. -* Distinguish between changlog for version 2 and 3 diff --git a/changelog.d/304.trivial.rst b/changelog.d/304.trivial.rst deleted file mode 100644 index fe0d012a..00000000 --- a/changelog.d/304.trivial.rst +++ /dev/null @@ -1,10 +0,0 @@ -Support PEP-561 :file:`py.typed`. - -According to the mentioned PEP: - - "Package maintainers who wish to support type checking - of their code MUST add a marker file named :file:`py.typed` - to their package supporting typing." - -Add package_data to :file:`setup.cfg` to include this marker in dist -and whl file. diff --git a/changelog.d/305.doc.rst b/changelog.d/305.doc.rst deleted file mode 100644 index 1ce69247..00000000 --- a/changelog.d/305.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add note about :class:`Version` rename. \ No newline at end of file diff --git a/changelog.d/305.feature.rst b/changelog.d/305.feature.rst deleted file mode 100644 index 98ef9665..00000000 --- a/changelog.d/305.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Rename :class:`VersionInfo` to :class:`Version` but keep an alias for compatibility \ No newline at end of file From 5d192dac3dc58f88ac654bb54b80f5e4b5472b79 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 1 Nov 2020 21:36:48 +0100 Subject: [PATCH 15/86] Add missing #213 type information in CHANGELOG --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 072ceb86..26f8ff79 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -146,6 +146,7 @@ Features * :gh:`276`: Document how to create a sublass from :class:`VersionInfo` class +* :gh:`213`: Add typing information Bug Fixes From 6a5ba3267fa9d0503581005e4b7d276377bc6cb0 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 1 Nov 2020 22:53:37 +0100 Subject: [PATCH 16/86] Use literal strings in setup.cfg Due to https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html#metadata we cannot use the "attr:" type in the keywords author, author_email, maintainer, description, and maintainer_email. If we do not change it to literal strings, twine (or PyPI) complains about the metadata with the following error: HTTPError: 400 Client Error: 'attr: semver.__about__.__author_email__' is an invalid value for Author-email. --- setup.cfg | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index 873be36d..681240ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,18 +1,18 @@ # # Metadata for setup.py # -# See https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html +# See https://setuptools.rtfd.io/en/latest/userguide/declarative_config.html [metadata] name = semver version = attr: semver.__about__.__version__ -description = attr: semver.__about__.__description__ +description = Python helper for Semantic Versioning (http://semver.org) long_description = file: README.rst long_description_content_type = text/x-rst -author = attr: semver.__about__.__author__ -author_email = attr: semver.__about__.__author_email__ -maintainer = attr: semver.__about__.__maintainer__ -maintainer_email = attr: semver.__about__.__maintainer_email__ +author = Kostiantyn Rybnikov +author_email = k-bx@k-bx.com +maintainer = Sebastien Celles, Tom Schraitle +maintainer_email = s.celles@gmail.com url = https://github.com/python-semver/python-semver download_url = https://github.com/python-semver/python-semver/downloads project_urls = From f0ed4703b8d36af9f301afbc4c2c44d03c5e11f2 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 1 Nov 2020 23:48:22 +0100 Subject: [PATCH 17/86] Add CONTRIBUTING.rst in root folder Make link in docs/development.rst --- CONTRIBUTING.rst | 241 ++++++++++++++++++++++++++++++++++++++++++ docs/development.rst | 242 +------------------------------------------ 2 files changed, 242 insertions(+), 241 deletions(-) create mode 100644 CONTRIBUTING.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..049fe1a3 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,241 @@ +.. _contributing: + +Contributing to semver +====================== + +The semver source code is managed using Git and is hosted on GitHub:: + + git clone git://github.com/python-semver/python-semver + + +Reporting Bugs and Feedback +--------------------------- + +If you think you have encountered a bug in semver or have an idea for a new +feature? Great! We like to hear from you. + +First, take the time to look into our GitHub `issues`_ tracker if +this already covered. If not, changes are good that we avoid double work. + + +Prerequisites +------------- + +Before you make changes to the code, we would highly appreciate if you +consider the following general requirements: + +* Make sure your code adheres to the `Semantic Versioning`_ specification. + +* Check if your feature is covered by the Semantic Versioning specification. + If not, ask on its GitHub project https://github.com/semver/semver. + + + +Modifying the Code +------------------ + +We recommend the following workflow: + +#. Fork our project on GitHub using this link: + https://github.com/python-semver/python-semver/fork + +#. Clone your forked Git repository (replace ``GITHUB_USER`` with your + account name on GitHub):: + + $ git clone git@github.com:GITHUB_USER/python-semver.git + +#. Create a new branch. You can name your branch whatever you like, but we + recommend to use some meaningful name. If your fix is based on a + existing GitHub issue, add also the number. Good examples would be: + + * ``feature/123-improve-foo`` when implementing a new feature in issue 123 + * ``bugfix/234-fix-security-bar`` a bugfixes for issue 234 + + Use this :command:`git` command:: + + $ git checkout -b feature/NAME_OF_YOUR_FEATURE + +#. Work on your branch and create a pull request: + + a. Write test cases and run the complete test suite, see :ref:`testsuite` + for details. + + b. Write a changelog entry, see section :ref:`changelog`. + + c. If you have implemented a new feature, document it into our + documentation to help our reader. See section :ref:`doc` for + further details. + + d. Create a `pull request`_. Describe in the pull request what you did + and why. If you have open questions, ask. + +#. Wait for feedback. If you receive any comments, address these. + +#. After your pull request got accepted, delete your branch. + + +.. _testsuite: + +Running the Test Suite +---------------------- + +We use `pytest`_ and `tox`_ to run tests against all supported Python +versions. All test dependencies are resolved automatically. + +You can decide to run the complete test suite or only part of it: + +* To run all tests, use:: + + $ tox + + If you have not all Python interpreters installed on your system + it will probably give you some errors (``InterpreterNotFound``). + To avoid such errors, use:: + + $ tox --skip-missing-interpreters + + It is possible to use only specific Python versions. Use the ``-e`` + option and one or more abbreviations (``py27`` for Python 2.7, ``py34`` for + Python 3.4 etc.):: + + $ tox -e py34 + $ tox -e py27,py34 + + To get a complete list, run:: + + $ tox -l + +* To run only a specific test, pytest requires the syntax + ``TEST_FILE::TEST_FUNCTION``. + + For example, the following line tests only the function + :func:`test_immutable_major` in the file :file:`test_semver.py` for all + Python versions:: + + $ tox test_semver.py::test_immutable_major + + By default, pytest prints a dot for each test function only. To + reveal the executed test function, use the following syntax:: + + $ tox -- -v + + You can combine the specific test function with the ``-e`` option, for + example, to limit the tests for Python 2.7 and 3.6 only:: + + $ tox -e py27,py36 test_semver.py::test_immutable_major + +Our code is checked against `flake8`_ for style guide issues. It is recommended +to run your tests in combination with :command:`flake8`, for example:: + + $ tox -e py27,py36,flake8 + + +.. _doc: + +Documenting semver +------------------ + +Documenting the features of semver is very important. It gives our developers +an overview what is possible with semver, how it "feels", and how it is +used efficiently. + +.. note:: + + To build the documentation locally use the following command:: + + $ tox -e docs + + The built documentation is available in :file:`dist/docs`. + + +A new feature is *not* complete if it isn't proberly documented. A good +documentation includes: + + * **A docstring** + + Each docstring contains a summary line, a linebreak, an optional + directive (see next item), the description of its arguments in + `Sphinx style`_, and an optional doctest. + The docstring is extracted and reused in the :ref:`api` section. + An appropriate docstring should look like this:: + + def compare(ver1, ver2): + """Compare two versions + + :param ver1: version string 1 + :param ver2: version string 2 + :return: The return value is negative if ver1 < ver2, + zero if ver1 == ver2 and strictly positive if ver1 > ver2 + :rtype: int + + >>> semver.compare("1.0.0", "2.0.0") + -1 + >>> semver.compare("2.0.0", "1.0.0") + 1 + >>> semver.compare("2.0.0", "2.0.0") + 0 + + """ + + * **An optional directive** + + If you introduce a new feature, change a function/method, or remove something, + it is a good practice to introduce Sphinx directives into the docstring. + This gives the reader an idea what version is affected by this change. + + The first required argument, ``VERSION``, defines the version when this change + was introduced. You can choose from: + + * ``.. versionadded:: VERSION`` + + Use this directive to describe a new feature. + + * ``.. versionchanged:: VERSION`` + + Use this directive to describe when something has changed, for example, + new parameters were added, changed side effects, different return values, etc. + + * ``.. deprecated:: VERSION`` + + Use this directive when a feature is deprecated. Describe what should + be used instead, if appropriate. + + + Add such a directive *after* the summary line, if needed. + An appropriate directive could look like this:: + + def to_tuple(self): + """ + Convert the VersionInfo object to a tuple. + + .. versionadded:: 2.10.0 + Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to + make this function available in the public API. + [...] + """ + + * **The documentation** + + A docstring is good, but in most cases it's too dense. Describe how + to use your new feature in our documentation. Here you can give your + readers more examples, describe it in a broader context or show + edge cases. + + +.. _changelog: + +Adding a Changelog Entry +------------------------ + +.. include:: ../changelog.d/README.rst + :start-after: -text-begin- + + +.. _flake8: https://flake8.readthedocs.io +.. _issues: https://github.com/python-semver/python-semver/issues +.. _pull request: https://github.com/python-semver/python-semver/pulls +.. _pytest: http://pytest.org/ +.. _Semantic Versioning: https://semver.org +.. _Sphinx style: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html +.. _tox: https://tox.readthedocs.org/ + diff --git a/docs/development.rst b/docs/development.rst index 049fe1a3..e582053e 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,241 +1 @@ -.. _contributing: - -Contributing to semver -====================== - -The semver source code is managed using Git and is hosted on GitHub:: - - git clone git://github.com/python-semver/python-semver - - -Reporting Bugs and Feedback ---------------------------- - -If you think you have encountered a bug in semver or have an idea for a new -feature? Great! We like to hear from you. - -First, take the time to look into our GitHub `issues`_ tracker if -this already covered. If not, changes are good that we avoid double work. - - -Prerequisites -------------- - -Before you make changes to the code, we would highly appreciate if you -consider the following general requirements: - -* Make sure your code adheres to the `Semantic Versioning`_ specification. - -* Check if your feature is covered by the Semantic Versioning specification. - If not, ask on its GitHub project https://github.com/semver/semver. - - - -Modifying the Code ------------------- - -We recommend the following workflow: - -#. Fork our project on GitHub using this link: - https://github.com/python-semver/python-semver/fork - -#. Clone your forked Git repository (replace ``GITHUB_USER`` with your - account name on GitHub):: - - $ git clone git@github.com:GITHUB_USER/python-semver.git - -#. Create a new branch. You can name your branch whatever you like, but we - recommend to use some meaningful name. If your fix is based on a - existing GitHub issue, add also the number. Good examples would be: - - * ``feature/123-improve-foo`` when implementing a new feature in issue 123 - * ``bugfix/234-fix-security-bar`` a bugfixes for issue 234 - - Use this :command:`git` command:: - - $ git checkout -b feature/NAME_OF_YOUR_FEATURE - -#. Work on your branch and create a pull request: - - a. Write test cases and run the complete test suite, see :ref:`testsuite` - for details. - - b. Write a changelog entry, see section :ref:`changelog`. - - c. If you have implemented a new feature, document it into our - documentation to help our reader. See section :ref:`doc` for - further details. - - d. Create a `pull request`_. Describe in the pull request what you did - and why. If you have open questions, ask. - -#. Wait for feedback. If you receive any comments, address these. - -#. After your pull request got accepted, delete your branch. - - -.. _testsuite: - -Running the Test Suite ----------------------- - -We use `pytest`_ and `tox`_ to run tests against all supported Python -versions. All test dependencies are resolved automatically. - -You can decide to run the complete test suite or only part of it: - -* To run all tests, use:: - - $ tox - - If you have not all Python interpreters installed on your system - it will probably give you some errors (``InterpreterNotFound``). - To avoid such errors, use:: - - $ tox --skip-missing-interpreters - - It is possible to use only specific Python versions. Use the ``-e`` - option and one or more abbreviations (``py27`` for Python 2.7, ``py34`` for - Python 3.4 etc.):: - - $ tox -e py34 - $ tox -e py27,py34 - - To get a complete list, run:: - - $ tox -l - -* To run only a specific test, pytest requires the syntax - ``TEST_FILE::TEST_FUNCTION``. - - For example, the following line tests only the function - :func:`test_immutable_major` in the file :file:`test_semver.py` for all - Python versions:: - - $ tox test_semver.py::test_immutable_major - - By default, pytest prints a dot for each test function only. To - reveal the executed test function, use the following syntax:: - - $ tox -- -v - - You can combine the specific test function with the ``-e`` option, for - example, to limit the tests for Python 2.7 and 3.6 only:: - - $ tox -e py27,py36 test_semver.py::test_immutable_major - -Our code is checked against `flake8`_ for style guide issues. It is recommended -to run your tests in combination with :command:`flake8`, for example:: - - $ tox -e py27,py36,flake8 - - -.. _doc: - -Documenting semver ------------------- - -Documenting the features of semver is very important. It gives our developers -an overview what is possible with semver, how it "feels", and how it is -used efficiently. - -.. note:: - - To build the documentation locally use the following command:: - - $ tox -e docs - - The built documentation is available in :file:`dist/docs`. - - -A new feature is *not* complete if it isn't proberly documented. A good -documentation includes: - - * **A docstring** - - Each docstring contains a summary line, a linebreak, an optional - directive (see next item), the description of its arguments in - `Sphinx style`_, and an optional doctest. - The docstring is extracted and reused in the :ref:`api` section. - An appropriate docstring should look like this:: - - def compare(ver1, ver2): - """Compare two versions - - :param ver1: version string 1 - :param ver2: version string 2 - :return: The return value is negative if ver1 < ver2, - zero if ver1 == ver2 and strictly positive if ver1 > ver2 - :rtype: int - - >>> semver.compare("1.0.0", "2.0.0") - -1 - >>> semver.compare("2.0.0", "1.0.0") - 1 - >>> semver.compare("2.0.0", "2.0.0") - 0 - - """ - - * **An optional directive** - - If you introduce a new feature, change a function/method, or remove something, - it is a good practice to introduce Sphinx directives into the docstring. - This gives the reader an idea what version is affected by this change. - - The first required argument, ``VERSION``, defines the version when this change - was introduced. You can choose from: - - * ``.. versionadded:: VERSION`` - - Use this directive to describe a new feature. - - * ``.. versionchanged:: VERSION`` - - Use this directive to describe when something has changed, for example, - new parameters were added, changed side effects, different return values, etc. - - * ``.. deprecated:: VERSION`` - - Use this directive when a feature is deprecated. Describe what should - be used instead, if appropriate. - - - Add such a directive *after* the summary line, if needed. - An appropriate directive could look like this:: - - def to_tuple(self): - """ - Convert the VersionInfo object to a tuple. - - .. versionadded:: 2.10.0 - Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to - make this function available in the public API. - [...] - """ - - * **The documentation** - - A docstring is good, but in most cases it's too dense. Describe how - to use your new feature in our documentation. Here you can give your - readers more examples, describe it in a broader context or show - edge cases. - - -.. _changelog: - -Adding a Changelog Entry ------------------------- - -.. include:: ../changelog.d/README.rst - :start-after: -text-begin- - - -.. _flake8: https://flake8.readthedocs.io -.. _issues: https://github.com/python-semver/python-semver/issues -.. _pull request: https://github.com/python-semver/python-semver/pulls -.. _pytest: http://pytest.org/ -.. _Semantic Versioning: https://semver.org -.. _Sphinx style: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html -.. _tox: https://tox.readthedocs.org/ - +.. include:: ../CONTRIBUTING.rst From 42bf3e27e683d4235dc28ede615cb9b96c9088aa Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 2 Nov 2020 00:09:59 +0100 Subject: [PATCH 18/86] Correct CONTRIBUTING text * Remove old occurances of Python 2.7 * Correct wrong single test for tox * Use better example * Use references to black, flake8, mypy, and docformatter --- CONTRIBUTING.rst | 81 +++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 049fe1a3..ac40563b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -94,40 +94,42 @@ You can decide to run the complete test suite or only part of it: $ tox --skip-missing-interpreters - It is possible to use only specific Python versions. Use the ``-e`` - option and one or more abbreviations (``py27`` for Python 2.7, ``py34`` for - Python 3.4 etc.):: + It is possible to use one or more specific Python versions. Use the ``-e`` + option and one or more abbreviations (``py36`` for Python 3.6, ``py37`` for + Python 3.7 etc.):: - $ tox -e py34 - $ tox -e py27,py34 + $ tox -e py36 + $ tox -e py36,py37 - To get a complete list, run:: + To get a complete list and a short description, run:: - $ tox -l + $ tox -av * To run only a specific test, pytest requires the syntax ``TEST_FILE::TEST_FUNCTION``. For example, the following line tests only the function - :func:`test_immutable_major` in the file :file:`test_semver.py` for all + :func:`test_immutable_major` in the file :file:`test_bump.py` for all Python versions:: - $ tox test_semver.py::test_immutable_major + $ tox -e py36 -- tests/test_bump.py::test_should_bump_major - By default, pytest prints a dot for each test function only. To + By default, pytest prints only a dot for each test function. To reveal the executed test function, use the following syntax:: $ tox -- -v You can combine the specific test function with the ``-e`` option, for - example, to limit the tests for Python 2.7 and 3.6 only:: + example, to limit the tests for Python 3.6 and 3.7 only:: - $ tox -e py27,py36 test_semver.py::test_immutable_major + $ tox -e py36,py37 -- tests/test_bump.py::test_should_bump_major -Our code is checked against `flake8`_ for style guide issues. It is recommended -to run your tests in combination with :command:`flake8`, for example:: +Our code is checked against formatting, style, type, and docstring issues +(`black`_, `flake8`_, `mypy`_, and `docformatter`_). +It is recommended to run your tests in combination with :command:`checks`, +for example:: - $ tox -e py27,py36,flake8 + $ tox -e checks,py36,py37 .. _doc: @@ -145,7 +147,7 @@ used efficiently. $ tox -e docs - The built documentation is available in :file:`dist/docs`. + The built documentation is available in :file:`docs/_build/html`. A new feature is *not* complete if it isn't proberly documented. A good @@ -159,22 +161,18 @@ documentation includes: The docstring is extracted and reused in the :ref:`api` section. An appropriate docstring should look like this:: - def compare(ver1, ver2): - """Compare two versions + def to_tuple(self) -> VersionTuple: + """ + Convert the Version object to a tuple. - :param ver1: version string 1 - :param ver2: version string 2 - :return: The return value is negative if ver1 < ver2, - zero if ver1 == ver2 and strictly positive if ver1 > ver2 - :rtype: int + .. versionadded:: 2.10.0 + Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to + make this function available in the public API. - >>> semver.compare("1.0.0", "2.0.0") - -1 - >>> semver.compare("2.0.0", "1.0.0") - 1 - >>> semver.compare("2.0.0", "2.0.0") - 0 + :return: a tuple with all the parts + >>> semver.Version(5, 3, 1).to_tuple() + (5, 3, 1, None, None) """ * **An optional directive** @@ -201,22 +199,12 @@ documentation includes: be used instead, if appropriate. - Add such a directive *after* the summary line, if needed. - An appropriate directive could look like this:: - - def to_tuple(self): - """ - Convert the VersionInfo object to a tuple. - - .. versionadded:: 2.10.0 - Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to - make this function available in the public API. - [...] - """ + Add such a directive *after* the summary line, as shown above. * **The documentation** - A docstring is good, but in most cases it's too dense. Describe how + A docstring is good, but in most cases it's too dense. API documentation + cannot replace a good user documentation. Describe how to use your new feature in our documentation. Here you can give your readers more examples, describe it in a broader context or show edge cases. @@ -231,11 +219,14 @@ Adding a Changelog Entry :start-after: -text-begin- -.. _flake8: https://flake8.readthedocs.io +.. _black: https://black.rtfd.io +.. _docformatter: https://pypi.org/project/docformatter/ +.. _flake8: https://flake8.rtfd.io +.. _mypy: http://mypy-lang.org/ .. _issues: https://github.com/python-semver/python-semver/issues .. _pull request: https://github.com/python-semver/python-semver/pulls .. _pytest: http://pytest.org/ .. _Semantic Versioning: https://semver.org -.. _Sphinx style: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html -.. _tox: https://tox.readthedocs.org/ +.. _Sphinx style: https://sphinx-rtd-tutorial.rtfd.io/en/latest/docstrings.html +.. _tox: https://tox.rtfd.org/ From dcdcd2ab744776f830ead0073b28f885a2b5e0e0 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 3 Nov 2020 17:24:31 +0100 Subject: [PATCH 19/86] Fix #310: Correct API documentation * Remove all automatic generation and use a more "semi-manual" approach (gives more control) * Improve docstrings in semver module * Remove docstrings in some dunder methods; Sphinx and autodoc uses the docstring from the parent class * Remove sphinx-apidoc command from :file:`tox.ini` --- changelog.d/310.bugfix.rst | 3 ++ docs/_api/semver.__about__.rst | 5 --- docs/_static/css/custom.css | 4 +++ docs/api.rst | 65 ++++++++++++++++++++++++++++++---- docs/conf.py | 10 +++--- src/semver/__about__.py | 8 +++++ src/semver/_types.py | 2 ++ src/semver/cli.py | 12 ++++++- src/semver/version.py | 53 +++++++++++++-------------- tests/conftest.py | 2 +- tox.ini | 8 ----- 11 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 changelog.d/310.bugfix.rst delete mode 100644 docs/_api/semver.__about__.rst diff --git a/changelog.d/310.bugfix.rst b/changelog.d/310.bugfix.rst new file mode 100644 index 00000000..6b042982 --- /dev/null +++ b/changelog.d/310.bugfix.rst @@ -0,0 +1,3 @@ +Rework API documentation. +Follow a more "semi-manual" attempt and add auto directives +into :file:`docs/api.rst`. \ No newline at end of file diff --git a/docs/_api/semver.__about__.rst b/docs/_api/semver.__about__.rst deleted file mode 100644 index 22395ebd..00000000 --- a/docs/_api/semver.__about__.rst +++ /dev/null @@ -1,5 +0,0 @@ -semver.\_\_about\_\_ module -=========================== - -.. automodule:: semver.__about__ - :members: diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 002e6b2f..81a5906f 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -35,6 +35,10 @@ div.related.top nav { font-weight: 700; } +.py.class { + margin-top: 1.5em; +} + .py.method { padding-top: 0.25em; padding-bottom: 1.25em; diff --git a/docs/api.rst b/docs/api.rst index 9d884601..d35c48fe 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,10 +1,63 @@ .. _api: -### -API -### +API Reference +============= -.. toctree:: - :maxdepth: 4 +.. currentmodule:: semver - _api/semver \ No newline at end of file + +Metadata :mod:`semver.__about__` +-------------------------------- + +.. automodule:: semver.__about__ + + +Deprecated Functions in :mod:`semver._deprecated` +------------------------------------------------- + +.. automodule:: semver._deprecated + +.. autofunction:: semver._deprecated.deprecated + + +CLI Parsing :mod:`semver.cli` +----------------------------- + +.. automodule:: semver.cli + +.. autofunction:: semver.cli.cmd_bump + +.. autofunction:: semver.cli.cmd_check + +.. autofunction:: semver.cli.cmd_compare + +.. autofunction:: semver.cli.createparser + +.. autofunction:: semver.cli.main + +.. autofunction:: semver.cli.process + + +Entry point :mod:`semver.__main__` +---------------------------------- + +.. automodule:: semver.__main__ + + + +Version Handling :mod:`semver.version` +-------------------------------------- + +.. automodule:: semver.version + +.. autofunction:: semver.version.cmp + +.. autofunction:: semver.version.ensure_str + +.. autofunction:: semver.version.comparator + +.. autoclass:: semver.version.VersionInfo + +.. autoclass:: semver.version.Version + :members: + :special-members: __iter__, __eq__, __ne__, __lt__, __le__, __gt__, __ge__, __getitem__, __hash__, __repr__, __str__ diff --git a/docs/conf.py b/docs/conf.py index f5e04b19..52a46704 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,8 @@ import re import sys -sys.path.insert(0, os.path.abspath("../src/")) +SRC_DIR = os.path.abspath("../src/") +sys.path.insert(0, SRC_DIR) # from semver import __version__ # noqa: E402 @@ -58,15 +59,16 @@ def find_version(*file_paths): # ones. extensions = [ "sphinx.ext.autodoc", - "sphinx.ext.autosummary", "sphinx_autodoc_typehints", "sphinx.ext.intersphinx", "sphinx.ext.extlinks", ] +# Autodoc configuration autoclass_content = "class" -autodoc_default_options = {} - +autodoc_typehints = "signature" +autodoc_member_order = "alphabetical" +add_function_parentheses = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/src/semver/__about__.py b/src/semver/__about__.py index aa293425..1f5fcae5 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -4,7 +4,15 @@ Contains information about semver's version, the implemented version of the semver specifictation, author, maintainers, and description. +.. autodata:: __author__ + +.. autodata:: __description__ + +.. autodata:: __maintainer__ + .. autodata:: __version__ + +.. autodata:: SEMVER_SPEC_VERSION """ #: Semver version diff --git a/src/semver/_types.py b/src/semver/_types.py index 823c7349..4f004a29 100644 --- a/src/semver/_types.py +++ b/src/semver/_types.py @@ -1,3 +1,5 @@ +"""Typing for semver.""" + from typing import Union, Optional, Tuple, Dict, Iterable, Callable, TypeVar VersionPart = Union[int, Optional[str]] diff --git a/src/semver/cli.py b/src/semver/cli.py index ca400373..65ca5187 100644 --- a/src/semver/cli.py +++ b/src/semver/cli.py @@ -1,4 +1,14 @@ -"""CLI parsing for :command:`pysemver` command.""" +""" +CLI parsing for :command:`pysemver` command. + +Each command in :command:`pysemver` is mapped to a ``cmd_`` function. +The :func:`main ` function calls +:func:`createparser ` and +:func:`process ` to parse and process +all the commandline options. + +The result of each command is printed on stdout. +""" import argparse import sys diff --git a/src/semver/version.py b/src/semver/version.py index 64353011..40132526 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -43,18 +43,14 @@ def ensure_str(s: String, encoding="utf-8", errors="strict") -> str: * `bytes` -> decoded to `str` :param s: the string to convert - :type s: str | bytes :param encoding: the encoding to apply, defaults to "utf-8" - :type encoding: str :param errors: set a different error handling scheme, defaults to "strict". Other possible values are `ignore`, `replace`, and `xmlcharrefreplace` as well as any other name registered with :func:`codecs.register_error`. - :type errors: str :raises TypeError: if ``s`` is not str or bytes type :return: the converted string - :rtype: str """ if isinstance(s, bytes): s = s.decode(encoding, errors) @@ -218,7 +214,7 @@ def build(self, value): def to_tuple(self) -> VersionTuple: """ - Convert the VersionInfo object to a tuple. + Convert the Version object to a tuple. .. versionadded:: 2.10.0 Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to @@ -233,7 +229,7 @@ def to_tuple(self) -> VersionTuple: def to_dict(self) -> VersionDict: """ - Convert the VersionInfo object to an OrderedDict. + Convert the Version object to an OrderedDict. .. versionadded:: 2.10.0 Renamed ``VersionInfo._asdict`` to ``VersionInfo.to_dict`` to @@ -257,7 +253,7 @@ def to_dict(self) -> VersionDict: ) def __iter__(self) -> VersionIterator: - """Implement iter(self).""" + """Return iter(self).""" yield from self.to_tuple() @staticmethod @@ -300,7 +296,6 @@ def bump_minor(self) -> "Version": :return: new object with the raised minor part - >>> ver = semver.parse("3.4.5") >>> ver.bump_minor() Version(major=3, minor=5, patch=0, prerelease=None, build=None) @@ -313,8 +308,7 @@ def bump_patch(self) -> "Version": Raise the patch part of the version, return a new object but leave self untouched. - :return: new object with the raised patch part - + :return: new object with the raised patch part >>> ver = semver.parse("3.4.5") >>> ver.bump_patch() @@ -328,7 +322,7 @@ def bump_prerelease(self, token: str = "rc") -> "Version": Raise the prerelease part of the version, return a new object but leave self untouched. - :param token: defaults to 'rc' + :param token: defaults to ``rc`` :return: new object with the raised prerelease part >>> ver = semver.parse("3.4.5") @@ -345,7 +339,7 @@ def bump_build(self, token: str = "build") -> "Version": Raise the build part of the version, return a new object but leave self untouched. - :param token: defaults to 'build' + :param token: defaults to ``build`` :return: new object with the raised build part >>> ver = semver.parse("3.4.5-rc.1+build.9") @@ -365,7 +359,6 @@ def compare(self, other: Comparable) -> int: :return: The return value is negative if ver1 < ver2, zero if ver1 == ver2 and strictly positive if ver1 > ver2 - >>> semver.compare("2.0.0") -1 >>> semver.compare("1.0.0") @@ -481,14 +474,17 @@ def __getitem__( self, index: Union[int, slice] ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]: """ - self.__getitem__(index) <==> self[index] Implement getitem. If the part - requested is undefined, or a part of the range requested is undefined, - it will throw an index error. Negative indices are not supported. + self.__getitem__(index) <==> self[index] Implement getitem. + + If the part requested is undefined, or a part of the range requested + is undefined, it will throw an index error. + Negative indices are not supported. :param Union[int, slice] index: a positive integer indicating the offset or a :func:`slice` object :raises IndexError: if index is beyond the range or a part is None :return: the requested part of the version at position index + >>> ver = semver.Version.parse("3.4.5") >>> ver[0], ver[1], ver[2] (3, 4, 5) @@ -519,7 +515,6 @@ def __repr__(self) -> str: return "%s(%s)" % (type(self).__name__, s) def __str__(self) -> str: - """str(self)""" version = "%d.%d.%d" % (self.major, self.minor, self.patch) if self.prerelease: version += "-%s" % self.prerelease @@ -533,7 +528,9 @@ def __hash__(self) -> int: def finalize_version(self) -> "Version": """ Remove any prerelease and build metadata from the version. + :return: a new instance with the finalized version string + >>> str(semver.Version.parse('1.2.3-rc.5').finalize_version()) '1.2.3' """ @@ -545,12 +542,12 @@ def match(self, match_expr: str) -> bool: Compare self to match a match expression. :param match_expr: operator and version; valid operators are - < smaller than - > greater than - >= greator or equal than - <= smaller or equal than - == equal - != not equal + ``<``` smaller than + ``>`` greater than + ``>=`` greator or equal than + ``<=`` smaller or equal than + ``==`` equal + ``!=`` not equal :return: True if the expression matches the version, otherwise False >>> semver.Version.parse("2.0.0").match(">=1.0.0") @@ -589,18 +586,18 @@ def match(self, match_expr: str) -> bool: @classmethod def parse(cls, version: String) -> "Version": """ - Parse version string to a VersionInfo instance. + Parse version string to a Version instance. .. versionchanged:: 2.11.0 Changed method from static to classmethod to allow subclasses. :param version: version string - :return: a :class:`VersionInfo` instance + :return: a new :class:`Version` instance :raises ValueError: if version is invalid >>> semver.Version.parse('3.4.5-pre.2+build.4') - VersionInfo(major=3, minor=4, patch=5, \ + Version(major=3, minor=4, patch=5, \ prerelease='pre.2', build='build.4') """ version_str = ensure_str(version) @@ -624,7 +621,7 @@ def replace(self, **parts: Union[int, Optional[str]]) -> "Version": ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` :return: the new :class:`Version` object with the changed parts - :raises TypeError: if ``parts`` contains invalid keys + :raises TypeError: if ``parts`` contain invalid keys """ version = self.to_dict() version.update(parts) @@ -632,7 +629,7 @@ def replace(self, **parts: Union[int, Optional[str]]) -> "Version": return Version(**version) # type: ignore except TypeError: unknownkeys = set(parts) - set(self.to_dict()) - error = "replace() got %d unexpected keyword " "argument(s): %s" % ( + error = "replace() got %d unexpected keyword argument(s): %s" % ( len(unknownkeys), ", ".join(unknownkeys), ) diff --git a/tests/conftest.py b/tests/conftest.py index f7f927cf..0450e0ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ @pytest.fixture(autouse=True) def add_semver(doctest_namespace): - doctest_namespace["Version"] = semver.Version + doctest_namespace["Version"] = semver.version.Version doctest_namespace["semver"] = semver doctest_namespace["coerce"] = coerce doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix diff --git a/tox.ini b/tox.ini index 73fbfc58..560084ef 100644 --- a/tox.ini +++ b/tox.ini @@ -67,15 +67,7 @@ deps = -r{toxinidir}/docs/requirements.txt skip_install = true allowlist_externals = make - rm echo - sed -commands_pre = - sphinx-apidoc --module-first -f --separate -H semver -o docs/_api src/semver src/semver/_types.py src/semver/_deprecated.py - # we don't need this, it just add another level and it's all in docs/api.rst - - rm docs/_api/modules.rst - # Include the semver.__about__ module before semver.cli: - sed -i '/semver\.cli/i\ \ \ semver.__about__' docs/_api/semver.rst commands = make -C docs html commands_post = From ff204d71e9665b454bcc2a13a911d39aac81ab01 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 3 Nov 2020 21:44:14 +0100 Subject: [PATCH 20/86] Remove already inserted changelog.d entry --- changelog.d/213.improvement.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/213.improvement.rst diff --git a/changelog.d/213.improvement.rst b/changelog.d/213.improvement.rst deleted file mode 100644 index dcedc695..00000000 --- a/changelog.d/213.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Add typing information \ No newline at end of file From a6a8115cc7feef77ad6d4db03260eaf73a66b911 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 3 Nov 2020 23:53:44 +0100 Subject: [PATCH 21/86] Tox: skip installation for changelog target We only need to install towncrier, but it is not needed to install semver too. Therefor, skip the installation of semver. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 73fbfc58..5ba0ba13 100644 --- a/tox.ini +++ b/tox.ini @@ -103,6 +103,7 @@ commands = [testenv:changelog] description = Run towncrier to check, build, or create the CHANGELOG.rst basepython = python3 +skip_install = true deps = git+https://github.com/twisted/towncrier.git commands = From 40c26e5c00c51507b4ec91b57d00f0e641af5d67 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 4 Nov 2020 00:00:32 +0100 Subject: [PATCH 22/86] Add changelog.d entry file for #313 --- changelog.d/313.trivial.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/313.trivial.rst diff --git a/changelog.d/313.trivial.rst b/changelog.d/313.trivial.rst new file mode 100644 index 00000000..963b4f31 --- /dev/null +++ b/changelog.d/313.trivial.rst @@ -0,0 +1,3 @@ +Correct :file:`tox.ini` for ``changelog`` entry to skip +installation for semver. This should speed up the execution +of towncrier. From a8dd4eae7a57fb5f56e3390998d8b8b09930770c Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 3 Nov 2020 21:48:58 +0100 Subject: [PATCH 23/86] Fix #312: Rework Usage section * Correct :class: directive and use :class:`~semver.version.Version` * Mention the rename of VersionInfo -> Version class * Remove semver. prefix in doctests to make examples shorter * Correct some references to dunder methods like __getitem__, __gt__ etc. * Use :py:exec: reference to Python exceptions. * Remove inconsistencies and mention module level function as deprecated and discouraged from using * Make empty super() call in semverwithvprefix.py example * Add changelog.d file --- CHANGELOG.rst | 53 +++++++++ changelog.d/312.doc.rst | 11 ++ docs/semverwithvprefix.py | 11 +- docs/usage.rst | 245 +++++++++++++++++--------------------- 4 files changed, 178 insertions(+), 142 deletions(-) create mode 100644 changelog.d/312.doc.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 26f8ff79..2e3f97a5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,59 @@ in our repository. .. towncrier release notes start +Version 3.0.0-dev.2 +=================== + +:Released: 2020-11-04 +:Maintainer: + + +Improved Documentation +---------------------- + +* :gh:`312`: Rework "Usage" section. + + * Mention the rename of :class:`~semver.version.VersionInfo` to + :class:`~semver.version.Version` class + * Remove semver. prefix in doctests to make examples shorter + * Correct some references to dunder methods like + :func:`~.semver.version.Version.__getitem__`, + :func:`~.semver.version.Version.__gt__` etc. + * Remove inconsistencies and mention module level function as + deprecated and discouraged from using + * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example + + + +---- + + +Version 3.0.0-dev.2 +=================== + +:Released: 2020-11-04 +:Maintainer: + + +Improved Documentation +---------------------- + +* :gh:`312`: Rework "Usage" section. + + * Mention the rename of :class:`~semver.version.VersionInfo` to + :class:`~semver.version.Version` class + * Remove semver. prefix in doctests to make examples shorter + * Correct some references to dunder methods like :func:`__getitem__`, + :func:`__gt__` etc. + * Remove inconsistencies and mention module level function as + deprecated and discouraged from using + * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example + + + +---- + + Version 3.0.0-dev.2 =================== diff --git a/changelog.d/312.doc.rst b/changelog.d/312.doc.rst new file mode 100644 index 00000000..6b18eb49 --- /dev/null +++ b/changelog.d/312.doc.rst @@ -0,0 +1,11 @@ +Rework "Usage" section. + +* Mention the rename of :class:`~semver.version.VersionInfo` to + :class:`~semver.version.Version` class +* Remove semver. prefix in doctests to make examples shorter +* Correct some references to dunder methods like + :func:`~.semver.version.Version.__getitem__`, + :func:`~.semver.version.Version.__gt__` etc. +* Remove inconsistencies and mention module level function as + deprecated and discouraged from using +* Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example diff --git a/docs/semverwithvprefix.py b/docs/semverwithvprefix.py index 304ce772..5e375031 100644 --- a/docs/semverwithvprefix.py +++ b/docs/semverwithvprefix.py @@ -7,15 +7,13 @@ class SemVerWithVPrefix(Version): """ @classmethod - def parse(cls, version): + def parse(cls, version: str) -> "SemVerWithVPrefix": """ Parse version string to a Version instance. :param version: version string with "v" or "V" prefix - :type version: str :raises ValueError: when version does not start with "v" or "V" :return: a new instance - :rtype: :class:`SemVerWithVPrefix` """ if not version[0] in ("v", "V"): raise ValueError( @@ -23,9 +21,8 @@ def parse(cls, version): v=version ) ) - self = super(SemVerWithVPrefix, cls).parse(version[1:]) - return self + return super().parse(version[1:]) - def __str__(self): + def __str__(self) -> str: # Reconstruct the tag - return "v" + super(SemVerWithVPrefix, self).__str__() + return "v" + super().__str__() diff --git a/docs/usage.rst b/docs/usage.rst index 94a115a8..ad44ecaf 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,7 +1,7 @@ Using semver ============ -The ``semver`` module can store a version in the :class:`semver.Version` class. +The :mod:`semver` module can store a version in the :class:`~semver.version.Version` class. For historical reasons, a version can be also stored as a string or dictionary. Each type can be converted into the other, if the minimum requirements @@ -32,70 +32,54 @@ To know the version of semver itself, use the following construct:: Creating a Version ------------------ -Due to historical reasons, the semver project offers two ways of -creating a version: +.. versionchanged:: 3.0.0 -* through an object oriented approach with the :class:`semver.Version` - class. This is the preferred method when using semver. + The former :class:`~semver.version.VersionInfo` + has been renamed to :class:`~semver.version.Version`. -* through module level functions and builtin datatypes (usually string - and dict). - This method is still available for compatibility reasons, but are - marked as deprecated. Using it will emit a :class:`DeprecationWarning`. +The preferred way to create a new version is with the class +:class:`~semver.version.Version`. +.. note:: -.. warning:: **Deprecation Warning** + In the previous major release semver 2 it was possible to + create a version with module level functions. + However, module level functions are marked as *deprecated* + since version 2.x.y now. + These functions will be removed in semver 3.1.0. + For details, see the sections :ref:`sec_replace_deprecated_functions` + and :ref:`sec_display_deprecation_warnings`. - Module level functions are marked as *deprecated* in version 2.x.y now. - These functions will be removed in semver 3. - For details, see the sections :ref:`sec_replace_deprecated_functions` and - :ref:`sec_display_deprecation_warnings`. +A :class:`~semver.version.Version` instance can be created in different ways: +* From a Unicode string:: -A :class:`semver.Version` instance can be created in different ways: - -* From a string (a Unicode string in Python 2):: - - >>> semver.Version.parse("3.4.5-pre.2+build.4") + >>> from semver.version import Version + >>> Version.parse("3.4.5-pre.2+build.4") Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - >>> semver.Version.parse(u"5.3.1") + >>> Version.parse(u"5.3.1") Version(major=5, minor=3, patch=1, prerelease=None, build=None) * From a byte string:: - >>> semver.Version.parse(b"2.3.4") + >>> Version.parse(b"2.3.4") Version(major=2, minor=3, patch=4, prerelease=None, build=None) * From individual parts by a dictionary:: >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - >>> semver.Version(**d) + >>> Version(**d) Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') Keep in mind, the ``major``, ``minor``, ``patch`` parts has to - be positive. + be positive integers or strings: - >>> semver.Version(-1) + >>> d = {'major': -3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> Version(**d) Traceback (most recent call last): ... ValueError: 'major' is negative. A version can only be positive. - As a minimum requirement, your dictionary needs at least the - be positive. - - >>> semver.Version(-1) - Traceback (most recent call last): - ... - ValueError: 'major' is negative. A version can only be positive. - - As a minimum requirement, your dictionary needs at least the - be positive. - - >>> semver.Version(-1) - Traceback (most recent call last): - ... - ValueError: 'major' is negative. A version can only be positive. - As a minimum requirement, your dictionary needs at least the ``major`` key, others can be omitted. You get a ``TypeError`` if your dictionary contains invalid keys. @@ -105,20 +89,23 @@ A :class:`semver.Version` instance can be created in different ways: * From a tuple:: >>> t = (3, 5, 6) - >>> semver.Version(*t) + >>> Version(*t) Version(major=3, minor=5, patch=6, prerelease=None, build=None) You can pass either an integer or a string for ``major``, ``minor``, or ``patch``:: - >>> semver.Version("3", "5", 6) + >>> Version("3", "5", 6) Version(major=3, minor=5, patch=6, prerelease=None, build=None) -The old, deprecated module level functions are still available. If you -need them, they return different builtin objects (string and dictionary). +The old, deprecated module level functions are still available but +using them are discoraged. They are available to convert old code +to semver3. + +If you need them, they return different builtin objects (string and dictionary). Keep in mind, once you have converted a version into a string or dictionary, it's an ordinary builtin object. It's not a special version object like -the :class:`semver.Version` class anymore. +the :class:`~semver.version.Version` class anymore. Depending on your use case, the following methods are available: @@ -136,7 +123,7 @@ Depending on your use case, the following methods are available: >>> semver.parse("3.4.5-pre.2+build.4") OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) - If you pass an invalid version string you will get a ``ValueError``:: + If you pass an invalid version string you will get a :py:exc:`ValueError`:: >>> semver.parse("1.2") Traceback (most recent call last): @@ -148,36 +135,23 @@ Parsing a Version String ------------------------ "Parsing" in this context means to identify the different parts in a string. +Use the function :func:`Version.parse `:: - -* With :func:`semver.parse_version_info`:: - - >>> semver.parse_version_info("3.4.5-pre.2+build.4") - Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - -* With :func:`semver.Version.parse` (basically the same as - :func:`semver.parse_version_info`):: - - >>> semver.Version.parse("3.4.5-pre.2+build.4") + >>> Version.parse("3.4.5-pre.2+build.4") Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') -* With :func:`semver.parse`:: - - >>> semver.parse("3.4.5-pre.2+build.4") == {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - True - Checking for a Valid Semver Version ----------------------------------- If you need to check a string if it is a valid semver version, use the -classmethod :func:`semver.Version.isvalid`: +classmethod :func:`Version.isvalid `: .. code-block:: python - >>> semver.Version.isvalid("1.0.0") + >>> Version.isvalid("1.0.0") True - >>> semver.Version.isvalid("invalid") + >>> Version.isvalid("invalid") False @@ -186,12 +160,12 @@ classmethod :func:`semver.Version.isvalid`: Accessing Parts of a Version Through Names ------------------------------------------ -The :class:`semver.Version` contains attributes to access the different +The :class:`~semver.version.Version` class contains attributes to access the different parts of a version: .. code-block:: python - >>> v = semver.Version.parse("3.4.5-pre.2+build.4") + >>> v = Version.parse("3.4.5-pre.2+build.4") >>> v.major 3 >>> v.minor @@ -203,8 +177,8 @@ parts of a version: >>> v.build 'build.4' -However, the attributes are read-only. You cannot change an attribute. -If you do, you get an ``AttributeError``:: +However, the attributes are read-only. You cannot change any of the above attributes. +If you do, you get an :py:exc:`AttributeError`:: >>> v.minor = 5 Traceback (most recent call last): @@ -213,16 +187,16 @@ If you do, you get an ``AttributeError``:: If you need to replace different parts of a version, refer to section :ref:`sec.replace.parts`. -In case you need the different parts of a version stepwise, iterate over the :class:`semver.Version` instance:: +In case you need the different parts of a version stepwise, iterate over the :class:`~semver.version.Version` instance:: - >>> for item in semver.Version.parse("3.4.5-pre.2+build.4"): + >>> for item in Version.parse("3.4.5-pre.2+build.4"): ... print(item) 3 4 5 pre.2 build.4 - >>> list(semver.Version.parse("3.4.5-pre.2+build.4")) + >>> list(Version.parse("3.4.5-pre.2+build.4")) [3, 4, 5, 'pre.2', 'build.4'] @@ -234,15 +208,15 @@ Accessing Parts Through Index Numbers .. versionadded:: 2.10.0 Another way to access parts of a version is to use an index notation. The underlying -:class:`Version ` object allows to access its data through -the magic method :func:`__getitem__ `. +:class:`~semver.version.Version` object allows to access its data through +the magic method :func:`~semver.version.Version.__getitem__`. For example, the ``major`` part can be accessed by index number 0 (zero). Likewise the other parts: .. code-block:: python - >>> ver = semver.Version.parse("10.3.2-pre.5+build.10") + >>> ver = Version.parse("10.3.2-pre.5+build.10") >>> ver[0], ver[1], ver[2], ver[3], ver[4] (10, 3, 2, 'pre.5', 'build.10') @@ -261,11 +235,11 @@ Or, as an alternative, you can pass a :func:`slice` object: >>> ver[sl] (10, 3, 2) -Negative numbers or undefined parts raise an :class:`IndexError` exception: +Negative numbers or undefined parts raise an :py:exc:`IndexError` exception: .. code-block:: python - >>> ver = semver.Version.parse("10.3.2") + >>> ver = Version.parse("10.3.2") >>> ver[3] Traceback (most recent call last): ... @@ -281,9 +255,9 @@ Replacing Parts of a Version ---------------------------- If you want to replace different parts of a version, but leave other parts -unmodified, use the function :func:`semver.Version.replace` or :func:`semver.replace`: +unmodified, use the function :func:`replace `: -* From a :class:`semver.Version` instance:: +* From a :class:`Version ` instance:: >>> version = semver.Version.parse("1.4.5-pre.1+build.6") >>> version.replace(major=2, minor=2) @@ -312,25 +286,25 @@ If you pass invalid keys you get an exception:: Converting a Version instance into Different Types ------------------------------------------------------ -Sometimes it is needed to convert a :class:`semver.Version` instance into +Sometimes it is needed to convert a :class:`Version ` instance into a different type. For example, for displaying or to access all parts. -It is possible to convert a :class:`semver.Version` instance: +It is possible to convert a :class:`Version ` instance: * Into a string with the builtin function :func:`str`:: - >>> str(semver.Version.parse("3.4.5-pre.2+build.4")) + >>> str(Version.parse("3.4.5-pre.2+build.4")) '3.4.5-pre.2+build.4' -* Into a dictionary with :func:`semver.Version.to_dict`:: +* Into a dictionary with :func:`to_dict `:: - >>> v = semver.Version(major=3, minor=4, patch=5) + >>> v = Version(major=3, minor=4, patch=5) >>> v.to_dict() OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) -* Into a tuple with :func:`semver.Version.to_tuple`:: +* Into a tuple with :func:`to_tuple `:: - >>> v = semver.Version(major=5, minor=4, patch=2) + >>> v = Version(major=5, minor=4, patch=2) >>> v.to_tuple() (5, 4, 2, None, None) @@ -341,27 +315,27 @@ Raising Parts of a Version The ``semver`` module contains the following functions to raise parts of a version: -* :func:`semver.Version.bump_major`: raises the major part and set all other parts to +* :func:`Version.bump_major `: raises the major part and set all other parts to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.Version.bump_minor`: raises the minor part and sets ``patch`` to zero. +* :func:`Version.bump_minor `: raises the minor part and sets ``patch`` to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.Version.bump_patch`: raises the patch part. Set ``prerelease`` and +* :func:`Version.bump_patch `: raises the patch part. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.Version.bump_prerelease`: raises the prerelease part and set +* :func:`Version.bump_prerelease `: raises the prerelease part and set ``build`` to ``None``. -* :func:`semver.Version.bump_build`: raises the build part. +* :func:`Version.bump_build `: raises the build part. .. code-block:: python - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_major()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_major()) '4.0.0' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_minor()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_minor()) '3.5.0' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_patch()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_patch()) '3.4.6' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) '3.4.5-pre.3' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_build()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_build()) '3.4.5-pre.2+build.5' Likewise the module level functions :func:`semver.bump_major`. @@ -371,23 +345,23 @@ Increasing Parts of a Version Taking into Account Prereleases ------------------------------------------------------------- .. versionadded:: 2.10.0 - Added :func:`semver.Version.next_version`. + Added :func:`Version.next_version `. If you want to raise your version and take prereleases into account, -the function :func:`semver.Version.next_version` would perhaps a -better fit. +the function :func:`next_version ` +would perhaps a better fit. .. code-block:: python - >>> v = semver.Version.parse("3.4.5-pre.2+build.4") + >>> v = Version.parse("3.4.5-pre.2+build.4") >>> str(v.next_version(part="prerelease")) '3.4.5-pre.3' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) + >>> str(Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) '3.4.5' - >>> str(semver.Version.parse("3.4.5+build.4").next_version(part="patch")) + >>> str(Version.parse("3.4.5+build.4").next_version(part="patch")) '3.4.5' - >>> str(semver.Version.parse("0.1.4").next_version("prerelease")) + >>> str(Version.parse("0.1.4").next_version("prerelease")) '0.1.5-rc.1' @@ -410,23 +384,23 @@ To compare two versions depends on your type: The return value is negative if ``version1 < version2``, zero if ``version1 == version2`` and strictly positive if ``version1 > version2``. -* **Two** :class:`semver.Version` **instances** +* **Two** :class:`Version ` **instances** Use the specific operator. Currently, the operators ``<``, ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: - >>> v1 = semver.Version.parse("3.4.5") - >>> v2 = semver.Version.parse("3.5.1") + >>> v1 = Version.parse("3.4.5") + >>> v2 = Version.parse("3.5.1") >>> v1 < v2 True >>> v1 > v2 False -* **A** :class:`semver.Version` **type and a** :func:`tuple` **or** :func:`list` +* **A** :class:`Version ` **type and a** :func:`tuple` **or** :func:`list` - Use the operator as with two :class:`semver.Version` types:: + Use the operator as with two :class:`Version ` types:: - >>> v = semver.Version.parse("3.4.5") + >>> v = Version.parse("3.4.5") >>> v > (1, 0) True >>> v < [3, 5] @@ -439,7 +413,7 @@ To compare two versions depends on your type: >>> [3, 5] > v True -* **A** :class:`semver.Version` **type and a** :func:`str` +* **A** :class:`Version ` **type and a** :func:`str` You can use also raw strings to compare:: @@ -455,14 +429,14 @@ To compare two versions depends on your type: >>> "3.5.0" > v True - However, if you compare incomplete strings, you get a :class:`ValueError` exception:: + However, if you compare incomplete strings, you get a :py:exc:`ValueError` exception:: >>> v > "1.0" Traceback (most recent call last): ... ValueError: 1.0 is not valid SemVer string -* **A** :class:`semver.Version` **type and a** :func:`dict` +* **A** :class:`Version ` **type and a** :func:`dict` You can also use a dictionary. In contrast to strings, you can have an "incomplete" version (as the other parts are set to zero):: @@ -475,7 +449,7 @@ To compare two versions depends on your type: >>> dict(major=1) < v True - If the dictionary contains unknown keys, you get a :class:`TypeError` exception:: + If the dictionary contains unknown keys, you get a :py:exc:`TypeError` exception:: >>> v > dict(major=1, unknown=42) Traceback (most recent call last): @@ -499,16 +473,16 @@ Version equality means for semver, that major, minor, patch, and prerelease parts are equal in both versions you compare. The build part is ignored. For example:: - >>> v = semver.Version.parse("1.2.3-rc4+1e4664d") + >>> v = Version.parse("1.2.3-rc4+1e4664d") >>> v == "1.2.3-rc4+dedbeef" True -This also applies when a :class:`semver.Version` is a member of a set, or a +This also applies when a :class:`Version ` is a member of a set, or a dictionary key:: >>> d = {} - >>> v1 = semver.Version.parse("1.2.3-rc4+1e4664d") - >>> v2 = semver.Version.parse("1.2.3-rc4+dedbeef") + >>> v1 = Version.parse("1.2.3-rc4+1e4664d") + >>> v2 = Version.parse("1.2.3-rc4+dedbeef") >>> d[v1] = 1 >>> d[v2] 1 @@ -554,34 +528,36 @@ Getting Minimum and Maximum of Multiple Versions The functions :func:`semver.max_ver` and :func:`semver.min_ver` are deprecated in favor of their builtin counterparts :func:`max` and :func:`min`. -Since :class:`semver.Version` implements :func:`__gt__()` and :func:`__lt__()`, it can be used with builtins requiring +Since :class:`Version ` implements +:func:`__gt__ ` and +:func:`__lt__ `, it can be used with builtins requiring: .. code-block:: python - >>> max([semver.Version(0, 1, 0), semver.Version(0, 2, 0), semver.Version(0, 1, 3)]) + >>> max([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) Version(major=0, minor=2, patch=0, prerelease=None, build=None) - >>> min([semver.Version(0, 1, 0), semver.Version(0, 2, 0), semver.Version(0, 1, 3)]) + >>> min([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) Version(major=0, minor=1, patch=0, prerelease=None, build=None) Incidentally, using :func:`map`, you can get the min or max version of any number of versions of the same type -(convertible to :class:`semver.Version`). +(convertible to :class:`Version `). For example, here are the maximum and minimum versions of a list of version strings: .. code-block:: python - >>> str(max(map(semver.Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> str(max(map(Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) '2.1.0' - >>> str(min(map(semver.Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> str(min(map(Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) '0.4.99' And the same can be done with tuples: .. code-block:: python - >>> max(map(lambda v: semver.Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + >>> max(map(lambda v: Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() (2, 1, 0, None, None) - >>> min(map(lambda v: semver.Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + >>> min(map(lambda v: Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() (0, 4, 99, None, None) For dictionaries, it is very similar to finding the max version tuple: see :ref:`sec.convert.versions`. @@ -616,7 +592,7 @@ information and returns a tuple with two items: :language: python -The function returns a *tuple*, containing a :class:`Version` +The function returns a *tuple*, containing a :class:`Version ` instance or None as the first element and the rest as the second element. The second element (the rest) can be used to make further adjustments. @@ -649,7 +625,7 @@ them with code which is compatible for future versions: * :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` - Replace them with the respective methods of the :class:`semver.Version` + Replace them with the respective methods of the :class:`Version ` class. For example, the function :func:`semver.bump_major` is replaced by :func:`semver.Version.bump_major` and calling the ``str(versionobject)``: @@ -657,7 +633,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.bump_major("3.4.5") - >>> s2 = str(semver.Version.parse("3.4.5").bump_major()) + >>> s2 = str(Version.parse("3.4.5").bump_major()) >>> s1 == s2 True @@ -681,7 +657,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') - >>> s2 = str(semver.Version(5, 4, 3, 'pre.2', 'build.1')) + >>> s2 = str(Version(5, 4, 3, 'pre.2', 'build.1')) >>> s1 == s2 True @@ -692,7 +668,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.max_ver("1.2.3", "1.2.4") - >>> s2 = str(max(map(semver.Version.parse, ("1.2.3", "1.2.4")))) + >>> s2 = str(max(map(Version.parse, ("1.2.3", "1.2.4")))) >>> s1 == s2 True @@ -703,7 +679,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.min_ver("1.2.3", "1.2.4") - >>> s2 = str(min(map(semver.Version.parse, ("1.2.3", "1.2.4")))) + >>> s2 = str(min(map(Version.parse, ("1.2.3", "1.2.4")))) >>> s1 == s2 True @@ -715,7 +691,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> v1 = semver.parse("1.2.3") - >>> v2 = semver.Version.parse("1.2.3").to_dict() + >>> v2 = Version.parse("1.2.3").to_dict() >>> v1 == v2 True @@ -726,7 +702,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> v1 = semver.parse_version_info("3.4.5") - >>> v2 = semver.Version.parse("3.4.5") + >>> v2 = Version.parse("3.4.5") >>> v1 == v2 True @@ -737,7 +713,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.replace("1.2.3", major=2, patch=10) - >>> s2 = str(semver.Version.parse('1.2.3').replace(major=2, patch=10)) + >>> s2 = str(Version.parse('1.2.3').replace(major=2, patch=10)) >>> s1 == s2 True @@ -785,7 +761,7 @@ Creating Subclasses from Version If you do not like creating functions to modify the behavior of semver (as shown in section :ref:`sec_dealing_with_invalid_versions`), you can -also create a subclass of the :class:`Version` class. +also create a subclass of the :class:`Version ` class. For example, if you want to output a "v" prefix before a version, but the other behavior is the same, use the following code: @@ -811,4 +787,3 @@ the original class: Traceback (most recent call last): ... ValueError: '1.2.4': not a valid semantic version tag. Must start with 'v' or 'V' - From 67933209234509b6bb6ac3a68a24bcc9c65f921f Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 7 Nov 2020 23:38:21 +0100 Subject: [PATCH 24/86] Fix #316: Return NotImplemented for comparisons The former code raised a TypeError exception for comparisons like __gt__, __lt__ etc. to indicate a wrong type. However, according to NotImplemented[1] documentation, we should return(!) NotImplemented (not raise) when a comparison with an invalid type is not implemented. [1] https://docs.python.org/3/library/constants.html#NotImplemented --- changelog.d/316.trivial.rst | 10 ++++++++++ src/semver/version.py | 4 +--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 changelog.d/316.trivial.rst diff --git a/changelog.d/316.trivial.rst b/changelog.d/316.trivial.rst new file mode 100644 index 00000000..edb555ff --- /dev/null +++ b/changelog.d/316.trivial.rst @@ -0,0 +1,10 @@ +Comparisons of :class:`~semver.version.Version` class and other +types return now a :py:const:`NotImplemented` constant instead +of a :py:exc:`TypeError` exception. + +The `NotImplemented`_ section of the Python documentation recommends +returning this constant when comparing with ``__gt__``, ``__lt__``, +and other comparison operators to "to indicate that the operation is +not implemented with respect to the other type". + +.. _NotImplemented: https://docs.python.org/3/library/constants.html#NotImplemented \ No newline at end of file diff --git a/src/semver/version.py b/src/semver/version.py index 40132526..4633f4bc 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -72,9 +72,7 @@ def wrapper(self: "Version", other: Comparable) -> bool: *String.__args__, # type: ignore ) if not isinstance(other, comparable_types): - raise TypeError( - "other type %r must be in %r" % (type(other), comparable_types) - ) + return NotImplemented return operator(self, other) return wrapper From 3031da4a56a2d1cb53b4588fc15ce5102357bff2 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 8 Nov 2020 14:42:43 +0100 Subject: [PATCH 25/86] Introduce stages in .travis.yml The config file contains now two stages: check and test. If check fails, the test stage won't be executed. This could speed up things. --- .travis.yml | 29 ++++++++++++++++------------- changelog.d/319.trivial.rst | 4 ++++ 2 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 changelog.d/319.trivial.rst diff --git a/.travis.yml b/.travis.yml index 665ebd19..3d31c894 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,40 +10,43 @@ install: - pip install virtualenv tox wheel - tox --version +stages: + - check + - test + script: tox -v matrix: include: - - - python: "3.6" + - stage: check + python: 3.6 env: TOXENV=checks - - python: "3.8" + - stage: test dist: xenial - env: TOXENV=mypy - - - python: "3.6" + python: "3.6" env: TOXENV=py36 - - python: "3.7" + - stage: test dist: xenial + python: "3.7" env: TOXENV=py37 - - python: "3.8" + - stage: test dist: xenial + python: "3.8" env: TOXENV=py38 - - python: "3.9-dev" + - stage: test dist: bionic + python: "3.9-dev" env: TOXENV=py39 - - python: "nightly" + - stage: test dist: bionic + python: "nightly" env: TOXENV=py310 - - python: "3.8" - dist: xenial - env: TOXENV=mypy jobs: allow_failures: diff --git a/changelog.d/319.trivial.rst b/changelog.d/319.trivial.rst new file mode 100644 index 00000000..c1c259a9 --- /dev/null +++ b/changelog.d/319.trivial.rst @@ -0,0 +1,4 @@ +Introduce stages in :file:`.travis.yml` +The config file contains now two stages: check and test. If +check fails, the test stage won't be executed. This could +speed up things when some checks fails. \ No newline at end of file From 201d783474f32520a91046833ff426f5453fcf42 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 4 Nov 2020 16:33:06 +0100 Subject: [PATCH 26/86] Fix #309: Make some functions private Make private function in semver.version module: * Rename comparator -> _comparator, cmp -> _cmp * Remove ensure_str and integrate it into Version.parse * Rework tests in test_typeerror-274.py * Move _nat_cmp to Version and make it a class method * Remove private functions from API documentation --- changelog.d/309.trivial.rst | 17 ++++++ docs/api.rst | 6 -- src/semver/version.py | 110 ++++++++++++++---------------------- tests/test_typeerror-274.py | 89 ++--------------------------- 4 files changed, 64 insertions(+), 158 deletions(-) create mode 100644 changelog.d/309.trivial.rst diff --git a/changelog.d/309.trivial.rst b/changelog.d/309.trivial.rst new file mode 100644 index 00000000..97bbba1e --- /dev/null +++ b/changelog.d/309.trivial.rst @@ -0,0 +1,17 @@ +Some (private) functions from the :mod:`semver.version` +module has been changed. + +The following functions got renamed: + +* function ``semver.version.comparator`` got renamed to + :func:`semver.version._comparator` as it is only useful + inside the :class:`~semver.version.Version` class. +* function ``semver.version.cmp`` got renamed to + :func:`semver.version._cmp` as it is only useful + inside the :class:`~semver.version.Version` class. + +The following functions got integrated into the +:class:`~semver.version.Version` class: + +* function ``semver.version._nat_cmd`` as a classmethod +* function ``semver.version.ensure_str`` diff --git a/docs/api.rst b/docs/api.rst index d35c48fe..196e30a9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -50,12 +50,6 @@ Version Handling :mod:`semver.version` .. automodule:: semver.version -.. autofunction:: semver.version.cmp - -.. autofunction:: semver.version.ensure_str - -.. autofunction:: semver.version.comparator - .. autoclass:: semver.version.VersionInfo .. autoclass:: semver.version.Version diff --git a/src/semver/version.py b/src/semver/version.py index 4633f4bc..9e02544f 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -29,37 +29,7 @@ Comparator = Callable[["Version", Comparable], bool] -def cmp(a, b): # TODO: type hints - """Return negative if ab.""" - return (a > b) - (a < b) - - -def ensure_str(s: String, encoding="utf-8", errors="strict") -> str: - # Taken from six project - """ - Coerce *s* to `str`. - - * `str` -> `str` - * `bytes` -> decoded to `str` - - :param s: the string to convert - :param encoding: the encoding to apply, defaults to "utf-8" - :param errors: set a different error handling scheme, - defaults to "strict". - Other possible values are `ignore`, `replace`, and - `xmlcharrefreplace` as well as any other name - registered with :func:`codecs.register_error`. - :raises TypeError: if ``s`` is not str or bytes type - :return: the converted string - """ - if isinstance(s, bytes): - s = s.decode(encoding, errors) - elif not isinstance(s, String.__args__): # type: ignore - raise TypeError("not expecting type '%s'" % type(s)) - return s - - -def comparator(operator: Comparator) -> Comparator: +def _comparator(operator: Comparator) -> Comparator: """Wrap a Version binary op method in a type-check.""" @wraps(operator) @@ -78,31 +48,9 @@ def wrapper(self: "Version", other: Comparable) -> bool: return wrapper -def _nat_cmp(a, b): # TODO: type hints - def convert(text): - return int(text) if re.match("^[0-9]+$", text) else text - - def split_key(key): - return [convert(c) for c in key.split(".")] - - def cmp_prerelease_tag(a, b): - if isinstance(a, int) and isinstance(b, int): - return cmp(a, b) - elif isinstance(a, int): - return -1 - elif isinstance(b, int): - return 1 - else: - return cmp(a, b) - - a, b = a or "", b or "" - a_parts, b_parts = split_key(a), split_key(b) - for sub_a, sub_b in zip(a_parts, b_parts): - cmp_result = cmp_prerelease_tag(sub_a, sub_b) - if cmp_result != 0: - return cmp_result - else: - return cmp(len(a), len(b)) +def _cmp(a, b): # TODO: type hints + """Return negative if ab.""" + return (a > b) - (a < b) class Version: @@ -165,6 +113,29 @@ def __init__( self._prerelease = None if prerelease is None else str(prerelease) self._build = None if build is None else str(build) + @classmethod + def _nat_cmp(cls, a, b): # TODO: type hints + def cmp_prerelease_tag(a, b): + if isinstance(a, int) and isinstance(b, int): + return _cmp(a, b) + elif isinstance(a, int): + return -1 + elif isinstance(b, int): + return 1 + else: + return _cmp(a, b) + + a, b = a or "", b or "" + a_parts, b_parts = a.split("."), b.split(".") + a_parts = [int(x) if re.match(r"^\d+$", x) else x for x in a_parts] + b_parts = [int(x) if re.match(r"^\d+$", x) else x for x in b_parts] + for sub_a, sub_b in zip(a_parts, b_parts): + cmp_result = cmp_prerelease_tag(sub_a, sub_b) + if cmp_result != 0: + return cmp_result + else: + return _cmp(len(a), len(b)) + @property def major(self) -> int: """The major part of a version (read-only).""" @@ -381,12 +352,12 @@ def compare(self, other: Comparable) -> int: v1 = self.to_tuple()[:3] v2 = other.to_tuple()[:3] - x = cmp(v1, v2) + x = _cmp(v1, v2) if x: return x rc1, rc2 = self.prerelease, other.prerelease - rccmp = _nat_cmp(rc1, rc2) + rccmp = self._nat_cmp(rc1, rc2) if not rccmp: return 0 @@ -444,27 +415,27 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": version = version.bump_patch() return version.bump_prerelease(prerelease_token) - @comparator + @_comparator def __eq__(self, other: Comparable) -> bool: # type: ignore return self.compare(other) == 0 - @comparator + @_comparator def __ne__(self, other: Comparable) -> bool: # type: ignore return self.compare(other) != 0 - @comparator + @_comparator def __lt__(self, other: Comparable) -> bool: return self.compare(other) < 0 - @comparator + @_comparator def __le__(self, other: Comparable) -> bool: return self.compare(other) <= 0 - @comparator + @_comparator def __gt__(self, other: Comparable) -> bool: return self.compare(other) > 0 - @comparator + @_comparator def __ge__(self, other: Comparable) -> bool: return self.compare(other) >= 0 @@ -593,15 +564,20 @@ def parse(cls, version: String) -> "Version": :param version: version string :return: a new :class:`Version` instance :raises ValueError: if version is invalid + :raises TypeError: if version contains the wrong type >>> semver.Version.parse('3.4.5-pre.2+build.4') Version(major=3, minor=4, patch=5, \ prerelease='pre.2', build='build.4') """ - version_str = ensure_str(version) - match = cls._REGEX.match(version_str) + if isinstance(version, bytes): + version = version.decode("UTF-8") + elif not isinstance(version, String.__args__): # type: ignore + raise TypeError("not expecting type '%s'" % type(version)) + + match = cls._REGEX.match(version) if match is None: - raise ValueError(f"{version_str} is not valid SemVer string") + raise ValueError(f"{version} is not valid SemVer string") matched_version_parts: Dict[str, Any] = match.groupdict() diff --git a/tests/test_typeerror-274.py b/tests/test_typeerror-274.py index 61480bcf..326304b8 100644 --- a/tests/test_typeerror-274.py +++ b/tests/test_typeerror-274.py @@ -1,95 +1,14 @@ -import sys - import pytest - import semver -import semver.version - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - - -def ensure_binary(s, encoding="utf-8", errors="strict"): - """ - Coerce ``s`` to bytes. - - * `str` -> encoded to `bytes` - * `bytes` -> `bytes` - - :param s: the string to convert - :type s: str | bytes - :param encoding: the encoding to apply, defaults to "utf-8" - :type encoding: str - :param errors: set a different error handling scheme; - other possible values are `ignore`, `replace`, and - `xmlcharrefreplace` as well as any other name - registered with :func:`codecs.register_error`. - Defaults to "strict". - :type errors: str - :raises TypeError: if ``s`` is not str or bytes type - :return: the converted string - :rtype: str - """ - if isinstance(s, str): - return s.encode(encoding, errors) - elif isinstance(s, bytes): - return s - else: - raise TypeError("not expecting type '%s'" % type(s)) -def test_should_work_with_string_and_unicode(): +def test_should_work_with_string_and_bytes(): result = semver.compare("1.1.0", b"1.2.2") assert result == -1 result = semver.compare(b"1.1.0", "1.2.2") assert result == -1 -class TestEnsure: - # From six project - # grinning face emoji - UNICODE_EMOJI = "\U0001F600" - BINARY_EMOJI = b"\xf0\x9f\x98\x80" - - def test_ensure_binary_raise_type_error(self): - with pytest.raises(TypeError): - semver.version.ensure_str(8) - - def test_errors_and_encoding(self): - ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore") - with pytest.raises(UnicodeEncodeError): - ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="strict") - - def test_ensure_binary_raise(self): - converted_unicode = ensure_binary( - self.UNICODE_EMOJI, encoding="utf-8", errors="strict" - ) - converted_binary = ensure_binary( - self.BINARY_EMOJI, encoding="utf-8", errors="strict" - ) - - # PY3: str -> bytes - assert converted_unicode == self.BINARY_EMOJI and isinstance( - converted_unicode, bytes - ) - # PY3: bytes -> bytes - assert converted_binary == self.BINARY_EMOJI and isinstance( - converted_binary, bytes - ) - - def test_ensure_str(self): - converted_unicode = semver.version.ensure_str( - self.UNICODE_EMOJI, encoding="utf-8", errors="strict" - ) - converted_binary = semver.version.ensure_str( - self.BINARY_EMOJI, encoding="utf-8", errors="strict" - ) - - # PY3: str -> str - assert converted_unicode == self.UNICODE_EMOJI and isinstance( - converted_unicode, str - ) - # PY3: bytes -> str - assert converted_binary == self.UNICODE_EMOJI and isinstance( - converted_unicode, str - ) +def test_should_not_work_with_invalid_args(): + with pytest.raises(TypeError): + semver.version.Version.parse(8) From 2818cdbacb4274386575197671e30a9cdcc61cee Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 10 Nov 2020 10:43:14 +0100 Subject: [PATCH 27/86] Fix #181: create issue templates Create GitHub issue templates for bugs and features. --- .github/ISSUE_TEMPLATE/bug_report.md | 33 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 24 +++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..461254e9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a bug report to help us improve semver +title: '' +labels: bug +assignees: '' + +--- + + + +# Situation + + +# To Reproduce + + +# Expected Behavior + + +# Environment +- OS: [e.g. Linux, MacOS, Windows, ...] +- Python version [e.g. 3.6, 3.7, ...] +- Version of semver library [e.g. 3.0.0] + +# Additional context + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..5a24681d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + + + +# Situation + + +# Possible Solution/Idea + + + +# Additional context + From aa58f62b1b6f8ef378a9c270a643182a17b72228 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 11 Nov 2020 08:57:21 +0100 Subject: [PATCH 28/86] Fix #322: Implement GitHub Action * Add "gh-action" section in tox.ini * Add .github/workflows/python-testing.yml * Use dependent jobs; first start check, then tests jobs * Add changelog * Remove black-formatting.yml * Remove .travis.yml --- ...lack-formatting.yml => python-testing.yml} | 51 +++++++++++++----- .travis.yml | 53 ------------------- changelog.d/322.trivial.rst | 1 + tox.ini | 14 +++-- 4 files changed, 48 insertions(+), 71 deletions(-) rename .github/workflows/{black-formatting.yml => python-testing.yml} (50%) delete mode 100644 .travis.yml create mode 100644 changelog.d/322.trivial.rst diff --git a/.github/workflows/black-formatting.yml b/.github/workflows/python-testing.yml similarity index 50% rename from .github/workflows/black-formatting.yml rename to .github/workflows/python-testing.yml index 25b34f21..1147d288 100644 --- a/.github/workflows/black-formatting.yml +++ b/.github/workflows/python-testing.yml @@ -1,15 +1,20 @@ -name: Black Formatting +--- +name: Python -on: [pull_request] +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] jobs: - build: + check: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v1 - name: Output env variables run: | + echo "Default branch=${default-branch}" echo "GITHUB_WORKFLOW=${GITHUB_WORKFLOW}" echo "GITHUB_ACTION=$GITHUB_ACTION" echo "GITHUB_ACTIONS=$GITHUB_ACTIONS" @@ -26,18 +31,36 @@ jobs: cat $GITHUB_EVENT_PATH echo "\n" echo "::debug::---end" - - - name: Set up Python 3.7 - uses: actions/setup-python@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 with: - python-version: 3.7 - + python-version: 3.6 - name: Install dependencies run: | - python -m pip install --upgrade pip black + python3 -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Check + run: | + tox -e checks + + tests: + needs: check + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] - - name: Run black - id: black + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox run: | - black --check . - echo "::set-output name=rc::$?" + tox diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3d31c894..00000000 --- a/.travis.yml +++ /dev/null @@ -1,53 +0,0 @@ -# config file for automatic testing at travis-ci.org -language: python -cache: pip - -before_install: - - sudo apt-get install -y python3-dev - -install: - - pip install --upgrade pip setuptools - - pip install virtualenv tox wheel - - tox --version - -stages: - - check - - test - -script: tox -v - -matrix: - include: - - stage: check - python: 3.6 - env: TOXENV=checks - - - stage: test - dist: xenial - python: "3.6" - env: TOXENV=py36 - - - stage: test - dist: xenial - python: "3.7" - env: TOXENV=py37 - - - stage: test - dist: xenial - python: "3.8" - env: TOXENV=py38 - - - stage: test - dist: bionic - python: "3.9-dev" - env: TOXENV=py39 - - - stage: test - dist: bionic - python: "nightly" - env: TOXENV=py310 - - -jobs: - allow_failures: - - python: "nightly" diff --git a/changelog.d/322.trivial.rst b/changelog.d/322.trivial.rst new file mode 100644 index 00000000..b9394c12 --- /dev/null +++ b/changelog.d/322.trivial.rst @@ -0,0 +1 @@ +Switch from Travis CI to GitHub Actions. diff --git a/tox.ini b/tox.ini index b9515b2b..ce566562 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,17 @@ [tox] envlist = - flake8 - py{36,37,38,39,310} - docs - mypy + checks + py{36,37,38,39} isolated_build = True +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + # 3.10: py310 + [testenv] description = Run test suite for {basepython} From 3a58a6578d7d0f5edd3b9dad58a6e22b515a333f Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 11 Nov 2020 22:16:30 +0100 Subject: [PATCH 29/86] Add Gitter badge to README --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index d4f29819..496bc993 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,7 @@ Quickstart A Python module for `semantic versioning`_. Simplifies comparing versions. |build-status| |python-support| |downloads| |license| |docs| |black| +|Gitter| .. teaser-end @@ -118,3 +119,6 @@ There are other functions to discover. Read on! .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Black Formatter +.. |Gitter| image:: https://badges.gitter.im/python-semver/community.svg + :target: https://gitter.im/python-semver/community + :alt: Gitter From d3d7b22f45462cf7e42fc03ab38d28fc8c8b64ca Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 12 Nov 2020 21:06:54 +0100 Subject: [PATCH 30/86] Improve batches in README * Add batch for isitmaintained.com * Add batch for GitHub Action --- README.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 496bc993..5180288f 100644 --- a/README.rst +++ b/README.rst @@ -10,8 +10,8 @@ Quickstart A Python module for `semantic versioning`_. Simplifies comparing versions. -|build-status| |python-support| |downloads| |license| |docs| |black| -|Gitter| +|GHAction| |python-support| |downloads| |license| |docs| |black| +|Gitter| |openissues| .. teaser-end @@ -100,9 +100,6 @@ There are other functions to discover. Read on! .. |latest-version| image:: https://img.shields.io/pypi/v/semver.svg :alt: Latest version on PyPI :target: https://pypi.org/project/semver -.. |build-status| image:: https://travis-ci.com/python-semver/python-semver.svg?branch=master - :alt: Build status - :target: https://travis-ci.com/python-semver/python-semver .. |python-support| image:: https://img.shields.io/pypi/pyversions/semver.svg :target: https://pypi.org/project/semver :alt: Python versions @@ -122,3 +119,8 @@ There are other functions to discover. Read on! .. |Gitter| image:: https://badges.gitter.im/python-semver/community.svg :target: https://gitter.im/python-semver/community :alt: Gitter +.. |openissues| image:: http://isitmaintained.com/badge/open/python-semver/python-semver.svg + :target: http://isitmaintained.com/project/python-semver/python-semver + :alt: Percentage of open issues +.. |GHAction| image:: https://github.com/python-semver/python-semver/workflows/Python/badge.svg + :alt: Python From 4eb6fa64c164d38617c3f83bfdb48e69d99291c5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 21 Nov 2020 12:50:51 +0100 Subject: [PATCH 31/86] Clean changelog remove double entries --- CHANGELOG.rst | 52 --------------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2e3f97a5..879a5bdd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,58 +16,6 @@ in our repository. .. towncrier release notes start -Version 3.0.0-dev.2 -=================== - -:Released: 2020-11-04 -:Maintainer: - - -Improved Documentation ----------------------- - -* :gh:`312`: Rework "Usage" section. - - * Mention the rename of :class:`~semver.version.VersionInfo` to - :class:`~semver.version.Version` class - * Remove semver. prefix in doctests to make examples shorter - * Correct some references to dunder methods like - :func:`~.semver.version.Version.__getitem__`, - :func:`~.semver.version.Version.__gt__` etc. - * Remove inconsistencies and mention module level function as - deprecated and discouraged from using - * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example - - - ----- - - -Version 3.0.0-dev.2 -=================== - -:Released: 2020-11-04 -:Maintainer: - - -Improved Documentation ----------------------- - -* :gh:`312`: Rework "Usage" section. - - * Mention the rename of :class:`~semver.version.VersionInfo` to - :class:`~semver.version.Version` class - * Remove semver. prefix in doctests to make examples shorter - * Correct some references to dunder methods like :func:`__getitem__`, - :func:`__gt__` etc. - * Remove inconsistencies and mention module level function as - deprecated and discouraged from using - * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example - - - ----- - Version 3.0.0-dev.2 =================== From bc3f96a206440c3a52a4123c45329bc4ff074d96 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Fri, 11 Dec 2020 23:49:34 +0100 Subject: [PATCH 32/86] Amend Contributing with link to GH Discussion page --- CONTRIBUTING.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ac40563b..5fd75ab2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -8,14 +8,18 @@ The semver source code is managed using Git and is hosted on GitHub:: git clone git://github.com/python-semver/python-semver -Reporting Bugs and Feedback ---------------------------- +Reporting Bugs and Asking Questions +----------------------------------- If you think you have encountered a bug in semver or have an idea for a new -feature? Great! We like to hear from you. +feature? Great! We like to hear from you! -First, take the time to look into our GitHub `issues`_ tracker if -this already covered. If not, changes are good that we avoid double work. +There are several options to participate: + +* Open a new topic on our `GitHub discussion `_ page. + Tell us our ideas or ask your questions. + +* Look into our GitHub `issues`_ tracker or open a new issue. Prerequisites @@ -229,4 +233,4 @@ Adding a Changelog Entry .. _Semantic Versioning: https://semver.org .. _Sphinx style: https://sphinx-rtd-tutorial.rtfd.io/en/latest/docstrings.html .. _tox: https://tox.rtfd.org/ - +.. _gh_discussions: https://github.com/python-semver/python-semver/discussions From 07cacb57a158c4627a424dd3bb6ba4c8bba9d73b Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Fri, 11 Dec 2020 23:50:30 +0100 Subject: [PATCH 33/86] Change README (Gitter -> GH Discussions) * Remove Gitter badge * Add GitHub Discussions badge --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5180288f..5c28cc69 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ Quickstart A Python module for `semantic versioning`_. Simplifies comparing versions. |GHAction| |python-support| |downloads| |license| |docs| |black| -|Gitter| |openissues| +|openissues| |GHDiscussion| .. teaser-end @@ -124,3 +124,6 @@ There are other functions to discover. Read on! :alt: Percentage of open issues .. |GHAction| image:: https://github.com/python-semver/python-semver/workflows/Python/badge.svg :alt: Python +.. |GHDiscussion| image:: https://shields.io/badge/GitHub-%20Discussions-green?logo=github + :target: https://github.com/python-semver/python-semver/discussions + :alt: GitHub Discussion From 198686009597ab27fb797b7865b2551a341144b2 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 12 Dec 2020 00:37:27 +0100 Subject: [PATCH 34/86] Create dependabot.yml --- .github/dependabot.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..0c5d1c56 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + day: "friday" + labels: + - "enhancement" + commit-message: + prefix: "pip" + # Allow up to 10 open pull requests for pip dependencies + open-pull-requests-limit: 5 From e7b0c088a15a003417b748fb6951b43771702a2d Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 17 Dec 2020 09:42:54 +0100 Subject: [PATCH 35/86] Add config.yml for GitHub issues Configure the template chooser and provide some additional information. --- .github/ISSUE_TEMPLATE/config.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..640cced4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Community Support + url: https://github.com/python-semver/python-semver/discussions + about: Ask and answer questions in our discussion forum. + - name: Documentation + url: https://python-semver.readthedocs.io/ + about: Find more information in our documentation. From 4aff99ea74cfd5fc8bad31e835d9d09b64af6319 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 17 Dec 2020 16:34:54 +0100 Subject: [PATCH 36/86] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..d4262f95 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master, maint/v2 ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '45 16 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 0db94e6cac797d9320b6817e3beda07ad2c48372 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 17 Dec 2020 16:45:22 +0100 Subject: [PATCH 37/86] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d4262f95..bff8173b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,7 +18,7 @@ on: # The branches below must be a subset of the branches above branches: [ master ] schedule: - - cron: '45 16 * * 5' + - cron: '50 16 * * 5' jobs: analyze: From f74e3a3cf658de08f5313c3aa868ed770a4a2c65 Mon Sep 17 00:00:00 2001 From: Zain Patel Date: Wed, 21 Apr 2021 17:06:00 +0100 Subject: [PATCH 38/86] Improve documentation for semver max/min; fix #337 --- docs/migratetosemver3.rst | 4 ++-- docs/usage.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/migratetosemver3.rst b/docs/migratetosemver3.rst index d6d90954..d977bc03 100644 --- a/docs/migratetosemver3.rst +++ b/docs/migratetosemver3.rst @@ -20,7 +20,7 @@ Use Version instead of VersionInfo The :class:`VersionInfo` has been renamed to :class:`Version` to have a more succinct name. An alias has been created to preserve compatibility but -using old name has been deprecated. +using the old name has been deprecated. If you still need the old version, use this line: @@ -39,4 +39,4 @@ import it from :mod:`semver.cli` in the future: .. code-block:: python - from semver.cli import cmd_bump \ No newline at end of file + from semver.cli import cmd_bump diff --git a/docs/usage.rst b/docs/usage.rst index ad44ecaf..eb4cc25b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -546,9 +546,9 @@ For example, here are the maximum and minimum versions of a list of version stri .. code-block:: python - >>> str(max(map(Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> max(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) '2.1.0' - >>> str(min(map(Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> min(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) '0.4.99' And the same can be done with tuples: From 0398baaa2fdb080b6e59161e6c0217f2025d8a42 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 22 Apr 2021 13:34:42 +0200 Subject: [PATCH 39/86] Fix type definition problem Define new type "Decorator" for function "deprecated" to avoid this mypy error: src/semver/_deprecated.py:69: error: Incompatible return value type (got "Callable[[VarArg(Any), KwArg(Any)], Callable[..., F]]", expected "Union[Callable[..., F], partial[Any]]") Co-authored-by: Thomas Laferriere --- src/semver/_deprecated.py | 6 +++--- src/semver/_types.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py index 545a2438..61ceae12 100644 --- a/src/semver/_deprecated.py +++ b/src/semver/_deprecated.py @@ -7,11 +7,11 @@ import warnings from functools import partial, wraps from types import FrameType -from typing import Type, Union, Callable, cast +from typing import Type, Callable, cast from . import cli from .version import Version -from ._types import F, String +from ._types import Decorator, F, String def deprecated( @@ -19,7 +19,7 @@ def deprecated( replace: str = None, version: str = None, category: Type[Warning] = DeprecationWarning, -) -> Union[Callable[..., F], partial]: +) -> Decorator: """ Decorates a function to output a deprecation warning. diff --git a/src/semver/_types.py b/src/semver/_types.py index 4f004a29..7afb6ff0 100644 --- a/src/semver/_types.py +++ b/src/semver/_types.py @@ -1,5 +1,6 @@ """Typing for semver.""" +from functools import partial from typing import Union, Optional, Tuple, Dict, Iterable, Callable, TypeVar VersionPart = Union[int, Optional[str]] @@ -8,3 +9,4 @@ VersionIterator = Iterable[VersionPart] String = Union[str, bytes] F = TypeVar("F", bound=Callable) +Decorator = Union[Callable[..., F], partial] From 4d2df08c9a489db6ceaf0ebbef692c092393f124 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 10 Jan 2022 21:11:46 +0100 Subject: [PATCH 40/86] Start supporting Python 3.10 --- .github/workflows/python-testing.yml | 2 +- changelog.d/347.trivial.rst | 1 + pyproject.toml | 2 +- setup.cfg | 1 + tox.ini | 4 ++-- 5 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog.d/347.trivial.rst diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 1147d288..8f36dbc9 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -49,7 +49,7 @@ jobs: strategy: max-parallel: 5 matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v1 diff --git a/changelog.d/347.trivial.rst b/changelog.d/347.trivial.rst new file mode 100644 index 00000000..2d44ceb1 --- /dev/null +++ b/changelog.d/347.trivial.rst @@ -0,0 +1 @@ +Support Python 3.10 in GitHub Action and other config files. diff --git a/pyproject.toml b/pyproject.toml index 1b406dac..e58eb25a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 -target-version = ['py36', 'py37', 'py38'] +target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] # diff = true exclude = ''' ( diff --git a/setup.cfg b/setup.cfg index 681240ac..9467709c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Topic :: Software Development :: Libraries :: Python Modules license = BSD diff --git a/tox.ini b/tox.ini index ce566562..8c7eb5e5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = checks - py{36,37,38,39} + py{36,37,38,39,310} isolated_build = True [gh-actions] @@ -10,7 +10,7 @@ python = 3.7: py37 3.8: py38 3.9: py39 - # 3.10: py310 + 3.10: py310 [testenv] From 221bfba902d13acc640c44602f8a090fb68215f3 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 19 Jan 2022 21:35:09 +0100 Subject: [PATCH 41/86] Fix #315: Create 3.0.0-dev.3 * Improve entries for PyPI in setup.cfg * Remove key "download_url" as it points to a broken URL * Add Changelog entry pointing to RTD * Raise version to 3.0.0-dev.3 * Update release procedure * Update Black formatter config * Replace "exclude" with "extend-exclude" * Ignore all *.py files in project's root directory * Include "setup.py" explicity --- changelog.d/315.doc.rst | 1 + docs/usage.rst | 2 +- pyproject.toml | 21 +++++---------- release-procedure.md | 58 +++++++++++++++++++++++++++++------------ setup.cfg | 2 +- src/semver/__about__.py | 2 +- 6 files changed, 53 insertions(+), 33 deletions(-) create mode 100644 changelog.d/315.doc.rst diff --git a/changelog.d/315.doc.rst b/changelog.d/315.doc.rst new file mode 100644 index 00000000..77ca8ba5 --- /dev/null +++ b/changelog.d/315.doc.rst @@ -0,0 +1 @@ +Improve release procedure text diff --git a/docs/usage.rst b/docs/usage.rst index eb4cc25b..f6983d17 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -26,7 +26,7 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.0-dev.2' + '3.0.0-dev.3' Creating a Version diff --git a/pyproject.toml b/pyproject.toml index e58eb25a..769b13d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,20 +11,13 @@ build-backend = "setuptools.build_meta" line-length = 88 target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] # diff = true -exclude = ''' -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.mypy_cache - | \.tox - | \.venv - | \.env - | _build - | build - | dist - )/ -) +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +^/*.py +''' +include = ''' +^/setup.py ''' [tool.towncrier] diff --git a/release-procedure.md b/release-procedure.md index db9ed1b5..251f23f0 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -5,22 +5,31 @@ create a new release. ## Prepare the Release -1. Verify that issues about new release are closed https://github.com/python-semver/python-semver/issues. +1. Verify: -1. Verify that no pull requests that should be included in this release haven't been left out https://github.com/python-semver/python-semver/pulls. + * all issues for a new release are closed: . -1. Verify that continuous integration for latest build was passing https://travis-ci.com/python-semver/python-semver. + * that all pull requests that should be included in this release are merged: . -1. Create a new branch `release/VERSION`. + * that continuous integration for latest build was passing: . + +1. Create a new branch `release/`. 1. If one or several supported Python versions have been removed or added, verify that the 3 following files have been updated: - * [setup.py](https://github.com/python-semver/python-semver/blob/master/setup.py) - * [tox.ini](https://github.com/python-semver/python-semver/blob/master/tox.ini) - * [.travis.yml](https://github.com/python-semver/python-semver/blob/master/.travis.yml) + * `setup.cfg` + * `tox.ini` + * `.git/workflows/pythonpackage.yml` + +1. Verify that the version has been updated and follow + : + + * `src/semver/__about__.py` + * `docs/usage.rst` 1. Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/python-semver/python-semver/blob/master/CONTRIBUTORS). -1. Verify that `__version__` in [semver.py](https://github.com/python-semver/python-semver/blob/master/semver.py) have been updated and follow https://semver.org. + +1. Check if all changelog entries are created. If some are missing, [create them](https://python-semver.readthedocs.io/en/latest/development.html#adding-a-changelog-entry). 1. Show the new draft [CHANGELOG](https://github.com/python-semver/python-semver/blob/master/CHANGELOG.rst) entry for the latest release with: @@ -36,32 +45,47 @@ create a new release. $ tox -e docs +1. Commit all changes, push, and create a pull request. + ## Create the New Release -1. Ensure that long description (ie [README.rst](https://github.com/python-semver/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` +1. Ensure that long description ([README.rst](https://github.com/python-semver/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` + +1. Clean up your local Git repository. Be careful, + as it **will remove all files** which are not + versioned by Git: + + $ git clean -xfd + + Before you create your distribution files, clean + the directory too: + + $ rm dist/* + +1. Create the distribution files (wheel and source): + + $ tox -e prepare-dist 1. Upload the wheel and source to TestPyPI first: - ```bash - $ git clean -xfd - $ rm dist/* - $ python3 setup.py sdist bdist_wheel + ```bash $ twine upload --repository-url https://test.pypi.org/legacy/ dist/* ``` - If you have a `~/.pypirc` with a `testpyi` section, the upload can be + If you have a `~/.pypirc` with a `testpypi` section, the upload can be simplified: - $ twine upload --repository testpyi dist/* + $ twine upload --repository testpypi dist/* 1. Check if everything is okay with the wheel. + Check also the web site `https://test.pypi.org/project//` 1. Upload to PyPI: ```bash $ git clean -xfd - $ python setup.py register sdist bdist_wheel + $ tox -e prepare-dist $ twine upload dist/* ``` @@ -78,4 +102,6 @@ create a new release. document the new release. Usually it's enough to take it from a commit message or the tag description. +1. Announce it in . + You're done! Celebrate! diff --git a/setup.cfg b/setup.cfg index 9467709c..de2d226c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,8 +14,8 @@ author_email = k-bx@k-bx.com maintainer = Sebastien Celles, Tom Schraitle maintainer_email = s.celles@gmail.com url = https://github.com/python-semver/python-semver -download_url = https://github.com/python-semver/python-semver/downloads project_urls = + Changelog = https://python-semver.readthedocs.io/en/latest/changelog.html Documentation = https://python-semver.rtfd.io Releases = https://github.com/python-semver/python-semver/releases Bug Tracker = https://github.com/python-semver/python-semver/issues diff --git a/src/semver/__about__.py b/src/semver/__about__.py index 1f5fcae5..fa448ebe 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -16,7 +16,7 @@ """ #: Semver version -__version__ = "3.0.0-dev.2" +__version__ = "3.0.0-dev.3" #: Original semver author __author__ = "Kostiantyn Rybnikov" From 47f9f346ddb0a920b7a65abdbee50c97bdc9e8a5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 19 Jan 2022 22:27:05 +0100 Subject: [PATCH 42/86] Update changelog --- CHANGELOG.rst | 85 +++++++++++++++++++++++++++++++++++++ changelog.d/309.trivial.rst | 17 -------- changelog.d/310.bugfix.rst | 3 -- changelog.d/312.doc.rst | 11 ----- changelog.d/313.trivial.rst | 3 -- changelog.d/315.doc.rst | 1 - changelog.d/316.trivial.rst | 10 ----- changelog.d/319.trivial.rst | 4 -- changelog.d/322.trivial.rst | 1 - changelog.d/347.trivial.rst | 1 - 10 files changed, 85 insertions(+), 51 deletions(-) delete mode 100644 changelog.d/309.trivial.rst delete mode 100644 changelog.d/310.bugfix.rst delete mode 100644 changelog.d/312.doc.rst delete mode 100644 changelog.d/313.trivial.rst delete mode 100644 changelog.d/315.doc.rst delete mode 100644 changelog.d/316.trivial.rst delete mode 100644 changelog.d/319.trivial.rst delete mode 100644 changelog.d/322.trivial.rst delete mode 100644 changelog.d/347.trivial.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 879a5bdd..3173507f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,91 @@ in our repository. .. towncrier release notes start +Version 3.0.0-dev.3 +=================== + +:Released: 2022-01-19 +:Maintainer: Tom Schraitle + + +Bug Fixes +--------- + +* :gh:`310`: Rework API documentation. + Follow a more "semi-manual" attempt and add auto directives + into :file:`docs/api.rst`. + + + +Improved Documentation +---------------------- + +* :gh:`312`: Rework "Usage" section. + + * Mention the rename of :class:`~semver.version.VersionInfo` to + :class:`~semver.version.Version` class + * Remove semver. prefix in doctests to make examples shorter + * Correct some references to dunder methods like + :func:`~.semver.version.Version.__getitem__`, + :func:`~.semver.version.Version.__gt__` etc. + * Remove inconsistencies and mention module level function as + deprecated and discouraged from using + * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example + +* :gh:`315`: Improve release procedure text + + + +Trivial/Internal Changes +------------------------ + +* :gh:`309`: Some (private) functions from the :mod:`semver.version` + module has been changed. + + The following functions got renamed: + + * function ``semver.version.comparator`` got renamed to + :func:`semver.version._comparator` as it is only useful + inside the :class:`~semver.version.Version` class. + * function ``semver.version.cmp`` got renamed to + :func:`semver.version._cmp` as it is only useful + inside the :class:`~semver.version.Version` class. + + The following functions got integrated into the + :class:`~semver.version.Version` class: + + * function ``semver.version._nat_cmd`` as a classmethod + * function ``semver.version.ensure_str`` + +* :gh:`313`: Correct :file:`tox.ini` for ``changelog`` entry to skip + installation for semver. This should speed up the execution + of towncrier. + +* :gh:`316`: Comparisons of :class:`~semver.version.Version` class and other + types return now a :py:const:`NotImplemented` constant instead + of a :py:exc:`TypeError` exception. + + The `NotImplemented`_ section of the Python documentation recommends + returning this constant when comparing with ``__gt__``, ``__lt__``, + and other comparison operators to "to indicate that the operation is + not implemented with respect to the other type". + + .. _NotImplemented: https://docs.python.org/3/library/constants.html#NotImplemented + +* :gh:`319`: Introduce stages in :file:`.travis.yml` + The config file contains now two stages: check and test. If + check fails, the test stage won't be executed. This could + speed up things when some checks fails. + +* :gh:`322`: Switch from Travis CI to GitHub Actions. + +* :gh:`347`: Support Python 3.10 in GitHub Action and other config files. + + + +---- + + Version 3.0.0-dev.2 =================== diff --git a/changelog.d/309.trivial.rst b/changelog.d/309.trivial.rst deleted file mode 100644 index 97bbba1e..00000000 --- a/changelog.d/309.trivial.rst +++ /dev/null @@ -1,17 +0,0 @@ -Some (private) functions from the :mod:`semver.version` -module has been changed. - -The following functions got renamed: - -* function ``semver.version.comparator`` got renamed to - :func:`semver.version._comparator` as it is only useful - inside the :class:`~semver.version.Version` class. -* function ``semver.version.cmp`` got renamed to - :func:`semver.version._cmp` as it is only useful - inside the :class:`~semver.version.Version` class. - -The following functions got integrated into the -:class:`~semver.version.Version` class: - -* function ``semver.version._nat_cmd`` as a classmethod -* function ``semver.version.ensure_str`` diff --git a/changelog.d/310.bugfix.rst b/changelog.d/310.bugfix.rst deleted file mode 100644 index 6b042982..00000000 --- a/changelog.d/310.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Rework API documentation. -Follow a more "semi-manual" attempt and add auto directives -into :file:`docs/api.rst`. \ No newline at end of file diff --git a/changelog.d/312.doc.rst b/changelog.d/312.doc.rst deleted file mode 100644 index 6b18eb49..00000000 --- a/changelog.d/312.doc.rst +++ /dev/null @@ -1,11 +0,0 @@ -Rework "Usage" section. - -* Mention the rename of :class:`~semver.version.VersionInfo` to - :class:`~semver.version.Version` class -* Remove semver. prefix in doctests to make examples shorter -* Correct some references to dunder methods like - :func:`~.semver.version.Version.__getitem__`, - :func:`~.semver.version.Version.__gt__` etc. -* Remove inconsistencies and mention module level function as - deprecated and discouraged from using -* Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example diff --git a/changelog.d/313.trivial.rst b/changelog.d/313.trivial.rst deleted file mode 100644 index 963b4f31..00000000 --- a/changelog.d/313.trivial.rst +++ /dev/null @@ -1,3 +0,0 @@ -Correct :file:`tox.ini` for ``changelog`` entry to skip -installation for semver. This should speed up the execution -of towncrier. diff --git a/changelog.d/315.doc.rst b/changelog.d/315.doc.rst deleted file mode 100644 index 77ca8ba5..00000000 --- a/changelog.d/315.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Improve release procedure text diff --git a/changelog.d/316.trivial.rst b/changelog.d/316.trivial.rst deleted file mode 100644 index edb555ff..00000000 --- a/changelog.d/316.trivial.rst +++ /dev/null @@ -1,10 +0,0 @@ -Comparisons of :class:`~semver.version.Version` class and other -types return now a :py:const:`NotImplemented` constant instead -of a :py:exc:`TypeError` exception. - -The `NotImplemented`_ section of the Python documentation recommends -returning this constant when comparing with ``__gt__``, ``__lt__``, -and other comparison operators to "to indicate that the operation is -not implemented with respect to the other type". - -.. _NotImplemented: https://docs.python.org/3/library/constants.html#NotImplemented \ No newline at end of file diff --git a/changelog.d/319.trivial.rst b/changelog.d/319.trivial.rst deleted file mode 100644 index c1c259a9..00000000 --- a/changelog.d/319.trivial.rst +++ /dev/null @@ -1,4 +0,0 @@ -Introduce stages in :file:`.travis.yml` -The config file contains now two stages: check and test. If -check fails, the test stage won't be executed. This could -speed up things when some checks fails. \ No newline at end of file diff --git a/changelog.d/322.trivial.rst b/changelog.d/322.trivial.rst deleted file mode 100644 index b9394c12..00000000 --- a/changelog.d/322.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Switch from Travis CI to GitHub Actions. diff --git a/changelog.d/347.trivial.rst b/changelog.d/347.trivial.rst deleted file mode 100644 index 2d44ceb1..00000000 --- a/changelog.d/347.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Support Python 3.10 in GitHub Action and other config files. From 769083c17773256e66b0c9a98d19c4f365b22d53 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Fri, 21 Jan 2022 08:04:20 +0100 Subject: [PATCH 43/86] Improve release procedure * Missing merge step * Introduce the subsection "Finish the release" --- release-procedure.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/release-procedure.md b/release-procedure.md index 251f23f0..d6c1701e 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -11,7 +11,8 @@ create a new release. * that all pull requests that should be included in this release are merged: . - * that continuous integration for latest build was passing: . + * that continuous integration for latest build was passing: + . 1. Create a new branch `release/`. @@ -81,6 +82,8 @@ create a new release. 1. Check if everything is okay with the wheel. Check also the web site `https://test.pypi.org/project//` +1. If everything looks fine, merge the pull request. + 1. Upload to PyPI: ```bash @@ -91,16 +94,24 @@ create a new release. 1. Go to https://pypi.org/project/semver/ to verify that new version is online and the page is rendered correctly. -1. Tag commit and push to GitHub using command line interface: +# Finish the release - ```bash - $ git tag -a x.x.x -m 'Version x.x.x' - $ git push python-semver master --tags - ``` +1. Create a tag: + + $ git tag -a x.x.x + + It's recommended to use the generated Tox output + from the Changelog. + +1. Push the tag: + + $ git push --tags 1. In [GitHub Release page](https://github.com/python-semver/python-semver/release) document the new release. - Usually it's enough to take it from a commit message or the tag description. + Select the tag from the last step and copy the + content of the tag description into the release + description. 1. Announce it in . From 098dccf699943dd0ae4e5e001e9b3f2c7cb40d42 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 22 Jan 2022 19:39:38 +0100 Subject: [PATCH 44/86] Split usage section into different files The usage.rst file become quite big and it is hard to maintain. It's now splitted up into different files in the subfolder "usage/". The index.rst collects all these files. --- docs/index.rst | 2 +- docs/usage.rst | 789 ------------------ docs/usage/access-parts-of-a-version.rst | 43 + docs/usage/access-parts-through-index.rst | 48 ++ docs/usage/check-valid-semver-version.rst | 12 + docs/{ => usage}/coerce.py | 0 .../compare-versions-through-expression.rst | 26 + docs/usage/compare-versions.rst | 99 +++ .../convert-version-into-different-types.rst | 26 + docs/usage/create-a-version.rst | 100 +++ docs/usage/create-subclasses-from-version.rst | 33 + docs/usage/deal-with-invalid-versions.rst | 32 + docs/usage/determine-version-equality.rst | 25 + docs/usage/display-deprecation-warnings.rst | 34 + .../get-min-and-max-of-multiple-versions.rst | 51 ++ ...ncrease-parts-of-a-version_prereleases.rst | 22 + docs/usage/index.rst | 24 + docs/usage/parse-version-string.rst | 8 + docs/usage/raise-parts-of-a-version.rst | 30 + docs/usage/replace-deprecated-functions.rst | 110 +++ docs/usage/replace-parts-of-a-version.rst | 30 + docs/usage/semver-version.rst | 7 + docs/usage/semver_org-version.rst | 10 + docs/{ => usage}/semverwithvprefix.py | 0 tests/conftest.py | 2 +- 25 files changed, 772 insertions(+), 791 deletions(-) delete mode 100644 docs/usage.rst create mode 100644 docs/usage/access-parts-of-a-version.rst create mode 100644 docs/usage/access-parts-through-index.rst create mode 100644 docs/usage/check-valid-semver-version.rst rename docs/{ => usage}/coerce.py (100%) create mode 100644 docs/usage/compare-versions-through-expression.rst create mode 100644 docs/usage/compare-versions.rst create mode 100644 docs/usage/convert-version-into-different-types.rst create mode 100644 docs/usage/create-a-version.rst create mode 100644 docs/usage/create-subclasses-from-version.rst create mode 100644 docs/usage/deal-with-invalid-versions.rst create mode 100644 docs/usage/determine-version-equality.rst create mode 100644 docs/usage/display-deprecation-warnings.rst create mode 100644 docs/usage/get-min-and-max-of-multiple-versions.rst create mode 100644 docs/usage/increase-parts-of-a-version_prereleases.rst create mode 100644 docs/usage/index.rst create mode 100644 docs/usage/parse-version-string.rst create mode 100644 docs/usage/raise-parts-of-a-version.rst create mode 100644 docs/usage/replace-deprecated-functions.rst create mode 100644 docs/usage/replace-parts-of-a-version.rst create mode 100644 docs/usage/semver-version.rst create mode 100644 docs/usage/semver_org-version.rst rename docs/{ => usage}/semverwithvprefix.py (100%) diff --git a/docs/index.rst b/docs/index.rst index 405d9e27..3e2771a0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ Semver |version| -- Semantic Versioning :hidden: install - usage + usage/index migratetosemver3 development api diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index f6983d17..00000000 --- a/docs/usage.rst +++ /dev/null @@ -1,789 +0,0 @@ -Using semver -============ - -The :mod:`semver` module can store a version in the :class:`~semver.version.Version` class. -For historical reasons, a version can be also stored as a string or dictionary. - -Each type can be converted into the other, if the minimum requirements -are met. - - -Getting the Implemented semver.org Version ------------------------------------------- - -The semver.org page is the authoritative specification of how semantic -versioning is defined. -To know which version of semver.org is implemented in the semver library, -use the following constant:: - - >>> semver.SEMVER_SPEC_VERSION - '2.0.0' - - -Getting the Version of semver ------------------------------ - -To know the version of semver itself, use the following construct:: - - >>> semver.__version__ - '3.0.0-dev.3' - - -Creating a Version ------------------- - -.. versionchanged:: 3.0.0 - - The former :class:`~semver.version.VersionInfo` - has been renamed to :class:`~semver.version.Version`. - -The preferred way to create a new version is with the class -:class:`~semver.version.Version`. - -.. note:: - - In the previous major release semver 2 it was possible to - create a version with module level functions. - However, module level functions are marked as *deprecated* - since version 2.x.y now. - These functions will be removed in semver 3.1.0. - For details, see the sections :ref:`sec_replace_deprecated_functions` - and :ref:`sec_display_deprecation_warnings`. - -A :class:`~semver.version.Version` instance can be created in different ways: - -* From a Unicode string:: - - >>> from semver.version import Version - >>> Version.parse("3.4.5-pre.2+build.4") - Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - >>> Version.parse(u"5.3.1") - Version(major=5, minor=3, patch=1, prerelease=None, build=None) - -* From a byte string:: - - >>> Version.parse(b"2.3.4") - Version(major=2, minor=3, patch=4, prerelease=None, build=None) - -* From individual parts by a dictionary:: - - >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - >>> Version(**d) - Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - - Keep in mind, the ``major``, ``minor``, ``patch`` parts has to - be positive integers or strings: - - >>> d = {'major': -3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - >>> Version(**d) - Traceback (most recent call last): - ... - ValueError: 'major' is negative. A version can only be positive. - - As a minimum requirement, your dictionary needs at least the ``major`` - key, others can be omitted. You get a ``TypeError`` if your - dictionary contains invalid keys. - Only the keys ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` - are allowed. - -* From a tuple:: - - >>> t = (3, 5, 6) - >>> Version(*t) - Version(major=3, minor=5, patch=6, prerelease=None, build=None) - - You can pass either an integer or a string for ``major``, ``minor``, or - ``patch``:: - - >>> Version("3", "5", 6) - Version(major=3, minor=5, patch=6, prerelease=None, build=None) - -The old, deprecated module level functions are still available but -using them are discoraged. They are available to convert old code -to semver3. - -If you need them, they return different builtin objects (string and dictionary). -Keep in mind, once you have converted a version into a string or dictionary, -it's an ordinary builtin object. It's not a special version object like -the :class:`~semver.version.Version` class anymore. - -Depending on your use case, the following methods are available: - -* From individual version parts into a string - - In some cases you only need a string from your version data:: - - >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') - '3.4.5-pre.2+build.4' - -* From a string into a dictionary - - To access individual parts, you can use the function :func:`semver.parse`:: - - >>> semver.parse("3.4.5-pre.2+build.4") - OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) - - If you pass an invalid version string you will get a :py:exc:`ValueError`:: - - >>> semver.parse("1.2") - Traceback (most recent call last): - ... - ValueError: 1.2 is not valid SemVer string - - -Parsing a Version String ------------------------- - -"Parsing" in this context means to identify the different parts in a string. -Use the function :func:`Version.parse `:: - - >>> Version.parse("3.4.5-pre.2+build.4") - Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - - -Checking for a Valid Semver Version ------------------------------------ - -If you need to check a string if it is a valid semver version, use the -classmethod :func:`Version.isvalid `: - -.. code-block:: python - - >>> Version.isvalid("1.0.0") - True - >>> Version.isvalid("invalid") - False - - -.. _sec.properties.parts: - -Accessing Parts of a Version Through Names ------------------------------------------- - -The :class:`~semver.version.Version` class contains attributes to access the different -parts of a version: - -.. code-block:: python - - >>> v = Version.parse("3.4.5-pre.2+build.4") - >>> v.major - 3 - >>> v.minor - 4 - >>> v.patch - 5 - >>> v.prerelease - 'pre.2' - >>> v.build - 'build.4' - -However, the attributes are read-only. You cannot change any of the above attributes. -If you do, you get an :py:exc:`AttributeError`:: - - >>> v.minor = 5 - Traceback (most recent call last): - ... - AttributeError: attribute 'minor' is readonly - -If you need to replace different parts of a version, refer to section :ref:`sec.replace.parts`. - -In case you need the different parts of a version stepwise, iterate over the :class:`~semver.version.Version` instance:: - - >>> for item in Version.parse("3.4.5-pre.2+build.4"): - ... print(item) - 3 - 4 - 5 - pre.2 - build.4 - >>> list(Version.parse("3.4.5-pre.2+build.4")) - [3, 4, 5, 'pre.2', 'build.4'] - - -.. _sec.getitem.parts: - -Accessing Parts Through Index Numbers -------------------------------------- - -.. versionadded:: 2.10.0 - -Another way to access parts of a version is to use an index notation. The underlying -:class:`~semver.version.Version` object allows to access its data through -the magic method :func:`~semver.version.Version.__getitem__`. - -For example, the ``major`` part can be accessed by index number 0 (zero). -Likewise the other parts: - -.. code-block:: python - - >>> ver = Version.parse("10.3.2-pre.5+build.10") - >>> ver[0], ver[1], ver[2], ver[3], ver[4] - (10, 3, 2, 'pre.5', 'build.10') - -If you need more than one part at the same time, use the slice notation: - -.. code-block:: python - - >>> ver[0:3] - (10, 3, 2) - -Or, as an alternative, you can pass a :func:`slice` object: - -.. code-block:: python - - >>> sl = slice(0,3) - >>> ver[sl] - (10, 3, 2) - -Negative numbers or undefined parts raise an :py:exc:`IndexError` exception: - -.. code-block:: python - - >>> ver = Version.parse("10.3.2") - >>> ver[3] - Traceback (most recent call last): - ... - IndexError: Version part undefined - >>> ver[-2] - Traceback (most recent call last): - ... - IndexError: Version index cannot be negative - -.. _sec.replace.parts: - -Replacing Parts of a Version ----------------------------- - -If you want to replace different parts of a version, but leave other parts -unmodified, use the function :func:`replace `: - -* From a :class:`Version ` instance:: - - >>> version = semver.Version.parse("1.4.5-pre.1+build.6") - >>> version.replace(major=2, minor=2) - Version(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') - -* From a version string:: - - >>> semver.replace("1.4.5-pre.1+build.6", major=2) - '2.4.5-pre.1+build.6' - -If you pass invalid keys you get an exception:: - - >>> semver.replace("1.2.3", invalidkey=2) - Traceback (most recent call last): - ... - TypeError: replace() got 1 unexpected keyword argument(s): invalidkey - >>> version = semver.Version.parse("1.4.5-pre.1+build.6") - >>> version.replace(invalidkey=2) - Traceback (most recent call last): - ... - TypeError: replace() got 1 unexpected keyword argument(s): invalidkey - - -.. _sec.convert.versions: - -Converting a Version instance into Different Types ------------------------------------------------------- - -Sometimes it is needed to convert a :class:`Version ` instance into -a different type. For example, for displaying or to access all parts. - -It is possible to convert a :class:`Version ` instance: - -* Into a string with the builtin function :func:`str`:: - - >>> str(Version.parse("3.4.5-pre.2+build.4")) - '3.4.5-pre.2+build.4' - -* Into a dictionary with :func:`to_dict `:: - - >>> v = Version(major=3, minor=4, patch=5) - >>> v.to_dict() - OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) - -* Into a tuple with :func:`to_tuple `:: - - >>> v = Version(major=5, minor=4, patch=2) - >>> v.to_tuple() - (5, 4, 2, None, None) - - -Raising Parts of a Version --------------------------- - -The ``semver`` module contains the following functions to raise parts of -a version: - -* :func:`Version.bump_major `: raises the major part and set all other parts to - zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`Version.bump_minor `: raises the minor part and sets ``patch`` to zero. - Set ``prerelease`` and ``build`` to ``None``. -* :func:`Version.bump_patch `: raises the patch part. Set ``prerelease`` and - ``build`` to ``None``. -* :func:`Version.bump_prerelease `: raises the prerelease part and set - ``build`` to ``None``. -* :func:`Version.bump_build `: raises the build part. - -.. code-block:: python - - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_major()) - '4.0.0' - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_minor()) - '3.5.0' - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_patch()) - '3.4.6' - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) - '3.4.5-pre.3' - >>> str(Version.parse("3.4.5-pre.2+build.4").bump_build()) - '3.4.5-pre.2+build.5' - -Likewise the module level functions :func:`semver.bump_major`. - - -Increasing Parts of a Version Taking into Account Prereleases -------------------------------------------------------------- - -.. versionadded:: 2.10.0 - Added :func:`Version.next_version `. - -If you want to raise your version and take prereleases into account, -the function :func:`next_version ` -would perhaps a better fit. - - -.. code-block:: python - - >>> v = Version.parse("3.4.5-pre.2+build.4") - >>> str(v.next_version(part="prerelease")) - '3.4.5-pre.3' - >>> str(Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) - '3.4.5' - >>> str(Version.parse("3.4.5+build.4").next_version(part="patch")) - '3.4.5' - >>> str(Version.parse("0.1.4").next_version("prerelease")) - '0.1.5-rc.1' - - -Comparing Versions ------------------- - -To compare two versions depends on your type: - -* **Two strings** - - Use :func:`semver.compare`:: - - >>> semver.compare("1.0.0", "2.0.0") - -1 - >>> semver.compare("2.0.0", "1.0.0") - 1 - >>> semver.compare("2.0.0", "2.0.0") - 0 - - The return value is negative if ``version1 < version2``, zero if - ``version1 == version2`` and strictly positive if ``version1 > version2``. - -* **Two** :class:`Version ` **instances** - - Use the specific operator. Currently, the operators ``<``, - ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: - - >>> v1 = Version.parse("3.4.5") - >>> v2 = Version.parse("3.5.1") - >>> v1 < v2 - True - >>> v1 > v2 - False - -* **A** :class:`Version ` **type and a** :func:`tuple` **or** :func:`list` - - Use the operator as with two :class:`Version ` types:: - - >>> v = Version.parse("3.4.5") - >>> v > (1, 0) - True - >>> v < [3, 5] - True - - The opposite does also work:: - - >>> (1, 0) < v - True - >>> [3, 5] > v - True - -* **A** :class:`Version ` **type and a** :func:`str` - - You can use also raw strings to compare:: - - >>> v > "1.0.0" - True - >>> v < "3.5.0" - True - - The opposite does also work:: - - >>> "1.0.0" < v - True - >>> "3.5.0" > v - True - - However, if you compare incomplete strings, you get a :py:exc:`ValueError` exception:: - - >>> v > "1.0" - Traceback (most recent call last): - ... - ValueError: 1.0 is not valid SemVer string - -* **A** :class:`Version ` **type and a** :func:`dict` - - You can also use a dictionary. In contrast to strings, you can have an "incomplete" - version (as the other parts are set to zero):: - - >>> v > dict(major=1) - True - - The opposite does also work:: - - >>> dict(major=1) < v - True - - If the dictionary contains unknown keys, you get a :py:exc:`TypeError` exception:: - - >>> v > dict(major=1, unknown=42) - Traceback (most recent call last): - ... - TypeError: ... got an unexpected keyword argument 'unknown' - - -Other types cannot be compared. - -If you need to convert some types into others, refer to :ref:`sec.convert.versions`. - -The use of these comparison operators also implies that you can use builtin -functions that leverage this capability; builtins including, but not limited to: :func:`max`, :func:`min` -(for examples, see :ref:`sec_max_min`) and :func:`sorted`. - - -Determining Version Equality ----------------------------- - -Version equality means for semver, that major, minor, patch, and prerelease -parts are equal in both versions you compare. The build part is ignored. -For example:: - - >>> v = Version.parse("1.2.3-rc4+1e4664d") - >>> v == "1.2.3-rc4+dedbeef" - True - -This also applies when a :class:`Version ` is a member of a set, or a -dictionary key:: - - >>> d = {} - >>> v1 = Version.parse("1.2.3-rc4+1e4664d") - >>> v2 = Version.parse("1.2.3-rc4+dedbeef") - >>> d[v1] = 1 - >>> d[v2] - 1 - >>> s = set() - >>> s.add(v1) - >>> v2 in s - True - - - -Comparing Versions through an Expression ----------------------------------------- - -If you need a more fine-grained approach of comparing two versions, -use the :func:`semver.match` function. It expects two arguments: - -1. a version string -2. a match expression - -Currently, the match expression supports the following operators: - -* ``<`` smaller than -* ``>`` greater than -* ``>=`` greater or equal than -* ``<=`` smaller or equal than -* ``==`` equal -* ``!=`` not equal - -That gives you the following possibilities to express your condition: - -.. code-block:: python - - >>> semver.match("2.0.0", ">=1.0.0") - True - >>> semver.match("1.0.0", ">1.0.0") - False - -.. _sec_max_min: - -Getting Minimum and Maximum of Multiple Versions ------------------------------------------------- -.. versionchanged:: 2.10.2 - The functions :func:`semver.max_ver` and :func:`semver.min_ver` are deprecated in - favor of their builtin counterparts :func:`max` and :func:`min`. - -Since :class:`Version ` implements -:func:`__gt__ ` and -:func:`__lt__ `, it can be used with builtins requiring: - -.. code-block:: python - - >>> max([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) - Version(major=0, minor=2, patch=0, prerelease=None, build=None) - >>> min([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) - Version(major=0, minor=1, patch=0, prerelease=None, build=None) - -Incidentally, using :func:`map`, you can get the min or max version of any number of versions of the same type -(convertible to :class:`Version `). - -For example, here are the maximum and minimum versions of a list of version strings: - -.. code-block:: python - - >>> max(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) - '2.1.0' - >>> min(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) - '0.4.99' - -And the same can be done with tuples: - -.. code-block:: python - - >>> max(map(lambda v: Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() - (2, 1, 0, None, None) - >>> min(map(lambda v: Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() - (0, 4, 99, None, None) - -For dictionaries, it is very similar to finding the max version tuple: see :ref:`sec.convert.versions`. - -The "old way" with :func:`semver.max_ver` or :func:`semver.min_ver` is still available, but not recommended: - -.. code-block:: python - - >>> semver.max_ver("1.0.0", "2.0.0") - '2.0.0' - >>> semver.min_ver("1.0.0", "2.0.0") - '1.0.0' - - -.. _sec_dealing_with_invalid_versions: - -Dealing with Invalid Versions ------------------------------ - -As semver follows the semver specification, it cannot parse version -strings which are considered "invalid" by that specification. The semver -library cannot know all the possible variations so you need to help the -library a bit. - -For example, if you have a version string ``v1.2`` would be an invalid -semver version. -However, "basic" version strings consisting of major, minor, -and patch part, can be easy to convert. The following function extract this -information and returns a tuple with two items: - -.. literalinclude:: coerce.py - :language: python - - -The function returns a *tuple*, containing a :class:`Version ` -instance or None as the first element and the rest as the second element. -The second element (the rest) can be used to make further adjustments. - -For example: - -.. code-block:: python - - >>> coerce("v1.2") - (Version(major=1, minor=2, patch=0, prerelease=None, build=None), '') - >>> coerce("v2.5.2-bla") - (Version(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') - - -.. _sec_replace_deprecated_functions: - -Replacing Deprecated Functions ------------------------------- - -.. versionchanged:: 2.10.0 - The development team of semver has decided to deprecate certain functions on - the module level. The preferred way of using semver is through the - :class:`semver.Version` class. - -The deprecated functions can still be used in version 2.10.0 and above. In version 3 of -semver, the deprecated functions will be removed. - -The following list shows the deprecated functions and how you can replace -them with code which is compatible for future versions: - - -* :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` - - Replace them with the respective methods of the :class:`Version ` - class. - For example, the function :func:`semver.bump_major` is replaced by - :func:`semver.Version.bump_major` and calling the ``str(versionobject)``: - - .. code-block:: python - - >>> s1 = semver.bump_major("3.4.5") - >>> s2 = str(Version.parse("3.4.5").bump_major()) - >>> s1 == s2 - True - - Likewise with the other module level functions. - -* :func:`semver.finalize_version` - - Replace it with :func:`semver.Version.finalize_version`: - - .. code-block:: python - - >>> s1 = semver.finalize_version('1.2.3-rc.5') - >>> s2 = str(semver.Version.parse('1.2.3-rc.5').finalize_version()) - >>> s1 == s2 - True - -* :func:`semver.format_version` - - Replace it with ``str(versionobject)``: - - .. code-block:: python - - >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') - >>> s2 = str(Version(5, 4, 3, 'pre.2', 'build.1')) - >>> s1 == s2 - True - -* :func:`semver.max_ver` - - Replace it with ``max(version1, version2, ...)`` or ``max([version1, version2, ...])``: - - .. code-block:: python - - >>> s1 = semver.max_ver("1.2.3", "1.2.4") - >>> s2 = str(max(map(Version.parse, ("1.2.3", "1.2.4")))) - >>> s1 == s2 - True - -* :func:`semver.min_ver` - - Replace it with ``min(version1, version2, ...)`` or ``min([version1, version2, ...])``: - - .. code-block:: python - - >>> s1 = semver.min_ver("1.2.3", "1.2.4") - >>> s2 = str(min(map(Version.parse, ("1.2.3", "1.2.4")))) - >>> s1 == s2 - True - -* :func:`semver.parse` - - Replace it with :func:`semver.Version.parse` and - :func:`semver.Version.to_dict`: - - .. code-block:: python - - >>> v1 = semver.parse("1.2.3") - >>> v2 = Version.parse("1.2.3").to_dict() - >>> v1 == v2 - True - -* :func:`semver.parse_version_info` - - Replace it with :func:`semver.Version.parse`: - - .. code-block:: python - - >>> v1 = semver.parse_version_info("3.4.5") - >>> v2 = Version.parse("3.4.5") - >>> v1 == v2 - True - -* :func:`semver.replace` - - Replace it with :func:`semver.Version.replace`: - - .. code-block:: python - - >>> s1 = semver.replace("1.2.3", major=2, patch=10) - >>> s2 = str(Version.parse('1.2.3').replace(major=2, patch=10)) - >>> s1 == s2 - True - - -.. _sec_display_deprecation_warnings: - -Displaying Deprecation Warnings -------------------------------- - -By default, deprecation warnings are `ignored in Python `_. -This also affects semver's own warnings. - -It is recommended that you turn on deprecation warnings in your scripts. Use one of -the following methods: - -* Use the option `-Wd `_ - to enable default warnings: - - * Directly running the Python command:: - - $ python3 -Wd scriptname.py - - * Add the option in the shebang line (something like ``#!/usr/bin/python3``) - after the command:: - - #!/usr/bin/python3 -Wd - -* In your own scripts add a filter to ensure that *all* warnings are displayed: - - .. code-block:: python - - import warnings - warnings.simplefilter("default") - # Call your semver code - - For further details, see the section - `Overriding the default filter `_ - of the Python documentation. - - -.. _sec_creating_subclasses_from_versioninfo: - -Creating Subclasses from Version ------------------------------------- - -If you do not like creating functions to modify the behavior of semver -(as shown in section :ref:`sec_dealing_with_invalid_versions`), you can -also create a subclass of the :class:`Version ` class. - -For example, if you want to output a "v" prefix before a version, -but the other behavior is the same, use the following code: - -.. literalinclude:: semverwithvprefix.py - :language: python - :lines: 4- - - -The derived class :class:`SemVerWithVPrefix` can be used like -the original class: - -.. code-block:: python - - >>> v1 = SemVerWithVPrefix.parse("v1.2.3") - >>> assert str(v1) == "v1.2.3" - >>> print(v1) - v1.2.3 - >>> v2 = SemVerWithVPrefix.parse("v2.3.4") - >>> v2 > v1 - True - >>> bad = SemVerWithVPrefix.parse("1.2.4") - Traceback (most recent call last): - ... - ValueError: '1.2.4': not a valid semantic version tag. Must start with 'v' or 'V' diff --git a/docs/usage/access-parts-of-a-version.rst b/docs/usage/access-parts-of-a-version.rst new file mode 100644 index 00000000..4eb9274f --- /dev/null +++ b/docs/usage/access-parts-of-a-version.rst @@ -0,0 +1,43 @@ +.. _sec.properties.parts: + +Accessing Parts of a Version Through Names +========================================== + +The :class:`~semver.version.Version` class contains attributes to access the different +parts of a version: + +.. code-block:: python + + >>> v = Version.parse("3.4.5-pre.2+build.4") + >>> v.major + 3 + >>> v.minor + 4 + >>> v.patch + 5 + >>> v.prerelease + 'pre.2' + >>> v.build + 'build.4' + +However, the attributes are read-only. You cannot change any of the above attributes. +If you do, you get an :py:exc:`AttributeError`:: + + >>> v.minor = 5 + Traceback (most recent call last): + ... + AttributeError: attribute 'minor' is readonly + +If you need to replace different parts of a version, refer to section :ref:`sec.replace.parts`. + +In case you need the different parts of a version stepwise, iterate over the :class:`~semver.version.Version` instance:: + + >>> for item in Version.parse("3.4.5-pre.2+build.4"): + ... print(item) + 3 + 4 + 5 + pre.2 + build.4 + >>> list(Version.parse("3.4.5-pre.2+build.4")) + [3, 4, 5, 'pre.2', 'build.4'] diff --git a/docs/usage/access-parts-through-index.rst b/docs/usage/access-parts-through-index.rst new file mode 100644 index 00000000..a261fda4 --- /dev/null +++ b/docs/usage/access-parts-through-index.rst @@ -0,0 +1,48 @@ +.. _sec.getitem.parts: + +Accessing Parts Through Index Numbers +===================================== + +.. versionadded:: 2.10.0 + +Another way to access parts of a version is to use an index notation. The underlying +:class:`~semver.version.Version` object allows to access its data through +the magic method :func:`~semver.version.Version.__getitem__`. + +For example, the ``major`` part can be accessed by index number 0 (zero). +Likewise the other parts: + +.. code-block:: python + + >>> ver = Version.parse("10.3.2-pre.5+build.10") + >>> ver[0], ver[1], ver[2], ver[3], ver[4] + (10, 3, 2, 'pre.5', 'build.10') + +If you need more than one part at the same time, use the slice notation: + +.. code-block:: python + + >>> ver[0:3] + (10, 3, 2) + +Or, as an alternative, you can pass a :func:`slice` object: + +.. code-block:: python + + >>> sl = slice(0,3) + >>> ver[sl] + (10, 3, 2) + +Negative numbers or undefined parts raise an :py:exc:`IndexError` exception: + +.. code-block:: python + + >>> ver = Version.parse("10.3.2") + >>> ver[3] + Traceback (most recent call last): + ... + IndexError: Version part undefined + >>> ver[-2] + Traceback (most recent call last): + ... + IndexError: Version index cannot be negative diff --git a/docs/usage/check-valid-semver-version.rst b/docs/usage/check-valid-semver-version.rst new file mode 100644 index 00000000..7aa9615b --- /dev/null +++ b/docs/usage/check-valid-semver-version.rst @@ -0,0 +1,12 @@ +Checking for a Valid Semver Version +=================================== + +If you need to check a string if it is a valid semver version, use the +classmethod :func:`Version.isvalid `: + +.. code-block:: python + + >>> Version.isvalid("1.0.0") + True + >>> Version.isvalid("invalid") + False diff --git a/docs/coerce.py b/docs/usage/coerce.py similarity index 100% rename from docs/coerce.py rename to docs/usage/coerce.py diff --git a/docs/usage/compare-versions-through-expression.rst b/docs/usage/compare-versions-through-expression.rst new file mode 100644 index 00000000..43a5a182 --- /dev/null +++ b/docs/usage/compare-versions-through-expression.rst @@ -0,0 +1,26 @@ +Comparing Versions through an Expression +======================================== + +If you need a more fine-grained approach of comparing two versions, +use the :func:`semver.match` function. It expects two arguments: + +1. a version string +2. a match expression + +Currently, the match expression supports the following operators: + +* ``<`` smaller than +* ``>`` greater than +* ``>=`` greater or equal than +* ``<=`` smaller or equal than +* ``==`` equal +* ``!=`` not equal + +That gives you the following possibilities to express your condition: + +.. code-block:: python + + >>> semver.match("2.0.0", ">=1.0.0") + True + >>> semver.match("1.0.0", ">1.0.0") + False diff --git a/docs/usage/compare-versions.rst b/docs/usage/compare-versions.rst new file mode 100644 index 00000000..b42ba1a7 --- /dev/null +++ b/docs/usage/compare-versions.rst @@ -0,0 +1,99 @@ +Comparing Versions +================== + +To compare two versions depends on your type: + +* **Two strings** + + Use :func:`semver.compare`:: + + >>> semver.compare("1.0.0", "2.0.0") + -1 + >>> semver.compare("2.0.0", "1.0.0") + 1 + >>> semver.compare("2.0.0", "2.0.0") + 0 + + The return value is negative if ``version1 < version2``, zero if + ``version1 == version2`` and strictly positive if ``version1 > version2``. + +* **Two** :class:`Version ` **instances** + + Use the specific operator. Currently, the operators ``<``, + ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: + + >>> v1 = Version.parse("3.4.5") + >>> v2 = Version.parse("3.5.1") + >>> v1 < v2 + True + >>> v1 > v2 + False + +* **A** :class:`Version ` **type and a** :func:`tuple` **or** :func:`list` + + Use the operator as with two :class:`Version ` types:: + + >>> v = Version.parse("3.4.5") + >>> v > (1, 0) + True + >>> v < [3, 5] + True + + The opposite does also work:: + + >>> (1, 0) < v + True + >>> [3, 5] > v + True + +* **A** :class:`Version ` **type and a** :func:`str` + + You can use also raw strings to compare:: + + >>> v > "1.0.0" + True + >>> v < "3.5.0" + True + + The opposite does also work:: + + >>> "1.0.0" < v + True + >>> "3.5.0" > v + True + + However, if you compare incomplete strings, you get a :py:exc:`ValueError` exception:: + + >>> v > "1.0" + Traceback (most recent call last): + ... + ValueError: 1.0 is not valid SemVer string + +* **A** :class:`Version ` **type and a** :func:`dict` + + You can also use a dictionary. In contrast to strings, you can have an "incomplete" + version (as the other parts are set to zero):: + + >>> v > dict(major=1) + True + + The opposite does also work:: + + >>> dict(major=1) < v + True + + If the dictionary contains unknown keys, you get a :py:exc:`TypeError` exception:: + + >>> v > dict(major=1, unknown=42) + Traceback (most recent call last): + ... + TypeError: ... got an unexpected keyword argument 'unknown' + + +Other types cannot be compared. + +If you need to convert some types into others, refer to :ref:`sec.convert.versions`. + +The use of these comparison operators also implies that you can use builtin +functions that leverage this capability; builtins including, but not limited to: :func:`max`, :func:`min` +(for examples, see :ref:`sec_max_min`) and :func:`sorted`. diff --git a/docs/usage/convert-version-into-different-types.rst b/docs/usage/convert-version-into-different-types.rst new file mode 100644 index 00000000..976283d8 --- /dev/null +++ b/docs/usage/convert-version-into-different-types.rst @@ -0,0 +1,26 @@ +.. _sec.convert.versions: + +Converting a Version instance into Different Types +================================================== + +Sometimes it is needed to convert a :class:`Version ` instance into +a different type. For example, for displaying or to access all parts. + +It is possible to convert a :class:`Version ` instance: + +* Into a string with the builtin function :func:`str`:: + + >>> str(Version.parse("3.4.5-pre.2+build.4")) + '3.4.5-pre.2+build.4' + +* Into a dictionary with :func:`to_dict `:: + + >>> v = Version(major=3, minor=4, patch=5) + >>> v.to_dict() + OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) + +* Into a tuple with :func:`to_tuple `:: + + >>> v = Version(major=5, minor=4, patch=2) + >>> v.to_tuple() + (5, 4, 2, None, None) diff --git a/docs/usage/create-a-version.rst b/docs/usage/create-a-version.rst new file mode 100644 index 00000000..3acb4c03 --- /dev/null +++ b/docs/usage/create-a-version.rst @@ -0,0 +1,100 @@ +Creating a Version +================== + +.. versionchanged:: 3.0.0 + + The former :class:`~semver.version.VersionInfo` + has been renamed to :class:`~semver.version.Version`. + +The preferred way to create a new version is with the class +:class:`~semver.version.Version`. + +.. note:: + + In the previous major release semver 2 it was possible to + create a version with module level functions. + However, module level functions are marked as *deprecated* + since version 2.x.y now. + These functions will be removed in semver 3.1.0. + For details, see the sections :ref:`sec_replace_deprecated_functions` + and :ref:`sec_display_deprecation_warnings`. + +A :class:`~semver.version.Version` instance can be created in different ways: + +* From a Unicode string:: + + >>> from semver.version import Version + >>> Version.parse("3.4.5-pre.2+build.4") + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + >>> Version.parse(u"5.3.1") + Version(major=5, minor=3, patch=1, prerelease=None, build=None) + +* From a byte string:: + + >>> Version.parse(b"2.3.4") + Version(major=2, minor=3, patch=4, prerelease=None, build=None) + +* From individual parts by a dictionary:: + + >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> Version(**d) + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + + Keep in mind, the ``major``, ``minor``, ``patch`` parts has to + be positive integers or strings: + + >>> d = {'major': -3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> Version(**d) + Traceback (most recent call last): + ... + ValueError: 'major' is negative. A version can only be positive. + + As a minimum requirement, your dictionary needs at least the ``major`` + key, others can be omitted. You get a ``TypeError`` if your + dictionary contains invalid keys. + Only the keys ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` + are allowed. + +* From a tuple:: + + >>> t = (3, 5, 6) + >>> Version(*t) + Version(major=3, minor=5, patch=6, prerelease=None, build=None) + + You can pass either an integer or a string for ``major``, ``minor``, or + ``patch``:: + + >>> Version("3", "5", 6) + Version(major=3, minor=5, patch=6, prerelease=None, build=None) + +The old, deprecated module level functions are still available but +using them are discoraged. They are available to convert old code +to semver3. + +If you need them, they return different builtin objects (string and dictionary). +Keep in mind, once you have converted a version into a string or dictionary, +it's an ordinary builtin object. It's not a special version object like +the :class:`~semver.version.Version` class anymore. + +Depending on your use case, the following methods are available: + +* From individual version parts into a string + + In some cases you only need a string from your version data:: + + >>> semver.format_version(3, 4, 5, 'pre.2', 'build.4') + '3.4.5-pre.2+build.4' + +* From a string into a dictionary + + To access individual parts, you can use the function :func:`semver.parse`:: + + >>> semver.parse("3.4.5-pre.2+build.4") + OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) + + If you pass an invalid version string you will get a :py:exc:`ValueError`:: + + >>> semver.parse("1.2") + Traceback (most recent call last): + ... + ValueError: 1.2 is not valid SemVer string diff --git a/docs/usage/create-subclasses-from-version.rst b/docs/usage/create-subclasses-from-version.rst new file mode 100644 index 00000000..7c97ee6f --- /dev/null +++ b/docs/usage/create-subclasses-from-version.rst @@ -0,0 +1,33 @@ +.. _sec_creating_subclasses_from_versioninfo: + +Creating Subclasses from Version +================================ + +If you do not like creating functions to modify the behavior of semver +(as shown in section :ref:`sec_dealing_with_invalid_versions`), you can +also create a subclass of the :class:`Version ` class. + +For example, if you want to output a "v" prefix before a version, +but the other behavior is the same, use the following code: + +.. literalinclude:: semverwithvprefix.py + :language: python + :lines: 4- + + +The derived class :class:`SemVerWithVPrefix` can be used like +the original class: + +.. code-block:: python + + >>> v1 = SemVerWithVPrefix.parse("v1.2.3") + >>> assert str(v1) == "v1.2.3" + >>> print(v1) + v1.2.3 + >>> v2 = SemVerWithVPrefix.parse("v2.3.4") + >>> v2 > v1 + True + >>> bad = SemVerWithVPrefix.parse("1.2.4") + Traceback (most recent call last): + ... + ValueError: '1.2.4': not a valid semantic version tag. Must start with 'v' or 'V' diff --git a/docs/usage/deal-with-invalid-versions.rst b/docs/usage/deal-with-invalid-versions.rst new file mode 100644 index 00000000..ee5e5704 --- /dev/null +++ b/docs/usage/deal-with-invalid-versions.rst @@ -0,0 +1,32 @@ +.. _sec_dealing_with_invalid_versions: + +Dealing with Invalid Versions +============================= + +As semver follows the semver specification, it cannot parse version +strings which are considered "invalid" by that specification. The semver +library cannot know all the possible variations so you need to help the +library a bit. + +For example, if you have a version string ``v1.2`` would be an invalid +semver version. +However, "basic" version strings consisting of major, minor, +and patch part, can be easy to convert. The following function extract this +information and returns a tuple with two items: + +.. literalinclude:: coerce.py + :language: python + + +The function returns a *tuple*, containing a :class:`Version ` +instance or None as the first element and the rest as the second element. +The second element (the rest) can be used to make further adjustments. + +For example: + +.. code-block:: python + + >>> coerce("v1.2") + (Version(major=1, minor=2, patch=0, prerelease=None, build=None), '') + >>> coerce("v2.5.2-bla") + (Version(major=2, minor=5, patch=2, prerelease=None, build=None), '-bla') diff --git a/docs/usage/determine-version-equality.rst b/docs/usage/determine-version-equality.rst new file mode 100644 index 00000000..211743c9 --- /dev/null +++ b/docs/usage/determine-version-equality.rst @@ -0,0 +1,25 @@ +Determining Version Equality +============================ + +Version equality means for semver, that major, minor, patch, and prerelease +parts are equal in both versions you compare. The build part is ignored. +For example:: + + >>> v = Version.parse("1.2.3-rc4+1e4664d") + >>> v == "1.2.3-rc4+dedbeef" + True + +This also applies when a :class:`Version ` is a member of a set, or a +dictionary key:: + + >>> d = {} + >>> v1 = Version.parse("1.2.3-rc4+1e4664d") + >>> v2 = Version.parse("1.2.3-rc4+dedbeef") + >>> d[v1] = 1 + >>> d[v2] + 1 + >>> s = set() + >>> s.add(v1) + >>> v2 in s + True + diff --git a/docs/usage/display-deprecation-warnings.rst b/docs/usage/display-deprecation-warnings.rst new file mode 100644 index 00000000..825bbe76 --- /dev/null +++ b/docs/usage/display-deprecation-warnings.rst @@ -0,0 +1,34 @@ +.. _sec_display_deprecation_warnings: + +Displaying Deprecation Warnings +=============================== + +By default, deprecation warnings are `ignored in Python `_. +This also affects semver's own warnings. + +It is recommended that you turn on deprecation warnings in your scripts. Use one of +the following methods: + +* Use the option `-Wd `_ + to enable default warnings: + + * Directly running the Python command:: + + $ python3 -Wd scriptname.py + + * Add the option in the shebang line (something like ``#!/usr/bin/python3``) + after the command:: + + #!/usr/bin/python3 -Wd + +* In your own scripts add a filter to ensure that *all* warnings are displayed: + + .. code-block:: python + + import warnings + warnings.simplefilter("default") + # Call your semver code + + For further details, see the section + `Overriding the default filter `_ + of the Python documentation. diff --git a/docs/usage/get-min-and-max-of-multiple-versions.rst b/docs/usage/get-min-and-max-of-multiple-versions.rst new file mode 100644 index 00000000..266ee50b --- /dev/null +++ b/docs/usage/get-min-and-max-of-multiple-versions.rst @@ -0,0 +1,51 @@ +.. _sec_max_min: + +Getting Minimum and Maximum of Multiple Versions +================================================ + +.. versionchanged:: 2.10.2 + The functions :func:`semver.max_ver` and :func:`semver.min_ver` are deprecated in + favor of their builtin counterparts :func:`max` and :func:`min`. + +Since :class:`Version ` implements +:func:`__gt__ ` and +:func:`__lt__ `, it can be used with builtins requiring: + +.. code-block:: python + + >>> max([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) + Version(major=0, minor=2, patch=0, prerelease=None, build=None) + >>> min([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) + Version(major=0, minor=1, patch=0, prerelease=None, build=None) + +Incidentally, using :func:`map`, you can get the min or max version of any number of versions of the same type +(convertible to :class:`Version `). + +For example, here are the maximum and minimum versions of a list of version strings: + +.. code-block:: python + + >>> max(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) + '2.1.0' + >>> min(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) + '0.4.99' + +And the same can be done with tuples: + +.. code-block:: python + + >>> max(map(lambda v: Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + (2, 1, 0, None, None) + >>> min(map(lambda v: Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + (0, 4, 99, None, None) + +For dictionaries, it is very similar to finding the max version tuple: see :ref:`sec.convert.versions`. + +The "old way" with :func:`semver.max_ver` or :func:`semver.min_ver` is still available, but not recommended: + +.. code-block:: python + + >>> semver.max_ver("1.0.0", "2.0.0") + '2.0.0' + >>> semver.min_ver("1.0.0", "2.0.0") + '1.0.0' diff --git a/docs/usage/increase-parts-of-a-version_prereleases.rst b/docs/usage/increase-parts-of-a-version_prereleases.rst new file mode 100644 index 00000000..98283937 --- /dev/null +++ b/docs/usage/increase-parts-of-a-version_prereleases.rst @@ -0,0 +1,22 @@ +Increasing Parts of a Version Taking into Account Prereleases +============================================================= + +.. versionadded:: 2.10.0 + Added :func:`Version.next_version `. + +If you want to raise your version and take prereleases into account, +the function :func:`next_version ` +would perhaps a better fit. + + +.. code-block:: python + + >>> v = Version.parse("3.4.5-pre.2+build.4") + >>> str(v.next_version(part="prerelease")) + '3.4.5-pre.3' + >>> str(Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) + '3.4.5' + >>> str(Version.parse("3.4.5+build.4").next_version(part="patch")) + '3.4.5' + >>> str(Version.parse("0.1.4").next_version("prerelease")) + '0.1.5-rc.1' diff --git a/docs/usage/index.rst b/docs/usage/index.rst new file mode 100644 index 00000000..b843809c --- /dev/null +++ b/docs/usage/index.rst @@ -0,0 +1,24 @@ +Using semver +============ + +.. toctree:: + + semver_org-version + semver-version + create-a-version + parse-version-string + check-valid-semver-version + access-parts-of-a-version + access-parts-through-index + replace-parts-of-a-version + convert-version-into-different-types + raise-parts-of-a-version + increase-parts-of-a-version_prereleases + compare-versions + determine-version-equality + compare-versions-through-expression + get-min-and-max-of-multiple-versions + deal-with-invalid-versions + replace-deprecated-functions + display-deprecation-warnings + create-subclasses-from-version diff --git a/docs/usage/parse-version-string.rst b/docs/usage/parse-version-string.rst new file mode 100644 index 00000000..ddd421e7 --- /dev/null +++ b/docs/usage/parse-version-string.rst @@ -0,0 +1,8 @@ +Parsing a Version String +======================== + +"Parsing" in this context means to identify the different parts in a string. +Use the function :func:`Version.parse `:: + + >>> Version.parse("3.4.5-pre.2+build.4") + Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') diff --git a/docs/usage/raise-parts-of-a-version.rst b/docs/usage/raise-parts-of-a-version.rst new file mode 100644 index 00000000..cc62fffb --- /dev/null +++ b/docs/usage/raise-parts-of-a-version.rst @@ -0,0 +1,30 @@ +Raising Parts of a Version +========================== + +The ``semver`` module contains the following functions to raise parts of +a version: + +* :func:`Version.bump_major `: raises the major part and set all other parts to + zero. Set ``prerelease`` and ``build`` to ``None``. +* :func:`Version.bump_minor `: raises the minor part and sets ``patch`` to zero. + Set ``prerelease`` and ``build`` to ``None``. +* :func:`Version.bump_patch `: raises the patch part. Set ``prerelease`` and + ``build`` to ``None``. +* :func:`Version.bump_prerelease `: raises the prerelease part and set + ``build`` to ``None``. +* :func:`Version.bump_build `: raises the build part. + +.. code-block:: python + + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_major()) + '4.0.0' + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_minor()) + '3.5.0' + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_patch()) + '3.4.6' + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) + '3.4.5-pre.3' + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_build()) + '3.4.5-pre.2+build.5' + +Likewise the module level functions :func:`semver.bump_major`. diff --git a/docs/usage/replace-deprecated-functions.rst b/docs/usage/replace-deprecated-functions.rst new file mode 100644 index 00000000..a8f2f8f6 --- /dev/null +++ b/docs/usage/replace-deprecated-functions.rst @@ -0,0 +1,110 @@ +.. _sec_replace_deprecated_functions: + +Replacing Deprecated Functions +============================== + +.. versionchanged:: 2.10.0 + The development team of semver has decided to deprecate certain functions on + the module level. The preferred way of using semver is through the + :class:`semver.Version` class. + +The deprecated functions can still be used in version 2.10.0 and above. In version 3 of +semver, the deprecated functions will be removed. + +The following list shows the deprecated functions and how you can replace +them with code which is compatible for future versions: + + +* :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` + + Replace them with the respective methods of the :class:`Version ` + class. + For example, the function :func:`semver.bump_major` is replaced by + :func:`semver.Version.bump_major` and calling the ``str(versionobject)``: + + .. code-block:: python + + >>> s1 = semver.bump_major("3.4.5") + >>> s2 = str(Version.parse("3.4.5").bump_major()) + >>> s1 == s2 + True + + Likewise with the other module level functions. + +* :func:`semver.finalize_version` + + Replace it with :func:`semver.Version.finalize_version`: + + .. code-block:: python + + >>> s1 = semver.finalize_version('1.2.3-rc.5') + >>> s2 = str(semver.Version.parse('1.2.3-rc.5').finalize_version()) + >>> s1 == s2 + True + +* :func:`semver.format_version` + + Replace it with ``str(versionobject)``: + + .. code-block:: python + + >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') + >>> s2 = str(Version(5, 4, 3, 'pre.2', 'build.1')) + >>> s1 == s2 + True + +* :func:`semver.max_ver` + + Replace it with ``max(version1, version2, ...)`` or ``max([version1, version2, ...])``: + + .. code-block:: python + + >>> s1 = semver.max_ver("1.2.3", "1.2.4") + >>> s2 = str(max(map(Version.parse, ("1.2.3", "1.2.4")))) + >>> s1 == s2 + True + +* :func:`semver.min_ver` + + Replace it with ``min(version1, version2, ...)`` or ``min([version1, version2, ...])``: + + .. code-block:: python + + >>> s1 = semver.min_ver("1.2.3", "1.2.4") + >>> s2 = str(min(map(Version.parse, ("1.2.3", "1.2.4")))) + >>> s1 == s2 + True + +* :func:`semver.parse` + + Replace it with :func:`semver.Version.parse` and + :func:`semver.Version.to_dict`: + + .. code-block:: python + + >>> v1 = semver.parse("1.2.3") + >>> v2 = Version.parse("1.2.3").to_dict() + >>> v1 == v2 + True + +* :func:`semver.parse_version_info` + + Replace it with :func:`semver.Version.parse`: + + .. code-block:: python + + >>> v1 = semver.parse_version_info("3.4.5") + >>> v2 = Version.parse("3.4.5") + >>> v1 == v2 + True + +* :func:`semver.replace` + + Replace it with :func:`semver.Version.replace`: + + .. code-block:: python + + >>> s1 = semver.replace("1.2.3", major=2, patch=10) + >>> s2 = str(Version.parse('1.2.3').replace(major=2, patch=10)) + >>> s1 == s2 + True diff --git a/docs/usage/replace-parts-of-a-version.rst b/docs/usage/replace-parts-of-a-version.rst new file mode 100644 index 00000000..b6c38865 --- /dev/null +++ b/docs/usage/replace-parts-of-a-version.rst @@ -0,0 +1,30 @@ +.. _sec.replace.parts: + +Replacing Parts of a Version +============================ + +If you want to replace different parts of a version, but leave other parts +unmodified, use the function :func:`replace `: + +* From a :class:`Version ` instance:: + + >>> version = semver.Version.parse("1.4.5-pre.1+build.6") + >>> version.replace(major=2, minor=2) + Version(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') + +* From a version string:: + + >>> semver.replace("1.4.5-pre.1+build.6", major=2) + '2.4.5-pre.1+build.6' + +If you pass invalid keys you get an exception:: + + >>> semver.replace("1.2.3", invalidkey=2) + Traceback (most recent call last): + ... + TypeError: replace() got 1 unexpected keyword argument(s): invalidkey + >>> version = semver.Version.parse("1.4.5-pre.1+build.6") + >>> version.replace(invalidkey=2) + Traceback (most recent call last): + ... + TypeError: replace() got 1 unexpected keyword argument(s): invalidkey diff --git a/docs/usage/semver-version.rst b/docs/usage/semver-version.rst new file mode 100644 index 00000000..b3d2c274 --- /dev/null +++ b/docs/usage/semver-version.rst @@ -0,0 +1,7 @@ +Getting the Version of semver +============================= + +To know the version of semver itself, use the following construct:: + + >>> semver.__version__ + '3.0.0-dev.3' diff --git a/docs/usage/semver_org-version.rst b/docs/usage/semver_org-version.rst new file mode 100644 index 00000000..b0a1ad87 --- /dev/null +++ b/docs/usage/semver_org-version.rst @@ -0,0 +1,10 @@ +Getting the Implemented semver.org Version +========================================== + +The semver.org page is the authoritative specification of how semantic +versioning is defined. +To know which version of semver.org is implemented in the semver library, +use the following constant:: + + >>> semver.SEMVER_SPEC_VERSION + '2.0.0' diff --git a/docs/semverwithvprefix.py b/docs/usage/semverwithvprefix.py similarity index 100% rename from docs/semverwithvprefix.py rename to docs/usage/semverwithvprefix.py diff --git a/tests/conftest.py b/tests/conftest.py index 0450e0ee..eb1911d1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import semver -sys.path.insert(0, "docs") +sys.path.insert(0, "docs/usage") from coerce import coerce # noqa:E402 from semverwithvprefix import SemVerWithVPrefix # noqa:E402 From 68553feff8e886250c40df12a2af45daea40c138 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 23 Jan 2022 20:26:56 +0100 Subject: [PATCH 45/86] Add missing changelog file for #350 --- changelog.d/350.doc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/350.doc.rst diff --git a/changelog.d/350.doc.rst b/changelog.d/350.doc.rst new file mode 100644 index 00000000..2fa92f0a --- /dev/null +++ b/changelog.d/350.doc.rst @@ -0,0 +1,2 @@ +Restructure usage section. Create subdirectory "usage/" and splitted +all section into different files. From 838527bf6bb34d3ed2ecbb2bc8b418af3012e44d Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 23 Jan 2022 16:53:40 +0100 Subject: [PATCH 46/86] Introduce new topics for doc * Move some files that better fit into an "Advanced topic" * Introduce "Migration to semver3" topic --- CONTRIBUTING.rst | 4 ++-- changelog.d/351.doc.rst | 4 ++++ docs/{usage => advanced}/coerce.py | 0 .../create-subclasses-from-version.rst | 0 .../{usage => advanced}/deal-with-invalid-versions.rst | 0 .../display-deprecation-warnings.rst | 0 docs/advanced/index.rst | 10 ++++++++++ docs/{usage => advanced}/semverwithvprefix.py | 3 ++- docs/changelog.rst | 2 ++ docs/index.rst | 4 +++- docs/migration/index.rst | 9 +++++++++ docs/{ => migration}/migratetosemver3.rst | 4 ++-- .../replace-deprecated-functions.rst | 0 docs/usage/index.rst | 4 ---- tests/coerce.py | 1 + tests/conftest.py | 2 +- tests/semverwithvprefix.py | 1 + 17 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 changelog.d/351.doc.rst rename docs/{usage => advanced}/coerce.py (100%) rename docs/{usage => advanced}/create-subclasses-from-version.rst (100%) rename docs/{usage => advanced}/deal-with-invalid-versions.rst (100%) rename docs/{usage => advanced}/display-deprecation-warnings.rst (100%) create mode 100644 docs/advanced/index.rst rename docs/{usage => advanced}/semverwithvprefix.py (86%) create mode 100644 docs/migration/index.rst rename docs/{ => migration}/migratetosemver3.rst (93%) rename docs/{usage => migration}/replace-deprecated-functions.rst (100%) create mode 120000 tests/coerce.py create mode 120000 tests/semverwithvprefix.py diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5fd75ab2..e0210cc9 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -64,7 +64,7 @@ We recommend the following workflow: a. Write test cases and run the complete test suite, see :ref:`testsuite` for details. - b. Write a changelog entry, see section :ref:`changelog`. + b. Write a changelog entry, see section :ref:`add-changelog`. c. If you have implemented a new feature, document it into our documentation to help our reader. See section :ref:`doc` for @@ -214,7 +214,7 @@ documentation includes: edge cases. -.. _changelog: +.. _add-changelog: Adding a Changelog Entry ------------------------ diff --git a/changelog.d/351.doc.rst b/changelog.d/351.doc.rst new file mode 100644 index 00000000..0b5199fa --- /dev/null +++ b/changelog.d/351.doc.rst @@ -0,0 +1,4 @@ +Introduce new topics for: + +* "Migration to semver3" +* "Advanced topics" diff --git a/docs/usage/coerce.py b/docs/advanced/coerce.py similarity index 100% rename from docs/usage/coerce.py rename to docs/advanced/coerce.py diff --git a/docs/usage/create-subclasses-from-version.rst b/docs/advanced/create-subclasses-from-version.rst similarity index 100% rename from docs/usage/create-subclasses-from-version.rst rename to docs/advanced/create-subclasses-from-version.rst diff --git a/docs/usage/deal-with-invalid-versions.rst b/docs/advanced/deal-with-invalid-versions.rst similarity index 100% rename from docs/usage/deal-with-invalid-versions.rst rename to docs/advanced/deal-with-invalid-versions.rst diff --git a/docs/usage/display-deprecation-warnings.rst b/docs/advanced/display-deprecation-warnings.rst similarity index 100% rename from docs/usage/display-deprecation-warnings.rst rename to docs/advanced/display-deprecation-warnings.rst diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst new file mode 100644 index 00000000..de7da166 --- /dev/null +++ b/docs/advanced/index.rst @@ -0,0 +1,10 @@ +Advanced topics +=============== + + + +.. toctree:: + + deal-with-invalid-versions + create-subclasses-from-version + display-deprecation-warnings \ No newline at end of file diff --git a/docs/usage/semverwithvprefix.py b/docs/advanced/semverwithvprefix.py similarity index 86% rename from docs/usage/semverwithvprefix.py rename to docs/advanced/semverwithvprefix.py index 5e375031..4395a95e 100644 --- a/docs/usage/semverwithvprefix.py +++ b/docs/advanced/semverwithvprefix.py @@ -17,7 +17,8 @@ def parse(cls, version: str) -> "SemVerWithVPrefix": """ if not version[0] in ("v", "V"): raise ValueError( - "{v!r}: not a valid semantic version tag. Must start with 'v' or 'V'".format( + "{v!r}: not a valid semantic version tag. " + "Must start with 'v' or 'V'".format( v=version ) ) diff --git a/docs/changelog.rst b/docs/changelog.rst index 565b0521..e1e273b4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1 +1,3 @@ +.. _change-log: + .. include:: ../CHANGELOG.rst diff --git a/docs/index.rst b/docs/index.rst index 3e2771a0..deac1cd0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,8 @@ Semver |version| -- Semantic Versioning install usage/index - migratetosemver3 + migration/index + advanced/index development api @@ -31,6 +32,7 @@ Semver |version| -- Semantic Versioning changelog changelog-semver2 + Indices and Tables ================== diff --git a/docs/migration/index.rst b/docs/migration/index.rst new file mode 100644 index 00000000..c6af7c05 --- /dev/null +++ b/docs/migration/index.rst @@ -0,0 +1,9 @@ +Migrating to semver3 +==================== + + +.. toctree:: + :maxdepth: 1 + + migratetosemver3 + replace-deprecated-functions.rst diff --git a/docs/migratetosemver3.rst b/docs/migration/migratetosemver3.rst similarity index 93% rename from docs/migratetosemver3.rst rename to docs/migration/migratetosemver3.rst index d977bc03..f869cad3 100644 --- a/docs/migratetosemver3.rst +++ b/docs/migration/migratetosemver3.rst @@ -3,7 +3,7 @@ Migrating from semver2 to semver3 ================================= -This chapter describes the visible differences for +This document describes the visible differences for users and how your code stays compatible for semver3. Although the development team tries to make the transition @@ -11,7 +11,7 @@ to semver3 as smooth as possible, at some point change is inevitable. For a more detailed overview of all the changes, refer -to our :ref:`changelog`. +to our :ref:`change-log`. Use Version instead of VersionInfo diff --git a/docs/usage/replace-deprecated-functions.rst b/docs/migration/replace-deprecated-functions.rst similarity index 100% rename from docs/usage/replace-deprecated-functions.rst rename to docs/migration/replace-deprecated-functions.rst diff --git a/docs/usage/index.rst b/docs/usage/index.rst index b843809c..ddfc2284 100644 --- a/docs/usage/index.rst +++ b/docs/usage/index.rst @@ -18,7 +18,3 @@ Using semver determine-version-equality compare-versions-through-expression get-min-and-max-of-multiple-versions - deal-with-invalid-versions - replace-deprecated-functions - display-deprecation-warnings - create-subclasses-from-version diff --git a/tests/coerce.py b/tests/coerce.py new file mode 120000 index 00000000..e79106a2 --- /dev/null +++ b/tests/coerce.py @@ -0,0 +1 @@ +../docs/advanced/coerce.py \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index eb1911d1..40b56ab1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import semver -sys.path.insert(0, "docs/usage") +# sys.path.insert(0, "docs/usage") from coerce import coerce # noqa:E402 from semverwithvprefix import SemVerWithVPrefix # noqa:E402 diff --git a/tests/semverwithvprefix.py b/tests/semverwithvprefix.py new file mode 120000 index 00000000..d1a8d995 --- /dev/null +++ b/tests/semverwithvprefix.py @@ -0,0 +1 @@ +../docs/advanced/semverwithvprefix.py \ No newline at end of file From 73bd190a1cb06517868c9b176ce386f95f66cdd9 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 27 Jan 2022 11:22:43 +0100 Subject: [PATCH 47/86] Describe Pydantic and semver in "Advanced topics" Related to issue #343 Co-authored-by: Caleb Stewart --- changelog.d/343.doc.rst | 2 + docs/advanced/combine-pydantic-and-semver.rst | 53 +++++++++++++++++++ docs/advanced/index.rst | 4 +- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 changelog.d/343.doc.rst create mode 100644 docs/advanced/combine-pydantic-and-semver.rst diff --git a/changelog.d/343.doc.rst b/changelog.d/343.doc.rst new file mode 100644 index 00000000..630d7474 --- /dev/null +++ b/changelog.d/343.doc.rst @@ -0,0 +1,2 @@ +Describe combining Pydantic with semver in the "Advanced topic" +section. diff --git a/docs/advanced/combine-pydantic-and-semver.rst b/docs/advanced/combine-pydantic-and-semver.rst new file mode 100644 index 00000000..a9249daf --- /dev/null +++ b/docs/advanced/combine-pydantic-and-semver.rst @@ -0,0 +1,53 @@ +Combining Pydantic and semver +============================= + +According to its homepage, `Pydantic `_ +"enforces type hints at runtime, and provides user friendly errors when data +is invalid." + +To work with Pydantic, use the following steps: + + +1. Derive a new class from :class:`~semver.version.Version` + first and add the magic methods :py:meth:`__get_validators__` + and :py:meth:`__modify_schema__` like this: + + .. code-block:: python + + from semver import Version + + class PydanticVersion(Version): + @classmethod + def __get_validators__(cls): + """Return a list of validator methods for pydantic models.""" + yield cls.parse + + @classmethod + def __modify_schema__(cls, field_schema): + """Inject/mutate the pydantic field schema in-place.""" + field_schema.update(examples=["1.0.2", + "2.15.3-alpha", + "21.3.15-beta+12345", + ] + ) + +2. Create a new model (in this example :class:`MyModel`) and derive + it from :class:`pydantic.BaseModel`: + + .. code-block:: python + + import pydantic + + class MyModel(pydantic.BaseModel): + version: PydanticVersion + +3. Use your model like this: + + .. code-block:: python + + model = MyModel.parse_obj({"version": "1.2.3"}) + + The attribute :py:attr:`model.version` will be an instance of + :class:`~semver.version.Version`. + If the version is invalid, the construction will raise a + :py:exc:`pydantic.ValidationError`. diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index de7da166..f45a2f9e 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -2,9 +2,9 @@ Advanced topics =============== - .. toctree:: deal-with-invalid-versions create-subclasses-from-version - display-deprecation-warnings \ No newline at end of file + display-deprecation-warnings + combine-pydantic-and-semver \ No newline at end of file From 0c4985c91d5ae9d0085d27a9484397ac47a3b437 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 25 Jan 2022 21:40:45 +0100 Subject: [PATCH 48/86] Describe conversion between PyPI and semver Add new section "Converting versions between PyPI and semver" the limitations and possible use cases to convert from one into the other versioning scheme. --- changelog.d/335.doc.rst | 2 + docs/advanced/convert-pypi-to-semver.rst | 207 ++++++++++++++++++ docs/advanced/index.rst | 3 +- docs/conf.py | 4 +- docs/install.rst | 13 +- .../replace-deprecated-functions.rst | 4 +- tests/conftest.py | 2 + 7 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 changelog.d/335.doc.rst create mode 100644 docs/advanced/convert-pypi-to-semver.rst diff --git a/changelog.d/335.doc.rst b/changelog.d/335.doc.rst new file mode 100644 index 00000000..1d29fb87 --- /dev/null +++ b/changelog.d/335.doc.rst @@ -0,0 +1,2 @@ +Add new section "Converting versions between PyPI and semver" the limitations +and possible use cases to convert from one into the other versioning scheme. diff --git a/docs/advanced/convert-pypi-to-semver.rst b/docs/advanced/convert-pypi-to-semver.rst new file mode 100644 index 00000000..76653ceb --- /dev/null +++ b/docs/advanced/convert-pypi-to-semver.rst @@ -0,0 +1,207 @@ +Converting versions between PyPI and semver +=========================================== + +.. Link + https://packaging.pypa.io/en/latest/_modules/packaging/version.html#InvalidVersion + +When packaging for PyPI, your versions are defined through `PEP 440`_. +This is the standard version scheme for Python packages and +implemented by the :class:`packaging.version.Version` class. + +However, these versions are different from semver versions +(cited from `PEP 440`_): + +* The "Major.Minor.Patch" (described in this PEP as "major.minor.micro") + aspects of semantic versioning (clauses 1-8 in the 2.0.0 + specification) are fully compatible with the version scheme defined + in this PEP, and abiding by these aspects is encouraged. + +* Semantic versions containing a hyphen (pre-releases - clause 10) + or a plus sign (builds - clause 11) are *not* compatible with this PEP + and are not permitted in the public version field. + +In other words, it's not always possible to convert between these different +versioning schemes without information loss. It depends on what parts are +used. The following table gives a mapping between these two versioning +schemes: + ++--------------+----------------+ +| PyPI Version | Semver version | ++==============+================+ +| ``epoch`` | n/a | ++--------------+----------------+ +| ``major`` | ``major`` | ++--------------+----------------+ +| ``minor`` | ``minor`` | ++--------------+----------------+ +| ``micro`` | ``patch`` | ++--------------+----------------+ +| ``pre`` | ``prerelease`` | ++--------------+----------------+ +| ``dev`` | ``build`` | ++--------------+----------------+ +| ``post`` | n/a | ++--------------+----------------+ + + +.. _convert_pypi_to_semver: + +From PyPI to semver +------------------- + +We distinguish between the following use cases: + + +* **"Incomplete" versions** + + If you only have a major part, this shouldn't be a problem. + The initializer of :class:`semver.Version ` takes + care to fill missing parts with zeros (except for major). + + .. code-block:: python + + >>> from packaging.version import Version as PyPIVersion + >>> from semver import Version + + >>> p = PyPIVersion("3.2") + >>> p.release + (3, 2) + >>> Version(*p.release) + Version(major=3, minor=2, patch=0, prerelease=None, build=None) + +* **Major, minor, and patch** + + This is the simplest and most compatible approch. Both versioning + schemes are compatible without information loss. + + .. code-block:: python + + >>> p = PyPIVersion("3.0.0") + >>> p.base_version + '3.0.0' + >>> p.release + (3, 0, 0) + >>> Version(*p.release) + Version(major=3, minor=0, patch=0, prerelease=None, build=None) + +* **With** ``pre`` **part only** + + A prerelease exists in both versioning schemes. As such, both are + a natural candidate. A prelease in PyPI version terms is the same + as a "release candidate", or "rc". + + .. code-block:: python + + >>> p = PyPIVersion("2.1.6.pre5") + >>> p.base_version + '2.1.6' + >>> p.pre + ('rc', 5) + >>> pre = "".join([str(i) for i in p.pre]) + >>> Version(*p.release, pre) + Version(major=2, minor=1, patch=6, prerelease='rc5', build=None) + +* **With only development version** + + Semver doesn't have a "development" version. + However, we could use Semver's ``build`` part: + + .. code-block:: python + + >>> p = PyPIVersion("3.0.0.dev2") + >>> p.base_version + '3.0.0' + >>> p.dev + 2 + >>> Version(*p.release, build=f"dev{p.dev}") + Version(major=3, minor=0, patch=0, prerelease=None, build='dev2') + +* **With a** ``post`` **version** + + Semver doesn't know the concept of a post version. As such, there + is currently no way to convert it reliably. + +* **Any combination** + + There is currently no way to convert a PyPI version which consists + of, for example, development *and* post parts. + + +You can use the following function to convert a PyPI version into +semver: + +.. code-block:: python + + def convert2semver(ver: packaging.version.Version) -> semver.Version: + """Converts a PyPI version into a semver version + + :param packaging.version.Version ver: the PyPI version + :return: a semver version + :raises ValueError: if epoch or post parts are used + """ + if not ver.epoch: + raise ValueError("Can't convert an epoch to semver") + if not ver.post: + raise ValueError("Can't convert a post part to semver") + + pre = None if not ver.pre else "".join([str(i) for i in ver.pre]) + semver.Version(*ver.release, prerelease=pre, build=ver.dev) + + +.. _convert_semver_to_pypi: + +From semver to PyPI +------------------- + +We distinguish between the following use cases: + + +* **Major, minor, and patch** + + .. code-block:: python + + >>> from packaging.version import Version as PyPIVersion + >>> from semver import Version + + >>> v = Version(1, 2, 3) + >>> PyPIVersion(str(v.finalize_version())) + + +* **With** ``pre`` **part only** + + .. code-block:: python + + >>> v = Version(2, 1, 4, prerelease="rc1") + >>> PyPIVersion(str(v)) + + +* **With only development version** + + .. code-block:: python + + >>> v = Version(3, 2, 8, build="dev4") + >>> PyPIVersion(f"{v.finalize_version()}{v.build}") + + +If you are unsure about the parts of the version, the following +function helps to convert the different parts: + +.. code-block:: python + + def convert2pypi(ver: semver.Version) -> packaging.version.Version: + """Converts a semver version into a version from PyPI + + A semver prerelease will be converted into a + prerelease of PyPI. + A semver build will be converted into a development + part of PyPI + :param semver.Version ver: the semver version + :return: a PyPI version + """ + v = ver.finalize_version() + prerelease = ver.prerelease if ver.prerelease else "" + build = ver.build if ver.build else "" + return PyPIVersion(f"{v}{prerelease}{build}") + + +.. _PEP 440: https://www.python.org/dev/peps/pep-0440/ diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index f45a2f9e..d82da1a1 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -7,4 +7,5 @@ Advanced topics deal-with-invalid-versions create-subclasses-from-version display-deprecation-warnings - combine-pydantic-and-semver \ No newline at end of file + combine-pydantic-and-semver + convert-pypi-to-semver diff --git a/docs/conf.py b/docs/conf.py index 52a46704..ed888361 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import codecs +from datetime import date import os import re import sys @@ -24,6 +25,7 @@ SRC_DIR = os.path.abspath("../src/") sys.path.insert(0, SRC_DIR) # from semver import __version__ # noqa: E402 +YEAR = date.today().year def read(*parts): @@ -83,7 +85,7 @@ def find_version(*file_paths): # General information about the project. project = "python-semver" -copyright = "2018, Kostiantyn Rybnikov and all" +copyright = f"{YEAR}, Kostiantyn Rybnikov and all" author = "Kostiantyn Rybnikov and all" # The version info for the project you're documenting, acts as replacement for diff --git a/docs/install.rst b/docs/install.rst index b603703c..f23186a2 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -18,8 +18,13 @@ This line avoids surprises. You will get any updates within the major 2 release Keep in mind, as this line avoids any major version updates, you also will never get new exciting features or bug fixes. -You can add this line in your file :file:`setup.py`, :file:`requirements.txt`, or any other -file that lists your dependencies. +Same applies for semver v3, if you want to get all updates for the semver v3 +development line, but not a major update to semver v4:: + + semver>=3,<4 + +You can add this line in your file :file:`setup.py`, :file:`requirements.txt`, +:file:`pyproject.toml`, or any other file that lists your dependencies. Pip --- @@ -28,12 +33,12 @@ Pip pip3 install semver -If you want to install this specific version (for example, 2.10.0), use the command :command:`pip` +If you want to install this specific version (for example, 3.0.0), use the command :command:`pip` with an URL and its version: .. parsed-literal:: - pip3 install git+https://github.com/python-semver/python-semver.git@2.11.0 + pip3 install git+https://github.com/python-semver/python-semver.git@3.0.0 Linux Distributions diff --git a/docs/migration/replace-deprecated-functions.rst b/docs/migration/replace-deprecated-functions.rst index a8f2f8f6..9738001c 100644 --- a/docs/migration/replace-deprecated-functions.rst +++ b/docs/migration/replace-deprecated-functions.rst @@ -60,7 +60,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.max_ver("1.2.3", "1.2.4") - >>> s2 = str(max(map(Version.parse, ("1.2.3", "1.2.4")))) + >>> s2 = max("1.2.3", "1.2.4", key=Version.parse) >>> s1 == s2 True @@ -71,7 +71,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.min_ver("1.2.3", "1.2.4") - >>> s2 = str(min(map(Version.parse, ("1.2.3", "1.2.4")))) + >>> s2 = min("1.2.3", "1.2.4", key=Version.parse) >>> s1 == s2 True diff --git a/tests/conftest.py b/tests/conftest.py index 40b56ab1..beecffc9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ from coerce import coerce # noqa:E402 from semverwithvprefix import SemVerWithVPrefix # noqa:E402 +import packaging.version @pytest.fixture(autouse=True) @@ -16,6 +17,7 @@ def add_semver(doctest_namespace): doctest_namespace["semver"] = semver doctest_namespace["coerce"] = coerce doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix + doctest_namespace["PyPIVersion"] = packaging.version.Version @pytest.fixture From 57690e827077a831ee6e6aebfd63ff5d5cf175c8 Mon Sep 17 00:00:00 2001 From: Thomas <616052+b0uh@users.noreply.github.com> Date: Thu, 24 Feb 2022 10:06:57 +0100 Subject: [PATCH 49/86] Use HTTPS instead of HTTP for the website URL --- README.rst | 2 +- docs/install.rst | 2 +- setup.cfg | 2 +- src/semver/__about__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 5c28cc69..5d939976 100644 --- a/README.rst +++ b/README.rst @@ -112,7 +112,7 @@ There are other functions to discover. Read on! .. |docs| image:: https://readthedocs.org/projects/python-semver/badge/?version=latest :target: http://python-semver.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status -.. _semantic versioning: http://semver.org/ +.. _semantic versioning: https://semver.org/ .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Black Formatter diff --git a/docs/install.rst b/docs/install.rst index f23186a2..5404882f 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -121,4 +121,4 @@ Ubuntu $ sudo apt-get install python3-semver -.. _semantic versioning: http://semver.org/ +.. _semantic versioning: https://semver.org/ diff --git a/setup.cfg b/setup.cfg index de2d226c..8991f1c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ [metadata] name = semver version = attr: semver.__about__.__version__ -description = Python helper for Semantic Versioning (http://semver.org) +description = Python helper for Semantic Versioning (https://semver.org) long_description = file: README.rst long_description_content_type = text/x-rst author = Kostiantyn Rybnikov diff --git a/src/semver/__about__.py b/src/semver/__about__.py index fa448ebe..d1dc8e3f 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -31,7 +31,7 @@ __maintainer_email__ = "s.celles@gmail.com" #: Short description about semver -__description__ = "Python helper for Semantic Versioning (http://semver.org)" +__description__ = "Python helper for Semantic Versioning (https://semver.org)" #: Supported semver specification SEMVER_SPEC_VERSION = "2.0.0" From ffe686a9b1d5adae75239da3592aa077a9970728 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 24 Feb 2022 21:36:49 +0100 Subject: [PATCH 50/86] Add topic to read version from file Fix #340 --- changelog.d/340.doc.rst | 1 + docs/advanced/index.rst | 1 + docs/advanced/version-from-file.rst | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 changelog.d/340.doc.rst create mode 100644 docs/advanced/version-from-file.rst diff --git a/changelog.d/340.doc.rst b/changelog.d/340.doc.rst new file mode 100644 index 00000000..807e401c --- /dev/null +++ b/changelog.d/340.doc.rst @@ -0,0 +1 @@ +Describe how to get version from a file \ No newline at end of file diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index d82da1a1..8a45d361 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -9,3 +9,4 @@ Advanced topics display-deprecation-warnings combine-pydantic-and-semver convert-pypi-to-semver + version-from-file diff --git a/docs/advanced/version-from-file.rst b/docs/advanced/version-from-file.rst new file mode 100644 index 00000000..6dc9bb48 --- /dev/null +++ b/docs/advanced/version-from-file.rst @@ -0,0 +1,23 @@ +.. _sec_reading_versions_from_file: + +Reading versions from file +========================== + +In cases where a version is stored inside a file, one possible solution +is to use the following function: + +.. code-block:: python + + from semver.version import Version + + def get_version(path: str = "version") -> Version: + """ + Construct a Version from a file + + :param path: A text file only containing the semantic version + :return: A :class:`Version` object containing the semantic + version from the file. + """ + with open(path,"r") as fh: + version = fh.read().strip() + return Version.parse(version) From 2aeb61b667f1df9c1bd98cf5822a8254e23ac993 Mon Sep 17 00:00:00 2001 From: OidaTiftla Date: Tue, 24 May 2022 10:42:19 +0200 Subject: [PATCH 51/86] Allow optional minor and patch parts (#359) * Change Version.parse to allow optional minor and patch parts * Add documentation and changelog entry Co-authored-by: Tom Schraitle --- changelog.d/pr359.feature.rst | 2 ++ docs/usage/parse-version-string.rst | 7 ++++ src/semver/version.py | 48 +++++++++++++++++++++------ tests/test_parsing.py | 50 +++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 changelog.d/pr359.feature.rst diff --git a/changelog.d/pr359.feature.rst b/changelog.d/pr359.feature.rst new file mode 100644 index 00000000..5c18c9d2 --- /dev/null +++ b/changelog.d/pr359.feature.rst @@ -0,0 +1,2 @@ +Add optional parameter ``optional_minor_and_patch`` in :meth:`.Version.parse` to allow optional +minor and patch parts. diff --git a/docs/usage/parse-version-string.rst b/docs/usage/parse-version-string.rst index ddd421e7..0a39c8a3 100644 --- a/docs/usage/parse-version-string.rst +++ b/docs/usage/parse-version-string.rst @@ -6,3 +6,10 @@ Use the function :func:`Version.parse `:: >>> Version.parse("3.4.5-pre.2+build.4") Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') + +Set the parameter ``optional_minor_and_patch=True`` to allow optional +minor and patch parts. Optional parts are set to zero. By default (False), the +version string to parse has to follow the semver specification:: + + >>> Version.parse("1.2", optional_minor_and_patch=True) + Version(major=1, minor=2, patch=0, prerelease=None, build=None) diff --git a/src/semver/version.py b/src/semver/version.py index 9e02544f..096acdf2 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -68,15 +68,19 @@ class Version: __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") #: Regex for number in a prerelease _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") - #: Regex for a semver version - _REGEX = re.compile( + #: Regex template for a semver version + _REGEX_TEMPLATE = \ r""" ^ (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) - \. - (?P0|[1-9]\d*) + (?: + \. + (?P0|[1-9]\d*) + (?: + \. + (?P0|[1-9]\d*) + ){opt_patch} + ){opt_minor} (?:-(?P (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* @@ -86,7 +90,15 @@ class Version: (?:\.[0-9a-zA-Z-]+)* ))? $ - """, + """ + #: Regex for a semver version + _REGEX = re.compile( + _REGEX_TEMPLATE.format(opt_patch='', opt_minor=''), + re.VERBOSE, + ) + #: Regex for a semver version that might be shorter + _REGEX_OPTIONAL_MINOR_AND_PATCH = re.compile( + _REGEX_TEMPLATE.format(opt_patch='?', opt_minor='?'), re.VERBOSE, ) @@ -553,15 +565,26 @@ def match(self, match_expr: str) -> bool: return cmp_res in possibilities @classmethod - def parse(cls, version: String) -> "Version": + def parse( + cls, + version: String, + optional_minor_and_patch: bool = False + ) -> "Version": """ Parse version string to a Version instance. .. versionchanged:: 2.11.0 Changed method from static to classmethod to allow subclasses. + .. versionchanged:: 3.0.0 + Added optional parameter optional_minor_and_patch to allow optional + minor and patch parts. :param version: version string + :param optional_minor_and_patch: if set to true, the version string to parse \ + can contain optional minor and patch parts. Optional parts are set to zero. + By default (False), the version string to parse has to follow the semver + specification. :return: a new :class:`Version` instance :raises ValueError: if version is invalid :raises TypeError: if version contains the wrong type @@ -575,11 +598,18 @@ def parse(cls, version: String) -> "Version": elif not isinstance(version, String.__args__): # type: ignore raise TypeError("not expecting type '%s'" % type(version)) - match = cls._REGEX.match(version) + if optional_minor_and_patch: + match = cls._REGEX_OPTIONAL_MINOR_AND_PATCH.match(version) + else: + match = cls._REGEX.match(version) if match is None: raise ValueError(f"{version} is not valid SemVer string") matched_version_parts: Dict[str, Any] = match.groupdict() + if not matched_version_parts['minor']: + matched_version_parts['minor'] = 0 + if not matched_version_parts['patch']: + matched_version_parts['patch'] = 0 return cls(**matched_version_parts) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 25c55c74..ddf52196 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -53,6 +53,56 @@ def test_should_parse_version(version, expected): assert result == expected +@pytest.mark.parametrize( + "version,expected", + [ + # no. 1 + ( + "1.2-alpha.1.2+build.11.e0f985a", + { + "major": 1, + "minor": 2, + "patch": 0, + "prerelease": "alpha.1.2", + "build": "build.11.e0f985a", + }, + ), + # no. 2 + ( + "1-alpha-1+build.11.e0f985a", + { + "major": 1, + "minor": 0, + "patch": 0, + "prerelease": "alpha-1", + "build": "build.11.e0f985a", + }, + ), + ( + "0.1-0f", + {"major": 0, "minor": 1, "patch": 0, "prerelease": "0f", "build": None}, + ), + ( + "0-0foo.1", + {"major": 0, "minor": 0, "patch": 0, "prerelease": "0foo.1", "build": None}, + ), + ( + "0-0foo.1+build.1", + { + "major": 0, + "minor": 0, + "patch": 0, + "prerelease": "0foo.1", + "build": "build.1", + }, + ), + ], +) +def test_should_parse_version_with_optional_minor_and_patch(version, expected): + result = Version.parse(version, optional_minor_and_patch=True) + assert result == expected + + def test_parse_version_info_str_hash(): s_version = "1.2.3-alpha.1.2+build.11.e0f985a" v = parse_version_info(s_version) From b5317af9a7e99e6a86df98320e73be72d5adf0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Miguel=20S=C3=A1nchez=20Garc=C3=ADa?= Date: Mon, 4 Jul 2022 15:51:44 +0200 Subject: [PATCH 52/86] Support matching 'equal' when no operator is provided (#362) * Add tests for new default equality match behavior * Change documentation and add changelog --- changelog.d/pr362.feature.rst | 2 ++ docs/usage/compare-versions-through-expression.rst | 13 +++++++++++++ src/semver/version.py | 7 ++++++- tests/test_match.py | 14 +++++++++++++- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pr362.feature.rst diff --git a/changelog.d/pr362.feature.rst b/changelog.d/pr362.feature.rst new file mode 100644 index 00000000..1b7cc120 --- /dev/null +++ b/changelog.d/pr362.feature.rst @@ -0,0 +1,2 @@ +Make :meth:`.Version.match` accept a bare version string as match expression, defaulting to +equality testing. diff --git a/docs/usage/compare-versions-through-expression.rst b/docs/usage/compare-versions-through-expression.rst index 43a5a182..28fad671 100644 --- a/docs/usage/compare-versions-through-expression.rst +++ b/docs/usage/compare-versions-through-expression.rst @@ -24,3 +24,16 @@ That gives you the following possibilities to express your condition: True >>> semver.match("1.0.0", ">1.0.0") False + +If no operator is specified, the match expression is interpreted as a +version to be compared for equality. This allows handling the common +case of version compatibility checking through either an exact version +or a match expression very easy to implement, as the same code will +handle both cases: + +.. code-block:: python + + >>> semver.match("2.0.0", "2.0.0") + True + >>> semver.match("1.0.0", "3.5.1") + False diff --git a/src/semver/version.py b/src/semver/version.py index 096acdf2..34eb51e0 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -522,7 +522,7 @@ def match(self, match_expr: str) -> bool: """ Compare self to match a match expression. - :param match_expr: operator and version; valid operators are + :param match_expr: optional operator and version; valid operators are ``<``` smaller than ``>`` greater than ``>=`` greator or equal than @@ -535,6 +535,8 @@ def match(self, match_expr: str) -> bool: True >>> semver.Version.parse("1.0.0").match(">1.0.0") False + >>> semver.Version.parse("4.0.4").match("4.0.4") + True """ prefix = match_expr[:2] if prefix in (">=", "<=", "==", "!="): @@ -542,6 +544,9 @@ def match(self, match_expr: str) -> bool: elif prefix and prefix[0] in (">", "<"): prefix = prefix[0] match_version = match_expr[1:] + elif match_expr and match_expr[0] in "0123456789": + prefix = "==" + match_version = match_expr else: raise ValueError( "match_expr parameter should be in format , " diff --git a/tests/test_match.py b/tests/test_match.py index b4cc50cc..2476eb01 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -23,6 +23,18 @@ def test_should_match_not_equal(left, right, expected): assert match(left, right) is expected +@pytest.mark.parametrize( + "left,right,expected", + [ + ("2.3.7", "2.3.7", True), + ("2.3.6", "2.3.6", True), + ("2.3.7", "4.3.7", False), + ], +) +def test_should_match_equal_by_default(left, right, expected): + assert match(left, right) is expected + + @pytest.mark.parametrize( "left,right,expected", [ @@ -49,7 +61,7 @@ def test_should_raise_value_error_for_unexpected_match_expression(left, right): @pytest.mark.parametrize( - "left,right", [("1.0.0", ""), ("1.0.0", "!"), ("1.0.0", "1.0.0")] + "left,right", [("1.0.0", ""), ("1.0.0", "!")] ) def test_should_raise_value_error_for_invalid_match_expression(left, right): with pytest.raises(ValueError): From 3eae18c2a4ac3236b2370b5be47d2078e03f6582 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 9 Nov 2020 00:06:22 +0100 Subject: [PATCH 53/86] Fix #365: Improve pyproject.toml * Improve pyproject.toml * Use setuptools * Add metadata * Taken approach from https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/ * Doc: Describe building of semver * Correct small glitches * Remove .travis.yml in MANIFEST.in (not needed anymore) * Distinguish between Python3.6 and others in tox.ini * Add skip_missing_interpreters option for tox.ini * Add changelog entry * GH Action: * Upgrade setuptools and setuptools-scm * Also test against 3.11.0-rc.2 --- .github/workflows/python-testing.yml | 8 ++-- BUILDING.rst | 59 ++++++++++++++++++++++++++++ MANIFEST.in | 3 +- changelog.d/364.feature.rst | 3 ++ docs/build.rst | 1 + docs/index.rst | 1 + pyproject.toml | 14 ++++++- setup.cfg | 2 + tox.ini | 16 ++++++-- 9 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 BUILDING.rst create mode 100644 changelog.d/364.feature.rst create mode 100644 docs/build.rst diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 8f36dbc9..336e4980 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -34,10 +34,10 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.7 - name: Install dependencies run: | - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip setuptools setuptools-scm pip install tox tox-gh-actions - name: Check run: | @@ -49,7 +49,9 @@ jobs: strategy: max-parallel: 5 matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2", + # "3.12" + ] steps: - uses: actions/checkout@v1 diff --git a/BUILDING.rst b/BUILDING.rst new file mode 100644 index 00000000..6de412f8 --- /dev/null +++ b/BUILDING.rst @@ -0,0 +1,59 @@ +.. _build-semver: + +Building semver +=============== + +.. _PEP 517: https://www.python.org/dev/peps/pep-0517/ +.. _PEP 621: https://www.python.org/dev/peps/pep-0621/ +.. _A Practical Guide to Setuptools and Pyproject.toml: https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/ +.. _Declarative config: https://setuptools.rtfd.io/en/latest/userguide/declarative_config.html + + +This project changed slightly its way how it is built. The reason for this +was to still support the "traditional" way with :command:`setup.py`, +but at the same time try out the newer way with :file:`pyproject.toml`. +Over time, once Python 3.6 gets deprecated, we will support only the newer way. + + +Background information +---------------------- + +Skip this section and head over to :ref:`build-pyproject-build` if you just +want to know how to build semver. +This section gives some background information how this project is set up. + +The traditional way with :command:`setup.py` in this project uses a +`Declarative config`_. With this approach, the :command:`setup.py` is +stripped down to its bare minimum and all the metadata is stored in +:file:`setup.cfg`. + +The new :file:`pyproject.toml` contains only information about the build backend, currently setuptools.build_meta. The idea is taken from +`A Practical Guide to Setuptools and Pyproject.toml`_. +Setuptools-specific configuration keys as defined in `PEP 621`_ are currently +not used. + + +.. _build-pyproject-build: + +Building with pyproject-build +----------------------------- + +To build semver you need: + +* The :mod:`build` module which implements the `PEP 517`_ build + frontend. + Install it with:: + + pip install build + + Some Linux distributions has already packaged it. If you prefer + to use the module with your package manager, search for + :file:`python-build` or :file:`python3-build` and install it. + +* The command :command:`pyproject-build` from the :mod:`build` module. + +To build semver, run:: + + pyproject-build + +After the command is finished, you can find two files in the :file:`dist` folder: a ``.tar.gz`` and a ``.whl`` file. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 80257f1f..7b2a7b61 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,7 @@ include *.rst include *.txt -include test_*.py +include tests/test_*.py -exclude .travis.yml prune docs/_build recursive-exclude .github * diff --git a/changelog.d/364.feature.rst b/changelog.d/364.feature.rst new file mode 100644 index 00000000..885a6c85 --- /dev/null +++ b/changelog.d/364.feature.rst @@ -0,0 +1,3 @@ +Enhance :file:`pyproject.toml` to make it possible to use the +:command:`pyproject-build` command from the build module. +For more information, see :ref:`build-semver`. diff --git a/docs/build.rst b/docs/build.rst new file mode 100644 index 00000000..ba0c84a4 --- /dev/null +++ b/docs/build.rst @@ -0,0 +1 @@ +.. include:: ../BUILDING.rst diff --git a/docs/index.rst b/docs/index.rst index deac1cd0..2dce2a50 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ Semver |version| -- Semantic Versioning :caption: Contents :hidden: + build install usage/index migration/index diff --git a/pyproject.toml b/pyproject.toml index 769b13d7..ba4be51b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,22 @@ +# +# +# See also https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html +# +# General idea taken from +# https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/ + [build-system] requires = [ # sync with setup.py until we discard non-pep-517/518 - "setuptools>=40.0", + "setuptools", "setuptools-scm", "wheel", + "build", ] build-backend = "setuptools.build_meta" + + [tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] @@ -22,7 +32,7 @@ include = ''' [tool.towncrier] package = "semver" -# package_dir = "src" +package_dir = "src" filename = "CHANGELOG.rst" directory = "changelog.d/" title_format = "Version {version}" diff --git a/setup.cfg b/setup.cfg index 8991f1c6..4087e787 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Topic :: Software Development :: Libraries :: Python Modules license = BSD @@ -56,6 +57,7 @@ norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ testpaths = tests docs filterwarnings = ignore:Function 'semver.*:DeprecationWarning + # ' <- This apostroph is just to fix syntax highlighting addopts = --no-cov-on-fail --cov=semver diff --git a/tox.ini b/tox.ini index 8c7eb5e5..8213cd55 100644 --- a/tox.ini +++ b/tox.ini @@ -1,28 +1,38 @@ [tox] envlist = checks - py{36,37,38,39,310} + py3{6,7,8,9,10,11,12} isolated_build = True +skip_missing_interpreters = True [gh-actions] python = 3.6: py36 - 3.7: py37 + # setuptools >=62 needs Python >=3.7 + 3.7: py37,check 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 [testenv] -description = Run test suite for {basepython} +description = + py36: Run a slightly different test suite for {basepython} + !py36: Run test suite for {basepython} allowlist_externals = make commands = pytest {posargs:} deps = pytest pytest-cov + # py36: dataclasses + !py36: setuptools>=62.0 + !py36: setuptools-scm setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 + [testenv:black] description = Check for formatting changes basepython = python3 From f7a6eda6f151d6df8db987425c25459dc87c7b20 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 10 Nov 2022 11:33:23 +0100 Subject: [PATCH 54/86] CI: Update GH Actions * Use v3 for some Actions * Move to 3.11 instead of RC --- .github/workflows/python-testing.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 336e4980..48352b12 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -11,7 +11,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Output env variables run: | echo "Default branch=${default-branch}" @@ -32,7 +32,7 @@ jobs: echo "\n" echo "::debug::---end" - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: 3.7 - name: Install dependencies @@ -49,14 +49,19 @@ jobs: strategy: max-parallel: 5 matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11.0-rc.2", + python-version: ["3.6", + "3.7", + "3.8", + "3.9", + "3.10", + "3.11", # "3.12" ] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 98e845c804635ad533bd1cc7707297c5c1af9dd9 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 10 Nov 2022 14:27:09 +0100 Subject: [PATCH 55/86] Add missing Optional type annotation --- src/semver/__main__.py | 4 ++-- src/semver/_deprecated.py | 8 ++++---- src/semver/cli.py | 4 ++-- src/semver/version.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/semver/__main__.py b/src/semver/__main__.py index 7fde54d7..a6d448aa 100644 --- a/src/semver/__main__.py +++ b/src/semver/__main__.py @@ -11,12 +11,12 @@ """ import os.path import sys -from typing import List +from typing import List, Optional from semver import cli -def main(cliargs: List[str] = None) -> int: +def main(cliargs: Optional[List[str]] = None) -> int: if __package__ == "": path = os.path.dirname(os.path.dirname(__file__)) sys.path[0:0] = [path] diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py index 61ceae12..5f51c8f3 100644 --- a/src/semver/_deprecated.py +++ b/src/semver/_deprecated.py @@ -7,7 +7,7 @@ import warnings from functools import partial, wraps from types import FrameType -from typing import Type, Callable, cast +from typing import Type, Callable, Optional, cast from . import cli from .version import Version @@ -15,9 +15,9 @@ def deprecated( - func: F = None, - replace: str = None, - version: str = None, + func: Optional[F] = None, + replace: Optional[str] = None, + version: Optional[str] = None, category: Type[Warning] = DeprecationWarning, ) -> Decorator: """ diff --git a/src/semver/cli.py b/src/semver/cli.py index 65ca5187..3c573d63 100644 --- a/src/semver/cli.py +++ b/src/semver/cli.py @@ -12,7 +12,7 @@ import argparse import sys -from typing import cast, List +from typing import cast, List, Optional from .version import Version from .__about__ import __version__ @@ -152,7 +152,7 @@ def process(args: argparse.Namespace) -> str: return args.func(args) -def main(cliargs: List[str] = None) -> int: +def main(cliargs: Optional[List[str]] = None) -> int: """ Entry point for the application script. diff --git a/src/semver/version.py b/src/semver/version.py index 34eb51e0..04e7faae 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -107,8 +107,8 @@ def __init__( major: SupportsInt, minor: SupportsInt = 0, patch: SupportsInt = 0, - prerelease: Union[String, int] = None, - build: Union[String, int] = None, + prerelease: Optional[Union[String, int]] = None, + build: Optional[Union[String, int]] = None, ): # Build a dictionary of the arguments except prerelease and build version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)} From 62a2fef61f78e9b558c57abf6452f475db10057d Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 16 Nov 2022 21:42:06 +0100 Subject: [PATCH 56/86] CI: Raise version for GH Actions To avoid deprecation warnings, the GitHub workflow file is adjusted to newer versions. --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bff8173b..f310a7e3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From 5d606141ebc32a6a1cff82a87a077ed0f6c69090 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 17 Nov 2022 09:10:31 +0100 Subject: [PATCH 57/86] Revert "CI: Raise version for GH Actions" This reverts commit 62a2fef61f78e9b558c57abf6452f475db10057d. --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f310a7e3..bff8173b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v1 From 90bff70d4521bce2656efd1ce67f88023324009e Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 16 Nov 2022 21:42:06 +0100 Subject: [PATCH 58/86] CI: Raise version for GH Actions To avoid deprecation warnings, the GitHub workflow file is adjusted to newer versions. --- .github/workflows/codeql-analysis.yml | 8 ++++---- .github/workflows/python-testing.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bff8173b..f310a7e3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 48352b12..9159ea07 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -55,13 +55,13 @@ jobs: "3.9", "3.10", "3.11", - # "3.12" + # "3.12-dev" ] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 24e70e902bf189ad84ef97018156485ca4935277 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 26 Nov 2022 11:59:49 +0100 Subject: [PATCH 59/86] Fix #374: Adapt pyproject.tom for Towncrier * Replace the old, deprecated ``[[tool.towncrier.type]]`` entries with ``[tool.towncrier.fragment.]``. Described in https://towncrier.readthedocs.io/en/stable/configuration.html#deprecated-defining-custom-fragment-types-with-an-array-of-toml-tables * Add a changelog news file Co-authored-by: Nagidal --- changelog.d/374.bugfix.rst | 3 +++ pyproject.toml | 46 ++++++++++++-------------------------- 2 files changed, 17 insertions(+), 32 deletions(-) create mode 100644 changelog.d/374.bugfix.rst diff --git a/changelog.d/374.bugfix.rst b/changelog.d/374.bugfix.rst new file mode 100644 index 00000000..fd4e7ea4 --- /dev/null +++ b/changelog.d/374.bugfix.rst @@ -0,0 +1,3 @@ +Correct Towncrier's config entries in the :file:`pyproject.toml` file. +The old entries ``[[tool.towncrier.type]]`` are deprecated and need +to be replaced by ``[tool.towncrier.fragment.]``. diff --git a/pyproject.toml b/pyproject.toml index ba4be51b..03e4873a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,42 +40,24 @@ template = "changelog.d/_template.rst" # issue_format = "`#{issue} `_" # issue_format = ":gh:`{issue}`" - # [[tool.towncrier.type]] - # directory = "breaking" - # name = "Breaking Changes" - # showcontent = true - [[tool.towncrier.type]] - directory = "deprecation" - name = "Deprecations" - showcontent = true +[tool.towncrier.fragment.breaking] +name = "Breaking Changes" - [[tool.towncrier.type]] - directory = "feature" - name = "Features" - showcontent = true +[tool.towncrier.fragment.bugfix] +name = "Bug fixes" - # [[tool.towncrier.type]] - # directory = "improvement" - # name = "Improvements" - # showcontent = true +[tool.towncrier.fragment.deprecation] +name = "Deprecations" - [[tool.towncrier.type]] - directory = "bugfix" - name = "Bug Fixes" - showcontent = true +[tool.towncrier.fragment.doc] +name = "Improved documentation" - [[tool.towncrier.type]] - directory = "doc" - name = "Improved Documentation" - showcontent = true +[tool.towncrier.fragment.feature] +name = "Features" - [[tool.towncrier.type]] - directory = "trivial" - name = "Trivial/Internal Changes" - showcontent = true +[tool.towncrier.fragment.removal] +name = "Removals" - [[tool.towncrier.type]] - directory = "removal" - name = "Removals" - showcontent = true +[tool.towncrier.fragment.trivial] +name = "Trivial/Internal Changes" From ae716f02d610039d389afa6f056e629b6372a212 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 18 Dec 2022 09:57:25 +0100 Subject: [PATCH 60/86] Fix #372: Remove support for Python 3.6 Python 3.6 reached its end of life and isn't supported anymore. At the time of writing (Dec 2022), the lowest version is 3.7. --- .github/workflows/python-testing.yml | 3 +-- BUILDING.rst | 3 ++- CONTRIBUTING.rst | 16 ++++++++-------- README.rst | 4 ++-- pyproject.toml | 2 +- setup.cfg | 4 ++-- setup.py | 4 ---- tox.ini | 17 ++++++++--------- 8 files changed, 24 insertions(+), 29 deletions(-) delete mode 100644 setup.py diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 9159ea07..b875bb9c 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -49,8 +49,7 @@ jobs: strategy: max-parallel: 5 matrix: - python-version: ["3.6", - "3.7", + python-version: ["3.7", "3.8", "3.9", "3.10", diff --git a/BUILDING.rst b/BUILDING.rst index 6de412f8..61f3d4cb 100644 --- a/BUILDING.rst +++ b/BUILDING.rst @@ -12,7 +12,8 @@ Building semver This project changed slightly its way how it is built. The reason for this was to still support the "traditional" way with :command:`setup.py`, but at the same time try out the newer way with :file:`pyproject.toml`. -Over time, once Python 3.6 gets deprecated, we will support only the newer way. +As Python 3.6 got deprecated, this project does support from now on only +:file:`pyproject.toml`. Background information diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e0210cc9..fe531990 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -99,11 +99,11 @@ You can decide to run the complete test suite or only part of it: $ tox --skip-missing-interpreters It is possible to use one or more specific Python versions. Use the ``-e`` - option and one or more abbreviations (``py36`` for Python 3.6, ``py37`` for - Python 3.7 etc.):: + option and one or more abbreviations (``py37`` for Python 3.7, + ``py38`` for Python 3.8 etc.):: - $ tox -e py36 - $ tox -e py36,py37 + $ tox -e py37 + $ tox -e py37,py38 To get a complete list and a short description, run:: @@ -116,7 +116,7 @@ You can decide to run the complete test suite or only part of it: :func:`test_immutable_major` in the file :file:`test_bump.py` for all Python versions:: - $ tox -e py36 -- tests/test_bump.py::test_should_bump_major + $ tox -e py37 -- tests/test_bump.py::test_should_bump_major By default, pytest prints only a dot for each test function. To reveal the executed test function, use the following syntax:: @@ -124,16 +124,16 @@ You can decide to run the complete test suite or only part of it: $ tox -- -v You can combine the specific test function with the ``-e`` option, for - example, to limit the tests for Python 3.6 and 3.7 only:: + example, to limit the tests for Python 3.7 and 3.8 only:: - $ tox -e py36,py37 -- tests/test_bump.py::test_should_bump_major + $ tox -e py37,py38 -- tests/test_bump.py::test_should_bump_major Our code is checked against formatting, style, type, and docstring issues (`black`_, `flake8`_, `mypy`_, and `docformatter`_). It is recommended to run your tests in combination with :command:`checks`, for example:: - $ tox -e checks,py36,py37 + $ tox -e checks,py37,py38 .. _doc: diff --git a/README.rst b/README.rst index 5d939976..cad99a04 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. .. note:: - This project works for Python 3.6 and greater only. If you are + This project works for Python 3.7 and greater only. If you are looking for a compatible version for Python 2, use the maintenance branch |MAINT|_. @@ -25,7 +25,7 @@ A Python module for `semantic versioning`_. Simplifies comparing versions. 2.x.y However, keep in mind, the major 2 release is frozen: no new features nor backports will be integrated. - We recommend to upgrade your workflow to Python 3.x to gain support, + We recommend to upgrade your workflow to Python 3 to gain support, bugfixes, and new features. .. |MAINT| replace:: ``maint/v2`` diff --git a/pyproject.toml b/pyproject.toml index 03e4873a..249facab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 -target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] # diff = true extend-exclude = ''' # A regex preceded with ^/ will apply only to files and directories diff --git a/setup.cfg b/setup.cfg index 4087e787..0181697d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,12 +26,12 @@ classifiers = Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Software Development :: Libraries :: Python Modules license = BSD @@ -39,7 +39,7 @@ license = BSD package_dir = =src packages = find: -python_requires = >=3.6.* +python_requires = >=3.7.* include_package_data = True [options.entry_points] diff --git a/setup.py b/setup.py deleted file mode 100644 index 88990ad8..00000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python3 -import setuptools - -setuptools.setup() # For compatibility with python 3.6 diff --git a/tox.ini b/tox.ini index 8213cd55..f55becd4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,12 @@ [tox] envlist = checks - py3{6,7,8,9,10,11,12} + py3{7,8,9,10,11,12} isolated_build = True skip_missing_interpreters = True [gh-actions] python = - 3.6: py36 # setuptools >=62 needs Python >=3.7 3.7: py37,check 3.8: py38 @@ -18,17 +17,14 @@ python = [testenv] -description = - py36: Run a slightly different test suite for {basepython} - !py36: Run test suite for {basepython} +description = Run test suite for {basepython} allowlist_externals = make commands = pytest {posargs:} deps = pytest pytest-cov - # py36: dataclasses - !py36: setuptools>=62.0 - !py36: setuptools-scm + setuptools>=62.0 + setuptools-scm setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 @@ -103,8 +99,11 @@ basepython = python3 deps = wheel twine + # PEP 517 build frontend + build commands = - python3 setup.py sdist bdist_wheel + # Same as python3 -m build + pyproject-build twine check dist/* From d8d80fa64cde14b22afe1aa5f220e1fc20b113f5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 18 Dec 2022 09:44:15 +0100 Subject: [PATCH 61/86] Fix #378: Typos in Towncrier config Co-authored-by: Nagidal --- changelog.d/378.trivial.rst | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/378.trivial.rst diff --git a/changelog.d/378.trivial.rst b/changelog.d/378.trivial.rst new file mode 100644 index 00000000..2ecd2b73 --- /dev/null +++ b/changelog.d/378.trivial.rst @@ -0,0 +1 @@ +Fix some typos in Towncrier configuration diff --git a/pyproject.toml b/pyproject.toml index 249facab..0dd64853 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,13 +45,13 @@ template = "changelog.d/_template.rst" name = "Breaking Changes" [tool.towncrier.fragment.bugfix] -name = "Bug fixes" +name = "Bug Fixes" [tool.towncrier.fragment.deprecation] name = "Deprecations" [tool.towncrier.fragment.doc] -name = "Improved documentation" +name = "Improved Documentation" [tool.towncrier.fragment.feature] name = "Features" From 223e027182121bc2622aa8408facffdaea81bab5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 18 Dec 2022 12:40:10 +0100 Subject: [PATCH 62/86] Add changelog entry for #372 --- changelog.d/372.deprecation.rst | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog.d/372.deprecation.rst diff --git a/changelog.d/372.deprecation.rst b/changelog.d/372.deprecation.rst new file mode 100644 index 00000000..c475f532 --- /dev/null +++ b/changelog.d/372.deprecation.rst @@ -0,0 +1,8 @@ +Deprecate support for Python 3.6. + +Python 3.6 reached its end of life and isn't supported anymore. +At the time of writing (Dec 2022), the lowest version is 3.7. + +Although the `poll ` +didn't cast many votes, the majority agree to remove support for +Python 3.6. From 61335e0953dc4dbae2884aee5e96d45ac009f80f Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 17 Dec 2022 15:44:07 +0100 Subject: [PATCH 63/86] General cleanup, reformat files * Reformat source code with black as some config options did accidently exclude the semver source code Mostly remove some includes/excludes in the black config. * Integrate concurrency in GH Action * Ignore Python files on project dirs in .gitignore * Remove unused patterns in MANIFEST.in * Use extend-exclude for flake in setup.cfg and adapt list. * Use skip_install=True in tox.ini for black --- .github/workflows/python-testing.yml | 7 +++++++ .gitignore | 10 +++++++--- MANIFEST.in | 2 +- changelog.d/pr384.bugfix.rst | 11 +++++++++++ docs/advanced/semverwithvprefix.py | 6 ++---- pyproject.toml | 9 +-------- setup.cfg | 15 +++++++-------- src/semver/version.py | 19 ++++++++----------- tests/test_match.py | 4 +--- tox.ini | 1 + 10 files changed, 46 insertions(+), 38 deletions(-) create mode 100644 changelog.d/pr384.bugfix.rst diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index b875bb9c..fb23fcf8 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -7,6 +7,13 @@ on: pull_request: branches: [ master ] +concurrency: + # only cancel in-progress runs of the same workflow + group: ${{ github.workflow }}-${{ github.ref }} + # ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + + jobs: check: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index d26b5515..c0859cd3 100644 --- a/.gitignore +++ b/.gitignore @@ -259,8 +259,12 @@ fabric.properties # -------- -# Patch/Diff Files -*.patch -*.diff +# Ignore files in the project's root: +/*.patch +/*.diff +/*.py +# but not this file: +!/setup.py + docs/_api !docs/_api/semver.__about__.rst diff --git a/MANIFEST.in b/MANIFEST.in index 7b2a7b61..e37851c9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,4 +5,4 @@ include tests/test_*.py prune docs/_build recursive-exclude .github * -global-exclude *.py[cod] __pycache__ *.so *.dylib +global-exclude __pycache__ diff --git a/changelog.d/pr384.bugfix.rst b/changelog.d/pr384.bugfix.rst new file mode 100644 index 00000000..ca0b08d0 --- /dev/null +++ b/changelog.d/pr384.bugfix.rst @@ -0,0 +1,11 @@ +General cleanup, reformat files: + +* Reformat source code with black again as some config options + did accidentely exclude the semver source code. + Mostly remove some includes/excludes in the black config. +* Integrate concurrency in GH Action +* Ignore Python files on project dirs in .gitignore +* Remove unused patterns in MANIFEST.in +* Use ``extend-exclude`` for flake in :file:`setup.cfg`` and adapt list. +* Use ``skip_install=True`` in :file:`tox.ini` for black + diff --git a/docs/advanced/semverwithvprefix.py b/docs/advanced/semverwithvprefix.py index 4395a95e..f2a7fecd 100644 --- a/docs/advanced/semverwithvprefix.py +++ b/docs/advanced/semverwithvprefix.py @@ -17,10 +17,8 @@ def parse(cls, version: str) -> "SemVerWithVPrefix": """ if not version[0] in ("v", "V"): raise ValueError( - "{v!r}: not a valid semantic version tag. " - "Must start with 'v' or 'V'".format( - v=version - ) + f"{version!r}: not a valid semantic version tag. " + "Must start with 'v' or 'V'" ) return super().parse(version[1:]) diff --git a/pyproject.toml b/pyproject.toml index 0dd64853..d288e68e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,14 +21,7 @@ build-backend = "setuptools.build_meta" line-length = 88 target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] # diff = true -extend-exclude = ''' -# A regex preceded with ^/ will apply only to files and directories -# in the root of the project. -^/*.py -''' -include = ''' -^/setup.py -''' + [tool.towncrier] package = "semver" diff --git a/setup.cfg b/setup.cfg index 0181697d..87ec3b8f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,10 +55,12 @@ semver = py.typed [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ testpaths = tests docs +# pythonpath = src filterwarnings = ignore:Function 'semver.*:DeprecationWarning # ' <- This apostroph is just to fix syntax highlighting addopts = + # --import-mode=importlib --no-cov-on-fail --cov=semver --cov-report=term-missing @@ -69,18 +71,15 @@ addopts = [flake8] max-line-length = 88 ignore = F821,W503 -exclude = - src/semver/__init__.py - .env - venv +extend-exclude = .eggs - .tox - .git - __pycache__ + .env build - dist docs + venv conftest.py + src/semver/__init__.py + tasks.py [pycodestyle] count = False diff --git a/src/semver/version.py b/src/semver/version.py index 04e7faae..96281192 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -69,8 +69,7 @@ class Version: #: Regex for number in a prerelease _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") #: Regex template for a semver version - _REGEX_TEMPLATE = \ - r""" + _REGEX_TEMPLATE = r""" ^ (?P0|[1-9]\d*) (?: @@ -93,12 +92,12 @@ class Version: """ #: Regex for a semver version _REGEX = re.compile( - _REGEX_TEMPLATE.format(opt_patch='', opt_minor=''), + _REGEX_TEMPLATE.format(opt_patch="", opt_minor=""), re.VERBOSE, ) #: Regex for a semver version that might be shorter _REGEX_OPTIONAL_MINOR_AND_PATCH = re.compile( - _REGEX_TEMPLATE.format(opt_patch='?', opt_minor='?'), + _REGEX_TEMPLATE.format(opt_patch="?", opt_minor="?"), re.VERBOSE, ) @@ -571,9 +570,7 @@ def match(self, match_expr: str) -> bool: @classmethod def parse( - cls, - version: String, - optional_minor_and_patch: bool = False + cls, version: String, optional_minor_and_patch: bool = False ) -> "Version": """ Parse version string to a Version instance. @@ -611,10 +608,10 @@ def parse( raise ValueError(f"{version} is not valid SemVer string") matched_version_parts: Dict[str, Any] = match.groupdict() - if not matched_version_parts['minor']: - matched_version_parts['minor'] = 0 - if not matched_version_parts['patch']: - matched_version_parts['patch'] = 0 + if not matched_version_parts["minor"]: + matched_version_parts["minor"] = 0 + if not matched_version_parts["patch"]: + matched_version_parts["patch"] = 0 return cls(**matched_version_parts) diff --git a/tests/test_match.py b/tests/test_match.py index 2476eb01..e2685cae 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -60,9 +60,7 @@ def test_should_raise_value_error_for_unexpected_match_expression(left, right): match(left, right) -@pytest.mark.parametrize( - "left,right", [("1.0.0", ""), ("1.0.0", "!")] -) +@pytest.mark.parametrize("left,right", [("1.0.0", ""), ("1.0.0", "!")]) def test_should_raise_value_error_for_invalid_match_expression(left, right): with pytest.raises(ValueError): match(left, right) diff --git a/tox.ini b/tox.ini index f55becd4..8ca917b8 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,7 @@ setenv = [testenv:black] description = Check for formatting changes basepython = python3 +skip_install = true deps = black commands = black --check {posargs:.} From 89d5423e3ecdb6605cc49f4af1b11d81d0e65005 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 18 Dec 2022 20:46:24 +0100 Subject: [PATCH 64/86] Create 3.0.0-dev.4 Release * Update CHANGELOG * Fix small typos inside changelog.d/ --- CHANGELOG.rst | 88 +++++++++++++++++++++++++++++++++ CONTRIBUTORS | 9 ++-- changelog.d/335.doc.rst | 2 - changelog.d/340.doc.rst | 1 - changelog.d/343.doc.rst | 2 - changelog.d/350.doc.rst | 2 - changelog.d/351.doc.rst | 4 -- changelog.d/364.feature.rst | 3 -- changelog.d/372.deprecation.rst | 8 --- changelog.d/374.bugfix.rst | 3 -- changelog.d/378.trivial.rst | 1 - changelog.d/pr359.feature.rst | 2 - changelog.d/pr362.feature.rst | 2 - docs/usage/semver-version.rst | 2 +- src/semver/__about__.py | 2 +- 15 files changed, 96 insertions(+), 35 deletions(-) delete mode 100644 changelog.d/335.doc.rst delete mode 100644 changelog.d/340.doc.rst delete mode 100644 changelog.d/343.doc.rst delete mode 100644 changelog.d/350.doc.rst delete mode 100644 changelog.d/351.doc.rst delete mode 100644 changelog.d/364.feature.rst delete mode 100644 changelog.d/372.deprecation.rst delete mode 100644 changelog.d/374.bugfix.rst delete mode 100644 changelog.d/378.trivial.rst delete mode 100644 changelog.d/pr359.feature.rst delete mode 100644 changelog.d/pr362.feature.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3173507f..7f515aa1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,94 @@ in our repository. .. towncrier release notes start +Version 3.0.0-dev.4 +=================== + +:Released: 2022-12-18 +:Maintainer: + + +Bug Fixes +--------- + +* :gh:`374`: Correct Towncrier's config entries in the :file:`pyproject.toml` file. + The old entries ``[[tool.towncrier.type]]`` are deprecated and need + to be replaced by ``[tool.towncrier.fragment.]``. + + + +Deprecations +------------ + +* :gh:`372`: Deprecate support for Python 3.6. + + Python 3.6 reached its end of life and isn't supported anymore. + At the time of writing (Dec 2022), the lowest version is 3.7. + + Although the `poll `_ + didn't cast many votes, the majority agree to remove support for + Python 3.6. + + + +Improved Documentation +---------------------- + +* :gh:`335`: Add new section "Converting versions between PyPI and semver" the limitations + and possible use cases to convert from one into the other versioning scheme. + +* :gh:`340`: Describe how to get version from a file + +* :gh:`343`: Describe combining Pydantic with semver in the "Advanced topic" + section. + +* :gh:`350`: Restructure usage section. Create subdirectory "usage/" and splitted + all section into different files. + +* :gh:`351`: Introduce new topics for: + + * "Migration to semver3" + * "Advanced topics" + + + +Features +-------- + +* :pr:`359`: Add optional parameter ``optional_minor_and_patch`` in :meth:`.Version.parse` to allow optional + minor and patch parts. + +* :pr:`362`: Make :meth:`.Version.match` accept a bare version string as match expression, defaulting to + equality testing. + +* :gh:`364`: Enhance :file:`pyproject.toml` to make it possible to use the + :command:`pyproject-build` command from the build module. + For more information, see :ref:`build-semver`. + +* :gh:`365`: Improve :file:`pyproject.toml`. + + * Use setuptools, add metadata. Taken approach from + `A Practical Guide to Setuptools and Pyproject.toml + `_. + * Doc: Describe building of semver + * Remove :file:`.travis.yml` in :file:`MANIFEST.in` + (not needed anymore) + * Distinguish between Python 3.6 and others in :file:`tox.ini` + * Add skip_missing_interpreters option for :file:`tox.ini` + * GH Action: Upgrade setuptools and setuptools-scm and test + against 3.11.0-rc.2 + + + +Trivial/Internal Changes +------------------------ + +* :gh:`378`: Fix some typos in Towncrier configuration + + + +---- + Version 3.0.0-dev.3 =================== diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9e63f4d4..e5fef99b 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -2,7 +2,7 @@ Contributors ############ -Python SemVer library +Python Semver library ##################### This document records the primary maintainers and significant @@ -14,9 +14,13 @@ Thank you to everyone whose work has made this possible. Primary maintainers =================== -* Kostiantyn Rybnikov +* Tom Schraitle * Sébastien Celles +Old maintainer: + +* Kostiantyn Rybnikov + Significant contributors ======================== @@ -37,7 +41,6 @@ Significant contributors * robi-wan * sbrudenell * T. Jameson Little -* Tom Schraitle * Thomas Laferriere * Tuure Laurinolli * Tyler Cross diff --git a/changelog.d/335.doc.rst b/changelog.d/335.doc.rst deleted file mode 100644 index 1d29fb87..00000000 --- a/changelog.d/335.doc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Add new section "Converting versions between PyPI and semver" the limitations -and possible use cases to convert from one into the other versioning scheme. diff --git a/changelog.d/340.doc.rst b/changelog.d/340.doc.rst deleted file mode 100644 index 807e401c..00000000 --- a/changelog.d/340.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Describe how to get version from a file \ No newline at end of file diff --git a/changelog.d/343.doc.rst b/changelog.d/343.doc.rst deleted file mode 100644 index 630d7474..00000000 --- a/changelog.d/343.doc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Describe combining Pydantic with semver in the "Advanced topic" -section. diff --git a/changelog.d/350.doc.rst b/changelog.d/350.doc.rst deleted file mode 100644 index 2fa92f0a..00000000 --- a/changelog.d/350.doc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Restructure usage section. Create subdirectory "usage/" and splitted -all section into different files. diff --git a/changelog.d/351.doc.rst b/changelog.d/351.doc.rst deleted file mode 100644 index 0b5199fa..00000000 --- a/changelog.d/351.doc.rst +++ /dev/null @@ -1,4 +0,0 @@ -Introduce new topics for: - -* "Migration to semver3" -* "Advanced topics" diff --git a/changelog.d/364.feature.rst b/changelog.d/364.feature.rst deleted file mode 100644 index 885a6c85..00000000 --- a/changelog.d/364.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Enhance :file:`pyproject.toml` to make it possible to use the -:command:`pyproject-build` command from the build module. -For more information, see :ref:`build-semver`. diff --git a/changelog.d/372.deprecation.rst b/changelog.d/372.deprecation.rst deleted file mode 100644 index c475f532..00000000 --- a/changelog.d/372.deprecation.rst +++ /dev/null @@ -1,8 +0,0 @@ -Deprecate support for Python 3.6. - -Python 3.6 reached its end of life and isn't supported anymore. -At the time of writing (Dec 2022), the lowest version is 3.7. - -Although the `poll ` -didn't cast many votes, the majority agree to remove support for -Python 3.6. diff --git a/changelog.d/374.bugfix.rst b/changelog.d/374.bugfix.rst deleted file mode 100644 index fd4e7ea4..00000000 --- a/changelog.d/374.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Correct Towncrier's config entries in the :file:`pyproject.toml` file. -The old entries ``[[tool.towncrier.type]]`` are deprecated and need -to be replaced by ``[tool.towncrier.fragment.]``. diff --git a/changelog.d/378.trivial.rst b/changelog.d/378.trivial.rst deleted file mode 100644 index 2ecd2b73..00000000 --- a/changelog.d/378.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Fix some typos in Towncrier configuration diff --git a/changelog.d/pr359.feature.rst b/changelog.d/pr359.feature.rst deleted file mode 100644 index 5c18c9d2..00000000 --- a/changelog.d/pr359.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Add optional parameter ``optional_minor_and_patch`` in :meth:`.Version.parse` to allow optional -minor and patch parts. diff --git a/changelog.d/pr362.feature.rst b/changelog.d/pr362.feature.rst deleted file mode 100644 index 1b7cc120..00000000 --- a/changelog.d/pr362.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Make :meth:`.Version.match` accept a bare version string as match expression, defaulting to -equality testing. diff --git a/docs/usage/semver-version.rst b/docs/usage/semver-version.rst index b3d2c274..8eeab62f 100644 --- a/docs/usage/semver-version.rst +++ b/docs/usage/semver-version.rst @@ -4,4 +4,4 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.0-dev.3' + '3.0.0-dev.4' diff --git a/src/semver/__about__.py b/src/semver/__about__.py index d1dc8e3f..0f7150bf 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -16,7 +16,7 @@ """ #: Semver version -__version__ = "3.0.0-dev.3" +__version__ = "3.0.0-dev.4" #: Original semver author __author__ = "Kostiantyn Rybnikov" From f24a1dace51759d57f00a22164156380be5ae3f9 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 20 Dec 2022 10:18:21 +0100 Subject: [PATCH 65/86] tox/pytest: Add testpaths and use importlib Switch to the more modern importlib approach as it doesn't require to modify sys.path: https://docs.pytest.org/en/7.2.x/explanation/pythonpath.html --- changelog.d/388.trivial.rst | 3 +++ setup.cfg | 4 ++-- tests/conftest.py | 2 -- tox.ini | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog.d/388.trivial.rst diff --git a/changelog.d/388.trivial.rst b/changelog.d/388.trivial.rst new file mode 100644 index 00000000..236bce6f --- /dev/null +++ b/changelog.d/388.trivial.rst @@ -0,0 +1,3 @@ +For pytest, switch to the more modern :mod:`importlib` approach +as it doesn't require to modify :data:`sys.path`: +https://docs.pytest.org/en/7.2.x/explanation/pythonpath.html \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 87ec3b8f..2673b0f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,12 +55,12 @@ semver = py.typed [tool:pytest] norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ testpaths = tests docs -# pythonpath = src +pythonpath = src tests filterwarnings = ignore:Function 'semver.*:DeprecationWarning # ' <- This apostroph is just to fix syntax highlighting addopts = - # --import-mode=importlib + --import-mode=importlib --no-cov-on-fail --cov=semver --cov-report=term-missing diff --git a/tests/conftest.py b/tests/conftest.py index beecffc9..9017bbbe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,8 +4,6 @@ import semver -# sys.path.insert(0, "docs/usage") - from coerce import coerce # noqa:E402 from semverwithvprefix import SemVerWithVPrefix # noqa:E402 import packaging.version diff --git a/tox.ini b/tox.ini index 8ca917b8..2b47562e 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ python = [testenv] description = Run test suite for {basepython} allowlist_externals = make +skip_install = true commands = pytest {posargs:} deps = pytest From 86fcf481e35d77802faec227845f05c8a1994e73 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Fri, 23 Dec 2022 16:19:29 +0100 Subject: [PATCH 66/86] Introduce public Version.NAMES class variable This class variable contains a tuple of strings that contains the names of all attributes of a Version (like "major", "minor" etc). In cases we need to have dynamical values, this makes it easier to iterate. --- changelog.d/pr389.trivial.rst | 6 ++++++ src/semver/version.py | 17 +++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 changelog.d/pr389.trivial.rst diff --git a/changelog.d/pr389.trivial.rst b/changelog.d/pr389.trivial.rst new file mode 100644 index 00000000..12829855 --- /dev/null +++ b/changelog.d/pr389.trivial.rst @@ -0,0 +1,6 @@ +Add public class variable :data:`Version.NAMES `. + +This class variable contains a tuple of strings that contains the names of +all attributes of a Version (like ``"major"``, ``"minor"`` etc). + +In cases we need to have dynamical values, this makes it easier to iterate. diff --git a/src/semver/version.py b/src/semver/version.py index 96281192..a121d3cf 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -66,6 +66,10 @@ class Version: """ __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") + + #: The names of the different parts of a version + NAMES = tuple([item[1:] for item in __slots__]) + #: Regex for number in a prerelease _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") #: Regex template for a semver version @@ -398,13 +402,9 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": :param prerelease_token: prefix string of prerelease, defaults to 'rc' :return: new object with the appropriate part raised """ - validparts = { - "major", - "minor", - "patch", - "prerelease", - # "build", # currently not used - } + cls = type(self) + # "build" is currently not used, that's why we use [:-1] + validparts = cls.NAMES[:-1] if part not in validparts: raise ValueError( "Invalid part. Expected one of {validparts}, but got {part!r}".format( @@ -419,7 +419,8 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": ): return version.replace(prerelease=None, build=None) - if part in ("major", "minor", "patch"): + # Only check the main parts: + if part in cls.NAMES[:3]: return getattr(version, "bump_" + part)() if not version.prerelease: From 35da4f6715ceaf251d6358de927547b5143a8184 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 31 Jan 2023 08:59:49 -0500 Subject: [PATCH 67/86] Fix pydantic/semver example The current example, when used with semver 3.0.0.dev4 and pydantic 1.10.4, produces the following error: pydantic.errors.ConfigError: Invalid signature for validator >: (version: Union[str, bytes], optional_minor_and_patch: bool = False) -> 'Version', should be: (value, values, config, field), "values", "config" and "field" are all optional. This commit fixes the example in the documentation so that it works and does not raise an error. --- changelog.d/pr392.doc.rst | 1 + docs/advanced/combine-pydantic-and-semver.rst | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog.d/pr392.doc.rst diff --git a/changelog.d/pr392.doc.rst b/changelog.d/pr392.doc.rst new file mode 100644 index 00000000..e542cef9 --- /dev/null +++ b/changelog.d/pr392.doc.rst @@ -0,0 +1 @@ +Fix the example in the documentation for combining semver and pydantic. diff --git a/docs/advanced/combine-pydantic-and-semver.rst b/docs/advanced/combine-pydantic-and-semver.rst index a9249daf..a00c2cff 100644 --- a/docs/advanced/combine-pydantic-and-semver.rst +++ b/docs/advanced/combine-pydantic-and-semver.rst @@ -17,10 +17,14 @@ To work with Pydantic, use the following steps: from semver import Version class PydanticVersion(Version): + @classmethod + def _parse(cls, version): + return cls.parse(version) + @classmethod def __get_validators__(cls): """Return a list of validator methods for pydantic models.""" - yield cls.parse + yield cls._parse @classmethod def __modify_schema__(cls, field_schema): From a5f3a69d524b06486b70b4c626d458edd0aa83ba Mon Sep 17 00:00:00 2001 From: "Zane.Geiger" Date: Wed, 8 Feb 2023 14:54:23 -0500 Subject: [PATCH 68/86] Fix: All `python -m semver` commands fail with 'error: invalid choice:' This was caused by __main__.py passing `sys.argv` to `cli.main()`, which includes the path of the script in `sys.argv[0]`. `argparse.ArgumentParser.parse_args()` parses this as the subcommand name, causing it to throw an error. --- src/semver/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semver/__main__.py b/src/semver/__main__.py index a6d448aa..6cb11f09 100644 --- a/src/semver/__main__.py +++ b/src/semver/__main__.py @@ -25,4 +25,4 @@ def main(cliargs: Optional[List[str]] = None) -> int: if __name__ == "__main__": - sys.exit(main(sys.argv)) + sys.exit(main(sys.argv[1:])) From a1604fb9e8841d6cddbaff97bd4b8057b9600ae5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 22 Feb 2023 13:09:37 +0100 Subject: [PATCH 69/86] Add changelog entry for pr #393 --- changelog.d/pr393.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr393.bugfix.rst diff --git a/changelog.d/pr393.bugfix.rst b/changelog.d/pr393.bugfix.rst new file mode 100644 index 00000000..74a81f65 --- /dev/null +++ b/changelog.d/pr393.bugfix.rst @@ -0,0 +1 @@ +Fix command :command:`python -m semver` to avoid the error "invalid choice" From 6176316e49c5279118178330d050548eefe71927 Mon Sep 17 00:00:00 2001 From: Dennis Felsing Date: Tue, 28 Feb 2023 08:04:52 +0100 Subject: [PATCH 70/86] Fix return type of classmethod parse (#396) So that calling parse on a derived class will show correct type of derived class --- src/semver/version.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/semver/version.py b/src/semver/version.py index a121d3cf..bf949bb4 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -14,6 +14,8 @@ cast, Callable, Collection, + Type, + TypeVar, ) from ._types import ( @@ -28,6 +30,8 @@ Comparable = Union["Version", Dict[str, VersionPart], Collection[VersionPart], str] Comparator = Callable[["Version", Comparable], bool] +T = TypeVar("T", bound="Version") + def _comparator(operator: Comparator) -> Comparator: """Wrap a Version binary op method in a type-check.""" @@ -571,8 +575,8 @@ def match(self, match_expr: str) -> bool: @classmethod def parse( - cls, version: String, optional_minor_and_patch: bool = False - ) -> "Version": + cls: Type[T], version: String, optional_minor_and_patch: bool = False + ) -> T: """ Parse version string to a Version instance. From bee273e9a5b825ff410e3405eb41a339814813d1 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Tue, 28 Feb 2023 08:10:43 +0100 Subject: [PATCH 71/86] Add changelog entry for PR #396 --- changelog.d/pr396.bug.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/pr396.bug.rst diff --git a/changelog.d/pr396.bug.rst b/changelog.d/pr396.bug.rst new file mode 100644 index 00000000..2b9032e2 --- /dev/null +++ b/changelog.d/pr396.bug.rst @@ -0,0 +1,2 @@ +Calling :func:`semver.Version.parse` on a derived class will show correct type +of derived class. From 5485b6bd2d03139b3bae82c7fd50195a5595322c Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 16 Nov 2022 08:34:21 +0100 Subject: [PATCH 72/86] Fix #284: implement "is_compatible" with method * Implement Version.is_compatible() method * Update test cases * Update documentation * Rename isvalid method to is_valid to make it consistent with is_compatible The result is True, if either of the following is true: * both versions are equal, or * both majors are equal and higher than 0. Same for both minors. Both pre-releases are equal, or * both majors are equal and higher than 0. The minor of b's minor version is higher then a's. Both pre-releases are equal. The algorithm does *not* check patches! Co-authored-by: Lexi Robinson Co-authored-by: Thomas Laferriere Co-authored-by: Raphael Krupinski --- .gitignore | 3 + changelog.d/284.deprecation.rst | 5 + changelog.d/284.doc.rst | 1 + changelog.d/284.feature.rst | 1 + docs/conf.py | 4 +- docs/migration/migratetosemver3.rst | 12 ++- .../replace-deprecated-functions.rst | 5 + .../usage/check-compatible-semver-version.rst | 95 +++++++++++++++++++ docs/usage/check-valid-semver-version.rst | 4 +- docs/usage/index.rst | 1 + src/semver/cli.py | 2 +- src/semver/version.py | 38 +++++++- tests/test_semver.py | 59 +++++++++++- tox.ini | 2 +- 14 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 changelog.d/284.deprecation.rst create mode 100644 changelog.d/284.doc.rst create mode 100644 changelog.d/284.feature.rst create mode 100644 docs/usage/check-compatible-semver-version.rst diff --git a/.gitignore b/.gitignore index c0859cd3..dead3352 100644 --- a/.gitignore +++ b/.gitignore @@ -268,3 +268,6 @@ fabric.properties docs/_api !docs/_api/semver.__about__.rst + +# For node +node_modules/ diff --git a/changelog.d/284.deprecation.rst b/changelog.d/284.deprecation.rst new file mode 100644 index 00000000..738a14fc --- /dev/null +++ b/changelog.d/284.deprecation.rst @@ -0,0 +1,5 @@ +Deprecate the use of :meth:`Version.isvalid`. + +Rename :meth:`Version.isvalid ` +to :meth:`Version.is_valid ` +for consistency reasons with :meth:`Version.is_compatible ` \ No newline at end of file diff --git a/changelog.d/284.doc.rst b/changelog.d/284.doc.rst new file mode 100644 index 00000000..6fa8e53a --- /dev/null +++ b/changelog.d/284.doc.rst @@ -0,0 +1 @@ +Document deprecation of :meth:`Version.isvalid`. \ No newline at end of file diff --git a/changelog.d/284.feature.rst b/changelog.d/284.feature.rst new file mode 100644 index 00000000..f13d7300 --- /dev/null +++ b/changelog.d/284.feature.rst @@ -0,0 +1 @@ +Implement :meth:`Version.is_compatible ` to make "is self compatible with X". diff --git a/docs/conf.py b/docs/conf.py index ed888361..9edfda4d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -118,8 +118,8 @@ def find_version(*file_paths): # Markup to shorten external links # See https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html extlinks = { - "gh": ("https://github.com/python-semver/python-semver/issues/%s", "#"), - "pr": ("https://github.com/python-semver/python-semver/pull/%s", "PR #"), + "gh": ("https://github.com/python-semver/python-semver/issues/%s", "#%s"), + "pr": ("https://github.com/python-semver/python-semver/pull/%s", "PR #%s"), } # -- Options for HTML output ---------------------------------------------- diff --git a/docs/migration/migratetosemver3.rst b/docs/migration/migratetosemver3.rst index f869cad3..852ea68b 100644 --- a/docs/migration/migratetosemver3.rst +++ b/docs/migration/migratetosemver3.rst @@ -3,8 +3,9 @@ Migrating from semver2 to semver3 ================================= -This document describes the visible differences for +This section describes the visible differences for users and how your code stays compatible for semver3. +Some changes are backward incompatible. Although the development team tries to make the transition to semver3 as smooth as possible, at some point change @@ -34,9 +35,16 @@ Use semver.cli instead of semver -------------------------------- All functions related to CLI parsing are moved to :mod:`semver.cli`. -If you are such functions, like :func:`semver.cmd_bump `, +If you need such functions, like :func:`semver.cmd_bump `, import it from :mod:`semver.cli` in the future: .. code-block:: python from semver.cli import cmd_bump + + +Use semver.Version.is_valid instead of semver.Version.isvalid +------------------------------------------------------------- + +The pull request :pr:`284` introduced the method :meth:`Version.is_compatible `. To keep consistency, the development team +decided to rename the :meth:`isvalid ` to :meth:`is_valid `. diff --git a/docs/migration/replace-deprecated-functions.rst b/docs/migration/replace-deprecated-functions.rst index 9738001c..8762087c 100644 --- a/docs/migration/replace-deprecated-functions.rst +++ b/docs/migration/replace-deprecated-functions.rst @@ -31,6 +31,11 @@ them with code which is compatible for future versions: Likewise with the other module level functions. +* :func:`semver.Version.isvalid` + + Replace it with :meth:`semver.Version.is_valid`: + + * :func:`semver.finalize_version` Replace it with :func:`semver.Version.finalize_version`: diff --git a/docs/usage/check-compatible-semver-version.rst b/docs/usage/check-compatible-semver-version.rst new file mode 100644 index 00000000..323de3ed --- /dev/null +++ b/docs/usage/check-compatible-semver-version.rst @@ -0,0 +1,95 @@ +Checking for a Compatible Semver Version +======================================== + +To check if a *change* from a semver version ``a`` to a semver +version ``b`` is *compatible* according to semver rule, use the method +:meth:`Version.is_compatible `. + +The expression ``a.is_compatible(b) is True`` if one of the following +statements is true: + +* both versions are equal, or +* both majors are equal and higher than 0. The same applies for both + minor parts. Both pre-releases are equal, or +* both majors are equal and higher than 0. The minor of ``b``'s + minor version is higher then ``a``'s. Both pre-releases are equal. + +In all other cases, the result is false. + +Keep in mind, the method *does not* check patches! + + +* Two different majors: + + .. code-block:: python + + >>> a = Version(1, 1, 1) + >>> b = Version(2, 0, 0) + >>> a.is_compatible(b) + False + >>> b.is_compatible(a) + False + +* Two different minors: + + .. code-block:: python + + >>> a = Version(1, 1, 0) + >>> b = Version(1, 0, 0) + >>> a.is_compatible(b) + False + >>> b.is_compatible(a) + True + +* The same two majors and minors: + + .. code-block:: python + + >>> a = Version(1, 1, 1) + >>> b = Version(1, 1, 0) + >>> a.is_compatible(b) + True + >>> b.is_compatible(a) + True + +* Release and pre-release: + + .. code-block:: python + + >>> a = Version(1, 1, 1) + >>> b = Version(1, 0, 0,'rc1') + >>> a.is_compatible(b) + False + >>> b.is_compatible(a) + False + +* Different pre-releases: + + .. code-block:: python + + >>> a = Version(1, 0, 0, 'rc1') + >>> b = Version(1, 0, 0, 'rc2') + >>> a.is_compatible(b) + False + >>> b.is_compatible(a) + False + +* Identical pre-releases: + + .. code-block:: python + + >>> a = Version(1, 0, 0,'rc1') + >>> b = Version(1, 0, 0,'rc1') + >>> a.is_compatible(b) + True + +* All major zero versions are incompatible with anything but itself: + + .. code-block:: python + + >>> Version(0, 1, 0).is_compatible(Version(0, 1, 1)) + False + + # Only identical versions are compatible for major zero versions: + >>> Version(0, 1, 0).is_compatible(Version(0, 1, 0)) + True diff --git a/docs/usage/check-valid-semver-version.rst b/docs/usage/check-valid-semver-version.rst index 7aa9615b..a0460df9 100644 --- a/docs/usage/check-valid-semver-version.rst +++ b/docs/usage/check-valid-semver-version.rst @@ -6,7 +6,7 @@ classmethod :func:`Version.isvalid `: .. code-block:: python - >>> Version.isvalid("1.0.0") + >>> Version.is_valid("1.0.0") True - >>> Version.isvalid("invalid") + >>> Version.is_valid("invalid") False diff --git a/docs/usage/index.rst b/docs/usage/index.rst index ddfc2284..4b8e3fc9 100644 --- a/docs/usage/index.rst +++ b/docs/usage/index.rst @@ -8,6 +8,7 @@ Using semver create-a-version parse-version-string check-valid-semver-version + check-compatible-semver-version access-parts-of-a-version access-parts-through-index replace-parts-of-a-version diff --git a/src/semver/cli.py b/src/semver/cli.py index 3c573d63..b2751429 100644 --- a/src/semver/cli.py +++ b/src/semver/cli.py @@ -54,7 +54,7 @@ def cmd_check(args: argparse.Namespace) -> None: :param args: The parsed arguments """ - if Version.isvalid(args.version): + if Version.is_valid(args.version): return None raise ValueError("Invalid version %r" % args.version) diff --git a/src/semver/version.py b/src/semver/version.py index bf949bb4..5edadb0a 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -647,7 +647,7 @@ def replace(self, **parts: Union[int, Optional[str]]) -> "Version": raise TypeError(error) @classmethod - def isvalid(cls, version: str) -> bool: + def is_valid(cls, version: str) -> bool: """ Check if the string is a valid semver version. @@ -663,6 +663,42 @@ def isvalid(cls, version: str) -> bool: except ValueError: return False + def is_compatible(self, other: "Version") -> bool: + """ + Check if current version is compatible with other version. + + The result is True, if either of the following is true: + + * both versions are equal, or + * both majors are equal and higher than 0. Same for both minors. + Both pre-releases are equal, or + * both majors are equal and higher than 0. The minor of b's + minor version is higher then a's. Both pre-releases are equal. + + The algorithm does *not* check patches. + + :param other: the version to check for compatibility + :return: True, if ``other`` is compatible with the old version, + otherwise False + + >>> Version(1, 1, 0).is_compatible(Version(1, 0, 0)) + False + >>> Version(1, 0, 0).is_compatible(Version(1, 1, 0)) + True + """ + if not isinstance(other, Version): + raise TypeError(f"Expected a Version type but got {type(other)}") + + # All major-0 versions should be incompatible with anything but itself + if (0 == self.major == other.major) and (self[:4] != other[:4]): + return False + + return ( + (self.major == other.major) + and (other.minor >= self.minor) + and (self.prerelease == other.prerelease) + ) + #: Keep the VersionInfo name for compatibility VersionInfo = Version diff --git a/tests/test_semver.py b/tests/test_semver.py index b15bfeaf..782d5c79 100644 --- a/tests/test_semver.py +++ b/tests/test_semver.py @@ -73,10 +73,65 @@ def test_should_be_able_to_use_integers_as_prerelease_build(): def test_should_versioninfo_isvalid(): - assert Version.isvalid("1.0.0") is True - assert Version.isvalid("foo") is False + assert Version.is_valid("1.0.0") is True + assert Version.is_valid("foo") is False def test_versioninfo_compare_should_raise_when_passed_invalid_value(): with pytest.raises(TypeError): Version(1, 2, 3).compare(4) + + +@pytest.mark.parametrize( + "old, new", + [ + ((1, 2, 3), (1, 2, 3)), + ((1, 2, 3), (1, 2, 4)), + ((1, 2, 4), (1, 2, 3)), + ((1, 2, 3, "rc.0"), (1, 2, 4, "rc.0")), + ((0, 1, 0), (0, 1, 0)), + ], +) +def test_should_succeed_compatible_match(old, new): + old = Version(*old) + new = Version(*new) + assert old.is_compatible(new) + + +@pytest.mark.parametrize( + "old, new", + [ + ((1, 1, 0), (1, 0, 0)), + ((2, 0, 0), (1, 5, 0)), + ((1, 2, 3, "rc.1"), (1, 2, 3, "rc.0")), + ((1, 2, 3, "rc.1"), (1, 2, 4, "rc.0")), + ((0, 1, 0), (0, 1, 1)), + ((1, 0, 0), (1, 0, 0, "rc1")), + ((1, 0, 0, "rc1"), (1, 0, 0)), + ], +) +def test_should_fail_compatible_match(old, new): + old = Version(*old) + new = Version(*new) + assert not old.is_compatible(new) + + +@pytest.mark.parametrize( + "wrongtype", + [ + "wrongtype", + dict(a=2), + list(), + ], +) +def test_should_fail_with_incompatible_type_for_compatible_match(wrongtype): + with pytest.raises(TypeError, match="Expected a Version type .*"): + v = Version(1, 2, 3) + v.is_compatible(wrongtype) + + +def test_should_succeed_with_compatible_subclass_for_is_compatible(): + class CustomVersion(Version): + ... + + assert CustomVersion(1, 0, 0).is_compatible(Version(1, 0, 0)) diff --git a/tox.ini b/tox.ini index 2b47562e..dd071721 100644 --- a/tox.ini +++ b/tox.ini @@ -18,8 +18,8 @@ python = [testenv] description = Run test suite for {basepython} -allowlist_externals = make skip_install = true +allowlist_externals = make commands = pytest {posargs:} deps = pytest From f8a182f5ec52ba29c60059d40a91e749d0f56df9 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 8 Oct 2022 21:56:12 +0200 Subject: [PATCH 73/86] Fix #344 Allow empty string for bump methods The methods .bump_prerelease() and .bump_build() allow now different types for the token argument: * A string. This was already allowed and default was "rc". If the string is empty, we get only the raised number (without the "rc" part). The number starts from 1. * None. Is the same as calling the method without any argument. --- changelog.d/344.bugfix.rst | 5 ++ ...ncrease-parts-of-a-version_prereleases.rst | 1 + docs/usage/raise-parts-of-a-version.rst | 40 +++++++++++++ src/semver/version.py | 57 +++++++++++++++---- tests/test_bump.py | 24 ++++++++ 5 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 changelog.d/344.bugfix.rst diff --git a/changelog.d/344.bugfix.rst b/changelog.d/344.bugfix.rst new file mode 100644 index 00000000..9daa9e73 --- /dev/null +++ b/changelog.d/344.bugfix.rst @@ -0,0 +1,5 @@ +Allow empty string, a string with a prefix, or ``None`` +as token in +:meth:`Version.bump_build ` and +:meth:`Version.bump_prerelease `. + diff --git a/docs/usage/increase-parts-of-a-version_prereleases.rst b/docs/usage/increase-parts-of-a-version_prereleases.rst index 98283937..87685b76 100644 --- a/docs/usage/increase-parts-of-a-version_prereleases.rst +++ b/docs/usage/increase-parts-of-a-version_prereleases.rst @@ -1,3 +1,4 @@ +.. _increase-parts-of-a-version: Increasing Parts of a Version Taking into Account Prereleases ============================================================= diff --git a/docs/usage/raise-parts-of-a-version.rst b/docs/usage/raise-parts-of-a-version.rst index cc62fffb..d369c575 100644 --- a/docs/usage/raise-parts-of-a-version.rst +++ b/docs/usage/raise-parts-of-a-version.rst @@ -1,6 +1,19 @@ Raising Parts of a Version ========================== +.. note:: + + Keep in mind, "raising" the pre-release only will make your + complete version *lower* than before. + + For example, having version ``1.0.0`` and raising the pre-release + will lead to ``1.0.0-rc.1``, but ``1.0.0-rc.1`` is smaller than ``1.0.0``. + + If you search for a way to take into account this behavior, look for the + method :meth:`Version.next_version ` + in section :ref:`increase-parts-of-a-version`. + + The ``semver`` module contains the following functions to raise parts of a version: @@ -14,6 +27,7 @@ a version: ``build`` to ``None``. * :func:`Version.bump_build `: raises the build part. + .. code-block:: python >>> str(Version.parse("3.4.5-pre.2+build.4").bump_major()) @@ -28,3 +42,29 @@ a version: '3.4.5-pre.2+build.5' Likewise the module level functions :func:`semver.bump_major`. + +For the methods :meth:`Version.bump_prerelease ` +and :meth:`Version.bump_build ` it's possible to pass an empty string or ``None``. +However, it gives different results:: + +.. code-block:: python + + >>> str(Version.parse("3.4.5").bump_prerelease('')) + '3.4.5-1' + >>> str(Version.parse("3.4.5").bump_prerelease(None)) + '3.4.5-rc.1' + +An empty string removes any prefix whereas ``None`` is the same as calling +the method without any argument. + +If you already have a prerelease, the argument for the method +is not taken into account: + +.. code-block:: python + + >>> str(Version.parse("3.4.5-rc.1").bump_prerelease(None)) + '3.4.5-rc.2' + >>> str(Version.parse("3.4.5-rc.1").bump_prerelease('')) + '3.4.5-rc.2' + + diff --git a/src/semver/version.py b/src/semver/version.py index bf949bb4..bfce8101 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -305,30 +305,44 @@ def bump_patch(self) -> "Version": cls = type(self) return cls(self._major, self._minor, self._patch + 1) - def bump_prerelease(self, token: str = "rc") -> "Version": + def bump_prerelease(self, token: Optional[str] = "rc") -> "Version": """ Raise the prerelease part of the version, return a new object but leave self untouched. - :param token: defaults to ``rc`` - :return: new object with the raised prerelease part + :param token: defaults to ``'rc'`` + :return: new :class:`Version` object with the raised prerelease part. + The original object is not modified. >>> ver = semver.parse("3.4.5") - >>> ver.bump_prerelease() - Version(major=3, minor=4, patch=5, prerelease='rc.2', \ -build=None) + >>> ver.bump_prerelease().prerelease + 'rc.2' + >>> ver.bump_prerelease('').prerelease + '1' + >>> ver.bump_prerelease(None).prerelease + 'rc.1' """ cls = type(self) - prerelease = cls._increment_string(self._prerelease or (token or "rc") + ".0") + if self._prerelease is not None: + prerelease = self._prerelease + elif token == "": + prerelease = "0" + elif token is None: + prerelease = "rc.0" + else: + prerelease = str(token) + ".0" + + prerelease = cls._increment_string(prerelease) return cls(self._major, self._minor, self._patch, prerelease) - def bump_build(self, token: str = "build") -> "Version": + def bump_build(self, token: Optional[str] = "build") -> "Version": """ Raise the build part of the version, return a new object but leave self untouched. - :param token: defaults to ``build`` - :return: new object with the raised build part + :param token: defaults to ``'build'`` + :return: new :class:`Version` object with the raised build part. + The original object is not modified. >>> ver = semver.parse("3.4.5-rc.1+build.9") >>> ver.bump_build() @@ -336,7 +350,28 @@ def bump_build(self, token: str = "build") -> "Version": build='build.10') """ cls = type(self) - build = cls._increment_string(self._build or (token or "build") + ".0") + if self._build is not None: + build = self._build + elif token == "": + build = "0" + elif token is None: + build = "build.0" + else: + build = str(token) + ".0" + + # self._build or (token or "build") + ".0" + build = cls._increment_string(build) + if self._build is not None: + build = self._build + elif token == "": + build = "0" + elif token is None: + build = "build.0" + else: + build = str(token) + ".0" + + # self._build or (token or "build") + ".0" + build = cls._increment_string(build) return cls(self._major, self._minor, self._patch, self._prerelease, build) def compare(self, other: Comparable) -> int: diff --git a/tests/test_bump.py b/tests/test_bump.py index c28e1905..34e0b2ac 100644 --- a/tests/test_bump.py +++ b/tests/test_bump.py @@ -66,6 +66,30 @@ def test_should_versioninfo_bump_multiple(): assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == expected +def test_should_versioninfo_bump_prerelease_with_empty_str(): + v = parse_version_info("3.4.5") + expected = parse_version_info("3.4.5-1") + assert v.bump_prerelease("") == expected + + +def test_should_versioninfo_bump_prerelease_with_none(): + v = parse_version_info("3.4.5") + expected = parse_version_info("3.4.5-rc.1") + assert v.bump_prerelease(None) == expected + + +def test_should_versioninfo_bump_build_with_empty_str(): + v = parse_version_info("3.4.5") + expected = parse_version_info("3.4.5+1") + assert v.bump_build("") == expected + + +def test_should_versioninfo_bump_build_with_none(): + v = parse_version_info("3.4.5") + expected = parse_version_info("3.4.5+build.1") + assert v.bump_build(None) == expected + + def test_should_ignore_extensions_for_bump(): assert bump_patch("3.4.5-rc1+build4") == "3.4.6" From 67464a70d0abb5c4a67c28369e49d112e364862b Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 6 Mar 2023 11:42:34 +0100 Subject: [PATCH 74/86] Fix #397: Remove asterisk in python_requires The asterisk in "python_requires = >= 3.7.*" make the pyproject-build command fail with this exception: setuptools.extern.packaging.specifiers.InvalidSpecifier: Invalid specifier: '>=3.7.*' This fix removes the asterisk which leads to a successful build. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2673b0f6..0ee8564c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,7 @@ license = BSD package_dir = =src packages = find: -python_requires = >=3.7.* +python_requires = >=3.7 include_package_data = True [options.entry_points] From 45e12ec787ae3879829e59193daf316fc6d87186 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 5 Mar 2023 11:51:33 +0100 Subject: [PATCH 75/86] Prepare for 3.0.0-rc.1 release * Combine all dev releases together in one changelog * Remove all changelog.d entries and integrate it into CHANGELOG * Move dev releases to file changelog-semver3-devel.rst * Split contributing into different sections * Some doc polishing; use :meth: consistently for semver.Version methods * Fix also some doc bugs * Amend list of contributors and sorted alphabetically by lastname * Correct docstrings on some deprecated functions --- CHANGELOG.rst | 352 +++++++---------- CONTRIBUTING.rst | 194 +-------- CONTRIBUTORS | 41 +- README.rst | 5 - changelog.d/284.deprecation.rst | 5 - changelog.d/284.doc.rst | 1 - changelog.d/284.feature.rst | 1 - changelog.d/344.bugfix.rst | 5 - changelog.d/388.trivial.rst | 3 - changelog.d/pr384.bugfix.rst | 11 - changelog.d/pr389.trivial.rst | 6 - changelog.d/pr392.doc.rst | 1 - changelog.d/pr393.bugfix.rst | 1 - changelog.d/pr396.bug.rst | 2 - docs/advanced/convert-pypi-to-semver.rst | 4 +- .../create-subclasses-from-version.rst | 5 +- docs/advanced/index.rst | 1 + docs/advanced/semverwithvprefix.py | 2 +- docs/advanced/version-from-file.rst | 9 +- docs/api.rst | 29 ++ BUILDING.rst => docs/build-semver.rst | 1 + docs/build.rst | 1 - docs/changelog-semver3-devel.rst | 367 ++++++++++++++++++ docs/contribute/add-changelog-entry.rst | 7 + docs/contribute/doc-semver.rst | 80 ++++ docs/contribute/finish-release.rst | 24 ++ docs/contribute/index.rst | 28 ++ docs/contribute/prerequisites.rst | 15 + docs/contribute/release-procedure.rst | 123 ++++++ docs/contribute/report-bugs.rst | 18 + docs/contribute/run-test-suite.rst | 64 +++ docs/development.rst | 1 - docs/index.rst | 7 +- docs/migration/migratetosemver3.rst | 13 +- .../replace-deprecated-functions.rst | 20 +- docs/usage/access-parts-through-index.rst | 2 +- .../usage/check-compatible-semver-version.rst | 2 +- docs/usage/check-valid-semver-version.rst | 2 +- .../compare-versions-through-expression.rst | 10 +- docs/usage/compare-versions.rst | 10 +- .../convert-version-into-different-types.rst | 8 +- docs/usage/create-a-version.rst | 4 +- .../get-min-and-max-of-multiple-versions.rst | 17 +- ...ncrease-parts-of-a-version_prereleases.rst | 5 +- docs/usage/parse-version-string.rst | 2 +- docs/usage/raise-parts-of-a-version.rst | 18 +- docs/usage/replace-parts-of-a-version.rst | 13 +- docs/usage/semver-version.rst | 2 +- src/semver/__about__.py | 2 +- src/semver/_deprecated.py | 33 +- src/semver/version.py | 30 +- tox.ini | 2 +- 52 files changed, 1039 insertions(+), 570 deletions(-) delete mode 100644 changelog.d/284.deprecation.rst delete mode 100644 changelog.d/284.doc.rst delete mode 100644 changelog.d/284.feature.rst delete mode 100644 changelog.d/344.bugfix.rst delete mode 100644 changelog.d/388.trivial.rst delete mode 100644 changelog.d/pr384.bugfix.rst delete mode 100644 changelog.d/pr389.trivial.rst delete mode 100644 changelog.d/pr392.doc.rst delete mode 100644 changelog.d/pr393.bugfix.rst delete mode 100644 changelog.d/pr396.bug.rst rename BUILDING.rst => docs/build-semver.rst (99%) delete mode 100644 docs/build.rst create mode 100644 docs/changelog-semver3-devel.rst create mode 100644 docs/contribute/add-changelog-entry.rst create mode 100644 docs/contribute/doc-semver.rst create mode 100644 docs/contribute/finish-release.rst create mode 100644 docs/contribute/index.rst create mode 100644 docs/contribute/prerequisites.rst create mode 100644 docs/contribute/release-procedure.rst create mode 100644 docs/contribute/report-bugs.rst create mode 100644 docs/contribute/run-test-suite.rst delete mode 100644 docs/development.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7f515aa1..28a401e2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Changes for the upcoming release can be found in the `"changelog.d" directory `_ in our repository. +This section covers the changes between major version 2 and version 3. + .. Do *NOT* add changelog entries here! @@ -16,65 +18,108 @@ in our repository. .. towncrier release notes start -Version 3.0.0-dev.4 -=================== +Version 3.0.0 +============= -:Released: 2022-12-18 -:Maintainer: +:Released: 2023-03-19 +:Maintainer: Tom Schraitle Bug Fixes --------- +* :gh:`291`: Disallow negative numbers in VersionInfo arguments + for ``major``, ``minor``, and ``patch``. + +* :gh:`310`: Rework API documentation. + Follow a more "semi-manual" attempt and add auto directives + into :file:`docs/api.rst`. + +* :gh:`344`: Allow empty string, a string with a prefix, or ``None`` + as token in + :meth:`~semver.version.Version.bump_build` and + :meth:`~semver.version.Version.bump_prerelease`. + * :gh:`374`: Correct Towncrier's config entries in the :file:`pyproject.toml` file. The old entries ``[[tool.towncrier.type]]`` are deprecated and need to be replaced by ``[tool.towncrier.fragment.]``. +* :pr:`384`: General cleanup, reformat files: + * Reformat source code with black again as some config options + did accidentely exclude the semver source code. + Mostly remove some includes/excludes in the black config. + * Integrate concurrency in GH Action + * Ignore Python files on project dirs in .gitignore + * Remove unused patterns in MANIFEST.in + * Use ``extend-exclude`` for flake in :file:`setup.cfg`` and adapt list. + * Use ``skip_install=True`` in :file:`tox.ini` for black -Deprecations ------------- +* :pr:`393`: Fix command :command:`python -m semver` to avoid the error "invalid choice" -* :gh:`372`: Deprecate support for Python 3.6. +* :pr:`396`: Calling :meth:`~semver.version.Version.parse` on a derived class will show correct type of derived class. - Python 3.6 reached its end of life and isn't supported anymore. - At the time of writing (Dec 2022), the lowest version is 3.7. - Although the `poll `_ - didn't cast many votes, the majority agree to remove support for - Python 3.6. +Deprecations +------------ +* :gh:`169`: Deprecate CLI functions not imported from ``semver.cli``. +* :gh:`234`: In :file:`setup.py` simplified file and remove + ``Tox`` and ``Clean`` classes -Improved Documentation ----------------------- +* :gh:`284`: Deprecate the use of :meth:`~Version.isvalid`. -* :gh:`335`: Add new section "Converting versions between PyPI and semver" the limitations - and possible use cases to convert from one into the other versioning scheme. + Rename :meth:`~semver.version.Version.isvalid` + to :meth:`~semver.version.Version.is_valid` + for consistency reasons with :meth:`~semver.version.Version.is_compatible`. -* :gh:`340`: Describe how to get version from a file -* :gh:`343`: Describe combining Pydantic with semver in the "Advanced topic" - section. +* :pr:`290`: For semver 3.0.0-alpha0 deprecated: -* :gh:`350`: Restructure usage section. Create subdirectory "usage/" and splitted - all section into different files. + * Remove anything related to Python2 + * In :file:`tox.ini` and :file:`.travis.yml` + Remove targets py27, py34, py35, and pypy. + Add py38, py39, and nightly (allow to fail) + * In :file:`setup.py` simplified file and remove + ``Tox`` and ``Clean`` classes + * Remove old Python versions (2.7, 3.4, 3.5, and pypy) + from Travis -* :gh:`351`: Introduce new topics for: +* :gh:`372`: Deprecate support for Python 3.6. - * "Migration to semver3" - * "Advanced topics" + Python 3.6 reached its end of life and isn't supported anymore. + At the time of writing (Dec 2022), the lowest version is 3.7. + Although the `poll `_ + didn't cast many votes, the majority agreed to remove support for + Python 3.6. Features -------- -* :pr:`359`: Add optional parameter ``optional_minor_and_patch`` in :meth:`.Version.parse` to allow optional +* :gh:`169`: Create semver package and split code among different modules in the packages: + + * Remove :file:`semver.py` + * Create :file:`src/semver/__init__.py` + * Create :file:`src/semver/cli.py` for all CLI methods + * Create :file:`src/semver/_deprecated.py` for the ``deprecated`` decorator and other deprecated functions + * Create :file:`src/semver/__main__.py` to allow calling the CLI using :command:`python -m semver` + * Create :file:`src/semver/_types.py` to hold type aliases + * Create :file:`src/semver/version.py` to hold the :class:`Version` class (old name :class:`VersionInfo`) and its utility functions + * Create :file:`src/semver/__about__.py` for all the metadata variables + +* :gh:`213`: Add typing information + +* :gh:`284`: Implement :meth:`~semver.version.Version.is_compatible` to make "is self compatible with X". + +* :gh:`305`: Rename :class:`~semver.version.VersionInfo` to :class:`~semver.version.Version` but keep an alias for compatibility + +* :pr:`359`: Add optional parameter ``optional_minor_and_patch`` in :meth:`~semver.version.Version.parse` to allow optional minor and patch parts. -* :pr:`362`: Make :meth:`.Version.match` accept a bare version string as match expression, defaulting to - equality testing. +* :pr:`362`: Make :meth:`~semver.version.Version.match` accept a bare version string as match expression, defaulting to equality testing. * :gh:`364`: Enhance :file:`pyproject.toml` to make it possible to use the :command:`pyproject-build` command from the build module. @@ -95,34 +140,28 @@ Features -Trivial/Internal Changes ------------------------- - -* :gh:`378`: Fix some typos in Towncrier configuration - - - ----- - - -Version 3.0.0-dev.3 -=================== +Improved Documentation +---------------------- -:Released: 2022-01-19 -:Maintainer: Tom Schraitle +* :gh:`276`: Document how to create a sublass from :class:`~semver.version.VersionInfo` class +* :gh:`284`: Document deprecation of :meth:`~semver.version.Version.isvalid`. -Bug Fixes ---------- +* :pr:`290`: Several improvements in the documentation: -* :gh:`310`: Rework API documentation. - Follow a more "semi-manual" attempt and add auto directives - into :file:`docs/api.rst`. + * New layout to distinguish from the semver2 development line. + * Create new logo. + * Remove any occurances of Python2. + * Describe changelog process with Towncrier. + * Update the release process. +* :gh:`304`: Several improvements in documentation: + * Reorganize API documentation. + * Add migration chapter from semver2 to semver3. + * Distinguish between changlog for version 2 and 3 -Improved Documentation ----------------------- +* :gh:`305`: Add note about :class:`~semver.version.Version` rename. * :gh:`312`: Rework "Usage" section. @@ -130,109 +169,31 @@ Improved Documentation :class:`~semver.version.Version` class * Remove semver. prefix in doctests to make examples shorter * Correct some references to dunder methods like - :func:`~.semver.version.Version.__getitem__`, - :func:`~.semver.version.Version.__gt__` etc. + :func:`~semver.version.Version.__getitem__`, + :func:`~semver.version.Version.__gt__` etc. * Remove inconsistencies and mention module level function as deprecated and discouraged from using * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example * :gh:`315`: Improve release procedure text +* :gh:`335`: Add new section "Converting versions between PyPI and semver" the limitations + and possible use cases to convert from one into the other versioning scheme. +* :gh:`340`: Describe how to get version from a file -Trivial/Internal Changes ------------------------- - -* :gh:`309`: Some (private) functions from the :mod:`semver.version` - module has been changed. - - The following functions got renamed: - - * function ``semver.version.comparator`` got renamed to - :func:`semver.version._comparator` as it is only useful - inside the :class:`~semver.version.Version` class. - * function ``semver.version.cmp`` got renamed to - :func:`semver.version._cmp` as it is only useful - inside the :class:`~semver.version.Version` class. - - The following functions got integrated into the - :class:`~semver.version.Version` class: - - * function ``semver.version._nat_cmd`` as a classmethod - * function ``semver.version.ensure_str`` - -* :gh:`313`: Correct :file:`tox.ini` for ``changelog`` entry to skip - installation for semver. This should speed up the execution - of towncrier. - -* :gh:`316`: Comparisons of :class:`~semver.version.Version` class and other - types return now a :py:const:`NotImplemented` constant instead - of a :py:exc:`TypeError` exception. - - The `NotImplemented`_ section of the Python documentation recommends - returning this constant when comparing with ``__gt__``, ``__lt__``, - and other comparison operators to "to indicate that the operation is - not implemented with respect to the other type". - - .. _NotImplemented: https://docs.python.org/3/library/constants.html#NotImplemented - -* :gh:`319`: Introduce stages in :file:`.travis.yml` - The config file contains now two stages: check and test. If - check fails, the test stage won't be executed. This could - speed up things when some checks fails. - -* :gh:`322`: Switch from Travis CI to GitHub Actions. - -* :gh:`347`: Support Python 3.10 in GitHub Action and other config files. - - - ----- - - -Version 3.0.0-dev.2 -=================== - -:Released: 2020-11-01 -:Maintainer: Tom Schraitle - - -Deprecations ------------- - -* :gh:`169`: Deprecate CLI functions not imported from ``semver.cli``. - - - -Features --------- - -* :gh:`169`: Create semver package and split code among different modules in the packages. - - * Remove :file:`semver.py` - * Create :file:`src/semver/__init__.py` - * Create :file:`src/semver/cli.py` for all CLI methods - * Create :file:`src/semver/_deprecated.py` for the ``deprecated`` decorator and other deprecated functions - * Create :file:`src/semver/__main__.py` to allow calling the CLI using :command:`python -m semver` - * Create :file:`src/semver/_types.py` to hold type aliases - * Create :file:`src/semver/version.py` to hold the :class:`Version` class (old name :class:`VersionInfo`) and its utility functions - * Create :file:`src/semver/__about__.py` for all the metadata variables - -* :gh:`305`: Rename :class:`VersionInfo` to :class:`Version` but keep an alias for compatibility - - - -Improved Documentation ----------------------- +* :gh:`343`: Describe combining Pydantic with semver in the "Advanced topic" + section. -* :gh:`304`: Several improvements in documentation: +* :gh:`350`: Restructure usage section. Create subdirectory "usage/" and splitted + all section into different files. - * Reorganize API documentation. - * Add migration chapter from semver2 to semver3. - * Distinguish between changlog for version 2 and 3 +* :gh:`351`: Introduce new topics for: -* :gh:`305`: Add note about :class:`Version` rename. + * "Migration to semver3" + * "Advanced topics" +* :pr:`392`: Fix the example in the documentation for combining semver and pydantic. Trivial/Internal Changes @@ -247,6 +208,8 @@ Trivial/Internal Changes Increase coverage to 100% for all non-deprecated APIs +* :pr:`290`: Add supported Python versions to :command:`black`. + * :gh:`304`: Support PEP-561 :file:`py.typed`. According to the mentioned PEP: @@ -258,96 +221,61 @@ Trivial/Internal Changes Add package_data to :file:`setup.cfg` to include this marker in dist and whl file. +* :gh:`309`: Some (private) functions from the :mod:`semver.version` + module has been changed. + The following functions got renamed: ----- - - -Version 3.0.0-dev.1 -=================== - -:Released: 2020-10-26 -:Maintainer: Tom Schraitle - - -Deprecations ------------- - -* :pr:`290`: For semver 3.0.0-alpha0: - - * Remove anything related to Python2 - * In :file:`tox.ini` and :file:`.travis.yml` - Remove targets py27, py34, py35, and pypy. - Add py38, py39, and nightly (allow to fail) - * In :file:`setup.py` simplified file and remove - ``Tox`` and ``Clean`` classes - * Remove old Python versions (2.7, 3.4, 3.5, and pypy) - from Travis - -* :gh:`234`: In :file:`setup.py` simplified file and remove - ``Tox`` and ``Clean`` classes - - + * function :func:`semver.version.comparator` got renamed to + :func:`semver.version._comparator` as it is only useful + inside the :class:`~semver.version.Version` class. + * function :func:`semver.version.cmp` got renamed to + :func:`semver.version._cmp` as it is only useful + inside the :class:`~semver.version.Version` class. -Features --------- + The following functions got integrated into the + :class:`~semver.version.Version` class: -* :pr:`290`: Create semver 3.0.0-alpha0 - - * Update :file:`README.rst`, mention maintenance - branch ``maint/v2``. - * Remove old code mainly used for Python2 compatibility, - adjusted code to support Python3 features. - * Split test suite into separate files under :file:`tests/` - directory - * Adjust and update :file:`setup.py`. Requires Python >=3.6.* - Extract metadata directly from source (affects all the ``__version__``, - ``__author__`` etc. variables) - -* :gh:`270`: Configure Towncrier (:pr:`273`:) - - * Add :file:`changelog.d/.gitignore` to keep this directory - * Create :file:`changelog.d/README.rst` with some descriptions - * Add :file:`changelog.d/_template.rst` as Towncrier template - * Add ``[tool.towncrier]`` section in :file:`pyproject.toml` - * Add "changelog" target into :file:`tox.ini`. Use it like - :command:`tox -e changelog -- CMD` whereas ``CMD`` is a - Towncrier command. The default :command:`tox -e changelog` - calls Towncrier to create a draft of the changelog file - and output it to stdout. - * Update documentation and add include a new section - "Changelog" included from :file:`changelog.d/README.rst`. - -* :gh:`276`: Document how to create a sublass from :class:`VersionInfo` class + * function :func:`semver.version._nat_cmd` as a classmethod + * function :func:`semver.version.ensure_str` -* :gh:`213`: Add typing information +* :gh:`313`: Correct :file:`tox.ini` for ``changelog`` entry to skip + installation for semver. This should speed up the execution + of towncrier. +* :gh:`316`: Comparisons of :class:`~semver.version.Version` class and other + types return now a :py:const:`NotImplemented` constant instead + of a :py:exc:`TypeError` exception. -Bug Fixes ---------- + The `NotImplemented`_ section of the Python documentation recommends + returning this constant when comparing with ``__gt__``, ``__lt__``, + and other comparison operators to "to indicate that the operation is + not implemented with respect to the other type". -* :gh:`291`: Disallow negative numbers in VersionInfo arguments - for ``major``, ``minor``, and ``patch``. + .. _NotImplemented: https://docs.python.org/3/library/constants.html#NotImplemented +* :gh:`319`: Introduce stages in :file:`.travis.yml` + The config file contains now two stages: check and test. If + check fails, the test stage won't be executed. This could + speed up things when some checks fails. +* :gh:`322`: Switch from Travis CI to GitHub Actions. -Improved Documentation ----------------------- +* :gh:`347`: Support Python 3.10 in GitHub Action and other config files. -* :pr:`290`: Several improvements in the documentation: +* :gh:`378`: Fix some typos in Towncrier configuration - * New layout to distinguish from the semver2 development line. - * Create new logo. - * Remove any occurances of Python2. - * Describe changelog process with Towncrier. - * Update the release process. +* :gh:`388`: For pytest, switch to the more modern :mod:`importlib` approach + as it doesn't require to modify :data:`sys.path`: + https://docs.pytest.org/en/7.2.x/explanation/pythonpath.html +* :pr:`389`: Add public class variable :data:`Version.NAMES `. + This class variable contains a tuple of strings that contains the names of + all attributes of a Version (like ``"major"``, ``"minor"`` etc). -Trivial/Internal Changes ------------------------- + In cases we need to have dynamical values, this makes it easier to iterate. -* :pr:`290`: Add supported Python versions to :command:`black`. .. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index fe531990..ce598e01 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -8,6 +8,7 @@ The semver source code is managed using Git and is hosted on GitHub:: git clone git://github.com/python-semver/python-semver + Reporting Bugs and Asking Questions ----------------------------------- @@ -34,193 +35,14 @@ consider the following general requirements: If not, ask on its GitHub project https://github.com/semver/semver. +More topics +----------- -Modifying the Code ------------------- - -We recommend the following workflow: - -#. Fork our project on GitHub using this link: - https://github.com/python-semver/python-semver/fork - -#. Clone your forked Git repository (replace ``GITHUB_USER`` with your - account name on GitHub):: - - $ git clone git@github.com:GITHUB_USER/python-semver.git - -#. Create a new branch. You can name your branch whatever you like, but we - recommend to use some meaningful name. If your fix is based on a - existing GitHub issue, add also the number. Good examples would be: - - * ``feature/123-improve-foo`` when implementing a new feature in issue 123 - * ``bugfix/234-fix-security-bar`` a bugfixes for issue 234 - - Use this :command:`git` command:: - - $ git checkout -b feature/NAME_OF_YOUR_FEATURE - -#. Work on your branch and create a pull request: - - a. Write test cases and run the complete test suite, see :ref:`testsuite` - for details. - - b. Write a changelog entry, see section :ref:`add-changelog`. - - c. If you have implemented a new feature, document it into our - documentation to help our reader. See section :ref:`doc` for - further details. - - d. Create a `pull request`_. Describe in the pull request what you did - and why. If you have open questions, ask. - -#. Wait for feedback. If you receive any comments, address these. - -#. After your pull request got accepted, delete your branch. - - -.. _testsuite: - -Running the Test Suite ----------------------- - -We use `pytest`_ and `tox`_ to run tests against all supported Python -versions. All test dependencies are resolved automatically. - -You can decide to run the complete test suite or only part of it: - -* To run all tests, use:: - - $ tox - - If you have not all Python interpreters installed on your system - it will probably give you some errors (``InterpreterNotFound``). - To avoid such errors, use:: - - $ tox --skip-missing-interpreters - - It is possible to use one or more specific Python versions. Use the ``-e`` - option and one or more abbreviations (``py37`` for Python 3.7, - ``py38`` for Python 3.8 etc.):: - - $ tox -e py37 - $ tox -e py37,py38 - - To get a complete list and a short description, run:: - - $ tox -av - -* To run only a specific test, pytest requires the syntax - ``TEST_FILE::TEST_FUNCTION``. - - For example, the following line tests only the function - :func:`test_immutable_major` in the file :file:`test_bump.py` for all - Python versions:: - - $ tox -e py37 -- tests/test_bump.py::test_should_bump_major - - By default, pytest prints only a dot for each test function. To - reveal the executed test function, use the following syntax:: - - $ tox -- -v - - You can combine the specific test function with the ``-e`` option, for - example, to limit the tests for Python 3.7 and 3.8 only:: - - $ tox -e py37,py38 -- tests/test_bump.py::test_should_bump_major - -Our code is checked against formatting, style, type, and docstring issues -(`black`_, `flake8`_, `mypy`_, and `docformatter`_). -It is recommended to run your tests in combination with :command:`checks`, -for example:: - - $ tox -e checks,py37,py38 - - -.. _doc: - -Documenting semver ------------------- - -Documenting the features of semver is very important. It gives our developers -an overview what is possible with semver, how it "feels", and how it is -used efficiently. - -.. note:: - - To build the documentation locally use the following command:: - - $ tox -e docs - - The built documentation is available in :file:`docs/_build/html`. - - -A new feature is *not* complete if it isn't proberly documented. A good -documentation includes: - - * **A docstring** - - Each docstring contains a summary line, a linebreak, an optional - directive (see next item), the description of its arguments in - `Sphinx style`_, and an optional doctest. - The docstring is extracted and reused in the :ref:`api` section. - An appropriate docstring should look like this:: - - def to_tuple(self) -> VersionTuple: - """ - Convert the Version object to a tuple. - - .. versionadded:: 2.10.0 - Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to - make this function available in the public API. - - :return: a tuple with all the parts - - >>> semver.Version(5, 3, 1).to_tuple() - (5, 3, 1, None, None) - """ - - * **An optional directive** - - If you introduce a new feature, change a function/method, or remove something, - it is a good practice to introduce Sphinx directives into the docstring. - This gives the reader an idea what version is affected by this change. - - The first required argument, ``VERSION``, defines the version when this change - was introduced. You can choose from: - - * ``.. versionadded:: VERSION`` - - Use this directive to describe a new feature. - - * ``.. versionchanged:: VERSION`` - - Use this directive to describe when something has changed, for example, - new parameters were added, changed side effects, different return values, etc. - - * ``.. deprecated:: VERSION`` - - Use this directive when a feature is deprecated. Describe what should - be used instead, if appropriate. - - - Add such a directive *after* the summary line, as shown above. - - * **The documentation** - - A docstring is good, but in most cases it's too dense. API documentation - cannot replace a good user documentation. Describe how - to use your new feature in our documentation. Here you can give your - readers more examples, describe it in a broader context or show - edge cases. - - -.. _add-changelog: - -Adding a Changelog Entry ------------------------- - -.. include:: ../changelog.d/README.rst - :start-after: -text-begin- +* `Running the Test Suite `_ +* `Documenting semver `_ +* `Adding a Changelog Entry `_ +* `Preparing the Release `_ +* `Finish the Release `_ .. _black: https://black.rtfd.io diff --git a/CONTRIBUTORS b/CONTRIBUTORS index e5fef99b..0d90ab3a 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -22,30 +22,37 @@ Old maintainer: * Kostiantyn Rybnikov -Significant contributors -======================== +List of Contributors +==================== -* Alexander Puzynia -* Alexander Shorin -* Anton Talevnin -* Ben Finney +(in alphabetical order) + +* Jelo Agnasin * Carles Barrobés -* Craig Blaszczyk -* Damien Nadé * Eli Bishop -* George Sakkis -* Jan Pieter Waagmeester -* Jelo Agnasin -* Karol Werner * Peter Bittner -* robi-wan -* sbrudenell +* Craig Blaszczyk +* Tyler Cross +* Dennis Felsing +* Ben Finney +* Zane Geiger * T. Jameson Little +* Raphael Krupinski * Thomas Laferriere -* Tuure Laurinolli -* Tyler Cross * Zack Lalanne - +* Tuure Laurinolli +* Damien Nadé +* Jan Pieter Waagmeester +* Alexander Puzynia +* Lexi Robinson +* robi-wan +* George Sakkis +* Mike Salvatore +* sbrudenell +* Alexander Shorin +* Anton Talevnin +* Karol Werner + .. Local variables: coding: utf-8 diff --git a/README.rst b/README.rst index cad99a04..ede10a18 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,3 @@ -.. warning:: - - This is a development version. Do **NOT** use it - in production before the final 3.0.0 is released. - Quickstart ========== diff --git a/changelog.d/284.deprecation.rst b/changelog.d/284.deprecation.rst deleted file mode 100644 index 738a14fc..00000000 --- a/changelog.d/284.deprecation.rst +++ /dev/null @@ -1,5 +0,0 @@ -Deprecate the use of :meth:`Version.isvalid`. - -Rename :meth:`Version.isvalid ` -to :meth:`Version.is_valid ` -for consistency reasons with :meth:`Version.is_compatible ` \ No newline at end of file diff --git a/changelog.d/284.doc.rst b/changelog.d/284.doc.rst deleted file mode 100644 index 6fa8e53a..00000000 --- a/changelog.d/284.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Document deprecation of :meth:`Version.isvalid`. \ No newline at end of file diff --git a/changelog.d/284.feature.rst b/changelog.d/284.feature.rst deleted file mode 100644 index f13d7300..00000000 --- a/changelog.d/284.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Implement :meth:`Version.is_compatible ` to make "is self compatible with X". diff --git a/changelog.d/344.bugfix.rst b/changelog.d/344.bugfix.rst deleted file mode 100644 index 9daa9e73..00000000 --- a/changelog.d/344.bugfix.rst +++ /dev/null @@ -1,5 +0,0 @@ -Allow empty string, a string with a prefix, or ``None`` -as token in -:meth:`Version.bump_build ` and -:meth:`Version.bump_prerelease `. - diff --git a/changelog.d/388.trivial.rst b/changelog.d/388.trivial.rst deleted file mode 100644 index 236bce6f..00000000 --- a/changelog.d/388.trivial.rst +++ /dev/null @@ -1,3 +0,0 @@ -For pytest, switch to the more modern :mod:`importlib` approach -as it doesn't require to modify :data:`sys.path`: -https://docs.pytest.org/en/7.2.x/explanation/pythonpath.html \ No newline at end of file diff --git a/changelog.d/pr384.bugfix.rst b/changelog.d/pr384.bugfix.rst deleted file mode 100644 index ca0b08d0..00000000 --- a/changelog.d/pr384.bugfix.rst +++ /dev/null @@ -1,11 +0,0 @@ -General cleanup, reformat files: - -* Reformat source code with black again as some config options - did accidentely exclude the semver source code. - Mostly remove some includes/excludes in the black config. -* Integrate concurrency in GH Action -* Ignore Python files on project dirs in .gitignore -* Remove unused patterns in MANIFEST.in -* Use ``extend-exclude`` for flake in :file:`setup.cfg`` and adapt list. -* Use ``skip_install=True`` in :file:`tox.ini` for black - diff --git a/changelog.d/pr389.trivial.rst b/changelog.d/pr389.trivial.rst deleted file mode 100644 index 12829855..00000000 --- a/changelog.d/pr389.trivial.rst +++ /dev/null @@ -1,6 +0,0 @@ -Add public class variable :data:`Version.NAMES `. - -This class variable contains a tuple of strings that contains the names of -all attributes of a Version (like ``"major"``, ``"minor"`` etc). - -In cases we need to have dynamical values, this makes it easier to iterate. diff --git a/changelog.d/pr392.doc.rst b/changelog.d/pr392.doc.rst deleted file mode 100644 index e542cef9..00000000 --- a/changelog.d/pr392.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Fix the example in the documentation for combining semver and pydantic. diff --git a/changelog.d/pr393.bugfix.rst b/changelog.d/pr393.bugfix.rst deleted file mode 100644 index 74a81f65..00000000 --- a/changelog.d/pr393.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix command :command:`python -m semver` to avoid the error "invalid choice" diff --git a/changelog.d/pr396.bug.rst b/changelog.d/pr396.bug.rst deleted file mode 100644 index 2b9032e2..00000000 --- a/changelog.d/pr396.bug.rst +++ /dev/null @@ -1,2 +0,0 @@ -Calling :func:`semver.Version.parse` on a derived class will show correct type -of derived class. diff --git a/docs/advanced/convert-pypi-to-semver.rst b/docs/advanced/convert-pypi-to-semver.rst index 76653ceb..04737d29 100644 --- a/docs/advanced/convert-pypi-to-semver.rst +++ b/docs/advanced/convert-pypi-to-semver.rst @@ -135,7 +135,7 @@ semver: def convert2semver(ver: packaging.version.Version) -> semver.Version: """Converts a PyPI version into a semver version - :param packaging.version.Version ver: the PyPI version + :param ver: the PyPI version :return: a semver version :raises ValueError: if epoch or post parts are used """ @@ -145,7 +145,7 @@ semver: raise ValueError("Can't convert a post part to semver") pre = None if not ver.pre else "".join([str(i) for i in ver.pre]) - semver.Version(*ver.release, prerelease=pre, build=ver.dev) + return semver.Version(*ver.release, prerelease=pre, build=ver.dev) .. _convert_semver_to_pypi: diff --git a/docs/advanced/create-subclasses-from-version.rst b/docs/advanced/create-subclasses-from-version.rst index 7c97ee6f..7e99e217 100644 --- a/docs/advanced/create-subclasses-from-version.rst +++ b/docs/advanced/create-subclasses-from-version.rst @@ -16,7 +16,8 @@ but the other behavior is the same, use the following code: The derived class :class:`SemVerWithVPrefix` can be used like -the original class: +the original class. Additionally, you can pass "incomplete" +version strings like ``v2.3``: .. code-block:: python @@ -24,7 +25,7 @@ the original class: >>> assert str(v1) == "v1.2.3" >>> print(v1) v1.2.3 - >>> v2 = SemVerWithVPrefix.parse("v2.3.4") + >>> v2 = SemVerWithVPrefix.parse("v2.3") >>> v2 > v1 True >>> bad = SemVerWithVPrefix.parse("1.2.4") diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index 8a45d361..47c23b9d 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -3,6 +3,7 @@ Advanced topics .. toctree:: + :maxdepth: 1 deal-with-invalid-versions create-subclasses-from-version diff --git a/docs/advanced/semverwithvprefix.py b/docs/advanced/semverwithvprefix.py index f2a7fecd..7e411d35 100644 --- a/docs/advanced/semverwithvprefix.py +++ b/docs/advanced/semverwithvprefix.py @@ -20,7 +20,7 @@ def parse(cls, version: str) -> "SemVerWithVPrefix": f"{version!r}: not a valid semantic version tag. " "Must start with 'v' or 'V'" ) - return super().parse(version[1:]) + return super().parse(version[1:], optional_minor_and_patch=True) def __str__(self) -> str: # Reconstruct the tag diff --git a/docs/advanced/version-from-file.rst b/docs/advanced/version-from-file.rst index 6dc9bb48..b49ff36b 100644 --- a/docs/advanced/version-from-file.rst +++ b/docs/advanced/version-from-file.rst @@ -8,16 +8,17 @@ is to use the following function: .. code-block:: python + import os + from typing import Union from semver.version import Version - def get_version(path: str = "version") -> Version: + def get_version(path: Union[str, os.PathLike]) -> semver.Version: """ - Construct a Version from a file + Construct a Version object from a file :param path: A text file only containing the semantic version :return: A :class:`Version` object containing the semantic version from the file. """ - with open(path,"r") as fh: - version = fh.read().strip() + version = open(path,"r").read().strip() return Version.parse(version) diff --git a/docs/api.rst b/docs/api.rst index 196e30a9..279df408 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -17,8 +17,37 @@ Deprecated Functions in :mod:`semver._deprecated` .. automodule:: semver._deprecated +.. autofunction:: semver._deprecated.bump_build + +.. autofunction:: semver._deprecated.bump_major + +.. autofunction:: semver._deprecated.bump_minor + +.. autofunction:: semver._deprecated.bump_patch + +.. autofunction:: semver._deprecated.bump_prerelease + +.. autofunction:: semver._deprecated.compare + .. autofunction:: semver._deprecated.deprecated +.. autofunction:: semver._deprecated.finalize_version + +.. autofunction:: semver._deprecated.format_version + +.. autofunction:: semver._deprecated.match + +.. autofunction:: semver._deprecated.max_ver + +.. autofunction:: semver._deprecated.min_ver + +.. autofunction:: semver._deprecated.parse + +.. autofunction:: semver._deprecated.parse_version_info + +.. autofunction:: semver._deprecated.replace + + CLI Parsing :mod:`semver.cli` ----------------------------- diff --git a/BUILDING.rst b/docs/build-semver.rst similarity index 99% rename from BUILDING.rst rename to docs/build-semver.rst index 61f3d4cb..c938a1ee 100644 --- a/BUILDING.rst +++ b/docs/build-semver.rst @@ -3,6 +3,7 @@ Building semver =============== + .. _PEP 517: https://www.python.org/dev/peps/pep-0517/ .. _PEP 621: https://www.python.org/dev/peps/pep-0621/ .. _A Practical Guide to Setuptools and Pyproject.toml: https://godatadriven.com/blog/a-practical-guide-to-setuptools-and-pyproject-toml/ diff --git a/docs/build.rst b/docs/build.rst deleted file mode 100644 index ba0c84a4..00000000 --- a/docs/build.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../BUILDING.rst diff --git a/docs/changelog-semver3-devel.rst b/docs/changelog-semver3-devel.rst new file mode 100644 index 00000000..2d40635d --- /dev/null +++ b/docs/changelog-semver3-devel.rst @@ -0,0 +1,367 @@ + +############################# +Changelog semver3 development +############################# + +This site contains all the changes during the development phase. + +.. _semver-3.0.0-dev.4: + +Version 3.0.0-dev.4 +=================== + +:Released: 2022-12-18 +:Maintainer: + + +.. _semver-3.0.0-dev.4-bugfixes: + +Bug Fixes +--------- + +* :gh:`374`: Correct Towncrier's config entries in the :file:`pyproject.toml` file. + The old entries ``[[tool.towncrier.type]]`` are deprecated and need + to be replaced by ``[tool.towncrier.fragment.]``. + + + +.. _semver-3.0.0-dev.4-deprecations: + +Deprecations +------------ + +* :gh:`372`: Deprecate support for Python 3.6. + + Python 3.6 reached its end of life and isn't supported anymore. + At the time of writing (Dec 2022), the lowest version is 3.7. + + Although the `poll `_ + didn't cast many votes, the majority agree to remove support for + Python 3.6. + + +.. _semver-3.0.0-dev.4-doc: + +Improved Documentation +---------------------- + +* :gh:`335`: Add new section "Converting versions between PyPI and semver" the limitations + and possible use cases to convert from one into the other versioning scheme. + +* :gh:`340`: Describe how to get version from a file + +* :gh:`343`: Describe combining Pydantic with semver in the "Advanced topic" + section. + +* :gh:`350`: Restructure usage section. Create subdirectory "usage/" and splitted + all section into different files. + +* :gh:`351`: Introduce new topics for: + + * "Migration to semver3" + * "Advanced topics" + + +.. _semver-3.0.0-dev.4-features: + +Features +-------- + +* :pr:`359`: Add optional parameter ``optional_minor_and_patch`` in :meth:`.Version.parse` to allow optional + minor and patch parts. + +* :pr:`362`: Make :meth:`.Version.match` accept a bare version string as match expression, defaulting to + equality testing. + +* :gh:`364`: Enhance :file:`pyproject.toml` to make it possible to use the + :command:`pyproject-build` command from the build module. + For more information, see :ref:`build-semver`. + +* :gh:`365`: Improve :file:`pyproject.toml`. + + * Use setuptools, add metadata. Taken approach from + `A Practical Guide to Setuptools and Pyproject.toml + `_. + * Doc: Describe building of semver + * Remove :file:`.travis.yml` in :file:`MANIFEST.in` + (not needed anymore) + * Distinguish between Python 3.6 and others in :file:`tox.ini` + * Add skip_missing_interpreters option for :file:`tox.ini` + * GH Action: Upgrade setuptools and setuptools-scm and test + against 3.11.0-rc.2 + + +.. _semver-3.0.0-dev.4-internal: + +Trivial/Internal Changes +------------------------ + +* :gh:`378`: Fix some typos in Towncrier configuration + + + +---- + +.. _semver-3.0.0-dev.3: + +Version 3.0.0-dev.3 +=================== + +:Released: 2022-01-19 +:Maintainer: Tom Schraitle + + +.. _semver-3.0.0-dev.3-bugfixes: + +Bug Fixes +--------- + +* :gh:`310`: Rework API documentation. + Follow a more "semi-manual" attempt and add auto directives + into :file:`docs/api.rst`. + + +.. _semver-3.0.0-dev.3-docs: + +Improved Documentation +---------------------- + +* :gh:`312`: Rework "Usage" section. + + * Mention the rename of :class:`~semver.version.VersionInfo` to + :class:`~semver.version.Version` class + * Remove semver. prefix in doctests to make examples shorter + * Correct some references to dunder methods like + :func:`~.semver.version.Version.__getitem__`, + :func:`~.semver.version.Version.__gt__` etc. + * Remove inconsistencies and mention module level function as + deprecated and discouraged from using + * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example + +* :gh:`315`: Improve release procedure text + + +.. _semver-3.0.0-dev.3-trivial: + +Trivial/Internal Changes +------------------------ + +* :gh:`309`: Some (private) functions from the :mod:`semver.version` + module has been changed. + + The following functions got renamed: + + * function ``semver.version.comparator`` got renamed to + :func:`semver.version._comparator` as it is only useful + inside the :class:`~semver.version.Version` class. + * function ``semver.version.cmp`` got renamed to + :func:`semver.version._cmp` as it is only useful + inside the :class:`~semver.version.Version` class. + + The following functions got integrated into the + :class:`~semver.version.Version` class: + + * function ``semver.version._nat_cmd`` as a classmethod + * function ``semver.version.ensure_str`` + +* :gh:`313`: Correct :file:`tox.ini` for ``changelog`` entry to skip + installation for semver. This should speed up the execution + of towncrier. + +* :gh:`316`: Comparisons of :class:`~semver.version.Version` class and other + types return now a :py:const:`NotImplemented` constant instead + of a :py:exc:`TypeError` exception. + + The `NotImplemented`_ section of the Python documentation recommends + returning this constant when comparing with ``__gt__``, ``__lt__``, + and other comparison operators to "to indicate that the operation is + not implemented with respect to the other type". + + .. _NotImplemented: https://docs.python.org/3/library/constants.html#NotImplemented + +* :gh:`319`: Introduce stages in :file:`.travis.yml` + The config file contains now two stages: check and test. If + check fails, the test stage won't be executed. This could + speed up things when some checks fails. + +* :gh:`322`: Switch from Travis CI to GitHub Actions. + +* :gh:`347`: Support Python 3.10 in GitHub Action and other config files. + + + +---- + +.. _semver-3.0.0-dev.2: + +Version 3.0.0-dev.2 +=================== + +:Released: 2020-11-01 +:Maintainer: Tom Schraitle + + +.. _semver-3.0.0-dev.2-deprecations: + +Deprecations +------------ + +* :gh:`169`: Deprecate CLI functions not imported from ``semver.cli``. + + +.. _semver-3.0.0-dev.2-features: + +Features +-------- + +* :gh:`169`: Create semver package and split code among different modules in the packages. + + * Remove :file:`semver.py` + * Create :file:`src/semver/__init__.py` + * Create :file:`src/semver/cli.py` for all CLI methods + * Create :file:`src/semver/_deprecated.py` for the ``deprecated`` decorator and other deprecated functions + * Create :file:`src/semver/__main__.py` to allow calling the CLI using :command:`python -m semver` + * Create :file:`src/semver/_types.py` to hold type aliases + * Create :file:`src/semver/version.py` to hold the :class:`Version` class (old name :class:`VersionInfo`) and its utility functions + * Create :file:`src/semver/__about__.py` for all the metadata variables + +* :gh:`305`: Rename :class:`VersionInfo` to :class:`Version` but keep an alias for compatibility + + +.. _semver-3.0.0-dev.2-docs: + +Improved Documentation +---------------------- + +* :gh:`304`: Several improvements in documentation: + + * Reorganize API documentation. + * Add migration chapter from semver2 to semver3. + * Distinguish between changlog for version 2 and 3 + +* :gh:`305`: Add note about :class:`Version` rename. + + +.. _semver-3.0.0-dev.2-trivial: + +Trivial/Internal Changes +------------------------ + +* :gh:`169`: Adapted infrastructure code to the new project layout. + + * Replace :file:`setup.py` with :file:`setup.cfg` because the :file:`setup.cfg` is easier to use + * Adapt documentation code snippets where needed + * Adapt tests + * Changed the ``deprecated`` to hardcode the ``semver`` package name in the warning. + + Increase coverage to 100% for all non-deprecated APIs + +* :gh:`304`: Support PEP-561 :file:`py.typed`. + + According to the mentioned PEP: + + "Package maintainers who wish to support type checking + of their code MUST add a marker file named :file:`py.typed` + to their package supporting typing." + + Add package_data to :file:`setup.cfg` to include this marker in dist + and whl file. + + + +---- + +.. _semver-3.0.0-dev.1: + +Version 3.0.0-dev.1 +=================== + +:Released: 2020-10-26 +:Maintainer: Tom Schraitle + + +.. _semver-3.0.0-dev.1-deprecations: + +Deprecations +------------ + +* :pr:`290`: For semver 3.0.0-alpha0: + + * Remove anything related to Python2 + * In :file:`tox.ini` and :file:`.travis.yml` + Remove targets py27, py34, py35, and pypy. + Add py38, py39, and nightly (allow to fail) + * In :file:`setup.py` simplified file and remove + ``Tox`` and ``Clean`` classes + * Remove old Python versions (2.7, 3.4, 3.5, and pypy) + from Travis + +* :gh:`234`: In :file:`setup.py` simplified file and remove + ``Tox`` and ``Clean`` classes + + +.. _semver-3.0.0-dev.1-features: + +Features +-------- + +* :pr:`290`: Create semver 3.0.0-alpha0 + + * Update :file:`README.rst`, mention maintenance + branch ``maint/v2``. + * Remove old code mainly used for Python2 compatibility, + adjusted code to support Python3 features. + * Split test suite into separate files under :file:`tests/` + directory + * Adjust and update :file:`setup.py`. Requires Python >=3.6.* + Extract metadata directly from source (affects all the ``__version__``, + ``__author__`` etc. variables) + +* :gh:`270`: Configure Towncrier (:pr:`273`:) + + * Add :file:`changelog.d/.gitignore` to keep this directory + * Create :file:`changelog.d/README.rst` with some descriptions + * Add :file:`changelog.d/_template.rst` as Towncrier template + * Add ``[tool.towncrier]`` section in :file:`pyproject.toml` + * Add "changelog" target into :file:`tox.ini`. Use it like + :command:`tox -e changelog -- CMD` whereas ``CMD`` is a + Towncrier command. The default :command:`tox -e changelog` + calls Towncrier to create a draft of the changelog file + and output it to stdout. + * Update documentation and add include a new section + "Changelog" included from :file:`changelog.d/README.rst`. + +* :gh:`276`: Document how to create a sublass from :class:`VersionInfo` class + +* :gh:`213`: Add typing information + + +.. _semver-3.0.0-dev.1-bugfixes: + +Bug Fixes +--------- + +* :gh:`291`: Disallow negative numbers in VersionInfo arguments + for ``major``, ``minor``, and ``patch``. + + +.. _semver-3.0.0-dev.1-docs: + +Improved Documentation +---------------------- + +* :pr:`290`: Several improvements in the documentation: + + * New layout to distinguish from the semver2 development line. + * Create new logo. + * Remove any occurances of Python2. + * Describe changelog process with Towncrier. + * Update the release process. + + +.. _semver-3.0.0-dev.1-trivial: + +Trivial/Internal Changes +------------------------ + +* :pr:`290`: Add supported Python versions to :command:`black`. diff --git a/docs/contribute/add-changelog-entry.rst b/docs/contribute/add-changelog-entry.rst new file mode 100644 index 00000000..c0d426a8 --- /dev/null +++ b/docs/contribute/add-changelog-entry.rst @@ -0,0 +1,7 @@ +.. _add-changelog: + +Adding a Changelog Entry +======================== + +.. include:: ../../changelog.d/README.rst + :start-after: -text-begin- \ No newline at end of file diff --git a/docs/contribute/doc-semver.rst b/docs/contribute/doc-semver.rst new file mode 100644 index 00000000..fcc6c1ac --- /dev/null +++ b/docs/contribute/doc-semver.rst @@ -0,0 +1,80 @@ +.. _doc: + +Documenting semver +================== + +Documenting the features of semver is very important. It gives our developers +an overview what is possible with semver, how it "feels", and how it is +used efficiently. + +.. note:: + + To build the documentation locally use the following command:: + + $ tox -e docs + + The built documentation is available in :file:`docs/_build/html`. + + +A new feature is *not* complete if it isn't proberly documented. A good +documentation includes: + + * **A docstring** + + Each docstring contains a summary line, a linebreak, an optional + directive (see next item), the description of its arguments in + `Sphinx style`_, and an optional doctest. + The docstring is extracted and reused in the :ref:`api` section. + An appropriate docstring should look like this:: + + def to_tuple(self) -> VersionTuple: + """ + Convert the Version object to a tuple. + + .. versionadded:: 2.10.0 + Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to + make this function available in the public API. + + :return: a tuple with all the parts + + >>> semver.Version(5, 3, 1).to_tuple() + (5, 3, 1, None, None) + + """ + + * **An optional directive** + + If you introduce a new feature, change a function/method, or remove something, + it is a good practice to introduce Sphinx directives into the docstring. + This gives the reader an idea what version is affected by this change. + + The first required argument, ``VERSION``, defines the version when this change + was introduced. You can choose from: + + * ``.. versionadded:: VERSION`` + + Use this directive to describe a new feature. + + * ``.. versionchanged:: VERSION`` + + Use this directive to describe when something has changed, for example, + new parameters were added, changed side effects, different return values, etc. + + * ``.. deprecated:: VERSION`` + + Use this directive when a feature is deprecated. Describe what should + be used instead, if appropriate. + + + Add such a directive *after* the summary line, as shown above. + + * **The documentation** + + A docstring is good, but in most cases it's too dense. API documentation + cannot replace a good user documentation. Describe how + to use your new feature in our documentation. Here you can give your + readers more examples, describe it in a broader context or show + edge cases. + + +.. _Sphinx style: https://sphinx-rtd-tutorial.rtfd.io/en/latest/docstrings.html diff --git a/docs/contribute/finish-release.rst b/docs/contribute/finish-release.rst new file mode 100644 index 00000000..947fcf96 --- /dev/null +++ b/docs/contribute/finish-release.rst @@ -0,0 +1,24 @@ +.. _finish-release: + +Finish the Release +================== + +1. Create a tag: + + $ git tag -a x.x.x + + It’s recommended to use the generated Tox output from the Changelog. + +2. Push the tag: + + $ git push –tags + +3. In `GitHub Release + page `_ + document the new release. Select the tag from the last step and copy + the content of the tag description into the release description. + +4. Announce it in + https://github.com/python-semver/python-semver/discussions/categories/announcements. + +You’re done! Celebrate! diff --git a/docs/contribute/index.rst b/docs/contribute/index.rst new file mode 100644 index 00000000..f09e6418 --- /dev/null +++ b/docs/contribute/index.rst @@ -0,0 +1,28 @@ +.. _contributing: + +Contributing to semver +====================== + +The semver source code is managed using Git and is hosted on GitHub:: + + git clone git://github.com/python-semver/python-semver + + +.. include:: prerequisites.rst + :start-after: -text-begin- + + +.. toctree:: + :maxdepth: 1 + :caption: More topics + :includehidden: + + report-bugs + run-test-suite + doc-semver + add-changelog-entry + release-procedure + finish-release + + +.. _pull request: https://github.com/python-semver/python-semver/pulls diff --git a/docs/contribute/prerequisites.rst b/docs/contribute/prerequisites.rst new file mode 100644 index 00000000..79bbb571 --- /dev/null +++ b/docs/contribute/prerequisites.rst @@ -0,0 +1,15 @@ +Prerequisites +------------- + +.. -text-begin- + +Before you make changes to the code, we would highly appreciate if you +consider the following general requirements: + +* Make sure your code adheres to the `Semantic Versioning`_ specification. + +* Check if your feature is covered by the Semantic Versioning specification. + If not, ask on its GitHub project https://github.com/semver/semver. + + +.. _Semantic Versioning: https://semver.org diff --git a/docs/contribute/release-procedure.rst b/docs/contribute/release-procedure.rst new file mode 100644 index 00000000..c02148fe --- /dev/null +++ b/docs/contribute/release-procedure.rst @@ -0,0 +1,123 @@ +Release Procedure +================= + +The following procedures gives a short overview of what steps are needed +to create a new release. + +These steps are interesting for the release manager only. + + +Prepare the Release +------------------- + +1. Verify that: + + - all issues for a new release are closed: + https://github.com/python-semver/python-semver/issues. + + - all pull requests that should be included in this release are + merged: https://github.com/python-semver/python-semver/pulls. + + - continuous integration for latest build was passing: + https://github.com/python-semver/python-semver/actions. + +2. Create a new branch ``release/``. + +3. If one or several supported Python versions have been removed or + added, verify that the following files have been updated: + + - :file:`setup.cfg` + - :file:`tox.ini` + - :file:`.git/workflows/pythonpackage.yml` + - :file:`.github/workflows/python-testing.yml` + +4. Verify that the version in file :file:`src/semver/__about__.py` + has been updated and follows the `Semver `_ + specification. + +5. Add eventually new contributor(s) to + `CONTRIBUTORS `_. + +6. Check if all changelog entries are created. If some are missing, + `create + them `__. + +7. Show the new draft + `CHANGELOG `_ entry for the latest release with: + + :: + + $ tox -e changelog + + Check the output. If you are not happy, update the files in the + ``changelog.d/`` directory. If everything is okay, build the new + ``CHANGELOG`` with: + + :: + + $ tox -e changelog -- build + +8. Build the documentation and check the output: + + :: + + $ tox -e docs + +9. Commit all changes, push, and create a pull request. + +Create the New Release +---------------------- + +1. Ensure that long description + (`README.rst `_) + can be correctly rendered by Pypi using + ``restview --long-description`` + +2. Clean up your local Git repository. Be careful, as it **will remove + all files** which are not versioned by Git: + + :: + + $ git clean -xfd + + Before you create your distribution files, clean the directory too: + + :: + + $ rm dist/* + +3. Create the distribution files (wheel and source): + + :: + + $ tox -e prepare-dist + +4. Upload the wheel and source to TestPyPI first: + + .. code:: bash + + $ twine upload --repository-url https://test.pypi.org/legacy/ dist/* + + If you have a ``~/.pypirc`` with a ``testpypi`` section, the upload + can be simplified: + + :: + + $ twine upload --repository testpypi dist/* + +5. Check if everything is okay with the wheel. Check also the web site + ``https://test.pypi.org/project//`` + +6. If everything looks fine, merge the pull request. + +7. Upload to PyPI: + + .. code:: bash + + $ git clean -xfd + $ tox -e prepare-dist + $ twine upload dist/* + +8. Go to https://pypi.org/project/semver/ to verify that new version is + online and the page is rendered correctly. + diff --git a/docs/contribute/report-bugs.rst b/docs/contribute/report-bugs.rst new file mode 100644 index 00000000..fa14eb18 --- /dev/null +++ b/docs/contribute/report-bugs.rst @@ -0,0 +1,18 @@ +.. _report-bugs: + +Reporting Bugs and Asking Questions +----------------------------------- + +If you think you have encountered a bug in semver or have an idea for a new +feature? Great! We like to hear from you! + +There are several options to participate: + +* Open a new topic on our `GitHub discussion `_ page. + Tell us our ideas or ask your questions. + +* Look into our GitHub `issues`_ tracker or open a new issue. + + +.. _issues: https://github.com/python-semver/python-semver/issues +.. _gh_discussions: https://github.com/python-semver/python-semver/discussions diff --git a/docs/contribute/run-test-suite.rst b/docs/contribute/run-test-suite.rst new file mode 100644 index 00000000..07c49fff --- /dev/null +++ b/docs/contribute/run-test-suite.rst @@ -0,0 +1,64 @@ +.. _testsuite: + +Running the Test Suite +====================== + +We use `pytest`_ and `tox`_ to run tests against all supported Python +versions. All test dependencies are resolved automatically. + +You can decide to run the complete test suite or only part of it: + +* To run all tests, use:: + + $ tox + + If you have not all Python interpreters installed on your system + it will probably give you some errors (``InterpreterNotFound``). + To avoid such errors, use:: + + $ tox --skip-missing-interpreters + + It is possible to use one or more specific Python versions. Use the ``-e`` + option and one or more abbreviations (``py37`` for Python 3.7, + ``py38`` for Python 3.8 etc.):: + + $ tox -e py37 + $ tox -e py37,py38 + + To get a complete list and a short description, run:: + + $ tox -av + +* To run only a specific test, pytest requires the syntax + ``TEST_FILE::TEST_FUNCTION``. + + For example, the following line tests only the function + :func:`test_immutable_major` in the file :file:`test_bump.py` for all + Python versions:: + + $ tox -e py37 -- tests/test_bump.py::test_should_bump_major + + By default, pytest prints only a dot for each test function. To + reveal the executed test function, use the following syntax:: + + $ tox -- -v + + You can combine the specific test function with the ``-e`` option, for + example, to limit the tests for Python 3.7 and 3.8 only:: + + $ tox -e py37,py38 -- tests/test_bump.py::test_should_bump_major + +Our code is checked against formatting, style, type, and docstring issues +(`black`_, `flake8`_, `mypy`_, and `docformatter`_). +It is recommended to run your tests in combination with :command:`checks`, +for example:: + + $ tox -e checks,py37,py38 + + +.. _black: https://black.rtfd.io +.. _docformatter: https://pypi.org/project/docformatter/ +.. _flake8: https://flake8.rtfd.io +.. _mypy: http://mypy-lang.org/ +.. _pytest: http://pytest.org/ +.. _tox: https://tox.rtfd.org/ diff --git a/docs/development.rst b/docs/development.rst deleted file mode 100644 index e582053e..00000000 --- a/docs/development.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst index 2dce2a50..4c9ccff7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,13 +8,14 @@ Semver |version| -- Semantic Versioning :maxdepth: 2 :caption: Contents :hidden: + :numbered: - build + build-semver install usage/index migration/index advanced/index - development + contribute/index api .. toctree:: @@ -27,7 +28,7 @@ Semver |version| -- Semantic Versioning .. toctree:: :maxdepth: 1 - :caption: Changelogs + :caption: Development :hidden: changelog diff --git a/docs/migration/migratetosemver3.rst b/docs/migration/migratetosemver3.rst index 852ea68b..e8b5c9ab 100644 --- a/docs/migration/migratetosemver3.rst +++ b/docs/migration/migratetosemver3.rst @@ -18,10 +18,11 @@ to our :ref:`change-log`. Use Version instead of VersionInfo ---------------------------------- -The :class:`VersionInfo` has been renamed to :class:`Version` -to have a more succinct name. +The :class:`~semver.version.VersionInfo` has been renamed to +:class:`~semver.version.Version` to have a more succinct name. An alias has been created to preserve compatibility but -using the old name has been deprecated. +using the old name has been deprecated and will be removed +in future versions. If you still need the old version, use this line: @@ -35,7 +36,7 @@ Use semver.cli instead of semver -------------------------------- All functions related to CLI parsing are moved to :mod:`semver.cli`. -If you need such functions, like :func:`semver.cmd_bump `, +If you need such functions, like :meth:`~semver.cli.cmd_bump`, import it from :mod:`semver.cli` in the future: .. code-block:: python @@ -46,5 +47,5 @@ import it from :mod:`semver.cli` in the future: Use semver.Version.is_valid instead of semver.Version.isvalid ------------------------------------------------------------- -The pull request :pr:`284` introduced the method :meth:`Version.is_compatible `. To keep consistency, the development team -decided to rename the :meth:`isvalid ` to :meth:`is_valid `. +The pull request :pr:`284` introduced the method :meth:`~semver.version.Version.is_compatible`. To keep consistency, the development team +decided to rename the :meth:`~semver.Version.isvalid` to :meth:`~semver.Version.is_valid`. diff --git a/docs/migration/replace-deprecated-functions.rst b/docs/migration/replace-deprecated-functions.rst index 8762087c..ebe8c354 100644 --- a/docs/migration/replace-deprecated-functions.rst +++ b/docs/migration/replace-deprecated-functions.rst @@ -6,7 +6,7 @@ Replacing Deprecated Functions .. versionchanged:: 2.10.0 The development team of semver has decided to deprecate certain functions on the module level. The preferred way of using semver is through the - :class:`semver.Version` class. + :class:`~semver.version.Version` class. The deprecated functions can still be used in version 2.10.0 and above. In version 3 of semver, the deprecated functions will be removed. @@ -17,10 +17,10 @@ them with code which is compatible for future versions: * :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` - Replace them with the respective methods of the :class:`Version ` + Replace them with the respective methods of the :class:`~semver.version.Version` class. For example, the function :func:`semver.bump_major` is replaced by - :func:`semver.Version.bump_major` and calling the ``str(versionobject)``: + :meth:`~semver.version.Version.bump_major` and calling the ``str(versionobject)``: .. code-block:: python @@ -33,12 +33,12 @@ them with code which is compatible for future versions: * :func:`semver.Version.isvalid` - Replace it with :meth:`semver.Version.is_valid`: + Replace it with :meth:`semver.version.Version.is_valid`: * :func:`semver.finalize_version` - Replace it with :func:`semver.Version.finalize_version`: + Replace it with :func:`semver.version.Version.finalize_version`: .. code-block:: python @@ -60,7 +60,7 @@ them with code which is compatible for future versions: * :func:`semver.max_ver` - Replace it with ``max(version1, version2, ...)`` or ``max([version1, version2, ...])``: + Replace it with ``max(version1, version2, ...)`` or ``max([version1, version2, ...])`` and a ``key``: .. code-block:: python @@ -82,8 +82,8 @@ them with code which is compatible for future versions: * :func:`semver.parse` - Replace it with :func:`semver.Version.parse` and - :func:`semver.Version.to_dict`: + Replace it with :meth:`semver.version.Version.parse` and call + :meth:`semver.version.Version.to_dict`: .. code-block:: python @@ -94,7 +94,7 @@ them with code which is compatible for future versions: * :func:`semver.parse_version_info` - Replace it with :func:`semver.Version.parse`: + Replace it with :meth:`semver.version.Version.parse`: .. code-block:: python @@ -105,7 +105,7 @@ them with code which is compatible for future versions: * :func:`semver.replace` - Replace it with :func:`semver.Version.replace`: + Replace it with :meth:`semver.version.Version.replace`: .. code-block:: python diff --git a/docs/usage/access-parts-through-index.rst b/docs/usage/access-parts-through-index.rst index a261fda4..c3651a5e 100644 --- a/docs/usage/access-parts-through-index.rst +++ b/docs/usage/access-parts-through-index.rst @@ -7,7 +7,7 @@ Accessing Parts Through Index Numbers Another way to access parts of a version is to use an index notation. The underlying :class:`~semver.version.Version` object allows to access its data through -the magic method :func:`~semver.version.Version.__getitem__`. +the magic method :meth:`~semver.version.Version.__getitem__`. For example, the ``major`` part can be accessed by index number 0 (zero). Likewise the other parts: diff --git a/docs/usage/check-compatible-semver-version.rst b/docs/usage/check-compatible-semver-version.rst index 323de3ed..20330456 100644 --- a/docs/usage/check-compatible-semver-version.rst +++ b/docs/usage/check-compatible-semver-version.rst @@ -3,7 +3,7 @@ Checking for a Compatible Semver Version To check if a *change* from a semver version ``a`` to a semver version ``b`` is *compatible* according to semver rule, use the method -:meth:`Version.is_compatible `. +:meth:`~semver.version.Version.is_compatible`. The expression ``a.is_compatible(b) is True`` if one of the following statements is true: diff --git a/docs/usage/check-valid-semver-version.rst b/docs/usage/check-valid-semver-version.rst index a0460df9..bdd57e7a 100644 --- a/docs/usage/check-valid-semver-version.rst +++ b/docs/usage/check-valid-semver-version.rst @@ -2,7 +2,7 @@ Checking for a Valid Semver Version =================================== If you need to check a string if it is a valid semver version, use the -classmethod :func:`Version.isvalid `: +classmethod :meth:`~semver.version.Version.is_valid`: .. code-block:: python diff --git a/docs/usage/compare-versions-through-expression.rst b/docs/usage/compare-versions-through-expression.rst index 28fad671..e2dee4d6 100644 --- a/docs/usage/compare-versions-through-expression.rst +++ b/docs/usage/compare-versions-through-expression.rst @@ -2,7 +2,7 @@ Comparing Versions through an Expression ======================================== If you need a more fine-grained approach of comparing two versions, -use the :func:`semver.match` function. It expects two arguments: +use the :meth:`~semver.version.Version.match` function. It expects two arguments: 1. a version string 2. a match expression @@ -20,9 +20,9 @@ That gives you the following possibilities to express your condition: .. code-block:: python - >>> semver.match("2.0.0", ">=1.0.0") + >>> Version.parse("2.0.0").match(">=1.0.0") True - >>> semver.match("1.0.0", ">1.0.0") + >>> Version.parse("1.0.0").match(">1.0.0") False If no operator is specified, the match expression is interpreted as a @@ -33,7 +33,7 @@ handle both cases: .. code-block:: python - >>> semver.match("2.0.0", "2.0.0") + >>> Version.parse("2.0.0").match("2.0.0") True - >>> semver.match("1.0.0", "3.5.1") + >>> Version.parse("1.0.0").match("3.5.1") False diff --git a/docs/usage/compare-versions.rst b/docs/usage/compare-versions.rst index b42ba1a7..cf55eae3 100644 --- a/docs/usage/compare-versions.rst +++ b/docs/usage/compare-versions.rst @@ -17,7 +17,7 @@ To compare two versions depends on your type: The return value is negative if ``version1 < version2``, zero if ``version1 == version2`` and strictly positive if ``version1 > version2``. -* **Two** :class:`Version ` **instances** +* **Two** :class:`~semver.version.Version` **instances** Use the specific operator. Currently, the operators ``<``, ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: @@ -29,9 +29,9 @@ To compare two versions depends on your type: >>> v1 > v2 False -* **A** :class:`Version ` **type and a** :func:`tuple` **or** :func:`list` +* **A** :class:`~semver.version.Version` **type and a** :func:`tuple` **or** :func:`list` - Use the operator as with two :class:`Version ` types:: + Use the operator as with two :class:`~semver.version.Version` types:: >>> v = Version.parse("3.4.5") >>> v > (1, 0) @@ -46,7 +46,7 @@ To compare two versions depends on your type: >>> [3, 5] > v True -* **A** :class:`Version ` **type and a** :func:`str` +* **A** :class:`~semver.version.Version` **type and a** :func:`str` You can use also raw strings to compare:: @@ -69,7 +69,7 @@ To compare two versions depends on your type: ... ValueError: 1.0 is not valid SemVer string -* **A** :class:`Version ` **type and a** :func:`dict` +* **A** :class:`~semver.version.Version` **type and a** :func:`dict` You can also use a dictionary. In contrast to strings, you can have an "incomplete" version (as the other parts are set to zero):: diff --git a/docs/usage/convert-version-into-different-types.rst b/docs/usage/convert-version-into-different-types.rst index 976283d8..6948438c 100644 --- a/docs/usage/convert-version-into-different-types.rst +++ b/docs/usage/convert-version-into-different-types.rst @@ -3,23 +3,23 @@ Converting a Version instance into Different Types ================================================== -Sometimes it is needed to convert a :class:`Version ` instance into +Sometimes it is needed to convert a :class:`~semver.version.Version` instance into a different type. For example, for displaying or to access all parts. -It is possible to convert a :class:`Version ` instance: +It is possible to convert a :class:`~semver.version.Version` instance: * Into a string with the builtin function :func:`str`:: >>> str(Version.parse("3.4.5-pre.2+build.4")) '3.4.5-pre.2+build.4' -* Into a dictionary with :func:`to_dict `:: +* Into a dictionary with :meth:`~semver.version.Version.to_dict`:: >>> v = Version(major=3, minor=4, patch=5) >>> v.to_dict() OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) -* Into a tuple with :func:`to_tuple `:: +* Into a tuple with :meth:`~semver.version.Version.to_tuple`:: >>> v = Version(major=5, minor=4, patch=2) >>> v.to_tuple() diff --git a/docs/usage/create-a-version.rst b/docs/usage/create-a-version.rst index 3acb4c03..48bb58a1 100644 --- a/docs/usage/create-a-version.rst +++ b/docs/usage/create-a-version.rst @@ -3,7 +3,7 @@ Creating a Version .. versionchanged:: 3.0.0 - The former :class:`~semver.version.VersionInfo` + The former :class:`~semver.version.VersionInfo` class has been renamed to :class:`~semver.version.Version`. The preferred way to create a new version is with the class @@ -15,7 +15,7 @@ The preferred way to create a new version is with the class create a version with module level functions. However, module level functions are marked as *deprecated* since version 2.x.y now. - These functions will be removed in semver 3.1.0. + These functions will be removed. For details, see the sections :ref:`sec_replace_deprecated_functions` and :ref:`sec_display_deprecation_warnings`. diff --git a/docs/usage/get-min-and-max-of-multiple-versions.rst b/docs/usage/get-min-and-max-of-multiple-versions.rst index 266ee50b..e143162a 100644 --- a/docs/usage/get-min-and-max-of-multiple-versions.rst +++ b/docs/usage/get-min-and-max-of-multiple-versions.rst @@ -7,9 +7,9 @@ Getting Minimum and Maximum of Multiple Versions The functions :func:`semver.max_ver` and :func:`semver.min_ver` are deprecated in favor of their builtin counterparts :func:`max` and :func:`min`. -Since :class:`Version ` implements -:func:`__gt__ ` and -:func:`__lt__ `, it can be used with builtins requiring: +Since :class:`~semver.version.Version` implements +:meth:`~semver.version.Version.__gt__` and +:meth:`~semver.version.Version.__lt__`, it can be used with builtins requiring: .. code-block:: python @@ -19,7 +19,7 @@ Since :class:`Version ` implements Version(major=0, minor=1, patch=0, prerelease=None, build=None) Incidentally, using :func:`map`, you can get the min or max version of any number of versions of the same type -(convertible to :class:`Version `). +(convertible to :class:`~semver.version.Version`). For example, here are the maximum and minimum versions of a list of version strings: @@ -40,12 +40,3 @@ And the same can be done with tuples: (0, 4, 99, None, None) For dictionaries, it is very similar to finding the max version tuple: see :ref:`sec.convert.versions`. - -The "old way" with :func:`semver.max_ver` or :func:`semver.min_ver` is still available, but not recommended: - -.. code-block:: python - - >>> semver.max_ver("1.0.0", "2.0.0") - '2.0.0' - >>> semver.min_ver("1.0.0", "2.0.0") - '1.0.0' diff --git a/docs/usage/increase-parts-of-a-version_prereleases.rst b/docs/usage/increase-parts-of-a-version_prereleases.rst index 87685b76..845f2290 100644 --- a/docs/usage/increase-parts-of-a-version_prereleases.rst +++ b/docs/usage/increase-parts-of-a-version_prereleases.rst @@ -1,12 +1,13 @@ .. _increase-parts-of-a-version: + Increasing Parts of a Version Taking into Account Prereleases ============================================================= .. versionadded:: 2.10.0 - Added :func:`Version.next_version `. + Added :meth:`~semver.version.Version.next_version`. If you want to raise your version and take prereleases into account, -the function :func:`next_version ` +the function :meth:`~semver.version.Version.next_version` would perhaps a better fit. diff --git a/docs/usage/parse-version-string.rst b/docs/usage/parse-version-string.rst index 0a39c8a3..0cf02650 100644 --- a/docs/usage/parse-version-string.rst +++ b/docs/usage/parse-version-string.rst @@ -2,7 +2,7 @@ Parsing a Version String ======================== "Parsing" in this context means to identify the different parts in a string. -Use the function :func:`Version.parse `:: +Use the function :meth:`~semver.version.Version.parse`:: >>> Version.parse("3.4.5-pre.2+build.4") Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') diff --git a/docs/usage/raise-parts-of-a-version.rst b/docs/usage/raise-parts-of-a-version.rst index d369c575..be89cf8d 100644 --- a/docs/usage/raise-parts-of-a-version.rst +++ b/docs/usage/raise-parts-of-a-version.rst @@ -10,22 +10,22 @@ Raising Parts of a Version will lead to ``1.0.0-rc.1``, but ``1.0.0-rc.1`` is smaller than ``1.0.0``. If you search for a way to take into account this behavior, look for the - method :meth:`Version.next_version ` + method :meth:`~semver.version.Version.next_version` in section :ref:`increase-parts-of-a-version`. The ``semver`` module contains the following functions to raise parts of a version: -* :func:`Version.bump_major `: raises the major part and set all other parts to +* :meth:`~semver.version.Version.bump_major`: raises the major part and set all other parts to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`Version.bump_minor `: raises the minor part and sets ``patch`` to zero. +* :meth:`~semver.version.Version.bump_minor`: raises the minor part and sets ``patch`` to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`Version.bump_patch `: raises the patch part. Set ``prerelease`` and +* :meth:`~semver.version.Version.bump_patch`: raises the patch part. Set ``prerelease`` and ``build`` to ``None``. -* :func:`Version.bump_prerelease `: raises the prerelease part and set +* :meth:`~semver.version.Version.bump_prerelease`: raises the prerelease part and set ``build`` to ``None``. -* :func:`Version.bump_build `: raises the build part. +* :meth:`~semver.version.Version.bump_build`: raises the build part. .. code-block:: python @@ -43,9 +43,9 @@ a version: Likewise the module level functions :func:`semver.bump_major`. -For the methods :meth:`Version.bump_prerelease ` -and :meth:`Version.bump_build ` it's possible to pass an empty string or ``None``. -However, it gives different results:: +For the methods :meth:`~semver.version.Version.bump_prerelease` +and :meth:`~semver.version.Version.bump_build` it's possible to pass an empty string or ``None``. +However, it gives different results: .. code-block:: python diff --git a/docs/usage/replace-parts-of-a-version.rst b/docs/usage/replace-parts-of-a-version.rst index b6c38865..57ab65e9 100644 --- a/docs/usage/replace-parts-of-a-version.rst +++ b/docs/usage/replace-parts-of-a-version.rst @@ -4,25 +4,14 @@ Replacing Parts of a Version ============================ If you want to replace different parts of a version, but leave other parts -unmodified, use the function :func:`replace `: - -* From a :class:`Version ` instance:: +unmodified, use the function :meth:`~semver.version.Version.replace`: >>> version = semver.Version.parse("1.4.5-pre.1+build.6") >>> version.replace(major=2, minor=2) Version(major=2, minor=2, patch=5, prerelease='pre.1', build='build.6') -* From a version string:: - - >>> semver.replace("1.4.5-pre.1+build.6", major=2) - '2.4.5-pre.1+build.6' - If you pass invalid keys you get an exception:: - >>> semver.replace("1.2.3", invalidkey=2) - Traceback (most recent call last): - ... - TypeError: replace() got 1 unexpected keyword argument(s): invalidkey >>> version = semver.Version.parse("1.4.5-pre.1+build.6") >>> version.replace(invalidkey=2) Traceback (most recent call last): diff --git a/docs/usage/semver-version.rst b/docs/usage/semver-version.rst index 8eeab62f..e8cc92b3 100644 --- a/docs/usage/semver-version.rst +++ b/docs/usage/semver-version.rst @@ -4,4 +4,4 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.0-dev.4' + '3.0.0-rc.1' diff --git a/src/semver/__about__.py b/src/semver/__about__.py index 0f7150bf..dd671d02 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -16,7 +16,7 @@ """ #: Semver version -__version__ = "3.0.0-dev.4" +__version__ = "3.0.0-rc.1" #: Original semver author __author__ = "Kostiantyn Rybnikov" diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py index 5f51c8f3..8dfb1933 100644 --- a/src/semver/_deprecated.py +++ b/src/semver/_deprecated.py @@ -25,7 +25,7 @@ def deprecated( :param func: the function to decorate :param replace: the function to replace (use the full qualified - name like ``semver.Version.bump_major``. + name like ``semver.version.Version.bump_major``. :param version: the first version when this function was deprecated. :param category: allow you to specify the deprecation warning class of your choice. By default, it's :class:`DeprecationWarning`, but @@ -75,7 +75,7 @@ def parse(version): Parse version to major, minor, patch, pre-release, build parts. .. deprecated:: 2.10.0 - Use :func:`semver.Version.parse` instead. + Use :meth:`~semver.version.Version.parse` instead. :param version: version string :return: dictionary with the keys 'build', 'major', 'minor', 'patch', @@ -98,13 +98,13 @@ def parse(version): return Version.parse(version).to_dict() -@deprecated(replace="semver.Version.parse", version="2.10.0") +@deprecated(replace="semver.version.Version.parse", version="2.10.0") def parse_version_info(version): """ - Parse version string to a VersionInfo instance. + Parse version string to a Version instance. .. deprecated:: 2.10.0 - Use :func:`semver.VersionInfo.parse` instead. + Use :meth:`~semver.version.Version.parse` instead. .. versionadded:: 2.7.2 Added :func:`semver.parse_version_info` @@ -153,6 +153,9 @@ def match(version, match_expr): """ Compare two versions strings through a comparison. + .. deprecated:: 2.10.0 + Use :meth:`~semver.version.Version.match` instead. + :param str version: a version string :param str match_expr: operator and version; valid operators are < smaller than @@ -178,6 +181,9 @@ def max_ver(ver1, ver2): """ Returns the greater version of two versions strings. + .. deprecated:: 2.10.2 + Use :func:`max` instead. + :param ver1: version string 1 :param ver2: version string 2 :return: the greater version of the two @@ -202,6 +208,9 @@ def min_ver(ver1, ver2): """ Returns the smaller version of two versions strings. + .. deprecated:: 2.10.2 + Use Use :func:`min` instead. + :param ver1: version string 1 :param ver2: version string 2 :return: the smaller version of the two @@ -246,7 +255,7 @@ def bump_major(version): Raise the major part of the version string. .. deprecated:: 2.10.0 - Use :func:`semver.Version.bump_major` instead. + Use :meth:`~semver.version.Version.bump_major` instead. :param: version string :return: the raised version string @@ -264,7 +273,7 @@ def bump_minor(version): Raise the minor part of the version string. .. deprecated:: 2.10.0 - Use :func:`semver.Version.bump_minor` instead. + Use :meth:`~semver.version.Version.bump_minor` instead. :param: version string :return: the raised version string @@ -282,7 +291,7 @@ def bump_patch(version): Raise the patch part of the version string. .. deprecated:: 2.10.0 - Use :func:`semver.Version.bump_patch` instead. + Use :meth:`~semver.version.Version.bump_patch` instead. :param: version string :return: the raised version string @@ -300,7 +309,7 @@ def bump_prerelease(version, token="rc"): Raise the prerelease part of the version string. .. deprecated:: 2.10.0 - Use :func:`semver.Version.bump_prerelease` instead. + Use :meth:`~semver.version.Version.bump_prerelease` instead. :param version: version string :param token: defaults to 'rc' @@ -319,7 +328,7 @@ def bump_build(version, token="build"): Raise the build part of the version string. .. deprecated:: 2.10.0 - Use :func:`semver.Version.bump_build` instead. + Use :meth:`~semver.version.Version.bump_build` instead. :param version: version string :param token: defaults to 'build' @@ -338,7 +347,7 @@ def finalize_version(version): Remove any prerelease and build metadata from the version string. .. deprecated:: 2.10.0 - Use :func:`semver.Version.finalize_version` instead. + Use :meth:`~semver.version.Version.finalize_version` instead. .. versionadded:: 2.7.9 Added :func:`finalize_version` @@ -360,7 +369,7 @@ def replace(version, **parts): Replace one or more parts of a version and return the new string. .. deprecated:: 2.10.0 - Use :func:`semver.Version.replace` instead. + Use :meth:`~semver.version.Version.replace` instead. .. versionadded:: 2.9.0 Added :func:`replace` diff --git a/src/semver/version.py b/src/semver/version.py index 9c135c5a..cca744a1 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -1,4 +1,4 @@ -"""Version handling.""" +"""Version handling by a semver compatible version class.""" import collections import re @@ -61,6 +61,8 @@ class Version: """ A semver compatible version class. + See specification at https://semver.org. + :param major: version when you make incompatible API changes. :param minor: version when you add functionality in a backwards-compatible manner. @@ -205,7 +207,7 @@ def to_tuple(self) -> VersionTuple: Convert the Version object to a tuple. .. versionadded:: 2.10.0 - Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to + Renamed :meth:`Version._astuple` to :meth:`Version.to_tuple` to make this function available in the public API. :return: a tuple with all the parts @@ -220,7 +222,7 @@ def to_dict(self) -> VersionDict: Convert the Version object to an OrderedDict. .. versionadded:: 2.10.0 - Renamed ``VersionInfo._asdict`` to ``VersionInfo.to_dict`` to + Renamed :meth:`Version._asdict` to :meth:`Version.to_dict` to make this function available in the public API. :return: an OrderedDict with the keys in the order ``major``, ``minor``, @@ -269,7 +271,6 @@ def bump_major(self) -> "Version": :return: new object with the raised major part - >>> ver = semver.parse("3.4.5") >>> ver.bump_major() Version(major=4, minor=0, patch=0, prerelease=None, build=None) @@ -446,9 +447,7 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": validparts = cls.NAMES[:-1] if part not in validparts: raise ValueError( - "Invalid part. Expected one of {validparts}, but got {part!r}".format( - validparts=validparts, part=part - ) + f"Invalid part. Expected one of {validparts}, but got {part!r}" ) version = self if (version.prerelease or version.build) and ( @@ -500,7 +499,7 @@ def __getitem__( is undefined, it will throw an index error. Negative indices are not supported. - :param Union[int, slice] index: a positive integer indicating the + :param index: a positive integer indicating the offset or a :func:`slice` object :raises IndexError: if index is beyond the range or a part is None :return: the requested part of the version at position index @@ -562,7 +561,7 @@ def match(self, match_expr: str) -> bool: Compare self to match a match expression. :param match_expr: optional operator and version; valid operators are - ``<``` smaller than + ``<`` smaller than ``>`` greater than ``>=`` greator or equal than ``<=`` smaller or equal than @@ -619,8 +618,8 @@ def parse( Changed method from static to classmethod to allow subclasses. .. versionchanged:: 3.0.0 - Added optional parameter optional_minor_and_patch to allow optional - minor and patch parts. + Added optional parameter ``optional_minor_and_patch`` to allow + optional minor and patch parts. :param version: version string :param optional_minor_and_patch: if set to true, the version string to parse \ @@ -665,8 +664,8 @@ def replace(self, **parts: Union[int, Optional[str]]) -> "Version": :param parts: the parts to be updated. Valid keys are: ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` - :return: the new :class:`Version` object with the changed - parts + :return: the new :class:`~semver.version.Version` object with + the changed parts :raises TypeError: if ``parts`` contain invalid keys """ version = self.to_dict() @@ -688,6 +687,9 @@ def is_valid(cls, version: str) -> bool: .. versionadded:: 2.9.1 + .. versionchanged:: 3.0.0 + Renamed from :meth:`~semver.version.Version.isvalid` + :param version: the version string to check :return: True if the version string is a valid semver version, False otherwise. @@ -712,6 +714,8 @@ def is_compatible(self, other: "Version") -> bool: The algorithm does *not* check patches. + .. versionadded:: 3.0.0 + :param other: the version to check for compatibility :return: True, if ``other`` is compatible with the old version, otherwise False diff --git a/tox.ini b/tox.ini index dd071721..b71ae78e 100644 --- a/tox.ini +++ b/tox.ini @@ -56,7 +56,7 @@ commands = mypy {posargs:--ignore-missing-imports --check-untyped-defs src} description = Check for PEP257 compatible docstrings basepython = python3 deps = docformatter -commands = docformatter --check {posargs:--pre-summary-newline -r src} +commands = docformatter --check --diff {posargs:--pre-summary-newline -r src} [testenv:checks] From c72c50c715126a652728f7be83c57d330edbead5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 2 Apr 2023 09:14:28 +0200 Subject: [PATCH 76/86] Simplify max_ver and min_ver Use max() and min() with argument key=Version.parse --- src/semver/_deprecated.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py index 8dfb1933..2c736fff 100644 --- a/src/semver/_deprecated.py +++ b/src/semver/_deprecated.py @@ -11,7 +11,7 @@ from . import cli from .version import Version -from ._types import Decorator, F, String +from ._types import Decorator, F def deprecated( @@ -192,15 +192,7 @@ def max_ver(ver1, ver2): >>> semver.max_ver("1.0.0", "2.0.0") '2.0.0' """ - if isinstance(ver1, String.__args__): # type: ignore - ver1 = Version.parse(ver1) - elif not isinstance(ver1, Version): - raise TypeError() - cmp_res = ver1.compare(ver2) - if cmp_res >= 0: - return str(ver1) - else: - return ver2 + return str(max(ver1, ver2, key=Version.parse)) @deprecated(replace="min", version="2.10.2") @@ -219,12 +211,7 @@ def min_ver(ver1, ver2): >>> semver.min_ver("1.0.0", "2.0.0") '1.0.0' """ - ver1 = Version.parse(ver1) - cmp_res = ver1.compare(ver2) - if cmp_res <= 0: - return str(ver1) - else: - return ver2 + return str(min(ver1, ver2, key=Version.parse)) @deprecated(replace="str(versionobject)", version="2.10.0") From 47b49cac131c24b1d3f9046ecb62fdace0c2de9e Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 19 Mar 2023 17:18:22 +0100 Subject: [PATCH 77/86] Fix #258: Keep semver._deprecated.compare Although it breaks consistency with module level functions, it seems it's a much needed/used function. Nevertheless it's a "deprecated" function. * Function is also available accessing semver.compare() * Decorate semver.compare() as PendingDeprecationWarning * Update docstring of semver.compare() * Adapt `deprecated` decorator and use enforce keyword arguments * Update CHANGELOG.rst * Use intersphinx to link to Python exception, use Python inventory It's still unclear if we should deprecate this function or not (that's why we use PendingDeprecationWarning). As we don't have a uniform initializer yet, this function stays in _deprecated.py for the time being until we find a better soltuion. See #258 for details --- CHANGELOG.rst | 10 +- docs/api.rst | 4 +- docs/changelog-semver2.rst | 670 ---------------------------- docs/conf.py | 11 + docs/index.rst | 2 +- docs/inventories/python-objects.inv | Bin 0 -> 130055 bytes docs/usage/compare-versions.rst | 2 +- src/semver/__init__.py | 2 +- src/semver/_deprecated.py | 89 ++-- tests/test_deprecated_functions.py | 2 - 10 files changed, 83 insertions(+), 709 deletions(-) delete mode 100644 docs/changelog-semver2.rst create mode 100644 docs/inventories/python-objects.inv diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 28a401e2..11b08e06 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,7 +21,7 @@ This section covers the changes between major version 2 and version 3. Version 3.0.0 ============= -:Released: 2023-03-19 +:Released: 2023-04-02 :Maintainer: Tom Schraitle @@ -95,6 +95,14 @@ Deprecations didn't cast many votes, the majority agreed to remove support for Python 3.6. +* :pr:`402`: Keep :func:`semver.compare `. + Although it breaks consistency with module level functions, it seems it's + a much needed/used function. It's still unclear if we should deprecate + this function or not (that's why we use :py:exc:`PendingDeprecationWarning`). + + As we don't have a uniform initializer yet, this function stays in the + :file:`_deprecated.py` file for the time being until we find a better solution. See :gh:`258` for details. + Features -------- diff --git a/docs/api.rst b/docs/api.rst index 279df408..f545ebc5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -17,6 +17,8 @@ Deprecated Functions in :mod:`semver._deprecated` .. automodule:: semver._deprecated +.. autofunction:: semver._deprecated.compare + .. autofunction:: semver._deprecated.bump_build .. autofunction:: semver._deprecated.bump_major @@ -27,8 +29,6 @@ Deprecated Functions in :mod:`semver._deprecated` .. autofunction:: semver._deprecated.bump_prerelease -.. autofunction:: semver._deprecated.compare - .. autofunction:: semver._deprecated.deprecated .. autofunction:: semver._deprecated.finalize_version diff --git a/docs/changelog-semver2.rst b/docs/changelog-semver2.rst deleted file mode 100644 index dca94413..00000000 --- a/docs/changelog-semver2.rst +++ /dev/null @@ -1,670 +0,0 @@ -################## -Change Log semver2 -################## - -This changelog contains older entries for semver2. - ----- - - -Version 2.13.0 -============== - -:Released: 2020-10-20 -:Maintainer: Tom Schraitle - - -Features --------- - -* :pr:`287`: Document how to create subclass from ``VersionInfo`` - - -Bug Fixes ---------- - -* :pr:`283`: Ensure equal versions have equal hashes. - Version equality means for semver, that ``major``, - ``minor``, ``patch``, and ``prerelease`` parts are - equal in both versions you compare. The ``build`` part - is ignored. - - -Additions ---------- - -n/a - - -Deprecations ------------- - -n/a - - ----- - - -Version 2.12.0 -============== - -:Released: 2020-10-19 -:Maintainer: Tom Schraitle - - -Bug Fixes ---------- - -* :gh:`291` (:pr:`292`): Disallow negative numbers of - ``major``, ``minor``, and ``patch`` for :class:`semver.VersionInfo` - - ----- - - -Version 2.11.0 -============== - -:Released: 2020-10-17 -:Maintainer: Tom Schraitle - - -Bug Fixes ---------- - -* :gh:`276` (:pr:`277`): ``VersionInfo.parse`` should be a class method - Also add authors and update changelog in :gh:`286` -* :gh:`274` (:pr:`275`): Py2 vs. Py3 incompatibility TypeError - - ----- - - -Version 2.10.2 -============== - -:Released: 2020-06-15 -:Maintainer: Tom Schraitle - -Features --------- - -:gh:`268`: Increase coverage - - -Bug Fixes ---------- - -* :gh:`260` (:pr:`261`): Fixed ``__getitem__`` returning None on wrong parts -* :pr:`263`: Doc: Add missing "install" subcommand for openSUSE - - -Deprecations ------------- - -* :gh:`160` (:pr:`264`): - * :func:`semver.max_ver` - * :func:`semver.min_ver` - - ----- - - -Version 2.10.1 -============== - -:Released: 2020-05-13 -:Maintainer: Tom Schraitle - - -Features --------- - -* :pr:`249`: Added release policy and version restriction in documentation to - help our users which would like to stay on the major 2 release. -* :pr:`250`: Simplified installation semver on openSUSE with ``obs://``. -* :pr:`256`: Made docstrings consistent - - - -Bug Fixes ---------- - -* :gh:`251` (:pr:`254`): Fixed return type of ``semver.VersionInfo.next_version`` - to always return a ``VersionInfo`` instance. - - ----- - - - -Version 2.10.0 -============== - -:Released: 2020-05-05 -:Maintainer: Tom Schraitle - -Features --------- - -* :pr:`138`: Added ``__getitem__`` magic method to ``semver.VersionInfo`` class. - Allows to access a version like ``version[1]``. -* :pr:`235`: Improved documentation and shift focus on ``semver.VersionInfo`` instead of advertising - the old and deprecated module-level functions. -* :pr:`230`: Add version information in some functions: - - * Use ``.. versionadded::`` RST directive in docstrings to - make it more visible when something was added - * Minor wording fix in docstrings (versions -> version strings) - - -Bug Fixes ---------- - -* :gh:`224` (:pr:`226`): In ``setup.py``, replaced in class ``clean``, - ``super(CleanCommand, self).run()`` with ``CleanCommand.run(self)`` -* :gh:`244` (:pr:`245`): Allow comparison with ``VersionInfo``, tuple/list, dict, and string. - - -Additions ---------- - -* :pr:`228`: Added better doctest integration - - -Deprecations ------------- -* :gh:`225` (:pr:`229`): Output a DeprecationWarning for the following functions: - - - ``semver.parse`` - - ``semver.parse_version_info`` - - ``semver.format_version`` - - ``semver.bump_{major,minor,patch,prerelease,build}`` - - ``semver.finalize_version`` - - ``semver.replace`` - - ``semver.VersionInfo._asdict`` (use the new, public available - function ``semver.VersionInfo.to_dict()``) - - ``semver.VersionInfo._astuple`` (use the new, public available - function ``semver.VersionInfo.to_tuple()``) - - These deprecated functions will be removed in semver 3. - - ----- - - -Version 2.9.1 -============= -:Released: 2020-02-16 -:Maintainer: Tom Schraitle - -Features --------- - -* :gh:`177` (:pr:`178`): Fixed repository and CI links (moved https://github.com/k-bx/python-semver/ repository to https://github.com/python-semver/python-semver/) -* :pr:`179`: Added note about moving this project to the new python-semver organization on GitHub -* :gh:`187` (:pr:`188`): Added logo for python-semver organization and documentation -* :gh:`191` (:pr:`194`): Created manpage for pysemver -* :gh:`196` (:pr:`197`): Added distribution specific installation instructions -* :gh:`201` (:pr:`202`): Reformatted source code with black -* :gh:`208` (:pr:`209`): Introduce new function :func:`semver.VersionInfo.isvalid` - and extend :command:`pysemver` with :command:`check` subcommand -* :gh:`210` (:pr:`215`): Document how to deal with invalid versions -* :pr:`212`: Improve docstrings according to PEP257 - -Bug Fixes ---------- - -* :gh:`192` (:pr:`193`): Fixed "pysemver" and "pysemver bump" when called without arguments - - ----- - -Version 2.9.0 -============= -:Released: 2019-10-30 -:Maintainer: Sébastien Celles - -Features --------- - -* :gh:`59` (:pr:`164`): Implemented a command line interface -* :gh:`85` (:pr:`147`, :pr:`154`): Improved contribution section -* :gh:`104` (:pr:`125`): Added iterator to :func:`semver.VersionInfo` -* :gh:`112`, :gh:`113`: Added Python 3.7 support -* :pr:`120`: Improved test_immutable function with properties -* :pr:`125`: Created :file:`setup.cfg` for pytest and tox -* :gh:`126` (:pr:`127`): Added target for documentation in :file:`tox.ini` -* :gh:`142` (:pr:`143`): Improved usage section -* :gh:`144` (:pr:`156`): Added :func:`semver.replace` and :func:`semver.VersionInfo.replace` - functions -* :gh:`145` (:pr:`146`): Added posargs in :file:`tox.ini` -* :pr:`157`: Introduce :file:`conftest.py` to improve doctests -* :pr:`165`: Improved code coverage -* :pr:`166`: Reworked :file:`.gitignore` file -* :gh:`167` (:pr:`168`): Introduced global constant :data:`SEMVER_SPEC_VERSION` - -Bug Fixes ---------- - -* :gh:`102`: Fixed comparison between VersionInfo and tuple -* :gh:`103`: Disallow comparison between VersionInfo and string (and int) -* :gh:`121` (:pr:`122`): Use python3 instead of python3.4 in :file:`tox.ini` -* :pr:`123`: Improved :func:`__repr__` and derive class name from :func:`type` -* :gh:`128` (:pr:`129`): Fixed wrong datatypes in docstring for :func:`semver.format_version` -* :gh:`135` (:pr:`140`): Converted prerelease and build to string -* :gh:`136` (:pr:`151`): Added testsuite to tarball -* :gh:`154` (:pr:`155`): Improved README description - -Removals --------- - -* :gh:`111` (:pr:`110`): Dropped Python 3.3 -* :gh:`148` (:pr:`149`): Removed and replaced ``python setup.py test`` - - ----- - -Version 2.8.2 -============= -:Released: 2019-05-19 -:Maintainer: Sébastien Celles - -Skipped, not released. - ----- - -Version 2.8.1 -============= -:Released: 2018-07-09 -:Maintainer: Sébastien Celles - -Features --------- - -* :gh:`40` (:pr:`88`): Added a static parse method to VersionInfo -* :gh:`77` (:pr:`47`): Converted multiple tests into pytest.mark.parametrize -* :gh:`87`, :gh:`94` (:pr:`93`): Removed named tuple inheritance. -* :gh:`89` (:pr:`90`): Added doctests. - -Bug Fixes ---------- - -* :gh:`98` (:pr:`99`): Set prerelease and build to None by default -* :gh:`96` (:pr:`97`): Made VersionInfo immutable - - ----- - -Version 2.8.0 -============= -:Released: 2018-05-16 -:Maintainer: Sébastien Celles - - -Changes -------- - -* :gh:`82` (:pr:`83`): Renamed :file:`test.py` to :file:`test_semver.py` so - py.test can autodiscover test file - -Additions ---------- - -* :gh:`79` (:pr:`81`, :pr:`84`): Defined and improve a release procedure file -* :gh:`72`, :gh:`73` (:pr:`75`): Implemented :func:`__str__` and :func:`__hash__` - -Removals --------- - -* :gh:`76` (:pr:`80`): Removed Python 2.6 compatibility - ----- - - -Version 2.7.9 -============= - -:Released: 2017-09-23 -:Maintainer: Kostiantyn Rybnikov - - -Additions ---------- - -* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. - - ----- - -Version 2.7.8 -============= - -:Released: 2017-08-25 -:Maintainer: Kostiantyn Rybnikov - -* :gh:`62`: Support custom default names for pre and build - - ----- - -Version 2.7.7 -============= - -:Released: 2017-05-25 -:Maintainer: Kostiantyn Rybnikov - -* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects -* :pr:`56`: Added support for Python 3.6 - - ----- - -Version 2.7.2 -============= - -:Released: 2016-11-08 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Added :func:`semver.parse_version_info` to parse a version string to a - version info tuple. - -Bug Fixes ---------- - -* :gh:`37`: Removed trailing zeros from prelease doesn't allow to - parse 0 pre-release version - -* Refine parsing to conform more strictly to SemVer 2.0.0. - - SemVer 2.0.0 specification §9 forbids leading zero on identifiers in - the prerelease version. - - ----- - -Version 2.6.0 -============= - -:Released: 2016-06-08 -:Maintainer: Kostiantyn Rybnikov - -Removals --------- - -* Remove comparison of build component. - - SemVer 2.0.0 specification recommends that build component is - ignored in comparisons. - - ----- - -Version 2.5.0 -============= - -:Released: 2016-05-25 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Support matching 'not equal' with “!=”. - -Changes -------- - -* Made separate builds for tests on Travis CI. - - ----- - -Version 2.4.2 -============= - -:Released: 2016-05-16 -:Maintainer: Kostiantyn Rybnikov - -Changes -------- - -* Migrated README document to reStructuredText format. - -* Used Setuptools for distribution management. - -* Migrated test cases to Py.test. - -* Added configuration for Tox test runner. - - ----- - -Version 2.4.1 -============= - -:Released: 2016-03-04 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* :gh:`23`: Compared build component of a version. - - ----- - -Version 2.4.0 -============= - -:Released: 2016-02-12 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* :gh:`21`: Compared alphanumeric components correctly. - - ----- - -Version 2.3.1 -============= - -:Released: 2016-01-30 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Declared granted license name in distribution metadata. - - ----- - -Version 2.3.0 -============= - -:Released: 2016-01-29 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Added functions to increment prerelease and build components in a - version. - - ----- - -Version 2.2.1 -============= - -:Released: 2015-08-04 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Corrected comparison when any component includes zero. - - ----- - -Version 2.2.0 -============= - -:Released: 2015-06-21 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Add functions to determined minimum and maximum version. - -* Add code examples for recently-added functions. - - ----- - -Version 2.1.2 -============= - -:Released: 2015-05-23 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Restored current README document to distribution manifest. - - ----- - -Version 2.1.1 -============= - -:Released: 2015-05-23 -:Maintainer: Kostiantyn Rybnikov - -Bug Fixes ---------- - -* Removed absent document from distribution manifest. - - ----- - -Version 2.1.0 -============= - -:Released: 2015-05-22 -:Maintainer: Kostiantyn Rybnikov - -Additions ---------- - -* Documented installation instructions. - -* Documented project home page. - -* Added function to format a version string from components. - -* Added functions to increment specific components in a version. - -Changes -------- - -* Migrated README document to Markdown format. - -Bug Fixes ---------- - -* Corrected code examples in README document. - - ----- - -Version 2.0.2 -============= - -:Released: 2015-04-14 -:Maintainer: Konstantine Rybnikov - -Additions ---------- - -* Added configuration for Travis continuous integration. - -* Explicitly declared supported Python versions. - - ----- - -Version 2.0.1 -============= - -:Released: 2014-09-24 -:Maintainer: Konstantine Rybnikov - -Bug Fixes ---------- - -* :gh:`9`: Fixed comparison of equal version strings. - - ----- - -Version 2.0.0 -============= - -:Released: 2014-05-24 -:Maintainer: Konstantine Rybnikov - -Additions ---------- - -* Grant license in this code base under BSD 3-clause license terms. - -Changes -------- - -* Update parser to SemVer standard 2.0.0. - -* Ignore build component for comparison. - - ----- - -Version 0.0.2 -============= - -:Released: 2012-05-10 -:Maintainer: Konstantine Rybnikov - -Changes -------- - -* Use standard library Distutils for distribution management. - - ----- - -Version 0.0.1 -============= - -:Released: 2012-04-28 -:Maintainer: Konstantine Rybnikov - -* Initial release. - - -.. - Local variables: - coding: utf-8 - mode: text - mode: rst - End: - vim: fileencoding=utf-8 filetype=rst : diff --git a/docs/conf.py b/docs/conf.py index 9edfda4d..eab3248d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -122,6 +122,17 @@ def find_version(*file_paths): "pr": ("https://github.com/python-semver/python-semver/pull/%s", "PR #%s"), } +# Link to other projects’ documentation +# See https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html +intersphinx_mapping = { + # Download it from the root with: + # wget -O docs/python-objects.inv https://docs.python.org/3/objects.inv + "python": ("https://docs.python.org/3", (None, "inventories/python-objects.inv")), +} +# Avoid side-effects (namely that documentations local references can +# suddenly resolve to an external location.) +intersphinx_disabled_reftypes = ["*"] + # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/docs/index.rst b/docs/index.rst index 4c9ccff7..1054c225 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,7 +32,7 @@ Semver |version| -- Semantic Versioning :hidden: changelog - changelog-semver2 + changelog-semver3-devel Indices and Tables diff --git a/docs/inventories/python-objects.inv b/docs/inventories/python-objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..6f01e284eb588b00fa34fab71b910eac28980526 GIT binary patch literal 130055 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 literal 0 HcmV?d00001 diff --git a/docs/usage/compare-versions.rst b/docs/usage/compare-versions.rst index cf55eae3..ddd03b68 100644 --- a/docs/usage/compare-versions.rst +++ b/docs/usage/compare-versions.rst @@ -5,7 +5,7 @@ To compare two versions depends on your type: * **Two strings** - Use :func:`semver.compare`:: + Use :func:`semver.compare `:: >>> semver.compare("1.0.0", "2.0.0") -1 diff --git a/src/semver/__init__.py b/src/semver/__init__.py index c6726f2e..433e5d5d 100644 --- a/src/semver/__init__.py +++ b/src/semver/__init__.py @@ -9,8 +9,8 @@ bump_major, bump_minor, bump_patch, - bump_prerelease, compare, + bump_prerelease, finalize_version, format_version, match, diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py index 2c736fff..efbdf439 100644 --- a/src/semver/_deprecated.py +++ b/src/semver/_deprecated.py @@ -16,8 +16,10 @@ def deprecated( func: Optional[F] = None, + *, replace: Optional[str] = None, version: Optional[str] = None, + remove: Optional[str] = None, category: Type[Warning] = DeprecationWarning, ) -> Decorator: """ @@ -34,7 +36,13 @@ def deprecated( """ if func is None: - return partial(deprecated, replace=replace, version=version, category=category) + return partial( + deprecated, + replace=replace, + version=version, + remove=remove, + category=category, + ) @wraps(func) def wrapper(*args, **kwargs) -> Callable[..., F]: @@ -42,7 +50,12 @@ def wrapper(*args, **kwargs) -> Callable[..., F]: if version: msg_list.append("Deprecated since version {v}. ") - msg_list.append("This function will be removed in semver 3.") + + if not remove: + msg_list.append("This function will be removed in semver 3.") + else: + msg_list.append(str(remove)) + if replace: msg_list.append("Use {r!r} instead.") else: @@ -69,6 +82,36 @@ def wrapper(*args, **kwargs) -> Callable[..., F]: return wrapper +@deprecated( + version="3.0.0", + remove="Still under investigation, see #258.", + category=PendingDeprecationWarning, +) +def compare(ver1: str, ver2: str) -> int: + """ + Compare two versions strings. + + .. deprecated:: 3.0.0 + The situation of this function is unclear and it might + disappear in the future. + If possible, use :meth:`semver.version.Version.compare`. + See :gh:`258` for details. + + :param ver1: first version string + :param ver2: second version string + :return: The return value is negative if ver1 < ver2, + zero if ver1 == ver2 and strictly positive if ver1 > ver2 + + >>> semver.compare("1.0.0", "2.0.0") + -1 + >>> semver.compare("2.0.0", "1.0.0") + 1 + >>> semver.compare("2.0.0", "2.0.0") + 0 + """ + return Version.parse(ver1).compare(ver2) + + @deprecated(version="2.10.0") def parse(version): """ @@ -126,28 +169,6 @@ def parse_version_info(version): return Version.parse(version) -@deprecated(version="2.10.0") -def compare(ver1, ver2): - """ - Compare two versions strings. - - :param ver1: version string 1 - :param ver2: version string 2 - :return: The return value is negative if ver1 < ver2, - zero if ver1 == ver2 and strictly positive if ver1 > ver2 - :rtype: int - - >>> semver.compare("1.0.0", "2.0.0") - -1 - >>> semver.compare("2.0.0", "1.0.0") - 1 - >>> semver.compare("2.0.0", "2.0.0") - 0 - """ - v1 = Version.parse(ver1) - return v1.compare(ver2) - - @deprecated(version="2.10.0") def match(version, match_expr): """ @@ -374,10 +395,16 @@ def replace(version, **parts): # CLI -cmd_bump = deprecated(cli.cmd_bump, "semver.cli.cmd_bump", "3.0.0") -cmd_check = deprecated(cli.cmd_check, "semver.cli.cmd_check", "3.0.0") -cmd_compare = deprecated(cli.cmd_compare, "semver.cli.cmd_compare", "3.0.0") -cmd_nextver = deprecated(cli.cmd_nextver, "semver.cli.cmd_nextver", "3.0.0") -createparser = deprecated(cli.createparser, "semver.cli.createparser", "3.0.0") -process = deprecated(cli.process, "semver.cli.process", "3.0.0") -main = deprecated(cli.main, "semver.cli.main", "3.0.0") +cmd_bump = deprecated(cli.cmd_bump, replace="semver.cli.cmd_bump", version="3.0.0") +cmd_check = deprecated(cli.cmd_check, replace="semver.cli.cmd_check", version="3.0.0") +cmd_compare = deprecated( + cli.cmd_compare, replace="semver.cli.cmd_compare", version="3.0.0" +) +cmd_nextver = deprecated( + cli.cmd_nextver, replace="semver.cli.cmd_nextver", version="3.0.0" +) +createparser = deprecated( + cli.createparser, replace="semver.cli.createparser", version="3.0.0" +) +process = deprecated(cli.process, replace="semver.cli.process", version="3.0.0") +main = deprecated(cli.main, replace="semver.cli.main", version="3.0.0") diff --git a/tests/test_deprecated_functions.py b/tests/test_deprecated_functions.py index 0b5123cc..88862689 100644 --- a/tests/test_deprecated_functions.py +++ b/tests/test_deprecated_functions.py @@ -5,7 +5,6 @@ from semver import ( parse, parse_version_info, - compare, match, max_ver, min_ver, @@ -36,7 +35,6 @@ (bump_minor, ("1.2.3",), {}), (bump_patch, ("1.2.3",), {}), (bump_prerelease, ("1.2.3",), {}), - (compare, ("1.2.1", "1.2.2"), {}), (format_version, (3, 4, 5), {}), (finalize_version, ("1.2.3-rc.5",), {}), (match, ("1.0.0", ">=1.0.0"), {}), From d26cc07a757fe3ac831ccbdec238ba66bc478128 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sun, 2 Apr 2023 14:32:27 +0200 Subject: [PATCH 78/86] Build 3.0.0 release of semver --- docs/usage/semver-version.rst | 2 +- src/semver/__about__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage/semver-version.rst b/docs/usage/semver-version.rst index e8cc92b3..b7d5ed9e 100644 --- a/docs/usage/semver-version.rst +++ b/docs/usage/semver-version.rst @@ -4,4 +4,4 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.0-rc.1' + '3.0.0' diff --git a/src/semver/__about__.py b/src/semver/__about__.py index dd671d02..89c6412e 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -16,7 +16,7 @@ """ #: Semver version -__version__ = "3.0.0-rc.1" +__version__ = "3.0.0" #: Original semver author __author__ = "Kostiantyn Rybnikov" From 3215a9931bfc501b41274f716a6966273bc0c287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 3 Apr 2023 16:34:29 +0200 Subject: [PATCH 79/86] Remove incorrect dependencies in pyproject.toml (#405) * Remove redundant wheel dep from pyproject.toml Remove the redundant `wheel` dependency, as it is added by the backend automatically. Listing it explicitly in the documentation was a historical mistake and has been fixed since, see: https://github.com/pypa/setuptools/commit/f7d30a9529378cf69054b5176249e5457aaf640a * Add build into deps (prepare-dist) Co-authored-by: Tom Schraitle --- pyproject.toml | 2 -- tox.ini | 2 -- 2 files changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d288e68e..e611abc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,6 @@ requires = [ # sync with setup.py until we discard non-pep-517/518 "setuptools", "setuptools-scm", - "wheel", - "build", ] build-backend = "setuptools.build_meta" diff --git a/tox.ini b/tox.ini index b71ae78e..fb91a518 100644 --- a/tox.ini +++ b/tox.ini @@ -99,9 +99,7 @@ commands = make -C docs man description = Prepare for TestPyPI basepython = python3 deps = - wheel twine - # PEP 517 build frontend build commands = # Same as python3 -m build From 7625129e911829e359c150b3d155345a05f83140 Mon Sep 17 00:00:00 2001 From: Benjamin K <53038537+treee111@users.noreply.github.com> Date: Wed, 12 Apr 2023 16:32:48 +0200 Subject: [PATCH 80/86] correct typo in function description --- src/semver/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semver/version.py b/src/semver/version.py index cca744a1..e9d0e15b 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -432,7 +432,7 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": This function is taking prereleases into account. The "major", "minor", and "patch" raises the respective parts like the ``bump_*`` functions. The real difference is using the - "preprelease" part. It gives you the next patch version of the + "prerelease" part. It gives you the next patch version of the prerelease, for example: >>> str(semver.parse("0.1.4").next_version("prerelease")) From 100d90b27f0d5ae26d2892a73b2525ba49c850f5 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Sat, 22 Apr 2023 16:08:00 +0200 Subject: [PATCH 81/86] Improve GitHub Action * Set timeout-minutes ti 15min * Add on.push.path on.pull_request.path * Raise Python 3.7 -> 3.8 * Use cache for actions/setup-python@v3 --- .github/workflows/python-testing.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index fb23fcf8..8f32ffd1 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -3,9 +3,21 @@ name: Python on: push: - branches: [ master ] + branches: [ "master", "main" ] + paths: + - 'pyproject.toml' + - '**.py' + - '.github/workflows/python-testing.yml' + pull_request: - branches: [ master ] + branches: [ "master", "main" ] + paths: + - 'pyproject.toml' + - '**.py' + - '.github/workflows/python-testing.yml' + +permissions: + contents: read concurrency: # only cancel in-progress runs of the same workflow @@ -17,6 +29,9 @@ concurrency: jobs: check: runs-on: ubuntu-latest + # Timout of 15min + timeout-minutes: 15 + steps: - uses: actions/checkout@v3 - name: Output env variables @@ -41,7 +56,8 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: - python-version: 3.7 + python-version: 3.8 + cache: 'pip' - name: Install dependencies run: | python3 -m pip install --upgrade pip setuptools setuptools-scm @@ -70,6 +86,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + cache: 'pip' - name: Install dependencies run: | python3 -m pip install --upgrade pip From 39bf287af9106986404762d91c20a27ca97b2219 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 27 Apr 2023 08:25:37 +0200 Subject: [PATCH 82/86] Add CITATION.cff for citation Benefits: * Users of our software can easily cite it using the metadata from CITATION.cff * GitHub shows the citation information in the sidebar * Browser plugins can import the correct reference Sources: * https://citation-file-format.github.io/ * https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-citation-files --- CITATION.cff | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000..708f4d0a --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,104 @@ +# This CITATION.cff file was generated with cffinit: +# https://bit.ly/cffinit + +cff-version: 1.2.0 +title: python-semver +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software + +authors: + - given-names: Kostiantyn + family-names: Rybnikov + email: k-bx@k-bx.com + - given-names: Tom + family-names: Schraitle + email: tom_schr@web.de + - given-names: Sebastian + family-names: Celles + email: s.celles@gmail.com + - name: "The python-semver software team" + +identifiers: + - type: url + value: 'https://github.com/python-semver/python-semver' + description: GitHub python-semver/python-semver +url: 'https://python-semver.readthedocs.io' +repository-code: 'https://github.com/python-semver/python-semver' +repository-artifact: 'https://pypi.org/project/semver/' + +abstract: >- + A Python module for semantic versioning. Simplifies + comparing versions. This modules follows the + MAJOR.MINOR.PATCH style. + +keywords: + - Python + - Python module + - semver + - versioning + - semantic versioning + - semver-format + - semver-tag + - versions + +references: + - authors: + - family-names: Preston-Werner + given-names: Tom + - name: "The semver team" + title: 'Semantic Versioning 2.0.0' + url: 'https://semver.org' + repository-code: 'https://github.com/semver/semver' + type: standard + version: 2.0.0 + languages: + - ar + - bg + - ca + - cs + - da + - de + - el + - en + - es + - fa + - fr + - he + - hin + - hr + - hu + - hy + - id + - it + - ja + - ka + - kab + - ko + - nl + - pl + - pt + - ro + - ru + - sk + - sl + - sr + - sv + - tr + - uk + - vi + - zh + abstract: >- + Given a version number MAJOR.MINOR.PATCH, increment the: + + 1. MAJOR version when you make incompatible API changes + 2. MINOR version when you add functionality in a backwards compatible manner + 3. PATCH version when you make backwards compatible bug fixes + + Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. + +license: BSD-3-Clause +commit: 3a7680dc436211227c0aeae84c9b45e0b3345b8f +version: 3.0.0 +date-released: '2023-04-02' From 2a8331f3d4f4fe824360487071599d4dc0aeab09 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Thu, 27 Apr 2023 09:57:53 +0200 Subject: [PATCH 83/86] Mention CITATION.cff in release procedure --- release-procedure.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-procedure.md b/release-procedure.md index d6c1701e..f7b8d7bb 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -20,6 +20,7 @@ create a new release. * `setup.cfg` * `tox.ini` * `.git/workflows/pythonpackage.yml` + * `CITATION.cff` 1. Verify that the version has been updated and follow : From 17bc2571c4f3c7faf85d5b024d5f9117d555d49b Mon Sep 17 00:00:00 2001 From: Dhaval Soneji Date: Wed, 14 Jun 2023 11:35:35 +0100 Subject: [PATCH 84/86] Fix #410 Export all names in __all__ variable * Export all semver names in `__all__` variable. * Fix linting issues --- changelog.d/410.bugfix.rst | 1 + src/semver/__init__.py | 35 ++++++++++++++++++++++++++++++++++- src/semver/cli.py | 2 +- src/semver/version.py | 4 ++-- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 changelog.d/410.bugfix.rst diff --git a/changelog.d/410.bugfix.rst b/changelog.d/410.bugfix.rst new file mode 100644 index 00000000..1b2dcf74 --- /dev/null +++ b/changelog.d/410.bugfix.rst @@ -0,0 +1 @@ +Export functions properly using `__all__` in `__init__.py`. \ No newline at end of file diff --git a/src/semver/__init__.py b/src/semver/__init__.py index 433e5d5d..19c88f78 100644 --- a/src/semver/__init__.py +++ b/src/semver/__init__.py @@ -1,5 +1,5 @@ """ -semver package major release 3. +Semver package major release 3. A Python module for semantic versioning. Simplifies comparing versions. """ @@ -37,3 +37,36 @@ __maintainer_email__, SEMVER_SPEC_VERSION, ) + +__all__ = [ + "bump_build", + "bump_major", + "bump_minor", + "bump_patch", + "compare", + "bump_prerelease", + "finalize_version", + "format_version", + "match", + "max_ver", + "min_ver", + "parse", + "parse_version_info", + "replace", + "cmd_bump", + "cmd_compare", + "cmd_nextver", + "cmd_check", + "createparser", + "process", + "main", + "Version", + "VersionInfo", + "__version__", + "__author__", + "__maintainer__", + "__author_email__", + "__description__", + "__maintainer_email__", + "SEMVER_SPEC_VERSION", +] diff --git a/src/semver/cli.py b/src/semver/cli.py index b2751429..43e101e1 100644 --- a/src/semver/cli.py +++ b/src/semver/cli.py @@ -61,7 +61,7 @@ def cmd_check(args: argparse.Namespace) -> None: def cmd_compare(args: argparse.Namespace) -> str: """ - Subcommand: Compare two versions + Subcommand: Compare two versions. Synopsis: compare diff --git a/src/semver/version.py b/src/semver/version.py index e9d0e15b..af437296 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -64,8 +64,8 @@ class Version: See specification at https://semver.org. :param major: version when you make incompatible API changes. - :param minor: version when you add functionality in - a backwards-compatible manner. + :param minor: version when you add functionality in a backwards- + compatible manner. :param patch: version when you make backwards-compatible bug fixes. :param prerelease: an optional prerelease string :param build: an optional build string From 90c34840b1932d1dca5a975ac366be11e4a4f214 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 14 Jun 2023 12:48:17 +0200 Subject: [PATCH 85/86] Configure docformatter (#412) * Add config options to pyproject.toml * Call docformatter without any options in tox.ini as they are set in pyproject.toml now --- pyproject.toml | 9 +++++++++ src/semver/version.py | 3 +-- tox.ini | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e611abc4..6b12deb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,15 @@ target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] # diff = true +[tool.docformatter] +wrap-summaries = 80 +close-quotes-on-newline = true +# make-summary-multi-line = true +black = true +pre-summary-newline = true +recursive = true + + [tool.towncrier] package = "semver" package_dir = "src" diff --git a/src/semver/version.py b/src/semver/version.py index af437296..d2f336c0 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -64,8 +64,7 @@ class Version: See specification at https://semver.org. :param major: version when you make incompatible API changes. - :param minor: version when you add functionality in a backwards- - compatible manner. + :param minor: version when you add functionality in a backwards-compatible manner. :param patch: version when you make backwards-compatible bug fixes. :param prerelease: an optional prerelease string :param build: an optional build string diff --git a/tox.ini b/tox.ini index fb91a518..94598c4c 100644 --- a/tox.ini +++ b/tox.ini @@ -56,7 +56,7 @@ commands = mypy {posargs:--ignore-missing-imports --check-untyped-defs src} description = Check for PEP257 compatible docstrings basepython = python3 deps = docformatter -commands = docformatter --check --diff {posargs:--pre-summary-newline -r src} +commands = docformatter --check --diff {posargs:src} [testenv:checks] From c2680608bc9b080a1f34161a36971e8331a19056 Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 14 Jun 2023 13:41:24 +0200 Subject: [PATCH 86/86] Prepare version 3.0.1 (#413) * Raise version * Ignore exit code from docformatter in checks `tox.ini` * Correct typo in release-procedure.md * Update CHANGELOG.rst --- CHANGELOG.rst | 17 +++++++++++++++++ changelog.d/410.bugfix.rst | 1 - docs/usage/semver-version.rst | 2 +- release-procedure.md | 2 +- src/semver/__about__.py | 2 +- tox.ini | 5 +++-- 6 files changed, 23 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/410.bugfix.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 11b08e06..a773e1f3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,23 @@ This section covers the changes between major version 2 and version 3. .. towncrier release notes start +Version 3.0.1 +============= + +:Released: 2023-06-14 +:Maintainer: Tom Schraitle + + +Bug Fixes +--------- + +* :gh:`410`: Export functions properly using ``__all__`` in ``__init__.py``. + + + +---- + + Version 3.0.0 ============= diff --git a/changelog.d/410.bugfix.rst b/changelog.d/410.bugfix.rst deleted file mode 100644 index 1b2dcf74..00000000 --- a/changelog.d/410.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Export functions properly using `__all__` in `__init__.py`. \ No newline at end of file diff --git a/docs/usage/semver-version.rst b/docs/usage/semver-version.rst index b7d5ed9e..0f2e2411 100644 --- a/docs/usage/semver-version.rst +++ b/docs/usage/semver-version.rst @@ -4,4 +4,4 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.0' + '3.0.1' diff --git a/release-procedure.md b/release-procedure.md index f7b8d7bb..7476c79a 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -26,7 +26,7 @@ create a new release. : * `src/semver/__about__.py` - * `docs/usage.rst` + * `docs/usage/semver-version.rst` 1. Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/python-semver/python-semver/blob/master/CONTRIBUTORS). diff --git a/src/semver/__about__.py b/src/semver/__about__.py index 89c6412e..2eff8c86 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -16,7 +16,7 @@ """ #: Semver version -__version__ = "3.0.0" +__version__ = "3.0.1" #: Original semver author __author__ = "Kostiantyn Rybnikov" diff --git a/tox.ini b/tox.ini index 94598c4c..b18aa1f7 100644 --- a/tox.ini +++ b/tox.ini @@ -56,7 +56,8 @@ commands = mypy {posargs:--ignore-missing-imports --check-untyped-defs src} description = Check for PEP257 compatible docstrings basepython = python3 deps = docformatter -commands = docformatter --check --diff {posargs:src} +commands = + docformatter --check --diff {posargs:src} [testenv:checks] @@ -68,10 +69,10 @@ deps = {[testenv:mypy]deps} {[testenv:docstrings]deps} commands = + - {[testenv:docstrings]commands} {[testenv:black]commands} {[testenv:flake8]commands} {[testenv:mypy]commands} - {[testenv:docstrings]commands} [testenv:docs]