1
0
mirror of https://github.com/openSUSE/osc.git synced 2025-02-03 18:16:17 +01:00

Merge pull request #1387 from dmach/config-type-checking

Switch 'osc.conf.config' from dict to Options class with type checking
This commit is contained in:
Daniel Mach 2023-09-11 21:45:02 +02:00 committed by GitHub
commit a5f56b1673
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2118 additions and 622 deletions

View File

@ -5,9 +5,9 @@
version: 2
build:
os: ubuntu-20.04
os: ubuntu-22.04
tools:
python: "3.9"
python: "3.11"
sphinx:
configuration: doc/conf.py

View File

@ -26,8 +26,11 @@
%endif
%define argparse_manpage_pkg %{use_python_pkg}-argparse-manpage
%define sphinx_pkg %{use_python_pkg}-Sphinx
%if 0%{?fedora}
%define argparse_manpage_pkg argparse-manpage
%define sphinx_pkg %{use_python_pkg}-sphinx
%endif
Name: osc
@ -50,6 +53,7 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-build
%if %{with man}
BuildRequires: %{argparse_manpage_pkg}
BuildRequires: %{sphinx_pkg}
%endif
BuildRequires: %{use_python_pkg}-cryptography
BuildRequires: %{use_python_pkg}-devel >= 3.6
@ -124,7 +128,7 @@ cat << EOF > macros.osc
%%osc_plugin_dir %{osc_plugin_dir}
EOF
# build man page
# build man pages
%if %{with man}
PYTHONPATH=. argparse-manpage \
--output=osc.1 \
@ -136,6 +140,8 @@ PYTHONPATH=. argparse-manpage \
--description="openSUSE Commander" \
--author="Contributors to the osc project. See the project's GIT history for the complete list." \
--url="https://github.com/openSUSE/osc/"
sphinx-build -b man doc .
%endif
%install
@ -157,6 +163,7 @@ install -Dm0644 macros.osc %{buildroot}%{_rpmmacrodir}/macros.osc
# install man page
%if %{with man}
install -Dm0644 osc.1 %{buildroot}%{_mandir}/man1/osc.1
install -Dm0644 oscrc.5 %{buildroot}%{_mandir}/man5/oscrc.5
%endif
%check
@ -169,7 +176,7 @@ install -Dm0644 osc.1 %{buildroot}%{_mandir}/man1/osc.1
%license COPYING
%doc AUTHORS README.md NEWS
%if %{with man}
%{_mandir}/man1/osc.*
%{_mandir}/man*/osc*
%endif
# executables

3
doc/_static/css/custom.css vendored Normal file
View File

@ -0,0 +1,3 @@
dl.property {
display: block !important;
}

View File

@ -1,13 +1,10 @@
.. py:module:: osc.conf
conf
====
This is the osc conf module.
It handles the configuration of osc
osc.conf
========
basic structures
----------------
.. automodule:: osc.conf
:members:
:exclude-members: maintained_attribute, maintenance_attribute, maintained_update_project_attribute

View File

@ -12,7 +12,12 @@
#
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import textwrap
TOPDIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(TOPDIR, ".."))
import osc.conf
# -- Project information -----------------------------------------------------
@ -51,6 +56,29 @@ rst_epilog = """
master_doc = 'index'
# order members by __all__ or their order in the source code
autodoc_default_options = {
'member-order': 'bysource',
}
autodoc_typehints = "both"
# -- Generate documents -------------------------------------------------
osc.conf._model_to_rst(
cls=osc.conf.Options,
title="Configuration file",
description=textwrap.dedent(
"""
The configuration file path is ``$XDG_CONFIG_HOME/osc/oscrc``, which usually translates into ``~/.config/osc/oscrc``.
"""
),
sections={
"Host options": osc.conf.HostOptions,
},
output_file=os.path.join(TOPDIR, "oscrc.rst"),
)
# -- Options for HTML output -------------------------------------------------
@ -64,3 +92,16 @@ html_theme = 'sphinx_rtd_theme'
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_css_files = [
# fixes https://github.com/readthedocs/sphinx_rtd_theme/issues/1301
'css/custom.css',
]
# -- Options for MAN output -------------------------------------------------
# (source start file, name, description, authors, manual section).
man_pages = [
("oscrc", "oscrc", "openSUSE Commander configuration file", "openSUSE project <opensuse-buildservice@opensuse.org>", 5),
]

View File

@ -21,6 +21,7 @@ API:
api/modules
plugins/index
oscrc

View File

@ -1078,10 +1078,7 @@ class Osc(cmdln.Cmdln):
except oscerr.NoConfigfile as e:
print(e.msg, file=sys.stderr)
print('Creating osc configuration file %s ...' % e.file, file=sys.stderr)
apiurl = conf.DEFAULTS['apiurl']
if self.options.apiurl:
apiurl = self.options.apiurl
conf.interactive_config_setup(e.file, apiurl)
conf.interactive_config_setup(e.file, self.options.apiurl)
print('done', file=sys.stderr)
self.post_argparse()
except oscerr.ConfigMissingApiurl as e:

File diff suppressed because it is too large Load Diff

View File

@ -19,41 +19,6 @@ from . import conf
from . import oscerr
class _LazyPassword:
def __init__(self, pwfunc):
self._pwfunc = pwfunc
self._password = None
def __str__(self):
if self._password is None:
password = self._pwfunc()
if callable(password):
print('Warning: use of a deprecated credentials manager API.',
file=sys.stderr)
password = password()
if password is None:
raise oscerr.OscIOError(None, 'Unable to retrieve password')
self._password = password
return self._password
def __format__(self, format_spec):
if format_spec.endswith("s"):
return f"{self.__str__():{format_spec}}"
return super().__format__(format_spec)
def __len__(self):
return len(str(self))
def __add__(self, other):
return str(self) + other
def __radd__(self, other):
return other + str(self)
def __getattr__(self, name):
return getattr(str(self), name)
class AbstractCredentialsManagerDescriptor:
def name(self):
raise NotImplementedError()
@ -90,9 +55,9 @@ class AbstractCredentialsManager:
def get_password(self, url, user, defer=True, apiurl=None):
if defer:
return _LazyPassword(lambda: self._get_password(url, user, apiurl=apiurl))
return conf.Password(lambda: self._get_password(url, user, apiurl=apiurl))
else:
return self._get_password(url, user, apiurl=apiurl)
return conf.Password(self._get_password(url, user, apiurl=apiurl))
def set_password(self, url, user, password):
raise NotImplementedError()

318
osc/util/models.py Normal file
View File

@ -0,0 +1,318 @@
"""
This module implements a lightweight and limited alternative
to pydantic's BaseModel and Field classes.
It works on python 3.6+.
This module IS NOT a supported API, it is meant for osc internal use only.
"""
import inspect
import sys
import types
from typing import get_type_hints
# supported types
from typing import Any
from typing import Dict
from typing import List
from typing import NewType
from typing import Optional
from typing import Tuple
from typing import Union
if sys.version_info < (3, 8):
def get_origin(typ):
result = getattr(typ, "__origin__", None)
bases = getattr(result, "__orig_bases__", None)
if bases:
result = bases[0]
return result
else:
from typing import get_origin
__all__ = (
"BaseModel",
"Field",
"NotSet",
"FromParent",
"Dict",
"List",
"NewType",
"Optional",
"Tuple",
"Union",
)
class NotSetClass:
def __repr__(self):
return "NotSet"
def __bool__(self):
return False
NotSet = NotSetClass()
class FromParent(NotSetClass):
def __init__(self, field_name):
self.field_name = field_name
def __repr__(self):
return f"FromParent(field_name={self.field_name})"
class Field(property):
def __init__(
self,
default: Any = NotSet,
description: Optional[str] = None,
exclude: bool = False,
**extra,
):
# the default value; it can be a factory function that is lazily evaluated on the first use
# model sets it to None if it equals to NotSet (for better usability)
self.default = default
# whether the field was set
self.is_set = False
# the name of model's attribute associated with this field instance - set from the model
self.name = None
# the type of this field instance - set from the model
self.type = None
# the description of the field
self.description = description
# docstring - for sphinx and help()
self.__doc__ = self.description
if self.__doc__:
# append information about the default value
if isinstance(self.default, FromParent):
self.__doc__ += f"\n\nDefault: inherited from parent config's field ``{self.default.field_name}``"
elif self.default is not NotSet:
self.__doc__ += f"\n\nDefault: ``{self.default}``"
# whether to exclude this field from export
self.exclude = exclude
# extra fields
self.extra = extra
# create an instance specific of self.get() so we can annotate it in the model
self.get_copy = types.FunctionType(
self.get.__code__,
self.get.__globals__,
self.get.__name__,
self.get.__defaults__,
self.get.__closure__,
)
# turn function into a method by binding it to the instance
self.get_copy = types.MethodType(self.get_copy, self)
super().__init__(fget=self.get_copy, fset=self.set, doc=description)
@property
def origin_type(self):
origin_type = get_origin(self.type) or self.type
if self.is_optional:
types = [i for i in self.type.__args__ if i != type(None)]
return types[0]
return origin_type
@property
def is_optional(self):
origin_type = get_origin(self.type) or self.type
return origin_type == Union and len(self.type.__args__) == 2 and type(None) in self.type.__args__
@property
def is_model(self):
return inspect.isclass(self.origin_type) and issubclass(self.origin_type, BaseModel)
def validate_type(self, value, expected_types=None):
if not expected_types and self.is_optional and value is None:
return True
if expected_types is None:
expected_types = (self.type,)
elif not isinstance(expected_types, (list, tuple)):
expected_types = (expected_types,)
valid_type = False
for expected_type in expected_types:
if valid_type:
break
origin_type = get_origin(expected_type) or expected_type
# unwrap Union
if origin_type == Union:
if value is None and type(None) in expected_type.__args__:
valid_type = True
continue
valid_type |= self.validate_type(value, expected_types=expected_type.__args__)
continue
# unwrap NewType
if (callable(NewType) or isinstance(origin_type, NewType)) and hasattr(origin_type, "__supertype__"):
valid_type |= self.validate_type(value, expected_types=(origin_type.__supertype__,))
continue
if (
inspect.isclass(expected_type)
and issubclass(expected_type, BaseModel)
and isinstance(value, (expected_type, dict))
):
valid_type = True
continue
if not isinstance(value, origin_type):
msg = f"Field '{self.name}' has type '{self.type}'. Cannot assign a value with type '{type(value).__name__}'."
raise TypeError(msg)
# the type annotation has no arguments -> no need to check those
if not getattr(expected_type, "__args__", None):
valid_type = True
continue
if origin_type in (list, tuple):
valid_type_items = True
for i in value:
valid_type_items &= self.validate_type(i, expected_type.__args__)
valid_type |= valid_type_items
elif origin_type in (dict,):
valid_type_items = True
for k, v in value.items():
valid_type_items &= self.validate_type(k, expected_type.__args__[0])
valid_type_items &= self.validate_type(v, expected_type.__args__[1])
valid_type |= valid_type_items
else:
raise TypeError(f"Field '{self.name}' has unsupported type '{self.type}'.")
return valid_type
def get(self, obj):
try:
return getattr(obj, f"_{self.name}")
except AttributeError:
pass
if isinstance(self.default, FromParent):
if obj._parent is None:
raise RuntimeError(f"The field '{self.name}' has default {self.default} but the model has no parent set")
return getattr(obj._parent, self.default.field_name or self.name)
if self.default is NotSet:
raise RuntimeError(f"The field '{self.name}' has no default")
# lazy evaluation of a factory function on first use
if callable(self.default):
self.default = self.default()
# if this is a model field, convert dict to a model instance
if self.is_model and isinstance(self.default, dict):
new_value = self.origin_type() # pylint: disable=not-callable
for k, v in self.default.items():
setattr(new_value, k, v)
self.default = new_value
return self.default
def set(self, obj, value):
# if this is a model field, convert dict to a model instance
if self.is_model and isinstance(value, dict):
new_value = self.origin_type() # pylint: disable=not-callable
for k, v in value.items():
setattr(new_value, k, v)
value = new_value
self.validate_type(value)
setattr(obj, f"_{self.name}", value)
self.is_set = True
class ModelMeta(type):
def __new__(mcs, name, bases, attrs):
new_cls = super().__new__(mcs, name, bases, attrs)
new_cls.__fields__ = {}
# NOTE: dir() doesn't preserve attribute order
# we need to iterate through __mro__ classes to workaround that
for parent_cls in reversed(new_cls.__mro__):
for field_name in parent_cls.__dict__:
if field_name in new_cls.__fields__:
continue
field = getattr(new_cls, field_name)
if not isinstance(field, Field):
continue
new_cls.__fields__[field_name] = field
# fill model specific details back to the fields
for field_name, field in new_cls.__fields__.items():
# property name associated with the field in this model
field.name = field_name
# field type associated with the field in this model
field.type = get_type_hints(new_cls)[field_name]
# set annotation for the getter so it shows up in sphinx
field.get_copy.__func__.__annotations__ = {"return": field.type}
# set 'None' as the default for optional fields
if field.default is NotSet and field.is_optional:
field.default = None
return new_cls
class BaseModel(metaclass=ModelMeta):
__fields__: Dict[str, Field]
def __init__(self, **kwargs):
self._parent = kwargs.pop("_parent", None)
uninitialized_fields = []
for name, field in self.__fields__.items():
if name not in kwargs:
if field.default is NotSet:
uninitialized_fields.append(field.name)
continue
value = kwargs.pop(name)
setattr(self, name, value)
if kwargs:
unknown_fields_str = ", ".join([f"'{i}'" for i in kwargs])
raise TypeError(f"The following kwargs do not match any field: {unknown_fields_str}")
if uninitialized_fields:
uninitialized_fields_str = ", ".join([f"'{i}'" for i in uninitialized_fields])
raise TypeError(
f"The following fields are not initialized and have no default either: {uninitialized_fields_str}"
)
for name, field in self.__fields__.items():
field.validate_type(getattr(self, name))
def dict(self, exclude_unset=False):
result = {}
for name, field in self.__fields__.items():
if field.exclude:
continue
if exclude_unset and not field.is_set and field.is_optional:
# include only mandatory fields and optional fields that were set to an actual value
continue
if field.is_model:
result[name] = getattr(self, name).dict(exclude_unset=exclude_unset)
else:
result[name] = getattr(self, name)
return result

6
osc/util/xdg.py Normal file
View File

@ -0,0 +1,6 @@
import os
XDG_DATA_HOME = os.environ.get("XDG_DATA_HOME", "~/.local/share")
XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME", "~/.config")
XDG_STATE_HOME = os.environ.get("XDG_STATE_HOME", "~/.local/state")
XDG_CACHE_HOME = os.environ.get("XDG_CACHE_HOME", "~/.cache")

View File

@ -70,6 +70,8 @@ submitrequest_declined_template = bla bla
linkcontrol = 0
include_request_from_project = 1
local_service_run = 1
include_files = incl *.incl
exclude_files = excl *.excl
maintained_attribute = OBS:Maintained
maintenance_attribute = OBS:MaintenanceProject
maintained_update_project_attribute = OBS:UpdateProject
@ -84,12 +86,13 @@ pass = opensuse
passx = unused
aliases = osc
http_headers =
authorization: Basic QWRtaW46b3BlbnN1c2U=
Authorization: Basic QWRtaW46b3BlbnN1c2U=
X-Foo: Bar
realname = The Administrator
email = admin@example.com
sslcertck = 1
cafile = /path/to/custom_cacert.pem
capath = /path/to/custom_cacert.d/
sslcertck = 1
trusted_prj = openSUSE:* SUSE:*
downloadurl = http://example.com/
sshkey = ~/.ssh/id_rsa.pub
@ -309,6 +312,12 @@ class TestExampleConfig(unittest.TestCase):
def test_local_service_run(self):
self.assertEqual(self.config["local_service_run"], True)
def test_exclude_files(self):
self.assertEqual(self.config["exclude_files"], ["excl", "*.excl"])
def test_include_files(self):
self.assertEqual(self.config["include_files"], ["incl", "*.incl"])
def test_maintained_attribute(self):
self.assertEqual(self.config["maintained_attribute"], "OBS:Maintained")
@ -339,7 +348,10 @@ class TestExampleConfig(unittest.TestCase):
host_options = self.config["api_host_options"][self.config["apiurl"]]
self.assertEqual(
host_options["http_headers"],
[("authorization", "Basic QWRtaW46b3BlbnN1c2U=")],
[
("Authorization", "Basic QWRtaW46b3BlbnN1c2U="),
("X-Foo", "Bar"),
],
)
def test_host_option_realname(self):
@ -390,5 +402,40 @@ class TestExampleConfig(unittest.TestCase):
self.assertEqual(host_options["disable_hdrmd5_check"], False)
class TestFromParent(unittest.TestCase):
def setUp(self):
self.options = osc.conf.Options()
self.host_options = osc.conf.HostOptions(apiurl="https://example.com", username="Admin", _parent=self.options)
self.options.api_host_options[self.host_options.apiurl] = self.host_options
def test_disable_hdrmd5_check(self):
self.assertEqual(self.options.disable_hdrmd5_check, False)
self.assertEqual(self.host_options.disable_hdrmd5_check, False)
self.options.disable_hdrmd5_check = True
self.assertEqual(self.options.disable_hdrmd5_check, True)
self.assertEqual(self.host_options.disable_hdrmd5_check, True)
self.host_options.disable_hdrmd5_check = False
self.assertEqual(self.options.disable_hdrmd5_check, True)
self.assertEqual(self.host_options.disable_hdrmd5_check, False)
def test_email(self):
self.assertEqual(self.options.email, None)
self.assertEqual(self.host_options.email, None)
self.options.email = "user@example.com"
self.assertEqual(self.options.email, "user@example.com")
self.assertEqual(self.host_options.email, "user@example.com")
self.host_options.email = "another-user@example.com"
self.assertEqual(self.options.email, "user@example.com")
self.assertEqual(self.host_options.email, "another-user@example.com")
if __name__ == "__main__":
unittest.main()

202
tests/test_models.py Normal file
View File

@ -0,0 +1,202 @@
import unittest
from typing import Set
from osc.util.models import *
from osc.util.models import get_origin
class TestTyping(unittest.TestCase):
def test_get_origin_list(self):
typ = get_origin(list)
self.assertEqual(typ, None)
def test_get_origin_list_str(self):
typ = get_origin(List[str])
self.assertEqual(typ, list)
class TestNotSet(unittest.TestCase):
def test_repr(self):
self.assertEqual(repr(NotSet), "NotSet")
def test_bool(self):
self.assertEqual(bool(NotSet), False)
class Test(unittest.TestCase):
def test_modified(self):
class TestModel(BaseModel):
a: str = Field(default="default")
b: Optional[str] = Field(default=None)
m = TestModel()
self.assertEqual(m.dict(exclude_unset=True), {"a": "default"})
m = TestModel(b=None)
self.assertEqual(m.dict(exclude_unset=True), {"a": "default", "b": None})
def test_unknown_fields(self):
class TestModel(BaseModel):
pass
self.assertRaises(TypeError, TestModel, does_not_exist=None)
def test_uninitialized(self):
class TestModel(BaseModel):
field: str = Field()
self.assertRaises(TypeError, TestModel)
def test_invalid_type(self):
class TestModel(BaseModel):
field: Optional[str] = Field()
m = TestModel()
self.assertRaises(TypeError, setattr, m.field, [])
def test_unsupported_type(self):
class TestModel(BaseModel):
field: Set[str] = Field(default=None)
self.assertRaises(TypeError, TestModel)
def test_is_set(self):
class TestModel(BaseModel):
field: Optional[str] = Field()
m = TestModel()
field = m.__fields__["field"]
self.assertEqual(field.is_set, False)
self.assertEqual(m.field, None)
m.field = "text"
self.assertEqual(field.is_set, True)
self.assertEqual(m.field, "text")
def test_str(self):
class TestModel(BaseModel):
field: str = Field(default="default")
m = TestModel()
field = m.__fields__["field"]
self.assertEqual(field.is_model, False)
self.assertEqual(field.is_optional, False)
self.assertEqual(field.is_set, False)
self.assertEqual(field.origin_type, str)
self.assertEqual(m.field, "default")
m.field = "text"
self.assertEqual(m.field, "text")
def test_optional_str(self):
class TestModel(BaseModel):
field: Optional[str] = Field()
m = TestModel()
field = m.__fields__["field"]
self.assertEqual(field.is_model, False)
self.assertEqual(field.is_optional, True)
self.assertEqual(field.is_set, False)
self.assertEqual(field.origin_type, str)
self.assertEqual(m.field, None)
m.field = "text"
self.assertEqual(m.field, "text")
def test_int(self):
class TestModel(BaseModel):
field: int = Field(default=0)
m = TestModel()
field = m.__fields__["field"]
self.assertEqual(field.is_model, False)
self.assertEqual(field.is_optional, False)
self.assertEqual(field.origin_type, int)
self.assertEqual(m.field, 0)
m.field = 1
self.assertEqual(m.field, 1)
def test_optional_int(self):
class TestModel(BaseModel):
field: Optional[int] = Field()
m = TestModel()
field = m.__fields__["field"]
self.assertEqual(field.is_model, False)
self.assertEqual(field.is_optional, True)
self.assertEqual(field.origin_type, int)
self.assertEqual(m.field, None)
m.field = 1
self.assertEqual(m.field, 1)
def test_submodel(self):
class TestSubmodel(BaseModel):
text: str = Field(default="default")
class TestModel(BaseModel):
field: TestSubmodel = Field(default={})
m = TestModel()
field = m.__fields__["field"]
self.assertEqual(field.is_model, True)
self.assertEqual(field.is_optional, False)
self.assertEqual(field.origin_type, TestSubmodel)
m = TestModel(field=TestSubmodel())
self.assertEqual(m.field.text, "default")
m = TestModel(field={"text": "text"})
self.assertEqual(m.field.text, "text")
def test_optional_submodel(self):
class TestSubmodel(BaseModel):
text: str = Field(default="default")
class TestModel(BaseModel):
field: Optional[TestSubmodel] = Field(default=None)
m = TestModel()
field = m.__fields__["field"]
self.assertEqual(field.is_model, True)
self.assertEqual(field.is_optional, True)
self.assertEqual(field.origin_type, TestSubmodel)
self.assertEqual(m.field, None)
m = TestModel(field=TestSubmodel())
self.assertIsInstance(m.field, TestSubmodel)
self.assertEqual(m.field.text, "default")
m = TestModel(field={"text": "text"})
self.assertNotEqual(m.field, None)
self.assertEqual(m.field.text, "text")
def test_parent(self):
class ParentModel(BaseModel):
field: str = Field(default="text")
class ChildModel(BaseModel):
field: str = Field(default=FromParent("field"))
field2: str = Field(default=FromParent("field"))
p = ParentModel()
c = ChildModel(_parent=p)
self.assertEqual(p.field, "text")
self.assertEqual(c.field, "text")
self.assertEqual(c.field2, "text")
c.field = "new-text"
self.assertEqual(p.field, "text")
self.assertEqual(c.field, "new-text")
self.assertEqual(c.field2, "text")
if __name__ == "__main__":
unittest.main()

View File

@ -82,7 +82,7 @@ class TestPrintMsg(unittest.TestCase):
importlib.reload(osc.conf)
def test_debug(self):
osc.conf.config["debug"] = 0
osc.conf.config["debug"] = False
stdout = io.StringIO()
stderr = io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
@ -90,7 +90,7 @@ class TestPrintMsg(unittest.TestCase):
self.assertEqual("", stdout.getvalue())
self.assertEqual("", stderr.getvalue())
osc.conf.config["debug"] = 1
osc.conf.config["debug"] = True
stdout = io.StringIO()
stderr = io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
@ -99,7 +99,7 @@ class TestPrintMsg(unittest.TestCase):
self.assertEqual("DEBUG: foo bar\n", stderr.getvalue())
def test_verbose(self):
osc.conf.config["verbose"] = 0
osc.conf.config["verbose"] = False
stdout = io.StringIO()
stderr = io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
@ -107,7 +107,7 @@ class TestPrintMsg(unittest.TestCase):
self.assertEqual("", stdout.getvalue())
self.assertEqual("", stderr.getvalue())
osc.conf.config["verbose"] = 1
osc.conf.config["verbose"] = True
stdout = io.StringIO()
stderr = io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
@ -115,8 +115,8 @@ class TestPrintMsg(unittest.TestCase):
self.assertEqual("foo bar\n", stdout.getvalue())
self.assertEqual("", stderr.getvalue())
osc.conf.config["verbose"] = 0
osc.conf.config["debug"] = 1
osc.conf.config["verbose"] = False
osc.conf.config["debug"] = True
stdout = io.StringIO()
stderr = io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):