aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/pkgdev/cli.py122
-rw-r--r--src/pkgdev/const.py41
-rw-r--r--src/pkgdev/scripts/pkgdev_commit.py5
-rw-r--r--src/pkgdev/scripts/pkgdev_manifest.py3
-rw-r--r--src/pkgdev/scripts/pkgdev_push.py5
-rw-r--r--src/pkgdev/scripts/pkgdev_showkw.py4
-rw-r--r--tests/scripts/test_cli.py90
-rw-r--r--tests/scripts/test_pkgdev_commit.py24
-rw-r--r--tests/scripts/test_pkgdev_push.py2
-rw-r--r--tests/scripts/test_pkgdev_showkw.py21
10 files changed, 297 insertions, 20 deletions
diff --git a/src/pkgdev/cli.py b/src/pkgdev/cli.py
index f8bac11..c4ca30e 100644
--- a/src/pkgdev/cli.py
+++ b/src/pkgdev/cli.py
@@ -1,8 +1,17 @@
"""Various command-line specific support."""
+import argparse
+import configparser
import logging
+import os
from pkgcore.util import commandline
+from snakeoil.cli import arghparse
+from snakeoil.contexts import patch
+from snakeoil.klass import jit_attr_none
+from snakeoil.mappings import OrderedSet
+
+from . import const
class Tool(commandline.Tool):
@@ -11,3 +20,116 @@ class Tool(commandline.Tool):
# suppress all pkgcore log messages
logging.getLogger('pkgcore').setLevel(100)
return super().main()
+
+
+class ConfigArg(argparse._StoreAction):
+ """Store config path string or False when explicitly disabled."""
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ if values.lower() in ('false', 'no', 'n'):
+ values = False
+ setattr(namespace, self.dest, values)
+
+
+class ConfigParser(configparser.ConfigParser):
+ """ConfigParser with case-sensitive keys (default forces lowercase)."""
+
+ def optionxform(self, option):
+ return option
+
+
+class ConfigFileParser:
+ """Argument parser that supports loading settings from specified config files."""
+
+ default_configs = (const.SYSTEM_CONF_FILE, const.USER_CONF_FILE)
+
+ def __init__(self, parser: arghparse.ArgumentParser, configs=(), **kwargs):
+ super().__init__(**kwargs)
+ self.parser = parser
+ self.configs = OrderedSet(configs)
+
+ @jit_attr_none
+ def config(self):
+ return self.parse_config()
+
+ def parse_config(self, configs=()):
+ """Parse given config files."""
+ configs = configs if configs else self.configs
+ config = ConfigParser(default_section=None)
+ try:
+ for f in configs:
+ config.read(f)
+ except configparser.ParsingError as e:
+ self.parser.error(f'parsing config file failed: {e}')
+ return config
+
+ def parse_config_sections(self, namespace, sections):
+ """Parse options from a given iterable of config section names."""
+ assert self.parser.prog.startswith('pkgdev ')
+ module = self.parser.prog.split(' ', 1)[1] + '.'
+ with patch('snakeoil.cli.arghparse.ArgumentParser.error', self._config_error):
+ for section in (x for x in sections if x in self.config):
+ config_args = ((k.split('.', 1)[1], v) for k, v in self.config.items(section) if k.startswith(module))
+ config_args = (f'--{k}={v}' if v else f'--{k}' for k, v in config_args)
+ namespace, args = self.parser.parse_known_optionals(config_args, namespace)
+ if args:
+ self.parser.error(f"unknown arguments: {' '.join(args)}")
+ return namespace
+
+ def parse_config_options(self, namespace, configs=()):
+ """Parse options from config if they exist."""
+ configs = list(filter(os.path.isfile, configs))
+ if not configs:
+ return namespace
+
+ self.configs.update(configs)
+ # reset jit attr to force reparse
+ self._config = None
+
+ # load default options
+ namespace = self.parse_config_sections(namespace, ['DEFAULT'])
+
+ return namespace
+
+ def _config_error(self, message, status=2):
+ """Stub to replace error method that notes config failure."""
+ self.parser.exit(status, f'{self.parser.prog}: failed loading config: {message}\n')
+
+class ArgumentParser(arghparse.ArgumentParser):
+ """Parse all known arguments, from command line and config file."""
+
+ def __init__(self, parents=(), **kwargs):
+ self.config_argparser = arghparse.ArgumentParser(suppress=True)
+ config_options = self.config_argparser.add_argument_group('config options')
+ config_options.add_argument(
+ '--config', action=ConfigArg, dest='config_file',
+ help='use custom pkgdev settings file',
+ docs="""
+ Load custom pkgdev scan settings from a given file.
+
+ Note that custom user settings override all other system and repo-level
+ settings.
+
+ It's also possible to disable all types of settings loading by
+ specifying an argument of 'false' or 'no'.
+ """)
+ super().__init__(parents=[*parents, self.config_argparser], **kwargs)
+
+ def parse_known_args(self, args=None, namespace=None):
+ temp_namespace, _ = self.config_argparser.parse_known_args(args, namespace)
+ # parser supporting config file options
+ config_parser = ConfigFileParser(self)
+ # always load settings from bundled config
+ namespace = config_parser.parse_config_options(
+ namespace, configs=[const.BUNDLED_CONF_FILE])
+
+ # load default args from system/user configs if config-loading is allowed
+ if temp_namespace.config_file is None:
+ namespace = config_parser.parse_config_options(
+ namespace, configs=ConfigFileParser.default_configs)
+ elif temp_namespace.config_file is not False:
+ namespace = config_parser.parse_config_options(
+ namespace, configs=(namespace.config_file, ))
+
+ # parse command line args to override config defaults
+ return super().parse_known_args(args, namespace)
diff --git a/src/pkgdev/const.py b/src/pkgdev/const.py
new file mode 100644
index 0000000..e724f5a
--- /dev/null
+++ b/src/pkgdev/const.py
@@ -0,0 +1,41 @@
+"""Internal constants."""
+
+import os
+import sys
+
+from snakeoil import mappings
+
+_reporoot = os.path.realpath(__file__).rsplit(os.path.sep, 3)[0]
+_module = sys.modules[__name__]
+
+try:
+ # This is a file written during installation;
+ # if it exists, we defer to it. If it doesn't, then we're
+ # running from a git checkout or a tarball.
+ from . import _const as _defaults
+except ImportError: # pragma: no cover
+ _defaults = object()
+
+
+def _GET_CONST(attr, default_value):
+ consts = mappings.ProxiedAttrs(_module)
+ default_value %= consts
+ return getattr(_defaults, attr, default_value)
+
+
+# determine XDG compatible paths
+for xdg_var, var_name, fallback_dir in (
+ ('XDG_CONFIG_HOME', 'USER_CONFIG_PATH', '~/.config'),
+ ('XDG_CACHE_HOME', 'USER_CACHE_PATH', '~/.cache'),
+ ('XDG_DATA_HOME', 'USER_DATA_PATH', '~/.local/share')):
+ setattr(
+ _module, var_name,
+ os.environ.get(xdg_var, os.path.join(os.path.expanduser(fallback_dir), 'pkgdev')))
+
+REPO_PATH = _GET_CONST('REPO_PATH', _reporoot)
+DATA_PATH = _GET_CONST('DATA_PATH', '%(REPO_PATH)s/data')
+
+USER_CACHE_DIR = getattr(_module, 'USER_CACHE_PATH')
+USER_CONF_FILE = os.path.join(getattr(_module, 'USER_CONFIG_PATH'), 'pkgdev.conf')
+SYSTEM_CONF_FILE = '/etc/pkgdev/pkgdev.conf'
+BUNDLED_CONF_FILE = os.path.join(DATA_PATH, 'pkgdev.conf')
diff --git a/src/pkgdev/scripts/pkgdev_commit.py b/src/pkgdev/scripts/pkgdev_commit.py
index 3bb9044..1c2ff66 100644
--- a/src/pkgdev/scripts/pkgdev_commit.py
+++ b/src/pkgdev/scripts/pkgdev_commit.py
@@ -26,18 +26,19 @@ from snakeoil.klass import jit_attr
from snakeoil.mappings import OrderedFrozenSet, OrderedSet
from snakeoil.osutils import pjoin
-from .. import git
+from .. import cli, git
from ..mangle import GentooMangler, Mangler
from .argparsers import cwd_repo_argparser, git_repo_argparser
-class ArgumentParser(arghparse.ArgumentParser):
+class ArgumentParser(cli.ArgumentParser):
"""Parse all known arguments, passing unknown arguments to ``git commit``."""
def parse_known_args(self, args=None, namespace=None):
namespace.footer = OrderedSet()
namespace.git_add_files = []
namespace, args = super().parse_known_args(args, namespace)
+
if namespace.dry_run:
args.append('--dry-run')
if namespace.verbosity:
diff --git a/src/pkgdev/scripts/pkgdev_manifest.py b/src/pkgdev/scripts/pkgdev_manifest.py
index b54b90f..26f6534 100644
--- a/src/pkgdev/scripts/pkgdev_manifest.py
+++ b/src/pkgdev/scripts/pkgdev_manifest.py
@@ -5,9 +5,10 @@ from pkgcore.restrictions import packages
from pkgcore.util.parserestrict import parse_match
from snakeoil.cli import arghparse
+from .. import cli
from .argparsers import cwd_repo_argparser
-manifest = arghparse.ArgumentParser(
+manifest = cli.ArgumentParser(
prog='pkgdev manifest', description='update package manifests',
parents=(cwd_repo_argparser,))
manifest.add_argument(
diff --git a/src/pkgdev/scripts/pkgdev_push.py b/src/pkgdev/scripts/pkgdev_push.py
index 9913167..e98e955 100644
--- a/src/pkgdev/scripts/pkgdev_push.py
+++ b/src/pkgdev/scripts/pkgdev_push.py
@@ -2,14 +2,13 @@ import argparse
import shlex
from pkgcheck import reporters, scan
-from snakeoil.cli import arghparse
from snakeoil.cli.input import userquery
-from .. import git
+from .. import cli, git
from .argparsers import cwd_repo_argparser, git_repo_argparser
-class ArgumentParser(arghparse.ArgumentParser):
+class ArgumentParser(cli.ArgumentParser):
"""Parse all known arguments, passing unknown arguments to ``git push``."""
def parse_known_args(self, args=None, namespace=None):
diff --git a/src/pkgdev/scripts/pkgdev_showkw.py b/src/pkgdev/scripts/pkgdev_showkw.py
index 1b5ab8e..27a5a25 100644
--- a/src/pkgdev/scripts/pkgdev_showkw.py
+++ b/src/pkgdev/scripts/pkgdev_showkw.py
@@ -6,13 +6,13 @@ from functools import partial
from pkgcore.ebuild import restricts
from pkgcore.util import commandline
from pkgcore.util import packages as pkgutils
-from snakeoil.cli import arghparse
from snakeoil.strings import pluralism
+from .. import cli
from .._vendor.tabulate import tabulate, tabulate_formats
-showkw = arghparse.ArgumentParser(
+showkw = cli.ArgumentParser(
prog='pkgdev showkw', description='show package keywords')
showkw.add_argument(
'targets', metavar='target', nargs='*',
diff --git a/tests/scripts/test_cli.py b/tests/scripts/test_cli.py
new file mode 100644
index 0000000..752f36d
--- /dev/null
+++ b/tests/scripts/test_cli.py
@@ -0,0 +1,90 @@
+import textwrap
+
+import pytest
+from pkgdev import cli
+from snakeoil.cli import arghparse
+
+
+class TestConfigFileParser:
+
+ @pytest.fixture(autouse=True)
+ def _create_argparser(self, tmp_path):
+ self.config_file = str(tmp_path / 'config')
+ self.parser = arghparse.ArgumentParser(prog='pkgdev cli_test')
+ self.namespace = arghparse.Namespace()
+ self.config_parser = cli.ConfigFileParser(self.parser)
+
+ def test_no_configs(self):
+ config = self.config_parser.parse_config(())
+ assert config.sections() == []
+ namespace = self.config_parser.parse_config_options(self.namespace)
+ assert vars(namespace) == {}
+
+ def test_ignored_configs(self):
+ # nonexistent config files are ignored
+ config = self.config_parser.parse_config(('foo', 'bar'))
+ assert config.sections() == []
+
+ def test_bad_config_format_no_section(self, capsys):
+ with open(self.config_file, 'w') as f:
+ f.write('foobar\n')
+ with pytest.raises(SystemExit) as excinfo:
+ self.config_parser.parse_config((self.config_file,))
+ out, err = capsys.readouterr()
+ assert not out
+ assert 'parsing config file failed: File contains no section headers' in err
+ assert self.config_file in err
+ assert excinfo.value.code == 2
+
+ def test_bad_config_format(self, capsys):
+ with open(self.config_file, 'w') as f:
+ f.write(textwrap.dedent("""
+ [DEFAULT]
+ foobar
+ """))
+ with pytest.raises(SystemExit) as excinfo:
+ self.config_parser.parse_config((self.config_file,))
+ out, err = capsys.readouterr()
+ assert not out
+ assert 'parsing config file failed: Source contains parsing errors' in err
+ assert excinfo.value.code == 2
+
+ def test_nonexistent_config_options(self, capsys):
+ """Nonexistent parser arguments don't cause errors."""
+ with open(self.config_file, 'w') as f:
+ f.write(textwrap.dedent("""
+ [DEFAULT]
+ cli_test.foo=bar
+ """))
+ with pytest.raises(SystemExit) as excinfo:
+ self.config_parser.parse_config_options(None, configs=(self.config_file,))
+ out, err = capsys.readouterr()
+ assert not out
+ assert 'failed loading config: unknown arguments: --foo=bar' in err
+ assert excinfo.value.code == 2
+
+ def test_config_options_other_prog(self):
+ self.parser.add_argument('--foo')
+ with open(self.config_file, 'w') as f:
+ f.write(textwrap.dedent("""
+ [DEFAULT]
+ other.foo=bar
+ """))
+ namespace = self.parser.parse_args(['--foo', 'foo'])
+ assert namespace.foo == 'foo'
+ # config args don't override not matching namespace attrs
+ namespace = self.config_parser.parse_config_options(namespace, configs=[self.config_file])
+ assert namespace.foo == 'foo'
+
+ def test_config_options(self):
+ self.parser.add_argument('--foo')
+ with open(self.config_file, 'w') as f:
+ f.write(textwrap.dedent("""
+ [DEFAULT]
+ cli_test.foo=bar
+ """))
+ namespace = self.parser.parse_args(['--foo', 'foo'])
+ assert namespace.foo == 'foo'
+ # config args override matching namespace attrs
+ namespace = self.config_parser.parse_config_options(namespace, configs=[self.config_file])
+ assert namespace.foo == 'bar'
diff --git a/tests/scripts/test_pkgdev_commit.py b/tests/scripts/test_pkgdev_commit.py
index ac3fc8b..550671f 100644
--- a/tests/scripts/test_pkgdev_commit.py
+++ b/tests/scripts/test_pkgdev_commit.py
@@ -167,7 +167,7 @@ class TestPkgdevCommit:
self.cache_dir = str(tmp_path)
self.scan_args = ['--pkgcheck-scan', f'--config no --cache-dir {self.cache_dir}']
# args for running pkgdev like a script
- self.args = ['pkgdev', 'commit'] + self.scan_args
+ self.args = ['pkgdev', 'commit', '--config', 'no'] + self.scan_args
def test_empty_repo(self, capsys, repo, make_git_repo):
git_repo = make_git_repo(repo.location, commit=True)
@@ -914,6 +914,28 @@ class TestPkgdevCommit:
self.script()
assert excinfo.value.code == 0
+ def test_config_opts(self, capsys, repo, make_git_repo, tmp_path):
+ config_file = str(tmp_path / 'config')
+ with open(config_file, 'w') as f:
+ f.write(textwrap.dedent("""
+ [DEFAULT]
+ commit.scan=
+ """))
+
+ git_repo = make_git_repo(repo.location)
+ repo.create_ebuild('cat/pkg-0')
+ git_repo.add_all('cat/pkg-0')
+ repo.create_ebuild('cat/pkg-1', license='')
+ git_repo.add_all('cat/pkg-1', commit=False)
+ with patch('sys.argv', ['pkgdev', 'commit', '--config', config_file] + self.scan_args), \
+ pytest.raises(SystemExit) as excinfo, \
+ chdir(git_repo.path):
+ self.script()
+ out, err = capsys.readouterr()
+ assert excinfo.value.code == 1
+ assert not err
+ assert 'MissingLicense' in out
+
def test_failed_manifest(self, capsys, repo, make_git_repo):
git_repo = make_git_repo(repo.location)
repo.create_ebuild('cat/pkg-0')
diff --git a/tests/scripts/test_pkgdev_push.py b/tests/scripts/test_pkgdev_push.py
index cc13fb1..4eeee4b 100644
--- a/tests/scripts/test_pkgdev_push.py
+++ b/tests/scripts/test_pkgdev_push.py
@@ -65,7 +65,7 @@ class TestPkgdevPush:
@pytest.fixture(autouse=True)
def _setup(self, tmp_path, make_repo, make_git_repo):
self.cache_dir = str(tmp_path / 'cache')
- self.scan_args = ['--pkgcheck-scan', f'--config no --cache-dir {self.cache_dir}']
+ self.scan_args = ['--config', 'no', '--pkgcheck-scan', f'--config no --cache-dir {self.cache_dir}']
# args for running pkgdev like a script
self.args = ['pkgdev', 'push'] + self.scan_args
diff --git a/tests/scripts/test_pkgdev_showkw.py b/tests/scripts/test_pkgdev_showkw.py
index 538744a..51348f2 100644
--- a/tests/scripts/test_pkgdev_showkw.py
+++ b/tests/scripts/test_pkgdev_showkw.py
@@ -3,8 +3,8 @@ from typing import NamedTuple, List
from unittest.mock import patch
import pytest
+from snakeoil.contexts import chdir
from pkgdev.scripts import run
-from snakeoil.contexts import chdir, os_environ
class Profile(NamedTuple):
"""Profile record used to create profiles in a repository."""
@@ -19,7 +19,7 @@ class TestPkgdevShowkwParseArgs:
def test_missing_target(self, capsys, tool):
with pytest.raises(SystemExit):
- tool.parse_args(['showkw'])
+ tool.parse_args(['showkw', '--config', 'no'])
captured = capsys.readouterr()
assert captured.err.strip() == (
'pkgdev showkw: error: missing target argument and not in a supported repo')
@@ -27,7 +27,7 @@ class TestPkgdevShowkwParseArgs:
def test_unknown_arches(self, capsys, tool, make_repo):
repo = make_repo(arches=['amd64'])
with pytest.raises(SystemExit):
- tool.parse_args(['showkw', '-a', 'unknown', '-r', repo.location])
+ tool.parse_args(['showkw', '--config', 'no', '-a', 'unknown', '-r', repo.location])
captured = capsys.readouterr()
assert captured.err.strip() == (
"pkgdev showkw: error: unknown arch: 'unknown' (choices: amd64)")
@@ -35,6 +35,7 @@ class TestPkgdevShowkwParseArgs:
class TestPkgdevShowkw:
script = partial(run, 'pkgdev')
+ base_args = ['pkgdev', 'showkw', '--config', 'n']
def _create_repo(self, make_repo):
repo = make_repo(arches=['amd64', 'ia64', 'mips', 'x86'])
@@ -47,7 +48,7 @@ class TestPkgdevShowkw:
return repo
def _run_and_parse(self, capsys, *args):
- with patch('sys.argv', ['pkgdev', 'showkw', "--format", "presto", *args]), \
+ with patch('sys.argv', [*self.base_args, "--format", "presto", *args]), \
pytest.raises(SystemExit) as excinfo:
self.script()
assert excinfo.value.code == None
@@ -63,7 +64,7 @@ class TestPkgdevShowkw:
def test_match(self, capsys, make_repo):
repo = self._create_repo(make_repo)
repo.create_ebuild('foo/bar-0')
- with patch('sys.argv', ['pkgdev', 'showkw', '-r', repo.location, 'foo/bar']), \
+ with patch('sys.argv', [*self.base_args, '-r', repo.location, 'foo/bar']), \
pytest.raises(SystemExit) as excinfo:
self.script()
assert excinfo.value.code == None
@@ -74,7 +75,7 @@ class TestPkgdevShowkw:
def test_match_short_name(self, capsys, make_repo):
repo = self._create_repo(make_repo)
repo.create_ebuild('foo/bar-0')
- with patch('sys.argv', ['pkgdev', 'showkw', '-r', repo.location, 'bar']), \
+ with patch('sys.argv', [*self.base_args, '-r', repo.location, 'bar']), \
pytest.raises(SystemExit) as excinfo:
self.script()
assert excinfo.value.code == None
@@ -85,7 +86,7 @@ class TestPkgdevShowkw:
def test_match_cwd_repo(self, capsys, make_repo):
repo = self._create_repo(make_repo)
repo.create_ebuild('foo/bar-0')
- with patch('sys.argv', ['pkgdev', 'showkw', 'foo/bar']), \
+ with patch('sys.argv', [*self.base_args, 'foo/bar']), \
pytest.raises(SystemExit) as excinfo, \
chdir(repo.location):
self.script()
@@ -97,7 +98,7 @@ class TestPkgdevShowkw:
def test_match_cwd_pkg(self, capsys, make_repo):
repo = self._create_repo(make_repo)
repo.create_ebuild('foo/bar-0')
- with patch('sys.argv', ['pkgdev', 'showkw']), \
+ with patch('sys.argv', self.base_args), \
pytest.raises(SystemExit) as excinfo, \
chdir(repo.location + '/foo/bar'):
self.script()
@@ -107,7 +108,7 @@ class TestPkgdevShowkw:
def test_no_matches(self, capsys, make_repo):
repo = self._create_repo(make_repo)
- with patch('sys.argv', ['pkgdev', 'showkw', '-r', repo.location, 'foo/bar']), \
+ with patch('sys.argv', [*self.base_args, '-r', repo.location, 'foo/bar']), \
pytest.raises(SystemExit) as excinfo:
self.script()
assert excinfo.value.code == 1
@@ -165,7 +166,7 @@ class TestPkgdevShowkw:
repo = self._create_repo(make_repo)
repo.create_ebuild('foo/bar-0', keywords=('amd64', '~ia64', '~mips', '~x86'))
repo.create_ebuild('foo/bar-1', keywords=('~amd64', '~ia64', '~mips', 'x86'))
- with patch('sys.argv', ['pkgdev', 'showkw', '-r', repo.location, 'foo/bar', "--collapse", arg]), \
+ with patch('sys.argv', [*self.base_args, '-r', repo.location, 'foo/bar', "--collapse", arg]), \
pytest.raises(SystemExit) as excinfo:
self.script()
out, err = capsys.readouterr()