Beispiel #1
0
    def check_prepared_snapshots(self) -> bool:
        logger = self._logger
        model = self._model
        prepared_snapshots = model.prepared_snapshots
        snapshots_for_addition = prepared_snapshots.snapshots_for_addition
        snapshots_for_removal = prepared_snapshots.snapshots_for_removal

        if has_items(snapshots_for_addition):
            subvolume = model.root_subvolume
            suffix = item_count_suffix(snapshots_for_addition)

            logger.info(
                f"Found {len(snapshots_for_addition)} snapshot{suffix} for addition."
            )

            for snapshot in snapshots_for_addition:
                try:
                    snapshot.validate_static_partition_table(subvolume)
                except SubvolumeError as e:
                    logger.warning(e.formatted_message)

            usable_snapshots_for_addition = model.usable_snapshots_for_addition

            if not has_items(usable_snapshots_for_addition):
                logger.warning(
                    "None of the snapshots for addition are usable!")

        if has_items(snapshots_for_removal):
            suffix = item_count_suffix(snapshots_for_removal)

            logger.info(
                f"Found {len(snapshots_for_removal)} snapshot{suffix} for removal."
            )

        return True
Beispiel #2
0
    def __str__(self) -> str:
        root_location = self._root_location
        root_mount_options = self._root_mount_options
        initrd_options = self._initrd_options
        other_options = self._other_options
        result: list[str] = [constants.EMPTY_STR
                             ] * (sum(
                                 (len(initrd_options), len(other_options))) +
                                  (1 if root_location is not None else 0) +
                                  (1 if root_mount_options is not None else 0))

        if root_location is not None:
            result[root_location[0]] = constants.ROOT_PREFIX + root_location[1]

        if root_mount_options is not None:
            result[root_mount_options[0]] = constants.ROOTFLAGS_PREFIX + str(
                root_mount_options[1])

        if has_items(initrd_options):
            for initrd_option in initrd_options:
                result[initrd_option[
                    0]] = constants.INITRD_PREFIX + initrd_option[1]

        if has_items(other_options):
            for other_option in other_options:
                result[other_option[0]] = other_option[1]

        if has_items(result):
            joined_options = constants.BOOT_OPTION_SEPARATOR.join(result)

            return constants.DOUBLE_QUOTE + joined_options + constants.DOUBLE_QUOTE

        return constants.EMPTY_STR
Beispiel #3
0
    def check_matched_boot_stanzas(self) -> bool:
        logger = self._logger
        model = self._model
        matched_boot_stanzas = model.matched_boot_stanzas

        if not has_items(matched_boot_stanzas):
            logger.error(
                "Could not find a boot stanza matched with the root partition!"
            )

            return False

        suffix = item_count_suffix(matched_boot_stanzas)

        logger.info(f"Found {len(matched_boot_stanzas)} boot "
                    f"stanza{suffix} matched with the root partition.")

        grouping_result = groupby(matched_boot_stanzas)

        for key, grouper in grouping_result:
            grouped_boot_stanzas = list(grouper)

            if not is_singleton(grouped_boot_stanzas):
                volume = key.volume
                loader_path = key.loader_path

                logger.error(
                    f"Found {len(grouped_boot_stanzas)} boot stanzas defined with "
                    f"the same volume ('{volume}') and loader ('{loader_path}') options!"
                )

                return False

        package_config = model.package_config
        boot_stanza_generation = package_config.boot_stanza_generation
        icon_generation_mode = boot_stanza_generation.icon.mode

        for boot_stanza in matched_boot_stanzas:
            try:
                boot_stanza.validate_boot_files_check_result()
            except RefindConfigError as e:
                logger.warning(e.formatted_message)

            boot_stanza.validate_icon_path(icon_generation_mode)

        usable_boot_stanzas = model.usable_boot_stanzas

        if not has_items(usable_boot_stanzas):
            logger.error("None of the matched boot stanzas are usable!")

            return False

        return True
Beispiel #4
0
    def actual_bootable_snapshots(self) -> list[Subvolume]:
        persistence_provider = self._persistence_provider
        prepared_snapshots = self.prepared_snapshots
        usable_snapshots_for_addition = self.usable_snapshots_for_addition
        previous_run_result = persistence_provider.get_previous_run_result()
        snapshots_for_removal = prepared_snapshots.snapshots_for_removal
        bootable_snapshots = set(previous_run_result.bootable_snapshots)

        if has_items(usable_snapshots_for_addition):
            bootable_snapshots |= set(usable_snapshots_for_addition)

        if has_items(snapshots_for_removal):
            bootable_snapshots -= set(snapshots_for_removal)

        return list(bootable_snapshots)
Beispiel #5
0
    def _get_all_boot_file_paths(
        self, ) -> Iterator[tuple[BootFilePathSource, str]]:
        source = BootFilePathSource.BOOT_STANZA
        is_disabled = self.is_disabled

        if not is_disabled:
            loader_path = self.loader_path
            initrd_path = self.initrd_path
            boot_options = self.boot_options

            if not is_none_or_whitespace(loader_path):
                yield (source, none_throws(loader_path))

            if not is_none_or_whitespace(initrd_path):
                yield (source, none_throws(initrd_path))

            yield from ((source, initrd_option)
                        for initrd_option in boot_options.initrd_options)

            sub_menus = self.sub_menus

            if has_items(sub_menus):
                yield from chain.from_iterable(
                    sub_menu.all_boot_file_paths
                    for sub_menu in none_throws(sub_menus))
Beispiel #6
0
    def initialize_block_devices(self) -> None:
        device_command_factory = self._device_command_factory
        physical_device_command = device_command_factory.physical_device_command(
        )
        all_block_devices = list(physical_device_command.get_block_devices())

        if has_items(all_block_devices):
            for block_device in all_block_devices:
                block_device.initialize_partition_tables_using(
                    device_command_factory)

            def block_device_filter(
                filter_func: Callable[[BlockDevice], bool],
            ) -> Optional[BlockDevice]:
                return only(block_device for block_device in all_block_devices
                            if filter_func(block_device))

            filtered_block_devices = BlockDevices(
                block_device_filter(BlockDevice.has_esp),
                block_device_filter(BlockDevice.has_root),
                block_device_filter(BlockDevice.has_boot),
            )
        else:
            filtered_block_devices = BlockDevices.none()

        self._filtered_block_devices = filtered_block_devices
Beispiel #7
0
    def migrate_from_to(
        self,
        source_subvolume: Subvolume,
        destination_subvolume: Subvolume,
        include_paths: bool,
    ) -> None:
        root_mount_options = self.root_mount_options

        if root_mount_options is not None:
            root_mount_options.migrate_from_to(source_subvolume,
                                               destination_subvolume)

        if include_paths:
            initrd_options = self._initrd_options

            if has_items(initrd_options):
                source_logical_path = source_subvolume.logical_path
                destination_logical_path = destination_subvolume.logical_path

                self._initrd_options = [(
                    initrd_option[0],
                    replace_root_part_in(
                        initrd_option[1],
                        source_logical_path,
                        destination_logical_path,
                        (
                            constants.FORWARD_SLASH,
                            constants.BACKSLASH,
                        ),
                    ),
                ) for initrd_option in initrd_options]
Beispiel #8
0
    def _is_snapshot_deleted(self, deleted_directory: Path) -> bool:
        persistence_provider = self._persistence_provider
        previous_run_result = persistence_provider.get_previous_run_result()
        bootable_snapshots = previous_run_result.bootable_snapshots

        if has_items(bootable_snapshots):
            deleted_snapshot = only(
                snapshot for snapshot in bootable_snapshots
                if snapshot.is_located_in(deleted_directory))

            if deleted_snapshot is not None:
                deleted_snapshots = self._deleted_snapshots
                deletion_lock = self._deletion_lock

                with deletion_lock:
                    if deleted_snapshot not in deleted_snapshots:
                        snapshot_manipulation = (
                            self.package_config.snapshot_manipulation)
                        cleanup_exclusion = snapshot_manipulation.cleanup_exclusion

                        deleted_snapshots.add(deleted_snapshot)

                        if deleted_snapshot in cleanup_exclusion:
                            raise SnapshotExcludedFromDeletionError(
                                f"The deleted snapshot ('{deleted_directory}') "
                                "is explicitly excluded from cleanup!")

                        return True

        return False
Beispiel #9
0
    def _get_directories_for_watch(self) -> Iterator[Path]:
        snapshot_searches = self.snapshot_searches

        if has_items(snapshot_searches):
            for snapshot_search in snapshot_searches:
                directory = snapshot_search.directory
                max_depth = snapshot_search.max_depth - 1

                yield from find_all_directories_in(directory, max_depth)
Beispiel #10
0
    def __str__(self) -> str:
        simple_options = self._simple_options
        parameterized_options = self._parameterized_options
        result: list[str] = [constants.EMPTY_STR] * sum(
            (len(simple_options), len(parameterized_options)))

        if has_items(simple_options):
            for simple_option in simple_options:
                result[simple_option[0]] = simple_option[1]

        if has_items(parameterized_options):
            for option_name, option_value in parameterized_options.items():
                result[option_value[
                    0]] = constants.PARAMETERIZED_OPTION_SEPARATOR.join(
                        (option_name, option_value[1]))

        if has_items(result):
            return constants.COLUMN_SEPARATOR.join(result)

        return constants.EMPTY_STR
Beispiel #11
0
    def is_matched_with(self, name: str) -> bool:
        if self.name == name:
            return True
        else:
            dependencies = self.dependencies

            if has_items(dependencies):
                return any(
                    dependency.is_matched_with(name)
                    for dependency in none_throws(dependencies))

        return False
    def _map_to_includes(
        config_option_contexts: list[RefindConfigParser.Config_optionContext],
    ) -> Iterator[str]:
        if has_items(config_option_contexts):
            include_visitor = IncludeVisitor()

            for config_option_context in config_option_contexts:
                include_context = config_option_context.include()

                if include_context is not None:
                    yield checked_cast(str,
                                       include_context.accept(include_visitor))
    def _map_to_boot_stanzas(
        config_option_contexts: list[RefindConfigParser.Config_optionContext],
    ) -> Iterator[BootStanza]:
        if has_items(config_option_contexts):
            boot_stanza_visitor = BootStanzaVisitor()

            for config_option_context in config_option_contexts:
                boot_stanza_context = config_option_context.boot_stanza()

                if boot_stanza_context is not None:
                    yield checked_cast(
                        BootStanza,
                        boot_stanza_context.accept(boot_stanza_visitor))
Beispiel #14
0
    def _process_snapshots(self) -> list[Subvolume]:
        subvolume_command_factory = self._subvolume_command_factory
        actual_bootable_snapshots = self.actual_bootable_snapshots
        usable_snapshots_for_addition = self.usable_snapshots_for_addition
        subvolume_command = subvolume_command_factory.subvolume_command()

        if has_items(usable_snapshots_for_addition):
            device_command_factory = self._device_command_factory
            subvolume = self.root_subvolume
            boot_stanzas_with_snapshots = self.boot_stanzas_with_snapshots
            all_usable_snapshots = set(
                chain.from_iterable(
                    self.usable_boot_stanzas_with_snapshots.values()))

            for addition in usable_snapshots_for_addition:
                if addition in all_usable_snapshots:
                    bootable_snapshot = subvolume_command.get_bootable_snapshot_from(
                        addition)

                    bootable_snapshot.modify_partition_table_using(
                        subvolume, device_command_factory)
                    replace_item_in(actual_bootable_snapshots, addition,
                                    bootable_snapshot)

                    for item in boot_stanzas_with_snapshots:
                        item.replace_matched_snapshot(addition,
                                                      bootable_snapshot)
                else:
                    actual_bootable_snapshots.remove(addition)

        prepared_snapshots = self.prepared_snapshots
        snapshots_for_removal = prepared_snapshots.snapshots_for_removal

        if has_items(snapshots_for_removal):
            for removal in snapshots_for_removal:
                subvolume_command.delete_snapshot(removal)

        return actual_bootable_snapshots
Beispiel #15
0
    def is_valid_fstab_entry(value: Optional[str]) -> bool:
        if is_none_or_whitespace(value):
            return False

        fstab_line = none_throws(value)
        comment_pattern = re.compile(r"^\s*#.*")

        if not comment_pattern.match(fstab_line):
            split_fstab_entry = fstab_line.split()

            return has_items(split_fstab_entry) and len(
                split_fstab_entry) == len(FstabColumn)

        return False
Beispiel #16
0
    def is_matched_with(self, block_device: BlockDevice) -> bool:
        if self.can_be_used_for_bootable_snapshot():
            boot_options = self.boot_options

            if boot_options.is_matched_with(block_device):
                return True
            else:
                sub_menus = self.sub_menus

                if has_items(sub_menus):
                    return any(
                        sub_menu.is_matched_with(block_device)
                        for sub_menu in none_throws(sub_menus))

        return False
Beispiel #17
0
    def check_boot_stanzas_with_snapshots(self) -> bool:
        logger = self._logger
        model = self._model
        boot_stanzas_with_snapshots = model.boot_stanzas_with_snapshots

        for item in boot_stanzas_with_snapshots:
            if item.has_unmatched_snapshots():
                unmatched_snapshots = item.unmatched_snapshots

                for snapshot in unmatched_snapshots:
                    try:
                        snapshot.validate_boot_files_check_result()
                    except SubvolumeError as e:
                        logger.warning(e.formatted_message)

            if not item.has_matched_snapshots():
                boot_stanza = item.boot_stanza
                normalized_name = boot_stanza.normalized_name

                logger.warning("None of the prepared snapshots are matched "
                               f"with the '{normalized_name}' boot stanza!")

        usable_boot_stanzas_with_snapshots = model.usable_boot_stanzas_with_snapshots

        if not has_items(usable_boot_stanzas_with_snapshots):
            logger.error("None of the matched boot stanzas can be "
                         "combined with any of the prepared snapshots!")

            return False

        package_config = model.package_config

        if package_config.exit_if_no_changes_are_detected:
            refind_config = model.refind_config
            prepared_snapshots = model.prepared_snapshots
            has_changes = (package_config.is_of_initialization_type(
                ConfigInitializationType.PARSED)
                           or refind_config.is_of_initialization_type(
                               ConfigInitializationType.PARSED)
                           or prepared_snapshots.has_changes())

            if not has_changes:
                raise NoChangesDetectedError(
                    "No changes were detected, aborting...")

        return True
Beispiel #18
0
    def __init__(
        self,
        logger_factory: BaseLoggerFactory,
        model: Model,
        states: States,
    ):
        self._logger = logger_factory.logger(__name__)

        if not has_items(states) or is_singleton(states):
            raise ValueError(
                "The 'states' collection must be initialized and contain at least two items!"
            )

        initial = checked_cast(State, first(states))
        expected_initial_name = StateNames.INITIAL.value

        if initial.name != expected_initial_name:
            raise ValueError("The first item of the 'states' collection must "
                             f"be a state named '{expected_initial_name}'!")

        final = checked_cast(State, last(states))
        expected_final_name = StateNames.FINAL.value

        if final.name != expected_final_name:
            raise ValueError("The last item of the 'states' collection must "
                             f"be a state named '{expected_final_name}'!")

        conditions = model.conditions

        super().__init__(
            model=model,
            states=list(states),
            initial=initial,
            auto_transitions=False,
            name=__name__,
        )
        self.add_ordered_transitions(
            loop=False,
            conditions=conditions,
        )

        self._initial_state = initial
Beispiel #19
0
    def __init__(
        self,
        boot_stanza: BootStanza,
        block_device: BlockDevice,
        bootable_snapshots: Collection[Subvolume],
    ) -> None:
        assert has_items(
            bootable_snapshots
        ), "Parameter 'bootable_snapshots' must contain at least one item!"

        if not boot_stanza.is_matched_with(block_device):
            raise RefindConfigError(
                "Boot stanza is not matched with the partition!")

        root_partition = none_throws(block_device.root)
        filesystem = none_throws(root_partition.filesystem)
        source_subvolume = none_throws(filesystem.subvolume)

        self._boot_stanza = boot_stanza
        self._source_subvolume = source_subvolume
        self._bootable_snapshots = list(bootable_snapshots)
    def save_config(self, config: RefindConfig) -> None:
        logger = self._logger
        persistence_provider = self._persistence_provider
        boot_stanzas = config.boot_stanzas

        if has_items(boot_stanzas):
            config_file_path = config.file_path
            destination_directory = config_file_path.parent
            refind_directory = destination_directory.parent

            if not destination_directory.exists():
                logger.info(
                    "Creating the "
                    f"'{destination_directory.relative_to(refind_directory)}' "
                    "destination directory.")

                destination_directory.mkdir()

            try:
                logger.info(
                    f"Writing to the '{config_file_path.relative_to(refind_directory)}' file."
                )

                with config_file_path.open("w") as config_file:
                    lines_for_writing: list[str] = []

                    lines_for_writing.append(
                        constants.NEWLINE.join(
                            str(boot_stanza)
                            for boot_stanza in none_throws(boot_stanzas)))
                    lines_for_writing.append(constants.NEWLINE)
                    config_file.writelines(lines_for_writing)
            except OSError as e:
                logger.exception("Path.open('w') call failed!")
                raise RefindConfigError(
                    f"Could not write to the '{config_file_path.name}' file!"
                ) from e

            config.refresh_file_stat()
            persistence_provider.save_refind_config(config)
Beispiel #21
0
    def initialize_prepared_snapshots(self) -> None:
        persistence_provider = self._persistence_provider
        snapshot_manipulation = self.package_config.snapshot_manipulation
        subvolume = self.root_subvolume
        previous_run_result = persistence_provider.get_previous_run_result()
        selected_snapshots = none_throws(
            subvolume.select_snapshots(snapshot_manipulation.selection_count))
        destination_directory = snapshot_manipulation.destination_directory
        snapshots_union = snapshot_manipulation.cleanup_exclusion.union(
            selected_snapshots)

        if previous_run_result.has_bootable_snapshots():
            bootable_snapshots = previous_run_result.bootable_snapshots
            snapshots_for_addition = [
                snapshot for snapshot in selected_snapshots
                if snapshot.can_be_added(bootable_snapshots)
            ]
            snapshots_for_removal = [
                snapshot
                for snapshot in bootable_snapshots if snapshot.can_be_removed(
                    destination_directory, snapshots_union)
            ]
        else:
            destination_snapshots = self.destination_snapshots
            snapshots_for_addition = selected_snapshots
            snapshots_for_removal = [
                snapshot for snapshot in destination_snapshots.difference(
                    snapshots_union) if snapshot.can_be_removed(
                        destination_directory, selected_snapshots)
            ]

        if has_items(snapshots_for_addition):
            device_command_factory = self._device_command_factory

            for snapshot in snapshots_for_addition:
                snapshot.initialize_partition_table_using(
                    device_command_factory)

        self._prepared_snapshots = PreparedSnapshots(snapshots_for_addition,
                                                     snapshots_for_removal)
    def get_config(self, partition: Partition) -> RefindConfig:
        logger = self._logger
        config_file_path = FileRefindConfigProvider.all_config_file_paths.get(
            partition)
        should_begin_search = config_file_path is None or not config_file_path.exists(
        )

        if should_begin_search:
            package_config_provider = self._package_config_provider
            package_config = package_config_provider.get_config()
            boot_stanza_generation = package_config.boot_stanza_generation
            refind_config_file = boot_stanza_generation.refind_config

            logger.info(
                f"Searching for the '{refind_config_file}' file on '{partition.name}'."
            )

            refind_config_search_result = partition.search_paths_for(
                refind_config_file)

            if not has_items(refind_config_search_result):
                raise RefindConfigError(
                    f"Could not find the '{refind_config_file}' file!")

            if not is_singleton(refind_config_search_result):
                raise RefindConfigError(
                    f"Found multiple '{refind_config_file}' files (at most one is expected)!"
                )

            config_file_path = one(
                none_throws(refind_config_search_result)).resolve()

            FileRefindConfigProvider.all_config_file_paths[
                partition] = config_file_path

        return self._read_config_from(none_throws(config_file_path))
Beispiel #23
0
 def has_unmatched_snapshots(self) -> bool:
     return has_items(self.unmatched_snapshots)
Beispiel #24
0
 def has_sub_menus(self) -> bool:
     return has_items(self.sub_menus)
Beispiel #25
0
 def has_changes(self) -> bool:
     return has_items(self.snapshots_for_addition) or has_items(
         self.snapshots_for_removal)
    def append_to_config(self, config: RefindConfig) -> None:
        logger = self._logger
        persistence_provider = self._persistence_provider
        config_file_path = config.file_path
        actual_config = persistence_provider.get_refind_config(
            config_file_path)

        if actual_config is not None:
            new_included_configs = config.get_included_configs_difference_from(
                actual_config)

            if has_items(new_included_configs):
                included_configs_for_appending = none_throws(
                    new_included_configs)

                try:
                    with config_file_path.open("r") as config_file:
                        all_lines = config_file.readlines()
                        last_line = last(all_lines)
                except OSError as e:
                    logger.exception("Path.open('r') call failed!")
                    raise RefindConfigError(
                        f"Could not read from the '{config_file_path}' file!"
                    ) from e
                else:
                    include_option = RefindOption.INCLUDE.value
                    suffix = item_count_suffix(included_configs_for_appending)

                    try:
                        logger.info(
                            f"Appending {len(included_configs_for_appending)} '{include_option}' "
                            f"directive{suffix} to the '{config_file_path.name}' file."
                        )

                        with config_file_path.open("a") as config_file:
                            lines_for_appending: list[str] = []
                            should_prepend_newline = False

                            if not is_none_or_whitespace(last_line):
                                include_option_pattern = re.compile(
                                    constants.INCLUDE_OPTION_PATTERN,
                                    re.DOTALL)

                                should_prepend_newline = (
                                    not include_option_pattern.match(last_line)
                                )

                            if should_prepend_newline:
                                lines_for_appending.append(constants.NEWLINE)

                            destination_directory = config_file_path.parent

                            for included_config in included_configs_for_appending:
                                included_config_relative_file_path = (
                                    included_config.file_path.relative_to(
                                        destination_directory))

                                lines_for_appending.append(
                                    f"{include_option} {included_config_relative_file_path}"
                                    f"{constants.NEWLINE}")

                            config_file.writelines(lines_for_appending)
                    except OSError as e:
                        logger.exception("Path.open('a') call failed!")
                        raise RefindConfigError(
                            f"Could not append to the '{config_file_path.name}' file!"
                        ) from e

                config.refresh_file_stat()

            persistence_provider.save_refind_config(config)
Beispiel #27
0
    def __str__(self) -> str:
        result: list[str] = []
        main_indent = constants.EMPTY_STR
        option_indent = constants.TAB

        name = self.name
        result.append(
            f"{main_indent}{RefindOption.MENU_ENTRY.value} {name} {{")

        icon_path = self.icon_path

        if not is_none_or_whitespace(icon_path):
            result.append(
                f"{option_indent}{RefindOption.ICON.value} {icon_path}")

        volume = self.volume

        if not is_none_or_whitespace(volume):
            result.append(
                f"{option_indent}{RefindOption.VOLUME.value} {volume}")

        loader_path = self.loader_path

        if not is_none_or_whitespace(loader_path):
            result.append(
                f"{option_indent}{RefindOption.LOADER.value} {loader_path}")

        initrd_path = self.initrd_path

        if not is_none_or_whitespace(initrd_path):
            result.append(
                f"{option_indent}{RefindOption.INITRD.value} {initrd_path}")

        os_type = self.os_type

        if not is_none_or_whitespace(os_type):
            result.append(
                f"{option_indent}{RefindOption.OS_TYPE.value} {os_type}")

        graphics = self.graphics

        if graphics is not None:
            graphics_parameter = (GraphicsParameter.ON
                                  if graphics else GraphicsParameter.OFF)
            result.append(
                f"{option_indent}{RefindOption.GRAPHICS.value} {graphics_parameter.value}"
            )

        boot_options_str = str(self.boot_options)

        if not is_none_or_whitespace(boot_options_str):
            result.append(
                f"{option_indent}{RefindOption.BOOT_OPTIONS.value} {boot_options_str}"
            )

        firmware_bootnum = self.firmware_bootnum

        if firmware_bootnum is not None:
            result.append(
                f"{option_indent}{RefindOption.FIRMWARE_BOOTNUM.value} {firmware_bootnum:04x}"
            )

        sub_menus = self.sub_menus

        if has_items(sub_menus):
            result.extend(str(sub_menu) for sub_menu in none_throws(sub_menus))

        is_disabled = self.is_disabled

        if is_disabled:
            result.append(f"{option_indent}{RefindOption.DISABLED.value}")

        result.append(f"{main_indent}}}")

        return constants.NEWLINE.join(result)
Beispiel #28
0
 def has_snapshots(self) -> bool:
     return has_items(self.snapshots)
Beispiel #29
0
 def has_bootable_snapshots(self) -> bool:
     return has_items(self.bootable_snapshots)
Beispiel #30
0
 def has_partitions(self) -> bool:
     return has_items(self.partitions)