Skip to content
This repository has been archived by the owner on Jan 14, 2024. It is now read-only.

Commit

Permalink
#59: Add rkd.api.parsing.SyntaxParsing interface
Browse files Browse the repository at this point in the history
  • Loading branch information
blackandred committed Nov 21, 2020
1 parent b5668df commit 6bc8a8d
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 83 deletions.
5 changes: 5 additions & 0 deletions docs/source/usage/tasks-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ Storing temporary files
.. autoclass:: rkd.api.temp.TempManager
:members:

Parsing RKD syntax
------------------
.. autoclass:: rkd.api.syntax.SyntaxParsing
:members:

Testing
-------

Expand Down
55 changes: 55 additions & 0 deletions src/rkd/api/parsing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import importlib
from types import FunctionType
from typing import List
from ..exception import ParsingException
from .syntax import TaskDeclaration


class SyntaxParsing(object):
@staticmethod
def parse_imports_by_list_of_classes(classes_or_modules: List[str]) -> List[TaskDeclaration]:
"""
Parses a List[str] of imports, like in YAML syntax.
Produces a List[TaskDeclaration] with imported list of tasks.
Could be used to import & validate RKD tasks.
Examples:
- rkd.standardlib
- rkd.standardlib.jinja.FileRendererTask
:raises ParsingException
:return:
"""

parsed: List[TaskDeclaration] = []

for import_str in classes_or_modules:
parts = import_str.split('.')
class_name = parts[-1]
import_path = '.'.join(parts[:-1])

# importing just a full module name eg. "rkd_python"
if len(parts) == 1:
import_path = import_str
class_name = 'imports'
# Test if it's not a class name
# In this case we treat is as a module and import an importing method imports()
elif class_name.lower() == class_name:
import_path += '.' + class_name
class_name = 'imports'

try:
module = importlib.import_module(import_path)
except ImportError as e:
raise ParsingException.from_import_error(import_str, class_name, e)

if class_name not in dir(module):
raise ParsingException.from_class_not_found_in_module_error(import_str, class_name, import_path)

if isinstance(module.__getattribute__(class_name), FunctionType):
parsed += module.__getattribute__(class_name)()
else:
parsed.append(TaskDeclaration(module.__getattribute__(class_name)()))

return parsed
21 changes: 21 additions & 0 deletions src/rkd/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ def __init__(self, err: ValidationError):
))


class ParsingException(ContextException):
"""Errors related to parsing YAML/Python syntax"""

@classmethod
def from_import_error(cls, import_str: str, class_name: str, error: Exception) -> 'ParsingException':
return cls(
'Import "%s" is invalid - cannot import class "%s" - error: %s' % (
import_str, class_name, str(error)
)
)

@classmethod
def from_class_not_found_in_module_error(cls, import_str: str, class_name: str,
import_path: str) -> 'ParsingException':
return cls(
'Import "%s" is invalid. Class "%s" not found in module "%s"' % (
import_str, class_name, import_path
)
)


class DeclarationException(ContextException):
"""Something wrong with the makefile.py/makefile.yaml """

Expand Down
1 change: 0 additions & 1 deletion src/rkd/standardlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from ..inputoutput import SystemIO
from ..inputoutput import clear_formatting
from ..aliasgroups import parse_alias_groups_from_env, AliasGroup

from .shell import ShellCommandTask


Expand Down
44 changes: 6 additions & 38 deletions src/rkd/yaml_context.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import yaml
import os
import importlib
from types import FunctionType
from typing import List, Tuple, Union, Callable
from dotenv import dotenv_values
from copy import deepcopy
from collections import OrderedDict
from .exception import YamlParsingException
from .api.parsing import SyntaxParsing
from .exception import YamlParsingException, ParsingException
from .exception import EnvironmentVariablesFileNotFound
from .api.syntax import TaskDeclaration
from .api.syntax import TaskAliasDeclaration
Expand Down Expand Up @@ -228,38 +227,7 @@ def parse_imports(classes: List[str]) -> List[TaskDeclaration]:
YamlParsingException: When a class or module does not exist
"""

parsed: List[TaskDeclaration] = []

for import_str in classes:
parts = import_str.split('.')
class_name = parts[-1]
import_path = '.'.join(parts[:-1])

# importing just a full module name eg. "rkd_python"
if len(parts) == 1:
import_path = import_str
class_name = 'imports'
# Test if it's not a class name
# In this case we treat is as a module and import an importing method imports()
elif class_name.lower() == class_name:
import_path += '.' + class_name
class_name = 'imports'

try:
module = importlib.import_module(import_path)
except ImportError as e:
raise YamlParsingException('Import "%s" is invalid - cannot import class "%s" - error: %s' % (
import_str, class_name, str(e)
))

if class_name not in dir(module):
raise YamlParsingException('Import "%s" is invalid. Class "%s" not found in module "%s"' % (
import_str, class_name, import_path
))

if isinstance(module.__getattribute__(class_name), FunctionType):
parsed += module.__getattribute__(class_name)()
else:
parsed.append(TaskDeclaration(module.__getattribute__(class_name)()))

return parsed
try:
return SyntaxParsing.parse_imports_by_list_of_classes(classes)
except ParsingException as e:
raise YamlParsingException(str(e))
46 changes: 46 additions & 0 deletions test/test_api_parsing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3

from rkd.api.testing import BasicTestingCase
from rkd.api.parsing import SyntaxParsing
from rkd.exception import DeclarationException, ParsingException


class TestApiParsing(BasicTestingCase):
def test_parse_imports_successful_case_single_task(self):
imported = SyntaxParsing.parse_imports_by_list_of_classes(['rkd.standardlib.jinja.RenderDirectoryTask'])

self.assertEqual(':j2:directory-to-directory', imported[0].to_full_name())

def test_parse_imports_successful_case_module(self):
imported = SyntaxParsing.parse_imports_by_list_of_classes(['rkd.standardlib.jinja'])

names_of_imported_tasks = []

for task in imported:
names_of_imported_tasks.append(task.to_full_name())

self.assertIn(':j2:render', names_of_imported_tasks)
self.assertIn(':j2:directory-to-directory', names_of_imported_tasks)

def test_parse_imports_wrong_class_type_but_existing(self):
def test():
SyntaxParsing.parse_imports_by_list_of_classes(['rkd.exception.ContextException'])

self.assertRaises(DeclarationException, test)

def test_parse_imports_cannot_import_non_existing_class(self):
def test():
SyntaxParsing.parse_imports_by_list_of_classes(['rkd.standardlib.python.WRONG_NAME'])

self.assertRaises(ParsingException, test)

def test_parse_imports_importing_whole_module_without_submodules(self):
imported = SyntaxParsing.parse_imports_by_list_of_classes(['rkd_python'])

names_of_imported_tasks = []

for task in imported:
names_of_imported_tasks.append(task.to_full_name())

self.assertIn(':py:build', names_of_imported_tasks)
self.assertIn(':py:publish', names_of_imported_tasks)
44 changes: 0 additions & 44 deletions test/test_yaml_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,6 @@


class TestYamlContext(BasicTestingCase):
def test_parse_imports_successful_case_single_task(self):
factory = YamlSyntaxInterpreter(NullSystemIO(), YamlFileLoader([]))
imported = factory.parse_imports(['rkd.standardlib.jinja.RenderDirectoryTask'])

self.assertEqual(':j2:directory-to-directory', imported[0].to_full_name())

def test_parse_imports_successful_case_module(self):
factory = YamlSyntaxInterpreter(NullSystemIO(), YamlFileLoader([]))
imported = factory.parse_imports(['rkd.standardlib.jinja'])

names_of_imported_tasks = []

for task in imported:
names_of_imported_tasks.append(task.to_full_name())

self.assertIn(':j2:render', names_of_imported_tasks)
self.assertIn(':j2:directory-to-directory', names_of_imported_tasks)

def test_parse_imports_wrong_class_type_but_existing(self):
def test():
factory = YamlSyntaxInterpreter(NullSystemIO(), YamlFileLoader([]))
factory.parse_imports(['rkd.exception.ContextException'])

self.assertRaises(DeclarationException, test)

def test_parse_imports_cannot_import_non_existing_class(self):
def test():
factory = YamlSyntaxInterpreter(NullSystemIO(), YamlFileLoader([]))
factory.parse_imports(['rkd.standardlib.python.WRONG_NAME'])

self.assertRaises(YamlParsingException, test)

def test_parse_imports_importing_whole_module_without_submodules(self):
factory = YamlSyntaxInterpreter(NullSystemIO(), YamlFileLoader([]))
imported = factory.parse_imports(['rkd_python'])

names_of_imported_tasks = []

for task in imported:
names_of_imported_tasks.append(task.to_full_name())

self.assertIn(':py:build', names_of_imported_tasks)
self.assertIn(':py:publish', names_of_imported_tasks)

def test_parse_tasks_successful_case(self):
"""Successful case with description, arguments and bash steps
"""
Expand Down

0 comments on commit 6bc8a8d

Please sign in to comment.