diff --git a/{{cookiecutter.app_name}}/.pylintrc b/{{cookiecutter.app_name}}/.pylintrc new file mode 100755 index 0000000..c8ce3d4 --- /dev/null +++ b/{{cookiecutter.app_name}}/.pylintrc @@ -0,0 +1,263 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook='import os; import sys; print(sys.path)' + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=3 + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). +# You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# W0511 - Warning for fix me and to;do +disable=W0511,locally-disabled,pointless-string-statement + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=colorized + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject,SQLAlchemy,scoped_session + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,id,db,rv,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=__.*__ + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/{{cookiecutter.app_name}}/manage.py b/{{cookiecutter.app_name}}/manage.py old mode 100644 new mode 100755 index 3d59f38..d86f354 --- a/{{cookiecutter.app_name}}/manage.py +++ b/{{cookiecutter.app_name}}/manage.py @@ -14,15 +14,12 @@ from {{cookiecutter.app_name}}.database import db from {{cookiecutter.app_name}}.settings import DevConfig, ProdConfig from {{cookiecutter.app_name}}.user.models import User -if os.environ.get('{{cookiecutter.app_name | upper}}_ENV') == 'prod': - app = create_app(ProdConfig) -else: - app = create_app(DevConfig) - +CONFIG = ProdConfig if os.environ.get('{{cookiecutter.app_name | upper}}_ENV') == 'prod' else DevConfig HERE = os.path.abspath(os.path.dirname(__file__)) TEST_PATH = os.path.join(HERE, 'tests') -manager = Manager(app) +app = create_app(CONFIG) # pylint: disable=invalid-name +manager = Manager(app) # pylint: disable=invalid-name def _make_context(): @@ -39,33 +36,37 @@ def test(): class Lint(Command): - """Lint and check code style.""" + """Lint and check code style with flake8, isort and, optionally, pylint.""" def get_options(self): """Command line options.""" return ( - Option('-f', '--fix', - action='store_true', - dest='fix_imports', - default=False, - help='Fix imports before linting'), + Option('-f', '--fix-imports', action='store_true', dest='fix_imports', default=False, + help='Fix imports using isort, before linting'), + Option('-p', '--pylint', action='store_true', dest='use_pylint', default=False, + help='Use pylint after flake8, for an extended strict check'), ) - def run(self, fix_imports): + def run(self, fix_imports, use_pylint): # pylint: disable=arguments-differ,method-hidden """Run command.""" skip = ['requirements'] root_files = glob('*.py') root_directories = [name for name in next(os.walk('.'))[1] if not name.startswith('.')] - arguments = [arg for arg in root_files + root_directories if arg not in skip] + files_and_directories = [arg for arg in root_files + root_directories if arg not in skip] - if fix_imports: - command_line = ['isort', '-rc'] + arguments - print('Fixing import order: %s' % ' '.join(command_line)) - call(command_line) + def execute_tool(description, *args): + """Execute a checking tool with its arguments.""" + command_line = list(args) + files_and_directories + print('%s: %s' % (description, ' '.join(command_line))) + rv = call(command_line) + if rv is not 0: + exit(rv) - command_line = ['flake8'] + arguments - print('Checking code style: %s' % ' '.join(command_line)) - exit(call(command_line)) + if fix_imports: + execute_tool('Fixing import order', 'isort', '-rc') + execute_tool('Checking code style', 'flake8') + if use_pylint: + execute_tool('Checking code style', 'pylint', '--rcfile=.pylintrc') manager.add_command('server', Server()) diff --git a/{{cookiecutter.app_name}}/requirements/dev.txt b/{{cookiecutter.app_name}}/requirements/dev.txt index 7c944e5..40fbcca 100644 --- a/{{cookiecutter.app_name}}/requirements/dev.txt +++ b/{{cookiecutter.app_name}}/requirements/dev.txt @@ -20,3 +20,4 @@ flake8-isort==0.2 flake8-quotes==0.1.1 isort==4.2.2 pep8-naming==0.3.3 +pylint==1.4.4 diff --git a/{{cookiecutter.app_name}}/tests/conftest.py b/{{cookiecutter.app_name}}/tests/conftest.py index f2472b5..b0b73ba 100644 --- a/{{cookiecutter.app_name}}/tests/conftest.py +++ b/{{cookiecutter.app_name}}/tests/conftest.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=redefined-outer-name """Defines fixtures available to all tests.""" import pytest diff --git a/{{cookiecutter.app_name}}/tests/factories.py b/{{cookiecutter.app_name}}/tests/factories.py index 33efe7a..794d2a2 100644 --- a/{{cookiecutter.app_name}}/tests/factories.py +++ b/{{cookiecutter.app_name}}/tests/factories.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=unnecessary-lambda """Factories to help in tests.""" from factory import PostGenerationMethodCall, Sequence from factory.alchemy import SQLAlchemyModelFactory diff --git a/{{cookiecutter.app_name}}/tests/test_forms.py b/{{cookiecutter.app_name}}/tests/test_forms.py index 590d07f..ff379bb 100644 --- a/{{cookiecutter.app_name}}/tests/test_forms.py +++ b/{{cookiecutter.app_name}}/tests/test_forms.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=no-self-use,no-member,invalid-name,unused-argument """Test forms.""" from {{cookiecutter.app_name}}.public.forms import LoginForm diff --git a/{{cookiecutter.app_name}}/tests/test_functional.py b/{{cookiecutter.app_name}}/tests/test_functional.py index 6b9151f..f683cf7 100644 --- a/{{cookiecutter.app_name}}/tests/test_functional.py +++ b/{{cookiecutter.app_name}}/tests/test_functional.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=no-self-use,no-member,invalid-name,unused-argument """Functional tests using WebTest. See: http://webtest.readthedocs.org/ diff --git a/{{cookiecutter.app_name}}/tests/test_models.py b/{{cookiecutter.app_name}}/tests/test_models.py index d321f5f..0bbb4b1 100644 --- a/{{cookiecutter.app_name}}/tests/test_models.py +++ b/{{cookiecutter.app_name}}/tests/test_models.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=no-self-use,no-member,invalid-name """Model unit tests.""" import datetime as dt @@ -61,7 +62,7 @@ class TestUser: """Add a role to a user.""" role = Role(name='admin') role.save() - u = UserFactory() - u.roles.append(role) - u.save() - assert role in u.roles + user = UserFactory() + user.roles.append(role) + user.save() + assert role in user.roles diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py index 833e2e3..3483a94 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/assets.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=invalid-name """Application assets.""" from flask_assets import Bundle, Environment diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py index f749d7c..718d9f9 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/compat.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: disable=invalid-name,used-before-assignment """Python 2/3 compatibility module.""" - - import sys PY2 = int(sys.version[0]) == 2 diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py index 8265fb8..8de1aec 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/database.py @@ -6,8 +6,8 @@ from .compat import basestring from .extensions import db # Alias common SQLAlchemy names -Column = db.Column -relationship = relationship +Column = db.Column # pylint: disable=invalid-name +relationship = relationship # pylint: disable=invalid-name class CRUDMixin(object): @@ -54,17 +54,17 @@ class SurrogatePK(object): id = db.Column(db.Integer, primary_key=True) @classmethod - def get_by_id(cls, id): + def get_by_id(cls, record_id): """Get record by ID.""" if any( - (isinstance(id, basestring) and id.isdigit(), - isinstance(id, (int, float))), + (isinstance(record_id, basestring) and record_id.isdigit(), + isinstance(record_id, (int, float))), ): - return cls.query.get(int(id)) + return cls.query.get(int(record_id)) # pylint: disable=no-member return None -def ReferenceCol(tablename, nullable=False, pk_name='id', **kwargs): # noqa +def ReferenceCol(tablename, nullable=False, pk_name='id', **kwargs): # noqa # pylint: disable=invalid-name """Column that adds primary key foreign key reference. Usage: :: diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py index 7d86238..e8b6e5c 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/extensions.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- +# pylint: disable=invalid-name """Extensions module. Each extension is initialized in the app factory located in app.py.""" - from flask_bcrypt import Bcrypt from flask_cache import Cache from flask_debugtoolbar import DebugToolbarExtension diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py index ed7f1fe..f76a69d 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/forms.py @@ -24,7 +24,7 @@ class LoginForm(Form): if not initial_validation: return False - self.user = User.query.filter_by(username=self.username.data).first() + self.user = User.query.filter_by(username=self.username.data).first() # pylint: disable=no-member if not self.user: self.username.errors.append('Unknown username') return False diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py index 381e1cd..2c6bfc7 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/public/views.py @@ -9,13 +9,13 @@ from {{cookiecutter.app_name}}.user.forms import RegisterForm from {{cookiecutter.app_name}}.user.models import User from {{cookiecutter.app_name}}.utils import flash_errors -blueprint = Blueprint('public', __name__, static_folder='../static') +blueprint = Blueprint('public', __name__, static_folder='../static') # pylint: disable=invalid-name @login_manager.user_loader -def load_user(id): +def load_user(user_id): """Load user by ID.""" - return User.get_by_id(int(id)) + return User.get_by_id(int(user_id)) @blueprint.route('/', methods=['GET', 'POST']) diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py index 1b0ff97..8d070ef 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/settings.py @@ -2,13 +2,11 @@ """Application configuration.""" import os -os_env = os.environ - class Config(object): """Base configuration.""" - SECRET_KEY = os_env.get('{{cookiecutter.app_name | upper}}_SECRET', 'secret-key') # TODO: Change me + SECRET_KEY = os.environ.get('{{cookiecutter.app_name | upper}}_SECRET', 'secret-key') # TODO: Change me APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) BCRYPT_LOG_ROUNDS = 13 diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py index 1ba4166..6ba9956 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/forms.py @@ -29,11 +29,11 @@ class RegisterForm(Form): initial_validation = super(RegisterForm, self).validate() if not initial_validation: return False - user = User.query.filter_by(username=self.username.data).first() + user = User.query.filter_by(username=self.username.data).first() # pylint: disable=no-member if user: self.username.errors.append('Username already registered') return False - user = User.query.filter_by(email=self.email.data).first() + user = User.query.filter_by(email=self.email.data).first() # pylint: disable=no-member if user: self.email.errors.append('Email already registered') return False diff --git a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py index 2dff555..f0ad971 100644 --- a/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py +++ b/{{cookiecutter.app_name}}/{{cookiecutter.app_name}}/user/views.py @@ -3,7 +3,7 @@ from flask import Blueprint, render_template from flask_login import login_required -blueprint = Blueprint('user', __name__, url_prefix='/users', static_folder='../static') +blueprint = Blueprint('user', __name__, url_prefix='/users', static_folder='../static') # pylint: disable=invalid-name @blueprint.route('/')