docs: Add init.py usage and instructions
This commit is contained in:
parent
cbec53af34
commit
a86e38b408
10
README.md
10
README.md
@ -4,11 +4,17 @@
|
||||
|
||||
## How to create your own package based on this package.
|
||||
|
||||
Use the following command to create a new package based on this package:
|
||||
|
||||
```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.
|
||||
|
||||
|
||||
269
init.py
Normal file
269
init.py
Normal 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()
|
||||
Loading…
x
Reference in New Issue
Block a user