diff --git a/docs/source/conf.py b/docs/source/conf.py index 49b86271..9939ad13 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,32 +17,34 @@ import sys # Load module from relative imports -module_path = os.path.abspath('../..') +module_path = os.path.abspath("../..") sys.path.insert(0, module_path) # Handle mock imports for non-platform-agnostic modules from unittest.mock import MagicMock + class Mock(MagicMock): @classmethod def __getattr__(cls, name): return MagicMock() -mock_imports = ['picamera', 'picamera.array', 'picamera.mmalobj'] + +mock_imports = ["picamera", "picamera.array", "picamera.mmalobj"] sys.modules.update((mod_name, Mock()) for mod_name in mock_imports) # -- Project information ----------------------------------------------------- -project = 'OpenFlexure Microscope Software' -copyright = '2018, Bath Open Instrumentation Group' -author = 'Bath Open Instrumentation Group' +project = "OpenFlexure Microscope Software" +copyright = "2018, Bath Open Instrumentation Group" +author = "Bath Open Instrumentation Group" # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '' +release = "" # -- General configuration --------------------------------------------------- @@ -55,32 +57,32 @@ release = '' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'sphinx.ext.ifconfig', - 'sphinxcontrib.httpdomain', - 'sphinxcontrib.autohttp.flask', - 'sphinxcontrib.autohttp.flaskqref' + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.ifconfig", + "sphinxcontrib.httpdomain", + "sphinxcontrib.autohttp.flask", + "sphinxcontrib.autohttp.flaskqref", ] # Override ordering -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -114,7 +116,7 @@ html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -130,7 +132,7 @@ html_static_path = ['_static'] # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'OpenFlexureMicroscopeSoftwaredoc' +htmlhelp_basename = "OpenFlexureMicroscopeSoftwaredoc" # -- Options for LaTeX output ------------------------------------------------ @@ -139,15 +141,12 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -157,8 +156,13 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'OpenFlexureMicroscopeSoftware.tex', 'OpenFlexure Microscope Software Documentation', - 'Bath Open Instrumentation Group', 'manual'), + ( + master_doc, + "OpenFlexureMicroscopeSoftware.tex", + "OpenFlexure Microscope Software Documentation", + "Bath Open Instrumentation Group", + "manual", + ) ] @@ -167,8 +171,13 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'openflexuremicroscopesoftware', 'OpenFlexure Microscope Software Documentation', - [author], 1) + ( + master_doc, + "openflexuremicroscopesoftware", + "OpenFlexure Microscope Software Documentation", + [author], + 1, + ) ] @@ -178,9 +187,15 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'OpenFlexureMicroscopeSoftware', 'OpenFlexure Microscope Software Documentation', - author, 'OpenFlexureMicroscopeSoftware', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "OpenFlexureMicroscopeSoftware", + "OpenFlexure Microscope Software Documentation", + author, + "OpenFlexureMicroscopeSoftware", + "One line description of project.", + "Miscellaneous", + ) ] @@ -199,7 +214,7 @@ epub_title = project # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- @@ -208,11 +223,11 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'openflexure_stage': ('https://openflexure-stage.readthedocs.io/en/latest/', None), - 'picamera': ('https://picamera.readthedocs.io/en/release-1.13/', None) - } + "openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None), + "picamera": ("https://picamera.readthedocs.io/en/release-1.13/", None), +} # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True \ No newline at end of file +todo_include_todos = True diff --git a/docs/source/plugins/example/plugin.py b/docs/source/plugins/example/plugin.py index 36da5c0d..0cab4a7e 100644 --- a/docs/source/plugins/example/plugin.py +++ b/docs/source/plugins/example/plugin.py @@ -13,6 +13,7 @@ FORM_PATH = os.path.join(HERE, "forms.json") ### MICROSCOPE PLUGIN ### + class MyPluginClass(MicroscopePlugin): """ A set of default plugins @@ -20,13 +21,13 @@ class MyPluginClass(MicroscopePlugin): global FORM_PATH - with open(FORM_PATH, 'r') as sc: + with open(FORM_PATH, "r") as sc: api_form = json.load(sc) api_views = { - '/identify': IdentifyAPI, - '/hello': HelloWorldAPI, - '/timelapse': TimelapseAPI, + "/identify": IdentifyAPI, + "/hello": HelloWorldAPI, + "/timelapse": TimelapseAPI, } def identify(self): @@ -34,8 +35,9 @@ class MyPluginClass(MicroscopePlugin): Demonstrate access to Microscope.camera, and Microscope.stage """ - response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, - self.microscope.stage) + response = "My parent camera is {}, and my parent stage is {}.".format( + self.microscope.camera, self.microscope.stage + ) return response def hello_world(self): @@ -58,33 +60,33 @@ class MyPluginClass(MicroscopePlugin): # Create a data stream to capture to capture_data = self.microscope.camera.new_image( - write_to_file=True, - temporary=False) + write_to_file=True, temporary=False + ) # Capture a still image from the Pi camera, into the data stream - self.microscope.camera.capture( - capture_data, - use_video_port=True) - + self.microscope.camera.capture(capture_data, use_video_port=True) + # Append the capture data to our list capture_array.append(capture_data) # Wait for 1 minute - time.sleep(60) + time.sleep(60) ### API VIEWS ### + class IdentifyAPI(MicroscopeViewPlugin): """ A simple example API plugin, attached through the main microscope plugin. """ + def get(self): """ Method to call when an HTTP GET request is made. """ # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut - data = self.plugin.identify() + data = self.plugin.identify() return Response(escape(data)) @@ -99,13 +101,11 @@ class HelloWorldAPI(MicroscopeViewPlugin): """ # If the microscope does not already contain our plugin_string attribute - if not hasattr(self.microscope, 'plugin_string'): + if not hasattr(self.microscope, "plugin_string"): # Make a string, using the MicroscopeViewPlugin.plugin shortcut self.microscope.plugin_string = self.plugin.hello_world() - json_response = jsonify({ - 'plugin_string': self.microscope.plugin_string - }) + json_response = jsonify({"plugin_string": self.microscope.plugin_string}) return Response(json_response) @@ -118,18 +118,17 @@ class HelloWorldAPI(MicroscopeViewPlugin): payload = JsonResponse(request) # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty. - new_plugin_string = payload.param('plugin_string', default='', convert=str) + new_plugin_string = payload.param("plugin_string", default="", convert=str) if new_plugin_string: # If not None or empty # Set microscope attribute to the specified string - self.microscope.plugin_string = new_plugin_string + self.microscope.plugin_string = new_plugin_string - json_response = jsonify({ - 'plugin_string': self.microscope.plugin_string - }) + json_response = jsonify({"plugin_string": self.microscope.plugin_string}) return Response(json_response) + class TimelapseAPI(MicroscopeViewPlugin): def post(self): @@ -137,10 +136,12 @@ class TimelapseAPI(MicroscopeViewPlugin): payload = JsonResponse(request) # Extract the "n_images" parameter if it was passed. Otherwise, default to 10. - n_images = payload.param('n_images', default=10, convert=int) + n_images = payload.param("n_images", default=10, convert=int) # Attach the long-running method as a microscope task - self.timelapse_task = self.microscope.task.start(self.plugin.timelapse, n_images) + self.timelapse_task = self.microscope.task.start( + self.plugin.timelapse, n_images + ) # Return the state of the task (will show ID, start time, and status before the task has finished) - return jsonify(self.timelapse_task.state), 202 \ No newline at end of file + return jsonify(self.timelapse_task.state), 202 diff --git a/tests/api_client.py b/tests/api_client.py index 4201b6d3..e2fd1090 100644 --- a/tests/api_client.py +++ b/tests/api_client.py @@ -30,63 +30,61 @@ class APIconnection: return r.json() def set_overlay(self, message="", size=50): - json = { - "text": message, - "size": size - } - return self.post('/camera/overlay', json=json) + json = {"text": message, "size": size} + return self.post("/camera/overlay", json=json) def get_overlay(self): - return self.get('/camera/overlay') + return self.get("/camera/overlay") def get_config(self): - return self.get('/config') + return self.get("/config") def set_config(self, config_dict): - return self.post('/config', json=config_dict) + return self.post("/config", json=config_dict) def get_state(self): - return self.get('/state') + return self.get("/state") def start_preview(self): - return self.post('/camera/preview/start') + return self.post("/camera/preview/start") def stop_preview(self): - return self.post('/camera/preview/stop') + return self.post("/camera/preview/stop") def move_by(self, x=0, y=0, z=0): - json = { - "x": x, - "y": y, - "z": z - } - return self.post('/stage/position', json=json) + json = {"x": x, "y": y, "z": z} + return self.post("/stage/position", json=json) def new_capture(self, use_video_port=True, keep_on_disk=False, resize=None): - json = { - "keep_on_disk": keep_on_disk, - "use_video_port": use_video_port - } + json = {"keep_on_disk": keep_on_disk, "use_video_port": use_video_port} if resize: - json['size'] = {'width': resize[0], 'height': resize[1]} + json["size"] = {"width": resize[0], "height": resize[1]} - return self.post('/camera/capture', json=json) + return self.post("/camera/capture", json=json) def get_capture(self, capture_id): - uri_route = '/camera/capture/{}/download'.format(capture_id) + uri_route = "/camera/capture/{}/download".format(capture_id) r = self.get(uri_route, json=False) img = Image.open(BytesIO(r.content)) array = np.asarray(img, dtype=np.int32) return array def del_capture(self, capture_id): - uri_route = '/camera/capture/{}'.format(capture_id) + uri_route = "/camera/capture/{}".format(capture_id) return self.delete(uri_route) - def capture(self, use_video_port=True, keep_on_disk=False, delete_after_use=True, resize=None): - p = self.new_capture(use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize) - capture_id = p['metadata']['id'] + def capture( + self, + use_video_port=True, + keep_on_disk=False, + delete_after_use=True, + resize=None, + ): + p = self.new_capture( + use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize + ) + capture_id = p["metadata"]["id"] img_array = self.get_capture(capture_id) if delete_after_use: @@ -95,10 +93,8 @@ class APIconnection: return img_array def set_zoom(self, zoom_value=1.0): - json = { - "zoom_value": zoom_value, - } - return self.post('/camera/zoom', json=json) + json = {"zoom_value": zoom_value} + return self.post("/camera/zoom", json=json) def get_zoom(self): - return self.get('/camera/zoom') + return self.get("/camera/zoom") diff --git a/tests/test_api.py b/tests/test_api.py index 60a2142b..83e53d08 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,27 +6,23 @@ import unittest import logging import sys + logging.basicConfig(stream=sys.stderr, level=logging.INFO) class TestCapture(unittest.TestCase): - def test_capture_config(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") config = connection.get_config() - - expected_keys = [ - 'image_resolution', - 'stream_resolution', - 'numpy_resolution', - ] + + expected_keys = ["image_resolution", "stream_resolution", "numpy_resolution"] for key in expected_keys: self.assertTrue(key in config) def test_capture_videoport(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") - resolution = connection.get_config()['stream_resolution'] + resolution = connection.get_config()["stream_resolution"] for resize in [None, (640, 480)]: @@ -34,16 +30,17 @@ class TestCapture(unittest.TestCase): resolution = resize capture_array = connection.capture( - use_video_port=True, - keep_on_disk=False, + use_video_port=True, + keep_on_disk=False, delete_after_use=True, - resize=resize) + resize=resize, + ) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) def test_capture_full(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") - resolution = connection.get_config()['image_resolution'] + resolution = connection.get_config()["image_resolution"] for resize in [None, (640, 480)]: @@ -51,23 +48,21 @@ class TestCapture(unittest.TestCase): resolution = resize capture_array = connection.capture( - use_video_port=False, - keep_on_disk=False, + use_video_port=False, + keep_on_disk=False, delete_after_use=True, - resize=resize) + resize=resize, + ) self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3)) class TestStage(unittest.TestCase): - def test_stage_config(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") config = connection.get_config() - - expected_keys = [ - 'backlash', - ] + + expected_keys = ["backlash"] for key in expected_keys: self.assertTrue(key in config) @@ -76,14 +71,12 @@ class TestStage(unittest.TestCase): connection = APIconnection(host="localhost", port=5000, api_ver="v1") state = connection.get_state() - self.assertTrue('stage' in state) - - expected_keys = [ - 'position', - ] + self.assertTrue("stage" in state) + + expected_keys = ["position"] for key in expected_keys: - self.assertTrue(key in state['stage']) + self.assertTrue(key in state["stage"]) def test_stage_movement(self): connection = APIconnection(host="localhost", port=5000, api_ver="v1") @@ -91,16 +84,16 @@ class TestStage(unittest.TestCase): move_distance = 500 for axis in range(3): for direction in [1, -1]: - pos_i_dict = connection.get_state()['stage']['position'] - pos_i = [pos_i_dict['x'], pos_i_dict['y'], pos_i_dict['z']] + pos_i_dict = connection.get_state()["stage"]["position"] + pos_i = [pos_i_dict["x"], pos_i_dict["y"], pos_i_dict["z"]] move = [0, 0, 0] - move[axis] = move_distance*direction + move[axis] = move_distance * direction connection.move_by(*move) - pos_f_dict = connection.get_state()['stage']['position'] - pos_f = [pos_f_dict['x'], pos_f_dict['y'], pos_f_dict['z']] + pos_f_dict = connection.get_state()["stage"]["position"] + pos_f = [pos_f_dict["x"], pos_f_dict["y"], pos_f_dict["z"]] diff = np.subtract(pos_f, pos_i) logging.debug("{} > {}".format(pos_i, pos_f)) @@ -108,7 +101,7 @@ class TestStage(unittest.TestCase): self.assertTrue(np.array_equal(diff, move)) -if __name__ == '__main__': +if __name__ == "__main__": suites = [ unittest.TestLoader().loadTestsFromTestCase(TestCapture), diff --git a/tests/test_camera.py b/tests/test_camera.py index 0188fa2f..6824c1d2 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -12,11 +12,11 @@ import unittest import logging import sys + logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) class TestCaptureMethods(unittest.TestCase): - def test_still_capture(self): """Tests capturing still images to a BytesIO stream.""" global camera @@ -30,11 +30,7 @@ class TestCaptureMethods(unittest.TestCase): # Capture to a context (auto-deletes files when done) with camera.new_image(write_to_file=False) as output: - camera.capture( - output, - use_video_port=use_video_port, - resize=resize - ) + camera.capture(output, use_video_port=use_video_port, resize=resize) # Ensure file deletion fails and returns False self.assertFalse(output.delete_file()) @@ -43,14 +39,8 @@ class TestCaptureMethods(unittest.TestCase): # BEFORE DELETE: Ensure StreamObject 'stream' has # a valid BytesIO object and byte string - self.assertTrue(isinstance( - output.data, - io.IOBase - )) - self.assertTrue(isinstance( - output.binary, - (bytes, bytearray) - )) + self.assertTrue(isinstance(output.data, io.IOBase)) + self.assertTrue(isinstance(output.binary, (bytes, bytearray))) # Save capture to file output.save_file() @@ -65,10 +55,7 @@ class TestCaptureMethods(unittest.TestCase): # AFTER DELETE: Ensure StreamObject 'stream' has # a valid BytesIO object and byte string self.assertTrue(isinstance(output.data, io.IOBase)) - self.assertTrue(isinstance( - output.binary, - (bytes, bytearray) - )) + self.assertTrue(isinstance(output.binary, (bytes, bytearray))) # Create a PIL image from stream image = Image.open(output.data) @@ -102,7 +89,8 @@ class TestCaptureMethods(unittest.TestCase): output = camera.capture( camera.new_image(write_to_file=True), use_video_port=use_video_port, - resize=resize) + resize=resize, + ) # Check file got saved self.assertTrue(os.path.isfile(output.file)) @@ -122,7 +110,6 @@ class TestCaptureMethods(unittest.TestCase): class TestUnencodedMethods(unittest.TestCase): - def test_yuv_array(self): """Tests capturing unencoded YUV data to a Numpy array.""" global camera @@ -136,9 +123,7 @@ class TestUnencodedMethods(unittest.TestCase): camera.wait_for_camera() # Capture RGB array - yuv = camera.yuv( - use_video_port=use_video_port, - resize=resize) + yuv = camera.yuv(use_video_port=use_video_port, resize=resize) # Ensure capture output is a valid numpy array self.assertTrue(isinstance(yuv, np.ndarray)) @@ -168,9 +153,7 @@ class TestUnencodedMethods(unittest.TestCase): camera.wait_for_camera() # Capture RGB array - rgb = camera.array( - use_video_port=use_video_port, - resize=resize) + rgb = camera.array(use_video_port=use_video_port, resize=resize) # Ensure capture output is a valid numpy array self.assertTrue(isinstance(rgb, np.ndarray)) @@ -189,7 +172,6 @@ class TestUnencodedMethods(unittest.TestCase): class TestRecordMethods(unittest.TestCase): - def test_video_record(self): """Tests recording videos to BytesIO stream, and to file on disk.""" global camera @@ -201,9 +183,7 @@ class TestRecordMethods(unittest.TestCase): # Wait for camera camera.wait_for_camera() - with camera.new_video( - write_to_file=write_to_file - ) as output: + with camera.new_video(write_to_file=write_to_file) as output: # Start recording camera.start_recording(output) @@ -278,7 +258,7 @@ class TestThreadStarting(unittest.TestCase): self.assertIsInstance(camera.stream, io.IOBase) -if __name__ == '__main__': +if __name__ == "__main__": with PiCameraStreamer() as camera: suites = [ diff --git a/tests/test_plugins.py b/tests/test_plugins.py index ebf9c449..4a215c66 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -8,17 +8,17 @@ import atexit import unittest import logging, sys + logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) class TestPluginMethods(unittest.TestCase): - def test_plugin_load(self): plugin_arr = microscope.plugin.plugins plugin_names = [plugin[0] for plugin in plugin_arr] - self.assertTrue('testing' in plugin_names) + self.assertTrue("testing" in plugin_names) def test_camera_access(self): identify = microscope.plugin.testing.identify() @@ -29,18 +29,16 @@ class TestPluginMethods(unittest.TestCase): self.assertTrue(identify[1] is microscope.stage) -if __name__ == '__main__': +if __name__ == "__main__": - with Microscope() as microscope: + with Microscope() as microscope: - microscope.attach(PiCameraStreamer(), OpenFlexureStage()) + microscope.attach(PiCameraStreamer(), OpenFlexureStage()) - microscope.plugin.attach("openflexure_microscope.plugins.testing:Plugin") + microscope.plugin.attach("openflexure_microscope.plugins.testing:Plugin") - suites = [ - unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods), - ] + suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)] - alltests = unittest.TestSuite(suites) + alltests = unittest.TestSuite(suites) - result = unittest.TextTestRunner(verbosity=2).run(alltests) + result = unittest.TextTestRunner(verbosity=2).run(alltests) diff --git a/tests/test_stage.py b/tests/test_stage.py index 90b147be..b211bc76 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -12,7 +12,6 @@ logging.basicConfig(stream=sys.stderr, level=logging.INFO) class TestMicroscope(unittest.TestCase): - def test_movement(self): move_distance = 500 for axis in range(3): @@ -20,9 +19,11 @@ class TestMicroscope(unittest.TestCase): pos_i = stage.position logging.debug(pos_i) - logging.info("Moving axis {} by {}".format(axis, move_distance*direction)) + logging.info( + "Moving axis {} by {}".format(axis, move_distance * direction) + ) move = [0, 0, 0] - move[axis] = move_distance*direction + move[axis] = move_distance * direction stage.move_rel(move) @@ -33,12 +34,10 @@ class TestMicroscope(unittest.TestCase): self.assertTrue(np.array_equal(diff, move)) -if __name__ == '__main__': +if __name__ == "__main__": with OpenFlexureStage("/dev/ttyUSB0") as stage: - suites = [ - unittest.TestLoader().loadTestsFromTestCase(TestMicroscope), - ] + suites = [unittest.TestLoader().loadTestsFromTestCase(TestMicroscope)] alltests = unittest.TestSuite(suites)