Source code for repo_helper_pycharm.iml_manager

#!/usr/bin/env python3
#
#  iml_manager.py
"""
Class to update PyCharm's ``*.iml`` configuration files.
"""
#
#  Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
import posixpath

# 3rd party
import click  # type: ignore[import]
import lxml  # type: ignore[import]
import lxml.etree  # type: ignore[import]
from consolekit.utils import coloured_diff
from domdf_python_tools.paths import PathPlus, unwanted_dirs
from domdf_python_tools.stringlist import StringList
from domdf_python_tools.typing import PathLike
from lxml import objectify
from repo_helper.core import RepoHelper

__all__ = ("ImlManager", )


[docs]class ImlManager: """ Class to update PyCharm's ``*.iml`` configuration files. :param repo_dir: """ excluded_dirs = { *unwanted_dirs, "build", "dist", "conda", "htmlcov", } def __init__(self, repo_dir: PathLike): self.rh = RepoHelper(repo_dir) self.rh.load_settings() if not (self.rh.target_repo / ".idea").is_dir(): # pragma: no cover raise FileNotFoundError("'.idea' directory not found. Perhaps this isn't a PyCharm project?") try: self.module_file: PathPlus = next((self.rh.target_repo / ".idea").glob("*.iml")) except StopIteration: raise FileNotFoundError("No '.idea/*.iml' file found. Perhaps this isn't a PyCharm project?") module_config = objectify.parse(str(self.module_file)) self.root = module_config.getroot() self.excluded_dirs = set(self.excluded_dirs) self.excluded_dirs.add(posixpath.join(self.rh.templates.globals["docs_dir"], "build"))
[docs] def run(self, show_diff: bool = False) -> int: """ Update the configuration in the file. :param show_diff: Whether to show a diff if changes are made. """ self.update_excludes() self.update_runner() self.remove_docstring_format() return self.write_out(show_diff)
[docs] def write_out(self, show_diff: bool = False) -> int: """ Write the modified output to file. :param show_diff: Whether to show a diff if changes are made. """ modified_xml = StringList(['<?xml version="1.0" encoding="UTF-8"?>']) modified_xml.append(lxml.etree.tostring(self.root, pretty_print=True).decode("UTF-8")) modified_xml.blankline(ensure_single=True) current_content = self.module_file.read_lines() changed = current_content != list(modified_xml) if not changed: return 0 if show_diff: click.echo( coloured_diff( current_content, list(modified_xml), self.module_file.name, self.module_file.name, "(original)", "(updated)", lineterm='', ) ) self.module_file.write_lines(modified_xml) return 1
[docs] def update_excludes(self) -> None: """ Update the list of directories which should be excluded from indexing. """ file_module_dir = "file://$MODULE_DIR$/" mypy_cache_dir = f"{file_module_dir}.mypy_cache" for component in self.root.findall("component"): if component.attrib["name"] != "NewModuleRootManager": continue # TODO: handle component not existing for exclude_node in component.content.findall("excludeFolder"): self.excluded_dirs.add( exclude_node.attrib.get( "url", mypy_cache_dir, ).split(file_module_dir)[-1], ) component.content.remove(exclude_node) # print(excluded_dirs) for directory in sorted(self.excluded_dirs): node = lxml.objectify.StringElement() # pylint: disable=loop-invariant-statement node.tag = "excludeFolder" node.attrib["url"] = file_module_dir + directory component.content.append(node) return
[docs] def update_runner(self) -> None: """ Set the project's test runner to pytest. """ for component in self.root.findall("component"): if component.attrib["name"] != "TestRunnerService": continue for option in component.findall("option"): if option.attrib["name"] == "PROJECT_TEST_RUNNER": option.attrib["value"] = "pytest" return
[docs] def remove_docstring_format(self) -> None: """ Remove any existing option for "PyDocumentationSettings", to revert to the default of reStructuredText. """ for component in self.root.findall("component"): if component.attrib["name"] == "PyDocumentationSettings": self.root.remove(component) return return