docs: Add init.py usage and instructions

This commit is contained in:
Yuyao Huang (Sam) 2025-05-29 19:01:53 +08:00
parent cbec53af34
commit a86e38b408
2 changed files with 277 additions and 2 deletions

View File

@ -4,11 +4,17 @@
## How to create your own package based on this package. ## How to create your own package based on this package.
Use the following command to create a new package based on this package:
```bash ```bash
python init.py python init.py your-package-name
``` ```
and follow the prompts. It will replace the current package name (`lazy_config`) with your own package name in the code base.
However, you need to modify the author name and email in `pyproject.toml`.
Then you should use `pip install -e .[dev]` to install the package in editable mode.
After the project is created, you can delete the `init.py` file and start working on your project. After the project is created, you can delete the `init.py` file and start working on your project.

269
init.py Normal file
View File

@ -0,0 +1,269 @@
import argparse
import keyword
import os
import re
import shutil
import sys
def main():
args = argparse.ArgumentParser()
args.add_argument("new_package_name")
args.add_argument("--dry-run", action="store_true")
args = args.parse_args()
is_valid_name, reason = is_valid_python_package_name(args.new_package_name)
assert is_valid_name, reason
replace_in_multiple_paths([
"lazy_config",
"projects",
"README.md",
"pyproject.toml"
],
old_word="lazy_config",
new_word=args.new_package_name,
file_extensions=('.py', '.md', '.toml'),
dry_run=args.dry_run
)
if not args.dry_run:
shutil.move("lazy_config", args.new_package_name)
def is_valid_python_package_name(name: str) -> tuple[bool, str]:
"""
检查一个字符串是否是合格的 Python /模块名
这个函数综合考虑了 Python 语法规则和一些常见的最佳实践
Args:
name (str): 要检查的字符串
Returns:
tuple[bool, str]: 一个元组第一个元素是布尔值True 表示合格False 表示不合格
第二个元素是不合格的原因或Valid.
"""
if not name:
return False, "包/模块名不能为空。"
# 将包名按点分割成各个组件
segments = name.split('.')
for segment in segments:
if not segment:
return False, f"包/模块名 '{name}' 包含空组件 (例如 'a..b' 或开头/结尾是点)。"
# 规则1: 每个组件都必须是有效的 Python 标识符。
# isidentifier() 检查了:
# - 以字母或下划线开头 (不能以数字开头)
# - 仅包含字母、数字或下划线
# - 非空 (虽然上面单独检查了)
if not segment.isidentifier():
return False, f"组件 '{segment}''{name}' 中不是一个有效的 Python 标识符。"
# 规则2: 每个组件都不能是 Python 关键字。
if keyword.iskeyword(segment):
return False, f"组件 '{segment}''{name}' 中是一个 Python 关键字。"
# 规则3 (强烈建议): 避免与内置模块名冲突。
# 尽管技术上可以导入,但这会导致混淆和潜在的导入问题。
if segment in sys.builtin_module_names:
return False, f"组件 '{segment}''{name}' 中与一个内置的 Python 模块名冲突。"
# 仅作为PEP 8风格的额外检查不影响“合格性”如果需要严格的PEP8可以将其改为返回False
# if not segment.islower() and not segment.startswith('__') and not segment.endswith('__'):
# # 允许像 __init__ 这样的特殊名称不是小写
# # 对于普通的模块/包名PEP 8 推荐全小写
# # return False, f"组件 '{segment}' 在 '{name}' 中不符合 PEP 8 的全小写命名约定。"
# if segment.startswith('_') and not segment == '__init__':
# # PEP 8 鼓励避免普通模块/包名的前导下划线
# # return False, f"组件 '{segment}' 在 '{name}' 中不符合 PEP 8 的前导下划线约定。"
# if segment.endswith('_'):
# # PEP 8 鼓励避免普通模块/包名的尾随下划线
# # return False, f"组件 '{segment}' 在 '{name}' 中不符合 PEP 8 的尾随下划线约定。"
pass
return True, "Valid."
def replace_in_single_file(file_path: str, old_word: str, new_word: str,
encoding: str = 'utf-8', dry_run: bool = False) -> tuple[bool, list[str]]:
"""
在单个文件中替换特定全字匹配的字符串并记录每一处替换内容
支持 dry_run 模式
Args:
file_path (str): 文件的完整路径
old_word (str): 要被替换的全字匹配字符串
new_word (str): 替换成的字符串
encoding (str): 读取和写入文件时使用的编码
dry_run (bool): 如果为 True, 则只记录将要进行的替换不实际修改文件
Returns:
tuple[bool, list[str]]:
- bool: 是否存在匹配并进行了或将要进行替换
- list[str]: 记录了所有替换操作或模拟操作的文本日志
每个日志条目包含行号和高亮显示的代码行
"""
replacement_logs = []
try:
if not os.path.exists(file_path):
replacement_logs.append(f" Error: File not found: {file_path}")
return False, replacement_logs
with open(file_path, 'r', encoding=encoding) as f:
content = f.read()
pattern = r"\b" + re.escape(old_word) + r"\b"
matches = list(re.finditer(pattern, content))
if not matches:
return False, [] # 没有匹配,直接返回
def repl_func(match):
match_start = match.start()
line_num = content[:match_start].count('\n') + 1
line_start_idx = content.rfind('\n', 0, match_start) + 1
line_end_idx = content.find('\n', match.end())
if line_end_idx == -1:
line_end_idx = len(content)
original_line = content[line_start_idx:line_end_idx].strip()
relative_match_start = match_start - line_start_idx
relative_match_end = match.end() - line_start_idx
highlighted_line = (
original_line[:relative_match_start] +
f"**{original_line[relative_match_start:relative_match_end]}**" +
original_line[relative_match_end:]
)
replacement_logs.append(f" L{line_num:<4}: '{highlighted_line}' -> '{new_word}'")
return new_word
new_content = re.sub(pattern, repl_func, content)
if not dry_run:
with open(file_path, 'w', encoding=encoding) as f:
f.write(new_content)
return True, replacement_logs
except Exception as e:
replacement_logs.append(f" Error processing {file_path}: {e}")
return False, replacement_logs
# --- 新增的辅助函数 (保持不变) ---
def _process_a_file(file_path: str, old_word: str, new_word: str,
file_extensions: tuple[str, ...], # 变更:现在是元组
encoding: str, dry_run: bool, stats: dict):
"""
辅助函数封装了对单个文件的处理逻辑并更新统计信息
"""
# 检查文件扩展名是否符合要求
# 如果 file_extensions 是空元组,表示不限制后缀,总是匹配
if file_extensions and not file_path.endswith(file_extensions): # endswith 可以直接接受元组
stats['skipped'] += 1
return
found_match, logs = replace_in_single_file(
file_path, old_word, new_word, encoding, dry_run
)
if logs:
print(f"File: {file_path}")
for log_entry in logs:
print(log_entry)
print("-" * (len(file_path) + 6))
if found_match:
if not dry_run:
print(f" ✔ Modified: {file_path}\n")
else:
print(f" ✔ Will modify: {file_path}\n")
stats['modified'] += 1
elif not logs and os.path.exists(file_path):
stats['skipped'] += 1
else:
stats['errors'] += 1
# --- 修改后的主协调函数 ---
def replace_in_multiple_paths(paths_to_process: list[str], old_word: str, new_word: str,
file_extensions: str | tuple[str, ...] = (), # 变更:接受字符串或元组
encoding: str = 'utf-8',
dry_run: bool = False):
"""
根据提供的文件和文件夹路径列表替换所有全字匹配的字符串
支持 dry_run 模式并允许多种文件后缀过滤
Args:
paths_to_process (list[str]): 包含要处理的文件和/或文件夹路径的列表
old_word (str): 要被替换的全字匹配字符串
new_word (str): 替换成的字符串
file_extensions (str | tuple[str, ...]):
要处理的文件扩展名可以是单个字符串 (例如 ".py")
也可以是字符串元组 (例如 (".py", ".txt"))
默认 (空元组) 表示处理所有文件扩展名
encoding (str): 读取和写入文件时使用的编码
dry_run (bool): 如果为 True, 则只打印将要进行的替换不实际修改文件
"""
# Normalize file_extensions to always be a tuple, empty if no filter
if isinstance(file_extensions, str):
if file_extensions == "":
normalized_file_extensions = ()
else:
normalized_file_extensions = (file_extensions,)
else:
normalized_file_extensions = file_extensions
action_verb = "Simulating replacement" if dry_run else "Performing replacement"
print(f"--- {action_verb} ---")
print(f"Searching for full word '{old_word}'")
print(f"Replacing with '{new_word}'")
print(f"In paths: {paths_to_process}")
if normalized_file_extensions:
print(f"Filtered by extensions: {','.join(normalized_file_extensions)}")
else:
print(f"No extension filter (processing all files).")
if dry_run:
print(" (DRY RUN: No files will be modified)")
print("-" * 60)
stats = {'modified': 0, 'skipped': 0, 'errors': 0, 'invalid_paths': 0}
for path in paths_to_process:
if not os.path.exists(path):
print(f"‼ Warning: Path does not exist and will be skipped: '{path}'\n")
stats['invalid_paths'] += 1
continue
if os.path.isfile(path):
_process_a_file(path, old_word, new_word, normalized_file_extensions, encoding, dry_run, stats)
elif os.path.isdir(path):
# print(f"Processing directory: '{path}'\n")
for root, _, files in os.walk(path):
for file_name in files:
file_path = os.path.join(root, file_name)
_process_a_file(file_path, old_word, new_word, normalized_file_extensions, encoding, dry_run, stats)
else:
print(f"‼ Warning: Skipping '{path}' (not a file or directory).\n")
stats['invalid_paths'] += 1
print("\n" + "=" * 60)
print("--- Summary ---")
print(f"Total files checked: {stats['modified'] + stats['skipped'] + stats['errors']}")
print(f"Files to be modified/modified: {stats['modified']}")
print(f"Files skipped (word not found or extension mismatch): {stats['skipped']}")
print(f"Files with errors: {stats['errors']}")
print(f"Invalid paths provided: {stats['invalid_paths']}")
print("=" * 60)
if __name__ == "__main__":
main()