From 4e189eb1113cca3a33f9821eb7aa3f45aace8b07 Mon Sep 17 00:00:00 2001 From: "Augusto W. Andreoli" Date: Sun, 28 Feb 2021 18:07:24 +0100 Subject: [PATCH] fix: display parsing errors as violations (#299) * test: setup.cfg with multiline comments * fix: don't abort on parsing error; display a violation instead * fix: don't change the file if there was a parsing error * test: simulate a parsing error when saving setup.cfg * refactor: last_item renamed to last_block in ConfigUpdater * build: new release of configupdater * docs: don't fail if a page is unreachable --- poetry.lock | 37 ++++++++++- setup.cfg | 9 ++- src/nitpick/cli.py | 2 +- src/nitpick/plugins/base.py | 32 +++++---- src/nitpick/plugins/setup_cfg.py | 30 ++++++--- tasks.py | 2 +- tests/test_setup_cfg.py | 111 +++++++++++++++++++++++++++++++ 7 files changed, 194 insertions(+), 29 deletions(-) diff --git a/poetry.lock b/poetry.lock index 77eb5fb0..74a407a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -135,12 +135,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "configupdater" -version = "1.1.3" +version = "2.0" description = "Parser like ConfigParser but for updating configuration files" category = "main" optional = false python-versions = ">=3.6" +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + [package.extras] testing = ["sphinx", "flake8", "pytest", "pytest-cov", "pytest-virtualenv", "pytest-xdist"] @@ -1069,8 +1072,8 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] configupdater = [ - {file = "ConfigUpdater-1.1.3-py2.py3-none-any.whl", hash = "sha256:d49f1d64d16cb92f2cffef442d42fdfd5e317fd9368ddfb8b96f7683b3ec751c"}, - {file = "ConfigUpdater-1.1.3.tar.gz", hash = "sha256:5363c45babe9c95df58f054b72ed513d1fca1be089ee4b2e32dc9fb3351f12b8"}, + {file = "ConfigUpdater-2.0-py2.py3-none-any.whl", hash = "sha256:bc62bd5141c45a89840a3e82e0a06f23fb2c00de82e2b72c8030cafb4daea9a2"}, + {file = "ConfigUpdater-2.0.tar.gz", hash = "sha256:6a60447fb25e5cb5036cdd5761287ac5649135a49094bc8bd71d999417483441"}, ] contextvars = [ {file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"}, @@ -1267,20 +1270,39 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] marshmallow = [ @@ -1410,20 +1432,29 @@ responses = [ {file = "ruamel.yaml.clib-0.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73b3d43e04cc4b228fa6fa5d796409ece6fcb53a6c270eb2048109cbcbc3b9c2"}, {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:53b9dd1abd70e257a6e32f934ebc482dac5edb8c93e23deb663eac724c30b026"}, {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:839dd72545ef7ba78fd2aa1a5dd07b33696adf3e68fae7f31327161c1093001b"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1236df55e0f73cd138c0eca074ee086136c3f16a97c2ac719032c050f7e0622f"}, {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win32.whl", hash = "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f"}, {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e52c96ca66de04be42ea2278012a2342d89f5e82b4512fb6fb7134e377e2e62"}, {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c"}, {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2fd336a5c6415c82e2deb40d08c222087febe0aebe520f4d21910629018ab0f3"}, {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win32.whl", hash = "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2"}, {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:2602e91bd5c1b874d6f93d3086f9830f3e907c543c7672cf293a97c3fabdcd91"}, {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44c7b0498c39f27795224438f1a6be6c5352f82cb887bc33d962c3a3acc00df6"}, {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8e8fd0a22c9d92af3a34f91e8a2594eeb35cba90ab643c5e0e643567dc8be43e"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:75f0ee6839532e52a3a53f80ce64925ed4aed697dd3fa890c4c918f3304bd4f4"}, {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win32.whl", hash = "sha256:464e66a04e740d754170be5e740657a3b3b6d2bcc567f0c3437879a6e6087ff6"}, {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:52ae5739e4b5d6317b52f5b040b1b6639e8af68a5b8fd606a8b08658fbd0cab5"}, {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df5019e7783d14b79217ad9c56edf1ba7485d614ad5a385d1b3c768635c81c0"}, {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5254af7d8bdf4d5484c089f929cb7f5bafa59b4f01d4f48adda4be41e6d29f99"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8be05be57dc5c7b4a0b24edcaa2f7275866d9c907725226cdde46da09367d923"}, {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win32.whl", hash = "sha256:74161d827407f4db9072011adcfb825b5258a5ccb3d2cd518dd6c9edea9e30f1"}, {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:058a1cc3df2a8aecc12f983a48bda99315cebf55a3b3a5463e37bb599b05727b"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1f8c0a4577c0e6c99d208de5c4d3fd8aceed9574bb154d7a2b21c16bb924154c"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win32.whl", hash = "sha256:46d6d20815064e8bb023ea8628cfb7402c0f0e83de2c2227a88097e239a7dffd"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6c0a5dc52fc74eb87c67374a4e554d4761fd42a4d01390b7e868b30d21f4b8bb"}, {file = "ruamel.yaml.clib-0.2.2.tar.gz", hash = "sha256:2d24bd98af676f4990c4d715bcdc2a60b19c56a3fb3a763164d2d8ca0e806ba7"}, ] six = [ diff --git a/setup.cfg b/setup.cfg index 3d305fce..4a4ceff5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,8 +80,8 @@ deps = pip>=19.2 safety # Run nitpick and pylint with tox, because local repos don't seem to work well with https://pre-commit.ci/ -# Run Nitpick locally on itself commands = + # Run Nitpick locally on itself flake8 --select=NIP pylint src/ safety check @@ -104,8 +104,11 @@ extras = doc commands = sphinx-apidoc --force --module-first --separate --implicit-namespaces --output-dir docs/source src/nitpick/ python3 docs/generate_rst.py - sphinx-build --color -b linkcheck docs "{toxworkdir}/docs_out" - sphinx-build -d "{toxworkdir}/docs_doctree" --color -b html docs "{toxworkdir}/docs_out" {posargs} + # Don't fail if a page is unreachable. + # https://tox.readthedocs.io/en/latest/example/basic.html#ignoring-exit-code + - sphinx-build --color -j auto -b linkcheck docs "{toxworkdir}/docs_out" + # Use these options to debug Sphinx: -nWT --keep-going -vvv + sphinx-build --color -j auto -d "{toxworkdir}/docs_doctree" -b html docs "{toxworkdir}/docs_out" {posargs} [coverage:run] # https://coverage.readthedocs.io/en/latest/config.html#run diff --git a/src/nitpick/cli.py b/src/nitpick/cli.py index 67dc2c56..9eaa44c2 100644 --- a/src/nitpick/cli.py +++ b/src/nitpick/cli.py @@ -10,7 +10,7 @@ - When you import __main__ it will get executed again (as a module) because there's no ``nitpick.__main__`` in ``sys.modules``. -Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration +Also see (1) from https://click.palletsprojects.com/en/5.x/setuptools/#setuptools-integration """ from pathlib import Path from typing import Optional diff --git a/src/nitpick/plugins/base.py b/src/nitpick/plugins/base.py index d96a39dd..5dad0d83 100644 --- a/src/nitpick/plugins/base.py +++ b/src/nitpick/plugins/base.py @@ -68,24 +68,31 @@ def entry_point(self) -> Iterator[Fuss]: """Entry point of the Nitpick plugin.""" self.init() - has_config_dict = bool(self.expected_config or self.nitpick_file_dict) should_exist: bool = bool(self.info.project.nitpick_files_section.get(self.filename, True)) - file_exists = self.file_path.exists() - should_write = True - - if has_config_dict and not file_exists: - yield from self._suggest_when_file_not_found() - elif not should_exist and file_exists: + if self.file_path.exists() and not should_exist: logger.info(f"{self}: File {self.filename} exists when it should not") # Only display this message if the style is valid. yield self.reporter.make_fuss(SharedViolations.DELETE_FILE) - should_write = False - elif file_exists and has_config_dict: + return + + has_config_dict = bool(self.expected_config or self.nitpick_file_dict) + if not has_config_dict: + return + + yield from self._enforce_file_configuration() + + def _enforce_file_configuration(self): + file_exists = self.file_path.exists() + if file_exists: logger.info(f"{self}: Enforcing rules") yield from self.enforce_rules() + else: + yield from self._suggest_when_file_not_found() - if should_write and self.apply: - self.write_file(file_exists) + if self.apply: + fuss = self.write_file(file_exists) # pylint: disable=assignment-from-none + if fuss: + yield fuss def init(self): """Hook for plugin initialization after the instance was created.""" @@ -101,8 +108,9 @@ def _suggest_when_file_not_found(self): else: yield self.reporter.make_fuss(SharedViolations.CREATE_FILE) - def write_file(self, file_exists: bool) -> None: + def write_file(self, file_exists: bool) -> Optional[Fuss]: # pylint: disable=unused-argument,no-self-use """Hook to write the new file when apply mode is on. Should be used by inherited classes.""" + return None @abc.abstractmethod def enforce_rules(self) -> Iterator[Fuss]: diff --git a/src/nitpick/plugins/setup_cfg.py b/src/nitpick/plugins/setup_cfg.py index 361c8ff6..165a3f0e 100644 --- a/src/nitpick/plugins/setup_cfg.py +++ b/src/nitpick/plugins/setup_cfg.py @@ -1,5 +1,5 @@ """Enforce config on `setup.cfg `.""" -from configparser import ConfigParser +from configparser import ConfigParser, DuplicateOptionError, ParsingError from io import StringIO from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type @@ -24,6 +24,7 @@ class Violations(ViolationEnum): KEY_HAS_DIFFERENT_VALUE = (323, ": [{section}]{key} is {actual} but it should be like this:") MISSING_KEY_VALUE_PAIRS = (324, ": section [{section}] has some missing key/value pairs. Use this:") INVALID_COMMA_SEPARATED_VALUES_SECTION = (325, f": invalid sections on {COMMA_SEPARATED_VALUES}:") + PARSING_ERROR = (326, ": parsing error ({cls}): {msg}") class SetupCfgPlugin(NitpickPlugin): @@ -64,12 +65,16 @@ def missing_sections(self) -> Set[str]: """Missing sections.""" return self.expected_sections - self.current_sections - def write_file(self, file_exists: bool) -> None: + def write_file(self, file_exists: bool) -> Optional[Fuss]: """Write the new file.""" - if file_exists: - self.updater.update_file() - else: - self.updater.write(self.file_path.open("w")) + try: + if file_exists: + self.updater.update_file() + else: + self.updater.write(self.file_path.open("w")) + except ParsingError as err: + return self.reporter.make_fuss(Violations.PARSING_ERROR, cls=err.__class__.__name__, msg=err) + return None def get_missing_output(self) -> str: """Get a missing output string example from the missing sections in setup.cfg.""" @@ -81,8 +86,8 @@ def get_missing_output(self) -> str: for section in sorted(missing): expected_config: Dict = self.expected_config[section] if self.apply: - if self.updater.last_item: - self.updater.last_item.add_after.space(1) + if self.updater.last_block: + self.updater.last_block.add_after.space(1) self.updater.add_section(section) self.updater[section].update(expected_config) parser[section] = expected_config @@ -91,7 +96,14 @@ def get_missing_output(self) -> str: # TODO: convert the contents to dict (with IniConfig().sections?) and mimic other plugins doing dict diffs def enforce_rules(self) -> Iterator[Fuss]: """Enforce rules on missing sections and missing key/value pairs in setup.cfg.""" - self.updater.read(str(self.file_path)) + try: + self.updater.read(str(self.file_path)) + except DuplicateOptionError as err: + # Don't change the file if there was a parsing error + self.apply = False + yield self.reporter.make_fuss(Violations.PARSING_ERROR, cls=err.__class__.__name__, msg=err) + return + yield from self.enforce_missing_sections() csv_sections = {v.split(".")[0] for v in self.comma_separated_values} diff --git a/tasks.py b/tasks.py index d4f416da..03235adc 100644 --- a/tasks.py +++ b/tasks.py @@ -80,7 +80,7 @@ def doc(c): c.run("tox -e docs") -@task +@task(help={"full": "Full build using tox", "recreate": "Recreate tox environment"}) def ci_build(c, full=False, recreate=False): """Simulate a CI build.""" tox_cmd = "tox -r" if recreate else "tox" diff --git a/tests/test_setup_cfg.py b/tests/test_setup_cfg.py index 8e82be95..523bc77f 100644 --- a/tests/test_setup_cfg.py +++ b/tests/test_setup_cfg.py @@ -1,4 +1,9 @@ """setup.cfg tests.""" +from configparser import ParsingError +from unittest import mock + +from configupdater import ConfigUpdater + from nitpick.constants import SETUP_CFG from nitpick.plugins.setup_cfg import SetupCfgPlugin, Violations from nitpick.violations import Fuss, ProjectViolations, SharedViolations @@ -309,3 +314,109 @@ def test_invalid_sections_comma_separated_values(tmp_path): ).api_apply().assert_violations( Fuss(False, SETUP_CFG, 325, ": invalid sections on comma_separated_values:", "aaa, falek8") ) + + +def test_multiline_comment(tmp_path): + """Test file with multiline comments should not raise a configparser.ParsingError.""" + original_file = """ + [flake8] + exclude = + # Trash and cache: + .git + __pycache__ + .venv + .eggs + *.egg + temp + # Bad code that I write to test things: + ex.py + another = + A valid line + ; Now a comment with semicolon + Another valid line + """ + ProjectMock(tmp_path).style( + """ + ["setup.cfg".flake8] + new = "value" + """ + ).setup_cfg(original_file).api_apply().assert_violations( + Fuss( + True, + SETUP_CFG, + 324, + ": section [flake8] has some missing key/value pairs. Use this:", + """ + [flake8] + new = value + """, + ) + ).assert_file_contents( + SETUP_CFG, + f""" + {original_file}new = value + """, + ) + + +def test_duplicated_option(tmp_path): + """Test a violation is raised if a file has a duplicated option.""" + original_file = """ + [abc] + easy = 123 + easy = as sunday morning + """ + project = ProjectMock(tmp_path) + full_path = project.root_dir / SETUP_CFG + project.style( + """ + ["setup.cfg".abc] + hard = "as a rock" + """ + ).setup_cfg(original_file).api_apply().assert_violations( + Fuss( + False, + SETUP_CFG, + Violations.PARSING_ERROR.code, + f": parsing error (DuplicateOptionError): While reading from {str(full_path)!r} " + f"[line 3]: option 'easy' in section 'abc' already exists", + ) + ).assert_file_contents( + SETUP_CFG, original_file + ) + + +@mock.patch.object(ConfigUpdater, "update_file") +def test_simulate_parsing_error_when_saving(update_file, tmp_path): + """Simulate a parsing error when saving setup.cfg.""" + update_file.side_effect = ParsingError(source="simulating a captured error") + + original_file = """ + [flake8] + existing = value + """ + ProjectMock(tmp_path).style( + """ + ["setup.cfg".flake8] + new = "value" + """ + ).setup_cfg(original_file).api_apply().assert_violations( + Fuss( + True, + SETUP_CFG, + 324, + ": section [flake8] has some missing key/value pairs. Use this:", + """ + [flake8] + new = value + """, + ), + Fuss( + False, + SETUP_CFG, + Violations.PARSING_ERROR.code, + ": parsing error (ParsingError): Source contains parsing errors: 'simulating a captured error'", + ), + ).assert_file_contents( + SETUP_CFG, original_file + )