New forge coding added

This commit is contained in:
=
2026-06-04 23:21:39 +08:00
parent 518eb06591
commit 8da92ced57
288 changed files with 52017 additions and 1913 deletions
+270
View File
@@ -0,0 +1,270 @@
"""Metadata recording helpers for component generation classes."""
import inspect
import json
import re
import sys
from datetime import datetime
from functools import wraps
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_METADATA_DIR = PROJECT_ROOT / "metafile"
_RECORDER_MARK = "_mxpic_generation_metadata_wrapped"
_INTERNAL_PREFIX = "_mxpic_generation_"
_AUTO_METADATA_ENABLED = True
_AUTO_METADATA_DIR = DEFAULT_METADATA_DIR
_AUTO_METADATA_INCLUDE_STATE = True
_AUTO_METADATA_INCLUDE_PRIVATE = False
_CONSTRUCTION_DEPTH = 0
def _json_safe(value, depth=0):
"""Return a JSON-friendly representation without walking huge objects."""
if value is None or isinstance(value, (bool, int, float, str)):
return value
if isinstance(value, Path):
return str(value)
if depth >= 4:
return _short_repr(value)
if isinstance(value, (list, tuple)):
return [_json_safe(item, depth + 1) for item in value]
if isinstance(value, set):
return [_json_safe(item, depth + 1) for item in sorted(value, key=repr)]
if isinstance(value, dict):
return {
str(_json_safe(key, depth + 1)): _json_safe(item, depth + 1)
for key, item in value.items()
}
return _object_summary(value)
def _object_summary(value):
summary = {
"type": f"{value.__class__.__module__}.{value.__class__.__name__}",
"repr": _short_repr(value),
}
for attr in ("name", "cell_name", "basename", "length", "width"):
if hasattr(value, attr):
try:
summary[attr] = _json_safe(getattr(value, attr), depth=4)
except Exception:
summary[attr] = "<unavailable>"
return summary
def _short_repr(value, limit=240):
try:
text = repr(value)
except Exception:
text = f"<unrepresentable {value.__class__.__name__}>"
if len(text) > limit:
return text[: limit - 3] + "..."
return text
def _sanitize_filename_part(value):
text = str(value or "")
text = re.sub(r"[^A-Za-z0-9_.-]+", "_", text).strip("._")
return text or "unnamed"
def _capture_init_parameters(init, self, args, kwargs):
try:
signature = inspect.signature(init)
bound = signature.bind(self, *args, **kwargs)
bound.apply_defaults()
except Exception:
return {
"args": _json_safe(args),
"kwargs": _json_safe(kwargs),
}
return {
name: _json_safe(value)
for name, value in bound.arguments.items()
if name != "self"
}
def get_generation_metadata(self, include_state=True, include_private=False):
"""Return the recorded generation metadata for this component instance."""
metadata = {
"class": self.__class__.__name__,
"module": self.__class__.__module__,
"created_at": getattr(self, "_mxpic_generation_created_at", None),
"init_parameters": getattr(self, "_mxpic_generation_parameters", {}),
}
metadata_file = getattr(self, "_mxpic_generation_metadata_file", None)
if metadata_file is not None:
metadata["metadata_file"] = metadata_file
metadata_error = getattr(self, "_mxpic_generation_metadata_error", None)
if metadata_error is not None:
metadata["metadata_error"] = metadata_error
if include_state:
metadata["state"] = _collect_state(self, include_private=include_private)
return metadata
def _collect_state(instance, include_private=False):
state = {}
for key, value in getattr(instance, "__dict__", {}).items():
if key.startswith(_INTERNAL_PREFIX):
continue
if not include_private and key.startswith("_"):
continue
if callable(value):
continue
state[key] = _json_safe(value)
return state
def save_generation_metadata(
self,
folder=None,
filename=None,
include_state=True,
include_private=False,
):
"""Write generation metadata to JSON and return the saved file path."""
folder_path = Path(folder) if folder is not None else DEFAULT_METADATA_DIR
folder_path.mkdir(parents=True, exist_ok=True)
metadata = self.get_generation_metadata(
include_state=include_state,
include_private=include_private,
)
if filename is None:
params = metadata.get("init_parameters", {})
name = params.get("name") or getattr(self, "name", None)
suffix = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
filename = (
f"{self.__class__.__name__}_"
f"{_sanitize_filename_part(name)}_"
f"{suffix}.json"
)
elif not str(filename).lower().endswith(".json"):
filename = f"{filename}.json"
file_path = folder_path / filename
file_path.write_text(json.dumps(metadata, indent=4), encoding="utf-8")
self._mxpic_generation_metadata_file = str(file_path)
return file_path
def set_generation_metadata_auto_save(
enabled=True,
folder=None,
include_state=True,
include_private=False,
):
"""Configure automatic metadata JSON writes after component construction."""
global _AUTO_METADATA_ENABLED
global _AUTO_METADATA_DIR
global _AUTO_METADATA_INCLUDE_STATE
global _AUTO_METADATA_INCLUDE_PRIVATE
_AUTO_METADATA_ENABLED = bool(enabled)
if folder is not None:
_AUTO_METADATA_DIR = Path(folder)
_AUTO_METADATA_INCLUDE_STATE = bool(include_state)
_AUTO_METADATA_INCLUDE_PRIVATE = bool(include_private)
def get_generation_metadata_auto_save_config():
"""Return the current automatic metadata write configuration."""
return {
"enabled": _AUTO_METADATA_ENABLED,
"folder": str(_AUTO_METADATA_DIR),
"include_state": _AUTO_METADATA_INCLUDE_STATE,
"include_private": _AUTO_METADATA_INCLUDE_PRIVATE,
}
def install_generation_metadata_recorders(package_prefixes=None):
"""Attach metadata methods to generation classes already imported."""
if package_prefixes is None:
package_prefixes = (
"mxpic_forge_new.components",
)
installed = []
for module in list(sys.modules.values()):
module_name = getattr(module, "__name__", "")
if not module_name.startswith(package_prefixes):
continue
for _, cls in inspect.getmembers(module, inspect.isclass):
if not getattr(cls, "__module__", "").startswith(package_prefixes):
continue
if _wrap_generation_class(cls):
installed.append(cls)
return installed
def _wrap_generation_class(cls):
if cls.__dict__.get(_RECORDER_MARK, False):
return False
init = cls.__dict__.get("__init__")
if init is None:
_attach_methods(cls)
setattr(cls, _RECORDER_MARK, True)
return True
@wraps(init)
def wrapped_init(self, *args, **kwargs):
global _CONSTRUCTION_DEPTH
captured = _capture_init_parameters(init, self, args, kwargs)
depth = getattr(self, "_mxpic_generation_record_depth", 0)
is_outermost_construction = _CONSTRUCTION_DEPTH == 0
_CONSTRUCTION_DEPTH += 1
self._mxpic_generation_record_depth = depth + 1
try:
result = init(self, *args, **kwargs)
finally:
_restore_record_depth(self, depth)
_CONSTRUCTION_DEPTH -= 1
if depth == 0:
self._mxpic_generation_parameters = captured
self._mxpic_generation_created_at = datetime.now().isoformat(timespec="seconds")
if is_outermost_construction:
_auto_save_generation_metadata(self)
return result
cls.__init__ = wrapped_init
_attach_methods(cls)
setattr(cls, _RECORDER_MARK, True)
return True
def _restore_record_depth(instance, depth):
if depth:
instance._mxpic_generation_record_depth = depth
elif hasattr(instance, "_mxpic_generation_record_depth"):
delattr(instance, "_mxpic_generation_record_depth")
def _auto_save_generation_metadata(instance):
if not _AUTO_METADATA_ENABLED:
return
try:
save_generation_metadata(
instance,
folder=_AUTO_METADATA_DIR,
include_state=_AUTO_METADATA_INCLUDE_STATE,
include_private=_AUTO_METADATA_INCLUDE_PRIVATE,
)
if hasattr(instance, "_mxpic_generation_metadata_error"):
delattr(instance, "_mxpic_generation_metadata_error")
except Exception as exc:
instance._mxpic_generation_metadata_error = f"{exc.__class__.__name__}: {exc}"
def _attach_methods(cls):
if not hasattr(cls, "get_generation_metadata"):
cls.get_generation_metadata = get_generation_metadata
if not hasattr(cls, "save_generation_metadata"):
cls.save_generation_metadata = save_generation_metadata
if not hasattr(cls, "record_generation_metadata"):
cls.record_generation_metadata = save_generation_metadata