From 8b718fda79703c90c88ec6dac5600ebe62a8a7b1 Mon Sep 17 00:00:00 2001 From: shmyga Date: Wed, 15 Mar 2023 23:02:04 +0300 Subject: [PATCH] feat: add presets --- poetry.lock | 163 +++++++++++++++++- project_manager/command.py | 16 +- project_manager/default_preset.toml | 35 ++++ project_manager/entries/nodejs.py | 24 --- project_manager/entries/python.py | 33 ---- project_manager/entry.py | 72 +++++--- project_manager/logging.py | 2 +- project_manager/main.py | 3 +- project_manager/manager.py | 17 +- .../{entries => preset}/__init__.py | 0 project_manager/preset/entry.py | 28 +++ project_manager/preset/model.py | 20 +++ project_manager/preset/util.py | 45 +++++ project_manager/resolver.py | 37 ---- pyproject.toml | 4 + 15 files changed, 357 insertions(+), 142 deletions(-) create mode 100644 project_manager/default_preset.toml delete mode 100644 project_manager/entries/nodejs.py delete mode 100644 project_manager/entries/python.py rename project_manager/{entries => preset}/__init__.py (100%) create mode 100644 project_manager/preset/entry.py create mode 100644 project_manager/preset/model.py create mode 100644 project_manager/preset/util.py delete mode 100644 project_manager/resolver.py diff --git a/poetry.lock b/poetry.lock index 7ea165a..5cd39e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,24 @@ # This file is automatically @generated by Poetry and should not be changed by hand. +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] + [[package]] name = "black" version = "23.1.0" @@ -77,6 +96,51 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "exceptiongroup" +version = "1.1.1" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, + {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -129,6 +193,99 @@ files = [ docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "1.10.6" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9289065611c48147c1dd1fd344e9d57ab45f1d99b0fb26c51f1cf72cd9bcd31"}, + {file = "pydantic-1.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c32b6bba301490d9bb2bf5f631907803135e8085b6aa3e5fe5a770d46dd0160"}, + {file = "pydantic-1.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd9b9e98068fa1068edfc9eabde70a7132017bdd4f362f8b4fd0abed79c33083"}, + {file = "pydantic-1.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c84583b9df62522829cbc46e2b22e0ec11445625b5acd70c5681ce09c9b11c4"}, + {file = "pydantic-1.10.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b41822064585fea56d0116aa431fbd5137ce69dfe837b599e310034171996084"}, + {file = "pydantic-1.10.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61f1f08adfaa9cc02e0cbc94f478140385cbd52d5b3c5a657c2fceb15de8d1fb"}, + {file = "pydantic-1.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:32937835e525d92c98a1512218db4eed9ddc8f4ee2a78382d77f54341972c0e7"}, + {file = "pydantic-1.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd5c531b22928e63d0cb1868dee76123456e1de2f1cb45879e9e7a3f3f1779b"}, + {file = "pydantic-1.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e277bd18339177daa62a294256869bbe84df1fb592be2716ec62627bb8d7c81d"}, + {file = "pydantic-1.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f15277d720aa57e173954d237628a8d304896364b9de745dcb722f584812c7"}, + {file = "pydantic-1.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b243b564cea2576725e77aeeda54e3e0229a168bc587d536cd69941e6797543d"}, + {file = "pydantic-1.10.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3ce13a558b484c9ae48a6a7c184b1ba0e5588c5525482681db418268e5f86186"}, + {file = "pydantic-1.10.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3ac1cd4deed871dfe0c5f63721e29debf03e2deefa41b3ed5eb5f5df287c7b70"}, + {file = "pydantic-1.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:b1eb6610330a1dfba9ce142ada792f26bbef1255b75f538196a39e9e90388bf4"}, + {file = "pydantic-1.10.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4ca83739c1263a044ec8b79df4eefc34bbac87191f0a513d00dd47d46e307a65"}, + {file = "pydantic-1.10.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4e2a7cb409951988e79a469f609bba998a576e6d7b9791ae5d1e0619e1c0f2"}, + {file = "pydantic-1.10.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53de12b4608290992a943801d7756f18a37b7aee284b9ffa794ee8ea8153f8e2"}, + {file = "pydantic-1.10.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:60184e80aac3b56933c71c48d6181e630b0fbc61ae455a63322a66a23c14731a"}, + {file = "pydantic-1.10.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:415a3f719ce518e95a92effc7ee30118a25c3d032455d13e121e3840985f2efd"}, + {file = "pydantic-1.10.6-cp37-cp37m-win_amd64.whl", hash = "sha256:72cb30894a34d3a7ab6d959b45a70abac8a2a93b6480fc5a7bfbd9c935bdc4fb"}, + {file = "pydantic-1.10.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3091d2eaeda25391405e36c2fc2ed102b48bac4b384d42b2267310abae350ca6"}, + {file = "pydantic-1.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:751f008cd2afe812a781fd6aa2fb66c620ca2e1a13b6a2152b1ad51553cb4b77"}, + {file = "pydantic-1.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12e837fd320dd30bd625be1b101e3b62edc096a49835392dcf418f1a5ac2b832"}, + {file = "pydantic-1.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d92831d0115874d766b1f5fddcdde0c5b6c60f8c6111a394078ec227fca6d"}, + {file = "pydantic-1.10.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:476f6674303ae7965730a382a8e8d7fae18b8004b7b69a56c3d8fa93968aa21c"}, + {file = "pydantic-1.10.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3a2be0a0f32c83265fd71a45027201e1278beaa82ea88ea5b345eea6afa9ac7f"}, + {file = "pydantic-1.10.6-cp38-cp38-win_amd64.whl", hash = "sha256:0abd9c60eee6201b853b6c4be104edfba4f8f6c5f3623f8e1dba90634d63eb35"}, + {file = "pydantic-1.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6195ca908045054dd2d57eb9c39a5fe86409968b8040de8c2240186da0769da7"}, + {file = "pydantic-1.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43cdeca8d30de9a897440e3fb8866f827c4c31f6c73838e3a01a14b03b067b1d"}, + {file = "pydantic-1.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c19eb5163167489cb1e0161ae9220dadd4fc609a42649e7e84a8fa8fff7a80f"}, + {file = "pydantic-1.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:012c99a9c0d18cfde7469aa1ebff922e24b0c706d03ead96940f5465f2c9cf62"}, + {file = "pydantic-1.10.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:528dcf7ec49fb5a84bf6fe346c1cc3c55b0e7603c2123881996ca3ad79db5bfc"}, + {file = "pydantic-1.10.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:163e79386c3547c49366e959d01e37fc30252285a70619ffc1b10ede4758250a"}, + {file = "pydantic-1.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:189318051c3d57821f7233ecc94708767dd67687a614a4e8f92b4a020d4ffd06"}, + {file = "pydantic-1.10.6-py3-none-any.whl", hash = "sha256:acc6783751ac9c9bc4680379edd6d286468a1dc8d7d9906cd6f1186ed682b2b0"}, + {file = "pydantic-1.10.6.tar.gz", hash = "sha256:cf95adb0d1671fc38d8c43dd921ad5814a735e7d9b4d9e437c088002863854fd"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pytest" +version = "7.2.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, + {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + [[package]] name = "termcolor" version = "2.2.0" @@ -148,7 +305,7 @@ tests = ["pytest", "pytest-cov"] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -160,7 +317,7 @@ files = [ name = "typing-extensions" version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -171,4 +328,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "95dd14e87750e4830d773cac5ea2f5b810d406fb34b1913243c8ba6a2145db54" +content-hash = "6482f7b9919f0671ca3ece7b69a1783b8b7284cc54f6b0e1b390613806965f7b" diff --git a/project_manager/command.py b/project_manager/command.py index 6e5e119..19da07c 100644 --- a/project_manager/command.py +++ b/project_manager/command.py @@ -1,6 +1,6 @@ __all__ = ( "CommandError", - "Command", + "CommandExecutor", ) @@ -8,7 +8,7 @@ import logging import subprocess from pathlib import Path -logger = logging.getLogger(">") +logger = logging.getLogger("command") class CommandError(RuntimeError): @@ -17,18 +17,16 @@ class CommandError(RuntimeError): return self.args[0].decode("utf-8").strip() -class Command: +class CommandExecutor: _logger = logger - def __init__(self, executor: str, *args: str) -> None: - self._executor = executor - self._args = args + def __init__(self, command: str) -> None: + self._command = command def run(self, basepath: Path): - args = [self._executor, *self._args] - self._logger.debug(" ".join(args)) + self._logger.debug(self._command) result = subprocess.run( - args, + self._command.split(" "), cwd=basepath, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/project_manager/default_preset.toml b/project_manager/default_preset.toml new file mode 100644 index 0000000..fd1d01b --- /dev/null +++ b/project_manager/default_preset.toml @@ -0,0 +1,35 @@ +[presets.python] +filename = "pyproject.toml" +loader = "tomli:load" + +[presets.python.tasks.setup.poetry] +conditions = { "build-system.build-backend" = "poetry.core.masonry.api" } +commands = ["poetry env use 3.8", "poetry install"] + +[presets.python.tasks.test.pytest] +conditions = { "tool.poetry.group.dev.dependencies.pytest" = "*" } +commands = ["poetry run pytest -p no:warnings"] + +[presets.python.tasks.lint.black] +conditions = { "tool.poetry.group.dev.dependencies.black" = "*" } +commands = ["poetry run black --check --diff ./"] + +[presets.python.tasks.format.black] +conditions = { "tool.poetry.group.dev.dependencies.black" = "*" } +commands = ["poetry run black ./"] + +[presets.python.tasks.lint.isort] +conditions = { "tool.poetry.group.dev.dependencies.isort" = "*" } +commands = ["poetry run isort --profile black --check-only ./"] + +[presets.python.tasks.format.isort] +conditions = { "tool.poetry.group.dev.dependencies.isort" = "*" } +commands = ["poetry run isort ./"] + +[presets.nodejs] +filename = "package.json" +loader = "json:load" + +[presets.nodejs.tasks.setup.npm] +conditions = {} +commands = ["npm install-clean"] diff --git a/project_manager/entries/nodejs.py b/project_manager/entries/nodejs.py deleted file mode 100644 index 47305d4..0000000 --- a/project_manager/entries/nodejs.py +++ /dev/null @@ -1,24 +0,0 @@ -from pathlib import Path -from typing import Optional - -from ..entry import Entry - - -class NodejsEntry(Entry): - TYPE: str = "nodejs" - COMMANDS = { - "setup": [ - ["npm", "install-clean"], - ], - "test": [ - ["npm", "test"], - ], - } - - def __init__(self, basepath: Path): - super().__init__(self.TYPE, self.COMMANDS, basepath) - - @classmethod - def detect(cls, basepath: Path) -> Optional["NodejsEntry"]: - if (basepath / "package.json").exists(): - return cls(basepath) diff --git a/project_manager/entries/python.py b/project_manager/entries/python.py deleted file mode 100644 index aeddc39..0000000 --- a/project_manager/entries/python.py +++ /dev/null @@ -1,33 +0,0 @@ -from pathlib import Path -from typing import Optional - -from ..entry import Entry - - -class PythonEntry(Entry): - TYPE: str = "python" - COMMANDS = { - "setup": [ - ["poetry", "env", "use", "3.8"], - ["poetry", "install"], - ], - "lint": [ - ["poetry", "run", "black", "--check", "--diff", "./"], - ["poetry", "run", "isort", "--profile", "black", "--check-only", "./"], - ], - "format": [ - ["poetry", "run", "isort", "./"], - ["poetry", "run", "black", "./"], - ], - "test": [ - ["poetry", "run", "pytest", "-p", "no:warnings"], - ], - } - - def __init__(self, basepath: Path): - super().__init__(self.TYPE, self.COMMANDS, basepath) - - @classmethod - def detect(cls, basepath: Path) -> Optional["PythonEntry"]: - if (basepath / "pyproject.toml").exists(): - return cls(basepath) diff --git a/project_manager/entry.py b/project_manager/entry.py index f2c6f77..cb931dd 100644 --- a/project_manager/entry.py +++ b/project_manager/entry.py @@ -1,19 +1,20 @@ -__all__ = ( - "EntryCommandNotImplementedError", - "Entry", -) - - import logging from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, Iterable, List, Optional -from .command import Command +from .command import CommandExecutor + +__all__ = ( + "EntryTaskNotImplementedError", + "Entry", + "EntryDetector", + "EntryResolver", +) logger = logging.getLogger("entry") -class EntryCommandNotImplementedError(NotImplementedError): +class EntryTaskNotImplementedError(NotImplementedError): pass @@ -22,27 +23,50 @@ class Entry: def __init__( self, - entry_type: str, - commands: Dict[str, List[List[str]]], + tasks: Dict[str, List[str]], basepath: Path, ): - self._type = entry_type - self._commands = commands + self._tasks = tasks self._basepath = basepath - def run_command(self, command_name: str): - if command_name == "show": - self._logger.info(f"{self}") + def show(self): + self._logger.info(f"{self}") + for task_name, commands in self._tasks.items(): + self._logger.debug(f" {task_name}") + for command in commands: + self._logger.trace(f" {command}") + + def run_task(self, task_name: str): + if task_name == "show": + self.show() return - self._logger.info(f"{command_name} {self}") - if command_name not in self._commands: - raise EntryCommandNotImplementedError(command_name) - for command in self._commands[command_name]: - Command(*command).run(self._basepath) + if task_name not in self._tasks: + raise EntryTaskNotImplementedError(task_name) + self._logger.info(f"{task_name} {self}") + for command in self._tasks[task_name]: + CommandExecutor(command).run(self._basepath) def __repr__(self): - return f"{self._type} {str(self._basepath)}" + return f"{str(self._basepath)}" - @classmethod - def detect(cls, basepath: Path) -> Optional["Entry"]: + +class EntryDetector: + def detect_entry(self, basepath: Path) -> Optional["Entry"]: raise NotImplemented + + +class EntryResolver: + def __init__(self, detector: EntryDetector, ignore: List[str]): + self._detector = detector + self._ignore = ignore + + def resolve(self, basepath: Path, depth: int = 128) -> Iterable[Entry]: + if basepath.is_dir(): + if basepath.name in self._ignore: + return + entry = self._detector.detect_entry(basepath) + if entry: + yield entry + if depth > 0 and basepath.is_dir(): + for path in basepath.iterdir(): + yield from self.resolve(path, depth - 1) diff --git a/project_manager/logging.py b/project_manager/logging.py index 4a94258..495fa5e 100644 --- a/project_manager/logging.py +++ b/project_manager/logging.py @@ -76,6 +76,6 @@ class CustomFormatter(logging.Formatter): } def format(self, record: logging.LogRecord): - log_fmt = colored(record.name, "green") + " " + self.FORMATS.get(record.levelno) + log_fmt = self.FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt) return formatter.format(record) diff --git a/project_manager/main.py b/project_manager/main.py index e55fdb6..05ec912 100644 --- a/project_manager/main.py +++ b/project_manager/main.py @@ -2,7 +2,6 @@ import logging import os import sys from argparse import ArgumentParser -from ast import arg from pathlib import Path from project_manager.logging import CustomFormatter @@ -43,7 +42,7 @@ def run(): basepath = Path(args.path) manager = ProjectManager(basepath) - result = manager.run_command(args.task) + result = manager.run_task(args.task) if result["failed"] > 0: sys.exit(1) diff --git a/project_manager/manager.py b/project_manager/manager.py index d33624b..3d8bc03 100644 --- a/project_manager/manager.py +++ b/project_manager/manager.py @@ -4,10 +4,8 @@ from pathlib import Path from typing import Dict, Iterable from .command import CommandError -from .entries.nodejs import NodejsEntry -from .entries.python import PythonEntry -from .entry import Entry, EntryCommandNotImplementedError -from .resolver import EntryDetector, EntryResolver +from .entry import Entry, EntryResolver, EntryTaskNotImplementedError +from .preset.entry import PresetEntryDetector __all__ = ("ProjectManager",) @@ -20,20 +18,21 @@ class ProjectManager: def __init__(self, basepath: Path): self._basepaht = basepath self._resolver = EntryResolver( - EntryDetector(PythonEntry, NodejsEntry), ["node_modules", ".venv", ".git"] + PresetEntryDetector(Path(__file__).parent / "default_preset.toml"), + ["node_modules", ".venv", ".git"], ) def _iter_entries(self) -> Iterable[Entry]: return self._resolver.resolve(self._basepaht) - def run_command(self, command_name: str) -> Dict[str, int]: + def run_task(self, task_name: str) -> Dict[str, int]: counter = Counter() for entry in self._iter_entries(): try: - entry.run_command(command_name) + entry.run_task(task_name) counter["success"] += 1 - except EntryCommandNotImplementedError: - self._logger.warn(f"{command_name} {entry}") + except EntryTaskNotImplementedError: + self._logger.warn(f"{task_name} {entry}") counter["skipped"] += 1 except CommandError as error: self._logger.error(f"{entry}\n{error.output}") diff --git a/project_manager/entries/__init__.py b/project_manager/preset/__init__.py similarity index 100% rename from project_manager/entries/__init__.py rename to project_manager/preset/__init__.py diff --git a/project_manager/preset/entry.py b/project_manager/preset/entry.py new file mode 100644 index 0000000..0b8935b --- /dev/null +++ b/project_manager/preset/entry.py @@ -0,0 +1,28 @@ +from collections import defaultdict +from pathlib import Path +from typing import Optional + +from ..entry import Entry, EntryDetector +from .util import PresetUtil + +__all__ = ("PresetEntryDetector",) + + +class PresetEntryDetector(EntryDetector): + def __init__(self, filename: Path): + self._preset = PresetUtil.resolve_preset(filename) + + def detect_entry(self, basepath: Path) -> Optional["Entry"]: + tasks = defaultdict(list) + for preset_name, preset in self._preset.presets.items(): + filename = basepath / preset.filename + if filename.exists(): + loader = PresetUtil.resolve_loader(preset.loader) + with open(filename, "rb") as fd: + data = loader(fd) + for task_name, task_pack in preset.tasks.items(): + for task_item_name, task in task_pack.items(): + if PresetUtil.check_conditions(data, task.conditions): + tasks[task_name] += task.commands + if tasks: + return Entry(tasks, basepath) diff --git a/project_manager/preset/model.py b/project_manager/preset/model.py new file mode 100644 index 0000000..d9e7f19 --- /dev/null +++ b/project_manager/preset/model.py @@ -0,0 +1,20 @@ +from typing import Dict, List + +from pydantic import BaseModel + +__all__ = ("TaskPreset", "FilePreset", "Preset") + + +class TaskPreset(BaseModel): + conditions: Dict[str, str] + commands: List[str] + + +class FilePreset(BaseModel): + filename: str + loader: str + tasks: Dict[str, Dict[str, TaskPreset]] + + +class Preset(BaseModel): + presets: Dict[str, FilePreset] diff --git a/project_manager/preset/util.py b/project_manager/preset/util.py new file mode 100644 index 0000000..c56851a --- /dev/null +++ b/project_manager/preset/util.py @@ -0,0 +1,45 @@ +__all__ = ("PresetUtil",) + +import importlib +from pathlib import Path +from typing import Any, Dict + +import tomli + +from .model import Preset + + +class PresetUtil: + @classmethod + def resolve_loader(cls, loader: str) -> Any: + module_name, item_name = loader.split(":") + module = importlib.import_module(module_name) + return getattr(module, item_name) + + @classmethod + def get_key_path_value(cls, key_path: str, data: dict): + path = key_path.split(".") + value = data.get(path[0]) + for key in path[1:]: + if value: + value = value.get(key) + return value + + @classmethod + def match_condition_value(cls, value: Any, pattern: str) -> bool: + if pattern == "*": + return bool(value) + return value == pattern + + @classmethod + def check_conditions(cls, data: dict, conditions: Dict[str, str]) -> bool: + for key, value in conditions.items(): + if not cls.match_condition_value(cls.get_key_path_value(key, data), value): + return False + return True + + @classmethod + def resolve_preset(cls, filename: Path) -> Preset: + with open(filename, "rb") as fd: + preset_data = tomli.load(fd) + return Preset(**preset_data) diff --git a/project_manager/resolver.py b/project_manager/resolver.py deleted file mode 100644 index 404edc8..0000000 --- a/project_manager/resolver.py +++ /dev/null @@ -1,37 +0,0 @@ -from pathlib import Path -from typing import Iterable, List, Optional, Type - -from .entry import Entry - -__all__ = ( - "EntryDetector", - "EntryResolver", -) - - -class EntryDetector: - def __init__(self, *entry_types: Type[Entry]): - self._entry_types = entry_types - - def detect(self, basepath: Path) -> Optional[Entry]: - for entry_type in self._entry_types: - entry = entry_type.detect(basepath) - if entry: - return entry - - -class EntryResolver: - def __init__(self, detector: EntryDetector, ignore: List[str]): - self._detector = detector - self._ignore = ignore - - def resolve(self, basepath: Path, depth: int = 128) -> Iterable[Entry]: - if basepath.is_dir(): - if basepath.name in self._ignore: - return - entry = self._detector.detect(basepath) - if entry: - yield entry - if depth > 0 and basepath.is_dir(): - for path in basepath.iterdir(): - yield from self.resolve(path, depth - 1) diff --git a/pyproject.toml b/pyproject.toml index 22f1796..8fabb35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,13 @@ packages = [{ include = "project_manager" }] [tool.poetry.dependencies] python = "^3.8" termcolor = "^2.2.0" +pydantic = "^1.10.6" +tomli = "^2.0.1" [tool.poetry.group.dev.dependencies] black = "^23.1.0" +pytest = "^7.2.2" +isort = "^5.12.0" [tool.poetry.scripts] pm = "project_manager.main:run"