Repository: oasis-open/cti-python-stix2 (python)

Issue:
Consistency when creating custom content
When creating custom object types or observable types, you must provide properties as a list of tuples (because of how we order properties when "pretty printing"), but when creating a custom observable extension, you must provide properties as a dict. This should be changed to require the properties as a list of tuples.

Also, the code adds an `extensions` property to custom-created Extension classes, whereas it should have been added to custom-created Observable classes instead (in the spec it's a common property on Observables).

Pull request:
Ensure consistency with custom Observable content
This PR was based off of #158 to update the most recent version of the documentation and avoid conflcts.

Fixes #159.

Patch:
diff --git a/docs/api_ref.rst b/docs/api_ref.rst
index ffc328c..dc66401 100644
--- a/docs/api_ref.rst
+++ b/docs/api_ref.rst
@@ -7,6 +7,6 @@ functions in the ``stix2`` API, as given by the package's docstrings.
 .. note::
     All the classes and functions detailed in the pages below are importable
     directly from `stix2`. See also:
-    :ref:`How imports will work </guide/ts_support.ipynb#How-imports-will-work>`.
+    :ref:`How imports work </guide/ts_support.ipynb#How-imports-work>`.
 
 .. automodule:: stix2
diff --git a/docs/datastore_api.rst b/docs/datastore_api.rst
deleted file mode 100644
index 5ecf09f..0000000
--- a/docs/datastore_api.rst
+++ /dev/null
@@ -1,39 +0,0 @@
-.. _datastore_api:
-
-DataStore API
-=============
-
-.. warning::
-
-    The DataStore API is still in the planning stages and may be subject to
-    major changes. We encourage anyone with feedback to contact the maintainers
-    to help ensure the API meets a large variety of use cases.
-
-One prominent feature of python-stix2 will be an interface for connecting
-different backend data stores containing STIX content. This will allow a uniform
-interface for querying and saving STIX content, and allow higher level code to
-be written without regard to the underlying data storage format. python-stix2
-will define the API and contain some default implementations of this API, but
-developers are encouraged to write their own implementations.
-
-Potential functions of the API include:
-
-* get a STIX Object by ID (returns the most recent version).
-* get all versions of a STIX object by ID.
-* get all relationships involving a given object, and all related objects.
-* save an object.
-* query for objects that match certain criteria (query syntax TBD).
-
-For all queries, the API will include a "filter" interface that can be used to
-either explicitly include or exclude results with certain criteria. For example,
-
-* only trust content from a set of object creators.
-* exclude content from certain (untrusted) object creators.
-* only include content with a confidence above a certain threshold (once
-  confidence is added to STIX).
-* only return content that can be shared with external parties (in other words,
-  that has TLP:GREEN markings).
-
-Additionally, the python-stix2 library will contain a "composite" data store,
-which implements the DataStore API while delegating functionality to one or more
-"child" data stores.
diff --git a/docs/guide.rst b/docs/guide.rst
index 80d2fb3..b722be3 100644
--- a/docs/guide.rst
+++ b/docs/guide.rst
@@ -1,6 +1,9 @@
 User's Guide
 ============
 
+This section of documentation contains guides and tutorials on how to use the
+``stix2`` library.
+
 .. toctree::
    :glob:
 
diff --git a/docs/guide/creating.ipynb b/docs/guide/creating.ipynb
index d9464e7..61bbe15 100644
--- a/docs/guide/creating.ipynb
+++ b/docs/guide/creating.ipynb
@@ -377,7 +377,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "To update the properties of an object, see the [Versioning](guide/versioning.ipynb) section."
+    "To update the properties of an object, see the [Versioning](versioning.ipynb) section."
    ]
   },
   {
diff --git a/docs/guide/custom.ipynb b/docs/guide/custom.ipynb
index bc19749..e651084 100644
--- a/docs/guide/custom.ipynb
+++ b/docs/guide/custom.ipynb
@@ -1155,10 +1155,10 @@
    "source": [
     "from stix2 import File, CustomExtension\n",
     "\n",
-    "@CustomExtension(File, 'x-new-ext', {\n",
-    "    'property1': properties.StringProperty(required=True),\n",
-    "    'property2': properties.IntegerProperty(),\n",
-    "})\n",
+    "@CustomExtension(File, 'x-new-ext', [\n",
+    "    ('property1', properties.StringProperty(required=True)),\n",
+    "    ('property2', properties.IntegerProperty()),\n",
+    "])\n",
     "class NewExtension():\n",
     "    pass\n",
     "\n",
diff --git a/docs/guide/datastore.ipynb b/docs/guide/datastore.ipynb
index 474c719..7d40930 100644
--- a/docs/guide/datastore.ipynb
+++ b/docs/guide/datastore.ipynb
@@ -58,9 +58,9 @@
    "source": [
     "# DataStore API\n",
     "\n",
-    "CTI Python STIX2 features a new interface for pulling and pushing STIX2 content. The new interface consists of [DataStore](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin), [DataSource](../api/stix2.datastore.rst#stix2.datastore.DataSource) and [DataSink](../api/stix2.datastore.rst#stix2.datastore.DataSink) constructs: a [DataSource](../api/stix2.datastore.rst#stix2.datastore.DataSource) for pulling STIX2 content, a [DataSink](../api/stix2.datastore.rst#stix2.datastore.DataSink) for pushing STIX2 content, and a [DataStore](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin) for both pulling and pushing.\n",
+    "The ``stix2`` library features an interface for pulling and pushing STIX 2 content. This interface consists of [DataStore](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin), [DataSource](../api/stix2.datastore.rst#stix2.datastore.DataSource) and [DataSink](../api/stix2.datastore.rst#stix2.datastore.DataSink) constructs: a [DataSource](../api/stix2.datastore.rst#stix2.datastore.DataSource) for pulling STIX 2 content, a [DataSink](../api/stix2.datastore.rst#stix2.datastore.DataSink) for pushing STIX 2 content, and a [DataStore](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin) for both pulling and pushing.\n",
     "\n",
-    "The DataStore, [DataSource](../api/stix2.datastore.rst#stix2.datastore.DataSource), [DataSink](../api/stix2.datastore.rst#stix2.datastore.DataSink) (collectively referred to as the \"DataStore suite\") APIs are not referenced directly by a user but are used as base classes, which are then subclassed by real DataStore suites. CTI Python STIX2 provides the DataStore suites of [FileSystem](../api/datastore/stix2.datastore.filesystem.rst), [Memory](../api/datastore/stix2.datastore.memory.rst), and [TAXII](../api/datastore/stix2.datastore.taxii.rst). Users are also encouraged to subclass the base classes and create their own custom DataStore suites."
+    "The DataStore, [DataSource](../api/stix2.datastore.rst#stix2.datastore.DataSource), [DataSink](../api/stix2.datastore.rst#stix2.datastore.DataSink) (collectively referred to as the \"DataStore suite\") APIs are not referenced directly by a user but are used as base classes, which are then subclassed by real DataStore suites. The ``stix2`` library provides the DataStore suites of [FileSystem](../api/datastore/stix2.datastore.filesystem.rst), [Memory](../api/datastore/stix2.datastore.memory.rst), and [TAXII](../api/datastore/stix2.datastore.taxii.rst). Users are also encouraged to subclass the base classes and create their own custom DataStore suites."
    ]
   },
   {
@@ -340,9 +340,16 @@
    "source": [
     "## Filters\n",
     "\n",
-    "The CTI Python STIX2 DataStore suites - [FileSystem](../api/datastore/stix2.datastore.filesystem.rst), [Memory](../api/datastore/stix2.datastore.memory.rst), and [TAXII](../api/datastore/stix2.datastore.taxii.rst) - all use the [Filters](../api/datastore/stix2.datastore.filters.rst) module to allow for the querying of STIX content. The basic functionality is that filters can be created and supplied everytime to calls to `query()`, and/or attached to a [DataStore](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin) so that every future query placed to that [DataStore](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin) is evaluated against the attached filters, supplemented with any further filters supplied with the query call. Attached filters can also be removed from [DataStores](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin).\n",
+    "The ``stix2`` DataStore suites - [FileSystem](../api/datastore/stix2.datastore.filesystem.rst), [Memory](../api/datastore/stix2.datastore.memory.rst), and [TAXII](../api/datastore/stix2.datastore.taxii.rst) - all use the [Filters](../api/datastore/stix2.datastore.filters.rst) module to allow for the querying of STIX content. Filters can be used to explicitly include or exclude results with certain criteria. For example:\n",
     "\n",
-    "Filters are very simple, as they consist of a field name, comparison operator and an object property value (i.e. value to compare to). All properties of STIX2 objects can be filtered on. In addition, TAXII2 Filtering parameters for fields can also be used in filters.\n",
+    "* only trust content from a set of object creators\n",
+    "* exclude content from certain (untrusted) object creators\n",
+    "* only include content with a confidence above a certain threshold (once confidence is added to STIX 2)\n",
+    "* only return content that can be shared with external parties (e.g. only content that has TLP:GREEN markings)\n",
+    "\n",
+    "Filters can be created and supplied with every call to `query()`, and/or attached to a [DataStore](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin) so that every future query placed to that [DataStore](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin) is evaluated against the attached filters, supplemented with any further filters supplied with the query call. Attached filters can also be removed from [DataStores](../api/stix2.datastore.rst#stix2.datastore.DataStoreMixin).\n",
+    "\n",
+    "Filters are very simple, as they consist of a field name, comparison operator and an object property value (i.e. value to compare to). All properties of STIX 2 objects can be filtered on. In addition, TAXII 2 Filtering parameters for fields can also be used in filters.\n",
     "\n",
     "TAXII2 filter fields:\n",
     "\n",
diff --git a/docs/guide/filesystem.ipynb b/docs/guide/filesystem.ipynb
index 28a1843..b3aca88 100644
--- a/docs/guide/filesystem.ipynb
+++ b/docs/guide/filesystem.ipynb
@@ -58,9 +58,9 @@
    "source": [
     "## FileSystem \n",
     "\n",
-    "The FileSystem suite contains [FileSystemStore](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemStore), [FileSystemSource](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource) and [FileSystemSink](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSink). Under the hood, all FileSystem objects point to a file directory (on disk) that contains STIX2 content. \n",
+    "The FileSystem suite contains [FileSystemStore](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemStore), [FileSystemSource](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource) and [FileSystemSink](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSink). Under the hood, all FileSystem objects point to a file directory (on disk) that contains STIX 2 content. \n",
     "\n",
-    "The directory and file structure of the intended STIX2 content should be:\n",
+    "The directory and file structure of the intended STIX 2 content should be:\n",
     "\n",
     "```\n",
     "stix2_content/\n",
@@ -82,7 +82,7 @@
     "    /STIX2 Domain Object type\n",
     "```\n",
     "\n",
-    "The master STIX2 content directory contains subdirectories, each of which aligns to a STIX2 domain object type (i.e. \"attack-pattern\", \"campaign\", \"malware\", etc.). Within each STIX2 domain object subdirectory are JSON files that are STIX2 domain objects of the specified type. The name of the json files correspond to the ID of the STIX2 domain object found within that file. A real example of the FileSystem directory structure:\n",
+    "The master STIX 2 content directory contains subdirectories, each of which aligns to a STIX 2 domain object type (i.e. \"attack-pattern\", \"campaign\", \"malware\", etc.). Within each STIX 2 domain object subdirectory are JSON files that are STIX 2 domain objects of the specified type. The name of the json files correspond to the ID of the STIX 2 domain object found within that file. A real example of the FileSystem directory structure:\n",
     "\n",
     "```\n",
     "stix2_content/\n",
@@ -107,13 +107,13 @@
     "    /vulnerability\n",
     "```\n",
     "\n",
-    "[FileSystemStore](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemStore) is intended for use cases where STIX2 content is retrieved and pushed to the same file directory. As [FileSystemStore](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemStore) is just a wrapper around a paired [FileSystemSource](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource) and [FileSystemSink](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSink) that point the same file directory.\n",
+    "[FileSystemStore](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemStore) is intended for use cases where STIX 2 content is retrieved and pushed to the same file directory. As [FileSystemStore](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemStore) is just a wrapper around a paired [FileSystemSource](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource) and [FileSystemSink](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSink) that point the same file directory.\n",
     "\n",
-    "For use cases where STIX2 content will only be retrieved or pushed, then a [FileSystemSource](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource) and [FileSystemSink](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSink) can be used individually. They can also be used individually when STIX2 content will be retrieved from one distinct file directory and pushed to another.\n",
+    "For use cases where STIX 2 content will only be retrieved or pushed, then a [FileSystemSource](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource) and [FileSystemSink](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSink) can be used individually. They can also be used individually when STIX 2 content will be retrieved from one distinct file directory and pushed to another.\n",
     "\n",
     "### FileSystem API\n",
     "\n",
-    "A note on [get()](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource.get), [all_versions()](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource.all_versions), and [query()](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource.query): The format of the STIX2 content targeted by the FileSystem suite is JSON files. When the [FileSystemStore](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemStore) retrieves STIX2 content (in JSON) from disk, it will attempt to parse the content into full-featured python-stix2 objects and returned as such. \n",
+    "A note on [get()](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource.get), [all_versions()](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource.all_versions), and [query()](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSource.query): The format of the STIX2 content targeted by the FileSystem suite is JSON files. When the [FileSystemStore](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemStore) retrieves STIX 2 content (in JSON) from disk, it will attempt to parse the content into full-featured python-stix2 objects and returned as such. \n",
     "\n",
     "A note on [add()](../api/datastore/stix2.datastore.filesystem.rst#stix2.datastore.filesystem.FileSystemSink.add): When STIX content is added (pushed) to the file system, the STIX content can be supplied in the following forms: Python STIX objects, Python dictionaries (of valid STIX objects or Bundles), JSON-encoded strings (of valid STIX objects or Bundles), or a (Python) list of any of the previously listed types. Any of the previous STIX content forms will be converted to a STIX JSON object (in a STIX Bundle) and written to disk. \n",
     "\n",
diff --git a/docs/guide/parsing.ipynb b/docs/guide/parsing.ipynb
index 284125e..4bd026f 100644
--- a/docs/guide/parsing.ipynb
+++ b/docs/guide/parsing.ipynb
@@ -676,20 +676,20 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## Parsing Custom STIX Content"
+    "### Parsing Custom STIX Content"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Parsing custom STIX objects and/or STIX objects with custom properties is also completed easily with [parse()](../api/stix2.core.rst#stix2.core.parse). Just supply the keyword argument *allow_custom=True*. When *allow_custom* is specified, [parse()](../api/stix2.core.rst#stix2.core.parse) will attempt to convert the supplied STIX content to known STIX2 domain objects and/or previously defined custom defined STIX2 objects. If the conversion cannot be completed (and *allow_custom* is specified), [parse()](../api/stix2.core.rst#stix2.core.parse) will treat the supplied STIX2 content as valid STIX2 objects and return them. **Warning: Specifying *allow_custom* may lead to critical errors if further processing (searching, filtering, modifying etc...) of the custom STIX2 content occurs where the custom STIX2 content supplied is not valid STIX2**. This is an axiomatic possibility as the STIX2 library cannot guarantee proper processing of unknown custom STIX2 objects that were explicitly flagged to be allowed, and thus may not be valid.\n",
+    "Parsing custom STIX objects and/or STIX objects with custom properties is also completed easily with [parse()](../api/stix2.core.rst#stix2.core.parse). Just supply the keyword argument ``allow_custom=True``. When ``allow_custom`` is specified, [parse()](../api/stix2.core.rst#stix2.core.parse) will attempt to convert the supplied STIX content to known STIX 2 domain objects and/or previously defined [custom STIX 2 objects](custom.ipynb). If the conversion cannot be completed (and ``allow_custom`` is specified), [parse()](../api/stix2.core.rst#stix2.core.parse) will treat the supplied STIX 2 content as valid STIX 2 objects and return them. **Warning: Specifying allow_custom may lead to critical errors if further processing (searching, filtering, modifying etc...) of the custom content occurs where the custom content supplied is not valid STIX 2**. This is an axiomatic possibility as the ``stix2`` library cannot guarantee proper processing of unknown custom STIX 2 objects that were explicitly flagged to be allowed, and thus may not be valid.\n",
     "\n",
-    "For examples on parsing STIX2 objects with custom STIX properties, see [Custom STIX Content:Custom Properties](custom.ipynb#Custom-Properties)\n",
+    "For examples of parsing STIX 2 objects with custom STIX properties, see [Custom STIX Content: Custom Properties](custom.ipynb#Custom-Properties)\n",
     "\n",
-    "For examples on parsing defined custom STIX2 objects, see [Custom STIX Content: Custom STIX Object Types](custom.ipynb#Custom-STIX-Object-Types)\n",
+    "For examples of parsing defined custom STIX 2 objects, see [Custom STIX Content: Custom STIX Object Types](custom.ipynb#Custom-STIX-Object-Types)\n",
     "\n",
-    "For the case where it is desired to retrieve STIX2 content from a source (e.g. file system, TAXII) that may possibly have custom STIX2 content unknown to the user, the user can create a STIX2 DataStore/Source with the flag *allow_custom=True*. As aforementioned this will configure the DataStore/Source to allow for unknown STIX2 content to be returned (albeit not converted to full STIX2 domain objects and properties); notable processing capabilites of the STIX2 library may be precluded by the unknown STIX2 content, if the content is not valid or actual STIX2 domain objects and properties."
+    "For retrieving STIX 2 content from a source (e.g. file system, TAXII) that may possibly have custom STIX 2 content unknown to the user, the user can create a STIX 2 DataStore/Source with the flag ``allow_custom=True``. As mentioned, this will configure the DataStore/Source to allow for unknown STIX 2 content to be returned (albeit not converted to full STIX 2 domain objects and properties); the ``stix2`` library may preclude processing the unknown content, if the content is not valid or actual STIX 2 domain objects and properties."
    ]
   },
   {
diff --git a/docs/guide/ts_support.ipynb b/docs/guide/ts_support.ipynb
index 2264eb5..445e263 100644
--- a/docs/guide/ts_support.ipynb
+++ b/docs/guide/ts_support.ipynb
@@ -59,7 +59,7 @@
    "source": [
     "## Technical Specification Support\n",
     "\n",
-    "### How imports will work\n",
+    "### How imports work\n",
     "\n",
     "Imports can be used in different ways depending on the use case and support levels.\n",
     "\n",
@@ -229,7 +229,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### How parsing will work\n",
+    "### How parsing works\n",
     "If the ``version`` positional argument is not provided. The data will be parsed using the latest version of STIX 2.X supported by the `stix2` library.\n",
     "\n",
     "You can lock your [parse()](../api/stix2.core.rst#stix2.core.parse) method to a specific STIX version by:"
@@ -363,7 +363,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### How will custom content work\n",
+    "### How custom content works\n",
     "\n",
     "[CustomObject](../api/stix2.v20.sdo.rst#stix2.v20.sdo.CustomObject), [CustomObservable](../api/stix2.v20.observables.rst#stix2.v20.observables.CustomObservable), [CustomMarking](../api/stix2.v20.common.rst#stix2.v20.common.CustomMarking) and [CustomExtension](../api/stix2.v20.observables.rst#stix2.v20.observables.CustomExtension) must be registered explicitly by STIX version. This is a design decision since properties or requirements may change as the STIX Technical Specification advances.\n",
     "\n",
diff --git a/docs/index.rst b/docs/index.rst
index 62d07ff..dd39903 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,8 +3,21 @@
    You can adapt this file completely to your liking, but it should at least
    contain the root `toctree` directive.
 
-Welcome to stix2's documentation!
-=================================
+STIX 2 Python API Documentation
+===============================
+
+.. warning::
+
+    Prior to version 1.0, all APIs are considered unstable and subject to change.
+
+Welcome to the STIX 2 Python API's documentation. This library is designed to
+help you work with STIX 2 content. For more information about STIX 2, see the
+`website <http://cti-tc.github.io>`_ of the OASIS Cyber Threat Intelligence
+Technical Committee.
+
+Get started with an `overview <overview.rst>`_ of the library, then take a look
+at the `guides and tutorials <guide.rst>`_ to see how to use it. For information
+about a specific class or function, see the `API reference <api_ref.rst>`_.
 
 .. toctree::
    :maxdepth: 3
@@ -13,8 +26,6 @@ Welcome to stix2's documentation!
    overview
    guide
    api_ref
-   datastore_api
-   roadmap
    contributing
 
 
diff --git a/docs/overview.rst b/docs/overview.rst
index 0306a22..0396ee8 100644
--- a/docs/overview.rst
+++ b/docs/overview.rst
@@ -4,7 +4,7 @@ Overview
 Goals
 -----
 
-High level goals/principles of the python-stix2 library:
+High level goals/principles of the Python ``stix2`` library:
 
 1. It should be as easy as possible (but no easier!) to perform common tasks of
    producing, consuming, and processing STIX 2 content.
@@ -17,22 +17,22 @@ Design Decisions
 ----------------
 
 To accomplish these goals, and to incorporate lessons learned while developing
-python-stix (for STIX 1.x), several decisions influenced the design of
-python-stix2:
+``python-stix`` (for STIX 1.x), several decisions influenced the design of the
+``stix2`` library:
 
 1. All data structures are immutable by default. In contrast to python-stix,
    where users would create an object and then assign attributes to it, in
-   python-stix2 all properties must be provided when creating the object.
+   ``stix2`` all properties must be provided when creating the object.
 2. Where necessary, library objects should act like ``dict``'s. When treated as
    a ``str``, the JSON reprentation of the object should be used.
 3. Core Python data types (including numeric types, ``datetime``) should be used
    when appropriate, and serialized to the correct format in JSON as specified
-   in the STIX 2.0 spec.
+   in the STIX 2 spec.
 
 Architecture
 ------------
 
-The `stix2` library APIs are divided into three logical layers, representing
+The ``stix2`` library is divided into three logical layers, representing
 different levels of abstraction useful in different types of scripts and larger
 applications. It is possible to combine multiple layers in the same program,
 and the higher levels build on the layers below.
@@ -41,7 +41,7 @@ and the higher levels build on the layers below.
 Object Layer
 ^^^^^^^^^^^^
 
-The lowest layer, **Object Layer**, is where Python objects representing STIX 2
+The lowest layer, the **Object Layer**, is where Python objects representing STIX 2
 data types (such as SDOs, SROs, and Cyber Observable Objects, as well as
 non-top-level objects like External References, Kill Chain phases, and Cyber
 Observable extensions) are created, and can be serialized and deserialized
@@ -57,8 +57,6 @@ not implemented as references between the Python objects themselves, but by
 simply having the same values in ``id`` and reference properties. There is no
 referential integrity maintained by the ``stix2`` library.
 
-*This layer is mostly complete.*
-
 Environment Layer
 ^^^^^^^^^^^^^^^^^
 
@@ -79,8 +77,7 @@ intelligence ecosystem.
 Each of these components can be used individually, or combined as part of an
 ``Environment``. These ``Environment`` objects allow different settings to be
 used by different users of a multi-user application (such as a web application).
-
-*This layer is mostly complete.*
+For more information, check out `this Environment tutorial <guide/environment.ipynb>`_.
 
 Workbench Layer
 ^^^^^^^^^^^^^^^
@@ -89,9 +86,7 @@ The highest layer of the ``stix2`` APIs is the **Workbench Layer**, designed for
 a single user in a highly-interactive analytical environment (such as a `Jupyter
 Notebook <https://jupyter.org/>`_). It builds on the lower layers of the API,
 while hiding most of their complexity. Unlike the other layers, this layer is
-designed to be used directly by end users. For users who are comfortable with,
+designed to be used directly by end users. For users who are comfortable with
 Python, the Workbench Layer makes it easy to quickly interact with STIX data
 from a variety of sources without needing to write and run one-off Python
-scripts.
-
-*This layer is currently being developed.*
+scripts. For more information, check out `this Workbench tutorial <guide/workbench.ipynb>`_.
diff --git a/docs/roadmap.rst b/docs/roadmap.rst
deleted file mode 100644
index 7fb9d8b..0000000
--- a/docs/roadmap.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-Development Roadmap
-===================
-
-.. warning::
-
-    Prior to version 1.0, all APIs are considered unstable and subject to
-    change.
-
-This is a list of (planned) features before version 1.0 is released.
-
-* Serialization of all STIX and Cyber Observable objects to JSON.
-* De-serialization (parsing) of all STIX and Cyber Observable objects.
-* APIs for versioning (revising and revoking) STIX objects.
-* APIs for marking STIX objects and interpreting markings of STIX objects.
-* :ref:`datastore_api`, providing a common interface for querying sources
-  of STIX content (such as objects in memory, on a filesystem, in a database, or
-  via a TAXII feed).
diff --git a/requirements.txt b/requirements.txt
index d6abb63..32e08f2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 bumpversion
 ipython
-nbsphinx>=0.3.0
+nbsphinx==0.3.2
 pre-commit
 pytest
 pytest-cov
diff --git a/stix2/datastore/__init__.py b/stix2/datastore/__init__.py
index 442a242..4ca9d5f 100644
--- a/stix2/datastore/__init__.py
+++ b/stix2/datastore/__init__.py
@@ -1,4 +1,4 @@
-"""Python STIX 2.0 DataStore API
+"""Python STIX 2.0 DataStore API.
 
 .. autosummary::
    :toctree: datastore
diff --git a/stix2/environment.py b/stix2/environment.py
index e40e991..a456e95 100644
--- a/stix2/environment.py
+++ b/stix2/environment.py
@@ -1,3 +1,6 @@
+"""Python STIX 2.0 Environment API.
+"""
+
 import copy
 
 from .core import parse as _parse
diff --git a/stix2/utils.py b/stix2/utils.py
index 37ff166..9febd78 100644
--- a/stix2/utils.py
+++ b/stix2/utils.py
@@ -263,11 +263,11 @@ def get_class_hierarchy_names(obj):
 
 
 def remove_custom_stix(stix_obj):
-    """remove any custom STIX objects or properties
+    """Remove any custom STIX objects or properties.
 
     Warning: This function is a best effort utility, in that
     it will remove custom objects and properties based on the
-    type names; i.e. if "x-" prefixes object types, and "x_"
+    type names; i.e. if "x-" prefixes object types, and "x\\_"
     prefixes property types. According to the STIX2 spec,
     those naming conventions are a SHOULDs not MUSTs, meaning
     that valid custom STIX content may ignore those conventions
diff --git a/stix2/v20/observables.py b/stix2/v20/observables.py
index 83600b0..39a8f19 100644
--- a/stix2/v20/observables.py
+++ b/stix2/v20/observables.py
@@ -979,6 +979,9 @@ def CustomObservable(type='x-custom-observable', properties=None):
                                      "is not a ListProperty containing ObjectReferenceProperty." % prop_name)
 
             _properties.update(properties)
+            _properties.update([
+                ('extensions', ExtensionsProperty(enclosing_type=_type)),
+            ])
 
             def __init__(self, **kwargs):
                 _Observable.__init__(self, **kwargs)
@@ -1030,12 +1033,10 @@ def CustomExtension(observable=None, type='x-custom-observable', properties=None
         class _Custom(cls, _Extension):
 
             _type = type
-            _properties = {
-                'extensions': ExtensionsProperty(enclosing_type=_type),
-            }
+            _properties = OrderedDict()
 
-            if not isinstance(properties, dict) or not properties:
-                raise ValueError("'properties' must be a dict!")
+            if not properties or not isinstance(properties, list):
+                raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]")
 
             _properties.update(properties)
 
diff --git a/stix2/v20/sdo.py b/stix2/v20/sdo.py
index 7ccc3e3..060b9f0 100644
--- a/stix2/v20/sdo.py
+++ b/stix2/v20/sdo.py
@@ -1,4 +1,5 @@
-"""STIX 2.0 Domain Objects"""
+"""STIX 2.0 Domain Objects.
+"""
 
 from collections import OrderedDict
 
diff --git a/stix2/workbench.py b/stix2/workbench.py
index 9e31b50..b47968e 100644
--- a/stix2/workbench.py
+++ b/stix2/workbench.py
@@ -131,7 +131,7 @@ def _setup_workbench():
     for obj_type in STIX_OBJS:
         new_class_dict = {
             '__new__': _constructor_wrapper(obj_type),
-            '__doc__': 'Workbench wrapper around the `{0} <stix2.v20.sdo.html#stix2.v20.sdo.{0}>`__ object. {1}'.format(obj_type.__name__, STIX_OBJ_DOCS)
+            '__doc__': 'Workbench wrapper around the `{0} <stix2.v20.sdo.rst#stix2.v20.sdo.{0}>`__ object. {1}'.format(obj_type.__name__, STIX_OBJ_DOCS)
         }
         new_class = type(obj_type.__name__, (), new_class_dict)
 


Test patch:
diff --git a/stix2/test/test_custom.py b/stix2/test/test_custom.py
index cc8b32b..a14503f 100644
--- a/stix2/test/test_custom.py
+++ b/stix2/test/test_custom.py
@@ -435,10 +435,10 @@ def test_observed_data_with_custom_observable_object():
     assert ob_data.objects['0'].property1 == 'something'
 
 
-@stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext', {
-    'property1': stix2.properties.StringProperty(required=True),
-    'property2': stix2.properties.IntegerProperty(),
-})
+@stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext', [
+    ('property1', stix2.properties.StringProperty(required=True)),
+    ('property2', stix2.properties.IntegerProperty()),
+])
 class NewExtension():
     def __init__(self, property2=None, **kwargs):
         if property2 and property2 < 10:
@@ -485,9 +485,9 @@ def test_custom_extension_invalid_observable():
     class Foo(object):
         pass
     with pytest.raises(ValueError) as excinfo:
-        @stix2.observables.CustomExtension(Foo, 'x-new-ext', {
-            'property1': stix2.properties.StringProperty(required=True),
-        })
+        @stix2.observables.CustomExtension(Foo, 'x-new-ext', [
+            ('property1', stix2.properties.StringProperty(required=True)),
+        ])
         class FooExtension():
             pass  # pragma: no cover
     assert str(excinfo.value) == "'observable' must be a valid Observable class!"
@@ -495,9 +495,9 @@ def test_custom_extension_invalid_observable():
     class Bar(stix2.observables._Observable):
         pass
     with pytest.raises(ValueError) as excinfo:
-        @stix2.observables.CustomExtension(Bar, 'x-new-ext', {
-            'property1': stix2.properties.StringProperty(required=True),
-        })
+        @stix2.observables.CustomExtension(Bar, 'x-new-ext', [
+            ('property1', stix2.properties.StringProperty(required=True)),
+        ])
         class BarExtension():
             pass
     assert "Unknown observable type" in str(excinfo.value)
@@ -506,9 +506,9 @@ def test_custom_extension_invalid_observable():
     class Baz(stix2.observables._Observable):
         _type = 'Baz'
     with pytest.raises(ValueError) as excinfo:
-        @stix2.observables.CustomExtension(Baz, 'x-new-ext', {
-            'property1': stix2.properties.StringProperty(required=True),
-        })
+        @stix2.observables.CustomExtension(Baz, 'x-new-ext', [
+            ('property1', stix2.properties.StringProperty(required=True)),
+        ])
         class BazExtension():
             pass
     assert "Unknown observable type" in str(excinfo.value)
@@ -520,21 +520,29 @@ def test_custom_extension_no_properties():
         @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', None)
         class BarExtension():
             pass
-    assert "'properties' must be a dict!" in str(excinfo.value)
+    assert "Must supply a list, containing tuples." in str(excinfo.value)
 
 
 def test_custom_extension_empty_properties():
+    with pytest.raises(ValueError) as excinfo:
+        @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', [])
+        class BarExtension():
+            pass
+    assert "Must supply a list, containing tuples." in str(excinfo.value)
+
+
+def test_custom_extension_dict_properties():
     with pytest.raises(ValueError) as excinfo:
         @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', {})
         class BarExtension():
             pass
-    assert "'properties' must be a dict!" in str(excinfo.value)
+    assert "Must supply a list, containing tuples." in str(excinfo.value)
 
 
 def test_custom_extension_no_init_1():
-    @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-extension', {
-        'property1': stix2.properties.StringProperty(required=True),
-    })
+    @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-extension', [
+        ('property1', stix2.properties.StringProperty(required=True)),
+    ])
     class NewExt():
         pass
 
@@ -543,9 +551,9 @@ def test_custom_extension_no_init_1():
 
 
 def test_custom_extension_no_init_2():
-    @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', {
-        'property1': stix2.properties.StringProperty(required=True),
-    })
+    @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', [
+        ('property1', stix2.properties.StringProperty(required=True)),
+    ])
     class NewExt2(object):
         pass
 
@@ -594,3 +602,8 @@ def test_register_custom_object():
     stix2._register_type(CustomObject2)
     # Note that we will always check against newest OBJ_MAP.
     assert (CustomObject2._type, CustomObject2) in stix2.OBJ_MAP.items()
+
+
+def test_extension_property_location():
+    assert 'extensions' in stix2.v20.observables.OBJ_MAP_OBSERVABLE['x-new-observable']._properties
+    assert 'extensions' not in stix2.v20.observables.EXT_MAP['domain-name']['x-new-ext']._properties