diff --git a/docs/usage.rst b/docs/usage.rst index 7cc77adc..92563de9 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -97,6 +97,26 @@ Possible values are ``verylow``, ``low``, ``medium``, ``high``, ``veryhigh``. Prospector does not include documentation warnings by default, but you can turn this on using the ``--doc-warnings`` flag. +Configuration files +''''''''''''''''''' + +The options can also be provided by a configuration file that can have one of the following names: + +- ``~/.prospectorrc`` +- ``/.prospectorrc`` +- ``.prospectorrc`` +- ``setup.cfg`` +- ``tox.ini`` +- ``pyproject.toml`` + +The configuration file should be in the ``ini`` format. except for the ``pyproject.toml`` which +should be in the ``toml`` format. + +The ``prospectorrc`` files can also be suffixed with ``.toml``, ``.yaml`` or ``.yml`` +to specify the format of the file. + +Notes that the YAML files should not provide the ``prospector`` global section, and the ``pyproject.toml`` +should not provide the ``tool.prospector`` section. .. _full_options: diff --git a/poetry.lock b/poetry.lock index f3b146b8..7b7e66f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "astroid" version = "3.3.5" @@ -1059,6 +1070,130 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pydocstyle" version = "6.3.0" @@ -1116,8 +1251,8 @@ files = [ astroid = ">=3.3.4,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" @@ -1522,20 +1657,6 @@ files = [ {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, ] -[[package]] -name = "setoptconf-tmp" -version = "0.3.1" -description = "A module for retrieving program settings from various sources in a consistant method." -optional = false -python-versions = "*" -files = [ - {file = "setoptconf-tmp-0.3.1.tar.gz", hash = "sha256:e0480addd11347ba52f762f3c4d8afa3e10ad0affbc53e3ffddc0ca5f27d5778"}, - {file = "setoptconf_tmp-0.3.1-py3-none-any.whl", hash = "sha256:76035d5cd1593d38b9056ae12d460eca3aaa34ad05c315b69145e138ba80a745"}, -] - -[package.extras] -yaml = ["pyyaml"] - [[package]] name = "setuptools" version = "75.1.0" @@ -1826,4 +1947,4 @@ with-vulture = ["vulture"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "de3329503e452b1ca74073fa9893ebfe6cb6812c92c6d7ace32fb5e46652df4e" +content-hash = "616980bab1adcf566291ac003d2f962bccbe9ba95b7b46efddbdd74051fbdb89" diff --git a/prospector/config/__init__.py b/prospector/config/__init__.py index f47386ab..1a47512f 100644 --- a/prospector/config/__init__.py +++ b/prospector/config/__init__.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import Any, Callable, Optional, Union -import setoptconf.config - from prospector.finder import FileFinder try: # Python >= 3.11 @@ -33,15 +31,15 @@ class ProspectorConfig: # is a config object and its sole purpose is to hold many properties! def __init__(self, workdir: Optional[Path] = None): - self.config, self.arguments = self._configure_prospector() - self.paths = self._get_work_path(self.config, self.arguments) + self.config = self._configure_prospector() + self.paths = self._get_work_path() self.explicit_file_mode = all(p.is_file for p in self.paths) self.workdir = workdir or Path.cwd() - self.profile, self.strictness = self._get_profile(self.workdir, self.config) - self.libraries = self._find_used_libraries(self.config, self.profile) - self.tools_to_run = self._determine_tool_runners(self.config, self.profile) - self.ignores = self._determine_ignores(self.config, self.profile, self.libraries) + self.profile, self.strictness = self._get_profile() + self.libraries = self._find_used_libraries() + self.tools_to_run = self._determine_tool_runners() + self.ignores = self._determine_ignores() self.configured_by: dict[str, Optional[Union[str, Path]]] = {} self.messages: list[Message] = [] @@ -112,28 +110,22 @@ def get_output_report(self) -> list[tuple[str, list[str]]]: return output_report - def _configure_prospector(self) -> tuple[setoptconf.config.Configuration, dict[str, str]]: - # first we will configure prospector as a whole - mgr = cfg.build_manager() - config = mgr.retrieve(*cfg.build_default_sources()) - return config, mgr.arguments + def _configure_prospector(self) -> cfg.ProspectorConfiguration: + # First we will configure prospector as a whole + return cfg.get_config() - def _get_work_path(self, config: setoptconf.config.Configuration, arguments: dict[str, str]) -> list[Path]: + def _get_work_path(self) -> list[Path]: # Figure out what paths we're prospecting - if config["path"]: - paths = [Path(self.config["path"])] - elif arguments["checkpath"]: - paths = [Path(p) for p in arguments["checkpath"]] + if self.config.path: + paths = [Path(self.config.path)] else: paths = [Path.cwd()] return [p.resolve() for p in paths] - def _get_profile( - self, workdir: Path, config: setoptconf.config.Configuration - ) -> tuple[ProspectorProfile, Optional[str]]: + def _get_profile(self) -> tuple[ProspectorProfile, cfg.Strictness]: # Use the specified profiles profile_provided = False - if len(config.profiles) > 0: + if len(self.config.profiles) > 0: profile_provided = True cmdline_implicit = [] @@ -142,55 +134,55 @@ def _get_profile( profile_name: Union[None, str, Path] = None if not profile_provided: for possible_profile in AUTO_LOADED_PROFILES: - prospector_yaml = os.path.join(workdir, possible_profile) + prospector_yaml = os.path.join(self.workdir, possible_profile) if os.path.exists(prospector_yaml) and os.path.isfile(prospector_yaml): profile_provided = True profile_name = possible_profile break - strictness = None + strictness = cfg.Strictness.none if profile_provided: if profile_name is None: - profile_name = config.profiles[0] - extra_profiles = config.profiles[1:] + profile_name = self.config.profiles[0] + extra_profiles = self.config.profiles[1:] else: - extra_profiles = config.profiles + extra_profiles = self.config.profiles - strictness = "from profile" + strictness = cfg.Strictness.from_profile else: # Use the preconfigured prospector profiles profile_name = "default" extra_profiles = [] - if config.doc_warnings is not None and config.doc_warnings: + if self.config.doc_warnings is not None and self.config.doc_warnings: cmdline_implicit.append("doc_warnings") - if config.test_warnings is not None and config.test_warnings: + if self.config.test_warnings is not None and self.config.test_warnings: cmdline_implicit.append("test_warnings") - if config.no_style_warnings is not None and config.no_style_warnings: + if self.config.no_style_warnings is not None and self.config.no_style_warnings: cmdline_implicit.append("no_pep8") - if config.full_pep8 is not None and config.full_pep8: + if self.config.full_pep8 is not None and self.config.full_pep8: cmdline_implicit.append("full_pep8") - if config.member_warnings is not None and config.member_warnings: + if self.config.member_warnings is not None and self.config.member_warnings: cmdline_implicit.append("member_warnings") # Use the strictness profile only if no profile has been given - if config.strictness is not None and config.strictness: - cmdline_implicit.append(f"strictness_{config.strictness}") - strictness = config.strictness + if self.config.strictness is not None: + cmdline_implicit.append(f"strictness_{self.config.strictness.value}") + strictness = self.config.strictness # the profile path is # * anything provided as an argument # * a directory called .prospector in the check path # * the check path # * prospector provided profiles - profile_path = [Path(path).absolute() for path in config.profile_path] + profile_path = [Path(path).absolute() for path in self.config.profile_path] - prospector_dir = workdir / ".prospector" + prospector_dir = self.workdir / ".prospector" if os.path.exists(prospector_dir) and os.path.isdir(prospector_dir): profile_path.append(prospector_dir) - profile_path.append(workdir) + profile_path.append(self.workdir) profile_path.append(BUILTIN_PROFILE_PATH) try: @@ -210,7 +202,7 @@ def _get_profile( sys.exit(1) except ProfileNotFound as nfe: search_path = ":".join(map(str, nfe.profile_path)) - module_name = nfe.name.split(":")[0] + module_name = str(nfe.name).split(":", maxsplit=1)[0] sys.stderr.write( f"""Failed to run: Could not find profile {nfe.name}. @@ -221,60 +213,64 @@ def _get_profile( else: return profile, strictness - def _find_used_libraries(self, config: setoptconf.config.Configuration, profile: ProspectorProfile) -> list[str]: + def _find_used_libraries(self) -> list[str]: libraries = [] # Bring in adaptors that we automatically detect are needed - if config.autodetect and profile.autodetect is True: + if self.config.autodetect and self.profile.autodetect is True: for found_dep in autodetect_libraries(self.workdir): libraries.append(found_dep) # Bring in adaptors for the specified libraries - for name in set(config.uses + profile.uses): + for name in set(self.config.uses + self.profile.uses): if name not in libraries: libraries.append(name) return libraries - def _determine_tool_runners(self, config: setoptconf.config.Configuration, profile: ProspectorProfile) -> list[str]: - if config.tools is None: + def _determine_tool_runners(self) -> list[str]: + if self.config.tools is None: # we had no command line settings for an explicit list of # tools, so we use the defaults - to_run = set(DEFAULT_TOOLS) + to_run: set[str] = set(DEFAULT_TOOLS) # we can also use any that the profiles dictate for tool in tools.TOOLS: - if profile.is_tool_enabled(tool): + if self.profile.is_tool_enabled(tool): to_run.add(tool) else: - to_run = set(config.tools) + to_run = set(self.config.tools) # profiles have no say in the list of tools run when # a command line is specified - for tool in config.with_tools: - to_run.add(tool) + for tool_name in self.config.with_tools: + to_run.add(tool_name) - for tool in config.without_tools: + for tool_name in self.config.without_tools: + tool = tool_name if tool in to_run: to_run.remove(tool) # if config.tools is None and len(config.with_tools) == 0 and len(config.without_tools) == 0: for tool in tools.TOOLS: - enabled = profile.is_tool_enabled(tool) + enabled = self.profile.is_tool_enabled(tool) if enabled is None: enabled = tool in DEFAULT_TOOLS - if tool in to_run and not enabled and tool not in config.with_tools and tool not in (config.tools or []): + if ( + tool in to_run + and not enabled + and tool not in self.config.with_tools + and tool not in (self.config.tools or []) + ): # if this is not enabled in a profile but is asked for in a command line arg, we keep it, otherwise # remove it from the list to run to_run.remove(tool) return sorted(list(to_run)) - def _determine_ignores( - self, config: setoptconf.config.Configuration, profile: ProspectorProfile, libraries: list[str] - ) -> list[re.Pattern[str]]: + def _determine_ignores(self) -> list[re.Pattern[str]]: # Grab ignore patterns from the options ignores = [] - for pattern in config.ignore_patterns + profile.ignore_patterns: + for pattern in self.config.ignore_patterns + self.profile.ignore_patterns: if pattern is None: # this can happen if someone has a profile with an empty ignore-patterns value, eg: # @@ -286,22 +282,22 @@ def _determine_ignores( # Convert ignore paths into patterns boundary = r"(^|/|\\)%s(/|\\|$)" - for ignore_path in config.ignore_paths + profile.ignore_paths: + for ignore_path in self.config.ignore_paths + self.profile.ignore_paths: ignore_path = str(ignore_path) if ignore_path.endswith("/") or ignore_path.endswith("\\"): ignore_path = ignore_path[:-1] ignores.append(re.compile(boundary % re.escape(ignore_path))) # some libraries have further automatic ignores - if "django" in libraries: + if "django" in self.libraries: ignores += [re.compile("(^|/)(south_)?migrations(/|$)")] return ignores - def get_summary_information(self) -> dict[str, Any]: + def get_summary_information(self) -> dict[str, Union[str, int, list[str]]]: return { "libraries": self.libraries, - "strictness": self.strictness, + "strictness": self.strictness.value, "profiles": ", ".join(self.profile.list_profiles()), "tools": self.tools_to_run, } @@ -351,7 +347,7 @@ def absolute_paths(self) -> bool: return self.config.absolute_paths @property - def max_line_length(self) -> int: + def max_line_length(self) -> Optional[int]: return self.config.max_line_length @property diff --git a/prospector/config/configuration.py b/prospector/config/configuration.py index fe588ec3..d90dc9ce 100644 --- a/prospector/config/configuration.py +++ b/prospector/config/configuration.py @@ -1,327 +1,371 @@ -import importlib.metadata -from typing import Optional +import argparse +import configparser +import os +import sys +from enum import Enum +from typing import Any, List, Optional, Union, get_args, get_origin -import setoptconf as soc +import toml # type: ignore[import-untyped] +import yaml +from pydantic import BaseModel +from pydantic.fields import FieldInfo -from prospector.config.datatype import OutputChoice +from prospector.config.datatype import parse_output_format from prospector.formatters import FORMATTERS from prospector.tools import DEFAULT_TOOLS, TOOLS -__all__ = ("build_manager",) -_VERSION = importlib.metadata.version("prospector") +class Strictness(Enum): + veryhigh = "veryhigh" + high = "high" + medium = "medium" + low = "low" + verylow = "verylow" + none = "none" + from_profile = "from profile" -def build_manager() -> soc.ConfigurationManager: - manager = soc.ConfigurationManager("prospector") +class ProspectorConfiguration(BaseModel): + """ + The configuration for Prospector. + """ - manager.add(soc.BooleanSetting("zero_exit", default=False)) + zero_exit: bool = False + autodetect: bool = True + uses: list[str] = [] + blending: bool = True + doc_warnings: Optional[bool] = None + test_warnings: Optional[bool] = None + no_style_warnings: Optional[bool] = None + member_warnings: Optional[bool] = None + full_pep8: Optional[bool] = None + max_line_length: Optional[int] = None + messages_only: bool = False + summary_only: bool = False + quiet: bool = False + output_format: Optional[list[tuple[str, list[str]]]] = None + absolute_paths: bool = False + tools: Optional[list[str]] = None + with_tools: list[str] = [] + without_tools: list[str] = [] + profiles: list[str] = [] + profile_path: list[str] = [] + strictness: Optional[Strictness] = None + show_profile: bool = False + no_external_config: bool = False + legacy_tool_names: bool = False + pylint_config_file: Optional[str] = None + path: Optional[str] = None + ignore_patterns: list[str] = [] + ignore_paths: list[str] = [] + die_on_tool_error: bool = False + include_tool_stdout: bool = False + direct_tool_stdout: bool = False - manager.add(soc.BooleanSetting("autodetect", default=True)) - manager.add(soc.ListSetting("uses", soc.String, default=[])) - manager.add(soc.BooleanSetting("blending", default=True)) +def _parse_value( + name: str, value_str: str, conf: FieldInfo +) -> Optional[Union[bool, int, float, str, list[bool], list[int], list[float], list[str]]]: + value: Any = None - manager.add(soc.BooleanSetting("doc_warnings", default=None)) - manager.add(soc.BooleanSetting("test_warnings", default=None)) - manager.add(soc.BooleanSetting("no_style_warnings", default=None)) - manager.add(soc.BooleanSetting("member_warnings", default=None)) - manager.add(soc.BooleanSetting("full_pep8", default=None)) - manager.add(soc.IntegerSetting("max_line_length", default=None)) + type_ = conf.annotation + origin = get_origin(type_) + if origin is Union: + args = get_args(type_) + if args[1] is type(None): + type_ = args[0] + origin = get_origin(type_) - manager.add(soc.BooleanSetting("messages_only", default=False)) - manager.add(soc.BooleanSetting("summary_only", default=False)) - manager.add(soc.BooleanSetting("quiet", default=False)) - manager.add( - soc.ListSetting( - "output_format", - OutputChoice(sorted(FORMATTERS.keys())), - default=None, - ) - ) - manager.add(soc.BooleanSetting("absolute_paths", default=False)) - - manager.add( - soc.ListSetting( - "tools", - soc.Choice(sorted(TOOLS.keys())), - default=None, - ) - ) - manager.add(soc.ListSetting("with_tools", soc.String, default=[])) - manager.add(soc.ListSetting("without_tools", soc.String, default=[])) - manager.add(soc.ListSetting("profiles", soc.String, default=[])) - manager.add(soc.ListSetting("profile_path", soc.String, default=[])) - manager.add( - soc.ChoiceSetting( - "strictness", - ["veryhigh", "high", "medium", "low", "verylow"], - default=None, - ) - ) - manager.add(soc.BooleanSetting("show_profile", default=False)) - - manager.add(soc.BooleanSetting("no_external_config", default=False)) - manager.add(soc.BooleanSetting("legacy_tool_names", default=False)) - manager.add(soc.StringSetting("pylint_config_file", default=None)) + if origin is None: + if type_ is bool: + value = value_str.lower() in ("1", "true", "yes") + elif type_ is int: + value = int(value_str) + elif type_ is float: + value = float(value_str) + elif type_ is str: + value = value_str + else: + # Enum + value = type_(value_str) + else: + if origin == list: + sub_type = get_args(type_)[0] + if sub_type is str: + value = value_str.split(",") + elif sub_type is bool: + value = [x.lower() in ("1", "true", "yes") for x in value_str.split(",")] + elif sub_type is int: + value = [int(x) for x in value_str.split(",")] + elif sub_type is float: + value = [float(x) for x in value_str.split(",")] + else: + # Enum + value = [sub_type(x) for x in value_str.split(",")] - manager.add(soc.StringSetting("path", default=None)) + if value is None: + print(f"Could not parse {name}={value_str}") + return value - manager.add(soc.ListSetting("ignore_patterns", soc.String, default=[])) - manager.add(soc.ListSetting("ignore_paths", soc.String, default=[])) - manager.add(soc.BooleanSetting("die_on_tool_error", default=False)) - manager.add(soc.BooleanSetting("include_tool_stdout", default=False)) - manager.add(soc.BooleanSetting("direct_tool_stdout", default=False)) +def _get_prospector_rc_config(base_path: str) -> list[tuple[list[str], str]]: + return [ + (["prospector"], base_path), + (["prospector"], f"{base_path}.toml"), + ([], f"{base_path}.yaml"), + ([], f"{base_path}.yml"), + ] - return manager +def get_config() -> ProspectorConfiguration: + config = {} -def build_default_sources() -> list[soc.Source]: - sources = [ - build_command_line_source(), - soc.EnvironmentVariableSource(), - soc.ConfigFileSource( - ( + for keys, config_filename in ( + *_get_prospector_rc_config(os.path.expanduser(os.path.join("~", ".prospectorrc"))), + *_get_prospector_rc_config( + os.path.join( + os.getenv("XDG_CONFIG_HOME") or os.path.expanduser(os.path.join("~", ".config")), ".prospectorrc", - "setup.cfg", - "tox.ini", ) ), - soc.ConfigFileSource( - ( - soc.ConfigDirectory(".prospectorrc"), - soc.HomeDirectory(".prospectorrc"), - ) - ), - ] + *_get_prospector_rc_config(".prospectorrc"), + (["prospector"], "setup.cfg"), + (["prospector"], "tox.ini"), + (["tool", "prospector"], "pyproject.toml"), + ): + if os.path.exists(config_filename): + with open(config_filename) as f: + if config_filename.endswith(".toml"): + new_config = toml.load(f) + elif config_filename.endswith(".yaml") or config_filename.endswith(".yml"): + new_config = yaml.safe_load(f) + else: + config_parser = configparser.ConfigParser() + config_parser.read_file(f) + new_config = {keys[0]: config_parser[keys[0]]} if keys[0] in config_parser else {} + for key in keys: + new_config = new_config.get(key, {}) + config.update(new_config) - return sources + for name, conf in ProspectorConfiguration.model_fields.items(): + env_name = f"PROSPECTOR_{name.upper()}" + if env_name in os.environ: + value_str = os.environ[env_name] + value = _parse_value(name, value_str, conf) + if value is not None: + config[name] = value + args = build_command_line_parser().parse_args() + for name, conf in ProspectorConfiguration.model_fields.items(): + if hasattr(args, name): + value = getattr(args, name) + if value is not None: + config[name] = value -def build_command_line_source( - prog: Optional[str] = None, description: Optional[str] = "Performs static analysis of Python code" -) -> soc.CommandLineSource: - parser_options = {} - if prog is not None: - parser_options["prog"] = prog - if description is not None: - parser_options["description"] = description + try: + if isinstance(config.get("output_format"), str): + config["output_format"] = [parse_output_format(config["output_format"])] + if isinstance(config.get("output_format"), list): + for of, index in enumerate(config["output_format"]): + if isinstance(index, str): + config["output_format"][of] = parse_output_format(index) + ProspectorConfiguration.model_validate(config) + except ValueError as e: + print() + print(e) + sys.exit(1) + return ProspectorConfiguration(**config) - options = { - "zero_exit": { - "flags": ["-0", "--zero-exit"], - "help": "Prospector will exit with a code of 1 (one) if any messages" - " are found. This makes automation easier; if there are any" - " problems at all, the exit code is non-zero. However this behaviour" - " is not always desirable, so if this flag is set, prospector will" - " exit with a code of 0 if it ran successfully, and non-zero if" - " it failed to run.", - }, - "autodetect": { - "flags": ["-A", "--no-autodetect"], - "help": "Turn off auto-detection of frameworks and libraries used." - " By default, autodetection will be used. To specify" - " manually, see the --uses option.", - }, - "uses": { - "flags": ["-u", "--uses"], - "help": "A list of one or more libraries or frameworks that the" - " project uses. Possible values are: django, celery, flask. This will be" - " autodetected by default, but if autodetection doesn't" - " work, manually specify them using this flag.", - }, - "blending": { - "flags": ["-B", "--no-blending"], - "help": "Turn off blending of messages. Prospector will merge" - " together messages from different tools if they represent" - " the same error. Use this option to see all unmerged" - " messages.", - }, - "doc_warnings": { - "flags": ["-D", "--doc-warnings"], - "help": "Include warnings about documentation.", - }, - "test_warnings": { - "flags": ["-T", "--test-warnings"], - "help": "Also check test modules and packages.", - }, - "legacy_tool_names": { - "flags": ["--legacy-tool-names"], - "help": "Output deprecated names for tools (pep8, pep257) " - "instead of updated names (pycodestyle, pydocstyle)", - }, - "no_style_warnings": { - "flags": ["-8", "--no-style-warnings"], - "help": "Don't create any warnings about style. This disables the" - " PEP8 tool and similar checks for formatting.", - }, - "member_warnings": { - "flags": ["-m", "--member-warnings"], - "help": "Attempt to warn when code tries to access an attribute of a " - "class or member of a module which does not exist. This is disabled " - "by default as it tends to be quite inaccurate.", - }, - "quiet": { - "flags": ["-q", "--quiet"], - "help": "Run but do not output anything to stdout. Useful to suppress " - "output in scripts without sending to a file (via -o)", - }, - "full_pep8": { - "flags": ["-F", "--full-pep8"], - "help": "Enables every PEP8 warning, so that all PEP8 style violation will be reported.", - }, - "max_line_length": { - "flags": ["--max-line-length"], - "help": "The maximum line length allowed. This will be set by the strictness if no" - " value is explicitly specified", - }, - "messages_only": { - "flags": ["-M", "--messages-only"], - "help": "Only output message information (don't output summary" " information about the checks)", - }, - "summary_only": { - "flags": ["-S", "--summary-only"], - "help": "Only output summary information about the checks (don't" "output message information)", - }, - "output_format": { - "flags": ["-o", "--output-format"], - "help": "The output format. Valid values are: {}. This will output to stdout by default, " - "however a target file can be used instead by adding :path-to-output-file, eg, -o json:output.json".format( - ", ".join(sorted(FORMATTERS.keys())) - ), - }, - "absolute_paths": { - "help": "Whether to output absolute paths when referencing files " - "in messages. By default, paths will be relative to the " - "project path", - }, - "tools": { - "flags": ["-t", "--tool"], - "help": "A list of tools to run. This lets you set exactly which " - "tools to run. To add extra tools to the defaults, see " - "--with-tool. Possible values are: {}. By " - "default, the following tools will be run: {}".format( - ", ".join(sorted(TOOLS.keys())), - ", ".join(sorted(DEFAULT_TOOLS)), - ), - }, - "with_tools": { - "flags": ["-w", "--with-tool"], - "help": "A list of tools to run in addition to the default tools. " - "To specify all tools explicitly, use the --tool argument. " - "Possible values are {}.".format(", ".join(sorted(TOOLS.keys()))), - }, - "without_tools": { - "flags": ["-W", "--without-tool"], - "help": "A list of tools that should not be run. Useful to turn off " - "only a single tool from the defaults. " - "To specify all tools explicitly, use the --tool argument. " - "Possible values are {}.".format(", ".join(sorted(TOOLS.keys()))), - }, - "profiles": { - "flags": ["-P", "--profile"], - "help": "The list of profiles to load. A profile is a certain" - " 'type' of behaviour for prospector, and is represented" - " by a YAML configuration file. Either a full path to the YAML" - " file describing the profile must be provided, or it must be" - " on the profile path (see --profile-path)", - }, - "profile_path": { - "flags": ["--profile-path"], - "help": "Additional paths to search for profile files. By default this" - " is the path that prospector will check, and a directory " - ' called ".prospector" in the path that prospector will check.', - }, - "show_profile": { - "flags": ["--show-profile"], - "help": "Include the computed profile in the summary. This will show what" - " prospector has decided the overall profile is once all profiles" - " have been combined and inherited from. This will produce a large" - " output in most cases so is only useful when trying to debug why" - " prospector is not behaving like you expect.", - }, - "strictness": { - "flags": ["-s", "--strictness"], - "help": "How strict the checker should be. This affects how" - " harshly the checker will enforce coding guidelines. The" - ' default value is "medium", possible values are' - ' "veryhigh", "high", "medium", "low" and "verylow".', - }, - "no_external_config": { - "flags": ["-E", "--no-external-config"], - "help": "Determines how prospector should behave when" - " configuration already exists for a tool. By default," - " prospector will use existing configuration. This flag" - " will cause prospector to ignore existing configuration" - " and use its own settings for every tool. Note that" - " prospector will always use its own config for tools which" - " do not have custom configuration.", - }, - "pylint_config_file": { - "flags": ["--pylint-config-file"], - "help": "The path to a pylintrc file to use to configure pylint. Prospector will find" - " .pylintrc files in the root of the project, but you can use this option to " - "specify manually where it is.", - }, - "ignore_patterns": { - "flags": ["-I", "--ignore-patterns"], - "help": "A list of paths to ignore, as a list of regular" - " expressions. Files and folders will be ignored if their" - " full path contains any of these patterns.", - }, - "ignore_paths": { - "flags": ["-i", "--ignore-paths"], - "help": "A list of file or directory names to ignore. If the" - " complete name matches any of the items in this list, the" - " file or directory (and all subdirectories) will be" - " ignored.", - }, - "die_on_tool_error": { - "flags": ["-X", "--die-on-tool-error"], - "help": "If a tool fails to run, prospector will try to carry on." - " Use this flag to cause prospector to die and raise the" - " exception the tool generated. Mostly useful for" - " development on prospector.", - }, - "include-tool-stdout": { - "flags": ["--include-tool-stdout"], - "help": "There are various places where tools will output warnings to " - "stdout/stderr, which breaks parsing of JSON output. Therefore while tols " - "is running, this is suppressed. For developing, it is sometimes useful to " - "see this. This flag will cause stdout/stderr from a tool to be shown as " - "a normal message amongst other warnings. See also --direct-tool-stdout", - }, - "direct-tool-stdout": { - "flags": ["--direct-tool-stdout"], - "help": "Same as --include-tool-stdout, except the output will be printed " - "directly rather than shown as a message.", - }, - "path": { - "flags": ["-p", "--path"], - "help": "The path to a Python project to inspect. Defaults to PWD" - " if not specified. Note: This command line argument is" - " deprecated and will be removed in a future update. Please" - " use the positional PATH argument instead.", - }, - } - positional = ( - ( - "checkpath", - { - "help": "The path to a Python project to inspect. Defaults to PWD" - " if not specified. If multiple paths are specified," - " they must all be files (no directories).", - "metavar": "PATH", - "nargs": "*", - }, +# flake8: noqa +def build_command_line_parser( + prog: str = "prospector", description: str = "Performs static analysis of Python code" +) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog=prog, description=description) + + parser.add_argument( + "-0", + "--zero-exit", + action="store_true", + help="Prospector will exit with a code of 1 (one) if any messages are found. This makes automation easier; if there are any problems at all, the exit code is non-zero. However this behavior is not always desirable, so if this flag is set, prospector will exit with a code of 0 if it ran successfully, and non-zero if it failed to run.", + ) + parser.add_argument( + "-A", + "--no-autodetect", + action="store_true", + help="Turn off auto-detection of frameworks and libraries used. By default, autodetection will be used. To specify manually, see the --uses option.", + ) + parser.add_argument( + "-u", + "--uses", + action="append", + help="A list of one or more libraries or frameworks that the project uses. Possible values are: django, celery, flask. This will be autodetected by default, but if autodetection doesn't work, manually specify them using this flag.", + ) + parser.add_argument( + "-B", + "--no-blending", + action="store_true", + help="Turn off blending of messages. Prospector will merge together messages from different tools if they represent the same error. Use this option to see all unmerged messages.", + ) + parser.add_argument("-D", "--doc-warnings", action="store_true", help="Include warnings about documentation.") + parser.add_argument("-T", "--test-warnings", action="store_true", help="Also check test modules and packages.") + parser.add_argument( + "--legacy-tool-names", + action="store_true", + help="Output deprecated names for tools (pep8, pep257) instead of updated names (pycodestyle, pydocstyle)", + ) + parser.add_argument( + "-8", + "--no-style-warnings", + action="store_true", + help="Don't create any warnings about style. This disables the PEP8 tool and similar checks for formatting.", + ) + parser.add_argument( + "-m", + "--member-warnings", + action="store_true", + help="Attempt to warn when code tries to access an attribute of a class or member of a module which does not exist. This is disabled by default as it tends to be quite inaccurate.", + ) + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="Run but do not output anything to stdout. Useful to suppress output in scripts without sending to a file (via -o)", + ) + parser.add_argument( + "-F", + "--full-pep8", + action="store_true", + help="Enables every PEP8 warning, so that all PEP8 style violation will be reported.", + ) + parser.add_argument( + "--max-line-length", + type=int, + help="The maximum line length allowed. This will be set by the strictness if no value is explicitly specified", + ) + parser.add_argument( + "-M", + "--messages-only", + action="store_true", + help="Only output message information (don't output summary information about the checks)", + ) + parser.add_argument( + "-S", + "--summary-only", + action="store_true", + help="Only output summary information about the checks (don't output message information)", + ) + parser.add_argument( + "-o", + "--output-format", + action="append", + help="The output format. Valid values are: {}. This will output to stdout by default, however a target file can be used instead by adding :path-to-output-file, eg, -o json:output.json".format( + ", ".join(sorted(FORMATTERS.keys())) + ), + ) + parser.add_argument( + "--absolute-paths", + action="store_true", + help="Whether to output absolute paths when referencing files in messages. By default, paths will be relative to the project path", + ) + parser.add_argument( + "-t", + "--tool", + dest="tools", + action="append", + help="A list of tools to run. This lets you set exactly which tools to run. To add extra tools to the defaults, see --with-tool. Possible values are: {}. By default, the following tools will be run: {}".format( + ", ".join(sorted(TOOLS.keys())), ", ".join(sorted(DEFAULT_TOOLS)) + ), + ) + parser.add_argument( + "-w", + "--with-tool", + dest="with_tools", + action="append", + help="A list of tools to run in addition to the default tools. To specify all tools explicitly, use the --tool argument. Possible values are {}.".format( + ", ".join(sorted(TOOLS.keys())) ), ) + parser.add_argument( + "-W", + "--without-tool", + dest="without_tools", + action="append", + help="A list of tools that should not be run. Useful to turn off only a single tool from the defaults. To specify all tools explicitly, use the --tool argument. Possible values are {}.".format( + ", ".join(sorted(TOOLS.keys())) + ), + ) + parser.add_argument( + "-P", + "--profile", + dest="profiles", + action="append", + help="The list of profiles to load. A profile is a certain 'type' of behavior for prospector, and is represented by a YAML configuration file. Either a full path to the YAML file describing the profile must be provided, or it must be on the profile path (see --profile-path)", + ) + parser.add_argument( + "--profile-path", + action="append", + help="Additional paths to search for profile files. By default this is the path that prospector will check, and a directory called '.prospector' in the path that prospector will check.", + ) + parser.add_argument( + "--show-profile", + action="store_true", + help="Include the computed profile in the summary. This will show what prospector has decided the overall profile is once all profiles have been combined and inherited from. This will produce a large output in most cases so is only useful when trying to debug why prospector is not behaving like you expect.", + ) + parser.add_argument( + "-s", + "--strictness", + help='How strict the checker should be. This affects how harshly the checker will enforce coding guidelines. The default value is "medium", possible values are "veryhigh", "high", "medium", "low" and "verylow".', + ) + parser.add_argument( + "-E", + "--no-external-config", + action="store_true", + help="Determines how prospector should behave when configuration already exists for a tool. By default, prospector will use existing configuration. This flag will cause prospector to ignore existing configuration and use its own settings for every tool. Note that prospector will always use its own config for tools which do not have custom configuration.", + ) + parser.add_argument( + "--pylint-config-file", + help="The path to a pylintrc file to use to configure pylint. Prospector will find .pylintrc files in the root of the project, but you can use this option to specify manually where it is.", + ) + parser.add_argument( + "-I", + "--ignore-patterns", + action="append", + help="A list of paths to ignore, as a list of regular expressions. Files and folders will be ignored if their full path contains any of these patterns.", + ) + parser.add_argument( + "-i", + "--ignore-paths", + action="append", + help="A list of file or directory names to ignore. If the complete name matches any of the items in this list, the file or directory (and all subdirectories) will be ignored.", + ) + parser.add_argument( + "-X", + "--die-on-tool-error", + action="store_true", + help="If a tool fails to run, prospector will try to carry on. Use this flag to cause prospector to die and raise the exception the tool generated. Mostly useful for development on prospector.", + ) + parser.add_argument( + "--include-tool-stdout", + action="store_true", + help="There are various places where tools will output warnings to stdout/stderr, which breaks parsing of JSON output. Therefore while tols is running, this is suppressed. For developing, it is sometimes useful to see this. This flag will cause stdout/stderr from a tool to be shown as a normal message amongst other warnings. See also --direct-tool-stdout", + ) + parser.add_argument( + "--direct-tool-stdout", + action="store_true", + help="Same as --include-tool-stdout, except the output will be printed directly rather than shown as a message.", + ) + parser.add_argument( + "-p", + "--path", + help="The path to a Python project to inspect. Defaults to PWD if not specified. Note: This command line argument is deprecated and will be removed in a future update. Please use the positional PATH argument instead.", + ) - return soc.CommandLineSource( - options=options, - version=_VERSION, - parser_options=parser_options, - positional=positional, + parser.add_argument( + "path", + help="The path to a Python project to inspect. Defaults to PWD if not specified. If multiple paths are specified, they must all be files (no directories).", + metavar="PATH", + nargs="?", ) + + return parser diff --git a/prospector/config/datatype.py b/prospector/config/datatype.py index 6c8394a6..bcd7c15c 100644 --- a/prospector/config/datatype.py +++ b/prospector/config/datatype.py @@ -2,18 +2,14 @@ import re import sys -from setoptconf.datatype import Choice - -class OutputChoice(Choice): - def sanitize(self, value: str) -> tuple[str, list[str]]: - parsed = re.split(r"[;:]", value) - output_format, output_targets = parsed[0], parsed[1:] - checked_targets = [] - for target in output_targets: - if sys.platform.startswith("win") and target.startswith((os.path.sep, os.path.altsep)): - checked_targets[-1] += ":" + target - else: - checked_targets.append(target) - validated_format = super().sanitize(output_format) - return validated_format, checked_targets +def parse_output_format(value: str) -> tuple[str, list[str]]: + parsed = re.split(r"[;:]", value) + output_format, output_targets = parsed[0], parsed[1:] + checked_targets: list[str] = [] + for target in output_targets: + if sys.platform.startswith("win") and target.startswith((os.path.sep, os.path.altsep)): + checked_targets[-1] += ":" + target + else: + checked_targets.append(target) + return output_format, checked_targets diff --git a/prospector/formatters/__init__.py b/prospector/formatters/__init__.py index 0399df65..a3656d61 100644 --- a/prospector/formatters/__init__.py +++ b/prospector/formatters/__init__.py @@ -1,7 +1,7 @@ from . import emacs, grouped, json, pylint, text, vscode, xunit, yaml from .base import Formatter -__all__ = ("FORMATTERS", "Formatter") +__all__ = ["Formatter", "FORMATTERS"] FORMATTERS: dict[str, type[Formatter]] = { diff --git a/prospector/formatters/base.py b/prospector/formatters/base.py index 2ad06dc7..3d14434a 100644 --- a/prospector/formatters/base.py +++ b/prospector/formatters/base.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from prospector.profiles.profile import ProspectorProfile +from prospector.profiles.profile import ProspectorProfile # pylint: disable=cyclic-import __all__ = ("Formatter",) diff --git a/prospector/profiles/exceptions.py b/prospector/profiles/exceptions.py index 1430529e..ee178d80 100644 --- a/prospector/profiles/exceptions.py +++ b/prospector/profiles/exceptions.py @@ -1,5 +1,9 @@ +from pathlib import Path +from typing import Union + + class ProfileNotFound(Exception): - def __init__(self, name: str, profile_path: str) -> None: + def __init__(self, name: Union[str, Path], profile_path: list[Path]) -> None: super().__init__() self.name = name self.profile_path = profile_path @@ -7,7 +11,7 @@ def __init__(self, name: str, profile_path: str) -> None: def __repr__(self) -> str: return "Could not find profile {}; searched in {}".format( self.name, - ":".join(self.profile_path), + ":".join([str(p) for p in self.profile_path]), ) diff --git a/prospector/profiles/profile.py b/prospector/profiles/profile.py index 5c489e48..d572a774 100644 --- a/prospector/profiles/profile.py +++ b/prospector/profiles/profile.py @@ -18,26 +18,28 @@ def __init__(self, name: str, profile_dict: dict[str, Any], inherit_order: list[ self.name = name self.inherit_order = inherit_order - self.ignore_paths = _ensure_list(profile_dict.get("ignore-paths", [])) + self.ignore_paths: list[str] = _ensure_list(profile_dict.get("ignore-paths", [])) # The 'ignore' directive is an old one which should be deprecated at some point - self.ignore_patterns = _ensure_list(profile_dict.get("ignore-patterns", []) + profile_dict.get("ignore", [])) + self.ignore_patterns: list[str] = _ensure_list( + profile_dict.get("ignore-patterns", []) + profile_dict.get("ignore", []) + ) - self.output_format = profile_dict.get("output-format") - self.output_target = profile_dict.get("output-target") - self.autodetect = profile_dict.get("autodetect", True) - self.uses = [ + self.output_format: Optional[str] = profile_dict["output-format"] if "output-format" in profile_dict else None + self.output_target: Optional[str] = profile_dict.get("output-target") + self.autodetect: bool = profile_dict.get("autodetect", True) + self.uses: list[str] = [ uses for uses in _ensure_list(profile_dict.get("uses", [])) if uses in ("django", "celery", "flask") ] - self.max_line_length = profile_dict.get("max-line-length") + self.max_line_length: Optional[int] = profile_dict.get("max-line-length") # informational shorthands - self.strictness = profile_dict.get("strictness") - self.test_warnings = profile_dict.get("test-warnings") - self.doc_warnings = profile_dict.get("doc-warnings") - self.member_warnings = profile_dict.get("member-warnings") + self.strictness: Optional[str] = profile_dict.get("strictness") + self.test_warnings: Optional[bool] = profile_dict.get("test-warnings") + self.doc_warnings: Optional[bool] = profile_dict.get("doc-warnings") + self.member_warnings: Optional[bool] = profile_dict.get("member-warnings") # TODO: this is needed by Landscape but not by prospector; there is probably a better place for it - self.requirements = _ensure_list(profile_dict.get("requirements", [])) + self.requirements: list[str] = _ensure_list(profile_dict.get("requirements", [])) for tool in TOOLS: tool_conf = profile_dict.get(tool, {}) @@ -57,12 +59,12 @@ def get_disabled_messages(self, tool_name: str) -> list[str]: enable = getattr(self, tool_name)["enable"] return list(set(disable) - set(enable)) - def is_tool_enabled(self, name: str) -> bool: - enabled: Optional[bool] = getattr(self, name).get("run") + def is_tool_enabled(self, tool: str) -> bool: + enabled: Optional[bool] = getattr(self, tool).get("run") if enabled is not None: return enabled # this is not explicitly enabled or disabled, so use the default - return name in DEFAULT_TOOLS + return tool in DEFAULT_TOOLS def list_profiles(self) -> list[str]: # this profile is itself included @@ -173,7 +175,7 @@ def _load_content(name_or_path: Union[str, Path], profile_path: list[Path]) -> d if optional: return {} - raise ProfileNotFound(str(name_or_path), str(profile_path)) + raise ProfileNotFound(name_or_path, profile_path) with codecs.open(filename) as fct: try: @@ -222,7 +224,7 @@ def _merge_tool_config(priority: dict[str, Any], base: dict[str, Any]) -> dict[s def _merge_profile_dict(priority: dict[str, Any], base: dict[str, Any]) -> dict[str, Any]: # copy the base dict into our output - out = dict(base.items()) + out = {**base} for key, value in priority.items(): if key in ( @@ -235,7 +237,7 @@ def _merge_profile_dict(priority: dict[str, Any], base: dict[str, Any]) -> dict[ "max-line-length", "pep8", ): - # some keys are simple values which are overwritten + # Some keys are simple values which are overwritten out[key] = value elif key in ( "ignore", @@ -246,10 +248,10 @@ def _merge_profile_dict(priority: dict[str, Any], base: dict[str, Any]) -> dict[ "python-targets", "output-target", ): - # some keys should be appended + # Some keys should be appended out[key] = _ensure_list(value) + _ensure_list(base.get(key, [])) elif key in TOOLS: - # this is tool config! + # This is tool config! out[key] = _merge_tool_config(value, base.get(key, {})) return out diff --git a/prospector/run.py b/prospector/run.py index 31b0f385..4788d810 100644 --- a/prospector/run.py +++ b/prospector/run.py @@ -5,7 +5,7 @@ import warnings from datetime import datetime from pathlib import Path -from typing import Any, Optional, TextIO +from typing import Any, Optional, TextIO, Union from prospector import blender, postfilter, tools from prospector.compat import is_relative_to @@ -22,7 +22,7 @@ class Prospector: def __init__(self, config: ProspectorConfig) -> None: self.config = config - self.summary: Optional[dict[str, Any]] = None + self.summary: Optional[dict[str, Union[str, int, datetime, list[str]]]] = None self.messages = config.messages def process_messages(self, found_files: FileFinder, messages: list[Message]) -> list[Message]: @@ -72,9 +72,9 @@ def execute(self) -> None: # Run the tools for tool in self.config.get_tools(found_files): - for name, cls in tools.TOOLS.items(): + for available_tool, cls in tools.TOOLS.items(): if cls == tool.__class__: - toolname = name + toolname = available_tool break else: toolname = "Unknown" @@ -180,9 +180,7 @@ def get_parser() -> argparse.ArgumentParser: This is a helper method to return an argparse parser, to be used with the Sphinx argparse plugin for documentation. """ - manager = cfg.build_manager() - source = cfg.build_command_line_source(prog="prospector", description=None) - return source.build_parser(manager.settings, None) + return cfg.build_command_line_parser() def main() -> None: diff --git a/prospector/tools/__init__.py b/prospector/tools/__init__.py index 7447ece6..e275af9a 100644 --- a/prospector/tools/__init__.py +++ b/prospector/tools/__init__.py @@ -82,4 +82,7 @@ def _optional_tool( "profile-validator", ) -DEPRECATED_TOOL_NAMES = {"pep8": "pycodestyle", "pep257": "pydocstyle"} +DEPRECATED_TOOL_NAMES = { + "pep8": "pycodestyle", + "pep257": "pydocstyle", +} diff --git a/prospector/tools/profile_validator/__init__.py b/prospector/tools/profile_validator/__init__.py index eb1bf5b5..cd9664f2 100644 --- a/prospector/tools/profile_validator/__init__.py +++ b/prospector/tools/profile_validator/__init__.py @@ -1,6 +1,6 @@ import re from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union from prospector.tools.base import ToolBase @@ -35,7 +35,7 @@ def _tool_names(with_deprecated: bool = True) -> list[str]: from prospector.tools import DEPRECATED_TOOL_NAMES, TOOLS # pylint: disable=import-outside-toplevel - tools = list(TOOLS) + tools: list[str] = list(TOOLS.keys()) if with_deprecated: tools += DEPRECATED_TOOL_NAMES.keys() return tools @@ -58,7 +58,7 @@ class ProfileValidationTool(ToolBase): ALL_SETTINGS = LIST_SETTINGS + BOOL_SETTINGS + OTHER_SETTINGS def __init__(self) -> None: - self.to_check = set(AUTO_LOADED_PROFILES) + self.to_check: set[Union[str, Path]] = set(AUTO_LOADED_PROFILES) self.ignore_codes: list[str] = [] def configure(self, prospector_config: "ProspectorConfig", found_files: FileFinder) -> None: diff --git a/pyproject.toml b/pyproject.toml index 9d7e4095..7c31dbd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,6 @@ pep8-naming = ">=0.3.3,<=0.10.0" pydocstyle = ">=2.0.0" dodgy = "^0.2.1" toml = "^0.10.2" -setoptconf-tmp = "^0.3.1" GitPython = "^3.1.27" packaging = "*" bandit = {version = ">=1.5.1", optional = true} @@ -59,6 +58,7 @@ vulture = {version = ">=1.5", optional = true} mypy = {version = ">=0.600", optional = true} pyright = {version = ">=1.1.3", optional = true} pyroma = {version = ">=2.4", optional = true} +pydantic = "^2.0.0" [tool.poetry.extras] with_bandit = ["bandit"] diff --git a/tests/config/test_datatype.py b/tests/config/test_datatype.py index 5fecbe2d..a1ea177d 100644 --- a/tests/config/test_datatype.py +++ b/tests/config/test_datatype.py @@ -1,7 +1,7 @@ from unittest import TestCase from unittest.mock import patch -from prospector.config.datatype import OutputChoice +from prospector.config.datatype import parse_output_format class TestOutputChoice(TestCase): @@ -9,138 +9,124 @@ class TestOutputChoice(TestCase): @patch("os.path.sep", "/") @patch("os.path.altsep", None) def test_sanitize_rel_path_colon_posix(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit:./test-results.xml"), - ("xunit", ["./test-results.xml"]), + parse_output_format("pylint:./test-results.xml"), + ("pylint", ["./test-results.xml"]), ) @patch("sys.platform", "linux") @patch("os.path.sep", "/") @patch("os.path.altsep", None) def test_sanitize_rel_path_semicolon_posix(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit;./test-results.xml"), - ("xunit", ["./test-results.xml"]), + parse_output_format("pylint;./test-results.xml"), + ("pylint", ["./test-results.xml"]), ) @patch("sys.platform", "win32") @patch("os.path.sep", "\\") @patch("os.path.altsep", "/") def test_sanitize_rel_path_colon_windows(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit:.\\test-results.xml"), - ("xunit", [".\\test-results.xml"]), + parse_output_format("pylint:.\\test-results.xml"), + ("pylint", [".\\test-results.xml"]), ) @patch("sys.platform", "win32") @patch("os.path.sep", "\\") @patch("os.path.altsep", "/") def test_sanitize_rel_path_semicolon_windows(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit;.\\test-results.xml"), - ("xunit", [".\\test-results.xml"]), + parse_output_format("pylint;.\\test-results.xml"), + ("pylint", [".\\test-results.xml"]), ) @patch("sys.platform", "linux") @patch("os.path.sep", "/") @patch("os.path.altsep", None) def test_sanitize_abs_path_colon_posix(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit:/home/test-results.xml"), - ("xunit", ["/home/test-results.xml"]), + parse_output_format("pylint:/home/test-results.xml"), + ("pylint", ["/home/test-results.xml"]), ) @patch("sys.platform", "linux") @patch("os.path.sep", "/") @patch("os.path.altsep", None) def test_sanitize_abs_path_semicolon_posix(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit;/home/test-results.xml"), - ("xunit", ["/home/test-results.xml"]), + parse_output_format("pylint;/home/test-results.xml"), + ("pylint", ["/home/test-results.xml"]), ) @patch("sys.platform", "win32") @patch("os.path.sep", "\\") @patch("os.path.altsep", "/") def test_sanitize_abs_path_colon_windows(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit:C:\\testResults\\test-results.xml"), - ("xunit", ["C:\\testResults\\test-results.xml"]), + parse_output_format("pylint:C:\\testResults\\test-results.xml"), + ("pylint", ["C:\\testResults\\test-results.xml"]), ) @patch("sys.platform", "win32") @patch("os.path.sep", "\\") @patch("os.path.altsep", "/") def test_sanitize_abs_path_semicolon_windows(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit;C:\\testResults\\test-results.xml"), - ("xunit", ["C:\\testResults\\test-results.xml"]), + parse_output_format("pylint;C:\\testResults\\test-results.xml"), + ("pylint", ["C:\\testResults\\test-results.xml"]), ) @patch("sys.platform", "win32") @patch("os.path.sep", "\\") @patch("os.path.altsep", "/") def test_sanitize_abs_path_colon_windows_alternate(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit:C:/testResults/test-results.xml"), - ("xunit", ["C:/testResults/test-results.xml"]), + parse_output_format("pylint:C:/testResults/test-results.xml"), + ("pylint", ["C:/testResults/test-results.xml"]), ) @patch("sys.platform", "win32") @patch("os.path.sep", "\\") @patch("os.path.altsep", "/") def test_sanitize_abs_path_semicolon_windows_alternate(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit;C:/testResults/test-results.xml"), - ("xunit", ["C:/testResults/test-results.xml"]), + parse_output_format("pylint;C:/testResults/test-results.xml"), + ("pylint", ["C:/testResults/test-results.xml"]), ) @patch("sys.platform", "linux") @patch("os.path.sep", "/") @patch("os.path.altsep", None) def test_sanitize_abs_rel_path_colon_posix(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit:/home/test-results.xml:./test-results.xml"), - ("xunit", ["/home/test-results.xml", "./test-results.xml"]), + parse_output_format("pylint:/home/test-results.xml:./test-results.xml"), + ("pylint", ["/home/test-results.xml", "./test-results.xml"]), ) @patch("sys.platform", "linux") @patch("os.path.sep", "/") @patch("os.path.altsep", None) def test_sanitize_abs_rel_path_semicolon_posix(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit;/home/test-results.xml;./test-results.xml"), - ("xunit", ["/home/test-results.xml", "./test-results.xml"]), + parse_output_format("pylint;/home/test-results.xml;./test-results.xml"), + ("pylint", ["/home/test-results.xml", "./test-results.xml"]), ) @patch("sys.platform", "win32") @patch("os.path.sep", "\\") @patch("os.path.altsep", "/") def test_sanitize_abs_rel_path_colon_windows(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit:C:\\home\\test-results.xml:.\\test-results.xml"), - ("xunit", ["C:\\home\\test-results.xml", ".\\test-results.xml"]), + parse_output_format("pylint:C:\\home\\test-results.xml:.\\test-results.xml"), + ("pylint", ["C:\\home\\test-results.xml", ".\\test-results.xml"]), ) @patch("sys.platform", "win32") @patch("os.path.sep", "\\") @patch("os.path.altsep", "/") def test_sanitize_abs_rel_path_semicolon_windows(self): - output_choice = OutputChoice(["xunit"]) self.assertEqual( - output_choice.sanitize("xunit;C:\\home\\test-results.xml;.\\test-results.xml"), - ("xunit", ["C:\\home\\test-results.xml", ".\\test-results.xml"]), + parse_output_format("pylint;C:\\home\\test-results.xml;.\\test-results.xml"), + ("pylint", ["C:\\home\\test-results.xml", ".\\test-results.xml"]), ) diff --git a/tests/suppression/test_suppression.py b/tests/suppression/test_suppression.py index cc2635c6..15b403c5 100644 --- a/tests/suppression/test_suppression.py +++ b/tests/suppression/test_suppression.py @@ -29,14 +29,12 @@ def test_ignore_enum_error(self): def test_filter_messages(self): with patch_workdir_argv( - target="setoptconf.source.commandline.sys.argv", workdir=Path(__file__).parent / "testdata/test_filter_messages", ) as pros: self.assertEqual(0, pros.summary["message_count"]) def test_filter_messages_negative(self): with patch_workdir_argv( - target="setoptconf.source.commandline.sys.argv", workdir=Path(__file__).parent / "testdata/test_filter_messages_negative", ) as pros: self.assertEqual(5, pros.summary["message_count"]) diff --git a/tests/utils.py b/tests/utils.py index b21471fa..daef4816 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,8 +10,8 @@ @contextlib.contextmanager -def patch_cli(*args: list[str], target: str = "sys.argv"): - with patch(target, args): +def patch_cli(*args: tuple[str], target: str = "sys.argv"): + with patch(target, [arg for arg in args]): yield @@ -36,7 +36,7 @@ def patch_cwd(set_cwd: Path): @contextlib.contextmanager -def patch_execution(*args: list[str], set_cwd: Path = None): +def patch_execution(*args: tuple[str], set_cwd: Path = None): """ Utility to patch builtins to simulate running prospector in a particular directory with particular commandline args @@ -44,7 +44,8 @@ def patch_execution(*args: list[str], set_cwd: Path = None): :param set_cwd: Simulate changing directory into the given directory :param args: Any additional command-line arguments to pass to prospector """ - args = ("prospector",) + args + print(f"args: {args}") + args = ["prospector"] + list(args) with patch_cli(*args): if set_cwd: with patch_cwd(set_cwd):