Repository.mk | 1 codemaker/Executable_pythonmaker.mk | 42 codemaker/Module_codemaker.mk | 1 codemaker/README.md | 1 codemaker/source/pythonmaker/pythonmaker.cxx | 117 ++ codemaker/source/pythonmaker/pythonoptions.cxx | 247 ++++ codemaker/source/pythonmaker/pythonoptions.hxx | 26 codemaker/source/pythonmaker/pythontype.cxx | 1306 +++++++++++++++++++++++++ codemaker/source/pythonmaker/pythontype.hxx | 97 + 9 files changed, 1838 insertions(+)
New commits: commit 2bdf17ee6e12636f77065f3577dcd81a8cb0f1af Author: Manish Bera <mbera.de...@gmail.com> AuthorDate: Mon May 19 12:55:29 2025 +0530 Commit: Hossein <hoss...@libreoffice.org> CommitDate: Thu Aug 28 15:21:01 2025 +0200 Add initial scaffolding for pythonmaker This commit introduces the 'pythonmaker' executable, a new tool in the codemaker module. pythonmaker generates Python type stub files (.pyi) from UNO IDL type libraries (.rdb files). This enables static type checking and modern IDE features like auto-completion for developers writing Python scripts for LibreOffice. This commit includes the complete, feature-complete tool capable of handling all major IDL entity types (enums, constants, typedefs, structs, exceptions, interfaces, services, and singletons). Change-Id: I52f783aa982e1de04a207a9a884aee207f99d193 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/185498 Tested-by: Jenkins Tested-by: Hossein <hoss...@libreoffice.org> Reviewed-by: Hossein <hoss...@libreoffice.org> Reviewed-by: Xisco Fauli <xiscofa...@libreoffice.org> diff --git a/Repository.mk b/Repository.mk index c61e5dd66968..0cb81f699ba4 100644 --- a/Repository.mk +++ b/Repository.mk @@ -96,6 +96,7 @@ $(eval $(call gb_Helper_register_executables_for_install,SDK,sdk, \ cppumaker \ javamaker \ netmaker \ + pythonmaker \ $(call gb_CondExeSp2bv,sp2bv) \ $(if $(filter ODK,$(BUILD_TYPE)),unoapploader) \ unoidl-read \ diff --git a/codemaker/Executable_pythonmaker.mk b/codemaker/Executable_pythonmaker.mk new file mode 100644 index 000000000000..f2b4144171e9 --- /dev/null +++ b/codemaker/Executable_pythonmaker.mk @@ -0,0 +1,42 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +$(eval $(call gb_Executable_Executable,pythonmaker)) + +$(eval $(call gb_Executable_use_external,pythonmaker,frozen)) + +$(eval $(call gb_Executable_use_external,pythonmaker,boost_headers)) + +ifeq ($(DISABLE_DYNLOADING),TRUE) +$(eval $(call gb_Executable_use_externals,pythonmaker,\ + dtoa \ +)) +endif + +$(eval $(call gb_Executable_use_libraries,pythonmaker,\ + unoidl \ + $(if $(filter TRUE,$(DISABLE_DYNLOADING)),reg) \ + $(if $(filter TRUE,$(DISABLE_DYNLOADING)),store) \ + salhelper \ + sal \ +)) + +$(eval $(call gb_Executable_use_static_libraries,pythonmaker,\ + codemaker \ +)) + +# Source files are located in codemaker/source/pythonmaker/ +# Paths are relative to the 'libreoffice' root directory +$(eval $(call gb_Executable_add_exception_objects,pythonmaker,\ + codemaker/source/pythonmaker/pythonmaker \ + codemaker/source/pythonmaker/pythonoptions \ + codemaker/source/pythonmaker/pythontype \ +)) + +# vim:set noet sw=4 ts=4: diff --git a/codemaker/Module_codemaker.mk b/codemaker/Module_codemaker.mk index 83624602422c..36f44f231195 100644 --- a/codemaker/Module_codemaker.mk +++ b/codemaker/Module_codemaker.mk @@ -18,6 +18,7 @@ $(eval $(call gb_Module_add_targets,codemaker,\ Executable_javamaker \ Executable_cppumaker \ Executable_netmaker \ + Executable_pythonmaker \ )) endif diff --git a/codemaker/README.md b/codemaker/README.md index 839ae7d7017b..c82f5019a64a 100644 --- a/codemaker/README.md +++ b/codemaker/README.md @@ -7,6 +7,7 @@ Generators for language-binding--specific representations of UNOIDL entities: - `javamaker` generates class files for the JVM language binding - `netmaker` generates C# code files for the .NET language binding - `climaker` (the old codemaker for .NET Framework) is in module `cli_ure` +- `pythonmaker` generates Python stub (pyi) files for the Python UNO language binding Some of the code is re-used by the skeletonmakers in module `unodevtools`. diff --git a/codemaker/source/pythonmaker/pythonmaker.cxx b/codemaker/source/pythonmaker/pythonmaker.cxx new file mode 100644 index 000000000000..d33347f055be --- /dev/null +++ b/codemaker/source/pythonmaker/pythonmaker.cxx @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/config.h> + +#include <cstdlib> +#include <iostream> +#include <vector> + +#include <codemaker/generatedtypeset.hxx> +#include <codemaker/typemanager.hxx> +#include <codemaker/global.hxx> +#include <rtl/ref.hxx> +#include <rtl/string.hxx> +#include <rtl/ustring.hxx> +#include <sal/main.h> +#include <unoidl/unoidl.hxx> +#include <o3tl/string_view.hxx> + +#include "pythonoptions.hxx" +#include "pythontype.hxx" + +SAL_IMPLEMENT_MAIN_WITH_ARGS(argc, argv) +{ + // Object to hold parsed command-line options. + PythonOptions options; + try + { + // Initialize the options object from command-line arguments. + // If it fails (e.g., --help or invalid options), it returns false. + if (!options.initOptions(argc, argv)) + { + return EXIT_FAILURE; + } + + rtl::Reference<TypeManager> typeMgr(new TypeManager); + for (const OString& f : options.getExtraInputFiles()) + { + typeMgr->loadProvider(convertToFileUrl(f), false); + } + + // Load the primary RDB files. Types from these files are the main candidates for stub generation. + for (const OString& f : options.getInputFiles()) + { + typeMgr->loadProvider(convertToFileUrl(f), true); + } + + // `GeneratedTypeSet` keeps track of which types have already been processed + // to avoid redundant work and infinite loops in case of circular dependencies. + codemaker::GeneratedTypeSet generated; + if (options.isValid("-T"_ostr)) + { + OUString names(b2u(options.getOption("-T"_ostr))); + for (sal_Int32 i = 0; i != -1;) + { + std::u16string_view name(o3tl::getToken(names, 0, ';', i)); + if (!name.empty()) + { + codemaker::pythonmaker::produce( + OUString(name == u"*" + ? u"" + : o3tl::ends_with(name, u".*") + ? name.substr(0, name.size() - std::strlen(".*")) + : name), + typeMgr, generated, options); + } + } + } + else + { + // If -T is not specified, start the generation process from the root (global) namespace. + codemaker::pythonmaker::produce(u""_ustr, typeMgr, generated, options); + } + } + // Catch specific, known exceptions to provide clear error messages. + catch (CannotDumpException const& e) + { + std::cerr << "ERROR: " << u2b(e.getMessage()) << ' '; + return EXIT_FAILURE; + } + catch (unoidl::NoSuchFileException const& e) + { + std::cerr << "ERROR: No such file <" << u2b(e.getUri()) << "> "; + return EXIT_FAILURE; + } + catch (unoidl::FileFormatException const& e) + { + std::cerr << "ERROR: Bad format of <" << u2b(e.getUri()) << ">, \"" << u2b(e.getDetail()) + << "\" "; + return EXIT_FAILURE; + } + catch (IllegalArgument const& e) + { + std::cerr << "Illegal option: " << e.m_message << ' '; + return EXIT_FAILURE; + } + catch (std::exception const& e) + { + std::cerr << "Standard exception: " << e.what() << ' '; + return EXIT_FAILURE; + } + catch (...) + { + std::cerr << "Unknown C++ exception occurred. "; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/codemaker/source/pythonmaker/pythonoptions.cxx b/codemaker/source/pythonmaker/pythonoptions.cxx new file mode 100644 index 000000000000..cf89e1e4a809 --- /dev/null +++ b/codemaker/source/pythonmaker/pythonoptions.cxx @@ -0,0 +1,247 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "pythonoptions.hxx" + +#include <iostream> +#include <vector> +#include <cstdio> +#include <cstring> + +#ifdef SAL_UNX +#define LOCAL_SEPARATOR '/' +#else +#define LOCAL_SEPARATOR '\' +#endif + +PythonOptions::PythonOptions() + : Options() +{ + m_program = "pythonmaker"_ostr; +} + +bool PythonOptions::initOptions(int ac, char* av[], bool bCmdFile) +{ + if (!bCmdFile) + { + OString name(av[0]); + sal_Int32 index = name.lastIndexOf(LOCAL_SEPARATOR); + m_program = name.copy(index > 0 ? index + 1 : 0); + + if (ac < 2) + { + std::cerr << prepareHelp(); + return false; + } + } + + // Start parsing from index 1 if it's the main call, or 0 if it's a command file call + for (int i = (bCmdFile ? 0 : 1); i < ac; ++i) + { + OString argument(av[i]); + + if (argument.startsWith("-")) + { + if (argument == "-O"_ostr) + { + if (i + 1 < ac && av[i + 1][0] != '-') + { + m_options["-O"_ostr] = OString(av[++i]); + } + else + { + throw IllegalArgument("'-O' option requires a path."_ostr); + } + } + else if (argument == "-h"_ostr || argument == "--help"_ostr) + { + std::cout << prepareHelp(); + return true; + } + else if (argument == "-nD"_ostr) + { + m_options["-nD"_ostr] = OString(); + } + else if (argument == "-T"_ostr) + { + if (i + 1 < ac && av[i + 1][0] != '-') + { + const char* types = av[++i]; + if (m_options.count("-T"_ostr) > 0) + { + m_options["-T"_ostr] += ";"_ostr + types; + } + else + { + m_options["-T"_ostr] = OString(types); + } + } + else + { + throw IllegalArgument("'-T' option requires a type list."_ostr); + } + } + else if (argument == "-G"_ostr) + { + m_options["-G"_ostr] = OString(); + } + else if (argument == "-Gc"_ostr) + { + m_options["-Gc"_ostr] = OString(); + } + else if (argument == "-X"_ostr) + { + if (i + 1 < ac && av[i + 1][0] != '-') + { + m_extra_input_files.emplace_back(av[++i]); + } + else + { + throw IllegalArgument("'-X' option requires a file path."_ostr); + } + } + else if (argument == "-v"_ostr || argument == "--verbose"_ostr) + { + m_options["--verbose"_ostr] = "true"_ostr; + } + else if (argument == "--version") + { + std::cout << prepareVersion() << " "; + return true; + } + else + { + throw IllegalArgument("Unknown option: "_ostr + argument); + } + } + else if (argument.startsWith("@")) + { + // Command file processing + FILE* cmdFile = fopen(argument.copy(1).getStr(), "r"); + if (cmdFile == nullptr) + { + std::cerr << "Cannot open command file: " << argument.copy(1) << ' '; + std::cerr << prepareHelp(); + return false; + } + + std::vector<char*> rargv; + char buffer[1024]; // Increased buffer size for safety + while (fscanf(cmdFile, "%1023s", buffer) != EOF) + { + rargv.push_back(strdup(buffer)); + } + fclose(cmdFile); + + if (!initOptions(rargv.size(), rargv.data(), true)) + { + // Free memory and propagate failure + for (char* arg : rargv) + { + free(arg); + } + return false; + } + + // Free memory + for (char* arg : rargv) + { + free(arg); + } + } + else + { + // It's an input file + m_inputFiles.emplace_back(argument); + } + } + + if (!bCmdFile) // Perform final checks only on the top-level call + { + if (m_inputFiles.empty()) + { + throw IllegalArgument("At least one .rdb input file must be specified."_ostr); + } + if (!isValid("-O"_ostr)) + { + throw IllegalArgument("Output directory '-O <path>' must be specified."_ostr); + } + } + + return true; +} + +OString PythonOptions::prepareHelp() +{ + OString programName = m_program; + if (programName.isEmpty()) + { + programName = "pythonmaker"_ostr; + } + + OString help + = prepareVersion() + " " + programName + + " - UNO Type Library to Python Stub Generator " + "About: " + " This tool generates Python stub files (.pyi) from UNO type libraries (.rdb). " + " These stubs enable static type checking and provide rich " + " auto-completion for the LibreOffice API in modern code editors. " + "Usage: " + " " + + programName + + " -O <out_dir> [options] <input.rdb>... " + + "Required Arguments: " + " -O, --output-dir <path> " + " The root directory for the generated Python stub package. The necessary " + " module directories (e.g., 'com/sun/star/') will be created inside. " + + "Filtering and Dependency Options: " + " -T, --types <type_list> " + " A semicolon-separated list of types to generate. Wildcards are supported. " + " If omitted, all types from the primary input RDB files are generated. " + " -X, --extra-types <file.rdb> " + " Load an extra RDB for dependency resolution (e.g., for base classes) " + " without generating stubs for its types. Can be specified multiple times. " + " -nD " + " No dependent types. Only generates stubs for the types explicitly matched " + " by the -T filter. " + + "Generation Control: " + " -G Generate a file only if it does not already exist. " + " -Gc Generate a file only if its content would change. " + + "Other Options: " + " -v, --verbose Enable verbose logging of processed types and created files. " + " -h, --help Display this help message and exit. " + " --version Display version information and exit. " + " @<cmdfile> Read arguments from a file (one argument per line). " + + "Examples: " + " 1. Generate all stubs from 'acme_api.rdb' into a 'stubs' directory: " + " " + + programName + + " -O ./stubs ./acme_api.rdb " + " 2. Generate stubs for only the 'org.company.widgets' module: " + " " + + programName + + " -O ./stubs -T \"org.company.widgets.*\" ./acme_api.rdb " + " 3. Generate a specific interface, using 'core_types.rdb' for dependencies " + " (like com.sun.star.uno.XInterface), without generating 'core_types.rdb' itself: " + " " + + programName + + " -O ./stubs -T org.company.widgets.XButton -X ./core_types.rdb ./acme_api.rdb " + + " "_ostr; + + return help; +} + +OString PythonOptions::prepareVersion() const { return m_program + " Version 1.0 "_ostr; } + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/codemaker/source/pythonmaker/pythonoptions.hxx b/codemaker/source/pythonmaker/pythonoptions.hxx new file mode 100644 index 000000000000..62f67eecf536 --- /dev/null +++ b/codemaker/source/pythonmaker/pythonoptions.hxx @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include <codemaker/options.hxx> +#include <rtl/string.hxx> +#include <rtl/ustring.hxx> + +class PythonOptions : public Options +{ +public: + PythonOptions(); + + bool initOptions(int ac, char* av[], bool bCmdFile = false) override; + OString prepareHelp() override; + OString prepareVersion() const; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/codemaker/source/pythonmaker/pythontype.cxx b/codemaker/source/pythonmaker/pythontype.cxx new file mode 100644 index 000000000000..db33afee195e --- /dev/null +++ b/codemaker/source/pythonmaker/pythontype.cxx @@ -0,0 +1,1306 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/config.h> + +#include "pythonoptions.hxx" +#include "pythontype.hxx" + +#include <codemaker/codemaker.hxx> +#include <codemaker/exceptiontree.hxx> +#include <codemaker/generatedtypeset.hxx> +#include <codemaker/global.hxx> +#include <codemaker/options.hxx> +#include <codemaker/typemanager.hxx> +#include <codemaker/unotype.hxx> +#include <osl/diagnose.h> +#include <rtl/strbuf.hxx> +#include <rtl/string.hxx> +#include <rtl/ustring.hxx> +#include <unoidl/unoidl.hxx> + +#include <algorithm> +#include <iostream> +#include <set> +#include <vector> + +#include <frozen/bits/defines.h> +#include <frozen/bits/elsa_std.h> +#include <frozen/unordered_set.h> + +namespace codemaker::pythonmaker +{ +namespace +{ +//All 36 keywords in python which will handle the case where variable names in idl files common with python keywords +constexpr auto PYTHON_KEYWORDS = frozen::make_unordered_set<std::string_view>( + { "False", "None", "True", "and", "as", "assert", "async", "await", "break", + "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", + "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", + "or", "pass", "raise", "return", "try", "while", "with", "yield" }); + +std::set<OUString> g_initPyiCache; + +//generates the proper naming of the import statements (the alias is a '_' that handles the file name conflicting with keywords) +OString generateImportStatementLocal(std::string_view fromModule, std::string_view importName, + std::string_view alias) +{ + OString statement = "from "_ostr + codemaker::convertString(b2u(fromModule)) + " import "_ostr + + codemaker::convertString(b2u(importName)); + if (!alias.empty()) + { + statement += " as "_ostr + codemaker::convertString(b2u(alias)); + } + return statement; +} +} + +//If idl has same varialbe name as a python keywords this function adds a '_' after it to avoid error. +// e.g varialbe 'and' changes to 'and_' +OString getSafePythonIdentifier(const OString& unoIdentifier) +{ + if (PYTHON_KEYWORDS.count(unoIdentifier)) + { + return unoIdentifier + "_"_ostr; + } + return unoIdentifier; +} + +OString unoNameToPyModulePath(const OUString& unoName) +{ + return codemaker::convertString(unoName).replace( + '.', '/'); //replaces the com.sun.star.... to com/sun/star/.... +} + +//Maps the Uno types to its respective Pythontype +OString mapUnoTypeToPythonHint(std::u16string_view unoTypeName, TypeManager const& typeManager, + OUString const& currentModuleUnoName, + OString const& currentClassName, std::set<OString>& imports, + std::set<OUString>& dependentTypes) +{ + if (unoTypeName.empty() || unoTypeName == u"void") + { + // UNO 'void' type maps to Python's 'None' + return "None"_ostr; + } + + OUString nucleus; //the core ,base name of the type + sal_Int32 rank; // The number of 'sequence<...>' wrappers (0 for non-sequences) + std::vector<OUString> + arguments; // Holds type parameters for generic/polymorphic structs. Empty for non-generics + codemaker::UnoType::Sort sort = typeManager.decompose( + unoTypeName, true, &nucleus, &rank, &arguments, nullptr); //fundamental kind of the type + + OString pyNucleusHint; //The fundamental kind of the UNO type + + switch (sort) + { + case codemaker::UnoType::Sort::Boolean: + pyNucleusHint = "bool"_ostr; + break; + case codemaker::UnoType::Sort::Byte: + case codemaker::UnoType::Sort::Short: + case codemaker::UnoType::Sort::UnsignedShort: + case codemaker::UnoType::Sort::Long: + case codemaker::UnoType::Sort::UnsignedLong: + case codemaker::UnoType::Sort::Hyper: + case codemaker::UnoType::Sort::UnsignedHyper: + // All UNO integer types map to Python's single, arbitrary-precision 'int' type. + pyNucleusHint = "int"_ostr; + break; + case codemaker::UnoType::Sort::Float: + case codemaker::UnoType::Sort::Double: //no separate double type in python + pyNucleusHint = "float"_ostr; + break; + case codemaker::UnoType::Sort::Char: //no separate char type in python + case codemaker::UnoType::Sort::String: + pyNucleusHint = "str"_ostr; + break; + case codemaker::UnoType::Sort::Type: + // UNO's 'type' concept requires 'Type' from Python's 'typing' module for hints + imports.insert(generateImportStatementLocal("typing", "Type", "")); + imports.insert(generateImportStatementLocal("typing", "Any", "")); + pyNucleusHint = "Type[Any]"_ostr; + break; + case codemaker::UnoType::Sort::Any: + imports.insert(generateImportStatementLocal("typing", "Any", "")); + pyNucleusHint = "Any"_ostr; + break; + + case codemaker::UnoType::Sort::Enum: + case codemaker::UnoType::Sort::PlainStruct: + case codemaker::UnoType::Sort::Exception: + case codemaker::UnoType::Sort::Interface: + case codemaker::UnoType::Sort::InstantiatedPolymorphicStruct: + { + OString pySimpleNameOfNucleus; // This will hold the final, safe Python class name + sal_Int32 lastDotInNucleus = nucleus.lastIndexOf('.'); + // Extract the simple name (e.g., "XInterface" from "com.sun.star.uno.XInterface") + if (lastDotInNucleus != -1) + { + pySimpleNameOfNucleus = getSafePythonIdentifier( + codemaker::convertString(OUString(nucleus.subView(lastDotInNucleus + 1)))); + } + else + { + pySimpleNameOfNucleus = getSafePythonIdentifier(codemaker::convertString(nucleus)); + } + + // Construct the full UNO name of the class currently being generated for comparison + OUString currentFullUnoName = currentModuleUnoName.isEmpty() + ? b2u(currentClassName) + : currentModuleUnoName + u"." + b2u(currentClassName); + + // Check for a forward reference (a type referring to itself) + if (nucleus == currentFullUnoName) + { + // If the type hint is for the very class we are defining, use a string literal + // to prevent a "name not yet defined" error + pyNucleusHint = "'"_ostr + pySimpleNameOfNucleus + "'"_ostr; + } + else + { + // The type is defined elsewhere, so we must import it and add it as a dependency + dependentTypes.insert(nucleus); + + OString fullModulePathForImport = codemaker::convertString(nucleus); + OString classToImport = pySimpleNameOfNucleus; + // Create a unique, safe alias to prevent any potential import name collisions + OString alias = fullModulePathForImport.replace('.', '_') + "_"_ostr; + + imports.insert( + generateImportStatementLocal(fullModulePathForImport, classToImport, alias)); + + // Use the safe alias as the type hint in the generated code + pyNucleusHint = alias; + } + + // Handle generic type parameters, e.g., for Pair<string, long>. + if (sort == codemaker::UnoType::Sort::InstantiatedPolymorphicStruct + && !arguments.empty()) + { + pyNucleusHint += "["; + for (size_t i = 0; i < arguments.size(); ++i) + { + // Recursively call this same function for each type inside the <...>. + // This allows for nesting, like Pair<string, Sequence<long>>. + pyNucleusHint + += mapUnoTypeToPythonHint(arguments[i], typeManager, currentModuleUnoName, + currentClassName, imports, dependentTypes); + if (i < arguments.size() - 1) + { + pyNucleusHint += ", "; //add ',' between parameters + } + } + pyNucleusHint += "]"; + } + break; + } + default: + imports.insert(generateImportStatementLocal("typing", "Any", "")); + pyNucleusHint = "Any"_ostr; // Fallback to 'Any' for unhandled or unknown types + break; + } + + if (rank > 0) + { + // If rank > 0, the UNO type is a sequence (e.g., sequence<long>). + imports.insert(generateImportStatementLocal("typing", "List", "")); + OString finalHint = pyNucleusHint; + + // Wrap the hint in "List[...]" for each level of nesting + // The 'rank' variable indicates how many wrappers are needed + for (sal_Int32 i = 0; i < rank; ++i) + { + finalHint = "List["_ostr + finalHint + "]"_ostr; + } + return finalHint; + } + return pyNucleusHint; +} + +// Create __init__.pyi files for the corresponding directory structure +// __init__.pyi file allows a directory to get identified as a python directory +void ensureInitPyi(const OString& baseOutputDir, const OUString& unoModuleName) +{ + if (unoModuleName.isEmpty() || g_initPyiCache.count(unoModuleName)) + { + // Optimization: If the module is empty or we've already processed it in this run, do nothing. + return; + } + + // Recursively ensure the parent module is also created. + // e.g., for "com.sun.star.sheet", this will first ensure "com.sun.star". + sal_Int32 lastDot = unoModuleName.lastIndexOf('.'); + if (lastDot != -1) + { + ensureInitPyi(baseOutputDir, unoModuleName.copy(0, lastDot)); + } + + // Now, handle the current module level. + OString initPyiNameAsType = codemaker::convertString(unoModuleName) + ".__init__"_ostr; + OString initPyiPath = createFileNameFromType(baseOutputDir, initPyiNameAsType, ".pyi"_ostr); + + // Because of the recursion and cache, we only need to check the final file. + // The parent directories are guaranteed to exist by the recursive call. + if (!fileExists(initPyiPath)) + { + FileStream initFile; + // The directory should already exist due to the parent's createFileNameFromType call. + // We just need a temp file to atomically create the __init__.pyi. + OString parentDir = getTempDir(initPyiPath); + initFile.createTempFile(parentDir); + + if (initFile.isValid()) + { + OString tempInitName = initFile.getName(); + initFile << "# Auto-generated __init__.pyi by pythonmaker "_ostr; + initFile.close(); + if (!makeValidTypeFile(initPyiPath, tempInitName, false)) + { + std::cerr << "Warning: Could not create/finalize __init__.pyi at " << initPyiPath + << std::endl; + } + } + else + { + std::cerr << "Warning: Could not create temp file for __init__.pyi in " << parentDir + << std::endl; + } + } + + // Mark this module as processed so we don't do it again. + g_initPyiCache.insert(unoModuleName); +} + +PythonStubGenerator::PythonStubGenerator(OUString const& name, + rtl::Reference<TypeManager> const& manager, + codemaker::GeneratedTypeSet& generatedSet, + PythonOptions const& options) + : m_unoName(name) // Store the full UNO name of the current type. + , m_typeManager(manager) // Store the reference to the type manager. + , m_generatedSet(generatedSet) // Store the reference to the set of generated types. + , m_options(options) // Store the command-line options. + , m_indentLevel(0) // Initialize indentation level to 0. +{ + // Retrieve the base output directory from the command-line options (e.g., the path after -O) + m_baseOutputDir = m_options.getOption("-O"_ostr); + m_verbose = m_options.isValid("--verbose"_ostr); + // Deconstruct the full UNO name into its module path and the simple class name + sal_Int32 lastDot = m_unoName.lastIndexOf('.'); + if (lastDot != -1) + { + m_moduleName = m_unoName.copy(0, lastDot); + m_pyClassName = u2b(m_unoName.subView(lastDot + 1)); + } + else + { + m_moduleName = OUString(); + m_pyClassName = u2b(m_unoName); + } + m_pySafeClassName = getSafePythonIdentifier(m_pyClassName); +} + +void PythonStubGenerator::generate() +{ + if (m_pyClassName.isEmpty()) + { + return; + } + + rtl::Reference<unoidl::Entity> entity; + codemaker::UnoType::Sort sort = m_typeManager->getSort(m_unoName, &entity); + + m_filePath + = createFileNameFromType(m_baseOutputDir, unoNameToPyModulePath(m_unoName), ".pyi"_ostr); + + // Get the UNO type to display in the --verbose(-V) option + if (m_verbose) + { + const char* sortName = "unknown"; + switch (sort) + { + case codemaker::UnoType::Sort::Enum: + sortName = "enum"; + break; + case codemaker::UnoType::Sort::ConstantGroup: + sortName = "constants"; + break; + case codemaker::UnoType::Sort::Typedef: + sortName = "typedef"; + break; + case codemaker::UnoType::Sort::PlainStruct: + sortName = "struct"; + break; + case codemaker::UnoType::Sort::Exception: + sortName = "exception"; + break; + case codemaker::UnoType::Sort::PolymorphicStructTemplate: + sortName = "polystruct"; + break; + case codemaker::UnoType::Sort::Interface: + sortName = "interface"; + break; + case codemaker::UnoType::Sort::SingleInterfaceBasedService: + sortName = "service"; + break; + case codemaker::UnoType::Sort::InterfaceBasedSingleton: + sortName = "singleton"; + break; + default: + break; + } + //display the message in here + std::cout << "[" << sortName << "] " << u2b(m_unoName) << " -> " << m_filePath << std::endl; + } + + // Check if the file already exists to respect the -G (generate if not exists) option + if (fileExists(m_filePath)) + { + if (m_options.isValid("-G"_ostr)) + return; + } + + // Creating a temporary files for safety and atomicity + FileStream tempFile; + tempFile.createTempFile(getTempDir(m_filePath)); + if (!tempFile.isValid()) + { + //if the temporary files aren't created something might be wrong with the permissions + OUString errorMessage = u"Cannot create temporary file for "_ustr; + errorMessage += b2u(m_filePath); + throw CannotDumpException(errorMessage); + } + OString tempFilePath = tempFile.getName(); + + /*The main dispatch logic. Based on the 'sort' of the UNO type, call the + appropriate specialized generator method.*/ + switch (sort) + { + case codemaker::UnoType::Sort::Enum: + generateEnum(static_cast<unoidl::EnumTypeEntity*>(entity.get())); + break; + case codemaker::UnoType::Sort::ConstantGroup: + generateConstantGroup(static_cast<unoidl::ConstantGroupEntity*>(entity.get())); + break; + case codemaker::UnoType::Sort::Typedef: + generateTypedef(static_cast<unoidl::TypedefEntity*>(entity.get())); + break; + case codemaker::UnoType::Sort::PlainStruct: + generateStruct(static_cast<unoidl::PlainStructTypeEntity*>(entity.get())); + break; + case codemaker::UnoType::Sort::Exception: + generateException(static_cast<unoidl::ExceptionTypeEntity*>(entity.get())); + break; + case codemaker::UnoType::Sort::PolymorphicStructTemplate: + generatePolyStruct( + static_cast<unoidl::PolymorphicStructTypeTemplateEntity*>(entity.get())); + break; + case codemaker::UnoType::Sort::Interface: + generateInterface(static_cast<unoidl::InterfaceTypeEntity*>(entity.get())); + break; + case codemaker::UnoType::Sort::SingleInterfaceBasedService: + generateService(static_cast<unoidl::SingleInterfaceBasedServiceEntity*>(entity.get())); + break; + case codemaker::UnoType::Sort::InterfaceBasedSingleton: + generateSingleton(static_cast<unoidl::InterfaceBasedSingletonEntity*>(entity.get())); + break; + default: + // For any UNO type we don't handle explicitly yet, generate a minimal placeholder + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + m_buffer.append(": "); + indent(); + m_buffer.append(getIndent()); + m_buffer.append("pass "); + dedent(); + break; + } + + // Assemble the final content of the .pyi file in the correct order + OStringBuffer finalBuffer( + "# Auto-generated by pythonmaker - DO NOT EDIT. from __future__ import annotations "); + if (!m_imports.empty()) + { + finalBuffer.append(" "); + } + // Add collected imports + for (const auto& imp : m_imports) + { + finalBuffer.append(imp + " "_ostr); + } + if (!m_imports.empty() || !m_buffer.isEmpty()) + { + finalBuffer.append(" "); + } + finalBuffer.append(m_buffer.makeStringAndClear()); // Append the main content + + // Write the fully assembled content to the temporary file. + tempFile << finalBuffer; + tempFile.close(); + + //checks if the new overwritten file will be same as the current file + if (!makeValidTypeFile(m_filePath, tempFilePath, m_options.isValid("-Gc"_ostr))) + { + OUString errorMessage = u"Cannot finalize file "_ustr; + errorMessage += b2u(m_filePath); + throw CannotDumpException(errorMessage); + } + + // the -nD option prevent generation of dependent types + if (!m_options.isValid("-nD"_ostr)) + { + m_generatedSet.add(u2b(m_unoName)); + for (const auto& dep : m_dependentTypes) + { + produce(dep, m_typeManager, m_generatedSet, m_options); + } + } +} + +void PythonStubGenerator::addImportLine(const OString& importLine) +{ + if (!importLine.isEmpty()) + { + m_imports.insert(importLine); + } +} + +std::vector<unoidl::PlainStructTypeEntity::Member> +PythonStubGenerator::getAllStructMembers(unoidl::PlainStructTypeEntity* entity) +{ + std::vector<unoidl::PlainStructTypeEntity::Member> members; + + // Get members from base classes first by recursing + if (!entity->getDirectBase().isEmpty()) + { + rtl::Reference<unoidl::Entity> baseEntity; + // Look up the base entity in the TypeManager + if (m_typeManager->getSort(entity->getDirectBase(), &baseEntity) + == codemaker::UnoType::Sort::PlainStruct) + { + unoidl::PlainStructTypeEntity* baseStructEntity + = static_cast<unoidl::PlainStructTypeEntity*>(baseEntity.get()); + members = getAllStructMembers(baseStructEntity); // Recursive call + } + } + + // Add members from the current entity + const auto& directMembers = entity->getDirectMembers(); + members.insert(members.end(), directMembers.begin(), directMembers.end()); + + return members; +} + +void PythonStubGenerator::generatePolyStruct(unoidl::PolymorphicStructTypeTemplateEntity* entity) +{ + addImportLine("from typing import TypeVar, Generic"_ostr); //imports to handle generic types + OStringBuffer typeVarBuffer; + OStringBuffer genericParams; // For the Generic[T, K] part + bool firstParam = true; + for (const auto& paramName : entity->getTypeParameters()) + { + OString safeParamName = getSafePythonIdentifier(codemaker::convertString(paramName)); + typeVarBuffer.append(safeParamName + " = TypeVar(\""_ostr); + typeVarBuffer.append(safeParamName + "\") "_ostr); + + if (!firstParam) + { + genericParams.append(", "); + } + genericParams.append(safeParamName); + firstParam = false; + } + typeVarBuffer.append(" "); + // Prepend this to the main buffer + m_buffer.insert(m_buffer.getLength(), typeVarBuffer.makeStringAndClear()); + + // Generate the class definition + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + m_buffer.append("(Generic["); + m_buffer.append(genericParams.makeStringAndClear()); + m_buffer.append("]): "); + indent(); + + // Generate the __init__ method + m_buffer.append(getIndent()); + m_buffer.append("def __init__(self"); + const auto& members = entity->getMembers(); + for (const auto& member : members) + { + OString safeMemberName = getSafePythonIdentifier(codemaker::convertString(member.name)); + OString memberTypeHint; + if (member.parameterized) + { + // It's a generic type parameter like 'T' + memberTypeHint = getSafePythonIdentifier(codemaker::convertString(member.type)); + } + else + { + // It's a concrete type, map it normally + memberTypeHint = mapUnoTypeToPythonHint(member.type, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + } + m_buffer.append(", "); + m_buffer.append(safeMemberName); + m_buffer.append(": "); + m_buffer.append(memberTypeHint); + m_buffer.append(" | None = ..."); + } + m_buffer.append(") -> None: "); + indent(); + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + m_buffer.append(" "); + + // Generate typed attributes + if (members.empty()) + { + m_buffer.append(getIndent()); + m_buffer.append("pass "); + } + else + { + for (const auto& member : members) + { + OString safeMemberName = getSafePythonIdentifier(codemaker::convertString(member.name)); + OString memberTypeHint; + if (member.parameterized) + { + memberTypeHint = getSafePythonIdentifier(codemaker::convertString(member.type)); + } + else + { + memberTypeHint = mapUnoTypeToPythonHint(member.type, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + } + m_buffer.append(getIndent()); + m_buffer.append(safeMemberName); + m_buffer.append(": "); + m_buffer.append(memberTypeHint); + m_buffer.append(" "); + } + } + dedent(); +} + +std::vector<unoidl::ExceptionTypeEntity::Member> +PythonStubGenerator::getAllExceptionMembers(unoidl::ExceptionTypeEntity* entity) +{ + std::vector<unoidl::ExceptionTypeEntity::Member> members; + + // Recurse up the inheritance chain + OUString baseName = entity->getDirectBase(); + if (!baseName.isEmpty()) + { + // Stop recursion at the root UNO Exception, as its members (Message, Context) + // are handled specially or are part of Python's built-in Exception. + if (baseName != u"com.sun.star.uno.Exception") + { + rtl::Reference<unoidl::Entity> baseEntity; + if (m_typeManager->getSort(baseName, &baseEntity) + == codemaker::UnoType::Sort::Exception) + { + unoidl::ExceptionTypeEntity* baseExceptionEntity + = static_cast<unoidl::ExceptionTypeEntity*>(baseEntity.get()); + members = getAllExceptionMembers(baseExceptionEntity); + } + } + } + + // Add members from the current entity + const auto& directMembers = entity->getDirectMembers(); + members.insert(members.end(), directMembers.begin(), directMembers.end()); + return members; +} + +void PythonStubGenerator::generateEnum(unoidl::EnumTypeEntity* entity) +{ + //just using the enum package provided in python itself + addImportLine("import enum"_ostr); + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + m_buffer.append("(enum.Enum): "); + indent(); + + const auto& members = entity->getMembers(); + if (members.empty()) + { + m_buffer.append(getIndent()); + m_buffer.append("pass "); + } + else + { + for (const auto& member : members) + { + OString safeMemberName = getSafePythonIdentifier(u2b(member.name)); + m_buffer.append(getIndent()); + m_buffer.append(safeMemberName); + m_buffer.append(" = "); + m_buffer.append(OString::number(member.value)); + m_buffer.append(" "); + } + } + dedent(); +} + +void PythonStubGenerator::generateConstantGroup(unoidl::ConstantGroupEntity* entity) +{ + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + m_buffer.append(": "); + indent(); + + const auto& members = entity->getMembers(); + if (members.empty()) + { + m_buffer.append(getIndent()); + m_buffer.append("# No constants defined in this group pass "); // Or just an empty file + } + else + { + for (const auto& member : members) + { + OString safeConstName = getSafePythonIdentifier(u2b(member.name)); + + OString pyValue; + OString pyTypeHint; + + switch (member.value.type) + { + case unoidl::ConstantValue::TYPE_BOOLEAN: + pyValue = member.value.booleanValue ? "True"_ostr : "False"_ostr; + pyTypeHint = "bool"_ostr; + break; + case unoidl::ConstantValue::TYPE_BYTE: + pyValue = OString::number(member.value.byteValue); + pyTypeHint = "int"_ostr; // Python bytes are different + break; + case unoidl::ConstantValue::TYPE_SHORT: + pyValue = OString::number(member.value.shortValue); + pyTypeHint = "int"_ostr; + break; + case unoidl::ConstantValue::TYPE_UNSIGNED_SHORT: + pyValue = OString::number(member.value.unsignedShortValue); + pyTypeHint = "int"_ostr; + break; + case unoidl::ConstantValue::TYPE_LONG: + pyValue = OString::number(member.value.longValue); + pyTypeHint = "int"_ostr; + break; + case unoidl::ConstantValue::TYPE_UNSIGNED_LONG: + pyValue + = OString::number(static_cast<sal_Int32>(member.value.unsignedLongValue)); + pyTypeHint = "int"_ostr; + break; + case unoidl::ConstantValue::TYPE_HYPER: + pyValue = OString::number(member.value.hyperValue); + pyTypeHint = "int"_ostr; + break; + case unoidl::ConstantValue::TYPE_UNSIGNED_HYPER: + pyValue + = OString::number(static_cast<sal_Int64>(member.value.unsignedHyperValue)); + pyTypeHint = "int"_ostr; + break; + case unoidl::ConstantValue::TYPE_FLOAT: + pyValue = OString::number(member.value.floatValue); + pyTypeHint = "float"_ostr; + break; + case unoidl::ConstantValue::TYPE_DOUBLE: + pyValue = OString::number(member.value.doubleValue); + pyTypeHint = "float"_ostr; // Python float is double precision + break; + default: + // Should not happen for valid constant groups + pyValue = "... # Unknown constant type"_ostr; + pyTypeHint = "Any"_ostr; + addImportLine("from typing import Any"_ostr); + break; + } + + m_buffer.append(getIndent()); + m_buffer.append(safeConstName); + m_buffer.append(": "); + m_buffer.append(pyTypeHint); + m_buffer.append(" = "); + m_buffer.append(pyValue); + m_buffer.append(" "); + } + } + dedent(); +} + +void PythonStubGenerator::generateTypedef(unoidl::TypedefEntity* entity) +{ + OUString originalUnoType = entity->getType(); + OString pythonTypeHintForOriginal = mapUnoTypeToPythonHint( + originalUnoType, *m_typeManager, m_moduleName, + m_pyClassName, // The UNO module name of the current typedef file + m_imports, // Set to collect import strings for this .pyi file + m_dependentTypes // Set to collect UNO type names that need to be generated + ); + + m_buffer.append(m_pySafeClassName); + m_buffer.append(" = "); + m_buffer.append(pythonTypeHintForOriginal); + m_buffer.append(" "); +} + +void PythonStubGenerator::generateStruct(unoidl::PlainStructTypeEntity* entity) +{ + // Handle the base class (inheritance) + OString baseClass = "object"_ostr; // Default base for Python classes + if (!entity->getDirectBase().isEmpty()) + { + baseClass = mapUnoTypeToPythonHint(entity->getDirectBase(), *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + } + + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + m_buffer.append("("); + m_buffer.append(baseClass); + m_buffer.append("): "); + indent(); + + // Generate the __init__ method signature + m_buffer.append(getIndent()); + m_buffer.append("def __init__(self"); + + // We need to collect all members, from this struct and all its base structs. + // This requires a helper function to walk the inheritance tree. + std::vector<unoidl::PlainStructTypeEntity::Member> allMembers = getAllStructMembers(entity); + + for (const auto& member : allMembers) + { + OString safeMemberName = getSafePythonIdentifier(codemaker::convertString(member.name)); + OString memberTypeHint = mapUnoTypeToPythonHint(member.type, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + + m_buffer.append(", "); + m_buffer.append(safeMemberName); + m_buffer.append(": "); + m_buffer.append(memberTypeHint); + m_buffer.append(" | None = ..."); + } + m_buffer.append(") -> None: "); + indent(); + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + m_buffer.append(" "); + + // Generate typed attributes for all members + if (allMembers.empty()) + { + m_buffer.append(getIndent()); + m_buffer.append("pass "); + } + else + { + for (const auto& member : allMembers) + { + OString safeMemberName = getSafePythonIdentifier(codemaker::convertString(member.name)); + OString memberTypeHint + = mapUnoTypeToPythonHint(member.type, *m_typeManager, m_moduleName, m_pyClassName, + m_imports, m_dependentTypes); + m_buffer.append(getIndent()); + m_buffer.append(safeMemberName); + m_buffer.append(": "); + m_buffer.append(memberTypeHint); + m_buffer.append(" "); + } + } + dedent(); +} + +void PythonStubGenerator::generateException(unoidl::ExceptionTypeEntity* entity) +{ + // Determine the base class for Python stub + OString baseClass; + OUString unoBaseName = entity->getDirectBase(); + bool isRootUnoException = (m_unoName == u"com.sun.star.uno.Exception"); + if (unoBaseName.isEmpty() || isRootUnoException) + { + // If it has no base or the base is the root UNO Exception, + // it should inherit from Python's built-in Exception. + baseClass = "Exception"_ostr; + } + else + { + // Inherit from the stub of its direct UNO base exception. + baseClass = mapUnoTypeToPythonHint(unoBaseName, *m_typeManager, m_moduleName, m_pyClassName, + m_imports, m_dependentTypes); + } + + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + if (isRootUnoException && m_pySafeClassName == "Exception") + { + m_buffer.append("(object): "); + } + else + { + m_buffer.append("("); + m_buffer.append(baseClass); + m_buffer.append("): "); + } + indent(); + + // Get all members, including those from base UNO exceptions + std::vector<unoidl::ExceptionTypeEntity::Member> allMembers = getAllExceptionMembers(entity); + + m_buffer.append(getIndent()); + m_buffer.append("def __init__(self"); + + for (const auto& member : allMembers) + { + OString safeMemberName = getSafePythonIdentifier(codemaker::convertString(member.name)); + OString memberTypeHint = mapUnoTypeToPythonHint(member.type, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + + m_buffer.append(", "); + m_buffer.append(safeMemberName); + m_buffer.append(": "); + m_buffer.append(memberTypeHint); + m_buffer.append(" | None = ..."); + } + + m_buffer.append(", *args: Any, **kwargs: Any) -> None: "); + addImportLine("from typing import Any"_ostr); + + indent(); + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + m_buffer.append(" "); + + for (const auto& member : allMembers) + { + OString safeMemberName = getSafePythonIdentifier(codemaker::convertString(member.name)); + OString memberTypeHint = mapUnoTypeToPythonHint(member.type, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + + m_buffer.append(getIndent()); + m_buffer.append(safeMemberName); + m_buffer.append(": "); + m_buffer.append(memberTypeHint); + m_buffer.append(" "); + } + + dedent(); +} + +void PythonStubGenerator::generateInterface(unoidl::InterfaceTypeEntity* entity) +{ + // Determine and generate the list of base classes. + OStringBuffer baseClassesStr; + const auto& bases = entity->getDirectMandatoryBases(); + + // The root UNO interface, XInterface, should inherit from ABC. All others + // will then inherit it transitively. + if (m_unoName == u"com.sun.star.uno.XInterface" || bases.empty()) + { + addImportLine("from abc import ABC, abstractmethod"_ostr); + baseClassesStr.append("ABC"); + } + else + { + addImportLine( + "from abc import abstractmethod"_ostr); //no need to import ABC as its already inheriting from XInteface + bool firstBase = true; + for (const auto& base : bases) + { + if (!firstBase) + { + baseClassesStr.append(", "); + } + baseClassesStr.append(mapUnoTypeToPythonHint(base.name, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, + m_dependentTypes)); + firstBase = false; + } + } + + // Write the class definition line. + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + m_buffer.append("("); + m_buffer.append(baseClassesStr.makeStringAndClear()); + m_buffer.append("): "); + indent(); + + const auto& attributes = entity->getDirectAttributes(); + const auto& methods = entity->getDirectMethods(); + bool hasNewMembers = !attributes.empty() || !methods.empty(); + + if (!hasNewMembers) + { + // Add a helper abstract method to explicitly mark this class as abstract. + m_buffer.append(getIndent()); + m_buffer.append("@abstractmethod "); + m_buffer.append(getIndent()); + m_buffer.append("def __uno_abstract__(self) -> None: "); + indent(); + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + } + else + { + // Generate properties and methods for members defined directly on this interface. + for (const auto& attr : attributes) + { + OString safeAttrName = getSafePythonIdentifier(codemaker::convertString(attr.name)); + OString attrTypeHint + = mapUnoTypeToPythonHint(attr.type, *m_typeManager, m_moduleName, m_pyClassName, + m_imports, m_dependentTypes); + + m_buffer.append(getIndent()); + m_buffer.append("@property "); + m_buffer.append(getIndent()); + m_buffer.append("@abstractmethod "); + m_buffer.append(getIndent()); + m_buffer.append("def "); + m_buffer.append(safeAttrName); + m_buffer.append("(self) -> "); + m_buffer.append(attrTypeHint); + m_buffer.append(": "); + indent(); + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + + if (!attr.readOnly) + { + m_buffer.append(getIndent()); + m_buffer.append("@"); + m_buffer.append(safeAttrName); + m_buffer.append(".setter "); + m_buffer.append(getIndent()); + m_buffer.append("@abstractmethod "); + m_buffer.append(getIndent()); + m_buffer.append("def "); + m_buffer.append(safeAttrName); + m_buffer.append("(self, value: "); + m_buffer.append(attrTypeHint); + m_buffer.append(") -> None: "); + indent(); + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + } + m_buffer.append(" "); + } + + for (const auto& method : methods) + { + OString safeMethodName = getSafePythonIdentifier(codemaker::convertString(method.name)); + m_buffer.append(getIndent()); + m_buffer.append("@abstractmethod "); + m_buffer.append(getIndent()); + m_buffer.append("def "); + m_buffer.append(safeMethodName); + m_buffer.append("(self"); + + for (const auto& param : method.parameters) + { + OString safeParamName + = getSafePythonIdentifier(codemaker::convertString(param.name)); + OString paramTypeHint + = mapUnoTypeToPythonHint(param.type, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + m_buffer.append(", "); + m_buffer.append(safeParamName); + m_buffer.append(": "); + m_buffer.append(paramTypeHint); + } + + OString returnTypeHint + = mapUnoTypeToPythonHint(method.returnType, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + + m_buffer.append(") -> "); + m_buffer.append(returnTypeHint); + m_buffer.append(": "); + indent(); + + if (!method.exceptions.empty()) + { + m_buffer.append(getIndent()); + m_buffer.append("\"\"\"Raises: "); + for (const auto& exc : method.exceptions) + { + OString excHint + = mapUnoTypeToPythonHint(exc, *m_typeManager, m_moduleName, m_pyClassName, + m_imports, m_dependentTypes); + m_buffer.append(getIndent()); + m_buffer.append(" "); + m_buffer.append(excHint); + m_buffer.append(" "); + } + m_buffer.append(getIndent()); + m_buffer.append("\"\"\" "); + } + + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + m_buffer.append(" "); + } + } + dedent(); +} + +void PythonStubGenerator::generateService(unoidl::SingleInterfaceBasedServiceEntity* entity) +{ + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + m_buffer.append(": "); + indent(); + + const auto& constructors = entity->getConstructors(); + if (constructors.empty()) + { + // If the service has no constructors defined in IDL, it cannot be created from Python. + // We generate a `pass` statement to make it a valid empty class. + m_buffer.append(getIndent()); + m_buffer.append("pass "); + } + else + { + // All factory methods will require an XComponentContext, so we resolve its Python type hint once. + OString contextTypeHint + = mapUnoTypeToPythonHint(u"com.sun.star.uno.XComponentContext", *m_typeManager, + m_moduleName, m_pyClassName, m_imports, m_dependentTypes); + + // Iterate through each constructor defined in the IDL. Each constructor + // will become a static factory method in the Python stub. + for (const auto& ctor : constructors) + { + m_buffer.append(getIndent()); + m_buffer.append("@staticmethod "); + + OString methodName; + if (ctor.defaultConstructor) + { + methodName = "create"_ostr; + } + else + { + methodName = getSafePythonIdentifier(codemaker::convertString(ctor.name)); + } + + m_buffer.append(getIndent()); + m_buffer.append("def "); + m_buffer.append(methodName); + m_buffer.append("(ctx: "); + m_buffer.append(contextTypeHint); + + // Iterate through the constructor's parameters to build the method signature. + for (const auto& param : ctor.parameters) + { + if (param.rest) + { + addImportLine("from typing import Any"_ostr); + m_buffer.append(", *args: Any"); + break; + } + OString safeParamName + = getSafePythonIdentifier(codemaker::convertString(param.name)); + OString paramTypeHint + = mapUnoTypeToPythonHint(param.type, *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + m_buffer.append(", "); + m_buffer.append(safeParamName); + m_buffer.append(": "); + m_buffer.append(paramTypeHint); + } + + // The return type of the factory method is the service's primary interface. + OString returnTypeHint + = mapUnoTypeToPythonHint(entity->getBase(), *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + + m_buffer.append(") -> "); + m_buffer.append(returnTypeHint); + m_buffer.append(": "); + indent(); + + // If the constructor can raise exceptions, document them in a docstring. + // This is the standard Pythonic way to convey this information in stubs. + if (!ctor.exceptions.empty()) + { + m_buffer.append(getIndent()); + m_buffer.append("\"\"\"Raises: "); + for (const auto& exc : ctor.exceptions) + { + OString excHint + = mapUnoTypeToPythonHint(exc, *m_typeManager, m_moduleName, m_pyClassName, + m_imports, m_dependentTypes); + m_buffer.append(getIndent()); + m_buffer.append(" "); + m_buffer.append(excHint); + m_buffer.append(" "); + } + m_buffer.append(getIndent()); + m_buffer.append("\"\"\" "); + } + + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + m_buffer.append(" "); + } + } + dedent(); +} + +void PythonStubGenerator::generateSingleton(unoidl::InterfaceBasedSingletonEntity* entity) +{ + // A singleton is represented as a class with a static 'get' method. + // The class itself is just a namespace for this method. + m_buffer.append("class "); + m_buffer.append(m_pySafeClassName); + m_buffer.append(": "); + indent(); + + // The get() method is static. This is indicated by the @staticmethod decorator. + m_buffer.append(getIndent()); + m_buffer.append("@staticmethod "); + + m_buffer.append(getIndent()); + m_buffer.append("def get("); + + // It takes the component context as a parameter. + OString contextTypeHint + = mapUnoTypeToPythonHint(u"com.sun.star.uno.XComponentContext", *m_typeManager, + m_moduleName, m_pyClassName, m_imports, m_dependentTypes); + m_buffer.append("ctx: "); + m_buffer.append(contextTypeHint); + + // It returns an instance of its base interface. + OString returnTypeHint = mapUnoTypeToPythonHint(entity->getBase(), *m_typeManager, m_moduleName, + m_pyClassName, m_imports, m_dependentTypes); + + m_buffer.append(") -> "); + m_buffer.append(returnTypeHint); + m_buffer.append(": "); + indent(); + + // A singleton's get() method can raise a DeploymentException if it fails. + // We document this in the docstring. + OString deploymentExcHint + = mapUnoTypeToPythonHint(u"com.sun.star.uno.DeploymentException", *m_typeManager, + m_moduleName, m_pyClassName, m_imports, m_dependentTypes); + + m_buffer.append(getIndent()); + m_buffer.append("\"\"\"Raises: "); + m_buffer.append(getIndent()); + m_buffer.append(" "); + m_buffer.append(deploymentExcHint); + m_buffer.append(" "); + m_buffer.append(getIndent()); + m_buffer.append("\"\"\" "); + + m_buffer.append(getIndent()); + m_buffer.append("... "); + dedent(); + dedent(); +} + +// Functions resposible for the indentation and dedentation of the python stubs. +void PythonStubGenerator::indent() { m_indentLevel++; } +void PythonStubGenerator::dedent() +{ + if (m_indentLevel > 0) + m_indentLevel--; +} +OString PythonStubGenerator::getIndent() const +{ + OStringBuffer sb; + for (int i = 0; i < m_indentLevel; ++i) + sb.append(" "); // add a single time indentation + return sb.makeStringAndClear(); +} + +void produce(OUString const& name, rtl::Reference<TypeManager> const& manager, + codemaker::GeneratedTypeSet& generated, PythonOptions const& options) +{ + // On the very first call (the root call), clear the __init__.pyi cache. + // This is a simple way to reset the state for a new pythonmaker run if it were ever + // called multiple times in the same process. + + if (name.isEmpty()) + { + g_initPyiCache.clear(); + rtl::Reference<unoidl::MapCursor> cursor = manager->getManager()->createCursor(OUString()); + OUString memberName; + while (cursor->getNext(&memberName).is()) + { + produce(memberName, manager, generated, options); + } + return; + } + + if (generated.contains(u2b(name))) + { + return; + } + + if (!manager->foundAtPrimaryProvider(name)) + { + return; + } + + rtl::Reference<unoidl::Entity> entity; + rtl::Reference<unoidl::MapCursor> cursor; + codemaker::UnoType::Sort sort = manager->getSort(name, &entity, &cursor); + + switch (sort) + { + case codemaker::UnoType::Sort::AccumulationBasedService: + case codemaker::UnoType::Sort::ServiceBasedSingleton: + // These types do not have a direct Python stub equivalent. + // Mark as processed and do not generate a file. + generated.add(u2b(name)); + return; // Exit the function for this type + default: + break; + } + + if (sort == codemaker::UnoType::Sort::Module) + { + ensureInitPyi(options.getOption("-O"_ostr), name); + generated.add(u2b(name)); // Mark module as processed + OUString memberNameInModule; + if (cursor.is()) + { // Ensure cursor is valid + while (cursor->getNext(&memberNameInModule).is()) + { + produce(name + u"."_ustr + memberNameInModule, manager, generated, options); + } + } + } + else + { + sal_Int32 lastDot = name.lastIndexOf('.'); + if (lastDot != -1) + { + ensureInitPyi(options.getOption("-O"_ostr), name.copy(0, lastDot)); + } + PythonStubGenerator generator(name, manager, generated, options); + generator.generate(); + } +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/codemaker/source/pythonmaker/pythontype.hxx b/codemaker/source/pythonmaker/pythontype.hxx new file mode 100644 index 000000000000..60ec1481cba5 --- /dev/null +++ b/codemaker/source/pythonmaker/pythontype.hxx @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <sal/config.h> +#include <rtl/ref.hxx> +#include <rtl/string.hxx> +#include <rtl/ustring.hxx> +#include <set> +#include <vector> +#include <codemaker/codemaker.hxx> +#include <codemaker/exceptiontree.hxx> +#include <codemaker/generatedtypeset.hxx> +#include <codemaker/global.hxx> +#include <codemaker/options.hxx> +#include <codemaker/typemanager.hxx> +#include <codemaker/unotype.hxx> +#include <unoidl/unoidl.hxx> +#include <osl/diagnose.h> +#include <algorithm> +#include <iostream> +#include <frozen/unordered_set.h> + +namespace codemaker +{ +class GeneratedTypeSet; +} +class PythonOptions; +class TypeManager; +class FileStream; + +namespace codemaker::pythonmaker +{ +// free functions +rtl::OString getSafePythonIdentifier(const rtl::OString& unoIdentifier); +rtl::OString unoNameToPyModulePath(const rtl::OUString& unoName); +rtl::OString mapUnoTypeToPythonHint(std::u16string_view unoTypeName, TypeManager const& typeManager, + rtl::OUString const& currentModuleUnoName, + rtl::OString const& currentClassName, + std::set<rtl::OString>& imports, + std::set<rtl::OUString>& dependentTypes); +void ensureInitPyi(const rtl::OString& baseOutputDir, const rtl::OUString& unoModuleName); +void produce(rtl::OUString const& name, rtl::Reference<TypeManager> const& manager, + codemaker::GeneratedTypeSet& generated, PythonOptions const& options); + +// PythonStubGenerator: declarations only +class PythonStubGenerator +{ +public: + PythonStubGenerator(rtl::OUString const& name, rtl::Reference<TypeManager> const& manager, + codemaker::GeneratedTypeSet& generatedSet, PythonOptions const& options); + + void generate(); + +private: + void addImportLine(const rtl::OString& importLine); + std::vector<unoidl::PlainStructTypeEntity::Member> + getAllStructMembers(unoidl::PlainStructTypeEntity* entity); + std::vector<unoidl::ExceptionTypeEntity::Member> + getAllExceptionMembers(unoidl::ExceptionTypeEntity* entity); + void generateEnum(unoidl::EnumTypeEntity* entity); + void generateConstantGroup(unoidl::ConstantGroupEntity* entity); + void generateTypedef(unoidl::TypedefEntity* entity); + void generateStruct(unoidl::PlainStructTypeEntity* entity); + void generatePolyStruct(unoidl::PolymorphicStructTypeTemplateEntity* entity); + void generateException(unoidl::ExceptionTypeEntity* entity); + void generateInterface(unoidl::InterfaceTypeEntity* entity); + void generateService(unoidl::SingleInterfaceBasedServiceEntity* entity); + void generateSingleton(unoidl::InterfaceBasedSingletonEntity* entity); + void indent(); + void dedent(); + rtl::OString getIndent() const; + + rtl::OUString m_unoName; + rtl::Reference<TypeManager> m_typeManager; + codemaker::GeneratedTypeSet& m_generatedSet; + PythonOptions const& m_options; + rtl::OString m_baseOutputDir; + rtl::OString m_filePath; + rtl::OUString m_moduleName; + rtl::OString m_pyClassName; + rtl::OString m_pySafeClassName; + int m_indentLevel; + bool m_verbose; + std::set<rtl::OString> m_imports; + std::set<rtl::OUString> m_dependentTypes; + rtl::OStringBuffer m_buffer; +}; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */