예제 #1
0
def test_bytes2human_raises_expected_errors():
    # Check TypeError(s)
    for invalid in (None, '123', [1, 2, 3], 5 - 3j):
        with pytest.raises(TypeError):
            utils.bytes2human(invalid)

    # Check ValueErrors(s)
    for invalid in (-9.99, -7.5, -1, -0.4):
        with pytest.raises(ValueError):
            utils.bytes2human(invalid)
예제 #2
0
    def test_cleanup_items_removes_and_yields_items_as_necessary(
            self, monkeypatch, tmp_folder):
        """Test ``cleanup_items()`` if it successfully removes every item.
        
        :param monkeypatch: pytest monkey patch fixture
        :type monkeypatch: _pytest.monkeypatch.MonkeyPatch
        :param tmp_folder: Test params and dummy test folder factory fixture pair.
        :type tmp_folder: (dict, dirtools.tests.factory.DummyFolderFactory)
        """
        params, _factory = tmp_folder
        if params['total_items'] == 0:
            return

        # Use another factory folder that is not shared with other tests
        with DummyFolderFactory(params['total_items'],
                                params['total_size'],
                                level=params['level']) as factory:
            scan = Folder(factory.path,
                          params['sort_by'],
                          level=params['level'])
            _humanise_item = Mock(side_effect=scan._humanise_item)
            monkeypatch.setattr(scan, '_humanise_item', _humanise_item)

            # cleanup must return a generator even the same (or greater) total size given
            result = scan.cleanup_items(
                bytes2human(scan.total_size, precision=11))
            assert isinstance(result, types.GeneratorType)
            with pytest.raises(StopIteration):
                next(result)
            _humanise_item.assert_not_called()

            # Get a copy of items and remove one by one
            items = tuple(scan.items(humanise=False))
            _humanise_item.assert_not_called()
            for item in items:
                last_total = scan.total_size
                last_items_len = scan._items_len
                mines_one_byte = bytes2human(scan.total_size - 1, precision=11)
                full_path = os.path.abspath(
                    os.path.join(factory.path, item['name']))
                assert os.path.exists(full_path)

                # attempt to remove single item
                deleted = next(
                    scan.cleanup_items(mines_one_byte, humanise=False))

                _humanise_item.assert_not_called()
                assert deleted == item
                assert scan.total_size == (last_total - deleted['size'])
                assert scan._items_len == last_items_len - 1
                assert not os.path.exists(full_path)
예제 #3
0
    async def _scan(self, sort_by: SortBy) -> None:
        """Async process that has been initialised at the time of object instantiation.

        It triggers bunch of other blocking / non-blocking methods and calculates the final :attr:`.exec_took`.

        Public methods should use :meth:`_await` to wait for this internal scanning to be completed.

        :param sort_by: SortBy enum attribute
        :type sort_by: SortBy
        :rtype: None
        """
        logger.debug("Scanning initialised")

        # those 2 lines are pretty much the BOTTLENECK of entire app.
        async for item in self._iter_items(self._root):
            self._insert_sorted(item, sort_by)

        self.resort(sort_by)
        self.exec_took = round(time.time() - self.exec_took, 3)
        logger.debug(
            "Scanning completed for {len:d} items with {size} of data; took {exec} second(s)."
            .format(
                len=self._items_len,
                size=utils.bytes2human(self._total_size),
                exec=self.exec_took,
            ))
예제 #4
0
 def _humanise_item(cls, item: dict, precision: int) -> dict:
     humanised = item.copy()
     humanised["size"] = utils.bytes2human(item["size"],
                                           precision=precision)
     humanised["atime"] = time.strftime(cls._time_format,
                                        time.gmtime(item["atime"]))
     humanised["mtime"] = time.strftime(cls._time_format,
                                        time.gmtime(item["mtime"]))
     humanised["ctime"] = time.strftime(cls._time_format,
                                        time.gmtime(item["ctime"]))
     return humanised
예제 #5
0
    def cleanup_items(self,
                      max_total_size: str,
                      humanise: bool = True,
                      precision: int = 2) -> Iterator[dict]:
        """Completely remove every item starting from the first in given sorting order until it reaches to ``max_total_size`` parameter.

        Returns empty generator if the given ``max_total_size`` parameter is equal or greater than entire total size. Otherwise removes and yields every deleted item.

        Blocks until the scanning operation has been completed on first access.

        .. warning::

            Result of this method is to DELETE the physical files / folders
            on your disk until the given size matches the actual size
            and there is NO UNDO for this operation.

        :param max_total_size: Human representation of total desired size.
                See: :func:``dirtools.utils.human2bytes``.
        :type max_total_size: str
        :param humanise: Humanise flag (required, no default value).
        :type humanise: bool
        :param precision: The floating precision of the human-readable size format (defaults to 2).
        :type precision: int
        :return: iterator
        """
        self._await()

        # Start deleting in the sorted order
        old_len = self._items_len
        old_size = self._total_size
        max_total_size = utils.human2bytes(max_total_size)
        while self._total_size > max_total_size:
            item = self._items.popleft()
            self._total_size -= item["size"]
            self._items_len -= 1
            item_path = os.path.abspath(os.path.join(self._root, item["name"]))

            # REMOVE THE ITEM PERMANENTLY
            try:
                shutil.rmtree(item_path)
            except NotADirectoryError:
                os.remove(item_path)

            # yield removed item
            yield self._humanise_item(item, precision) if humanise else item

        # Reduced to desired size
        logger.debug(
            "{del_len:d} items with total of {del_size} data has been deleted."
            .format(
                del_len=old_len - self._items_len,
                del_size=utils.bytes2human(old_size - self._total_size),
            ))
예제 #6
0
    def test_invoke_trimming_down_instead_of_listing(self, monkeypatch,
                                                     tmp_folder,
                                                     clone_factory):
        """
        :param monkeypatch: pytest monkey patch fixture
        :type monkeypatch: _pytest.monkeypatch.MonkeyPatch
        :param tmp_folder: Test params and dummy test folder factory fixture pair.
        :type tmp_folder: (dict, dirtools.tests.factory.DummyFolderFactory)
        :param clone_factory: Factory to create `Folder` clone instance.
        :type clone_factory: dirtools.tests.conftest.clone_factory._factory
        :return:
        """
        params, _factory = tmp_folder
        if params['total_items'] == 0:
            return

        trim_down = int(human2bytes(params['total_size']) / 2)
        trim_down_human = bytes2human(trim_down)
        # Use another factory folder that is not shared with other tests
        with DummyFolderFactory(params['total_items'],
                                params['total_size'],
                                level=params['level']) as factory:
            # Create a scanner mock from the factory
            scan = clone_factory(factory.path,
                                 params['sort_by'],
                                 level=params['level'])
            FolderScanMock = Mock(return_value=scan)
            monkeypatch.setattr(self.dirt, 'Folder', FolderScanMock)

            # Give only numeric trim-down value that shouldn't be accepted
            result = self.runner.invoke(self.dirt.invoke_dirtools3, [
                factory.path, '-s',
                str(params['sort_by']).lower(), '--trim-down',
                str(trim_down)
            ])
            assert result.exception is None
            assert '--trim-down value cannot be only numeric' in result.output

            result = self.runner.invoke(self.dirt.invoke_dirtools3, [
                factory.path, '-s',
                str(params['sort_by']).lower(), '--trim-down', trim_down_human
            ])
            assert result.exception is None
            scan.items.assert_not_called()
            scan.cleanup_items.assert_called_once_with(
                trim_down_human,
                humanise=not self._DEFAULTS['nohuman'],
                precision=self._DEFAULTS['precision'])
예제 #7
0
    def test_each_item_has_been_scanned_correctly(self, monkeypatch,
                                                  tmp_folder):
        """Compare every scanned item to the testing factory class.

        :param monkeypatch: pytest monkey patch fixture
        :type monkeypatch: _pytest.monkeypatch.MonkeyPatch
        :param tmp_folder: Test params and dummy test folder factory fixture pair.
        :type tmp_folder: (dict, dirtools.tests.factory.DummyFolderFactory)
        """
        params, factory = tmp_folder
        scan = Folder(factory.path, params['sort_by'], params['level'])
        _humanise_item = Mock(side_effect=scan._humanise_item)
        monkeypatch.setattr(scan, '_humanise_item', _humanise_item)

        factory_items = list(factory.items)
        scanned_human = list(scan.items(humanise=True))
        scanned_nonhuman = list(scan.items(humanise=False))
        _humanise_item.assert_has_calls(
            call(s, precision=2) for s in scanned_nonhuman)
        _humanise_item.reset_mock()

        assert len(factory_items) == len(scanned_nonhuman) == len(
            scan) == scan._items_len

        for item in factory_items:
            raw = next(i for i in scanned_nonhuman
                       if i['name'] == item['name'])
            human = next(i for i in scanned_human if i['name'] == item['name'])

            assert tuple(raw.keys()) == tuple(human.keys()) == tuple(
                item.keys())
            assert raw['size'] == item['size']
            assert human['size'] == bytes2human(item['size'])
            assert raw['depth'] == item['depth']
            assert raw['num_of_files'] == item['num_of_files']
            assert raw['atime'] == item['atime']
            assert raw['mtime'] == item['mtime']
예제 #8
0
    def test_public_methods_should_block_before_processing(
            self, monkeypatch, tmp_folder):
        """Make sure public methods and properties blocks until scanning
        finishes.

        :param monkeypatch: pytest monkey patch fixture
        :type monkeypatch: _pytest.monkeypatch.MonkeyPatch
        :param tmp_folder: Test params and dummy test folder factory fixture pair.
        :type tmp_folder: (dict, dirtools.tests.factory.DummyFolderFactory)
        """
        params, factory = tmp_folder
        # Mock the _await internal function first
        scan = Folder(factory.path, params['sort_by'], params['level'])
        _ = scan.items()

        blocker = Mock()
        monkeypatch.setattr(scan, '_await', blocker)

        # len(scan)
        assert len(scan) == factory.total_items
        blocker.assert_called_once()
        blocker.reset_mock()

        # .total_size
        assert scan.total_size == factory.total_size
        blocker.assert_called_once()
        blocker.reset_mock()

        # .items
        _ = scan.items()
        blocker.assert_called_once()
        blocker.reset_mock()

        # .cleanup_items() calls human2bytes once
        _human2bytes = Mock(side_effect=human2bytes)
        monkeypatch.setattr('dirtools.utils.human2bytes', _human2bytes)
        factory_size_human = bytes2human(factory.total_size, precision=11)
        with pytest.raises(StopIteration):
            next(scan.cleanup_items(factory_size_human))
        _human2bytes.assert_called_once_with(factory_size_human)
        blocker.assert_called_once()
        blocker.reset_mock()

        # So give -1 to make it block actually, run only for >0 sizes
        if factory.total_size > 0:
            sh_rmtree = Mock()
            os_remove = Mock()
            monkeypatch.setattr('shutil.rmtree', sh_rmtree)
            monkeypatch.setattr(os, 'remove', os_remove)
            # Attempt to remove single item
            next(
                scan.cleanup_items(
                    bytes2human(factory.total_size - 1, precision=11)))
            blocker.assert_called_once()
            blocker.reset_mock()
            assert sh_rmtree.called or os_remove.called

        # .resort() called at the end of async loop, so it shouldn't _await
        scan.resort(params['sort_by'])
        blocker.assert_not_called()
        blocker.reset_mock()
예제 #9
0
def test_bytes2human_calculates_correct_bytes_to_human():
    for byte_val, human in VALID_BYTES2HUMAN:
        # Also assert well-formatted human value to bytes
        assert byte_val == utils.human2bytes(human)
        assert utils.bytes2human(byte_val, precision=11) == human
예제 #10
0
def invoke_dirtools3(args):
    """Command line interface to the dirtools package."""
    # Get SortBy enum val
    try:
        sortby = next(s for s in SortBy if args.sortby.upper() == str(s))
    except StopIteration:
        sys.stderr.write("Invalid sort by option: {0}".format(sortby))
        return
    path = args.path
    precision = args.precision
    depth = args.depth
    nohuman = args.nohuman
    trim_down = args.trim_down
    output = args.output
    # Create folder object and start its scanning process
    scan = Folder(path, sortby, level=depth)
    old_size = scan.total_size
    old_items_len = len(scan)

    # Only folder scanning and listing etc
    if trim_down is None:
        items = scan.items(humanise=not nohuman, precision=precision)
    # Do not allow to pass only digit value because it will be interpreted as
    # byte value and probably this was an accident.
    elif trim_down.isdigit():
        sys.stderr.write(
            "--trim-down value cannot be only numeric to prevent accident, {0} given.".format(
                trim_down
            )
        )
        return
    # Folder trimming
    else:
        items = scan.cleanup_items(trim_down, humanise=not nohuman, precision=precision)

    # CSV - custom
    if output.lower() == "csv":
        with io.StringIO() as csv_io:
            writer = csv.writer(csv_io, quoting=csv.QUOTE_NONNUMERIC)
            writer.writerow(TABLE_HEADERS.values())
            writer.writerows(i.values() for i in items)
            rows = csv_io.getvalue().rstrip()
    # Display it with tabulate
    else:
        rows = tabulate(items, TABLE_HEADERS, tablefmt=output, stralign="right")

    sys.stdout.write(rows)

    # Give summary info regarding to its listing
    lit = lambda n, sing, plur: str(n) + (f" {sing}" if n == 1 else f" {plur}")
    if trim_down is None:
        sys.stdout.write(
            "\n{ct} with total of {size} data; took {exec}.".format(
                exec=lit(scan.exec_took, "second", "seconds"),
                size=bytes2human(scan.total_size, precision=precision),
                ct=lit(len(scan), "item", "items"),
            )
        )
    # or cleaning operation
    else:
        del_len = len(scan) - old_items_len
        del_size = bytes2human(old_size - scan.total_size, precision=precision)
        sys.stderr.write(
            "{del_len:d} items with total of {del_size} data has been deleted.".format(
                del_len=del_len, del_size=del_size
            )
        )
        sys.stderr.write(
            "Currently {len} items left with {size} of data; took {exec} second(s).".format(
                len=len(scan),
                size=bytes2human(scan.total_size, precision=precision),
                exec=scan.exec_took,
            )
        )