├── hacking ├── __init__.py ├── checks │ ├── __init__.py │ ├── vim_check.py │ ├── dictlist.py │ ├── other.py │ ├── imports.py │ ├── localization.py │ ├── docstrings.py │ ├── mock_checks.py │ ├── comments.py │ └── except_checks.py ├── tests │ ├── checks │ │ ├── __init__.py │ │ ├── test_mock.py │ │ ├── test_eventlet_imports.py │ │ ├── test_other.py │ │ ├── test_comments.py │ │ └── test_except_checks.py │ ├── test_import_exceptions.py │ ├── test_config.py │ ├── __init__.py │ └── test_doctest.py ├── config.py └── core.py ├── releasenotes ├── source │ ├── _static │ │ └── .placeholder │ ├── unreleased.rst │ ├── v1.0.0.rst │ ├── v1.1.0.rst │ ├── v2.0.0.rst │ ├── v3.0.0.rst │ ├── v4.0.0.rst │ ├── index.rst │ └── conf.py └── notes │ ├── drop-python38-39-cca191b19e091b6c.yaml │ ├── drop-py-2-7-c2bcdd28e4513532.yaml │ ├── drop-python36-834bb59e53d5fd71.yaml │ ├── add-eventlet-ban-hacking-check-H905-ef8a441d7ab85926.yaml │ ├── start-of-queens-c3024ebbb49aef6f.yaml │ ├── flake8-3-x-support-cd478de79fe7b63d.yaml │ ├── remove-python23-checks-f1be3588a2a83522.yaml │ ├── rocky-intermediate-release-60db6e8f66539e4b.yaml │ └── migrating-most-common-hacking-checks.yaml ├── requirements.txt ├── doc ├── source │ ├── user │ │ ├── hacking.rst │ │ ├── usage.rst │ │ └── index.rst │ ├── index.rst │ ├── conf.py │ └── contributor │ │ └── contributing.rst └── requirements.txt ├── .stestr.conf ├── .gitreview ├── .mailmap ├── MANIFEST.in ├── .gitignore ├── .pre-commit-hooks.yaml ├── CONTRIBUTING.rst ├── setup.py ├── test-requirements.txt ├── .zuul.yaml ├── integration-test └── test.sh ├── tox.ini ├── setup.cfg ├── README.rst ├── LICENSE └── HACKING.rst /hacking/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hacking/checks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hacking/tests/checks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /releasenotes/source/_static/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8~=7.3.0 # MIT 2 | -------------------------------------------------------------------------------- /doc/source/user/hacking.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../../HACKING.rst 2 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./hacking/tests 3 | top_dir=./ 4 | 5 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/hacking.git 5 | -------------------------------------------------------------------------------- /doc/source/user/usage.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Using hacking 3 | ============= 4 | 5 | .. include:: ../../../README.rst 6 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=2.0.0,!=2.1.0 # BSD 2 | openstackdocstheme>=2.2.1 # Apache-2.0 3 | reno>=3.1.0 # Apache-2.0 4 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # Format is: 2 | # Name 3 | Joe Gordon 4 | -------------------------------------------------------------------------------- /releasenotes/source/unreleased.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Current Series Release Notes 3 | ============================== 4 | 5 | .. release-notes:: 6 | -------------------------------------------------------------------------------- /releasenotes/source/v1.0.0.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | v1.0.0 Release Notes 3 | ===================== 4 | 5 | .. release-notes:: 1.0.0 Release Notes 6 | :version: 1.0.0 7 | -------------------------------------------------------------------------------- /releasenotes/source/v1.1.0.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | v1.1.0 Release Notes 3 | ===================== 4 | 5 | .. release-notes:: 1.1.0 Release Notes 6 | :version: 1.1.0 7 | -------------------------------------------------------------------------------- /releasenotes/source/v2.0.0.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | v2.0.0 Release Notes 3 | ===================== 4 | 5 | .. release-notes:: 2.0.0 Release Notes 6 | :version: 2.0.0 7 | -------------------------------------------------------------------------------- /releasenotes/source/v3.0.0.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | v3.0.0 Release Notes 3 | ===================== 4 | 5 | .. release-notes:: 3.0.0 Release Notes 6 | :version: 3.0.0 7 | -------------------------------------------------------------------------------- /releasenotes/source/v4.0.0.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | v4.0.0 Release Notes 3 | ===================== 4 | 5 | .. release-notes:: 4.0.0 Release Notes 6 | :version: 4.0.0 7 | -------------------------------------------------------------------------------- /doc/source/user/index.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | User Documentation 3 | ================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | hacking 9 | usage 10 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-python38-39-cca191b19e091b6c.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 3.8 and 3.9 support have been removed. The minimum python version 5 | supported is 3.10. 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include ChangeLog 3 | include README.rst 4 | include requirements.txt 5 | include test-requirements.txt 6 | exclude .gitignore 7 | exclude .gitreview 8 | 9 | global-exclude *.pyc 10 | -------------------------------------------------------------------------------- /releasenotes/source/index.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Hacking Release Notes 3 | ======================= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | unreleased 9 | v4.0.0 10 | v3.0.0 11 | v2.0.0 12 | v1.1.0 13 | v1.0.0 14 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-py-2-7-c2bcdd28e4513532.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Python 2.7 support has been dropped. Last release of hacking 5 | to support py2.7 is OpenStack Train. The minimum version of Python now 6 | supported by hacking is Python 3.5. 7 | -------------------------------------------------------------------------------- /releasenotes/notes/drop-python36-834bb59e53d5fd71.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Support for Python 3.5 through 3.7 has been dropped. The minimum 5 | supported Python version is now 3.8. 6 | - | 7 | ``hacking`` is now compatible with ``flake8~=4.0.1``. flake8 3.x 8 | is no longer supported. 9 | -------------------------------------------------------------------------------- /releasenotes/notes/add-eventlet-ban-hacking-check-H905-ef8a441d7ab85926.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Added new hacking check ``H905`` to detect and ban ``eventlet`` usage in 5 | OpenStack projects. This check supports the ongoing ``eventlet`` removal 6 | effort across OpenStack as part of the async migration to ``threading`` 7 | and ``asyncio``. 8 | -------------------------------------------------------------------------------- /releasenotes/notes/start-of-queens-c3024ebbb49aef6f.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | This release includes new checks, which are disabled by default: 5 | 6 | * [H210] Require 'autospec', 'spec', or 'spec_set' in mock.patch/mock.patch.object calls 7 | 8 | * [H204] Use assert(Not)Equal to check for equality. 9 | 10 | * [H205] Use assert(Greater|Less)(Equal) for comparison. 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.py[co] 3 | *.a 4 | *.o 5 | *.so 6 | 7 | # Sphinx 8 | _build 9 | 10 | # Packages/installer info 11 | *.egg 12 | *.egg-info 13 | dist 14 | build 15 | eggs 16 | parts 17 | bin 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | 23 | # Other 24 | .stestr/ 25 | .tox 26 | .*.swp 27 | .coverage 28 | cover 29 | AUTHORS 30 | ChangeLog 31 | 32 | # Editors 33 | *~ 34 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: hacking 2 | name: hacking 3 | description: 'Runs hacking, the OpenStack blend of flake8' 4 | entry: flake8 5 | language: python 6 | types: [python] 7 | require_serial: true 8 | additional_dependencies: 9 | # This enables the pep257 extra from hacking by default. If you want 10 | # to avoid it, just override additional_dependencies in your hook config 11 | # by listing only `- .`. We assume that most users want this feature. 12 | - .[pep257] 13 | -------------------------------------------------------------------------------- /releasenotes/notes/flake8-3-x-support-cd478de79fe7b63d.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | *hacking* is now compatible with flake8 3.x, which is a large rewrite of 5 | flake8. flake8 2.x is no longer supported. 6 | upgrade: 7 | - | 8 | Support for local checks has been removed. This was not compatible with 9 | *flake8* 3.x. Users should migrate to the flake8's `native local plugins 10 | support`__. 11 | 12 | __ https://flake8.pycqa.org/en/3.7.0/user/configuration.html#using-local-plugins 13 | -------------------------------------------------------------------------------- /releasenotes/notes/remove-python23-checks-f1be3588a2a83522.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | The following checks have been removed: 5 | 6 | - H231 (incompatible ``except x,y:`` construct) 7 | - H232 (incompatible octal format) 8 | - H233 (incompatible use of ``print`` operator) 9 | - H234 (``assertEquals``/``assertNotEquals`` is deprecated) 10 | - H235 (``assert_`` is deprecated) 11 | - H236 (incompatible ``__metaclass__``) 12 | - H237 (removed module) 13 | - H238 (old style classes) 14 | 15 | These all checked for Python 3-incompatible code. They were disabled on 16 | Python 3 codebases and have therefore been no-ops for some time now. 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | The source repository for this project can be found at: 2 | 3 | https://opendev.org/openstack/hacking 4 | 5 | Pull requests submitted through GitHub are not monitored. 6 | 7 | To start contributing to OpenStack, follow the steps in the contribution guide 8 | to set up and use Gerrit: 9 | 10 | https://docs.openstack.org/contributors/code-and-documentation/quick-start.html 11 | 12 | Bugs should be filed on Launchpad: 13 | 14 | https://bugs.launchpad.net/hacking 15 | 16 | For more specific information about contributing to this repository, see the 17 | Hacking contributor guide: 18 | 19 | https://docs.openstack.org/hacking/latest/contributor/contributing.html 20 | -------------------------------------------------------------------------------- /releasenotes/notes/rocky-intermediate-release-60db6e8f66539e4b.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | prelude: | 3 | This release includes below changes: 4 | 5 | - Transition to flake8 2.6.x: 6 | 7 | * flake8 2.6.x performed the conversion to pycodestyle (which is 8 | the new name of pep8). Remove the explicit dependencies of 9 | hacking as flake8 is going to pull in mccabe, pyflakes and 10 | pycodestyle in the versions that are needed. 11 | 12 | - Allow 'wraps' to be an alternative to autospec: 13 | 14 | * Don't cause an H210 error if the mock.patch/mock.patch.object call uses 15 | the 'wraps' keyword. As that serves the same purpose in catching wrong 16 | attributes. 17 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | ================================================ 2 | hacking: OpenStack Hacking Guideline Enforcement 3 | ================================================ 4 | 5 | hacking is a set of flake8 plugins that test and enforce the :ref:`StyleGuide`. 6 | 7 | Hacking pins its dependencies, as a new release of some dependency can break 8 | hacking based gating jobs. This is because new versions of dependencies can 9 | introduce new rules, or make existing rules stricter. 10 | 11 | * Contributing: If you are a new contributor to Hacking please refer: :doc:`contributor/contributing` 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 3 16 | 17 | user/index 18 | 19 | .. toctree:: 20 | :hidden: 21 | 22 | contributor/contributing 23 | -------------------------------------------------------------------------------- /releasenotes/notes/migrating-most-common-hacking-checks.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | This release added new checks related to unittest module: 5 | 6 | * [H211] Change assertTrue(isinstance(A, B)) by optimal assert like 7 | assertIsInstance(A, B). 8 | 9 | * [H212] Change assertEqual(type(A), B) by optimal assert like 10 | assertIsInstance(A, B) 11 | 12 | * [H213] Check for usage of deprecated assertRaisesRegexp 13 | 14 | * [H214] Change assertTrue/False(A in/not in B, message) to the more 15 | specific assertIn/NotIn(A, B, message) 16 | 17 | * [H215] Change assertEqual(A in B, True), assertEqual(True, A in B), 18 | assertEqual(A in B, False) or assertEqual(False, A in B) to the more 19 | specific assertIn/NotIn(A, B) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import setuptools 17 | 18 | setuptools.setup( 19 | setup_requires=['pbr>=2.0.0'], 20 | pbr=True) 21 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | coverage!=4.4,>=4.0 # Apache-2.0 5 | fixtures>=3.0.0 # Apache-2.0/BSD 6 | python-subunit>=1.0.0 # Apache-2.0/BSD 7 | stestr>=2.0.0 # Apache-2.0 8 | testscenarios>=0.4 # Apache-2.0/BSD 9 | testtools>=2.2.0 # MIT 10 | ddt>=1.2.1 # MIT 11 | 12 | # hacking doesn't use this anywhere, but nova imports this in nova/__init__.py 13 | # since eventlet is such a common universal import, add it to the hacking test 14 | # virtualenv, so importing things like 'nova.hacking.checks.factory' will just 15 | # work. 16 | # See https://bugs.launchpad.net/hacking/+bug/1403270 17 | eventlet!=0.18.3,!=0.20.1,>=0.18.2 # MIT 18 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | - project: 2 | templates: 3 | - publish-openstack-docs-pti 4 | - openstack-cover-jobs 5 | - openstack-python3-jobs 6 | - release-notes-jobs-python3 7 | check: 8 | jobs: 9 | - hacking-integration-nova: 10 | voting: false 11 | - openstack-tox-py312: 12 | voting: true 13 | gate: 14 | jobs: 15 | - openstack-tox-py312: 16 | voting: true 17 | 18 | - job: 19 | name: hacking-integration-nova 20 | parent: openstack-tox 21 | description: | 22 | Run the integration job against nova with proposed hacking change. 23 | required-projects: 24 | - openstack/nova 25 | vars: 26 | tox_envlist: integration 27 | tox_extra_args: -- openstack nova {{ ansible_user_dir }}/{{ zuul.projects['opendev.org/openstack/nova'].src_dir }} 28 | -------------------------------------------------------------------------------- /integration-test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: test.sh openstack keystone path-to-repo 4 | # path-to-repo is an optional parameter, if it exists 5 | # no cloning will happen and the local directory will be used, 6 | # the first two parameter get ignored. 7 | # Note: you can clone from a local file with REPO_ROOT=file:////~/path/to/repo 8 | 9 | set -x 10 | set -e 11 | 12 | REPO_ROOT=${REPO_ROOT:-https://git.openstack.org} 13 | HACKING="$(pwd)" 14 | 15 | if [[ $# -lt 2 ]] ; then 16 | echo "Script needs at least two arguments:" 17 | echo "$0 organization name [path-to-repo]" 18 | exit 1 19 | fi 20 | org=$1 21 | project=$2 22 | 23 | if [[ $# -eq 3 ]] ; then 24 | projectdir=$3 25 | clone=0 26 | else 27 | projectdir=$project 28 | clone=1 29 | fi 30 | 31 | if [ "$clone" = "1" ] ; then 32 | 33 | tempdir="$(mktemp -d)" 34 | 35 | trap "rm -rf $tempdir" EXIT 36 | pushd $tempdir 37 | git clone $REPO_ROOT/$org/$project --depth=1 38 | fi 39 | 40 | pushd $projectdir 41 | set +e 42 | 43 | # Install project with test-requirements so that hacking's 44 | # local-check-factory works 45 | pip install -r test-requirements.txt 46 | pip install . 47 | # Reinstall hacking, the above might have uninstalled it 48 | pip install $HACKING 49 | 50 | flake8 --select E,H --statistics 51 | RET=$? 52 | popd 53 | 54 | if [ "$clone" = "1" ] ; then 55 | popd 56 | fi 57 | 58 | exit $RET 59 | -------------------------------------------------------------------------------- /hacking/tests/checks/test_mock.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import ddt 15 | 16 | from hacking.checks import mock_checks 17 | from hacking import tests 18 | 19 | 20 | @ddt.ddt 21 | class MockingTestCase(tests.TestCase): 22 | """This tests hacking checks related to the use of mocking.""" 23 | 24 | @ddt.unpack 25 | @ddt.data( 26 | (1, 'import mock', None), 27 | (0, 'from unittest import mock', None), 28 | (1, 'from mock import patch', None), 29 | (0, 'from unittest.mock import patch', None), 30 | (0, 'from mock', '# noqa')) 31 | def test_H216_hacking_no_third_party_mock(self, err_count, line, noqa): 32 | if err_count > 0: 33 | self.assertCheckFails(mock_checks.hacking_no_third_party_mock, 34 | line, noqa) 35 | else: 36 | self.assertCheckPasses(mock_checks.hacking_no_third_party_mock, 37 | line, noqa) 38 | -------------------------------------------------------------------------------- /hacking/checks/vim_check.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import re 14 | 15 | from hacking import core 16 | 17 | 18 | vim_header_re = re.compile(r"^#\s+vim?:.+") 19 | 20 | 21 | @core.flake8ext 22 | @core.off_by_default 23 | def no_vim_headers(physical_line, line_number, lines): 24 | r"""Check for vim editor configuration in source files. 25 | 26 | By default vim modelines can only appear in the first or 27 | last 5 lines of a source file. 28 | 29 | Examples: 30 | H106: # vim: set tabstop=4 shiftwidth=4\n#\n#\n#\n#\n# 31 | H106: # Lic\n# vim: set tabstop=4 shiftwidth=4\n#\n#\n#\n#\n# 32 | H106: # Lic\n#\n#\n#\n#\n#\n#\n#\n#\n# vim: set tabstop=4 shiftwidth=4 33 | Okay: # Lic\n#\n#\n#\n#\n#\n#\n# 34 | Okay: # viminal hill is located in Rome 35 | Okay: # vim, ze nemluvis cesky 36 | """ 37 | if ((line_number <= 5 or line_number > len(lines) - 5) and 38 | vim_header_re.match(physical_line)): 39 | return 0, "H106: Don't put vim configuration in source files" 40 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.18.0 3 | envlist = py3,pep8 4 | ignore_basepython_conflict = True 5 | 6 | [testenv] 7 | basepython = python3 8 | usedevelop = True 9 | deps = 10 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 11 | -r{toxinidir}/requirements.txt 12 | -r{toxinidir}/test-requirements.txt 13 | commands = 14 | stestr run --slowest {posargs} 15 | 16 | [testenv:integration] 17 | allowlist_externals = bash 18 | commands = 19 | bash integration-test/test.sh {posargs} 20 | 21 | [testenv:cover] 22 | setenv = 23 | PYTHON=coverage run --source hacking --parallel-mode 24 | commands = 25 | stestr run {posargs} 26 | coverage combine 27 | coverage html -d cover 28 | coverage xml -o cover/coverage.xml 29 | 30 | [testenv:docs] 31 | deps = 32 | -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} 33 | -r{toxinidir}/doc/requirements.txt 34 | commands = 35 | sphinx-build -W -b html doc/source doc/build/html 36 | 37 | [testenv:pdf-docs] 38 | allowlist_externals = make 39 | deps = {[testenv:docs]deps} 40 | commands = 41 | sphinx-build -W -b latex doc/source doc/build/pdf 42 | make -C doc/build/pdf 43 | 44 | [testenv:releasenotes] 45 | deps = 46 | {[testenv:docs]deps} 47 | commands = 48 | sphinx-build -W -b html releasenotes/source releasenotes/build/html 49 | 50 | [testenv:venv] 51 | commands = {posargs} 52 | 53 | [testenv:pep8] 54 | commands = flake8 {posargs} 55 | 56 | [flake8] 57 | exclude = .venv,.tox,dist,doc,*.egg,build 58 | show-source = true 59 | enable-extensions = H106 60 | 61 | [hacking] 62 | local-check = hacking.tests.test_local.check 63 | -------------------------------------------------------------------------------- /hacking/checks/dictlist.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import tokenize 14 | 15 | from hacking import core 16 | 17 | 18 | LOCALS_TEXT_MAP = { 19 | 'locals': 'locals()', 20 | 'self': 'self.__dict__' 21 | } 22 | 23 | 24 | @core.flake8ext 25 | def hacking_no_locals(logical_line, tokens, noqa): 26 | """Do not use locals() or self.__dict__ for string formatting. 27 | 28 | Okay: 'locals()' 29 | Okay: 'locals' 30 | Okay: locals() 31 | Okay: print(locals()) 32 | H501: print("%(something)" % locals()) 33 | H501: LOG.info(_("%(something)") % self.__dict__) 34 | Okay: print("%(something)" % locals()) # noqa 35 | """ 36 | if noqa: 37 | return 38 | for_formatting = False 39 | for token_type, text, start, _, _ in tokens: 40 | if text == "%" and token_type == tokenize.OP: 41 | for_formatting = True 42 | if for_formatting and token_type == tokenize.NAME: 43 | for k, v in LOCALS_TEXT_MAP.items(): 44 | if text == k and v in logical_line: 45 | yield (start[1], 46 | "H501: Do not use %s for string formatting" % v) 47 | -------------------------------------------------------------------------------- /hacking/config.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import configparser 15 | 16 | 17 | class Config(object): 18 | def __init__(self, default_section=None, tox_file='tox.ini'): 19 | conf = configparser.RawConfigParser() 20 | conf.read(tox_file) 21 | 22 | self.conf = conf 23 | self.default_section = default_section 24 | 25 | def get(self, option, section=None, default=None): 26 | section = section or self.default_section 27 | 28 | if not self.conf.has_section(section): 29 | return default 30 | 31 | if self.conf.has_option(section, option): 32 | return self.conf.get(section, option).strip() 33 | 34 | return default 35 | 36 | def get_multiple(self, option, section=None, default=None): 37 | section = section or self.default_section 38 | 39 | values = self.get(option, section) 40 | if not values: 41 | return default 42 | 43 | values = [v.strip() for v in values.split('\n') if v.strip()] 44 | result = [] 45 | for vals in values: 46 | result.extend([v.strip() for v in vals.split(',') if v.strip()]) 47 | 48 | return result 49 | -------------------------------------------------------------------------------- /hacking/tests/test_import_exceptions.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import os 15 | 16 | import fixtures 17 | 18 | from hacking import config 19 | from hacking import core 20 | from hacking import tests 21 | 22 | 23 | TEST_TOX_INI = """[hacking] 24 | import_exceptions = 25 | a.b.c 26 | z.x 27 | """ 28 | 29 | 30 | class ImportExceptionsTest(tests.TestCase): 31 | def _setUpConfig(self, content): 32 | tox_ini_path = os.path.join(self.useFixture(fixtures.TempDir()).path, 33 | 'tox.ini') 34 | 35 | with open(tox_ini_path, 'w') as tox_ini: 36 | tox_ini.write(content) 37 | 38 | return config.Config('hacking', tox_ini_path) 39 | 40 | def setUp(self): 41 | super(ImportExceptionsTest, self).setUp() 42 | 43 | def test_default_import_exceptions(self): 44 | conf = self._setUpConfig("") 45 | self.assertEqual(core.DEFAULT_IMPORT_EXCEPTIONS, 46 | conf.get_multiple( 47 | 'import_exceptions', 48 | default=core.DEFAULT_IMPORT_EXCEPTIONS)) 49 | 50 | def test_import_exceptions(self): 51 | conf = self._setUpConfig(TEST_TOX_INI) 52 | self.assertEqual(['a.b.c', 'z.x'], 53 | conf.get_multiple('import_exceptions')) 54 | -------------------------------------------------------------------------------- /hacking/tests/checks/test_eventlet_imports.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import ddt 15 | 16 | from hacking.checks import imports 17 | from hacking import tests 18 | 19 | 20 | @ddt.ddt 21 | class ImportsTestCase(tests.TestCase): 22 | """This tests hacking checks from the 'imports' group.""" 23 | 24 | @ddt.unpack 25 | @ddt.data( 26 | (True, 'import eventlet', None), 27 | (True, 'from eventlet import monkey_patch', None), 28 | (True, 'from eventlet.green import socket', None), 29 | (True, ' import eventlet # with indentation', None), 30 | (True, ' from eventlet import something', None), 31 | (False, 'import eventlet # noqa', '# noqa'), 32 | (False, 'import asyncio', None), 33 | (False, 'import threading', None), 34 | (False, 'from threading import Thread', None), 35 | (False, 'import concurrent.futures', None), 36 | (False, 'import some_other_module', None), 37 | (False, 'import not_eventlet', None), 38 | (False, 'from not_eventlet import something', None), 39 | ) 40 | def test_H905_hacking_no_eventlet(self, should_fail, line, noqa): 41 | if should_fail: 42 | self.assertCheckFails(imports.hacking_no_eventlet, line, noqa) 43 | else: 44 | self.assertCheckPasses(imports.hacking_no_eventlet, line, noqa) 45 | -------------------------------------------------------------------------------- /hacking/tests/test_config.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import os 15 | 16 | import fixtures 17 | 18 | from hacking import config 19 | from hacking import tests 20 | 21 | 22 | TEST_TOX_INI = """[hacking] 23 | option_1 = val_1 24 | option_2 = 25 | val_2 26 | option_3 = 27 | val_1,val_2, val_3, 28 | val_4 , val_5 , val_6, 29 | val_7 30 | , 31 | val_8 32 | val_9 33 | """ 34 | 35 | 36 | class ConfigTest(tests.TestCase): 37 | def setUp(self): 38 | tox_ini_path = os.path.join(self.useFixture(fixtures.TempDir()).path, 39 | 'tox.ini') 40 | 41 | with open(tox_ini_path, 'w') as tox_ini: 42 | tox_ini.write(TEST_TOX_INI) 43 | self.conf = config.Config('hacking', tox_ini_path) 44 | super(ConfigTest, self).setUp() 45 | 46 | def test_get(self): 47 | self.assertEqual('val_1', self.conf.get('option_1')) 48 | self.assertEqual('val_2', self.conf.get('option_2')) 49 | self.assertEqual('val_3', self.conf.get('option_4', default='val_3')) 50 | 51 | def test_get_multiple(self): 52 | self.assertEqual(['val_1', 'val_2', 'val_3', 'val_4', 'val_5', 'val_6', 53 | 'val_7', 'val_8', 'val_9'], 54 | self.conf.get_multiple('option_3')) 55 | 56 | self.assertEqual(['val_1', 'val_2'], 57 | self.conf.get_multiple('option_4', 58 | default=['val_1', 'val_2'])) 59 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.abspath('../..')) 7 | # -- General configuration ---------------------------------------------------- 8 | 9 | # Add any Sphinx extension module names here, as strings. They can be 10 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 11 | extensions = ['sphinx.ext.autodoc', 'openstackdocstheme'] 12 | 13 | # openstackdocstheme options 14 | openstackdocs_repo_name = 'openstack/hacking' 15 | openstackdocs_bug_project = 'hacking' 16 | openstackdocs_bug_tag = '' 17 | 18 | # autodoc generation is a bit aggressive and a nuisance when doing heavy 19 | # text edit cycles. 20 | # execute "export SPHINX_DEBUG=1" in your terminal to disable 21 | 22 | # Add any paths that contain templates here, relative to this directory. 23 | templates_path = ['_templates'] 24 | 25 | # The suffix of source filenames. 26 | source_suffix = '.rst' 27 | 28 | # The master toctree document. 29 | master_doc = 'index' 30 | 31 | # General information about the project. 32 | project = 'hacking' 33 | copyright = '2013, OpenStack Foundation' 34 | 35 | # If true, '()' will be appended to :func: etc. cross-reference text. 36 | add_function_parentheses = True 37 | 38 | # If true, the current module name will be prepended to all description 39 | # unit titles (such as .. function::). 40 | add_module_names = True 41 | 42 | # The name of the Pygments (syntax highlighting) style to use. 43 | pygments_style = 'native' 44 | 45 | # -- Options for HTML output -------------------------------------------------- 46 | 47 | html_theme = 'openstackdocs' 48 | 49 | # Output file base name for HTML help builder. 50 | htmlhelp_basename = '%sdoc' % project 51 | 52 | # Grouping the document tree into LaTeX files. List of tuples 53 | # (source start file, target name, title, author, documentclass 54 | # [howto/manual]). 55 | latex_documents = [ 56 | ('index', 57 | 'doc-%s.tex' % project, 58 | '%s Documentation' % project, 59 | 'OpenStack Foundation', 'manual'), 60 | ] 61 | -------------------------------------------------------------------------------- /doc/source/contributor/contributing.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | So You Want to Contribute... 3 | ============================ 4 | 5 | For general information on contributing to OpenStack, please check out the 6 | `contributor guide `_ to get started. 7 | It covers all the basics that are common to all OpenStack projects: the accounts 8 | you need, the basics of interacting with our Gerrit review system, how we 9 | communicate as a community, etc. 10 | 11 | Below will cover the more project specific information you need to get started 12 | with Hacking. 13 | 14 | Communication 15 | ~~~~~~~~~~~~~ 16 | * IRC channel ``#openstack-qa`` at OFTC 17 | * Mailing list (prefix subjects with ``[qa]`` for faster responses) 18 | http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss 19 | 20 | Contacting the Core Team 21 | ~~~~~~~~~~~~~~~~~~~~~~~~ 22 | Please refer to the `Hacking Core Team 23 | `_ contacts. 24 | 25 | New Feature Planning 26 | ~~~~~~~~~~~~~~~~~~~~ 27 | If you want to propose a new feature please read `Feature Proposal Process`_ 28 | Hacking features are tracked on `Launchpad BP `_. 29 | 30 | Task Tracking 31 | ~~~~~~~~~~~~~ 32 | We track our tasks in `Launchpad `_. 33 | 34 | Reporting a Bug 35 | ~~~~~~~~~~~~~~~ 36 | You found an issue and want to make sure we are aware of it? You can do so on 37 | `Launchpad `__. 38 | More info about Launchpad usage can be found on `OpenStack docs page 39 | `_ 40 | 41 | Getting Your Patch Merged 42 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | All changes proposed to the Hacking requires two ``Code-Review +2`` votes from 44 | Hacking core reviewers before one of the core reviewers can approve patch by 45 | giving ``Workflow +1`` vote. 46 | 47 | Project Team Lead Duties 48 | ~~~~~~~~~~~~~~~~~~~~~~~~ 49 | All common PTL duties are enumerated in the `PTL guide 50 | `_. 51 | 52 | The Release Process for QA is documented in `QA Release Process 53 | `_. 54 | 55 | .. _Feature Proposal Process: https://wiki.openstack.org/wiki/QA#Feature_Proposal_.26_Design_discussions 56 | -------------------------------------------------------------------------------- /hacking/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | 18 | import fixtures 19 | import testtools 20 | 21 | 22 | _TRUE_VALUES = ('True', 'true', '1', 'yes') 23 | 24 | 25 | class TestCase(testtools.TestCase): 26 | """Test case base class for all unit tests.""" 27 | 28 | def setUp(self): 29 | """Run before each test method to initialize test environment.""" 30 | super(TestCase, self).setUp() 31 | test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) 32 | try: 33 | test_timeout = int(test_timeout) 34 | except ValueError: 35 | # If timeout value is invalid do not set a timeout. 36 | test_timeout = 0 37 | if test_timeout > 0: 38 | self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) 39 | 40 | if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: 41 | stdout = self.useFixture(fixtures.StringStream('stdout')).stream 42 | self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) 43 | if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: 44 | stderr = self.useFixture(fixtures.StringStream('stderr')).stream 45 | self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) 46 | 47 | def assertCheckFails(self, check_func, *args, **kwargs): 48 | if not list(check_func(*args, **kwargs)): 49 | raise AssertionError("Check %s did not fail." % 50 | check_func.__name__) 51 | 52 | def assertCheckPasses(self, check_func, *args, **kwargs): 53 | try: 54 | self.assertCheckFails(check_func, *args, **kwargs) 55 | except AssertionError: 56 | return 57 | else: 58 | raise AssertionError("Check %s failed." % check_func.__name__) 59 | -------------------------------------------------------------------------------- /hacking/tests/checks/test_other.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import ddt 15 | 16 | from hacking.checks import other 17 | from hacking import tests 18 | 19 | 20 | @ddt.ddt 21 | class OthersTestCase(tests.TestCase): 22 | """This tests hacking checks from the 'other' group.""" 23 | 24 | @ddt.unpack 25 | @ddt.data( 26 | (False, 'LOG.debug("Test %s" % foo)', None), 27 | (True, 'LOG.info("Test %s", foo)', None), 28 | (False, 'LOG.info("Test {}".format(foo))', None), 29 | (True, 'LOG.error("Test %s" % foo)', '# noqa'), 30 | (False, 'LOG.debug("Test %s" % "foo")', None), 31 | (True, 'LOG.debug("Test %s", "foo")', None), 32 | (True, 'LOG.warning("Test %s", ",".join("%s:%s" % (a, b)))', None), 33 | (True, "LOG.warning('Testing some stuff')", None)) 34 | def test_H904_hacking_delayed_string_interpolation( 35 | self, passes, line, noqa): 36 | if passes: 37 | self.assertCheckPasses(other.hacking_delayed_string_interpolation, 38 | line, noqa) 39 | else: 40 | self.assertCheckFails(other.hacking_delayed_string_interpolation, 41 | line, noqa) 42 | 43 | @ddt.unpack 44 | @ddt.data( 45 | (False, 'import os\r\nimport sys'), 46 | (True, 'import os\nimport sys')) 47 | def test_H903_hacking_no_cr(self, passes, line): 48 | if passes: 49 | self.assertCheckPasses(other.hacking_no_cr, line) 50 | else: 51 | self.assertCheckFails(other.hacking_no_cr, line) 52 | 53 | @ddt.unpack 54 | @ddt.data( 55 | (False, 'LOG.warn("LOG.warn is deprecated")'), 56 | (True, 'LOG.warning("LOG.warn is deprecated")')) 57 | def test_H905_no_log_warn(self, passes, line): 58 | if passes: 59 | self.assertCheckPasses(other.hacking_no_log_warn, line) 60 | else: 61 | self.assertCheckFails(other.hacking_no_log_warn, line) 62 | -------------------------------------------------------------------------------- /hacking/checks/other.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import re 14 | 15 | from hacking import core 16 | 17 | 18 | log_string = re.compile(r".*LOG\.(?:error|warn|warning|info" 19 | r"|critical|exception|debug)") 20 | 21 | 22 | @core.flake8ext 23 | def hacking_no_cr(physical_line): 24 | r"""Check that we only use newlines not carriage returns. 25 | 26 | Okay: import os\nimport sys 27 | # pep8 doesn't yet replace \r in strings, will work on an 28 | # upstream fix 29 | H903 import os\r\nimport sys 30 | """ 31 | if '\r' in physical_line: 32 | yield (0, "H903: Windows style line endings not allowed in code") 33 | 34 | 35 | @core.flake8ext 36 | @core.off_by_default 37 | def hacking_delayed_string_interpolation(logical_line, noqa): 38 | r"""String interpolation should be delayed at logging calls. 39 | 40 | H904: LOG.debug('Example: %s' % 'bad') 41 | Okay: LOG.debug('Example: %s', 'good') 42 | """ 43 | msg = ("H904: String interpolation should be delayed to be " 44 | "handled by the logging code, rather than being done " 45 | "at the point of the logging call. " 46 | "Use ',' instead of '%'.") 47 | 48 | if noqa: 49 | return 50 | 51 | if log_string.match(logical_line): 52 | # Line is a log statement, strip out strings and see if % is used, 53 | # just to make sure we don't match on a format specifier in a string. 54 | line = re.sub(r"[\"'].+?[\"']", '', logical_line) 55 | # There are some cases where string formatting of the arguments are 56 | # needed, so don't include those when checking. 57 | line = re.sub(r",.*", '', line) 58 | if '%' in line or '.format(' in line: 59 | yield 0, msg 60 | 61 | 62 | @core.flake8ext 63 | def hacking_no_log_warn(logical_line): 64 | """Disallow 'LOG.warn(' 65 | 66 | Use LOG.warning() instead of Deprecated LOG.warn(). 67 | https://docs.python.org/3/library/logging.html#logging.warning 68 | """ 69 | 70 | if "LOG.warn(" in logical_line: 71 | yield (0, "H905: LOG.warn is deprecated, please use LOG.warning!") 72 | -------------------------------------------------------------------------------- /hacking/tests/test_doctest.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import re 18 | import subprocess 19 | import sys 20 | import tempfile 21 | 22 | import importlib.metadata 23 | import testscenarios 24 | from testtools import content 25 | 26 | import hacking.tests 27 | 28 | SELFTEST_REGEX = re.compile(r'\b(Okay|[HEW]\d{3}):\s(.*)') 29 | # Each scenario is (name, {lines=.., raw=..., code=...}) 30 | file_cases = [] 31 | 32 | 33 | class HackingTestCase(hacking.tests.TestCase): 34 | 35 | scenarios = file_cases 36 | 37 | def test_flake8(self): 38 | 39 | with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: 40 | f.write(''.join(self.lines)) 41 | 42 | cmd = [sys.executable, '-mflake8', '--isolated', 43 | '--select=%s' % self.code, 44 | '--enable-extensions=%s' % self.code, '--ignore=F', 45 | '--format=%(code)s\t%(path)s\t%(row)d', f.name] 46 | out, _ = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate() 47 | 48 | out = out.decode('utf-8') 49 | 50 | if self.code == 'Okay': 51 | self.assertEqual('', out) 52 | else: 53 | self.addDetail('reason', 54 | content.text_content("Failed to trigger rule %s" % 55 | self.code)) 56 | 57 | self.assertNotEqual('', out) 58 | self.assertEqual(self.code, out.split('\t')[0].rstrip(':'), out) 59 | 60 | os.remove(f.name) 61 | 62 | 63 | def _get_lines(check): 64 | for line in check.__doc__.splitlines(): 65 | line = line.lstrip() 66 | match = SELFTEST_REGEX.match(line) 67 | if match is None: 68 | continue 69 | yield line, match.groups() 70 | 71 | 72 | def load_tests(loader, tests, pattern): 73 | 74 | for entry in importlib.metadata.entry_points().select( 75 | group='flake8.extension' 76 | ): 77 | if not entry.module.startswith('hacking.'): 78 | continue 79 | 80 | check = entry.load() 81 | if check.skip_on_py3: 82 | continue 83 | 84 | for lineno, (raw, (code, source)) in enumerate(_get_lines(check)): 85 | lines = [part.replace(r'\t', '\t') + '\n' 86 | for part in source.split(r'\n')] 87 | file_cases.append(( 88 | '%s-%s-line-%s' % (entry.name, entry.attr, lineno), 89 | {'lines': lines, 'raw': raw, 'code': code}, 90 | )) 91 | 92 | return testscenarios.load_tests_apply_scenarios(loader, tests, pattern) 93 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = hacking 3 | author = OpenStack 4 | author_email = openstack-discuss@lists.openstack.org 5 | summary = OpenStack Hacking Guideline Enforcement 6 | description_file = 7 | README.rst 8 | home_page = https://docs.openstack.org/hacking/latest/ 9 | python_requires = >=3.10 10 | project_urls = 11 | Bug Tracker = https://bugs.launchpad.net/hacking 12 | Reviews = https://review.opendev.org/q/p:openstack/hacking+status:open 13 | CI = https://zuul.opendev.org/t/openstack/builds?project=openstack%%2Fhacking 14 | Source Code = https://opendev.org/openstack/hacking 15 | classifier = 16 | Development Status :: 4 - Beta 17 | Environment :: Console 18 | Environment :: OpenStack 19 | Intended Audience :: Developers 20 | Intended Audience :: Information Technology 21 | License :: OSI Approved :: Apache Software License 22 | Operating System :: OS Independent 23 | Programming Language :: Python 24 | Programming Language :: Python :: 3 25 | Programming Language :: Python :: 3.10 26 | Programming Language :: Python :: 3.11 27 | Programming Language :: Python :: 3.12 28 | Programming Language :: Python :: 3.13 29 | 30 | [files] 31 | packages = 32 | hacking 33 | 34 | [entry_points] 35 | flake8.extension = 36 | H101 = hacking.checks.comments:hacking_todo_format 37 | H102 = hacking.checks.comments:hacking_has_license 38 | H103 = hacking.checks.comments:hacking_has_correct_license 39 | H104 = hacking.checks.comments:hacking_has_only_comments 40 | H105 = hacking.checks.comments:hacking_no_author_tags 41 | H106 = hacking.checks.vim_check:no_vim_headers 42 | H201 = hacking.checks.except_checks:hacking_except_format 43 | H202 = hacking.checks.except_checks:hacking_except_format_assert 44 | H203 = hacking.checks.except_checks:hacking_assert_is_none 45 | H204 = hacking.checks.except_checks:hacking_assert_equal 46 | H205 = hacking.checks.except_checks:hacking_assert_greater_less 47 | H210 = hacking.checks.mock_checks:MockAutospecCheck 48 | H211 = hacking.checks.except_checks:hacking_assert_true_instance 49 | H212 = hacking.checks.except_checks:hacking_assert_equal_type 50 | H213 = hacking.checks.except_checks:hacking_assert_raises_regexp 51 | H214 = hacking.checks.except_checks:hacking_assert_true_or_false_with_in 52 | H215 = hacking.checks.except_checks:hacking_assert_equal_in 53 | H216 = hacking.checks.mock_checks:hacking_no_third_party_mock 54 | H301 = hacking.checks.imports:hacking_import_rules 55 | H306 = hacking.checks.imports:hacking_import_alphabetical 56 | H401 = hacking.checks.docstrings:hacking_docstring_start_space 57 | H403 = hacking.checks.docstrings:hacking_docstring_multiline_end 58 | H404 = hacking.checks.docstrings:hacking_docstring_multiline_start 59 | H405 = hacking.checks.docstrings:hacking_docstring_summary 60 | H501 = hacking.checks.dictlist:hacking_no_locals 61 | H700 = hacking.checks.localization:hacking_localization_strings 62 | H903 = hacking.checks.other:hacking_no_cr 63 | H904 = hacking.checks.other:hacking_delayed_string_interpolation 64 | H905 = hacking.checks.imports:hacking_no_eventlet 65 | 66 | [extras] 67 | pep257 = 68 | flake8-docstrings==1.7.0 # MIT 69 | -------------------------------------------------------------------------------- /hacking/core.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Cloudscaling 2 | # All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """OpenStack HACKING file compliance testing 17 | 18 | Built as a sets of pycodestyle checks using flake8. 19 | """ 20 | 21 | import gettext 22 | import warnings 23 | 24 | from hacking import config 25 | 26 | # Import tests need to inject _ properly into the builtins 27 | gettext.install('hacking') 28 | 29 | 30 | def flake8ext(f): 31 | f.name = __name__ 32 | f.version = '0.0.1' 33 | f.skip_on_py3 = False 34 | if not hasattr(f, 'off_by_default'): 35 | f.off_by_default = False 36 | return f 37 | 38 | 39 | def off_by_default(f): 40 | """Decorator to turn check off by default. 41 | 42 | To enable the check use the flake8 select setting in 43 | tox.ini. 44 | 45 | flake8 documentation: 46 | http://flake8.readthedocs.org/en/latest/extensions.html. 47 | """ 48 | f.off_by_default = True 49 | return f 50 | 51 | 52 | def skip_on_py3(f): 53 | warnings.warn( 54 | "The skip_on_py3 decorator is deprecated for removal: any check that " 55 | "does not run on py3 should be removed", 56 | UserWarning, 57 | ) 58 | f.skip_on_py3 = True 59 | return f 60 | 61 | # Error code block layout 62 | 63 | # H1xx comments 64 | # H20x except 65 | # H3xx imports 66 | # H4xx docstrings 67 | # H5xx dictionaries/lists 68 | # H6xx calling methods 69 | # H7xx localization 70 | # H8xx git commit messages 71 | # H9xx other 72 | 73 | 74 | CONF = config.Config('hacking') 75 | 76 | 77 | DEFAULT_IMPORT_EXCEPTIONS = [ 78 | 'sqlalchemy', 79 | 'migrate', 80 | ] 81 | 82 | IMPORT_EXCEPTIONS = CONF.get_multiple('import_exceptions', default=[]) 83 | IMPORT_EXCEPTIONS += DEFAULT_IMPORT_EXCEPTIONS 84 | 85 | 86 | def is_import_exception(mod): 87 | """Check module name to see if import has been whitelisted. 88 | 89 | Import based rules should not run on any whitelisted module 90 | """ 91 | return (mod in IMPORT_EXCEPTIONS or 92 | any(mod.startswith(m + '.') for m in IMPORT_EXCEPTIONS)) 93 | 94 | 95 | def import_normalize(line): 96 | # convert "from x import y" to "import x.y" 97 | # handle "from x import y as z" to "import x.y as z" 98 | split_line = line.split() 99 | if ("import" in line and line.startswith("from ") and "," not in line and 100 | split_line[2] == "import" and split_line[3] != "*" and 101 | split_line[1] != "__future__" and 102 | (len(split_line) == 4 or 103 | (len(split_line) == 6 and split_line[4] == "as"))): 104 | return "import %s.%s" % (split_line[1], split_line[3]) 105 | else: 106 | return line 107 | 108 | 109 | class GlobalCheck(object): 110 | """Base class for checks that should be run only once.""" 111 | 112 | name = None 113 | version = '0.0.1' 114 | skip_on_py3 = False 115 | _has_run = set() 116 | 117 | def __init__(self, tree, *args): 118 | pass 119 | 120 | def run(self): 121 | """Make run a no-op if run() has been called before. 122 | 123 | Store in a global registry the list of checks we've run. If we have 124 | run that one before, just skip doing anything the subsequent times. 125 | This way, since pycodestyle is file/line based, we don't wind 126 | up re-running a check on a git commit message over and over again. 127 | """ 128 | if self.name and self.name not in self.__class__._has_run: 129 | self.__class__._has_run.add(self.name) 130 | ret = self.run_once() 131 | if ret is not None: 132 | yield ret 133 | 134 | def run_once(self): 135 | pass 136 | -------------------------------------------------------------------------------- /hacking/checks/imports.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import re 14 | 15 | from hacking import core 16 | 17 | RE_RELATIVE_IMPORT = re.compile(r'^from\s*[.]') 18 | RE_EVENTLET_IMPORT = re.compile( 19 | r'^\s*(?:import\s+eventlet(?:\s|$|\.)|from\s+eventlet(?:\s|$|\.))') 20 | 21 | 22 | @core.flake8ext 23 | def hacking_import_rules(logical_line, filename, noqa): 24 | r"""Check for imports. 25 | 26 | OpenStack HACKING guide recommends one import per line: 27 | Do not import more than one module per line 28 | 29 | Examples: 30 | Okay: from nova.compute import api 31 | H301: from nova.compute import api, utils 32 | 33 | 34 | Do not use wildcard import 35 | 36 | Do not make relative imports 37 | 38 | Examples: 39 | Okay: from os import path 40 | Okay: from os import path as p 41 | Okay: from os import (path as p) 42 | Okay: import os.path 43 | Okay: from nova.compute import rpcapi 44 | Okay: from six.moves.urllib import parse 45 | H303: from os.path import * 46 | H304: from .compute import rpcapi 47 | """ 48 | # TODO(jogo): make the following doctests pass: 49 | # H301: import os, sys 50 | # TODO(mordred: We need to split this into different checks so that they 51 | # can be disabled by command line switches properly 52 | 53 | if noqa: 54 | return 55 | 56 | split_line = logical_line.split() 57 | split_line_len = len(split_line) 58 | if (split_line_len > 1 and split_line[0] in ('import', 'from') and 59 | not core.is_import_exception(split_line[1])): 60 | pos = logical_line.find(',') 61 | if pos != -1: 62 | if split_line[0] == 'from': 63 | yield pos, "H301: one import per line" 64 | pos = logical_line.find('*') 65 | if pos != -1: 66 | yield pos, "H303: No wildcard (*) import." 67 | return 68 | 69 | if split_line_len in (2, 4, 6) and split_line[1] != "__future__": 70 | if 'from' == split_line[0] and split_line_len > 3: 71 | mod = '.'.join((split_line[1], split_line[3])) 72 | if core.is_import_exception(mod): 73 | return 74 | if RE_RELATIVE_IMPORT.search(logical_line): 75 | yield logical_line.find('.'), ( 76 | "H304: No relative imports. '%s' is a relative import" 77 | % logical_line) 78 | return 79 | 80 | 81 | @core.flake8ext 82 | def hacking_import_alphabetical(logical_line, blank_before, previous_logical, 83 | indent_level, previous_indent_level): 84 | r"""Check for imports in alphabetical order. 85 | 86 | OpenStack HACKING guide recommendation for imports: 87 | imports in human alphabetical order 88 | 89 | Okay: import os\nimport sys\n\nimport nova\nfrom nova import test 90 | Okay: import os\nimport sys 91 | H306: import sys\nimport os 92 | Okay: import sys\n\n# foo\nimport six 93 | """ 94 | # handle import x 95 | # use .lower since capitalization shouldn't dictate order 96 | if blank_before < 1 and indent_level == previous_indent_level: 97 | split_line = core.import_normalize(logical_line. 98 | strip()).lower().split() 99 | split_previous = core.import_normalize(previous_logical. 100 | strip()).lower().split() 101 | length = [2, 4] 102 | if (len(split_line) in length and len(split_previous) in length and 103 | split_line[0] == "import" and split_previous[0] == "import"): 104 | if split_line[1] < split_previous[1]: 105 | yield (0, "H306: imports not in alphabetical order (%s, %s)" 106 | % (split_previous[1], split_line[1])) 107 | 108 | 109 | @core.flake8ext 110 | @core.off_by_default 111 | def hacking_no_eventlet(logical_line, noqa): 112 | r"""Check that eventlet is not imported. 113 | 114 | Eventlet is being removed from OpenStack projects as part of the 115 | async migration effort. New code should use threading or asyncio instead. 116 | 117 | Examples: 118 | H905: import eventlet 119 | H905: from eventlet import something 120 | Okay: import asyncio 121 | Okay: import threading 122 | """ 123 | if noqa: 124 | return 125 | 126 | if RE_EVENTLET_IMPORT.match(logical_line): 127 | yield (0, "H905: eventlet import detected. " 128 | "Eventlet is banned, use threading or asyncio instead.") 129 | -------------------------------------------------------------------------------- /hacking/checks/localization.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import re 14 | import tokenize 15 | 16 | from hacking import core 17 | 18 | 19 | FORMAT_RE = re.compile(r"%(?:" 20 | r"%|" # Ignore plain percents 21 | r"(\(\w+\))?" # mapping key 22 | r"([#0 +-]?" # flag 23 | r"(?:\d+|\*)?" # width 24 | r"(?:\.\d+)?" # precision 25 | r"[hlL]?" # length mod 26 | r"\w))") # type 27 | 28 | 29 | class LocalizationError(Exception): 30 | pass 31 | 32 | 33 | def check_i18n(): 34 | """Generator that checks token stream for localization errors. 35 | 36 | Expects tokens to be ``send``ed one by one. 37 | Raises LocalizationError if some error is found. 38 | """ 39 | while True: 40 | try: 41 | token_type, text, _, _, line = yield 42 | except GeneratorExit: 43 | return 44 | 45 | if text == "def" and token_type == tokenize.NAME: 46 | # explicitly ignore function definitions, as oslo defines these 47 | return 48 | if (token_type == tokenize.NAME and 49 | text in ["_", "_LI", "_LW", "_LE", "_LC"]): 50 | 51 | while True: 52 | token_type, text, start, _, _ = yield 53 | if token_type != tokenize.NL: 54 | break 55 | if token_type != tokenize.OP or text != "(": 56 | continue # not a localization call 57 | 58 | format_string = '' 59 | while True: 60 | token_type, text, start, _, _ = yield 61 | if token_type == tokenize.STRING: 62 | format_string += eval(text) 63 | elif token_type == tokenize.NL: 64 | pass 65 | else: 66 | break 67 | 68 | if not format_string: 69 | raise LocalizationError( 70 | start, "H701: Empty localization string") 71 | if token_type != tokenize.OP: 72 | raise LocalizationError( 73 | start, "H701: Invalid localization call") 74 | if text != ")": 75 | if text == "%": 76 | raise LocalizationError( 77 | start, 78 | "H702: Formatting operation should be outside" 79 | " of localization method call") 80 | elif text == "+": 81 | raise LocalizationError( 82 | start, 83 | "H702: Use bare string concatenation instead of +") 84 | else: 85 | raise LocalizationError( 86 | start, "H702: Argument to _, _LI, _LW, _LC, or _LE " 87 | "must be just a string") 88 | 89 | format_specs = FORMAT_RE.findall(format_string) 90 | positional_specs = [(key, spec) for key, spec in format_specs 91 | if not key and spec] 92 | # not spec means %%, key means %(smth)s 93 | if len(positional_specs) > 1: 94 | raise LocalizationError( 95 | start, "H703: Multiple positional placeholders") 96 | 97 | 98 | @core.flake8ext 99 | def hacking_localization_strings(logical_line, tokens, noqa): 100 | r"""Check localization in line. 101 | 102 | Okay: _("This is fine") 103 | Okay: _LI("This is fine") 104 | Okay: _LW("This is fine") 105 | Okay: _LE("This is fine") 106 | Okay: _LC("This is fine") 107 | Okay: _("This is also fine %s") 108 | Okay: _("So is this %s, %(foo)s") % {foo: 'foo'} 109 | H701: _('') 110 | Okay: def _(msg):\n pass 111 | Okay: def _LE(msg):\n pass 112 | H701: _LI('') 113 | H701: _LW('') 114 | H701: _LE('') 115 | H701: _LC('') 116 | Okay: _('') # noqa 117 | H702: _("Bob" + " foo") 118 | H702: _LI("Bob" + " foo") 119 | H702: _LW("Bob" + " foo") 120 | H702: _LE("Bob" + " foo") 121 | H702: _LC("Bob" + " foo") 122 | Okay: _("Bob" + " foo") # noqa 123 | H702: _("Bob %s" % foo) 124 | H702: _LI("Bob %s" % foo) 125 | H702: _LW("Bob %s" % foo) 126 | H702: _LE("Bob %s" % foo) 127 | H702: _LC("Bob %s" % foo) 128 | H702: _("%s %s" % (foo, bar)) 129 | H703: _("%s %s") % (foo, bar) 130 | """ 131 | if noqa: 132 | return 133 | gen = check_i18n() 134 | next(gen) 135 | try: 136 | list(map(gen.send, tokens)) 137 | gen.close() 138 | except LocalizationError as e: 139 | yield e.args 140 | 141 | # TODO(jogo) Dict and list objects 142 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | hacking is a set of flake8 plugins that test and enforce the 5 | `OpenStack StyleGuide `_ 6 | 7 | Hacking pins its dependencies, as a new release of some dependency can break 8 | hacking based gating jobs. This is because new versions of dependencies can 9 | introduce new rules, or make existing rules stricter. 10 | 11 | Installation 12 | ============ 13 | 14 | hacking is available from pypi, so just run:: 15 | 16 | pip install hacking 17 | 18 | This will install specific versions of ``flake8`` with the ``hacking``, 19 | ``pep8``, ``mccabe`` and ``pyflakes`` plugins. 20 | 21 | Origin 22 | ====== 23 | 24 | Hacking started its life out as a text file in Nova's first commit. It was 25 | initially based on the `Google Python Style Guide`_, and over time more 26 | OpenStack specific rules were added. Hacking serves several purposes: 27 | 28 | 1. Agree on a common style guide so reviews don't get bogged down on style 29 | nit picks. (example: docstring guidelines) 30 | 2. Make code written by many different authors easier to read by making the 31 | style more uniform. (example: unix vs windows newlines) 32 | 3. Call out dangerous patterns and avoid them. (example: shadowing built-in 33 | or reserved words) 34 | 35 | Initially the hacking style guide was enforced manually by reviewers, but this 36 | was a big waste of time so hacking, the tool, was born to automate 37 | the process and remove the extra burden from human reviewers. 38 | 39 | .. _`Google Python Style Guide`: https://google.github.io/styleguide/pyguide.html 40 | 41 | Versioning 42 | ========== 43 | 44 | hacking uses the ``major.minor.maintenance`` release notation, where maintenance 45 | releases cannot contain new checks. This way projects can gate on hacking 46 | by pinning on the ``major.minor`` number while accepting maintenance updates 47 | without being concerned that a new version will break the gate with a new 48 | check. 49 | 50 | For example a project can depend on ``hacking>=0.10.0,<0.11.0``, and can know 51 | that ``0.10.1`` will not fail in places where ``0.10.0`` passed. 52 | 53 | 54 | Adding additional checks 55 | ======================== 56 | 57 | Each check is a pep8 plugin so read 58 | 59 | - https://github.com/jcrocholl/pep8/blob/master/docs/developer.rst#contribute 60 | 61 | The focus of new or changed rules should be to do one of the following 62 | 63 | - Substantially increase the reviewability of the code (eg: H301, H303) 64 | as they make it easy to understand where symbols come from) 65 | - Catch a common programming error that may arise in the future (H201) 66 | - Prevent a situation that would 100% of the time be -1ed by 67 | developers (H903) 68 | 69 | But, as always, remember that these are Guidelines. Treat them as 70 | such. There are always times for exceptions. All new rules should 71 | support noqa. 72 | 73 | If a check needs to be staged in, or it does not apply to every project or its 74 | branch, it can be added as off by default. 75 | 76 | Requirements 77 | ------------ 78 | - The check must already have community support. We do not want to dictate 79 | style, only enforce it. 80 | - The canonical source of the OpenStack Style Guidelines is 81 | `StyleGuide `_, 82 | and hacking just enforces 83 | them; so when adding a new check, it must be in ``HACKING.rst`` 84 | - False negatives are ok, but false positives are not 85 | - Cannot be project specific, project specific checks should be `Local Checks`_ 86 | - Include extensive tests 87 | - Registered as entry_points in ``setup.cfg`` 88 | - Error code must be in the relevant ``Hxxx`` group 89 | - The check should not attempt to import modules from the code being checked. 90 | Importing random modules, has caused all kinds of trouble for us in the past. 91 | 92 | 93 | Enabling off-by-default checks 94 | ============================== 95 | 96 | Some of the available checks are disabled by default. These checks are: 97 | 98 | - [H106] Don't put vim configuration in source files. 99 | - [H203] Use assertIs(Not)None to check for None. 100 | - [H204] Use assert(Not)Equal to check for equality. 101 | - [H205] Use assert(Greater|Less)(Equal) for comparison. 102 | - [H210] Require 'autospec', 'spec', or 'spec_set' in 103 | mock.patch/mock.patch.object calls 104 | - [H216] Make sure unittest.mock is used instead of the third party mock 105 | library 106 | - [H904] Delay string interpolations at logging calls. 107 | - [H905] Log.warn is deprecated. Enforce use of LOG.warning. 108 | 109 | To enable these checks, edit the ``flake8`` section of the ``tox.ini`` file. 110 | For example to enable H106 and H203: 111 | 112 | .. code-block:: ini 113 | 114 | [flake8] 115 | enable-extensions = H106,H203 116 | 117 | Local Checks 118 | ============ 119 | 120 | hacking supports having local changes in a source tree. They need to 121 | be registered individually in tox.ini: 122 | 123 | Add to tox.ini a new section ``flake8:local-plugins`` and list each plugin with 124 | its entry-point. Additionally, you can add the path to the files 125 | containing the plugins so that the repository does not need to be 126 | installed with the ``paths`` directive. 127 | 128 | .. code-block:: ini 129 | 130 | [flake8:local-plugins] 131 | extension = 132 | N307 = checks:import_no_db_in_virt 133 | N325 = checks:CheckForStrUnicodeExc 134 | paths = 135 | ./nova/hacking 136 | 137 | The plugins, in the example above they live in 138 | ``nova/hacking/checks.py``, need to annotate all functions with ``@core.flake8ext`` 139 | 140 | .. code-block:: python 141 | 142 | from hacking import core 143 | ... 144 | @core.flake8ext 145 | def import_no_db_in_virt(logical_line, filename): 146 | ... 147 | 148 | class CheckForStrUnicodeExc(BaseASTChecker): 149 | name = "check_for_str_unicode_exc" 150 | version = "1.0" 151 | ... 152 | 153 | Further details are part of the `flake8 documentation 154 | `_. 155 | -------------------------------------------------------------------------------- /hacking/checks/docstrings.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import tokenize 14 | 15 | from hacking import core 16 | 17 | START_DOCSTRING_TRIPLE = ['u"""', 'r"""', '"""', "u'''", "r'''", "'''"] 18 | END_DOCSTRING_TRIPLE = ['"""', "'''"] 19 | 20 | 21 | @core.flake8ext 22 | def hacking_docstring_start_space(physical_line, previous_logical, tokens): 23 | r"""Check for docstring not starting with space. 24 | 25 | OpenStack HACKING guide recommendation for docstring: 26 | Docstring should not start with space 27 | 28 | Okay: def foo():\n '''This is good.''' 29 | Okay: def foo():\n r'''This is good.''' 30 | Okay: def foo():\n a = ''' This is not a docstring.''' 31 | Okay: def foo():\n pass\n ''' This is not.''' 32 | H401: def foo():\n ''' This is not.''' 33 | H401: def foo():\n r''' This is not.''' 34 | """ 35 | docstring = is_docstring(tokens, previous_logical) 36 | if docstring: 37 | start, start_triple = _find_first_of(docstring, START_DOCSTRING_TRIPLE) 38 | if docstring[len(start_triple)] == ' ': 39 | # docstrings get tokenized on the last line of the docstring, so 40 | # we don't know the exact position. 41 | return (0, "H401: docstring should not start with" 42 | " a space") 43 | 44 | 45 | @core.flake8ext 46 | def hacking_docstring_multiline_end(physical_line, previous_logical, tokens): 47 | r"""Check multi line docstring end. 48 | 49 | OpenStack HACKING guide recommendation for docstring: 50 | Docstring should end on a new line 51 | 52 | Okay: '''foobar\nfoo\nbar\n''' 53 | Okay: def foo():\n '''foobar\n\nfoo\nbar\n''' 54 | Okay: class Foo(object):\n '''foobar\n\nfoo\nbar\n''' 55 | Okay: def foo():\n a = '''not\na\ndocstring''' 56 | Okay: def foo():\n a = '''not\na\ndocstring''' # blah 57 | Okay: def foo():\n pass\n'''foobar\nfoo\nbar\n d''' 58 | H403: def foo():\n '''foobar\nfoo\nbar\ndocstring''' 59 | H403: def foo():\n '''foobar\nfoo\nbar\npretend raw: r''' 60 | H403: class Foo(object):\n '''foobar\nfoo\nbar\ndocstring'''\n\n 61 | """ 62 | docstring = is_docstring(tokens, previous_logical) 63 | if docstring: 64 | if '\n' not in docstring: 65 | # not a multi line 66 | return 67 | else: 68 | last_line = docstring.split('\n')[-1] 69 | pos = max(last_line.rfind(i) for i in END_DOCSTRING_TRIPLE) 70 | if len(last_line[:pos].strip()) > 0: 71 | # Something before the end docstring triple 72 | return (pos, 73 | "H403: multi line docstrings should end on a new line") 74 | 75 | 76 | @core.flake8ext 77 | def hacking_docstring_multiline_start(physical_line, previous_logical, tokens): 78 | r"""Check multi line docstring starts immediately with summary. 79 | 80 | OpenStack HACKING guide recommendation for docstring: 81 | Docstring should start with a one-line summary, less than 80 characters. 82 | 83 | Okay: '''foobar\n\nfoo\nbar\n''' 84 | Okay: def foo():\n a = '''\nnot\na docstring\n''' 85 | H404: def foo():\n '''\nfoo\nbar\n'''\n\n 86 | H404: def foo():\n r'''\nfoo\nbar\n'''\n\n 87 | """ 88 | docstring = is_docstring(tokens, previous_logical) 89 | if docstring: 90 | if '\n' not in docstring: 91 | # single line docstring 92 | return 93 | start, start_triple = _find_first_of(docstring, START_DOCSTRING_TRIPLE) 94 | lines = docstring.split('\n') 95 | if lines[0].strip() == start_triple: 96 | # docstrings get tokenized on the last line of the docstring, so 97 | # we don't know the exact position. 98 | return (0, "H404: multi line docstring " 99 | "should start without a leading new line") 100 | 101 | 102 | @core.flake8ext 103 | def hacking_docstring_summary(physical_line, previous_logical, tokens): 104 | r"""Check multi line docstring summary is separated with empty line. 105 | 106 | OpenStack HACKING guide recommendation for docstring: 107 | Docstring should start with a one-line summary, less than 80 characters. 108 | 109 | Okay: def foo():\n a = '''\nnot\na docstring\n''' 110 | Okay: '''foobar\n\nfoo\nbar\n''' 111 | H405: def foo():\n '''foobar\nfoo\nbar\n''' 112 | H405: def foo():\n r'''foobar\nfoo\nbar\n''' 113 | H405: def foo():\n '''foobar\n''' 114 | """ 115 | docstring = is_docstring(tokens, previous_logical) 116 | if docstring: 117 | if '\n' not in docstring: 118 | # not a multi line docstring 119 | return 120 | lines = docstring.split('\n') 121 | if len(lines) > 1 and len(lines[1].strip()) != 0: 122 | # docstrings get tokenized on the last line of the docstring, so 123 | # we don't know the exact position. 124 | return (0, "H405: multi line docstring " 125 | "summary not separated with an empty line") 126 | 127 | 128 | def is_docstring(tokens, previous_logical): 129 | """Return found docstring 130 | 131 | 'A docstring is a string literal that occurs as the first statement in a 132 | module, function, class,' 133 | http://www.python.org/dev/peps/pep-0257/#what-is-a-docstring 134 | """ 135 | for token_type, text, start, _, _ in tokens: 136 | if token_type == tokenize.STRING: 137 | break 138 | elif token_type != tokenize.INDENT: 139 | return False 140 | else: 141 | return False 142 | line = text.lstrip() 143 | start, start_triple = _find_first_of(line, START_DOCSTRING_TRIPLE) 144 | if (previous_logical.startswith("def ") or 145 | previous_logical.startswith("class ")): 146 | if start == 0: 147 | return text 148 | 149 | 150 | def _find_first_of(line, substrings): 151 | """Find earliest occurrence of one of substrings in line. 152 | 153 | Returns pair of index and found substring, or (-1, None) 154 | if no occurrences of any of substrings were found in line. 155 | """ 156 | starts = ((line.find(i), i) for i in substrings) 157 | found = [(i, sub) for i, sub in starts if i != -1] 158 | if found: 159 | return min(found) 160 | else: 161 | return -1, None 162 | -------------------------------------------------------------------------------- /hacking/checks/mock_checks.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import ast 14 | import re 15 | 16 | from hacking import core 17 | 18 | 19 | @core.off_by_default 20 | @core.flake8ext 21 | class MockAutospecCheck(object): 22 | """Check for 'autospec' in mock.patch/mock.patch.object calls 23 | 24 | Okay: mock.patch('target_module_1', autospec=True) 25 | Okay: mock.patch('target_module_1', autospec=False) 26 | Okay: mock.patch('target_module_1', autospec=None) 27 | Okay: mock.patch('target_module_1', defined_mock) 28 | Okay: mock.patch('target_module_1', new=defined_mock) 29 | Okay: mock.patch('target_module_1', new_callable=SomeFunc) 30 | Okay: mock.patch('target_module_1', defined_mock) 31 | Okay: mock.patch('target_module_1', spec=1000) 32 | Okay: mock.patch('target_module_1', spec_set=['data']) 33 | Okay: mock.patch('target_module_1', wraps=some_obj) 34 | 35 | H210: mock.patch('target_module_1') 36 | Okay: mock.patch('target_module_1') # noqa 37 | H210: mock.patch('target_module_1', somearg=23) 38 | Okay: mock.patch('target_module_1', somearg=23) # noqa 39 | 40 | Okay: mock.patch.object('target_module_2', 'attribute', autospec=True) 41 | Okay: mock.patch.object('target_module_2', 'attribute', autospec=False) 42 | Okay: mock.patch.object('target_module_2', 'attribute', autospec=None) 43 | Okay: mock.patch.object('target_module_2', 'attribute', new=defined_mock) 44 | Okay: mock.patch.object('target_module_2', 'attribute', defined_mock) 45 | Okay: mock.patch.object('target_module_2', 'attribute', new_callable=AFunc) 46 | Okay: mock.patch.object('target_module_2', 'attribute', spec=3) 47 | Okay: mock.patch.object('target_module_2', 'attribute', spec_set=[3]) 48 | Okay: mock.patch.object('target_module_2', 'attribute', wraps=some_obj) 49 | 50 | 51 | H210: mock.patch.object('target_module_2', 'attribute', somearg=2) 52 | H210: mock.patch.object('target_module_2', 'attribute') 53 | 54 | """ 55 | 56 | name = "mock_check" 57 | version = "1.00" 58 | 59 | def __init__(self, tree, filename): 60 | self.filename = filename 61 | self.tree = tree 62 | 63 | def run(self): 64 | mcv = MockCheckVisitor(self.filename) 65 | mcv.visit(self.tree) 66 | for message in mcv.messages: 67 | yield message 68 | 69 | 70 | class MockCheckVisitor(ast.NodeVisitor): 71 | # Patchers we are looking for and minimum number of 'args' without 72 | # 'autospec' to not be flagged 73 | patchers = {'mock.patch': 2, 'mock.patch.object': 3} 74 | spec_keywords = {"autospec", "new", "new_callable", "spec", "spec_set", 75 | "wraps"} 76 | 77 | def __init__(self, filename): 78 | super(MockCheckVisitor, self).__init__() 79 | self.messages = [] 80 | self.filename = filename 81 | 82 | def check_missing_autospec(self, call_node): 83 | 84 | def find_autospec_keyword(keyword_node): 85 | for keyword_obj in keyword_node: 86 | keyword = keyword_obj.arg 87 | # If they have defined autospec or new then it is okay 88 | if keyword in self.spec_keywords: 89 | return True 90 | return False 91 | 92 | if isinstance(call_node, ast.Call): 93 | func_info = FunctionNameFinder(self.filename) 94 | func_info.visit(call_node) 95 | 96 | # We are only looking at our patchers 97 | if func_info.function_name not in self.patchers: 98 | return 99 | 100 | min_args = self.patchers[func_info.function_name] 101 | 102 | if not find_autospec_keyword(call_node.keywords): 103 | if len(call_node.args) < min_args: 104 | self.messages.append( 105 | (call_node.lineno, call_node.col_offset, 106 | "H210 Missing 'autospec' or 'spec_set' keyword in " 107 | "mock.patch/mock.patch.object", MockCheckVisitor) 108 | ) 109 | 110 | def visit_Call(self, node): 111 | self.check_missing_autospec(node) 112 | self.generic_visit(node) 113 | 114 | 115 | class FunctionNameFinder(ast.NodeVisitor): 116 | """Finds the name of the function""" 117 | def __init__(self, filename): 118 | super(FunctionNameFinder, self).__init__() 119 | self._func_name = [] 120 | self.filename = filename 121 | 122 | @property 123 | def function_name(self): 124 | return '.'.join(reversed(self._func_name)) 125 | 126 | def visit_Name(self, node): 127 | self._func_name.append(node.id) 128 | self.generic_visit(node) 129 | 130 | def visit_Attribute(self, node): 131 | try: 132 | self._func_name.append(node.attr) 133 | self._func_name.append(node.value.id) 134 | except AttributeError: 135 | self.generic_visit(node) 136 | 137 | def visit(self, node): 138 | # If we get called with an ast.Call node, then work on the 'node.func', 139 | # as we want the function name. 140 | if isinstance(node, ast.Call): 141 | return super(FunctionNameFinder, self).visit(node.func) 142 | return super(FunctionNameFinder, self).visit(node) 143 | 144 | 145 | third_party_mock = re.compile('^import.mock') 146 | from_third_party_mock = re.compile('^from.mock.import') 147 | 148 | 149 | @core.flake8ext 150 | def hacking_no_third_party_mock(logical_line, noqa): 151 | """Check for use of mock instead of unittest.mock. 152 | 153 | Projects have had issues with using mock without including it in their 154 | requirements, thinking it is the standard library version. This makes it so 155 | these projects need to explicitly turn off this check to make it clear they 156 | intended to use it. 157 | 158 | Okay: from unittest import mock 159 | Okay: from unittest.mock import patch 160 | Okay: import unittest.mock 161 | H216: import mock 162 | H216: from mock import patch 163 | Okay: try: import mock 164 | Okay: import mock # noqa 165 | """ 166 | msg = ('H216: The unittest.mock module should be used rather than the ' 167 | 'third party mock package unless actually needed. If so, disable ' 168 | 'the H216 check in hacking config and ensure mock is declared in ' 169 | "the project's requirements.") 170 | 171 | if noqa: 172 | return 173 | 174 | if (re.match(third_party_mock, logical_line) or 175 | re.match(from_third_party_mock, logical_line)): 176 | yield (0, msg) 177 | -------------------------------------------------------------------------------- /hacking/checks/comments.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | import re 14 | import tokenize 15 | 16 | from hacking import core 17 | 18 | 19 | AUTHOR_TAG_RE = (re.compile(r"^\s*#\s*@?(a|A)uthors?:"), 20 | re.compile(r"^\.\.\s+moduleauthor::")) 21 | 22 | 23 | @core.flake8ext 24 | def hacking_todo_format(physical_line, tokens): 25 | """Check for 'TODO()'. 26 | 27 | OpenStack HACKING guide recommendation for TODO: 28 | Include your name with TODOs as in "# TODO(termie)" 29 | 30 | Okay: # TODO(sdague) 31 | H101: # TODO fail 32 | H101: # TODO 33 | H101: # TODO (jogo) fail 34 | Okay: TODO = 5 35 | """ 36 | # TODO(jogo): make the following doctests pass: 37 | # H101: #TODO(jogo fail 38 | # H101: #TODO(jogo 39 | # TODO(jogo): make this check docstrings as well (don't have to be at top 40 | # of function) 41 | for token_type, text, start_index, _, _ in tokens: 42 | if token_type == tokenize.COMMENT: 43 | pos = text.find('TODO') 44 | pos1 = text.find('TODO(') 45 | if (pos != pos1): 46 | return pos + start_index[1], "H101: Use TODO(NAME)" 47 | 48 | 49 | @core.flake8ext 50 | def hacking_has_license(physical_line, filename, lines, line_number): 51 | """Check for Apache 2.0 license. 52 | 53 | H102 license header not found 54 | """ 55 | # don't work about init files for now 56 | # TODO(sdague): enforce license in init file if it's not empty of content 57 | license_found = False 58 | 59 | # skip files that are < 10 lines, which isn't enough for a license to fit 60 | # this allows us to handle empty files, as well as not fail on the Okay 61 | # doctests. 62 | if line_number == 1 and len(lines) > 10 and _project_is_apache(): 63 | for idx, line in enumerate(lines): 64 | # if it's more than 10 characters in, it's probably not in the 65 | # header 66 | if 0 <= line.find('Licensed under the Apache License') < 10: 67 | license_found = True 68 | if 0 <= line.find('SPDX-License-Identifier:') < 10: 69 | license_found = True 70 | if not license_found: 71 | return (0, "H102: Apache 2.0 license header not found") 72 | 73 | 74 | @core.flake8ext 75 | def hacking_has_correct_license(physical_line, filename, lines, line_number): 76 | """Check for Apache 2.0 license. 77 | 78 | H103 header does not match Apache 2.0 License notice 79 | """ 80 | # don't work about init files for now 81 | # TODO(sdague): enforce license in init file if it's not empty of content 82 | 83 | # skip files that are < 10 lines, which isn't enough for a license to fit 84 | # this allows us to handle empty files, as well as not fail on the Okay 85 | # doctests. 86 | if line_number == 1 and len(lines) > 10 and _project_is_apache(): 87 | for idx, line in enumerate(lines): 88 | column = line.find('Licensed under the Apache License') 89 | if 0 < column < 10: 90 | exact, cmp_str = _check_for_exact_apache(idx, lines) 91 | if (not exact and 92 | line.find('SPDX-License-Identifier: Apache-2.0') <= 0): 93 | return (column, "H103: Header does not match Apache 2.0 " 94 | "License notice" + cmp_str) 95 | 96 | 97 | EMPTY_LINE_RE = re.compile(r"^\s*(#.*|$)") 98 | 99 | 100 | @core.flake8ext 101 | def hacking_has_only_comments(physical_line, filename, lines, line_number): 102 | """Check for empty files with only comments 103 | 104 | H104 empty file with only comments 105 | """ 106 | if line_number == 1 and all(map(EMPTY_LINE_RE.match, lines)): 107 | return (0, "H104: File contains nothing but comments") 108 | 109 | 110 | _is_apache_cache = None 111 | 112 | 113 | def _project_is_apache(): 114 | """Determine if a project is Apache. 115 | 116 | Look for a key string in a set of possible license files to figure out 117 | if a project looks to be Apache. This is used as a precondition for 118 | enforcing license headers. 119 | """ 120 | 121 | global _is_apache_cache 122 | if _is_apache_cache is not None: 123 | return _is_apache_cache 124 | license_files = ["LICENSE"] 125 | for filename in license_files: 126 | try: 127 | with open(filename, "r") as file: 128 | for line in file: 129 | if re.search('Apache License', line): 130 | _is_apache_cache = True 131 | return True 132 | except IOError: 133 | pass 134 | _is_apache_cache = False 135 | return False 136 | 137 | 138 | def _check_for_exact_apache(start, lines): 139 | """Check for the Apache 2.0 license header. 140 | 141 | We strip all the newlines and extra spaces so this license string 142 | should work regardless of indentation in the file. 143 | """ 144 | APACHE2 = """ 145 | Licensed under the Apache License, Version 2.0 (the "License"); you may 146 | not use this file except in compliance with the License. You may obtain 147 | a copy of the License at 148 | 149 | http://www.apache.org/licenses/LICENSE-2.0 150 | 151 | Unless required by applicable law or agreed to in writing, software 152 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 153 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 154 | License for the specific language governing permissions and limitations 155 | under the License.""" 156 | 157 | # out of all the formatting I've seen, a 12 line version seems to be the 158 | # longest in the source tree. So just take the 12 lines starting with where 159 | # the Apache starting words were found, strip all the '#' and collapse the 160 | # spaces. 161 | content = ''.join(lines[start:(start + 12)]) 162 | content = re.sub(r'\#', '', content) 163 | content = re.sub(r'\s+', ' ', content).strip() 164 | stripped_apache2 = re.sub(r'\s+', ' ', APACHE2).strip() 165 | 166 | if stripped_apache2 in content: 167 | return (True, None) 168 | else: 169 | return (False, "\n!=:\n'%s' !=\n'%s'" % 170 | (content, stripped_apache2)) 171 | 172 | 173 | @core.flake8ext 174 | def hacking_no_author_tags(physical_line): 175 | """Check that no author tags are used. 176 | 177 | H105 don't use author tags 178 | """ 179 | for regex in AUTHOR_TAG_RE: 180 | if regex.match(physical_line): 181 | physical_line = physical_line.lower() 182 | pos = physical_line.find('moduleauthor') 183 | if pos < 0: 184 | pos = physical_line.find('author') 185 | return (pos, "H105: Don't use author tags") 186 | -------------------------------------------------------------------------------- /hacking/tests/checks/test_comments.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 eNovance 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from hacking.checks import comments 17 | from hacking import tests 18 | 19 | 20 | class CoreTestCase(tests.TestCase): 21 | def test_H102_none(self): 22 | """Verify that the H102 check finds an SPDX header""" 23 | self.assertEqual( 24 | (0, 'H102: Apache 2.0 license header not found'), 25 | comments.hacking_has_license( 26 | None, 27 | None, 28 | [ 29 | '# foo', 30 | '# bar', 31 | '# foo', 32 | '# bar', 33 | '# foo', 34 | '# bar', 35 | '# foo', 36 | '# bar', 37 | '# foo', 38 | '# bar', 39 | '# foo', 40 | '# bar', 41 | ], 42 | 1, 43 | ), 44 | ) 45 | 46 | def test_H102_full(self): 47 | """Verify that the H102 check finds an SPDX header""" 48 | self.assertIsNone(comments.hacking_has_license( 49 | None, 50 | None, 51 | [ 52 | '# foo', 53 | '# Licensed under the Apache License, Version 2.0', 54 | '# foo', 55 | '# bar', 56 | '# foo', 57 | '# bar', 58 | '# foo', 59 | '# bar', 60 | '# foo', 61 | '# bar', 62 | '# foo', 63 | '# bar', 64 | ], 65 | 1, 66 | )) 67 | 68 | def test_H102_SPDX(self): 69 | """Verify that the H102 check finds an SPDX header""" 70 | self.assertIsNone(comments.hacking_has_license( 71 | None, 72 | None, 73 | [ 74 | '# foo', 75 | '# SPDX-License-Identifier: Apache-2.0', 76 | '# foo', 77 | '# bar', 78 | '# foo', 79 | '# bar', 80 | '# foo', 81 | '# bar', 82 | '# foo', 83 | '# bar', 84 | '# foo', 85 | '# bar', 86 | ], 87 | 1, 88 | )) 89 | 90 | def test_H103_full_fail(self): 91 | """Verify that the H103 check finds an SPDX header""" 92 | self.assertEqual( 93 | (2, 'H103: Header does not match Apache 2.0 License notice\n' 94 | '!=:\n' 95 | '\'Licensed under the Apache License, Version 2.0 foo bar foo ' 96 | 'bar foo bar foo bar foo bar\' !=\n' 97 | '\'Licensed under the Apache License, Version 2.0 (the ' 98 | '"License"); you may not use this file except in compliance ' 99 | 'with the License. You may obtain a copy of the License at ' 100 | 'http://www.apache.org/licenses/LICENSE-2.0 Unless required ' 101 | 'by applicable law or agreed to in writing, software ' 102 | 'distributed under the License is distributed on an "AS IS" ' 103 | 'BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either ' 104 | 'express or implied. See the License for the specific ' 105 | 'language governing permissions and limitations under the ' 106 | 'License.\''), 107 | comments.hacking_has_correct_license( 108 | None, 109 | None, 110 | [ 111 | '# foo', 112 | '# Licensed under the Apache License, Version 2.0', 113 | '# foo', 114 | '# bar', 115 | '# foo', 116 | '# bar', 117 | '# foo', 118 | '# bar', 119 | '# foo', 120 | '# bar', 121 | '# foo', 122 | '# bar', 123 | ], 124 | 1, 125 | ), 126 | ) 127 | 128 | def test_H103_full(self): 129 | """Verify that the H103 check finds an SPDX header""" 130 | self.assertIsNone(comments.hacking_has_correct_license( 131 | None, 132 | None, 133 | [ 134 | """ 135 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 136 | # not use this file except in compliance with the License. You may obtain 137 | # a copy of the License at 138 | # 139 | # http://www.apache.org/licenses/LICENSE-2.0 140 | # 141 | # Unless required by applicable law or agreed to in writing, software 142 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 143 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 144 | # License for the specific language governing permissions and limitations 145 | # under the License. 146 | """ # noqa 147 | ], 148 | 1, 149 | )) 150 | 151 | def test_H103_SPDX(self): 152 | """Verify that the H103 check finds an SPDX header""" 153 | self.assertIsNone(comments.hacking_has_correct_license( 154 | None, 155 | None, 156 | [ 157 | '# foo', 158 | '# SPDX-License-Identifier: Apache-2.0', 159 | '# foo', 160 | '# bar', 161 | '# foo', 162 | '# bar', 163 | '# foo', 164 | '# bar', 165 | '# foo', 166 | '# bar', 167 | '# foo', 168 | '# bar', 169 | ], 170 | 1, 171 | )) 172 | 173 | def test_H104_regex(self): 174 | """Verify that the H104 regex matches correct lines.""" 175 | self.assertTrue(comments.hacking_has_only_comments( 176 | None, 177 | None, 178 | ['# foo', 179 | '# bar'], 180 | 1)) 181 | self.assertTrue(comments.hacking_has_only_comments( 182 | None, 183 | None, 184 | ['# foo', 185 | '# bar', 186 | ''], 187 | 1)) 188 | self.assertTrue(comments.hacking_has_only_comments( 189 | None, 190 | None, 191 | ['# foo', 192 | ' ', 193 | '# bar'], 194 | 1)) 195 | 196 | self.assertIsNone(comments.hacking_has_only_comments( 197 | None, 198 | None, 199 | ['# foo', 200 | ' ', 201 | '"""foobar"""'], 202 | 1)) 203 | self.assertIsNone(comments.hacking_has_only_comments( 204 | None, 205 | None, 206 | ['# foo', 207 | '', 208 | 'print(42)'], 209 | 1)) 210 | self.assertIsNone(comments.hacking_has_only_comments( 211 | None, 212 | None, 213 | ['# foo'], 214 | 100)) 215 | 216 | def test_H105(self): 217 | self.assertTrue(comments.hacking_no_author_tags( 218 | '# @author: Foo Bar')) 219 | 220 | self.assertTrue(comments.hacking_no_author_tags( 221 | '# @Author: Foo Bar')) 222 | 223 | self.assertTrue(comments.hacking_no_author_tags( 224 | '# author: Foo Bar')) 225 | 226 | self.assertTrue(comments.hacking_no_author_tags( 227 | '# authors: Foo Bar')) 228 | 229 | self.assertTrue(comments.hacking_no_author_tags( 230 | '# Author: Foo Bar')) 231 | 232 | self.assertTrue(comments.hacking_no_author_tags( 233 | '# Authors: Foo Bar')) 234 | 235 | self.assertTrue(comments.hacking_no_author_tags( 236 | '.. moduleauthor:: Foo Bar')) 237 | -------------------------------------------------------------------------------- /hacking/tests/checks/test_except_checks.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | # implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import textwrap 15 | from unittest import mock 16 | 17 | import pycodestyle 18 | 19 | from hacking.checks import except_checks 20 | from hacking import tests 21 | 22 | 23 | class ExceptChecksTest(tests.TestCase): 24 | 25 | def _assert_has_errors(self, code, checker, expected_errors=None, 26 | filename=None): 27 | actual_errors = [e[:3] for e in 28 | self._run_check(code, checker, filename)] 29 | self.assertEqual(expected_errors or [], actual_errors) 30 | 31 | # We are patching pycodestyle so that only the check under test is actually 32 | # installed. 33 | @mock.patch('pycodestyle._checks', 34 | {'physical_line': {}, 'logical_line': {}, 'tree': {}}) 35 | def _run_check(self, code, checker, filename=None): 36 | pycodestyle.register_check(checker) 37 | 38 | lines = textwrap.dedent(code).lstrip().splitlines(True) 39 | 40 | checker = pycodestyle.Checker(filename=filename, lines=lines) 41 | # NOTE(sdague): the standard reporter has printing to stdout 42 | # as a normal part of check_all, which bleeds through to the 43 | # test output stream in an unhelpful way. This blocks that printing. 44 | with mock.patch('pycodestyle.StandardReport.get_file_results'): 45 | checker.check_all() 46 | checker.report._deferred_print.sort() 47 | return checker.report._deferred_print 48 | 49 | def test_hacking_assert_true_instance(self): 50 | self.assertEqual( 51 | len(list(except_checks.hacking_assert_true_instance( 52 | "self.assertTrue(isinstance(e, " 53 | "exception.BuildAbortException))" 54 | ))), 55 | 1) 56 | 57 | self.assertEqual( 58 | len(list(except_checks.hacking_assert_true_instance( 59 | "self.assertTrue()" 60 | ))), 61 | 0) 62 | 63 | def test_hacking_assert_equal_in(self): 64 | self.assertEqual( 65 | len(list(except_checks.hacking_assert_equal_in( 66 | "self.assertEqual(a in b, True)" 67 | ))), 68 | 1) 69 | 70 | self.assertEqual( 71 | len(list(except_checks.hacking_assert_equal_in( 72 | "self.assertEqual('str' in 'string', True)" 73 | ))), 74 | 1) 75 | 76 | self.assertEqual( 77 | len(list(except_checks.hacking_assert_equal_in( 78 | "self.assertEqual(any(a==1 for a in b), True)" 79 | ))), 80 | 0) 81 | 82 | self.assertEqual( 83 | len(list(except_checks.hacking_assert_equal_in( 84 | "self.assertEqual(a in b, True)" 85 | ))), 86 | 1) 87 | 88 | self.assertEqual( 89 | len(list(except_checks.hacking_assert_equal_in( 90 | "self.assertEqual('str' in 'string', True)" 91 | ))), 92 | 1) 93 | 94 | self.assertEqual(len(list(except_checks.hacking_assert_equal_in( 95 | "self.assertEqual(any(a==1 for a in b, True))" 96 | ))), 97 | 0) 98 | 99 | self.assertEqual( 100 | len(list(except_checks.hacking_assert_equal_in( 101 | "self.assertEqual(a in b, False)" 102 | ))), 103 | 1) 104 | 105 | self.assertEqual( 106 | len(list(except_checks.hacking_assert_equal_in( 107 | "self.assertEqual('str' in 'string', False)" 108 | ))), 109 | 1) 110 | 111 | self.assertEqual( 112 | len(list(except_checks.hacking_assert_equal_in( 113 | "self.assertEqual(any(a==1 for a in b), False)" 114 | ))), 115 | 0) 116 | 117 | self.assertEqual( 118 | len(list(except_checks.hacking_assert_equal_in( 119 | "self.assertEqual(a in b, False)" 120 | ))), 121 | 1) 122 | 123 | self.assertEqual( 124 | len(list(except_checks.hacking_assert_equal_in( 125 | "self.assertEqual('str' in 'string', False)" 126 | ))), 127 | 1) 128 | 129 | self.assertEqual( 130 | len(list(except_checks.hacking_assert_equal_in( 131 | "self.assertEqual(any(a==1 for a in b, False))" 132 | ))), 133 | 0) 134 | 135 | def test_hacking_assert_true_or_false_with_in_or_not_in(self): 136 | self.assertEqual( 137 | len(list(except_checks.hacking_assert_true_or_false_with_in( 138 | "self.assertTrue(A in B)" 139 | ))), 140 | 1) 141 | 142 | self.assertEqual( 143 | len(list(except_checks.hacking_assert_true_or_false_with_in( 144 | "self.assertFalse(A in B)" 145 | ))), 146 | 1) 147 | 148 | self.assertEqual( 149 | len(list(except_checks.hacking_assert_true_or_false_with_in( 150 | "self.assertTrue(A not in B)" 151 | ))), 152 | 1) 153 | 154 | self.assertEqual( 155 | len(list(except_checks.hacking_assert_true_or_false_with_in( 156 | "self.assertFalse(A not in B)" 157 | ))), 158 | 1) 159 | 160 | self.assertEqual( 161 | len(list(except_checks.hacking_assert_true_or_false_with_in( 162 | "self.assertTrue(A in B, 'some message')" 163 | ))), 164 | 1) 165 | 166 | self.assertEqual( 167 | len(list(except_checks.hacking_assert_true_or_false_with_in( 168 | "self.assertFalse(A in B, 'some message')" 169 | ))), 170 | 1) 171 | 172 | self.assertEqual( 173 | len(list(except_checks.hacking_assert_true_or_false_with_in( 174 | "self.assertTrue(A not in B, 'some message')" 175 | ))), 176 | 1) 177 | 178 | self.assertEqual( 179 | len(list(except_checks.hacking_assert_true_or_false_with_in( 180 | "self.assertFalse(A not in B, 'some message')" 181 | ))), 182 | 1) 183 | 184 | self.assertEqual( 185 | len(list(except_checks.hacking_assert_true_or_false_with_in( 186 | "self.assertTrue(A in 'some string with spaces')" 187 | ))), 188 | 1) 189 | 190 | self.assertEqual( 191 | len(list(except_checks.hacking_assert_true_or_false_with_in( 192 | "self.assertTrue(A in ['1', '2', '3'])" 193 | ))), 194 | 1) 195 | 196 | self.assertEqual(len( 197 | list(except_checks.hacking_assert_true_or_false_with_in( 198 | "self.assertTrue(A in [1, 2, 3])" 199 | ))), 200 | 1) 201 | 202 | self.assertEqual( 203 | len(list(except_checks.hacking_assert_true_or_false_with_in( 204 | "self.assertTrue(any(A > 5 for A in B))" 205 | ))), 206 | 0) 207 | 208 | self.assertEqual( 209 | len(list(except_checks.hacking_assert_true_or_false_with_in( 210 | "self.assertTrue(any(A > 5 for A in B), 'some message')" 211 | ))), 212 | 0) 213 | 214 | self.assertEqual( 215 | len(list(except_checks.hacking_assert_true_or_false_with_in( 216 | "self.assertFalse(some in list1 and some2 in list2)" 217 | ))), 218 | 0) 219 | 220 | def test_hacking_oslo_assert_raises_regexp(self): 221 | code = """ 222 | self.assertRaisesRegexp(ValueError, 223 | "invalid literal for.*XYZ'$", 224 | int, 225 | 'XYZ') 226 | """ 227 | self._assert_has_errors( 228 | code, 229 | except_checks.hacking_assert_raises_regexp, expected_errors=[ 230 | (1, 0, "H213") 231 | ]) 232 | 233 | def test_hacking_assert_equal_type(self): 234 | self.assertEqual( 235 | len(list(except_checks.hacking_assert_equal_type( 236 | "self.assertEqual(type(als['QuicAssist']), list)" 237 | ))), 238 | 1) 239 | 240 | self.assertEqual( 241 | len(list(except_checks.hacking_assert_equal_type( 242 | "self.assertTrue()" 243 | ))), 244 | 0) 245 | -------------------------------------------------------------------------------- /releasenotes/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 11 | # implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Hacking Release Notes documentation build configuration file, created by 16 | # sphinx-quickstart on Tue Nov 3 17:40:50 2015. 17 | # 18 | # This file is execfile()d with the current directory set to its 19 | # containing dir. 20 | # 21 | # Note that not all possible configuration values are present in this 22 | # autogenerated file. 23 | # 24 | # All configuration values have a default; values that are commented out 25 | # serve to show the default. 26 | 27 | # If extensions (or modules to document with autodoc) are in another directory, 28 | # add these directories to sys.path here. If the directory is relative to the 29 | # documentation root, use os.path.abspath to make it absolute, like shown here. 30 | # sys.path.insert(0, os.path.abspath('.')) 31 | 32 | # -- General configuration ------------------------------------------------ 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'openstackdocstheme', 42 | 'reno.sphinxext', 43 | ] 44 | 45 | # openstackdocstheme options 46 | openstackdocs_repo_name = 'openstack/hacking' 47 | openstackdocs_bug_project = 'hacking' 48 | openstackdocs_bug_tag = '' 49 | openstackdocs_auto_name = False 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ['_templates'] 53 | 54 | # The suffix of source filenames. 55 | source_suffix = '.rst' 56 | 57 | # The encoding of source files. 58 | # source_encoding = 'utf-8-sig' 59 | 60 | # The master toctree document. 61 | master_doc = 'index' 62 | 63 | # General information about the project. 64 | project = 'Hacking Release Notes' 65 | copyright = '2015, Hacking Developers' 66 | 67 | # Release notes are version independent 68 | # The short X.Y version. 69 | release = '' 70 | # The short X.Y version. 71 | version = '' 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | # today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | # today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = [] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | # default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | # add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | # add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | # show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = 'native' 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | # modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | # keep_warnings = False 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | html_theme = 'openstackdocs' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | # html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | # html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | # html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | # html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | # html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | # html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | # html_static_path = ['_static'] 146 | 147 | # Add any extra paths that contain custom files (such as robots.txt or 148 | # .htaccess) here, relative to this directory. These files are copied 149 | # directly to the root of the documentation. 150 | # html_extra_path = [] 151 | 152 | # If true, SmartyPants will be used to convert quotes and dashes to 153 | # typographically correct entities. 154 | # html_use_smartypants = True 155 | 156 | # Custom sidebar templates, maps document names to template names. 157 | # html_sidebars = {} 158 | 159 | # Additional templates that should be rendered to pages, maps page names to 160 | # template names. 161 | # html_additional_pages = {} 162 | 163 | # If false, no module index is generated. 164 | # html_domain_indices = True 165 | 166 | # If false, no index is generated. 167 | # html_use_index = True 168 | 169 | # If true, the index is split into individual pages for each letter. 170 | # html_split_index = False 171 | 172 | # If true, links to the reST sources are added to the pages. 173 | # html_show_sourcelink = True 174 | 175 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 176 | # html_show_sphinx = True 177 | 178 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 179 | # html_show_copyright = True 180 | 181 | # If true, an OpenSearch description file will be output, and all pages will 182 | # contain a tag referring to it. The value of this option must be the 183 | # base URL from which the finished HTML is served. 184 | # html_use_opensearch = '' 185 | 186 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 187 | # html_file_suffix = None 188 | 189 | # Output file base name for HTML help builder. 190 | htmlhelp_basename = 'HackingReleaseNotesdoc' 191 | 192 | 193 | # -- Options for LaTeX output --------------------------------------------- 194 | 195 | latex_elements = { 196 | # The paper size ('letterpaper' or 'a4paper'). 197 | # 'papersize': 'letterpaper', 198 | 199 | # The font size ('10pt', '11pt' or '12pt'). 200 | # 'pointsize': '10pt', 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | # 'preamble': '', 204 | } 205 | 206 | # Grouping the document tree into LaTeX files. List of tuples 207 | # (source start file, target name, title, 208 | # author, documentclass [howto, manual, or own class]). 209 | latex_documents = [ 210 | ('index', 'HackingReleaseNotes.tex', 211 | 'Hacking Release Notes Documentation', 212 | 'Hacking Developers', 'manual'), 213 | ] 214 | 215 | # The name of an image file (relative to this directory) to place at the top of 216 | # the title page. 217 | # latex_logo = None 218 | 219 | # For "manual" documents, if this is true, then toplevel headings are parts, 220 | # not chapters. 221 | # latex_use_parts = False 222 | 223 | # If true, show page references after internal links. 224 | # latex_show_pagerefs = False 225 | 226 | # If true, show URL addresses after external links. 227 | # latex_show_urls = False 228 | 229 | # Documents to append as an appendix to all manuals. 230 | # latex_appendices = [] 231 | 232 | # If false, no module index is generated. 233 | # latex_domain_indices = True 234 | 235 | 236 | # -- Options for manual page output --------------------------------------- 237 | 238 | # One entry per manual page. List of tuples 239 | # (source start file, name, description, authors, manual section). 240 | man_pages = [ 241 | ('index', 'hackingreleasenotes', 'Hacking Release Notes Documentation', 242 | ['Hacking Developers'], 1) 243 | ] 244 | 245 | # If true, show URL addresses after external links. 246 | # man_show_urls = False 247 | 248 | 249 | # -- Options for Texinfo output ------------------------------------------- 250 | 251 | # Grouping the document tree into Texinfo files. List of tuples 252 | # (source start file, target name, title, author, 253 | # dir menu entry, description, category) 254 | texinfo_documents = [ 255 | ('index', 'HackingReleaseNotes', 'Hacking Release Notes Documentation', 256 | 'Hacking Developers', 'HackingReleaseNotes', 257 | 'One line description of project.', 258 | 'Miscellaneous'), 259 | ] 260 | 261 | # Documents to append as an appendix to all manuals. 262 | # texinfo_appendices = [] 263 | 264 | # If false, no module index is generated. 265 | # texinfo_domain_indices = True 266 | 267 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 268 | # texinfo_show_urls = 'footnote' 269 | 270 | # If true, do not generate a @detailmenu in the "Top" node's menu. 271 | # texinfo_no_detailmenu = False 272 | 273 | # -- Options for Internationalization output ------------------------------ 274 | locale_dirs = ['locale/'] 275 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | -------------------------------------------------------------------------------- /hacking/checks/except_checks.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | 13 | # module cannot be called except since that is a reserved word 14 | 15 | import ast 16 | import re 17 | 18 | from hacking import core 19 | 20 | RE_ASSERT_RAISES_EXCEPTION = re.compile(r"self\.assertRaises\(Exception[,\)]") 21 | RE_ASSERT_TRUE_INST = re.compile( 22 | r"(.)*assertTrue\(isinstance\((\w|\.|\'|\"|\[|\])+, " 23 | r"(\w|\.|\'|\"|\[|\])+\)\)") 24 | RE_ASSERT_EQUAL_TYPE = re.compile( 25 | r"(.)*assertEqual\(type\((\w|\.|\'|\"|\[|\])+\), " 26 | r"(\w|\.|\'|\"|\[|\])+\)") 27 | RE_ASSERT_EQUAL_IN_START_WITH_TRUE_OR_FALSE = re.compile( 28 | r"assertEqual\(" 29 | r"(True|False), (\w|[][.'\"])+ in (\w|[][.'\", ])+\)") 30 | RE_ASSERT_RAISES_REGEXP = re.compile(r"assertRaisesRegexp\(") 31 | # NOTE(snikitin): Next two regexes weren't united to one for more readability. 32 | # asse_true_false_with_in_or_not_in regex checks 33 | # assertTrue/False(A in B) cases where B argument has no spaces 34 | # asse_true_false_with_in_or_not_in_spaces regex checks cases 35 | # where B argument has spaces and starts/ends with [, ', ". 36 | # For example: [1, 2, 3], "some string", 'another string'. 37 | # We have to separate these regexes to escape a false positives 38 | # results. B argument should have spaces only if it starts 39 | # with [, ", '. Otherwise checking of string 40 | # "assertFalse(A in B and C in D)" will be false positives. 41 | # In this case B argument is "B and C in D". 42 | RE_ASSERT_TRUE_FALSE_WITH_IN_OR_NOT_IN = re.compile( 43 | r"assert(True|False)\(" 44 | r"(\w|[][.'\"])+( not)? in (\w|[][.'\",])+(, .*)?\)") 45 | RE_ASSERT_TRUE_FALSE_WITH_IN_OR_NOT_IN_SPACES = re.compile( 46 | r"assert(True|False)" 47 | r"\((\w|[][.'\"])+( not)? in [\[|'|\"](\w|[][.'\", ])+" 48 | r"[\[|'|\"](, .*)?\)") 49 | RE_ASSERT_EQUAL_IN_END_WITH_TRUE_OR_FALSE = re.compile( 50 | r"assertEqual\(" 51 | r"(\w|[][.'\"])+ in (\w|[][.'\", ])+, (True|False)\)") 52 | 53 | 54 | @core.flake8ext 55 | def hacking_except_format(logical_line, noqa): 56 | r"""Check for 'except:'. 57 | 58 | OpenStack HACKING guide recommends not using except: 59 | Do not write "except:", use "except Exception:" at the very least 60 | 61 | Okay: try:\n pass\nexcept Exception:\n pass 62 | H201: try:\n pass\nexcept:\n pass 63 | Okay: try:\n pass\nexcept: # noqa\n pass 64 | """ 65 | if noqa: 66 | return 67 | if logical_line.startswith("except:"): 68 | yield 6, "H201: no 'except:' at least use 'except Exception:'" 69 | 70 | 71 | @core.flake8ext 72 | def hacking_except_format_assert(logical_line, noqa): 73 | r"""Check for 'assertRaises(Exception'. 74 | 75 | OpenStack HACKING guide recommends not using assertRaises(Exception...): 76 | Do not use overly broad Exception type 77 | 78 | Okay: self.assertRaises(NovaException, foo) 79 | Okay: self.assertRaises(ExceptionStrangeNotation, foo) 80 | H202: self.assertRaises(Exception, foo) 81 | H202: self.assertRaises(Exception) 82 | Okay: self.assertRaises(Exception) # noqa 83 | Okay: self.assertRaises(Exception, foo) # noqa 84 | """ 85 | if noqa: 86 | return 87 | if RE_ASSERT_RAISES_EXCEPTION.search(logical_line): 88 | yield 1, "H202: assertRaises Exception too broad" 89 | 90 | 91 | def is_none(node): 92 | '''Check whether an AST node corresponds to None.''' 93 | return isinstance(node, ast.Constant) and node.value is None 94 | 95 | 96 | def _get_local_func_name(node): 97 | if isinstance(node.func, ast.Attribute): 98 | return node.func.attr 99 | elif isinstance(node.func, ast.Name): 100 | return node.func.id 101 | else: 102 | return None 103 | 104 | 105 | class NoneArgChecker(ast.NodeVisitor): 106 | '''NodeVisitor to check function calls for None arguments. 107 | 108 | :param func_name: only check calls to functions with this name 109 | :param num_args: number of arguments to check for None 110 | 111 | self.none_found will be True if any None arguments were found. 112 | ''' 113 | 114 | def __init__(self, func_name, num_args=2): 115 | self.func_name = func_name 116 | self.num_args = num_args 117 | self.none_found = False 118 | 119 | def visit_Call(self, node): 120 | local_func_name = _get_local_func_name(node) 121 | 122 | if local_func_name == self.func_name: 123 | args_to_check = node.args[:self.num_args] 124 | self.none_found |= any(is_none(x) for x in args_to_check) 125 | self.generic_visit(node) 126 | 127 | 128 | @core.flake8ext 129 | @core.off_by_default 130 | def hacking_assert_is_none(logical_line, noqa): 131 | """Use assertIs(Not)None to check for None in assertions. 132 | 133 | Okay: self.assertEqual('foo', 'bar') 134 | Okay: self.assertNotEqual('foo', {}.get('bar', None)) 135 | Okay: self.assertIs('foo', 'bar') 136 | Okay: self.assertIsNot('foo', 'bar', None) 137 | Okay: foo(self.assertIsNot('foo', 'bar')) 138 | H203: self.assertEqual(None, 'foo') 139 | H203: self.assertNotEqual('foo', None) 140 | H203: self.assertIs(None, 'foo', 'bar') 141 | H203: self.assertIsNot('foo', None, 'bar') 142 | H203: foo(self.assertIsNot('foo', None, 'bar')) 143 | Okay: self.assertEqual(None, 'foo') # noqa 144 | Okay: self.assertIs(None, 'foo') # noqa 145 | Okay: self.assertIsNone('foo') 146 | """ 147 | if noqa: 148 | return 149 | for func_name in ('assertEqual', 'assertIs', 'assertNotEqual', 150 | 'assertIsNot'): 151 | try: 152 | start = logical_line.index('.%s(' % func_name) + 1 153 | except ValueError: 154 | continue 155 | checker = NoneArgChecker(func_name) 156 | try: 157 | parsed_logical_line = ast.parse(logical_line) 158 | except SyntaxError: 159 | # let flake8 catch this itself 160 | # https://github.com/PyCQA/flake8/issues/1948 161 | continue 162 | checker.visit(parsed_logical_line) 163 | if checker.none_found: 164 | yield start, "H203: Use assertIs(Not)None to check for None" 165 | 166 | 167 | class AssertTrueFalseChecker(ast.NodeVisitor): 168 | '''NodeVisitor to find "assert[True|False](some comparison)" statements. 169 | 170 | :param method_names: methods to look for: assertTrue and/or assertFalse 171 | :param ops: list of comparisons we want to look for (objects from the ast 172 | module) 173 | ''' 174 | 175 | def __init__(self, method_names, ops): 176 | self.method_names = method_names 177 | self.ops = tuple(ops) 178 | self.error = False 179 | 180 | def visit_Call(self, node): 181 | # No need to keep visiting the AST if we already found something. 182 | if self.error: 183 | return 184 | 185 | self.generic_visit(node) 186 | 187 | local_func_name = _get_local_func_name(node) 188 | 189 | if (local_func_name in self.method_names and 190 | len(node.args) == 1 and 191 | isinstance(node.args[0], ast.Compare) and 192 | len(node.args[0].ops) == 1 and 193 | isinstance(node.args[0].ops[0], self.ops)): 194 | self.error = True 195 | 196 | 197 | @core.flake8ext 198 | @core.off_by_default 199 | def hacking_assert_equal(logical_line, noqa): 200 | r"""Check that self.assertEqual and self.assertNotEqual are used. 201 | 202 | Okay: self.assertEqual(x, y) 203 | Okay: self.assertNotEqual(x, y) 204 | H204: self.assertTrue(x == y) 205 | H204: self.assertTrue(x != y) 206 | H204: self.assertFalse(x == y) 207 | H204: self.assertFalse(x != y) 208 | """ 209 | if noqa: 210 | return 211 | 212 | methods = ['assertTrue', 'assertFalse'] 213 | for method in methods: 214 | start = logical_line.find('.%s' % method) + 1 215 | if start != 0: 216 | break 217 | else: 218 | return 219 | comparisons = [ast.Eq, ast.NotEq] 220 | checker = AssertTrueFalseChecker(methods, comparisons) 221 | try: 222 | parsed_logical_line = ast.parse(logical_line) 223 | except SyntaxError: 224 | # let flake8 catch this itself 225 | # https://github.com/PyCQA/flake8/issues/1948 226 | return 227 | checker.visit(parsed_logical_line) 228 | if checker.error: 229 | yield start, 'H204: Use assert(Not)Equal()' 230 | 231 | 232 | @core.flake8ext 233 | @core.off_by_default 234 | def hacking_assert_greater_less(logical_line, noqa): 235 | r"""Check that self.assert{Greater,Less}[Equal] are used. 236 | 237 | Okay: self.assertGreater(x, y) 238 | Okay: self.assertGreaterEqual(x, y) 239 | Okay: self.assertLess(x, y) 240 | Okay: self.assertLessEqual(x, y) 241 | H205: self.assertTrue(x > y) 242 | H205: self.assertTrue(x >= y) 243 | H205: self.assertTrue(x < y) 244 | H205: self.assertTrue(x <= y) 245 | """ 246 | if noqa: 247 | return 248 | 249 | methods = ['assertTrue', 'assertFalse'] 250 | for method in methods: 251 | start = logical_line.find('.%s' % method) + 1 252 | if start != 0: 253 | break 254 | else: 255 | return 256 | comparisons = [ast.Gt, ast.GtE, ast.Lt, ast.LtE] 257 | checker = AssertTrueFalseChecker(methods, comparisons) 258 | try: 259 | parsed_logical_line = ast.parse(logical_line) 260 | except SyntaxError: 261 | # let flake8 catch this itself 262 | # https://github.com/PyCQA/flake8/issues/1948 263 | return 264 | checker.visit(parsed_logical_line) 265 | if checker.error: 266 | yield start, 'H205: Use assert{Greater,Less}[Equal]' 267 | 268 | 269 | @core.flake8ext 270 | def hacking_assert_true_instance(logical_line): 271 | """Check for assertTrue(isinstance(a, b)) sentences 272 | 273 | H211 274 | """ 275 | if RE_ASSERT_TRUE_INST.match(logical_line): 276 | yield ( 277 | 0, 278 | "H211: Use assert{Is,IsNot}instance") 279 | 280 | 281 | @core.flake8ext 282 | def hacking_assert_equal_type(logical_line): 283 | """Check for assertEqual(type(A), B) sentences 284 | 285 | H212 286 | """ 287 | if RE_ASSERT_EQUAL_TYPE.match(logical_line): 288 | yield ( 289 | 0, 290 | "H211: Use assert{Is,IsNot}instance") 291 | 292 | 293 | @core.flake8ext 294 | def hacking_assert_raises_regexp(logical_line): 295 | """Check for usage of deprecated assertRaisesRegexp 296 | 297 | H213 298 | """ 299 | res = RE_ASSERT_RAISES_REGEXP.search(logical_line) 300 | if res: 301 | yield ( 302 | 0, 303 | "H213: assertRaisesRegex must be used instead " 304 | "of assertRaisesRegexp") 305 | 306 | 307 | @core.flake8ext 308 | def hacking_assert_true_or_false_with_in(logical_line): 309 | """Check for assertTrue/False(A in B), assertTrue/False(A not in B), 310 | 311 | assertTrue/False(A in B, message) or assertTrue/False(A not in B, message) 312 | sentences. 313 | H214 314 | """ 315 | res = (RE_ASSERT_TRUE_FALSE_WITH_IN_OR_NOT_IN.search(logical_line) 316 | or RE_ASSERT_TRUE_FALSE_WITH_IN_OR_NOT_IN_SPACES.search 317 | (logical_line)) 318 | if res: 319 | yield ( 320 | 0, 321 | "H214: Use assertIn/NotIn(A, B) rather than " 322 | "assertTrue/False(A in/not in B) when checking collection " 323 | "contents.") 324 | 325 | 326 | @core.flake8ext 327 | def hacking_assert_equal_in(logical_line): 328 | """Check for assertEqual(A in B, True), assertEqual(True, A in B), 329 | 330 | assertEqual(A in B, False) or assertEqual(False, A in B) sentences 331 | 332 | H215 333 | """ 334 | res = (RE_ASSERT_EQUAL_IN_START_WITH_TRUE_OR_FALSE.search(logical_line) or 335 | RE_ASSERT_EQUAL_IN_END_WITH_TRUE_OR_FALSE.search(logical_line)) 336 | if res: 337 | yield ( 338 | 0, 339 | "H215: Use assertIn/NotIn(A, B) rather than " 340 | "assertEqual(A in B, True/False) when checking collection " 341 | "contents.") 342 | -------------------------------------------------------------------------------- /HACKING.rst: -------------------------------------------------------------------------------- 1 | .. _StyleGuide: 2 | 3 | OpenStack Style Guidelines 4 | ========================== 5 | 6 | OpenStack has a set of style guidelines for clarity. OpenStack is a 7 | very large code base (over 1 Million lines of python), spanning dozens 8 | of git trees, with over a thousand developers contributing every 12 9 | months. As such common style helps developers understand code in 10 | reviews, move between projects smoothly, and overall make the code 11 | more maintainable. 12 | 13 | 14 | Step 0 15 | ------ 16 | 17 | - Step 1: Read `pep8`_ 18 | - Step 2: Read `pep8`_ again 19 | - Step 3: Read on 20 | 21 | .. _`pep8`: http://www.python.org/dev/peps/pep-0008/ 22 | 23 | General 24 | ------- 25 | - [H903] Use only UNIX style newlines (``\n``), not Windows style (``\r\n``) 26 | - It is preferred to wrap long lines in parentheses and not a backslash 27 | for line continuation. 28 | - [H201] Do not write ``except:``, use ``except Exception:`` at the very least. 29 | When catching an exception you should be as specific so you don't mistakenly 30 | catch unexpected exceptions. 31 | - [H101] Include your name with TODOs as in ``# TODO(yourname)``. This makes 32 | it easier to find out who the author of the comment was. 33 | - [H105] Don't use author tags. We use version control instead. 34 | - [H106] Don't put vim configuration in source files (off by default). 35 | - [H904] Delay string interpolations at logging calls (off by default). 36 | - [H905] Do not import eventlet (off by default). Eventlet usage is being 37 | removed from OpenStack projects as part of the async migration effort. 38 | Use threading or asyncio instead. 39 | - Do not shadow a built-in or reserved word. Shadowing built -in or reserved 40 | words makes the code harder to understand. Example:: 41 | 42 | def list(): 43 | return [1, 2, 3] 44 | 45 | mylist = list() # BAD, shadows `list` built-in 46 | 47 | class Foo(object): 48 | def list(self): 49 | return [1, 2, 3] 50 | 51 | mylist = Foo().list() # OKAY, does not shadow built-in 52 | 53 | - [H905] Log.warn is deprecated. Enforce use of LOG.warning. 54 | 55 | Imports 56 | ------- 57 | 58 | - Do not import objects, only modules (*) 59 | - [H301] Do not import more than one module per line (*) 60 | - [H303] Do not use wildcard ``*`` import (*) 61 | - [H304] Do not make relative imports 62 | - [H306] Alphabetically order your imports by the full module path. 63 | Organize your imports according to the `Import order 64 | template`_ and `Real-world Import Order Examples`_ below. 65 | For the purposes of import order, OpenStack projects other than the 66 | one to which the file belongs are considered "third party". Only 67 | imports from the same Git repo are considered "project imports" 68 | 69 | (*) exceptions are: 70 | 71 | - imports from ``migrate`` package 72 | - imports from ``sqlalchemy`` package 73 | - function imports from ``i18n`` module 74 | 75 | Import order template 76 | ^^^^^^^^^^^^^^^^^^^^^ 77 | 78 | :: 79 | 80 | {{stdlib imports in human alphabetical order}} 81 | \n 82 | {{third-party lib imports in human alphabetical order}} 83 | \n 84 | {{project imports in human alphabetical order}} 85 | \n 86 | \n 87 | {{begin your code}} 88 | 89 | Real-world Import Order Examples 90 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 91 | Example:: 92 | 93 | import asyncio 94 | import httplib 95 | import logging 96 | import random 97 | import StringIO 98 | import threading 99 | import time 100 | import unittest 101 | 102 | import webob.exc 103 | 104 | import nova.api.ec2 105 | from nova.api import manager 106 | from nova.api import openstack 107 | from nova.auth import users 108 | from nova.endpoint import cloud 109 | import nova.flags 110 | from nova.i18n import _, _LC 111 | from nova import test 112 | 113 | 114 | Docstrings 115 | ---------- 116 | - [H401] Docstrings should not start with a space. 117 | - [H403] Multi line docstrings should end on a new line. 118 | - [H404] Multi line docstrings should start without a leading new line. 119 | - [H405] Multi line docstrings should start with a one line summary followed 120 | by an empty line. 121 | 122 | Example:: 123 | 124 | """A multi line docstring has a one-line summary, less than 80 characters. 125 | 126 | Then a new paragraph after a newline that explains in more detail any 127 | general information about the function, class or method. Example usages 128 | are also great to have here if it is a complex class or function. 129 | 130 | When writing the docstring for a class, an extra line should be placed 131 | after the closing quotations. For more in-depth explanations for these 132 | decisions see http://www.python.org/dev/peps/pep-0257/ 133 | 134 | If you are going to describe parameters and return values, use Sphinx, the 135 | appropriate syntax is as follows. 136 | 137 | :param foo: the foo parameter 138 | :param bar: the bar parameter 139 | :returns: return_type -- description of the return value 140 | :returns: description of the return value 141 | :raises: AttributeError, KeyError 142 | """ 143 | 144 | 145 | Dictionaries/Lists 146 | ------------------ 147 | If a dictionary (dict) or list object is longer than 80 characters, its items 148 | should be split with newlines. Embedded iterables should have their items 149 | indented. Additionally, the last item in the dictionary should have a trailing 150 | comma. This increases readability and simplifies future diffs. 151 | 152 | Example:: 153 | 154 | my_dictionary = { 155 | "image": { 156 | "name": "Just a Snapshot", 157 | "size": 2749573, 158 | "properties": { 159 | "user_id": 12, 160 | "arch": "x86_64", 161 | }, 162 | "things": [ 163 | "thing_one", 164 | "thing_two", 165 | ], 166 | "status": "ACTIVE", 167 | }, 168 | } 169 | 170 | 171 | - [H501] Do not use ``locals()`` or ``self.__dict__`` for formatting strings, 172 | it is not clear as using explicit dictionaries and can hide errors during 173 | refactoring. 174 | 175 | Calling Methods 176 | --------------- 177 | Calls to methods 80 characters or longer should format each argument with 178 | newlines. This is not a requirement, but a guideline:: 179 | 180 | unnecessarily_long_function_name('string one', 181 | 'string two', 182 | kwarg1=constants.ACTIVE, 183 | kwarg2=['a', 'b', 'c']) 184 | 185 | 186 | Rather than constructing parameters inline, it is better to break things up:: 187 | 188 | list_of_strings = [ 189 | 'what_a_long_string', 190 | 'not as long', 191 | ] 192 | 193 | dict_of_numbers = { 194 | 'one': 1, 195 | 'two': 2, 196 | 'twenty four': 24, 197 | } 198 | 199 | object_one.call_a_method('string three', 200 | 'string four', 201 | kwarg1=list_of_strings, 202 | kwarg2=dict_of_numbers) 203 | 204 | 205 | Internationalization (i18n) Strings 206 | ----------------------------------- 207 | In order to support multiple languages, we have a mechanism to support 208 | automatic translations of exception and log strings. 209 | 210 | Example:: 211 | 212 | msg = _("An error occurred") 213 | raise HTTPBadRequest(explanation=msg) 214 | 215 | - [H702] If you have a variable to place within the string, first 216 | internationalize the template string then do the replacement. 217 | 218 | Example:: 219 | 220 | msg = _LE("Missing parameter: %s") 221 | LOG.error(msg, "flavor") 222 | 223 | - [H703] If you have multiple variables to place in the string, use keyword 224 | parameters. This helps our translators reorder parameters when needed. 225 | 226 | Example:: 227 | 228 | msg = _LE("The server with id %(s_id)s has no key %(m_key)s") 229 | LOG.error(msg, {"s_id": "1234", "m_key": "imageId"}) 230 | 231 | .. seealso:: 232 | 233 | * `oslo.i18n Guidelines `__ 234 | 235 | Creating Unit Tests 236 | ------------------- 237 | For every new feature, unit tests should be created that both test and 238 | (implicitly) document the usage of said feature. If submitting a patch for a 239 | bug that had no unit test, a new passing unit test should be added. If a 240 | submitted bug fix does have a unit test, be sure to add a new one that fails 241 | without the patch and passes with the patch. 242 | 243 | Unit Tests and assertRaises 244 | --------------------------- 245 | 246 | A properly written test asserts that particular behavior occurs. This can 247 | be a success condition or a failure condition, including an exception. 248 | When asserting that a particular exception is raised, the most specific 249 | exception possible should be used. 250 | 251 | - [H202] Testing for ``Exception`` being raised is almost always a 252 | mistake since it will match (almost) every exception, even those 253 | unrelated to the exception intended to be tested. 254 | 255 | This applies to catching exceptions manually with a try/except block, 256 | or using ``assertRaises()``. 257 | 258 | Example:: 259 | 260 | with self.assertRaises(exception.InstanceNotFound): 261 | db.instance_get_by_uuid(elevated, instance_uuid) 262 | 263 | - [H203] Use assertIs(Not)None to check for None (off by default) 264 | Unit test assertions tend to give better messages for more specific 265 | assertions. As a result, ``assertIsNone(...)`` is preferred over 266 | ``assertEqual(None, ...)`` and ``assertIs(None, ...)``, and 267 | ``assertIsNotNone(...)`` is preferred over ``assertNotEqual(None, ...)`` 268 | and ``assertIsNot(None, ...)``. Off by default. 269 | 270 | - [H204] Use assert(Not)Equal to check for equality. 271 | Unit test assertions tend to give better messages for more specific 272 | assertions. As a result, ``assertEqual(...)`` is preferred over 273 | ``assertTrue(... == ...)``, and ``assertNotEqual(...)`` is preferred over 274 | ``assertFalse(... == ...)``. Off by default. 275 | 276 | - [H205] Use assert(Greater|Less)(Equal) for comparison. 277 | Unit test assertions tend to give better messages for more specific 278 | assertions. As a result, ``assertGreater(Equal)(...)`` is preferred over 279 | ``assertTrue(... >(=) ...)``, and ``assertLess(Equal)(...)`` is preferred over 280 | ``assertTrue(... <(=) ...)``. Off by default. 281 | 282 | - [H210] Require ``autospec``, ``spec``, or ``spec_set`` in ``mock.patch()`` or 283 | ``mock.patch.object()`` calls (off by default) 284 | 285 | Users of ``mock.patch()`` or ``mock.patch.object()`` may think they are doing 286 | a correct assertion for example:: 287 | 288 | my_mock_obj.called_once_with() 289 | 290 | When the correct call is:: 291 | 292 | my_mock_obj.assert_called_once_with() 293 | 294 | By using ``autospec=True`` those kind of errors can be caught. This test does 295 | not force them to use ``autospec=True``, but requires that they define some 296 | value for ``autospec``, ``spec``, or ``spec_set``. It could be 297 | ``autospec=False``. We just want them to make a conscious decision on using 298 | or not using ``autospec``. If any of the following are used then ``autospec`` 299 | will not be required: ``new``, ``new_callable``, ``spec``, ``spec_set``, 300 | ``wraps`` 301 | 302 | - [H211] Change assertTrue(isinstance(A, B)) by optimal assert like 303 | assertIsInstance(A, B). 304 | 305 | - [H212] Change assertEqual(type(A), B) by optimal assert like 306 | assertIsInstance(A, B) 307 | 308 | - [H213] Check for usage of deprecated assertRaisesRegexp 309 | 310 | - [H214] Change assertTrue/False(A in/not in B, message) to the more 311 | specific assertIn/NotIn(A, B, message) 312 | 313 | - [H215] Change assertEqual(A in B, True), assertEqual(True, A in B), 314 | assertEqual(A in B, False) or assertEqual(False, A in B) to the more 315 | specific assertIn/NotIn(A, B) 316 | 317 | - [H216] Make sure unittest.mock is used instead of the third party mock 318 | library. On by default. 319 | 320 | Starting with Python 3.3 and later, the mock module was added under unittest. 321 | Previously, this functionality was only available by using the third party 322 | ``mock`` library. 323 | 324 | Most users are not aware of this subtle distinction. This results in issues 325 | where the project does not declare the ``mock`` library in its requirements 326 | file, but the code does an ``import mock`` assuming that the module is 327 | present. This may work initially if one of the project's dependencies ends up 328 | pulling that dependency in indirectly, but then can cause things to suddenly 329 | break if that transitive dependency goes away. 330 | 331 | Since this third party library usage is done without being aware of it, this 332 | check is enabled by default to make sure those projects that actually do 333 | intend to use the ``mock`` library are doing so explicitly. 334 | 335 | OpenStack Trademark 336 | ------------------- 337 | 338 | OpenStack is a registered trademark of the OpenStack Foundation, and uses the 339 | following capitalization:: 340 | 341 | OpenStack 342 | 343 | 344 | OpenStack Licensing 345 | ------------------- 346 | 347 | - [H102 H103] Newly contributed Source Code should be licensed under the 348 | Apache 2.0 license. All source files should have the following header:: 349 | 350 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 351 | # not use this file except in compliance with the License. You may obtain 352 | # a copy of the License at 353 | # 354 | # http://www.apache.org/licenses/LICENSE-2.0 355 | # 356 | # Unless required by applicable law or agreed to in writing, software 357 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 358 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 359 | # License for the specific language governing permissions and limitations 360 | # under the License. 361 | 362 | Alternately also check for the 363 | `SPDX license header `__ for Apache 2.0:: 364 | 365 | # SPDX-License-Identifier: Apache-2.0 366 | 367 | - [H104] Files with no code shouldn't contain any license header nor comments, 368 | and must be left completely empty. 369 | 370 | Commit Messages 371 | --------------- 372 | Using a common format for commit messages will help keep our git history 373 | readable. 374 | 375 | For further information on constructing high quality commit messages, 376 | and how to split up commits into a series of changes, consult the 377 | project wiki: 378 | https://wiki.openstack.org/GitCommitMessages 379 | --------------------------------------------------------------------------------