Repository: kedro-org/kedro
File: kedro/io/kedro_data_catalog.py (Python)

Code before:
            SyntaxError: If the provided regex patterns are invalid.

        Example:
        ::

            >>> catalog = KedroDataCatalog()
            >>> # get datasets where the substring 'raw' is present
            >>> raw_data = catalog.filter(name_regex='raw')
            >>> # get datasets of a specific type
            >>> csv_datasets = catalog.filter(type_regex='pandas.excel_dataset.ExcelDataset')
            >>> # get datasets where names start with 'model_' and are of a specific type
            >>> model_datasets = catalog.filter(
            ...     name_regex='^model_',
            ...     type_regex='ModelDataset',
            ... )
            >>> # get datasets where names include 'data' and are of a specific types
            >>> from kedro_datasets.pandas import SQLQueryDataset
            >>> from kedro.io import MemoryDataset
            >>> catalog.filter(name_regex="data", by_type=[MemoryDataset, SQLQueryDataset])
        """
        filtered = self.keys()

        # Apply name filter if specified
        if name_regex:
            pattern = _compile_regex_pattern(name_regex, name_regex_flags)
            filtered = [ds_name for ds_name in filtered if pattern.search(ds_name)]

        # Apply type filters if specified
        by_type_set = set()
        if by_type:
            if not isinstance(by_type, list):
                by_type = [by_type]
            for _type in by_type:
                by_type_set.add(f"{_type.__module__}.{_type.__qualname__}")

        type_str_pattern = None
        if type_regex:
            type_str_pattern = _compile_regex_pattern(type_regex, type_regex_flags)

        if by_type_set or type_str_pattern:
            # Use provided pattern or compile new to match everything
            type_str_pattern = type_str_pattern or _compile_regex_pattern(".*", 0)
            filtered_types = []
            for ds_name in filtered:
                # Retrieve the dataset type
                if ds_name in self._lazy_datasets:
                    str_type = str(self._lazy_datasets[ds_name])
                else:
                    class_type = type(self.__datasets[ds_name])
                    str_type = f"{class_type.__module__}.{class_type.__qualname__}"
                # Match the dataset type against the type_regex and apply by_type filtering

Review comment (performance):
Instead of compiling the regex on behalf of the user, shouldn't we do `re.search(pattern, ds_name)`? That way we don't need to receive `*_regex_flags` and we don't need the compilation step either.

Use case: Imagine compiling the regex is very expensive. But the `.filter()` function is used in a hot loop. We would be compiling a regex many times unnecessarily. Instead, the user can `.compile` the regex outside of the function and pass it. And if they don't care, `re.search` will work with a normal string too.

Thoughts?

Code after:
            >>> # get datasets where names start with 'model_' (precompiled regex)
            >>> model_datasets = catalog.filter(name_regex=re.compile('^model_'))
            >>> # get datasets of a specific type using type_regex
            >>> csv_datasets = catalog.filter(type_regex='pandas.excel_dataset.ExcelDataset')
            >>> # get datasets where names contain 'train' and type matches 'CSV' in the path
            >>> catalog.filter(name_regex="train", type_regex="CSV")
            >>> # get datasets where names include 'data' and are of a specific type
            >>> from kedro_datasets.pandas import SQLQueryDataset
            >>> catalog.filter(name_regex="data", by_type=SQLQueryDataset)
            >>> # get datasets where names include 'data' and are of multiple specific types
            >>> from kedro.io import MemoryDataset
            >>> catalog.filter(name_regex="data", by_type=[MemoryDataset, SQLQueryDataset])
        """
        filtered = self.keys()

        # Apply name filter if specified
        if name_regex:
            filtered = [
                ds_name for ds_name in filtered if re.search(name_regex, ds_name)
            ]

        # Apply type filters if specified
        by_type_set = set()
        if by_type:
            if not isinstance(by_type, list):
                by_type = [by_type]
            for _type in by_type:
                by_type_set.add(f"{_type.__module__}.{_type.__qualname__}")

        if by_type_set or type_regex:
            filtered_types = []
            for ds_name in filtered:
                # Retrieve the dataset type
                if ds_name in self._lazy_datasets:
                    str_type = str(self._lazy_datasets[ds_name])
                else:
                    class_type = type(self.__datasets[ds_name])
                    str_type = f"{class_type.__module__}.{class_type.__qualname__}"
                # Match against type_regex and apply by_type filtering
                if (not type_regex or re.search(type_regex, str_type)) and (
                    not by_type_set or str_type in by_type_set
                ):
                    filtered_types.append(ds_name)

            return filtered_types

        return filtered

    def list(
        self, regex_search: str | None = None, regex_flags: int | re.RegexFlag = 0
    ) -> List[str]:  # noqa: UP006