Skip to content

Commit ba3b32d

Browse files
author
Joel Collins
committed
2 parents 924521c + 39c306f commit ba3b32d

17 files changed

+867
-60
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
branch = True
33
source = ./labthings
44
omit = .venv/*
5+
concurrency = greenlet
56

67
[report]
78
# Regexes for lines to exclude from consideration

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: CI
2+
3+
on: [push]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v1
11+
with:
12+
fetch-depth: 1
13+
14+
- name: Set up Python 3.7
15+
uses: actions/setup-python@v1
16+
with:
17+
python-version: 3.7
18+
19+
- name: Install Poetry
20+
uses: dschep/install-poetry-action@v1.3
21+
22+
- name: Cache Poetry virtualenv
23+
uses: actions/cache@v1
24+
id: cache
25+
with:
26+
path: ~/.virtualenvs
27+
key: poetry-${{ hashFiles('**/poetry.lock') }}
28+
restore-keys: |
29+
poetry-${{ hashFiles('**/poetry.lock') }}
30+
31+
- name: Set Poetry config
32+
run: |
33+
poetry config virtualenvs.in-project false
34+
poetry config virtualenvs.path ~/.virtualenvs
35+
36+
- name: Install Dependencies
37+
run: poetry install
38+
if: steps.cache.outputs.cache-hit != 'true'
39+
40+
- name: Code Quality
41+
run: poetry run black . --check
42+
43+
- name: Test with pytest
44+
run: poetry run pytest --cov-report term-missing --cov-report=xml --cov=labthings ./tests
45+
46+
- name: Upload coverage to Codecov
47+
uses: codecov/codecov-action@v1
48+
with:
49+
file: ./coverage.xml
50+
flags: unittests
51+
name: codecov-umbrella
52+
fail_ci_if_error: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ coverage.xml
5151
*.py,cover
5252
.hypothesis/
5353
.pytest_cache/
54+
coverage_html_report/
5455

5556
# Translations
5657
*.mo

labthings/core/lock.py

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ def __repr__(self):
3131
self._owner,
3232
)
3333

34+
def locked(self):
35+
return self._block.locked()
36+
3437
def acquire(self, blocking=True, timeout=None):
3538
"""
3639
Acquire the mutex, blocking if *blocking* is true, for up to
@@ -68,22 +71,6 @@ def release(self):
6871
def __exit__(self, typ, value, tb):
6972
self.release()
7073

71-
# Internal methods used by condition variables
72-
73-
def _acquire_restore(self, count_owner):
74-
count, owner = count_owner
75-
self._block.acquire()
76-
self._count = count
77-
self._owner = owner
78-
79-
def _release_save(self):
80-
count = self._count
81-
self._count = 0
82-
owner = self._owner
83-
self._owner = None
84-
self._block.release()
85-
return (count, owner)
86-
8774
def _is_owned(self):
8875
return self._owner is getcurrent()
8976

@@ -109,22 +96,35 @@ def __init__(self, timeout=1, name=None):
10996
def locked(self):
11097
return self._lock.locked()
11198

112-
def acquire(self, blocking=True):
113-
return self._lock.acquire(blocking, timeout=self.timeout)
99+
def acquire(self, blocking=True, timeout=None, _strict=True):
100+
if not timeout:
101+
timeout = self.timeout
102+
result = self._lock.acquire(blocking, timeout=timeout)
103+
if _strict and not result:
104+
raise LockError("ACQUIRE_ERROR", self)
105+
else:
106+
return result
114107

115108
def __enter__(self):
116-
result = self._lock.acquire(blocking=True, timeout=self.timeout)
117-
if result:
118-
return result
119-
else:
120-
raise LockError("ACQUIRE_ERROR", self)
109+
return self.acquire(blocking=True, timeout=self.timeout)
121110

122111
def __exit__(self, *args):
123-
self._lock.release()
112+
self.release()
124113

125114
def release(self):
126115
self._lock.release()
127116

117+
@property
118+
def _owner(self):
119+
return self._lock._owner
120+
121+
@_owner.setter
122+
def _owner(self, new_owner):
123+
self._lock._owner = new_owner
124+
125+
def _is_owned(self):
126+
return self._lock._is_owned()
127+
128128

129129
class CompositeLock:
130130
"""
@@ -144,20 +144,53 @@ def __init__(self, locks, timeout=1):
144144
self.locks = locks
145145
self.timeout = timeout
146146

147-
def acquire(self, blocking=True):
148-
return (lock.acquire(blocking=blocking) for lock in self.locks)
147+
def acquire(self, blocking=True, timeout=None):
148+
if not timeout:
149+
timeout = self.timeout
150+
151+
lock_all = all(
152+
[
153+
lock.acquire(blocking=blocking, timeout=timeout, _strict=False)
154+
for lock in self.locks
155+
]
156+
)
149157

150-
def __enter__(self):
151-
result = (lock.acquire(blocking=True) for lock in self.locks)
152-
if all(result):
153-
return result
154-
else:
158+
if not lock_all:
159+
self._emergency_release()
155160
raise LockError("ACQUIRE_ERROR", self)
156161

162+
return True
163+
164+
def __enter__(self):
165+
return self.acquire(blocking=True, timeout=self.timeout)
166+
157167
def __exit__(self, *args):
158-
for lock in self.locks:
159-
lock.release()
168+
return self.release()
160169

161170
def release(self):
171+
# If not all child locks are owner by caller
172+
if not all([owner is getcurrent() for owner in self._owner]):
173+
raise RuntimeError("cannot release un-acquired lock")
162174
for lock in self.locks:
163-
lock.release()
175+
if lock.locked():
176+
lock.release()
177+
178+
def _emergency_release(self):
179+
for lock in self.locks:
180+
if lock.locked() and lock._is_owned():
181+
lock.release()
182+
183+
def locked(self):
184+
return any([lock.locked() for lock in self.locks])
185+
186+
@property
187+
def _owner(self):
188+
return [lock._owner for lock in self.locks]
189+
190+
@_owner.setter
191+
def _owner(self, new_owner):
192+
for lock in self.locks:
193+
lock._owner = new_owner
194+
195+
def _is_owned(self):
196+
return all([lock._is_owned() for lock in self.locks])

labthings/core/tasks/pool.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import threading
21
import logging
32
from functools import wraps
43
from gevent import getcurrent
54

65
from .thread import TaskThread
76

8-
from flask import copy_current_request_context
7+
from flask import copy_current_request_context, has_request_context
98

109

1110
class TaskMaster:
@@ -38,21 +37,26 @@ def states(self):
3837

3938
def new(self, f, *args, **kwargs):
4039
# copy_current_request_context allows threads to access flask current_app
41-
task = TaskThread(
42-
target=copy_current_request_context(f), args=args, kwargs=kwargs
43-
)
40+
if has_request_context():
41+
target = copy_current_request_context(f)
42+
else:
43+
target = f
44+
task = TaskThread(target=target, args=args, kwargs=kwargs)
4445
self._tasks.append(task)
4546
return task
4647

4748
def remove(self, task_id):
4849
for task in self._tasks:
49-
if (task.id == task_id) and not task.isAlive():
50-
del task
50+
if (str(task.id) == str(task_id)) and task.dead:
51+
self._tasks.remove(task)
5152

5253
def cleanup(self):
53-
for task in self._tasks:
54-
if not task.isAlive():
55-
del task
54+
for i, task in enumerate(self._tasks):
55+
if task.dead:
56+
# Mark for delection
57+
self._tasks[i] = None
58+
# Remove items marked for deletion
59+
self._tasks = [t for t in self._tasks if t]
5660

5761

5862
# Task management

labthings/core/utilities.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,15 @@ def rupdate(destination_dict, update_dict):
5555
for k, v in update_dict.items():
5656
# Merge lists if they're present in both objects
5757
if isinstance(v, list):
58+
# If key is missing from destination, create the list
5859
if k not in destination_dict:
5960
destination_dict[k] = []
61+
# If destination value is also a list, merge
6062
if isinstance(destination_dict[k], list):
6163
destination_dict[k].extend(v)
64+
# If destination exists but isn't a list, replace
65+
else:
66+
destination_dict[k] = v
6267
# Recursively merge dictionaries if the element is a dictionary
6368
elif isinstance(v, collections.abc.Mapping):
6469
if k not in destination_dict:

labthings/server/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from flask import jsonify, escape
1+
from flask import escape
22
from werkzeug.exceptions import default_exceptions
33
from werkzeug.exceptions import HTTPException
44

@@ -31,7 +31,7 @@ def std_handler(self, error):
3131
or getattr(getattr(error, "__class__", None), "__name__", None)
3232
or None,
3333
}
34-
return jsonify(response), status_code
34+
return (response, status_code)
3535

3636
def init_app(self, app):
3737
self.app = app

labthings/server/spec/utilities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..fields import Field
77
from marshmallow import Schema as BaseSchema
88

9-
from collections import Mapping
9+
from collections.abc import Mapping
1010

1111

1212
def update_spec(obj, spec: dict):

poetry.lock

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ zeroconf = ">=0.24.5,<0.26.0"
1818
[tool.poetry.dev-dependencies]
1919
pytest = "^5.2"
2020
black = {version = "^19.10b0",allow-prereleases = true}
21-
coverage = "^5.0.4"
21+
pytest-cov = "^2.8.1"
2222
numpy = "^1.18.2"
2323

2424
[build-system]

0 commit comments

Comments
 (0)