Skip to content
Snippets Groups Projects
Commit 8ebbd216 authored by vlorentz's avatar vlorentz
Browse files

Split Content class into two classes, for missing and non-missing contents.

parent 4b779e1e
No related branches found
No related tags found
No related merge requests found
...@@ -16,7 +16,7 @@ from .from_disk import DentryPerms ...@@ -16,7 +16,7 @@ from .from_disk import DentryPerms
from .model import ( from .model import (
Person, Timestamp, TimestampWithTimezone, Origin, OriginVisit, Person, Timestamp, TimestampWithTimezone, Origin, OriginVisit,
Snapshot, SnapshotBranch, TargetType, Release, Revision, Snapshot, SnapshotBranch, TargetType, Release, Revision,
Directory, DirectoryEntry, Content Directory, DirectoryEntry, Content, SkippedContent
) )
from .identifiers import snapshot_identifier, identifier_to_bytes from .identifiers import snapshot_identifier, identifier_to_bytes
...@@ -128,14 +128,12 @@ def directories(): ...@@ -128,14 +128,12 @@ def directories():
entries=lists(directory_entries())) entries=lists(directory_entries()))
@composite def contents():
def contents(draw): return one_of(present_contents(), skipped_contents())
(status, data, reason) = draw(one_of(
tuples(just('visible'), binary(), none()),
tuples(just('absent'), none(), pgsql_text()),
tuples(just('hidden'), binary(), none()),
))
@composite
def present_contents(draw):
return draw(builds( return draw(builds(
Content, Content,
length=integers(min_value=0, max_value=2**63-1), length=integers(min_value=0, max_value=2**63-1),
...@@ -143,9 +141,25 @@ def contents(draw): ...@@ -143,9 +141,25 @@ def contents(draw):
sha1_git=sha1_git(), sha1_git=sha1_git(),
sha256=binary(min_size=32, max_size=32), sha256=binary(min_size=32, max_size=32),
blake2s256=binary(min_size=32, max_size=32), blake2s256=binary(min_size=32, max_size=32),
status=just(status), status=one_of(just('visible'), just('hidden')),
data=just(data), data=binary(),
reason=just(reason), ))
@composite
def skipped_contents(draw):
def optional(strategy):
return one_of(none(), strategy)
return draw(builds(
SkippedContent,
length=optional(integers(min_value=0, max_value=2**63-1)),
sha1=optional(sha1()),
sha1_git=optional(sha1_git()),
sha256=optional(binary(min_size=32, max_size=32)),
blake2s256=optional(binary(min_size=32, max_size=32)),
status=just('absent'),
reason=pgsql_text(),
)) ))
......
...@@ -361,18 +361,42 @@ class Directory(BaseModel, HashableObject): ...@@ -361,18 +361,42 @@ class Directory(BaseModel, HashableObject):
@attr.s(frozen=True) @attr.s(frozen=True)
class Content(BaseModel): class BaseContent(BaseModel):
def to_dict(self):
content = super().to_dict()
if content['ctime'] is None:
del content['ctime']
return content
@classmethod
def from_dict(cls, d, use_subclass=True):
if use_subclass:
# Chooses a subclass to instantiate instead.
if d['status'] == 'absent':
return SkippedContent.from_dict(d)
else:
return Content.from_dict(d)
else:
return super().from_dict(d)
def get_hash(self, hash_name):
if hash_name not in DEFAULT_ALGORITHMS:
raise ValueError('{} is not a valid hash name.'.format(hash_name))
return getattr(self, hash_name)
@attr.s(frozen=True)
class Content(BaseContent):
sha1 = attr.ib(type=bytes) sha1 = attr.ib(type=bytes)
sha1_git = attr.ib(type=Sha1Git) sha1_git = attr.ib(type=Sha1Git)
sha256 = attr.ib(type=bytes) sha256 = attr.ib(type=bytes)
blake2s256 = attr.ib(type=bytes) blake2s256 = attr.ib(type=bytes)
length = attr.ib(type=int) length = attr.ib(type=int)
status = attr.ib( status = attr.ib(
type=str, type=str,
validator=attr.validators.in_(['visible', 'absent', 'hidden'])) validator=attr.validators.in_(['visible', 'hidden']))
reason = attr.ib(type=Optional[str],
default=None)
data = attr.ib(type=Optional[bytes], data = attr.ib(type=Optional[bytes],
default=None) default=None)
...@@ -382,29 +406,64 @@ class Content(BaseModel): ...@@ -382,29 +406,64 @@ class Content(BaseModel):
@length.validator @length.validator
def check_length(self, attribute, value): def check_length(self, attribute, value):
"""Checks the length is positive.""" """Checks the length is positive."""
if self.status == 'absent' and value < -1: if self.status != 'absent' and value < 0:
raise ValueError('Length must be positive or -1.') raise ValueError('Length must be positive.')
elif self.status != 'absent' and value < 0:
raise ValueError('Length must be positive, unless status=absent.') def to_dict(self):
content = super().to_dict()
if content['data'] is None:
del content['data']
return content
@classmethod
def from_dict(cls, d):
return super().from_dict(d, use_subclass=False)
@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])
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.validator @reason.validator
def check_reason(self, attribute, value): def check_reason(self, attribute, value):
"""Checks the reason is full if status != absent.""" """Checks the reason is full if status != absent."""
assert self.reason == value assert self.reason == value
if self.status == 'absent' and value is None: if value is None:
raise ValueError('Must provide a reason if content is absent.') raise ValueError('Must provide a reason if content is absent.')
elif self.status != 'absent' and value is not None:
raise ValueError( @length.validator
'Must not provide a reason if content is not absent.') def check_length(self, attribute, value):
"""Checks the length is positive or -1."""
if value is not None and value < -1:
raise ValueError('Length must be positive or -1.')
def to_dict(self): def to_dict(self):
content = super().to_dict() content = super().to_dict()
for field in ('data', 'reason', 'ctime'): if content['origin'] is None:
if content[field] is None: del content['origin']
del content[field]
return content return content
def get_hash(self, hash_name): @classmethod
if hash_name not in DEFAULT_ALGORITHMS: def from_dict(cls, d):
raise ValueError('{} is not a valid hash name.'.format(hash_name)) d2 = d
return getattr(self, hash_name) d = d.copy()
if d.pop('data', None) is not None:
raise ValueError('SkippedContent has no "data" attribute %r' % d2)
return super().from_dict(d, use_subclass=False)
...@@ -55,7 +55,7 @@ def gen_content(): ...@@ -55,7 +55,7 @@ def gen_content():
**h.digest()} **h.digest()}
if status == 'absent': if status == 'absent':
content['reason'] = 'why not' content['reason'] = 'why not'
content['data'] = b'' content['data'] = None
return content return content
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
from .generate_testdata import gen_contents, gen_origins, ORIGINS from .generate_testdata import gen_contents, gen_origins, ORIGINS
from swh.model.model import Origin, Content from swh.model.model import Origin, BaseContent
def test_gen_origins_empty(): def test_gen_origins_empty():
...@@ -43,12 +43,12 @@ def test_gen_contents_empty(): ...@@ -43,12 +43,12 @@ def test_gen_contents_empty():
def test_gen_contents_one(): def test_gen_contents_one():
contents = gen_contents(1) contents = gen_contents(1)
assert len(contents) == 1 assert len(contents) == 1
assert [Content.from_dict(d) for d in contents] assert [BaseContent.from_dict(d) for d in contents]
def test_gen_contents_default(): def test_gen_contents_default():
contents = gen_contents() contents = gen_contents()
assert len(contents) == 20 assert len(contents) == 20
models = {Content.from_dict(d) for d in contents} models = {BaseContent.from_dict(d) for d in contents}
# ensure we did not generate the same content twice # ensure we did not generate the same content twice
assert len(contents) == len(models) assert len(contents) == len(models)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment