We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
GitHub tags are only read up to the first `-` character, breaking tags such as `1.0.0-rc1`
### The problem

The software update plugin does a load of parsing to compare the GitHub tag version to the one from the installed Python packages. I unwrapped some of it to figure out what was going on here.

We have a tag on GitHub with the name `1.0.0-rc1`, which according to [PEP440](https://www.python.org/dev/peps/pep-0440/#pre-release-separators) is allowed but would be normalized to `1.0.0rc1`, which is how it comes through if this tag is set in setup.py.

The plugin runs it through (as far as I could trace) `octoprint.plugins.softwareupdate.version_checks.github_release.get_comparable_version`. This calls some functions in `octoprint.util.version` including `normalize_version`, which only reads the tag up to and including the `-`, stripping it down to just `1.0.0` in this case, which would be seen as 'ahead' of `1.0.0rc1` of course.

Some testing to demonstrate, the first line is the problem one:
```python
>>> get_comparable_version("1.1.1-rc1")
<Version('1.1.1')>
>>> get_comparable_version("1.1.1rc1")
<Version('1.1.1rc1')>
>>> get_comparable_version("1.1.1.rc1")
<Version('1.1.1rc1')>
>>> get_comparable_version("1.1.1_rc1")
<Version('1.1.1rc1')>
```

They should all be parsed equally according to PEP440.

This resulted in an update loop where `1.0.0-rc1` was set the same in both `setup.py` and the GitHub tag, but it became GitHub tag = 1.0.0 and setup.py = 1.0.0rc1 when parsed and the comparison would always show an update available.

**TLDR**; everything after the `-` in a GitHub tag is ignored, when it should be included.

Discussion on discord: https://discord.com/channels/704958479194128507/708230829050036236/928402397435420775

### Did the issue persist even in safe mode?

Yes, it did persist

### If you could not test in safe mode, please state why

NA

### Version of OctoPrint

1.7.2

### Operating system running OctoPrint

All

### Printer model & used firmware incl. version

All

### Browser and version of browser, operating system running browser

All

### Checklist of files to include below

- [X] Systeminfo Bundle (always include!)
- [ ] Contents of the JavaScript browser console (always include in cases of issues with the user interface)
- [ ] Screenshots and/or videos showing the problem (always include in case of issues with the user interface)
- [ ] GCODE file with which to reproduce (always include in case of issues with GCODE analysis or printing behaviour)

### Additional information & file uploads

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/octoprint/util/version.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 
4 """
5 This module provides a bunch of utility methods and helpers for version handling.
6 """
7 
8 __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
9 
10 import logging
11 
12 import pkg_resources
13 from past.builtins import basestring
14 
15 from octoprint import __version__
16 
17 
18 def get_octoprint_version_string():
19     return __version__
20 
21 
22 def get_octoprint_version(cut=None, **kwargs):
23     octoprint_version_string = normalize_version(get_octoprint_version_string())
24     return get_comparable_version(octoprint_version_string, cut=cut, **kwargs)
25 
26 
27 def is_released_octoprint_version(version=None):
28     if version is None:
29         version = get_octoprint_version()
30     return is_release(version)
31 
32 
33 def is_stable_octoprint_version(version=None):
34     if version is None:
35         version = get_octoprint_version()
36     return is_stable(version)
37 
38 
39 def is_octoprint_compatible(*compatibility_entries, **kwargs):
40     """
41     Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``.
42 
43     Arguments:
44             compatibility_entries (str): compatibility string(s) to test against, result will be `True` if any match
45                     is found
46             octoprint_version (tuple or SetuptoolsVersion): optional OctoPrint version to match against, if not current
47                     base version will be determined via :func:`get_octoprint_version`.
48 
49     Returns:
50             (bool) ``True`` if any of the provided compatibility entries matches or there are no entries, else ``False``
51     """
52 
53     logger = logging.getLogger(__name__)
54 
55     if not compatibility_entries:
56         return True
57 
58     octoprint_version = kwargs.get("octoprint_version")
59     if octoprint_version is None:
60         octoprint_version = get_octoprint_version(base=True)
61 
62     for octo_compat in compatibility_entries:
63         try:
64             if not any(
65                 octo_compat.startswith(c)
66                 for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")
67             ):
68                 octo_compat = ">={}".format(octo_compat)
69 
70             s = pkg_resources.Requirement.parse("OctoPrint" + octo_compat)
71             if octoprint_version in s:
72                 break
73         except Exception:
74             logger.exception(
75                 "Something is wrong with this compatibility string for OctoPrint: {}".format(
76                     octo_compat
77                 )
78             )
79     else:
80         return False
81 
82     return True
83 
84 
85 def get_python_version_string():
86     from platform import python_version
87 
88     version_string = normalize_version(python_version())
89 
90     return version_string
91 
92 
93 def get_python_version():
94     return get_comparable_version(get_python_version_string())
95 
96 
97 def is_python_compatible(compat, **kwargs):
98     if not compat:
99         return True
100 
101     python_version = kwargs.get("python_version")
102     if python_version is None:
103         python_version = get_python_version_string()
104 
105     s = pkg_resources.Requirement.parse("Python" + compat)
106     return python_version in s
107 
108 
109 def get_comparable_version(version_string, cut=None, **kwargs):
110     """
111     Args:
112         version_string: The version string for which to create a comparable version instance
113         cut: optional, how many version digits to remove (e.g., cut=1 will turn 1.2.3 into 1.2).
114              Defaults to ``None``, meaning no further action. Settings this to 0 will remove
115              anything up to the last digit, e.g. dev or rc information.
116 
117     Returns:
118         A comparable version
119     """
120 
121     if "base" in kwargs and kwargs.get("base", False) and cut is None:
122         cut = 0
123     if cut is not None and (cut < 0 or not isinstance(cut, int)):
124         raise ValueError("level must be a positive integer")
125 
126     version_string = normalize_version(version_string)
127     version = pkg_resources.parse_version(version_string)
128 
129     if cut is not None:
130         if isinstance(version, tuple):
131             # old setuptools
132             base_version = []
133             for part in version:
134                 if part.startswith("*"):
135                     break
136                 base_version.append(part)
137             if 0 < cut < len(base_version):
138                 base_version = base_version[:-cut]
139             base_version.append("*final")
140             version = tuple(base_version)
141         else:
142             # new setuptools
143             version = pkg_resources.parse_version(version.base_version)
144             if cut is not None:
145                 parts = version.base_version.split(".")
146                 if 0 < cut < len(parts):
147                     reduced = parts[:-cut]
148                     version = pkg_resources.parse_version(
149                         ".".join(str(x) for x in reduced)
150                     )
151 
152     return version
153 
154 
155 def is_stable(version):
156     """
157     >>> import pkg_resources
158     >>> is_stable(pkg_resources.parse_version("1.3.6rc3"))
159     False
160     >>> is_stable(pkg_resources.parse_version("1.3.6rc3.dev2+g1234"))
161     False
162     >>> is_stable(pkg_resources.parse_version("1.3.6"))
163     True
164     >>> is_stable(pkg_resources.parse_version("1.3.6.post1+g1234"))
165     True
166     >>> is_stable(pkg_resources.parse_version("1.3.6.post1.dev0+g1234"))
167     False
168     >>> is_stable(pkg_resources.parse_version("1.3.7.dev123+g23545"))
169     False
170     """
171 
172     if isinstance(version, basestring):
173         version = get_comparable_version(version)
174 
175     if not is_release(version):
176         return False
177 
178     if isinstance(version, tuple):
179         return "*a" not in version and "*b" not in version and "*c" not in version
180     else:
181         return not version.is_prerelease
182 
183 
184 def is_release(version):
185     """
186     >>> import pkg_resources
187     >>> is_release(pkg_resources.parse_version("1.3.6rc3"))
188     True
189     >>> is_release(pkg_resources.parse_version("1.3.6rc3.dev2+g1234"))
190     False
191     >>> is_release(pkg_resources.parse_version("1.3.6"))
192     True
193     >>> is_release(pkg_resources.parse_version("1.3.6.post1+g1234"))
194     True
195     >>> is_release(pkg_resources.parse_version("1.3.6.post1.dev0+g1234"))
196     False
197     >>> is_release(pkg_resources.parse_version("1.3.7.dev123+g23545"))
198     False
199     """
200 
201     if isinstance(version, basestring):
202         version = get_comparable_version(version)
203 
204     if isinstance(version, tuple):
205         # old setuptools
206         return "*@" not in version
207     else:
208         # new setuptools
209         return "dev" not in version.public
210     pass
211 
212 
213 def is_prerelease(version):
214     if isinstance(version, basestring):
215         version = get_comparable_version(version)
216 
217     if isinstance(version, tuple):
218         # old setuptools
219         return any(map(lambda x: x in version, ("*a", "*b", "*c", "*rc")))
220     else:
221         # new setuptools
222         return version.is_prerelease
223 
224 
225 def normalize_version(version):
226     if "-" in version:
227         version = version[: version.find("-")]
228 
229     # Debian has the python version set to 2.7.15+ which is not PEP440 compliant (bug 914072)
230     if version.endswith("+"):
231         version = version[:-1]
232 
233     if version[0].lower() == "v":
234         version = version[1:]
235 
236     return version.strip()
237
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/octoprint/util/version.py b/src/octoprint/util/version.py
--- a/src/octoprint/util/version.py
+++ b/src/octoprint/util/version.py
@@ -208,8 +208,6 @@
 
 
 def normalize_version(version):
-    if "-" in version:
-        version = version[: version.find("-")]
 
     # Debian has the python version set to 2.7.15+ which is not PEP440 compliant (bug 914072)
     if version.endswith("+"):

{"golden_diff": "diff --git a/src/octoprint/util/version.py b/src/octoprint/util/version.py\n--- a/src/octoprint/util/version.py\n+++ b/src/octoprint/util/version.py\n@@ -208,8 +208,6 @@\n \n \n def normalize_version(version):\n-    if \"-\" in version:\n-        version = version[: version.find(\"-\")]\n \n     # Debian has the python version set to 2.7.15+ which is not PEP440 compliant (bug 914072)\n     if version.endswith(\"+\"):\n", "issue": "GitHub tags are only read up to the first `-` character, breaking tags such as `1.0.0-rc1`\n### The problem\n\nThe software update plugin does a load of parsing to compare the GitHub tag version to the one from the installed Python packages. I unwrapped some of it to figure out what was going on here.\r\n\r\nWe have a tag on GitHub with the name `1.0.0-rc1`, which according to [PEP440](https://www.python.org/dev/peps/pep-0440/#pre-release-separators) is allowed but would be normalized to `1.0.0rc1`, which is how it comes through if this tag is set in setup.py.\r\n\r\nThe plugin runs it through (as far as I could trace) `octoprint.plugins.softwareupdate.version_checks.github_release.get_comparable_version`. This calls some functions in `octoprint.util.version` including `normalize_version`, which only reads the tag up to and including the `-`, stripping it down to just `1.0.0` in this case, which would be seen as 'ahead' of `1.0.0rc1` of course.\r\n\r\nSome testing to demonstrate, the first line is the problem one:\r\n```python\r\n>>> get_comparable_version(\"1.1.1-rc1\")\r\n<Version('1.1.1')>\r\n>>> get_comparable_version(\"1.1.1rc1\")\r\n<Version('1.1.1rc1')>\r\n>>> get_comparable_version(\"1.1.1.rc1\")\r\n<Version('1.1.1rc1')>\r\n>>> get_comparable_version(\"1.1.1_rc1\")\r\n<Version('1.1.1rc1')>\r\n```\r\n\r\nThey should all be parsed equally according to PEP440.\r\n\r\nThis resulted in an update loop where `1.0.0-rc1` was set the same in both `setup.py` and the GitHub tag, but it became GitHub tag = 1.0.0 and setup.py = 1.0.0rc1 when parsed and the comparison would always show an update available.\r\n\r\n**TLDR**; everything after the `-` in a GitHub tag is ignored, when it should be included.\r\n\r\nDiscussion on discord: https://discord.com/channels/704958479194128507/708230829050036236/928402397435420775\n\n### Did the issue persist even in safe mode?\n\nYes, it did persist\n\n### If you could not test in safe mode, please state why\n\nNA\n\n### Version of OctoPrint\n\n1.7.2\n\n### Operating system running OctoPrint\n\nAll\n\n### Printer model & used firmware incl. version\n\nAll\n\n### Browser and version of browser, operating system running browser\n\nAll\n\n### Checklist of files to include below\n\n- [X] Systeminfo Bundle (always include!)\n- [ ] Contents of the JavaScript browser console (always include in cases of issues with the user interface)\n- [ ] Screenshots and/or videos showing the problem (always include in case of issues with the user interface)\n- [ ] GCODE file with which to reproduce (always include in case of issues with GCODE analysis or printing behaviour)\n\n### Additional information & file uploads\n\n_No response_\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\n\"\"\"\nThis module provides a bunch of utility methods and helpers for version handling.\n\"\"\"\n\n__license__ = \"GNU Affero General Public License http://www.gnu.org/licenses/agpl.html\"\n\nimport logging\n\nimport pkg_resources\nfrom past.builtins import basestring\n\nfrom octoprint import __version__\n\n\ndef get_octoprint_version_string():\n    return __version__\n\n\ndef get_octoprint_version(cut=None, **kwargs):\n    octoprint_version_string = normalize_version(get_octoprint_version_string())\n    return get_comparable_version(octoprint_version_string, cut=cut, **kwargs)\n\n\ndef is_released_octoprint_version(version=None):\n    if version is None:\n        version = get_octoprint_version()\n    return is_release(version)\n\n\ndef is_stable_octoprint_version(version=None):\n    if version is None:\n        version = get_octoprint_version()\n    return is_stable(version)\n\n\ndef is_octoprint_compatible(*compatibility_entries, **kwargs):\n    \"\"\"\n    Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``.\n\n    Arguments:\n            compatibility_entries (str): compatibility string(s) to test against, result will be `True` if any match\n                    is found\n            octoprint_version (tuple or SetuptoolsVersion): optional OctoPrint version to match against, if not current\n                    base version will be determined via :func:`get_octoprint_version`.\n\n    Returns:\n            (bool) ``True`` if any of the provided compatibility entries matches or there are no entries, else ``False``\n    \"\"\"\n\n    logger = logging.getLogger(__name__)\n\n    if not compatibility_entries:\n        return True\n\n    octoprint_version = kwargs.get(\"octoprint_version\")\n    if octoprint_version is None:\n        octoprint_version = get_octoprint_version(base=True)\n\n    for octo_compat in compatibility_entries:\n        try:\n            if not any(\n                octo_compat.startswith(c)\n                for c in (\"<\", \"<=\", \"!=\", \"==\", \">=\", \">\", \"~=\", \"===\")\n            ):\n                octo_compat = \">={}\".format(octo_compat)\n\n            s = pkg_resources.Requirement.parse(\"OctoPrint\" + octo_compat)\n            if octoprint_version in s:\n                break\n        except Exception:\n            logger.exception(\n                \"Something is wrong with this compatibility string for OctoPrint: {}\".format(\n                    octo_compat\n                )\n            )\n    else:\n        return False\n\n    return True\n\n\ndef get_python_version_string():\n    from platform import python_version\n\n    version_string = normalize_version(python_version())\n\n    return version_string\n\n\ndef get_python_version():\n    return get_comparable_version(get_python_version_string())\n\n\ndef is_python_compatible(compat, **kwargs):\n    if not compat:\n        return True\n\n    python_version = kwargs.get(\"python_version\")\n    if python_version is None:\n        python_version = get_python_version_string()\n\n    s = pkg_resources.Requirement.parse(\"Python\" + compat)\n    return python_version in s\n\n\ndef get_comparable_version(version_string, cut=None, **kwargs):\n    \"\"\"\n    Args:\n        version_string: The version string for which to create a comparable version instance\n        cut: optional, how many version digits to remove (e.g., cut=1 will turn 1.2.3 into 1.2).\n             Defaults to ``None``, meaning no further action. Settings this to 0 will remove\n             anything up to the last digit, e.g. dev or rc information.\n\n    Returns:\n        A comparable version\n    \"\"\"\n\n    if \"base\" in kwargs and kwargs.get(\"base\", False) and cut is None:\n        cut = 0\n    if cut is not None and (cut < 0 or not isinstance(cut, int)):\n        raise ValueError(\"level must be a positive integer\")\n\n    version_string = normalize_version(version_string)\n    version = pkg_resources.parse_version(version_string)\n\n    if cut is not None:\n        if isinstance(version, tuple):\n            # old setuptools\n            base_version = []\n            for part in version:\n                if part.startswith(\"*\"):\n                    break\n                base_version.append(part)\n            if 0 < cut < len(base_version):\n                base_version = base_version[:-cut]\n            base_version.append(\"*final\")\n            version = tuple(base_version)\n        else:\n            # new setuptools\n            version = pkg_resources.parse_version(version.base_version)\n            if cut is not None:\n                parts = version.base_version.split(\".\")\n                if 0 < cut < len(parts):\n                    reduced = parts[:-cut]\n                    version = pkg_resources.parse_version(\n                        \".\".join(str(x) for x in reduced)\n                    )\n\n    return version\n\n\ndef is_stable(version):\n    \"\"\"\n    >>> import pkg_resources\n    >>> is_stable(pkg_resources.parse_version(\"1.3.6rc3\"))\n    False\n    >>> is_stable(pkg_resources.parse_version(\"1.3.6rc3.dev2+g1234\"))\n    False\n    >>> is_stable(pkg_resources.parse_version(\"1.3.6\"))\n    True\n    >>> is_stable(pkg_resources.parse_version(\"1.3.6.post1+g1234\"))\n    True\n    >>> is_stable(pkg_resources.parse_version(\"1.3.6.post1.dev0+g1234\"))\n    False\n    >>> is_stable(pkg_resources.parse_version(\"1.3.7.dev123+g23545\"))\n    False\n    \"\"\"\n\n    if isinstance(version, basestring):\n        version = get_comparable_version(version)\n\n    if not is_release(version):\n        return False\n\n    if isinstance(version, tuple):\n        return \"*a\" not in version and \"*b\" not in version and \"*c\" not in version\n    else:\n        return not version.is_prerelease\n\n\ndef is_release(version):\n    \"\"\"\n    >>> import pkg_resources\n    >>> is_release(pkg_resources.parse_version(\"1.3.6rc3\"))\n    True\n    >>> is_release(pkg_resources.parse_version(\"1.3.6rc3.dev2+g1234\"))\n    False\n    >>> is_release(pkg_resources.parse_version(\"1.3.6\"))\n    True\n    >>> is_release(pkg_resources.parse_version(\"1.3.6.post1+g1234\"))\n    True\n    >>> is_release(pkg_resources.parse_version(\"1.3.6.post1.dev0+g1234\"))\n    False\n    >>> is_release(pkg_resources.parse_version(\"1.3.7.dev123+g23545\"))\n    False\n    \"\"\"\n\n    if isinstance(version, basestring):\n        version = get_comparable_version(version)\n\n    if isinstance(version, tuple):\n        # old setuptools\n        return \"*@\" not in version\n    else:\n        # new setuptools\n        return \"dev\" not in version.public\n    pass\n\n\ndef is_prerelease(version):\n    if isinstance(version, basestring):\n        version = get_comparable_version(version)\n\n    if isinstance(version, tuple):\n        # old setuptools\n        return any(map(lambda x: x in version, (\"*a\", \"*b\", \"*c\", \"*rc\")))\n    else:\n        # new setuptools\n        return version.is_prerelease\n\n\ndef normalize_version(version):\n    if \"-\" in version:\n        version = version[: version.find(\"-\")]\n\n    # Debian has the python version set to 2.7.15+ which is not PEP440 compliant (bug 914072)\n    if version.endswith(\"+\"):\n        version = version[:-1]\n\n    if version[0].lower() == \"v\":\n        version = version[1:]\n\n    return version.strip()\n", "path": "src/octoprint/util/version.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\n\"\"\"\nThis module provides a bunch of utility methods and helpers for version handling.\n\"\"\"\n\n__license__ = \"GNU Affero General Public License http://www.gnu.org/licenses/agpl.html\"\n\nimport logging\n\nimport pkg_resources\n\nfrom octoprint import __version__\n\n\ndef get_octoprint_version_string():\n    return __version__\n\n\ndef get_octoprint_version(cut=None, **kwargs):\n    octoprint_version_string = normalize_version(get_octoprint_version_string())\n    return get_comparable_version(octoprint_version_string, cut=cut, **kwargs)\n\n\ndef is_released_octoprint_version(version=None):\n    \"\"\"\n    >>> import pkg_resources\n    >>> is_released_octoprint_version(version=pkg_resources.parse_version(\"1.3.6rc3\"))\n    True\n    >>> is_released_octoprint_version(version=pkg_resources.parse_version(\"1.3.6rc3.dev2+g1234\"))\n    False\n    >>> is_released_octoprint_version(version=pkg_resources.parse_version(\"1.3.6\"))\n    True\n    >>> is_released_octoprint_version(version=pkg_resources.parse_version(\"1.3.6.post1+g1234\"))\n    True\n    >>> is_released_octoprint_version(version=pkg_resources.parse_version(\"1.3.6.post1.dev0+g1234\"))\n    False\n    >>> is_released_octoprint_version(version=pkg_resources.parse_version(\"1.3.7.dev123+g23545\"))\n    False\n    \"\"\"\n\n    if version is None:\n        version = get_octoprint_version()\n\n    if isinstance(version, tuple):\n        # old setuptools\n        return \"*@\" not in version\n    else:\n        # new setuptools\n        return \"dev\" not in version.public\n\n\ndef is_stable_octoprint_version(version=None):\n    \"\"\"\n    >>> import pkg_resources\n    >>> is_stable_octoprint_version(version=pkg_resources.parse_version(\"1.3.6rc3\"))\n    False\n    >>> is_stable_octoprint_version(version=pkg_resources.parse_version(\"1.3.6rc3.dev2+g1234\"))\n    False\n    >>> is_stable_octoprint_version(version=pkg_resources.parse_version(\"1.3.6\"))\n    True\n    >>> is_stable_octoprint_version(version=pkg_resources.parse_version(\"1.3.6.post1+g1234\"))\n    True\n    >>> is_stable_octoprint_version(version=pkg_resources.parse_version(\"1.3.6.post1.dev0+g1234\"))\n    False\n    >>> is_stable_octoprint_version(version=pkg_resources.parse_version(\"1.3.7.dev123+g23545\"))\n    False\n    \"\"\"\n\n    if version is None:\n        version = get_octoprint_version()\n\n    if not is_released_octoprint_version(version=version):\n        return False\n\n    if isinstance(version, tuple):\n        return \"*a\" not in version and \"*b\" not in version and \"*c\" not in version\n    else:\n        return not version.is_prerelease\n\n\ndef is_octoprint_compatible(*compatibility_entries, **kwargs):\n    \"\"\"\n    Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``.\n\n    Arguments:\n            compatibility_entries (str): compatibility string(s) to test against, result will be `True` if any match\n                    is found\n            octoprint_version (tuple or SetuptoolsVersion): optional OctoPrint version to match against, if not current\n                    base version will be determined via :func:`get_octoprint_version`.\n\n    Returns:\n            (bool) ``True`` if any of the provided compatibility entries matches or there are no entries, else ``False``\n    \"\"\"\n\n    logger = logging.getLogger(__name__)\n\n    if not compatibility_entries:\n        return True\n\n    octoprint_version = kwargs.get(\"octoprint_version\")\n    if octoprint_version is None:\n        octoprint_version = get_octoprint_version(base=True)\n\n    for octo_compat in compatibility_entries:\n        try:\n            if not any(\n                octo_compat.startswith(c)\n                for c in (\"<\", \"<=\", \"!=\", \"==\", \">=\", \">\", \"~=\", \"===\")\n            ):\n                octo_compat = \">={}\".format(octo_compat)\n\n            s = pkg_resources.Requirement.parse(\"OctoPrint\" + octo_compat)\n            if octoprint_version in s:\n                break\n        except Exception:\n            logger.exception(\n                \"Something is wrong with this compatibility string for OctoPrint: {}\".format(\n                    octo_compat\n                )\n            )\n    else:\n        return False\n\n    return True\n\n\ndef get_python_version_string():\n    from platform import python_version\n\n    version_string = normalize_version(python_version())\n\n    return version_string\n\n\ndef get_python_version():\n    return get_comparable_version(get_python_version_string())\n\n\ndef is_python_compatible(compat, **kwargs):\n    if not compat:\n        return True\n\n    python_version = kwargs.get(\"python_version\")\n    if python_version is None:\n        python_version = get_python_version_string()\n\n    s = pkg_resources.Requirement.parse(\"Python\" + compat)\n    return python_version in s\n\n\ndef get_comparable_version(version_string, cut=None, **kwargs):\n    \"\"\"\n    Args:\n        version_string: The version string for which to create a comparable version instance\n        cut: optional, how many version digits to remove (e.g., cut=1 will turn 1.2.3 into 1.2).\n             Defaults to ``None``, meaning no further action. Settings this to 0 will remove\n             anything up to the last digit, e.g. dev or rc information.\n\n    Returns:\n        A comparable version\n    \"\"\"\n\n    if \"base\" in kwargs and kwargs.get(\"base\", False) and cut is None:\n        cut = 0\n    if cut is not None and (cut < 0 or not isinstance(cut, int)):\n        raise ValueError(\"level must be a positive integer\")\n\n    version_string = normalize_version(version_string)\n    version = pkg_resources.parse_version(version_string)\n\n    if cut is not None:\n        if isinstance(version, tuple):\n            # old setuptools\n            base_version = []\n            for part in version:\n                if part.startswith(\"*\"):\n                    break\n                base_version.append(part)\n            if 0 < cut < len(base_version):\n                base_version = base_version[:-cut]\n            base_version.append(\"*final\")\n            version = tuple(base_version)\n        else:\n            # new setuptools\n            version = pkg_resources.parse_version(version.base_version)\n            if cut is not None:\n                parts = version.base_version.split(\".\")\n                if 0 < cut < len(parts):\n                    reduced = parts[:-cut]\n                    version = pkg_resources.parse_version(\n                        \".\".join(str(x) for x in reduced)\n                    )\n\n    return version\n\n\ndef is_prerelease(version_string):\n    version = get_comparable_version(version_string)\n\n    if isinstance(version, tuple):\n        # old setuptools\n        return any(map(lambda x: x in version, (\"*a\", \"*b\", \"*c\", \"*rc\")))\n    else:\n        # new setuptools\n        return version.is_prerelease\n\n\ndef normalize_version(version):\n\n    # Debian has the python version set to 2.7.15+ which is not PEP440 compliant (bug 914072)\n    if version.endswith(\"+\"):\n        version = version[:-1]\n\n    if version[0].lower() == \"v\":\n        version = version[1:]\n\n    return version.strip()\n", "path": "src/octoprint/util/version.py"}]}