Skip to content

Commit 5373307

Browse files
authored
Merge pull request #77 from mwcraig/remove-image-attributes
Remove `image_height` and `image_width` attributes; test that every method/attribute has docs
2 parents 79e13c1 + 1cae1e3 commit 5373307

File tree

4 files changed

+65
-97
lines changed

4 files changed

+65
-97
lines changed

src/astro_image_display_api/api_test.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818

1919
__all__ = ["ImageAPITest"]
2020

21+
DEFAULT_IMAGE_SHAPE = (100, 150)
22+
2123

2224
class ImageAPITest:
2325
@pytest.fixture
2426
def data(self):
2527
rng = np.random.default_rng(1234)
26-
return rng.random((100, 150))
28+
return rng.random(DEFAULT_IMAGE_SHAPE)
2729

2830
@pytest.fixture
2931
def wcs(self):
@@ -49,8 +51,8 @@ def catalog(self, wcs: WCS) -> Table:
4951
expected columns.
5052
"""
5153
rng = np.random.default_rng(45328975)
52-
x = rng.uniform(0, self.image.image_width, size=10)
53-
y = rng.uniform(0, self.image.image_height, size=10)
54+
x = rng.uniform(0, DEFAULT_IMAGE_SHAPE[0], size=10)
55+
y = rng.uniform(0, DEFAULT_IMAGE_SHAPE[1], size=10)
5456
coord = wcs.pixel_to_world(x, y)
5557

5658
cat = Table(
@@ -70,7 +72,7 @@ def setup(self):
7072
Subclasses MUST define ``image_widget_class`` -- doing so as a
7173
class variable does the trick.
7274
"""
73-
self.image = self.image_widget_class(image_width=250, image_height=100)
75+
self.image = self.image_widget_class()
7476

7577
def _assert_empty_catalog_table(self, table):
7678
assert isinstance(table, Table)
@@ -81,17 +83,6 @@ def _get_catalog_names_as_set(self):
8183
marks = self.image.catalog_names
8284
return set(marks)
8385

84-
def test_width_height(self):
85-
assert self.image.image_width == 250
86-
assert self.image.image_height == 100
87-
88-
width = 200
89-
height = 300
90-
self.image.image_width = width
91-
self.image.image_height = height
92-
assert self.image.image_width == width
93-
assert self.image.image_height == height
94-
9586
@pytest.mark.parametrize("load_type", ["fits", "nddata", "array"])
9687
def test_load(self, data, tmp_path, load_type):
9788
match load_type:
@@ -915,3 +906,25 @@ def test_all_methods_accept_additional_kwargs(self, data, catalog, tmp_path):
915906
"The following methods failed when called with additional kwargs:\n\t"
916907
f"{'\n\t'.join(failed_methods)}"
917908
)
909+
910+
def test_every_method_attribute_has_docstring(self):
911+
"""
912+
Check that every method and attribute in the protocol has a docstring.
913+
"""
914+
from astro_image_display_api import ImageViewerInterface
915+
916+
all_methods_and_attributes = ImageViewerInterface.__protocol_attrs__
917+
918+
method_attrs_no_docs = []
919+
920+
for method in all_methods_and_attributes:
921+
attr = getattr(self.image, method)
922+
# Make list of methods and attributes that have no docstring
923+
# and assert that list is empty at the end of the test.
924+
if not attr.__doc__:
925+
method_attrs_no_docs.append(method)
926+
927+
assert not method_attrs_no_docs, (
928+
"The following methods and attributes have no docstring:\n\t"
929+
f"{'\n\t'.join(method_attrs_no_docs)}"
930+
)

src/astro_image_display_api/image_viewer_logic.py

Lines changed: 14 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,27 @@ class ViewportInfo:
5353
data: ArrayLike | NDData | CCDData | None = None
5454

5555

56+
def docs_from_interface(cls):
57+
"""
58+
Decorator to copy the docstrings from the interface methods to the
59+
methods in the class.
60+
"""
61+
for name, method in cls.__dict__.items():
62+
if not name.startswith("_"):
63+
interface_method = getattr(ImageViewerInterface, name, None)
64+
if interface_method:
65+
method.__doc__ = interface_method.__doc__
66+
return cls
67+
68+
5669
@dataclass
70+
@docs_from_interface
5771
class ImageViewerLogic:
5872
"""
5973
This viewer does not do anything except making changes to its internal
6074
state to simulate the behavior of a real viewer.
6175
"""
6276

63-
# These are attributes, not methods. The type annotations are there
64-
# to make sure Protocol knows they are attributes. Python does not
65-
# do any checking at all of these types.
66-
image_width: int = 0
67-
image_height: int = 0
68-
zoom_level: float = 1
6977
_cuts: BaseInterval | tuple[float, float] = AsymmetricPercentileInterval(
7078
upper_percentile=95
7179
)
@@ -206,8 +214,6 @@ def set_colormap(
206214
)
207215
self._images[image_label].colormap = map_name
208216

209-
set_colormap.__doc__ = ImageViewerInterface.set_colormap.__doc__
210-
211217
def get_colormap(
212218
self,
213219
image_label: str | None = None,
@@ -220,28 +226,13 @@ def get_colormap(
220226
)
221227
return self._images[image_label].colormap
222228

223-
get_colormap.__doc__ = ImageViewerInterface.get_colormap.__doc__
224-
225229
# The methods, grouped loosely by purpose
226230

227231
def get_catalog_style(
228232
self,
229233
catalog_label=None,
230234
**kwargs, # noqa: ARG002
231235
) -> dict[str, Any]:
232-
"""
233-
Get the style for the catalog.
234-
235-
Parameters
236-
----------
237-
catalog_label : str, optional
238-
The label of the catalog. Default is ``None``.
239-
240-
Returns
241-
-------
242-
dict
243-
The style for the catalog.
244-
"""
245236
catalog_label = self._resolve_catalog_label(catalog_label)
246237

247238
style = self._catalogs[catalog_label].style.copy()
@@ -256,22 +247,6 @@ def set_catalog_style(
256247
size: float = 5,
257248
**kwargs,
258249
) -> None:
259-
"""
260-
Set the style for the catalog.
261-
262-
Parameters
263-
----------
264-
catalog_label : str, optional
265-
The label of the catalog.
266-
shape : str, optional
267-
The shape of the markers.
268-
color : str, optional
269-
The color of the markers.
270-
size : float, optional
271-
The size of the markers.
272-
**kwargs
273-
Additional keyword arguments to pass to the marker style.
274-
"""
275250
catalog_label = self._resolve_catalog_label(catalog_label)
276251

277252
if self._catalogs[catalog_label].data is None:
@@ -324,18 +299,6 @@ def load_image(
324299
image_label: str | None = None,
325300
**kwargs, # noqa: ARG002
326301
) -> None:
327-
"""
328-
Load a FITS file into the viewer.
329-
330-
Parameters
331-
----------
332-
file : str or array-like
333-
The FITS file to load. If a string, it can be a URL or a
334-
file path.
335-
336-
image_label : str, optional
337-
A label for the image.
338-
"""
339302
image_label = self._resolve_image_label(image_label)
340303

341304
# Delete the current viewport if it exists
@@ -375,8 +338,6 @@ def get_image(
375338
def image_labels(self) -> tuple[str, ...]:
376339
return tuple(k for k in self._images.keys() if k is not None)
377340

378-
image_labels.__doc__ = ImageViewerInterface.image_labels.__doc__
379-
380341
def _determine_largest_dimension(self, shape: tuple[int, int]) -> int:
381342
"""
382343
Determine which index is the largest dimension.
@@ -497,19 +458,6 @@ def save(
497458
overwrite: bool = False,
498459
**kwargs, # noqa: ARG002
499460
) -> None:
500-
"""
501-
Save the current view to a file.
502-
503-
Parameters
504-
----------
505-
filename : str or `os.PathLike`
506-
The file to save to. The format is determined by the
507-
extension.
508-
509-
overwrite : bool, optional
510-
If `True`, overwrite the file if it exists. Default is
511-
`False`.
512-
"""
513461
p = Path(filename)
514462
if p.exists() and not overwrite:
515463
raise FileExistsError(
@@ -586,8 +534,6 @@ def load_catalog(
586534

587535
self._catalogs[catalog_label].style = catalog_style
588536

589-
load_catalog.__doc__ = ImageViewerInterface.load_catalog.__doc__
590-
591537
def remove_catalog(
592538
self,
593539
catalog_label: str | None = None,
@@ -645,14 +591,10 @@ def get_catalog(
645591

646592
return result
647593

648-
get_catalog.__doc__ = ImageViewerInterface.get_catalog.__doc__
649-
650594
@property
651595
def catalog_names(self) -> tuple[str, ...]:
652596
return tuple(self._user_catalog_labels())
653597

654-
catalog_names.__doc__ = ImageViewerInterface.catalog_names.__doc__
655-
656598
# Methods that modify the view
657599
def set_viewport(
658600
self,
@@ -728,8 +670,6 @@ def set_viewport(
728670
self._images[image_label].center = center
729671
self._images[image_label].fov = fov
730672

731-
set_viewport.__doc__ = ImageViewerInterface.set_viewport.__doc__
732-
733673
def get_viewport(
734674
self,
735675
sky_or_pixel: str | None = None,
@@ -805,5 +745,3 @@ def get_viewport(
805745
fov = viewport.fov
806746

807747
return dict(center=center, fov=fov, image_label=image_label)
808-
809-
get_viewport.__doc__ = ImageViewerInterface.get_viewport.__doc__

src/astro_image_display_api/interface_definition.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@
1515

1616
@runtime_checkable
1717
class ImageViewerInterface(Protocol):
18-
# These are attributes, not methods. The type annotations are there
19-
# to make sure Protocol knows they are attributes. Python does not
20-
# do any checking at all of these types.
21-
image_width: int
22-
image_height: int
23-
2418
# The methods, grouped loosely by purpose
2519

2620
# Method for loading image data

tests/test_astro_image_display_api.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,26 @@ def test_api_test_class_covers_all_attributes_and_only_those_attributes():
7272
"ImageWidgetAPITest does not access these "
7373
f"attributes/methods:\n{missing_attributes_msg}\n"
7474
)
75+
76+
77+
def test_every_method_attribute_has_docstring():
78+
"""
79+
Check that every method and attribute in the protocol has a docstring.
80+
"""
81+
from astro_image_display_api import ImageViewerInterface
82+
83+
all_methods_and_attributes = ImageViewerInterface.__protocol_attrs__
84+
85+
method_attrs_no_docs = []
86+
87+
for method in all_methods_and_attributes:
88+
attr = getattr(ImageViewerInterface, method)
89+
# Make list of methods and attributes that have no docstring
90+
# and assert that list is empty at the end of the test.
91+
if not attr.__doc__:
92+
method_attrs_no_docs.append(method)
93+
94+
assert not method_attrs_no_docs, (
95+
"The following methods and attributes have no docstring:\n\t"
96+
f"{'\n\t'.join(method_attrs_no_docs)}"
97+
)

0 commit comments

Comments
 (0)