def test_exhaustive_roundtrip(self) -> None: for example in variations("sect", "a", "1"): oracle = configparser.RawConfigParser() try: oracle.read_string(example) self.assertEqual(oracle["sect"]["a"], "1") except Exception: # pragma: no cover continue conf = None try: conf = imperfect.parse_string(example) # Did we parse the same value as configparser self.assertEqual(len(conf["sect"].entries), 1) self.assertEqual(conf["sect"].entries[0].key, "a") self.assertEqual(conf["sect"].entries[0].interpret_value(), "1") except Exception: # pragma: no cover print("Input: ", repr(example)) if conf: # if conf.default: # print("default:") # for v in conf.default.entries: # print(" ", v) for s in conf.sections: print(f"{s.name}:") for v in s.entries: print(" ", v) raise self.assertEqual(example, conf.text)
def from_setup_cfg(path: Path, markers: Dict[str, Any]) -> Distribution: cfg = imperfect.parse_string((path / "setup.cfg").read_text()) d = Distribution() d.metadata_version = "2.1" for field in SETUP_ARGS: name = field.get_distribution_key() if not hasattr(d, name): continue cls = field.cfg.writer_cls if cls is SectionWriter: try: raw_section_data = cfg[field.cfg.section] except KeyError: continue # ConfigSection behaves like a Dict[str, str] so this is fine parsed = SectionWriter().from_ini_section( raw_section_data) # type: ignore else: try: # All fields are defined as underscore, but it appears # setuptools normalizes so dashes are ok too. key = field.cfg.key if key not in cfg[field.cfg.section]: key = key.replace("_", "-") raw_data = cfg[field.cfg.section][key] except KeyError: continue parsed = cls().from_ini(raw_data) setattr(d, name, parsed) return d
def test_example_from_readme(self) -> None: INPUT = """ [metadata] # the package name name = imperfect # slurp the readme long_description = file: README.md [options] packages = imperfect """ EXPECTED = """ [metadata] # the package name name = imperfect # slurp the readme long_description = file: README.md long_description_content_type = text/markdown [options] packages = imperfect """ import imperfect import moreorless data = INPUT conf: imperfect.ConfigFile = imperfect.parse_string(data) metadata_section = conf["metadata"] # Ignoring some whitespace for now, this looks like # long_description_content_type = text/markdown\n # [ entry ] # [ key ][eq][ value ] value = imperfect.ValueLine( whitespace_before_text="", text="text/markdown", whitespace_after_text="", newline="\n", ) new_entry = imperfect.ConfigEntry( key="long_description_content_type", equals="=", value=[value], whitespace_before_equals=" ", whitespace_before_value=" ", ) for i, entry in enumerate(metadata_section.entries): if entry.key == "long_description": metadata_section.entries.insert(i + 1, new_entry) break self.assertEqual(EXPECTED, conf.text) temp = moreorless.unified_diff(data, conf.text, "setup.cfg") # print(temp, end="") self.assertTrue(temp)
def test_simple_parse(self, example: str) -> None: conf = imperfect.parse_string(example) # Minimal mapping api self.assertEqual(["s"], conf.keys()) self.assertTrue("s" in conf) self.assertEqual(["a"], conf["s"].keys()) self.assertTrue("a" in conf["s"]) # KeyError coverage self.assertFalse("b" in conf) self.assertFalse("b" in conf["s"]) self.assertIn(conf["s"]["a"], ("1", "\n1"))
def check(self, env: Env, autoapply: bool = False) -> None: tox_ini_path = env.base_path / "tox.ini" setup_cfg_path = env.base_path / "setup.cfg" if not tox_ini_path.exists(): return tox_ini_text = tox_ini_path.read_text() setup_cfg_text = "" if setup_cfg_path.exists(): setup_cfg_text = setup_cfg_path.read_text() setup_cfg = parse_string(setup_cfg_text) tox_ini = parse_string(tox_ini_text) for section in tox_ini.keys(): # This doesn't iterate and use set_value because that wouldn't # preserve comments. obj = tox_ini[section] if obj.name == "tox": obj.name = "tox:tox" if obj.name in setup_cfg: LOG.error(f"Section {obj.name!r} already exists in setup.cfg") else: if obj.leading_whitespace == "" and setup_cfg.sections: obj.leading_whitespace = "\n" setup_cfg.sections.append(obj) new_text = setup_cfg.text if new_text != setup_cfg_text: echo_color_unified_diff(setup_cfg_text, new_text, "setup.cfg") if autoapply: setup_cfg_path.write_text(new_text) tox_ini_path.unlink() print("Written") else: print("Rerun with -a instead to apply")
def verify(name: str) -> None: with open(name) as f: data = f.read() conf = parse_string(data) buf = io.StringIO() conf.build(buf) if data != buf.getvalue(): print(name, "FAIL ROUND TRIP") return try: co = configparser.RawConfigParser() co.read_string(data) except Exception as e: print(name, "FAIL TO READ IN CONFIGPARSER", e) return compares = 0 for section_name in co: for key in co[section_name]: compares += 1 expected_value = co[section_name][key] try: if conf[section_name][key] != expected_value: print(name, section_name, key, "FAIL COMPARE") print("configpar:", repr(expected_value)) print("imperfect:", repr(conf[section_name][key])) return except Exception as e: print(name, section_name, key, "FAIL", str(e)) return if compares == 0: print(name, "FAIL EMPTY") return print(name, "OK")
def check(self, env: Env, autoapply: bool = False) -> None: # TODO: This duplicates some of the logic in # setup_py_parsing because we don't want a Distribution and # want to get the cst object back. setup_py = env.base_path / "setup.py" if not setup_py.exists(): return module = cst.parse_module(setup_py.read_text()) analyzer = SetupCallAnalyzer() wrapper = cst.MetadataWrapper(module) wrapper.visit(analyzer) if not analyzer.found_setup: self.skip("Could not find setup() call in setup.py") setup_cfg = env.base_path / "setup.cfg" if setup_cfg.exists(): cfg_data = setup_cfg.read_text() else: cfg_data = "" cfg = imperfect.parse_string( cfg_data + ("\n" if not cfg_data.endswith("\n") else "")) setup_args_dict = {t.keyword: t for t in SETUP_ARGS} keywords_to_change: Dict[str, Optional[cst.CSTNode]] = {} for k, v in analyzer.saved_args.items(): # This is a cursory check on the value; really we should get an # AlmostTooComplicated or TooComplicated that works well. if (k in setup_args_dict and isinstance(v, Literal) and "??" not in str(v.value)): LOG.debug(f"{k}: can move {v.value!r}") keywords_to_change[k] = None if setup_args_dict[k].cfg.writer_cls is SectionWriter: for k2, v2 in v.value.items(): cfg.set_value( setup_args_dict[k].cfg.section, k2, setup_args_dict[k].cfg.writer_cls().to_ini(v2), ) else: cfg.set_value( setup_args_dict[k].cfg.section, setup_args_dict[k].cfg.key, setup_args_dict[k].cfg.writer_cls().to_ini(v.value), ) elif k in setup_args_dict: # pragma: no cover LOG.error(f"{k}: too complicated") else: # pragma: no cover LOG.debug(f"{k}: unknown keyword") buf = io.StringIO() cfg.build(buf) echo_color_unified_diff(cfg_data, buf.getvalue(), "setup.cfg") old_code = module.code transformer = SetupCallTransformer( cst.ensure_type(analyzer.setup_node, cst.Call), keywords_to_change) new_module = wrapper.visit(transformer) new_code = new_module.code echo_color_unified_diff(old_code, new_code, "setup.py") # TODO validate that the right args got removed # print(f"And remove {sorted(keywords_to_change)}") if autoapply: setup_cfg.write_text(buf.getvalue()) setup_py.write_text(new_code) elif keywords_to_change: print("Rerun with -a instead to apply")
def test_simple_roundtrips(self, example: str) -> None: conf = imperfect.parse_string(example) self.assertEqual(example, conf.text)
def test_comment_prefixes(self) -> None: conf = imperfect.parse_string("[s]\n@comment\na=1", comment_prefixes=("@",)) self.assertEqual("1", conf["s"]["a"])
def test_alternate_delimiters_allow_no_value(self) -> None: conf = imperfect.parse_string( "[s]\naqq", delimiters=("qq",), allow_no_value=True ) self.assertEqual("", conf["s"]["a"])
def test_alternate_delimiters(self) -> None: conf = imperfect.parse_string("[s]\naqq1", delimiters=("qq",)) self.assertEqual("1", conf["s"]["a"])
def test_allow_no_value(self) -> None: conf = imperfect.parse_string("[s]\na=", allow_no_value=True) self.assertEqual("", conf["s"]["a"])
def test_multiline_with_comment(self) -> None: conf = imperfect.parse_string("[s]\na=\n #comment\n b\n") self.assertEqual("\nb", conf["s"]["a"])
def test_fail_to_parse(self, example: str) -> None: with self.assertRaises(imperfect.ParseError): imperfect.parse_string(example)