Add types.is_set() to test whether an arbitrary object is a set as defined by new_sets.bzl. (#181)

* Add sets.is_set() to test whether an arbitrary object is a set.

Since using sets requires special API, it can be useful to determine
whether an object is a set so special handling can be used.
For example, if a method wants to be able to take a list or a set.
diff --git a/lib/BUILD b/lib/BUILD
index 8ae8259..d5c8983 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -40,7 +40,9 @@
 bzl_library(
     name = "new_sets",
     srcs = ["new_sets.bzl"],
-    deps = [":dicts"],
+    deps = [
+      ":dicts",
+    ],
 )
 
 bzl_library(
diff --git a/lib/new_sets.bzl b/lib/new_sets.bzl
index a213cad..06e2058 100644
--- a/lib/new_sets.bzl
+++ b/lib/new_sets.bzl
@@ -18,6 +18,9 @@
   if you pass it an sequence: `sets.make([1, 2, 3])`. This returns a struct containing all of the
   values as keys in a dictionary - this means that all passed in values must be hashable.  The
   values in the set can be retrieved using `sets.to_list(my_set)`.
+
+  An arbitrary object can be tested whether it is a set generated by `sets.make()` or not with the
+  `types.is_set()` method in types.bzl.
 """
 
 load(":dicts.bzl", "dicts")
@@ -33,6 +36,8 @@
     Returns:
       A set containing the passed in values.
     """
+    # If you change the structure of a set, you need to also update the _is_set method
+    # in types.bzl.
     elements = elements if elements else []
     return struct(_values = {e: None for e in elements})
 
@@ -233,4 +238,5 @@
     remove = _remove,
     repr = _repr,
     str = _repr,
+    # is_set is declared in types.bzl
 )
diff --git a/lib/types.bzl b/lib/types.bzl
index 7824893..db1ca18 100644
--- a/lib/types.bzl
+++ b/lib/types.bzl
@@ -21,6 +21,7 @@
 _a_tuple_type = type(())
 _an_int_type = type(1)
 _a_depset_type = type(depset())
+_a_struct_type = type(struct())
 
 def _a_function():
     pass
@@ -126,6 +127,17 @@
     """
     return type(v) == _a_depset_type
 
+def _is_set(v):
+    """Returns True if v is a set created by sets.make().
+
+    Args:
+      v: The value whose type should be checked.
+
+    Returns:
+      True if v was created by sets.make(), False otherwise.
+    """
+    return type(v) == _a_struct_type and hasattr(v, "_values") and _is_dict(v._values)
+
 types = struct(
     is_list = _is_list,
     is_string = _is_string,
@@ -136,4 +148,5 @@
     is_dict = _is_dict,
     is_function = _is_function,
     is_depset = _is_depset,
+    is_set = _is_set,
 )
diff --git a/tests/types_tests.bzl b/tests/types_tests.bzl
index 6681d1c..aaf7cab 100644
--- a/tests/types_tests.bzl
+++ b/tests/types_tests.bzl
@@ -15,6 +15,7 @@
 
 load("//lib:types.bzl", "types")
 load("//lib:unittest.bzl", "asserts", "unittest")
+load("//lib:new_sets.bzl", "sets")
 
 def _a_function():
     """A dummy function for testing."""
@@ -215,6 +216,21 @@
 
 is_depset_test = unittest.make(_is_depset_test)
 
+def _is_set_test(ctx):
+    """Unit test for types.is_set."""
+    env = unittest.begin(ctx)
+
+    asserts.true(env, types.is_set(sets.make()))
+    asserts.true(env, types.is_set(sets.make([1])))
+    asserts.false(env, types.is_set(None))
+    asserts.false(env, types.is_set({}))
+    asserts.false(env, types.is_set(struct(foo = 1)))
+    asserts.false(env, types.is_set(struct(_values = "not really values")))
+
+    return unittest.end(env)
+
+is_set_test = unittest.make(_is_set_test)
+
 def types_test_suite():
     """Creates the test targets and test suite for types.bzl tests."""
     unittest.suite(
@@ -228,4 +244,5 @@
         is_dict_test,
         is_function_test,
         is_depset_test,
+        is_set_test,
     )