package cc.unitmesh.agent.artifact /** * PEP 723 Inline Script Metadata Parser & Generator. * * Parses and generates PEP 723 compliant inline metadata blocks in Python scripts. * * PEP 723 format example: * ```python * # /// script * # requires-python = ">=3.11" * # dependencies = [ * # "requests>=2.28.0", * # "pandas>=1.5.0", * # ] * # /// * ``` * * @see PEP 723 */ object PEP723Parser { /** * Parsed result from a PEP 723 metadata block. */ data class PEP723Metadata( /** Required Python version constraint, e.g. ">=3.11" */ val requiresPython: String? = null, /** List of dependency specifiers, e.g. ["requests>=2.28.0", "pandas>=1.5.0"] */ val dependencies: List = emptyList(), /** AutoDev Unit custom metadata embedded in [tool.autodev-unit] */ val autodevContext: Map = emptyMap(), /** The raw text of the entire metadata block (including comment prefixes) */ val rawBlock: String? = null ) // ---- Parsing ---- private val PEP723_BLOCK_PATTERN = Regex( """(?s)#\s*///\s*script\s*\n(.*?)#\s*///""" ) private val REQUIRES_PYTHON_PATTERN = Regex( """requires-python\s*=\s*"([^"]*)"""" ) private val DEPENDENCIES_PATTERN = Regex( """(?s)dependencies\s*=\s*\[(.*?)\]""" ) private val DEP_ITEM_PATTERN = Regex("""["']([^"']+)["']""") private val AUTODEV_SECTION_PATTERN = Regex( """(?s)\[tool\.autodev-unit\]\s*\n(.*?)(?=#\s*///|\[tool\.|$)""" ) private val AUTODEV_KV_PATTERN = Regex( """#\s*(\S+)\s*=\s*"([^"]*)"""" ) /** * Parse PEP 723 inline metadata from a Python script. * * @param pythonContent Full text of the Python script. * @return Parsed metadata, or a default empty metadata if no block is found. */ fun parse(pythonContent: String): PEP723Metadata { val blockMatch = PEP723_BLOCK_PATTERN.find(pythonContent) ?: return PEP723Metadata() val metadataBlock = blockMatch.groupValues[1] val rawBlock = blockMatch.value // Parse requires-python val requiresPython = REQUIRES_PYTHON_PATTERN.find(metadataBlock)?.groupValues?.get(1) // Parse dependencies val dependencies = parseDependencies(metadataBlock) // Parse [tool.autodev-unit] section val autodevContext = parseAutodevContext(metadataBlock) return PEP723Metadata( requiresPython = requiresPython, dependencies = dependencies, autodevContext = autodevContext, rawBlock = rawBlock ) } /** * Extract only the dependency list from a Python script (convenience method). */ fun parseDependencies(pythonContent: String): List { val depsMatch = DEPENDENCIES_PATTERN.find(pythonContent) ?: return emptyList() val depsContent = depsMatch.groupValues[1] return DEP_ITEM_PATTERN.findAll(depsContent) .map { it.groupValues[1] } .toList() } private fun parseAutodevContext(metadataBlock: String): Map { val sectionMatch = AUTODEV_SECTION_PATTERN.find(metadataBlock) ?: return emptyMap() val sectionContent = sectionMatch.groupValues[1] return AUTODEV_KV_PATTERN.findAll(sectionContent) .associate { it.groupValues[1] to it.groupValues[2] } } // ---- Generation ---- /** * Generate a PEP 723 metadata header block. * * @param dependencies List of dependency specifiers (e.g. "requests>=2.28.0"). * @param requiresPython Python version constraint (default ">=3.11"). * @param autodevContext Optional AutoDev Unit context key-value pairs to embed. * @return The generated metadata block as a string, ready to prepend to a script. */ fun generate( dependencies: List = emptyList(), requiresPython: String = ">=3.11", autodevContext: Map = emptyMap() ): String = buildString { appendLine("# /// script") appendLine("# requires-python = \"$requiresPython\"") if (dependencies.isNotEmpty()) { appendLine("# dependencies = [") dependencies.forEach { dep -> appendLine("# \"$dep\",") } appendLine("# ]") } if (autodevContext.isNotEmpty()) { appendLine("# [tool.autodev-unit]") autodevContext.forEach { (key, value) -> appendLine("# $key = \"$value\"") } } appendLine("# ///") } /** * Inject or replace a PEP 723 metadata block in a Python script. * * If the script already contains a PEP 723 block, it is replaced. * Otherwise the new block is prepended. * * @param pythonContent The original script content. * @param dependencies Dependency list. * @param requiresPython Python version constraint. * @param autodevContext Optional AutoDev context map. * @return The script with the metadata block injected/replaced. */ fun injectMetadata( pythonContent: String, dependencies: List = emptyList(), requiresPython: String = ">=3.11", autodevContext: Map = emptyMap() ): String { val newBlock = generate(dependencies, requiresPython, autodevContext) return if (PEP723_BLOCK_PATTERN.containsMatchIn(pythonContent)) { PEP723_BLOCK_PATTERN.replace(pythonContent, newBlock.trimEnd()) } else { newBlock + "\n" + pythonContent } } /** * Strip the PEP 723 metadata block from a Python script, returning only the code body. */ fun stripMetadata(pythonContent: String): String { return PEP723_BLOCK_PATTERN.replace(pythonContent, "").trimStart('\r', '\n') } }