diff --git a/tests/unit/development/__init__.py b/tests/unit/development/__init__.py index e69de29bb2..b8d2f0b34c 100644 --- a/tests/unit/development/__init__.py +++ b/tests/unit/development/__init__.py @@ -0,0 +1,63 @@ +import os + +_dir = os.path.dirname(os.path.abspath(__file__)) + + +def has_trailing_space(s): + return any(line != line.rstrip() for line in s.splitlines()) + + +expected_table_component_doc = [ + "A Table component.", + "This is a description of the component.", + "It's multiple lines long.", + "", + "Keyword arguments:", + "- children (a list of or a singular dash component, string or number; optional)", + "- optionalArray (list; optional): Description of optionalArray", + "- optionalBool (boolean; optional)", + "- optionalNumber (number; default 42)", + "- optionalObject (dict; optional)", + "- optionalString (string; default 'hello world')", + "- optionalNode (a list of or a singular dash component, " + "string or number; optional)", + "- optionalElement (dash component; optional)", + "- optionalEnum (a value equal to: 'News', 'Photos'; optional)", + "- optionalUnion (string | number; optional)", + "- optionalArrayOf (list of numbers; optional)", + "- optionalObjectOf (dict with strings as keys and values " + "of type number; optional)", + "- optionalObjectWithExactAndNestedDescription (dict; optional): " + "optionalObjectWithExactAndNestedDescription has the " + "following type: dict containing keys " + "'color', 'fontSize', 'figure'.", + "Those keys have the following types:", + " - color (string; optional)", + " - fontSize (number; optional)", + " - figure (dict; optional): Figure is a plotly graph object. " + "figure has the following type: dict containing " + "keys 'data', 'layout'.", + "Those keys have the following types:", + " - data (list of dicts; optional): data is a collection of traces", + " - layout (dict; optional): layout describes " "the rest of the figure", + "- optionalObjectWithShapeAndNestedDescription (dict; optional): " + "optionalObjectWithShapeAndNestedDescription has the " + "following type: dict containing keys " + "'color', 'fontSize', 'figure'.", + "Those keys have the following types:", + " - color (string; optional)", + " - fontSize (number; optional)", + " - figure (dict; optional): Figure is a plotly graph object. " + "figure has the following type: dict containing " + "keys 'data', 'layout'.", + "Those keys have the following types:", + " - data (list of dicts; optional): data is a collection of traces", + " - layout (dict; optional): layout describes " "the rest of the figure", + "- optionalAny (boolean | number | string | dict | " "list; optional)", + "- customProp (optional)", + "- customArrayProp (list; optional)", + "- data-* (string; optional)", + "- aria-* (string; optional)", + "- in (string; optional)", + "- id (string; optional)", +] diff --git a/tests/unit/development/conftest.py b/tests/unit/development/conftest.py new file mode 100644 index 0000000000..68a523d453 --- /dev/null +++ b/tests/unit/development/conftest.py @@ -0,0 +1,16 @@ +import json +import os +from collections import OrderedDict + +import pytest + +from . import _dir + + +@pytest.fixture +def load_test_metadata_json(): + json_path = os.path.join(_dir, "metadata_test.json") + with open(json_path) as data_file: + json_string = data_file.read() + data = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(json_string) + return data diff --git a/tests/unit/development/test_base_component.py b/tests/unit/development/test_base_component.py index c34c655de8..7f074e9670 100644 --- a/tests/unit/development/test_base_component.py +++ b/tests/unit/development/test_base_component.py @@ -1,27 +1,14 @@ -from collections import OrderedDict -import inspect import json -import os -import shutil -import unittest + import plotly -from dash.development.base_component import Component -from dash.development.component_generator import reserved_words -from dash.development._py_components_generation import ( - generate_class_string, - generate_class_file, - generate_class, - create_docstring, - prohibit_events, - js_to_py_type -) +import pytest -_dir = os.path.dirname(os.path.abspath(__file__)) +from dash.development.base_component import Component -Component._prop_names = ('id', 'a', 'children', 'style', ) -Component._type = 'TestComponent' -Component._namespace = 'test_namespace' -Component._valid_wildcard_attributes = ['data-', 'aria-'] +Component._prop_names = ("id", "a", "children", "style") +Component._type = "TestComponent" +Component._namespace = "test_namespace" +Component._valid_wildcard_attributes = ["data-", "aria-"] def nested_tree(): @@ -33,1060 +20,400 @@ def nested_tree(): - children contains numbers (as in c2) - children contains "None" items (as in c2) """ - c1 = Component( - id='0.1.x.x.0', - children='string' - ) + c1 = Component(id="0.1.x.x.0", children="string") c2 = Component( - id='0.1.x.x', - children=[10, None, 'wrap string', c1, 'another string', 4.51] + id="0.1.x.x", children=[10, None, "wrap string", c1, "another string", 4.51] ) c3 = Component( - id='0.1.x', + id="0.1.x", # children is just a component - children=c2 - ) - c4 = Component( - id='0.1', - children=c3 + children=c2, ) - c5 = Component(id='0.0') - c = Component(id='0', children=[c5, c4]) + c4 = Component(id="0.1", children=c3) + c5 = Component(id="0.0") + c = Component(id="0", children=[c5, c4]) return c, c1, c2, c3, c4, c5 -class TestComponent(unittest.TestCase): - def test_init(self): - Component(a=3) - - def test_get_item_with_children(self): - c1 = Component(id='1') - c2 = Component(children=[c1]) - self.assertEqual(c2['1'], c1) - - def test_get_item_with_children_as_component_instead_of_list(self): - c1 = Component(id='1') - c2 = Component(id='2', children=c1) - self.assertEqual(c2['1'], c1) - - def test_get_item_with_nested_children_one_branch(self): - c1 = Component(id='1') - c2 = Component(id='2', children=[c1]) - c3 = Component(children=[c2]) - self.assertEqual(c2['1'], c1) - self.assertEqual(c3['2'], c2) - self.assertEqual(c3['1'], c1) - - def test_get_item_with_nested_children_two_branches(self): - c1 = Component(id='1') - c2 = Component(id='2', children=[c1]) - c3 = Component(id='3') - c4 = Component(id='4', children=[c3]) - c5 = Component(children=[c2, c4]) - self.assertEqual(c2['1'], c1) - self.assertEqual(c4['3'], c3) - self.assertEqual(c5['2'], c2) - self.assertEqual(c5['4'], c4) - self.assertEqual(c5['1'], c1) - self.assertEqual(c5['3'], c3) - - def test_get_item_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501 - c, c1, c2, c3, c4, c5 = nested_tree() - keys = [k for k in c] - self.assertEqual( - keys, - [ - '0.0', - '0.1', - '0.1.x', - '0.1.x.x', - '0.1.x.x.0' - ] - ) - - # Try to get each item - for comp in [c1, c2, c3, c4, c5]: - self.assertEqual(c[comp.id], comp) - - # Get an item that doesn't exist - with self.assertRaises(KeyError): - c['x'] - - def test_len_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501 - c = nested_tree()[0] - self.assertEqual( - len(c), - 5 + # 5 components - 5 + # c2 has 2 strings, 2 numbers, and a None - 1 # c1 has 1 string - ) - - def test_set_item_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501 - keys = [ - '0.0', - '0.1', - '0.1.x', - '0.1.x.x', - '0.1.x.x.0' - ] - c = nested_tree()[0] - - # Test setting items starting from the innermost item - for key in reversed(keys): - new_id = 'new {}'.format(key) - new_component = Component( - id=new_id, - children='new string' - ) - c[key] = new_component - self.assertEqual(c[new_id], new_component) - - def test_del_item_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501 - c = nested_tree()[0] - keys = reversed([k for k in c]) - for key in keys: - c[key] - del c[key] - with self.assertRaises(KeyError): - c[key] - - def test_traverse_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501 - c, c1, c2, c3, c4, c5 = nested_tree() - elements = [i for i in c._traverse()] - self.assertEqual( - elements, - c.children + [c3] + [c2] + c2.children - ) - - def test_traverse_with_tuples(self): # noqa: E501 - c, c1, c2, c3, c4, c5 = nested_tree() - c2.children = tuple(c2.children) - c.children = tuple(c.children) - elements = [i for i in c._traverse()] - self.assertEqual( - elements, - list(c.children) + [c3] + [c2] + list(c2.children) - ) - - def test_to_plotly_json_with_nested_children_with_mixed_strings_and_without_lists(self): # noqa: E501 - c = nested_tree()[0] - Component._namespace - Component._type - - self.assertEqual(json.loads(json.dumps( - c.to_plotly_json(), - cls=plotly.utils.PlotlyJSONEncoder - )), { - 'type': 'TestComponent', - 'namespace': 'test_namespace', - 'props': { - 'children': [ - { - 'type': 'TestComponent', - 'namespace': 'test_namespace', - 'props': { - 'id': '0.0' - } - }, - { - 'type': 'TestComponent', - 'namespace': 'test_namespace', - 'props': { - 'children': { - 'type': 'TestComponent', - 'namespace': 'test_namespace', - 'props': { - 'children': { - 'type': 'TestComponent', - 'namespace': 'test_namespace', - 'props': { - 'children': [ - 10, - None, - 'wrap string', - { - 'type': 'TestComponent', - 'namespace': 'test_namespace', # noqa: E501 - 'props': { - 'children': 'string', - 'id': '0.1.x.x.0' - } - }, - 'another string', - 4.51 - ], - 'id': '0.1.x.x' - } - }, - 'id': '0.1.x' - } - }, - 'id': '0.1' - } - } - ], - 'id': '0' - } - }) - - def test_get_item_raises_key_if_id_doesnt_exist(self): - c = Component() - with self.assertRaises(KeyError): - c['1'] - - c1 = Component(id='1') - with self.assertRaises(KeyError): - c1['1'] - - c2 = Component(id='2', children=[c1]) - with self.assertRaises(KeyError): - c2['0'] - - c3 = Component(children='string with no id') - with self.assertRaises(KeyError): - c3['0'] - - def test_set_item(self): - c1a = Component(id='1', children='Hello world') - c2 = Component(id='2', children=c1a) - self.assertEqual(c2['1'], c1a) - c1b = Component(id='1', children='Brave new world') - c2['1'] = c1b - self.assertEqual(c2['1'], c1b) - - def test_set_item_with_children_as_list(self): - c1 = Component(id='1') - c2 = Component(id='2', children=[c1]) - self.assertEqual(c2['1'], c1) - c3 = Component(id='3') - c2['1'] = c3 - self.assertEqual(c2['3'], c3) - - def test_set_item_with_nested_children(self): - c1 = Component(id='1') - c2 = Component(id='2', children=[c1]) - c3 = Component(id='3') - c4 = Component(id='4', children=[c3]) - c5 = Component(id='5', children=[c2, c4]) - - c3b = Component(id='3') - self.assertEqual(c5['3'], c3) - self.assertTrue(c5['3'] != '3') - self.assertTrue(c5['3'] is not c3b) - - c5['3'] = c3b - self.assertTrue(c5['3'] is c3b) - self.assertTrue(c5['3'] is not c3) - - c2b = Component(id='2') - c5['2'] = c2b - self.assertTrue(c5['4'] is c4) - self.assertTrue(c5['2'] is not c2) - self.assertTrue(c5['2'] is c2b) - with self.assertRaises(KeyError): - c5['1'] - - def test_set_item_raises_key_error(self): - c1 = Component(id='1') - c2 = Component(id='2', children=[c1]) - with self.assertRaises(KeyError): - c2['3'] = Component(id='3') - - def test_del_item_from_list(self): - c1 = Component(id='1') - c2 = Component(id='2') - c3 = Component(id='3', children=[c1, c2]) - self.assertEqual(c3['1'], c1) - self.assertEqual(c3['2'], c2) - del c3['2'] - with self.assertRaises(KeyError): - c3['2'] - self.assertEqual(c3.children, [c1]) - - del c3['1'] - with self.assertRaises(KeyError): - c3['1'] - self.assertEqual(c3.children, []) - - def test_del_item_from_class(self): - c1 = Component(id='1') - c2 = Component(id='2', children=c1) - self.assertEqual(c2['1'], c1) - del c2['1'] - with self.assertRaises(KeyError): - c2['1'] - - self.assertEqual(c2.children, None) - - def test_to_plotly_json_without_children(self): - c = Component(id='a') - c._prop_names = ('id',) - c._type = 'MyComponent' - c._namespace = 'basic' - self.assertEqual( - c.to_plotly_json(), - {'namespace': 'basic', 'props': {'id': 'a'}, 'type': 'MyComponent'} - ) - - def test_to_plotly_json_with_null_arguments(self): - c = Component(id='a') - c._prop_names = ('id', 'style',) - c._type = 'MyComponent' - c._namespace = 'basic' - self.assertEqual( - c.to_plotly_json(), - {'namespace': 'basic', 'props': {'id': 'a'}, 'type': 'MyComponent'} - ) - - c = Component(id='a', style=None) - c._prop_names = ('id', 'style',) - c._type = 'MyComponent' - c._namespace = 'basic' - self.assertEqual( - c.to_plotly_json(), - { - 'namespace': 'basic', 'props': {'id': 'a', 'style': None}, - 'type': 'MyComponent' - } - ) - - def test_to_plotly_json_with_children(self): - c = Component(id='a', children='Hello World') - c._prop_names = ('id', 'children',) - c._type = 'MyComponent' - c._namespace = 'basic' - self.assertEqual( - c.to_plotly_json(), - { - 'namespace': 'basic', - 'props': { - 'id': 'a', - # TODO - Rename 'children' to 'children' - 'children': 'Hello World' - }, - 'type': 'MyComponent' - } - ) - - def test_to_plotly_json_with_nested_children(self): - c1 = Component(id='1', children='Hello World') - c1._prop_names = ('id', 'children',) - c1._type = 'MyComponent' - c1._namespace = 'basic' - - c2 = Component(id='2', children=c1) - c2._prop_names = ('id', 'children',) - c2._type = 'MyComponent' - c2._namespace = 'basic' - - c3 = Component(id='3', children='Hello World') - c3._prop_names = ('id', 'children',) - c3._type = 'MyComponent' - c3._namespace = 'basic' - - c4 = Component(id='4', children=[c2, c3]) - c4._prop_names = ('id', 'children',) - c4._type = 'MyComponent' - c4._namespace = 'basic' - - def to_dict(id, children): - return { - 'namespace': 'basic', - 'props': { - 'id': id, - 'children': children - }, - 'type': 'MyComponent' - } - - """ - self.assertEqual( - json.dumps(c4.to_plotly_json(), - cls=plotly.utils.PlotlyJSONEncoder), - json.dumps(to_dict('4', [ - to_dict('2', to_dict('1', 'Hello World')), - to_dict('3', 'Hello World') - ])) - ) - """ - - def test_to_plotly_json_with_wildcards(self): - c = Component(id='a', **{'aria-expanded': 'true', - 'data-toggle': 'toggled', - 'data-none': None}) - c._prop_names = ('id',) - c._type = 'MyComponent' - c._namespace = 'basic' - self.assertEqual( - c.to_plotly_json(), - {'namespace': 'basic', - 'props': { - 'aria-expanded': 'true', - 'data-toggle': 'toggled', - 'data-none': None, - 'id': 'a', - }, - 'type': 'MyComponent'} - ) - - def test_len(self): - self.assertEqual(len(Component()), 0) - self.assertEqual(len(Component(children='Hello World')), 1) - self.assertEqual(len(Component(children=Component())), 1) - self.assertEqual(len(Component(children=[Component(), Component()])), - 2) - self.assertEqual(len(Component(children=[ - Component(children=Component()), - Component() - ])), 3) - - def test_iter(self): - # The mixin methods from MutableMapping were cute but probably never - # used - at least not by us. Test that they're gone - - # keys, __contains__, items, values, and more are all mixin methods - # that we get for free by inheriting from the MutableMapping - # and behave as according to our implementation of __iter__ - - c = Component( - id='1', - children=[ - Component(id='2', children=[ - Component(id='3', children=Component(id='4')) - ]), - Component(id='5', children=[ - Component(id='6', children='Hello World') - ]), - Component(), - Component(children='Hello World'), - Component(children=Component(id='7')), - Component(children=[Component(id='8')]), - ] - ) - - mixins = ['clear', 'get', 'items', 'keys', 'pop', 'popitem', - 'setdefault', 'update', 'values'] - - for m in mixins: - assert not hasattr(c, m), 'should not have method ' + m - - keys = ['2', '3', '4', '5', '6', '7', '8'] - - for k in keys: - # test __contains__() - assert k in c, 'should find key ' + k - # test __getitem__() - assert c[k].id == k, 'key {} points to the right item'.format(k) - - # test __iter__() - keys2 = [] - for k in c: - keys2.append(k) - assert k in keys, 'iteration produces key ' + k - - assert len(keys) == len(keys2), 'iteration produces no extra keys' - - -class TestGenerateClassFile(unittest.TestCase): - def setUp(self): - json_path = os.path.join(_dir, 'metadata_test.json') - with open(json_path) as data_file: - json_string = data_file.read() - data = json\ - .JSONDecoder(object_pairs_hook=OrderedDict)\ - .decode(json_string) - self.data = data - - # Create a folder for the new component file - os.makedirs('TableComponents') - - # Import string not included in generated class string - import_string =\ - "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \ - "from dash.development.base_component import" + \ - " Component, _explicitize_args\n\n\n" - - # Class string generated from generate_class_string - self.component_class_string = import_string + generate_class_string( - typename='Table', - props=data['props'], - description=data['description'], - namespace='TableComponents' - ) - - # Class string written to file - generate_class_file( - typename='Table', - props=data['props'], - description=data['description'], - namespace='TableComponents' - ) - written_file_path = os.path.join( - 'TableComponents', "Table.py" - ) - with open(written_file_path, 'r') as f: - self.written_class_string = f.read() - - # The expected result for both class string and class file generation - expected_string_path = os.path.join(_dir, 'metadata_test.py') - with open(expected_string_path, 'r') as f: - self.expected_class_string = f.read() - - def tearDown(self): - shutil.rmtree('TableComponents') - - def assert_no_trailing_spaces(self, s): - for line in s.split('\n'): - self.assertEqual(line, line.rstrip()) - - def match_lines(self, val, expected): - for val1, exp1 in zip(val.splitlines(), expected.splitlines()): - assert val1 == exp1 - - def test_class_string(self): - self.match_lines( - self.expected_class_string, - self.component_class_string - ) - self.assert_no_trailing_spaces(self.component_class_string) - - def test_class_file(self): - self.match_lines( - self.expected_class_string, - self.written_class_string - ) - self.assert_no_trailing_spaces(self.written_class_string) - - -class TestGenerateClass(unittest.TestCase): - def setUp(self): - path = os.path.join(_dir, 'metadata_test.json') - with open(path) as data_file: - json_string = data_file.read() - data = json\ - .JSONDecoder(object_pairs_hook=OrderedDict)\ - .decode(json_string) - self.data = data - - self.ComponentClass = generate_class( - typename='Table', - props=data['props'], - description=data['description'], - namespace='TableComponents' - ) - - path = os.path.join(_dir, 'metadata_required_test.json') - with open(path) as data_file: - json_string = data_file.read() - required_data = json\ - .JSONDecoder(object_pairs_hook=OrderedDict)\ - .decode(json_string) - self.required_data = required_data - - self.ComponentClassRequired = generate_class( - typename='TableRequired', - props=required_data['props'], - description=required_data['description'], - namespace='TableComponents' - ) - - def test_to_plotly_json(self): - c = self.ComponentClass() - self.assertEqual(c.to_plotly_json(), { - 'namespace': 'TableComponents', - 'type': 'Table', - 'props': { - 'children': None - } - }) - - c = self.ComponentClass(id='my-id') - self.assertEqual(c.to_plotly_json(), { - 'namespace': 'TableComponents', - 'type': 'Table', - 'props': { - 'children': None, - 'id': 'my-id' - } - }) - - c = self.ComponentClass(id='my-id', optionalArray=None) - self.assertEqual(c.to_plotly_json(), { - 'namespace': 'TableComponents', - 'type': 'Table', - 'props': { - 'children': None, - 'id': 'my-id', - 'optionalArray': None - } - }) - - def test_arguments_become_attributes(self): - kwargs = { - 'id': 'my-id', - 'children': 'text children', - 'optionalArray': [[1, 2, 3]] - } - component_instance = self.ComponentClass(**kwargs) - for k, v in list(kwargs.items()): - self.assertEqual(getattr(component_instance, k), v) - - def test_repr_single_default_argument(self): - c1 = self.ComponentClass('text children') - c2 = self.ComponentClass(children='text children') - self.assertEqual( - repr(c1), - "Table('text children')" - ) - self.assertEqual( - repr(c2), - "Table('text children')" - ) - - def test_repr_single_non_default_argument(self): - c = self.ComponentClass(id='my-id') - self.assertEqual( - repr(c), - "Table(id='my-id')" - ) - - def test_repr_multiple_arguments(self): - # Note how the order in which keyword arguments are supplied is - # not always equal to the order in the repr of the component - c = self.ComponentClass(id='my id', optionalArray=[1, 2, 3]) - self.assertEqual( - repr(c), - "Table(optionalArray=[1, 2, 3], id='my id')" - ) - - def test_repr_nested_arguments(self): - c1 = self.ComponentClass(id='1') - c2 = self.ComponentClass(id='2', children=c1) - c3 = self.ComponentClass(children=c2) - self.assertEqual( - repr(c3), - "Table(Table(children=Table(id='1'), id='2'))" - ) - - def test_repr_with_wildcards(self): - c = self.ComponentClass(id='1', **{"data-one": "one", - "aria-two": "two"}) - data_first = "Table(id='1', data-one='one', aria-two='two')" - aria_first = "Table(id='1', aria-two='two', data-one='one')" - repr_string = repr(c) - if not (repr_string == data_first or repr_string == aria_first): - raise Exception("%s\nDoes not equal\n%s\nor\n%s" % - (repr_string, data_first, aria_first)) - - def test_docstring(self): - assert_docstring(self.assertEqual, self.ComponentClass.__doc__) - - def test_no_events(self): - self.assertEqual( - hasattr(self.ComponentClass(), 'available_events'), - False - ) - - # This one is kind of pointless now - def test_call_signature(self): - __init__func = self.ComponentClass.__init__ - # TODO: Will break in Python 3 - # http://stackoverflow.com/questions/2677185/ - self.assertEqual( - inspect.getargspec(__init__func).args, - ['self', - 'children', - 'optionalArray', - 'optionalBool', - 'optionalFunc', - 'optionalNumber', - 'optionalObject', - 'optionalString', - 'optionalSymbol', - 'optionalNode', - 'optionalElement', - 'optionalMessage', - 'optionalEnum', - 'optionalUnion', - 'optionalArrayOf', - 'optionalObjectOf', - 'optionalObjectWithExactAndNestedDescription', - 'optionalObjectWithShapeAndNestedDescription', - 'optionalAny', - 'customProp', - 'customArrayProp', - 'id'] if hasattr(inspect, 'signature') else [] - - - ) - self.assertEqual( - inspect.getargspec(__init__func).varargs, - None if hasattr(inspect, 'signature') else 'args' - ) - self.assertEqual( - inspect.getargspec(__init__func).keywords, - 'kwargs' - ) - if hasattr(inspect, 'signature'): - self.assertEqual( - [str(x) for x in inspect.getargspec(__init__func).defaults], - ['None'] + ['undefined'] * 20 - ) - - def test_required_props(self): - with self.assertRaises(Exception): - self.ComponentClassRequired() - self.ComponentClassRequired(id='test') - with self.assertRaises(Exception): - self.ComponentClassRequired(id='test', lahlah='test') - with self.assertRaises(Exception): - self.ComponentClassRequired(children='test') - - def test_attrs_match_forbidden_props(self): - assert '_.*' in reserved_words, 'props cannot have leading underscores' - - # props are not added as attrs unless explicitly provided - # except for children, which is always set if it's a prop at all. - expected_attrs = set(reserved_words + ['children']) - set(['_.*']) - c = self.ComponentClass() - base_attrs = set(dir(c)) - extra_attrs = set(a for a in base_attrs if a[0] != '_') - - assert extra_attrs == expected_attrs, \ - 'component has only underscored and reserved word attrs' - - # setting props causes them to show up as attrs - c2 = self.ComponentClass('children', id='c2', optionalArray=[1]) - prop_attrs = set(dir(c2)) - - assert base_attrs - prop_attrs == set([]), 'no attrs were removed' - assert ( - prop_attrs - base_attrs == set(['id', 'optionalArray']) - ), 'explicit props were added as attrs' - - -class TestMetaDataConversions(unittest.TestCase): - def setUp(self): - path = os.path.join(_dir, 'metadata_test.json') - with open(path) as data_file: - json_string = data_file.read() - data = json\ - .JSONDecoder(object_pairs_hook=OrderedDict)\ - .decode(json_string) - self.data = data - - self.expected_arg_strings = OrderedDict([ - ['children', - 'a list of or a singular dash component, string or number'], - - ['optionalArray', 'list'], - - ['optionalBool', 'boolean'], - - ['optionalFunc', ''], - - ['optionalNumber', 'number'], - - ['optionalObject', 'dict'], - - ['optionalString', 'string'], - - ['optionalSymbol', ''], - - ['optionalElement', 'dash component'], - - ['optionalNode', - 'a list of or a singular dash component, string or number'], - - ['optionalMessage', ''], - - ['optionalEnum', 'a value equal to: \'News\', \'Photos\''], - - ['optionalUnion', 'string | number'], - - ['optionalArrayOf', 'list of numbers'], - - ['optionalObjectOf', - 'dict with strings as keys and values of type number'], - - ['optionalObjectWithExactAndNestedDescription', '\n'.join([ - - "dict containing keys 'color', 'fontSize', 'figure'.", - "Those keys have the following types:", - " - color (string; optional)", - " - fontSize (number; optional)", - " - figure (dict; optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.", # noqa: E501 - "Those keys have the following types:", - " - data (list of dicts; optional): data is a collection of traces", - " - layout (dict; optional): layout describes the rest of the figure" # noqa: E501 +def test_init(): + Component(a=3) - ])], - ['optionalObjectWithShapeAndNestedDescription', '\n'.join([ +def test_get_item_with_children(): + c1 = Component(id="1") + c2 = Component(children=[c1]) + assert c2["1"] == c1 - "dict containing keys 'color', 'fontSize', 'figure'.", - "Those keys have the following types:", - " - color (string; optional)", - " - fontSize (number; optional)", - " - figure (dict; optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.", # noqa: E501 - "Those keys have the following types:", - " - data (list of dicts; optional): data is a collection of traces", - " - layout (dict; optional): layout describes the rest of the figure" # noqa: E501 - ])], +def test_get_item_with_children_as_component_instead_of_list(): + c1 = Component(id="1") + c2 = Component(id="2", children=c1) + assert c2["1"] == c1 - ['optionalAny', 'boolean | number | string | dict | list'], - ['customProp', ''], +def test_get_item_with_nested_children_one_branch(): + c1 = Component(id="1") + c2 = Component(id="2", children=[c1]) + c3 = Component(children=[c2]) + assert c2["1"] == c1 + assert c3["2"] == c2 + assert c3["1"] == c1 - ['customArrayProp', 'list'], - ['data-*', 'string'], +def test_get_item_with_nested_children_two_branches(): + c1 = Component(id="1") + c2 = Component(id="2", children=[c1]) + c3 = Component(id="3") + c4 = Component(id="4", children=[c3]) + c5 = Component(children=[c2, c4]) + assert c2["1"] == c1 + assert c4["3"] == c3 + assert c5["2"] == c2 + assert c5["4"] == c4 + assert c5["1"] == c1 + assert c5["3"] == c3 - ['aria-*', 'string'], - ['in', 'string'], +def test_get_item_with_nested_children_with_mixed_strings_and_without_lists(): + c, c1, c2, c3, c4, c5 = nested_tree() + keys = [k for k in c] - ['id', 'string'] - ]) + assert keys == ["0.0", "0.1", "0.1.x", "0.1.x.x", "0.1.x.x.0"] - def test_docstring(self): - docstring = create_docstring( - 'Table', - self.data['props'], - self.data['description'], - ) - prohibit_events(self.data['props']), - assert_docstring(self.assertEqual, docstring) + # Try to get each item + for comp in [c1, c2, c3, c4, c5]: + assert c[comp.id] == comp - def test_docgen_to_python_args(self): + # Get an item that doesn't exist + with pytest.raises(KeyError): + c["x"] - props = self.data['props'] - for prop_name, prop in list(props.items()): - self.assertEqual( - js_to_py_type(prop['type']), - self.expected_arg_strings[prop_name] - ) +def test_len_with_nested_children_with_mixed_strings_and_without_lists(): + c = nested_tree()[0] + assert ( + len(c) == 5 + 5 + 1 + ), "the length of the nested children should match the total of 5 \ + components, 2 strings + 2 numbers + none in c2, and 1 string in c1" -def assert_docstring(assertEqual, docstring): - for i, line in enumerate(docstring.split('\n')): - assertEqual(line, ([ - "A Table component.", - "This is a description of the component.", - "It's multiple lines long.", - '', - "Keyword arguments:", - "- children (a list of or a singular dash component, string or number; optional)", # noqa: E501 - "- optionalArray (list; optional): Description of optionalArray", - "- optionalBool (boolean; optional)", - "- optionalNumber (number; default 42)", - "- optionalObject (dict; optional)", - "- optionalString (string; default 'hello world')", +def test_set_item_with_nested_children_with_mixed_strings_and_without_lists(): + keys = ["0.0", "0.1", "0.1.x", "0.1.x.x", "0.1.x.x.0"] + c = nested_tree()[0] - "- optionalNode (a list of or a singular dash component, " - "string or number; optional)", + # Test setting items starting from the innermost item + for key in reversed(keys): + new_id = "new {}".format(key) + new_component = Component(id=new_id, children="new string") + c[key] = new_component + assert c[new_id] == new_component - "- optionalElement (dash component; optional)", - "- optionalEnum (a value equal to: 'News', 'Photos'; optional)", - "- optionalUnion (string | number; optional)", - "- optionalArrayOf (list of numbers; optional)", - "- optionalObjectOf (dict with strings as keys and values " - "of type number; optional)", +def test_del_item_with_nested_children_with_mixed_strings_and_without_lists(): + c = nested_tree()[0] + keys = reversed([k for k in c]) + for key in keys: + c[key] + del c[key] + with pytest.raises(KeyError): + c[key] - "- optionalObjectWithExactAndNestedDescription (dict; optional): " - "optionalObjectWithExactAndNestedDescription has the " - "following type: dict containing keys " - "'color', 'fontSize', 'figure'.", - "Those keys have the following types:", - " - color (string; optional)", - " - fontSize (number; optional)", +def test_traverse_with_nested_children_with_mixed_strings_and_without_lists(): + c, c1, c2, c3, c4, c5 = nested_tree() + elements = [i for i in c._traverse()] + assert elements == c.children + [c3] + [c2] + c2.children - " - figure (dict; optional): Figure is a plotly graph object. " - "figure has the following type: dict containing " - "keys 'data', 'layout'.", - "Those keys have the following types:", - " - data (list of dicts; optional): data is a collection of traces", - - " - layout (dict; optional): layout describes " - "the rest of the figure", - - "- optionalObjectWithShapeAndNestedDescription (dict; optional): " - "optionalObjectWithShapeAndNestedDescription has the " - "following type: dict containing keys " - "'color', 'fontSize', 'figure'.", +def test_traverse_with_tuples(): + c, c1, c2, c3, c4, c5 = nested_tree() + c2.children = tuple(c2.children) + c.children = tuple(c.children) + elements = [i for i in c._traverse()] + assert elements == list(c.children) + [c3] + [c2] + list(c2.children) - "Those keys have the following types:", - " - color (string; optional)", - " - fontSize (number; optional)", - " - figure (dict; optional): Figure is a plotly graph object. " - "figure has the following type: dict containing " - "keys 'data', 'layout'.", - - "Those keys have the following types:", - " - data (list of dicts; optional): data is a collection of traces", +def test_to_plotly_json_with_nested_children_with_mixed_strings_and_without_lists(): + c = nested_tree()[0] + Component._namespace + Component._type - " - layout (dict; optional): layout describes " - "the rest of the figure", + expected = { + "type": "TestComponent", + "namespace": "test_namespace", + "props": { + "children": [ + { + "type": "TestComponent", + "namespace": "test_namespace", + "props": {"id": "0.0"}, + }, + { + "type": "TestComponent", + "namespace": "test_namespace", + "props": { + "children": { + "type": "TestComponent", + "namespace": "test_namespace", + "props": { + "children": { + "type": "TestComponent", + "namespace": "test_namespace", + "props": { + "children": [ + 10, + None, + "wrap string", + { + "type": "TestComponent", + "namespace": "test_namespace", + "props": { + "children": "string", + "id": "0.1.x.x.0", + }, + }, + "another string", + 4.51, + ], + "id": "0.1.x.x", + }, + }, + "id": "0.1.x", + }, + }, + "id": "0.1", + }, + }, + ], + "id": "0", + }, + } + + res = json.loads(json.dumps(c.to_plotly_json(), cls=plotly.utils.PlotlyJSONEncoder)) + assert res == expected + + +def test_get_item_raises_key_if_id_doesnt_exist(): + c = Component() + with pytest.raises(KeyError): + c["1"] + + c1 = Component(id="1") + with pytest.raises(KeyError): + c1["1"] + + c2 = Component(id="2", children=[c1]) + with pytest.raises(KeyError): + c2["0"] + + c3 = Component(children="string with no id") + with pytest.raises(KeyError): + c3["0"] + + +def test_set_item(): + c1a = Component(id="1", children="Hello world") + c2 = Component(id="2", children=c1a) + assert c2["1"] == c1a + + c1b = Component(id="1", children="Brave new world") + c2["1"] = c1b + assert c2["1"] == c1b + + +def test_set_item_with_children_as_list(): + c1 = Component(id="1") + c2 = Component(id="2", children=[c1]) + assert c2["1"] == c1 + c3 = Component(id="3") + c2["1"] = c3 + assert c2["3"] == c3 + + +def test_set_item_with_nested_children(): + c1 = Component(id="1") + c2 = Component(id="2", children=[c1]) + c3 = Component(id="3") + c4 = Component(id="4", children=[c3]) + c5 = Component(id="5", children=[c2, c4]) + + c3b = Component(id="3") + assert c5["3"] == c3 + assert c5["3"] != "3" + assert c5["3"] is not c3b + + c5["3"] = c3b + assert c5["3"] is c3b + assert c5["3"] is not c3 + + c2b = Component(id="2") + c5["2"] = c2b + assert c5["4"] is c4 + assert c5["2"] is not c2 + assert c5["2"] is c2b + with pytest.raises(KeyError): + c5["1"] + + +def test_set_item_raises_key_error(): + c1 = Component(id="1") + c2 = Component(id="2", children=[c1]) + with pytest.raises(KeyError): + c2["3"] = Component(id="3") + + +def test_del_item_from_list(): + c1 = Component(id="1") + c2 = Component(id="2") + c3 = Component(id="3", children=[c1, c2]) + assert c3["1"] == c1 + assert c3["2"] == c2 + del c3["2"] + with pytest.raises(KeyError): + c3["2"] + assert c3.children == [c1] + + del c3["1"] + with pytest.raises(KeyError): + c3["1"] + assert c3.children == [] + + +def test_del_item_from_class(): + c1 = Component(id="1") + c2 = Component(id="2", children=c1) + assert c2["1"] == c1 + del c2["1"] + with pytest.raises(KeyError): + c2["1"] + + assert c2.children is None + + +def test_to_plotly_json_without_children(): + c = Component(id="a") + c._prop_names = ("id",) + c._type = "MyComponent" + c._namespace = "basic" + assert c.to_plotly_json() == { + "namespace": "basic", + "props": {"id": "a"}, + "type": "MyComponent", + } + + +def test_to_plotly_json_with_null_arguments(): + c = Component(id="a") + c._prop_names = ("id", "style") + c._type = "MyComponent" + c._namespace = "basic" + assert c.to_plotly_json() == { + "namespace": "basic", + "props": {"id": "a"}, + "type": "MyComponent", + } + + c = Component(id="a", style=None) + c._prop_names = ("id", "style") + c._type = "MyComponent" + c._namespace = "basic" + assert c.to_plotly_json() == { + "namespace": "basic", + "props": {"id": "a", "style": None}, + "type": "MyComponent", + } + + +def test_to_plotly_json_with_children(): + c = Component(id="a", children="Hello World") + c._prop_names = ("id", "children") + c._type = "MyComponent" + c._namespace = "basic" + assert c.to_plotly_json() == { + "namespace": "basic", + "props": { + "id": "a", + # TODO - Rename 'children' to 'children' + "children": "Hello World", + }, + "type": "MyComponent", + } + + +def test_to_plotly_json_with_wildcards(): + c = Component( + id="a", **{"aria-expanded": "true", "data-toggle": "toggled", "data-none": None} + ) + c._prop_names = ("id",) + c._type = "MyComponent" + c._namespace = "basic" + assert c.to_plotly_json() == { + "namespace": "basic", + "props": { + "aria-expanded": "true", + "data-toggle": "toggled", + "data-none": None, + "id": "a", + }, + "type": "MyComponent", + } + + +def test_len(): + assert len(Component()) == 0 + assert len(Component(children="Hello World")) == 1 + assert len(Component(children=Component())) == 1 + assert len(Component(children=[Component(), Component()])) == 2 + assert len(Component(children=[Component(children=Component()), Component()])) == 3 + + +def test_iter(): + # The mixin methods from MutableMapping were cute but probably never + # used - at least not by us. Test that they're gone + + # keys, __contains__, items, values, and more are all mixin methods + # that we get for free by inheriting from the MutableMapping + # and behave as according to our implementation of __iter__ + + c = Component( + id="1", + children=[ + Component(id="2", children=[Component(id="3", children=Component(id="4"))]), + Component(id="5", children=[Component(id="6", children="Hello World")]), + Component(), + Component(children="Hello World"), + Component(children=Component(id="7")), + Component(children=[Component(id="8")]), + ], + ) - "- optionalAny (boolean | number | string | dict | " - "list; optional)", - - "- customProp (optional)", - "- customArrayProp (list; optional)", - '- data-* (string; optional)', - '- aria-* (string; optional)', - '- in (string; optional)', - '- id (string; optional)', - ' ' - ])[i] - ) - - -class TestFlowMetaDataConversions(unittest.TestCase): - def setUp(self): - path = os.path.join(_dir, 'flow_metadata_test.json') - with open(path) as data_file: - json_string = data_file.read() - data = json\ - .JSONDecoder(object_pairs_hook=OrderedDict)\ - .decode(json_string) - self.data = data - - self.expected_arg_strings = OrderedDict([ - ['children', 'a list of or a singular dash component, string or number'], - - ['requiredString', 'string'], - - ['optionalString', 'string'], - - ['optionalBoolean', 'boolean'], - - ['optionalFunc', ''], - - ['optionalNode', 'a list of or a singular dash component, string or number'], - - ['optionalArray', 'list'], - - ['requiredUnion', 'string | number'], - - ['optionalSignature(shape)', '\n'.join([ - - "dict containing keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', 'style', 'value'.", - "Those keys have the following types:", - "- checked (boolean; optional)", - "- children (a list of or a singular dash component, string or number; optional)", - "- customData (bool | number | str | dict | list; required): A test description", - "- disabled (boolean; optional)", - "- label (string; optional)", - "- primaryText (string; required): Another test description", - "- secondaryText (string; optional)", - "- style (dict; optional)", - "- value (bool | number | str | dict | list; required)" - - ])], - - ['requiredNested', '\n'.join([ - - "dict containing keys 'customData', 'value'.", - "Those keys have the following types:", - "- customData (dict; required): customData has the following type: dict containing keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', 'style', 'value'.", - " Those keys have the following types:", - " - checked (boolean; optional)", - " - children (a list of or a singular dash component, string or number; optional)", - " - customData (bool | number | str | dict | list; required)", - " - disabled (boolean; optional)", - " - label (string; optional)", - " - primaryText (string; required)", - " - secondaryText (string; optional)", - " - style (dict; optional)", - " - value (bool | number | str | dict | list; required)", - "- value (bool | number | str | dict | list; required)", - - ])], - ]) - - def test_docstring(self): - docstring = create_docstring( - 'Flow_component', - self.data['props'], - self.data['description'], - ) - prohibit_events(self.data['props']), - assert_flow_docstring(self.assertEqual, docstring) - - def test_docgen_to_python_args(self): - - props = self.data['props'] - - for prop_name, prop in list(props.items()): - self.assertEqual( - js_to_py_type(prop['flowType'], is_flow_type=True), - self.expected_arg_strings[prop_name] - ) - - -def assert_flow_docstring(assertEqual, docstring): - for i, line in enumerate(docstring.split('\n')): - assertEqual(line, ([ - "A Flow_component component.", - "This is a test description of the component.", - "It's multiple lines long.", - "", - "Keyword arguments:", - "- requiredString (string; required): A required string", - "- optionalString (string; default ''): A string that isn't required.", - "- optionalBoolean (boolean; default False): A boolean test", - - "- optionalNode (a list of or a singular dash component, string or number; optional): " - "A node test", - - "- optionalArray (list; optional): An array test with a particularly ", - "long description that covers several lines. It includes the newline character ", - "and should span 3 lines in total.", - - "- requiredUnion (string | number; required)", - - "- optionalSignature(shape) (dict; optional): This is a test of an object's shape. " - "optionalSignature(shape) has the following type: dict containing keys 'checked', " - "'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', " - "'style', 'value'.", - - " Those keys have the following types:", - " - checked (boolean; optional)", - " - children (a list of or a singular dash component, string or number; optional)", - " - customData (bool | number | str | dict | list; required): A test description", - " - disabled (boolean; optional)", - " - label (string; optional)", - " - primaryText (string; required): Another test description", - " - secondaryText (string; optional)", - " - style (dict; optional)", - " - value (bool | number | str | dict | list; required)", - - "- requiredNested (dict; required): requiredNested has the following type: dict containing " - "keys 'customData', 'value'.", - - " Those keys have the following types:", - - " - customData (dict; required): customData has the following type: dict containing " - "keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', " - "'secondaryText', 'style', 'value'.", - - " Those keys have the following types:", - " - checked (boolean; optional)", - " - children (a list of or a singular dash component, string or number; optional)", - " - customData (bool | number | str | dict | list; required)", - " - disabled (boolean; optional)", - " - label (string; optional)", - " - primaryText (string; required)", - " - secondaryText (string; optional)", - " - style (dict; optional)", - " - value (bool | number | str | dict | list; required)", - " - value (bool | number | str | dict | list; required)", - ])[i] - ) + mixins = [ + "clear", + "get", + "items", + "keys", + "pop", + "popitem", + "setdefault", + "update", + "values", + ] + + for m in mixins: + assert not hasattr(c, m), "should not have method " + m + + keys = ["2", "3", "4", "5", "6", "7", "8"] + + for k in keys: + # test __contains__() + assert k in c, "should find key " + k + # test __getitem__() + assert c[k].id == k, "key {} points to the right item".format(k) + + # test __iter__() + keys2 = [] + for k in c: + keys2.append(k) + assert k in keys, "iteration produces key " + k + + assert len(keys) == len(keys2), "iteration produces no extra keys" diff --git a/tests/unit/development/test_component_loader.py b/tests/unit/development/test_component_loader.py index 7f3ce871fb..4c525fdf6d 100644 --- a/tests/unit/development/test_component_loader.py +++ b/tests/unit/development/test_component_loader.py @@ -2,16 +2,16 @@ import json import os import shutil -import unittest -from dash.development.component_loader import load_components, generate_classes -from dash.development.base_component import ( - Component -) + +import pytest + from dash.development._py_components_generation import generate_class +from dash.development.base_component import Component +from dash.development.component_loader import load_components, generate_classes -METADATA_PATH = 'metadata.json' +METADATA_PATH = "metadata.json" -METADATA_STRING = '''{ +METADATA_STRING = """{ "MyComponent.react.js": { "props": { "foo": { @@ -96,125 +96,99 @@ } } } -}''' -METADATA = json\ - .JSONDecoder(object_pairs_hook=collections.OrderedDict)\ - .decode(METADATA_STRING) - - -class TestLoadComponents(unittest.TestCase): - def setUp(self): - with open(METADATA_PATH, 'w') as f: - f.write(METADATA_STRING) - - def tearDown(self): - os.remove(METADATA_PATH) - - def test_loadcomponents(self): - MyComponent = generate_class( - 'MyComponent', - METADATA['MyComponent.react.js']['props'], - METADATA['MyComponent.react.js']['description'], - 'default_namespace' - ) - - A = generate_class( - 'A', - METADATA['A.react.js']['props'], - METADATA['A.react.js']['description'], - 'default_namespace' - ) - - c = load_components(METADATA_PATH) - - MyComponentKwargs = { - 'foo': 'Hello World', - 'bar': 'Lah Lah', - 'baz': 'Lemons', - 'data-foo': 'Blah', - 'aria-bar': 'Seven', - 'children': 'Child' - } - AKwargs = { - 'children': 'Child', - 'href': 'Hello World' - } +}""" +METADATA = json.JSONDecoder(object_pairs_hook=collections.OrderedDict).decode( + METADATA_STRING +) - self.assertTrue( - isinstance(MyComponent(**MyComponentKwargs), Component) - ) - - self.assertEqual( - repr(MyComponent(**MyComponentKwargs)), - repr(c[0](**MyComponentKwargs)) - ) - - self.assertEqual( - repr(A(**AKwargs)), - repr(c[1](**AKwargs)) - ) - - -class TestGenerateClasses(unittest.TestCase): - def setUp(self): - with open(METADATA_PATH, 'w') as f: - f.write(METADATA_STRING) - os.makedirs('default_namespace') - - init_file_path = 'default_namespace/__init__.py' - with open(init_file_path, 'a'): - os.utime(init_file_path, None) - - def tearDown(self): - os.remove(METADATA_PATH) - shutil.rmtree('default_namespace') - - def test_loadcomponents(self): - MyComponent_runtime = generate_class( - 'MyComponent', - METADATA['MyComponent.react.js']['props'], - METADATA['MyComponent.react.js']['description'], - 'default_namespace' - ) - - A_runtime = generate_class( - 'A', - METADATA['A.react.js']['props'], - METADATA['A.react.js']['description'], - 'default_namespace' - ) - - generate_classes('default_namespace', METADATA_PATH) - from default_namespace.MyComponent import MyComponent \ - as MyComponent_buildtime - from default_namespace.A import A as A_buildtime - - MyComponentKwargs = { - 'foo': 'Hello World', - 'bar': 'Lah Lah', - 'baz': 'Lemons', - 'data-foo': 'Blah', - 'aria-bar': 'Seven', - 'children': 'Child' - } - AKwargs = { - 'children': 'Child', - 'href': 'Hello World' - } - self.assertTrue( - isinstance( - MyComponent_buildtime(**MyComponentKwargs), - Component - ) - ) - - self.assertEqual( - repr(MyComponent_buildtime(**MyComponentKwargs)), - repr(MyComponent_runtime(**MyComponentKwargs)), - ) - - self.assertEqual( - repr(A_runtime(**AKwargs)), - repr(A_buildtime(**AKwargs)) - ) +@pytest.fixture +def write_metada_file(): + with open(METADATA_PATH, "w") as f: + f.write(METADATA_STRING) + yield + os.remove(METADATA_PATH) + + +@pytest.fixture +def make_namespace(): + os.makedirs("default_namespace") + init_file_path = "default_namespace/__init__.py" + with open(init_file_path, "a"): + os.utime(init_file_path, None) + yield + shutil.rmtree("default_namespace") + + +def test_loadcomponents(write_metada_file): + my_component = generate_class( + "MyComponent", + METADATA["MyComponent.react.js"]["props"], + METADATA["MyComponent.react.js"]["description"], + "default_namespace", + ) + + a_component = generate_class( + "A", + METADATA["A.react.js"]["props"], + METADATA["A.react.js"]["description"], + "default_namespace", + ) + + c = load_components(METADATA_PATH) + + my_component_kwargs = { + "foo": "Hello World", + "bar": "Lah Lah", + "baz": "Lemons", + "data-foo": "Blah", + "aria-bar": "Seven", + "children": "Child", + } + a_kwargs = {"children": "Child", "href": "Hello World"} + + assert isinstance(my_component(**my_component_kwargs), Component) + + assert repr(my_component(**my_component_kwargs)) == repr( + c[0](**my_component_kwargs) + ) + + assert repr(a_component(**a_kwargs)) == repr(c[1](**a_kwargs)) + + +def test_loadcomponents_from_generated_class(write_metada_file, make_namespace): + my_component_runtime = generate_class( + "MyComponent", + METADATA["MyComponent.react.js"]["props"], + METADATA["MyComponent.react.js"]["description"], + "default_namespace", + ) + + a_runtime = generate_class( + "A", + METADATA["A.react.js"]["props"], + METADATA["A.react.js"]["description"], + "default_namespace", + ) + + generate_classes("default_namespace", METADATA_PATH) + from default_namespace.MyComponent import MyComponent as MyComponent_buildtime + from default_namespace.A import A as A_buildtime + + my_component_kwargs = { + "foo": "Hello World", + "bar": "Lah Lah", + "baz": "Lemons", + "data-foo": "Blah", + "aria-bar": "Seven", + "children": "Child", + } + a_kwargs = {"children": "Child", "href": "Hello World"} + + assert isinstance(MyComponent_buildtime(**my_component_kwargs), Component) + + assert repr(MyComponent_buildtime(**my_component_kwargs)) == repr( + my_component_runtime(**my_component_kwargs) + ) + + assert repr(a_runtime(**a_kwargs)) == repr(A_buildtime(**a_kwargs)) diff --git a/tests/unit/development/test_flow_metadata_conversions.py b/tests/unit/development/test_flow_metadata_conversions.py new file mode 100644 index 0000000000..822df417e9 --- /dev/null +++ b/tests/unit/development/test_flow_metadata_conversions.py @@ -0,0 +1,143 @@ +import json +import os +from collections import OrderedDict +from difflib import unified_diff + +import pytest + +from dash.development._py_components_generation import ( + create_docstring, + prohibit_events, + js_to_py_type, +) + +_dir = os.path.dirname(os.path.abspath(__file__)) + +expected_arg_strings = OrderedDict( + [ + ["children", "a list of or a singular dash component, string or number"], + ["requiredString", "string"], + ["optionalString", "string"], + ["optionalBoolean", "boolean"], + ["optionalFunc", ""], + ["optionalNode", "a list of or a singular dash component, string or number"], + ["optionalArray", "list"], + ["requiredUnion", "string | number"], + [ + "optionalSignature(shape)", + "\n".join( + [ + "dict containing keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', 'style', 'value'.", + "Those keys have the following types:", + "- checked (boolean; optional)", + "- children (a list of or a singular dash component, string or number; optional)", + "- customData (bool | number | str | dict | list; required): A test description", + "- disabled (boolean; optional)", + "- label (string; optional)", + "- primaryText (string; required): Another test description", + "- secondaryText (string; optional)", + "- style (dict; optional)", + "- value (bool | number | str | dict | list; required)", + ] + ), + ], + [ + "requiredNested", + "\n".join( + [ + "dict containing keys 'customData', 'value'.", + "Those keys have the following types:", + "- customData (dict; required): customData has the following type: dict containing keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', 'style', 'value'.", + " Those keys have the following types:", + " - checked (boolean; optional)", + " - children (a list of or a singular dash component, string or number; optional)", + " - customData (bool | number | str | dict | list; required)", + " - disabled (boolean; optional)", + " - label (string; optional)", + " - primaryText (string; required)", + " - secondaryText (string; optional)", + " - style (dict; optional)", + " - value (bool | number | str | dict | list; required)", + "- value (bool | number | str | dict | list; required)", + ] + ), + ], + ] +) + +expected_doc = [ + "A Flow_component component.", + "This is a test description of the component.", + "It's multiple lines long.", + "", + "Keyword arguments:", + "- requiredString (string; required): A required string", + "- optionalString (string; default ''): A string that isn't required.", + "- optionalBoolean (boolean; default False): A boolean test", + "- optionalNode (a list of or a singular dash component, string or number; optional): " + "A node test", + "- optionalArray (list; optional): An array test with a particularly ", + "long description that covers several lines. It includes the newline character ", + "and should span 3 lines in total.", + "- requiredUnion (string | number; required)", + "- optionalSignature(shape) (dict; optional): This is a test of an object's shape. " + "optionalSignature(shape) has the following type: dict containing keys 'checked', " + "'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', " + "'style', 'value'.", + " Those keys have the following types:", + " - checked (boolean; optional)", + " - children (a list of or a singular dash component, string or number; optional)", + " - customData (bool | number | str | dict | list; required): A test description", + " - disabled (boolean; optional)", + " - label (string; optional)", + " - primaryText (string; required): Another test description", + " - secondaryText (string; optional)", + " - style (dict; optional)", + " - value (bool | number | str | dict | list; required)", + "- requiredNested (dict; required): requiredNested has the following type: dict containing " + "keys 'customData', 'value'.", + " Those keys have the following types:", + " - customData (dict; required): customData has the following type: dict containing " + "keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', " + "'secondaryText', 'style', 'value'.", + " Those keys have the following types:", + " - checked (boolean; optional)", + " - children (a list of or a singular dash component, string or number; optional)", + " - customData (bool | number | str | dict | list; required)", + " - disabled (boolean; optional)", + " - label (string; optional)", + " - primaryText (string; required)", + " - secondaryText (string; optional)", + " - style (dict; optional)", + " - value (bool | number | str | dict | list; required)", + " - value (bool | number | str | dict | list; required)", +] + + +@pytest.fixture +def load_test_flow_metadata_json(): + path = os.path.join(_dir, "flow_metadata_test.json") + with open(path) as data_file: + json_string = data_file.read() + data = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(json_string) + return data + + +def test_docstring(load_test_flow_metadata_json): + docstring = create_docstring( + "Flow_component", + load_test_flow_metadata_json["props"], + load_test_flow_metadata_json["description"], + ) + prohibit_events(load_test_flow_metadata_json["props"]), + assert not list(unified_diff(expected_doc, docstring.splitlines())) + + +def test_docgen_to_python_args(load_test_flow_metadata_json): + props = load_test_flow_metadata_json["props"] + + for prop_name, prop in list(props.items()): + assert ( + js_to_py_type(prop["flowType"], is_flow_type=True) + == expected_arg_strings[prop_name] + ) diff --git a/tests/unit/development/test_generate_class.py b/tests/unit/development/test_generate_class.py new file mode 100644 index 0000000000..49ba7d5684 --- /dev/null +++ b/tests/unit/development/test_generate_class.py @@ -0,0 +1,147 @@ +import json +import os +from collections import OrderedDict +from difflib import unified_diff + +import pytest + +from dash.development._py_components_generation import generate_class +from dash.development.component_generator import reserved_words +from . import _dir, expected_table_component_doc + + +@pytest.fixture +def component_class(load_test_metadata_json): + return generate_class( + typename="Table", + props=load_test_metadata_json["props"], + description=load_test_metadata_json["description"], + namespace="TableComponents", + ) + + +@pytest.fixture +def component_written_class(): + path = os.path.join(_dir, "metadata_required_test.json") + with open(path) as data_file: + json_string = data_file.read() + required_data = json.JSONDecoder(object_pairs_hook=OrderedDict).decode( + json_string + ) + + return generate_class( + typename="TableRequired", + props=required_data["props"], + description=required_data["description"], + namespace="TableComponents", + ) + + +def test_to_plotly_json(component_class): + c = component_class() + assert c.to_plotly_json() == { + "namespace": "TableComponents", + "type": "Table", + "props": {"children": None}, + } + + c = component_class(id="my-id") + assert c.to_plotly_json() == { + "namespace": "TableComponents", + "type": "Table", + "props": {"children": None, "id": "my-id"}, + } + + c = component_class(id="my-id", optionalArray=None) + assert c.to_plotly_json() == { + "namespace": "TableComponents", + "type": "Table", + "props": {"children": None, "id": "my-id", "optionalArray": None}, + } + + +def test_arguments_become_attributes(component_class): + kwargs = {"id": "my-id", "children": "text children", "optionalArray": [[1, 2, 3]]} + component_instance = component_class(**kwargs) + for k, v in list(kwargs.items()): + assert getattr(component_instance, k) == v + + +def test_repr_single_default_argument(component_class): + c1 = component_class("text children") + c2 = component_class(children="text children") + assert repr(c1) == "Table('text children')" + assert repr(c2) == "Table('text children')" + + +def test_repr_single_non_default_argument(component_class): + c = component_class(id="my-id") + assert repr(c) == "Table(id='my-id')" + + +def test_repr_multiple_arguments(component_class): + # Note how the order in which keyword arguments are supplied is + # not always equal to the order in the repr of the component + c = component_class(id="my id", optionalArray=[1, 2, 3]) + assert repr(c) == "Table(optionalArray=[1, 2, 3], id='my id')" + + +def test_repr_nested_arguments(component_class): + c1 = component_class(id="1") + c2 = component_class(id="2", children=c1) + c3 = component_class(children=c2) + assert repr(c3) == "Table(Table(children=Table(id='1'), id='2'))" + + +def test_repr_with_wildcards(component_class): + c = component_class(id="1", **{"data-one": "one", "aria-two": "two"}) + data_first = "Table(id='1', data-one='one', aria-two='two')" + aria_first = "Table(id='1', aria-two='two', data-one='one')" + repr_string = repr(c) + + assert repr_string == data_first or repr_string == aria_first + + +def test_docstring(component_class): + assert not list( + unified_diff(expected_table_component_doc, component_class.__doc__.splitlines()) + ) + + +def test_no_events(component_class): + assert not hasattr(component_class, "available_events") + + +def test_required_props(component_written_class): + with pytest.raises(Exception): + component_written_class() + component_written_class(id="test") + with pytest.raises(Exception): + component_written_class(id="test", lahlah="test") + with pytest.raises(Exception): + component_written_class(children="test") + + +def test_attrs_match_forbidden_props(component_class): + assert "_.*" in reserved_words, "props cannot have leading underscores" + + # props are not added as attrs unless explicitly provided + # except for children, which is always set if it's a prop at all. + expected_attrs = set(reserved_words + ["children"]) - {"_.*"} + c = component_class() + base_attrs = set(dir(c)) + extra_attrs = set(a for a in base_attrs if a[0] != "_") + + assert ( + extra_attrs == expected_attrs + ), "component has only underscored and reserved word attrs" + + # setting props causes them to show up as attrs + c2 = component_class("children", id="c2", optionalArray=[1]) + prop_attrs = set(dir(c2)) + + assert base_attrs - prop_attrs == set([]), "no attrs were removed" + assert prop_attrs - base_attrs == { + "id", + "optionalArray", + }, "explicit props were added as attrs" diff --git a/tests/unit/development/test_generate_class_file.py b/tests/unit/development/test_generate_class_file.py new file mode 100644 index 0000000000..eecc2ba34e --- /dev/null +++ b/tests/unit/development/test_generate_class_file.py @@ -0,0 +1,80 @@ +import os +import shutil +from difflib import unified_diff + +import pytest + +# noinspection PyProtectedMember +from dash.development._py_components_generation import ( + generate_class_string, + generate_class_file, +) +from . import _dir, has_trailing_space + +# Import string not included in generated class string +import_string = ( + "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + + "from dash.development.base_component import" + + " Component, _explicitize_args\n\n\n" +) + + +@pytest.fixture +def make_component_dir(load_test_metadata_json): + # Create a folder for the new component file + os.makedirs("TableComponents") + + yield load_test_metadata_json + + shutil.rmtree("TableComponents") + + +@pytest.fixture +def expected_class_string(): + # The expected result for both class string and class file generation + expected_string_path = os.path.join(_dir, "metadata_test.py") + with open(expected_string_path, "r") as f: + return f.read() + + +@pytest.fixture +def component_class_string(make_component_dir): + return import_string + generate_class_string( + typename="Table", + props=make_component_dir["props"], + description=make_component_dir["description"], + namespace="TableComponents", + ) + + +@pytest.fixture +def written_class_string(make_component_dir): + # Class string written to file + generate_class_file( + typename="Table", + props=make_component_dir["props"], + description=make_component_dir["description"], + namespace="TableComponents", + ) + written_file_path = os.path.join("TableComponents", "Table.py") + with open(written_file_path, "r") as f: + return f.read() + + +def test_class_string(expected_class_string, component_class_string): + assert not list( + unified_diff( + expected_class_string.splitlines(), component_class_string.splitlines() + ) + ) + + assert not has_trailing_space(component_class_string) + + +def test_class_file(expected_class_string, written_class_string): + assert not list( + unified_diff( + expected_class_string.splitlines(), written_class_string.splitlines() + ) + ) + assert not has_trailing_space(written_class_string) diff --git a/tests/unit/development/test_metadata_conversions.py b/tests/unit/development/test_metadata_conversions.py new file mode 100644 index 0000000000..008efebe66 --- /dev/null +++ b/tests/unit/development/test_metadata_conversions.py @@ -0,0 +1,85 @@ +from collections import OrderedDict +from difflib import unified_diff + +from dash.development._py_components_generation import ( + create_docstring, + prohibit_events, + js_to_py_type, +) +from . import expected_table_component_doc + +expected_arg_strings = OrderedDict( + [ + ["children", "a list of or a singular dash component, string or number"], + ["optionalArray", "list"], + ["optionalBool", "boolean"], + ["optionalFunc", ""], + ["optionalNumber", "number"], + ["optionalObject", "dict"], + ["optionalString", "string"], + ["optionalSymbol", ""], + ["optionalElement", "dash component"], + ["optionalNode", "a list of or a singular dash component, string or number"], + ["optionalMessage", ""], + ["optionalEnum", "a value equal to: 'News', 'Photos'"], + ["optionalUnion", "string | number"], + ["optionalArrayOf", "list of numbers"], + ["optionalObjectOf", "dict with strings as keys and values of type number"], + [ + "optionalObjectWithExactAndNestedDescription", + "\n".join( + [ + "dict containing keys 'color', 'fontSize', 'figure'.", + "Those keys have the following types:", + " - color (string; optional)", + " - fontSize (number; optional)", + " - figure (dict; optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.", + # noqa: E501 + "Those keys have the following types:", + " - data (list of dicts; optional): data is a collection of traces", + " - layout (dict; optional): layout describes the rest of the figure", # noqa: E501 + ] + ), + ], + [ + "optionalObjectWithShapeAndNestedDescription", + "\n".join( + [ + "dict containing keys 'color', 'fontSize', 'figure'.", + "Those keys have the following types:", + " - color (string; optional)", + " - fontSize (number; optional)", + " - figure (dict; optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.", + # noqa: E501 + "Those keys have the following types:", + " - data (list of dicts; optional): data is a collection of traces", + " - layout (dict; optional): layout describes the rest of the figure", # noqa: E501 + ] + ), + ], + ["optionalAny", "boolean | number | string | dict | list"], + ["customProp", ""], + ["customArrayProp", "list"], + ["data-*", "string"], + ["aria-*", "string"], + ["in", "string"], + ["id", "string"], + ] +) + + +def test_docstring(load_test_metadata_json): + docstring = create_docstring( + "Table", + load_test_metadata_json["props"], + load_test_metadata_json["description"], + ) + prohibit_events(load_test_metadata_json["props"]), + assert not list(unified_diff(expected_table_component_doc, docstring.splitlines())) + + +def test_docgen_to_python_args(load_test_metadata_json): + props = load_test_metadata_json["props"] + + for prop_name, prop in list(props.items()): + assert js_to_py_type(prop["type"]) == expected_arg_strings[prop_name] diff --git a/tests/unit/test_configs.py b/tests/unit/test_configs.py index 341be7ad94..2481174b99 100644 --- a/tests/unit/test_configs.py +++ b/tests/unit/test_configs.py @@ -1,156 +1,158 @@ import os -import unittest + import pytest from flask import Flask + +from dash import Dash, exceptions as _exc + # noinspection PyProtectedMember from dash._configs import ( - pathname_configs, DASH_ENV_VARS, get_combined_config, load_dash_env_vars) -from dash import Dash, exceptions as _exc + pathname_configs, + DASH_ENV_VARS, + get_combined_config, + load_dash_env_vars, +) from dash._utils import get_asset_path -class TestConfigs(unittest.TestCase): +@pytest.fixture +def empty_environ(): + for k in DASH_ENV_VARS.keys(): + if k in os.environ: + os.environ.pop(k) - def setUp(self): - for k in DASH_ENV_VARS.keys(): - if k in os.environ: - os.environ.pop(k) - def test_dash_env_vars(self): - self.assertEqual( - {None}, {val for _, val in DASH_ENV_VARS.items()}, - "initial var values are None without extra OS environ setting") +def test_dash_env_vars(empty_environ): + assert {None} == { + val for _, val in DASH_ENV_VARS.items() + }, "initial var values are None without extra OS environ setting" - def test_valid_pathname_prefix_init(self): - _, routes, req = pathname_configs() - self.assertEqual('/', routes) - self.assertEqual('/', req) +@pytest.mark.parametrize( + "route_prefix, req_prefix, expected_route, expected_req", + [ + (None, None, "/", "/"), + ("/dash/", None, None, "/dash/"), + (None, "/my-dash-app/", "/", "/my-dash-app/"), + ("/dash/", "/my-dash-app/dash/", "/dash/", "/my-dash-app/dash/"), + ], +) +def test_valid_pathname_prefix_init( + empty_environ, route_prefix, req_prefix, expected_route, expected_req +): + _, routes, req = pathname_configs( + routes_pathname_prefix=route_prefix, requests_pathname_prefix=req_prefix + ) - _, routes, req = pathname_configs( - routes_pathname_prefix='/dash/') + if expected_route is not None: + assert routes == expected_route + assert req == expected_req - self.assertEqual('/dash/', req) - _, routes, req = pathname_configs( - requests_pathname_prefix='/my-dash-app/', - ) +def test_invalid_pathname_prefix(empty_environ): + with pytest.raises(_exc.InvalidConfig, match="url_base_pathname"): + _, _, _ = pathname_configs("/my-path", "/another-path") - self.assertEqual(routes, '/') - self.assertEqual(req, '/my-dash-app/') + with pytest.raises(_exc.InvalidConfig) as excinfo: + _, _, _ = pathname_configs( + url_base_pathname="/invalid", routes_pathname_prefix="/invalid" + ) + assert str(excinfo.value).split(".")[0].endswith("`routes_pathname_prefix`") - _, routes, req = pathname_configs( - routes_pathname_prefix='/dash/', - requests_pathname_prefix='/my-dash-app/dash/' + with pytest.raises(_exc.InvalidConfig) as excinfo: + _, _, _ = pathname_configs( + url_base_pathname="/my-path", requests_pathname_prefix="/another-path" ) + assert str(excinfo.value).split(".")[0].endswith("`requests_pathname_prefix`") - self.assertEqual('/dash/', routes) - self.assertEqual('/my-dash-app/dash/', req) - - def test_invalid_pathname_prefix(self): - with self.assertRaises(_exc.InvalidConfig) as context: - _, _, _ = pathname_configs('/my-path', '/another-path') - - self.assertTrue('url_base_pathname' in str(context.exception)) - - with self.assertRaises(_exc.InvalidConfig) as context: - _, _, _ = pathname_configs( - url_base_pathname='/invalid', - routes_pathname_prefix='/invalid') - - self.assertTrue(str(context.exception).split('.')[0] - .endswith('`routes_pathname_prefix`')) - - with self.assertRaises(_exc.InvalidConfig) as context: - _, _, _ = pathname_configs( - url_base_pathname='/my-path', - requests_pathname_prefix='/another-path') - - self.assertTrue(str(context.exception).split('.')[0] - .endswith('`requests_pathname_prefix`')) - - with self.assertRaises(_exc.InvalidConfig) as context: - _, _, _ = pathname_configs('my-path') - - self.assertTrue('start with `/`' in str(context.exception)) - - with self.assertRaises(_exc.InvalidConfig) as context: - _, _, _ = pathname_configs('/my-path') - - self.assertTrue('end with `/`' in str(context.exception)) - - def test_pathname_prefix_from_environ_app_name(self): - os.environ['DASH_APP_NAME'] = 'my-dash-app' - _, routes, req = pathname_configs() - self.assertEqual('/my-dash-app/', req) - self.assertEqual('/', routes) - - def test_pathname_prefix_environ_routes(self): - os.environ['DASH_ROUTES_PATHNAME_PREFIX'] = '/routes/' - _, routes, _ = pathname_configs() - self.assertEqual('/routes/', routes) - - def test_pathname_prefix_environ_requests(self): - os.environ['DASH_REQUESTS_PATHNAME_PREFIX'] = '/requests/' - _, _, req = pathname_configs() - self.assertEqual('/requests/', req) - - def test_pathname_prefix_assets(self): - req = '/' - path = get_asset_path(req, 'reset.css', 'assets') - self.assertEqual('/assets/reset.css', path) - - req = '/requests/' - path = get_asset_path(req, 'reset.css', 'assets') - self.assertEqual('/requests/assets/reset.css', path) - - req = '/requests/routes/' - path = get_asset_path(req, 'reset.css', 'assets') - self.assertEqual('/requests/routes/assets/reset.css', path) - - def test_get_combined_config_dev_tools_ui(self): - val1 = get_combined_config('ui', None, default=False) - self.assertEqual( - val1, False, - "should return the default value if None is provided for init and environment") - os.environ['DASH_UI'] = 'true' - val2 = get_combined_config('ui', None, default=False) - self.assertEqual(val2, True, "should return the set environment value as True") - val3 = get_combined_config('ui', False, default=True) - self.assertEqual(val3, False, "init value overrides the environment value") - - def test_get_combined_config_props_check(self): - val1 = get_combined_config('props_check', None, default=False) - self.assertEqual( - val1, False, - "should return the default value if None is provided for init and environment") - os.environ['DASH_PROPS_CHECK'] = 'true' - val2 = get_combined_config('props_check', None, default=False) - self.assertEqual(val2, True, "should return the set environment value as True") - val3 = get_combined_config('props_check', False, default=True) - self.assertEqual(val3, False, "init value overrides the environment value") - - def test_load_dash_env_vars_refects_to_os_environ(self): - for var in DASH_ENV_VARS.keys(): - os.environ[var] = 'true' - vars = load_dash_env_vars() - self.assertEqual(vars[var], 'true') - os.environ[var] = 'false' - vars = load_dash_env_vars() - self.assertEqual(vars[var], 'false') - - -@pytest.mark.parametrize('name, server, expected', [ - (None, True, '__main__'), - ('test', True, 'test'), - ('test', False, 'test'), - (None, Flask('test'), 'test'), - ('test', Flask('other'), 'test'), -]) -def test_app_name_server(name, server, expected): - app = Dash(name=name, server=server) - assert app.config.name == expected + with pytest.raises(_exc.InvalidConfig, match="start with `/`"): + _, _, _ = pathname_configs("my-path") + + with pytest.raises(_exc.InvalidConfig, match="end with `/`"): + _, _, _ = pathname_configs("/my-path") + + +def test_pathname_prefix_from_environ_app_name(empty_environ): + os.environ["DASH_APP_NAME"] = "my-dash-app" + _, routes, req = pathname_configs() + assert req == "/my-dash-app/" + assert routes == "/" + + +def test_pathname_prefix_environ_routes(empty_environ): + os.environ["DASH_ROUTES_PATHNAME_PREFIX"] = "/routes/" + _, routes, _ = pathname_configs() + assert routes == "/routes/" + + +def test_pathname_prefix_environ_requests(empty_environ): + os.environ["DASH_REQUESTS_PATHNAME_PREFIX"] = "/requests/" + _, _, req = pathname_configs() + assert req == "/requests/" -if __name__ == '__main__': - unittest.main() +@pytest.mark.parametrize( + "req, expected", + [ + ("/", "/assets/reset.css"), + ("/requests/", "/requests/assets/reset.css"), + ("/requests/routes/", "/requests/routes/assets/reset.css"), + ], +) +def test_pathname_prefix_assets(empty_environ, req, expected): + path = get_asset_path(req, "reset.css", "assets") + assert path == expected + + +def test_get_combined_config_dev_tools_ui(empty_environ): + val1 = get_combined_config("ui", None, default=False) + assert ( + not val1 + ), "should return the default value if None is provided for init and environment" + + os.environ["DASH_UI"] = "true" + val2 = get_combined_config("ui", None, default=False) + assert val2, "should return the set environment value as True" + + val3 = get_combined_config("ui", False, default=True) + assert not val3, "init value overrides the environment value" + + +def test_get_combined_config_props_check(empty_environ): + val1 = get_combined_config("props_check", None, default=False) + assert ( + not val1 + ), "should return the default value if None is provided for init and environment" + + os.environ["DASH_PROPS_CHECK"] = "true" + val2 = get_combined_config("props_check", None, default=False) + assert val2, "should return the set environment value as True" + + val3 = get_combined_config("props_check", False, default=True) + assert not val3, "init value overrides the environment value" + + +def test_load_dash_env_vars_refects_to_os_environ(empty_environ): + for var in DASH_ENV_VARS.keys(): + os.environ[var] = "true" + vars = load_dash_env_vars() + assert vars[var] == "true" + + os.environ[var] = "false" + vars = load_dash_env_vars() + assert vars[var] == "false" + + +@pytest.mark.parametrize( + "name, server, expected", + [ + (None, True, "__main__"), + ("test", True, "test"), + ("test", False, "test"), + (None, Flask("test"), "test"), + ("test", Flask("other"), "test"), + ], +) +def test_app_name_server(empty_environ, name, server, expected): + app = Dash(name=name, server=server) + assert app.config.name == expected