Module: env_util

Expand source code
# Copyright (C) 2023-present The Project Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import inspect
import os
from typing import cast
from cl.runtime.primitive.case_util import CaseUtil


class EnvUtil:
    """Helper methods for environment selection."""

    @classmethod
    def is_inside_test(cls, *, test_module_pattern: str | None = None) -> bool:
        """
        Return True if invoked from a test, detection is based on test module pattern.

        Args:
            test_module_pattern: Glob pattern to identify the test module, defaults to 'test_*.py'
        """

        if test_module_pattern is not None:
            # TODO: test_module_pattern custom patterns
            raise RuntimeError("Custom test module patterns are not yet supported.")
        test_module_pattern = "test_"

        stack = inspect.stack()
        for frame_info in stack:
            filename = os.path.basename(frame_info.filename)
            if filename.startswith(test_module_pattern) and filename.endswith(".py"):
                return True
        return False

    @classmethod
    def get_env_dir(
        cls,
        *,
        default_dir: str | None = None,
        test_function_pattern: str | None = None,
    ) -> str:
        """
        Return module_dir/test_module/test_function or module_dir/test_module/test_class/test_method,
        collapsing levels with identical name into one.

        Notes:
            Implemented by searching the stack frame for 'test_' or a custom test function name pattern.

        Args:
            default_dir: When not running inside a test, return this directory if specified, error if not specified
            test_function_pattern: Glob pattern for function or method in stack frame, defaults to 'test_*'
        """
        result = cls._get_test_env_dir_or_name(
            is_name=False,
            test_function_pattern=test_function_pattern,
        )

        # If the end of the frame is reached and no function or method starting from test_ is found,
        # the function was not called from inside a test and default_dir will be returned if specified
        if result is None:
            if default_dir is not None and default_dir != "":
                result = default_dir
            else:
                RuntimeError(
                    f"Not invoked inside a function or method that starts from '{test_function_pattern}' "
                    f"and 'default_dir' is None or empty."
                )
        return result

    @classmethod
    def get_env_name(
        cls,
        *,
        test_function_pattern: str | None = None,
    ) -> str:
        """
        Return module_dir/test_module.test_function or module_dir/test_module.test_class.test_method,
        collapsing levels with identical name into one.

        Notes:
            Implemented by searching the stack frame for 'test_' or a custom test function name pattern.

        Args:
            test_function_pattern: Glob pattern for function or method in stack frame, defaults to 'test_*'
        """

        # Get test environment if inside test
        result = cls._get_test_env_dir_or_name(
            is_name=True,
            test_function_pattern=test_function_pattern,
        )

        # Otherwise assign default name
        if result is None:
            result = "main"
        return result

    @classmethod
    def _get_test_env_dir_or_name(
        cls,
        *,
        is_name: bool,
        test_function_pattern: str | None = None,
    ) -> str | None:
        """
        Return test_module.test_function or test_module.test_class.test_function by searching the stack frame
        for 'test_' or a custom test function name pattern.

        Args:
            is_name: If True, return dot delimited name, otherwise return directory path
            test_function_pattern: Glob pattern for function or method in stack frame, defaults to 'test_*'
        """

        if test_function_pattern is not None:
            # TODO: Support custom patterns
            raise RuntimeError("Custom test function or method name patterns are not yet supported.")
        test_function_pattern = "test_"

        stack = inspect.stack()
        for frame_info in stack:
            if frame_info.function.startswith(test_function_pattern):
                frame_globals = frame_info.frame.f_globals
                module_file = frame_globals["__file__"]
                test_name = frame_info.function
                cls_instance = frame_info.frame.f_locals.get("self", None)
                class_name = cast(type, cls_instance).__class__.__name__ if cls_instance else None

                if module_file.endswith(".py"):
                    module_file_without_ext = module_file.removesuffix(".py")
                else:
                    raise RuntimeError(f"Test module file {module_file} does not end with '.py'.")

                # Determine delimiter based on is_name flag
                delim = "." if is_name else os.sep

                module_dir = os.path.dirname(module_file_without_ext)
                module_name = os.path.basename(module_file_without_ext)
                if class_name is None:
                    # Remove repeated identical tokens to shorten the path
                    if module_name != test_name:
                        result = delim.join((module_name, test_name))
                    else:
                        result = module_name
                else:
                    # Convert class name to snake_case
                    class_name = CaseUtil.pascal_to_snake_case(class_name)

                    # Remove repeated identical tokens to shorten the path
                    if module_name != class_name:
                        if class_name != test_name:
                            result = delim.join((module_name, class_name, test_name))
                        else:
                            result = delim.join((module_name, class_name))
                    else:
                        if module_name != test_name:
                            result = delim.join((module_name, test_name))
                        else:
                            result = module_name
                if not is_name:
                    result = os.path.join(module_dir, result)
                return result

        # Not inside test, return None
        return None

Classes

class EnvUtil

Helper methods for environment selection.

Expand source code
class EnvUtil:
    """Helper methods for environment selection."""

    @classmethod
    def is_inside_test(cls, *, test_module_pattern: str | None = None) -> bool:
        """
        Return True if invoked from a test, detection is based on test module pattern.

        Args:
            test_module_pattern: Glob pattern to identify the test module, defaults to 'test_*.py'
        """

        if test_module_pattern is not None:
            # TODO: test_module_pattern custom patterns
            raise RuntimeError("Custom test module patterns are not yet supported.")
        test_module_pattern = "test_"

        stack = inspect.stack()
        for frame_info in stack:
            filename = os.path.basename(frame_info.filename)
            if filename.startswith(test_module_pattern) and filename.endswith(".py"):
                return True
        return False

    @classmethod
    def get_env_dir(
        cls,
        *,
        default_dir: str | None = None,
        test_function_pattern: str | None = None,
    ) -> str:
        """
        Return module_dir/test_module/test_function or module_dir/test_module/test_class/test_method,
        collapsing levels with identical name into one.

        Notes:
            Implemented by searching the stack frame for 'test_' or a custom test function name pattern.

        Args:
            default_dir: When not running inside a test, return this directory if specified, error if not specified
            test_function_pattern: Glob pattern for function or method in stack frame, defaults to 'test_*'
        """
        result = cls._get_test_env_dir_or_name(
            is_name=False,
            test_function_pattern=test_function_pattern,
        )

        # If the end of the frame is reached and no function or method starting from test_ is found,
        # the function was not called from inside a test and default_dir will be returned if specified
        if result is None:
            if default_dir is not None and default_dir != "":
                result = default_dir
            else:
                RuntimeError(
                    f"Not invoked inside a function or method that starts from '{test_function_pattern}' "
                    f"and 'default_dir' is None or empty."
                )
        return result

    @classmethod
    def get_env_name(
        cls,
        *,
        test_function_pattern: str | None = None,
    ) -> str:
        """
        Return module_dir/test_module.test_function or module_dir/test_module.test_class.test_method,
        collapsing levels with identical name into one.

        Notes:
            Implemented by searching the stack frame for 'test_' or a custom test function name pattern.

        Args:
            test_function_pattern: Glob pattern for function or method in stack frame, defaults to 'test_*'
        """

        # Get test environment if inside test
        result = cls._get_test_env_dir_or_name(
            is_name=True,
            test_function_pattern=test_function_pattern,
        )

        # Otherwise assign default name
        if result is None:
            result = "main"
        return result

    @classmethod
    def _get_test_env_dir_or_name(
        cls,
        *,
        is_name: bool,
        test_function_pattern: str | None = None,
    ) -> str | None:
        """
        Return test_module.test_function or test_module.test_class.test_function by searching the stack frame
        for 'test_' or a custom test function name pattern.

        Args:
            is_name: If True, return dot delimited name, otherwise return directory path
            test_function_pattern: Glob pattern for function or method in stack frame, defaults to 'test_*'
        """

        if test_function_pattern is not None:
            # TODO: Support custom patterns
            raise RuntimeError("Custom test function or method name patterns are not yet supported.")
        test_function_pattern = "test_"

        stack = inspect.stack()
        for frame_info in stack:
            if frame_info.function.startswith(test_function_pattern):
                frame_globals = frame_info.frame.f_globals
                module_file = frame_globals["__file__"]
                test_name = frame_info.function
                cls_instance = frame_info.frame.f_locals.get("self", None)
                class_name = cast(type, cls_instance).__class__.__name__ if cls_instance else None

                if module_file.endswith(".py"):
                    module_file_without_ext = module_file.removesuffix(".py")
                else:
                    raise RuntimeError(f"Test module file {module_file} does not end with '.py'.")

                # Determine delimiter based on is_name flag
                delim = "." if is_name else os.sep

                module_dir = os.path.dirname(module_file_without_ext)
                module_name = os.path.basename(module_file_without_ext)
                if class_name is None:
                    # Remove repeated identical tokens to shorten the path
                    if module_name != test_name:
                        result = delim.join((module_name, test_name))
                    else:
                        result = module_name
                else:
                    # Convert class name to snake_case
                    class_name = CaseUtil.pascal_to_snake_case(class_name)

                    # Remove repeated identical tokens to shorten the path
                    if module_name != class_name:
                        if class_name != test_name:
                            result = delim.join((module_name, class_name, test_name))
                        else:
                            result = delim.join((module_name, class_name))
                    else:
                        if module_name != test_name:
                            result = delim.join((module_name, test_name))
                        else:
                            result = module_name
                if not is_name:
                    result = os.path.join(module_dir, result)
                return result

        # Not inside test, return None
        return None

Static methods

def get_env_dir(*, default_dir: str | None = None, test_function_pattern: str | None = None) -> str

Return module_dir/test_module/test_function or module_dir/test_module/test_class/test_method, collapsing levels with identical name into one.

Notes

Implemented by searching the stack frame for ‘test_’ or a custom test function name pattern.

Args

default_dir
When not running inside a test, return this directory if specified, error if not specified
test_function_pattern
Glob pattern for function or method in stack frame, defaults to ‘test_*’
def get_env_name(*, test_function_pattern: str | None = None) -> str

Return module_dir/test_module.test_function or module_dir/test_module.test_class.test_method, collapsing levels with identical name into one.

Notes

Implemented by searching the stack frame for ‘test_’ or a custom test function name pattern.

Args

test_function_pattern
Glob pattern for function or method in stack frame, defaults to ‘test_*’
def is_inside_test(*, test_module_pattern: str | None = None) -> bool

Return True if invoked from a test, detection is based on test module pattern.

Args

test_module_pattern
Glob pattern to identify the test module, defaults to ‘test_*.py’