如何编写Python脚本实现Pycharm配置Black自动保存格式化及外部工具添加
Step 1: The Complete Script
import os import subprocess import xml.etree.ElementTree as ET from xml.dom import minidom def install_black(): """Install or upgrade Black using pip""" try: subprocess.run(["pip3", "install", "--upgrade", "black"], check=True) print("✅ Black installed/upgraded successfully") except subprocess.CalledProcessError: print("❌ Failed to install Black. Ensure pip3 is installed.") exit(1) def find_pycharm_config_dirs(): """Locate all PyCharm config directories in ~/.config/JetBrains/""" jetbrains_dir = os.path.expanduser("~/.config/JetBrains") if not os.path.exists(jetbrains_dir): print("❌ No PyCharm config directories found. Run PyCharm at least once first.") exit(1) pycharm_dirs = [d for d in os.listdir(jetbrains_dir) if d.startswith("PyCharm")] if not pycharm_dirs: print("❌ No PyCharm config directories found. Run PyCharm at least once first.") exit(1) return [os.path.join(jetbrains_dir, d) for d in pycharm_dirs] def prettify_xml(element): """Prettify XML output with proper indentation""" rough_string = ET.tostring(element, 'utf-8') reparsed = minidom.parseString(rough_string) return reparsed.toprettyxml(indent=" ") def add_external_tool(config_dir): """Add Black as an external tool in PyCharm""" tools_dir = os.path.join(config_dir, "tools") os.makedirs(tools_dir, exist_ok=True) tools_file = os.path.join(tools_dir, "ExternalTools.xml") # Define the Black tool XML structure if os.path.exists(tools_file): tree = ET.parse(tools_file) root = tree.getroot() else: root = ET.Element("application") component = ET.SubElement(root, "component", name="ExternalTools") root.append(component) # Check if Black tool already exists component = root.find(".//component[@name='ExternalTools']") existing_tool = component.find(".//tool[@id='Black']") if existing_tool is not None: print(f"ℹ️ Black external tool already exists in {config_dir}") return # Create the tool element tool = ET.SubElement(component, "tool", { "id": "Black", "name": "Black", "description": "Black code formatter", "showInMainMenu": "false", "showInEditor": "true", "showInProject": "true", "showInSearchPopup": "true", "enabled": "true", "useConsole": "false", "showConsoleOnStdOut": "false", "showConsoleOnStdErr": "false", "synchronizeAfterRun": "true" }) exec_elem = ET.SubElement(tool, "exec") ET.SubElement(exec_elem, "option", {"name": "COMMAND", "value": "black"}) ET.SubElement(exec_elem, "option", {"name": "PARAMETERS", "value": "$FilePath$"}) ET.SubElement(exec_elem, "option", {"name": "WORKING_DIRECTORY", "value": "$ProjectFileDir$"}) # Write the prettified XML with open(tools_file, "w") as f: f.write(prettify_xml(root)) print(f"✅ Added Black as external tool in {config_dir}") def add_file_watcher(config_dir): """Add a File Watcher to run Black on Python file save""" watchers_dir = os.path.join(config_dir, "fileWatches") os.makedirs(watchers_dir, exist_ok=True) watchers_file = os.path.join(watchers_dir, "fileWatches.xml") if os.path.exists(watchers_file): tree = ET.parse(watchers_file) root = tree.getroot() else: root = ET.Element("application") component = ET.SubElement(root, "component", name="FileWatcherManager") watchers = ET.SubElement(component, "watchers") root.append(component) # Check if Black watcher already exists component = root.find(".//component[@name='FileWatcherManager']") watchers = component.find("watchers") if component is not None else None if not watchers: watchers = ET.SubElement(component, "watchers") existing_watcher = watchers.find(".//watcher[@id='Black']") if existing_watcher is not None: print(f"ℹ️ Black File Watcher already exists in {config_dir}") return # Create the watcher element watcher = ET.SubElement(watchers, "watcher", { "id": "Black", "name": "Black", "fileType": "Python", "scope": "Project Files", "program": "black", "parameters": "$FilePath$", "workingDir": "$ProjectFileDir$", "trackOnlyRoots": "false", "showConsole": "ALWAYS", "isEnabled": "true", "hideIgnoredFiles": "false" }) filters = ET.SubElement(watcher, "filters") filter_elem = ET.SubElement(filters, "filter") ET.SubElement(filter_elem, "option", {"name": "FILE_EXTENSIONS", "value": "py"}) ET.SubElement(filter_elem, "option", {"name": "FILE_NAMES"}) triggering = ET.SubElement(watcher, "triggering") ET.SubElement(triggering, "option", {"name": "AFTER_SAVE", "value": "true"}) advanced = ET.SubElement(watcher, "advancedOptions") ET.SubElement(advanced, "option", {"name": "autoSave", "value": "true"}) ET.SubElement(advanced, "option", {"name": "autoSync", "value": "true"}) # Write the prettified XML with open(watchers_file, "w") as f: f.write(prettify_xml(root)) print(f"✅ Added Black File Watcher (runs on save) in {config_dir}") def main(): print("=== Starting Black setup for PyCharm ===") install_black() pycharm_dirs = find_pycharm_config_dirs() for dir_path in pycharm_dirs: print(f"\nProcessing config directory: {dir_path}") add_external_tool(dir_path) add_file_watcher(dir_path) print("\n=== Setup Complete ===") print("⚠️ Restart PyCharm for changes to take effect.") print("🔧 Black will now automatically format Python files when you save them.") print("🖥️ You can also run Black manually via Tools > External Tools > Black.") if __name__ == "__main__": main()
Step 2: How to Use the Script
- Save the script as
setup_black_pycharm.py - Run it with Python 3:
python3 setup_black_pycharm.py - Use user-level pip install (preferred) or run with sudo only if necessary for system-wide installation.
Step 3: Key Details Explained
Installing Black
The script uses pip3 install --upgrade black to ensure the latest version is installed. It exits with an error if pip3 isn't available—ensure Python 3 and pip3 are installed first.
Locating PyCharm Configs
PyCharm stores user settings in ~/.config/JetBrains/. The script detects all folders starting with PyCharm, covering both Community/Professional editions and different versions.
Adding External Tool
The script modifies tools/ExternalTools.xml to add Black as an external tool accessible via PyCharm's Tools menu. It uses built-in variables like $FilePath$ to target the current open file.
Setting Up Auto-Save Formatting
A File Watcher is added in fileWatches/fileWatches.xml that triggers Black whenever a .py file is saved. The watcher is enabled to run automatically after save, with auto-sync enabled to reload the formatted file in PyCharm.
Notes
- Restart Required: PyCharm must be restarted to apply the new configuration changes.
- Multiple PyCharm Versions: The script configures all detected PyCharm installations automatically.
- Existing Configs: If Black is already set up, the script skips duplicate setup steps to avoid conflicts.
内容的提问来源于stack exchange,提问作者TatevAram

