list(APPEND tox_envs py3)
endif()
if(DEFINED TOXTEST_TOX_ENVS)
- set(tox_envs ${TOXTEST_TOX_ENVS})
+ list(APPEND tox_envs ${TOXTEST_TOX_ENVS})
endif()
string(REPLACE ";" "," tox_envs "${tox_envs}")
add_custom_command(
# although pip comes with virtualenv, having a recent version
# of pip matters when it comes to using wheel packages
- pip --timeout 300 $install 'setuptools >= 0.8' 'pip >= 7.0' 'wheel >= 0.24' || return 1
+ PIP_OPTS="--timeout 300 --exists-action i"
+ pip $PIP_OPTS $install \
+ 'setuptools >= 0.8' 'pip >= 7.0' 'wheel >= 0.24' 'tox >= 2.9.1' || return 1
if test $# != 0 ; then
- pip --timeout 300 $install $@ || return 1
+ pip $PIP_OPTS $install $@ || return 1
fi
}
find . -name tox.ini | while read ini ; do
(
cd $(dirname $ini)
- require=$(ls *requirements.txt 2>/dev/null | sed -e 's/^/-r /')
+ require_files=$(ls *requirements*.txt 2>/dev/null) || true
+ constraint_files=$(ls *constraints*.txt 2>/dev/null) || true
+ require=$(echo -n "$require_files" | sed -e 's/^/-r /')
+ constraint=$(echo -n "$constraint_files" | sed -e 's/^/-c /')
md5=wheelhouse/md5
if test "$require"; then
- if ! test -f $md5 || ! md5sum -c $md5 ; then
+ if ! test -f $md5 || ! md5sum -c $md5 > /dev/null; then
rm -rf wheelhouse
fi
fi
for interpreter in python2.7 python3 ; do
type $interpreter > /dev/null 2>&1 || continue
activate_virtualenv $top_srcdir $interpreter || exit 1
- populate_wheelhouse "wheel -w $wip_wheelhouse" $require || exit 1
+ populate_wheelhouse "download -d $wip_wheelhouse" $require $constraint || exit 1
done
mv $wip_wheelhouse wheelhouse
- md5sum *requirements.txt > $md5
+ md5sum $require_files $constraint_files > $md5
fi
)
done
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
-ignored-modules=cherrypy,distutils
+ignored-modules=cherrypy,distutils,rados,rbd,cephfs
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
ignore-docstrings=yes
# Ignore imports when computing similarities.
-ignore-imports=no
+ignore-imports=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
if(WITH_TESTS)
include(AddCephTest)
- if(WITH_PYTHON2)
- list(APPEND dashboard_tox_envs py27-cov py27-lint py27-check)
- endif()
- if(WITH_PYTHON3)
- list(APPEND dashboard_tox_envs py3-cov py3-lint py3-check)
- endif()
- add_tox_test(mgr-dashboard
- TOX_ENVS ${dashboard_tox_envs})
+ add_tox_test(mgr-dashboard TOX_ENVS lint check)
endif()
<https://facebook.github.io/jest/>`_.
If you get errors on all tests, it could be because `Jest
-<https://facebook.github.io/jest/>`_ or something else was updated.
+<https://facebook.github.io/jest/>`__ or something else was updated.
There are a few ways how you can try to resolve this:
- Remove all modules with ``rm -rf dist node_modules`` and run ``npm install``
`src/pybind/mgr/dashboard/frontend/src/app/shared/components`.
Helper
-......
+~~~~~~
This component should be used to provide additional information to the user.
``src/pybind/mgr/dashboard/frontend/i18n.config.json``:: and make sure that the
organization is *ceph*, the project is *ceph-dashboard* and the resource is
the one you want to pull from and push to e.g. *Master:master*. To find a list
-of avaiable resources visit ``https://www.transifex.com/ceph/ceph-dashboard/content/``::
+of avaiable resources visit `<https://www.transifex.com/ceph/ceph-dashboard/content/>`_.
-After you checked the config go to the directory ``src/pybind/mgr/dashboard/frontend``:: and run
+After you checked the config go to the directory ``src/pybind/mgr/dashboard/frontend`` and run::
$ npm run i18n
$ npm run i18n:token
-To create a transifex api token visit ``https://www.transifex.com/user/settings/api/``::
+To create a transifex api token visit `<https://www.transifex.com/user/settings/api/>`_.
After the command ran successfully, build the UI and check if everything is
working as expected. You also might want to run the frontend tests.
Strings need to start and end in the same line as the element:
-.. code-block:: xml
+.. code-block:: html
<!-- avoid -->
<span i18n>
Isolated interpolations should not be translated:
-.. code-block:: xml
+.. code-block:: html
<!-- avoid -->
<span i18n>{{ foo }}</span>
Interpolations used in a sentence should be kept in the translation:
-.. code-block:: xml
+.. code-block:: html
<!-- recommended -->
<span i18n>There are {{ x }} OSDs.</span>
Remove elements that are outside the context of the translation:
-.. code-block:: xml
+.. code-block:: html
<!-- avoid -->
<label i18n>
Keep elements that affect the sentence:
-.. code-block:: xml
+.. code-block:: html
<!-- recommended -->
<span i18n>Profile <b>foo</b> will be removed.</span>
$ pip install tox
$ pip install coverage
-To run the tests, run ``run-tox.sh`` in the dashboard directory (where
+To run the tests, run ``run_tox.sh`` in the dashboard directory (where
``tox.ini`` is located)::
## Run Python 2+3 tests+lint commands:
- $ ./run-tox.sh
+ $ ../../../script/run_tox.sh --tox-env py27,py3,lint,check
## Run Python 3 tests+lint commands:
- $ WITH_PYTHON2=OFF ./run-tox.sh
+ $ ../../../script/run_tox.sh --tox-env py3,lint,check
## Run Python 3 arbitrary command (e.g. 1 single test):
- $ WITH_PYTHON2=OFF ./run-tox.sh pytest tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
+ $ WITH_PYTHON2=OFF ../../../script/run_tox.sh --tox-env py3 "" tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
-You can also run tox instead of ``run-tox.sh``::
+You can also run tox instead of ``run_tox.sh``::
## Run Python 3 tests command:
- $ CEPH_BUILD_DIR=.tox tox -e py3-cov
+ $ tox -e py3
## Run Python 3 arbitrary command (e.g. 1 single test):
- $ CEPH_BUILD_DIR=.tox tox -e py3-run pytest tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
+ $ tox -e py3 tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
We also collect coverage information from the backend code when you run tests. You can check the
coverage information provided by the tox output, or by running the following
# URL: /ping/{key}?opt1=...&opt2=...
@Endpoint(path="/", query_params=['opt1'])
def index(self, key, opt1, opt2=None):
- # ...
+ """..."""
# URL: /ping/{key}?opt1=...&opt2=...
@Endpoint(query_params=['opt1'])
def __call__(self, key, opt1, opt2=None):
- # ...
+ """..."""
# URL: /ping/post/{key1}/{key2}
@Endpoint('POST', path_params=['key1', 'key2'])
def post(self, key1, key2, data1, data2=None):
- # ...
+ """..."""
In the above example we see how the ``path`` option can be used to override the
# URL: /ping/{node}/stats/{date}/latency?unit=...
@Endpoint(path="/{date}/latency")
def latency(self, node, date, unit="ms"):
- # ...
+ """ ..."""
In this example we explicitly declare a path parameter ``{node}`` in the
controller URL path, and a path parameter ``{date}`` in the ``latency``
@Proxy()
def proxy(self, path, **params):
- # if requested URL is "/foo/proxy/access/service?opt=1"
- # then path is "access/service" and params is {'opt': '1'}
- # ...
+ """
+ if requested URL is "/foo/proxy/access/service?opt=1"
+ then path is "access/service" and params is {'opt': '1'}
+ """
How does the RESTController work?
.. code-block:: python
@EndpointDoc(parameters={'my_string': (str, 'Description of my_string')})
+ def method(my_string): pass
For body parameters, more complex cases are possible. If the parameter is a
dictionary, the type should be replaced with a ``dict`` containing its nested
'item2': (str, 'Description of item2', True), # item2 is optional
'item3': (str, 'Description of item3', True, 'foo'), # item3 is optional with 'foo' as default value
}, 'Description of my_dictionary')})
+ def method(my_dictionary): pass
If the parameter is a ``list`` of primitive types, the type should be
surrounded with square brackets.
.. code-block:: python
@EndpointDoc(parameters={'my_list': ([int], 'Description of my_list')})
+ def method(my_list): pass
If the parameter is a ``list`` with nested parameters, the nested parameters
should be placed in a dictionary and surrounded with square brackets.
'list_item': (str, 'Description of list_item'),
'list_item2': (str, 'Description of list_item2')
}], 'Description of my_list')})
+ def method(my_list): pass
``responses``: A dict used for describing responses. Rules for describing
.. code-block:: python
@EndpointDoc(responses={
- '400':{'my_response': (str, 'Description of my_response')}
+ '400':{'my_response': (str, 'Description of my_response')}})
+ def method(): pass
Error Handling in Python
- ``CanLog``: provides the plug-in with access to the Ceph Dashboard logger under ``self.log``.
- ``Setupable``: requires overriding ``setup()`` hook. This method is run in the Ceph Dashboard ``serve()`` method, right after CherryPy has been configured, but before it is started. It's a placeholder for the plug-in initialization logic.
- ``HasOptions``: requires overriding ``get_options()`` hook by returning a list of ``Options()``. The options returned here are added to the ``MODULE_OPTIONS``.
-- ``HasCommands``: requires overriding ``register_commands()`` hook by defining the commands the plug-in can handle and decorating them with ``@CLICommand`. The commands can be optionally returned, so that they can be invoked externally (which makes unit testing easier).
+- ``HasCommands``: requires overriding ``register_commands()`` hook by defining the commands the plug-in can handle and decorating them with ``@CLICommand``. The commands can be optionally returned, so that they can be invoked externally (which makes unit testing easier).
- ``HasControllers``: requires overriding ``get_controllers()`` hook by defining and returning the controllers as usual.
- ``FilterRequest.BeforeHandler``: requires overriding ``filter_request_before_handler()`` hook. This method receives a ``cherrypy.request`` object for processing. A usual implementation of this method will allow some requests to pass or will raise a ``cherrypy.HTTPError` based on the ``request`` metadata and other conditions.
mgr = _ModuleProxy()
logger = _LoggerProxy()
- from .module import Module, StandbyModule
+ # DO NOT REMOVE: required for ceph-mgr to load a module
+ from .module import Module, StandbyModule # noqa: F401
else:
import logging
logging.basicConfig(level=logging.DEBUG)
# Mock ceph module otherwise every module that is involved in a testcase and imports it will
# raise an ImportError
import sys
- import mock
+ try:
+ import mock
+ except ImportError:
+ import unittest.mock as mock
+
sys.modules['ceph_module'] = mock.Mock()
mgr = mock.Mock()
--- /dev/null
+import sys
+
+try:
+ from mock import Mock
+except ImportError:
+ from unittest.mock import Mock
+
+
+class MockRadosError(Exception):
+ def __init__(self, message, errno=None):
+ super(MockRadosError, self).__init__(message)
+ self.errno = errno
+
+ def __str__(self):
+ msg = super(MockRadosError, self).__str__()
+ if self.errno is None:
+ return msg
+ return '[errno {0}] {1}'.format(self.errno, msg)
+
+
+def pytest_configure(config):
+ sys.modules.update({
+ 'rados': Mock(Error=MockRadosError, OSError=MockRadosError),
+ 'rbd': Mock(),
+ 'cephfs': Mock(),
+ })
--- /dev/null
+CherryPy==13.1.0
+enum34==1.1.6
+more-itertools==4.1.0
+PyJWT==1.6.4
+pyopenssl==17.5.0
+bcrypt==3.1.4
+python3-saml==1.4.1
+requests==2.20.0
+Routes==2.4.1
+six==1.11.0
import pkgutil
import sys
-if sys.version_info >= (3, 0):
- from urllib.parse import unquote # pylint: disable=no-name-in-module,import-error
-else:
- from urllib import unquote # pylint: disable=no-name-in-module
+import six
+from six.moves.urllib.parse import unquote
# pylint: disable=wrong-import-position
import cherrypy
@wraps(func)
def inner(*args, **kwargs):
for key, value in kwargs.items():
- # pylint: disable=undefined-variable
- if (sys.version_info < (3, 0) and isinstance(value, unicode)) \
- or isinstance(value, str):
+ if isinstance(value, six.text_type):
kwargs[key] = unquote(value)
# Process method arguments.
from ..services.exception import handle_send_command_error
from ..tools import str_to_bool
try:
- from typing import Dict, List, Any, Union # pylint: disable=unused-import
+ from typing import Dict, List, Any, Union # noqa: F401 pylint: disable=unused-import
except ImportError:
pass # For typing only
def _get(cls, pool_name, attrs=None, stats=False):
# type: (str, str, bool) -> dict
pools = cls._pool_list(attrs, stats)
- pool = [pool for pool in pools if pool['pool_name'] == pool_name]
+ pool = [p for p in pools if p['pool_name'] == pool_name]
if not pool:
raise cherrypy.NotFound('No such pool')
return pool[0]
try:
status = json.loads(status['json'])
- except (ValueError, KeyError) as _:
+ except (ValueError, KeyError):
status = {}
instance_id = metadata['instance_id']
# -*- coding: utf-8 -*-
from __future__ import absolute_import
-import sys
import cherrypy
try:
@staticmethod
def _check_python_saml():
if not python_saml_imported:
- python_saml_name = 'python3-saml' if sys.version_info >= (3, 0) else 'python-saml'
- raise cherrypy.HTTPError(400,
- 'Required library not found: `{}`'.format(python_saml_name))
+ raise cherrypy.HTTPError(400, 'Required library not found: `python3-saml`')
try:
OneLogin_Saml2_Settings(mgr.SSO_DB.saml2.onelogin_settings)
except OneLogin_Saml2_Error:
PLUGIN_MANAGER = DashboardPluginManager("ceph-mgr.dashboard")
# Load all interfaces and their hooks
-from . import interfaces # pylint: disable=wrong-import-position,cyclic-import
+from . import interfaces # noqa: F401 pylint: disable=wrong-import-position,cyclic-import
from mgr_module import CLICommand, Option
from . import PLUGIN_MANAGER as PM
-from . import interfaces as I
+from . import interfaces as I # noqa: E741
from .ttl_cache import ttl_cache
from ..controllers.rbd import Rbd, RbdSnapshot, RbdTrash
--- /dev/null
+pylint
+flake8
+flake8-colors
+#TODO: Fix docstring issues: https://tracker.ceph.com/issues/41224
+#flake8-docstrings
+#flake8-import-order
+#flake8-typing-imports; python_version >= '3'
+#pep8-naming
+rstcheck
+++ /dev/null
-astroid==1.6.1
-pylint==1.8.2
-python-saml==2.4.2
+++ /dev/null
-astroid==2.1.0
-pylint==2.2.2
-python3-saml==1.4.1
--- /dev/null
+mock; python_version <= '3.3'
+pytest<4
+pytest-cov
+pytest-instafail
-attrs==17.4.0
-backports.functools-lru-cache==1.4
-bcrypt==3.1.4
-cheroot==6.0.0
-CherryPy==13.1.0
-configparser==3.5.0
-coverage==4.5.2
-enum34==1.1.6
-funcsigs==1.0.2
-isort==4.2.15
-lazy-object-proxy==1.3.1
-mccabe==0.6.1
-mock==2.0.0
-more-itertools==4.1.0
-pbr==3.1.1
-pluggy==0.6.0
-portend==2.2
-py==1.5.2
-pycodestyle==2.4.0
-pycparser==2.18
-PyJWT==1.6.4
-pyopenssl==17.5.0
-pytest==3.3.2
-pytest-cov==2.5.1
-pytest-faulthandler==1.0.1
-pytz==2017.3
-requests==2.20.0
-Routes==2.4.1
-singledispatch==3.4.0.3
-six==1.11.0
-tempora==1.10
-tox==2.9.1
-virtualenv==15.1.0
-werkzeug==0.14.1
-wrapt==1.10.11
+bcrypt
+CherryPy
+enum34; python_version<'3.4'
+more-itertools
+PyJWT
+pyopenssl
+python3-saml
+requests
+Routes
+six
logger.error(
"%s REST API failed %s req status: %s", self.client_name,
method.upper(), resp.status_code)
- from pprint import pprint as pp
from pprint import pformat as pf
raise RequestException(
from __future__ import absolute_import
import json
-import sys
from contextlib import contextmanager
+import six
import cherrypy
from ..exceptions import ViewCacheNoDataException, DashboardException
from ..tools import wraps
-if sys.version_info < (3, 0):
+if six.PY2:
# Monkey-patch a __call__ method into @contextmanager to make
# it compatible to Python 3
GeneratorContextManager.__call__ = call
# pylint: disable=function-redefined
- def contextmanager(func):
+ def contextmanager(func): # noqa: F811
@wraps(func)
def helper(*args, **kwds):
except ImportError:
from urllib.parse import urlparse
-from .iscsi_config import IscsiGatewaysConfig
+from .iscsi_config import IscsiGatewaysConfig # pylint: disable=cyclic-import
from .. import logger
from ..settings import Settings
from ..rest_client import RestClient
import errno
import json
-import sys
import threading
+import six
+if six.PY2:
+ FileNotFoundError = IOError
+
try:
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.errors import OneLogin_Saml2_Error
return -errno.ENOSYS, '', ''
if not python_saml_imported:
- python_saml_name = 'python3-saml' if sys.version_info >= (3, 0) else 'python-saml'
- return -errno.EPERM, '', 'Required library not found: `{}`'.format(python_saml_name)
+ return -errno.EPERM, '', 'Required library not found: `python3-saml`'
if cmd['prefix'] == 'dashboard sso enable saml2':
try:
if not sp_x_509_cert and sp_private_key:
return -errno.EINVAL, '', 'Missing parameter `sp_x_509_cert`.'
has_sp_cert = sp_x_509_cert != "" and sp_private_key != ""
- try:
- # pylint: disable=undefined-variable
- FileNotFoundError
- except NameError:
- # pylint: disable=redefined-builtin
- FileNotFoundError = IOError
try:
f = open(sp_x_509_cert, 'r')
sp_x_509_cert = f.read()
from cherrypy._cptools import HandlerWrapperTool
from cherrypy.test import helper
-from mgr_module import CLICommand, MgrModule
+from mgr_module import CLICommand
from .. import logger, mgr
from ..controllers import json_error_page, generate_controller_routes
else:
self.status = 500
self.body = json.dumps(thread.res_task['exception'])
- return
def _task_post(self, url, data=None, timeout=60):
self._task_request('POST', url, data, timeout)
import re
import json
import cherrypy
-import mock
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
from . import ControllerTestCase, KVStoreMockMixin
from ..controllers import RESTController, Controller
from __future__ import absolute_import
import unittest
-from mock import Mock, patch
+try:
+ from mock import Mock, patch
+except ImportError:
+ from unittest.mock import Mock, patch
from . import KVStoreMockMixin
from ..plugins.feature_toggles import FeatureToggles, Features
import unittest
-from mock import MagicMock, Mock
+try:
+ from mock import MagicMock, Mock
+except ImportError:
+ from unittest.mock import MagicMock, Mock
import orchestrator
from . import KVStoreMockMixin
import copy
import errno
import json
-import mock
+
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
from . import CmdException, ControllerTestCase, CLICommandTestMixin
from .. import mgr
import uuid
from contextlib import contextmanager
-from mock import patch
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
from . import ControllerTestCase
from ..controllers.osd import Osd
# -*- coding: utf-8 -*-
# pylint: disable=protected-access
import time
-import mock
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
from . import ControllerTestCase
from ..controllers.pool import Pool
# -*- coding: utf-8 -*-
# pylint: disable=protected-access
-from mock import patch
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
from . import ControllerTestCase
from .. import mgr
from __future__ import absolute_import
import json
-import mock
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
from . import ControllerTestCase
from .. import mgr
import unittest
import requests.exceptions
-from mock import patch
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
+
from urllib3.exceptions import MaxRetryError, ProtocolError
from .. import mgr
from ..rest_client import RequestException, RestClient
import time
-import mock
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
from . import ControllerTestCase
from ..controllers import Controller, RESTController, Task
-import mock
+try:
+ import mock
+except ImportError:
+ import unittest.mock as mock
from . import ControllerTestCase
from ..controllers.rgw import RgwUser
# -*- coding: utf-8 -*-
import unittest
-from mock import patch
+
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
from .. import mgr
from ..services.rgw_client import RgwClient
import cherrypy
from cherrypy.lib.sessions import RamSession
-from mock import patch
+try:
+ from mock import patch
+except ImportError:
+ from unittest.mock import patch
from . import ControllerTestCase
from ..services.exception import handle_rados_error
from .services.auth import JwtManager
try:
- from typing import Any, AnyStr, Dict, List # pylint: disable=unused-import
+ from typing import Any, AnyStr, Dict, List # noqa pylint: disable=unused-import
except ImportError:
pass # For typing only
[tox]
-envlist = py27-{cov,lint,run,check},py3-{cov,lint,run,check}
+envlist =
+ py{27,3},
+ lint,
+ check,
+ run,
skipsdist = true
-toxworkdir = {env:CEPH_BUILD_DIR}/dashboard
-minversion = 2.8.1
+minversion = 2.9.1
+
+[pytest]
+addopts =
+ --cov --cov-append --cov-report=term
+ --doctest-modules
+ --ignore=frontend/ --ignore=module.py
+ --instafail
[testenv]
-setenv=
+deps =
+ -rrequirements.txt
+ -cconstraints.txt
+ -rrequirements-test.txt
+setenv =
CFLAGS = -DXMLSEC_NO_SIZE_T
+ PYTHONUNBUFFERED=1
+ PYTHONDONTWRITEBYTECODE=1
UNITTEST = true
WEBTEST_INTERACTIVE = false
- LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
- PATH = {toxinidir}/../../../../build/bin:$PATH
- py27: PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.2
- py3: PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3
- cov: UNITTEST = true
- cov: COVERAGE_FILE = .coverage.{envname}
-commands=
- pip install -r {toxinidir}/requirements.txt
- py27: pip install -r {toxinidir}/requirements-py27.txt
- py3: pip install -r {toxinidir}/requirements-py3.txt
- cov: coverage erase
- cov: {envbindir}/py.test --cov=. --cov-report= --junitxml=junit.{envname}.xml --doctest-modules controllers/rbd.py services/ tests/ tools.py
- cov: coverage combine {toxinidir}/{env:COVERAGE_FILE}
- cov: coverage report
- cov: coverage xml
- lint: pylint --rcfile=.pylintrc --jobs=5 . module.py tools.py controllers tests services exceptions.py grafana.py ci/check_grafana_uids.py
- lint: pycodestyle --max-line-length=100 --exclude=.tox,venv,frontend,.vscode --ignore=E402,E121,E123,E126,E226,E24,E704,W503,E741 .
- check: python ci/check_grafana_uids.py frontend/src/app ../../../../monitoring/grafana/dashboards
- run: {posargs}
+commands =
+ pytest {posargs}
+
+[testenv:run]
+whitelist_externals = *
+commands = {posargs}
+
+[flake8]
+max-line-length = 100
+ignore = E123 E126 E226 E402 W503 E741 F812
+exclude = venv, frontend, .*
+statistics = True
+#TODO: Uncomment and refactor (https://tracker.ceph.com/issues/41221)
+#max-complexity = 10
+format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
+
+[pylint]
+# Allow similarity/code duplication detection
+jobs = 1
+dirs = . controllers plugins services tests
+addopts =
+ -rn
+ --rcfile=.pylintrc
+ --jobs={[pylint]jobs}
+# Detect Python 2-3 migration issues
+ --py3k
+
+[rstlint]
+dirs =
+ README.rst
+ HACKING.rst
+
+[testenv:lint]
+basepython = python3
+deps =
+ -rrequirements-lint.txt
+commands =
+ rstcheck --report info --debug {posargs:{[rstlint]dirs}}
+ flake8 {posargs}
+ pylint {[pylint]addopts} {posargs:{[pylint]dirs}}
+
+
+[testenv:check]
+deps =
+ six==1.11.0
+commands =
+ python ci/check_grafana_uids.py frontend/src/app ../../../../monitoring/grafana/dashboards
fi
pip $DISABLE_PIP_VERSION_CHECK --log $DIR/log.txt install $NO_INDEX --find-links=file://$(pwd)/wheelhouse 'tox >=2.9.1'
-if test -f requirements.txt ; then
- if ! test -f wheelhouse/md5 || ! md5sum -c wheelhouse/md5 > /dev/null; then
+
+require_files=$(ls *requirements*.txt 2>/dev/null) || true
+constraint_files=$(ls *constraints*.txt 2>/dev/null) || true
+require=$(echo -n "$require_files" | sed -e 's/^/-r /')
+constraint=$(echo -n "$constraint_files" | sed -e 's/^/-c /')
+md5=wheelhouse/md5
+if test "$require"; then
+ if ! test -f $md5 || ! md5sum -c wheelhouse/md5 > /dev/null; then
NO_INDEX=''
fi
- pip $DISABLE_PIP_VERSION_CHECK --log $DIR/log.txt install $NO_INDEX --find-links=file://$(pwd)/wheelhouse -r requirements.txt
+ pip --exists-action i $DISABLE_PIP_VERSION_CHECK --log $DIR/log.txt install $NO_INDEX \
+ --find-links=file://$(pwd)/wheelhouse $require $constraint
fi