Пример #1
0
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()
Пример #3
0
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
Пример #4
0
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"
Пример #5
0
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')
Пример #7
0
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
Пример #8
0
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")
Пример #9
0
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)