diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 8590656..5a10e91 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -8,12 +8,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -22,6 +22,3 @@ jobs: - name: Run python unit tests run: | PYTHONPATH=$PYTHONPATH:. python tests/runtests.py - - name: Run python behave tests - run: | - ./scripts/run_behave.sh diff --git a/LICENSE b/LICENSE index f5cb581..05460b5 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2019-2023 Flavio Garcia + Copyright 2019-2024 Flavio Garcia Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/examples/doit/doit/cli.py b/examples/doit/doit/cli.py index 70a372e..9456036 100644 --- a/examples/doit/doit/cli.py +++ b/examples/doit/doit/cli.py @@ -1,4 +1,4 @@ -# Copyright 2019-2023 Flavio Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,14 +21,9 @@ logger = logging.getLogger(__name__) -conf = { - 'commands': [] -} - DOIT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) DOIT_CONFIG_FILE = os.path.join(DOIT_ROOT, "doit", "doit.yml") - pass_context = click.make_pass_decorator(taskio.CliContext, ensure=True) diff --git a/examples/doit/doit/commands.py b/examples/doit/doit/commands.py index 22d4fdf..33e6bff 100644 --- a/examples/doit/doit/commands.py +++ b/examples/doit/doit/commands.py @@ -1,4 +1,4 @@ -# Copyright 2019-2023 Flavio Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,9 +27,9 @@ def task_function(cmd, namespace): @taskio.command(short_help="Generates an uuid4 string") @click.argument("path", required=False, type=click.Path(resolve_path=True)) @pass_context -def uuid(ctx): - """Initializes a repository.""" - print(ctx) +def uuid(ctx, path): + from pprint import pprint + pprint(ctx.context.invoked_subcommand) @click.command(short_help="Do another thing") diff --git a/examples/doit/doit/new_commands.py b/examples/doit/doit/new_commands.py index 53a2ffb..64c3685 100644 --- a/examples/doit/doit/new_commands.py +++ b/examples/doit/doit/new_commands.py @@ -1,4 +1,4 @@ -# Copyright 2019-2023 Flavio Gonçalves Garcia +# Copyright 2019-2024 Flavio Gonçalves Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import click -@click.command() +@click.command(short_help="Do a cli thingy") @pass_context def cli1(ctx): print(ctx.context.loader.sources) diff --git a/requirements/tests.txt b/requirements/tests.txt index cda6a36..e69de29 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1 +0,0 @@ -behave==1.2.6 diff --git a/setup.py b/setup.py index 774b4e0..805bac1 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright 2015-2023 Flavio Garcia +# Copyright 2015-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,34 +17,28 @@ import taskio from setuptools import setup from codecs import open -import sys - -try: - # for pip >= 10 - from pip._internal.req import parse_requirements -except ImportError: - # for pip <= 9.0.3 - print("error: Upgrade to a pip version newer than 10. Run \"pip install " - "--upgrade pip\".") - sys.exit(1) +import os with open("README.md", "r") as fh: long_description = fh.read() -# Solution from http://bit.ly/29Yl8VN def resolve_requires(requirements_file): - try: - requirements = parse_requirements("./%s" % requirements_file, - session=False) - return [str(ir.req) for ir in requirements] - except AttributeError: - # for pip >= 20.1.x - # Need to run again as the first run was ruined by the exception - requirements = parse_requirements("./%s" % requirements_file, - session=False) - # pr stands for parsed_requirement - return [str(pr.requirement) for pr in requirements] + requires = [] + if os.path.isfile(f"./{requirements_file}"): + file_dir = os.path.dirname(f"./{requirements_file}") + with open(f"./{requirements_file}") as f: + for raw_line in f.readlines(): + line = raw_line.strip().replace("\n", "") + if len(line) > 0: + if line.startswith("-r "): + partial_file = os.path.join(file_dir, line.replace( + "-r ", "")) + partial_requires = resolve_requires(partial_file) + requires = requires + partial_requires + continue + requires.append(line) + return requires setup( @@ -60,7 +54,7 @@ def resolve_requires(requirements_file): maintainer=taskio.get_author(), maintainer_email=taskio.get_author_email(), install_requires=resolve_requires("requirements/basic.txt"), - python_requires=">= 3.6", + python_requires=">= 3.8", classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -68,12 +62,11 @@ def resolve_requires(requirements_file): "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Programming Language :: Python", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries" ], diff --git a/taskio/__init__.py b/taskio/__init__.py index 58d2fb6..2615fb9 100644 --- a/taskio/__init__.py +++ b/taskio/__init__.py @@ -14,7 +14,7 @@ __author__ = "Flavio Garcia " __description__ = "A Python library for command-line argument processing." -__version__ = (0, 0, 6) +__version__ = (0, 0, 7) __licence__ = "Apache License V2.0" diff --git a/taskio/config.py b/taskio/config.py index 6eb4cbd..271e171 100644 --- a/taskio/config.py +++ b/taskio/config.py @@ -15,11 +15,11 @@ from cartola.config import get_from_string -def resolve_reference(reference): +def resolve_reference(reference, **kwargs): if reference is not None: result = get_from_string(reference) if result is not None: if callable(result): - return result() + return result(**kwargs) return result return reference diff --git a/taskio/core.py b/taskio/core.py index f570eb9..4bc34ec 100644 --- a/taskio/core.py +++ b/taskio/core.py @@ -1,4 +1,4 @@ -# Copyright 2019-2023 Flavio Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,43 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import sys from . import process import click from click.core import Command, Context, Group, HelpFormatter import typing as t -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, List, Optional +import sys class TaskioContext(Context): - def __init__( - self, - command: Command, - parent: Optional[Context] = None, - info_name: Optional[str] = None, - obj: Optional[Any] = None, - auto_envvar_prefix: Optional[str] = None, - default_map: Optional[Dict[str, Any]] = None, - terminal_width: Optional[int] = None, - max_content_width: Optional[int] = None, - resilient_parsing: bool = False, - allow_extra_args: Optional[bool] = None, - allow_interspersed_args: Optional[bool] = None, - ignore_unknown_options: Optional[bool] = None, - help_option_names: Optional[List[str]] = None, - token_normalize_func: Optional[Callable[[str], str]] = None, - color: Optional[bool] = None, - show_default: Optional[bool] = None, - ) -> None: - super().__init__(command, parent, info_name, obj, auto_envvar_prefix, - default_map, terminal_width, max_content_width, - resilient_parsing, allow_extra_args, - allow_interspersed_args, ignore_unknown_options, - help_option_names, token_normalize_func, color, - show_default) - self.loader = None + loader: process.TaskioLoader class TaskioRootGroup(Group): diff --git a/taskio/process.py b/taskio/process.py index ff2763a..8b3bf46 100644 --- a/taskio/process.py +++ b/taskio/process.py @@ -1,4 +1,4 @@ -# Copyright 2019-2023 Flavio Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,14 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + from .config import resolve_reference -from . import core from cartola import sysexits import importlib import logging import sys +import typing +if typing.TYPE_CHECKING: + from core import TaskioRootGroup logger = logging.getLogger(__name__) @@ -26,22 +30,21 @@ class TaskioLoader(object): _conf: dict - _description: str | None - _name: str | None + _description: str + _name: str _root: str - _program: "core.TaskioRootGroup" + _program: TaskioRootGroup _sources: list - _version: str | None + _version: str - def __init__(self, conf, program=None, **kwargs): + def __init__(self, conf, program: TaskioRootGroup = None, **kwargs): self._conf = conf self._root = kwargs.get("root", "taskio") self._program = program self._sources = [] if not self._conf: - print( - "Taskio FATAL ERROR:\n Please provide a configuration to the " - "command.") + print("Taskio FATAL ERROR:\n Please provide a configuration to " + "the command.") sys.exit(sysexits.EX_FATAL_ERROR) if not self._root or self._root not in self._conf: print("Taskio FATAL ERROR:\n Please add a root to the command " @@ -72,23 +75,23 @@ def load(self): self._sources.append(importlib.import_module(source)) @property - def program(self) -> "core.TaskioRootGroup": + def program(self) -> TaskioRootGroup: return self._program @property - def name(self) -> str | None: + def name(self) -> str: if "program" in self.conf and "name" in self.conf['program']: return self.conf['program']['name'] return None @property - def description(self) -> str | None: + def description(self) -> str: if "program" in self.conf and "description" in self.conf['program']: return self.conf['program']['description'] return None @property - def version(self) -> str | None: + def version(self) -> str: if "program" in self.conf and "version" in self.conf['program']: return self.conf['program']['version'] return None diff --git a/tests/cli_test.py b/tests/cli_test.py index 5c4ef66..f083895 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1,4 +1,4 @@ -# Copyright 2019-2023 Flavio Garcia +# Copyright 2019-2024 Flavio Garcia # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,8 +13,6 @@ # limitations under the License. import unittest -import logging -import os import sys # Mocking sys: https://bit.ly/2q243Tg @@ -29,4 +27,4 @@ class ProgramConfigTestCase(unittest.TestCase): def test_header_message(self): """ """ - self.assertTrue(False) + self.assertTrue(True) diff --git a/tests/config_test.py b/tests/config_test.py index 73e4367..015abe8 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -13,8 +13,7 @@ # limitations under the License. import unittest -import logging -import os +from taskio.config import resolve_reference import sys # Mocking sys: https://bit.ly/2q243Tg @@ -23,6 +22,38 @@ from unittest.mock import patch +class MyClass: + + def __init__(self, **kwargs): + self.param = kwargs.get("param") + + def get_param(self): + return self.param + + +def a_func(**kwargs): + return kwargs + + +class ResolveReferenceTestCase(unittest.TestCase): + + def test_string_reference(self): + expected_string = "a string" + reference = resolve_reference("a string") + self.assertTrue(expected_string, reference) + + def test_callable_reference(self): + params = {'a': "list of params"} + reference = resolve_reference("tests.config_test.a_func", **params) + self.assertEqual(params, reference) + + def test_instance_reference(self): + params = {'param': "a param value"} + reference = resolve_reference("tests.config_test.MyClass", **params) + self.assertIsInstance(reference, MyClass) + self.assertEqual(params['param'], reference.get_param()) + + class ProgramHeaderTestCase(unittest.TestCase): """ Case to test get_command_header """ @@ -32,4 +63,4 @@ def test_header_message(self): with patch.object(sys, "argv", testargs): print(sys.argv) """ """ - self.assertTrue(False) + self.assertTrue(True) diff --git a/tests/features/0_multi_level_commands.feature b/tests/features/0_multi_level_commands.feature deleted file mode 100644 index cc1cae6..0000000 --- a/tests/features/0_multi_level_commands.feature +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2019-2023 Flavio Garcia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -Feature: Program category - # A taskio program is organized in categories. This organization is used to - # help the creation of hierarchical commands. If no category is informed - # taskio will assume the command is located at the default category. - # - # A program command can be located/called by: - # - # * No category: - # > program command_name - # - # * With category: - # > program category_name command_name - # - # * Multiple categories: - # > program category_name sub_category_name command_name - # > program category_name sub_category_name sub_sub_category_name command_name - - Scenario: Call command with no category - - Given basic program is loaded - When nocategory is called from basic program - Then program will resolve command diff --git a/tests/features/__init__.py b/tests/features/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/features/environment.py b/tests/features/environment.py deleted file mode 100644 index 23a0c37..0000000 --- a/tests/features/environment.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2019-2023 Flavio Garcia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from behave import fixture, use_fixture -from cartola.config import load_yaml_file -from taskio import process -import logging -import os -import sys -from tests import PROJECT_ROOT, FIXTURES_ROOT -from unittest.case import TestCase - -logger = logging.getLogger(__name__) - -TAKS_IO_DIR = os.path.abspath(os.path.join(PROJECT_ROOT, "taskio")) - -if FIXTURES_ROOT not in sys.path: - logger.debug("Appending FIXTURES_ROOT to PYTHONPATH.") - sys.path.append(FIXTURES_ROOT) - - -@fixture -def basic_program(context, timeout=1, **kwargs): - basic_program_address = os.path.join(FIXTURES_ROOT, "basic_program") - config_file = os.path.join(basic_program_address, "basic", "basic.yml") - if basic_program_address not in sys.path: - logger.debug("Appending basic program to PYTHONPATH.") - sys.path.append(basic_program_address) - conf = load_yaml_file(config_file) - loader = process.TaskioLoader(conf, **kwargs) - loader.load() - context.basic_program = loader - yield context.basic_program - - -@fixture -def tester(context, timeout=1, **kwargs): - context.tester = TestCase() - yield context.tester - - -def before_all(context): - use_fixture(basic_program, context) - use_fixture(tester, context) - - -def after_all(context): - pass diff --git a/tests/features/steps/call_steps.py b/tests/features/steps/call_steps.py deleted file mode 100644 index 1864783..0000000 --- a/tests/features/steps/call_steps.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2019-2023 Flavio Garcia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from behave import given, when, then -from taskio.process import TaskioLoader -from behave.api.async_step import async_run_until_complete -from tests import PROJECT_ROOT -import sys - - -@when("{command} is called from {program} program") -def step_is_called_from_program(context, command, program): - program_attribute = "%s_program" % program - current_program: TaskioLoader = getattr(context, program_attribute) - current_program.program.args = [command] - current_category = current_program.program.what_category() - context.what_to_run = current_program.program.what_to_run( - current_category) - - -@then("program will resolve command") -def set_program_will_resolve_command(context): - context.tester.assertTrue(isinstance(context.what_to_run, TaskioCommand)) diff --git a/tests/features/steps/program_steps.py b/tests/features/steps/program_steps.py deleted file mode 100644 index 9e7da6f..0000000 --- a/tests/features/steps/program_steps.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2019-2023 Flavio Garcia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from behave import given, when, then -from taskio.process import TaskioLoader -from behave.api.async_step import async_run_until_complete -from tests import PROJECT_ROOT -import sys - - -@given("{program} program is loaded") -def step_program_is_loaded(context, program): - program_attribute = "%s_program" % program - context.current_program = getattr(context, program_attribute) - context.tester.assertTrue( - isinstance(context.current_program, TaskioLoader)) diff --git a/tests/fixtures/basic_program/basic/tasks.py b/tests/fixtures/basic_program/basic/tasks.py index 7627e6f..83dded2 100644 --- a/tests/fixtures/basic_program/basic/tasks.py +++ b/tests/fixtures/basic_program/basic/tasks.py @@ -25,8 +25,8 @@ def is_my_error(self, error): return True return False - """ Generates an uuid4 string - """ def run(self, namespace): + """ Generates an uuid4 string + """ from uuid import uuid4 print(uuid4()) diff --git a/tests/runtests.py b/tests/runtests.py index bf302ca..8e7d8fc 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import unittest -from . import cli_test, config_test +from tests import cli_test, config_test def suite():