Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 3 additions & 14 deletions osc/gitea_api/maintainership.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ class MaintainerInfo(BaseModel):
"""
A model representing users and groups associated with a project or package.
"""
users: Optional[List[str]] = Field(default=None)
groups: Optional[List[str]] = Field(default=None)
users: Optional[List[str]] = Field()
groups: Optional[List[str]] = Field()


class MaintainershipDocumentType(str, Enum):
Expand All @@ -19,7 +19,7 @@ class MaintainershipHeader(BaseModel):
"""
A model representing the maintainership document header.
"""
document: MaintainershipDocumentType = Field()
document: MaintainershipDocumentType = Field(default="obs-maintainers")
version: str = Field(default="1.0")


Expand Down Expand Up @@ -73,17 +73,6 @@ def from_string(cls, text: str) -> "Maintainership":

return cls(**data)

def to_string(self):
"""
Export _maintainership.json contents.

We always:
- exclude entries that have empty (None) value
- sort keys
- indent by 2 spaces
"""
return super().to_string(exclude_none=True, sort_keys=True, indent=2)

def get_package_maintainers_users(self, package: str) -> List[str]:
if package not in self.packages:
raise ValueError(f"Package '{package}' not found in maintainership data.")
Expand Down
54 changes: 21 additions & 33 deletions osc/util/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ class UnionType:
"BaseModel",
"XmlModel",
"Field",
"NotSet",
"FromParent",
"Enum",
"Dict",
Expand All @@ -71,19 +70,8 @@ class UnionType:
)


class NotSetClass:
def __repr__(self):
return "NotSet"

def __bool__(self):
return False


NotSet = NotSetClass()


class FromParent(NotSetClass):
def __init__(self, field_name, *, fallback=NotSet):
class FromParent:
def __init__(self, field_name, *, fallback=None):
self.field_name = field_name
self.fallback = fallback

Expand All @@ -95,14 +83,13 @@ def __repr__(self):
class Field(property, *([Any] if typing.TYPE_CHECKING else [])):
def __init__(
self,
default: Any = NotSet,
default: Any = None,
description: Optional[str] = None,
exclude: bool = False,
get_callback: Optional[Callable] = None,
**extra,
):
# the default value; it can be a factory function that is lazily evaluated on the first use
# model sets it to None if it equals to NotSet (for better usability)
self.default = default

# a flag indicating, whether the default is a callable with lazy evalution
Expand All @@ -123,7 +110,7 @@ def __init__(
# append information about the default value
if isinstance(self.default, FromParent):
self.__doc__ += f"\n\nDefault: inherited from parent config's field ``{self.default.field_name}``"
elif self.default is not NotSet:
elif self.default is not None:
self.__doc__ += f"\n\nDefault: ``{self.default}``"

# whether to exclude this field from export
Expand Down Expand Up @@ -308,13 +295,13 @@ def get(self, obj):

if isinstance(self.default, FromParent):
if obj._parent is None:
if self.default.fallback is not NotSet:
if self.default.fallback is not None or self.is_optional:
return self.default.fallback
else:
raise RuntimeError(f"The field '{self.name}' has default {self.default} but the model has no parent set")
return getattr(obj._parent, self.default.field_name or self.name)

if self.default is NotSet:
if self.default is None and not self.is_optional:
raise RuntimeError(f"The field '{self.name}' has no default")

# make a deepcopy to avoid problems with mutable defaults
Expand Down Expand Up @@ -396,10 +383,6 @@ def __new__(mcs, name, bases, attrs):
# set annotation for the getter so it shows up in sphinx
field.get_copy.__func__.__annotations__ = {"return": field.type}

# set 'None' as the default for optional fields
if field.default is NotSet and field.is_optional:
field.default = None

return new_cls


Expand All @@ -424,7 +407,7 @@ def __init__(self, **kwargs):

for name, field in self.__fields__.items():
if name not in kwargs:
if field.default is NotSet:
if field.default is None and not field.is_optional:
uninitialized_fields.append(field.name)
continue
value = kwargs.pop(name)
Expand Down Expand Up @@ -469,20 +452,20 @@ def __lt__(self, other):
return False
return self._get_cmp_data() < other._get_cmp_data()

def dict(self, *, exclude_none: bool = False):
def dict(self):
result = {}
for name, field in self.__fields__.items():
if field.exclude:
continue
value = getattr(self, name)
if exclude_none and value is None:
if value is None:
continue
if value is not None and field.is_model:
result[name] = value.dict(exclude_none=exclude_none)
result[name] = value.dict()
elif value is not None and field.is_model_list:
result[name] = [i.dict(exclude_none=exclude_none) for i in value]
result[name] = [i.dict() for i in value]
elif value is not None and field.is_model_dict:
result[name] = {k: v.dict(exclude_none=exclude_none) for k, v in value.items()}
result[name] = {k: v.dict() for k, v in value.items()}
else:
result[name] = value

Expand All @@ -508,8 +491,8 @@ def to_file(self, path: str):
import json

with open(path, "w", encoding="utf-8") as f:
# we prefer key ordering according to the fields in the model
json.dump(self.dict(), f, sort_keys=False, indent=4)
# we prefer fixed, well-defined key ordering
json.dump(self.dict(), f, sort_keys=True, indent=2)

@classmethod
def from_string(cls, text: str) -> "Self":
Expand All @@ -522,13 +505,18 @@ def from_string(cls, text: str) -> "Self":
obj = cls(**data)
return obj

def to_string(self, *, exclude_none: bool = False, sort_keys: bool = False, indent: int = 4) -> str:
def to_string(self) -> str:
"""
Dump model to a json string.

We always:
- exclude entries that have empty (None) value
- sort keys
- indent by 2 spaces
"""
import json

result = json.dumps(self.dict(exclude_none=exclude_none), sort_keys=sort_keys, indent=indent)
result = json.dumps(self.dict(), sort_keys=True, indent=2)
return result

def do_snapshot(self):
Expand Down
12 changes: 2 additions & 10 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,14 @@ def test_get_origin_list_str(self):
self.assertEqual(typ, list)


class TestNotSet(unittest.TestCase):
def test_repr(self):
self.assertEqual(repr(NotSet), "NotSet")

def test_bool(self):
self.assertEqual(bool(NotSet), False)


class Test(unittest.TestCase):
@unittest.skipIf(sys.version_info[:2] < (3, 10), "added in python 3.10")
def test_union_or(self):
class TestModel(BaseModel):
text: str | None = Field()

m = TestModel()
self.assertEqual(m.dict(), {"text": None})
self.assertEqual(m.dict(), {})

self.assertRaises(TypeError, setattr, m.text, 123)

Expand All @@ -45,7 +37,7 @@ class TestModel(BaseModel):
sub: Optional[List[TestSubmodel]] = Field(default=None)

m = TestModel()
self.assertEqual(m.dict(), {"a": "default", "b": None, "sub": None})
self.assertEqual(m.dict(), {"a": "default"})

m.b = "B"
m.sub = [{"text": "one"}, {"text": "two"}]
Expand Down
Loading