def test_get_method(setup_cfg_path): updater = ConfigUpdater() updater.read(setup_cfg_path) value = updater.get('metadata', 'license').value assert value == 'mit' with pytest.raises(NoSectionError): updater.get('non_existent_section', 'license') with pytest.raises(NoOptionError): updater.get('metadata', 'wrong_key')
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 write_config_updater(path: Path, config: ConfigUpdater) -> None: """Write the config file.""" to_write_config = copy.deepcopy(config) # Do not save the pwd if that's not wanted! if (config.has_option("discovergy_account", "save_password") and not config.get("discovergy_account", "save_password").value): to_write_config.set("discovergy_account", "password", value="") with os.fdopen(os.open(path.as_posix(), os.O_WRONLY | os.O_CREAT, 0o600), "w") as fh: to_write_config.write(fh)
def test_entry_point(tmpfolder): args = ["--no-config", "--custom-extension", "pyscaffoldext-some_extension"] # --no-config: avoid extra config from dev's machine interference cli.main(args) assert Path("pyscaffoldext-some_extension/setup.cfg").exists() setup_cfg = ConfigUpdater() setup_cfg.read_string(Path("pyscaffoldext-some_extension/setup.cfg").read_text()) entry_point = setup_cfg.get("options.entry_points", "pyscaffold.cli").value expected = "\nsome_extension = pyscaffoldext.some_extension.extension:SomeExtension" assert entry_point == expected
def test_add_install_requires(tmpfolder): args = [ "--no-config", "--custom-extension", "pyscaffoldext-some_extension" ] # --no-config: avoid extra config from dev's machine interference cli.main(args) assert Path("pyscaffoldext-some_extension/setup.cfg").exists() setup_cfg = ConfigUpdater() setup_cfg.read_string( Path("pyscaffoldext-some_extension/setup.cfg").read_text()) install_requires = setup_cfg.get("options", "install_requires").value assert "pyscaffold" in install_requires
def test_get_method(setup_cfg_path): updater = ConfigUpdater() updater.read(setup_cfg_path) value = updater.get("metadata", "license").value assert value == "mit" with pytest.raises(NoSectionError): updater.get("non_existent_section", "license") with pytest.raises(NoOptionError): updater.get("metadata", "wrong_key") assert updater.get("metadata", "wrong_key", fallback=None) is None
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)