Skip to content

Commit

Permalink
Merge e8e97df into a0e4a95
Browse files Browse the repository at this point in the history
  • Loading branch information
ernestoarbitrio committed Oct 28, 2020
2 parents a0e4a95 + e8e97df commit 12dd365
Show file tree
Hide file tree
Showing 6 changed files with 16 additions and 157 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- echo $PATH
- name: "Python 3.8.3 on macOS"
os: osx
osx_image: xcode12u
osx_image: xcode12.2
language: shell # 'language: python' is an error on Travis CI macOS
before_install:
- brew install openslide
Expand Down
4 changes: 2 additions & 2 deletions src/histolab/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
"""

import math
import ntpath
import os
import pathlib
from functools import lru_cache
from typing import List, Tuple, Union

import ntpath
import numpy as np
import openslide
import PIL
Expand All @@ -37,7 +38,6 @@
from .types import CoordinatePair, Region
from .util import (
lazyproperty,
lru_cache,
polygon_to_mask_array,
region_coordinates,
regions_from_binary_mask,
Expand Down
16 changes: 6 additions & 10 deletions src/histolab/tiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import csv
import os
from abc import abstractmethod
from functools import lru_cache
from typing import List, Tuple

import numpy as np
Expand All @@ -28,12 +29,7 @@
from .slide import Slide
from .tile import Tile
from .types import CoordinatePair
from .util import (
lru_cache,
region_coordinates,
regions_from_binary_mask,
scale_coordinates,
)
from .util import region_coordinates, regions_from_binary_mask, scale_coordinates

try:
from typing import Protocol, runtime_checkable
Expand Down Expand Up @@ -661,8 +657,8 @@ def _save_report(
dict(zip(header, values))
for values in zip(
filenames,
np.array(highest_score_tiles)[:, 0],
np.array(highest_scaled_score_tiles)[:, 0],
np.array(highest_score_tiles, dtype=object)[:, 0],
np.array(highest_scaled_score_tiles, dtype=object)[:, 0],
)
]

Expand All @@ -689,8 +685,8 @@ def _scale_scores(
List[Tuple[float, CoordinatePair]])
Scaled scores
"""
scores_ = np.array(scores)[:, 0]
coords = np.array(scores)[:, 1]
scores_ = np.array(scores, dtype=object)[:, 0]
coords = np.array(scores, dtype=object)[:, 1]
scores_scaled = (scores_ - np.min(scores_)) / (
np.max(scores_) - np.min(scores_)
)
Expand Down
138 changes: 4 additions & 134 deletions src/histolab/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@

import functools
import warnings
from collections import deque
from itertools import filterfalse as ifilterfalse
from typing import Callable, List, Tuple
from typing import Callable, List, Tuple, Any

import numpy as np
import PIL
Expand Down Expand Up @@ -209,14 +207,7 @@ def threshold_to_mask(
return relate(img_arr, threshold)


class Counter(dict):
"""Mapping where default values are zero"""

def __missing__(self, key):
return 0


class lazyproperty:
def lazyproperty(f: Callable[..., Any]):
"""Decorator like @property, but evaluated only on first access.
Like @property, this can only be used to decorate methods having only
Expand All @@ -243,18 +234,15 @@ class lazyproperty:
code from any sequencing considerations; if it's accessed from more than
one location, it's assured it will be ready whenever needed.
Loosely based on: https://stackoverflow.com/a/6849299/1902513.
A lazyproperty is read-only. There is no counterpart to the optional
"setter" (or deleter) behavior of an @property. This is critically
important to maintaining its immutability and idempotence guarantees.
Attempting to assign to a lazyproperty raises AttributeError
unconditionally.
The parameter names in the methods below correspond to this usage
example::
class Obj(object)
class Obj(object):
@lazyproperty
def fget(self):
Expand All @@ -265,122 +253,4 @@ def fget(self):
Not suitable for wrapping a function (as opposed to a method) because it
is not callable.
"""

def __init__(self, fget):
"""*fget* is the decorated method (a "getter" function).
A lazyproperty is read-only, so there is only an *fget* function (a
regular @property can also have an fset and fdel function). This name
was chosen for consistency with Python's `property` class which uses
this name for the corresponding parameter.
"""
# ---maintain a reference to the wrapped getter method
self._fget = fget
# ---adopt fget's __name__, __doc__, and other attributes
functools.update_wrapper(self, fget)

def __get__(self, obj, type=None):
"""Called on each access of 'fget' attribute on class or instance.
*self* is this instance of a lazyproperty descriptor "wrapping" the
property method it decorates (`fget`, nominally).
*obj* is the "host" object instance when the attribute is accessed
from an object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None
when accessed on the class, e.g. `Obj.fget`.
*type* is the class hosting the decorated getter method (`fget`) on
both class and instance attribute access.
"""
if obj is None:
return self

value = obj.__dict__.get(self.__name__)
if value is None:
value = self._fget(obj)
obj.__dict__[self.__name__] = value
return value

def __set__(self, obj, value):
raise AttributeError("can't set attribute")


def lru_cache(maxsize=100): # pragma: no cover
"""Least-recently-used cache decorator.
Arguments to the cached function must be hashable.
Cache performance statistics stored in f.hits and f.misses.
Clear the cache with f.clear().
http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
"""
maxqueue = maxsize * 10

def decorating_function(
user_function, len=len, iter=iter, tuple=tuple, sorted=sorted, KeyError=KeyError
):
cache = {} # mapping of args to results
queue = deque() # order that keys have been used
refcount = Counter() # times each key is in the queue
sentinel = object() # marker for looping around the queue
kwd_mark = object() # separate positional and keyword args

# lookup optimizations (ugly but fast)
queue_append, queue_popleft = queue.append, queue.popleft
queue_appendleft, queue_pop = queue.appendleft, queue.pop

@functools.wraps(user_function)
def wrapper(*args, **kwds):
# cache key records both positional and keyword args
key = args
if kwds:
key += (kwd_mark,) + tuple(sorted(kwds.items()))

# record recent use of this key
queue_append(key)
refcount[key] += 1

# get cache entry or compute if not found
try:
result = cache[key]
wrapper.hits += 1
except KeyError:
result = user_function(*args, **kwds)
cache[key] = result
wrapper.misses += 1

# purge least recently used cache entry
if len(cache) > maxsize:
key = queue_popleft()
refcount[key] -= 1
while refcount[key]:
key = queue_popleft()
refcount[key] -= 1
del cache[key], refcount[key]

# periodically compact the queue by eliminating duplicate keys
# while preserving order of most recent access
if len(queue) > maxqueue:
refcount.clear()
queue_appendleft(sentinel)
for key in ifilterfalse(
refcount.__contains__, iter(queue_pop, sentinel)
):
queue_appendleft(key)
refcount[key] = 1

return result

def clear(): # pragma: no cover
cache.clear()
queue.clear()
refcount.clear()
wrapper.hits = wrapper.misses = 0

wrapper.hits = wrapper.misses = 0
wrapper.clear = clear
return wrapper

return decorating_function


memoize = lru_cache(100)
return property(functools.lru_cache(maxsize=100)(f))
2 changes: 1 addition & 1 deletion tests/unit/test_tiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def it_can_generate_random_coordinates(self, request, tmpdir):

random_tiler._random_tile_coordinates(slide)

_box_mask_thumb.assert_called_once_with(random_tiler, slide)
_box_mask_thumb.assert_called_once_with(slide)
_tile_size.assert_has_calls([call((128, 128))])
_scale_coordinates.assert_called_once_with(
reference_coords=CP(x_ul=0, y_ul=0, x_br=128, y_br=128),
Expand Down
11 changes: 2 additions & 9 deletions tests/unit/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,8 @@ def test_region_coordinates():
class DescribeLazyPropertyDecorator:
"""Tests @lazyproperty decorator class."""

def it_is_a_lazyproperty_object_on_class_access(self, Obj):
assert isinstance(Obj.fget, lazyproperty)

def but_it_adopts_the_name_of_the_decorated_method(self, Obj):
assert Obj.fget.__name__ == "fget"

def and_it_adopts_the_module_of_the_decorated_method(self, Obj):
# ---the module name actually, not a module object
assert Obj.fget.__module__ == __name__
def it_is_a_property_object_on_class_access(self, Obj):
assert isinstance(Obj.fget, property)

def and_it_adopts_the_docstring_of_the_decorated_method(self, Obj):
assert Obj.fget.__doc__ == "Docstring of Obj.fget method definition."
Expand Down

0 comments on commit 12dd365

Please sign in to comment.