def test_set_option(): updater = ConfigUpdater(allow_no_value=True) updater.read_string(test1_cfg_in) updater.set("default", "key", "1") assert updater["default"]["key"].value == "1" updater.set("default", "key", 2) assert updater["default"]["key"].value == 2 assert str(updater) == test1_cfg_out updater.set("default", "key") assert updater["default"]["key"].value is None assert str(updater) == test1_cfg_out_none updater.read_string(test1_cfg_in) updater.set("default", "other_key", 3) assert str(updater) == test1_cfg_out_added updater.read_string(test1_cfg_in) values = ["param1", "param2"] updater["default"]["key"].set_values(values) assert str(updater) == test1_cfg_out_values assert values == ["param1", "param2"] # non destructive operation with pytest.raises(NoSectionError): updater.set("wrong_section", "key", "1") new_option = copy.deepcopy(updater["default"]["key"]) updater["default"]["key"] = new_option assert updater["default"]["key"] is new_option new_option = copy.deepcopy(updater["default"]["key"]) new_option.key = "wrong_key" with pytest.raises(ValueError): updater["default"]["key"] = new_option
def test_validate_format(setup_cfg_path): updater = ConfigUpdater(allow_no_value=False) updater.read(setup_cfg_path) updater.validate_format() updater.set('metadata', 'author') with pytest.raises(ParsingError): updater.validate_format()
def test_validate_format_propagates_kwargs(setup_cfg_path): updater = ConfigUpdater(allow_no_value=True) updater.read(setup_cfg_path) updater.set("pyscaffold", "namespace") assert updater["pyscaffold"]["namespace"].value is None assert str( updater["pyscaffold"]["namespace"]) == "namespace\n" # no `=` sign assert updater.validate_format() is True
def test_update_file_issue_68(tmp_path): # allow_no_value = False file = tmp_path / "file.cfg" file.write_text("[section]") cfg = ConfigUpdater(allow_no_value=False).read_file(open(file)) cfg.set("section", "option") with pytest.warns(NoneValueDisallowed): # A warning is issued, but the method will not fail cfg.update_file() assert file.read_text().strip() == "[section]" # allow_no_value = True file.write_text("[section]") cfg = ConfigUpdater(allow_no_value=True).read_file(open(file)) cfg.set("section", "option") cfg.update_file() assert file.read_text().strip() == "[section]\noption"
class ProdUpdater(Updater): """Production dependency (setup.cfg) updater.""" def __init__(self): self.file = Path("setup.cfg") self.file_py = self.file.with_suffix(".py") self.config = ConfigUpdater() self.indent = " " # default to 2 spaces indent? def read(self): try: text = self.file.read_text("utf8") except OSError: return self.indent = detect_indent(text) or self.indent self.config.read_string(text) def get_requirements(self): self.read() try: return self.config.get("options", "install_requires").value except configparser.Error: pass return "" def get_name(self): self.read() return self.config.get("metadata", "name").value def get_spec(self, name, version): if not version.startswith("0."): version = re.match("\d+\.\d+", version).group() return "{}~={}".format(name, version) def write_requirements(self, lines): if "options" not in self.config: self.config.add_section("options") self.config.set("options", "install_requires", "".join("\n" + self.indent + l for l in lines)) self.file.write_text(str(self.config).replace("\r", ""), encoding="utf8") if not self.file_py.exists(): self.file_py.write_text("\n".join( ["from setuptools import setup", "setup()"]), encoding="utf8")
def test_set_option(): updater = ConfigUpdater() updater.read_string(test1_cfg_in) updater.set('default', 'key', '1') assert updater['default']['key'].value == '1' updater.set('default', 'key', 2) assert updater['default']['key'].value == 2 assert str(updater) == test1_cfg_out updater.set('default', 'key') assert updater['default']['key'].value is None assert str(updater) == test1_cfg_out_none updater.read_string(test1_cfg_in) updater.set('default', 'other_key', 3) assert str(updater) == test1_cfg_out_added updater.read_string(test1_cfg_in) updater['default']['key'].set_values(['param1', 'param2']) assert str(updater) == test1_cfg_out_values with pytest.raises(NoSectionError): updater.set('wrong_section', 'key', '1')
def bootstrap_config(path: Optional[Path] = None) -> ConfigUpdater: """Create the Config file and populate it.""" if path is None: path = Path( os.path.expanduser("~")) / ".config" / "discovergy" / "config.ini" if not path.parent.is_dir(): os.makedirs(path.parent.as_posix()) config_updater = ConfigUpdater() config_updater.read_string(DEFAULT_CONFIG) discovergy_account_email = input( "Please tell me your Discovergy account user e-mail: ") discovergy_account_password = getpass.getpass( "Please tell me your Discovergy account password: "******"Do you want to save the password to the config file? It will be required to fetch a new " "Oauth token once the current one expires. (y/n)") if save_account_password_input.lower() == "y": save_account_password = True break elif save_account_password_input.lower() == "n": save_account_password = False break open_weather_map = input( "Please enter the Open Weather Map id. <Enter> for none.") latitude = input( "Please enter the latitude of the location. <Enter> for none.") longitude = input( "Please enter the latitude of the location. <Enter> for none.") config_updater.set("discovergy_account", "email", value=discovergy_account_email) config_updater.set("discovergy_account", "password", value=discovergy_account_password) config_updater.set("discovergy_account", "save_password", value=save_account_password) config_updater.set("open_weather_map", "id", value=open_weather_map) config_updater.set("open_weather_map", "latitude", value=latitude) config_updater.set("open_weather_map", "longitude", value=longitude) write_config_updater(path, config_updater) return config_updater
class EncryptedConfigFile(object): """Wrap encrypted config files.""" lockfd = None _cleartext = None # Additional GPG parameters. Used for testing. gpg_opts = "" GPG_BINARY_CANDIDATES = ["gpg", "gpg2"] def __init__(self, encrypted_file, write_lock=False, quiet=False): """Context manager that opens an encrypted file. Use the read() and write() methods in the subordinate "with" block to manipulate cleartext content. If the cleartext content has been replaced, the encrypted file is updated. `write_lock` must be set True if a modification of the file is intended. """ self.encrypted_file = encrypted_file self.write_lock = write_lock self.quiet = quiet def __enter__(self): self._lock() return self def __exit__(self, _exc_type, _exc_value, _traceback): self.lockfd.close() def gpg(self, cmdline): null = tempfile.TemporaryFile() for gpg in self.GPG_BINARY_CANDIDATES: try: subprocess.check_call([gpg, "--version"], stdout=null, stderr=null) except (subprocess.CalledProcessError, OSError): pass else: return "{} {}".format(gpg, cmdline) raise RuntimeError("Could not find gpg binary." " Is GPG installed? I tried looking for: {}".format( ", ".join("`{}`".format(x) for x in self.GPG_BINARY_CANDIDATES))) @property def cleartext(self): if self._cleartext is None: return NEW_FILE_TEMPLATE return self._cleartext @cleartext.setter def cleartext(self, value): self.config = ConfigUpdater() self.config.read_string(value) self.set_members(self.get_members()) s = io.StringIO() self.config.write(s) self._cleartext = s.getvalue() def read(self): if self._cleartext is None: if os.stat(self.encrypted_file).st_size: self._decrypt() return self.cleartext def write(self, cleartext): """Replace encrypted file with new content.""" if not self.write_lock: raise RuntimeError("write() needs a write lock") self.cleartext = cleartext self._encrypt() def write_config(self): s = io.StringIO() self.config.write(s) self.write(s.getvalue()) def _lock(self): self.lockfd = open(self.encrypted_file, self.write_lock and "a+" or "r+") try: if self.write_lock: fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB) else: fcntl.lockf(self.lockfd, fcntl.LOCK_SH | fcntl.LOCK_NB) except BlockingIOError: raise FileLockedError(self.encrypted_file) def _decrypt(self): opts = self.gpg_opts if self.quiet: opts += " -q --no-tty --batch" self.cleartext = subprocess.check_output( [self.gpg("{} --decrypt {}".format(opts, self.encrypted_file))], stderr=NULL, shell=True, ).decode("utf-8") def get_members(self): members = self.config.get("batou", "members").value.split(",") members = [x.strip() for x in members] members = [_f for _f in members if _f] members.sort() return members def set_members(self, members): # The whitespace here is exactly what # "members = " looks like in the config file so we get # proper indentation. members = ",\n ".join(members) self.config.set("batou", "members", members) def _encrypt(self): recipients = self.get_members() if not recipients: raise ValueError("Need at least one recipient.") self.set_members(self.get_members()) recipients = " ".join( ["-r {}".format(shlex.quote(r.strip())) for r in recipients]) os.rename(self.encrypted_file, self.encrypted_file + ".old") try: gpg = subprocess.Popen( [ self.gpg("{} --encrypt {} -o {}".format( self.gpg_opts, recipients, self.encrypted_file)) ], stdin=subprocess.PIPE, shell=True, ) gpg.communicate(self.cleartext.encode("utf-8")) if gpg.returncode != 0: raise RuntimeError("GPG returned non-zero exit code.") except Exception: os.rename(self.encrypted_file + ".old", self.encrypted_file) raise else: os.unlink(self.encrypted_file + ".old")
class EncryptedConfigFile(object): """Wrap encrypted config files. Manages keys based on the data in the configuration. Also allows management of additional files with the same keys. """ def __init__(self, encrypted_file, add_files_for_env: Optional[str] = None, write_lock=False, quiet=False): self.add_files_for_env = add_files_for_env self.write_lock = write_lock self.quiet = quiet self.files = {} self.main_file = self.add_file(encrypted_file) # Add all existing files to the session if self.add_files_for_env: for path in iter_other_secrets(self.add_files_for_env): self.add_file(path) def add_file(self, filename): # Ensure compatibility with pathlib. filename = str(filename) if filename not in self.files: self.files[filename] = f = EncryptedFile(filename, self.write_lock, self.quiet) f.read() return self.files[filename] def __enter__(self): self.main_file.__enter__() # Ensure `self.config` self.read() return self def __exit__(self, _exc_type=None, _exc_value=None, _traceback=None): self.main_file.__exit__() if not self.get_members(): os.unlink(self.main_file.encrypted_filename) def read(self): self.main_file.read() if not self.main_file.cleartext: self.main_file.cleartext = NEW_FILE_TEMPLATE self.config = ConfigUpdater() self.config.read_string(self.main_file.cleartext) self.set_members(self.get_members()) def write(self): s = io.StringIO() self.config.write(s) self.main_file.cleartext = s.getvalue() for file in self.files.values(): file.recipients = self.get_members() file.write() def get_members(self): if 'batou' not in self.config: self.config.add_section('batou') try: members = self.config.get("batou", "members").value.split(",") except Exception: return [] members = [x.strip() for x in members] members = [_f for _f in members if _f] members.sort() return members def set_members(self, members): # The whitespace here is exactly what # "members = " looks like in the config file so we get # proper indentation. members = ",\n ".join(members) self.config.set("batou", "members", members)