Skip to content

Commit 08baae5

Browse files
authored
Merge pull request #47 from labthings/improved-test-coverage
Improved test coverage
2 parents 7e32b72 + cecd8e8 commit 08baae5

26 files changed

+788
-188
lines changed

.coveragerc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[run]
22
branch = True
33
source = ./labthings
4-
omit = .venv/*
4+
omit = .venv/*, labthings/server/wsgi/*, , labthings/server/monkey/*
55
concurrency = greenlet
66

77
[report]

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ coverage.xml
5252
.hypothesis/
5353
.pytest_cache/
5454
coverage_html_report/
55+
prof/
5556

5657
# Translations
5758
*.mo

labthings/server/decorators.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from marshmallow import Schema as _Schema
1010

11-
from .spec.utilities import update_spec
11+
from .spec.utilities import update_spec, tag_spec
1212
from .schema import TaskSchema, Schema, FieldSchema
1313
from .fields import Field
1414
from .view import View
@@ -100,8 +100,7 @@ def ThingAction(viewcls: View):
100100
View: View class with Action spec tags
101101
"""
102102
# Update Views API spec
103-
update_spec(viewcls, {"tags": ["actions"]})
104-
update_spec(viewcls, {"_groups": ["actions"]})
103+
tag_spec(viewcls, "actions")
105104
return viewcls
106105

107106

@@ -173,8 +172,7 @@ def wrapped(*args, **kwargs):
173172
viewcls.put = property_notify(viewcls.put)
174173

175174
# Update Views API spec
176-
update_spec(viewcls, {"tags": ["properties"]})
177-
update_spec(viewcls, {"_groups": ["properties"]})
175+
tag_spec(viewcls, "properties")
178176
return viewcls
179177

180178

@@ -281,16 +279,11 @@ def __call__(self, f):
281279

282280
class Tag:
283281
def __init__(self, tags):
284-
if isinstance(tags, str):
285-
self.tags = [tags]
286-
elif isinstance(tags, list) and all([isinstance(e, str) for e in tags]):
287-
self.tags = tags
288-
else:
289-
raise TypeError("Tags must be a string or list of strings")
282+
self.tags = tags
290283

291284
def __call__(self, f):
292285
# Pass params to call function attribute for external access
293-
update_spec(f, {"tags": self.tags})
286+
tag_spec(f, self.tags)
294287
return f
295288

296289

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from ..find import current_labthing
2+
from ..view import View
3+
4+
5+
class RootView(View):
6+
def get(self):
7+
return current_labthing().thing_description.to_dict()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from ..sockets import SocketSubscriber, socket_handler_loop
2+
from ..find import current_labthing
3+
4+
import logging
5+
6+
7+
def socket_handler(ws):
8+
# Create a socket subscriber
9+
wssub = SocketSubscriber(ws)
10+
current_labthing().subscribers.add(wssub)
11+
logging.info(f"Added subscriber {wssub}")
12+
# Start the socket connection handler loop
13+
socket_handler_loop(ws)
14+
# Remove the subscriber once the loop returns
15+
current_labthing().subscribers.remove(wssub)
16+
logging.info(f"Removed subscriber {wssub}")

labthings/server/find.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from flask import current_app
3+
import weakref
34

45
from .names import EXTENSION_NAME
56

@@ -13,14 +14,15 @@ def current_labthing(app=None):
1314
# reach the Flask app object. Just using current_app returns
1415
# a wrapper, which breaks it's use in Task threads
1516
if not app:
16-
app = current_app._get_current_object() # skipcq: PYL-W0212
17-
if not app:
18-
return None
19-
logging.debug("Active app extensions:")
20-
logging.debug(app.extensions)
21-
logging.debug("Active labthing:")
22-
logging.debug(app.extensions.get(EXTENSION_NAME))
23-
return app.extensions.get(EXTENSION_NAME, None)
17+
try:
18+
app = current_app._get_current_object() # skipcq: PYL-W0212
19+
except RuntimeError:
20+
return None
21+
ext = app.extensions.get(EXTENSION_NAME, None)
22+
if isinstance(ext, weakref.ref):
23+
return ext()
24+
else:
25+
return ext
2426

2527

2628
def registered_extensions(labthing_instance=None):

labthings/server/labthing.py

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
from .spec.utilities import get_spec
1818
from .spec.td import ThingDescription
1919
from .decorators import tag
20-
from .sockets import Sockets, SocketSubscriber, socket_handler_loop
20+
from .sockets import Sockets
2121

2222
from .default_views.extensions import ExtensionList
2323
from .default_views.tasks import TaskList, TaskView
2424
from .default_views.docs import docs_blueprint, SwaggerUIView
25+
from .default_views.root import RootView
26+
from .default_views.sockets import socket_handler
2527

2628
from ..core.utilities import get_docstring
2729

30+
import weakref
2831
import logging
2932

3033

@@ -72,7 +75,7 @@ def __init__(
7275

7376
# Logging handler
7477
# TODO: Add cleanup code
75-
self.log_handler = LabThingLogger(self)
78+
self.log_handler = LabThingLogger()
7679
logging.getLogger().addHandler(self.log_handler)
7780

7881
self.spec = APISpec(
@@ -97,14 +100,23 @@ def description(self, description: str):
97100
self.spec.description = description
98101

99102
@property
100-
def title(self,):
103+
def title(self):
101104
return self._title
102105

103106
@title.setter
104107
def title(self, title: str):
105108
self._title = title
106109
self.spec.title = title
107110

111+
@property
112+
def safe_title(self):
113+
title = self.title
114+
if not title:
115+
title = "unknown"
116+
title = title.replace(" ", "")
117+
title = title.lower()
118+
return title
119+
108120
@property
109121
def version(self,):
110122
return str(self._version)
@@ -119,11 +131,9 @@ def version(self, version: str):
119131
def init_app(self, app):
120132
self.app = app
121133

122-
app.teardown_appcontext(self.teardown)
123-
124134
# Register Flask extension
125135
app.extensions = getattr(app, "extensions", {})
126-
app.extensions[EXTENSION_NAME] = self
136+
app.extensions[EXTENSION_NAME] = weakref.ref(self)
127137

128138
# Flask error formatter
129139
if self.format_flask_exceptions:
@@ -145,12 +155,9 @@ def init_app(self, app):
145155
self.sockets = Sockets(app)
146156
self._create_base_sockets()
147157

148-
def teardown(self, exception):
149-
pass
150-
151158
def _create_base_routes(self):
152159
# Add root representation
153-
self.app.add_url_rule(self._complete_url("/", ""), "root", self.root)
160+
self.add_view(RootView, "/", endpoint="root")
154161
# Add thing descriptions
155162
self.app.register_blueprint(
156163
docs_blueprint, url_prefix=f"{self.url_prefix}/docs"
@@ -166,19 +173,7 @@ def _create_base_routes(self):
166173
self.add_view(TaskView, "/tasks/<task_id>", endpoint=TASK_ENDPOINT)
167174

168175
def _create_base_sockets(self):
169-
self.sockets.add_url_rule(self._complete_url("", ""), self._socket_handler)
170-
171-
def _socket_handler(self, ws):
172-
# Create a socket subscriber
173-
wssub = SocketSubscriber(ws)
174-
self.subscribers.add(wssub)
175-
logging.info(f"Added subscriber {wssub}")
176-
# Start the socket connection handler loop
177-
socket_handler_loop(ws)
178-
# Remove the subscriber once the loop returns
179-
self.subscribers.remove(wssub)
180-
logging.info(f"Removed subscriber {wssub}")
181-
logging.debug(list(self.subscribers))
176+
self.sockets.add_view(self._complete_url("", ""), socket_handler)
182177

183178
# Device stuff
184179

@@ -300,17 +295,19 @@ def _register_view(self, app, view, *urls, endpoint=None, **kwargs):
300295

301296
# There might be a better way to do this than _rules_by_endpoint,
302297
# but I can't find one so this will do for now. Skipping PYL-W0212
298+
# FIXME: There is a MASSIVE memory leak or something going on in APISpec!
299+
# This is grinding tests to a halt, and is really annoying... Should be fixed.
303300
flask_rules = app.url_map._rules_by_endpoint.get(endpoint) # skipcq: PYL-W0212
304301
for flask_rule in flask_rules:
305302
self.spec.path(**rule_to_apispec_path(flask_rule, view, self.spec))
306303

307304
# Handle resource groups listed in API spec
308305
view_spec = get_spec(view)
309-
view_groups = view_spec.get("_groups", [])
310-
if "actions" in view_groups:
306+
view_tags = view_spec.get("tags", set())
307+
if "actions" in view_tags:
311308
self.thing_description.action(flask_rules, view)
312309
self._action_views[view.endpoint] = view
313-
if "properties" in view_groups:
310+
if "properties" in view_tags:
314311
self.thing_description.property(flask_rules, view)
315312
self._property_views[view.endpoint] = view
316313

@@ -336,8 +333,3 @@ def add_root_link(self, view, rel, kwargs=None, params=None):
336333
if params is None:
337334
params = {}
338335
self.thing_description.add_link(view, rel, kwargs=kwargs, params=params)
339-
340-
# Description
341-
def root(self):
342-
"""Root representation"""
343-
return self.thing_description.to_dict()

labthings/server/logging.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1+
from .find import current_labthing
2+
13
from logging import StreamHandler
24
import datetime
35

46

57
class LabThingLogger(StreamHandler):
6-
def __init__(self, labthing):
7-
StreamHandler.__init__(self)
8-
self.labthing = labthing
8+
def __init__(self, *args, **kwargs):
9+
StreamHandler.__init__(self, *args, **kwargs)
910

1011
def emit(self, record):
1112
log_event = self.rest_format_record(record)
1213

1314
# Broadcast to subscribers
14-
subscribers = getattr(self.labthing, "subscribers", [])
15+
subscribers = getattr(current_labthing(), "subscribers", [])
1516
for sub in subscribers:
1617
sub.event_notify(log_event)
1718

labthings/server/monkey.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
from gevent.monkey import patch_all
22

33
__all__ = ["patch_all"]
4+
5+
"""
6+
NOTE: THIS FILE IS EXCLUDED FROM OUR UNIT TESTS.
7+
8+
MONKEY PATCHING IN THE MIDDLE OF A TEST SUITE RUNNING
9+
MAY CAUSE PROBLEMS.
10+
11+
GIVEN THAT THIS MODULE IS SIMPLY A PROXY FOR GEVENTS
12+
MONKEY PATCHER, TESTSING IS FAIRLY REDUNDANT ANYWAY.
13+
14+
THIS SHOULD BE PERIODICALLY REVISITED TO MAKE SURE ITS
15+
STILL TRUE.
16+
17+
THANKS
18+
"""

labthings/server/representations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class LabThingsJSONEncoder(JSONEncoder):
1010
"""
1111

1212
def default(self, o):
13+
if isinstance(o, set):
14+
return list(o)
1315
return JSONEncoder.default(self, o)
1416

1517

0 commit comments

Comments
 (0)