def test_regular_iteration(self): filenames = ["01.png", "01.xml", "03.jpg"] fobs = [] for name in filenames: fobs.append(open(Path(self.test_dir.name) / name, 'a')) # Valid paths that should be produced by an iteration over the entire test directory expected_paths = { Path(self.test_dir.name) / "01.png", Path(self.test_dir.name) / "03.jpg" } specimen = Carousel(Path(self.test_dir.name)) # Perform forward iteration until StopIteration # This movement must uncover all the images full_scan_results = set() for _ in range(0, len(expected_paths)): self.assertTrue(specimen.has_next()) full_scan_results.add(specimen.next()) self.assertFalse(specimen.has_next()) self.assertRaises(StopIteration, lambda: specimen.next()) self.assertEqual(expected_paths, full_scan_results) # Perform reverse iteration until StopIteration # This movement must go back to the first item, not returning the current one again reverse_scan_results = set() self.assertTrue(specimen.has_prev()) reverse_scan_results.add(specimen.prev()) self.assertFalse(specimen.has_prev()) self.assertRaises(StopIteration, lambda: specimen.prev()) self.assertTrue(len(reverse_scan_results) > 0) self.assertTrue(expected_paths > reverse_scan_results) # Perform the movement once again and move forward # This movement must move again towards the last item, without returning the current one again forward_scan_results = set() self.assertTrue(specimen.has_next()) forward_scan_results.add(specimen.next()) self.assertFalse(specimen.has_next()) self.assertRaises(StopIteration, lambda: specimen.next()) self.assertTrue(len(forward_scan_results) > 0) self.assertTrue(expected_paths > forward_scan_results) self.assertNotEqual(reverse_scan_results, forward_scan_results) for file in fobs: file.close()
def test_file_deletion_after_creation(self): test_dir_path = Path(self.test_dir.name) first = "first.png" fo = open(test_dir_path / first, 'a') second = "second.png" so = open(test_dir_path / second, 'a') third = "third.png" to = open(test_dir_path / third, 'a') fourth = "fourth.png" yo = open(test_dir_path / fourth, 'a') specimen = Carousel(test_dir_path) fo.close() os.remove(test_dir_path / first) results = set() # Perform a full scan for _ in range(0, 3): results.add(specimen.next()) # 'First' shouldn't be among the results self.assertEqual( { test_dir_path / second, test_dir_path / third, test_dir_path / fourth }, results) self.assertFalse(specimen.has_next()) self.assertRaises(StopIteration, lambda: specimen.next()) so.close() os.remove(test_dir_path / second) to.close() os.remove(test_dir_path / third) # Iteration should now jump straight at the fourth element self.assertTrue(specimen.has_prev()) self.assertEqual(test_dir_path / fourth, specimen.prev()) self.assertFalse(specimen.has_prev()) self.assertRaises(StopIteration, lambda: specimen.prev()) yo.close() os.remove(test_dir_path / fourth) # Carousel should now be stuck self.assertFalse(specimen.has_next()) self.assertRaises(StopIteration, lambda: specimen.next()) self.assertFalse(specimen.has_prev()) self.assertRaises(StopIteration, lambda: specimen.prev())
class View(metaclass=ABCMeta): """ The state of the current view. It wraps the inner carousel and retrieves images and metadata objects, exposing them to the frontend. This class is meant to be subclassed by UI concrete implementations. """ _carousel: Carousel _image_path: Path # Image metadata _id: UUID # TODO: this is inconsistent with the real metadata, as there's a URI there, now; rethink these attributes _filename: str author: Optional[str] universe: Optional[str] characters: Optional[Iterable[str]] tags: Optional[Iterable[str]] def __init__(self, context_dir: Path, filter_factory: Optional[FilterBuilder] = None): """ Instantiate a new view over the image/metadata file pairs at the specified path. The new view will have most of its data uninitialized, since to load them means to start scanning the contents. Therefore, before attempting to retrieve any data, call the `load_next()` method. Optionally, a `FilterBuilder` can be provided as a second argument, which will be used for obtaining image filters. :arg context_dir: path to the directory under which all operations will be performed :arg filter_factory: a filter builder providing filters for the new view :raise FileNotFoundError: when the path points to an invalid location :raise NotADirectoryException: when the path point to a file that is not a directory """ # If given a filter provider, use it to generate a set of filters and apply them on the carousel if filter_factory is not None: self._carousel = Carousel(context_dir, filter_factory.get_all_filters()) else: self._carousel = Carousel(context_dir) def _update_meta(self, meta: ImageMetadata) -> None: self._id = meta.img_id self._filename = meta.file.path.name self.author = meta.author self.universe = meta.universe self.characters = meta.characters self.tags = meta.tags @abstractmethod def has_image_data(self) -> bool: """Tell if the current view contains valid image data.""" pass def has_prev(self) -> bool: """Check whether there is a previous image.""" return self._carousel.has_prev() def has_next(self) -> bool: """Check whether there is a next image.""" return self._carousel.has_next() def load_prev(self) -> None: """ Retrieve the previous image and its metadata. :raise StopIteration: when the start of the collection has already been reached """ self._image_path = self._carousel.prev() self._update_meta(load_meta(self._image_path)) def load_next(self) -> None: """Retrieve the next image and its metadata. :raise StopIteration: when the end of the collection has already been reached """ self._image_path = self._carousel.next() self._update_meta(load_meta(self._image_path)) @property def image_id(self) -> UUID: return self._id @property def filename(self) -> str: return self._filename @abstractmethod def get_image_data(self): """Retrieve the currently-displayed image, if available.""" pass def set_author(self, author: str) -> None: if len(author) == 0: author = None else: author = remove_control_and_redundant_space(author) self.author = author def get_author(self) -> Optional[str]: return self.author def set_universe(self, universe: str) -> None: if len(universe) == 0: universe = None else: universe = remove_control_and_redundant_space(universe) self.universe = universe def get_universe(self) -> Optional[str]: return self.universe def set_characters(self, characters: str) -> None: """Take a comma-separated list of characters in input and use it to update the metadata.""" if len(characters) == 0: new_chars = None else: new_chars = remove_control_and_redundant_space(characters) new_chars = new_chars.split(',') new_chars = [c.strip() for c in new_chars] self.characters = new_chars def get_characters(self) -> Optional[str]: """Return the characters in a comma-separated list, or None if no character metadata is present.""" if self.characters is not None: return ', '.join(self.characters) else: return None def set_tags(self, tags: str) -> None: """Take a comma-separated list of tags in input and use it to update the metadata.""" if len(tags) == 0: new_tags = None else: new_tags = remove_control_and_redundant_space(tags) new_tags = new_tags.split(',') new_tags = [t.strip() for t in new_tags] self.tags = new_tags def get_tags(self) -> Optional[str]: """Return the image tags in a comma-separated list, or None if no tag metadata is present.""" if self.tags is not None: return ', '.join(self.tags) else: return None def write(self) -> None: """ Persist the updated metadata. :raise OSError: when the metadata file couldn't be opened """ meta_obj = ImageMetadata(self._id, URI(self._image_path), self.author, self.universe, self.characters, self.tags) write_meta(meta_obj, self._image_path)