Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attr.__version_info__ #580

Merged
merged 10 commits into from Oct 1, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/580.change.rst
@@ -0,0 +1,2 @@
Added ``attr.__version_info__`` that can be used to reliably check the version of ``attrs`` and write forward- and backward-compatible code.
Please check out the `section on deprecated APIs <http://www.attrs.org/en/stable/api.html#deprecated-apis>`_ on how to use it.
24 changes: 24 additions & 0 deletions docs/api.rst
Expand Up @@ -506,7 +506,31 @@ Converters
Deprecated APIs
---------------

.. _version-info:

To help you writing backward compatible code that doesn't throw warnings on modern releases, the ``attr`` module has an ``__version_info__`` attribute as of version 19.2.0.
hynek marked this conversation as resolved.
Show resolved Hide resolved
It behaves similarly to `sys.version_info` and is an instance of `VersionInfo`:

.. autoclass:: VersionInfo

With its help you can write code like this:

>>> if getattr(attr, "__version_info__", (0,)) >= (19, 2):
... cmp_off = {"eq": False}
... else:
... cmp_off = {"cmp": False}
>>> cmp_off == {"eq": False}
True
>>> @attr.s(**cmp_off)
... class C(object):
... pass


----

The serious business aliases used to be called ``attr.attributes`` and ``attr.attr``.
There are no plans to remove them but they shouldn't be used in new code.

The ``cmp`` argument to both `attr.s` and `attr.ib` has been deprecated in 19.2 and shouldn't be used.

.. autofunction:: assoc
3 changes: 3 additions & 0 deletions src/attr/__init__.py
Expand Up @@ -16,9 +16,11 @@
make_class,
validate,
)
from ._version import VersionInfo


__version__ = "19.2.0.dev0"
__version_info__ = VersionInfo._from_version_string(__version__)

__title__ = "attrs"
__description__ = "Classes Without Boilerplate"
Expand All @@ -37,6 +39,7 @@
ib = attr = attrib
dataclass = partial(attrs, auto_attribs=True) # happy Easter ;)


__all__ = [
"Attribute",
"Factory",
Expand Down
13 changes: 13 additions & 0 deletions src/attr/__init__.pyi
Expand Up @@ -20,6 +20,19 @@ from . import filters as filters
from . import converters as converters
from . import validators as validators

from ._version import VersionInfo

__version__: str
__version_info__: VersionInfo
__title__: str
__description__: str
__url__: str
__uri__: str
__author__: str
__email__: str
__license__: str
__copyright__: str

_T = TypeVar("_T")
_C = TypeVar("_C", bound=type)

hynek marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
85 changes: 85 additions & 0 deletions src/attr/_version.py
@@ -0,0 +1,85 @@
from __future__ import absolute_import, division, print_function

from functools import total_ordering

from ._funcs import astuple
from ._make import attrib, attrs


@total_ordering
@attrs(eq=False, order=False, slots=True, frozen=True)
class VersionInfo(object):
"""
A version object that can be compared to tuple of length 1--4:

>>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2)
True
>>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1)
True
>>> vi = attr.VersionInfo(19, 2, 0, "final")
>>> vi < (19, 1, 1)
False
>>> vi < (19,)
False
>>> vi == (19, 2,)
True
>>> vi == (19, 2, 1)
False

.. versionadded:: 19.2
"""

year = attrib(type=int)
minor = attrib(type=int)
micro = attrib(type=int)
releaselevel = attrib(type=str)

@classmethod
def _from_version_string(cls, s):
"""
Parse *s* and return a _VersionInfo.
"""
v = s.split(".")
if len(v) == 3:
v.append("final")

return cls(
year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3]
)

def _ensure_tuple(self, other):
"""
Ensure *other* is a tuple of a valid length.

Returns a possibly transformed *other* and ourselves as a tuple of
the same length as *other*.
"""

if self.__class__ is other.__class__:
other = astuple(other)

if not isinstance(other, tuple):
raise NotImplementedError

if not (1 <= len(other) <= 4):
raise NotImplementedError

return astuple(self)[: len(other)], other

def __eq__(self, other):
try:
us, them = self._ensure_tuple(other)
except NotImplementedError:
return NotImplemented

return us == them

def __lt__(self, other):
try:
us, them = self._ensure_tuple(other)
except NotImplementedError:
return NotImplemented

# Since alphabetically "dev0" < "final" < "post1" < "post2", we don't
# have to do anything special with releaselevel for now.
return us < them
9 changes: 9 additions & 0 deletions src/attr/_version.pyi
@@ -0,0 +1,9 @@
class VersionInfo:
@property
def year(self) -> int: ...
@property
def minor(self) -> int: ...
@property
def micro(self) -> int: ...
@property
def releaselevel(self) -> str: ...
49 changes: 49 additions & 0 deletions tests/test_version.py
@@ -0,0 +1,49 @@
from __future__ import absolute_import, division, print_function

import pytest

from attr import VersionInfo
from attr._compat import PY2


@pytest.fixture(name="vi")
def fixture_vi():
return VersionInfo(19, 2, 0, "final")


class TestVersionInfo:
def test_from_string_no_releaselevel(self, vi):
"""
If there is no suffix, the releaselevel becomes "final" by default.
"""
assert vi == VersionInfo._from_version_string("19.2.0")

@pytest.mark.parametrize("other", [(), (19, 2, 0, "final", "garbage")])
def test_wrong_len(self, vi, other):
"""
Comparing with a tuple that has the wrong length raises an error.
"""
assert vi != other

if not PY2:
hynek marked this conversation as resolved.
Show resolved Hide resolved
with pytest.raises(TypeError):
vi < other

@pytest.mark.parametrize("other", [[19, 2, 0, "final"]])
def test_wrong_type(self, vi, other):
"""
Only compare to other VersionInfos or tuples.
"""
assert vi != other

def test_order(self, vi):
"""
Ordering works as expected.
"""
assert vi < (20,)
assert vi < (19, 2, 1)
assert vi > (0,)
assert vi <= (19, 2)
assert vi >= (19, 2)
assert vi > (19, 2, 0, "dev0")
assert vi < (19, 2, 0, "post1")
2 changes: 1 addition & 1 deletion tox.ini
Expand Up @@ -93,5 +93,5 @@ commands = towncrier --draft
basepython = python3.7
deps = mypy
commands =
mypy src/attr/__init__.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/validators.pyi
mypy src/attr/__init__.pyi src/attr/_version.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/validators.pyi
mypy tests/typing_example.py