diff --git a/README.md b/README.md index f8fa225..e069756 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,11 @@ def echo(*args,**kwargs): print args print kwargs +# You can optionally enforce strict task name matching; if left out, the first +# few characters of the task name are enough to execute the task, as long as +# the partial name is unambigious +# __TASK_NAME_RESOLVER__ = 'strict' + # Default task (if specified) is run when no task is specified in the command line # make sure you define the variable __DEFAULT__ after the task is defined # A good convention is to define it at the end of the module @@ -151,7 +156,9 @@ Starting server at localhost:80 [ example.py - Completed task "start_server" ] ``` -The first few characters of the task name is enough to execute the task, as long as the partial name is unambigious. You can specify multiple tasks to run in the commandline. Again the dependencies are taken taken care of. +The first few characters of the task name are enough to execute the task, as long as the partial name is unambigious. This behaviour can be disabled by adding the statement `__TASK_NAME_RESOLVER__ = 'strict'` to your build file. + +You can specify multiple tasks to run in the commandline. Again the dependencies are taken taken care of. ```bash $ pynt cle ht cl diff --git a/README.rst b/README.rst index dafcfa1..59cad96 100644 --- a/README.rst +++ b/README.rst @@ -100,6 +100,11 @@ still be run with ``pynt _private_task_name`` print args print kwargs + # You can optionally enforce strict task name matching; if left out, the first + # few characters of the task name are enough to execute the task, as long as + # the partial name is unambigious + # __TASK_NAME_RESOLVER__ = 'strict' + # Default task (if specified) is run when no task is specified in the command line # make sure you define the variable __DEFAULT__ after the task is defined # A good convention is to define it at the end of the module @@ -160,10 +165,13 @@ ignored). Starting server at localhost:80 [ example.py - Completed task "start_server" ] -The first few characters of the task name is enough to execute the task, -as long as the partial name is unambigious. You can specify multiple -tasks to run in the commandline. Again the dependencies are taken taken -care of. +The first few characters of the task name are enough to execute the task, +as long as the partial name is unambigious. This behaviour can be disabled +by adding the statement `__TASK_NAME_RESOLVER__ = 'strict'` to your build +file. + +You can specify multiple tasks to run in the commandline. Again the dependencies +are taken taken care of. :: diff --git a/pynt/__init__.py b/pynt/__init__.py index d298323..6c84b25 100644 --- a/pynt/__init__.py +++ b/pynt/__init__.py @@ -2,7 +2,7 @@ Lightweight Python Build Tool """ -__version__ = "0.8.2" +__version__ = "0.8.3" __license__ = "MIT License" __contact__ = "http://rags.github.com/pynt/" from ._pynt import task, main diff --git a/pynt/_pynt.py b/pynt/_pynt.py index 8374fcb..801aee0 100644 --- a/pynt/_pynt.py +++ b/pynt/_pynt.py @@ -92,6 +92,12 @@ def _load_buildscript(file_path): with open(file_path, 'r') as script_file: return imp.load_module(module_name, script_file, file_path, description) +def _match_task_names_heuristically(module) -> bool: + for name, value in inspect.getmembers(module, lambda v: isinstance(v, str)): + if name == '__TASK_NAME_RESOLVER__': + return value.strip().lower() != 'strict' + return True + def _get_default_task(module): matching_tasks = [task for name,task in inspect.getmembers(module,Task.is_task) if name == "__DEFAULT__"] @@ -130,17 +136,18 @@ def _get_task(module, name, tasks): args, kwargs= _parse_args(args_str) if hasattr(module, task_name): return getattr(module, task_name), args, kwargs - matching_tasks = [task for task in tasks if task.name.startswith(task_name)] - - if not matching_tasks: - raise Exception("Invalid task '%s'. Task should be one of %s" % - (name, - ', '.join([task.name for task in tasks]))) - if len(matching_tasks) == 1: - return matching_tasks[0], args, kwargs - raise Exception("Conflicting matches %s for task %s" % ( - ', '.join([task.name for task in matching_tasks]), task_name - )) + + if _match_task_names_heuristically(module): + matching_tasks = [task for task in tasks if task.name.startswith(task_name)] + if len(matching_tasks) == 1: + return matching_tasks[0], args, kwargs + elif len(matching_tasks) > 1: + raise Exception("Conflicting matches %s for task %s" % ( + ', '.join([task.name for task in matching_tasks]), task_name + )) + raise Exception("Invalid task '%s'. Task should be one of %s" % + (name, + ', '.join([task.name for task in tasks]))) def _parse_args(args_str): args = [] diff --git a/pynt/tests/build_scripts/build_with_strict_task_names.py b/pynt/tests/build_scripts/build_with_strict_task_names.py new file mode 100644 index 0000000..774d8d8 --- /dev/null +++ b/pynt/tests/build_scripts/build_with_strict_task_names.py @@ -0,0 +1,50 @@ +#!/usr/bin/python + +from pynt import task + +tasks_run = [] + +@task() +def clean(directory='/tmp'): + tasks_run.append('clean[%s]' % directory) + + +@task(clean) +def html(): + tasks_run.append('html') + + +@task() +def tests(*test_names): + tasks_run.append('tests[%s]' % ','.join(test_names)) + + +@task(clean) +def copy_file(from_, to, fail_on_error='True'): + tasks_run.append('copy_file[%s,%s,%s]' % (from_, to, fail_on_error)) + + +@task(clean) +def start_server(port='80', debug='True'): + tasks_run.append('start_server[%s,%s]' % (port, debug)) + +@task(ignore=True) +def ignored(file, contents): + tasks_run.append('append_to_file[%s,%s]' % (file, contents)) + +@task(clean, ignored) +def append_to_file(file, contents): + tasks_run.append('append_to_file[%s,%s]' % (file, contents)) + + +@task(ignored) +def echo(*args,**kwargs): + args_str = [] + if args: + args_str.append(','.join(args)) + if kwargs: + args_str.append(','.join("%s=%s" % (kw, kwargs[kw]) for kw in sorted(kwargs))) + + tasks_run.append('echo[%s]' % ','.join(args_str)) + +__TASK_NAME_RESOLVER__ = 'strict' \ No newline at end of file diff --git a/pynt/tests/test_pynt.py b/pynt/tests/test_pynt.py index f67cefd..51df31e 100644 --- a/pynt/tests/test_pynt.py +++ b/pynt/tests/test_pynt.py @@ -203,6 +203,24 @@ def test_exception_on_conflicting_partial_names(self): 'Conflicting matches copy_file, clean for task c' in str(exc.value)) +class TestStrictTaskNames: + + def setup_method(self): + from .build_scripts import build_with_strict_task_names + self._mod = build_with_strict_task_names + + def test_task_resolver_statement(self): + assert False == _pynt._match_task_names_heuristically(self._mod) + + def test_exception_on_partial_task_name(self): + with pytest.raises(Exception) as exc: + build(self._mod, ["c"]) + assert 'Invalid task \'c\'' in str(exc.value) + + def test_with_valid_task_name_and_dependencies(self): + mod = build(self._mod, ["html"]) + assert ['clean[/tmp]','html'] == mod.tasks_run + class TestDefaultTask: def test_simple_default_task(self):