text fitting pro feature
This commit is contained in:
@@ -15,6 +15,128 @@ from typing import Dict, List, Optional
|
||||
import tempfile
|
||||
import argparse
|
||||
import fnmatch
|
||||
import ast
|
||||
import tokenize
|
||||
import io
|
||||
|
||||
def minify_python_code(source_code: str) -> str:
|
||||
"""
|
||||
Minify Python source code by removing comments and docstrings.
|
||||
|
||||
This function:
|
||||
1. Removes all # comments (inline and standalone)
|
||||
2. Preserves bl_info dictionary (Blender requirement)
|
||||
3. Removes other function/class docstrings
|
||||
4. Optimizes whitespace (max 1 blank line between definitions)
|
||||
5. Maintains valid Python syntax
|
||||
|
||||
Args:
|
||||
source_code: Python source code to minify
|
||||
|
||||
Returns:
|
||||
Minified Python code as a string
|
||||
"""
|
||||
# Parse the code to find docstring line ranges to remove
|
||||
try:
|
||||
tree = ast.parse(source_code)
|
||||
except SyntaxError:
|
||||
return source_code
|
||||
|
||||
# Collect line numbers of docstrings to remove
|
||||
docstring_lines_to_remove = set()
|
||||
|
||||
def process_body(body, parent_is_module=False):
|
||||
"""Process a body of statements to find docstrings."""
|
||||
if not body:
|
||||
return
|
||||
|
||||
first_stmt = body[0]
|
||||
if isinstance(first_stmt, ast.Expr):
|
||||
value = first_stmt.value
|
||||
if isinstance(value, (ast.Str, ast.Constant)):
|
||||
# Check if this is a docstring (string literal as first statement)
|
||||
if isinstance(value, ast.Constant) and not isinstance(value.value, str):
|
||||
return
|
||||
|
||||
# Always remove docstrings (module, class, and function)
|
||||
# The bl_info DICT itself will be preserved by not being marked
|
||||
# Mark these lines for removal
|
||||
for lineno in range(first_stmt.lineno, first_stmt.end_lineno + 1):
|
||||
docstring_lines_to_remove.add(lineno)
|
||||
|
||||
# Process module-level docstrings
|
||||
process_body(tree.body, parent_is_module=True)
|
||||
|
||||
# Process function and class docstrings
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
process_body(node.body, parent_is_module=False)
|
||||
|
||||
# First pass: Remove comments and docstring lines using tokenize
|
||||
# This safely handles # symbols inside strings
|
||||
result_lines = []
|
||||
try:
|
||||
tokens = tokenize.generate_tokens(io.StringIO(source_code).readline)
|
||||
last_lineno = -1
|
||||
last_col = 0
|
||||
|
||||
for tok in tokens:
|
||||
token_type = tok[0]
|
||||
token_string = tok[1]
|
||||
start_line, start_col = tok[2]
|
||||
end_line, end_col = tok[3]
|
||||
|
||||
# Skip comments
|
||||
if token_type == tokenize.COMMENT:
|
||||
continue
|
||||
|
||||
# Skip tokens that are part of docstrings we want to remove
|
||||
if start_line in docstring_lines_to_remove:
|
||||
continue
|
||||
|
||||
# Handle newlines and track lines
|
||||
if start_line > last_lineno:
|
||||
last_col = 0
|
||||
|
||||
# Add spacing if needed
|
||||
if start_col > last_col:
|
||||
result_lines.append(" " * (start_col - last_col))
|
||||
|
||||
# Add the token
|
||||
result_lines.append(token_string)
|
||||
|
||||
# Update position tracking
|
||||
last_col = end_col
|
||||
last_lineno = end_line
|
||||
|
||||
except tokenize.TokenError:
|
||||
# If tokenization fails, return original
|
||||
return source_code
|
||||
|
||||
# Join tokens back into source
|
||||
result = "".join(result_lines)
|
||||
|
||||
# Second pass: Optimize whitespace (max 1 blank line between definitions)
|
||||
lines = result.split('\n')
|
||||
optimized_lines = []
|
||||
blank_count = 0
|
||||
|
||||
for line in lines:
|
||||
if line.strip() == '':
|
||||
blank_count += 1
|
||||
# Allow max 1 consecutive blank line
|
||||
if blank_count <= 1:
|
||||
optimized_lines.append(line)
|
||||
else:
|
||||
blank_count = 0
|
||||
optimized_lines.append(line)
|
||||
|
||||
# Remove trailing blank lines
|
||||
while optimized_lines and optimized_lines[-1].strip() == '':
|
||||
optimized_lines.pop()
|
||||
|
||||
return '\n'.join(optimized_lines)
|
||||
|
||||
|
||||
def get_project_root() -> Path:
|
||||
"""Get the project root directory."""
|
||||
@@ -78,7 +200,7 @@ def should_exclude_file(file_path: Path, source_dir: Path, exclude_patterns: Lis
|
||||
|
||||
return False
|
||||
|
||||
def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[str], exclude_files: List[str] = None):
|
||||
def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[str], exclude_files: List[str] = None, minify: bool = True):
|
||||
"""Copy source files to destination, excluding specified patterns and files."""
|
||||
if exclude_files is None:
|
||||
exclude_files = []
|
||||
@@ -104,9 +226,23 @@ def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[s
|
||||
# Create parent directories
|
||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy file
|
||||
shutil.copy2(item, dest_file)
|
||||
print(f" Copied: {rel_path}")
|
||||
# Copy file with optional minification for Python files
|
||||
if minify and item.suffix == '.py':
|
||||
# Read, minify, and write Python files
|
||||
try:
|
||||
source_code = item.read_text(encoding='utf-8')
|
||||
minified_code = minify_python_code(source_code)
|
||||
dest_file.write_text(minified_code, encoding='utf-8')
|
||||
print(f" Copied (minified): {rel_path}")
|
||||
except Exception as e:
|
||||
# If minification fails, fall back to regular copy
|
||||
print(f" Warning: Could not minify {rel_path}, copying as-is: {e}")
|
||||
shutil.copy2(item, dest_file)
|
||||
print(f" Copied: {rel_path}")
|
||||
else:
|
||||
# Copy non-Python files or when minification is disabled
|
||||
shutil.copy2(item, dest_file)
|
||||
print(f" Copied: {rel_path}")
|
||||
copied_count += 1
|
||||
|
||||
print(f"Files copied: {copied_count}, excluded: {excluded_count}")
|
||||
@@ -145,11 +281,19 @@ def get_version_from_git() -> Optional[str]:
|
||||
|
||||
return None
|
||||
|
||||
def build_addon(version_type: str, version: Optional[str] = None, output_dir: Optional[Path] = None):
|
||||
def build_addon(version_type: str, version: Optional[str] = None, output_dir: Optional[Path] = None, minify: bool = True):
|
||||
"""Build addon for specified version type."""
|
||||
config = load_config()
|
||||
exclusions_config = load_exclusions_config()
|
||||
project_root = get_project_root()
|
||||
# Use current working directory as project root (supports testing with temp dirs)
|
||||
project_root = Path.cwd()
|
||||
|
||||
# Load configs from project root
|
||||
config_path = project_root / "build" / "config.json"
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
exclusions_path = project_root / "build" / "file_exclusions.json"
|
||||
with open(exclusions_path, 'r') as f:
|
||||
exclusions_config = json.load(f)
|
||||
|
||||
# Determine version
|
||||
if version is None:
|
||||
@@ -191,7 +335,7 @@ def build_addon(version_type: str, version: Optional[str] = None, output_dir: Op
|
||||
|
||||
# Copy source files to temp directory with version-specific exclusions
|
||||
build_temp_dir = temp_dir / f"{config['project']['id']}_{version_type}"
|
||||
copy_source_files(source_dir, build_temp_dir, exclude_patterns, exclude_files)
|
||||
copy_source_files(source_dir, build_temp_dir, exclude_patterns, exclude_files, minify)
|
||||
|
||||
# Create output filename
|
||||
version_suffix = f"_{version_type}" if version_type == "free" else ""
|
||||
@@ -210,7 +354,7 @@ def build_addon(version_type: str, version: Optional[str] = None, output_dir: Op
|
||||
|
||||
return output_file
|
||||
|
||||
def build_all_versions(version: Optional[str] = None):
|
||||
def build_all_versions(version: Optional[str] = None, minify: bool = True):
|
||||
"""Build both full and free versions."""
|
||||
print("Building all versions...")
|
||||
|
||||
@@ -220,14 +364,14 @@ def build_all_versions(version: Optional[str] = None):
|
||||
print("\n" + "="*50)
|
||||
print("BUILDING FULL VERSION")
|
||||
print("="*50)
|
||||
full_package = build_addon("full", version)
|
||||
full_package = build_addon("full", version, minify=minify)
|
||||
built_packages.append(("full", full_package))
|
||||
|
||||
# Build free version
|
||||
print("\n" + "="*50)
|
||||
print("BUILDING FREE VERSION")
|
||||
print("="*50)
|
||||
free_package = build_addon("free", version)
|
||||
free_package = build_addon("free", version, minify=minify)
|
||||
built_packages.append(("free", free_package))
|
||||
|
||||
print("\n" + "="*50)
|
||||
@@ -246,14 +390,18 @@ def main():
|
||||
parser.add_argument("--type", choices=["free", "full", "all"], default="all",
|
||||
help="Version type to build")
|
||||
parser.add_argument("--output", type=Path, help="Output directory")
|
||||
parser.add_argument("--minify", dest="minify", action="store_true", default=True,
|
||||
help="Minify Python code (default: enabled)")
|
||||
parser.add_argument("--no-minify", dest="minify", action="store_false",
|
||||
help="Disable Python code minification")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.type == "all":
|
||||
build_all_versions(args.version)
|
||||
build_all_versions(args.version, args.minify)
|
||||
else:
|
||||
build_addon(args.type, args.version, args.output)
|
||||
build_addon(args.type, args.version, args.output, args.minify)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Build failed: {e}")
|
||||
|
||||
Reference in New Issue
Block a user