diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 8f32ffd1..bbbe2a42 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -1,20 +1,23 @@ --- name: Python +# HINT: Sync this paths with the egrep in step check_files on: push: branches: [ "master", "main" ] paths: - 'pyproject.toml' + - 'setup.cfg' - '**.py' - - '.github/workflows/python-testing.yml' + - '.github/workflows/*.yml' pull_request: branches: [ "master", "main" ] paths: - 'pyproject.toml' + - 'setup.cfg' - '**.py' - - '.github/workflows/python-testing.yml' + - '.github/workflows/*.yml' permissions: contents: read @@ -22,45 +25,67 @@ permissions: 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-files: + runs-on: ubuntu-latest + outputs: + can_run: ${{ steps.check_files.outputs.can_run }} + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: GitHub variables + id: gh-vars + run: | + for var in GITHUB_WORKFLOW GITHUB_ACTION GITHUB_ACTIONS GITHUB_REPOSITORY GITHUB_EVEN_NAME GITHUB_EVENT_PATH GITHUB_WORKSPACE GITHUB_SHA GITHUB_REF GITHUB_HEAD_REF GITHUB_BASE_REF; do + echo "$var = ${!var}" + done + + - name: Check for file changes + id: check_files + run: | + # ${{ github.event.after }} ${{ github.event.before }} + can_run=$(git diff --name-only HEAD~1 HEAD | \ + egrep -q '.github/workflows/|pyproject.toml|setup.cfg|\.py$' && echo 1 || echo 0) + echo "can_run=$can_run" + echo "can_run=$can_run" >> $GITHUB_OUTPUT + + skip_test: + runs-on: ubuntu-latest + needs: check-files + timeout-minutes: 2 + if: ${{ needs.check-files.outputs.can_run == '0' }} + + steps: + - name: Skip test + run: | + echo "Nothing to do as no TOML, Python, or YAML file has been changed. + " + echo "Skipping." + check: runs-on: ubuntu-latest + needs: check-files # Timout of 15min timeout-minutes: 15 + # needs.check-files.outputs.can_run + if: ${{ needs.check-files.outputs.can_run == '1' }} 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 + uses: actions/setup-python@v4 with: python-version: 3.8 cache: 'pip' - name: Install dependencies run: | - python3 -m pip install --upgrade pip setuptools setuptools-scm + python3 -m pip install --upgrade pip setuptools>60 setuptools-scm>=60 pip install tox tox-gh-actions - name: Check run: | @@ -68,9 +93,10 @@ jobs: tests: needs: check - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: max-parallel: 5 + fail-fast: true matrix: python-version: ["3.7", "3.8", @@ -79,10 +105,11 @@ jobs: "3.11", # "3.12-dev" ] + os: [ubuntu-latest, "macos-latest"] steps: - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} for ${{ matrix.os }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..33e3e3aa --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,20 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a773e1f3..2281f167 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,58 @@ This section covers the changes between major version 2 and version 3. .. towncrier release notes start +Version 3.0.2 +============= + +:Released: 2023-10-09 +:Maintainer: + + +Bug Fixes +--------- + +* :pr:`418`: Replace :class:`~collection.OrderedDict` with :class:`dict`. + + The dict datatype is ordered since Python 3.7. As we do not support + Python 3.6 anymore, it can be considered safe to avoid :class:`~collection.OrderedDict`. + Related to :gh:`419`. + +* :pr:`426`: Fix :meth:`~semver.version.Version.replace` method to use the derived class + of an instance instead of :class:`~semver.version.Version` class. + + + +Improved Documentation +---------------------- + +* :pr:`431`: Clarify version policy for the different semver versions (v2, v3, >v3) + and the supported Python versions. + +* :gh:`432`: Improve external doc links to Python and Pydantic. + + + +Features +-------- + +* :pr:`417`: Amend GitHub Actions to check against MacOS. + + + +Trivial/Internal Changes +------------------------ + +* :pr:`420`: Introduce :py:class:`~typing.ClassVar` for some :class:`~semver.version.Version` + class variables, mainly :data:`~semver.version.Version.NAMES` and some private. + +* :pr:`421`: Insert mypy configuration into :file:`pyproject.toml` and remove + config options from :file:`tox.ini`. + + + +---- + + Version 3.0.1 ============= diff --git a/README.rst b/README.rst index ede10a18..66552796 100644 --- a/README.rst +++ b/README.rst @@ -3,30 +3,13 @@ Quickstart .. teaser-begin -A Python module for `semantic versioning`_. Simplifies comparing versions. +A Python module to simplify `semantic versioning`_. |GHAction| |python-support| |downloads| |license| |docs| |black| |openissues| |GHDiscussion| .. teaser-end -.. note:: - - 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|_. - - 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 to gain support, - bugfixes, and new features. - -.. |MAINT| replace:: ``maint/v2`` -.. _MAINT: https://github.com/python-semver/python-semver/tree/maint/v2 - - The module follows the ``MAJOR.MINOR.PATCH`` style: * ``MAJOR`` version when you make incompatible API changes, diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 00000000..4702eb08 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,14 @@ +# Getting support + +If you need help, try these ways: + +* Ask your questions on the [discussion](https://github.com/python-semver/python-semver/discussions) page. + + Our forum is a good way to post your questions. +* Read the [python-semver documentation](https://python-semver.readthedocs.io/). + + The documentation contains all the information to install and use the library. +* Suggest a new feature or a new [issue](https://github.com/python-semver/python-semver/issues/new) + + If you found a problem or would like to suggest a new feature that could improve python-semver, + this is the place to go. diff --git a/docs/advanced/combine-pydantic-and-semver.rst b/docs/advanced/combine-pydantic-and-semver.rst index a00c2cff..3236c2a2 100644 --- a/docs/advanced/combine-pydantic-and-semver.rst +++ b/docs/advanced/combine-pydantic-and-semver.rst @@ -5,45 +5,67 @@ 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: +To work with Pydantic>2.0, 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: + first and add the magic methods :py:meth:`__get_pydantic_core_schema__` + and :py:meth:`__get_pydantic_json_schema__` like this: .. code-block:: python + from typing import Annotated, Any, Callable + from pydantic import GetJsonSchemaHandler + from pydantic_core import core_schema + from pydantic.json_schema import JsonSchemaValue from semver import Version - class PydanticVersion(Version): - @classmethod - def _parse(cls, version): - return cls.parse(version) + class _VersionPydanticAnnotation: @classmethod - def __get_validators__(cls): - """Return a list of validator methods for pydantic models.""" - yield cls._parse + def __get_pydantic_core_schema__( + cls, + _source_type: Any, + _handler: Callable[[Any], core_schema.CoreSchema], + ) -> core_schema.CoreSchema: + def validate_from_str(value: str) -> Version: + return Version.parse(value) + + from_str_schema = core_schema.chain_schema( + [ + core_schema.str_schema(), + core_schema.no_info_plain_validator_function(validate_from_str), + ] + ) + + return core_schema.json_or_python_schema( + json_schema=from_str_schema, + python_schema=core_schema.union_schema( + [ + core_schema.is_instance_schema(Version), + from_str_schema, + ] + ), + serialization = core_schema.to_string_ser_schema(), + ) @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", - ] - ) + def __get_pydantic_json_schema__( + cls, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return handler(core_schema.str_schema()) + + ManifestVersion = Annotated[Version, _VersionPydanticAnnotation] 2. Create a new model (in this example :class:`MyModel`) and derive - it from :class:`pydantic.BaseModel`: + it from :py:class:`pydantic:pydantic.BaseModel`: .. code-block:: python import pydantic class MyModel(pydantic.BaseModel): - version: PydanticVersion + version: _VersionPydanticAnnotation 3. Use your model like this: @@ -54,4 +76,4 @@ To work with Pydantic, use the following steps: 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`. + :py:class:`pydantic:pydantic_core.ValidationError`. diff --git a/docs/changelog-semver3-devel.rst b/docs/changelog-semver3-devel.rst index 2d40635d..75579d0f 100644 --- a/docs/changelog-semver3-devel.rst +++ b/docs/changelog-semver3-devel.rst @@ -132,11 +132,11 @@ Improved Documentation :class:`~semver.version.Version` class * Remove semver. prefix in doctests to make examples shorter * Correct some references to dunder methods like - :func:`~.semver.version.Version.__getitem__`, - :func:`~.semver.version.Version.__gt__` etc. + :func:`~semver.version.Version.__getitem__`, + :func:`~semver.version.Version.__gt__` etc. * Remove inconsistencies and mention module level function as deprecated and discouraged from using - * Make empty :py:func:`super` call in :file:`semverwithvprefix.py` example + * Make empty :py:class:`python:super` call in :file:`semverwithvprefix.py` example * :gh:`315`: Improve release procedure text @@ -151,34 +151,32 @@ Trivial/Internal Changes The following functions got renamed: - * function ``semver.version.comparator`` got renamed to + * 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 ``semver.version.cmp`` got renamed to + * function :func:`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`` + * function :func:`semver.version._nat_cmd` as a classmethod + * function :func:`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. + types return now a :py:data:`python:NotImplemented` constant instead + of a :py:exc:`python: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 + In the Python documentation, :py:data:`python:NotImplemented` recommends + returning this constant when comparing with :py:meth:`__gt__ `, :py:meth:`__lt__ `, + and other comparison operators "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 @@ -206,7 +204,7 @@ Version 3.0.0-dev.2 Deprecations ------------ -* :gh:`169`: Deprecate CLI functions not imported from ``semver.cli``. +* :gh:`169`: Deprecate CLI functions not imported from :mod:`semver.cli`. .. _semver-3.0.0-dev.2-features: @@ -222,10 +220,10 @@ Features * 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/version.py` to hold the :class:`~semver.version.Version` class (old name :class:`~semver.version.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 +* :gh:`305`: Rename :class:`~semver.version.VersionInfo` to :class:`~semver.version.Version` but keep an alias for compatibility .. _semver-3.0.0-dev.2-docs: @@ -239,7 +237,7 @@ Improved Documentation * Add migration chapter from semver2 to semver3. * Distinguish between changlog for version 2 and 3 -* :gh:`305`: Add note about :class:`Version` rename. +* :gh:`305`: Add note about :class:`~semver.version.Version` rename. .. _semver-3.0.0-dev.2-trivial: @@ -314,8 +312,8 @@ 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) + Extract metadata directly from source (affects all the :data:`~semver.__about__.__version__`, + :data:`~semver.__about__.__author__` etc. variables) * :gh:`270`: Configure Towncrier (:pr:`273`:) @@ -331,7 +329,7 @@ Features * 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:`276`: Document how to create a sublass from :class:`~semver.version.VersionInfo` class * :gh:`213`: Add typing information diff --git a/docs/conf.py b/docs/conf.py index eab3248d..39d8cb4b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -126,8 +126,13 @@ def find_version(*file_paths): # 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 + # wget -O docs/inventories/python-objects.inv https://docs.python.org/3/objects.inv "python": ("https://docs.python.org/3", (None, "inventories/python-objects.inv")), + # wget -O docs/inventories/pydantic.inv https://docs.pydantic.dev/latest/objects.inv + "pydantic": ( + "https://docs.pydantic.dev/latest/", + (None, "inventories/pydantic.inv"), + ), } # Avoid side-effects (namely that documentations local references can # suddenly resolve to an external location.) diff --git a/docs/contribute/doc-semver.rst b/docs/contribute/doc-semver.rst index fcc6c1ac..e5237eaf 100644 --- a/docs/contribute/doc-semver.rst +++ b/docs/contribute/doc-semver.rst @@ -19,13 +19,19 @@ used efficiently. A new feature is *not* complete if it isn't proberly documented. A good documentation includes: + * **Type annotations** + + This library supports type annotations. Therefore, each function + or method requires types for each arguments and return objects. + Exception of this rule is ``self``. + * **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:: + An appropriate docstring looks like this:: def to_tuple(self) -> VersionTuple: """ @@ -70,11 +76,11 @@ documentation includes: * **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. + A docstring is good, but in most cases it is too short. API documentation + cannot replace good user documentation. + Describe *how* to use your new feature in the 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/index.rst b/docs/index.rst index 1054c225..0f1f32d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ Semver |version| -- Semantic Versioning migration/index advanced/index contribute/index + version-policy api .. toctree:: diff --git a/docs/install.rst b/docs/install.rst index 5404882f..dee441de 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -6,25 +6,20 @@ Release Policy As semver uses `Semantic Versioning`_, breaking changes are only introduced in major releases (incremented ``X`` in "X.Y.Z"). +Refer to section :ref:`version-policy` for a general overview. -For users who want to stay with major 2 releases only, add the following version -restriction:: +For users who want or need to stay with major 3 releases only, add the +following version restriction (:file:`setup.py`, :file:`requirements.txt`, +or :file:`pyproject.toml`):: - semver>=2,<3 - -This line avoids surprises. You will get any updates within the major 2 release like -2.11.0 or above. However, you will never get an update for semver 3.0.0. + semver>=3,<4 -Keep in mind, as this line avoids any major version updates, you also will never -get new exciting features or bug fixes. +This line avoids surprises. You will get any updates within the major 3 release like 3.1.x and above. However, you will never get an update for semver 4.0.0. -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:: +For users who have to stay with major 2 releases only, use the following line:: - semver>=3,<4 + semver>=2,<3 -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 --- diff --git a/docs/inventories/pydantic.inv b/docs/inventories/pydantic.inv new file mode 100644 index 00000000..455215a7 Binary files /dev/null and b/docs/inventories/pydantic.inv differ diff --git a/docs/inventories/python-objects.inv b/docs/inventories/python-objects.inv index 6f01e284..90bfa0a6 100644 Binary files a/docs/inventories/python-objects.inv and b/docs/inventories/python-objects.inv differ diff --git a/docs/migration/replace-deprecated-functions.rst b/docs/migration/replace-deprecated-functions.rst index ebe8c354..c0091b73 100644 --- a/docs/migration/replace-deprecated-functions.rst +++ b/docs/migration/replace-deprecated-functions.rst @@ -8,19 +8,26 @@ Replacing Deprecated Functions 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 deprecated functions can still be used in version 2.10.0 and above. +However, in future versions 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: +Deprecated Module Level Functions +--------------------------------- + +The following list shows the deprecated module level 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` +* :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)``: + For example, the function :func:`semver.bump_major ` is replaced by + :meth:`Version.bump_major ` and calling the ``str(versionobject)``: .. code-block:: python @@ -31,14 +38,9 @@ them with code which is compatible for future versions: Likewise with the other module level functions. -* :func:`semver.Version.isvalid` - - Replace it with :meth:`semver.version.Version.is_valid`: +* :func:`semver.finalize_version ` - -* :func:`semver.finalize_version` - - Replace it with :func:`semver.version.Version.finalize_version`: + Replace it with :meth:`Version.finalize_version `: .. code-block:: python @@ -47,7 +49,7 @@ them with code which is compatible for future versions: >>> s1 == s2 True -* :func:`semver.format_version` +* :func:`semver.format_version ` Replace it with ``str(versionobject)``: @@ -58,7 +60,7 @@ them with code which is compatible for future versions: >>> s1 == s2 True -* :func:`semver.max_ver` +* :func:`semver.max_ver ` Replace it with ``max(version1, version2, ...)`` or ``max([version1, version2, ...])`` and a ``key``: @@ -69,9 +71,9 @@ them with code which is compatible for future versions: >>> s1 == s2 True -* :func:`semver.min_ver` +* :func:`semver.min_ver ` - Replace it with ``min(version1, version2, ...)`` or ``min([version1, version2, ...])``: + Replace it with ``min(version1, version2, ...)`` or ``min([version1, version2, ...])`` and a ``key``: .. code-block:: python @@ -80,10 +82,10 @@ them with code which is compatible for future versions: >>> s1 == s2 True -* :func:`semver.parse` +* :func:`semver.parse ` - Replace it with :meth:`semver.version.Version.parse` and call - :meth:`semver.version.Version.to_dict`: + Replace it with :meth:`Version.parse ` and call + :meth:`Version.to_dict `: .. code-block:: python @@ -92,9 +94,9 @@ them with code which is compatible for future versions: >>> v1 == v2 True -* :func:`semver.parse_version_info` +* :func:`semver.parse_version_info ` - Replace it with :meth:`semver.version.Version.parse`: + Replace it with :meth:`Version.parse `: .. code-block:: python @@ -103,9 +105,9 @@ them with code which is compatible for future versions: >>> v1 == v2 True -* :func:`semver.replace` +* :func:`semver.replace ` - Replace it with :meth:`semver.version.Version.replace`: + Replace it with :meth:`Version.replace `: .. code-block:: python @@ -113,3 +115,22 @@ them with code which is compatible for future versions: >>> s2 = str(Version.parse('1.2.3').replace(major=2, patch=10)) >>> s1 == s2 True + + +Deprected Version methods +------------------------- + +The following list shows the deprecated methods of the :class:`~semver.version.Version` class. + +* :meth:`Version.isvalid ` + + Replace it with :meth:`Version.is_valid `: + + +Deprecated Classes +------------------ + +* :class:`VersionInfo ` + + The class was renamed to :class:`~semver.version.Version`. + Don't use the old name anymore. \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 1186b4c3..2a86234d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ # requirements file for documentation # sphinx +# required in .readthedocs.yaml sphinx-argparse sphinx-autodoc-typehints diff --git a/docs/usage/access-parts-of-a-version.rst b/docs/usage/access-parts-of-a-version.rst index 4eb9274f..b1ce3a14 100644 --- a/docs/usage/access-parts-of-a-version.rst +++ b/docs/usage/access-parts-of-a-version.rst @@ -21,7 +21,7 @@ parts of a version: '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`:: +If you do, you get an :py:exc:`python:AttributeError`:: >>> v.minor = 5 Traceback (most recent call last): diff --git a/docs/usage/access-parts-through-index.rst b/docs/usage/access-parts-through-index.rst index c3651a5e..4553056a 100644 --- a/docs/usage/access-parts-through-index.rst +++ b/docs/usage/access-parts-through-index.rst @@ -33,7 +33,7 @@ Or, as an alternative, you can pass a :func:`slice` object: >>> ver[sl] (10, 3, 2) -Negative numbers or undefined parts raise an :py:exc:`IndexError` exception: +Negative numbers or undefined parts raise an :py:exc:`python:IndexError` exception: .. code-block:: python diff --git a/docs/usage/compare-versions.rst b/docs/usage/compare-versions.rst index ddd03b68..839ad68b 100644 --- a/docs/usage/compare-versions.rst +++ b/docs/usage/compare-versions.rst @@ -62,7 +62,7 @@ To compare two versions depends on your type: >>> "3.5.0" > v True - However, if you compare incomplete strings, you get a :py:exc:`ValueError` exception:: + However, if you compare incomplete strings, you get a :py:exc:`python:ValueError` exception:: >>> v > "1.0" Traceback (most recent call last): @@ -82,7 +82,7 @@ To compare two versions depends on your type: >>> dict(major=1) < v True - If the dictionary contains unknown keys, you get a :py:exc:`TypeError` exception:: + If the dictionary contains unknown keys, you get a :py:exc:`python:TypeError` exception:: >>> v > dict(major=1, unknown=42) Traceback (most recent call last): diff --git a/docs/usage/convert-version-into-different-types.rst b/docs/usage/convert-version-into-different-types.rst index 6948438c..6a447d0d 100644 --- a/docs/usage/convert-version-into-different-types.rst +++ b/docs/usage/convert-version-into-different-types.rst @@ -17,7 +17,7 @@ It is possible to convert a :class:`~semver.version.Version` instance: >>> v = Version(major=3, minor=4, patch=5) >>> v.to_dict() - OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', None), ('build', None)]) + {'major': 3, 'minor': 4, 'patch': 5, 'prerelease': None, 'build': None} * Into a tuple with :meth:`~semver.version.Version.to_tuple`:: diff --git a/docs/usage/create-a-version.rst b/docs/usage/create-a-version.rst index 48bb58a1..9404ae4b 100644 --- a/docs/usage/create-a-version.rst +++ b/docs/usage/create-a-version.rst @@ -41,7 +41,7 @@ A :class:`~semver.version.Version` instance can be created in different ways: 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: + be positive integers or strings, otherwise a :py:exc:`python:ValueError` is raised: >>> d = {'major': -3, 'minor': 4, 'patch': 5, 'prerelease': 'pre.2', 'build': 'build.4'} >>> Version(**d) @@ -50,7 +50,7 @@ A :class:`~semver.version.Version` instance can be created in different ways: ValueError: 'major' is negative. A version can only be positive. As a minimum requirement, your dictionary needs at least the ``major`` - key, others can be omitted. You get a ``TypeError`` if your + key, others can be omitted. You get a :py:exc:`python:TypeError` if your dictionary contains invalid keys. Only the keys ``major``, ``minor``, ``patch``, ``prerelease``, and ``build`` are allowed. @@ -87,12 +87,12 @@ Depending on your use case, the following methods are available: * From a string into a dictionary - To access individual parts, you can use the function :func:`semver.parse`:: + To access individual parts, you can use the function :func:`~semver._deprecated.parse`:: >>> semver.parse("3.4.5-pre.2+build.4") - OrderedDict([('major', 3), ('minor', 4), ('patch', 5), ('prerelease', 'pre.2'), ('build', 'build.4')]) + {'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`:: + If you pass an invalid version string you will get a :py:exc:`python:ValueError`:: >>> semver.parse("1.2") Traceback (most recent call last): diff --git a/docs/usage/semver-version.rst b/docs/usage/semver-version.rst index 0f2e2411..ac580f98 100644 --- a/docs/usage/semver-version.rst +++ b/docs/usage/semver-version.rst @@ -4,4 +4,4 @@ Getting the Version of semver To know the version of semver itself, use the following construct:: >>> semver.__version__ - '3.0.1' + '3.0.2' diff --git a/docs/version-policy.rst b/docs/version-policy.rst new file mode 100644 index 00000000..f0c423f8 --- /dev/null +++ b/docs/version-policy.rst @@ -0,0 +1,52 @@ +.. _version-policy: + +Version Policy +============== + +.. |MAINT| replace:: ``maint/v2`` +.. _MAINT: https://github.com/python-semver/python-semver/tree/maint/v2 +.. |CHANGELOG| replace:: ``Changelog`` +.. _CHANGELOG: https://github.com/python-semver/python-semver/blob/maint/v2/CHANGELOG.rst + +The move from v2 to v3 introduced many changes and deprecated module functions. +The main functionality is handled by the :class:`~semver.version.Version` class +now. Find more information in the section :ref:`semver2-to-3`. + + +semver Version 2 +---------------- + +Active development of major version 2 has stopped. No new features nor +backports will be integrated. +We recommend to upgrade your workflow to Python 3 to gain support, +bugfixes, and new features. + +If you still need this old version, use the |MAINT|_ branch. There you +can look for the |CHANGELOG|_ if you need some details about the history. + + +semver Version 3 +---------------- + +We will not intentionally make breaking changes in minor releases of V3. + +Methods marked as ``deprecated`` raise a warning message when used from the +:py:mod:`python:warnings` module. +Refer to section :ref:`sec_display_deprecation_warnings` to get more information about how to customize it. +Check section :ref:`sec_replace_deprecated_functions` to make your code +ready for future major releases. + + +semver Version 3 and beyond +--------------------------- + +Methods that were marked as deprecated will be very likely be removed. + + +Support for Python versions +--------------------------- + +This project will drop support for a Python version when the +following conditions are met: + +* The Python version has reached `EOL `_. diff --git a/pyproject.toml b/pyproject.toml index 6b12deb0..ce540deb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,16 @@ requires = [ ] build-backend = "setuptools.build_meta" +[tool.setuptools_scm] +version_scheme = "post-release" +local_scheme = "dirty-tag" + +[tool.mypy] +# ignore_missing_imports = true +check_untyped_defs = true +show_error_codes = true +# strict = true +pretty = true [tool.black] diff --git a/release-procedure.md b/release-procedure.md index 7476c79a..4225bfd9 100644 --- a/release-procedure.md +++ b/release-procedure.md @@ -99,14 +99,18 @@ create a new release. 1. Create a tag: - $ git tag -a x.x.x + ```bash + $ git tag -a x.y.z + ``` It's recommended to use the generated Tox output from the Changelog. 1. Push the tag: - $ git push --tags + ```bash + $ git push origin x.y.z + ``` 1. In [GitHub Release page](https://github.com/python-semver/python-semver/release) document the new release. diff --git a/src/semver/__about__.py b/src/semver/__about__.py index 2eff8c86..a0d9cf90 100644 --- a/src/semver/__about__.py +++ b/src/semver/__about__.py @@ -16,7 +16,7 @@ """ #: Semver version -__version__ = "3.0.1" +__version__ = "3.0.2" #: Original semver author __author__ = "Kostiantyn Rybnikov" diff --git a/src/semver/version.py b/src/semver/version.py index d2f336c0..29309ab4 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -1,13 +1,14 @@ """Version handling by a semver compatible version class.""" -import collections import re from functools import wraps from typing import ( Any, + ClassVar, Dict, Iterable, Optional, + Pattern, SupportsInt, Tuple, Union, @@ -73,12 +74,14 @@ class Version: __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") #: The names of the different parts of a version - NAMES = tuple([item[1:] for item in __slots__]) + NAMES: ClassVar[Tuple[str, ...]] = tuple([item[1:] for item in __slots__]) #: Regex for number in a prerelease - _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") + _LAST_NUMBER: ClassVar[Pattern[str]] = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") #: Regex template for a semver version - _REGEX_TEMPLATE = r""" + _REGEX_TEMPLATE: ClassVar[ + str + ] = r""" ^ (?P0|[1-9]\d*) (?: @@ -100,12 +103,12 @@ class Version: $ """ #: Regex for a semver version - _REGEX = re.compile( + _REGEX: ClassVar[Pattern[str]] = 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_OPTIONAL_MINOR_AND_PATCH: ClassVar[Pattern[str]] = re.compile( _REGEX_TEMPLATE.format(opt_patch="?", opt_minor="?"), re.VERBOSE, ) @@ -218,27 +221,24 @@ def to_tuple(self) -> VersionTuple: def to_dict(self) -> VersionDict: """ - Convert the Version object to an OrderedDict. + Convert the Version object to an dict. .. 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``, + :return: an dict 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), - ) + {'major': 3, 'minor': 2, 'patch': 1, 'prerelease': None, 'build': None} + """ + return dict( + major=self.major, + minor=self.minor, + patch=self.patch, + prerelease=self.prerelease, + build=self.build, ) def __iter__(self) -> VersionIterator: @@ -655,8 +655,8 @@ def parse( 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 + 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` @@ -670,7 +670,7 @@ def replace(self, **parts: Union[int, Optional[str]]) -> "Version": version = self.to_dict() version.update(parts) try: - return Version(**version) # type: ignore + return type(self)(**version) # type: ignore except TypeError: unknownkeys = set(parts) - set(self.to_dict()) error = "replace() got %d unexpected keyword argument(s): %s" % ( diff --git a/tests/test_subclass.py b/tests/test_subclass.py index cbf9d271..b33f4969 100644 --- a/tests/test_subclass.py +++ b/tests/test_subclass.py @@ -17,3 +17,37 @@ def __str__(self): v = SemVerWithVPrefix.parse("v1.2.3") assert str(v) == "v1.2.3" + + +def test_replace_from_subclass(): + # Issue#426 + # Taken from the example "Creating Subclasses from 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__() + + version = SemVerWithVPrefix.parse("v1.1.0") + dev_version = version.replace(prerelease="dev.0") + + assert str(dev_version) == "v1.1.0-dev.0" diff --git a/tox.ini b/tox.ini index b18aa1f7..1bc77e1b 100644 --- a/tox.ini +++ b/tox.ini @@ -49,7 +49,7 @@ commands = flake8 {posargs:} description = Check code style basepython = python3 deps = mypy -commands = mypy {posargs:--ignore-missing-imports --check-untyped-defs src} +commands = mypy {posargs:src} [testenv:docstrings]