From df16010d669226f88efb9131a8f8f2a1bd867ca0 Mon Sep 17 00:00:00 2001 From: Moises Lopez Date: Sat, 10 Oct 2015 23:48:26 -0700 Subject: [PATCH] [ADD] pylint_odoo: Add main package. --- .coveragerc | 17 + .travis.yml | 39 +++ README.md | 56 +++ install.sh | 4 + pylint_odoo/__init__.py | 32 ++ pylint_odoo/augmentations/__init__.py | 1 + pylint_odoo/augmentations/main.py | 23 ++ pylint_odoo/checkers/__init__.py | 3 + pylint_odoo/checkers/format.py | 87 +++++ pylint_odoo/checkers/modules_odoo.py | 157 +++++++++ pylint_odoo/checkers/no_modules.py | 323 ++++++++++++++++++ pylint_odoo/misc.py | 209 ++++++++++++ pylint_odoo/settings.py | 21 ++ pylint_odoo/test/__init__.py | 1 + pylint_odoo/test/main.py | 78 +++++ .../test_repo/broken_module/__init__.py | 3 + .../test_repo/broken_module/__openerp__.py | 14 + .../test_repo/broken_module/broken_example.js | 5 + .../test_repo/broken_module/coding_latin.py | 1 + .../test_repo/broken_module/doc/index.rst | 6 + .../test_repo/broken_module/encoding_utf8.py | 1 + .../broken_module/interpreter_wox.py | 5 + .../test_repo/broken_module/interpreter_wx.py | 4 + .../test_repo/broken_module/model_view.xml | 44 +++ .../test_repo/broken_module/model_view2.xml | 26 ++ .../broken_module/models/__init__.py | 3 + .../broken_module/models/broken_model.py | 37 ++ .../broken_module/pylint_oca_broken.py | 95 ++++++ .../broken_module/wointerpreter_wx.py | 4 + .../test_repo/broken_module/xml_empty.xml | 0 .../broken_module/xml_special_char.xml | 8 + .../broken_module/xml_syntax_error.XML | 2 + .../test_repo/broken_module2/README.rst | 4 + .../test_repo/broken_module2/__init__.py | 1 + .../test_repo/broken_module2/__openerp__.py | 11 + pylint_odoo/test_repo/test_module/README.rst | 15 + pylint_odoo/test_repo/test_module/__init__.py | 0 .../test_repo/test_module/__openerp__.py | 21 ++ .../test_repo/test_module/model_view.xml | 24 ++ .../test_repo/test_module/test_example.js | 5 + .../test_repo/womanifest_module/__init__.py | 1 + .../test_repo/womanifest_module/doc/index.rst | 4 + requirements.txt | 6 + setup.cfg | 18 + setup.py | 8 + 45 files changed, 1427 insertions(+) create mode 100644 .coveragerc create mode 100644 .travis.yml create mode 100644 README.md create mode 100755 install.sh create mode 100644 pylint_odoo/__init__.py create mode 100644 pylint_odoo/augmentations/__init__.py create mode 100644 pylint_odoo/augmentations/main.py create mode 100644 pylint_odoo/checkers/__init__.py create mode 100644 pylint_odoo/checkers/format.py create mode 100644 pylint_odoo/checkers/modules_odoo.py create mode 100644 pylint_odoo/checkers/no_modules.py create mode 100644 pylint_odoo/misc.py create mode 100644 pylint_odoo/settings.py create mode 100644 pylint_odoo/test/__init__.py create mode 100644 pylint_odoo/test/main.py create mode 100644 pylint_odoo/test_repo/broken_module/__init__.py create mode 100644 pylint_odoo/test_repo/broken_module/__openerp__.py create mode 100644 pylint_odoo/test_repo/broken_module/broken_example.js create mode 100644 pylint_odoo/test_repo/broken_module/coding_latin.py create mode 100644 pylint_odoo/test_repo/broken_module/doc/index.rst create mode 100644 pylint_odoo/test_repo/broken_module/encoding_utf8.py create mode 100644 pylint_odoo/test_repo/broken_module/interpreter_wox.py create mode 100755 pylint_odoo/test_repo/broken_module/interpreter_wx.py create mode 100644 pylint_odoo/test_repo/broken_module/model_view.xml create mode 100644 pylint_odoo/test_repo/broken_module/model_view2.xml create mode 100644 pylint_odoo/test_repo/broken_module/models/__init__.py create mode 100644 pylint_odoo/test_repo/broken_module/models/broken_model.py create mode 100644 pylint_odoo/test_repo/broken_module/pylint_oca_broken.py create mode 100755 pylint_odoo/test_repo/broken_module/wointerpreter_wx.py create mode 100644 pylint_odoo/test_repo/broken_module/xml_empty.xml create mode 100644 pylint_odoo/test_repo/broken_module/xml_special_char.xml create mode 100644 pylint_odoo/test_repo/broken_module/xml_syntax_error.XML create mode 100644 pylint_odoo/test_repo/broken_module2/README.rst create mode 100644 pylint_odoo/test_repo/broken_module2/__init__.py create mode 100644 pylint_odoo/test_repo/broken_module2/__openerp__.py create mode 100644 pylint_odoo/test_repo/test_module/README.rst create mode 100644 pylint_odoo/test_repo/test_module/__init__.py create mode 100644 pylint_odoo/test_repo/test_module/__openerp__.py create mode 100644 pylint_odoo/test_repo/test_module/model_view.xml create mode 100644 pylint_odoo/test_repo/test_module/test_example.js create mode 100644 pylint_odoo/test_repo/womanifest_module/__init__.py create mode 100644 pylint_odoo/test_repo/womanifest_module/doc/index.rst create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..0aa19c17 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,17 @@ +# Config file .coveragerc + +[report] +include = + pylint_odoo/* + +omit = + */test_repo/* + *__init__.py + *__openerp__.py + +# Regexes for lines to exclude from consideration +exclude_lines = + # Don't complain about self-execute + if __name__ == '__main__': + # Don't complain about uninstalled packages + except OSError as oserr: diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..3b0201e9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,39 @@ +language: python + +sudo: false +cache: + apt: true + directories: + - $HOME/.cache/pip + +python: + - "2.7" + +addons: + apt: + packages: + - python-lxml # because pip installation is slow + - nodejs + +virtualenv: + system_site_packages: true + +install: + # Remove packages installed from addons-apt-packages of travis file + - find -L ${TRAVIS_BUILD_DIR} -name requirements.txt -exec sed -i '/lxml/d' {} \; + - find -L ${TRAVIS_BUILD_DIR} -name install.sh -exec sed -i '/node/d' {} \; + + # Install dependencies + - ${TRAVIS_BUILD_DIR}/install.sh + + # Install testing dependencies + - pip install --upgrade pyopenssl ndg-httpsclient pyasn1 + - pip install coveralls flake8 + +script: + - flake8 --exclude=__init__.py . + - coverage run setup.py test + +after_success: + - coveralls + - coverage report -m diff --git a/README.md b/README.md new file mode 100644 index 00000000..a0b26a10 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +[![Build Status](https://travis-ci.org/OCA/pylint-odoo.svg?branch=master)](https://travis-ci.org/OCA/pylint-odoo) +[![Coverage Status](https://coveralls.io/repos/OCA/pylint-odoo/badge.svg?branch=master&service=github)](https://coveralls.io/github/OCA/pylint-odoo?branch=master) +[![Pypi Package](https://img.shields.io/pypi/v/pylint-odoo.svg)](https://pypi.python.org/pypi/pylint-odoo) + + + +# Pylint Odoo plugin + +Enable custom checks for Odoo modules. + +[//]: # (checks) +Code | Description | short name +--- | --- | --- +C7902 | Missing ./README.rst file. Template here: %s | missing-readme +C8101 | Missing author required "%s" in manifest file | manifest-required-author +C8102 | Missing required key "%s" in manifest file | manifest-required-key +C8103 | Deprecated key "%s" in manifest file | manifest-deprecated-key +C8104 | Use `CamelCase` "%s" in class name "%s". You can use oca-autopep8 of https://github.com/OCA/maintainer-tools to auto fix it. | class-camelcase +C8105 | License "%s" not allowed in manifest file. | license-allowed +C8201 | No UTF-8 coding comment found: Use `# coding: utf-8` or `# -*- coding: utf-8 -*-` | no-utf8-coding-comment +E7901 | %s:%s %s | rst-syntax-error +E7902 | %s error: %s | xml-syntax-error +R8101 | Import `Warning` should be renamed as UserError `from openerp.exceptions import Warning as UserError` | openerp-exception-warning +W7901 | Dangerous filter without explicit `user_id` in xml_id %s | dangerous-filter-wo-user +W7902 | Duplicate xml record id %s | duplicate-xml-record-id +W7903 | %s | javascript-lint +W8101 | Detected api.one and api.multi decorators together. | api-one-multi-together +W8102 | Missing api.one or api.multi in copy function. | copy-wo-api-one +W8103 | Translation method _("string") in fields is not necessary. | translation-field +W8104 | api.one deprecated | api-one-deprecated +W8105 | attribute "%s" deprecated | attribute-deprecated +W8106 | Missing `super` call in "%s" method. | method-required-super +W8201 | Incoherent interpreter comment and executable permission. Interpreter: [%s] Exec perm: %s | incoherent-interpreter-exec-perm +W8202 | Use of vim comment | use-vim-comment + +[//]: # (end checks) + + +## Install +`# pip install --upgrade git+https://github.com/oca/pylint-odoo.git` + +Or + +`# pip install --upgrade --pre pylint-odoo` + + +## Usage + + `pylint --load-plugins=pylint_odoo -e odoolint ...` + + + Example to test just odoo-lint case: + + `touch {ADDONS-PATH}/__init__.py` + + `pylint --load-plugins=pylint_odoo -d all -e odoolint {ADDONS-PATH}` diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..dac9e26a --- /dev/null +++ b/install.sh @@ -0,0 +1,4 @@ +wget -qO- https://deb.nodesource.com/setup | bash - \ + && apt-get install nodejs +npm install -g jshint +pip install . diff --git a/pylint_odoo/__init__.py b/pylint_odoo/__init__.py new file mode 100644 index 00000000..97bdde65 --- /dev/null +++ b/pylint_odoo/__init__.py @@ -0,0 +1,32 @@ + +from . import checkers +from . augmentations.main import apply_augmentations + + +def register(linter): + """Required method to auto register this checker""" + linter.register_checker(checkers.modules_odoo.ModuleChecker(linter)) + linter.register_checker(checkers.no_modules.NoModuleChecker(linter)) + linter.register_checker(checkers.format.FormatChecker(linter)) + + # register any checking fiddlers + apply_augmentations(linter) + + +def get_all_messages(): + """Get all messages of this plugin""" + all_msgs = {} + all_msgs.update(checkers.modules_odoo.ODOO_MSGS) + all_msgs.update(checkers.no_modules.ODOO_MSGS) + all_msgs.update(checkers.format.ODOO_MSGS) + return all_msgs + + +def messages2md(): + all_msgs = get_all_messages() + md_msgs = 'Code | Description | short name\n--- | --- | ---' + for msg_code, (title, name_key, description) in \ + sorted(all_msgs.iteritems()): + md_msgs += "\n{0} | {1} | {2}".format(msg_code, title, name_key) + md_msgs += '\n' + return md_msgs diff --git a/pylint_odoo/augmentations/__init__.py b/pylint_odoo/augmentations/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/pylint_odoo/augmentations/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/pylint_odoo/augmentations/main.py b/pylint_odoo/augmentations/main.py new file mode 100644 index 00000000..8b008cc5 --- /dev/null +++ b/pylint_odoo/augmentations/main.py @@ -0,0 +1,23 @@ + +import os + +from pylint_plugin_utils import suppress_message +from pylint.checkers.base import BasicChecker +from .. import settings + + +def is_manifest_file(node): + """Verify if the node file is a manifest file + :return: Boolean `True` if is manifest file else `False`""" + filename = os.path.basename(node.root().file) + is_manifest = filename in settings.MANIFEST_FILES + return is_manifest + + +def apply_augmentations(linter): + """Apply suppression rules.""" + + # W0104 - pointless-statement + # manifest file have a valid pointless-statement dict + suppress_message(linter, BasicChecker.visit_discard, + 'W0104', is_manifest_file) diff --git a/pylint_odoo/checkers/__init__.py b/pylint_odoo/checkers/__init__.py new file mode 100644 index 00000000..f8eab67c --- /dev/null +++ b/pylint_odoo/checkers/__init__.py @@ -0,0 +1,3 @@ +from . import modules_odoo +from . import no_modules +from . import format diff --git a/pylint_odoo/checkers/format.py b/pylint_odoo/checkers/format.py new file mode 100644 index 00000000..87930988 --- /dev/null +++ b/pylint_odoo/checkers/format.py @@ -0,0 +1,87 @@ + +import os +import tokenize + +from pylint.checkers import BaseTokenChecker +from pylint.interfaces import ITokenChecker + +from .. import settings + +ODOO_MSGS = { + # C->convention R->refactor W->warning E->error F->fatal + + 'C%d01' % settings.BASE_FORMAT_ID: ( + 'No UTF-8 coding comment found: ' + 'Use `# coding: utf-8` or `# -*- coding: utf-8 -*-`', + 'no-utf8-coding-comment', + settings.DESC_DFLT + ), + 'W%d01' % settings.BASE_FORMAT_ID: ( + 'Incoherent interpreter comment and executable permission. ' + 'Interpreter: [%s] Exec perm: %s', + 'incoherent-interpreter-exec-perm', + settings.DESC_DFLT + ), + 'W%d02' % settings.BASE_FORMAT_ID: ( + 'Use of vim comment', + 'use-vim-comment', + settings.DESC_DFLT + ), +} + +MAGIC_COMMENT_CODING = 1 +MAGIC_COMMENT_ENCODING = 2 +MAGIC_COMMENT_INTERPRETER = 3 +MAGIC_COMMENT_CODING_UTF8 = 4 +NO_IDENTIFIED = -1 + + +class FormatChecker(BaseTokenChecker): + + # Auto call to `process_tokens` method + __implements__ = (ITokenChecker) + + name = settings.CFG_SECTION + msgs = ODOO_MSGS + + def get_magic_comment_type(self, comment, line_num): + if line_num >= 1 and line_num <= 2: + if "#!" == comment[:2]: + return MAGIC_COMMENT_INTERPRETER + elif "# -*- coding: " in comment or "# coding: " in comment: + if "# -*- coding: utf-8 -*-" in comment \ + or "# coding: utf-8" in comment: + return MAGIC_COMMENT_CODING_UTF8 + return MAGIC_COMMENT_CODING + elif "# -*- encoding: " in comment: + return MAGIC_COMMENT_ENCODING + return NO_IDENTIFIED + + def is_vim_comment(self, comment): + return True if comment.strip('# ').lower().startswith('vim:') \ + else False + + def process_tokens(self, tokens): + tokens_identified = {} + for idx, (tok_type, token_content, + start_line_col, end_line_col, + line_content) in enumerate(tokens): + if tokenize.COMMENT == tok_type: + line_num = start_line_col[0] + magic_comment_type = self.get_magic_comment_type( + token_content, line_num) + if magic_comment_type != NO_IDENTIFIED: + tokens_identified[magic_comment_type] = [ + token_content, line_num] + elif self.is_vim_comment(token_content): + self.add_message('use-vim-comment', line=line_num) + if not tokens_identified.get(MAGIC_COMMENT_CODING_UTF8) and \ + not os.path.basename(self.linter.current_file) == '__init__.py': + self.add_message('no-utf8-coding-comment', line=1) + access_x = os.access(self.linter.current_file, os.X_OK) + interpreter_content, line_num = tokens_identified.get( + MAGIC_COMMENT_INTERPRETER, ['', 0]) + if bool(interpreter_content) != access_x: + self.add_message( + 'incoherent-interpreter-exec-perm', + line=line_num, args=(interpreter_content, access_x)) diff --git a/pylint_odoo/checkers/modules_odoo.py b/pylint_odoo/checkers/modules_odoo.py new file mode 100644 index 00000000..1493b6ed --- /dev/null +++ b/pylint_odoo/checkers/modules_odoo.py @@ -0,0 +1,157 @@ + +''' +Visit module to add odoo checks +''' + +import os + +from pylint.checkers import utils + +from .. import misc, settings + +ODOO_MSGS = { + # C->convention R->refactor W->warning E->error F->fatal + + # Visit odoo module with settings.BASE_OMODULE_ID + 'C%d02' % settings.BASE_OMODULE_ID: ( + 'Missing ./README.rst file. Template here: %s', + 'missing-readme', + settings.DESC_DFLT + ), + 'E%d01' % settings.BASE_OMODULE_ID: ( + '%s:%s %s', + 'rst-syntax-error', + settings.DESC_DFLT + ), + 'E%d02' % settings.BASE_OMODULE_ID: ( + '%s error: %s', + 'xml-syntax-error', + settings.DESC_DFLT + ), + 'W%d01' % settings.BASE_OMODULE_ID: ( + 'Dangerous filter without explicit `user_id` in xml_id %s', + 'dangerous-filter-wo-user', + settings.DESC_DFLT + ), + 'W%d02' % settings.BASE_OMODULE_ID: ( + 'Duplicate xml record id %s', + 'duplicate-xml-record-id', + settings.DESC_DFLT + ), + 'W%d03' % settings.BASE_OMODULE_ID: ( + '%s', + 'javascript-lint', + settings.DESC_DFLT + ), +} + + +DFTL_README_TMPL_URL = 'https://github.com/OCA/maintainer-tools' + \ + '/blob/master/template/module/README.rst' + + +class ModuleChecker(misc.WrapperModuleChecker): + name = settings.CFG_SECTION + msgs = ODOO_MSGS + options = ( + ('readme_template_url', { + 'type': 'string', + 'metavar': '', + 'default': DFTL_README_TMPL_URL, + 'help': 'URL of README.rst template file', + }), + ) + + @utils.check_messages(*(ODOO_MSGS.keys())) + def visit_module(self, node): + self.wrapper_visit_module(node) + + def _check_rst_syntax_error(self): + '''Check if rst file there is syntax error + :return: False if exists errors and + add list of errors in self.msg_args + ''' + rst_files = self.filter_files_ext('rst') + self.msg_args = [] + for rst_file in rst_files: + errors = self.check_rst_syntax( + os.path.join(self.module_path, rst_file)) + for error in errors: + self.msg_args.append(( + rst_file, error.line, + error.full_message.strip('\n').replace('\n', '|'))) + if self.msg_args: + return False + return True + + def _check_missing_readme(self): + '''Check if exists ./README.rst file + :return: If exists return True else False + ''' + self.msg_args = (self.config.readme_template_url,) + return os.path.isfile(os.path.join(self.module_path, 'README.rst')) + + def _check_xml_syntax_error(self): + '''Check if xml file there is syntax error + :return: False if exists errors and + add list of errors in self.msg_args + ''' + self.msg_args = [] + for xml_file in self.filter_files_ext('xml', relpath=True): + result = self.parse_xml(os.path.join(self.module_path, xml_file)) + if isinstance(result, basestring): + self.msg_args.append(( + xml_file, result.strip('\n').replace('\n', '|'))) + if self.msg_args: + return False + return True + + def _check_duplicate_xml_record_id(self): + '''Check duplicate xml record id all xml files of a odoo module. + :return: False if exists errors and + add list of errors in self.msg_args + ''' + all_xml_ids = [] + for xml_file in self.filter_files_ext('xml', relpath=False): + all_xml_ids.extend(self.get_xml_record_ids(xml_file, self.module)) + duplicated_xml_ids = self.get_duplicated_items(all_xml_ids) + if duplicated_xml_ids: + self.msg_args = duplicated_xml_ids + return False + return True + + def _check_dangerous_filter_wo_user(self): + '''Check dangeorous filter without a user assigned. + :return: False if exists errors and + add list of errors in self.msg_args + ''' + xml_files = self.filter_files_ext('xml') + for xml_file in xml_files: + ir_filter_records = self.get_xml_records( + os.path.join(self.module_path, xml_file), model='ir.filters') + for ir_filter_record in ir_filter_records: + ir_filter_fields = ir_filter_record.xpath( + "field[@name='name' or @name='user_id']") + # if exists field="name" then is a new record + # then should be field="user_id" too + if ir_filter_fields and len(ir_filter_fields) == 1: + self.msg_args = ( + xml_file + ':' + ir_filter_record.get('id'),) + return False + return True + + def _check_javascript_lint(self): + '''Check javascript lint + :return: False if exists errors and + add list of errors in self.msg_args + ''' + self.msg_args = [] + for js_file in self.filter_files_ext('js', relpath=True): + errors = self.check_js_lint( + os.path.join(self.module_path, js_file)) + for error in errors: + self.msg_args.append(( + js_file + error)) + if self.msg_args: + return False + return True diff --git a/pylint_odoo/checkers/no_modules.py b/pylint_odoo/checkers/no_modules.py new file mode 100644 index 00000000..99dbb010 --- /dev/null +++ b/pylint_odoo/checkers/no_modules.py @@ -0,0 +1,323 @@ + +''' +Enable checkers to visit all nodes different to modules. +You can use: + visit_arguments + visit_assattr + visit_assert + visit_assign + visit_assname + visit_backquote + visit_binop + visit_boolop + visit_break + visit_callfunc + visit_class + visit_compare + visit_continue + visit_default + visit_delattr + visit_delname + visit_dict + visit_dictcomp + visit_discard + visit_excepthandler + visit_exec + visit_extslice + visit_for + visit_from + visit_function + visit_genexpr + visit_getattr + visit_global + visit_if + visit_ifexp + visit_import + visit_index + visit_lambda + visit_listcomp + visit_name + visit_pass + visit_print + visit_project + visit_raise + visit_return + visit_setcomp + visit_slice + visit_subscript + visit_tryexcept + visit_tryfinally + visit_unaryop + visit_while + visit_yield +for more info visit pylint doc +''' + +import ast +import os +import re + +import astroid +from pylint.checkers import BaseChecker, utils +from pylint.interfaces import IAstroidChecker + +from .. import settings + +ODOO_MSGS = { + # C->convention R->refactor W->warning E->error F->fatal + + 'R%d01' % settings.BASE_NOMODULE_ID: ( + 'Import `Warning` should be renamed as UserError ' + '`from openerp.exceptions import Warning as UserError`', + 'openerp-exception-warning', + settings.DESC_DFLT + ), + 'W%d01' % settings.BASE_NOMODULE_ID: ( + 'Detected api.one and api.multi decorators together.', + 'api-one-multi-together', + settings.DESC_DFLT + ), + 'W%d02' % settings.BASE_NOMODULE_ID: ( + 'Missing api.one or api.multi in copy function.', + 'copy-wo-api-one', + settings.DESC_DFLT + ), + 'W%d03' % settings.BASE_NOMODULE_ID: ( + 'Translation method _("string") in fields is not necessary.', + 'translation-field', + settings.DESC_DFLT + ), + 'W%d04' % settings.BASE_NOMODULE_ID: ( + 'api.one deprecated', + 'api-one-deprecated', + settings.DESC_DFLT + ), + 'W%d05' % settings.BASE_NOMODULE_ID: ( + 'attribute "%s" deprecated', + 'attribute-deprecated', + settings.DESC_DFLT + ), + 'W%d06' % settings.BASE_NOMODULE_ID: ( + 'Missing `super` call in "%s" method.', + 'method-required-super', + settings.DESC_DFLT + ), + 'C%d01' % settings.BASE_NOMODULE_ID: ( + 'Missing author required "%s" in manifest file', + 'manifest-required-author', + settings.DESC_DFLT + ), + 'C%d02' % settings.BASE_NOMODULE_ID: ( + 'Missing required key "%s" in manifest file', + 'manifest-required-key', + settings.DESC_DFLT + ), + 'C%d03' % settings.BASE_NOMODULE_ID: ( + 'Deprecated key "%s" in manifest file', + 'manifest-deprecated-key', + settings.DESC_DFLT + ), + 'C%d04' % settings.BASE_NOMODULE_ID: ( + 'Use `CamelCase` "%s" in class name "%s". ' + 'You can use oca-autopep8 of https://github.com/OCA/maintainer-tools' + ' to auto fix it.', + 'class-camelcase', + settings.DESC_DFLT + ), + 'C%d05' % settings.BASE_NOMODULE_ID: ( + 'License "%s" not allowed in manifest file.', + 'license-allowed', + settings.DESC_DFLT + ), +} + +DFTL_MANIFEST_REQUIRED_KEYS = ['license'] +DFTL_MANIFEST_REQUIRED_AUTHOR = 'Odoo Community Association (OCA)' +DFTL_MANIFEST_DEPRECATED_KEYS = ['description'] +DFTL_LICENSE_ALLOWED = [ + 'AGPL-3', 'GPL-2', 'GPL-2 or any later version', + 'GPL-3', 'GPL-3 or any later version', 'LGPL-3', + 'Other OSI approved licence', 'Other proprietary', +] +DFTL_ATTRIBUTE_DEPRECATED = [ + '_fields', '_defaults', +] +DFTL_METHOD_REQUIRED_SUPER = [ + 'create', 'write', 'read', 'unlink', 'copy', + 'setUp', 'tearDown', 'default_get', +] + + +class NoModuleChecker(BaseChecker): + + __implements__ = IAstroidChecker + + name = settings.CFG_SECTION + msgs = ODOO_MSGS + options = ( + ('manifest_required_author', { + 'type': 'string', + 'metavar': '', + 'default': DFTL_MANIFEST_REQUIRED_AUTHOR, + 'help': 'Name of author required in manifest file.' + }), + ('manifest_required_keys', { + 'type': 'csv', + 'metavar': '', + 'default': DFTL_MANIFEST_REQUIRED_KEYS, + 'help': 'List of keys required in manifest file, ' + + 'separated by a comma.' + }), + ('manifest_deprecated_keys', { + 'type': 'csv', + 'metavar': '', + 'default': DFTL_MANIFEST_DEPRECATED_KEYS, + 'help': 'List of keys deprecated in manifest file, ' + + 'separated by a comma.' + }), + ('license_allowed', { + 'type': 'csv', + 'metavar': '', + 'default': DFTL_LICENSE_ALLOWED, + 'help': 'List of license allowed in manifest file, ' + + 'separated by a comma.' + }), + ('attribute_deprecated', { + 'type': 'csv', + 'metavar': '', + 'default': DFTL_ATTRIBUTE_DEPRECATED, + 'help': 'List of attributes deprecated, ' + + 'separated by a comma.' + }), + ('method_required_super', { + 'type': 'csv', + 'metavar': '', + 'default': DFTL_METHOD_REQUIRED_SUPER, + 'help': 'List of methods where call to `super` is required.' + + 'separated by a comma.' + }), + ) + + @utils.check_messages('translation-field',) + def visit_callfunc(self, node): + if node.as_string().lower().startswith('fields.'): + for argument in node.args: + argument_aux = argument + if isinstance(argument, astroid.Keyword): + argument_aux = argument.value + if isinstance(argument_aux, astroid.CallFunc) and \ + isinstance(argument_aux.func, astroid.Name) and \ + argument_aux.func.name == '_': + self.add_message('translation-field', + node=argument_aux) + + @utils.check_messages('manifest-required-author', 'manifest-required-key', + 'manifest-deprecated-key') + def visit_dict(self, node): + if os.path.basename(self.linter.current_file) in \ + settings.MANIFEST_FILES \ + and isinstance(node.parent, astroid.Discard): + manifest_dict = ast.literal_eval(node.as_string()) + + # Check author required + authors = map( + lambda author: author.strip(), + manifest_dict.get('author', '').split(',')) + required_author = self.config.manifest_required_author + if required_author not in authors: + self.add_message('manifest-required-author', + node=node, args=(required_author,)) + + # Check keys required + required_keys = self.config.manifest_required_keys + for required_key in required_keys: + if required_key not in manifest_dict: + self.add_message('manifest-required-key', + node=node, args=(required_key,)) + + # Check keys deprecated + deprecated_keys = self.config.manifest_deprecated_keys + for deprecated_key in deprecated_keys: + if deprecated_key in manifest_dict: + self.add_message('manifest-deprecated-key', + node=node, args=(deprecated_key,)) + + # Check license allowed + license = manifest_dict.get('license', None) + if license and license not in self.config.license_allowed: + self.add_message('license-allowed', + node=node, args=(license,)) + + @utils.check_messages('api-one-multi-together', + 'copy-wo-api-one', 'api-one-deprecated', + 'method-required-super') + def visit_function(self, node): + '''Check that `api.one` and `api.multi` decorators not exists together + Check that method `copy` exists `api.one` decorator + Check deprecated `api.one`. + ''' + if not node.is_method(): + return + + decor_names = self.get_decorators_names(node.decorators) + decor_lastnames = [ + decor.split('.')[-1] + for decor in decor_names] + if self.linter.is_message_enabled('api-one-multi-together'): + if 'one' in decor_lastnames \ + and 'multi' in decor_lastnames: + self.add_message('api-one-multi-together', + node=node) + + if self.linter.is_message_enabled('copy-wo-api-one'): + if 'copy' == node.name and ('one' not in decor_lastnames and + 'multi' not in decor_lastnames): + self.add_message('copy-wo-api-one', node=node) + + if self.linter.is_message_enabled('api-one-deprecated'): + if 'one' in decor_lastnames: + self.add_message('api-one-deprecated', + node=node) + + if node.name in self.config.method_required_super: + calls = [ + call_func.func.name + for call_func in node.nodes_of_class((astroid.CallFunc,)) + if isinstance(call_func.func, astroid.Name)] + if 'super' not in calls: + self.add_message('method-required-super', + node=node, args=(node.name)) + + @utils.check_messages('openerp-exception-warning') + def visit_from(self, node): + if node.modname == 'openerp.exceptions': + for (import_name, import_as_name) in node.names: + if import_name == 'Warning' \ + and import_as_name != 'UserError': + self.add_message( + 'openerp-exception-warning', node=node) + + @utils.check_messages('class-camelcase') + def visit_class(self, node): + camelized = self.camelize(node.name) + if camelized != node.name: + self.add_message('class-camelcase', node=node, + args=(camelized, node.name)) + + @utils.check_messages('attribute-deprecated') + def visit_assign(self, node): + node_left = node.targets[0] + if isinstance(node_left, astroid.node_classes.AssName): + if node_left.name in self.config.attribute_deprecated: + self.add_message('attribute-deprecated', + node=node_left, args=(node_left.name,)) + + def camelize(self, string): + return re.sub(r"(?:^|_)(.)", lambda m: m.group(1).upper(), string) + + def get_decorators_names(self, decorators): + nodes = [] + if decorators: + nodes = decorators.nodes + return [getattr(decorator, 'attrname', '') + for decorator in nodes if decorator is not None] diff --git a/pylint_odoo/misc.py b/pylint_odoo/misc.py new file mode 100644 index 00000000..2bfec798 --- /dev/null +++ b/pylint_odoo/misc.py @@ -0,0 +1,209 @@ + +import os +import subprocess + +from lxml import etree +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker + +from restructuredtext_lint import lint_file as rst_lint + +from . import settings + + +def get_plugin_msgs(pylint_run_res): + '''Get all message of this pylint plugin. + :param pylint_run_res: Object returned by pylint.run method. + :return: List of strings with message name. + ''' + all_plugin_msgs = [ + key + for key in pylint_run_res.linter.msgs_store._messages + if pylint_run_res.linter.msgs_store._messages[key].checker.name == + settings.CFG_SECTION + ] + return all_plugin_msgs + + +def get_sum_fails(pylint_stats): + '''Get a sum of all fails. + :param pylint_stats: Object returned by pylint.run method. + :return: Integer with sum of all errors found. + ''' + return sum([ + pylint_stats['by_msg'][msg] + for msg in pylint_stats['by_msg']]) + + +# TODO: Change all methods here + +class WrapperModuleChecker(BaseChecker): + + __implements__ = IAstroidChecker + + node = None + module_path = None + msg_args = None + msg_code = None + msg_name_key = None + + def get_manifest_file(self, node_file): + '''Get manifest file path + :param node_file: String with full path of a python module file. + :return: Full path of manifest file if exists else return None''' + if os.path.basename(node_file) == '__init__.py': + for manifest_basename in settings.MANIFEST_FILES: + manifest_file = os.path.join( + os.path.dirname(node_file), manifest_basename) + if os.path.isfile(manifest_file): + return manifest_file + + def wrapper_visit_module(self, node): + '''Call methods named with name-key from self.msgs + Method should be named with next standard: + def _check_{NAME_KEY}(self, module_path) + by example: def _check_missing_icon(self, module_path) + to check missing-icon message name key + And should return True if all fine else False. + if a False is returned then add message of name-key. + Assign object variables to use in methods. + :param node: A astroid.scoped_nodes.Module + :return: None + ''' + self.manifest_file = self.get_manifest_file(node.file) + self.node = node + self.module_path = os.path.dirname(node.file) + self.module = os.path.basename(self.module_path) + for msg_code, (title, name_key, description) in \ + sorted(self.msgs.iteritems()): + self.msg_code = msg_code + self.msg_name_key = name_key + self.msg_args = None + if not self.linter.is_message_enabled(msg_code): + continue + check_method = getattr( + self, '_check_' + name_key.replace('-', '_'), + None) + is_odoo_check = self.manifest_file and \ + msg_code[1:3] == str(settings.BASE_OMODULE_ID) + is_py_check = msg_code[1:3] == str(settings.BASE_PYMODULE_ID) + if callable(check_method) and (is_odoo_check or is_py_check): + if not check_method(): + if not isinstance(self.msg_args, list): + self.msg_args = [self.msg_args] + for msg_args in self.msg_args: + self.add_message(msg_code, node=node, + args=msg_args) + + def filter_files_ext(self, fext, relpath=True): + '''Filter files of odoo modules with a file extension. + :param fext: Extension name of files to filter. + :param relpath: Boolean to choose absolute path or relative path + If relpath is True then return relative paths + else return absolute paths + :return: List of paths of files matched + with extension fext. + ''' + if not fext.startswith('.'): + fext = '.' + fext + fext = fext.lower() + fnames_filtered = [] + + for root, dirnames, filenames in os.walk( + self.module_path, followlinks=True): + for filename in filenames: + if os.path.splitext(filename)[1].lower() == fext: + fname_filtered = os.path.join(root, filename) + if relpath: + fname_filtered = os.path.relpath( + fname_filtered, self.module_path) + fnames_filtered.append(fname_filtered) + return fnames_filtered + + def check_rst_syntax(self, fname): + '''Check syntax in rst files. + :param fname: String with file name path to check + :return: Return list of errors. + ''' + return rst_lint(fname) + + def check_js_lint(self, fname): + '''Check javascript lint in fname. + :param fname: String with full path of file to check + :return: Return list of errors. + ''' + cmd = ['jshint', '--reporter=unix', fname] + try: + output = subprocess.Popen( + cmd, stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).stdout.read() + except OSError as oserr: + output_err = ' - ' + cmd[0] + ': ' + oserr.strerror + return [output_err] + output = output.replace(fname, '') + output_spplited = [] + if output: + output_spplited.extend( + output.strip('\n').split('\n')[:-2]) + return output_spplited + + def get_duplicated_items(self, items): + '''Get duplicated items + :param items: Iterable items + :return: List with tiems duplicated + ''' + unique_items = set() + duplicated_items = set() + for item in items: + if item in unique_items: + duplicated_items.add(item) + else: + unique_items.add(item) + return list(duplicated_items) + + def parse_xml(self, xml_file): + '''Get xml parsed. + :param xml_file: Path of file xml + :return: Doc parsed (lxml.etree object) + if there is syntax error return string error message + ''' + try: + doc = etree.parse(open(xml_file)) + except etree.XMLSyntaxError as xmlsyntax_error_exception: + return xmlsyntax_error_exception.message + return doc + + def get_xml_records(self, xml_file, model=None): + '''Get tag `record` of a openerp xml file. + :param xml_file: Path of file xml + :param model: String with record model to filter. + if model is None then get all. + Default None. + :return: List of lxml `record` nodes + If there is syntax error return [] + ''' + if model is None: + model_filter = '' + else: + model_filter = "[@model='{model}']".format(model=model) + doc = self.parse_xml(xml_file) + return doc.xpath("/openerp//record" + model_filter) \ + if not isinstance(doc, basestring) else [] + + def get_xml_record_ids(self, xml_file, module=None): + '''Get xml ids from tags `record of a openerp xml file + :param xml_file: Path of file xml + :param model: String with record model to filter. + if model is None then get all. + Default None. + :return: List of string with module.xml_id found + ''' + xml_ids = [] + for record in self.get_xml_records(xml_file): + xml_module, xml_id = record.get('id').split('.') \ + if '.' in record.get('id') \ + else [self.module, record.get('id')] + if module and xml_module != module: + continue + xml_ids.append(xml_module + '.' + xml_id) + return xml_ids diff --git a/pylint_odoo/settings.py b/pylint_odoo/settings.py new file mode 100644 index 00000000..7854f91d --- /dev/null +++ b/pylint_odoo/settings.py @@ -0,0 +1,21 @@ + +# pylint plugin global msg number for odoo modules msgs +BASE_OMODULE_ID = 79 + +# pylint plugin global msg number for python modules msgs +BASE_PYMODULE_ID = 80 + +# pylint plugin global msg number for no modules msgs +BASE_NOMODULE_ID = 81 + +# pylint plugin global msg number for format msgs +BASE_FORMAT_ID = 82 + +# Manifest files of odoo +MANIFEST_FILES = ['__odoo__.py', '__openerp__.py', '__terp__.py'] + +# Message description default +DESC_DFLT = 'You can review guidelines here: ' + \ + 'https://github.com/OCA/maintainer-tools/blob/master/CONTRIBUTING.md' + +CFG_SECTION = 'odoolint' diff --git a/pylint_odoo/test/__init__.py b/pylint_odoo/test/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/pylint_odoo/test/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/pylint_odoo/test/main.py b/pylint_odoo/test/main.py new file mode 100644 index 00000000..455c17d6 --- /dev/null +++ b/pylint_odoo/test/main.py @@ -0,0 +1,78 @@ + +import os + +import unittest + +from pylint.lint import Run + +from pylint_odoo import misc + + +EXPECTED_ERRORS = 45 + + +class MainTest(unittest.TestCase): + def setUp(self): + self.default_options = [ + '--load-plugins=pylint_odoo', '--report=no', + '--msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}', + '--output-format=colorized', + ] + path_modules = os.path.join( + os.path.dirname(os.path.dirname(os.path.realpath(__file__))), + 'test_repo') + self.paths_modules = [] + root, dirs, _ = os.walk(path_modules).next() + for path in dirs: + self.paths_modules.append(os.path.join(root, path)) + self.default_extra_params = [ + '--disable=all', + '--enable=odoolint,pointless-statement', + ] + + def run_pylint(self, paths, extra_params=None): + for path in paths: + if not os.path.exists(path): + raise OSError('Path "{path}" not found.'.format(path=path)) + if extra_params is None: + extra_params = self.default_extra_params + return Run(self.default_options + extra_params + paths, exit=False) + + def test_10_path_dont_exist(self): + "self-test if path don't exist" + path_unexist = u'/tmp/____unexist______' + with self.assertRaisesRegexp( + OSError, + r'Path "{path}" not found.$'.format(path=path_unexist)): + self.run_pylint([path_unexist]) + + def test_20_expected_errors(self): + pylint_res = self.run_pylint(self.paths_modules) + # Expected vs found errors + sum_fails_found = misc.get_sum_fails(pylint_res.linter.stats) + self.assertEqual( + sum_fails_found, EXPECTED_ERRORS, + "Errors found {fnd} different to expected {exp}.".format( + fnd=sum_fails_found, exp=EXPECTED_ERRORS)) + # All odoolint name errors vs found + msgs_found = pylint_res.linter.stats['by_msg'].keys() + plugin_msgs = misc.get_plugin_msgs(pylint_res) + test_missed_msgs = sorted(list(set(plugin_msgs) - set(msgs_found))) + self.assertEqual( + test_missed_msgs, [], + "Checks without test case: {test_missed_msgs}".format( + test_missed_msgs=test_missed_msgs)) + + def test_30_disabling_errors(self): + # Disabling + self.default_extra_params.append('--disable=dangerous-filter-wo-user') + pylint_res = self.run_pylint(self.paths_modules) + sum_fails_found = misc.get_sum_fails(pylint_res.linter.stats) + self.assertEqual( + sum_fails_found, EXPECTED_ERRORS - 1, + "Errors found {fnd} different to expected {exp}.".format( + fnd=sum_fails_found, exp=EXPECTED_ERRORS - 1)) + + +if __name__ == '__main__': + unittest.main() diff --git a/pylint_odoo/test_repo/broken_module/__init__.py b/pylint_odoo/test_repo/broken_module/__init__.py new file mode 100644 index 00000000..cde864ba --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/pylint_odoo/test_repo/broken_module/__openerp__.py b/pylint_odoo/test_repo/broken_module/__openerp__.py new file mode 100644 index 00000000..f8807d64 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/__openerp__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Broken module for tests', + # missing license + 'author': 'Many People', # Missing oca author + 'description': 'Should be a README.rst file', + 'version': '1.0', + 'depends': ['base'], + 'data': ['model_view.xml', 'model_view2.xml'], + 'test': ['test.yml'], + 'installable': True, + 'name': 'Duplicated value', + 'active': True, # Deprecated active key +} diff --git a/pylint_odoo/test_repo/broken_module/broken_example.js b/pylint_odoo/test_repo/broken_module/broken_example.js new file mode 100644 index 00000000..a09b3c93 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/broken_example.js @@ -0,0 +1,5 @@ +$(document).ready(function () { + $('.example').each(function () { + var oe_website_sale = this; + }) /*missing semicolon*/ +}) /*missing semicolon*/ diff --git a/pylint_odoo/test_repo/broken_module/coding_latin.py b/pylint_odoo/test_repo/broken_module/coding_latin.py new file mode 100644 index 00000000..c6b50eeb --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/coding_latin.py @@ -0,0 +1 @@ +# -*- coding: latin-1 -*- diff --git a/pylint_odoo/test_repo/broken_module/doc/index.rst b/pylint_odoo/test_repo/broken_module/doc/index.rst new file mode 100644 index 00000000..aceca33f --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/doc/index.rst @@ -0,0 +1,6 @@ + Module broken +====================== + + +`````````` +syntax error diff --git a/pylint_odoo/test_repo/broken_module/encoding_utf8.py b/pylint_odoo/test_repo/broken_module/encoding_utf8.py new file mode 100644 index 00000000..dae354a6 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/encoding_utf8.py @@ -0,0 +1 @@ +# -*- encoding: utf-8 -*- diff --git a/pylint_odoo/test_repo/broken_module/interpreter_wox.py b/pylint_odoo/test_repo/broken_module/interpreter_wox.py new file mode 100644 index 00000000..188711aa --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/interpreter_wox.py @@ -0,0 +1,5 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + + +"Module python with interpreter but without execute permission." diff --git a/pylint_odoo/test_repo/broken_module/interpreter_wx.py b/pylint_odoo/test_repo/broken_module/interpreter_wx.py new file mode 100755 index 00000000..a10b88d8 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/interpreter_wx.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# !/usr/bin/python + +"Module python with interpreter and execute permission." diff --git a/pylint_odoo/test_repo/broken_module/model_view.xml b/pylint_odoo/test_repo/broken_module/model_view.xml new file mode 100644 index 00000000..6e61defa --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/model_view.xml @@ -0,0 +1,44 @@ + + + + + + view.model.form + test.model + +
+ + +
+
+ + + view.model.form + test.model + + + + + + + + + + view.model.form + test.model + + + + + + + + + + By name + test.model + {'group_by': ['name']} + + +
+
diff --git a/pylint_odoo/test_repo/broken_module/model_view2.xml b/pylint_odoo/test_repo/broken_module/model_view2.xml new file mode 100644 index 00000000..118da023 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/model_view2.xml @@ -0,0 +1,26 @@ + + + + + + view.model.form2 + test.model + +
+ + +
+
+ + + view.model.form2 + test.model + + + + + + + +
+
diff --git a/pylint_odoo/test_repo/broken_module/models/__init__.py b/pylint_odoo/test_repo/broken_module/models/__init__.py new file mode 100644 index 00000000..1ed0a308 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/models/__init__.py @@ -0,0 +1,3 @@ +# coding: utf-8 + +from . import broken_model diff --git a/pylint_odoo/test_repo/broken_module/models/broken_model.py b/pylint_odoo/test_repo/broken_module/models/broken_model.py new file mode 100644 index 00000000..6d257cf1 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/models/broken_model.py @@ -0,0 +1,37 @@ +# coding: utf-8 + +from openerp import fields, models, _ + + +def function_no_method(): + pass + + +class TestModel(models.Model): + _name = 'test.model' + + _fields = {} # deprecated fields + _defaults = {} # deprecated defaults + + name = fields.Char( + _(u"Näme"), # Don't need translate + help=u"My hëlp", + required=False) + + # Imported openerp.fields use Char (Upper case) + other_field = fields.char( + name=_("Other field"), + copy=True, + ) + + other_field2 = fields.char( + 'Other Field2', + copy=True, + ) + + def my_method1(self, variable1): + # Shouldn't show error of field-argument-translate + self.my_method2(_('hello world')) + + def my_method2(self, variable2): + return variable2 diff --git a/pylint_odoo/test_repo/broken_module/pylint_oca_broken.py b/pylint_odoo/test_repo/broken_module/pylint_oca_broken.py new file mode 100644 index 00000000..50753436 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/pylint_oca_broken.py @@ -0,0 +1,95 @@ + + +import openerp + +from openerp import api +from openerp.api import one, multi + +from openerp.exceptions import Warning as UserError # pylint: disable=W0622 +from openerp.exceptions import Warning as OtherName # pylint: disable=W0404 +from openerp.exceptions import Warning # pylint: disable=W0404,W0622 +from openerp.exceptions import (AccessError as AE, # pylint: disable=W0404 + ValidationError, + Warning as UserError2) + + +class snake_case(object): + pass + + +class UseUnusedImport(object): + def method1(self): + return UserError, OtherName, Warning, AE, ValidationError, UserError2 + + +class ApiOne(object): + @api.one + def copy(self): + # Missing super() + pass + + def create(self): + # Missing super() + pass + + def write(self): + # Missing super() + pass + + def unlink(self): + # Missing super() + pass + + def read(self): + # Missing super() + pass + + def setUp(self): + # Missing super() + pass + + def tearDown(self): + # Missing super() + pass + + def default_get(self): + # Missing super() + pass + + +class One(object): + @one + def copy(self): + return super(One, self).copy() + + +class OpenerpApiOne(object): + @openerp.api.one + def copy(self): + return super(OpenerpApiOne, self).copy() + + +class WOApiOne(object): + # copy without api.one decorator + def copy(self): + return super(WOApiOne, self).copy() + + +class ApiOneMultiTogether(object): + + @api.multi + @api.one + def copy(self): + return super(ApiOneMultiTogether, self).copy() + + @multi + @one + def copy2(self): + return super(ApiOneMultiTogether, self).copy2() + + @openerp.api.multi + @openerp.api.one + def copy3(self): + return super(ApiOneMultiTogether, self).copy3() + +# vim:comment vim diff --git a/pylint_odoo/test_repo/broken_module/wointerpreter_wx.py b/pylint_odoo/test_repo/broken_module/wointerpreter_wx.py new file mode 100755 index 00000000..050f59d0 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/wointerpreter_wx.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + + +"Module python without interpreter but with execute permission." diff --git a/pylint_odoo/test_repo/broken_module/xml_empty.xml b/pylint_odoo/test_repo/broken_module/xml_empty.xml new file mode 100644 index 00000000..e69de29b diff --git a/pylint_odoo/test_repo/broken_module/xml_special_char.xml b/pylint_odoo/test_repo/broken_module/xml_special_char.xml new file mode 100644 index 00000000..38e17ee3 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/xml_special_char.xml @@ -0,0 +1,8 @@ + + + + + México + + + \ No newline at end of file diff --git a/pylint_odoo/test_repo/broken_module/xml_syntax_error.XML b/pylint_odoo/test_repo/broken_module/xml_syntax_error.XML new file mode 100644 index 00000000..4fd850cc --- /dev/null +++ b/pylint_odoo/test_repo/broken_module/xml_syntax_error.XML @@ -0,0 +1,2 @@ + +<{xml-syntax-error}> \ No newline at end of file diff --git a/pylint_odoo/test_repo/broken_module2/README.rst b/pylint_odoo/test_repo/broken_module2/README.rst new file mode 100644 index 00000000..81f53ca2 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module2/README.rst @@ -0,0 +1,4 @@ +Test module 2 +============= + +This module was written to check the test lint diff --git a/pylint_odoo/test_repo/broken_module2/__init__.py b/pylint_odoo/test_repo/broken_module2/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/pylint_odoo/test_repo/broken_module2/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/pylint_odoo/test_repo/broken_module2/__openerp__.py b/pylint_odoo/test_repo/broken_module2/__openerp__.py new file mode 100644 index 00000000..8c70c668 --- /dev/null +++ b/pylint_odoo/test_repo/broken_module2/__openerp__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Broken module 2 for tests', + 'license': 'unknow license', # unknow license + 'author': 'Other,Odoo Community Association (OCA)', # Missing oca author + 'version': '1.0', + 'depends': ['base'], + 'data': [], + 'test': [], + 'installable': False, +} diff --git a/pylint_odoo/test_repo/test_module/README.rst b/pylint_odoo/test_repo/test_module/README.rst new file mode 100644 index 00000000..bee08d2a --- /dev/null +++ b/pylint_odoo/test_repo/test_module/README.rst @@ -0,0 +1,15 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License: AGPL-3 + +Test module +=========== + +This module was written to check the test of rst syntax. +This is a rst file without syntax error. + +Pygments test + +.. code-block:: python + + if True: + pass diff --git a/pylint_odoo/test_repo/test_module/__init__.py b/pylint_odoo/test_repo/test_module/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pylint_odoo/test_repo/test_module/__openerp__.py b/pylint_odoo/test_repo/test_module/__openerp__.py new file mode 100644 index 00000000..2831c9ca --- /dev/null +++ b/pylint_odoo/test_repo/test_module/__openerp__.py @@ -0,0 +1,21 @@ +# coding: utf-8 +{ + 'name': 'Empty module for tests', + 'license': 'AGPL-3', + 'author': u'Moisés, Odoo Community Association (OCA), author2', + 'version': '1.0', + 'depends': [ + 'base', + ], + 'data': [ + 'security/ir.model.access.csv', + ], + 'external_dependencies': { + 'bin': [ + 'sh', + ], + 'python': [ + 'os', + ], + }, +} diff --git a/pylint_odoo/test_repo/test_module/model_view.xml b/pylint_odoo/test_repo/test_module/model_view.xml new file mode 100644 index 00000000..622e091e --- /dev/null +++ b/pylint_odoo/test_repo/test_module/model_view.xml @@ -0,0 +1,24 @@ + + + + + + view.model.form + test.model + +
+ + +
+
+ + + + By name + test.model + {'group_by': ['name']} + + + +
+
diff --git a/pylint_odoo/test_repo/test_module/test_example.js b/pylint_odoo/test_repo/test_module/test_example.js new file mode 100644 index 00000000..787c16fb --- /dev/null +++ b/pylint_odoo/test_repo/test_module/test_example.js @@ -0,0 +1,5 @@ +$(document).ready(function () { + $('.example').each(function () { + var oe_website_sale = this; + }); +}); diff --git a/pylint_odoo/test_repo/womanifest_module/__init__.py b/pylint_odoo/test_repo/womanifest_module/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/pylint_odoo/test_repo/womanifest_module/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/pylint_odoo/test_repo/womanifest_module/doc/index.rst b/pylint_odoo/test_repo/womanifest_module/doc/index.rst new file mode 100644 index 00000000..caf2e50c --- /dev/null +++ b/pylint_odoo/test_repo/womanifest_module/doc/index.rst @@ -0,0 +1,4 @@ +Module broken +=============== +`````````` +syntax error diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..addc0688 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +pylint==1.4.4 +pylint-plugin-utils==0.2.3 +docutils==0.12 +lxml>=2.3.2 +Pygments==2.0.2 +restructuredtext_lint==0.12.2 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..f28dd8e5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[metadata] +name = pylint-odoo +author = OCA - Odoo Community Association +summary = Pylint plugin for Odoo +license = APGL3 +description-file = README.md +requires-python = >=2.7 +classifier = + Development Status :: 4 - Beta + Environment :: Console + Intended Audience :: Developers + Intended Audience :: Information Technology + License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) + Operating System :: POSIX :: Linux + Programming Language :: Python +[files] +packages = + pylint_odoo diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..bf79d19a --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ + +import setuptools + + +setuptools.setup(setup_requires=['pbr'], + pbr=True, + test_suite="pylint_odoo.test", + package_data={'': ['*.yaml']})