diff --git a/README.md b/README.md index f79e2db..cf646cc 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,51 @@ -# Krita: Python Auto-complete generator -Iif you have the Krita source code, you can use this to generate the auto-complete file for Python.Many Python editors need a PY file to read for auto complete information. This script reads the C++ header files and creates a python file that can be used for auto-completion. the file is generated in the same location as the header files are. The final file is independent, so it doesn't matter where it ends up being stored. Pointing your Python editor to the file location for a module path should be enough for the auto-complete to pick up. +# Fake pyKrita: A Python Auto-completion generator +Is a Python "fake" module generator for PyKrita. Its purpose is to provide intellisense and code completion in your code editor to simplify the development process when making krita extensions. -The Python file has a lot of notes, so make sure to check that out to see exactly what is going on. +Traditionally, language servers & code editors like VS Code, PyCharm & Vim (etc) wont be able to recognize the krita module and hence throw warnings and errors at you everywhere as it simply do not know of this “krita” module, which is what “Fake pyKrita” is meant to solve. +A quick demo video: -If you look in the Output folder, you can see what the final generated file looks like. This is what you would use in your Python editor. +https://user-images.githubusercontent.com/20190653/163692838-c6f9d7a2-2b3d-4649-a077-32df41c57842.mp4 -Grab the Krita source code from here as a pre-requisite -https://github.com/KDE/krita +Originally forked from: https://github.com/scottpetrovic/krita-python-auto-complete . I built on his code to make it functional with todays code base & potentially more future proof, added parameter types into method comments & had an attempt at creating an automated deployment system to Pypi (when you run the script it will try to upload the package to Pypi immediatly, this is still quite wip). + + +## Usage & installation + +### Pre-packaged packages + +This is the easiest way of benefiting from the generator. These pre packaged packages are installed like any other python package, simply download a tar.gz file from any of the prepackaged releases https://github.com/ItsCubeTime/fake-pykrita/releases/latest & run `pip install C:/Path/To/Tar/Gz/file.tar.gz`. Make sure that you call the correct pip - the one thats associated with the intrerpreter of which your IDE is using. + +After this, restart your code editor & enjoy <3 + + +### Using the generator itself + +Download the `generate-python-autocomplete-file.py` and modify the 2 strings `kritaHomeDir` & `moduleDestinationPath` to suit your needs. + +`kritaHomeDir` should point towards the directory that has kritas main CMakeLists.txt - it should point towards the source code of Krita. + +`moduleDestinationPath` This is where the package will get created locally on your PC. Make sure you dont have any important files in this directory :3 + +Now you can run the script file with `python generate-python-autocomplete-file.py`. As of writing this, its known to work on Python 3.10.2 in a Windows 11 environment. + +### Installing the module for your code editor + +First you will need to figure out what Python installation your code editors language server uses and then find its associated pip executable. Once located, run `pip install pathToTheGeneratedTarGz.tar.gz`. Which will be live under `kritaHomeDir/pyKrita/dist` where `kritaHomeDir` is where you set the variable `kritaHomeDir` in `generate-python-autocomplete-file.py` to point towards. + + + + +### Uploading to Pypi +*As of writing this, uploading to pip may not work correctly. Still trying to figure this one out, contributions are welcome* + +As the script runs it will attempt uploading to Pypi, where "twine" will ask you for your Pypi login credentials, if you dont want to upload to pypi, you can simply hit ctrl C with your terminal focused to cancel. You will still have your generated files where you pointed your `KritaHomeDir`. + +Anyone is welcome to try & upload the generated files to Pypi <3 If you do manage to successfully upload it, please do also open a discussion at https://github.com/ItsCubeTime/fake-pykrita/discussions & drop a link to the pypi adress! + +## Contributing + +Im open to accept any PRs \^-^/ diff --git a/generate-python-autocomplete-file.py b/generate-python-autocomplete-file.py index 5bfe963..32bd90a 100644 --- a/generate-python-autocomplete-file.py +++ b/generate-python-autocomplete-file.py @@ -1,12 +1,38 @@ import glob, os, re #re allows for regular expression patterns - +generatorVersion = 0.1 # SETUP # --------------------------- # change current directory to the libkis folder in the krita source code # this is where all the python API files are at that we will read #os.chdir("C:\\dev\\krita\\libs\\libkis") # possible Windows OS dir. need to have two "\" symbols -os.chdir("/home/scottpetrovic/krita/src/libs/libkis") +# os.chdir("/home/scottpetrovic/krita/src/libs/libkis") + +kritaHomeDir = r"F:\Krita\krita".replace('\\', '/') # Change this to an apprioporite path, depending on where your Krita source code is. +# Note that you need to have the actual source code & not a prebuilt version of Krita. THIS DIRETORY SHOULD HAVE THE CMakeLists.txt FILE directly in it + +kritaLibLibKisPath = f"{kritaHomeDir}/libs\libkis" + + +kritaCMakeListTxtPathIncludingFileNameAndExtension = f"{kritaHomeDir}/CMakeLists.txt" +moduleDestinationPath = r"C:\Users\olliv\Desktop\pyKritaOuter".replace('\\', '/') # Where to store the output module + +import os +import glob + +packageDestinationPath = moduleDestinationPath + '/pyKrita' +import shutil +try: + shutil.rmtree(packageDestinationPath) +except: + pass +# raise "" +# files = glob.glob(packageDestinationPath + '/*') +# for f in files: +# os.remove(f) +moduleDestinationPath = packageDestinationPath + '/src/krita' +setupPyFilePathIncludingFileNameAndExtension = packageDestinationPath + '/setup.py' +os.chdir(kritaLibLibKisPath) @@ -14,7 +40,63 @@ #---------------------------------------------- # create new file to save to. "w+" means write and create a file. saves in directory specified above -exportFile = open("PyKrita.py", "w+") +os.makedirs(f"{moduleDestinationPath}", exist_ok=True) + +exportFile = open(f"{moduleDestinationPath}/__init__.py", "w+") +readMeFile = open(f"{packageDestinationPath}/README.md", "w+") + +def search_string_in_file(file_name: str, string_to_search: str) -> tuple[int, str]: + """Returns all occurances of string_to_search as a list, where each list element is a tuple, where index 0 is the line number & index 1 is the full line as a string.""" + line_number = 0 + list_of_results = [] + # Open the file in read only mode + with open(file_name, 'r') as read_obj: + # Read all lines in the file one by one + for line in read_obj: + # For each line, check if line contains the string + line_number += 1 + if string_to_search in line: + # If yes, then add the line number & line as a tuple in the list + list_of_results.append((line_number, line.rstrip())) + # Return list of tuples containing line numbers and lines where string is found + return list_of_results + +kritaVersion = search_string_in_file(kritaCMakeListTxtPathIncludingFileNameAndExtension, "set(KRITA_VERSION_STRING")[0][1] +kritaVersion = kritaVersion[kritaVersion.find('"')+1:kritaVersion.rfind('"')] +readMeFile.write(f""" +# Fake PyKrita +## Auto completion Python module for Krita version {kritaVersion}. + + +""") +readMeFile.close() +from setuptools import setup, find_packages +setup +import datetime + +formattedDateTime = datetime.datetime.now().strftime(r'%d %B %Y %H %M %S') +# print(formattedDateTime) + +setupPyFile = open(setupPyFilePathIncludingFileNameAndExtension, "w+") +setupPyFile.write(f"""from setuptools import setup, find_packages + +setup( + # name='PyKrita{kritaVersion}', + name='Fake PyKrita for Krita v {kritaVersion} build date {formattedDateTime}', + version='{generatorVersion}', + # version = '0.1', + license='Unknown', + author="Source code generator by scottpetrovic, modified by Olliver Aira aka officernickwilde.", + author_email='olliver.aira@gmail.com', + packages=find_packages('src'), + package_dir={{'': 'src'}}, + # url='https://github.com', + keywords='Krita intellisense autocomplete autocompletion Python PyKrita code highlight fake module', + install_requires=[], + +) +""") +setupPyFile.close() # a bit of a readme for what this does exportFile.write("# Auto-generated file that reads from H files in the libkis folder \n" + @@ -56,6 +138,11 @@ bracesPrecursor = allFileLines[lineWithClassFile] className = bracesPrecursor.split(' ')[2] + if className[-1] == ":": + className = className[:-1] + if className[-1] == "\n": + className = className[:-1] + # start getting the format ready for the class exportFile.write("class " + className + ":\n") @@ -69,13 +156,13 @@ # if there aren't don't try to grab the comments later classCommentsEnd = -1 if "*/" in allFileLines[lineWithClassFile]: - classCommentsEnd = lineWithClassFile; + classCommentsEnd = lineWithClassFile if "*/" in allFileLines[lineWithClassFile - 1]: - classCommentsEnd = lineWithClassFile - 1; + classCommentsEnd = lineWithClassFile - 1 if "*/" in allFileLines[lineWithClassFile - 2]: - classCommentsEnd = lineWithClassFile - 2; + classCommentsEnd = lineWithClassFile - 2 # if we see some comments for the class, so try to read them... @@ -104,7 +191,7 @@ - exportFile.write(" \"\"\"" + classCommentsOutput + " \"\"\"" + "\n\n") + exportFile.write(" \"\"\" " + classCommentsOutput + " \"\"\"" + "\n\n") @@ -117,12 +204,42 @@ #we save the line of the previous function to get exactly range between signatures of the methods previousFunctionLineNumber = -1 - + isInBlockComment = False fnPattern = re.compile("\(.*\)") for j, line in enumerate(allFileLines): + if line.__contains__('*/'): + isInBlockComment = False + + if isInBlockComment: + continue + + if line.__contains__('/*'): + isInBlockComment = True + + line = line[:(line.find('/*')-1)] + line = line.strip() # strip white beginning and trailing white space + def removeCharactersWithinLimiters(input: str, limitBegin: str, limitEnd) -> str: + output: str = "" + + areWeWithinLimiters = False + i = -1 + for char in input: + i += 1 + if char == limitBegin: + areWeWithinLimiters = True + continue + if char == limitEnd: + areWeWithinLimiters = False + + if not areWeWithinLimiters: + output += char + return output + + line = removeCharactersWithinLimiters(line, '<', '>') + for match in re.finditer(fnPattern, line): if line.strip()[0][0] != "*": # this means it is part of a comments @@ -147,6 +264,7 @@ if "explicit" in functionList: isExplicit = True functionList = functionList.split("explicit")[1] + functionList = functionList.strip() # extra spaces at the beginning need to be removed @@ -156,27 +274,66 @@ returnType = functionList.split(' ')[0] functionList = functionList.split(' ')[1] + if functionList.__len__() < 1: + continue + if functionList[0] == '=': + continue + # save off the parameters elsewhere as we have to parse each differently # we need to clean it up a bit since it is loose and doesn't need variable names and types. we will just keep the types paramsList = "(" + line.split("(")[1] - paramsList = paramsList.replace("const", "").replace("&", "").replace("*", "").replace(";", "") # remove const stuff + paramsList = paramsList.replace("const", "").replace("&", "").replace("*", "").replace(";", "").replace("override", "") # remove const stuff paramsList = paramsList.replace("(", "").replace(")", "").strip() # clean up parameters with multiple - if "," in paramsList: + class ParamTypeAndName: + name: str = "" + type: str = "" + listOfParamTypesAndNames: list[ParamTypeAndName] = [] + longestParamName = 0 + if True: + # if "," in paramsList: multipleParamsList = [] + if paramsList.__contains__(','): + paramsListSplit = paramsList.split(",") + else: + paramsListSplit = [paramsList] # break it apart and clear everything after the first word - for p in paramsList.split(","): - multipleParamsList.append(p.strip().split(" ")[0]) + for p in paramsListSplit: + print(p) + try: + splittedStr = p.strip().split(" ", 1) + print(f"splittedStr {splittedStr}") + if splittedStr[1].__contains__('='): + parameterName = splittedStr[1][:((splittedStr[1].find('=')))].strip() + else: + parameterName = splittedStr[1] + parameterType = splittedStr[0] + print(f"splittedStr: {splittedStr}") + multipleParamsList.append(f"{parameterName}") + paramTypeAndName = ParamTypeAndName() + paramTypeAndName.name = parameterName + paramTypeAndName.type = parameterType + listOfParamTypesAndNames.append(paramTypeAndName) + longestParamName = max(parameterName.__len__(), longestParamName) + # multipleParamsList.append(f"{parameterName}: {splittedStr[0]}") # @todo Make sure all types are declared in Python, currently we cant type hint + # due to the actual types never being declared. + + except Exception as exception: + print(f"Paramsplitting error -> {exception} <- for '{p}' in {paramsList} in '{line}'") + # raise exception + paramsList = ",".join(multipleParamsList ) elif paramsList != "": paramsList = paramsList.strip().split(" ")[0] #Only one parameter. remove everything after the first word + if paramsList.__len__() < 2: + continue exportFile.write(" def " + functionList + "(" + paramsList + "):" "\n") @@ -186,7 +343,7 @@ # now let's figure out what comments we have. we figured out the return type above. but we need to scrape the h file for comments #functionLineNumber - functionCommentsEnd = functionLineNumber - 1; + functionCommentsEnd = functionLineNumber - 1 functionCommentsStartIndex = previousFunctionLineNumber for b in range(functionCommentsEnd, functionCommentsStartIndex+1, -1 ) : #go in decreasing order @@ -204,10 +361,24 @@ functionCommentsOutput = functionCommentsOutput.replace("*", "").replace("/ ", "") else: functionCommentsOutput = "Missing function documentation" - - + longestParamName = max(longestParamName, 'return'.__len__()) + def formatParamForDocString(paramName: str, paramType: str) -> str: + formatParamForDocStringSpacing = "" + i9 = -1 + while i9 < longestParamName - paramName.__len__(): + i9 += 1 + formatParamForDocStringSpacing += " " + return f"\n{paramName}: {formatParamForDocStringSpacing}{paramType}" # finally export the final file - exportFile.write(" \"\"\"Return Type: " + returnType + "\n " + functionCommentsOutput + "\"\"\" \n\n" ) + # listOfParamTypesAndNames.reverse() + parameterPartOfComment = "" + for param in listOfParamTypesAndNames: + parameterPartOfComment = f"{parameterPartOfComment}{formatParamForDocString(param.name, param.type)}" + newLine = '\n' + + functionCommentsOutput = f"{newLine}{functionCommentsOutput}{newLine}{parameterPartOfComment}{formatParamForDocString('return', returnType)}{newLine}" + + exportFile.write(" \"\"\" " + functionCommentsOutput + " \"\"\" \n\n" ) @@ -215,7 +386,42 @@ # file is done. add some spacing for readability exportFile.write("\n") + # close the file that we are done with it currentFile.close() +# exportFileAsStr = exportFile.__str__() +# exportFileAsListOfStringsWhereEachStringRepresentsALine: list[str] = exportFileAsStr.split('\n') +# exportFileAsStrFINAL = "" +# newLine = '\n' +# for line in exportFileAsListOfStringsWhereEachStringRepresentsALine: +# if line.__contains__('#include'): +# continue +# exportFileAsStrFINAL += f"{line}{newLine}" exportFile.close() + +import subprocess +import os +# print(f"""yo f'cd "{packageDestinationPath}" & Python setup.py sdist'""") + +os.chdir(packageDestinationPath) +os.system(f'Python setup.py sdist') +# os.system(f'start /d "{packageDestinationPath}" cmd /c "Python setup.py sdist" ') +# import time +# time.sleep(10) +# os.system(f'start cmd /k "cd \"{packageDestinationPath}\" & Python \"{packageDestinationPath}/setup.py\" sdist" /d "{packageDestinationPath}"') +# os.system(f'start cmd /k cd Python "{setupPyFilePathIncludingFileNameAndExtension}" sdist') +# os.system(f'cd "{packageDestinationPath}" & Python setup.py sdist') +# raise "wa" +os.system(f'pip install twine') +os.system(f'start /d "{packageDestinationPath}" cmd /c "Python setup.py sdist" ') +os.system(f'cd "{packageDestinationPath}" & twine upload --verbose -r pypi "dist/*"') + + +# import os +# os.chdir(packageDestinationPath) +# os.system(f'Python setup.py sdist') +# import time +# os.system(f'pip install twine') +# os.system(f'start /d "{packageDestinationPath}" cmd /c "Python setup.py sdist" ') +# os.system(f'cd "{packageDestinationPath}" & twine upload --verbose -r pypi "dist/*"')