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/.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/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. 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 + 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 diff --git a/.github/workflows/black-formatting.yml b/.github/workflows/black-formatting.yml deleted file mode 100644 index 25b34f21..00000000 --- a/.github/workflows/black-formatting.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Black Formatting - -on: [pull_request] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - name: Output env variables - run: | - echo "GITHUB_WORKFLOW=${GITHUB_WORKFLOW}" - echo "GITHUB_ACTION=$GITHUB_ACTION" - echo "GITHUB_ACTIONS=$GITHUB_ACTIONS" - echo "GITHUB_ACTOR=$GITHUB_ACTOR" - echo "GITHUB_REPOSITORY=$GITHUB_REPOSITORY" - echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME" - echo "GITHUB_EVENT_PATH=$GITHUB_EVENT_PATH" - echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" - echo "GITHUB_SHA=$GITHUB_SHA" - echo "GITHUB_REF=$GITHUB_REF" - echo "GITHUB_HEAD_REF=$GITHUB_HEAD_REF" - echo "GITHUB_BASE_REF=$GITHUB_BASE_REF" - echo "::debug::---Start content of file $GITHUB_EVENT_PATH" - cat $GITHUB_EVENT_PATH - echo "\n" - echo "::debug::---end" - - - name: Set up Python 3.7 - uses: actions/setup-python@v1 - with: - python-version: 3.7 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip black - - - name: Run black - id: black - run: | - black --check . - echo "::set-output name=rc::$?" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..f310a7e3 --- /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: '50 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@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + 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. + # 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@v2 + + # ℹ️ 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@v2 diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml new file mode 100644 index 00000000..8f32ffd1 --- /dev/null +++ b/.github/workflows/python-testing.yml @@ -0,0 +1,96 @@ +--- +name: Python + +on: + push: + branches: [ "master", "main" ] + paths: + - 'pyproject.toml' + - '**.py' + - '.github/workflows/python-testing.yml' + + pull_request: + 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 + group: ${{ github.workflow }}-${{ github.ref }} + # ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + + +jobs: + check: + runs-on: ubuntu-latest + # Timout of 15min + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v3 + - 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" + echo "GITHUB_ACTOR=$GITHUB_ACTOR" + echo "GITHUB_REPOSITORY=$GITHUB_REPOSITORY" + echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME" + echo "GITHUB_EVENT_PATH=$GITHUB_EVENT_PATH" + echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" + echo "GITHUB_SHA=$GITHUB_SHA" + echo "GITHUB_REF=$GITHUB_REF" + echo "GITHUB_HEAD_REF=$GITHUB_HEAD_REF" + echo "GITHUB_BASE_REF=$GITHUB_BASE_REF" + echo "::debug::---Start content of file $GITHUB_EVENT_PATH" + cat $GITHUB_EVENT_PATH + echo "\n" + echo "::debug::---end" + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: 3.8 + cache: 'pip' + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip setuptools setuptools-scm + 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.7", + "3.8", + "3.9", + "3.10", + "3.11", + # "3.12-dev" + ] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: | + tox diff --git a/.gitignore b/.gitignore index 2ef76af8..dead3352 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,240 @@ 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 + +# -------- + + +# Ignore files in the project's root: +/*.patch +/*.diff +/*.py +# but not this file: +!/setup.py + +docs/_api +!docs/_api/semver.__about__.rst + +# For node +node_modules/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 54165f6e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,36 +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 - -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.6" - env: TOXENV=py36 - - - python: "3.7" - dist: xenial - env: TOXENV=py37 - - - python: "pypy" - env: TOXENV=pypy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 10a8d20f..a773e1f3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,615 +2,305 @@ Change Log ########## +Changes for the upcoming release can be found in +the `"changelog.d" directory `_ +in our repository. -All notable changes to this code base will be documented in this file, -in every released version. +This section covers the changes between major version 2 and version 3. -Version 2.13.0 -============== - -:Released: 2020-10-20 -:Maintainer: Tom Schraitle - -Features --------- +.. + Do *NOT* add changelog entries here! -* :pr:`287`: Document how to create subclass from ``VersionInfo`` + 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. -Bug Fixes ---------- +.. towncrier release notes start -* :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. - - -Version 2.12.0 -============== +Version 3.0.1 +============= -:Released: 2020-10-19 +:Released: 2023-06-14 :Maintainer: Tom Schraitle -Features --------- - -n/a - Bug Fixes --------- -* :gh:`291` (:pr:`292`): Disallow negative numbers of - major, minor, and patch for ``semver.VersionInfo`` +* :gh:`410`: Export functions properly using ``__all__`` in ``__init__.py``. -Additions ---------- - -n/a - - -Deprecations ------------- -n/a +---- -Version 2.11.0 -============== +Version 3.0.0 +============= -:Released: 2020-10-17 +:Released: 2023-04-02 :Maintainer: Tom Schraitle -Features --------- - -n/a - 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 +* :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`. -Additions ---------- +* :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`. -n/a +* :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: -Deprecations ------------- + * 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 -n/a +* :pr:`393`: Fix command :command:`python -m semver` to avoid the error "invalid choice" +* :pr:`396`: Calling :meth:`~semver.version.Version.parse` on a derived class will show correct type of derived class. -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 - - -Additions ---------- - -n/a Deprecations ------------ -* :gh:`160` (:pr:`264`): - * :func:`semver.max_ver` - * :func:`semver.min_ver` +* :gh:`169`: Deprecate CLI functions not imported from ``semver.cli``. +* :gh:`234`: In :file:`setup.py` simplified file and remove + ``Tox`` and ``Clean`` classes -Version 2.10.1 -============== +* :gh:`284`: Deprecate the use of :meth:`~Version.isvalid`. -:Released: 2020-05-13 -:Maintainer: Tom Schraitle + Rename :meth:`~semver.version.Version.isvalid` + to :meth:`~semver.version.Version.is_valid` + for consistency reasons with :meth:`~semver.version.Version.is_compatible`. -Features --------- +* :pr:`290`: For semver 3.0.0-alpha0 deprecated: -* :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 + * 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:`372`: Deprecate support for Python 3.6. -Bug Fixes ---------- + 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. -* :gh:`251` (:pr:`254`): Fixed return type of ``semver.VersionInfo.next_version`` - to always return a ``VersionInfo`` instance. + Although the `poll `_ + 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`). -Version 2.10.0 -============== + 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. -: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. +* :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 -Bug Fixes ---------- +* :gh:`213`: Add typing information -* :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. +* :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 -Additions ---------- +* :pr:`359`: Add optional parameter ``optional_minor_and_patch`` in :meth:`~semver.version.Version.parse` to allow optional + minor and patch parts. -* :pr:`228`: Added better doctest integration +* :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. + For more information, see :ref:`build-semver`. -Deprecations ------------- -* :gh:`225` (:pr:`229`): Output a DeprecationWarning for the following functions: +* :gh:`365`: Improve :file:`pyproject.toml`. - - ``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()``) + * 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 - These deprecated functions will be removed in semver 3. +Improved Documentation +---------------------- -Version 2.9.1 -============= -:Released: 2020-02-16 -:Maintainer: Tom Schraitle +* :gh:`276`: Document how to create a sublass from :class:`~semver.version.VersionInfo` class -Features --------- +* :gh:`284`: Document deprecation of :meth:`~semver.version.Version.isvalid`. -* :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 +* :pr:`290`: Several improvements in the documentation: -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 ---------- + * 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:`98` (:pr:`99`): Set prerelease and build to None by default -* :gh:`96` (:pr:`97`): Made VersionInfo immutable +* :gh:`304`: Several improvements in documentation: + * Reorganize API documentation. + * Add migration chapter from semver2 to semver3. + * Distinguish between changlog for version 2 and 3 -Version 2.8.0 -============= -:Released: 2018-05-16 -:Maintainer: Sébastien Celles +* :gh:`305`: Add note about :class:`~semver.version.Version` rename. +* :gh:`312`: Rework "Usage" section. -Changes -------- + * 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:`82` (:pr:`83`): Renamed :file:`test.py` to :file:`test_semver.py` so - py.test can autodiscover test file +* :gh:`315`: Improve release procedure text -Additions ---------- +* :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:`79` (:pr:`81`, :pr:`84`): Defined and improve a release procedure file -* :gh:`72`, :gh:`73` (:pr:`75`): Implemented :func:`__str__` and :func:`__hash__` +* :gh:`340`: Describe how to get version from a file -Removals --------- +* :gh:`343`: Describe combining Pydantic with semver in the "Advanced topic" + section. -* :gh:`76` (:pr:`80`): Removed Python 2.6 compatibility +* :gh:`350`: Restructure usage section. Create subdirectory "usage/" and splitted + all section into different files. +* :gh:`351`: Introduce new topics for: -Version 2.7.9 -============= + * "Migration to semver3" + * "Advanced topics" -:Released: 2017-09-23 -:Maintainer: Kostiantyn Rybnikov +* :pr:`392`: Fix the example in the documentation for combining semver and pydantic. -Additions ---------- +Trivial/Internal Changes +------------------------ -* :gh:`65` (:pr:`66`): Added :func:`semver.finalize_version` function. +* :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. -Version 2.7.8 -============= + Increase coverage to 100% for all non-deprecated APIs -:Released: 2017-08-25 -:Maintainer: Kostiantyn Rybnikov +* :pr:`290`: Add supported Python versions to :command:`black`. -* :gh:`62`: Support custom default names for pre and build +* :gh:`304`: Support PEP-561 :file:`py.typed`. + According to the mentioned PEP: -Version 2.7.7 -============= + "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." -:Released: 2017-05-25 -:Maintainer: Kostiantyn Rybnikov + Add package_data to :file:`setup.cfg` to include this marker in dist + and whl file. -* :gh:`54` (:pr:`55`): Added comparision between VersionInfo objects -* :pr:`56`: Added support for Python 3.6 +* :gh:`309`: Some (private) functions from the :mod:`semver.version` + module has been changed. + The following functions got renamed: -Version 2.7.2 -============= + * 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. -:Released: 2016-11-08 -:Maintainer: Kostiantyn Rybnikov + The following functions got integrated into the + :class:`~semver.version.Version` class: -Additions ---------- + * function :func:`semver.version._nat_cmd` as a classmethod + * function :func:`semver.version.ensure_str` -* Added :func:`semver.parse_version_info` to parse a version string to a - version info tuple. +* :gh:`313`: Correct :file:`tox.ini` for ``changelog`` entry to skip + installation for semver. This should speed up the execution + of towncrier. -Bug Fixes ---------- +* :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. -* :gh:`37`: Removed trailing zeros from prelease doesn't allow to - parse 0 pre-release version + 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". -* Refine parsing to conform more strictly to SemVer 2.0.0. + .. _NotImplemented: https://docs.python.org/3/library/constants.html#NotImplemented - SemVer 2.0.0 specification §9 forbids leading zero on identifiers in - the prerelease version. +* :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. -Version 2.6.0 -============= +* :gh:`347`: Support Python 3.10 in GitHub Action and other config files. -:Released: 2016-06-08 -:Maintainer: Kostiantyn Rybnikov +* :gh:`378`: Fix some typos in Towncrier configuration -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 -============= +* :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 -:Released: 2016-05-16 -:Maintainer: Kostiantyn Rybnikov +* :pr:`389`: Add public class variable :data:`Version.NAMES `. -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 -============= + This class variable contains a tuple of strings that contains the names of + all attributes of a Version (like ``"major"``, ``"minor"`` etc). -:Released: 2012-04-28 -:Maintainer: Konstantine Rybnikov + In cases we need to have dynamical values, this makes it easier to iterate. -* Initial release. .. 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' diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..ce598e01 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,58 @@ +.. _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 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. + + +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. + + +More topics +----------- + +* `Running the Test Suite `_ +* `Documenting semver `_ +* `Adding a Changelog Entry `_ +* `Preparing the Release `_ +* `Finish the Release `_ + + +.. _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.rtfd.io/en/latest/docstrings.html +.. _tox: https://tox.rtfd.org/ +.. _gh_discussions: https://github.com/python-semver/python-semver/discussions diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9e63f4d4..0d90ab3a 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,35 +14,45 @@ Thank you to everyone whose work has made this possible. Primary maintainers =================== -* Kostiantyn Rybnikov +* Tom Schraitle * Sébastien Celles +Old maintainer: -Significant contributors -======================== +* Kostiantyn Rybnikov -* Alexander Puzynia -* Alexander Shorin -* Anton Talevnin -* Ben Finney + +List of Contributors +==================== + +(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 -* Tom Schraitle +* 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/MANIFEST.in b/MANIFEST.in index 80257f1f..e37851c9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,8 @@ include *.rst include *.txt -include test_*.py +include tests/test_*.py -exclude .travis.yml prune docs/_build recursive-exclude .github * -global-exclude *.py[cod] __pycache__ *.so *.dylib +global-exclude __pycache__ diff --git a/README.rst b/README.rst index 0a1fe664..ede10a18 100644 --- a/README.rst +++ b/README.rst @@ -5,27 +5,28 @@ Quickstart A Python module for `semantic versioning`_. Simplifies comparing versions. -|build-status| |python-support| |downloads| |license| |docs| |black| +|GHAction| |python-support| |downloads| |license| |docs| |black| +|openissues| |GHDiscussion| .. 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.7 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.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`` +.. _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, @@ -41,11 +42,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 @@ -58,21 +59,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 @@ -94,9 +95,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 @@ -109,7 +107,18 @@ 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 +.. |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 +.. |GHDiscussion| image:: https://shields.io/badge/GitHub-%20Discussions-green?logo=github + :target: https://github.com/python-semver/python-semver/discussions + :alt: GitHub Discussion 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/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/conftest.py b/conftest.py deleted file mode 100644 index e6a1f048..00000000 --- a/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest -import semver -import sys - -sys.path.insert(0, "docs") - -from coerce import coerce # noqa:E402 -from semverwithvprefix import SemVerWithVPrefix - - -@pytest.fixture(autouse=True) -def add_semver(doctest_namespace): - doctest_namespace["semver"] = semver - doctest_namespace["coerce"] = coerce - doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 00000000..81a5906f --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,83 @@ +/* +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; +} + +.sphinxsidebarwrapper .caption { + margin-top: 1em; + margin-bottom: -0.75em; +} + +.section h1 { + font-weight: 700; +} + +.py.class { + margin-top: 1.5em; +} + +.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/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/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/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000..7a114d41 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,49 @@ +{# + 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/coerce.py b/docs/advanced/coerce.py similarity index 77% rename from docs/coerce.py rename to docs/advanced/coerce.py index 3e5eb21b..7da20315 100644 --- a/docs/coerce.py +++ b/docs/advanced/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,9 +17,9 @@ ) -def coerce(version): +def coerce(version: str) -> Tuple[Version, Optional[str]]: """ - 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 +27,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 +39,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 = Version(**ver) rest = match.string[match.end() :] # noqa:E203 return ver, rest diff --git a/docs/advanced/combine-pydantic-and-semver.rst b/docs/advanced/combine-pydantic-and-semver.rst new file mode 100644 index 00000000..a00c2cff --- /dev/null +++ b/docs/advanced/combine-pydantic-and-semver.rst @@ -0,0 +1,57 @@ +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 _parse(cls, version): + return cls.parse(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/convert-pypi-to-semver.rst b/docs/advanced/convert-pypi-to-semver.rst new file mode 100644 index 00000000..04737d29 --- /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 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]) + return 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/create-subclasses-from-version.rst b/docs/advanced/create-subclasses-from-version.rst new file mode 100644 index 00000000..7e99e217 --- /dev/null +++ b/docs/advanced/create-subclasses-from-version.rst @@ -0,0 +1,34 @@ +.. _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. Additionally, you can pass "incomplete" +version strings like ``v2.3``: + +.. 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") + >>> 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/advanced/deal-with-invalid-versions.rst b/docs/advanced/deal-with-invalid-versions.rst new file mode 100644 index 00000000..ee5e5704 --- /dev/null +++ b/docs/advanced/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/advanced/display-deprecation-warnings.rst b/docs/advanced/display-deprecation-warnings.rst new file mode 100644 index 00000000..825bbe76 --- /dev/null +++ b/docs/advanced/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/advanced/index.rst b/docs/advanced/index.rst new file mode 100644 index 00000000..47c23b9d --- /dev/null +++ b/docs/advanced/index.rst @@ -0,0 +1,13 @@ +Advanced topics +=============== + + +.. toctree:: + :maxdepth: 1 + + deal-with-invalid-versions + create-subclasses-from-version + display-deprecation-warnings + combine-pydantic-and-semver + convert-pypi-to-semver + version-from-file diff --git a/docs/advanced/semverwithvprefix.py b/docs/advanced/semverwithvprefix.py new file mode 100644 index 00000000..7e411d35 --- /dev/null +++ b/docs/advanced/semverwithvprefix.py @@ -0,0 +1,27 @@ +from semver import Version + + +class SemVerWithVPrefix(Version): + """ + A subclass of Version which allows a "v" prefix + """ + + @classmethod + def parse(cls, version: str) -> "SemVerWithVPrefix": + """ + Parse version string to a Version instance. + + :param version: version string with "v" or "V" prefix + :raises ValueError: when version does not start with "v" or "V" + :return: a new instance + """ + if not version[0] in ("v", "V"): + raise ValueError( + f"{version!r}: not a valid semantic version tag. " + "Must start with 'v' or 'V'" + ) + return super().parse(version[1:], optional_minor_and_patch=True) + + def __str__(self) -> str: + # Reconstruct the tag + return "v" + super().__str__() diff --git a/docs/advanced/version-from-file.rst b/docs/advanced/version-from-file.rst new file mode 100644 index 00000000..b49ff36b --- /dev/null +++ b/docs/advanced/version-from-file.rst @@ -0,0 +1,24 @@ +.. _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 + + import os + from typing import Union + from semver.version import Version + + def get_version(path: Union[str, os.PathLike]) -> semver.Version: + """ + 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. + """ + version = open(path,"r").read().strip() + return Version.parse(version) diff --git a/docs/api.rst b/docs/api.rst index 0003fefc..f545ebc5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,8 +1,86 @@ .. _api: -API -=== +API Reference +============= -.. automodule:: semver +.. currentmodule:: semver + + +Metadata :mod:`semver.__about__` +-------------------------------- + +.. automodule:: semver.__about__ + + +Deprecated Functions in :mod:`semver._deprecated` +------------------------------------------------- + +.. automodule:: semver._deprecated + +.. autofunction:: semver._deprecated.compare + +.. 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.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` +----------------------------- + +.. 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 + +.. autoclass:: semver.version.VersionInfo + +.. autoclass:: semver.version.Version :members: - :undoc-members: + :special-members: __iter__, __eq__, __ne__, __lt__, __le__, __gt__, __ge__, __getitem__, __hash__, __repr__, __str__ diff --git a/docs/build-semver.rst b/docs/build-semver.rst new file mode 100644 index 00000000..c938a1ee --- /dev/null +++ b/docs/build-semver.rst @@ -0,0 +1,61 @@ +.. _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`. +As Python 3.6 got deprecated, this project does support from now on only +:file:`pyproject.toml`. + + +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/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/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/conf.py b/docs/conf.py index a07c94a8..eab3248d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,12 +16,39 @@ # 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 +from datetime import date import os +import re import sys -sys.path.insert(0, os.path.abspath("..")) +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): + """ + 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.") -from semver import __version__ # noqa: E402 # -- General configuration ------------------------------------------------ @@ -34,11 +61,17 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", "sphinx.ext.intersphinx", - "sphinx.ext.napoleon", "sphinx.ext.extlinks", ] +# Autodoc configuration +autoclass_content = "class" +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"] @@ -52,7 +85,7 @@ # 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 @@ -60,16 +93,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. @@ -85,47 +118,95 @@ # 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"), } +# 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 # 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": "'Roboto',sans-serif", + "head_font_family": "'Roboto Slab',serif", + "code_font_family": "'Roboto Mono',monospace", + "font_size": "1.20rem", +} -# 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/custom.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/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 3f5e9b6d..00000000 --- a/docs/development.rst +++ /dev/null @@ -1,232 +0,0 @@ -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. - - -Fixing Bugs and Implementing New Features ------------------------------------------ - -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. - -* 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 ------------------- - -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. Commit your work. - -#. Write test cases and run the 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. - -#. Wait for feedback. If you receive any comments, address these. - -#. After your pull request got accepted, delete your branch. - -#. Use the ``clean`` command to remove build and test files and folders:: - - $ python setup.py clean - - -.. _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. - - -.. _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/index.rst b/docs/index.rst index 4cc5a966..1054c225 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,38 @@ Semver |version| -- Semantic Versioning ======================================= +.. include:: readme.rst + .. toctree:: :maxdepth: 2 :caption: Contents + :hidden: + :numbered: - readme + build-semver install - usage - pysemver - development + usage/index + migration/index + advanced/index + contribute/index api + +.. toctree:: + :maxdepth: 2 + :caption: CLI + :hidden: + + pysemver + + +.. toctree:: + :maxdepth: 1 + :caption: Development + :hidden: + changelog + changelog-semver3-devel Indices and Tables diff --git a/docs/install.rst b/docs/install.rst index 7086fc5d..5404882f 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,35 +13,32 @@ 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. -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:: -Pip ---- + semver>=3,<4 -For Python 2: - -.. code-block:: bash +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 install semver - -For Python 3: +Pip +--- .. code-block:: bash 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.10.0 + pip3 install git+https://github.com/python-semver/python-semver.git@3.0.0 Linux Distributions @@ -124,4 +121,4 @@ Ubuntu $ sudo apt-get install python3-semver -.. _semantic versioning: http://semver.org/ +.. _semantic versioning: https://semver.org/ diff --git a/docs/inventories/python-objects.inv b/docs/inventories/python-objects.inv new file mode 100644 index 00000000..6f01e284 Binary files /dev/null and b/docs/inventories/python-objects.inv differ 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/migration/migratetosemver3.rst b/docs/migration/migratetosemver3.rst new file mode 100644 index 00000000..e8b5c9ab --- /dev/null +++ b/docs/migration/migratetosemver3.rst @@ -0,0 +1,51 @@ +.. _semver2-to-3: + +Migrating from semver2 to semver3 +================================= + +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 +is inevitable. + +For a more detailed overview of all the changes, refer +to our :ref:`change-log`. + + +Use Version instead of VersionInfo +---------------------------------- + +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 and will be removed +in future versions. + +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 need such functions, like :meth:`~semver.cli.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:`~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 new file mode 100644 index 00000000..ebe8c354 --- /dev/null +++ b/docs/migration/replace-deprecated-functions.rst @@ -0,0 +1,115 @@ +.. _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.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:`~semver.version.Version` + class. + For example, the function :func:`semver.bump_major` is replaced by + :meth:`~semver.version.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.Version.isvalid` + + Replace it with :meth:`semver.version.Version.is_valid`: + + +* :func:`semver.finalize_version` + + Replace it with :func:`semver.version.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, ...])`` and a ``key``: + + .. code-block:: python + + >>> s1 = semver.max_ver("1.2.3", "1.2.4") + >>> s2 = max("1.2.3", "1.2.4", key=Version.parse) + >>> 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 = min("1.2.3", "1.2.4", key=Version.parse) + >>> s1 == s2 + True + +* :func:`semver.parse` + + Replace it with :meth:`semver.version.Version.parse` and call + :meth:`semver.version.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 :meth:`semver.version.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 :meth:`semver.version.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/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/requirements.txt b/docs/requirements.txt index 28467ce6..1186b4c3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ # requirements file for documentation -sphinx -sphinx_rtd_theme +# sphinx sphinx-argparse +sphinx-autodoc-typehints diff --git a/docs/semverwithvprefix.py b/docs/semverwithvprefix.py deleted file mode 100644 index 13298d5f..00000000 --- a/docs/semverwithvprefix.py +++ /dev/null @@ -1,31 +0,0 @@ -from semver import VersionInfo - - -class SemVerWithVPrefix(VersionInfo): - """ - A subclass of VersionInfo which allows a "v" prefix - """ - - @classmethod - def parse(cls, version): - """ - Parse version string to a VersionInfo 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( - "{v!r}: not a valid semantic version tag. Must start with 'v' or 'V'".format( - v=version - ) - ) - self = super(SemVerWithVPrefix, cls).parse(version[1:]) - return self - - def __str__(self): - # Reconstruct the tag - return "v" + super(SemVerWithVPrefix, self).__str__() diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index cda55670..00000000 --- a/docs/usage.rst +++ /dev/null @@ -1,789 +0,0 @@ -Using semver -============ - -The ``semver`` module can store a version in the :class:`semver.VersionInfo` 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. - - -Knowing 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' - - -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` - class. This is the preferred method when using semver. - -* 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`. - - -.. warning:: **Deprecation Warning** - - Module level functions are marked as *deprecated* in version 2.10.0 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.VersionInfo` 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) - -* From a byte string:: - - >>> semver.VersionInfo.parse(b"2.3.4") - VersionInfo(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') - - Keep in mind, the ``major``, ``minor``, ``patch`` parts has to - be positive. - - >>> semver.VersionInfo(-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. - Only the keys ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` - are allowed. - -* From a tuple:: - - >>> t = (3, 5, 6) - >>> semver.VersionInfo(*t) - VersionInfo(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) - -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. - -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 ``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. - - -* 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') - -* With :func:`semver.VersionInfo.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') - -* 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.VersionInfo.isvalid`: - -.. code-block:: python - - >>> semver.VersionInfo.isvalid("1.0.0") - True - >>> semver.VersionInfo.isvalid("invalid") - False - - -.. _sec.properties.parts: - -Accessing Parts of a Version Through Names ------------------------------------------- - -The :class:`semver.VersionInfo` 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.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 an attribute. -If you do, you get an ``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.VersionInfo` instance:: - - >>> for item in semver.VersionInfo.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")) - [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:`VersionInfo ` 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[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 :class:`IndexError` exception: - -.. code-block:: python - - >>> ver = semver.VersionInfo.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:`semver.VersionInfo.replace` or :func:`semver.replace`: - -* From a :class:`semver.VersionInfo` instance:: - - >>> version = semver.VersionInfo.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') - -* 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.VersionInfo.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 VersionInfo instance into Different Types ------------------------------------------------------- - -Sometimes it is needed to convert a :class:`semver.VersionInfo` instance into -a different type. For example, for displaying or to access all parts. - -It is possible to convert a :class:`semver.VersionInfo` instance: - -* Into a string with the builtin function :func:`str`:: - - >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4")) - '3.4.5-pre.2+build.4' - -* Into a dictionary with :func:`semver.VersionInfo.to_dict`:: - - >>> v = semver.VersionInfo(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`:: - - >>> v = semver.VersionInfo(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:`semver.VersionInfo.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. - Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_patch`: raises the patch part. Set ``prerelease`` and - ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_prerelease`: raises the prerelease part and set - ``build`` to ``None``. -* :func:`semver.VersionInfo.bump_build`: raises the build part. - -.. code-block:: python - - >>> str(semver.VersionInfo.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()) - '3.5.0' - >>> str(semver.VersionInfo.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()) - '3.4.5-pre.3' - >>> str(semver.VersionInfo.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:`semver.VersionInfo.next_version`. - -If you want to raise your version and take prereleases into account, -the function :func:`semver.VersionInfo.next_version` would perhaps a -better fit. - - -.. code-block:: python - - >>> v = semver.VersionInfo.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")) - '3.4.5' - >>> str(semver.VersionInfo.parse("3.4.5+build.4").next_version(part="patch")) - '3.4.5' - >>> str(semver.VersionInfo.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:`semver.VersionInfo` **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 < v2 - True - >>> v1 > v2 - False - -* **A** :class:`semver.VersionInfo` **type and a** :func:`tuple` **or** :func:`list` - - Use the operator as with two :class:`semver.VersionInfo` types:: - - >>> v = semver.VersionInfo.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:`semver.VersionInfo` **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 :class:`ValueError` exception:: - - >>> v > "1.0" - Traceback (most recent call last): - ... - ValueError: 1.0 is not valid SemVer string - -* **A** :class:`semver.VersionInfo` **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 :class:`TypeError` exception:: - - >>> v > dict(major=1, unknown=42) - Traceback (most recent call last): - ... - TypeError: __init__() 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 = semver.VersionInfo.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 -dictionary key:: - - >>> d = {} - >>> v1 = semver.VersionInfo.parse("1.2.3-rc4+1e4664d") - >>> v2 = semver.VersionInfo.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:`semver.VersionInfo` 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) - -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`). - -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']))) - '2.1.0' - >>> str(min(map(semver.VersionInfo.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() - (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() - (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:`VersionInfo` -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") - (VersionInfo(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') - - -.. _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.VersionInfo` 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:`semver.VersionInfo` - class. - For example, the function :func:`semver.bump_major` is replaced by - :func:`semver.VersionInfo.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()) - >>> s1 == s2 - True - - Likewise with the other module level functions. - -* :func:`semver.finalize_version` - - Replace it with :func:`semver.VersionInfo.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()) - >>> 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(semver.VersionInfo(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(semver.VersionInfo.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(semver.VersionInfo.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`: - - .. code-block:: python - - >>> v1 = semver.parse("1.2.3") - >>> v2 = semver.VersionInfo.parse("1.2.3").to_dict() - >>> v1 == v2 - True - -* :func:`semver.parse_version_info` - - Replace it with :func:`semver.VersionInfo.parse`: - - .. code-block:: python - - >>> v1 = semver.parse_version_info("3.4.5") - >>> v2 = semver.VersionInfo.parse("3.4.5") - >>> v1 == v2 - True - -* :func:`semver.replace` - - Replace it with :func:`semver.VersionInfo.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)) - >>> 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 VersionInfo ------------------------------------- - -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. - -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..c3651a5e --- /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 :meth:`~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-compatible-semver-version.rst b/docs/usage/check-compatible-semver-version.rst new file mode 100644 index 00000000..20330456 --- /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:`~semver.version.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 new file mode 100644 index 00000000..bdd57e7a --- /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 :meth:`~semver.version.Version.is_valid`: + +.. code-block:: python + + >>> Version.is_valid("1.0.0") + True + >>> Version.is_valid("invalid") + False diff --git a/docs/usage/compare-versions-through-expression.rst b/docs/usage/compare-versions-through-expression.rst new file mode 100644 index 00000000..e2dee4d6 --- /dev/null +++ b/docs/usage/compare-versions-through-expression.rst @@ -0,0 +1,39 @@ +Comparing Versions through an Expression +======================================== + +If you need a more fine-grained approach of comparing two versions, +use the :meth:`~semver.version.Version.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 + + >>> Version.parse("2.0.0").match(">=1.0.0") + True + >>> Version.parse("1.0.0").match(">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 + + >>> Version.parse("2.0.0").match("2.0.0") + True + >>> Version.parse("1.0.0").match("3.5.1") + False diff --git a/docs/usage/compare-versions.rst b/docs/usage/compare-versions.rst new file mode 100644 index 00000000..ddd03b68 --- /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:`~semver.version.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:`~semver.version.Version` **type and a** :func:`tuple` **or** :func:`list` + + Use the operator as with two :class:`~semver.version.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:`~semver.version.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:`~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):: + + >>> 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..6948438c --- /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:`~semver.version.Version` instance into +a different type. For example, for displaying or to access all parts. + +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 :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 :meth:`~semver.version.Version.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..48bb58a1 --- /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` class + 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. + 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/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/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..e143162a --- /dev/null +++ b/docs/usage/get-min-and-max-of-multiple-versions.rst @@ -0,0 +1,42 @@ +.. _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:`~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 + + >>> 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:`~semver.version.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`. 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..845f2290 --- /dev/null +++ b/docs/usage/increase-parts-of-a-version_prereleases.rst @@ -0,0 +1,24 @@ +.. _increase-parts-of-a-version: + +Increasing Parts of a Version Taking into Account Prereleases +============================================================= + +.. versionadded:: 2.10.0 + Added :meth:`~semver.version.Version.next_version`. + +If you want to raise your version and take prereleases into account, +the function :meth:`~semver.version.Version.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..4b8e3fc9 --- /dev/null +++ b/docs/usage/index.rst @@ -0,0 +1,21 @@ +Using semver +============ + +.. toctree:: + + semver_org-version + semver-version + 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 + 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 diff --git a/docs/usage/parse-version-string.rst b/docs/usage/parse-version-string.rst new file mode 100644 index 00000000..0cf02650 --- /dev/null +++ b/docs/usage/parse-version-string.rst @@ -0,0 +1,15 @@ +Parsing a Version String +======================== + +"Parsing" in this context means to identify the different parts in a string. +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') + +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/docs/usage/raise-parts-of-a-version.rst b/docs/usage/raise-parts-of-a-version.rst new file mode 100644 index 00000000..be89cf8d --- /dev/null +++ b/docs/usage/raise-parts-of-a-version.rst @@ -0,0 +1,70 @@ +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:`~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: + +* :meth:`~semver.version.Version.bump_major`: raises the major part and set all other parts to + zero. Set ``prerelease`` and ``build`` to ``None``. +* :meth:`~semver.version.Version.bump_minor`: raises the minor part and sets ``patch`` to zero. + Set ``prerelease`` and ``build`` to ``None``. +* :meth:`~semver.version.Version.bump_patch`: raises the patch part. Set ``prerelease`` and + ``build`` to ``None``. +* :meth:`~semver.version.Version.bump_prerelease`: raises the prerelease part and set + ``build`` to ``None``. +* :meth:`~semver.version.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`. + +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 + + >>> 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/docs/usage/replace-parts-of-a-version.rst b/docs/usage/replace-parts-of-a-version.rst new file mode 100644 index 00000000..57ab65e9 --- /dev/null +++ b/docs/usage/replace-parts-of-a-version.rst @@ -0,0 +1,19 @@ +.. _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 :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') + +If you pass invalid keys you get an exception:: + + >>> 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..0f2e2411 --- /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.1' 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/pyproject.toml b/pyproject.toml index eca41891..6b12deb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,63 @@ +# +# +# 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", + "setuptools-scm", +] +build-backend = "setuptools.build_meta" + + + [tool.black] line-length = 88 -target-version = ['py37'] -include = '\.pyi?$' +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] # diff = true -exclude = ''' -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.mypy_cache - | \.tox - | \.venv - | \.env - | _build - | build - | dist - )/ -) -''' + + +[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" +filename = "CHANGELOG.rst" +directory = "changelog.d/" +title_format = "Version {version}" +template = "changelog.d/_template.rst" +# issue_format = "`#{issue} `_" +# issue_format = ":gh:`{issue}`" + + +[tool.towncrier.fragment.breaking] +name = "Breaking Changes" + +[tool.towncrier.fragment.bugfix] +name = "Bug Fixes" + +[tool.towncrier.fragment.deprecation] +name = "Deprecations" + +[tool.towncrier.fragment.doc] +name = "Improved Documentation" + +[tool.towncrier.fragment.feature] +name = "Features" + +[tool.towncrier.fragment.removal] +name = "Removals" + +[tool.towncrier.fragment.trivial] +name = "Trivial/Internal Changes" diff --git a/release-procedure.md b/release-procedure.md index f2571be9..7476c79a 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -1,52 +1,119 @@ # 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: -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! + * all issues for a new release are closed: . + + * that all pull requests that should be included in this release are merged: . + + * 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` + * `CITATION.cff` -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. Verify that the version has been updated and follow + : -1. Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/python-semver/python-semver/blob/master/CONTRIBUTORS) + * `src/semver/__about__.py` + * `docs/usage/semver-version.rst` -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. Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/python-semver/python-semver/blob/master/CONTRIBUTORS). -1. Upload it to TestPyPI first: - ```bash - git clean -xfd - python setup.py sdist bdist_wheel --universal - twine upload --repository-url https://test.pypi.org/legacy/ dist/* +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: + + $ 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 + +1. Build the documentation and check the output: + + $ tox -e docs + +1. Commit all changes, push, and create a pull request. + + +## Create the New Release + +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 + $ 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. If everything looks fine, merge the pull request. -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 + $ tox -e prepare-dist + $ 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 +# 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. + Select the tag from the last step and copy the + content of the tag description into the release + description. + +1. Announce it in . -or using GitHub web interface available at https://github.com/python-semver/python-semver/releases +You're done! Celebrate! diff --git a/semver.py b/semver.py deleted file mode 100644 index ce8816af..00000000 --- a/semver.py +++ /dev/null @@ -1,1259 +0,0 @@ -"""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 - - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - - -__version__ = "2.13.0" -__author__ = "Kostiantyn Rybnikov" -__author_email__ = "k-bx@k-bx.com" -__maintainer__ = ["Sebastien Celles", "Tom Schraitle"] -__maintainer_email__ = "s.celles@gmail.com" - -#: 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" - - -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 - - -else: # pragma: no cover - string_types = unicode, str - text_type = unicode - binary_type = str - - def b(s): - return s - - # Workaround for standalone backslash - def u(s): - return unicode(s.replace(r"\\", r"\\\\"), "unicode_escape") - - -def ensure_str(s, encoding="utf-8", errors="strict"): - # 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` - """ - 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): - s = s.decode(encoding, errors) - return s - - -def deprecated(func=None, replace=None, version=None, category=DeprecationWarning): - """ - 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 - name like ``semver.VersionInfo.bump_major``. - :param str 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. - """ - - 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."] - - if version: - msg.append("Deprecated since version {v}. ") - msg.append("This function will be removed in semver 3.") - if replace: - msg.append("Use {r!r} instead.") - else: - msg.append("Use the respective 'semver.VersionInfo.{r}' instead.") - - # hasattr is needed for Python2 compatibility: - f = func.__qualname__ if hasattr(func, "__qualname__") else func.__name__ - r = replace or f - - frame = inspect.currentframe().f_back - - msg = " ".join(msg) - 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) - - 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 - :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 VersionInfo.parse(version).to_dict() - - -def comparator(operator): - """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) - 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(object): - """ - 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 - """ - - __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, minor=0, patch=0, prerelease=None, build=None): - # Build a dictionary of the arguments except prerelease and build - version_parts = { - "major": major, - "minor": minor, - "patch": 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) - ) - - 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): - """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): - """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): - """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): - """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): - """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): - """ - 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 - :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): - """ - 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``. - :rtype: :class:`collections.OrderedDict` - - >>> 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), - ) - ) - - # 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): - """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 - - @staticmethod - def _increment_string(string): - """ - Look for the last sequence of number(s) in a string and increment. - - :param str 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): - """ - 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() - VersionInfo(major=4, minor=0, patch=0, prerelease=None, build=None) - """ - cls = type(self) - return cls(self._major + 1) - - def bump_minor(self): - """ - 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() - 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): - """ - 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() - 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="rc"): - """ - 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() - 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="build"): - """ - 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() - 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): - """ - Compare self with other. - - :param other: the second version (can be string, a dict, tuple/list, or - a VersionInfo instance) - :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 - >>> 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_types): - 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( - "Expected str or {} instance, but got {}".format( - cls.__name__, 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, prerelease_token="rc"): - """ - 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 - :rtype: :class:`VersionInfo` - """ - 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): - return self.compare(other) == 0 - - @comparator - def __ne__(self, other): - return self.compare(other) != 0 - - @comparator - def __lt__(self, other): - return self.compare(other) < 0 - - @comparator - def __le__(self, other): - return self.compare(other) <= 0 - - @comparator - def __gt__(self, other): - return self.compare(other) > 0 - - @comparator - def __ge__(self, other): - return self.compare(other) >= 0 - - def __getitem__(self, index): - """ - 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) - - 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, self.to_tuple()[index])) - - if len(part) == 1: - part = part[0] - elif not part: - raise IndexError("Version part undefined") - return part - - def __repr__(self): - 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(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): - return hash(self.to_tuple()[:4]) - - def finalize_version(self): - """ - 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' - """ - cls = type(self) - return cls(self.major, self.minor, self.patch) - - def match(self, match_expr): - """ - Compare self to match a match expression. - - :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.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): - """ - 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. - - >>> 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)) - if match is None: - raise ValueError("%s is not valid SemVer string" % version) - - version_parts = 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(**version_parts) - - def replace(self, **parts): - """ - 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 dict 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 - """ - version = self.to_dict() - version.update(parts) - try: - return VersionInfo(**version) - 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): - """ - Check if the string is a valid semver version. - - .. versionadded:: 2.9.1 - - :param str 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) - 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 - :rtype: :class:`VersionInfo` - - >>> 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, b): - 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)) - - -@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 = 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 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 = 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 - :rtype: :class:`VersionInfo` - - >>> semver.max_ver("1.0.0", "2.0.0") - '2.0.0' - """ - if isinstance(ver1, string_types): - 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 - :rtype: :class:`VersionInfo` - - >>> 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 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(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 - :rtype: str - - >>> 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 - :rtype: str - - >>> 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 - :rtype: str - - >>> 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 - :rtype: str - - >>> 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 - :rtype: str - - >>> 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 - :rtype: str - - >>> 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 str version: the version string to replace - :param dict 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 - - >>> 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): - """ - Subcommand: Bumps a version. - - Synopsis: bump - can be major, minor, patch, prerelease, or build - - :param args: The parsed arguments - :type args: :class:`argparse.Namespace` - :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[args.bump]) - return str(func()) - - -def cmd_check(args): - """ - 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): - """ - 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): - """ - 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(): - """ - Create an :class:`argparse.ArgumentParser` instance. - - :return: parser instance - :rtype: :class:`argparse.ArgumentParser` - """ - 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): - """ - 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() - raise SystemExit() - - # Call the respective function object: - return args.func(args) - - -def main(cliargs=None): - """ - 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() - 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 5b2a59b0..0ee8564c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,66 @@ +# +# Metadata for setup.py +# +# See https://setuptools.rtfd.io/en/latest/userguide/declarative_config.html + +[metadata] +name = semver +version = attr: semver.__about__.__version__ +description = Python helper for Semantic Versioning (https://semver.org) +long_description = file: README.rst +long_description_content_type = text/x-rst +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 +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 +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.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 + +[options] +package_dir = + =src +packages = find: +python_requires = >=3.7 +include_package_data = True + +[options.entry_points] +console_scripts = + pysemver = semver.cli:main + +[options.packages.find] +where = src + +[options.package_data] +semver = py.typed + [tool:pytest] -norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ -testpaths = . docs +norecursedirs = .git build .env/ env/ .pyenv/ .tmp/ .eggs/ venv/ +testpaths = tests docs +pythonpath = src tests 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 @@ -14,13 +71,25 @@ addopts = [flake8] max-line-length = 88 ignore = F821,W503 +extend-exclude = + .eggs + .env + build + docs + venv + conftest.py + src/semver/__init__.py + tasks.py + +[pycodestyle] +count = False +# ignore = E226,E302,E41 +max-line-length = 88 +statistics = True exclude = + src/semver/__init__.py .env, .eggs, .tox, .git, - __pycache__, - build, - dist docs - conftest.py diff --git a/setup.py b/setup.py deleted file mode 100755 index 746c1436..00000000 --- a/setup.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python -import semver as package -from glob import glob -from os import remove -from os.path import dirname, join -from setuptools import setup -from setuptools.command.test import test as TestCommand - -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 - - -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) - - -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)) - - -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 read_file(filename): - with open(join(dirname(__file__), filename)) as f: - return f.read() - - -setup( - name=package.__name__, - version=package.__version__, - description=package.__doc__.strip(), - long_description=read_file("README.rst"), - long_description_content_type="text/x-rst", - author=package.__author__, - author_email=package.__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=[package.__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 :: 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", - "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}, - entry_points={"console_scripts": ["pysemver = semver:main"]}, -) diff --git a/src/semver/__about__.py b/src/semver/__about__.py new file mode 100644 index 00000000..2eff8c86 --- /dev/null +++ b/src/semver/__about__.py @@ -0,0 +1,37 @@ +""" +Metadata about semver. + +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 +__version__ = "3.0.1" + +#: 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 (https://semver.org)" + +#: Supported semver specification +SEMVER_SPEC_VERSION = "2.0.0" diff --git a/src/semver/__init__.py b/src/semver/__init__.py new file mode 100644 index 00000000..19c88f78 --- /dev/null +++ b/src/semver/__init__.py @@ -0,0 +1,72 @@ +""" +Semver package major release 3. + +A Python module for semantic versioning. Simplifies comparing versions. +""" + +from ._deprecated import ( + 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, +) +from .version import Version, VersionInfo +from .__about__ import ( + __version__, + __author__, + __maintainer__, + __author_email__, + __description__, + __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/__main__.py b/src/semver/__main__.py new file mode 100644 index 00000000..6cb11f09 --- /dev/null +++ b/src/semver/__main__.py @@ -0,0 +1,28 @@ +""" +Module to support call with :file:`__main__.py`. Used to support the following +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 + +""" +import os.path +import sys +from typing import List, Optional + +from semver import cli + + +def main(cliargs: Optional[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[1:])) diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py new file mode 100644 index 00000000..efbdf439 --- /dev/null +++ b/src/semver/_deprecated.py @@ -0,0 +1,410 @@ +""" +Contains all deprecated functions. + +.. autofunction: deprecated +""" +import inspect +import warnings +from functools import partial, wraps +from types import FrameType +from typing import Type, Callable, Optional, cast + +from . import cli +from .version import Version +from ._types import Decorator, F + + +def deprecated( + func: Optional[F] = None, + *, + replace: Optional[str] = None, + version: Optional[str] = None, + remove: Optional[str] = None, + category: Type[Warning] = DeprecationWarning, +) -> Decorator: + """ + 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.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, + remove=remove, + 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}. ") + + 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: + 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="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): + """ + Parse version to major, minor, patch, pre-release, build parts. + + .. deprecated:: 2.10.0 + Use :meth:`~semver.version.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.Version.parse", version="2.10.0") +def parse_version_info(version): + """ + Parse version string to a Version instance. + + .. deprecated:: 2.10.0 + Use :meth:`~semver.version.Version.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 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 + > 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. + + .. 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 + :rtype: :class:`Version` + + >>> semver.max_ver("1.0.0", "2.0.0") + '2.0.0' + """ + return str(max(ver1, ver2, key=Version.parse)) + + +@deprecated(replace="min", version="2.10.2") +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 + :rtype: :class:`Version` + + >>> semver.min_ver("1.0.0", "2.0.0") + '1.0.0' + """ + return str(min(ver1, ver2, key=Version.parse)) + + +@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 :meth:`~semver.version.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 :meth:`~semver.version.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 :meth:`~semver.version.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 :meth:`~semver.version.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 :meth:`~semver.version.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 :meth:`~semver.version.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 :meth:`~semver.version.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, 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/src/semver/_types.py b/src/semver/_types.py new file mode 100644 index 00000000..7afb6ff0 --- /dev/null +++ b/src/semver/_types.py @@ -0,0 +1,12 @@ +"""Typing for semver.""" + +from functools import partial +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) +Decorator = Union[Callable[..., F], partial] diff --git a/src/semver/cli.py b/src/semver/cli.py new file mode 100644 index 00000000..43e101e1 --- /dev/null +++ b/src/semver/cli.py @@ -0,0 +1,174 @@ +""" +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 +from typing import cast, List, Optional + +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.is_valid(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: Optional[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/py.typed b/src/semver/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/semver/version.py b/src/semver/version.py new file mode 100644 index 00000000..d2f336c0 --- /dev/null +++ b/src/semver/version.py @@ -0,0 +1,742 @@ +"""Version handling by a semver compatible version class.""" + +import collections +import re +from functools import wraps +from typing import ( + Any, + Dict, + Iterable, + Optional, + SupportsInt, + Tuple, + Union, + cast, + Callable, + Collection, + Type, + TypeVar, +) + +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] + +T = TypeVar("T", bound="Version") + + +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): + return NotImplemented + return operator(self, other) + + return wrapper + + +def _cmp(a, b): # TODO: type hints + """Return negative if ab.""" + return (a > b) - (a < b) + + +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. + :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") + + #: 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 + _REGEX_TEMPLATE = r""" + ^ + (?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-]*))* + ))? + (?:\+(?P + [0-9a-zA-Z-]+ + (?:\.[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, + ) + + def __init__( + self, + major: SupportsInt, + minor: SupportsInt = 0, + patch: SupportsInt = 0, + 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)} + + 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) + + @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).""" + 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 Version object to a tuple. + + .. versionadded:: 2.10.0 + 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 + + >>> 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 Version object to an OrderedDict. + + .. versionadded:: 2.10.0 + 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``, + ``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: + """Return 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: 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 :class:`Version` object with the raised prerelease part. + The original object is not modified. + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_prerelease().prerelease + 'rc.2' + >>> ver.bump_prerelease('').prerelease + '1' + >>> ver.bump_prerelease(None).prerelease + 'rc.1' + """ + cls = type(self) + 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: 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 :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() + Version(major=3, minor=4, patch=5, prerelease='rc.1', \ +build='build.10') + """ + cls = type(self) + 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: + """ + 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 = self._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 + "prerelease" 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 + """ + 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( + f"Invalid part. Expected one of {validparts}, but got {part!r}" + ) + 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) + + # Only check the main parts: + if part in cls.NAMES[:3]: + 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 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: + 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: optional 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 + >>> semver.Version.parse("4.0.4").match("4.0.4") + True + """ + 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:] + elif match_expr and match_expr[0] in "0123456789": + prefix = "==" + match_version = match_expr + 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: Type[T], version: String, optional_minor_and_patch: bool = False + ) -> T: + """ + 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 + + >>> semver.Version.parse('3.4.5-pre.2+build.4') + Version(major=3, minor=4, patch=5, \ +prerelease='pre.2', build='build.4') + """ + 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)) + + 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) + + 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:`~semver.version.Version` object with + the changed parts + :raises TypeError: if ``parts`` contain 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 is_valid(cls, version: str) -> bool: + """ + Check if the string is a valid semver version. + + .. 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. + """ + try: + cls.parse(version) + return True + 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. + + .. versionadded:: 3.0.0 + + :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/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/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 new file mode 100644 index 00000000..9017bbbe --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,31 @@ +import sys + +import pytest + +import semver + +from coerce import coerce # noqa:E402 +from semverwithvprefix import SemVerWithVPrefix # noqa:E402 +import packaging.version + + +@pytest.fixture(autouse=True) +def add_semver(doctest_namespace): + doctest_namespace["Version"] = semver.version.Version + doctest_namespace["semver"] = semver + doctest_namespace["coerce"] = coerce + doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix + doctest_namespace["PyPIVersion"] = packaging.version.Version + + +@pytest.fixture +def version(): + """ + Creates a version + + :return: a version type + :rtype: Version + """ + return semver.Version( + major=1, minor=2, patch=3, prerelease="alpha.1.2", build="build.11.e0f985a" + ) 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 diff --git a/tests/test_bump.py b/tests/test_bump.py new file mode 100644 index 00000000..34e0b2ac --- /dev/null +++ b/tests/test_bump.py @@ -0,0 +1,125 @@ +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_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" + + +@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..1c99f450 --- /dev/null +++ b/tests/test_compare.py @@ -0,0 +1,304 @@ +import pytest + +import semver +from semver import Version, 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 = 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 = 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 + assert v1 > v2 + assert v1 >= v2 + assert not (v1 < v2) + assert not (v1 <= v2) + assert not (v1 == v2) + + v3 = Version(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 = Version(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 = Version(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 = 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 + 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 = 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 + 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 = 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 + 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 = Version(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 = 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): + semver.compare(1) + + +def test_should_compare_prerelease_and_build_with_numbers(): + assert Version(major=1, minor=9, patch=1, prerelease=1, build=1) < Version( + major=1, minor=9, patch=1, prerelease=2, build=1 + ) + 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 new file mode 100644 index 00000000..88862689 --- /dev/null +++ b/tests/test_deprecated_functions.py @@ -0,0 +1,75 @@ +from argparse import Namespace + +import pytest + +from semver import ( + parse, + parse_version_info, + match, + max_ver, + min_ver, + format_version, + bump_major, + bump_minor, + bump_patch, + bump_prerelease, + bump_build, + finalize_version, + replace, + cmd_bump, + cmd_compare, + cmd_check, + cmd_nextver, + createparser, + process, + main, +) +from semver._deprecated import deprecated + + +@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",), {}), + (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"), {}), + (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): + 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..73ff3122 --- /dev/null +++ b/tests/test_format.py @@ -0,0 +1,65 @@ +import pytest + +from semver import Version, 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 = Version.parse(s_version) + assert str(v) == s_version + + +@pytest.mark.parametrize( + "version, expected", + [ + ( + Version(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="r.1", build=None), + "Version(major=1, minor=2, patch=3, prerelease='r.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)", + ), + ( + 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')", + ), + ( + 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')", + ), + ( + 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')", + ), + ], +) +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..79e45025 --- /dev/null +++ b/tests/test_index.py @@ -0,0 +1,95 @@ +import pytest + +from semver import Version + + +@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 = Version.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 = Version.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 = Version.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 = Version.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..e2685cae --- /dev/null +++ b/tests/test_match.py @@ -0,0 +1,66 @@ +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.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", + [ + ("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", "!")]) +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..ddf52196 --- /dev/null +++ b/tests/test_parsing.py @@ -0,0 +1,207 @@ +import pytest + +from semver import Version, 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 + + +@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) + assert v.__str__() == s_version + d = {} + d[v] = "" # to ensure that Version 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 = Version.parse(s_version) + assert str(v) == s_version + + +def test_next_version_with_invalid_parts(): + version = Version.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 = Version.parse(version) + next_version = ver.next_version(part) + assert isinstance(next_version, Version) + 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..e783a0b4 --- /dev/null +++ b/tests/test_pysemver-cli.py @@ -0,0 +1,144 @@ +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, + __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 + + +@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 new file mode 100644 index 00000000..f223eddb --- /dev/null +++ b/tests/test_replace.py @@ -0,0 +1,50 @@ +import pytest + +from semver import Version, 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 Version.parse(version).replace(**parts) == Version.parse(expected) + + +def test_replace_raises_ValueError_for_non_numeric_values(): + with pytest.raises(ValueError): + Version.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..782d5c79 --- /dev/null +++ b/tests/test_semver.py @@ -0,0 +1,137 @@ +import pytest # noqa + +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 Version._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. .*"): + Version(**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 = 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 Version("1", "2", "3") == Version(1, 2, 3) + + +def test_using_non_numeric_string_as_major_minor_patch_throws(): + with pytest.raises(ValueError): + Version("a") + with pytest.raises(ValueError): + Version(1, "a") + with pytest.raises(ValueError): + Version(1, 2, "a") + + +def test_should_be_able_to_use_integers_as_prerelease_build(): + v = Version(1, 2, 3, 4, 5) + assert isinstance(v.prerelease, str) + assert isinstance(v.build, str) + assert Version(1, 2, 3, 4, 5) == Version(1, 2, 3, "4", "5") + + +def test_should_versioninfo_isvalid(): + 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/tests/test_subclass.py b/tests/test_subclass.py new file mode 100644 index 00000000..cbf9d271 --- /dev/null +++ b/tests/test_subclass.py @@ -0,0 +1,19 @@ +from semver import Version + + +def test_subclass_from_versioninfo(): + class SemVerWithVPrefix(Version): + @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..326304b8 --- /dev/null +++ b/tests/test_typeerror-274.py @@ -0,0 +1,14 @@ +import pytest +import semver + + +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 + + +def test_should_not_work_with_invalid_args(): + with pytest.raises(TypeError): + semver.version.Version.parse(8) diff --git a/tox.ini b/tox.ini index 833c9655..b18aa1f7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,36 +1,64 @@ [tox] envlist = - flake8 - py{27,34,35,36,37} - pypy + checks + py3{7,8,9,10,11,12} +isolated_build = True +skip_missing_interpreters = True + +[gh-actions] +python = + # 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 -whitelist_externals = make +description = Run test suite for {basepython} +skip_install = true +allowlist_externals = make commands = pytest {posargs:} deps = pytest pytest-cov + setuptools>=62.0 + setuptools-scm setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 + [testenv:black] description = Check for formatting changes basepython = python3 +skip_install = true 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 --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 --diff {posargs:src} + [testenv:checks] description = Run code style checks @@ -38,18 +66,27 @@ basepython = python3 deps = {[testenv:black]deps} {[testenv:flake8]deps} + {[testenv:mypy]deps} {[testenv:docstrings]deps} commands = + - {[testenv:docstrings]commands} {[testenv:black]commands} {[testenv:flake8]commands} - {[testenv:docstrings]commands} + {[testenv:mypy]commands} + [testenv:docs] description = Build HTML documentation basepython = python3 deps = -r{toxinidir}/docs/requirements.txt skip_install = true -commands = make -C docs html +allowlist_externals = + make + echo +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 @@ -63,8 +100,19 @@ commands = make -C docs man description = Prepare for TestPyPI basepython = python3 deps = - wheel twine + build commands = - python3 setup.py sdist bdist_wheel --universal + # Same as python3 -m build + pyproject-build twine check dist/* + + +[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 = + towncrier {posargs:build --draft}