diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index 15cd6cb..0000000 --- a/babel.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[python: **.py] - diff --git a/muranopkgcheck/cmd/__init__.py b/muranopkgcheck/cmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/muranopkgcheck/cmd/run.py b/muranopkgcheck/cmd/run.py new file mode 100644 index 0000000..a271592 --- /dev/null +++ b/muranopkgcheck/cmd/run.py @@ -0,0 +1,138 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +import argparse +import os +import sys + +import six + +from muranopkgcheck import log + +LOG = log.get_logger(__name__) + + +def parse_cli_args(args=None): + + usage_string = 'murano-pkg-checker [options] ' + + parser = argparse.ArgumentParser( + description='murano-pkg-checker arguments', + formatter_class=argparse.HelpFormatter, + usage=usage_string + ) + + parser.add_argument('--select', + dest='select', + required=False, + type=str, + help='select errors and warnings (e.g. E001,W002)') + + parser.add_argument('--ignore', + dest='ignore', + required=False, + type=str, + help='skip errors and warnings (e.g. E042,W007)') + + parser.add_argument('--verbose', '-v', + dest='verbose', + default=0, + action='count', + help='Verbosity level') + + parser.add_argument('--discover', + dest='discover', + action='store_true', + help='Run discovery packages') + + parser.add_argument('path', + type=str, + help='Path to package or catalog') + + return parser.parse_args(args=args) + + +def setup_logs(args): + if args.verbose == 0: + log.setup(level=log.CRITICAL) + elif args.verbose == 1: + log.setup(level=log.ERROR) + elif args.verbose == 2: + log.setup(level=log.INFO) + else: + log.setup(level=log.DEBUG) + + +def run(args, pkg_path=None): + from muranopkgcheck import manager + m = manager.Manager(pkg_path or args.path) + m.load_plugins() + if args.select: + select = args.select.split(',') + else: + select = None + if args.ignore: + ignore = args.ignore.split(',') + else: + ignore = None + errors = m.validate(select=select, ignore=ignore) + fmt = manager.PlainTextFormatter() + return fmt.format(errors) + + +def discover(args): + errors = [] + for dirpath, dirnames, filenames in os.walk(args.path): + items = dirnames + for item in items: + if item.startswith('.'): + continue + try: + path = os.path.join(dirpath, item) + pkg_errors = run(args, path) + LOG.info("Package {} discovered".format(path)) + if pkg_errors: + errors.append("Errors in package {}\n{}\n" + "".format(path, pkg_errors)) + except ValueError: + pass + return '\n'.join(errors) + + +def main(): + args = parse_cli_args() + setup_logs(args) + global LOG + LOG = log.get_logger(__name__) + try: + if args.discover: + errors = discover(args) + else: + errors = run(args) + + except ValueError as e: + LOG.critical(e) + return 2 + except Exception as e: + LOG.critical(six.text_type(e)) + LOG.exception(e) + return 3 + if errors: + print(errors) + return 1 + else: + print('No errors found!') + +if __name__ == '__main__': + sys.exit(main()) diff --git a/muranopkgcheck/consts.py b/muranopkgcheck/consts.py new file mode 100644 index 0000000..23ed7d6 --- /dev/null +++ b/muranopkgcheck/consts.py @@ -0,0 +1,17 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +MANIFEST_PATH = 'manifest.yaml' +DEFAULT_FORMAT = 'MuranoPL' +DEFAULT_FORMAT_VERSION = '1.0' diff --git a/muranopkgcheck/error.py b/muranopkgcheck/error.py new file mode 100644 index 0000000..70a1258 --- /dev/null +++ b/muranopkgcheck/error.py @@ -0,0 +1,60 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + + +class CheckError(Exception): + + def __init__(self, code, message, filename=None, + line=0, column=0, source=None): + self.code = code + self.message = message + self.filename = filename + self.line = line + self.column = column + self.source = source + + def to_dict(self): + fields = ('code', 'message', 'filename', 'line', 'column', 'source') + serialized = {} + for f in fields: + serialized[f] = self.__getattribute__(f) + return serialized + + def __repr__(self): + return 'CheckError({0})'.format(self.message) + + +def error(code, message, filename=None, line=0, column=0, source=None): + return CheckError(code=code, message=message, filename=filename, + line=line, column=column, source=source) + + +def _report(code): + def _report_(message, yaml_obj=None, filename=None): + meta = getattr(yaml_obj, '__yaml_meta__', None) + kwargs = {} + if meta is not None: + kwargs['line'] = meta.line + 1 + kwargs['column'] = meta.column + 1 + kwargs['source'] = meta.get_snippet() + kwargs['filename'] = filename or meta.name + return CheckError(code=code, message=message, **kwargs) + return _report_ + + +class Report(object): + def __getattr__(self, name): + return _report(name) + +report = Report() diff --git a/muranopkgcheck/log.py b/muranopkgcheck/log.py new file mode 100644 index 0000000..68e0682 --- /dev/null +++ b/muranopkgcheck/log.py @@ -0,0 +1,45 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +import logging + +CRITICAL = logging.CRITICAL +ERROR = logging.ERROR +WARNING = logging.WARNING +INFO = logging.INFO +DEBUG = logging.DEBUG + +LOG_FORMAT = "%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s" +DEFAULT_LEVEL = logging.DEBUG +LOG_HANDLER = None +LOG_LEVEL = DEFAULT_LEVEL + + +def setup(log_format=LOG_FORMAT, level=DEFAULT_LEVEL): + console_log_handler = logging.StreamHandler() + console_log_handler.setFormatter(logging.Formatter(log_format)) + global LOG_HANDLER, LOG_LEVEL + LOG_HANDLER = console_log_handler + LOG_LEVEL = level + + +def get_logger(name): + logger = logging.getLogger(name) + for h in logger.handlers: + logger.removeHandler(h) + logger.addHandler(LOG_HANDLER) + logger.setLevel(LOG_LEVEL) + return logger + +setup() diff --git a/muranopkgcheck/manager.py b/muranopkgcheck/manager.py new file mode 100644 index 0000000..23beef1 --- /dev/null +++ b/muranopkgcheck/manager.py @@ -0,0 +1,101 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +import itertools +import types + +import stevedore + +from muranopkgcheck import error +from muranopkgcheck import log +from muranopkgcheck import pkg_loader +from muranopkgcheck.validators import VALIDATORS + +LOG = log.get_logger(__name__) + + +class Formatter(object): + + def format(self, error): + pass + + +class PlainTextFormatter(Formatter): + + def format(self, errors): + lines = [] + for e in errors: + if e.filename: + lines.append('{filename}:{line}:{column}: {code} {message}' + ''.format(**e.to_dict())) + else: + lines.append('{code} {message}' + ''.format(**e.to_dict())) + + return '\n'.join(lines) + + +class Manager(object): + + def __init__(self, pkg_path): + self.pkg = pkg_loader.load_package(pkg_path) + self.validators = VALIDATORS + self.plugins = None + + def _to_list(self, error_chain, select=None, ignore=None): + errors = [] + while True: + try: + e = next(error_chain, None) + if e is None: + break + except Exception: + LOG.exception('Checker failed') + e = error.report.E000( + 'Checker failed more information in logs') + + if isinstance(e, types.GeneratorType): + errors.extend(self._to_list(e, select, ignore)) + else: + if ((select and e.code not in select) + or (ignore and e.code in ignore)): + continue + errors.append(e) + + return sorted(errors, key=lambda err: err.code) + + def load_plugins(self): + if self.plugins is not None: + return + self.plugins = stevedore.ExtensionManager( + 'muranopkgcheck.plugins', invoke_on_load=True, + propagate_map_exceptions=True, + on_load_failure_callback=self.failure_hook) + plugin_validators = list(itertools.chain( + *(p.obj.validators() for p in self.plugins) + )) + self.validators += plugin_validators + + @staticmethod + def failure_hook(_, ep, err): + LOG.error('Could not load %r: %s', ep.name, err) + raise err + + def validate(self, validators=None, select=None, ignore=None): + validators = validators or self.validators + report_chains = [] + for validator in validators: + v = validator(self.pkg) + report_chains.append(v.run()) + return self._to_list(itertools.chain(*report_chains), select, ignore) diff --git a/muranopkgcheck/pkg_loader.py b/muranopkgcheck/pkg_loader.py new file mode 100644 index 0000000..a1de9e1 --- /dev/null +++ b/muranopkgcheck/pkg_loader.py @@ -0,0 +1,188 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + + +import abc +import os +import re +import zipfile + +import six +import yaml + +from muranopkgcheck import consts +from muranopkgcheck import log +from muranopkgcheck import yaml_loader + +LOG = log.get_logger(__name__) + + +class FileWrapper(object): + + def __init__(self, pkg, path): + self._path = path + with pkg.open_file(path) as file_: + self._raw = file_.read() + + with pkg.open_file(path) as file_: + try: + self._yaml = list(yaml.load_all(file_, yaml_loader.YamlLoader)) + except yaml.YAMLError: + self._yaml = None + + def raw(self): + return self._raw + + def yaml(self): + return self._yaml + + +@six.add_metaclass(abc.ABCMeta) +class BaseLoader(object): + def __init__(self, path): + self.path = path + self._cached_files = dict() + self.format = consts.DEFAULT_FORMAT + self.format_version = consts.DEFAULT_FORMAT_VERSION + + @classmethod + @abc.abstractmethod + def _try_load(cls, path): + pass + + @classmethod + def try_load(cls, path): + loader = cls._try_load(path) + if loader is not None and loader.exists(consts.MANIFEST_PATH): + loader.try_set_format() + return loader + + @abc.abstractmethod + def list_files(self, subdir=None): + pass + + @abc.abstractmethod + def open_file(self, path, mode='r'): + pass + + @abc.abstractmethod + def exists(self, name): + pass + + def search_for(self, regex='.*', subdir=None): + r = re.compile(regex) + return (f for f in self.list_files(subdir) if r.match(f)) + + def read(self, path): + if path in self._cached_files: + return self._cached_files[path] + self._cached_files[path] = FileWrapper(self, path) + return self._cached_files[path] + + def try_set_format(self): + if self.exists(consts.MANIFEST_PATH): + manifest = self.read(consts.MANIFEST_PATH).yaml() + if manifest and 'Format' in manifest: + if '/' in str(manifest['Format']): + fmt, version = manifest['Format'].split('/', 1) + self.format = fmt + self.format_version = version + else: + self.format_version = str(manifest['Format']) + + +class DirectoryLoader(BaseLoader): + + @classmethod + def _try_load(cls, path): + if os.path.isdir(path): + return cls(path) + return None + + def open_file(self, path, mode='r'): + return open(os.path.join(self.path, path), mode) + + def list_files(self, subdir=None): + path = self.path + if subdir is not None: + path = os.path.join(path, subdir) + + files = [] + for dirpath, dirnames, filenames in os.walk(path): + files.extend( + os.path.relpath( + os.path.join(dirpath, filename), self.path) + for filename in filenames) + if subdir is None: + return files + subdir_len = len(subdir) + return [file_[subdir_len:].lstrip('/') for file_ in files] + + def exists(self, name): + return os.path.exists(os.path.join(self.path, name)) + + +class ZipLoader(BaseLoader): + + def __init__(self, path): + super(ZipLoader, self).__init__(path) + self._zipfile = zipfile.ZipFile(self.path) + + @classmethod + def _try_load(cls, path): + try: + return cls(path) + except (IOError, zipfile.BadZipfile): + return None + + def open_file(self, name, mode='r'): + return self._zipfile.open(name, mode) + + def list_files(self, subdir=None): + files = self._zipfile.namelist() + if subdir is None: + return files + subdir_len = len(subdir) + return [file_[subdir_len:].lstrip('/') for file_ in files + if file_.startswith(subdir)] + + def exists(self, name): + try: + self._zipfile.getinfo(name) + return True + except KeyError: + pass + + if not name.endswith('/'): + try: + self._zipfile.getinfo(name + '/') + return True + except KeyError: + pass + return False + + +PACKAGE_LOADERS = [DirectoryLoader, ZipLoader] + + +def load_package(path): + for loader_cls in PACKAGE_LOADERS: + loader = loader_cls.try_load(path) + if loader is not None: + return loader + else: + LOG.debug("{} failed to load '{}'" + "".format(loader_cls.__name__, path)) + else: + raise ValueError("Cannot load package: '{}'".format(path)) diff --git a/muranopkgcheck/plugin.py b/muranopkgcheck/plugin.py new file mode 100644 index 0000000..ead89c6 --- /dev/null +++ b/muranopkgcheck/plugin.py @@ -0,0 +1,29 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class Plugin(object): + + @abc.abstractmethod + def validators(self): + pass + + @abc.abstractmethod + def errors(self): + pass diff --git a/muranopkgcheck/tests/test_manager.py b/muranopkgcheck/tests/test_manager.py new file mode 100644 index 0000000..8251be9 --- /dev/null +++ b/muranopkgcheck/tests/test_manager.py @@ -0,0 +1,67 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +import mock + +from muranopkgcheck import error +from muranopkgcheck import manager +from muranopkgcheck.tests import base + + +class PlainTextFormatterTest(base.TestCase): + + def test_format(self): + + class FakeYamlMeta(object): + + def __init__(self): + self.line = 0 + self.column = 0 + self.name = 'fake' + + def get_snippet(self): + return 'fake' + + class FakeYamlNode(str): + + def __init__(self, value): + super(FakeYamlNode, self).__init__() + self.value = value + self.__yaml_meta__ = FakeYamlMeta() + + formater = manager.PlainTextFormatter() + fake_yaml_node = FakeYamlNode('Fake!!!') + + errors = [error.report.E007('Fake!!!', fake_yaml_node)] + self.assertEqual('fake:1:1: E007 Fake!!!', formater.format(errors)) + + +class ManagerTest(base.TestCase): + + @mock.patch('muranopkgcheck.manager.pkg_loader') + def test_validate(self, m_pkg_loader): + fake_error = error.report.E007('Fake!') + + def error_generator(): + yield fake_error + MockValidator = mock.Mock() + m_validator = MockValidator.return_value + m_validator.run.return_value = (e for e in [ + fake_error, + error_generator() + ]) + mgr = manager.Manager('fake') + errors = mgr.validate(validators=[MockValidator]) + self.assertEqual([fake_error, fake_error], errors) + m_pkg_loader.load_package.assert_called_once_with('fake') diff --git a/muranopkgcheck/tests/test_muranopkgclient.py b/muranopkgcheck/tests/test_muranopkgclient.py deleted file mode 100644 index 1d9afb8..0000000 --- a/muranopkgcheck/tests/test_muranopkgclient.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2010-2011 OpenStack Foundation -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# 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 muranopkgcheck.tests import base - - -class TestCase(base.TestCase): - - """Test case base class for all unit tests.""" - - def test_stub(self): - pass diff --git a/muranopkgcheck/tests/test_pkg_loader.py b/muranopkgcheck/tests/test_pkg_loader.py new file mode 100644 index 0000000..fdfd89e --- /dev/null +++ b/muranopkgcheck/tests/test_pkg_loader.py @@ -0,0 +1,155 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +import mock + +from muranopkgcheck import consts +from muranopkgcheck import pkg_loader +from muranopkgcheck.tests import base + + +class FileWrapperTest(base.TestCase): + + def test_file_wrapper(self): + fake_pkg = mock.Mock() + fake_pkg.open_file.side_effect = \ + lambda f: mock.mock_open(read_data='text')() + f = pkg_loader.FileWrapper(fake_pkg, 'fake_path') + self.assertEqual('text', f.raw()) + self.assertEqual(['text'], f.yaml()) + + fake_pkg.open_file.side_effect = \ + lambda f: mock.mock_open(read_data='!@#$%')() + f = pkg_loader.FileWrapper(fake_pkg, 'fake_path') + self.assertEqual('!@#$%', f.raw()) + self.assertEqual(None, f.yaml()) + + +class FakeLoader(pkg_loader.BaseLoader): + + @classmethod + def _try_load(cls, path): + return cls(path) + + def open_file(self, path, mode='r'): + pass + + def exists(self, name): + if name == consts.MANIFEST_PATH: + return True + + def list_files(self, subdir=None): + if subdir is None: + return ['1.yaml', '2.sh', 'sub/3.yaml'] + else: + return ['3.yaml'] + + +class BaseLoaderTest(base.TestCase): + + @mock.patch.object(FakeLoader, '_try_load') + @mock.patch.object(FakeLoader, 'try_set_format') + def test_try_load(self, m_format, m_load): + m_load.return_value = FakeLoader('fake') + FakeLoader.try_load('fake') + m_load.assert_called_once_with('fake') + m_format.assert_called_once_with() + + @mock.patch.object(FakeLoader, '_try_load') + def test_try_set_version(self, m_load): + m_file_wrapper = mock.Mock() + m_file = m_file_wrapper.return_value + m_file.yaml.return_value = {'Format': 'Fake/42'} + with mock.patch('muranopkgcheck.pkg_loader.FileWrapper', + m_file_wrapper): + m_load.return_value = FakeLoader('fake') + loader = FakeLoader.try_load('fake') + self.assertEqual('Fake', loader.format) + self.assertEqual('42', loader.format_version) + m_file.yaml.assert_called_once_with() + + m_load.return_value = FakeLoader('fake') + m_file.yaml.return_value = {'Format': '4.2'} + loader = FakeLoader.try_load('fake') + self.assertEqual(consts.DEFAULT_FORMAT, loader.format) + self.assertEqual('4.2', loader.format_version) + + def test_search_for(self): + fake = FakeLoader('fake') + self.assertEqual(['1.yaml', 'sub/3.yaml'], + list(fake.search_for('.*\.yaml$'))) + self.assertEqual(['3.yaml'], + list(fake.search_for('.*\.yaml$', subdir='sub'))) + + def test_read(self): + fake = FakeLoader('fake') + m_file_wrapper = mock.Mock() + m_file = m_file_wrapper.return_value + with mock.patch('muranopkgcheck.pkg_loader.FileWrapper', + m_file_wrapper): + loaded = fake.read('fake') + self.assertEqual(m_file, loaded) + # check that cache works + loaded = fake.read('fake') + self.assertEqual(m_file, loaded) + + +class DirectoryLoaderTest(base.TestCase): + + def _load_fake_pkg(self): + with mock.patch('muranopkgcheck.pkg_loader.os.path.isdir') as m_isdir: + with mock.patch.object(pkg_loader.DirectoryLoader, + 'try_set_format') as m: + with mock.patch.object(pkg_loader.DirectoryLoader, + 'exists') as m_exists: + m_exists.return_value = True + m_isdir.return_value = True + loader = pkg_loader.DirectoryLoader.try_load('fake') + m.assert_called_once_with() + return loader + + def test_try_load(self): + # NOTE(sslypushenko) Using mock.patch here as decorator breaks pdb + pkg = self._load_fake_pkg() + self.assertEqual('fake', pkg.path) + with mock.patch('muranopkgcheck.pkg_loader.os.path.isdir') as m_isdir: + m_isdir.return_value = False + pkg = pkg_loader.DirectoryLoader.try_load('fake') + self.assertEqual(None, pkg) + + def test_list_files(self): + # NOTE(sslypushenko) Using mock.patch here as decorator breaks pdb + pkg = self._load_fake_pkg() + with mock.patch('muranopkgcheck.pkg_loader.os.walk') as m_walk: + m_walk.return_value = (item for item in [ + ('fake', ['subdir'], ['1', '2']), + ('fake/subdir', [], ['3', '4']), + ]) + self.assertEqual(['1', '2', 'subdir/3', 'subdir/4'], + pkg.list_files()) + m_walk.return_value = (item for item in [ + ('fake/subdir', [], ['3', '4']), + ]) + self.assertEqual(['3', '4'], + pkg.list_files(subdir='subdir')) + + def test_exist(self): + # NOTE(sslypushenko) Using mock.patch here as decorator breaks pdb + pkg = self._load_fake_pkg() + with mock.patch('muranopkgcheck.pkg_loader' + '.os.path.exists') as m_exists: + m_exists.return_value = True + self.assertTrue(pkg.exists('1.yaml')) + m_exists.return_value = False + self.assertFalse(pkg.exists('1.yaml')) diff --git a/muranopkgcheck/validators/__init__.py b/muranopkgcheck/validators/__init__.py new file mode 100644 index 0000000..704db22 --- /dev/null +++ b/muranopkgcheck/validators/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +VALIDATORS = [ +] diff --git a/muranopkgcheck/yaml_loader.py b/muranopkgcheck/yaml_loader.py new file mode 100644 index 0000000..fcd51da --- /dev/null +++ b/muranopkgcheck/yaml_loader.py @@ -0,0 +1,87 @@ +# Copyright (c) 2016 Mirantis, Inc. +# +# 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. + +import six +import yaml + +__all__ = ['YamlLoader'] + + +class YamlMetadata(object): + def __init__(self, mark): + self.mark = mark + + @property + def line(self): + return self.mark.line + + @property + def column(self): + return self.mark.column + + def get_snippet(self, indent=4, max_length=75): + return self.mark.get_snippet(indent, max_length) + + +class YamlObject(object): + pass + + +class YamlMapping(YamlObject, dict): + pass + + +class YamlSequence(YamlObject, list): + pass + + +class YamlString(YamlObject, six.text_type): + pass + + +BaseLoader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader) + + +class YamlLoader(BaseLoader): + + def construct_yaml_seq(self, node): + data = YamlSequence() + yield data + data.extend(self.construct_sequence(node)) + data.__yaml_meta__ = node.start_mark + + def construct_yaml_str(self, node): + value = super(YamlLoader, self).construct_yaml_str(node) + value = YamlString(value) + value.__yaml_meta__ = node.start_mark + return value + + def construct_yaml_map(self, node): + data = YamlMapping() + yield data + value = self.construct_mapping(node) + data.update(value) + data.__yaml_meta__ = node.start_mark + +YamlLoader.add_constructor( + u'tag:yaml.org,2002:seq', + YamlLoader.construct_yaml_seq) + +YamlLoader.add_constructor( + u'tag:yaml.org,2002:str', + YamlLoader.construct_yaml_str) + +YamlLoader.add_constructor( + u'tag:yaml.org,2002:map', + YamlLoader.construct_yaml_map) diff --git a/requirements.txt b/requirements.txt index 95d0fe8..e685baa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,7 @@ # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 +PyYAML>=3.1.0 # MIT +yaql>=1.1.0 # Apache 2.0 License +six>=1.9.0 # MIT +stevedore>=1.16.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 0b68bb1..8929d60 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,10 @@ all_files = 1 [upload_sphinx] upload-dir = doc/build/html +[entry_points] +console_scripts = + murano-pkg-check = muranopkgcheck.cmd.run:main + [build_releasenotes] all_files = 1 build-dir = releasenotes/build diff --git a/tox.ini b/tox.ini index 76e3e41..c642e82 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skipsdist = True [testenv] usedevelop = True -install_command = pip install -U {opts} {packages} +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} deps =