diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 2e14359d1af..92613266885 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -302,6 +302,55 @@ def domain_name(self): return f'InEnum[{self._domain.__name__}]' +class IsInstance(object): + """ + Domain validator for type checking. + + Parameters + ---------- + *bases : tuple of type + Valid types. + """ + + def __init__(self, *bases): + assert bases + self.baseClasses = bases + + @staticmethod + def _fullname(klass): + """ + Get full name of class, including appropriate module qualifier. + """ + module_name = klass.__module__ + module_qual = "" if module_name == "builtins" else f"{module_name}." + return f"{module_qual}{klass.__name__}" + + def __call__(self, obj): + if isinstance(obj, self.baseClasses): + return obj + if len(self.baseClasses) > 1: + class_names = ", ".join( + f"{self._fullname(kls)!r}" for kls in self.baseClasses + ) + msg = ( + "Expected an instance of one of these types: " + f"{class_names}, but received value {obj!r} of type " + f"{self._fullname(type(obj))!r}" + ) + else: + msg = ( + f"Expected an instance of " + f"{self._fullname(self.baseClasses[0])!r}, " + f"but received value {obj!r} of type {self._fullname(type(obj))!r}" + ) + raise ValueError(msg) + + def domain_name(self): + return ( + f"IsInstance({', '.join(self._fullname(kls) for kls in self.baseClasses)})" + ) + + class ListOf(object): """Domain validator for lists of a specified type @@ -454,7 +503,7 @@ def __init__(self, basePath=None, expandPath=None): self.expandPath = expandPath def __call__(self, path): - path = str(path) + path = os.fsdecode(path) _expand = self.expandPath if _expand is None: _expand = not Path.SuppressPathExpansion @@ -709,6 +758,7 @@ def from_enum_or_string(cls, arg): NonNegativeFloat In InEnum + IsInstance ListOf Module Path diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 0cc71169a34..068017d836f 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -60,6 +60,7 @@ def yaml_load(arg): NonPositiveFloat, NonNegativeFloat, In, + IsInstance, ListOf, Module, Path, @@ -448,12 +449,62 @@ class TestEnum(enum.Enum): with self.assertRaisesRegex(ValueError, '.*invalid value'): cfg.enum = 'ITEM_THREE' + def test_IsInstance(self): + c = ConfigDict() + c.declare("val", ConfigValue(None, IsInstance(int))) + c.val = 1 + self.assertEqual(c.val, 1) + exc_str = ( + "Expected an instance of 'int', but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val = 2.4 + + class TestClass: + def __repr__(self): + return f"{TestClass.__name__}()" + + c.declare("val2", ConfigValue(None, IsInstance(TestClass))) + testinst = TestClass() + c.val2 = testinst + self.assertEqual(c.val2, testinst) + exc_str = ( + r"Expected an instance of '.*\.TestClass', " + "but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val2 = 2.4 + + c.declare("val3", ConfigValue(None, IsInstance(int, TestClass))) + self.assertRegex( + c.get("val3").domain_name(), r"IsInstance\(int, .*\.TestClass\)" + ) + c.val3 = 2 + self.assertEqual(c.val3, 2) + exc_str = ( + r"Expected an instance of one of these types: 'int', '.*\.TestClass'" + r", but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val3 = 2.4 + def test_Path(self): def norm(x): if cwd[1] == ':' and x[0] == '/': x = cwd[:2] + x return x.replace('/', os.path.sep) + class ExamplePathLike: + def __init__(self, path_str_or_bytes): + self.path = path_str_or_bytes + + def __fspath__(self): + return self.path + + def __str__(self): + path_str = str(self.path) + return f"{type(self).__name__}({path_str})" + cwd = os.getcwd() + os.path.sep c = ConfigDict() @@ -462,12 +513,30 @@ def norm(x): c.a = "/a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm('/a/b/c')) + c.a = b"/a/b/c" + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm('/a/b/c')) + c.a = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm('/a/b/c')) c.a = "a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = b'a/b/c' + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = ExamplePathLike('a/b/c') + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) c.a = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = b'${CWD}/a/b/c' + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = ExamplePathLike('${CWD}/a/b/c') + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) c.a = None self.assertIs(c.a, None) @@ -476,12 +545,30 @@ def norm(x): c.b = "/a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm('/a/b/c')) + c.b = b"/a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm('/a/b/c')) + c.b = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm('/a/b/c')) c.b = "a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm(cwd + 'rel/path/a/b/c')) + c.b = b"a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'rel/path/a/b/c')) + c.b = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + "rel/path/a/b/c")) c.b = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm(cwd + 'a/b/c')) + c.b = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'a/b/c')) + c.b = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'a/b/c')) c.b = None self.assertIs(c.b, None) @@ -490,12 +577,30 @@ def norm(x): c.c = "/a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm('/a/b/c')) + c.c = b"/a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/a/b/c')) + c.c = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/a/b/c')) c.c = "a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm('/my/dir/a/b/c')) + c.c = b"a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/my/dir/a/b/c')) + c.c = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm("/my/dir/a/b/c")) c.c = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm(cwd + 'a/b/c')) + c.c = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm(cwd + 'a/b/c')) + c.c = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm(cwd + 'a/b/c')) c.c = None self.assertIs(c.c, None) @@ -505,12 +610,30 @@ def norm(x): c.d = "/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm('/a/b/c')) + c.d = b"/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) + c.d = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) c.d = "a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) c.d = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) c.d_base = '/my/dir' c.d = "/a/b/c" @@ -527,12 +650,30 @@ def norm(x): c.d = "/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm('/a/b/c')) + c.d = b"/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) + c.d = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) c.d = "a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) + c.d = b"a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) + c.d = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) c.d = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) try: Path.SuppressPathExpansion = True @@ -540,14 +681,38 @@ def norm(x): self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, '/a/b/c') + c.d = b"/a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, '/a/b/c') + c.d = ExamplePathLike("/a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, '/a/b/c') c.d = "a/b/c" self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, 'a/b/c') + c.d = b"a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, 'a/b/c') + c.d = ExamplePathLike("a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, 'a/b/c') c.d = "${CWD}/a/b/c" self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, "${CWD}/a/b/c") + c.d = b"${CWD}/a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, "${CWD}/a/b/c") + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, "${CWD}/a/b/c") finally: Path.SuppressPathExpansion = False