Fix issues in var replacement with defaults; allow dotted vars; allow nested replacements

master
Douglas Blank 2018-07-28 07:42:59 -04:00
rodzic c502fe0982
commit edcfea4ef6
2 zmienionych plików z 149 dodań i 15 usunięć

Wyświetl plik

@ -1,4 +1,5 @@
import mimetypes
import copy
class ActivityPubBase():
CLASSES = {}
@ -17,7 +18,7 @@ class ActivityPubBase():
for key in self.manager.defaults:
# Person.id
if key.startswith(self.ap_type + "."):
attr = self.manager.defaults[key]
attr = copy.deepcopy(self.manager.defaults[key])
if callable(attr):
attr = attr()
attr_name = "ap_" + key[len(self.ap_type + "."):]
@ -27,26 +28,113 @@ class ActivityPubBase():
## recursive:
setattr(self, attr_name,
attr.replace("$" + attr_name[3:], getattr(self, attr_name)))
## Now expand the field defaults:
## Now expand the field defaults, and build dependencies:
dependencies = {}
for attr_name in dir(self):
if attr_name.startswith("ap_"):
attr = getattr(self, attr_name)
if isinstance(attr, str) and "$" in attr:
dependencies[attr_name[3:]] = {x[1:] for x in self.manager.parse(attr)
parsed = self.manager.parse(attr)
dependencies[attr_name[3:]] = {x[1:].split(".")[0] for x in parsed
if x.startswith("$") and x[1:] != attr_name[3:]}
elif isinstance(attr, dict):
deps = self.build_dependencies_from_dict(attr, set())
for item in deps:
dependencies[attr_name[3:]] = dependencies.get(key, set())
dependencies[attr_name[3:]].add(item)
## Now, replace them in order:
for attr_name in self.topological_sort(dependencies):
if "$" + attr_name in self.manager.defaults:
attr = self.manager.defaults["$" + attr_name]
attr = copy.deepcopy(self.manager.defaults["$" + attr_name])
else:
attr = getattr(self, "ap_" + attr_name)
if hasattr(self, "ap_" + attr_name):
attr = getattr(self, "ap_" + attr_name)
elif "." in attr_name:
attr = self.get_item_from_dotted(attr_name)
else:
raise Exception("unknown variable: %s" % attr_name)
if callable(attr):
attr = attr()
if attr is None:
raise Exception("variable depends on field that is empty: %s" % attr_name)
if isinstance(attr, str) and "$" in attr:
setattr(self, attr_name, self.manager.expand_defaults(attr, self))
elif isinstance(attr, dict):
## traverse dict recursively, looking for replacements:
self.replace_items_in_dict(attr)
## Finally, remove any temporary variables:
for attr_name in dir(self):
if attr_name.startswith("ap_temp"):
del self.__dict__[attr_name]
def get_item_from_dotted(self, dotted_word):
"""
Get dictionary item from a dotted-word.
>>> n = Note()
>>> n.key1 = {"key2": {"key3": 42}}
>>> n.get_item_from_dotted("key1.key2.key3")
42
>>> n.ap_key4 = {"key5": {"key6": 43}}
>>> n.get_item_from_dotted("key4.key5.key6")
43
"""
current = {key: getattr(self, key) for key in dir(self)}
for word in dotted_word.split("."):
if "ap_" + word in current:
current = current["ap_" + word]
elif word in current:
current = current[word]
else:
return None
return current
def build_dependencies_from_dict(self, dictionary, s):
"""
Given {"val": "$x"} return set("x")
s is a set, returns s with dependencies.
>>> n = Note()
>>> n.build_dependencies_from_dict({"val": "$x"}, set())
{'x'}
>>> n.build_dependencies_from_dict({"key1": {"val": "$x"}}, set())
{'x'}
>>> s = n.build_dependencies_from_dict({"key1": {"val": "$x"},
... "key2": {"key3": "$y"}}, set())
>>> "x" in s
True
>>> "y" in s
True
>>> len(s)
2
"""
for key in dictionary:
if isinstance(dictionary[key], str):
if dictionary[key].startswith("$"):
s.add(dictionary[key][1:])
elif isinstance(dictionary[key], dict):
self.build_dependencies_from_dict(dictionary[key], s)
return s
def replace_items_in_dict(self, dictionary):
"""
Replace the "$x" in {"val": "$x"} with self.ap_x
>>> n = Note()
>>> n.ap_x = 41
>>> n.ap_y = 43
>>> dictionary = {"key1": {"val": "$x"},
... "key2": {"key3": "$y"}}
>>> n.replace_items_in_dict(dictionary)
>>> dictionary
{'key1': {'val': 41}, 'key2': {'key3': 43}}
"""
for key in dictionary:
if isinstance(dictionary[key], str):
if dictionary[key].startswith("$"):
dictionary[key] = getattr(self, "ap_" + dictionary[key][1:])
elif isinstance(dictionary[key], dict):
self.replace_items_in_dict(dictionary[key])
def topological_sort(self, data):
"""

Wyświetl plik

@ -1,3 +1,4 @@
import datetime
import inspect
import binascii
import os
@ -100,7 +101,33 @@ class Manager():
>>> from activitypub.database import ListDatabase
>>> db = ListDatabase()
>>> manager = Manager(database=db)
>>>
>>> note = manager.Note(
... **{'sensitive': False,
... 'attributedTo': 'http://localhost:5005',
... 'cc': ['http://localhost:5005/followers'],
... 'to': ['https://www.w3.org/ns/activitystreams#Public'],
... 'content': '<p>$source.content</p>',
... 'tag': [],
... 'source': {'mediaType': 'text/markdown', 'content': '$temp_text'},
... 'published': '$NOW',
... 'temp_uuid': "$UUID",
... 'temp_text': 'Hello',
... 'id': 'http://localhost:5005/outbox/$temp_uuid/activity',
... 'url': 'http://localhost:5005/note/$temp_uuid'
... })
>>> note.content == '<p>Hello</p>'
True
>>> note.source == {'mediaType': 'text/markdown', 'content': 'Hello'}
True
>>> "$temp_uuid" not in note.id
True
>>> "$temp_uuid" not in note.url
True
>>> hasattr(note, "ap_temp_text")
False
>>> hasattr(note, "ap_temp_uuid")
False
"""
app_name = "activitypub"
version = "1.0.0"
@ -112,6 +139,7 @@ class Manager():
self.context = context
self.defaults = defaults or self.make_defaults()
self.defaults["$UUID"] = lambda: str(uuid.uuid4())
self.defaults["$NOW"] = lambda: datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
self.database = database
self.port = port
self.config = {}
@ -207,24 +235,42 @@ class Manager():
def expand_defaults(self, string, obj=None):
"""
Expand a string with defaults.
>>> m = Manager()
>>> m.defaults = {"$TEST": "hello",
... "Note.id": {"key1": "xxx"},
... }
>>> m.expand_defaults("$TEST")
'hello'
>>> m.expand_defaults("<p>$TEST</p>")
'<p>hello</p>'
>>> n = m.Note()
>>> n.ap_id == {"key1": "xxx"}
True
"""
for key in self.defaults:
if key.startswith("$"):
if callable(self.defaults[key]):
string = string.replace(key, self.defaults[key]())
else:
string = string.replace(key, self.defaults[key])
if key in string:
if callable(self.defaults[key]):
string = string.replace(key, str(self.defaults[key]()))
else:
string = string.replace(key, str(self.defaults[key]))
if obj:
for key in self.parse(string):
if key.startswith("$"):
if getattr(obj, "ap_" + key[1:]) is None:
raise Exception("expansion requires %s" % key[1:])
string = string.replace(key, getattr(obj, "ap_" + key[1:]))
if key in string:
if hasattr(obj, "ap_" + key[1:]):
val = getattr(obj, "ap_" + key[1:])
elif "." in key:
val = obj.get_item_from_dotted("ap_" + key[1:])
else:
raise Exception("expansion requires %s" % key[1:])
string = string.replace(key, str(val))
return string
def parse(self, string):
"""
Parse a string delimited by non-alpha, non-$ symbols.
Parse a string delimited by non-alphanum, non-$/_ symbols.
>>> from activitypub.manager import Manager
>>> m = Manager()
@ -234,7 +280,7 @@ class Manager():
retval = []
current = []
for s in string:
if s.isalpha() or (s in ["$"] and len(current) == 0):
if (s.isalnum() or s in "_.") or (s in ["$"] and len(current) == 0):
current.append(s)
else:
if current: