Skip to content

Commit 4594442

Browse files
committed
fix: store unknown metadata fields
1 parent 70c2825 commit 4594442

File tree

2 files changed

+61
-9
lines changed

2 files changed

+61
-9
lines changed

pins/meta.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from typing import ClassVar
2-
from dataclasses import dataclass, asdict, field
2+
from dataclasses import dataclass, asdict, field, fields, InitVar
33
from pathlib import Path
44

55
import yaml
@@ -65,6 +65,8 @@ class Meta:
6565
6666
"""
6767

68+
_excluded: ClassVar["set[str]"] = {"name", "version", "local"}
69+
6870
title: Optional[str]
6971
description: Optional[str]
7072

@@ -89,6 +91,19 @@ class Meta:
8991
user: Mapping = field(default_factory=dict)
9092
local: Mapping = field(default_factory=dict)
9193

94+
unknown_fields: InitVar["dict | None"] = None
95+
96+
def __post_init__(self, unknown_fields: "dict | None"):
97+
unknown_fields = {} if unknown_fields is None else unknown_fields
98+
99+
self._unknown_fields = unknown_fields
100+
101+
def __getattr__(self, k):
102+
try:
103+
return self._unknown_fields[k]
104+
except KeyError:
105+
raise AttributeError(f"No metadata field not found: {k}")
106+
92107
def to_dict(self) -> Mapping:
93108
data = asdict(self)
94109

@@ -97,24 +112,34 @@ def to_dict(self) -> Mapping:
97112
def to_pin_dict(self):
98113
d = self.to_dict()
99114

100-
del d["name"]
101-
del d["version"]
102-
del d["local"]
103-
104-
# TODO: once tag writing is implemented, should keep tags field
105-
del d["tags"]
115+
for k in self._excluded:
116+
del d[k]
106117

107118
return d
108119

109120
@classmethod
110121
def from_pin_dict(cls, data, pin_name, version, local=None) -> "Meta":
111-
112122
# TODO: re-arrange Meta argument positions to reflect what's been
113123
# learned about default arguments. e.g. title was not used at some
114124
# point in api_version 1
125+
all_field_names = {entry.name for entry in fields(Meta)}
126+
127+
keep_fields = all_field_names - cls._excluded
128+
115129
extra = {"title": None} if "title" not in data else {}
116130
local = {} if local is None else local
117-
return cls(**data, **extra, name=pin_name, version=version, local=local)
131+
132+
meta_data = {k: v for k, v in data.items() if k in keep_fields}
133+
unknown = {k: v for k, v in data.items() if k not in keep_fields}
134+
135+
return cls(
136+
**meta_data,
137+
**extra,
138+
name=pin_name,
139+
version=version,
140+
local=local,
141+
unknown_fields=unknown,
142+
)
118143

119144
def to_pin_yaml(self, f: Optional[IOBase] = None) -> "str | None":
120145
data = self.to_pin_dict()

pins/tests/test_meta.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
import tempfile
3+
import yaml
34

45
from datetime import datetime
56
from io import StringIO
@@ -37,6 +38,18 @@ def test_meta_to_pin_dict_roundtrip(meta):
3738
assert meta == meta2
3839

3940

41+
def test_meta_unknown_fields():
42+
m = Meta(**META_DEFAULTS, unknown_fields={"some_other_field": 1})
43+
44+
assert m.some_other_field == 1
45+
46+
with pytest.raises(AttributeError):
47+
m.should_not_exist_here
48+
49+
assert "unknown_fields" not in m.to_pin_dict()
50+
assert "some_other_field" not in m.to_pin_dict()
51+
52+
4053
def test_meta_factory_create():
4154
mf = MetaFactory()
4255
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -72,3 +85,17 @@ def test_meta_factory_read_yaml_roundtrip(meta):
7285
meta2 = mf.read_pin_yaml(StringIO(pin_yaml), meta.name, meta.version)
7386

7487
assert meta == meta2
88+
89+
90+
def test_meta_factory_roundtrip_unknown(meta):
91+
meta_dict = meta.to_pin_dict()
92+
meta_dict["some_other_field"] = 1
93+
94+
pin_yaml = yaml.dump(meta_dict)
95+
96+
mf = MetaFactory()
97+
98+
meta2 = mf.read_pin_yaml(StringIO(pin_yaml), meta.name, meta.version)
99+
100+
assert meta2 == meta
101+
assert meta2.some_other_field == 1

0 commit comments

Comments
 (0)