Skip to content

Commit 6f4d84d

Browse files
committed
Merge remote-tracking branch 'origin/main' into main
2 parents e57f8b5 + 5e087af commit 6f4d84d

File tree

9 files changed

+169
-96
lines changed

9 files changed

+169
-96
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Changed
1313

14+
* Changed new artist registration to check if subclass.
15+
1416
### Removed
1517

1618

1719
## [1.9.1] 2021-10-22
1820

1921
### Added
2022

21-
* Added `Plane.offset`
22-
* Added `is_mesh_closed` property to `compas.datastructures.mesh_slice_plane`
23+
* Added `Plane.offset`.
24+
* Added `is_mesh_closed` property to `compas.datastructures.mesh_slice_plane`.
2325

2426
### Changed
2527

src/compas/artists/artist.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
from __future__ import division
33
from __future__ import print_function
44

5+
import inspect
56
from abc import abstractmethod
7+
8+
from compas.artists import DataArtistNotRegistered
69
from compas.plugins import pluggable
710

811

@@ -55,6 +58,21 @@ def build_as(item, artist_type, **kwargs):
5558
artist = artist_type(item, **kwargs)
5659
return artist
5760

61+
@staticmethod
62+
def get_artist_cls(data, **kwargs):
63+
dtype = type(data)
64+
cls = None
65+
if 'artist_type' in kwargs:
66+
cls = kwargs['artist_type']
67+
else:
68+
for type_ in inspect.getmro(dtype):
69+
cls = Artist.ITEM_ARTIST.get(type_)
70+
if cls is not None:
71+
break
72+
if cls is None:
73+
raise DataArtistNotRegistered('No artist is registered for this data type: {}'.format(dtype))
74+
return cls
75+
5876
@staticmethod
5977
def clear():
6078
return clear()

src/compas/plugins.py

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
:toctree: generated/
4646
:nosignatures:
4747
48+
IncompletePluginImplError
4849
PluginNotInstalledError
4950
"""
5051
from __future__ import absolute_import
@@ -60,8 +61,10 @@
6061
'pluggable',
6162
'plugin',
6263
'plugin_manager',
64+
'IncompletePluginImplError',
6365
'PluginManager',
6466
'PluginNotInstalledError',
67+
'PluginValidator',
6568
]
6669

6770

@@ -81,6 +84,11 @@ def _get_extension_point_url_from_method(domain, category, plugin_method):
8184
return '{}/{}/{}'.format(domain, category, name).replace('//', '/')
8285

8386

87+
class IncompletePluginImplError(Exception):
88+
"""Exception raised when a plugin does not have implementations for all abstract methods of its base class."""
89+
pass
90+
91+
8492
class PluginImpl(object):
8593
"""Internal data class to keep track of a loaded plugin implementation.
8694
@@ -392,7 +400,11 @@ def try_import(self, module_name):
392400
try:
393401
module = __import__(module_name, fromlist=['__name__'], level=0)
394402
self._cache[module_name] = True
395-
except ImportError:
403+
404+
# There are two types of possible failure modes:
405+
# 1) cannot be imported, or
406+
# 2) is a python 3 module and we're in IPY, which causes a SyntaxError
407+
except (ImportError, SyntaxError):
396408
self._cache[module_name] = False
397409

398410
return module
@@ -416,46 +428,56 @@ def check_importable(self, module_name):
416428
return self._cache[module_name]
417429

418430

419-
def verify_requirement(manager, requirement):
420-
if callable(requirement):
421-
return requirement()
431+
class PluginValidator(object):
432+
"""Plugin Validator handles validation of plugins."""
422433

423-
return manager.importer.check_importable(requirement)
434+
def __init__(self, manager):
435+
self.manager = manager
424436

437+
def verify_requirement(self, requirement):
438+
if callable(requirement):
439+
return requirement()
425440

426-
def is_plugin_selectable(plugin, manager):
427-
if plugin.opts['requires']:
428-
importable_requirements = (verify_requirement(manager, requirement) for requirement in plugin.opts['requires'])
441+
return self.manager.importer.check_importable(requirement)
429442

430-
if not all(importable_requirements):
431-
if manager.DEBUG:
432-
print('Requirements not satisfied. Plugin will not be used: {}'.format(plugin.id))
433-
return False
443+
def is_plugin_selectable(self, plugin):
444+
if plugin.opts['requires']:
445+
importable_requirements = (self.verify_requirement(requirement) for requirement in plugin.opts['requires'])
434446

435-
return True
447+
if not all(importable_requirements):
448+
if self.manager.DEBUG:
449+
print('Requirements not satisfied. Plugin will not be used: {}'.format(plugin.id))
450+
return False
436451

452+
return True
437453

438-
def select_plugin(extension_point_url, manager):
439-
if manager.DEBUG:
440-
print('Extension Point URL {} invoked. Will select a matching plugin'.format(extension_point_url))
454+
def select_plugin(self, extension_point_url):
455+
if self.manager.DEBUG:
456+
print('Extension Point URL {} invoked. Will select a matching plugin'.format(extension_point_url))
441457

442-
plugins = manager.registry.get(extension_point_url) or []
443-
for plugin in plugins:
444-
if is_plugin_selectable(plugin, manager):
445-
return plugin
458+
plugins = self.manager.registry.get(extension_point_url) or []
459+
for plugin in plugins:
460+
if self.is_plugin_selectable(plugin):
461+
return plugin
446462

447-
# Nothing found, raise
448-
raise PluginNotInstalledError('Plugin not found for extension point URL: {}'.format(extension_point_url))
463+
# Nothing found, raise
464+
raise PluginNotInstalledError('Plugin not found for extension point URL: {}'.format(extension_point_url))
449465

466+
def collect_plugins(self, extension_point_url):
467+
if self.manager.DEBUG:
468+
print('Extension Point URL {} invoked. Will select a matching plugin'.format(extension_point_url))
450469

451-
def collect_plugins(extension_point_url, manager):
452-
if manager.DEBUG:
453-
print('Extension Point URL {} invoked. Will select a matching plugin'.format(extension_point_url))
470+
plugins = self.manager.registry.get(extension_point_url) or []
471+
return [plugin for plugin in plugins if self.is_plugin_selectable(plugin)]
454472

455-
plugins = manager.registry.get(extension_point_url) or []
456-
return [plugin for plugin in plugins if is_plugin_selectable(plugin, manager)]
473+
@staticmethod
474+
def ensure_implementations(cls):
475+
for name, value in inspect.getmembers(cls):
476+
if inspect.isfunction(value) or inspect.ismethod(value):
477+
if hasattr(value, '__isabstractmethod__'):
478+
raise IncompletePluginImplError('Abstract method not implemented: {}'.format(value))
457479

458480

459481
plugin_manager = PluginManager()
460-
_select_plugin = functools.partial(select_plugin, manager=plugin_manager)
461-
_collect_plugins = functools.partial(collect_plugins, manager=plugin_manager)
482+
_select_plugin = PluginValidator(plugin_manager).select_plugin
483+
_collect_plugins = PluginValidator(plugin_manager).collect_plugins

src/compas_blender/artists/__init__.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,12 @@
6262
BlenderArtist
6363
6464
"""
65-
import inspect
6665

6766
import compas_blender
6867

6968
from compas.plugins import plugin
69+
from compas.plugins import PluginValidator
7070
from compas.artists import Artist
71-
from compas.artists import DataArtistNotRegistered
7271

7372
from compas.geometry import Box
7473
from compas.geometry import Capsule
@@ -130,20 +129,9 @@ def new_artist_blender(cls, *args, **kwargs):
130129

131130
data = args[0]
132131

133-
if 'artist_type' in kwargs:
134-
cls = kwargs['artist_type']
135-
else:
136-
dtype = type(data)
137-
if dtype not in BlenderArtist.ITEM_ARTIST:
138-
raise DataArtistNotRegistered('No Blender artist is registered for this data type: {}'.format(dtype))
139-
cls = BlenderArtist.ITEM_ARTIST[dtype]
132+
cls = Artist.get_artist_cls(data, **kwargs)
140133

141-
# TODO: move this to the plugin module and/or to a dedicated function
142-
143-
for name, value in inspect.getmembers(cls):
144-
if inspect.isfunction(value):
145-
if hasattr(value, '__isabstractmethod__'):
146-
raise Exception('Abstract method not implemented: {}'.format(value))
134+
PluginValidator.ensure_implementations(cls)
147135

148136
return super(Artist, cls).__new__(cls)
149137

src/compas_ghpython/artists/__init__.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,10 @@
6363
"""
6464
from __future__ import absolute_import
6565

66-
import inspect
67-
6866
from compas.plugins import plugin
67+
from compas.plugins import PluginValidator
6968
from compas.artists import Artist
7069
from compas.artists import ShapeArtist
71-
from compas.artists import DataArtistNotRegistered
7270

7371
from compas.geometry import Box
7472
from compas.geometry import Capsule
@@ -170,20 +168,9 @@ def new_artist_gh(cls, *args, **kwargs):
170168

171169
data = args[0]
172170

173-
if 'artist_type' in kwargs:
174-
cls = kwargs['artist_type']
175-
else:
176-
dtype = type(data)
177-
if dtype not in GHArtist.ITEM_ARTIST:
178-
raise DataArtistNotRegistered('No GH artist is registered for this data type: {}'.format(dtype))
179-
cls = GHArtist.ITEM_ARTIST[dtype]
180-
181-
# TODO: move this to the plugin module and/or to a dedicated function
171+
cls = Artist.get_artist_cls(data, **kwargs)
182172

183-
for name, value in inspect.getmembers(cls):
184-
if inspect.ismethod(value):
185-
if hasattr(value, '__isabstractmethod__'):
186-
raise Exception('Abstract method not implemented: {}'.format(value))
173+
PluginValidator.ensure_implementations(cls)
187174

188175
return super(Artist, cls).__new__(cls)
189176

@@ -204,7 +191,7 @@ def new_artist_gh(cls, *args, **kwargs):
204191
'PolygonArtist',
205192
'PolyhedronArtist',
206193
'PolylineArtist',
207-
'RobotModelArtist'
194+
'RobotModelArtist',
208195
'SphereArtist',
209196
'TorusArtist',
210197
'VectorArtist',

src/compas_plotters/artists/__init__.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,10 @@
4242
PlotterArtist
4343
4444
"""
45-
import inspect
4645

4746
from compas.plugins import plugin
47+
from compas.plugins import PluginValidator
4848
from compas.artists import Artist
49-
from compas.artists import DataArtistNotRegistered
5049

5150
from compas.geometry import Point
5251
from compas.geometry import Vector
@@ -102,20 +101,9 @@ def new_artist_plotter(cls, *args, **kwargs):
102101

103102
data = args[0]
104103

105-
if 'artist_type' in kwargs:
106-
cls = kwargs['artist_type']
107-
else:
108-
dtype = type(data)
109-
if dtype not in PlotterArtist.ITEM_ARTIST:
110-
raise DataArtistNotRegistered('No Plotter artist is registered for this data type: {}'.format(dtype))
111-
cls = PlotterArtist.ITEM_ARTIST[dtype]
112-
113-
# TODO: move this to the plugin module and/or to a dedicated function
104+
cls = Artist.get_artist_cls(data, **kwargs)
114105

115-
for name, value in inspect.getmembers(cls):
116-
if inspect.isfunction(value):
117-
if hasattr(value, '__isabstractmethod__'):
118-
raise Exception('Abstract method not implemented: {}'.format(value))
106+
PluginValidator.ensure_implementations(cls)
119107

120108
return super(Artist, cls).__new__(cls)
121109

@@ -130,5 +118,5 @@ def new_artist_plotter(cls, *args, **kwargs):
130118
'CircleArtist',
131119
'EllipseArtist',
132120
'MeshArtist',
133-
'NetworkArtist'
121+
'NetworkArtist',
134122
]

src/compas_rhino/artists/__init__.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,10 @@
7373
"""
7474
from __future__ import absolute_import
7575

76-
import inspect
77-
7876
from compas.plugins import plugin
77+
from compas.plugins import PluginValidator
7978
from compas.artists import Artist
8079
from compas.artists import ShapeArtist
81-
from compas.artists import DataArtistNotRegistered
8280

8381
from compas.geometry import Circle
8482
from compas.geometry import Frame
@@ -196,20 +194,9 @@ def new_artist_rhino(cls, *args, **kwargs):
196194

197195
data = args[0]
198196

199-
if 'artist_type' in kwargs:
200-
cls = kwargs['artist_type']
201-
else:
202-
dtype = type(data)
203-
if dtype not in RhinoArtist.ITEM_ARTIST:
204-
raise DataArtistNotRegistered('No Rhino artist is registered for this data type: {}'.format(dtype))
205-
cls = RhinoArtist.ITEM_ARTIST[dtype]
206-
207-
# TODO: move this to the plugin module and/or to a dedicated function
197+
cls = Artist.get_artist_cls(data, **kwargs)
208198

209-
for name, value in inspect.getmembers(cls):
210-
if inspect.ismethod(value):
211-
if hasattr(value, '__isabstractmethod__'):
212-
raise Exception('Abstract method not implemented: {}'.format(value))
199+
PluginValidator.ensure_implementations(cls)
213200

214201
return super(Artist, cls).__new__(cls)
215202

@@ -235,5 +222,5 @@ def new_artist_rhino(cls, *args, **kwargs):
235222
'MeshArtist',
236223
'NetworkArtist',
237224
'VolMeshArtist',
238-
'RobotModelArtist'
225+
'RobotModelArtist',
239226
]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from compas.artists import Artist
2+
3+
4+
class FakeArtist(Artist):
5+
def draw(self):
6+
pass
7+
8+
9+
class FakeSubArtist(Artist):
10+
def draw(self):
11+
pass
12+
13+
14+
class FakeItem(object):
15+
pass
16+
17+
18+
class FakeSubItem(FakeItem):
19+
pass
20+
21+
22+
def test_get_artist_cls_with_orderly_registration():
23+
Artist.register(FakeItem, FakeArtist)
24+
Artist.register(FakeSubItem, FakeSubArtist)
25+
item = FakeItem()
26+
artist = Artist.get_artist_cls(item)
27+
assert artist == FakeArtist
28+
29+
item = FakeSubItem()
30+
artist = Artist.get_artist_cls(item)
31+
assert artist == FakeSubArtist
32+
33+
34+
def test_get_artist_cls_with_out_of_order_registration():
35+
Artist.register(FakeSubItem, FakeSubArtist)
36+
Artist.register(FakeItem, FakeArtist)
37+
item = FakeItem()
38+
artist = Artist.get_artist_cls(item)
39+
assert artist == FakeArtist
40+
41+
item = FakeSubItem()
42+
artist = Artist.get_artist_cls(item)
43+
assert artist == FakeSubArtist

0 commit comments

Comments
 (0)