#!/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-untyped]
import lxml # type: ignore[import-untyped]
import lxml.etree # type: ignore[import-untyped]
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