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",
]