certflow.utils.path_utils 源代码

# src/certflow/utils/path_utils.py

"""路径工具模块 - 提供路径常量和辅助函数

本模块用于跨环境(开发环境 / PyInstaller / Nuitka 打包环境)的路径管理。

使用场景:
    - 开发环境:从项目根目录获取 config/、scripts/、src/ 等路径
    - 打包环境:从 EXE 所在目录获取 config/ 路径

环境检测优先级:
    1. 环境变量 CERTFLOW_ROOT(最高优先级)
    2. sys.frozen / sys._MEIPASS2(打包环境自动检测)
    3. 开发环境:向上查找 pyproject.toml 或 config/config.yaml

Example:
    >>> from certflow.utils.path_utils import PROJECT_ROOT, CONFIG_PATH
    >>> print(PROJECT_ROOT)
    /path/to/project
    >>> print(CONFIG_PATH)
    /path/to/project/config/config.yaml
"""

from __future__ import annotations

import os
import sys
from pathlib import Path
from typing import Final  # ← 移到文件顶部

# ============================================================
# 内部辅助函数(不对外暴露)
# ============================================================


def _is_frozen() -> bool:
    """
    检测当前程序是否在打包环境中运行。

    支持 PyInstaller (sys.frozen) 和 Nuitka (sys._MEIPASS2)。

    Returns:
        bool: True 表示在打包环境中,False 表示在开发环境中。
    """
    return getattr(sys, "frozen", False) or hasattr(sys, "_MEIPASS2")


def _get_exe_dir() -> Path:
    """
    获取可执行文件所在目录。

    仅在打包环境下有效,开发环境下返回当前工作目录。

    Returns:
        Path: EXE 所在目录路径(打包环境)或当前工作目录(开发环境)。
    """
    if _is_frozen():
        return Path(sys.executable).parent
    return Path.cwd()


def _find_project_root() -> Path:
    """
    从文件系统向上查找项目根目录。

    从当前模块所在目录向上遍历,查找以下标志文件:
        - pyproject.toml(项目根目录标志)
        - config/config.yaml(项目根目录标志)

    Returns:
        Path: 找到的项目根目录路径,未找到时返回当前工作目录。
    """
    current = Path(__file__).resolve()
    for parent in current.parents:
        if (parent / "pyproject.toml").exists():
            return parent
        if (parent / "config" / "config.yaml").exists():
            return parent
    return Path.cwd()


def _resolve_project_root() -> Path:
    """
    解析项目根目录(带环境变量优先)。

    按以下优先级检测:
        1. 环境变量 CERTFLOW_ROOT
        2. 打包环境:EXE 所在目录
        3. 开发环境:从当前文件向上查找标志文件

    Returns:
        Path: 项目根目录路径。
    """
    if root := os.environ.get("CERTFLOW_ROOT"):
        return Path(root)
    if _is_frozen():
        return Path(sys.executable).parent
    return _find_project_root()


def _resolve_config_dir() -> Path:
    """
    解析配置文件目录。

    打包环境:
        1. 优先返回 EXE 同级 config 目录(推荐结构)
        2. 回退到 EXE 上级 config 目录(兼容旧结构)
    开发环境:
        从项目根目录返回 config/ 目录

    Returns:
        Path: config 目录路径。
    """
    if _is_frozen():
        exe_config = _get_exe_dir() / "config"
        if exe_config.exists():
            return exe_config
        parent_config = _get_exe_dir().parent / "config"
        if parent_config.exists():
            return parent_config
        return exe_config
    return _resolve_project_root() / "config"


def _resolve_build_config() -> Path:
    """
    解析构建配置文件路径。

    仅在开发环境下有效,打包环境返回空 Path。
    搜索顺序:
        1. scripts/builder/build_config.yaml(优先)
        2. 项目根目录/build_config.yaml

    Returns:
        Path: 构建配置文件路径,未找到时返回空 Path。
    """
    if _is_frozen():
        return Path()
    root = _resolve_project_root()
    candidates = [
        root / "scripts" / "builder" / "build_config.yaml",
        root / "build_config.yaml",
    ]
    for p in candidates:
        if p.exists():
            return p
    return Path()


# ============================================================
# 公开常量(可直接导入使用)
# ============================================================

#: 运行时环境标识,True 表示在 PyInstaller/Nuitka 打包环境中运行
IS_FROZEN: bool = _is_frozen()

#: 可执行文件所在目录路径(打包环境)或当前工作目录(开发环境)
EXE_DIR: Path = _get_exe_dir()

#: 项目根目录路径(优先使用环境变量 CERTFLOW_ROOT)
PROJECT_ROOT: Final[Path] = _resolve_project_root()

#: 配置文件目录路径(开发环境:项目根目录/config,打包环境:EXE 同级或上级 config)
CONFIG_DIR: Path = _resolve_config_dir()

#: scripts/ 目录路径(仅在开发环境下有效,打包环境返回空 Path)
SCRIPTS_DIR: Path = PROJECT_ROOT / "scripts" if not IS_FROZEN else Path()

#: scripts/builder/ 目录路径(仅在开发环境下有效,打包环境返回空 Path)
BUILDER_DIR: Path = SCRIPTS_DIR / "builder" if SCRIPTS_DIR else Path()

#: 主配置文件完整路径(config/config.yaml)
CONFIG_PATH: Path = CONFIG_DIR / "config.yaml"

#: 构建配置文件完整路径(仅在开发环境下有效,打包环境返回空 Path)
BUILD_CONFIG_PATH: Path = _resolve_build_config()


# ============================================================
# 公开函数(向后兼容,支持动态场景)
# ============================================================


[文档] def is_frozen() -> bool: """ 检测当前程序是否在打包环境中运行。 支持 PyInstaller (sys.frozen) 和 Nuitka (sys._MEIPASS2)。 Returns: bool: True 表示在打包环境中,False 表示在开发环境中。 Example: >>> if is_frozen(): ... print("Running as packaged executable") ... else: ... print("Running in development environment") """ return IS_FROZEN
[文档] def get_exe_dir() -> Path: """ 获取可执行文件所在目录。 仅在打包环境下有效,开发环境下返回当前工作目录。 Returns: Path: EXE 所在目录路径(打包环境)或当前工作目录(开发环境)。 Example: >>> exe_dir = get_exe_dir() >>> print(exe_dir) /path/to/dist """ return EXE_DIR
[文档] def get_project_root() -> Path: """ 获取项目根目录。 按以下优先级检测: 1. 环境变量 CERTFLOW_ROOT 2. 打包环境:EXE 所在目录 3. 开发环境:从当前文件向上查找 pyproject.toml 或 config/config.yaml Returns: Path: 项目根目录路径。 Example: >>> root = get_project_root() >>> print(root) /path/to/project """ return PROJECT_ROOT
[文档] def get_config_dir() -> Path: """ 获取配置文件目录。 打包环境: 1. 优先返回 EXE 同级 config 目录(推荐结构) 2. 回退到 EXE 上级 config 目录(兼容旧结构) 开发环境: 从项目根目录返回 config/ 目录 Returns: Path: config 目录路径。 Example: >>> config_dir = get_config_dir() >>> print(config_dir) /path/to/project/config """ return CONFIG_DIR
[文档] def get_config_path(filename: str = "config.yaml") -> Path: """ 获取配置文件的完整路径。 Args: filename: 配置文件名,默认为 config.yaml。 Returns: Path: 配置文件的完整路径。 Example: >>> config_path = get_config_path() >>> print(config_path) /path/to/project/config/config.yaml >>> custom_path = get_config_path("custom.yaml") >>> print(custom_path) /path/to/project/config/custom.yaml """ return CONFIG_DIR / filename
[文档] def get_build_config_path() -> Path: """ 获取构建配置文件路径。 仅在开发环境下有效,打包环境返回空 Path。 搜索顺序: 1. scripts/builder/build_config.yaml(优先) 2. 项目根目录/build_config.yaml Returns: Path: 构建配置文件路径,未找到时返回空 Path。 Example: >>> build_config = get_build_config_path() >>> if build_config: ... print(f"Found build config: {build_config}") ... else: ... print("Build config not found") """ return BUILD_CONFIG_PATH
[文档] def get_scripts_dir() -> Path: """ 获取 scripts/ 目录路径。 仅在开发环境下有效,打包环境返回空 Path。 Returns: Path: scripts 目录路径,打包环境返回空 Path。 Example: >>> scripts_dir = get_scripts_dir() >>> if scripts_dir: ... print(f"Scripts directory: {scripts_dir}") ... else: ... print("Running in packaged environment, scripts not available") """ return SCRIPTS_DIR
[文档] def get_builder_dir() -> Path: """ 获取 scripts/builder/ 目录路径。 仅在开发环境下有效,打包环境返回空 Path。 Returns: Path: builder 目录路径,打包环境返回空 Path。 Example: >>> builder_dir = get_builder_dir() >>> if builder_dir: ... print(f"Builder directory: {builder_dir}") ... else: ... print("Running in packaged environment, builder not available") """ return BUILDER_DIR
[文档] def setup_path(project_root: Path | None = None) -> None: """ 将 src/ 目录添加到 sys.path。 仅在开发环境下执行,打包环境自动跳过(源码已打包进 EXE)。 Args: project_root: 项目根目录,为 None 时自动使用 PROJECT_ROOT。 仅在需要手动指定根目录时传入。 Returns: None Example: >>> # 自动检测项目根目录并添加 src/ 到 sys.path >>> setup_path() >>> >>> # 手动指定项目根目录 >>> setup_path(Path("/custom/project/root")) >>> >>> # 在打包环境中,此函数无任何效果 >>> if not is_frozen(): ... setup_path() """ if IS_FROZEN: return root = project_root or PROJECT_ROOT src_path = root / "src" if str(src_path) not in sys.path: sys.path.insert(0, str(src_path))
# ============================================================ # 导出列表 # ============================================================ __all__ = [ # 常量 "IS_FROZEN", "EXE_DIR", "PROJECT_ROOT", "CONFIG_DIR", "SCRIPTS_DIR", "BUILDER_DIR", "CONFIG_PATH", "BUILD_CONFIG_PATH", # 函数 "is_frozen", "get_exe_dir", "get_project_root", "get_config_dir", "get_config_path", "get_build_config_path", "get_scripts_dir", "get_builder_dir", "setup_path", ]