diff --git a/mypy.ini b/mypy.ini --- a/mypy.ini +++ b/mypy.ini @@ -1,10 +1,13 @@ [mypy] namespace_packages = True warn_unused_ignores = True - +plugins = swh.model.mypy_plugin # 3rd party libraries without stubs (yet) +[mypy-attrs_strict.*] # a bit sad, but... +ignore_missing_imports = True + [mypy-django.*] # false positive, only used my hypotesis' extras ignore_missing_imports = True diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ # dependency lines, see https://pip.readthedocs.org/en/1.1/requirements.html vcversioner attrs +attrs_strict hypothesis python-dateutil iso8601 diff --git a/swh/model/model.py b/swh/model/model.py --- a/swh/model/model.py +++ b/swh/model/model.py @@ -7,9 +7,10 @@ from abc import ABCMeta, abstractmethod from enum import Enum -from typing import List, Optional, Dict, Union +from typing import Any, Dict, List, Optional, Type, Union import attr +from attrs_strict import type_validator import dateutil.parser import iso8601 @@ -32,6 +33,14 @@ Sha1Git = bytes +# cannot use a partial here because of +# https://github.com/bloomberg/attrs-strict/issues/24 +def ibv(default: Any = attr.NOTHING, + type: Optional[Type] = None): + "A 'partial' of attr.ib that prefill the validator with type_validator" + return attr.ib(default=default, type=type, validator=type_validator()) + + def dictify(value): "Helper function used by BaseModel.to_dict()" if isinstance(value, BaseModel): @@ -84,9 +93,9 @@ @attr.s(frozen=True) class Person(BaseModel): """Represents the author/committer of a revision or release.""" - fullname = attr.ib(type=bytes) - name = attr.ib(type=Optional[bytes]) - email = attr.ib(type=Optional[bytes]) + fullname = ibv(type=bytes) + name = ibv(type=Optional[bytes]) + email = ibv(type=Optional[bytes]) @classmethod def from_fullname(cls, fullname: bytes): @@ -131,8 +140,8 @@ @attr.s(frozen=True) class Timestamp(BaseModel): """Represents a naive timestamp from a VCS.""" - seconds = attr.ib(type=int) - microseconds = attr.ib(type=int) + seconds = ibv(type=int) + microseconds = ibv(type=int) @seconds.validator def check_seconds(self, attribute, value): @@ -150,9 +159,9 @@ @attr.s(frozen=True) class TimestampWithTimezone(BaseModel): """Represents a TZ-aware timestamp from a VCS.""" - timestamp = attr.ib(type=Timestamp) - offset = attr.ib(type=int) - negative_utc = attr.ib(type=bool) + timestamp = ibv(type=Timestamp) + offset = ibv(type=int) + negative_utc = ibv(type=bool) @offset.validator def check_offset(self, attribute, value): @@ -193,25 +202,24 @@ @attr.s(frozen=True) class Origin(BaseModel): """Represents a software source: a VCS and an URL.""" - url = attr.ib(type=str) + url = ibv(type=str) @attr.s(frozen=True) class OriginVisit(BaseModel): """Represents a visit of an origin at a given point in time, by a SWH loader.""" - origin = attr.ib(type=str) - date = attr.ib(type=datetime.datetime) + origin = ibv(type=str) + date = ibv(type=datetime.datetime) status = attr.ib( type=str, validator=attr.validators.in_(['ongoing', 'full', 'partial'])) - type = attr.ib(type=str) - snapshot = attr.ib(type=Optional[Sha1Git]) - metadata = attr.ib(type=Optional[Dict[str, object]], - default=None) - - visit = attr.ib(type=Optional[int], - default=None) + type = ibv(type=str) + snapshot = ibv(type=Optional[Sha1Git]) + metadata = ibv(type=Optional[Dict[str, object]], + default=None) + visit = ibv(type=Optional[int], + default=None) """Should not be set before calling 'origin_visit_add()'.""" def to_dict(self): @@ -257,8 +265,8 @@ @attr.s(frozen=True) class SnapshotBranch(BaseModel): """Represents one of the branches of a snapshot.""" - target = attr.ib(type=bytes) - target_type = attr.ib(type=TargetType) + target = ibv(type=bytes) + target_type = ibv(type=TargetType) @target.validator def check_target(self, attribute, value): @@ -279,8 +287,8 @@ @attr.s(frozen=True) class Snapshot(BaseModel, HashableObject): """Represents the full state of an origin at a given point in time.""" - branches = attr.ib(type=Dict[bytes, Optional[SnapshotBranch]]) - id = attr.ib(type=Sha1Git, default=b'') + branches = ibv(type=Dict[bytes, Optional[SnapshotBranch]]) + id = ibv(type=Sha1Git, default=b'') @staticmethod def compute_hash(object_dict): @@ -299,18 +307,18 @@ @attr.s(frozen=True) class Release(BaseModel, HashableObject): - name = attr.ib(type=bytes) - message = attr.ib(type=bytes) - target = attr.ib(type=Optional[Sha1Git]) - target_type = attr.ib(type=ObjectType) - synthetic = attr.ib(type=bool) - author = attr.ib(type=Optional[Person], - default=None) - date = attr.ib(type=Optional[TimestampWithTimezone], + name = ibv(type=bytes) + message = ibv(type=bytes) + target = ibv(type=Optional[Sha1Git]) + target_type = ibv(type=ObjectType) + synthetic = ibv(type=bool) + author = ibv(type=Optional[Person], + default=None) + date = ibv(type=Optional[TimestampWithTimezone], + default=None) + metadata = ibv(type=Optional[Dict[str, object]], default=None) - metadata = attr.ib(type=Optional[Dict[str, object]], - default=None) - id = attr.ib(type=Sha1Git, default=b'') + id = ibv(type=Sha1Git, default=b'') @staticmethod def compute_hash(object_dict): @@ -350,19 +358,19 @@ @attr.s(frozen=True) class Revision(BaseModel, HashableObject): - message = attr.ib(type=bytes) - author = attr.ib(type=Person) - committer = attr.ib(type=Person) - date = attr.ib(type=Optional[TimestampWithTimezone]) - committer_date = attr.ib(type=Optional[TimestampWithTimezone]) - type = attr.ib(type=RevisionType) - directory = attr.ib(type=Sha1Git) - synthetic = attr.ib(type=bool) - metadata = attr.ib(type=Optional[Dict[str, object]], - default=None) - parents = attr.ib(type=List[Sha1Git], - default=attr.Factory(list)) - id = attr.ib(type=Sha1Git, default=b'') + message = ibv(type=bytes) + author = ibv(type=Person) + committer = ibv(type=Person) + date = ibv(type=Optional[TimestampWithTimezone]) + committer_date = ibv(type=Optional[TimestampWithTimezone]) + type = ibv(type=RevisionType) + directory = ibv(type=Sha1Git) + synthetic = ibv(type=bool) + metadata = ibv(type=Optional[Dict[str, object]], + default=None) + parents = ibv(type=List[Sha1Git], + default=attr.Factory(list)) + id = ibv(type=Sha1Git, default=b'') @staticmethod def compute_hash(object_dict): @@ -391,18 +399,18 @@ @attr.s(frozen=True) class DirectoryEntry(BaseModel): - name = attr.ib(type=bytes) + name = ibv(type=bytes) type = attr.ib(type=str, validator=attr.validators.in_(['file', 'dir', 'rev'])) - target = attr.ib(type=Sha1Git) - perms = attr.ib(type=int) + target = ibv(type=Sha1Git) + perms = ibv(type=int) """Usually one of the values of `swh.model.from_disk.DentryPerms`.""" @attr.s(frozen=True) class Directory(BaseModel, HashableObject): - entries = attr.ib(type=List[DirectoryEntry]) - id = attr.ib(type=Sha1Git, default=b'') + entries = ibv(type=List[DirectoryEntry]) + id = ibv(type=Sha1Git, default=b'') @staticmethod def compute_hash(object_dict): @@ -461,22 +469,22 @@ @attr.s(frozen=True) class Content(BaseContent): - sha1 = attr.ib(type=bytes) - sha1_git = attr.ib(type=Sha1Git) - sha256 = attr.ib(type=bytes) - blake2s256 = attr.ib(type=bytes) + sha1 = ibv(type=bytes) + sha1_git = ibv(type=Sha1Git) + sha256 = ibv(type=bytes) + blake2s256 = ibv(type=bytes) - length = attr.ib(type=int) + length = ibv(type=int) status = attr.ib( type=str, default='visible', validator=attr.validators.in_(['visible', 'hidden'])) - data = attr.ib(type=Optional[bytes], default=None) + data = ibv(type=Optional[bytes], default=None) - ctime = attr.ib(type=Optional[datetime.datetime], - default=None) + ctime = ibv(type=Optional[datetime.datetime], + default=None) @length.validator def check_length(self, attribute, value): @@ -518,24 +526,24 @@ @attr.s(frozen=True) class SkippedContent(BaseContent): - sha1 = attr.ib(type=Optional[bytes]) - sha1_git = attr.ib(type=Optional[Sha1Git]) - sha256 = attr.ib(type=Optional[bytes]) - blake2s256 = attr.ib(type=Optional[bytes]) + sha1 = ibv(type=Optional[bytes]) + sha1_git = ibv(type=Optional[Sha1Git]) + sha256 = ibv(type=Optional[bytes]) + blake2s256 = ibv(type=Optional[bytes]) - length = attr.ib(type=Optional[int]) + length = ibv(type=Optional[int]) status = attr.ib( type=str, validator=attr.validators.in_(['absent'])) - reason = attr.ib(type=Optional[str], - default=None) + reason = ibv(type=Optional[str], + default=None) - origin = attr.ib(type=Optional[Origin], - default=None) + origin = ibv(type=Optional[Origin], + default=None) - ctime = attr.ib(type=Optional[datetime.datetime], - default=None) + ctime = ibv(type=Optional[datetime.datetime], + default=None) @reason.validator def check_reason(self, attribute, value): diff --git a/swh/model/mypy_plugin.py b/swh/model/mypy_plugin.py new file mode 100644 --- /dev/null +++ b/swh/model/mypy_plugin.py @@ -0,0 +1,16 @@ +from mypy.plugin import Plugin +from mypy.plugins.attrs import ( + attr_attrib_makers, +) + +# These are our attr.ib makers. +attr_attrib_makers.add("swh.model.model.ibv") + + +class MyPlugin(Plugin): + # Our plugin does nothing but it has to exist so this file gets loaded. + pass + + +def plugin(version): + return MyPlugin