Skip to content

Commit c2c062d

Browse files
committed
Fix all model classes to meet invariants
Most model classes had some bugs causing them to fail invariant checks. Fix them all. The most common issue was missing validation against type. Two larger changes include: - MaintenanceReport: some fields were declared and documented as type=datetime, but all of the code was implemented using strings (see related test update). This has been fixed to use datetime throughout. - Page previously was not frozen at all. Now it is, like the other model classes.
1 parent d6bf3c2 commit c2c062d

File tree

12 files changed

+93
-69
lines changed

12 files changed

+93
-69
lines changed

pubtools/pulplib/_impl/compat_attr.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ def fields_dict(cls):
4747
fields = attr.fields
4848
evolve = attr.evolve
4949
has = attr.has
50+
validators = attr.validators

pubtools/pulplib/_impl/model/attr.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import six
2+
13
from pubtools.pulplib._impl import compat_attr as attr
24

35

@@ -17,10 +19,14 @@
1719
PY_PULP2_CONVERTER = "_pubtools.pulplib.py_to_pulp2_converter"
1820

1921

20-
# make usage of the above less ugly
2122
def pulp_attrib(
2223
pulp_field=None, pulp_py_converter=None, py_pulp_converter=None, **kwargs
2324
):
25+
"""Drop-in replacement for attr.ib with added features:
26+
27+
- applies a validator based on type automatically
28+
- supports pulplib-specific metadata via extra keyword arguments
29+
"""
2430
metadata = kwargs.get("metadata") or {}
2531

2632
if pulp_field:
@@ -32,5 +38,19 @@ def pulp_attrib(
3238
if py_pulp_converter:
3339
metadata[PY_PULP2_CONVERTER] = py_pulp_converter
3440

41+
if "type" in kwargs:
42+
# As a convenience, you may define string types as type=str
43+
# on any python version, but what you'll actually get is
44+
# whatever's the primary string type (e.g. basestr on py2)
45+
if kwargs["type"] is str:
46+
kwargs["type"] = six.string_types[0]
47+
48+
# If you haven't defined a validator, you get one automatically
49+
# for your requested type
50+
if "validator" not in kwargs:
51+
kwargs["validator"] = attr.validators.optional(
52+
attr.validators.instance_of(kwargs["type"])
53+
)
54+
3555
kwargs["metadata"] = metadata
3656
return attr.ib(**kwargs)

pubtools/pulplib/_impl/model/convert.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ def null_convert(value):
2929

3030
def read_timestamp(value):
3131
return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
32+
33+
34+
def write_timestamp(value):
35+
return value.strftime("%Y-%m-%dT%H:%M:%SZ")

pubtools/pulplib/_impl/model/maintenance.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import jsonschema
66

77
from pubtools.pulplib._impl import compat_attr as attr
8+
from .attr import pulp_attrib
9+
from .convert import read_timestamp, write_timestamp
810
from ..schema import load_schema
911
from .frozenlist import FrozenList
1012
from .common import InvalidDataException
@@ -17,27 +19,23 @@
1719
HOSTNAME = os.environ.get("HOSTNAME")
1820

1921

20-
def iso_time_now():
21-
return datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
22-
23-
2422
@attr.s(kw_only=True, frozen=True)
2523
class MaintenanceEntry(object):
2624
"""Details about the maintenance status of a specific repository.
2725
2826
.. versionadded:: 1.4.0
2927
"""
3028

31-
repo_id = attr.ib(type=str)
29+
repo_id = pulp_attrib(type=str)
3230
"""ID of repository in maintenance.
3331
3432
Note: there is no guarantee that a repository of this ID currently exists
3533
in the Pulp server."""
36-
message = attr.ib(default=None, type=str)
34+
message = pulp_attrib(default=None, type=str)
3735
"""Why this repository is in maintenance."""
38-
owner = attr.ib(default=None, type=str)
36+
owner = pulp_attrib(default=None, type=str)
3937
"""Who set this repository in maintenance mode."""
40-
started = attr.ib(default=None, type=datetime.datetime)
38+
started = pulp_attrib(default=None, type=datetime.datetime)
4139
""":class:`~datetime.datetime` in UTC at when the maintenance started."""
4240

4341

@@ -58,14 +56,16 @@ class MaintenanceReport(object):
5856

5957
_SCHEMA = load_schema("maintenance")
6058

61-
last_updated = attr.ib(default=None, type=datetime.datetime)
59+
last_updated = pulp_attrib(default=None, type=datetime.datetime)
6260
""":class:`~datetime.datetime` in UTC when this report was last updated,
6361
if it's the first time the report is created, current time is used."""
6462

65-
last_updated_by = attr.ib(default=None, type=str)
63+
last_updated_by = pulp_attrib(default=None, type=str)
6664
"""Person/party who updated the report last time."""
6765

68-
entries = attr.ib(default=attr.Factory(FrozenList), type=list, converter=FrozenList)
66+
entries = pulp_attrib(
67+
default=attr.Factory(FrozenList), type=list, converter=FrozenList
68+
)
6969
"""A list of :class:`MaintenanceEntry` objects, indicating
7070
which repositories are in maintenance mode and details.
7171
If empty, then it means no repositories are in maintenance mode.
@@ -101,10 +101,17 @@ def _from_data(cls, data):
101101

102102
entries = []
103103
for repo_id, details in data["repos"].items():
104-
entries.append(MaintenanceEntry(repo_id=repo_id, **details))
104+
entries.append(
105+
MaintenanceEntry(
106+
repo_id=repo_id,
107+
message=details["message"],
108+
owner=details["owner"],
109+
started=read_timestamp(details["started"]),
110+
)
111+
)
105112

106113
maintenance = cls(
107-
last_updated=data["last_updated"],
114+
last_updated=read_timestamp(data["last_updated"]),
108115
last_updated_by=data["last_updated_by"],
109116
entries=entries,
110117
)
@@ -114,7 +121,7 @@ def _from_data(cls, data):
114121
def _export_dict(self):
115122
"""export a raw dictionary of maintenance report"""
116123
report = {
117-
"last_updated": self.last_updated,
124+
"last_updated": write_timestamp(self.last_updated),
118125
"last_updated_by": self.last_updated_by,
119126
"repos": {},
120127
}
@@ -125,7 +132,7 @@ def _export_dict(self):
125132
entry.repo_id: {
126133
"message": entry.message,
127134
"owner": entry.owner,
128-
"started": entry.started,
135+
"started": write_timestamp(entry.started),
129136
}
130137
}
131138
)
@@ -162,7 +169,10 @@ def add(self, repo_ids, **kwargs):
162169
for repo in repo_ids:
163170
to_add.append(
164171
MaintenanceEntry(
165-
repo_id=repo, owner=owner, message=message, started=iso_time_now()
172+
repo_id=repo,
173+
owner=owner,
174+
message=message,
175+
started=datetime.datetime.utcnow(),
166176
)
167177
)
168178
entries = list(self.entries)
@@ -181,7 +191,7 @@ def add(self, repo_ids, **kwargs):
181191
self,
182192
entries=filtered_entries,
183193
last_updated_by=owner,
184-
last_updated=iso_time_now(),
194+
last_updated=datetime.datetime.utcnow(),
185195
)
186196

187197
def remove(self, repo_ids, **kwargs):

pubtools/pulplib/_impl/model/repository/base.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import logging
33

4+
from attr import validators
45
from more_executors.futures import f_map
56

67
from ..common import PulpObject, DetachedException
@@ -32,22 +33,22 @@ class PublishOptions(object):
3233
:meth:`~pubtools.pulplib.Repository.publish`.
3334
"""
3435

35-
force = attr.ib(default=None, type=bool)
36+
force = pulp_attrib(default=None, type=bool)
3637
"""If True, Pulp should publish all data within a repository, rather than attempting
3738
to publish only changed data (or even skipping the publish).
3839
3940
Setting ``force=True`` may have a major performance impact when publishing large repos.
4041
"""
4142

42-
clean = attr.ib(default=None, type=bool)
43+
clean = pulp_attrib(default=None, type=bool)
4344
"""If True, certain publish tasks will not only publish new/changed content, but
4445
will also attempt to erase formerly published content which is no longer present
4546
in the repo.
4647
4748
Setting ``clean=True`` generally implies ``force=True``.
4849
"""
4950

50-
origin_only = attr.ib(default=None, type=bool)
51+
origin_only = pulp_attrib(default=None, type=bool)
5152
"""If ``True``, Pulp should only update the content units / origin path on
5253
remote hosts.
5354
@@ -111,21 +112,24 @@ class Repository(PulpObject):
111112
)
112113
"""ID of the product to which this repository belongs (if any)."""
113114

114-
relative_url = attr.ib(default=None, type=str)
115+
relative_url = pulp_attrib(default=None, type=str)
115116
"""Default publish URL for this repository, relative to the Pulp content root."""
116117

117-
mutable_urls = attr.ib(
118+
mutable_urls = pulp_attrib(
118119
default=attr.Factory(FrozenList), type=list, converter=FrozenList
119120
)
120121
"""A list of URLs relative to repository publish root which are expected
121122
to change at every publish (if any content of repo changed)."""
122123

123-
is_sigstore = attr.ib(default=False, type=bool)
124+
is_sigstore = pulp_attrib(default=False, type=bool)
124125
"""True if this is a sigstore repository, used for container image manifest
125126
signatures."""
126127

127128
is_temporary = pulp_attrib(
128-
default=False, type=bool, pulp_field="notes.pub_temp_repo"
129+
default=False,
130+
type=bool,
131+
validator=validators.instance_of(bool),
132+
pulp_field="notes.pub_temp_repo",
129133
)
130134
"""True if this is a temporary repository.
131135
@@ -147,12 +151,12 @@ class Repository(PulpObject):
147151
)
148152
"""A list of GPG signing key IDs used to sign content in this repository."""
149153

150-
skip_rsync_repodata = attr.ib(default=False, type=bool)
154+
skip_rsync_repodata = pulp_attrib(default=False, type=bool)
151155
"""True if this repository is explicitly configured such that a publish of
152156
this repository will not publish repository metadata to remote hosts.
153157
"""
154158

155-
_client = attr.ib(default=None, init=False, repr=False, cmp=False)
159+
_client = attr.ib(default=None, init=False, repr=False, cmp=False, hash=False)
156160
# hidden attribute for client attached to this object
157161

158162
@property

pubtools/pulplib/_impl/model/repository/container.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ContainerImageRepository(Repository):
1010

1111
type = pulp_attrib(default="docker-repo", type=str, pulp_field="notes._repo-type")
1212

13-
registry_id = attr.ib(
13+
registry_id = pulp_attrib(
1414
default=attr.Factory(lambda self: self.id, takes_self=True), type=str
1515
)
1616
"""The ID of this repository in a container image registry.

pubtools/pulplib/_impl/model/repository/file.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import logging
33

4+
from attr import validators
45
from more_executors.futures import f_flat_map, f_map
56

67
from .base import Repository, repo_type
@@ -27,6 +28,7 @@ class FileRepository(Repository):
2728
lambda self: self.id == "redhat-sigstore", takes_self=True
2829
),
2930
type=bool,
31+
validator=validators.instance_of(bool),
3032
)
3133

3234
mutable_urls = attr.ib(

pubtools/pulplib/_impl/model/task.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,26 @@ class Task(PulpObject):
1616
id = pulp_attrib(type=str, pulp_field="task_id")
1717
"""ID of this task (str)."""
1818

19-
completed = attr.ib(default=None, type=bool)
19+
completed = pulp_attrib(default=None, type=bool)
2020
"""True if this task has completed, successfully or otherwise.
2121
2222
May be `None` if the state of this task is unknown.
2323
"""
2424

25-
succeeded = attr.ib(default=None, type=bool)
25+
succeeded = pulp_attrib(default=None, type=bool)
2626
"""True if this task has completed successfully.
2727
2828
May be `None` if the state of this task is unknown.
2929
"""
3030

31-
error_summary = attr.ib(default=None, type=str)
31+
error_summary = pulp_attrib(default=None, type=str)
3232
"""A summary of the reason for this task's failure (if any).
3333
3434
This is a short string, generally a single line, suitable for display to users.
3535
The string includes the ID of the failed task.
3636
"""
3737

38-
error_details = attr.ib(default=None, type=str)
38+
error_details = pulp_attrib(default=None, type=str)
3939
"""Detailed information for this task's failure (if any).
4040
4141
This may be a multi-line string and may include technical information such as
@@ -62,7 +62,7 @@ class Task(PulpObject):
6262
"pulp:action:publish"]
6363
"""
6464

65-
repo_id = attr.ib(type=str)
65+
repo_id = pulp_attrib(type=str)
6666
"""The ID of the repository associated with this task, otherwise None."""
6767

6868
units = pulp_attrib(

pubtools/pulplib/_impl/page.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import logging
22
import weakref
3+
from concurrent.futures import Future
34

45
from . import compat_attr as attr
6+
from .model.frozenlist import FrozenList
7+
from .model.attr import pulp_attrib
58

69
LOG = logging.getLogger("pubtools.pulplib")
710

811

9-
@attr.s(kw_only=True)
12+
# pylint shows these spurious errors on this file, not sure why:
13+
# page.py:86:23: E1101: Instance of '_CountingAttr' has no 'done' member (no-member)
14+
# Could be due to bug https://github.com/PyCQA/pylint/issues/1694 ?
15+
# pylint: disable=no-member
16+
17+
18+
@attr.s(kw_only=True, frozen=True)
1019
class Page(object):
1120
"""A page of Pulp search results.
1221
@@ -54,14 +63,16 @@ def handle_results(page):
5463
5564
"""
5665

57-
data = attr.ib(default=attr.Factory(list))
66+
data = pulp_attrib(
67+
default=attr.Factory(FrozenList), type=list, converter=FrozenList
68+
)
5869
"""List of Pulp objects in this page.
5970
6071
This list will contain instances of the appropriate Pulp object type
6172
(e.g. :class:`~pubtools.pulplib.Repository` for a repository search).
6273
"""
6374

64-
next = attr.ib(default=None)
75+
next = pulp_attrib(default=None, type=Future)
6576
"""None, if this is the last page of results.
6677
6778
Otherwise, a Future[:class:`Page`] for the next page of results.

tests/client/test_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import datetime
23
import json
34
import pytest
45
import requests_mock
@@ -201,7 +202,7 @@ def test_can_get_maintenance_report(client, requests_mocker):
201202
assert requests_mocker.call_count == 1
202203
assert isinstance(report, MaintenanceReport)
203204
assert report.last_updated_by == "Content Delivery"
204-
assert report.last_updated == "2019-08-15T14:21:12Z"
205+
assert report.last_updated == datetime.datetime(2019, 8, 15, 14, 21, 12)
205206
assert report.entries[0].message == "Maintenance Mode Enabled"
206207

207208

tests/maintenance/test_maintenance.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
import datetime
23

34
from pubtools.pulplib import MaintenanceReport, MaintenanceEntry, InvalidDataException
45

@@ -18,7 +19,7 @@ def test_add_entry_existed():
1819
repo_id="repo1",
1920
owner="someone",
2021
message="Enabled",
21-
started="2019-08-15T14:21:12Z",
22+
started=datetime.datetime(2019, 8, 15, 14, 21, 12),
2223
)
2324

2425
report = MaintenanceReport(entries=(entry,))

0 commit comments

Comments
 (0)