diff --git a/stix2/pattern_visitor.py b/stix2/pattern_visitor.py
index 6ac3e98..c0a0fdb 100644
--- a/stix2/pattern_visitor.py
+++ b/stix2/pattern_visitor.py
@@ -2,11 +2,19 @@ import importlib
 import inspect
 
 from stix2patterns.exceptions import ParseException
-from stix2patterns.grammars.STIXPatternParser import (
-    STIXPatternParser, TerminalNode,
-)
-from stix2patterns.grammars.STIXPatternVisitor import STIXPatternVisitor
-from stix2patterns.v20.pattern import Pattern
+from stix2patterns.grammars.STIXPatternParser import TerminalNode
+from stix2patterns.v20.grammars.STIXPatternParser import \
+    STIXPatternParser as STIXPatternParser20
+from stix2patterns.v20.grammars.STIXPatternVisitor import \
+    STIXPatternVisitor as STIXPatternVisitor20
+from stix2patterns.v20.pattern import Pattern as Pattern20
+from stix2patterns.v21.grammars.STIXPatternParser import \
+    STIXPatternParser as STIXPatternParser21
+from stix2patterns.v21.grammars.STIXPatternVisitor import \
+    STIXPatternVisitor as STIXPatternVisitor21
+from stix2patterns.v21.pattern import Pattern as Pattern21
+
+import stix2
 
 from .patterns import *
 from .patterns import _BooleanExpression
@@ -32,22 +40,11 @@ def remove_terminal_nodes(parse_tree_nodes):
     return values
 
 
-# This class defines a complete generic visitor for a parse tree produced by STIXPatternParser.
 
 
-class STIXPatternVisitorForSTIX2(STIXPatternVisitor):
-    classes = {}
 
-    def __init__(self, module_suffix, module_name):
-        if module_suffix and module_name:
-            self.module_suffix = module_suffix
-            if not STIXPatternVisitorForSTIX2.classes:
-                module = importlib.import_module(module_name)
-                for k, c in inspect.getmembers(module, inspect.isclass):
-                    STIXPatternVisitorForSTIX2.classes[k] = c
-        else:
-            self.module_suffix = None
-        super(STIXPatternVisitor, self).__init__()
+class STIXPatternVisitorForSTIX2():
+    classes = {}
 
     def get_class(self, class_name):
         if class_name in STIXPatternVisitorForSTIX2.classes:
@@ -106,7 +103,10 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor):
     # Visit a parse tree produced by STIXPatternParser#observationExpressionCompound.
     def visitObservationExpressionCompound(self, ctx):
         children = self.visitChildren(ctx)
-        return self.instantiate("ObservationExpression", children[1])
+        if isinstance(children[0], TerminalNode) and children[0].symbol.type == self.parser_class.LPAREN:
+            return self.instantiate("ParentheticalExpression", children[1])
+        else:
+            return self.instantiate("ObservationExpression", children[0])
 
     # Visit a parse tree produced by STIXPatternParser#observationExpressionWithin.
     def visitObservationExpressionWithin(self, ctx):
@@ -147,7 +147,7 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor):
     def visitPropTestEqual(self, ctx):
         children = self.visitChildren(ctx)
         operator = children[1].symbol.type
-        negated = operator != STIXPatternParser.EQ
+        negated = operator != self.parser_class.EQ
         return self.instantiate(
             "EqualityComparisonExpression", children[0], children[3 if len(children) > 3 else 2],
             negated,
@@ -157,22 +157,22 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor):
     def visitPropTestOrder(self, ctx):
         children = self.visitChildren(ctx)
         operator = children[1].symbol.type
-        if operator == STIXPatternParser.GT:
+        if operator == self.parser_class.GT:
             return self.instantiate(
                 "GreaterThanComparisonExpression", children[0],
                 children[3 if len(children) > 3 else 2], False,
             )
-        elif operator == STIXPatternParser.LT:
+        elif operator == self.parser_class.LT:
             return self.instantiate(
                 "LessThanComparisonExpression", children[0],
                 children[3 if len(children) > 3 else 2], False,
             )
-        elif operator == STIXPatternParser.GE:
+        elif operator == self.parser_class.GE:
             return self.instantiate(
                 "GreaterThanEqualComparisonExpression", children[0],
                 children[3 if len(children) > 3 else 2], False,
             )
-        elif operator == STIXPatternParser.LE:
+        elif operator == self.parser_class.LE:
             return self.instantiate(
                 "LessThanEqualComparisonExpression", children[0],
                 children[3 if len(children) > 3 else 2], False,
@@ -294,22 +294,22 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor):
         return children[0]
 
     def visitTerminal(self, node):
-        if node.symbol.type == STIXPatternParser.IntPosLiteral or node.symbol.type == STIXPatternParser.IntNegLiteral:
+        if node.symbol.type == self.parser_class.IntPosLiteral or node.symbol.type == self.parser_class.IntNegLiteral:
             return IntegerConstant(node.getText())
-        elif node.symbol.type == STIXPatternParser.FloatPosLiteral or node.symbol.type == STIXPatternParser.FloatNegLiteral:
+        elif node.symbol.type == self.parser_class.FloatPosLiteral or node.symbol.type == self.parser_class.FloatNegLiteral:
             return FloatConstant(node.getText())
-        elif node.symbol.type == STIXPatternParser.HexLiteral:
+        elif node.symbol.type == self.parser_class.HexLiteral:
             return HexConstant(node.getText(), from_parse_tree=True)
-        elif node.symbol.type == STIXPatternParser.BinaryLiteral:
+        elif node.symbol.type == self.parser_class.BinaryLiteral:
             return BinaryConstant(node.getText(), from_parse_tree=True)
-        elif node.symbol.type == STIXPatternParser.StringLiteral:
+        elif node.symbol.type == self.parser_class.StringLiteral:
             if node.getText()[0] == "'" and node.getText()[-1] == "'":
                 return StringConstant(node.getText()[1:-1], from_parse_tree=True)
             else:
                 raise ParseException("The pattern does not start and end with a single quote")
-        elif node.symbol.type == STIXPatternParser.BoolLiteral:
+        elif node.symbol.type == self.parser_class.BoolLiteral:
             return BooleanConstant(node.getText())
-        elif node.symbol.type == STIXPatternParser.TimestampLiteral:
+        elif node.symbol.type == self.parser_class.TimestampLiteral:
             return TimestampConstant(node.getText())
         else:
             return node
@@ -321,12 +321,51 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor):
             aggregate = [nextResult]
         return aggregate
 
+# This class defines a complete generic visitor for a parse tree produced by STIXPatternParser.
+class STIXPatternVisitorForSTIX21(STIXPatternVisitorForSTIX2, STIXPatternVisitor21):
+    classes = {}
 
-def create_pattern_object(pattern, module_suffix="", module_name=""):
+    def __init__(self, module_suffix, module_name):
+        if module_suffix and module_name:
+            self.module_suffix = module_suffix
+            if not STIXPatternVisitorForSTIX2.classes:
+                module = importlib.import_module(module_name)
+                for k, c in inspect.getmembers(module, inspect.isclass):
+                    STIXPatternVisitorForSTIX2.classes[k] = c
+        else:
+            self.module_suffix = None
+        self.parser_class = STIXPatternParser21
+        super(STIXPatternVisitor21, self).__init__()
+
+
+class STIXPatternVisitorForSTIX20(STIXPatternVisitorForSTIX2, STIXPatternVisitor20):
+    classes = {}
+
+    def __init__(self, module_suffix, module_name):
+        if module_suffix and module_name:
+            self.module_suffix = module_suffix
+            if not STIXPatternVisitorForSTIX2.classes:
+                module = importlib.import_module(module_name)
+                for k, c in inspect.getmembers(module, inspect.isclass):
+                    STIXPatternVisitorForSTIX2.classes[k] = c
+        else:
+            self.module_suffix = None
+        self.parser_class = STIXPatternParser20
+        super(STIXPatternVisitor20, self).__init__()
+
+
+def create_pattern_object(pattern, module_suffix="", module_name="", version=stix2.DEFAULT_VERSION):
     """
     Create a STIX pattern AST from a pattern string.
     """
 
-    pattern_obj = Pattern(pattern)
-    builder = STIXPatternVisitorForSTIX2(module_suffix, module_name)
+    if version == "2.1":
+        pattern_class = Pattern21
+        visitor_class = STIXPatternVisitorForSTIX21
+    else:
+        pattern_class = Pattern20
+        visitor_class = STIXPatternVisitorForSTIX20
+
+    pattern_obj = pattern_class(pattern)
+    builder = visitor_class(module_suffix, module_name)
     return pattern_obj.visit(builder)
diff --git a/stix2/patterns.py b/stix2/patterns.py
index 2e149be..f0cceb8 100644
--- a/stix2/patterns.py
+++ b/stix2/patterns.py
@@ -551,7 +551,7 @@ class ObservationExpression(_PatternExpression):
         self.operand = operand
 
     def __str__(self):
-        return "[%s]" % self.operand
+        return "%s" % self.operand if isinstance(self.operand, (ObservationExpression, _CompoundObservationExpression)) else "[%s]" % self.operand
 
 
 class _CompoundObservationExpression(_PatternExpression):
diff --git a/stix2/v21/bundle.py b/stix2/v21/bundle.py
index 574ab48..168771f 100644
--- a/stix2/v21/bundle.py
+++ b/stix2/v21/bundle.py
@@ -9,8 +9,9 @@ from ..properties import (
 
 
 class Bundle(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_nuwp4rox8c7r>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'bundle'
diff --git a/stix2/v21/common.py b/stix2/v21/common.py
index 373a7fc..ac8daf1 100644
--- a/stix2/v21/common.py
+++ b/stix2/v21/common.py
@@ -16,8 +16,9 @@ from ..utils import NOW, _get_dict
 
 
 class ExternalReference(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_bajcvqteiard>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _properties = OrderedDict([
@@ -50,8 +51,9 @@ class ExternalReference(_STIXBase):
 
 
 class KillChainPhase(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_i4tjv75ce50h>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _properties = OrderedDict([
@@ -61,8 +63,9 @@ class KillChainPhase(_STIXBase):
 
 
 class GranularMarking(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_robezi5egfdr>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _properties = OrderedDict([
@@ -77,8 +80,9 @@ class GranularMarking(_STIXBase):
 
 
 class LanguageContent(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_nfwr8z9ax2bi>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'language-content'
@@ -104,8 +108,9 @@ class LanguageContent(_STIXBase):
 
 
 class TLPMarking(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_yd3ar14ekwrs>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'tlp'
@@ -115,8 +120,9 @@ class TLPMarking(_STIXBase):
 
 
 class StatementMarking(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_3ru8r05saera>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'statement'
@@ -145,8 +151,9 @@ class MarkingProperty(Property):
 
 
 class MarkingDefinition(_STIXBase, _MarkingsMixin):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_hr5vgqxjk7ns>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'marking-definition'
diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py
index 89de0fc..88f1ff7 100644
--- a/stix2/v21/observables.py
+++ b/stix2/v21/observables.py
@@ -22,8 +22,9 @@ from .common import GranularMarking
 
 
 class Artifact(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_rqwyxo6gp7cv>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'artifact'
@@ -51,8 +52,9 @@ class Artifact(_Observable):
 
 
 class AutonomousSystem(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_bxebwa6l91fb>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'autonomous-system'
@@ -72,8 +74,9 @@ class AutonomousSystem(_Observable):
 
 
 class Directory(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_vhpkn06q7fvl>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'directory'
@@ -97,8 +100,9 @@ class Directory(_Observable):
 
 
 class DomainName(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_i2zf5h7vnrd9>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'domain-name'
@@ -117,8 +121,9 @@ class DomainName(_Observable):
 
 
 class EmailAddress(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_am7srelb9c14>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'email-addr'
@@ -138,8 +143,9 @@ class EmailAddress(_Observable):
 
 
 class EmailMIMEComponent(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_kzv52qqc0xw1>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _properties = OrderedDict([
@@ -155,8 +161,9 @@ class EmailMIMEComponent(_STIXBase):
 
 
 class EmailMessage(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_loz634bn09om>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'email-message'
@@ -195,8 +202,9 @@ class EmailMessage(_Observable):
 
 
 class ArchiveExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_mm25z9wuw4tr>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'archive-ext'
@@ -207,8 +215,9 @@ class ArchiveExt(_Extension):
 
 
 class AlternateDataStream(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_nbqgazg6fsma>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _properties = OrderedDict([
@@ -219,8 +228,9 @@ class AlternateDataStream(_STIXBase):
 
 
 class NTFSExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_tb77nk1g3y6f>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'ntfs-ext'
@@ -231,8 +241,9 @@ class NTFSExt(_Extension):
 
 
 class PDFExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_30hzxqrmkg8w>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'pdf-ext'
@@ -246,8 +257,9 @@ class PDFExt(_Extension):
 
 
 class RasterImageExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_20mnz0u5ppxr>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'raster-image-ext'
@@ -260,8 +272,9 @@ class RasterImageExt(_Extension):
 
 
 class WindowsPEOptionalHeaderType(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_wyp5qdc2wugy>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _properties = OrderedDict([
@@ -304,8 +317,9 @@ class WindowsPEOptionalHeaderType(_STIXBase):
 
 
 class WindowsPESection(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_wiqw87xsov3t>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _properties = OrderedDict([
@@ -317,8 +331,9 @@ class WindowsPESection(_STIXBase):
 
 
 class WindowsPEBinaryExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_5f9bgdmj91h5>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'windows-pebinary-ext'
@@ -339,8 +354,9 @@ class WindowsPEBinaryExt(_Extension):
 
 
 class File(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_vq03pryd7u32>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'file'
@@ -374,8 +390,9 @@ class File(_Observable):
 
 
 class IPv4Address(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_ta83c412bfsc>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'ipv4-addr'
@@ -395,8 +412,9 @@ class IPv4Address(_Observable):
 
 
 class IPv6Address(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_f76hsv2pvwwq>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'ipv6-addr'
@@ -416,8 +434,9 @@ class IPv6Address(_Observable):
 
 
 class MACAddress(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_6lhrrdef8852>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'mac-addr'
@@ -435,8 +454,9 @@ class MACAddress(_Observable):
 
 
 class Mutex(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_u65ia5eoc7cv>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'mutex'
@@ -454,8 +474,9 @@ class Mutex(_Observable):
 
 
 class HTTPRequestExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_60k6dn28qicj>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'http-request-ext'
@@ -472,7 +493,7 @@ class HTTPRequestExt(_Extension):
 class ICMPExt(_Extension):
     # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_3g6wds21zwzl>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'icmp-ext'
@@ -483,8 +504,9 @@ class ICMPExt(_Extension):
 
 
 class SocketExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_f54f1hripxsg>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'socket-ext'
@@ -544,8 +566,9 @@ class SocketExt(_Extension):
 
 
 class TCPExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_2z78x4m8ewcw>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'tcp-ext'
@@ -556,8 +579,9 @@ class TCPExt(_Extension):
 
 
 class NetworkTraffic(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_e5nyr5squmsd>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'network-traffic'
@@ -611,8 +635,9 @@ class NetworkTraffic(_Observable):
 
 
 class WindowsProcessExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_4wfs4ve800kf>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'windows-process-ext'
@@ -635,8 +660,9 @@ class WindowsProcessExt(_Extension):
 
 
 class WindowsServiceExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_s2rmoe7djlt>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'windows-service-ext'
@@ -678,8 +704,9 @@ class WindowsServiceExt(_Extension):
 
 
 class Process(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_ur7snm473t1d>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'process'
@@ -722,8 +749,9 @@ class Process(_Observable):
 
 
 class Software(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_jru33yeokrmh>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'software'
@@ -746,8 +774,9 @@ class Software(_Observable):
 
 
 class URL(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_6bsklda6vc0c>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'url'
@@ -765,8 +794,9 @@ class URL(_Observable):
 
 
 class UNIXAccountExt(_Extension):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_z25gmwyz67kl>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'unix-account-ext'
@@ -779,8 +809,9 @@ class UNIXAccountExt(_Extension):
 
 
 class UserAccount(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_hah33g4ntxnx>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'user-account'
@@ -811,8 +842,9 @@ class UserAccount(_Observable):
 
 
 class WindowsRegistryValueType(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_6jiqabgqp2hp>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'windows-registry-value-type'
@@ -840,8 +872,9 @@ class WindowsRegistryValueType(_STIXBase):
 
 
 class WindowsRegistryKey(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_bdim4of4dl37>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'windows-registry-key'
@@ -864,8 +897,9 @@ class WindowsRegistryKey(_Observable):
 
 
 class X509V3ExtenstionsType(_STIXBase):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_c1kt4dheb6vz>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'x509-v3-extensions-type'
@@ -890,8 +924,9 @@ class X509V3ExtenstionsType(_STIXBase):
 
 
 class X509Certificate(_Observable):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_g3kniyun8ykv>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'x509-certificate'
diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py
index 3253a02..f2dc0ea 100644
--- a/stix2/v21/sdo.py
+++ b/stix2/v21/sdo.py
@@ -22,8 +22,9 @@ from .common import ExternalReference, GranularMarking, KillChainPhase
 
 
 class AttackPattern(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_4ohsa4pay4h4>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'attack-pattern'
@@ -49,8 +50,9 @@ class AttackPattern(STIXDomainObject):
 
 
 class Campaign(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_vvysvm8mt434>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'campaign'
@@ -88,8 +90,9 @@ class Campaign(STIXDomainObject):
 
 
 class CourseOfAction(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_d5yf99f0a230>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'course-of-action'
@@ -113,8 +116,9 @@ class CourseOfAction(STIXDomainObject):
 
 
 class Grouping(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_9e3uldaqqha2>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'grouping'
@@ -140,8 +144,9 @@ class Grouping(STIXDomainObject):
 
 
 class Identity(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_ru8fmldl2p6w>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'identity'
@@ -169,8 +174,9 @@ class Identity(STIXDomainObject):
 
 
 class Indicator(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_wfiae74706sw>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'indicator'
@@ -228,8 +234,9 @@ class Indicator(STIXDomainObject):
 
 
 class Infrastructure(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_l2alfbbcmfep>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'infrastructure'
@@ -268,8 +275,9 @@ class Infrastructure(STIXDomainObject):
 
 
 class IntrusionSet(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_ticprjb32bc4>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'intrusion-set'
@@ -310,8 +318,9 @@ class IntrusionSet(STIXDomainObject):
 
 
 class Location(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_sqez6sri9vtz>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'location'
@@ -417,8 +426,9 @@ class Location(STIXDomainObject):
 
 
 class Malware(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_gc4ooz6oaz7y>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'malware'
@@ -469,8 +479,9 @@ class Malware(STIXDomainObject):
 
 
 class MalwareAnalysis(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_dw67pa20zss5>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'malware-analysis'
@@ -513,8 +524,9 @@ class MalwareAnalysis(STIXDomainObject):
 
 
 class Note(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_hr77jvcbs9jk>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'note'
@@ -540,8 +552,9 @@ class Note(STIXDomainObject):
 
 
 class ObservedData(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_h1590esrzg5f>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'observed-data'
@@ -595,8 +608,9 @@ class ObservedData(STIXDomainObject):
 
 
 class Opinion(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_sr2hswmu5t1>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'opinion'
@@ -632,8 +646,9 @@ class Opinion(STIXDomainObject):
 
 
 class Report(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_ha4fpad0r9pf>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'report'
@@ -660,8 +675,9 @@ class Report(STIXDomainObject):
 
 
 class ThreatActor(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_2wowmlcbkqst>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'threat-actor'
@@ -706,8 +722,9 @@ class ThreatActor(STIXDomainObject):
 
 
 class Tool(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_m21z3a1f3lou>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'tool'
@@ -735,8 +752,9 @@ class Tool(STIXDomainObject):
 
 
 class Vulnerability(STIXDomainObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_d9f0iay06wtx>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'vulnerability'
diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py
index 30320b3..059bb66 100644
--- a/stix2/v21/sro.py
+++ b/stix2/v21/sro.py
@@ -12,8 +12,9 @@ from .common import ExternalReference, GranularMarking
 
 
 class Relationship(STIXRelationshipObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_al0fb8fcd9e7>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _invalid_source_target_types = ['bundle', 'language-content', 'marking-definition', 'relationship', 'sighting']
@@ -68,8 +69,9 @@ class Relationship(STIXRelationshipObject):
 
 
 class Sighting(STIXRelationshipObject):
+    # TODO: Add link
     """For more detailed information on this object's properties, see
-    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_7p0n81ikux8f>`__.
+    `the STIX 2.1 specification <link here>`__.
     """
 
     _type = 'sighting'

Add more tests for patterns
We should write some more tests for patterns.py and increase code coverage.
diff --git a/stix2/test/v20/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py
index 23a401b..d5cbb5b 100644
--- a/stix2/test/v20/test_pattern_expressions.py
+++ b/stix2/test/v20/test_pattern_expressions.py
@@ -494,13 +494,14 @@ def test_make_constant_already_a_constant():
 
 
 def test_parsing_comparison_expression():
-    patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']")
+    patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']", version="2.0")
     assert str(patt_obj) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']"
 
 
 def test_parsing_qualified_expression():
     patt_obj = create_pattern_object(
         "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS",
+        version="2.0",
     )
     assert str(
         patt_obj,
@@ -508,5 +509,5 @@ def test_parsing_qualified_expression():
 
 
 def test_list_constant():
-    patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]")
+    patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]", version="2.0")
     assert str(patt_obj) == "[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]"
diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py
index 0c298f8..198edac 100644
--- a/stix2/test/v21/test_pattern_expressions.py
+++ b/stix2/test/v21/test_pattern_expressions.py
@@ -175,20 +175,34 @@ def test_greater_than():
     assert str(exp) == "[file:extensions.'windows-pebinary-ext'.sections[*].entropy > 7.0]"
 
 
+def test_parsing_greater_than():
+    patt_obj = create_pattern_object("[file:extensions.'windows-pebinary-ext'.sections[*].entropy > 7.478901]", version="2.1")
+    assert str(patt_obj) == "[file:extensions.'windows-pebinary-ext'.sections[*].entropy > 7.478901]"
+
+
 def test_less_than():
     exp = stix2.LessThanComparisonExpression("file:size", 1024)
     assert str(exp) == "file:size < 1024"
 
 
+def test_parsing_less_than():
+    patt_obj = create_pattern_object("[file:size < 1024]", version="2.1")
+    assert str(patt_obj) == "[file:size < 1024]"
+
+
 def test_greater_than_or_equal():
     exp = stix2.GreaterThanEqualComparisonExpression(
         "file:size",
         1024,
     )
-
     assert str(exp) == "file:size >= 1024"
 
 
+def test_parsing_greater_than_or_equal():
+    patt_obj = create_pattern_object("[file:size >= 1024]", version="2.1")
+    assert str(patt_obj) == "[file:size >= 1024]"
+
+
 def test_less_than_or_equal():
     exp = stix2.LessThanEqualComparisonExpression(
         "file:size",
@@ -197,6 +211,36 @@ def test_less_than_or_equal():
     assert str(exp) == "file:size <= 1024"
 
 
+def test_parsing_less_than_or_equal():
+    patt_obj = create_pattern_object("[file:size <= 1024]", version="2.1")
+    assert str(patt_obj) == "[file:size <= 1024]"
+
+
+def test_parsing_issubset():
+    patt_obj = create_pattern_object("[network-traffic:dst_ref.value ISSUBSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']", version="2.1")
+    assert str(patt_obj) == "[network-traffic:dst_ref.value ISSUBSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']"
+
+
+def test_parsing_issuperset():
+    patt_obj = create_pattern_object("[network-traffic:dst_ref.value ISSUPERSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']", version="2.1")
+    assert str(patt_obj) == "[network-traffic:dst_ref.value ISSUPERSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']"
+
+
+def test_parsing_like():
+    patt_obj = create_pattern_object("[directory:path LIKE 'C:\\\\Windows\\\\%\\\\foo']", version="2.1")
+    assert str(patt_obj) == "[directory:path LIKE 'C:\\\\Windows\\\\%\\\\foo']"
+
+
+def test_parsing_match():
+    patt_obj = create_pattern_object("[process:command_line MATCHES '^.+>-add GlobalSign.cer -c -s -r localMachine Root$'] FOLLOWEDBY [process:command_line MATCHES '^.+>-add GlobalSign.cer -c -s -r localMachineTrustedPublisher$'] WITHIN 300 SECONDS", version="2.1")  # noqa
+    assert str(patt_obj) == "[process:command_line MATCHES '^.+>-add GlobalSign.cer -c -s -r localMachine Root$'] FOLLOWEDBY [process:command_line MATCHES '^.+>-add GlobalSign.cer -c -s -r localMachineTrustedPublisher$'] WITHIN 300 SECONDS"  # noqa
+
+
+def test_parsing_followed_by():
+    patt_obj = create_pattern_object("([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [windows-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS", version="2.1")  # noqa
+    assert str(patt_obj) == "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [windows-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS"  # noqa
+
+
 def test_not():
     exp = stix2.LessThanComparisonExpression(
         "file:size",
@@ -257,6 +301,67 @@ def test_and_observable_expression():
     assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1009' AND user-account:account_login = 'Mary']"  # noqa
 
 
+def test_parsing_and_observable_expression():
+    exp = create_pattern_object("[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']", version="2.1")  # noqa
+    assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']"  # noqa
+
+
+def test_or_observable_expression():
+    exp1 = stix2.AndBooleanExpression([
+        stix2.EqualityComparisonExpression(
+            "user-account:account_type",
+            "unix",
+        ),
+        stix2.EqualityComparisonExpression(
+            "user-account:user_id",
+            stix2.StringConstant("1007"),
+        ),
+        stix2.EqualityComparisonExpression(
+            "user-account:account_login",
+            "Peter",
+        ),
+    ])
+    exp2 = stix2.AndBooleanExpression([
+        stix2.EqualityComparisonExpression(
+            "user-account:account_type",
+            "unix",
+        ),
+        stix2.EqualityComparisonExpression(
+            "user-account:user_id",
+            stix2.StringConstant("1008"),
+        ),
+        stix2.EqualityComparisonExpression(
+            "user-account:account_login",
+            "Paul",
+        ),
+    ])
+    exp3 = stix2.AndBooleanExpression([
+        stix2.EqualityComparisonExpression(
+            "user-account:account_type",
+            "unix",
+        ),
+        stix2.EqualityComparisonExpression(
+            "user-account:user_id",
+            stix2.StringConstant("1009"),
+        ),
+        stix2.EqualityComparisonExpression(
+            "user-account:account_login",
+            "Mary",
+        ),
+    ])
+    exp = stix2.OrObservationExpression([
+        stix2.ObservationExpression(exp1),
+        stix2.ObservationExpression(exp2),
+        stix2.ObservationExpression(exp3),
+    ])
+    assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1009' AND user-account:account_login = 'Mary']"  # noqa
+
+
+def test_parsing_or_observable_expression():
+    exp = create_pattern_object("[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']", version="2.1")  # noqa
+    assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']"  # noqa
+
+
 def test_invalid_and_observable_expression():
     with pytest.raises(ValueError):
         stix2.AndBooleanExpression([
@@ -286,6 +391,11 @@ def test_hex():
     assert str(exp) == "[file:mime_type = 'image/bmp' AND file:magic_number_hex = h'ffd8']"
 
 
+def test_parsing_hex():
+    patt_obj = create_pattern_object("[file:magic_number_hex = h'ffd8']", version="2.1")
+    assert str(patt_obj) == "[file:magic_number_hex = h'ffd8']"
+
+
 def test_multiple_qualifiers():
     exp_and = stix2.AndBooleanExpression([
         stix2.EqualityComparisonExpression(
@@ -334,6 +444,11 @@ def test_binary():
     assert str(exp) == "artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q='"
 
 
+def test_parsing_binary():
+    patt_obj = create_pattern_object("[artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q=']", version="2.1")
+    assert str(patt_obj) == "[artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q=']"
+
+
 def test_list():
     exp = stix2.InComparisonExpression(
         "process:name",
@@ -495,29 +610,45 @@ def test_make_constant_already_a_constant():
 
 
 def test_parsing_comparison_expression():
-    patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']")
+    patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']", version="2.1")
     assert str(patt_obj) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']"
 
 
-def test_parsing_qualified_expression():
+def test_parsing_repeat_and_within_qualified_expression():
     patt_obj = create_pattern_object(
         "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS",
+        version="2.1",
     )
     assert str(
         patt_obj,
     ) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS"
 
 
+def test_parsing_start_stop_qualified_expression():
+    patt_obj = create_pattern_object(
+        "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] START t'2016-06-01T00:00:00Z' STOP t'2017-03-12T08:30:00Z'",  # noqa
+        version="2.1",
+    )
+    assert str(
+        patt_obj,
+    ) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] START t'2016-06-01T00:00:00Z' STOP t'2017-03-12T08:30:00Z'"  # noqa
+
+
 def test_list_constant():
-    patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]")
+    patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]", version="2.1")
     assert str(patt_obj) == "[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]"
 
 
+def test_parsing_boolean():
+    patt_obj = create_pattern_object("[network-traffic:is_active = true]", version="2.1")
+    assert str(patt_obj) == "[network-traffic:is_active = true]"
+
+
 def test_parsing_multiple_slashes_quotes():
-    patt_obj = create_pattern_object("[ file:name = 'weird_name\\'' ]")
+    patt_obj = create_pattern_object("[ file:name = 'weird_name\\'' ]", version="2.1")
     assert str(patt_obj) == "[file:name = 'weird_name\\'']"
 
 
 def test_parse_error():
     with pytest.raises(ParseException):
-        create_pattern_object("[ file: name = 'weirdname]")
+        create_pattern_object("[ file: name = 'weirdname]", version="2.1")

pip install -e .[dev]
parse_log_pytest
requirements.txt
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
antlr4-python3-runtime==4.9.3
async-generator==1.10
attrs==22.2.0
Babel==2.11.0
backcall==0.2.0
bleach==4.1.0
bump2version==1.0.1
bumpversion==0.6.0
certifi==2021.5.30
cfgv==3.3.1
charset-normalizer==2.0.12
coverage==6.2
decorator==5.1.1
defusedxml==0.7.1
distlib==0.3.9
docutils==0.17.1
entrypoints==0.4
filelock==3.4.1
identify==2.4.4
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
importlib-resources==5.2.3
iniconfig==1.1.1
ipython==7.16.3
ipython-genutils==0.2.0
jedi==0.17.2
Jinja2==3.0.3
jsonschema==3.2.0
jupyter-client==7.1.2
jupyter-core==4.9.2
jupyterlab-pygments==0.1.2
MarkupSafe==2.0.1
mistune==0.8.4
nbclient==0.5.9
nbconvert==6.0.7
nbformat==5.1.3
nbsphinx==0.4.3
nest-asyncio==1.6.0
nodeenv==1.6.0
packaging==21.3
pandocfilters==1.5.1
parso==0.7.1
pexpect==4.9.0
pickleshare==0.7.5
platformdirs==2.4.0
pluggy==1.0.0
pre-commit==2.17.0
prompt-toolkit==3.0.36
ptyprocess==0.7.0
py==1.11.0
Pygments==2.14.0
pyparsing==3.1.4
pyrsistent==0.18.0
pytest==7.0.1
pytest-cov==4.0.0
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.1
pyzmq==25.1.2
requests==2.27.1
simplejson==3.20.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==1.8.6
sphinx-prompt==1.5.0
sphinxcontrib-serializinghtml==1.1.5
sphinxcontrib-websupport==1.2.4
-e git+https://github.com/oasis-open/cti-python-stix2.git@38817a603f8d02a614dadba6cccee05bf5622c2b#egg=stix2
stix2-patterns==2.0.0
testpath==0.6.0
toml==0.10.2
tomli==1.2.3
tornado==6.1
tox==3.28.0
traitlets==4.3.3
typing_extensions==4.1.1
urllib3==1.26.20
virtualenv==20.16.2
wcwidth==0.2.13
webencodings==0.5.1
zipp==3.6.0

name: cti-python-stix2
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - antlr4-python3-runtime==4.9.3
      - async-generator==1.10
      - attrs==22.2.0
      - babel==2.11.0
      - backcall==0.2.0
      - bleach==4.1.0
      - bump2version==1.0.1
      - bumpversion==0.6.0
      - cfgv==3.3.1
      - charset-normalizer==2.0.12
      - coverage==6.2
      - decorator==5.1.1
      - defusedxml==0.7.1
      - distlib==0.3.9
      - docutils==0.17.1
      - entrypoints==0.4
      - filelock==3.4.1
      - identify==2.4.4
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - importlib-resources==5.2.3
      - iniconfig==1.1.1
      - ipython==7.16.3
      - ipython-genutils==0.2.0
      - jedi==0.17.2
      - jinja2==3.0.3
      - jsonschema==3.2.0
      - jupyter-client==7.1.2
      - jupyter-core==4.9.2
      - jupyterlab-pygments==0.1.2
      - markupsafe==2.0.1
      - mistune==0.8.4
      - nbclient==0.5.9
      - nbconvert==6.0.7
      - nbformat==5.1.3
      - nbsphinx==0.4.3
      - nest-asyncio==1.6.0
      - nodeenv==1.6.0
      - packaging==21.3
      - pandocfilters==1.5.1
      - parso==0.7.1
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - platformdirs==2.4.0
      - pluggy==1.0.0
      - pre-commit==2.17.0
      - prompt-toolkit==3.0.36
      - ptyprocess==0.7.0
      - py==1.11.0
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pyrsistent==0.18.0
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.1
      - pyzmq==25.1.2
      - requests==2.27.1
      - simplejson==3.20.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==1.8.6
      - sphinx-prompt==1.5.0
      - sphinxcontrib-serializinghtml==1.1.5
      - sphinxcontrib-websupport==1.2.4
      - stix2-patterns==2.0.0
      - testpath==0.6.0
      - toml==0.10.2
      - tomli==1.2.3
      - tornado==6.1
      - tox==3.28.0
      - traitlets==4.3.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - virtualenv==20.16.2
      - wcwidth==0.2.13
      - webencodings==0.5.1
      - zipp==3.6.0
prefix: /opt/conda/envs/cti-python-stix2

stix2/test/v20/test_pattern_expressions.py::test_parsing_comparison_expression stix2/test/v20/test_pattern_expressions.py::test_parsing_qualified_expression stix2/test/v20/test_pattern_expressions.py::test_list_constant stix2/test/v21/test_pattern_expressions.py::test_parsing_greater_than stix2/test/v21/test_pattern_expressions.py::test_parsing_less_than stix2/test/v21/test_pattern_expressions.py::test_parsing_greater_than_or_equal stix2/test/v21/test_pattern_expressions.py::test_parsing_less_than_or_equal stix2/test/v21/test_pattern_expressions.py::test_parsing_issubset stix2/test/v21/test_pattern_expressions.py::test_parsing_issuperset stix2/test/v21/test_pattern_expressions.py::test_parsing_like stix2/test/v21/test_pattern_expressions.py::test_parsing_match stix2/test/v21/test_pattern_expressions.py::test_parsing_followed_by stix2/test/v21/test_pattern_expressions.py::test_parsing_and_observable_expression stix2/test/v21/test_pattern_expressions.py::test_parsing_or_observable_expression stix2/test/v21/test_pattern_expressions.py::test_parsing_hex stix2/test/v21/test_pattern_expressions.py::test_parsing_binary stix2/test/v21/test_pattern_expressions.py::test_parsing_comparison_expression stix2/test/v21/test_pattern_expressions.py::test_parsing_repeat_and_within_qualified_expression stix2/test/v21/test_pattern_expressions.py::test_parsing_start_stop_qualified_expression stix2/test/v21/test_pattern_expressions.py::test_list_constant stix2/test/v21/test_pattern_expressions.py::test_parsing_boolean stix2/test/v21/test_pattern_expressions.py::test_parsing_multiple_slashes_quotes stix2/test/v21/test_pattern_expressions.py::test_parse_error
stix2/test/v20/test_pattern_expressions.py::test_create_comparison_expression stix2/test/v20/test_pattern_expressions.py::test_boolean_expression stix2/test/v20/test_pattern_expressions.py::test_boolean_expression_with_parentheses stix2/test/v20/test_pattern_expressions.py::test_hash_followed_by_registryKey_expression_python_constant stix2/test/v20/test_pattern_expressions.py::test_hash_followed_by_registryKey_expression stix2/test/v20/test_pattern_expressions.py::test_file_observable_expression stix2/test/v20/test_pattern_expressions.py::test_multiple_file_observable_expression[AndObservationExpression-AND] stix2/test/v20/test_pattern_expressions.py::test_multiple_file_observable_expression[OrObservationExpression-OR] stix2/test/v20/test_pattern_expressions.py::test_root_types stix2/test/v20/test_pattern_expressions.py::test_artifact_payload stix2/test/v20/test_pattern_expressions.py::test_greater_than_python_constant stix2/test/v20/test_pattern_expressions.py::test_greater_than stix2/test/v20/test_pattern_expressions.py::test_less_than stix2/test/v20/test_pattern_expressions.py::test_greater_than_or_equal stix2/test/v20/test_pattern_expressions.py::test_less_than_or_equal stix2/test/v20/test_pattern_expressions.py::test_not stix2/test/v20/test_pattern_expressions.py::test_and_observable_expression stix2/test/v20/test_pattern_expressions.py::test_invalid_and_observable_expression stix2/test/v20/test_pattern_expressions.py::test_hex stix2/test/v20/test_pattern_expressions.py::test_multiple_qualifiers stix2/test/v20/test_pattern_expressions.py::test_set_op stix2/test/v20/test_pattern_expressions.py::test_timestamp stix2/test/v20/test_pattern_expressions.py::test_boolean stix2/test/v20/test_pattern_expressions.py::test_binary stix2/test/v20/test_pattern_expressions.py::test_list stix2/test/v20/test_pattern_expressions.py::test_list2 stix2/test/v20/test_pattern_expressions.py::test_invalid_constant_type stix2/test/v20/test_pattern_expressions.py::test_invalid_integer_constant stix2/test/v20/test_pattern_expressions.py::test_invalid_timestamp_constant stix2/test/v20/test_pattern_expressions.py::test_invalid_float_constant stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[True-True0] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[False-False0] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[True-True1] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[False-False1] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[true-True] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[false-False] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[t-True] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[f-False] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[T-True] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[F-False] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[1-True] stix2/test/v20/test_pattern_expressions.py::test_boolean_constant[0-False] stix2/test/v20/test_pattern_expressions.py::test_invalid_boolean_constant stix2/test/v20/test_pattern_expressions.py::test_invalid_hash_constant[MD5-zzz] stix2/test/v20/test_pattern_expressions.py::test_invalid_hash_constant[ssdeep-zzz==] stix2/test/v20/test_pattern_expressions.py::test_invalid_hex_constant stix2/test/v20/test_pattern_expressions.py::test_invalid_binary_constant stix2/test/v20/test_pattern_expressions.py::test_escape_quotes_and_backslashes stix2/test/v20/test_pattern_expressions.py::test_like stix2/test/v20/test_pattern_expressions.py::test_issuperset stix2/test/v20/test_pattern_expressions.py::test_repeat_qualifier stix2/test/v20/test_pattern_expressions.py::test_invalid_repeat_qualifier stix2/test/v20/test_pattern_expressions.py::test_invalid_within_qualifier stix2/test/v20/test_pattern_expressions.py::test_startstop_qualifier stix2/test/v20/test_pattern_expressions.py::test_invalid_startstop_qualifier stix2/test/v20/test_pattern_expressions.py::test_make_constant_already_a_constant stix2/test/v21/test_pattern_expressions.py::test_create_comparison_expression stix2/test/v21/test_pattern_expressions.py::test_boolean_expression stix2/test/v21/test_pattern_expressions.py::test_boolean_expression_with_parentheses stix2/test/v21/test_pattern_expressions.py::test_hash_followed_by_registryKey_expression_python_constant stix2/test/v21/test_pattern_expressions.py::test_hash_followed_by_registryKey_expression stix2/test/v21/test_pattern_expressions.py::test_file_observable_expression stix2/test/v21/test_pattern_expressions.py::test_multiple_file_observable_expression[AndObservationExpression-AND] stix2/test/v21/test_pattern_expressions.py::test_multiple_file_observable_expression[OrObservationExpression-OR] stix2/test/v21/test_pattern_expressions.py::test_root_types stix2/test/v21/test_pattern_expressions.py::test_artifact_payload stix2/test/v21/test_pattern_expressions.py::test_greater_than_python_constant stix2/test/v21/test_pattern_expressions.py::test_greater_than stix2/test/v21/test_pattern_expressions.py::test_less_than stix2/test/v21/test_pattern_expressions.py::test_greater_than_or_equal stix2/test/v21/test_pattern_expressions.py::test_less_than_or_equal stix2/test/v21/test_pattern_expressions.py::test_not stix2/test/v21/test_pattern_expressions.py::test_and_observable_expression stix2/test/v21/test_pattern_expressions.py::test_or_observable_expression stix2/test/v21/test_pattern_expressions.py::test_invalid_and_observable_expression stix2/test/v21/test_pattern_expressions.py::test_hex stix2/test/v21/test_pattern_expressions.py::test_multiple_qualifiers stix2/test/v21/test_pattern_expressions.py::test_set_op stix2/test/v21/test_pattern_expressions.py::test_timestamp stix2/test/v21/test_pattern_expressions.py::test_boolean stix2/test/v21/test_pattern_expressions.py::test_binary stix2/test/v21/test_pattern_expressions.py::test_list stix2/test/v21/test_pattern_expressions.py::test_list2 stix2/test/v21/test_pattern_expressions.py::test_invalid_constant_type stix2/test/v21/test_pattern_expressions.py::test_invalid_integer_constant stix2/test/v21/test_pattern_expressions.py::test_invalid_timestamp_constant stix2/test/v21/test_pattern_expressions.py::test_invalid_float_constant stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[True-True0] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[False-False0] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[True-True1] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[False-False1] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[true-True] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[false-False] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[t-True] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[f-False] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[T-True] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[F-False] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[1-True] stix2/test/v21/test_pattern_expressions.py::test_boolean_constant[0-False] stix2/test/v21/test_pattern_expressions.py::test_invalid_boolean_constant stix2/test/v21/test_pattern_expressions.py::test_invalid_hash_constant[MD5-zzz] stix2/test/v21/test_pattern_expressions.py::test_invalid_hash_constant[ssdeep-zzz==] stix2/test/v21/test_pattern_expressions.py::test_invalid_hex_constant stix2/test/v21/test_pattern_expressions.py::test_invalid_binary_constant stix2/test/v21/test_pattern_expressions.py::test_escape_quotes_and_backslashes stix2/test/v21/test_pattern_expressions.py::test_like stix2/test/v21/test_pattern_expressions.py::test_issuperset stix2/test/v21/test_pattern_expressions.py::test_repeat_qualifier stix2/test/v21/test_pattern_expressions.py::test_invalid_repeat_qualifier stix2/test/v21/test_pattern_expressions.py::test_invalid_within_qualifier stix2/test/v21/test_pattern_expressions.py::test_startstop_qualifier stix2/test/v21/test_pattern_expressions.py::test_invalid_startstop_qualifier stix2/test/v21/test_pattern_expressions.py::test_make_constant_already_a_constant
stix2/v21/common.py stix2/v21/bundle.py stix2/v21/observables.py stix2/v21/sro.py stix2/patterns.py stix2/pattern_visitor.py stix2/v21/sdo.py
stix2/v21/common.py stix2/v21/bundle.py stix2/v21/observables.py stix2/v21/sro.py stix2/patterns.py stix2/pattern_visitor.py stix2/v21/sdo.py