Skip to content

Commit f130d66

Browse files
author
Joel Collins
committed
Started testing class-attribute spec definitions
1 parent 80ef05b commit f130d66

File tree

12 files changed

+272
-382
lines changed

12 files changed

+272
-382
lines changed

examples/simple_thing.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ def average_data(self, n: int):
7777
@semantics.moz.LevelProperty(100, 500, example=200)
7878
@Doc(description="Value of magic_denoise",)
7979
class DenoiseProperty(PropertyView):
80-
8180
# Main function to handle GET requests (read)
8281
def get(self):
8382
"""Show the current magic_denoise value"""

src/labthings/core/utilities.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
PY3 = sys.version_info > (3,)
1111

1212

13+
class classproperty(object):
14+
15+
__slots__ = ("getter",)
16+
17+
def __init__(self, getter):
18+
self.getter = getter
19+
20+
def __get__(self, obj, cls):
21+
return self.getter(cls, obj)
22+
23+
1324
def get_docstring(obj, remove_newlines=True):
1425
"""Return the docstring of an object
1526

src/labthings/server/decorators.py

Lines changed: 17 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,21 @@
1-
from webargs import flaskparser
2-
from functools import wraps, update_wrapper
3-
from flask import abort, request
4-
from werkzeug.wrappers import Response as ResponseBase
51
from http import HTTPStatus
6-
from marshmallow.exceptions import ValidationError
7-
from collections.abc import Mapping
82

9-
from marshmallow import Schema as _Schema
3+
from .view import View
104

11-
from .spec.utilities import update_spec, tag_spec
12-
from .schema import TaskSchema, Schema, FieldSchema
13-
from .fields import Field
14-
from .view import View, ActionView, PropertyView
15-
from .utilities import unpack
16-
17-
from labthings.core.tasks.pool import TaskThread
185
from labthings.core.utilities import merge
196

20-
import logging
217

228
# Useful externals to have included here
239
from marshmallow import pre_dump, pre_load
2410

2511
__all__ = [
2612
"pre_dump",
2713
"pre_load",
28-
"marshal_with",
29-
"marshal_task",
30-
"ThingAction",
31-
"thing_action",
3214
"Safe",
3315
"safe",
3416
"Idempotent",
3517
"idempotent",
36-
"ThingProperty",
37-
"thing_property",
3818
"PropertySchema",
39-
"use_body",
40-
"use_args",
4119
"Doc",
4220
"doc",
4321
"Tag",
@@ -46,72 +24,6 @@
4624
]
4725

4826

49-
class marshal_with:
50-
def __init__(self, schema, code=200):
51-
"""Decorator to format the return of a function with a Marshmallow schema
52-
53-
Args:
54-
schema: Marshmallow schema, field, or dict of Fields, describing
55-
the format of data to be returned by a View
56-
"""
57-
self.schema = schema
58-
self.code = code
59-
60-
# Case of schema as a dictionary
61-
if isinstance(self.schema, Mapping):
62-
self.converter = Schema.from_dict(self.schema)().dump
63-
# Case of schema as a single Field
64-
elif isinstance(self.schema, Field):
65-
self.converter = FieldSchema(self.schema).dump
66-
# Case of schema as a Schema
67-
elif isinstance(self.schema, _Schema):
68-
self.converter = self.schema.dump
69-
else:
70-
raise TypeError(
71-
f"Unsupported schema type {type(self.schema)} for marshal_with"
72-
)
73-
74-
def __call__(self, f):
75-
# Pass params to call function attribute for external access
76-
update_spec(f, {"_schema": {self.code: self.schema}})
77-
# Wrapper function
78-
@wraps(f)
79-
def wrapper(*args, **kwargs):
80-
resp = f(*args, **kwargs)
81-
if isinstance(resp, ResponseBase):
82-
resp.data = self.converter(resp.data)
83-
return resp
84-
elif isinstance(resp, tuple):
85-
resp, code, headers = unpack(resp)
86-
return (self.converter(resp), code, headers)
87-
return self.converter(resp)
88-
89-
return wrapper
90-
91-
92-
def ThingAction(viewcls: View):
93-
"""Decorator to tag a view as a Thing Action
94-
95-
Args:
96-
viewcls (View): View class to tag as an Action
97-
98-
Returns:
99-
View: View class with Action spec tags
100-
"""
101-
logging.warning(
102-
"ThingAction decorator is deprecated and will be removed in LabThings 1.0."
103-
"Please use the ActionView class instead."
104-
)
105-
# Set to PropertyView.dispatch_request
106-
viewcls.dispatch_request = ActionView.dispatch_request
107-
# Update Views API spec
108-
tag_spec(viewcls, "actions")
109-
return viewcls
110-
111-
112-
thing_action = ThingAction
113-
114-
11527
def Safe(viewcls: View):
11628
"""Decorator to tag a view or function as being safe
11729
@@ -122,7 +34,7 @@ def Safe(viewcls: View):
12234
View: View class with Safe spec tags
12335
"""
12436
# Update Views API spec
125-
update_spec(viewcls, {"_safe": True})
37+
viewcls.safe = True
12638
return viewcls
12739

12840

@@ -139,142 +51,49 @@ def Idempotent(viewcls: View):
13951
View: View class with idempotent spec tags
14052
"""
14153
# Update Views API spec
142-
update_spec(viewcls, {"_idempotent": True})
54+
viewcls.idempotent = True
14355
return viewcls
14456

14557

14658
idempotent = Idempotent
14759

14860

149-
def ThingProperty(viewcls):
150-
"""Decorator to tag a view as a Thing Property
151-
152-
Args:
153-
viewcls (View): View class to tag as an Property
154-
155-
Returns:
156-
View: View class with Property spec tags
157-
"""
158-
logging.warning(
159-
"ThingProperty decorator is deprecated and will be removed in LabThings 1.0."
160-
"Please use the PropertyView class instead."
161-
)
162-
# Set to PropertyView.dispatch_request
163-
viewcls.dispatch_request = PropertyView.dispatch_request
164-
# Update Views API spec
165-
tag_spec(viewcls, "properties")
166-
return viewcls
167-
168-
169-
thing_property = ThingProperty
170-
171-
17261
class PropertySchema:
173-
def __init__(self, schema, code=200):
62+
def __init__(self, schema):
17463
"""
17564
:param schema: a dict of whose keys will make up the final
17665
serialized response output
17766
"""
17867
self.schema = schema
179-
self.code = code
180-
181-
def __call__(self, viewcls):
182-
update_spec(viewcls, {"_propertySchema": self.schema})
183-
184-
if hasattr(viewcls, "get") and callable(viewcls.get):
185-
viewcls.get = marshal_with(self.schema, code=self.code)(viewcls.get)
186-
187-
if hasattr(viewcls, "post") and callable(viewcls.post):
188-
viewcls.post = marshal_with(self.schema, code=self.code)(viewcls.post)
189-
viewcls.post = use_args(self.schema)(viewcls.post)
190-
191-
if hasattr(viewcls, "put") and callable(viewcls.put):
192-
viewcls.put = marshal_with(self.schema, code=self.code)(viewcls.put)
193-
viewcls.put = use_args(self.schema)(viewcls.put)
19468

69+
def __call__(self, viewcls: View):
70+
viewcls.schema = self.schema
19571
return viewcls
19672

19773

198-
class use_body:
199-
"""Gets the request body as a single value and adds it as a positional argument"""
200-
201-
def __init__(self, schema, **kwargs):
202-
self.schema = schema
203-
204-
def __call__(self, f):
205-
# Pass params to call function attribute for external access
206-
update_spec(f, {"_params": self.schema})
207-
208-
# Wrapper function
209-
@wraps(f)
210-
def wrapper(*args, **kwargs):
211-
# Get data from request
212-
data = request.data or None
213-
214-
# If no data is there
215-
if not data:
216-
# If data is required
217-
if self.schema.required:
218-
# Abort
219-
return abort(400)
220-
# Otherwise, look for the schema fields 'missing' property
221-
if self.schema.missing:
222-
data = self.schema.missing
223-
224-
# Serialize data if it exists
225-
if data:
226-
try:
227-
data = FieldSchema(self.schema).deserialize(data)
228-
except ValidationError as e:
229-
logging.error(e)
230-
return abort(400)
231-
232-
# Inject argument and return wrapped function
233-
return f(*args, data, **kwargs)
234-
235-
return wrapper
236-
237-
238-
class use_args:
239-
"""Equivalent to webargs.flask_parser.use_args"""
240-
241-
def __init__(self, schema, **kwargs):
242-
self.schema = schema
243-
244-
if isinstance(schema, Field):
245-
self.wrapper = use_body(schema, **kwargs)
246-
else:
247-
self.wrapper = flaskparser.use_args(schema, **kwargs)
248-
249-
def __call__(self, f):
250-
# Pass params to call function attribute for external access
251-
update_spec(f, {"_params": self.schema})
252-
# Wrapper function
253-
update_wrapper(self.wrapper, f)
254-
return self.wrapper(f)
255-
256-
25774
class Doc:
25875
def __init__(self, **kwargs):
25976
self.kwargs = kwargs
26077

261-
def __call__(self, f):
78+
def __call__(self, viewcls: View):
26279
# Pass params to call function attribute for external access
263-
update_spec(f, self.kwargs)
264-
return f
80+
viewcls.docs.update(self.kwargs)
81+
return viewcls
26582

26683

26784
doc = Doc
26885

26986

27087
class Tag:
27188
def __init__(self, tags):
89+
if type(tags) is str:
90+
tags = [tags]
27291
self.tags = tags
27392

274-
def __call__(self, f):
93+
def __call__(self, viewcls: View):
27594
# Pass params to call function attribute for external access
276-
tag_spec(f, self.tags)
277-
return f
95+
viewcls.tags.extend(self.tags)
96+
return viewcls
27897

27998

28099
tag = Tag
@@ -284,10 +103,10 @@ class Semtype:
284103
def __init__(self, semtype: str):
285104
self.semtype = semtype
286105

287-
def __call__(self, f):
106+
def __call__(self, viewcls: View):
288107
# Pass params to call function attribute for external access
289-
update_spec(f, {"@type": self.semtype})
290-
return f
108+
viewcls.semtype = self.semtype
109+
return viewcls
291110

292111

293112
semtype = Semtype

src/labthings/server/labthing.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .representations import LabThingsJSONEncoder
1717
from .spec.apispec import rule_to_apispec_path
1818
from .spec.apispec_plugins import MarshmallowPlugin
19-
from .spec.utilities import get_spec, compile_view_spec
19+
from .spec.utilities import get_spec
2020
from .spec.td import ThingDescription
2121
from .decorators import tag
2222
from .sockets import Sockets
@@ -318,24 +318,17 @@ def _register_view(self, app, view, *urls, endpoint=None, **kwargs):
318318
# Add the url to the application or blueprint
319319
app.add_url_rule(rule, view_func=resource_func, **kwargs)
320320

321-
# Compile the View classes API spec
322-
compile_view_spec(view)
323-
324321
# There might be a better way to do this than _rules_by_endpoint,
325322
# but I can't find one so this will do for now. Skipping PYL-W0212
326323
flask_rules = app.url_map._rules_by_endpoint.get(endpoint) # skipcq: PYL-W0212
327324
for flask_rule in flask_rules:
328-
self.spec.path(
329-
**rule_to_apispec_path(flask_rule, get_spec(view), self.spec)
330-
)
325+
self.spec.path(**rule_to_apispec_path(flask_rule, view, self.spec))
331326

332327
# Handle resource groups listed in API spec
333-
view_tags = get_spec(view).get("tags", set())
334-
if "actions" in view_tags:
328+
if "actions" in getattr(view, "tags", []):
335329
self.thing_description.action(flask_rules, view)
336-
# TODO: Use this for top-level action POST
337330
self._action_views[view.endpoint] = view
338-
if "properties" in view_tags:
331+
if "properties" in getattr(view, "tags", []):
339332
self.thing_description.property(flask_rules, view)
340333
self._property_views[view.endpoint] = view
341334

0 commit comments

Comments
 (0)