diff --git a/mypy.ini b/mypy.ini --- a/mypy.ini +++ b/mypy.ini @@ -2,9 +2,11 @@ namespace_packages = True warn_unused_ignores = True - # 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 Dict, List, Optional, Union import attr +from attrs_strict import type_validator import dateutil.parser import iso8601 @@ -84,9 +85,15 @@ @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 = attr.ib( + type=bytes, + validator=type_validator()) + name = attr.ib( + type=Optional[bytes], + validator=type_validator()) + email = attr.ib( + type=Optional[bytes], + validator=type_validator()) @classmethod def from_fullname(cls, fullname: bytes): @@ -131,8 +138,12 @@ @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 = attr.ib( + type=int, + validator=type_validator()) + microseconds = attr.ib( + type=int, + validator=type_validator()) @seconds.validator def check_seconds(self, attribute, value): @@ -150,9 +161,15 @@ @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 = attr.ib( + type=Timestamp, + validator=type_validator()) + offset = attr.ib( + type=int, + validator=type_validator()) + negative_utc = attr.ib( + type=bool, + validator=type_validator()) @offset.validator def check_offset(self, attribute, value): @@ -193,25 +210,38 @@ @attr.s(frozen=True) class Origin(BaseModel): """Represents a software source: a VCS and an URL.""" - url = attr.ib(type=str) + url = attr.ib( + type=str, + validator=type_validator()) @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 = attr.ib( + type=str, + validator=type_validator()) + date = attr.ib( + type=datetime.datetime, + validator=type_validator()) 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 = attr.ib( + type=str, + validator=type_validator()) + snapshot = attr.ib( + type=Optional[Sha1Git], + validator=type_validator()) + metadata = attr.ib( + type=Optional[Dict[str, object]], + validator=type_validator(), + default=None) + visit = attr.ib( + type=Optional[int], + validator=type_validator(), + default=None) """Should not be set before calling 'origin_visit_add()'.""" def to_dict(self): @@ -225,13 +255,10 @@ @classmethod def from_dict(cls, d): """Parses the date from a string, and accepts missing visit ids.""" - d = d.copy() - date = d.pop('date') - return cls( - date=(date - if isinstance(date, datetime.datetime) - else dateutil.parser.parse(date)), - **d) + if isinstance(d['date'], str): + d = d.copy() + d['date'] = dateutil.parser.parse(d['date']) + return super().from_dict(d) class TargetType(Enum): @@ -257,8 +284,12 @@ @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 = attr.ib( + type=bytes, + validator=type_validator()) + target_type = attr.ib( + type=TargetType, + validator=type_validator()) @target.validator def check_target(self, attribute, value): @@ -279,8 +310,13 @@ @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 = attr.ib( + type=Dict[bytes, Optional[SnapshotBranch]], + validator=type_validator()) + id = attr.ib( + type=Sha1Git, + validator=type_validator(), + default=b'') @staticmethod def compute_hash(object_dict): @@ -299,18 +335,37 @@ @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], - default=None) - metadata = attr.ib(type=Optional[Dict[str, object]], - default=None) - id = attr.ib(type=Sha1Git, default=b'') + name = attr.ib( + type=bytes, + validator=type_validator()) + message = attr.ib( + type=bytes, + validator=type_validator()) + target = attr.ib( + type=Optional[Sha1Git], + validator=type_validator()) + target_type = attr.ib( + type=ObjectType, + validator=type_validator()) + synthetic = attr.ib( + type=bool, + validator=type_validator()) + author = attr.ib( + type=Optional[Person], + validator=type_validator(), + default=None) + date = attr.ib( + type=Optional[TimestampWithTimezone], + validator=type_validator(), + default=None) + metadata = attr.ib( + type=Optional[Dict[str, object]], + validator=type_validator(), + default=None) + id = attr.ib( + type=Sha1Git, + validator=type_validator(), + default=b'') @staticmethod def compute_hash(object_dict): @@ -350,19 +405,42 @@ @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 = attr.ib( + type=bytes, + validator=type_validator()) + author = attr.ib( + type=Person, + validator=type_validator()) + committer = attr.ib( + type=Person, + validator=type_validator()) + date = attr.ib( + type=Optional[TimestampWithTimezone], + validator=type_validator()) + committer_date = attr.ib( + type=Optional[TimestampWithTimezone], + validator=type_validator()) + type = attr.ib( + type=RevisionType, + validator=type_validator()) + directory = attr.ib( + type=Sha1Git, + validator=type_validator()) + synthetic = attr.ib( + type=bool, + validator=type_validator()) + metadata = attr.ib( + type=Optional[Dict[str, object]], + validator=type_validator(), + default=None) + parents = attr.ib( + type=List[Sha1Git], + validator=type_validator(), + default=attr.Factory(list)) + id = attr.ib( + type=Sha1Git, + validator=type_validator(), + default=b'') @staticmethod def compute_hash(object_dict): @@ -391,18 +469,30 @@ @attr.s(frozen=True) class DirectoryEntry(BaseModel): - name = attr.ib(type=bytes) - type = attr.ib(type=str, - validator=attr.validators.in_(['file', 'dir', 'rev'])) - target = attr.ib(type=Sha1Git) - perms = attr.ib(type=int) + name = attr.ib( + type=bytes, + validator=type_validator()) + type = attr.ib( + type=str, + validator=attr.validators.in_(['file', 'dir', 'rev'])) + target = attr.ib( + type=Sha1Git, + validator=type_validator()) + perms = attr.ib( + type=int, + validator=type_validator()) """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 = attr.ib( + type=List[DirectoryEntry], + validator=type_validator()) + id = attr.ib( + type=Sha1Git, + validator=type_validator(), + default=b'') @staticmethod def compute_hash(object_dict): @@ -461,22 +551,37 @@ @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) - - length = attr.ib(type=int) + sha1 = attr.ib( + type=bytes, + validator=type_validator()) + sha1_git = attr.ib( + type=Sha1Git, + validator=type_validator()) + sha256 = attr.ib( + type=bytes, + validator=type_validator()) + blake2s256 = attr.ib( + type=bytes, + validator=type_validator()) + + length = attr.ib( + type=int, + validator=type_validator()) status = attr.ib( type=str, - default='visible', - validator=attr.validators.in_(['visible', 'hidden'])) + validator=attr.validators.in_(['visible', 'hidden']), + default='visible') - data = attr.ib(type=Optional[bytes], default=None) + data = attr.ib( + type=Optional[bytes], + validator=type_validator(), + default=None) - ctime = attr.ib(type=Optional[datetime.datetime], - default=None) + ctime = attr.ib( + type=Optional[datetime.datetime], + validator=type_validator(), + default=None) @length.validator def check_length(self, attribute, value): @@ -518,24 +623,40 @@ @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]) - - length = attr.ib(type=Optional[int]) + sha1 = attr.ib( + type=Optional[bytes], + validator=type_validator()) + sha1_git = attr.ib( + type=Optional[Sha1Git], + validator=type_validator()) + sha256 = attr.ib( + type=Optional[bytes], + validator=type_validator()) + blake2s256 = attr.ib( + type=Optional[bytes], + validator=type_validator()) + + length = attr.ib( + type=Optional[int], + validator=type_validator()) status = attr.ib( type=str, validator=attr.validators.in_(['absent'])) - reason = attr.ib(type=Optional[str], - default=None) - - origin = attr.ib(type=Optional[Origin], - default=None) - - ctime = attr.ib(type=Optional[datetime.datetime], - default=None) + reason = attr.ib( + type=Optional[str], + validator=type_validator(), + default=None) + + origin = attr.ib( + type=Optional[Origin], + validator=type_validator(), + default=None) + + ctime = attr.ib( + type=Optional[datetime.datetime], + validator=type_validator(), + default=None) @reason.validator def check_reason(self, attribute, value):