class Ctx:
# Return all classes in the MRO excluding Ctx and object.
def _non_root_mro_classes(self):
return [cls for cls in self.__class__.__mro__[1:] if cls not in (Ctx, object)]
# Return all Ctx-derived classes in the MRO.
def _inherited_ctx_classes(self):
classes = []
for cls in self._non_root_mro_classes():
if issubclass(cls, Ctx):
classes.append(cls)
return classes
# Collect values from zero-argument methods in inherited Ctx classes.
def _inherited_field_values(self):
values = {}
for cls in self._inherited_ctx_classes():
values.update(self._class_zero_arg_values(cls))
return values
# Return field values defined in a specific class's methods.
def _class_zero_arg_values(self, cls):
values = {}
for name in dir(cls):
if self._skip_ctx_member(name):
continue
value = self._evaluate_zero_arg_class_member(cls, name)
if value is not _Missing:
values[f"{cls.__name__}.{name}"] = value
return values
# Safely evaluate a zero-argument method from a class.
def _evaluate_zero_arg_class_member(self, cls, name):
try:
member = getattr(cls, name, None)
if not callable(member):
return member
if isinstance(member, type):
return member
sig = signature(member)
params = list(sig.parameters.values())
if len(params) == 0:
return member()
elif len(params) == 1 and params[0].name == "self":
return member(self)
return _Missing
except Exception:
return _Missing
# Return True if the member name should be skipped during context collection.
def _skip_ctx_member(self, name):
return name.startswith("_") or name in CTX_RESERVED_NAMES
# Identify fields that override values defined in parent classes.
def _detect_overrides(self):
overrides = []
parent_values = self._inherited_field_values()
current_ctx = self.ctx(template_only=False)
for key, value in current_ctx.items():
if self._is_overridden_value(key, value, parent_values):
overrides.append(key)
return overrides
# Return True if the field value differs from any parent class definition.
def _is_overridden_value(self, key, value, parent_values):
for parent_key, parent_value in parent_values.items():
if key == parent_key.split(".")[-1] and value != parent_value:
return True
return False
# Calculate the set of requirements that differ from parent classes.
def _delta_requirements(self):
deltas = {}
own_doc = self._instance_notes()
parent_docs = set(self._inherited_docstrings())
if own_doc and own_doc not in parent_docs:
deltas["notes"] = own_doc
for key, value in self.ctx(template_only=False).items():
if key == "_in_ctx":
continue
if not self._parent_has_same_value(key, value):
deltas[key] = value
return deltas
# Return a list of docstrings from all inherited classes.
def _inherited_docstrings(self):
docs = []
for cls in self._non_root_mro_classes():
if cls.__doc__:
docs.append(cleandoc(cls.__doc__))
return docs
# Return True if any parent class shares the same field value.
def _parent_has_same_value(self, key, value):
for cls in self._non_root_mro_classes():
if not hasattr(cls, key):
continue
parent_val = self._safe_call(getattr(cls, key))
if parent_val is _Missing:
continue
if parent_val == value:
return True
return False
# Safely invoke a callable and return its result or _Missing on failure.
def _safe_call(self, maybe_callable):
if not callable(maybe_callable):
return maybe_callable
if isinstance(maybe_callable, type):
return maybe_callable
try:
sig = signature(maybe_callable)
params = list(sig.parameters.values())
if len(params) == 0:
return maybe_callable()
elif len(params) == 1 and params[0].name == "self":
return maybe_callable(self)
return _Missing
except Exception:
return _Missing
# Collect FQNs of all Requirement-derived classes in the MRO.
def _effective_requirement_ids(self):
from .spec_types import Requirement
req_ids = []
for cls in self.__class__.__mro__:
if not issubclass(cls, Requirement) or cls is Requirement:
continue
try:
class_id = fqn(cls)
if class_id:
req_ids.append(class_id)
except Exception:
continue
return req_ids
# Concatenate docstring templates from parent classes.
def _base_template(self):
templates = []
for cls in self._non_root_mro_classes():
cleaned = self._class_docstring(cls)
if cleaned:
templates.append(cleaned)
templates.reverse()
return "\n\n".join(templates)
# Return the cleaned docstring for a given class.
def _class_docstring(self, cls):
doc = cls.__doc__
return cleandoc(doc) if doc else ""
# Return the docstring template for the current class.
def _compiled_docstring_template(self):
return self._class_docstring(self.__class__)
# Return notes specific to the current instance (its docstring).
def _instance_notes(self):
return self._compiled_docstring_template()
# Return source location metadata for an object or the current class.
def _source_info(self, obj=None):
target = obj if obj is not None else self.__class__
try:
source_file = inspect.getsourcefile(target)
source_file = os.path.abspath(source_file) if source_file else None
lines, start_line = inspect.getsourcelines(target)
return {
"file": source_file,
"start_line": start_line,
"end_line": start_line + len(lines) - 1,
"name": getattr(target, "__name__", str(target)),
}
except (OSError, TypeError):
return None
# Build and return the context data used for template rendering.
def ctx(self, template_only=True):
if getattr(self, "_in_ctx", False):
return {}
self._in_ctx = True
try:
return self._build_context(template_only)
finally:
self._in_ctx = False
# Internal method to assemble the template context.
def _build_context(self, template_only=True):
expected = self._expected_template_vars()
context = self._collect_template_context(expected)
if template_only:
return context
return self._collect_non_template_context(context)
# Identify all undeclared variables in the docstring templates.
def _expected_template_vars(self):
env = Environment()
template_text = f"{self._base_template()}\n{self._instance_notes()}"
return meta.find_undeclared_variables(env.parse(template_text))
# Resolve and collect values for all required template variables.
def _collect_template_context(self, expected_vars):
context = {}
if "fields" in expected_vars and hasattr(self, "fields"):
context["fields"] = self.fields()
for var_name in sorted(expected_vars):
if var_name == "fields":
continue
context[var_name] = self._resolve_template_var(var_name)
return context
# Map template variable names to methods or attributes and return their values.
def _resolve_template_var(self, var_name):
member_name = var_name.replace("-", "_")
if hasattr(self, member_name):
member = getattr(self, member_name)
if callable(member) and not isinstance(member, type):
try:
sig = signature(member)
params = list(sig.parameters.values())
if len(params) == 0:
return member()
elif len(params) == 1 and params[0].name == "self":
return member()
except Exception:
pass
return member
# Check annotations in class MRO for default value or missing value declaration
cls = self.__class__
for base in cls.__mro__:
if hasattr(base, "__annotations__") and member_name in base.__annotations__:
if hasattr(self, member_name):
return getattr(self, member_name)
raise AttributeError(
f"Field '{member_name}' is declared via type annotations but lacks a value."
)
raise AttributeError(self._missing_template_var_message(var_name, member_name))
# Generate a descriptive error message for missing template variables.
def _missing_template_var_message(self, var_name, member_name):
src = self._source_info()
location = f"{src['file']}:{src['start_line']}" if src else "unknown location"
return (
f"\nThe variable '{{{{{var_name}}}}}' was found in a docstring template "
f"for class '{self.__class__.__name__}',\n"
f"defined at {location},\n"
f"but no matching method or attribute '{member_name}' was found.\n\n"
f"FIX: implement 'def {member_name}(self):' in class "
f"'{self.__class__.__name__}' or one of its bases.\n"
)
# Supplement the context with all available public methods and attributes.
def _collect_non_template_context(self, context):
for name in sorted(dir(self)):
if self._skip_runtime_context_member(name, context):
continue
value = self._resolve_runtime_context_member(name)
if value is not _Missing:
context[name] = value
return context
# Return True if a member should be excluded from the runtime context.
def _skip_runtime_context_member(self, name, context):
if name.startswith("_") or name in CTX_RESERVED_NAMES:
return True
if name in CTX_INTERNAL_NAMES or name in context:
return True
return False
# Resolve a member name to its value for runtime context inclusion.
def _resolve_runtime_context_member(self, name):
member = getattr(self, name)
if not callable(member):
return member
try:
if len(signature(member).parameters) != 0:
return _Missing
return member()
except (TypeError, ValueError, UnimplementedMethodError):
return _Missing
# Recursively convert Python values to XML elements.
def _to_xml_element(self, name, value):
elem = ET.Element(name)
if isinstance(value, dict):
for key in sorted(value.keys()):
if key in SKIPPED_SOURCE_LINE_KEYS:
continue
child_name = str(key).replace("-", "_")
elem.append(self._to_xml_element(child_name, value[key]))
return elem
if isinstance(value, list):
for item in value:
elem.append(self._to_xml_element("item", item))
return elem
elem.text = str(value)
return elem
# Render the specification as structured XML using minidom for formatting.
def render_xml(self):
try:
return minidom.parseString(
ET.tostring(self.to_xml_element(), encoding="utf-8")
).toprettyxml(indent=" ")
except Exception as e:
return f"<!-- Error rendering XML: {e} -->\n" + ET.tostring(
self.to_xml_element(), encoding="unicode"
)
# Build the XML representation of the specification.
def to_xml_element(self):
"""Convert the specification to an XML element."""
root = ET.Element("specification")
root.set("type", self.__class__.__name__)
root.set("ref", fqn(self.__class__))
ctx_data = self.ctx()
self._append_source_metadata(root)
self._append_docstring(root, ctx_data)
self._append_context(root)
self._append_inheritance(root)
self._append_effective_req_ids(root)
self._append_overrides(root)
self._append_delta_requirements(root)
return root
# Append source file and line information to the XML element.
def _append_source_metadata(self, root):
src = self._source_info()
if not src:
return
source_elem = ET.SubElement(root, "source")
source_elem.set("target", src["name"])
source_elem.set("file", src["file"])
# Render and append the docstring to the XML element.
def _append_docstring(self, root, ctx_data):
template_text = self._compiled_docstring_template()
if not template_text:
return
try:
rendered = Template(template_text).render(**ctx_data).strip()
docstring_elem = ET.SubElement(root, "docstring")
docstring_elem.text = rendered
except Exception as e:
print(f"Error rendering docstring for {self.__class__.__name__}: {e}")
# Append all context fields to the XML element.
def _append_context(self, root):
context_elem = ET.SubElement(root, "context")
for key, value in sorted(self.ctx(template_only=False).items()):
name = str(key).replace("-", "_")
context_elem.append(self._to_xml_element(name, value))
# Append inheritance references to the XML element.
def _append_inheritance(self, root):
inherited = [
cls for cls in self._non_root_mro_classes() if self._class_docstring(cls)
]
if not inherited:
return
inherits_elem = ET.SubElement(root, "inherits")
for cls in inherited:
try:
ref_elem = self._to_xml_element("ref", fqn(cls))
inherits_elem.append(ref_elem)
except Exception:
continue
# Append all effective requirement IDs to the XML element.
def _append_effective_req_ids(self, root):
req_ids = self._effective_requirement_ids()
if not req_ids:
return
req_elem = ET.SubElement(root, "effective_req_ids")
for req_id in req_ids:
req_elem.append(self._to_xml_element("id", req_id))
# Append field override information to the XML element.
def _append_overrides(self, root):
overrides = self._detect_overrides()
if not overrides:
return
overrides_elem = ET.SubElement(root, "overrides")
for name in overrides:
overrides_elem.append(self._to_xml_element("field", name))
# Append requirement deltas to the XML element.
def _append_delta_requirements(self, root):
deltas = self._delta_requirements()
if not deltas:
return
delta_elem = ET.SubElement(root, "delta_requirements")
for key, value in deltas.items():
name = str(key).replace("-", "_")
delta_elem.append(self._to_xml_element(name, value))