Skip to content

Commit

Permalink
paquo.classes: allow instantiating QuPathPathClass directly
Browse files Browse the repository at this point in the history
more python zen
the classmethod create path was unintuitive
  • Loading branch information
ap-- committed Aug 12, 2020
1 parent 26f33b8 commit 7aceaa4
Show file tree
Hide file tree
Showing 10 changed files with 50 additions and 38 deletions.
4 changes: 2 additions & 2 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ QuPathPathClasses
-----------------

Classes are used to group your annotation into *(you've guessed it)* classes. :class:`QuPathPathClasses` can
have names, a color and they can be children of other classes. If you want to create a `QuPathPathClass` always
use the :meth:`QuPathPathClass.create` classmethod.
have names, a color and they can be children of other classes. If you want to create a new `QuPathPathClass`
just instantiate it by providing a name and an optional color and optional parent class.

.. autoclass:: paquo.classes.QuPathPathClass
:members:
Expand Down
2 changes: 1 addition & 1 deletion examples/example_03_project_with_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
new_classes = []
for class_name, class_color in MY_CLASSES_AND_COLORS:
new_classes.append(
QuPathPathClass.create(name=class_name, color=class_color)
QuPathPathClass(name=class_name, color=class_color)
)

# setting QuPathProject.path_class always replaces all classes
Expand Down
4 changes: 2 additions & 2 deletions examples/prepare_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def prepare_example_resources():
with QuPathProject(example_project_dir) as qp:
for img_fn in images:
qp.add_image(img_fn, image_type=QuPathImageType.BRIGHTFIELD_H_E)
qp.path_classes = map(QuPathPathClass.create, ["myclass_0", "myclass_1", "myclass_2"])
qp.path_classes = map(QuPathPathClass, ["myclass_0", "myclass_1", "myclass_2"])
for idx, image in enumerate(qp.images):
image.metadata['image_number'] = str(1000 + idx)

Expand All @@ -68,7 +68,7 @@ def _get_bounds(idx_x, idx_y):
image_0 = qp.images[0]
for x, y in itertools.product(range(4), repeat=2):
roi = Polygon.from_bounds(*_get_bounds(x, y))
image_0.hierarchy.add_annotation(roi, path_class=QuPathPathClass.create("myclass_1"))
image_0.hierarchy.add_annotation(roi, path_class=QuPathPathClass("myclass_1"))

with open(DATA_DIR / "annotations.geojson", "w") as f:
json.dump(image_0.hierarchy.to_geojson(), f)
Expand Down
2 changes: 1 addition & 1 deletion paquo/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def create_project(project_path, class_names_colors, images,
path_classes = list(qp.path_classes)
for name, color in class_names_colors:
path_classes.append(
QuPathPathClass.create(name, color=color)
QuPathPathClass(name, color=color)
)
qp.path_classes = path_classes

Expand Down
34 changes: 23 additions & 11 deletions paquo/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
from paquo.colors import QuPathColor, ColorType
from paquo.java import PathClass, PathClassFactory, String

__all__ = ['QuPathPathClass']


class QuPathPathClass(QuPathBase[PathClass]):

def __init__(self, path_class: PathClass) -> None:
@classmethod
def from_java(cls, path_class: PathClass) -> 'QuPathPathClass':
"""initialize a QuPathPathClass from its corresponding java PathClass"""
if not isinstance(path_class, PathClass):
raise TypeError("use PathClass.create() to instantiate")
super().__init__(path_class)

@classmethod
def create(cls,
name: str,
color: Optional[ColorType] = None,
parent: Optional['QuPathPathClass'] = None) -> 'QuPathPathClass':
raise TypeError("use PathClass(name='myclass') to instantiate")
# keep type annotation requirements intact by providing
# empty string, which is ignored when providing _java_path_class
return cls('', _java_path_class=path_class)

def __init__(self,
name: str,
color: Optional[ColorType] = None,
parent: Optional['QuPathPathClass'] = None,
**_kwargs) -> None:
"""create a QuPathPathClass
The QuPathPathClasses are wrappers around singletons defined by their
Expand All @@ -40,6 +45,13 @@ def create(cls,
path_class:
the QuPathPathClass
"""
# internal: check if a java path class was already provided
_java_path_class = _kwargs.pop('_java_path_class', None)
if _java_path_class is not None:
super().__init__(_java_path_class)
return

# called by user
if name is None:
if parent is None:
# note:
Expand Down Expand Up @@ -68,7 +80,7 @@ def create(cls,
java_color = QuPathColor.from_any(color).to_java_rgba() # use rgba?

path_class = PathClassFactory.getDerivedPathClass(java_parent, name, java_color)
return cls(path_class)
super().__init__(path_class)

@property
def name(self) -> str:
Expand All @@ -92,7 +104,7 @@ def parent(self) -> Optional['QuPathPathClass']:
path_class = self.java_object.getParentClass()
if path_class is None:
return None
return QuPathPathClass(path_class)
return QuPathPathClass.from_java(path_class)

@property
def origin(self) -> 'QuPathPathClass':
Expand Down
2 changes: 1 addition & 1 deletion paquo/pathobjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def path_class(self) -> Optional[QuPathPathClass]:
pc = self.java_object.getPathClass()
if not pc:
return None
return QuPathPathClass(pc)
return QuPathPathClass.from_java(pc)

@property
def path_class_probability(self) -> float:
Expand Down
2 changes: 1 addition & 1 deletion paquo/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ def uri(self) -> str:
@property
def path_classes(self) -> Tuple[QuPathPathClass, ...]:
"""return path_classes stored in the project"""
return tuple(map(QuPathPathClass, self.java_object.getPathClasses()))
return tuple(map(QuPathPathClass.from_java, self.java_object.getPathClasses()))

@path_classes.setter
def path_classes(self, path_classes: Iterable[QuPathPathClass]):
Expand Down
30 changes: 15 additions & 15 deletions paquo/tests/test_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

@pytest.fixture(scope='session')
def pathclass():
yield QuPathPathClass.create("MyClass")
yield QuPathPathClass("MyClass")


def test_pathclass_creation():
with pytest.raises(TypeError):
QuPathPathClass("abc")
QuPathPathClass.from_java("abc")

pc = QuPathPathClass.create("MyClass", color=None)
pc = QuPathPathClass("MyClass", color=None)
assert pc.parent is None
assert pc.name == pc.id == "MyClass"
assert pc.is_valid
Expand All @@ -22,39 +22,39 @@ def test_pathclass_creation():
def test_deny_name_none_creation():
with pytest.raises(NotImplementedError):
# noinspection PyTypeChecker
QuPathPathClass.create(None, parent=None)
QuPathPathClass(None, parent=None)

pc = QuPathPathClass.create("MyClass")
pc = QuPathPathClass("MyClass")
with pytest.raises(ValueError):
# noinspection PyTypeChecker
QuPathPathClass.create(None, parent=pc)
QuPathPathClass(None, parent=pc)


def test_incorrect_parent_type():
with pytest.raises(TypeError):
# noinspection PyTypeChecker
QuPathPathClass.create("new class", parent="parent_class")
QuPathPathClass("new class", parent="parent_class")


def test_incorrect_class_name():
with pytest.raises(TypeError):
# noinspection PyTypeChecker
QuPathPathClass.create(1)
QuPathPathClass(1)
with pytest.raises(ValueError):
QuPathPathClass.create("my::class")
QuPathPathClass("my::class")


def test_pathclass_equality(pathclass):
other = QuPathPathClass.create("MyClass2")
same = QuPathPathClass.create("MyClass")
other = QuPathPathClass("MyClass2")
same = QuPathPathClass("MyClass")
assert pathclass == pathclass
assert pathclass != other
assert pathclass == same
assert pathclass != 123


def test_pathclass_creation_with_parent(pathclass):
pc = QuPathPathClass.create("MyChild", parent=pathclass)
pc = QuPathPathClass("MyChild", parent=pathclass)
assert pc.parent == pathclass
assert pc.name == "MyChild"
assert pc.id == "MyClass: MyChild"
Expand All @@ -69,18 +69,18 @@ def test_pathclass_creation_with_parent(pathclass):


def test_pathclass_colors():
pc = QuPathPathClass.create("MyNew", color=None)
pc = QuPathPathClass("MyNew", color=None)
my_class_color = (49, 139, 153) # based on string MyNew
assert pc.color.to_rgb() == my_class_color

pc = QuPathPathClass.create("MyNew2", color=(1, 2, 3))
pc = QuPathPathClass("MyNew2", color=(1, 2, 3))
assert pc.color.to_rgb() == (1, 2, 3)

pc.color = "#ff0000"
assert pc.color.to_rgb() == (255, 0, 0)


def test_pathclass_none_colors():
pc = QuPathPathClass.create("MyNew")
pc = QuPathPathClass("MyNew")
pc.color = None
assert pc.color is None
6 changes: 3 additions & 3 deletions paquo/tests/test_pathobjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_shapely_to_qupath_conversion():
def path_annotation(request):
"""parameterized fixture for different Annotation Objects"""
roi = request.param
path_class = QuPathPathClass.create("myclass")
path_class = QuPathPathClass("myclass")

ao = QuPathPathAnnotationObject.from_shapely(roi, path_class)

Expand Down Expand Up @@ -78,7 +78,7 @@ def test_geojson_serialization(path_annotation):
def test_annotation_object():
ao = QuPathPathAnnotationObject.from_shapely(
shapely.geometry.Point(1, 1),
path_class=QuPathPathClass.create('myclass'),
path_class=QuPathPathClass('myclass'),
measurements={'measurement1': 1.23},
path_class_probability=0.5,
)
Expand Down Expand Up @@ -118,7 +118,7 @@ def test_annotation_object():
def test_measurements():
ao = QuPathPathAnnotationObject.from_shapely(
shapely.geometry.Point(1, 1),
path_class=QuPathPathClass.create('myclass'),
path_class=QuPathPathClass('myclass'),
measurements={'measurement1': 1.23},
path_class_probability=0.5,
)
Expand Down
2 changes: 1 addition & 1 deletion paquo/tests/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def test_project_add_path_classes(new_project):
from paquo.classes import QuPathPathClass

names = {'a', 'b', 'c'}
new_project.path_classes = map(QuPathPathClass.create, names)
new_project.path_classes = map(QuPathPathClass, names)

assert len(new_project.path_classes) == 3
assert set(c.name for c in new_project.path_classes) == names
Expand Down

0 comments on commit 7aceaa4

Please sign in to comment.