568 lines
19 KiB
Python
Executable file
568 lines
19 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
import argparse
|
|
import csv
|
|
import glob
|
|
import io
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
import xmltodict
|
|
|
|
new_path = os.path.normpath(
|
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
|
)
|
|
sys.path.append(new_path)
|
|
|
|
from script.git.git_tool import GitTool
|
|
|
|
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
PROJECT_NAME = os.path.basename(os.getcwd())
|
|
IDEA_PATH = "./.idea"
|
|
DEFAULT_ODOO_BIN = (
|
|
"./odoo/odoo-bin" # Need this to replace static configuration
|
|
)
|
|
IDEA_MISC = os.path.join(IDEA_PATH, "misc.xml")
|
|
IDEA_WORKSPACE = os.path.join(IDEA_PATH, "workspace.xml")
|
|
VCS_WORKSPACE = os.path.join(IDEA_PATH, "vcs.xml")
|
|
|
|
PATH_EXCLUDE_FOLDER = "./conf/pycharm_exclude_folder.txt"
|
|
PATH_DEFAULT_CONFIGURATION = "./conf/pycharm_default_configuration.csv"
|
|
PATH_DEFAULT_CONFIGURATION_PRIVATE = (
|
|
"./private/pycharm_default_configuration.private.csv"
|
|
)
|
|
if os.path.isfile(".odoo-version"):
|
|
with open(".odoo-version") as txt:
|
|
odoo_version = txt.read()
|
|
DEFAULT_ODOO_BIN_UPDATE = f"./odoo{odoo_version}/odoo/odoo-bin"
|
|
else:
|
|
DEFAULT_ODOO_BIN_UPDATE = None
|
|
|
|
|
|
def get_config():
|
|
"""Parse command line arguments, extracting the config file name,
|
|
returning the union of config file and command line arguments
|
|
|
|
:return: dict of config file settings and command line arguments
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description="""\
|
|
Manipulate configuration for your IDE Pycharm about ERPLibre.
|
|
This script manage only Python configuration, other configuration will be ignore.
|
|
""",
|
|
epilog="""\
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"--list_configuration",
|
|
action="store_true",
|
|
help=(
|
|
"List all configuration, the configuration is executable with"
|
|
" debugger."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--list_configuration_full",
|
|
action="store_true",
|
|
help=(
|
|
"List all configuration full details, the configuration is"
|
|
" executable with debugger."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--init",
|
|
action="store_true",
|
|
help="Initialize minimum configuration for Pycharm.",
|
|
)
|
|
parser.add_argument(
|
|
"--overwrite",
|
|
action="store_true",
|
|
help="Remove old data and replace it.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if (
|
|
args.init
|
|
or args.list_configuration
|
|
or args.list_configuration_full
|
|
or args.overwrite
|
|
):
|
|
args.configuration = True
|
|
if args.overwrite:
|
|
args.init = True
|
|
else:
|
|
args.configuration = False
|
|
|
|
return args
|
|
|
|
|
|
def main():
|
|
has_execute = False
|
|
config = get_config()
|
|
_logger.info(f"Work on directory {os.getcwd()}")
|
|
|
|
die(
|
|
not os.path.isdir(IDEA_PATH),
|
|
f"Missing {IDEA_PATH} path, are you sure you run this script at root"
|
|
" of the project, are you sure you have a Pycharm project?",
|
|
)
|
|
# Find a file ended by iml and take it
|
|
project_path = None
|
|
for file in glob.glob(IDEA_PATH + "/*.iml"):
|
|
project_path = file
|
|
break
|
|
die(
|
|
not project_path,
|
|
f"Missing .iml file into {IDEA_PATH}, wait after Pycharm analyze file"
|
|
" to create Python environnement.",
|
|
)
|
|
|
|
if config.init:
|
|
has_execute = True
|
|
read_xml_and_execute(project_path, add_exclude_folder, "init", config)
|
|
|
|
die(
|
|
not os.path.isfile(IDEA_MISC),
|
|
f"Missing {IDEA_MISC} file, wait after Pycharm analyze file to create"
|
|
" Python environnement. You can call --init to accelerate first"
|
|
" Pycharm analyze file.",
|
|
)
|
|
|
|
if config.configuration:
|
|
die(
|
|
not os.path.isfile(IDEA_WORKSPACE),
|
|
f"Missing {IDEA_WORKSPACE} file, be sure Pycharm open a project to"
|
|
" your workspace.",
|
|
)
|
|
has_execute = True
|
|
read_xml_and_execute(
|
|
IDEA_WORKSPACE, add_configuration, "workspace", config
|
|
)
|
|
|
|
die(
|
|
not os.path.isfile(VCS_WORKSPACE),
|
|
f"Missing {VCS_WORKSPACE} file, be sure Pycharm open a project to"
|
|
" your workspace.",
|
|
)
|
|
has_execute = True
|
|
read_xml_and_execute(VCS_WORKSPACE, add_version_control, "vcs", config)
|
|
|
|
if not has_execute:
|
|
_logger.info("Nothing to do")
|
|
|
|
|
|
def read_xml_and_execute(file_name, cb, name, config):
|
|
_logger.info(f"Pycharm configuration on file {file_name}")
|
|
with open(file_name) as xml:
|
|
xml_as_string = xml.read()
|
|
dct_project_xml = xmltodict.parse(xml_as_string)
|
|
# Add exclude folder
|
|
has_change = cb(dct_project_xml, file_name, config)
|
|
if has_change:
|
|
xml_format = xmltodict.unparse(
|
|
dct_project_xml, pretty=True, indent=" "
|
|
)
|
|
with open(file_name, mode="w") as xml:
|
|
xml.write(xml_format)
|
|
_logger.info(f"File {file_name} has been write.")
|
|
subprocess.call(
|
|
f"script/maintenance/prettier_xml.sh '{file_name}'",
|
|
shell=True,
|
|
)
|
|
else:
|
|
_logger.info(f"No change '{name}'.")
|
|
|
|
|
|
def add_version_control(dct_xml, file_name, config):
|
|
# The file .idea/vcs.xml need to be generated before
|
|
|
|
git_tool = GitTool()
|
|
lst_repo = git_tool.get_repo_info_manifest_xml()
|
|
lst_mapping_directory = []
|
|
# Create a mapping of git directory from ERPLibre
|
|
for dct_repo in lst_repo:
|
|
relative_path = dct_repo.get("path")
|
|
if not relative_path:
|
|
continue
|
|
vcs_path = os.path.join("$PROJECT_DIR$", relative_path)
|
|
lst_mapping_directory.append(vcs_path)
|
|
dct_component = dct_xml.get("project", {}).get("component", {})
|
|
lst_mapping = dct_component.get("mapping", [])
|
|
if not lst_mapping:
|
|
print(
|
|
"By default, need 1 value, the actual ERPLibre project, or create"
|
|
" it manually ;-)"
|
|
)
|
|
return False
|
|
# Detect difference
|
|
if type(lst_mapping) is dict:
|
|
# Transform it into list for multiple mapping
|
|
lst_mapping = [lst_mapping]
|
|
dct_component["mapping"] = lst_mapping
|
|
|
|
for dct_mapping in lst_mapping:
|
|
directory = dct_mapping.get("@directory")
|
|
if directory in lst_mapping_directory:
|
|
lst_mapping_directory.remove(directory)
|
|
# Update difference
|
|
if lst_mapping_directory:
|
|
for mapping_directory in lst_mapping_directory:
|
|
dct_mapping = {"@directory": mapping_directory, "@vcs": "Git"}
|
|
lst_mapping.append(dct_mapping)
|
|
return True
|
|
return False
|
|
|
|
|
|
def add_configuration(dct_xml, file_name, config):
|
|
has_change = False
|
|
dct_project = dct_xml.get("project")
|
|
die(not bool(dct_project), f"Missing 'project' into {file_name}")
|
|
lst_component = dct_project.get("component")
|
|
die(
|
|
not bool(lst_component),
|
|
f"Missing 'project/component' into {file_name}",
|
|
)
|
|
# Add python configuration
|
|
dct_component_py_conf = [
|
|
a
|
|
for a in lst_component
|
|
if a.get("@name") == "PyConsoleOptionsProvider"
|
|
]
|
|
if dct_component_py_conf:
|
|
# TODO check and force value
|
|
pass
|
|
else:
|
|
lst_component.append(
|
|
{
|
|
"@name": "PyConsoleOptionsProvider",
|
|
"option": {
|
|
"@name": "myShowDebugConsoleByDefault",
|
|
"@value": "false",
|
|
},
|
|
}
|
|
)
|
|
has_change = True
|
|
|
|
# Add configuration
|
|
dct_component = [
|
|
a for a in lst_component if a.get("@name") == "RunManager"
|
|
]
|
|
if not dct_component:
|
|
# Create it
|
|
dct_component = {
|
|
"@name": "RunManager",
|
|
# "@selected": "Python.pycharm_configuration",
|
|
# "recent_temporary": {
|
|
# "list": {
|
|
# "item": {"@itemvalue": "Python.pycharm_configuration"}
|
|
# }
|
|
# },
|
|
"configuration": [],
|
|
"list": {"item": []},
|
|
}
|
|
lst_component.append(dct_component)
|
|
else:
|
|
dct_component = dct_component[0]
|
|
if config.list_configuration_full:
|
|
print("Configuration list detail:")
|
|
lst_configuration_full = dct_component.get("configuration")
|
|
# Create a unique list of configuration to know if we need to add a new configuration
|
|
lst_unique_configuration = []
|
|
if type(lst_configuration_full) is dict:
|
|
lst_configuration_full = [lst_configuration_full]
|
|
for conf in lst_configuration_full:
|
|
if conf.get("@factoryName") == "Python":
|
|
folder_name = conf.get("@folderName")
|
|
conf_name = conf.get("@name")
|
|
if folder_name:
|
|
conf_name = f"{folder_name}/{conf_name}"
|
|
else:
|
|
folder_name = ""
|
|
if config.list_configuration_full:
|
|
print(f"\t{conf_name}")
|
|
script_name = [
|
|
a.get("@value").replace("$PROJECT_DIR$", ".")
|
|
for a in conf.get("option")
|
|
if a.get("@name") == "SCRIPT_NAME"
|
|
]
|
|
if not script_name:
|
|
script_name = ""
|
|
else:
|
|
script_name = script_name[0]
|
|
param = [
|
|
a.get("@value")
|
|
for a in conf.get("option")
|
|
if a.get("@name") == "PARAMETERS"
|
|
]
|
|
if param:
|
|
param = param[0]
|
|
else:
|
|
param = ""
|
|
if config.list_configuration_full:
|
|
print(f"\t\t{script_name} {param}")
|
|
lst_unique_configuration.append(
|
|
f"d {folder_name} s {script_name} p {param}"
|
|
)
|
|
if config.list_configuration:
|
|
print("Configuration list:")
|
|
lst_configuration = []
|
|
lst_xml_configuration_name = []
|
|
dct_component_list = dct_component.get("list")
|
|
if dct_component_list:
|
|
lst_xml_configuration_name = dct_component_list.get("item")
|
|
lst_configuration = [
|
|
a.get("@itemvalue")[7:]
|
|
for a in lst_xml_configuration_name
|
|
if a.get("@itemvalue").startswith("Python.")
|
|
]
|
|
if config.list_configuration:
|
|
for conf in lst_configuration:
|
|
print(f"\t{conf}")
|
|
elif config.list_configuration:
|
|
print(f"\tNo configuration found.")
|
|
|
|
last_default = None
|
|
if not config.init:
|
|
return has_change
|
|
if not os.path.isfile(PATH_DEFAULT_CONFIGURATION):
|
|
_logger.error(f"Cannot read file '{PATH_DEFAULT_CONFIGURATION}'")
|
|
return has_change
|
|
with open(PATH_DEFAULT_CONFIGURATION) as txt:
|
|
content1 = txt.read()
|
|
if os.path.isfile(PATH_DEFAULT_CONFIGURATION_PRIVATE):
|
|
with open(PATH_DEFAULT_CONFIGURATION_PRIVATE) as txt:
|
|
content2 = txt.read()
|
|
merged_content = content1 + content2
|
|
else:
|
|
merged_content = content1
|
|
txt_merged = io.StringIO(merged_content)
|
|
for default_conf in csv.DictReader(txt_merged):
|
|
conf_name = default_conf.get("name")
|
|
conf_folder = default_conf.get("folder")
|
|
conf_script_path = default_conf.get("script_path")
|
|
conf_default = bool(default_conf.get("default"))
|
|
if conf_default:
|
|
last_default = f"Python.{conf_name}"
|
|
if DEFAULT_ODOO_BIN_UPDATE:
|
|
conf_script_path = conf_script_path.replace(
|
|
DEFAULT_ODOO_BIN, DEFAULT_ODOO_BIN_UPDATE
|
|
)
|
|
conf_script_path_replace = (
|
|
conf_script_path
|
|
if not conf_script_path.startswith("./")
|
|
else f"$PROJECT_DIR${conf_script_path[1:]}"
|
|
)
|
|
conf_parameter = default_conf.get("parameters")
|
|
s_unique_key = (
|
|
f"d {conf_folder} s {conf_script_path} p {conf_parameter}"
|
|
)
|
|
if config.overwrite or s_unique_key not in lst_unique_configuration:
|
|
# add it!
|
|
if not config.overwrite and conf_name in lst_configuration:
|
|
_logger.error(
|
|
f"Cannot add configuration name '{conf_name}',"
|
|
" already exist. Delete it manually."
|
|
)
|
|
else:
|
|
has_change = True
|
|
lst_xml_configuration_name.append(
|
|
{"@itemvalue": f"Python.{conf_name}"}
|
|
)
|
|
conf_full = {
|
|
"@name": conf_name,
|
|
"@type": "PythonConfigurationType",
|
|
"@factoryName": "Python",
|
|
"module": {"@name": PROJECT_NAME},
|
|
"option": [
|
|
{
|
|
"@name": "INTERPRETER_OPTIONS",
|
|
"@value": "",
|
|
},
|
|
{"@name": "PARENT_ENVS", "@value": "true"},
|
|
{
|
|
"@name": "SDK_HOME",
|
|
"@value": "$PROJECT_DIR$/.venv.erplibre/bin/python",
|
|
},
|
|
{
|
|
"@name": "WORKING_DIRECTORY",
|
|
"@value": "$PROJECT_DIR$",
|
|
},
|
|
{
|
|
"@name": "IS_MODULE_SDK",
|
|
"@value": "false",
|
|
},
|
|
{
|
|
"@name": "ADD_CONTENT_ROOTS",
|
|
"@value": "true",
|
|
},
|
|
{
|
|
"@name": "ADD_SOURCE_ROOTS",
|
|
"@value": "true",
|
|
},
|
|
{
|
|
"@name": "SCRIPT_NAME",
|
|
"@value": conf_script_path_replace,
|
|
},
|
|
{
|
|
"@name": "PARAMETERS",
|
|
"@value": conf_parameter,
|
|
},
|
|
{
|
|
"@name": "SHOW_COMMAND_LINE",
|
|
"@value": "false",
|
|
},
|
|
{
|
|
"@name": "EMULATE_TERMINAL",
|
|
"@value": "false",
|
|
},
|
|
{
|
|
"@name": "MODULE_MODE",
|
|
"@value": "false",
|
|
},
|
|
{
|
|
"@name": "REDIRECT_INPUT",
|
|
"@value": "false",
|
|
},
|
|
{"@name": "INPUT_FILE", "@value": ""},
|
|
],
|
|
"envs": {
|
|
"env": {
|
|
"@name": "PYTHONUNBUFFERED",
|
|
"@value": "1",
|
|
}
|
|
},
|
|
"EXTENSION": {
|
|
"@ID": "PythonCoverageRunConfigurationExtension",
|
|
"@runner": "coverage.py",
|
|
},
|
|
"method": {"@v": "2"},
|
|
}
|
|
if conf_folder:
|
|
conf_full["@folderName"] = conf_folder
|
|
lst_configuration_full.insert(0, conf_full)
|
|
else:
|
|
_logger.info(f"Configuration already exist: '{s_unique_key}'")
|
|
|
|
if last_default:
|
|
dct_component["@selected"] = last_default
|
|
dct_component["recent_temporary"] = {
|
|
"list": {"item": {"@itemvalue": last_default}}
|
|
}
|
|
return has_change
|
|
|
|
|
|
def get_lst_exclude_folder():
|
|
if os.path.isfile(PATH_EXCLUDE_FOLDER):
|
|
with open(PATH_EXCLUDE_FOLDER) as txt:
|
|
txt_read = txt.read()
|
|
lst_exclude_folder = txt_read.strip().split("\n")
|
|
else:
|
|
lst_exclude_folder = []
|
|
|
|
# lst_to_exclude = ["./.venv.*", "./addons.*"]
|
|
lst_to_exclude = ["./.venv.*", "./addons"]
|
|
for s_exclude in lst_to_exclude:
|
|
for dir_venv in glob.glob(s_exclude):
|
|
lst_exclude_folder.append(dir_venv[2:])
|
|
|
|
# Dynamic exclude
|
|
lst_exclude_from_repo = ["setup"]
|
|
|
|
git_tool = GitTool()
|
|
lst_repo = git_tool.get_repo_info_manifest_xml()
|
|
# Create a mapping of git directory from ERPLibre
|
|
for dct_repo in lst_repo:
|
|
relative_path = dct_repo.get("path")
|
|
for exclude_from_repo in lst_exclude_from_repo:
|
|
path_to_check = os.path.join(relative_path, exclude_from_repo)
|
|
if os.path.isdir(path_to_check):
|
|
lst_exclude_folder.append(path_to_check)
|
|
return lst_exclude_folder
|
|
|
|
|
|
def add_exclude_folder(dct_xml, file_name, config):
|
|
has_change = False
|
|
dct_module = dct_xml.get("module")
|
|
die(not bool(dct_module), f"Missing 'module' into {file_name}")
|
|
lst_component = dct_module.get("component")
|
|
die(
|
|
not bool(lst_component), f"Missing 'module/component' into {file_name}"
|
|
)
|
|
if type(lst_component) is list:
|
|
dct_component = [
|
|
a
|
|
for a in lst_component
|
|
if a.get("@name") == "NewModuleRootManager"
|
|
]
|
|
elif type(lst_component) is dict:
|
|
dct_component = [lst_component]
|
|
else:
|
|
raise Exception(f"Wrong value for {lst_component}")
|
|
|
|
die(
|
|
not bool(dct_component),
|
|
"Missing 'module/component @name NewModuleRootManager' into"
|
|
f" {file_name}",
|
|
)
|
|
dct_component = dct_component[0]
|
|
dct_content = dct_component.get("content")
|
|
die(
|
|
not bool(dct_content),
|
|
"Missing 'module/component @name NewModuleRootManager/content' into"
|
|
f" {file_name}",
|
|
)
|
|
lst_exclude_folder = dct_content.get("excludeFolder")
|
|
|
|
lst_exclude_item_raw = get_lst_exclude_folder()
|
|
lst_exclude_item = [
|
|
f"file://$MODULE_DIR$/{a}" for a in lst_exclude_item_raw
|
|
]
|
|
|
|
if lst_exclude_folder:
|
|
if type(lst_exclude_folder) is list:
|
|
lst_existing_exclude_item = [
|
|
a.get("@url") for a in lst_exclude_folder
|
|
]
|
|
elif type(lst_exclude_folder) is dict:
|
|
lst_existing_exclude_item = [lst_exclude_folder.get("@url")]
|
|
else:
|
|
die(
|
|
True,
|
|
"Cannot understand type of variable lst_exclude_folder:"
|
|
f" {lst_exclude_folder}",
|
|
)
|
|
lst_diff = list(
|
|
set(lst_exclude_item).difference(set(lst_existing_exclude_item))
|
|
)
|
|
else:
|
|
lst_exclude_folder = []
|
|
dct_content["excludeFolder"] = lst_exclude_folder
|
|
lst_diff = lst_exclude_item
|
|
|
|
if lst_diff:
|
|
if type(lst_exclude_folder) is dict:
|
|
# Contain only 1 item
|
|
lst_exclude_folder = [lst_exclude_folder]
|
|
dct_content["excludeFolder"] = lst_exclude_folder
|
|
for diff in lst_diff:
|
|
lst_exclude_folder.append({"@url": diff})
|
|
has_change = True
|
|
return has_change
|
|
|
|
|
|
def die(cond, message, code=1):
|
|
if cond:
|
|
print(message, file=sys.stderr)
|
|
sys.exit(code)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|