diff --git a/lib/iris/common/lenient.py b/lib/iris/common/lenient.py index 20e5634694..b12b1ebcdf 100644 --- a/lib/iris/common/lenient.py +++ b/lib/iris/common/lenient.py @@ -117,7 +117,7 @@ def lenient_client_inner(*args, **kwargs): as active at runtime before executing it. """ - with LENIENT.context(*services, active=qualname(func)): + with LENIENT.context(qualname(func), services): result = func(*args, **kwargs) return result @@ -214,7 +214,7 @@ def qualname(func): if callable(func): module = getmodule(func) result = f"{module.__name__}.{func.__qualname__}" - + result = result.replace(".", "_x_") return result @@ -276,11 +276,8 @@ def __call__(self, func): active = self.__dict__["active"] if active is not None and active in self: services = self.__dict__[active] - if isinstance(services, str) or not isinstance( - services, Iterable - ): - services = (services,) - result = service in services + found = [val for svc, val in services if svc == service] + result = found[0] if found else False return result def __contains__(self, name): @@ -334,47 +331,87 @@ def __setitem__(self, name, value): value = tuple([qualname(item) for item in value]) self.__dict__[name] = value + @staticmethod + def _services_asdict(services): + # Convert list of services, or dict(service:value) to dictionary form. + # For lists, also accepts callables, converting to qualnames. + if services is None: + services = {} + elif isinstance(services, str) or not isinstance(services, Iterable): + services = [services] + + if hasattr(services, "items"): + # Dictionary form contains setting values e.g. {'x':1, 'y':2}. + services = { + qualname(service): value for service, value in services.items() + } + else: + # List form (x, y) is equivalent to {x:True. y:True} + services = {qualname(service): True for service in services} + + return services + @contextmanager - def context(self, *args, **kwargs): + def context( + self, + active=None, + services=None, + enable=None, + modify_existing=False, + **service_values_dict, + ): """ Return a context manager which allows temporary modification of the lenient option state for the active thread. - On entry to the context manager, all provided keyword arguments are - applied. On exit from the context manager, the previous lenient option - state is restored. - For example:: - with iris.LENIENT.context(example_lenient_flag=False): - # ... code that expects some non-lenient behaviour + with iris.LENIENT.context(client, (srv1, srv2)): + # ... code that expects some lenient behaviour - .. note:: - iris.LENIENT.example_lenient_flag does not exist and is - provided only as an example. + with iris.LENIENT.context(client, srv1=True, srv2=True): + # ... code that expects some lenient behaviour + + with iris.LENIENT.context(client, srv2=False, modify_existing=True): + # ... code that amends for some NON-lenient behaviour + + with iris.LENIENT.context(client, set1=3, set3='adjust'): + # ... code using non-binary settings. """ def update_client(client, services): if client in self.__dict__: - existing_services = self.__dict__[client] + # Convert existing set of pairs to dict + new_services = {svc: val for svc, val in self.__dict__[client]} else: - existing_services = () + new_services = {} + + # Update dict with new settings. + if not hasattr(services, "keys"): + services = {svc: True for svc in services} + new_services.update(services) - self.__dict__[client] = tuple(set(existing_services + services)) + # Save back, as a set-of-pairs. + self.__dict__[client] = set( + (svc, val) for svc, val in new_services.items() + ) # Save the original state. original_state = deepcopy(self.__dict__) - # Temporarily update the state with the kwargs first. - for name, value in kwargs.items(): - self[name] = value + # First update the state with the fixed keyword controls. + if active is not None: + self.active = qualname(active) + if enable is not None: + self.enable = enable # Get the active client. active = self.__dict__["active"] - if args: + if services or service_values_dict: # Update the client with the provided services. - new_services = tuple([qualname(arg) for arg in args]) + new_services = self._services_asdict(services) + new_services.update(self._services_asdict(service_values_dict)) if active is None: # Ensure not to use "context" as the ephemeral name @@ -382,11 +419,10 @@ def update_client(client, services): # as this causes a namespace clash with this method # i.e., Lenient.context, via Lenient.__getattr__ active = "__context" - self.__dict__["active"] = active - self.__dict__[active] = new_services - else: - # Append provided services to any pre-existing services of the active client. - update_client(active, new_services) + self.active = active + + update_client(active, new_services) + else: # Append previous ephemeral services (for non-specific client) to the active client. if ( @@ -394,7 +430,7 @@ def update_client(client, services): and active != "__context" and "__context" in self.__dict__ ): - new_services = self.__dict__["__context"] + new_services = self.__context update_client(active, new_services) try: @@ -429,14 +465,14 @@ def enable(self, state): raise ValueError(emsg) self.__dict__["enable"] = state - def register_client(self, func, services, append=False): + def register_client(self, client, services, append=False): """ Add the provided mapping of lenient client function/method to required lenient service function/methods. Args: - * func (callable or str): + * client (callable or str): A client function/method or fully qualified string name of the client function/method. @@ -451,26 +487,30 @@ def register_client(self, func, services, append=False): services for the provided lenient client. Default is False. """ - func = qualname(func) + client = qualname(client) cls = self.__class__.__name__ - if func in LENIENT_PROTECTED: - emsg = ( - f"Cannot register {cls!r} protected non-client, got {func!r}." - ) + if client in LENIENT_PROTECTED: + emsg = f"Cannot register {cls!r} protected non-client, got {client!r}." raise ValueError(emsg) - if isinstance(services, str) or not isinstance(services, Iterable): - services = (services,) - if not len(services): + + services = self._services_asdict(services) + if not services: emsg = f"Require at least one {cls!r} lenient client service." raise ValueError(emsg) - services = tuple([qualname(service) for service in services]) + if append: # Service order is not significant, therefore there is no # requirement to preserve it. - existing = self.__dict__[func] if func in self else () - services = tuple(sorted(set(existing) | set(services))) - self.__dict__[func] = services + existing = self.__dict__[client] if client in self else () + updated = dict(existing) + updated.update(services) + services = updated + + # Convert dictionary to a set of pairs. + services = set((svc, services[svc]) for svc in sorted(services.keys())) + # N.B. must be in standard order?? + self.__dict__[client] = services def register_service(self, func): """ diff --git a/lib/iris/tests/unit/common/lenient/test_Lenient.py b/lib/iris/tests/unit/common/lenient/test_Lenient.py index 5c721dab51..31cd66c970 100644 --- a/lib/iris/tests/unit/common/lenient/test_Lenient.py +++ b/lib/iris/tests/unit/common/lenient/test_Lenient.py @@ -17,8 +17,10 @@ from iris.common.lenient import ( LENIENT_ENABLE_DEFAULT, LENIENT_PROTECTED, + LENIENT, Lenient, qualname, + lenient_client, ) @@ -59,14 +61,17 @@ def service2(): def test_kwargs_client_str(self): client = dict(client1="service1") lenient = Lenient(**client) - self.expected.update(dict(client1=("service1",))) + self.expected.update(dict(client1=set([("service1", True)]))) self.assertEqual(self.expected, lenient.__dict__) def test_kwargs_clients_str(self): clients = dict(client1="service1", client2="service2") lenient = Lenient(**clients) self.expected.update( - dict(client1=("service1",), client2=("service2",)) + dict( + client1=set([("service1", True)]), + client2=set([("service2", True)]), + ) ) self.assertEqual(self.expected, lenient.__dict__) @@ -92,8 +97,10 @@ def service2(): lenient = Lenient(**clients) self.expected.update( { - qualname(client1): (qualname(service1),), - qualname(client2): (qualname(service1), qualname(service2)), + qualname(client1): set([(qualname(service1), True)]), + qualname(client2): set( + [(qualname(service1), True), (qualname(service2), True)] + ), } ) self.assertEqual(self.expected, lenient.__dict__) @@ -165,7 +172,9 @@ def test_service_str_with_active_client_with_unmatched_registered_services( service = "myservice" self.lenient.__dict__[service] = True self.lenient.__dict__["active"] = self.client - self.lenient.__dict__[self.client] = ("service1", "service2") + self.lenient.__dict__[self.client] = set( + [("service1", True), ("service2", True)] + ) self.assertFalse(self.lenient(service)) def test_service_callable_with_active_client_with_unmatched_registered_services( @@ -181,14 +190,18 @@ def myclient(): qualname_client = qualname(myclient) self.lenient.__dict__[qualname_service] = True self.lenient.__dict__["active"] = qualname_client - self.lenient.__dict__[qualname_client] = ("service1", "service2") + self.lenient.__dict__[qualname_client] = set( + [("service1", True), ("service2", True)] + ) self.assertFalse(self.lenient(myservice)) def test_service_str_with_active_client_with_registered_services(self): service = "myservice" self.lenient.__dict__[service] = True self.lenient.__dict__["active"] = self.client - self.lenient.__dict__[self.client] = ("service1", "service2", service) + self.lenient.__dict__[self.client] = set( + [("service1", True), ("service2", True), (service, True),] + ) self.assertTrue(self.lenient(service)) def test_service_callable_with_active_client_with_registered_services( @@ -204,10 +217,8 @@ def myclient(): qualname_client = qualname(myclient) self.lenient.__dict__[qualname_service] = True self.lenient.__dict__["active"] = qualname_client - self.lenient.__dict__[qualname_client] = ( - "service1", - "service2", - qualname_service, + self.lenient.__dict__[qualname_client] = set( + [(qualname_service, True), ("service1", True), ("service2", True),] ) self.assertTrue(self.lenient(myservice)) @@ -217,7 +228,7 @@ def test_service_str_with_active_client_with_unmatched_registered_service_str( service = "myservice" self.lenient.__dict__[service] = True self.lenient.__dict__["active"] = self.client - self.lenient.__dict__[self.client] = "serviceXXX" + self.lenient.__dict__[self.client] = set([("serviceXXX", True)]) self.assertFalse(self.lenient(service)) def test_service_callable_with_active_client_with_unmatched_registered_service_str( @@ -233,14 +244,16 @@ def myclient(): qualname_client = qualname(myclient) self.lenient.__dict__[qualname_service] = True self.lenient.__dict__["active"] = qualname_client - self.lenient.__dict__[qualname_client] = f"{qualname_service}XXX" + self.lenient.__dict__[qualname_client] = set( + [(f"{qualname_service}XXX", True)] + ) self.assertFalse(self.lenient(myservice)) def test_service_str_with_active_client_with_registered_service_str(self): service = "myservice" self.lenient.__dict__[service] = True self.lenient.__dict__["active"] = self.client - self.lenient.__dict__[self.client] = service + self.lenient.__dict__[self.client] = set([(service, True)]) self.assertTrue(self.lenient(service)) def test_service_callable_with_active_client_with_registered_service_str( @@ -256,14 +269,16 @@ def myclient(): qualname_client = qualname(myclient) self.lenient.__dict__[qualname_service] = True self.lenient.__dict__["active"] = qualname_client - self.lenient.__dict__[qualname_client] = qualname_service + self.lenient.__dict__[qualname_client] = set( + [(qualname_service, True)] + ) self.assertTrue(self.lenient(myservice)) def test_enable(self): service = "myservice" self.lenient.__dict__[service] = True self.lenient.__dict__["active"] = self.client - self.lenient.__dict__[self.client] = service + self.lenient.__dict__[self.client] = set([(service, True)]) self.assertTrue(self.lenient(service)) self.lenient.__dict__["enable"] = False self.assertFalse(self.lenient(service)) @@ -499,7 +514,7 @@ def test_nop(self): def test_active_str(self): client = "client" pre = self.copy() - with self.lenient.context(active=client): + with self.lenient.context(client): context = self.copy() post = self.copy() self.assertEqual(pre, self.default) @@ -513,7 +528,7 @@ def client(): pass pre = self.copy() - with self.lenient.context(active=client): + with self.lenient.context(client): context = self.copy() post = self.copy() qualname_client = qualname(client) @@ -523,31 +538,33 @@ def client(): self.assertEqual(context, expected) self.assertEqual(post, self.default) - def test_kwargs(self): - client = "client" - self.lenient.__dict__["service1"] = False - self.lenient.__dict__["service2"] = False - pre = self.copy() - with self.lenient.context(active=client, service1=True, service2=True): - context = self.copy() - post = self.copy() - self.default.update(dict(service1=False, service2=False)) - self.assertEqual(pre, self.default) - expected = self.default.copy() - expected.update(dict(active=client, service1=True, service2=True)) - self.assertEqual(context, expected) - self.assertEqual(post, self.default) + # def test_kwargs(self): + # client = "client" + # self.lenient.__dict__["service1"] = False + # self.lenient.__dict__["service2"] = False + # pre = self.copy() + # with self.lenient.context(active=client, service1=True, service2=True): + # context = self.copy() + # post = self.copy() + # self.default.update(dict(service1=False, service2=False)) + # self.assertEqual(pre, self.default) + # expected = self.default.copy() + # expected.update(dict(active=client, service1=True, service2=True)) + # self.assertEqual(context, expected) + # self.assertEqual(post, self.default) def test_args_str(self): client = "client" services = ("service1", "service2") pre = self.copy() - with self.lenient.context(*services, active=client): + with self.lenient.context(client, services): context = self.copy() post = self.copy() self.assertEqual(pre, self.default) expected = self.default.copy() - expected.update(dict(active=client, client=services)) + expected.update( + dict(active=client, client=set((svc, True) for svc in services)) + ) self.assertEqual(context["active"], expected["active"]) self.assertEqual(set(context["client"]), set(expected["client"])) self.assertEqual(post, self.default) @@ -562,30 +579,168 @@ def service2(): client = "client" services = (service1, service2) pre = self.copy() - with self.lenient.context(*services, active=client): + with self.lenient.context(client, services): context = self.copy() post = self.copy() qualname_services = tuple([qualname(service) for service in services]) self.assertEqual(pre, self.default) expected = self.default.copy() - expected.update(dict(active=client, client=qualname_services)) + expected.update( + dict( + active=client, + client=set((svc, True) for svc in qualname_services), + ) + ) self.assertEqual(context["active"], expected["active"]) - self.assertEqual(set(context["client"]), set(expected["client"])) + self.assertEqual(context["client"], expected["client"]) self.assertEqual(post, self.default) def test_context_runtime(self): services = ("service1", "service2") pre = self.copy() - with self.lenient.context(*services): + with self.lenient.context(active=None, services=services): context = self.copy() post = self.copy() self.assertEqual(pre, self.default) expected = self.default.copy() - expected.update(dict(active="__context", __context=services)) + expected.update( + dict( + active="__context", + __context=set([(srv, True) for srv in services]), + ) + ) self.assertEqual(context, expected) self.assertEqual(post, self.default) +class Test_context2__newstyles(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + self.default = dict(active=None, enable=LENIENT_ENABLE_DEFAULT) + + def copy(self): + return self.lenient.__dict__.copy() + + def test_service_keys(self): + client = "client" + services = ("service1", "service2") + pre = self.copy() + with self.lenient.context(client, service1=True, service2=True): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update( + dict(active=client, client=set((svc, True) for svc in services)) + ) + self.assertEqual(context["active"], expected["active"]) + self.assertEqual(set(context["client"]), set(expected["client"])) + self.assertEqual(post, self.default) + + def test_setting_false(self): + def service1(): + pass + + def service2(): + pass + + qualname1, qualname2 = [qualname(svc) for svc in (service1, service2)] + pre = self.copy() + + # Note: use dict for the keywords, as qualnames are really long ! + settings = {qualname1: True, qualname2: False} + + with self.lenient.context("client", **settings): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update( + dict( + active="client", + client=set([(qualname1, True), (qualname2, False)]), + ) + ) + self.assertEqual(context["active"], expected["active"]) + self.assertEqual(context["client"], expected["client"]) + self.assertEqual(post, self.default) + + def test_setting_nonbinary(self): + service_names = ("svc1", "svc2", "svc3", "svc4") + + def service_values(): + return [LENIENT("svc" + str(ix)) for ix in range(1, 5)] + + pre = self.copy() + + client_settings = dict(svc1=True, svc2=False, svc3="seven", svc4=-5.3) + + @lenient_client(services=client_settings) + def client(): + return service_values() + + client_qualname = qualname(client) + + for name in service_names: + LENIENT.register_service(name) + + svcs_noclient = service_values() + svcs_inclient = client() + with self.lenient.context(client, **client_settings): + context = self.copy() + + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update( + dict( + active=client_qualname, + client=set( + [ + ("svc1", True), + ("svc2", False), + ("svc3", "seven"), + ("svc4", -5.3), + ] + ), + ) + ) + self.assertEqual(context["active"], expected["active"]) + self.assertEqual(context[client_qualname], expected["client"]) + self.assertEqual(post, self.default) + self.assertEqual(svcs_noclient, [False, False, False, False]) + self.assertEqual(svcs_inclient, [True, False, "seven", -5.3]) + + def test_setting_modify(self): + pre = self.copy() + + with self.lenient.context("client", set1=1, set2=2): + context1 = self.copy() + with self.lenient.context( + "client", set2="not two", modify_existing=True + ): + context2 = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected1 = self.default.copy() + expected1.update( + dict(active="client", client=set([("set1", 1), ("set2", 2)]),) + ) + self.assertEqual(context1["active"], "client") + self.assertEqual(context1["client"], expected1["client"]) + self.assertEqual(post, self.default) + expected2 = self.default.copy() + expected2.update( + dict( + active="client", + client=set([("set1", 1), ("set2", "not two")]), + ) + ) + self.assertEqual(context2["active"], "client") + self.assertEqual(context2["client"], expected2["client"]) + self.assertEqual(post, self.default) + + class Test_enable(tests.IrisTest): def setUp(self): self.lenient = Lenient() @@ -616,17 +771,19 @@ def test_not_protected(self): def test_str_service_str(self): client = "client" - services = "service" - self.lenient.register_client(client, services) + service = "service" + self.lenient.register_client(client, service) self.assertIn(client, self.lenient.__dict__) - self.assertEqual(self.lenient.__dict__[client], (services,)) + self.assertEqual(self.lenient.__dict__[client], set([(service, True)])) def test_str_services_str(self): client = "client" services = ("service1", "service2") self.lenient.register_client(client, services) self.assertIn(client, self.lenient.__dict__) - self.assertEqual(self.lenient.__dict__[client], services) + self.assertEqual( + self.lenient.__dict__[client], set((srv, True) for srv in services) + ) def test_callable_service_callable(self): def client(): @@ -640,7 +797,8 @@ def service(): self.lenient.register_client(client, service) self.assertIn(qualname_client, self.lenient.__dict__) self.assertEqual( - self.lenient.__dict__[qualname_client], (qualname_service,) + self.lenient.__dict__[qualname_client], + set([(qualname_service, True)]), ) def test_callable_services_callable(self): @@ -658,7 +816,8 @@ def service2(): self.lenient.register_client(client, (service1, service2)) self.assertIn(qualname_client, self.lenient.__dict__) self.assertEqual( - self.lenient.__dict__[qualname_client], qualname_services + self.lenient.__dict__[qualname_client], + set((srv, True) for srv in qualname_services), ) def test_services_empty(self): @@ -673,18 +832,21 @@ def test_services_overwrite(self): self.assertEqual(self.lenient[client], services) new_services = ("service3", "service4") self.lenient.register_client(client, services=new_services) - self.assertEqual(self.lenient[client], new_services) + self.assertEqual( + self.lenient[client], set((srv, True) for srv in new_services) + ) def test_services_append(self): client = "client" - services = ("service1", "service2") - self.lenient.__dict__[client] = services - self.assertEqual(self.lenient[client], services) + services = ("service1", "service2") # old style + services_set = set((srv, True) for srv in services) + self.lenient.__dict__[client] = services_set + self.assertEqual(self.lenient[client], services_set) new_services = ("service3", "service4") self.lenient.register_client( client, services=new_services, append=True ) - expected = set(services + new_services) + expected = set((srv, True) for srv in services + new_services) self.assertEqual(set(self.lenient[client]), expected) diff --git a/lib/iris/tests/unit/common/lenient/test_lenient_client.py b/lib/iris/tests/unit/common/lenient/test_lenient_client.py index 45370a0dd7..9f76b437f3 100644 --- a/lib/iris/tests/unit/common/lenient/test_lenient_client.py +++ b/lib/iris/tests/unit/common/lenient/test_lenient_client.py @@ -21,8 +21,9 @@ class Test(tests.IrisTest): def setUp(self): module_name = getmodule(self).__name__ - self.client = f"{module_name}" + ".Test.{}..myclient" - self.service = f"{module_name}" + ".Test.{}..myservice" + module_name = module_name.replace(".", "_x_") + self.client = f"{module_name}" + "_x_Test_x_{}_x__x_myclient" + self.service = f"{module_name}" + "_x_Test_x_{}_x__x_myservice" self.active = "active" self.args_in = sentinel.arg1, sentinel.arg2 self.kwargs_in = dict(kwarg1=sentinel.kwarg1, kwarg2=sentinel.kwarg2) @@ -128,7 +129,7 @@ def myclient(): qualname_client = self.client.format("test_call_kwargs_single") self.assertEqual(result[self.active], qualname_client) self.assertIn(qualname_client, result) - self.assertEqual(result[qualname_client], (service,)) + self.assertEqual(result[qualname_client], set([(service, True)])) def test_call_kwargs_single_callable(self): def myservice(): @@ -144,8 +145,10 @@ def myclient(): qualname_client = self.client.format(test_name) self.assertEqual(result[self.active], qualname_client) self.assertIn(qualname_client, result) - qualname_services = (self.service.format(test_name),) - self.assertEqual(result[qualname_client], qualname_services) + qualname_service = self.service.format(test_name) + self.assertEqual( + result[qualname_client], set([(qualname_service, True)]) + ) def test_call_kwargs_iterable(self): services = (sentinel.service1, sentinel.service2) @@ -159,7 +162,9 @@ def myclient(): qualname_client = self.client.format("test_call_kwargs_iterable") self.assertEqual(result[self.active], qualname_client) self.assertIn(qualname_client, result) - self.assertEqual(set(result[qualname_client]), set(services)) + self.assertEqual( + result[qualname_client], set((svc, True) for svc in services) + ) def test_call_client_args_kwargs(self): @lenient_client() diff --git a/lib/iris/tests/unit/common/lenient/test_lenient_service.py b/lib/iris/tests/unit/common/lenient/test_lenient_service.py index 1124b0d01f..9d3ee493ff 100644 --- a/lib/iris/tests/unit/common/lenient/test_lenient_service.py +++ b/lib/iris/tests/unit/common/lenient/test_lenient_service.py @@ -21,7 +21,8 @@ class Test(tests.IrisTest): def setUp(self): module_name = getmodule(self).__name__ - self.service = f"{module_name}" + ".Test.{}..myservice" + module_name = module_name.replace(".", "_x_") + self.service = f"{module_name}" + "_x_Test_x_{}_x__x_myservice" self.args_in = sentinel.arg1, sentinel.arg2 self.kwargs_in = dict(kwarg1=sentinel.kwarg1, kwarg2=sentinel.kwarg2) diff --git a/lib/iris/tests/unit/common/lenient/test_qualname.py b/lib/iris/tests/unit/common/lenient/test_qualname.py index a5622873d0..5d8675574b 100644 --- a/lib/iris/tests/unit/common/lenient/test_qualname.py +++ b/lib/iris/tests/unit/common/lenient/test_qualname.py @@ -21,7 +21,8 @@ class Test(tests.IrisTest): def setUp(self): module_name = getmodule(self).__name__ - self.locals = f"{module_name}" + ".Test.{}..{}" + module_name = module_name.replace(".", "_x_") + self.locals = f"{module_name}" + "_x_Test_x_{}_x__x_{}" def test_pass_thru_non_callable(self): func = sentinel.func @@ -42,7 +43,7 @@ def test_callable_function(self): import iris result = qualname(iris.load) - self.assertEqual(result, "iris.load") + self.assertEqual(result, "iris_x_load") def test_callable_method_local(self): class MyClass: @@ -50,7 +51,7 @@ def mymethod(self): pass qualname_method = self.locals.format( - "test_callable_method_local", "MyClass.mymethod" + "test_callable_method_local", "MyClass_x_mymethod" ) result = qualname(MyClass.mymethod) self.assertEqual(result, qualname_method) @@ -59,7 +60,7 @@ def test_callable_method(self): import iris result = qualname(iris.cube.Cube.add_ancillary_variable) - self.assertEqual(result, "iris.cube.Cube.add_ancillary_variable") + self.assertEqual(result, "iris_x_cube_x_Cube_x_add_ancillary_variable") if __name__ == "__main__":