Commit 6c989cf4 authored by Hueser, Christian (FWCC) - 138593's avatar Hueser, Christian (FWCC) - 138593 Committed by Erxleben, Fredo (FWCC) - 136987
Browse files

Add a CLI option to select analysis scripts to be executed

* New CLI option is called "names" which defaults to no value set, interpreted as "all".
* All scripts selected from scripts folder will be executed.
* README has been adapted.
parent ba9186fa
Pipeline #28781 passed with stages
in 5 minutes and 11 seconds
......@@ -81,7 +81,8 @@ The program accepts a couple of flags:
1. Help flag
2. Verbosity flag
3. Scripts flag
4. Output Format flag
4. Names flag
5. Output Format flag
#### Help flag
......@@ -96,14 +97,18 @@ Usage: survey_analysis [OPTIONS] COMMAND [ARGS]...
Analyze a given CSV file with a set of independent python scripts.
Options:
-v, --verbose Enable verbose output. Repeat up to 2 times for
increased effect [default: 0]
-v, --verbose Enable verbose output. Increase verbosity by
setting this option up to 3 times. [default: 0]
-s, --scripts TEXT Select the folder containing analysis scripts
-s, --scripts TEXT Select the folder containing analysis scripts.
[default: scripts]
-n, --names TEXT Select the script names contained in the scripts
folder as comma-separated list (omitting file
endings) which should be executed. [default: all]
-f, --output-format TEXT Designate output format. Supported values are:
SCREEN, PDF, PNG, SVG [default: screen]
SCREEN, PDF, PNG, SVG. [default: screen]
--help Show this message and exit.
......@@ -141,17 +146,32 @@ Beside verbosity there is a _scripts_-flag called `--scripts` or
`-s` for short:
```shell script
$ pipenv run survey_analysis --scripts scripts/ <COMMAND>
$ pipenv run survey_analysis --scripts "scripts/" <COMMAND>
```
```shell script
$ pipenv run survey_analysis -s scripts/ <COMMAND>
$ pipenv run survey_analysis -s "scripts/" <COMMAND>
```
This will tell the program in which folder to look for the actual
analysis scripts.
In case the _scripts_-flag is omitted it defaults to sub-folder `scripts/`.
#### Output Format flag
#### Names flag
The _names_-flag called `--names` or `-n` for short:
```shell script
$ pipenv run survey_analysis --names "example_script_1" --names "example_script_2" <COMMAND>
```
```shell script
$ pipenv run survey_analysis -n "example_script_1" -n "example_script_2" <COMMAND>
```
This will tell the program which scripts in the scripts folder to execute.
In case the _names_-flag is omitted it defaults to all scripts in the
scripts folder.
#### Output format flag
The user is also able to let the application know in which output format the
diagrams should be generated during the analysis.
......
......@@ -45,13 +45,20 @@ from .__init__ import __version__
@click.option("--scripts", "-s",
default="scripts",
show_default=True,
help="Select the folder containing analysis scripts")
help="Select the folder containing analysis scripts.")
@click.option("--names", "-n",
multiple=True,
default=[],
help="Optionally select the specific script names contained "
"in the scripts folder (while omitting file endings) "
"which should be executed.")
@click.option("--output-format", "-f",
default="screen",
show_default=True,
help=f"Designate output format. "
f"Supported values are: {OutputFormat.list_supported()}")
def cli(verbose: int, scripts: str, output_format: str) -> None:
f"Supported values are: {OutputFormat.list_supported()}.")
def cli(verbose: int, scripts: str, names: List[str],
output_format: str) -> None:
"""Analyze a given CSV file with a set of independent python scripts."""
# NOTE that click takes above documentation for generating help text
# Thus the documentation refers to the application per se and not the
......@@ -61,6 +68,8 @@ def cli(verbose: int, scripts: str, output_format: str) -> None:
logging.info(f"Selected script folder {scripts}")
globals.settings.script_folder = Path(scripts)
# Set a list of selected module names contained in the module folder.
globals.settings.script_names = names
sys.path.insert(0, scripts)
......@@ -74,7 +83,7 @@ def version() -> None:
@click.argument("file_name", type=click.File(mode="r"))
@click.option("--metadata", "-m",
default="data/HIFIS_Software_Survey_2020_Questions.yml",
help="Give file name which contains survey metadata")
help="Give file name which contains survey metadata.")
def analyze(file_name, metadata: str) -> None:
"""
Read the given files into global data and metadata objects.
......@@ -116,7 +125,8 @@ def analyze(file_name, metadata: str) -> None:
logging.error("Could not parse the metadata file as YAML.")
exit(1)
dispatcher = dispatch.Dispatcher(globals.settings.script_folder)
dispatcher = dispatch.Dispatcher(globals.settings.script_folder,
globals.settings.script_names)
dispatcher.discover()
dispatcher.load_all_modules()
......
......@@ -15,38 +15,58 @@ class Dispatcher(object):
"""
Provides analysis function module and execution facilities.
The operations are based on a module folder to be given at initialization.
The operations are based on a module folder and optionally a list of
module names to be given at initialization.
"""
def __init__(self, module_folder: Path):
def __init__(self, module_folder: Path, module_names: List[str]):
"""
Initialize the Dispatcher.
Args:
module_folder: The path to a directory containing loadable modules.
If the given path does not exist or is not a directory a ValueError
will be thrown.
If the given path does not exist or is not a directory a
ValueError will be thrown.
module_names: A list of module names (without file ending)
within module folder to be executed.
If the selected module does not exist in the module directory
a ValueError will be thrown.
"""
if not (module_folder.exists() and module_folder.is_dir()):
raise ValueError("Module folder should be a directory")
raise ValueError("Module folder should be a directory.")
self.module_folder: Path = module_folder
self.module_names: List[str] = module_names
self.module_name_paths: List[Path] = []
self._discovered_modules: List[str] = []
# Check that all selected modules exist in module folder.
if self.module_names:
for module_name in self.module_names:
module_path: Path = Path(module_folder, f"{module_name}.py")
if not module_path.exists():
raise ValueError(f"Module {module_name} not found in "
f"module folder.")
self.module_name_paths.append(module_path)
else:
self.module_name_paths.extend(self.module_folder.iterdir())
def discover(self) -> None:
"""
Discover all potential modules in the module folder.
Discover all potential or selected modules in the module folder.
Iterate over the module folder (non-recursive) and cache the names all
python (.py) files.
Iterate over all modules in the module folder (non-recursive) or
selected modules only and cache the names of those python (.py) files.
Exception: __init__.py is excluded.
"""
for entry in self.module_folder.iterdir():
if entry.is_file() \
and entry.suffix == ".py" \
and not entry.stem == "__init__":
logging.info(f"Discovered module {entry.stem}")
self._discovered_modules.append(entry.stem)
# Execute all scripts in scripts folder or selected scripts only.
for name_path in self.module_name_paths:
if (name_path.is_file()
and name_path.suffix == ".py"
and not name_path.stem == "__init__"):
logging.info(f"Discovered module {name_path.stem}.")
self._discovered_modules.append(name_path.stem)
def load_all_modules(self) -> None:
"""
......@@ -58,7 +78,7 @@ class Dispatcher(object):
"""
if not self._discovered_modules:
logging.warning("No modules have been discovered - Nothing to do")
logging.warning("No modules have been discovered - Nothing to do.")
return
for module_name in self._discovered_modules:
......@@ -79,12 +99,12 @@ class Dispatcher(object):
module_path: Path = self.module_folder / module_name
module_dot_path: str = str(module_path).replace('/', '.')
logging.info(f"Running Module {module_dot_path}")
logging.info(f"Running Module {module_dot_path}.")
try:
module = importlib.import_module(module_dot_path)
except ImportError:
logging.error(f"Failed to load module {module_dot_path}")
logging.error(f"Failed to load module {module_dot_path}.")
try:
module.run()
......@@ -92,4 +112,4 @@ class Dispatcher(object):
traceback.print_exc()
logging.error(f"Module {module_dot_path}: "
f"Error when calling run() - method: "
f"{error}")
f"{error}.")
......@@ -33,5 +33,10 @@ class Settings(object):
def __init__(self): # Note: This object must have an empty constructor.
"""Create a new instance."""
self.verbosity: int = logging.NOTSET
# Path in which modules to be executed are located which defaults
# to "scripts" folder.
self.script_folder: Path = Path("scripts")
# List of selected module names to be executed which defaults to
# an empty list for all modules in the module folder.
self.script_names: List[str] = []
self.output_format: OutputFormat = OutputFormat.SCREEN
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment