def test_sections(setup_cfg_path): updater = ConfigUpdater() updater.read(setup_cfg_path) exp_sections = [ 'metadata', 'options', 'options.packages.find', 'options.extras_require', 'test', 'tool:pytest', 'aliases', 'bdist_wheel', 'build_sphinx', 'devpi:upload', 'flake8', 'pyscaffold' ] assert updater.sections() == exp_sections
def save_config(): config = configparser.ConfigParser(allow_no_value=True) config.read("./config.ini") updater = ConfigUpdater() try: updater.read_file('config.ini') except Exception as rt_err: raise rt_err # config.set('Configuration', 'locale', self.config['locale']) # config.set('Configuration', 'font-size', self.config['font-size']) # Try New format all together for section_name in updater.sections(): print(section_name)
def config_updater_factory(config: Box) -> Tuple[Path, ConfigUpdater]: """Return a ConfigUpdater object and the path obj to the config file. Note, the config file must exist. There isn't too much error checking in this function. E. g. that will not work with an empty config file. """ config_updater_data = copy.deepcopy(config) if "config_file_path" in config_updater_data: path = Path(config_updater_data.pop("config_file_path")) else: raise AttributeError( "The config is missing the config_file_path. This should not happen." ) config_updater = ConfigUpdater() to_add = [] with open(path.as_posix()) as fh: config_updater.read_file(fh) for section in config_updater_data: if config_updater.has_section(section): config_updater_section = config_updater[section] last_option = config_updater_section.options()[-1] for option, value in config[section].items(): if option in config_updater_section: config_updater_section[option].value = value else: config_updater_section[last_option].add_after.option( option, value) last_option = option config_updater[section] = config_updater_section else: tmp_updater = ConfigUpdater() section_txt = f"[{section}]\n" + "\n".join( (f"{option}: {value}" for option, value in config_updater_data[section].items())) tmp_updater.read_string(section_txt) to_add.append(tmp_updater[section]) if to_add: last_section = config_updater[config_updater.sections()[-1]] for section in to_add: # Add a new line for readability config_updater[last_section.name].add_after.space().section( section) last_section = section return path, config_updater
def test_sections(setup_cfg_path): updater = ConfigUpdater() updater.read(setup_cfg_path) exp_sections = [ "metadata", "options", "options.packages.find", "options.extras_require", "test", "tool:pytest", "aliases", "bdist_wheel", "build_sphinx", "devpi:upload", "flake8", "pyscaffold", ] assert updater.sections() == exp_sections
class IniPlugin(NitpickPlugin): """Enforce configurations and autofix INI files. Examples of ``.ini`` files handled by this plugin: - `setup.cfg <https://docs.python.org/3/distutils/configfile.html>`_ - `.editorconfig <https://editorconfig.org/>`_ - `tox.ini <https://github.com/tox-dev/tox>`_ - `.pylintrc <https://pylint.readthedocs.io/en/latest/user_guide/run.html#command-line-options>`_ Style examples enforcing values on INI files: :gitref:`flake8 configuration <src/nitpick/resources/python/flake8.toml>`. """ fixable = True identify_tags = {"ini", "editorconfig"} violation_base_code = 320 updater: ConfigUpdater comma_separated_values: set[str] def post_init(self): """Post initialization after the instance was created.""" self.updater = ConfigUpdater() self.comma_separated_values = set(self.nitpick_file_dict.get(COMMA_SEPARATED_VALUES, [])) if not self.needs_top_section: return if all(isinstance(v, dict) for v in self.expected_config.values()): return new_config = dict({TOP_SECTION: {}}) for key, value in self.expected_config.items(): if isinstance(value, dict): new_config[key] = value continue new_config[TOP_SECTION][key] = value self.expected_config = new_config @property def needs_top_section(self) -> bool: """Return True if this .ini file needs a top section (e.g.: .editorconfig).""" return "editorconfig" in self.info.tags @property def current_sections(self) -> set[str]: """Current sections of the .ini file, including updated sections.""" return set(self.updater.sections()) @property def initial_contents(self) -> str: """Suggest the initial content for this missing file.""" return self.get_missing_output() @property def expected_sections(self) -> set[str]: """Expected sections (from the style config).""" return set(self.expected_config.keys()) @property def missing_sections(self) -> set[str]: """Missing sections.""" return self.expected_sections - self.current_sections def write_file(self, file_exists: bool) -> Fuss | None: """Write the new file.""" try: if self.needs_top_section: self.file_path.write_text(self.contents_without_top_section(str(self.updater))) return None 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 @staticmethod def contents_without_top_section(multiline_text: str) -> str: """Remove the temporary top section from multiline text, and keep the newline at the end of the file.""" return "\n".join(line for line in multiline_text.splitlines() if TOP_SECTION not in line) + "\n" def get_missing_output(self) -> str: """Get a missing output string example from the missing sections in an INI file.""" missing = self.missing_sections if not missing: return "" parser = ConfigParser() for section in sorted(missing, key=lambda s: "0" if s == TOP_SECTION else f"1{s}"): expected_config: dict = self.expected_config[section] if self.autofix: if self.updater.last_block: self.updater.last_block.add_after.space(1) self.updater.add_section(section) self.updater[section].update(expected_config) self.dirty = True parser[section] = expected_config return self.contents_without_top_section(self.get_example_cfg(parser)) # TODO: refactor: 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 an INI file.""" try: yield from self._read_file() except Error: return yield from self.enforce_missing_sections() csv_sections = {v.split(SECTION_SEPARATOR)[0] for v in self.comma_separated_values} missing_csv = csv_sections.difference(self.current_sections) if missing_csv: yield self.reporter.make_fuss( Violations.INVALID_COMMA_SEPARATED_VALUES_SECTION, ", ".join(sorted(missing_csv)) ) # Don't continue if the comma-separated values are invalid return for section in self.expected_sections.intersection(self.current_sections) - self.missing_sections: yield from self.enforce_section(section) def _read_file(self) -> Iterator[Fuss]: """Read the .ini file or special files like .editorconfig.""" parsing_err: Error | None = None try: self.updater.read(str(self.file_path)) except MissingSectionHeaderError as err: if self.needs_top_section: original_contents = self.file_path.read_text() self.updater.read_string(f"[{TOP_SECTION}]\n{original_contents}") return # If this is not an .editorconfig file, report this as a regular parsing error parsing_err = err except DuplicateOptionError as err: parsing_err = err if not parsing_err: return # Don't change the file if there was a parsing error self.autofix = False yield self.reporter.make_fuss(Violations.PARSING_ERROR, cls=parsing_err.__class__.__name__, msg=parsing_err) raise Error def enforce_missing_sections(self) -> Iterator[Fuss]: """Enforce missing sections.""" missing = self.get_missing_output() if missing: yield self.reporter.make_fuss(Violations.MISSING_SECTIONS, missing, self.autofix) def enforce_section(self, section: str) -> Iterator[Fuss]: """Enforce rules for a section.""" expected_dict = self.expected_config[section] actual_dict = {k: v.value for k, v in self.updater[section].items()} # TODO: refactor: add a class Ini(BaseDoc) and move this dictdiffer code there for diff_type, key, values in dictdiffer.diff(actual_dict, expected_dict): if diff_type == dictdiffer.CHANGE: if f"{section}.{key}" in self.comma_separated_values: yield from self.enforce_comma_separated_values(section, key, values[0], values[1]) else: yield from self.compare_different_keys(section, key, values[0], values[1]) elif diff_type == dictdiffer.ADD: yield from self.show_missing_keys(section, values) def enforce_comma_separated_values(self, section, key, raw_actual: Any, raw_expected: Any) -> Iterator[Fuss]: """Enforce sections and keys with comma-separated values. The values might contain spaces.""" actual_set = {s.strip() for s in raw_actual.split(",")} expected_set = {s.strip() for s in raw_expected.split(",")} missing = expected_set - actual_set if not missing: return joined_values = ",".join(sorted(missing)) value_to_append = f",{joined_values}" if self.autofix: self.updater[section][key].value += value_to_append self.dirty = True section_header = "" if section == TOP_SECTION else f"[{section}]\n" # TODO: test: top section with separated values in https://github.com/andreoliwa/nitpick/issues/271 yield self.reporter.make_fuss( Violations.MISSING_VALUES_IN_LIST, f"{section_header}{key} = (...){value_to_append}", key=key, fixed=self.autofix, ) def compare_different_keys(self, section, key, raw_actual: Any, raw_expected: Any) -> Iterator[Fuss]: """Compare different keys, with special treatment when they are lists or numeric.""" if isinstance(raw_actual, (int, float, bool)) or isinstance(raw_expected, (int, float, bool)): # A boolean "True" or "true" has the same effect on ConfigParser files. actual = str(raw_actual).lower() expected = str(raw_expected).lower() else: actual = raw_actual expected = raw_expected if actual == expected: return if self.autofix: self.updater[section][key].value = expected self.dirty = True if section == TOP_SECTION: yield self.reporter.make_fuss( Violations.TOP_SECTION_HAS_DIFFERENT_VALUE, f"{key} = {raw_expected}", key=key, actual=raw_actual, fixed=self.autofix, ) else: yield self.reporter.make_fuss( Violations.OPTION_HAS_DIFFERENT_VALUE, f"[{section}]\n{key} = {raw_expected}", section=section, key=key, actual=raw_actual, fixed=self.autofix, ) def show_missing_keys(self, section: str, values: list[tuple[str, Any]]) -> Iterator[Fuss]: """Show the keys that are not present in a section.""" parser = ConfigParser() missing_dict = dict(values) parser[section] = missing_dict output = self.get_example_cfg(parser) self.add_options_before_space(section, missing_dict) if section == TOP_SECTION: yield self.reporter.make_fuss( Violations.TOP_SECTION_MISSING_OPTION, self.contents_without_top_section(output), self.autofix ) else: yield self.reporter.make_fuss(Violations.MISSING_OPTION, output, self.autofix, section=section) def add_options_before_space(self, section: str, options: dict) -> None: """Add new options before a blank line in the end of the section.""" if not self.autofix: return space_removed = False while isinstance(self.updater[section].last_block, Space): space_removed = True self.updater[section].last_block.detach() self.updater[section].update(options) self.dirty = True if space_removed: self.updater[section].last_block.add_after.space(1) @staticmethod def get_example_cfg(parser: ConfigParser) -> str: """Print an example of a config parser in a string instead of a file.""" string_stream = StringIO() parser.write(string_stream) output = string_stream.getvalue().strip() return output
class CCFile: def __init__(self, filename): self.filename = filename self.updater = ConfigUpdater() self.updater.read(filename) for section in self.updater.sections_blocks(): validate(section, filename) self._readonly = None self._last_modified = time.time() self.slm = {} def __iter__(self) -> CCNode: for section in self.updater.sections_blocks(): yield CCNode(self, section) @property def readonly(self) -> bool: file = None try: file = open(self.filename, 'w') except: readonly = True else: readonly = False finally: if file is not None: file.close() return readonly def write(self): with contextlib.suppress(Exception): self.updater.update_file() def move_after(self, section: Section, reference: Section = None): self.updater.remove_section(section.name) if reference is None: sections = self.updater.sections() if len(sections) > 0: reference = self.updater[sections[0]] reference.add_before.section(section) else: self.updater.add_section(section) else: reference.add_after.section(section) self.tick() def add_node(self) -> Optional[CCNode]: if self.readonly is True: return None while 1: id = uuid.uuid4().hex try: self.updater.add_section(id) except DuplicateSectionError: continue except: return None else: break return CCNode(self, self.updater[id]) def remove_node(self, section: Section) -> bool: if self.readonly is True: return False self.updater.remove_section(section.name) return True @property def last_modified(self): return int(self._last_modified) def tick(self, last_modified=None): self.write() if last_modified is None: last_modified = time.time() self._last_modified = last_modified def get_section_last_modified(self, id) -> int: return self.slm.get(id, 0) def set_section_last_modified(self, id, last_modified=None): if last_modified is None: last_modified = time.time() self.slm[id] = last_modified
if sec == '_global_': continue for key in values[sec]: if values[sec][key] == 'comment_out' and updater[sec].get(key): logger.debug('commenting out [{}]->{}={}'.format( sec, key, updater[sec][key].value)) updater[sec][key].key = '#{}'.format(key) else: logger.debug('setting [{}]->{}={}'.format(sec, key, values[sec][key])) updater[sec][key] = values[sec][key] if values.get('_global_'): for key in values.get('_global_'): for secname in updater.sections(): if updater.has_option(secname, key): if values['_global_'][key] == 'comment_out' and updater[ secname].get(key): logger.debug('commenting out [{}]->{}={}'.format( secname, key, updater[secname][key].value)) updater[secname][key].key = '#{}'.format(key) else: updater[secname][key] = values['_global_'][key] logger.debug('{} found in [{}] setting to {}'.format( key, secname, values['_global_'][key])) if args.get('output'): logger.debug('writing output: {}'.format(args.get('output'))) output_file_handle = open(args['output'], 'w') else: