From 8178a543a25e70050c3a913155bac277c192cec3 Mon Sep 17 00:00:00 2001 From: KAAANG <79990647+SAKURA-CAT@users.noreply.github.com> Date: Sat, 15 Jun 2024 03:39:00 +0800 Subject: [PATCH] init repo (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 初始化项目 --- .github/ISSUE_TEMPLATE/ask for question.md | 12 + .github/ISSUE_TEMPLATE/bug report.md | 33 +++ .github/ISSUE_TEMPLATE/feature advice.md | 18 ++ .github/ISSUE_TEMPLATE/feature request.md | 18 ++ .github/PULL_REQUEST_TEMPLATE.md | 17 ++ .github/workflows/publish-to-pypi.yml | 43 +++ .github/workflows/test-when-pr.yml | 31 +++ .gitignore | 7 + build_pypi.py | 7 + docs/Home.md | 1 + docs/unit-test.md | 1 + pyproject.toml | 59 ++++ requirements-dev.txt | 2 + swankit/__init__.py | 13 + swankit/callback/__init__.py | 9 + swankit/env.py | 64 +++++ swankit/error.py | 20 ++ swankit/log/__init__.py | 13 + swankit/log/log.py | 199 +++++++++++++ swankit/log/utils.py | 310 +++++++++++++++++++++ test/unit/conftest.py | 27 ++ test/unit/log/test_font.py | 9 + test/unit/log/test_log.py | 69 +++++ test/unit/test_env.py | 76 +++++ tutils/__init__.py | 10 + tutils/path.py | 15 + 26 files changed, 1083 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/ask for question.md create mode 100644 .github/ISSUE_TEMPLATE/bug report.md create mode 100644 .github/ISSUE_TEMPLATE/feature advice.md create mode 100644 .github/ISSUE_TEMPLATE/feature request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/publish-to-pypi.yml create mode 100644 .github/workflows/test-when-pr.yml create mode 100644 .gitignore create mode 100644 build_pypi.py create mode 100644 docs/Home.md create mode 100644 docs/unit-test.md create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 swankit/__init__.py create mode 100644 swankit/callback/__init__.py create mode 100644 swankit/env.py create mode 100644 swankit/error.py create mode 100644 swankit/log/__init__.py create mode 100644 swankit/log/log.py create mode 100644 swankit/log/utils.py create mode 100644 test/unit/conftest.py create mode 100644 test/unit/log/test_font.py create mode 100644 test/unit/log/test_log.py create mode 100644 test/unit/test_env.py create mode 100644 tutils/__init__.py create mode 100644 tutils/path.py diff --git a/.github/ISSUE_TEMPLATE/ask for question.md b/.github/ISSUE_TEMPLATE/ask for question.md new file mode 100644 index 0000000..23a466f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask for question.md @@ -0,0 +1,12 @@ +--- +name: 🙋 Ask for question +about: Look for some help or ask question +title: '[QUESTION] ' +labels: question +assignees: '' +--- + +### 🤔 Question description [Please make everyone to understand it] + +### 🧑‍💻 Expected result + diff --git a/.github/ISSUE_TEMPLATE/bug report.md b/.github/ISSUE_TEMPLATE/bug report.md new file mode 100644 index 0000000..338e483 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug report.md @@ -0,0 +1,33 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +title: '[BUG] ' +labels: bug +assignees: '' +--- + + + +## 🐛 Bug description [Please make everyone to understand it] + +Describe the main elements of the bug + +## 🧑‍💻 Step to reproduce + +1. Go to '....' + +2. Click '....' + +3. Something happened '....' + +## 👾 Expected result + +Write down the results you expect + +## 🚑 Any additional [like screenshots] + +- **SwanLab Version**: + +- **Swanboard Version**: + +- **Platform**: diff --git a/.github/ISSUE_TEMPLATE/feature advice.md b/.github/ISSUE_TEMPLATE/feature advice.md new file mode 100644 index 0000000..ffa0126 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature advice.md @@ -0,0 +1,18 @@ +--- +name: 🏠 Feature advice +about: Suggest an idea for this project +title: '[ADVICE] ' +labels: enhancement +assignees: '' +--- + +## 🤪 Features description [Please make everyone to understand it] + +Briefly describe this feature + +## 👍 What problem does this feature solve + +## 👾 What does the proposed API look like + +## 🚑 Any additional [like screenshots] + diff --git a/.github/ISSUE_TEMPLATE/feature request.md b/.github/ISSUE_TEMPLATE/feature request.md new file mode 100644 index 0000000..3151cbc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature request.md @@ -0,0 +1,18 @@ +--- +name: 💪 Feature request +about: The iterative goal of this project +title: '[REQUEST] ' +labels: enhancement +assignees: '' +--- + +## 🤩 Features description [Please make everyone to understand it] + +Briefly describe this feature + +## 👍 What problem does this feature solve + +## 👾 What does the proposed API look like + +## 🚑 Any additional [like screenshots] + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ec924af --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +## Description + +Please include a concise summary, in clear English, of the changes in this pull request. If it closes an issue, please +mention it here. + +Closes: #(issue) + +## 🎯 PRs Should Target Issues + +Before your creating a PR, please check to see if there +is [an existing issue](https://github.com/SwanHubX/SwanLab-Toolkit/issues) +for this change. If not, please create an issue before you create this PR, unless the fix is very small. + +Not adhering to this guideline will result in the PR being closed. + + + diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..27e8e65 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,43 @@ +name: Publish to PyPI + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set Python + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install Dependencies + run: | + pip install -r requirements-dev.txt + pip install build + pip install twine + + - name: Build and Publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + + run: | + python -m build + python -m twine upload dist/* + + # - run: cp dist/*.whl . + # - name: Release + # uses: softprops/action-gh-release@v1 + # if: startsWith(github.ref, 'refs/tags/') + # with: + # body: ${{ github.event.head_commit.message }} + # files: | + # *.whl diff --git a/.github/workflows/test-when-pr.yml b/.github/workflows/test-when-pr.yml new file mode 100644 index 0000000..b59a70b --- /dev/null +++ b/.github/workflows/test-when-pr.yml @@ -0,0 +1,31 @@ +name: Test When PR + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Dependencies + run: | + pip install -r requirements-dev.txt + + - name: Test + run: | + export PYTHONPATH=$PYTHONPATH:. + pytest test/unit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3eddab3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +venv/ +test/temp/ +dist/ +__pycache__ +.pytest_cache/ +.DS_Store \ No newline at end of file diff --git a/build_pypi.py b/build_pypi.py new file mode 100644 index 0000000..56d141a --- /dev/null +++ b/build_pypi.py @@ -0,0 +1,7 @@ +import subprocess +import shutil +import os + +if os.path.exists("dist"): + shutil.rmtree("dist") +subprocess.run("python -m build", shell=True) diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 0000000..c7814c6 --- /dev/null +++ b/docs/Home.md @@ -0,0 +1 @@ +Welcome to the SwanLab-Toolkit wiki! diff --git a/docs/unit-test.md b/docs/unit-test.md new file mode 100644 index 0000000..5884e2a --- /dev/null +++ b/docs/unit-test.md @@ -0,0 +1 @@ +# 单元测试 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d6d6461 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = [ + "hatchling", + "hatch-requirements-txt", + "hatch-fancy-pypi-readme>=22.5.0", +] +build-backend = "hatchling.build" + + +[project] +name = "swankit" +version = "0.1.0b1" +dynamic = ["readme"] +description = "Base toolkit for SwanLab" +license = "Apache-2.0" +requires-python = ">=3.8" +authors = [ + { name = "Cunyue", email = "team@swanhub.co" }, +] + +[project.urls] +"Homepage" = "https://swanlab.cn" +"Source" = "https://github.com/SwanHubX/SwanLab-Toolkit" +"Bug Reports" = "https://github.com/SwanHubX/SwanLab-Toolkit/issues" + + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [{ path = "README.md" }] + + +[tool.hatch.build] +artifacts = [ + "*.pyi", +] + + +[tool.hatch.build.targets.sdist] +include = [ + "/swankit", + "/requirements-dev.txt", # 用于测试的依赖 + "/test", # 包含一些测试脚本,确保测试成功 + "/README.md", # 包含readme,因为是动态设置的 +] + +[tool.hatch.build.targets.wheel] +packages = ["swankit"] + +[tool.pyright] +include = ["swankit/**/*.py"] +exclude = [] + +[tool.ruff] +target-version = "py37" +extend-select = ["B", "C", "I", "N", "SIM", "UP"] +ignore = [] + +[tool.black] +line-length = 120 diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..9fcbf4f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest +nanoid \ No newline at end of file diff --git a/swankit/__init__.py b/swankit/__init__.py new file mode 100644 index 0000000..7b82c2c --- /dev/null +++ b/swankit/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 22:54 +@File: __init__.py.py +@IDE: pycharm +@Description: + swankit —— SwanLab Toolkit + 为 SwanLab 提供的工具包,包含了一些常用的工具函数、类、模块等 + 为了语意方便,应该通过swankit的不同模块调用各个功能函数 +""" + +__all__ = ["env", "error", "callback", "log"] diff --git a/swankit/callback/__init__.py b/swankit/callback/__init__.py new file mode 100644 index 0000000..8f0fd82 --- /dev/null +++ b/swankit/callback/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 23:06 +@File: __init__.py.py +@IDE: pycharm +@Description: + 回调类,规定回调函数的接口规范。 +""" diff --git a/swankit/env.py b/swankit/env.py new file mode 100644 index 0000000..008ad7d --- /dev/null +++ b/swankit/env.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 23:07 +@File: env.py +@IDE: pycharm +@Description: + 规定可以被复用的、全局的环境变量,原则上,这里的环境变量和工具应该影响到所有基于 SwanLab-Toolkit 开发的项目 +""" +import os +import sys +from .error import UnKnownSystemError +from enum import Enum + + +class SwanLabEnv(Enum): + """ + 环境变量Key,枚举类 + """ + + SAVE_FOLDER = "SWANLAB_SAVE_FOLDER" + """ + swanlab全局文件夹保存的路径,默认为用户主目录下的.swanlab文件夹 + """ + + +# ---------------------------------- 获取环境变量/配置的值 ---------------------------------- + + +def is_windows() -> bool: + """判断当前操作系统是否是windows还是类unix系统,主要是路径分隔上的差别 + 此外的系统会报错为 UnKnownSystemError + :raise UnKnownSystemError: 未知系统错误,此时swanlab运行在未知系统上,这个系统不是windows或者类unix系统 + :return: True表示是windows系统,False表示是类unix系统 + """ + if sys.platform.startswith("win"): + return True + elif sys.platform.startswith("linux") or sys.platform.startswith("darwin"): + return False + raise UnKnownSystemError("Unknown system, not windows or unix-like system") + + +def get_swanlab_save_folder() -> str: + """ + 获取存放swanlab全局文件的文件夹路径,如果不存在就创建 + 此函数对应为SWANLAB_SAVE_FOLDER全局变量,如果没有设置,默认为用户主目录下的.swanlab文件夹 + 执行此函数时,如果文件夹不存在,自动创建,但是出于安全考虑,不会自动创建父文件夹 + :raises + :raise FileNotFoundError: folder的父目录不存在 + :raise NotADirectoryError: folder不是一个文件夹 + :return: swanlab全局文件夹保存的路径,返回处理后的绝对路径 + """ + folder = os.getenv(SwanLabEnv.SAVE_FOLDER.value) + if folder is None: + folder = os.path.join(os.path.expanduser("~"), ".swanlab") + folder = os.path.abspath(folder) + if not os.path.exists(os.path.dirname(folder)): + raise FileNotFoundError(f"{os.path.dirname(folder)} not found") + if not os.path.exists(folder): + # 只创建当前文件夹,不创建父文件夹 + os.mkdir(folder) + if not os.path.isdir(folder): + raise NotADirectoryError(f"{folder} is not a directory") + return folder diff --git a/swankit/error.py b/swankit/error.py new file mode 100644 index 0000000..c81ba79 --- /dev/null +++ b/swankit/error.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/15 01:16 +@File: error.py +@IDE: pycharm +@Description: + swanlab内部错误,方便捕获 +""" + + +class SwanLabError(Exception): + """SwanLab内部错误基类""" + pass + + +class UnKnownSystemError(Exception): + """未知系统错误,此时swanlab运行在未知系统上,这个系统不是windows或者类unix系统 + """ + pass diff --git a/swankit/log/__init__.py b/swankit/log/__init__.py new file mode 100644 index 0000000..d7f0154 --- /dev/null +++ b/swankit/log/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 23:12 +@File: __init__.py +@IDE: pycharm +@Description: + 日志模块,提供swanlab标准日志记录功能 +""" +from .utils import FONT +from .log import SwanKitLog, Levels + +__all__ = ["FONT", "SwanKitLog", "Levels"] diff --git a/swankit/log/log.py b/swankit/log/log.py new file mode 100644 index 0000000..695b328 --- /dev/null +++ b/swankit/log/log.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 23:19 +@File: log.py +@IDE: pycharm +@Description: + 日志模块,封装logging模块,提供swanlab标准日志记录功能 +""" +import logging +from .utils import FONT +from typing import Literal, Union +import sys + + +class ColoredFormatter(logging.Formatter, FONT): + def __init__(self, fmt=None, datefmt=None, style="%", handle=None): + super().__init__(fmt, datefmt, style) + self.__handle = handle + # 打印等级对应的颜色装载器 + self.__color_map = { + logging.DEBUG: self.grey, + logging.INFO: self.bold_blue, + logging.WARNING: self.yellow, + logging.ERROR: self.red, + logging.CRITICAL: self.bold_red, + } + + def bold_red(self, s: str) -> str: + """在终端中加粗的红色字符串 + + Parameters + ---------- + s : str + 需要加粗的字符串 + + Returns + ------- + str + 加粗后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return self.bold(self.red(s)) + + def bold_blue(self, s: str) -> str: + """在终端中加粗的蓝色字符串 + + Parameters + ---------- + s : str + 需要加粗的字符串 + + Returns + ------- + str + 加粗后的字符串 + """ + return self.bold(self.blue(s)) + + def __get_colored_str(self, levelno, message): + """获取使用打印等级对应的颜色装载的字符串 + + Parameters + ---------- + levelno : logging.levelno + logging 等级对象 + message : string + 需要装载的颜色 + """ + + return self.__color_map.get(levelno)(message) + + def format(self, record): + """格式化打印字符串 + 1. 分割消息头和消息体 + 2. 消息头根据 logging 等级装载颜色 + 3. 使用空格填充,统一消息头长度为 20 个字符 + 4.. 拼接消息头和消息体 + + Parameters + ---------- + record : logging.record + logging 信息实例 + + Returns + ------- + string + 格式化后的字符串 + """ + log_message = super().format(record) + self.__handle(log_message + "\n") if self.__handle else None + # 分割消息,分别处理头尾 + messages: list = log_message.split(":", 1) + # 填充空格,统一消息头的长度 + message_header = messages[0] + return f"{self.__get_colored_str(record.levelno, message_header)}:{messages[1]}" + + +Levels = Union[Literal["debug", "info", "warning", "error", "critical"], str] +""" +SwanKitLog 预先定义好的日志等级 +""" + + +class SwanKitLog: + # 日志系统支持的输出等级 + levels = { + "debug": logging.DEBUG, + "info": logging.INFO, + "warning": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL, + } + + def __init__(self, name=__name__.lower(), level: Levels = "info"): + """ + + :param name: + :param level: + """ + super().__init__() + self.prefix = name + ':' + self.__logger = logging.getLogger(name) + self.__original_level = self.__get_level(level) + self.__installed = False + self.__logger.setLevel(self.__original_level) + # 初始化控制台日志处理器,输出到标准输出流 + self.__handler = logging.StreamHandler(sys.stdout) + # 添加颜色格式化,并在此处设置格式化后的输出流是否可以被其他处理器处理 + colored_formatter = ColoredFormatter("%(name)s: %(message)s") + self.__handler.setFormatter(colored_formatter) + self.enable_log() + + def disable_log(self): + """ + 是否开启日志输出,实例化时默认开启 + """ + self.__logger.removeHandler(self.__handler) + + def enable_log(self): + self.__logger.addHandler(self.__handler) + + def set_level(self, level: Levels): + """ + Set the logging level of the logger. + + :param level: The level to set the logger to. This should be one of the following: + - "debug" + - "info" + - "warning" + - "error" + - "critical" + + :raises: KeyError: If an invalid level is passed. + """ + self.__logger.setLevel(self.__get_level(level)) + + def __get_level(self, level: Levels): + """私有属性,获取等级对应的 logging 对象 + + Parameters + ---------- + level : string + 日志级别,可以是 "debug", "info", "warning", "error", 或 "critical" + + Returns + ------- + logging.level : object + logging 模块中的日志等级 + + Raises + ------ + KeyError + 无效的日志级别 + """ + if level.lower() in self.levels: + return self.levels.get(level.lower()) + else: + raise KeyError("log_level must be one of ['debug', 'info', 'warning', 'error', 'critical']: %s" % level) + + def debug(self, message: str): + self.__logger.debug(message) + return + + def info(self, message: str): + self.__logger.info(message) + return + + def warning(self, message: str): + self.__logger.warning(message) + return + + def error(self, message: str): + self.__logger.error(message) + return + + def critical(self, message: str): + self.__logger.critical(message) + return diff --git a/swankit/log/utils.py b/swankit/log/utils.py new file mode 100644 index 0000000..6bef1bb --- /dev/null +++ b/swankit/log/utils.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 23:14 +@File: utils.py +@IDE: pycharm +@Description: + 日志工具 +""" +from typing import Callable, Tuple +import threading +import sys +import time +import re + + +class FONT: + + @staticmethod + def loading( + s: str, + func: Callable, + args: Tuple = (), + interval: float = 0.4, + prefix: str = None, + brush_length: int = 100 + ): + """ + 实现终端打印的加载效果,输入的字符串会在开头出现loading效果以等待传入的函数执行完毕 + + Parameters + ---------- + s : str + 需要打印的字符串 + func : coroutine + 执行的同步函数 + args : Tuple, optional + 传入函数的参数,默认为空 + interval : float, optional + loading的速度,即每个字符的间隔时间,单位为秒 + prefix : str, optional + 前缀字符串,打印在loading效果之前,默认为swanlab + brush_length : int, optional + 刷去的长度,默认为100 + """ + # FIXME 因为协程有一些适配性问题,暂时使用线程 + prefix = FONT.bold(FONT.blue("swanlab")) + ": " if prefix is None else prefix + symbols = ["\\", "|", "/", "-"] + + running, result, error = True, None, None + + def loading(): + index = 0 + while True: + sys.stdout.write("\r" + prefix + symbols[index % len(symbols)] + " " + s) + sys.stdout.flush() + index += 1 + time.sleep(interval) + if not running: + break + + # 再次封装传入的func,当func执行完毕以后,将running置为False + def task(): + nonlocal result, error, running + try: + result = func(*args) + except Exception as e: + error = e + finally: + running = False + + # 开启新线程 + t1 = threading.Thread(target=loading) + t2 = threading.Thread(target=task) + t1.start() + t2.start() + try: + t2.join() + except KeyboardInterrupt: + running = False + raise KeyboardInterrupt + t1.join() + if error is not None: + raise error + FONT.brush("", brush_length) + return result + + @staticmethod + def swanlab(s: str, color: str = "blue"): + """用于为某一条信息添加swanlab前缀""" + return FONT.bold(getattr(FONT, color)("swanlab")) + ": " + s + + @staticmethod + def brush(s: str, length: int = 20) -> None: + """ + 将当前终端行刷去,替换为新的字符串 + + Parameters + ---------- + s : str + 需要刷去的字符串 + length : int, optional + 需要刷去的长度,默认为20,如果当前行的长度大于length,但又需要刷去整行,则需要传入更大的length + """ + sys.stdout.write("\r" + " " * length + "\r" + s) + sys.stdout.flush() + + @staticmethod + def bold(s: str) -> str: + """在终端中加粗字符串 + + Parameters + ---------- + s : str + 需要加粗的字符串 + + Returns + ------- + str + 加粗后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[1m{s}\033[0m" + + @staticmethod + def default(s: str) -> str: + """在终端中将字符串着色为默认颜色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[39m{s}\033[0m" + + @staticmethod + def blue(s: str) -> str: + """在终端中将字符串着色为蓝色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[34m{s}\033[0m" + + @staticmethod + def grey(s: str) -> str: + """在终端中将字符串着色为灰色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[90m{s}\033[0m" + + @staticmethod + def underline(s: str) -> str: + """在终端中将字符串着色为下划线 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[4m{s}\033[0m" + + @staticmethod + def green(s: str) -> str: + """在终端中将字符串着色为绿色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[32m{s}\033[0m" + + @staticmethod + def dark_green(s: str) -> str: + """在终端中将字符串着色为深绿色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[38;5;22m{s}\033[0m" + + @staticmethod + def dark_gray(s: str) -> str: + """在终端中将字符串着色为深灰色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[38;5;236m{s}\033[0m" + + @staticmethod + def yellow(s: str) -> str: + """在终端中将字符串着色为黄色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[33m{s}\033[0m" + + @staticmethod + def red(s: str) -> str: + """在终端中将字符串着色为红色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[31m{s}\033[0m" + + @staticmethod + def magenta(s: str) -> str: + """在终端中将字符串着色为品红色 + + Parameters + ---------- + s : str + 需要着色的字符串 + + Returns + ------- + str + 着色后的字符串 + """ + # ANSI 转义码用于在终端中改变文本样式 + return f"\033[35m{s}\033[0m" + + @staticmethod + def clear(s: str) -> str: + """清除字符串中的颜色编码 + + Parameters + ---------- + s : str + 需要清除颜色的字符串 + + Returns + ------- + str + 清除颜色后的字符串 + """ + ansi_escape_pattern = re.compile(r"\033\[[0-9;]+m") + return ansi_escape_pattern.sub("", s) diff --git a/test/unit/conftest.py b/test/unit/conftest.py new file mode 100644 index 0000000..078d07e --- /dev/null +++ b/test/unit/conftest.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/4/3 16:52 +@File: conftest.py.py +@IDE: pycharm +@Description: + 配置pytest +""" +import pytest +from tutils import TEMP_DIR +from swankit.env import SwanLabEnv +import shutil +import os + + +@pytest.fixture(scope="function", autouse=True) +def setup_before_each(): + # ---------------------------------- 在每一个函数执行前,删除临时文件夹 ---------------------------------- + if os.path.exists(TEMP_DIR): + shutil.rmtree(TEMP_DIR) + os.mkdir(TEMP_DIR) + # ---------------------------------- 每个函数执行前,清空环境变量 ---------------------------------- + for key in SwanLabEnv: + if key.value in os.environ: + del os.environ[key.value] + yield diff --git a/test/unit/log/test_font.py b/test/unit/log/test_font.py new file mode 100644 index 0000000..ec2a698 --- /dev/null +++ b/test/unit/log/test_font.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 23:42 +@File: test_font.py +@IDE: pycharm +@Description: + +""" diff --git a/test/unit/log/test_log.py b/test/unit/log/test_log.py new file mode 100644 index 0000000..bb1711c --- /dev/null +++ b/test/unit/log/test_log.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 23:41 +@File: test_log.py +@IDE: pycharm +@Description: + 测试日志模块 +""" +from swankit.log import SwanKitLog +import nanoid + + +class TestSwanKitLog: + """ + 测试日志模块 + """ + + def test_enable_default(self, capsys): + """ + 测试开启日志 + """ + levels = ["debug", "info", "warning", "error", "critical"] + for level in levels: + name = nanoid.generate() + text = nanoid.generate() + t = SwanKitLog(name, level=level) + for le in levels: + getattr(t, le)(text) + out, err = capsys.readouterr() + if levels.index(le) >= levels.index(level): + assert text in out + assert name in out + assert err == "" + else: + assert out == "" + assert err == "" + + def test_disable(self, capsys): + """ + 测试关闭日志 + """ + levels = ["debug", "info", "warning", "error", "critical"] + for level in levels: + name = nanoid.generate() + text = nanoid.generate() + t = SwanKitLog(name, level=level) + t.disable_log() + for le in levels: + getattr(t, le)(text) + out, err = capsys.readouterr() + assert out == "" + assert err == "" + + def test_set_level(self, capsys): + """ + 测试设置日志等级 + """ + levels = ["debug", "info", "warning", "error", "critical"] + for le in levels: + name = nanoid.generate() + text = nanoid.generate() + t = SwanKitLog(name, level="debug") + t.set_level(le) + getattr(t, le)(text) + out, err = capsys.readouterr() + assert text in out + assert name in out + assert err == "" diff --git a/test/unit/test_env.py b/test/unit/test_env.py new file mode 100644 index 0000000..0f6eb5b --- /dev/null +++ b/test/unit/test_env.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/14 23:42 +@File: env.py +@IDE: pycharm +@Description: + 测试环境变量 +""" +from swankit import env as E +from tutils import TEMP_DIR +import pytest +import shutil +import nanoid +import os + + +class TestGetFolder: + """ + 测试获取全局保存文件夹 + """ + + def test_default(self): + """ + 默认情况 + """ + # 获取当前用户的主目录 + home = os.path.expanduser("~") + folder = os.path.join(home, ".swanlab") + if os.path.exists(folder): + shutil.rmtree(folder) + assert E.get_swanlab_save_folder() == folder + assert os.path.exists(folder) + + def test_env_abs(self): + """ + 设置了一个绝对路径的环境变量 + """ + path = os.path.join(TEMP_DIR, nanoid.generate("1234567890abcdef", 10)) + os.environ[E.SwanLabEnv.SAVE_FOLDER.value] = path + assert not os.path.exists(path) + assert E.get_swanlab_save_folder() == path + assert os.path.exists(path) + + def test_env_rel(self): + """ + 设置了一个相对路径的环境变量 + """ + abs_path = os.path.join(TEMP_DIR, nanoid.generate("1234567890abcdef", 10)) + rel_path = os.path.relpath(abs_path.__str__(), os.getcwd()) + os.environ[E.SwanLabEnv.SAVE_FOLDER.value] = rel_path + assert not os.path.exists(abs_path) + assert E.get_swanlab_save_folder() == abs_path + assert os.path.exists(abs_path) + + def test_env_parent_not_exist(self): + """ + 父目录不存在 + """ + path = os.path.join(TEMP_DIR, nanoid.generate("1234567890abcdef", 10), "a") + os.environ[E.SwanLabEnv.SAVE_FOLDER.value] = path + assert not os.path.exists(path) + with pytest.raises(FileNotFoundError): + E.get_swanlab_save_folder() + + def test_env_not_a_folder(self): + """ + 文件夹不存在,但是文件存在 + """ + path = os.path.join(TEMP_DIR, nanoid.generate("1234567890abcdef", 10)) + with open(path, "w") as f: + f.write("test") + os.environ[E.SwanLabEnv.SAVE_FOLDER.value] = path + assert os.path.exists(path) + with pytest.raises(NotADirectoryError): + E.get_swanlab_save_folder() diff --git a/tutils/__init__.py b/tutils/__init__.py new file mode 100644 index 0000000..3fdefd4 --- /dev/null +++ b/tutils/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/15 01:48 +@File: __init__.py +@IDE: pycharm +@Description: + 测试时使用的工具 +""" +from .path import TEMP_DIR diff --git a/tutils/path.py b/tutils/path.py new file mode 100644 index 0000000..bf3c341 --- /dev/null +++ b/tutils/path.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024/6/15 01:49 +@File: path.py +@IDE: pycharm +@Description: + 路径操作 +""" + +import os + +_ = os.path.dirname(os.path.dirname(__file__)) + +TEMP_DIR = os.path.join(_, 'test', 'temp')