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..bff8173b --- /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@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml new file mode 100644 index 00000000..8f36dbc9 --- /dev/null +++ b/.github/workflows/python-testing.yml @@ -0,0 +1,66 @@ +--- +name: Python + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Output env variables + run: | + echo "Default branch=${default-branch}" + echo "GITHUB_WORKFLOW=${GITHUB_WORKFLOW}" + echo "GITHUB_ACTION=$GITHUB_ACTION" + echo "GITHUB_ACTIONS=$GITHUB_ACTIONS" + 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@v2 + with: + python-version: 3.6 + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Check + run: | + tox -e checks + + tests: + needs: check + runs-on: ubuntu-latest + strategy: + max-parallel: 5 + matrix: + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: | + tox diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 665ebd19..00000000 --- a/.travis.yml +++ /dev/null @@ -1,50 +0,0 @@ -# config file for automatic testing at travis-ci.org -language: python -cache: pip - -before_install: - - sudo apt-get install -y python3-dev - -install: - - pip install --upgrade pip setuptools - - pip install virtualenv tox wheel - - tox --version - -script: tox -v - -matrix: - include: - - - python: "3.6" - env: TOXENV=checks - - - python: "3.8" - dist: xenial - env: TOXENV=mypy - - - python: "3.6" - env: TOXENV=py36 - - - python: "3.7" - dist: xenial - env: TOXENV=py37 - - - python: "3.8" - dist: xenial - env: TOXENV=py38 - - - python: "3.9-dev" - dist: bionic - env: TOXENV=py39 - - - python: "nightly" - dist: bionic - env: TOXENV=py310 - - - python: "3.8" - dist: xenial - env: TOXENV=mypy - -jobs: - allow_failures: - - python: "nightly" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 26f8ff79..3173507f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,92 @@ in our repository. .. towncrier release notes start + +Version 3.0.0-dev.3 +=================== + +:Released: 2022-01-19 +:Maintainer: Tom Schraitle + + +Bug Fixes +--------- + +* :gh:`310`: Rework API documentation. + Follow a more "semi-manual" attempt and add auto directives + into :file:`docs/api.rst`. + + + +Improved Documentation +---------------------- + +* :gh:`312`: Rework "Usage" section. + + * Mention the rename of :class:`~semver.version.VersionInfo` to + :class:`~semver.version.Version` class + * Remove semver. prefix in doctests to make examples shorter + * Correct some references to dunder methods like + :func:`~.semver.version.Version.__getitem__`, + :func:`~.semver.version.Version.__gt__` etc. + * Remove inconsistencies and mention module level function as + deprecated and discouraged from using + * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example + +* :gh:`315`: Improve release procedure text + + + +Trivial/Internal Changes +------------------------ + +* :gh:`309`: Some (private) functions from the :mod:`semver.version` + module has been changed. + + The following functions got renamed: + + * function ``semver.version.comparator`` got renamed to + :func:`semver.version._comparator` as it is only useful + inside the :class:`~semver.version.Version` class. + * function ``semver.version.cmp`` got renamed to + :func:`semver.version._cmp` as it is only useful + inside the :class:`~semver.version.Version` class. + + The following functions got integrated into the + :class:`~semver.version.Version` class: + + * function ``semver.version._nat_cmd`` as a classmethod + * function ``semver.version.ensure_str`` + +* :gh:`313`: Correct :file:`tox.ini` for ``changelog`` entry to skip + installation for semver. This should speed up the execution + of towncrier. + +* :gh:`316`: Comparisons of :class:`~semver.version.Version` class and other + types return now a :py:const:`NotImplemented` constant instead + of a :py:exc:`TypeError` exception. + + The `NotImplemented`_ section of the Python documentation recommends + returning this constant when comparing with ``__gt__``, ``__lt__``, + and other comparison operators to "to indicate that the operation is + not implemented with respect to the other type". + + .. _NotImplemented: https://docs.python.org/3/library/constants.html#NotImplemented + +* :gh:`319`: Introduce stages in :file:`.travis.yml` + The config file contains now two stages: check and test. If + check fails, the test stage won't be executed. This could + speed up things when some checks fails. + +* :gh:`322`: Switch from Travis CI to GitHub Actions. + +* :gh:`347`: Support Python 3.10 in GitHub Action and other config files. + + + +---- + + Version 3.0.0-dev.2 =================== diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..5fd75ab2 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,236 @@ +.. _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. + + + +Modifying the Code +------------------ + +We recommend the following workflow: + +#. Fork our project on GitHub using this link: + https://github.com/python-semver/python-semver/fork + +#. Clone your forked Git repository (replace ``GITHUB_USER`` with your + account name on GitHub):: + + $ git clone git@github.com:GITHUB_USER/python-semver.git + +#. Create a new branch. You can name your branch whatever you like, but we + recommend to use some meaningful name. If your fix is based on a + existing GitHub issue, add also the number. Good examples would be: + + * ``feature/123-improve-foo`` when implementing a new feature in issue 123 + * ``bugfix/234-fix-security-bar`` a bugfixes for issue 234 + + Use this :command:`git` command:: + + $ git checkout -b feature/NAME_OF_YOUR_FEATURE + +#. Work on your branch and create a pull request: + + a. Write test cases and run the complete test suite, see :ref:`testsuite` + for details. + + b. Write a changelog entry, see section :ref:`changelog`. + + c. If you have implemented a new feature, document it into our + documentation to help our reader. See section :ref:`doc` for + further details. + + d. Create a `pull request`_. Describe in the pull request what you did + and why. If you have open questions, ask. + +#. Wait for feedback. If you receive any comments, address these. + +#. After your pull request got accepted, delete your branch. + + +.. _testsuite: + +Running the Test Suite +---------------------- + +We use `pytest`_ and `tox`_ to run tests against all supported Python +versions. All test dependencies are resolved automatically. + +You can decide to run the complete test suite or only part of it: + +* To run all tests, use:: + + $ tox + + If you have not all Python interpreters installed on your system + it will probably give you some errors (``InterpreterNotFound``). + To avoid such errors, use:: + + $ tox --skip-missing-interpreters + + It is possible to use one or more specific Python versions. Use the ``-e`` + option and one or more abbreviations (``py36`` for Python 3.6, ``py37`` for + Python 3.7 etc.):: + + $ tox -e py36 + $ tox -e py36,py37 + + 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 py36 -- 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.6 and 3.7 only:: + + $ tox -e py36,py37 -- tests/test_bump.py::test_should_bump_major + +Our code is checked against formatting, style, type, and docstring issues +(`black`_, `flake8`_, `mypy`_, and `docformatter`_). +It is recommended to run your tests in combination with :command:`checks`, +for example:: + + $ tox -e checks,py36,py37 + + +.. _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. + + +.. _changelog: + +Adding a Changelog Entry +------------------------ + +.. include:: ../changelog.d/README.rst + :start-after: -text-begin- + + +.. _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/README.rst b/README.rst index d4f29819..5c28cc69 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,8 @@ 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 @@ -99,9 +100,6 @@ There are other functions to discover. Read on! .. |latest-version| image:: https://img.shields.io/pypi/v/semver.svg :alt: Latest version on PyPI :target: https://pypi.org/project/semver -.. |build-status| image:: https://travis-ci.com/python-semver/python-semver.svg?branch=master - :alt: Build status - :target: https://travis-ci.com/python-semver/python-semver .. |python-support| image:: https://img.shields.io/pypi/pyversions/semver.svg :target: https://pypi.org/project/semver :alt: Python versions @@ -118,3 +116,14 @@ There are other functions to discover. Read on! .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Black Formatter +.. |Gitter| image:: https://badges.gitter.im/python-semver/community.svg + :target: https://gitter.im/python-semver/community + :alt: Gitter +.. |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/213.improvement.rst b/changelog.d/213.improvement.rst deleted file mode 100644 index dcedc695..00000000 --- a/changelog.d/213.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Add typing information \ No newline at end of file diff --git a/docs/_api/semver.__about__.rst b/docs/_api/semver.__about__.rst deleted file mode 100644 index 22395ebd..00000000 --- a/docs/_api/semver.__about__.rst +++ /dev/null @@ -1,5 +0,0 @@ -semver.\_\_about\_\_ module -=========================== - -.. automodule:: semver.__about__ - :members: diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 002e6b2f..81a5906f 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -35,6 +35,10 @@ div.related.top nav { font-weight: 700; } +.py.class { + margin-top: 1.5em; +} + .py.method { padding-top: 0.25em; padding-bottom: 1.25em; diff --git a/docs/api.rst b/docs/api.rst index 9d884601..196e30a9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,10 +1,57 @@ .. _api: -### -API -### +API Reference +============= -.. toctree:: - :maxdepth: 4 +.. currentmodule:: semver - _api/semver \ No newline at end of file + +Metadata :mod:`semver.__about__` +-------------------------------- + +.. automodule:: semver.__about__ + + +Deprecated Functions in :mod:`semver._deprecated` +------------------------------------------------- + +.. automodule:: semver._deprecated + +.. autofunction:: semver._deprecated.deprecated + + +CLI Parsing :mod:`semver.cli` +----------------------------- + +.. automodule:: semver.cli + +.. autofunction:: semver.cli.cmd_bump + +.. autofunction:: semver.cli.cmd_check + +.. autofunction:: semver.cli.cmd_compare + +.. autofunction:: semver.cli.createparser + +.. autofunction:: semver.cli.main + +.. autofunction:: semver.cli.process + + +Entry point :mod:`semver.__main__` +---------------------------------- + +.. automodule:: semver.__main__ + + + +Version Handling :mod:`semver.version` +-------------------------------------- + +.. automodule:: semver.version + +.. autoclass:: semver.version.VersionInfo + +.. autoclass:: semver.version.Version + :members: + :special-members: __iter__, __eq__, __ne__, __lt__, __le__, __gt__, __ge__, __getitem__, __hash__, __repr__, __str__ diff --git a/docs/conf.py b/docs/conf.py index f5e04b19..52a46704 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,8 @@ import re import sys -sys.path.insert(0, os.path.abspath("../src/")) +SRC_DIR = os.path.abspath("../src/") +sys.path.insert(0, SRC_DIR) # from semver import __version__ # noqa: E402 @@ -58,15 +59,16 @@ def find_version(*file_paths): # ones. extensions = [ "sphinx.ext.autodoc", - "sphinx.ext.autosummary", "sphinx_autodoc_typehints", "sphinx.ext.intersphinx", "sphinx.ext.extlinks", ] +# Autodoc configuration autoclass_content = "class" -autodoc_default_options = {} - +autodoc_typehints = "signature" +autodoc_member_order = "alphabetical" +add_function_parentheses = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/development.rst b/docs/development.rst index 049fe1a3..e582053e 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,241 +1 @@ -.. _contributing: - -Contributing to semver -====================== - -The semver source code is managed using Git and is hosted on GitHub:: - - git clone git://github.com/python-semver/python-semver - - -Reporting Bugs and Feedback ---------------------------- - -If you think you have encountered a bug in semver or have an idea for a new -feature? Great! We like to hear from you. - -First, take the time to look into our GitHub `issues`_ tracker if -this already covered. If not, changes are good that we avoid double work. - - -Prerequisites -------------- - -Before you make changes to the code, we would highly appreciate if you -consider the following general requirements: - -* Make sure your code adheres to the `Semantic Versioning`_ specification. - -* Check if your feature is covered by the Semantic Versioning specification. - If not, ask on its GitHub project https://github.com/semver/semver. - - - -Modifying the Code ------------------- - -We recommend the following workflow: - -#. Fork our project on GitHub using this link: - https://github.com/python-semver/python-semver/fork - -#. Clone your forked Git repository (replace ``GITHUB_USER`` with your - account name on GitHub):: - - $ git clone git@github.com:GITHUB_USER/python-semver.git - -#. Create a new branch. You can name your branch whatever you like, but we - recommend to use some meaningful name. If your fix is based on a - existing GitHub issue, add also the number. Good examples would be: - - * ``feature/123-improve-foo`` when implementing a new feature in issue 123 - * ``bugfix/234-fix-security-bar`` a bugfixes for issue 234 - - Use this :command:`git` command:: - - $ git checkout -b feature/NAME_OF_YOUR_FEATURE - -#. Work on your branch and create a pull request: - - a. Write test cases and run the complete test suite, see :ref:`testsuite` - for details. - - b. Write a changelog entry, see section :ref:`changelog`. - - c. If you have implemented a new feature, document it into our - documentation to help our reader. See section :ref:`doc` for - further details. - - d. Create a `pull request`_. Describe in the pull request what you did - and why. If you have open questions, ask. - -#. Wait for feedback. If you receive any comments, address these. - -#. After your pull request got accepted, delete your branch. - - -.. _testsuite: - -Running the Test Suite ----------------------- - -We use `pytest`_ and `tox`_ to run tests against all supported Python -versions. All test dependencies are resolved automatically. - -You can decide to run the complete test suite or only part of it: - -* To run all tests, use:: - - $ tox - - If you have not all Python interpreters installed on your system - it will probably give you some errors (``InterpreterNotFound``). - To avoid such errors, use:: - - $ tox --skip-missing-interpreters - - It is possible to use only specific Python versions. Use the ``-e`` - option and one or more abbreviations (``py27`` for Python 2.7, ``py34`` for - Python 3.4 etc.):: - - $ tox -e py34 - $ tox -e py27,py34 - - To get a complete list, run:: - - $ tox -l - -* To run only a specific test, pytest requires the syntax - ``TEST_FILE::TEST_FUNCTION``. - - For example, the following line tests only the function - :func:`test_immutable_major` in the file :file:`test_semver.py` for all - Python versions:: - - $ tox test_semver.py::test_immutable_major - - By default, pytest prints a dot for each test function only. To - reveal the executed test function, use the following syntax:: - - $ tox -- -v - - You can combine the specific test function with the ``-e`` option, for - example, to limit the tests for Python 2.7 and 3.6 only:: - - $ tox -e py27,py36 test_semver.py::test_immutable_major - -Our code is checked against `flake8`_ for style guide issues. It is recommended -to run your tests in combination with :command:`flake8`, for example:: - - $ tox -e py27,py36,flake8 - - -.. _doc: - -Documenting semver ------------------- - -Documenting the features of semver is very important. It gives our developers -an overview what is possible with semver, how it "feels", and how it is -used efficiently. - -.. note:: - - To build the documentation locally use the following command:: - - $ tox -e docs - - The built documentation is available in :file:`dist/docs`. - - -A new feature is *not* complete if it isn't proberly documented. A good -documentation includes: - - * **A docstring** - - Each docstring contains a summary line, a linebreak, an optional - directive (see next item), the description of its arguments in - `Sphinx style`_, and an optional doctest. - The docstring is extracted and reused in the :ref:`api` section. - An appropriate docstring should look like this:: - - def compare(ver1, ver2): - """Compare two versions - - :param ver1: version string 1 - :param ver2: version string 2 - :return: The return value is negative if ver1 < ver2, - zero if ver1 == ver2 and strictly positive if ver1 > ver2 - :rtype: int - - >>> semver.compare("1.0.0", "2.0.0") - -1 - >>> semver.compare("2.0.0", "1.0.0") - 1 - >>> semver.compare("2.0.0", "2.0.0") - 0 - - """ - - * **An optional directive** - - If you introduce a new feature, change a function/method, or remove something, - it is a good practice to introduce Sphinx directives into the docstring. - This gives the reader an idea what version is affected by this change. - - The first required argument, ``VERSION``, defines the version when this change - was introduced. You can choose from: - - * ``.. versionadded:: VERSION`` - - Use this directive to describe a new feature. - - * ``.. versionchanged:: VERSION`` - - Use this directive to describe when something has changed, for example, - new parameters were added, changed side effects, different return values, etc. - - * ``.. deprecated:: VERSION`` - - Use this directive when a feature is deprecated. Describe what should - be used instead, if appropriate. - - - Add such a directive *after* the summary line, if needed. - An appropriate directive could look like this:: - - def to_tuple(self): - """ - Convert the VersionInfo object to a tuple. - - .. versionadded:: 2.10.0 - Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to - make this function available in the public API. - [...] - """ - - * **The documentation** - - A docstring is good, but in most cases it's too dense. Describe how - to use your new feature in our documentation. Here you can give your - readers more examples, describe it in a broader context or show - edge cases. - - -.. _changelog: - -Adding a Changelog Entry ------------------------- - -.. include:: ../changelog.d/README.rst - :start-after: -text-begin- - - -.. _flake8: https://flake8.readthedocs.io -.. _issues: https://github.com/python-semver/python-semver/issues -.. _pull request: https://github.com/python-semver/python-semver/pulls -.. _pytest: http://pytest.org/ -.. _Semantic Versioning: https://semver.org -.. _Sphinx style: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html -.. _tox: https://tox.readthedocs.org/ - +.. include:: ../CONTRIBUTING.rst diff --git a/docs/migratetosemver3.rst b/docs/migratetosemver3.rst index d6d90954..d977bc03 100644 --- a/docs/migratetosemver3.rst +++ b/docs/migratetosemver3.rst @@ -20,7 +20,7 @@ Use Version instead of VersionInfo The :class:`VersionInfo` has been renamed to :class:`Version` to have a more succinct name. An alias has been created to preserve compatibility but -using old name has been deprecated. +using the old name has been deprecated. If you still need the old version, use this line: @@ -39,4 +39,4 @@ import it from :mod:`semver.cli` in the future: .. code-block:: python - from semver.cli import cmd_bump \ No newline at end of file + from semver.cli import cmd_bump diff --git a/docs/semverwithvprefix.py b/docs/semverwithvprefix.py index 304ce772..5e375031 100644 --- a/docs/semverwithvprefix.py +++ b/docs/semverwithvprefix.py @@ -7,15 +7,13 @@ class SemVerWithVPrefix(Version): """ @classmethod - def parse(cls, version): + def parse(cls, version: str) -> "SemVerWithVPrefix": """ Parse version string to a Version instance. :param version: version string with "v" or "V" prefix - :type version: str :raises ValueError: when version does not start with "v" or "V" :return: a new instance - :rtype: :class:`SemVerWithVPrefix` """ if not version[0] in ("v", "V"): raise ValueError( @@ -23,9 +21,8 @@ def parse(cls, version): v=version ) ) - self = super(SemVerWithVPrefix, cls).parse(version[1:]) - return self + return super().parse(version[1:]) - def __str__(self): + def __str__(self) -> str: # Reconstruct the tag - return "v" + super(SemVerWithVPrefix, self).__str__() + return "v" + super().__str__() diff --git a/docs/usage.rst b/docs/usage.rst index 94a115a8..f6983d17 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,7 +1,7 @@ Using semver ============ -The ``semver`` module can store a version in the :class:`semver.Version` class. +The :mod:`semver` module can store a version in the :class:`~semver.version.Version` class. For historical reasons, a version can be also stored as a string or dictionary. Each type can be converted into the other, if the minimum requirements @@ -26,76 +26,60 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.0-dev.2' + '3.0.0-dev.3' Creating a Version ------------------ -Due to historical reasons, the semver project offers two ways of -creating a version: +.. versionchanged:: 3.0.0 -* through an object oriented approach with the :class:`semver.Version` - class. This is the preferred method when using semver. + The former :class:`~semver.version.VersionInfo` + has been renamed to :class:`~semver.version.Version`. -* through module level functions and builtin datatypes (usually string - and dict). - This method is still available for compatibility reasons, but are - marked as deprecated. Using it will emit a :class:`DeprecationWarning`. +The preferred way to create a new version is with the class +:class:`~semver.version.Version`. +.. note:: -.. warning:: **Deprecation Warning** + In the previous major release semver 2 it was possible to + create a version with module level functions. + However, module level functions are marked as *deprecated* + since version 2.x.y now. + These functions will be removed in semver 3.1.0. + For details, see the sections :ref:`sec_replace_deprecated_functions` + and :ref:`sec_display_deprecation_warnings`. - Module level functions are marked as *deprecated* in version 2.x.y now. - These functions will be removed in semver 3. - For details, see the sections :ref:`sec_replace_deprecated_functions` and - :ref:`sec_display_deprecation_warnings`. +A :class:`~semver.version.Version` instance can be created in different ways: +* From a Unicode string:: -A :class:`semver.Version` instance can be created in different ways: - -* From a string (a Unicode string in Python 2):: - - >>> semver.Version.parse("3.4.5-pre.2+build.4") + >>> from semver.version import Version + >>> Version.parse("3.4.5-pre.2+build.4") Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - >>> semver.Version.parse(u"5.3.1") + >>> Version.parse(u"5.3.1") Version(major=5, minor=3, patch=1, prerelease=None, build=None) * From a byte string:: - >>> semver.Version.parse(b"2.3.4") + >>> Version.parse(b"2.3.4") Version(major=2, minor=3, patch=4, prerelease=None, build=None) * From individual parts by a dictionary:: >>> d = {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - >>> semver.Version(**d) + >>> Version(**d) Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') Keep in mind, the ``major``, ``minor``, ``patch`` parts has to - be positive. + be positive integers or strings: - >>> semver.Version(-1) + >>> d = {'major': -3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} + >>> Version(**d) Traceback (most recent call last): ... ValueError: 'major' is negative. A version can only be positive. - As a minimum requirement, your dictionary needs at least the - be positive. - - >>> semver.Version(-1) - Traceback (most recent call last): - ... - ValueError: 'major' is negative. A version can only be positive. - - As a minimum requirement, your dictionary needs at least the - be positive. - - >>> semver.Version(-1) - Traceback (most recent call last): - ... - ValueError: 'major' is negative. A version can only be positive. - As a minimum requirement, your dictionary needs at least the ``major`` key, others can be omitted. You get a ``TypeError`` if your dictionary contains invalid keys. @@ -105,20 +89,23 @@ A :class:`semver.Version` instance can be created in different ways: * From a tuple:: >>> t = (3, 5, 6) - >>> semver.Version(*t) + >>> Version(*t) Version(major=3, minor=5, patch=6, prerelease=None, build=None) You can pass either an integer or a string for ``major``, ``minor``, or ``patch``:: - >>> semver.Version("3", "5", 6) + >>> Version("3", "5", 6) Version(major=3, minor=5, patch=6, prerelease=None, build=None) -The old, deprecated module level functions are still available. If you -need them, they return different builtin objects (string and dictionary). +The old, deprecated module level functions are still available but +using them are discoraged. They are available to convert old code +to semver3. + +If you need them, they return different builtin objects (string and dictionary). Keep in mind, once you have converted a version into a string or dictionary, it's an ordinary builtin object. It's not a special version object like -the :class:`semver.Version` class anymore. +the :class:`~semver.version.Version` class anymore. Depending on your use case, the following methods are available: @@ -136,7 +123,7 @@ Depending on your use case, the following methods are available: >>> semver.parse("3.4.5-pre.2+build.4") OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) - If you pass an invalid version string you will get a ``ValueError``:: + If you pass an invalid version string you will get a :py:exc:`ValueError`:: >>> semver.parse("1.2") Traceback (most recent call last): @@ -148,36 +135,23 @@ Parsing a Version String ------------------------ "Parsing" in this context means to identify the different parts in a string. +Use the function :func:`Version.parse `:: - -* With :func:`semver.parse_version_info`:: - - >>> semver.parse_version_info("3.4.5-pre.2+build.4") - Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') - -* With :func:`semver.Version.parse` (basically the same as - :func:`semver.parse_version_info`):: - - >>> semver.Version.parse("3.4.5-pre.2+build.4") + >>> Version.parse("3.4.5-pre.2+build.4") Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4') -* With :func:`semver.parse`:: - - >>> semver.parse("3.4.5-pre.2+build.4") == {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} - True - Checking for a Valid Semver Version ----------------------------------- If you need to check a string if it is a valid semver version, use the -classmethod :func:`semver.Version.isvalid`: +classmethod :func:`Version.isvalid `: .. code-block:: python - >>> semver.Version.isvalid("1.0.0") + >>> Version.isvalid("1.0.0") True - >>> semver.Version.isvalid("invalid") + >>> Version.isvalid("invalid") False @@ -186,12 +160,12 @@ classmethod :func:`semver.Version.isvalid`: Accessing Parts of a Version Through Names ------------------------------------------ -The :class:`semver.Version` contains attributes to access the different +The :class:`~semver.version.Version` class contains attributes to access the different parts of a version: .. code-block:: python - >>> v = semver.Version.parse("3.4.5-pre.2+build.4") + >>> v = Version.parse("3.4.5-pre.2+build.4") >>> v.major 3 >>> v.minor @@ -203,8 +177,8 @@ parts of a version: >>> v.build 'build.4' -However, the attributes are read-only. You cannot change an attribute. -If you do, you get an ``AttributeError``:: +However, the attributes are read-only. You cannot change any of the above attributes. +If you do, you get an :py:exc:`AttributeError`:: >>> v.minor = 5 Traceback (most recent call last): @@ -213,16 +187,16 @@ If you do, you get an ``AttributeError``:: If you need to replace different parts of a version, refer to section :ref:`sec.replace.parts`. -In case you need the different parts of a version stepwise, iterate over the :class:`semver.Version` instance:: +In case you need the different parts of a version stepwise, iterate over the :class:`~semver.version.Version` instance:: - >>> for item in semver.Version.parse("3.4.5-pre.2+build.4"): + >>> for item in Version.parse("3.4.5-pre.2+build.4"): ... print(item) 3 4 5 pre.2 build.4 - >>> list(semver.Version.parse("3.4.5-pre.2+build.4")) + >>> list(Version.parse("3.4.5-pre.2+build.4")) [3, 4, 5, 'pre.2', 'build.4'] @@ -234,15 +208,15 @@ Accessing Parts Through Index Numbers .. versionadded:: 2.10.0 Another way to access parts of a version is to use an index notation. The underlying -:class:`Version ` object allows to access its data through -the magic method :func:`__getitem__ `. +:class:`~semver.version.Version` object allows to access its data through +the magic method :func:`~semver.version.Version.__getitem__`. For example, the ``major`` part can be accessed by index number 0 (zero). Likewise the other parts: .. code-block:: python - >>> ver = semver.Version.parse("10.3.2-pre.5+build.10") + >>> ver = Version.parse("10.3.2-pre.5+build.10") >>> ver[0], ver[1], ver[2], ver[3], ver[4] (10, 3, 2, 'pre.5', 'build.10') @@ -261,11 +235,11 @@ Or, as an alternative, you can pass a :func:`slice` object: >>> ver[sl] (10, 3, 2) -Negative numbers or undefined parts raise an :class:`IndexError` exception: +Negative numbers or undefined parts raise an :py:exc:`IndexError` exception: .. code-block:: python - >>> ver = semver.Version.parse("10.3.2") + >>> ver = Version.parse("10.3.2") >>> ver[3] Traceback (most recent call last): ... @@ -281,9 +255,9 @@ Replacing Parts of a Version ---------------------------- If you want to replace different parts of a version, but leave other parts -unmodified, use the function :func:`semver.Version.replace` or :func:`semver.replace`: +unmodified, use the function :func:`replace `: -* From a :class:`semver.Version` instance:: +* From a :class:`Version ` instance:: >>> version = semver.Version.parse("1.4.5-pre.1+build.6") >>> version.replace(major=2, minor=2) @@ -312,25 +286,25 @@ If you pass invalid keys you get an exception:: Converting a Version instance into Different Types ------------------------------------------------------ -Sometimes it is needed to convert a :class:`semver.Version` instance into +Sometimes it is needed to convert a :class:`Version ` instance into a different type. For example, for displaying or to access all parts. -It is possible to convert a :class:`semver.Version` instance: +It is possible to convert a :class:`Version ` instance: * Into a string with the builtin function :func:`str`:: - >>> str(semver.Version.parse("3.4.5-pre.2+build.4")) + >>> str(Version.parse("3.4.5-pre.2+build.4")) '3.4.5-pre.2+build.4' -* Into a dictionary with :func:`semver.Version.to_dict`:: +* Into a dictionary with :func:`to_dict `:: - >>> v = semver.Version(major=3, minor=4, patch=5) + >>> v = Version(major=3, minor=4, patch=5) >>> v.to_dict() OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) -* Into a tuple with :func:`semver.Version.to_tuple`:: +* Into a tuple with :func:`to_tuple `:: - >>> v = semver.Version(major=5, minor=4, patch=2) + >>> v = Version(major=5, minor=4, patch=2) >>> v.to_tuple() (5, 4, 2, None, None) @@ -341,27 +315,27 @@ Raising Parts of a Version The ``semver`` module contains the following functions to raise parts of a version: -* :func:`semver.Version.bump_major`: raises the major part and set all other parts to +* :func:`Version.bump_major `: raises the major part and set all other parts to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.Version.bump_minor`: raises the minor part and sets ``patch`` to zero. +* :func:`Version.bump_minor `: raises the minor part and sets ``patch`` to zero. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.Version.bump_patch`: raises the patch part. Set ``prerelease`` and +* :func:`Version.bump_patch `: raises the patch part. Set ``prerelease`` and ``build`` to ``None``. -* :func:`semver.Version.bump_prerelease`: raises the prerelease part and set +* :func:`Version.bump_prerelease `: raises the prerelease part and set ``build`` to ``None``. -* :func:`semver.Version.bump_build`: raises the build part. +* :func:`Version.bump_build `: raises the build part. .. code-block:: python - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_major()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_major()) '4.0.0' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_minor()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_minor()) '3.5.0' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_patch()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_patch()) '3.4.6' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_prerelease()) '3.4.5-pre.3' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").bump_build()) + >>> str(Version.parse("3.4.5-pre.2+build.4").bump_build()) '3.4.5-pre.2+build.5' Likewise the module level functions :func:`semver.bump_major`. @@ -371,23 +345,23 @@ Increasing Parts of a Version Taking into Account Prereleases ------------------------------------------------------------- .. versionadded:: 2.10.0 - Added :func:`semver.Version.next_version`. + Added :func:`Version.next_version `. If you want to raise your version and take prereleases into account, -the function :func:`semver.Version.next_version` would perhaps a -better fit. +the function :func:`next_version ` +would perhaps a better fit. .. code-block:: python - >>> v = semver.Version.parse("3.4.5-pre.2+build.4") + >>> v = Version.parse("3.4.5-pre.2+build.4") >>> str(v.next_version(part="prerelease")) '3.4.5-pre.3' - >>> str(semver.Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) + >>> str(Version.parse("3.4.5-pre.2+build.4").next_version(part="patch")) '3.4.5' - >>> str(semver.Version.parse("3.4.5+build.4").next_version(part="patch")) + >>> str(Version.parse("3.4.5+build.4").next_version(part="patch")) '3.4.5' - >>> str(semver.Version.parse("0.1.4").next_version("prerelease")) + >>> str(Version.parse("0.1.4").next_version("prerelease")) '0.1.5-rc.1' @@ -410,23 +384,23 @@ To compare two versions depends on your type: The return value is negative if ``version1 < version2``, zero if ``version1 == version2`` and strictly positive if ``version1 > version2``. -* **Two** :class:`semver.Version` **instances** +* **Two** :class:`Version ` **instances** Use the specific operator. Currently, the operators ``<``, ``<=``, ``>``, ``>=``, ``==``, and ``!=`` are supported:: - >>> v1 = semver.Version.parse("3.4.5") - >>> v2 = semver.Version.parse("3.5.1") + >>> v1 = Version.parse("3.4.5") + >>> v2 = Version.parse("3.5.1") >>> v1 < v2 True >>> v1 > v2 False -* **A** :class:`semver.Version` **type and a** :func:`tuple` **or** :func:`list` +* **A** :class:`Version ` **type and a** :func:`tuple` **or** :func:`list` - Use the operator as with two :class:`semver.Version` types:: + Use the operator as with two :class:`Version ` types:: - >>> v = semver.Version.parse("3.4.5") + >>> v = Version.parse("3.4.5") >>> v > (1, 0) True >>> v < [3, 5] @@ -439,7 +413,7 @@ To compare two versions depends on your type: >>> [3, 5] > v True -* **A** :class:`semver.Version` **type and a** :func:`str` +* **A** :class:`Version ` **type and a** :func:`str` You can use also raw strings to compare:: @@ -455,14 +429,14 @@ To compare two versions depends on your type: >>> "3.5.0" > v True - However, if you compare incomplete strings, you get a :class:`ValueError` exception:: + However, if you compare incomplete strings, you get a :py:exc:`ValueError` exception:: >>> v > "1.0" Traceback (most recent call last): ... ValueError: 1.0 is not valid SemVer string -* **A** :class:`semver.Version` **type and a** :func:`dict` +* **A** :class:`Version ` **type and a** :func:`dict` You can also use a dictionary. In contrast to strings, you can have an "incomplete" version (as the other parts are set to zero):: @@ -475,7 +449,7 @@ To compare two versions depends on your type: >>> dict(major=1) < v True - If the dictionary contains unknown keys, you get a :class:`TypeError` exception:: + If the dictionary contains unknown keys, you get a :py:exc:`TypeError` exception:: >>> v > dict(major=1, unknown=42) Traceback (most recent call last): @@ -499,16 +473,16 @@ Version equality means for semver, that major, minor, patch, and prerelease parts are equal in both versions you compare. The build part is ignored. For example:: - >>> v = semver.Version.parse("1.2.3-rc4+1e4664d") + >>> v = Version.parse("1.2.3-rc4+1e4664d") >>> v == "1.2.3-rc4+dedbeef" True -This also applies when a :class:`semver.Version` is a member of a set, or a +This also applies when a :class:`Version ` is a member of a set, or a dictionary key:: >>> d = {} - >>> v1 = semver.Version.parse("1.2.3-rc4+1e4664d") - >>> v2 = semver.Version.parse("1.2.3-rc4+dedbeef") + >>> v1 = Version.parse("1.2.3-rc4+1e4664d") + >>> v2 = Version.parse("1.2.3-rc4+dedbeef") >>> d[v1] = 1 >>> d[v2] 1 @@ -554,34 +528,36 @@ Getting Minimum and Maximum of Multiple Versions The functions :func:`semver.max_ver` and :func:`semver.min_ver` are deprecated in favor of their builtin counterparts :func:`max` and :func:`min`. -Since :class:`semver.Version` implements :func:`__gt__()` and :func:`__lt__()`, it can be used with builtins requiring +Since :class:`Version ` implements +:func:`__gt__ ` and +:func:`__lt__ `, it can be used with builtins requiring: .. code-block:: python - >>> max([semver.Version(0, 1, 0), semver.Version(0, 2, 0), semver.Version(0, 1, 3)]) + >>> max([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) Version(major=0, minor=2, patch=0, prerelease=None, build=None) - >>> min([semver.Version(0, 1, 0), semver.Version(0, 2, 0), semver.Version(0, 1, 3)]) + >>> min([Version(0, 1, 0), Version(0, 2, 0), Version(0, 1, 3)]) Version(major=0, minor=1, patch=0, prerelease=None, build=None) Incidentally, using :func:`map`, you can get the min or max version of any number of versions of the same type -(convertible to :class:`semver.Version`). +(convertible to :class:`Version `). For example, here are the maximum and minimum versions of a list of version strings: .. code-block:: python - >>> str(max(map(semver.Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> max(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) '2.1.0' - >>> str(min(map(semver.Version.parse, ['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99']))) + >>> min(['1.1.0', '1.2.0', '2.1.0', '0.5.10', '0.4.99'], key=Version.parse) '0.4.99' And the same can be done with tuples: .. code-block:: python - >>> max(map(lambda v: semver.Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + >>> max(map(lambda v: Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() (2, 1, 0, None, None) - >>> min(map(lambda v: semver.Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() + >>> min(map(lambda v: Version(*v), [(1, 1, 0), (1, 2, 0), (2, 1, 0), (0, 5, 10), (0, 4, 99)])).to_tuple() (0, 4, 99, None, None) For dictionaries, it is very similar to finding the max version tuple: see :ref:`sec.convert.versions`. @@ -616,7 +592,7 @@ information and returns a tuple with two items: :language: python -The function returns a *tuple*, containing a :class:`Version` +The function returns a *tuple*, containing a :class:`Version ` instance or None as the first element and the rest as the second element. The second element (the rest) can be used to make further adjustments. @@ -649,7 +625,7 @@ them with code which is compatible for future versions: * :func:`semver.bump_major`, :func:`semver.bump_minor`, :func:`semver.bump_patch`, :func:`semver.bump_prerelease`, :func:`semver.bump_build` - Replace them with the respective methods of the :class:`semver.Version` + Replace them with the respective methods of the :class:`Version ` class. For example, the function :func:`semver.bump_major` is replaced by :func:`semver.Version.bump_major` and calling the ``str(versionobject)``: @@ -657,7 +633,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.bump_major("3.4.5") - >>> s2 = str(semver.Version.parse("3.4.5").bump_major()) + >>> s2 = str(Version.parse("3.4.5").bump_major()) >>> s1 == s2 True @@ -681,7 +657,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.format_version(5, 4, 3, 'pre.2', 'build.1') - >>> s2 = str(semver.Version(5, 4, 3, 'pre.2', 'build.1')) + >>> s2 = str(Version(5, 4, 3, 'pre.2', 'build.1')) >>> s1 == s2 True @@ -692,7 +668,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.max_ver("1.2.3", "1.2.4") - >>> s2 = str(max(map(semver.Version.parse, ("1.2.3", "1.2.4")))) + >>> s2 = str(max(map(Version.parse, ("1.2.3", "1.2.4")))) >>> s1 == s2 True @@ -703,7 +679,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.min_ver("1.2.3", "1.2.4") - >>> s2 = str(min(map(semver.Version.parse, ("1.2.3", "1.2.4")))) + >>> s2 = str(min(map(Version.parse, ("1.2.3", "1.2.4")))) >>> s1 == s2 True @@ -715,7 +691,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> v1 = semver.parse("1.2.3") - >>> v2 = semver.Version.parse("1.2.3").to_dict() + >>> v2 = Version.parse("1.2.3").to_dict() >>> v1 == v2 True @@ -726,7 +702,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> v1 = semver.parse_version_info("3.4.5") - >>> v2 = semver.Version.parse("3.4.5") + >>> v2 = Version.parse("3.4.5") >>> v1 == v2 True @@ -737,7 +713,7 @@ them with code which is compatible for future versions: .. code-block:: python >>> s1 = semver.replace("1.2.3", major=2, patch=10) - >>> s2 = str(semver.Version.parse('1.2.3').replace(major=2, patch=10)) + >>> s2 = str(Version.parse('1.2.3').replace(major=2, patch=10)) >>> s1 == s2 True @@ -785,7 +761,7 @@ Creating Subclasses from Version If you do not like creating functions to modify the behavior of semver (as shown in section :ref:`sec_dealing_with_invalid_versions`), you can -also create a subclass of the :class:`Version` class. +also create a subclass of the :class:`Version ` class. For example, if you want to output a "v" prefix before a version, but the other behavior is the same, use the following code: @@ -811,4 +787,3 @@ the original class: Traceback (most recent call last): ... ValueError: '1.2.4': not a valid semantic version tag. Must start with 'v' or 'V' - diff --git a/pyproject.toml b/pyproject.toml index 1b406dac..769b13d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,22 +9,15 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 -target-version = ['py36', 'py37', 'py38'] +target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] # diff = true -exclude = ''' -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.mypy_cache - | \.tox - | \.venv - | \.env - | _build - | build - | dist - )/ -) +extend-exclude = ''' +# A regex preceded with ^/ will apply only to files and directories +# in the root of the project. +^/*.py +''' +include = ''' +^/setup.py ''' [tool.towncrier] diff --git a/release-procedure.md b/release-procedure.md index db9ed1b5..251f23f0 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -5,22 +5,31 @@ create a new release. ## Prepare the Release -1. Verify that issues about new release are closed https://github.com/python-semver/python-semver/issues. +1. Verify: -1. Verify that no pull requests that should be included in this release haven't been left out https://github.com/python-semver/python-semver/pulls. + * all issues for a new release are closed: . -1. Verify that continuous integration for latest build was passing https://travis-ci.com/python-semver/python-semver. + * that all pull requests that should be included in this release are merged: . -1. Create a new branch `release/VERSION`. + * that continuous integration for latest build was passing: . + +1. Create a new branch `release/`. 1. If one or several supported Python versions have been removed or added, verify that the 3 following files have been updated: - * [setup.py](https://github.com/python-semver/python-semver/blob/master/setup.py) - * [tox.ini](https://github.com/python-semver/python-semver/blob/master/tox.ini) - * [.travis.yml](https://github.com/python-semver/python-semver/blob/master/.travis.yml) + * `setup.cfg` + * `tox.ini` + * `.git/workflows/pythonpackage.yml` + +1. Verify that the version has been updated and follow + : + + * `src/semver/__about__.py` + * `docs/usage.rst` 1. Add eventually new contributor(s) to [CONTRIBUTORS](https://github.com/python-semver/python-semver/blob/master/CONTRIBUTORS). -1. Verify that `__version__` in [semver.py](https://github.com/python-semver/python-semver/blob/master/semver.py) have been updated and follow https://semver.org. + +1. Check if all changelog entries are created. If some are missing, [create them](https://python-semver.readthedocs.io/en/latest/development.html#adding-a-changelog-entry). 1. Show the new draft [CHANGELOG](https://github.com/python-semver/python-semver/blob/master/CHANGELOG.rst) entry for the latest release with: @@ -36,32 +45,47 @@ create a new release. $ tox -e docs +1. Commit all changes, push, and create a pull request. + ## Create the New Release -1. Ensure that long description (ie [README.rst](https://github.com/python-semver/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` +1. Ensure that long description ([README.rst](https://github.com/python-semver/python-semver/blob/master/README.rst)) can be correctly rendered by Pypi using `restview --long-description` + +1. Clean up your local Git repository. Be careful, + as it **will remove all files** which are not + versioned by Git: + + $ git clean -xfd + + Before you create your distribution files, clean + the directory too: + + $ rm dist/* + +1. Create the distribution files (wheel and source): + + $ tox -e prepare-dist 1. Upload the wheel and source to TestPyPI first: - ```bash - $ git clean -xfd - $ rm dist/* - $ python3 setup.py sdist bdist_wheel + ```bash $ twine upload --repository-url https://test.pypi.org/legacy/ dist/* ``` - If you have a `~/.pypirc` with a `testpyi` section, the upload can be + If you have a `~/.pypirc` with a `testpypi` section, the upload can be simplified: - $ twine upload --repository testpyi dist/* + $ twine upload --repository testpypi dist/* 1. Check if everything is okay with the wheel. + Check also the web site `https://test.pypi.org/project//` 1. Upload to PyPI: ```bash $ git clean -xfd - $ python setup.py register sdist bdist_wheel + $ tox -e prepare-dist $ twine upload dist/* ``` @@ -78,4 +102,6 @@ create a new release. document the new release. Usually it's enough to take it from a commit message or the tag description. +1. Announce it in . + You're done! Celebrate! diff --git a/setup.cfg b/setup.cfg index 681240ac..de2d226c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,8 +14,8 @@ author_email = k-bx@k-bx.com maintainer = Sebastien Celles, Tom Schraitle maintainer_email = s.celles@gmail.com url = https://github.com/python-semver/python-semver -download_url = https://github.com/python-semver/python-semver/downloads project_urls = + Changelog = https://python-semver.readthedocs.io/en/latest/changelog.html Documentation = https://python-semver.rtfd.io Releases = https://github.com/python-semver/python-semver/releases Bug Tracker = https://github.com/python-semver/python-semver/issues @@ -30,6 +30,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Topic :: Software Development :: Libraries :: Python Modules license = BSD diff --git a/src/semver/__about__.py b/src/semver/__about__.py index aa293425..fa448ebe 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -4,11 +4,19 @@ 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.0-dev.2" +__version__ = "3.0.0-dev.3" #: Original semver author __author__ = "Kostiantyn Rybnikov" diff --git a/src/semver/_deprecated.py b/src/semver/_deprecated.py index 545a2438..61ceae12 100644 --- a/src/semver/_deprecated.py +++ b/src/semver/_deprecated.py @@ -7,11 +7,11 @@ import warnings from functools import partial, wraps from types import FrameType -from typing import Type, Union, Callable, cast +from typing import Type, Callable, cast from . import cli from .version import Version -from ._types import F, String +from ._types import Decorator, F, String def deprecated( @@ -19,7 +19,7 @@ def deprecated( replace: str = None, version: str = None, category: Type[Warning] = DeprecationWarning, -) -> Union[Callable[..., F], partial]: +) -> Decorator: """ Decorates a function to output a deprecation warning. diff --git a/src/semver/_types.py b/src/semver/_types.py index 823c7349..7afb6ff0 100644 --- a/src/semver/_types.py +++ b/src/semver/_types.py @@ -1,3 +1,6 @@ +"""Typing for semver.""" + +from functools import partial from typing import Union, Optional, Tuple, Dict, Iterable, Callable, TypeVar VersionPart = Union[int, Optional[str]] @@ -6,3 +9,4 @@ 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 index ca400373..65ca5187 100644 --- a/src/semver/cli.py +++ b/src/semver/cli.py @@ -1,4 +1,14 @@ -"""CLI parsing for :command:`pysemver` command.""" +""" +CLI parsing for :command:`pysemver` command. + +Each command in :command:`pysemver` is mapped to a ``cmd_`` function. +The :func:`main ` function calls +:func:`createparser ` and +:func:`process ` to parse and process +all the commandline options. + +The result of each command is printed on stdout. +""" import argparse import sys diff --git a/src/semver/version.py b/src/semver/version.py index 64353011..9e02544f 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -29,41 +29,7 @@ Comparator = Callable[["Version", Comparable], bool] -def cmp(a, b): # TODO: type hints - """Return negative if ab.""" - return (a > b) - (a < b) - - -def ensure_str(s: String, encoding="utf-8", errors="strict") -> str: - # Taken from six project - """ - Coerce *s* to `str`. - - * `str` -> `str` - * `bytes` -> decoded to `str` - - :param s: the string to convert - :type s: str | bytes - :param encoding: the encoding to apply, defaults to "utf-8" - :type encoding: str - :param errors: set a different error handling scheme, - defaults to "strict". - Other possible values are `ignore`, `replace`, and - `xmlcharrefreplace` as well as any other name - registered with :func:`codecs.register_error`. - :type errors: str - :raises TypeError: if ``s`` is not str or bytes type - :return: the converted string - :rtype: str - """ - if isinstance(s, bytes): - s = s.decode(encoding, errors) - elif not isinstance(s, String.__args__): # type: ignore - raise TypeError("not expecting type '%s'" % type(s)) - return s - - -def comparator(operator: Comparator) -> Comparator: +def _comparator(operator: Comparator) -> Comparator: """Wrap a Version binary op method in a type-check.""" @wraps(operator) @@ -76,39 +42,15 @@ def wrapper(self: "Version", other: Comparable) -> bool: *String.__args__, # type: ignore ) if not isinstance(other, comparable_types): - raise TypeError( - "other type %r must be in %r" % (type(other), comparable_types) - ) + return NotImplemented return operator(self, other) return wrapper -def _nat_cmp(a, b): # TODO: type hints - def convert(text): - return int(text) if re.match("^[0-9]+$", text) else text - - def split_key(key): - return [convert(c) for c in key.split(".")] - - def cmp_prerelease_tag(a, b): - if isinstance(a, int) and isinstance(b, int): - return cmp(a, b) - elif isinstance(a, int): - return -1 - elif isinstance(b, int): - return 1 - else: - return cmp(a, b) - - a, b = a or "", b or "" - a_parts, b_parts = split_key(a), split_key(b) - for sub_a, sub_b in zip(a_parts, b_parts): - cmp_result = cmp_prerelease_tag(sub_a, sub_b) - if cmp_result != 0: - return cmp_result - else: - return cmp(len(a), len(b)) +def _cmp(a, b): # TODO: type hints + """Return negative if ab.""" + return (a > b) - (a < b) class Version: @@ -171,6 +113,29 @@ def __init__( self._prerelease = None if prerelease is None else str(prerelease) self._build = None if build is None else str(build) + @classmethod + def _nat_cmp(cls, a, b): # TODO: type hints + def cmp_prerelease_tag(a, b): + if isinstance(a, int) and isinstance(b, int): + return _cmp(a, b) + elif isinstance(a, int): + return -1 + elif isinstance(b, int): + return 1 + else: + return _cmp(a, b) + + a, b = a or "", b or "" + a_parts, b_parts = a.split("."), b.split(".") + a_parts = [int(x) if re.match(r"^\d+$", x) else x for x in a_parts] + b_parts = [int(x) if re.match(r"^\d+$", x) else x for x in b_parts] + for sub_a, sub_b in zip(a_parts, b_parts): + cmp_result = cmp_prerelease_tag(sub_a, sub_b) + if cmp_result != 0: + return cmp_result + else: + return _cmp(len(a), len(b)) + @property def major(self) -> int: """The major part of a version (read-only).""" @@ -218,7 +183,7 @@ def build(self, value): def to_tuple(self) -> VersionTuple: """ - Convert the VersionInfo object to a tuple. + Convert the Version object to a tuple. .. versionadded:: 2.10.0 Renamed ``VersionInfo._astuple`` to ``VersionInfo.to_tuple`` to @@ -233,7 +198,7 @@ def to_tuple(self) -> VersionTuple: def to_dict(self) -> VersionDict: """ - Convert the VersionInfo object to an OrderedDict. + Convert the Version object to an OrderedDict. .. versionadded:: 2.10.0 Renamed ``VersionInfo._asdict`` to ``VersionInfo.to_dict`` to @@ -257,7 +222,7 @@ def to_dict(self) -> VersionDict: ) def __iter__(self) -> VersionIterator: - """Implement iter(self).""" + """Return iter(self).""" yield from self.to_tuple() @staticmethod @@ -300,7 +265,6 @@ def bump_minor(self) -> "Version": :return: new object with the raised minor part - >>> ver = semver.parse("3.4.5") >>> ver.bump_minor() Version(major=3, minor=5, patch=0, prerelease=None, build=None) @@ -313,8 +277,7 @@ def bump_patch(self) -> "Version": Raise the patch part of the version, return a new object but leave self untouched. - :return: new object with the raised patch part - + :return: new object with the raised patch part >>> ver = semver.parse("3.4.5") >>> ver.bump_patch() @@ -328,7 +291,7 @@ def bump_prerelease(self, token: str = "rc") -> "Version": Raise the prerelease part of the version, return a new object but leave self untouched. - :param token: defaults to 'rc' + :param token: defaults to ``rc`` :return: new object with the raised prerelease part >>> ver = semver.parse("3.4.5") @@ -345,7 +308,7 @@ def bump_build(self, token: str = "build") -> "Version": Raise the build part of the version, return a new object but leave self untouched. - :param token: defaults to 'build' + :param token: defaults to ``build`` :return: new object with the raised build part >>> ver = semver.parse("3.4.5-rc.1+build.9") @@ -365,7 +328,6 @@ def compare(self, other: Comparable) -> int: :return: The return value is negative if ver1 < ver2, zero if ver1 == ver2 and strictly positive if ver1 > ver2 - >>> semver.compare("2.0.0") -1 >>> semver.compare("1.0.0") @@ -390,12 +352,12 @@ def compare(self, other: Comparable) -> int: v1 = self.to_tuple()[:3] v2 = other.to_tuple()[:3] - x = cmp(v1, v2) + x = _cmp(v1, v2) if x: return x rc1, rc2 = self.prerelease, other.prerelease - rccmp = _nat_cmp(rc1, rc2) + rccmp = self._nat_cmp(rc1, rc2) if not rccmp: return 0 @@ -453,27 +415,27 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": version = version.bump_patch() return version.bump_prerelease(prerelease_token) - @comparator + @_comparator def __eq__(self, other: Comparable) -> bool: # type: ignore return self.compare(other) == 0 - @comparator + @_comparator def __ne__(self, other: Comparable) -> bool: # type: ignore return self.compare(other) != 0 - @comparator + @_comparator def __lt__(self, other: Comparable) -> bool: return self.compare(other) < 0 - @comparator + @_comparator def __le__(self, other: Comparable) -> bool: return self.compare(other) <= 0 - @comparator + @_comparator def __gt__(self, other: Comparable) -> bool: return self.compare(other) > 0 - @comparator + @_comparator def __ge__(self, other: Comparable) -> bool: return self.compare(other) >= 0 @@ -481,14 +443,17 @@ def __getitem__( self, index: Union[int, slice] ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]: """ - self.__getitem__(index) <==> self[index] Implement getitem. If the part - requested is undefined, or a part of the range requested is undefined, - it will throw an index error. Negative indices are not supported. + self.__getitem__(index) <==> self[index] Implement getitem. + + If the part requested is undefined, or a part of the range requested + is undefined, it will throw an index error. + Negative indices are not supported. :param Union[int, slice] index: a positive integer indicating the offset or a :func:`slice` object :raises IndexError: if index is beyond the range or a part is None :return: the requested part of the version at position index + >>> ver = semver.Version.parse("3.4.5") >>> ver[0], ver[1], ver[2] (3, 4, 5) @@ -519,7 +484,6 @@ def __repr__(self) -> str: return "%s(%s)" % (type(self).__name__, s) def __str__(self) -> str: - """str(self)""" version = "%d.%d.%d" % (self.major, self.minor, self.patch) if self.prerelease: version += "-%s" % self.prerelease @@ -533,7 +497,9 @@ def __hash__(self) -> int: def finalize_version(self) -> "Version": """ Remove any prerelease and build metadata from the version. + :return: a new instance with the finalized version string + >>> str(semver.Version.parse('1.2.3-rc.5').finalize_version()) '1.2.3' """ @@ -545,12 +511,12 @@ def match(self, match_expr: str) -> bool: Compare self to match a match expression. :param match_expr: operator and version; valid operators are - < smaller than - > greater than - >= greator or equal than - <= smaller or equal than - == equal - != not equal + ``<``` smaller than + ``>`` greater than + ``>=`` greator or equal than + ``<=`` smaller or equal than + ``==`` equal + ``!=`` not equal :return: True if the expression matches the version, otherwise False >>> semver.Version.parse("2.0.0").match(">=1.0.0") @@ -589,24 +555,29 @@ def match(self, match_expr: str) -> bool: @classmethod def parse(cls, version: String) -> "Version": """ - Parse version string to a VersionInfo instance. + Parse version string to a Version instance. .. versionchanged:: 2.11.0 Changed method from static to classmethod to allow subclasses. :param version: version string - :return: a :class:`VersionInfo` instance + :return: a new :class:`Version` instance :raises ValueError: if version is invalid + :raises TypeError: if version contains the wrong type >>> semver.Version.parse('3.4.5-pre.2+build.4') - VersionInfo(major=3, minor=4, patch=5, \ + Version(major=3, minor=4, patch=5, \ prerelease='pre.2', build='build.4') """ - version_str = ensure_str(version) - match = cls._REGEX.match(version_str) + if isinstance(version, bytes): + version = version.decode("UTF-8") + elif not isinstance(version, String.__args__): # type: ignore + raise TypeError("not expecting type '%s'" % type(version)) + + match = cls._REGEX.match(version) if match is None: - raise ValueError(f"{version_str} is not valid SemVer string") + raise ValueError(f"{version} is not valid SemVer string") matched_version_parts: Dict[str, Any] = match.groupdict() @@ -624,7 +595,7 @@ def replace(self, **parts: Union[int, Optional[str]]) -> "Version": ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` :return: the new :class:`Version` object with the changed parts - :raises TypeError: if ``parts`` contains invalid keys + :raises TypeError: if ``parts`` contain invalid keys """ version = self.to_dict() version.update(parts) @@ -632,7 +603,7 @@ def replace(self, **parts: Union[int, Optional[str]]) -> "Version": return Version(**version) # type: ignore except TypeError: unknownkeys = set(parts) - set(self.to_dict()) - error = "replace() got %d unexpected keyword " "argument(s): %s" % ( + error = "replace() got %d unexpected keyword argument(s): %s" % ( len(unknownkeys), ", ".join(unknownkeys), ) diff --git a/tests/conftest.py b/tests/conftest.py index f7f927cf..0450e0ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ @pytest.fixture(autouse=True) def add_semver(doctest_namespace): - doctest_namespace["Version"] = semver.Version + doctest_namespace["Version"] = semver.version.Version doctest_namespace["semver"] = semver doctest_namespace["coerce"] = coerce doctest_namespace["SemVerWithVPrefix"] = SemVerWithVPrefix diff --git a/tests/test_typeerror-274.py b/tests/test_typeerror-274.py index 61480bcf..326304b8 100644 --- a/tests/test_typeerror-274.py +++ b/tests/test_typeerror-274.py @@ -1,95 +1,14 @@ -import sys - import pytest - import semver -import semver.version - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - - -def ensure_binary(s, encoding="utf-8", errors="strict"): - """ - Coerce ``s`` to bytes. - - * `str` -> encoded to `bytes` - * `bytes` -> `bytes` - - :param s: the string to convert - :type s: str | bytes - :param encoding: the encoding to apply, defaults to "utf-8" - :type encoding: str - :param errors: set a different error handling scheme; - other possible values are `ignore`, `replace`, and - `xmlcharrefreplace` as well as any other name - registered with :func:`codecs.register_error`. - Defaults to "strict". - :type errors: str - :raises TypeError: if ``s`` is not str or bytes type - :return: the converted string - :rtype: str - """ - if isinstance(s, str): - return s.encode(encoding, errors) - elif isinstance(s, bytes): - return s - else: - raise TypeError("not expecting type '%s'" % type(s)) -def test_should_work_with_string_and_unicode(): +def test_should_work_with_string_and_bytes(): result = semver.compare("1.1.0", b"1.2.2") assert result == -1 result = semver.compare(b"1.1.0", "1.2.2") assert result == -1 -class TestEnsure: - # From six project - # grinning face emoji - UNICODE_EMOJI = "\U0001F600" - BINARY_EMOJI = b"\xf0\x9f\x98\x80" - - def test_ensure_binary_raise_type_error(self): - with pytest.raises(TypeError): - semver.version.ensure_str(8) - - def test_errors_and_encoding(self): - ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="ignore") - with pytest.raises(UnicodeEncodeError): - ensure_binary(self.UNICODE_EMOJI, encoding="latin-1", errors="strict") - - def test_ensure_binary_raise(self): - converted_unicode = ensure_binary( - self.UNICODE_EMOJI, encoding="utf-8", errors="strict" - ) - converted_binary = ensure_binary( - self.BINARY_EMOJI, encoding="utf-8", errors="strict" - ) - - # PY3: str -> bytes - assert converted_unicode == self.BINARY_EMOJI and isinstance( - converted_unicode, bytes - ) - # PY3: bytes -> bytes - assert converted_binary == self.BINARY_EMOJI and isinstance( - converted_binary, bytes - ) - - def test_ensure_str(self): - converted_unicode = semver.version.ensure_str( - self.UNICODE_EMOJI, encoding="utf-8", errors="strict" - ) - converted_binary = semver.version.ensure_str( - self.BINARY_EMOJI, encoding="utf-8", errors="strict" - ) - - # PY3: str -> str - assert converted_unicode == self.UNICODE_EMOJI and isinstance( - converted_unicode, str - ) - # PY3: bytes -> str - assert converted_binary == self.UNICODE_EMOJI and isinstance( - converted_unicode, str - ) +def test_should_not_work_with_invalid_args(): + with pytest.raises(TypeError): + semver.version.Version.parse(8) diff --git a/tox.ini b/tox.ini index 73fbfc58..8c7eb5e5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,17 @@ [tox] envlist = - flake8 + checks py{36,37,38,39,310} - docs - mypy isolated_build = True +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + [testenv] description = Run test suite for {basepython} @@ -67,15 +73,7 @@ deps = -r{toxinidir}/docs/requirements.txt skip_install = true allowlist_externals = make - rm echo - sed -commands_pre = - sphinx-apidoc --module-first -f --separate -H semver -o docs/_api src/semver src/semver/_types.py src/semver/_deprecated.py - # we don't need this, it just add another level and it's all in docs/api.rst - - rm docs/_api/modules.rst - # Include the semver.__about__ module before semver.cli: - sed -i '/semver\.cli/i\ \ \ semver.__about__' docs/_api/semver.rst commands = make -C docs html commands_post = @@ -103,6 +101,7 @@ commands = [testenv:changelog] description = Run towncrier to check, build, or create the CHANGELOG.rst basepython = python3 +skip_install = true deps = git+https://github.com/twisted/towncrier.git commands =