Repository: metergroup/GRANOLA (language: python)

Issue:
Write/Line Terminator for serial commands needs to be configurable (currently hard coded to carriage return)
## 🚀 Feature Request

Because of the history of what GRANOLA grew out of, the write terminator was \r, but that needs to be a configurable option.

## 🔈 Motivation

Lots of other serial devices won't use a carriage return as the write/line terminator

Add module level docstrings and update pytest config to run doctest automatically
## Type of Change

<!-- Mark with an `x` all the checkboxes that apply (like `[x]`) -->

- [x] 📚 Examples / docs / tutorials / dependencies update
- [ ] 🔧 Bug fix (non-breaking change which fixes an issue)
- [ ] 🥂 Improvement (non-breaking change which improves an existing feature)
- [ ] 🚀 New feature (non-breaking change which adds functionality)
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🔐 Security fix


Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
index 8a743c2..9579bcb 100644
--- a/.flake8
+++ b/.flake8
@@ -14,5 +14,6 @@ exclude =
     .venv,
     granola/_version.py,
     versioneer.py
+    docs
 
 max-line-length = 120
\ No newline at end of file
diff --git a/.github/workflows/ISSUE_TEMPLATE/bug_report.md b/.github/workflows/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 7f928ca..0000000
--- a/.github/workflows/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-name: 🐛 Bug report
-about: If something isn't working 🔧
-title: ''
-labels: bug
-assignees:
----
-
-## 🐛 Bug Report
-
-<!-- A clear and concise description of what the bug is. -->
-
-## 🔬 How To Reproduce
-
-Steps to reproduce the behavior:
-
-1. ...
-
-### Code sample
-
-<!-- If applicable, attach a minimal code sample to reproduce the decried issue. -->
-
-### Environment
-
-* OS: [e.g. Linux / Windows / macOS]
-* Python version, get it with:
-
-```bash
-python --version
-```
-
-### Screenshots
-
-<!-- If applicable, add screenshots to help explain your problem. -->
-
-## 📈 Expected behavior
-
-<!-- A clear and concise description of what you expected to happen. -->
-
-## 📎 Additional context
-
-<!-- Add any other context about the problem here. -->
\ No newline at end of file
diff --git a/.github/workflows/ISSUE_TEMPLATE/config.yaml b/.github/workflows/ISSUE_TEMPLATE/config.yaml
deleted file mode 100644
index ec4bb38..0000000
--- a/.github/workflows/ISSUE_TEMPLATE/config.yaml
+++ /dev/null
@@ -1,1 +0,0 @@
-blank_issues_enabled: false
\ No newline at end of file
diff --git a/.github/workflows/ISSUE_TEMPLATE/feature_request.md b/.github/workflows/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 2f905ac..0000000
--- a/.github/workflows/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-name: 🚀 Feature request
-about: Suggest an idea for this project 🏖
-title: ''
-labels: enhancement
-assignees:
----
-
-## 🚀 Feature Request
-
-<!-- A clear and concise description of the feature proposal. -->
-
-## 🔈 Motivation
-
-<!-- Please describe the motivation for this proposal. -->
-
-## 🛰 Alternatives
-
-<!-- A clear and concise description of any alternative solutions or features you've considered. -->
-
-## 📎 Additional context
-
-<!-- Add any other context or screenshots about the feature request here. -->
\ No newline at end of file
diff --git a/.github/workflows/ISSUE_TEMPLATE/question.md b/.github/workflows/ISSUE_TEMPLATE/question.md
deleted file mode 100644
index 02bf09f..0000000
--- a/.github/workflows/ISSUE_TEMPLATE/question.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-name: ❓ Question
-about: Ask a question about this project 🎓
-title: ''
-labels: question
-assignees:
----
-
-## Checklist
-
-<!-- Mark with an `x` all the checkboxes that apply (like `[x]`) -->
-
-- [ ] I've searched the project's [`issues`](https://github.com/TezRomacH/python-package-template/issues?q=is%3Aissue).
-
-## ❓ Question
-
-<!-- What is your question -->
-
-How can I [...]?
-
-Is it possible to [...]?
-
-## 📎 Additional context
-
-<!-- Add any other context or screenshots about the feature request here. -->
\ No newline at end of file
diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md
deleted file mode 100644
index 5d3457f..0000000
--- a/.github/workflows/pull_request_template.md
+++ /dev/null
@@ -1,28 +0,0 @@
-## Description
-
-<!-- Add a more detailed description of the changes if needed. -->
-
-## Related Issue
-
-<!-- If your PR refers to a related issue, link it here. -->
-
-## Type of Change
-
-<!-- Mark with an `x` all the checkboxes that apply (like `[x]`) -->
-
-- [ ] 📚 Examples / docs / tutorials / dependencies update
-- [ ] 🔧 Bug fix (non-breaking change which fixes an issue)
-- [ ] 🥂 Improvement (non-breaking change which improves an existing feature)
-- [ ] 🚀 New feature (non-breaking change which adds functionality)
-- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change)
-- [ ] 🔐 Security fix
-
-## Checklist
-
-<!-- Mark with an `x` all the checkboxes that apply (like `[x]`) -->
-
-- [ ] I've read the [`CODE_OF_CONDUCT.md`](https://github.com/TezRomacH/python-package-template/blob/master/CODE_OF_CONDUCT.md) document.
-- [ ] I've read the [`CONTRIBUTING.md`](https://github.com/TezRomacH/python-package-template/blob/master/CONTRIBUTING.md) guide.
-- [ ] I've updated the code style using `make codestyle`.
-- [ ] I've written tests for all new methods and classes that I created.
-- [ ] I've written the docstring in `Google` format for all the methods and classes that I used.
\ No newline at end of file
diff --git a/.ipynb_checkpoints/Untitled-checkpoint.ipynb b/.ipynb_checkpoints/Untitled-checkpoint.ipynb
new file mode 100644
index 0000000..363fcab
--- /dev/null
+++ b/.ipynb_checkpoints/Untitled-checkpoint.ipynb
@@ -0,0 +1,6 @@
+{
+ "cells": [],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5eafe55..4b134bb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,7 +7,6 @@ repos:
       - id: black
         language_version: python3.7
         stages: [commit]
-        args: [--py36-plus]
   - repo: https://github.com/PyCQA/isort
     rev: 5.10.0
     hooks:
diff --git a/README.md b/README.md
index 888c2ab..284fb5c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # Welcome to GRANOLA's documentation!
 
 
-GRANOLA (Generating Real-time Autonomous N-device Output without Linked Apparatuses) is a package aiming to mock pyserial's Serial
+GRANOLA (Generating Real-time Autonomous N-device Output without Linked Apparatuses) is a package aiming to mock [Pyserial](https://pyserial.readthedocs.io/en/latest/)'s Serial
 class with the goal of enabling automated testing, faster QA, and faster delivery. The core of this package is the class
 Cereal. Cereal allows you to define complicated command and response sequences, getters and setters for commands that set attributes
 on your device (such as the serial number), a hook system to inject your own needed functionality, and a Serial Sniffer to capture serial
@@ -28,12 +28,7 @@ TODO (madeline) fill in with link to readthedocs later
 ``` python
 >>> from granola import Cereal
 
->>> config = {
-...     "canned_queries": {
-...         "data": {
-...             "`DEFAULT`": {"1\r": "1",
-...                           "2\r": ["2a", "2b"]}
-...         }}}
+>>> config = {"command_readers":{"CannedQueries": {"data": [{"1\r": "1", "2\r": ["2a", "2b"]}]}}}
 ... mock_cereal = Cereal(config)
 
 >>> mock_cereal.write(b"2\r")
@@ -45,3 +40,7 @@ b'2a'
 >>> mock_cereal.read(10)
 b'1'
 ```
+
+### More Examples
+
+TODO put link to examples docs stuff
diff --git a/cereal_cmds.csv b/cereal_cmds.csv
new file mode 100644
index 0000000..c843fec
--- /dev/null
+++ b/cereal_cmds.csv
@@ -0,0 +1,10 @@
+cmd,response
+start\r, OK\r>
+get -volt\r, 7800\r>
+get -volt\r, 8000\r>
+get -volt\r, 4000\r>
+check -integrity\r, GOOD\r>
+radio on\r, OK\r>
+get name\r, Cereal Test Fixture\r
+reset\r, OK\r>
+get batt\r, 100\r>
\ No newline at end of file
diff --git a/config.json b/config.json
index 07e2790..142a6ba 100644
--- a/config.json
+++ b/config.json
@@ -1,18 +1,21 @@
 {
   "fake device": {
-    "canned_queries": {"data": {"`DEFAULT`": "granola\\tests\\data\\cereal_cmds.csv"}},
     "encoding": "ascii",
-    "getters_and_setters": {
-        "default_values": {
-            "my_value": "You got it"
-        },
-        "getters": [
-            {
-                "cmd": "get -my value\r",
-                "response": "{{ my_value }}"
-            }
-        ],
-        "setters":[]
+    "command_readers":{
+      "CannedQueries": {
+        "data": ["granola\\tests\\data\\cereal_cmds.csv"]},
+      "GettersAndSetters": {
+          "default_values": {
+              "my_value": "You got it"
+          },
+          "getters": [
+              {
+                  "cmd": "get -my value\r",
+                  "response": "{{ my_value }}"
+              }
+          ],
+          "setters":[]
+      }
     }
   }
 }
\ No newline at end of file
diff --git a/docs/api.rst b/docs/api.rst
index 5b77e3e..255e018 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -3,7 +3,9 @@ API
 ###
 
 
-Some words and workds
+.. todo::
+
+    Fill this in
 
 Mock Cereal
 =============
diff --git a/docs/bk_cereal.rst b/docs/bk_cereal.rst
index a4eb48d..1532870 100644
--- a/docs/bk_cereal.rst
+++ b/docs/bk_cereal.rst
@@ -6,5 +6,6 @@ granola.breakfast_cereal module
 
 .. automodule:: granola.breakfast_cereal
    :members:
+   :special-members: __call__
    :undoc-members:
    :show-inheritance:
diff --git a/docs/conf.py b/docs/conf.py
index 3d817c9..75cf93d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -13,14 +13,14 @@
 import os
 import sys
 
-sys.path.insert(0, os.path.abspath('..'))
+sys.path.insert(0, os.path.abspath(".."))
 
 
 # -- Project information -----------------------------------------------------
 
-project = 'GRANOLA'
-copyright = '2021, Madeline Scyphers Helen Cantanese'
-author = 'Madeline Scyphers Helen Cantanese'
+project = "GRANOLA"
+copyright = "2021, Madeline Scyphers Helen Cantanese"
+author = "Madeline Scyphers Helen Cantanese"
 
 
 # -- General configuration ---------------------------------------------------
@@ -29,39 +29,39 @@
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx.ext.autodoc',
-    'sphinx.ext.todo',
-    'sphinx.ext.coverage',
-    'sphinx.ext.napoleon',
-    'sphinx.ext.doctest',
-    'myst_parser',
-    'sphinx.ext.autosectionlabel',
-    'sphinx.ext.intersphinx',
+    "sphinx.ext.autodoc",
+    "sphinx.ext.todo",
+    "sphinx.ext.coverage",
+    "sphinx.ext.napoleon",
+    "sphinx.ext.doctest",
+    "sphinx.ext.autosectionlabel",
+    "sphinx.ext.intersphinx",
+    "myst_nb",
 ]
-doctest_global_setup = '''
-import granola
-'''
+
+jupyter_execute_notebooks = "force"
 
 intersphinx_mapping = {
-    'jinja2': ('https://jinja.palletsprojects.com/', None),
+    "jinja2": ("https://jinja.palletsprojects.com/", None),
+    "pyserial": ("https://pyserial.readthedocs.io/en/latest/", None),
 }
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
 #
 # source_suffix = ['.rst', '.md']
 source_suffix = {
-    '.rst': 'restructuredtext',
-    '.txt': 'markdown',
-    '.md': 'markdown',
+    ".rst": "restructuredtext",
+    ".txt": "markdown",
+    ".md": "markdown",
 }
 # source_suffix = '.rst'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -73,7 +73,7 @@
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = [u"_build", "Thumbs.db", ".DS_Store"]
 
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = None
@@ -83,7 +83,7 @@
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = 'sphinx_rtd_theme'
+html_theme = "sphinx_rtd_theme"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
@@ -94,7 +94,7 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # Custom sidebar templates, must be a dictionary that maps document names
 # to template names.
@@ -110,7 +110,7 @@
 # -- Options for HTMLHelp output ---------------------------------------------
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'sphinxtestdoc'
+htmlhelp_basename = "sphinxtestdoc"
 
 
 # -- Options for LaTeX output ------------------------------------------------
@@ -119,15 +119,12 @@
     # The paper size ('letterpaper' or 'a4paper').
     #
     # 'papersize': 'letterpaper',
-
     # The font size ('10pt', '11pt' or '12pt').
     #
     # 'pointsize': '10pt',
-
     # Additional stuff for the LaTeX preamble.
     #
     # 'preamble': '',
-
     # Latex figure (float) alignment
     #
     # 'figure_align': 'htbp',
@@ -137,8 +134,7 @@
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-    (master_doc, 'sphinxtest.tex', u'sphinx test Documentation',
-     u'Maddie', 'manual'),
+    (master_doc, "sphinxtest.tex", u"sphinx test Documentation", u"Maddie", "manual"),
 ]
 
 
@@ -146,10 +142,7 @@
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, 'sphinxtest', u'sphinx test Documentation',
-     [author], 1)
-]
+man_pages = [(master_doc, "sphinxtest", u"sphinx test Documentation", [author], 1)]
 
 
 # -- Options for Texinfo output ----------------------------------------------
@@ -158,9 +151,15 @@
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-    (master_doc, 'sphinxtest', u'sphinx test Documentation',
-     author, 'sphinxtest', 'One line description of project.',
-     'Miscellaneous'),
+    (
+        master_doc,
+        "sphinxtest",
+        u"sphinx test Documentation",
+        author,
+        "sphinxtest",
+        "One line description of project.",
+        "Miscellaneous",
+    ),
 ]
 
 
@@ -179,7 +178,7 @@
 # epub_uid = ''
 
 # A list of files that should not be packed into the epub file.
-epub_exclude_files = ['search.html']
+epub_exclude_files = ["search.html"]
 
 
 # -- Extension configuration -------------------------------------------------
diff --git a/docs/config/canned_queries.rst b/docs/config/canned_queries.rst
index 4927352..d9cd87c 100644
--- a/docs/config/canned_queries.rst
+++ b/docs/config/canned_queries.rst
@@ -1,65 +1,56 @@
 =================================
 Canned Queries Configuration
 =================================
-
-To use the :mod:`Command Reader <granola.command_readers>` :class:`~granola.command_readers.CannedQueries`, you must define ``"canned_queries"`` as a dictionary in your configuration.
+..
+    >>> import sys, pytest
+    >>> is_python_35 = sys.version_info[0] == 3 and sys.version_info[1] == 5
+    >>> if is_python_35:
+    ...     pytest.skip("This doctest doesn't work with Python 3.5 because dictionary ordering is not guaranteed."
+    ...                 " All of the behavior is the same, just the underlying pandas DataFrame order will be different")
+
+To use the :mod:`Command Reader <granola.command_readers>` :class:`~granola.command_readers.CannedQueries`,
+you must define ``"canned_queries"`` as a dictionary in your configuration.
 Which involves having a ``"data"`` dictionary with either file paths listed or serial commands directly defined.
 
 File Path Option
 ******************
 
-To define your serial commands with file paths
-
-.. code-block:: JSON
-
-    {
-        "canned_queries": {
-            "data": {
-                "`DEFAULT`": "data\\fixture\\fixture_serial_cmds.csv",
-                "sdicmd 1 \\?Xc!": "data\\sensor\\teros_12_get_reading_cmds.csv",
-                "sdicmd 1": "data\\sensor\\teros_12_serial_cmds.csv"
-            }
-        }
-    }
-
-Where each key is a regex of a serial command you want to get routed to that csv. Searches
-occur in order of top to bottom with the exception of ``"`DEFAULT`"``, which is
-the csv that any command that doesn't have a matching key will search in.
-
-Each file can also be defined either as a relative path to your configuration file (if you define your configuration
-directly in python, and don't include the a ``config_path`` input, it defaults to the current working directory
-for the relative base directory).
-
-Direct Serial Command Definitions
-************************************
-
-To define your serial commands directly, here is a basic example
-
-.. code-block:: JSON
-
-    {
-        "canned_queries": {
-            "data": {
-                "`DEFAULT`": {"get -temp\r": {"response": ["20\r>",
-                                                            "22\r>"]},
-                                "test -off\r": "OK\r>"}}
-        }
-    }
-
-You can also additionally pass extra fields to to the resulting DataFrame, as such
-
-.. code-block:: JSON
-
-    {
-        "canned_queries": {
-            "data": {
-                "`DEFAULT`": {"get -temp\r": {"response": ["20\r>",
-                                                            "22\r>"]},
-                                "test -off\r": "OK\r>"}},
-            "extra_col1": 2
-            "extra_col2": [1, 2, 3]
-        }
-    }
+>>> from granola import CannedQueries, Cereal
+
+To define your serial commands with file paths given a cereal_cmds.csv like this::
+
+    cmd,response
+    start\r, OK\r>
+    get -volt\r, 7800\r>
+    get -volt\r, 8000\r>
+    get -volt\r, 4000\r>
+    check -integrity\r, GOOD\r>
+    radio on\r, OK\r>
+    get name\r, Cereal Test Fixture\r
+    reset\r, OK\r>
+    get batt\r, 100\r>
+
+>>> command_readers = {"CannedQueries": {"data": ["cereal_cmds.csv"]}}
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal._readers_["CannedQueries"].serial_df
+                    cmd               response
+0             start\r                  OK\r>
+1         get -volt\r                7800\r>
+2         get -volt\r                8000\r>
+3         get -volt\r                4000\r>
+4  check -integrity\r                GOOD\r>
+5          radio on\r                  OK\r>
+6          get name\r  Cereal Test Fixture\r
+7             reset\r                  OK\r>
+8          get batt\r                 100\r>
+
+
+Each data file can also be defined either as an absolute path or as a relative path path.
+If you define your paths as a relative path, they are defined in relation to :class:`.Cereal`'s input parameter
+``data_path_root``. See :class:`.Cereal` for more details.
+
+Direct Serial Commands Option
+*****************************
 
 You can pass either a single value to be broadcasted to every value in a DataFrame,
 or make a list of values that must be the same length as the DataFrames it is matching
@@ -68,126 +59,88 @@ against.
 The specific formats you must follow can be seen with the :py:class:`~granola.command_readers.CannedQueries` Command Reader.
 This is a quick overview of all the options for inside the serial commands dictionary.
 
-
-.. doctest::
-    :pyversion: >= 3.6
-
-    >>> config = {
-    ...     "canned_queries": {
-    ...         "data": {
-    ...             "`DEFAULT`": {"cmd1\r": "some response\r>",
-    ...                         "cmd2\r": {"response": "some response\r"},
-    ...                         "cmd3\r": {"response": "some response\r>", "another_column": 1},
-    ...                         "cmd4\r": {"response": ["some response1\r>",
-    ...                                                 "some response2\r>"]},
-    ...                         "cmd5\r": {"response": ["some response1\r>",
-    ...                                                 "some response2\r>"],
-    ...                                                 "another_column": 1},
-    ...                         "cmd6\r": {"response": ["some response1\r>",
-    ...                                                 "some response2\r>"],
-    ...                                                 "another_column": [1,
-    ...                                                                     2]},
-    ...                         "cmd7\r": {"response": [["some response1\r>", {"another_column": 42}],
-    ...                                                 "some response2\r>"]},
-    ...                         "cmd8\r": {"response": [["some response1\r>", {"another_column": 42}],
-    ...                                                 "some response2\r>"],
-    ...                                                 "another_column": 1},
-    ...                         "cmd9\r": [["some response1\r>", {"another_column": 42}],
-    ...                                     "some response2\r>"]}
-    ...         }
-    ...     }
-    ... }
-    >>> canned_queries = granola.CannedQueries()
-    >>> canned_queries.initialize_config(config)
-    >>> canned_queries.serial_dfs
-    OrderedDict([('`DEFAULT`',        cmd           response  another_column
-    0   cmd1\r   some response\r>             NaN
-    1   cmd2\r    some response\r             NaN
-    2   cmd3\r   some response\r>             1.0
-    3   cmd4\r  some response1\r>             NaN
-    4   cmd4\r  some response2\r>             NaN
-    5   cmd5\r  some response1\r>             1.0
-    6   cmd5\r  some response2\r>             1.0
-    7   cmd6\r  some response1\r>             1.0
-    8   cmd6\r  some response2\r>             2.0
-    9   cmd7\r  some response1\r>            42.0
-    10  cmd7\r  some response2\r>             NaN
-    11  cmd8\r  some response1\r>            42.0
-    12  cmd8\r  some response2\r>             1.0
-    13  cmd9\r  some response1\r>            42.0
-    14  cmd9\r  some response2\r>             NaN)])
+>>> command_readers = {
+...     "CannedQueries": {
+...         "data": [
+...             {
+...                 "cmd1\r": "some response\r>",
+...                 "cmd2\r": {"response": "some response\r"},
+...                 "cmd3\r": {"response": "some response\r>", "another_column": 1},
+...                 "cmd4\r": {"response": ["some response1\r>", "some response2\r>"]},
+...                 "cmd5\r": {"response": ["some response1\r>", "some response2\r>"], "another_column": 1},
+...                 "cmd6\r": {"response": ["some response1\r>", "some response2\r>"], "another_column": [1, 2]},
+...                 "cmd7\r": {"response": [["some response1\r>", {"another_column": 42}], "some response2\r>"]},
+...                 "cmd8\r": {
+...                     "response": [["some response1\r>", {"another_column": 42}], "some response2\r>"],
+...                     "another_column": 1,
+...                 },
+...                 "cmd9\r": [["some response1\r>", {"another_column": 42}], "some response2\r>"],
+...             }
+...         ]
+...     },
+... }
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal._readers_["CannedQueries"].serial_df
+       cmd           response  another_column
+0   cmd1\r   some response\r>             NaN
+1   cmd2\r    some response\r             NaN
+2   cmd3\r   some response\r>             1.0
+3   cmd4\r  some response1\r>             NaN
+4   cmd4\r  some response2\r>             NaN
+5   cmd5\r  some response1\r>             1.0
+6   cmd5\r  some response2\r>             1.0
+7   cmd6\r  some response1\r>             1.0
+8   cmd6\r  some response2\r>             2.0
+9   cmd7\r  some response1\r>            42.0
+10  cmd7\r  some response2\r>             NaN
+11  cmd8\r  some response1\r>            42.0
+12  cmd8\r  some response2\r>             1.0
+13  cmd9\r  some response1\r>            42.0
+14  cmd9\r  some response2\r>             NaN
 
 This can be expressed either in the JSON configuration or directly in python. Let's step through those options.
 Generate a `SerialCmds` from a dictionary of serial commands. Here is the most basic form, where each command is mapped directly to a single response.
 
-.. doctest::
-    :pyversion: >= 3.6
-
-    >>> config = {
-    ...     "canned_queries": {
-    ...         "data": {
-    ...             "`DEFAULT`": {"test -off\r": "OK\r>",
-    ...                         "get -sn\r": "1234|r>"}
-    ...         }
-    ...     }
-    ... }
-    >>> canned_queries = granola.CannedQueries()
-    >>> canned_queries.initialize_config(config)
-    >>> canned_queries.serial_dfs
-    OrderedDict([('`DEFAULT`',            cmd response
-    0  test -off\r    OK\r>
-    1    get -sn\r  1234|r>)])
+>>> command_readers = {
+...     "CannedQueries": {"data": [{"test -off\r": "OK\r>", "get -sn\r": "1234|r>"}]
+... }}
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal._readers_["CannedQueries"].serial_df
+           cmd response
+0  test -off\r    OK\r>
+1    get -sn\r  1234|r>
 
 Here we can map multiple responses to ``get -temp\r``. They are ordered as well.
 Just as any normal python list is ordered (20\r comes before 22\r).
 
 Notice also that we also can map just a single response to ``test -off\r`` with this more verbose form
 
-.. doctest::
-    :pyversion: >= 3.6
-
-    >>> config = {
-    ...     "canned_queries": {
-    ...         "data": {
-    ...             "`DEFAULT`": {"get -temp\r": {"response": ["20\r>",
-    ...                                                     "22\r>"]},
-    ...                         "test -off\r": {"response": "OK\r>"}}
-    ...         }
-    ...     }
-    ... }
-    >>> canned_queries = granola.CannedQueries()
-    >>> canned_queries.initialize_config(config)
-    >>> canned_queries.serial_dfs
-    OrderedDict([('`DEFAULT`',            cmd response
-    0  get -temp\r    20\r>
-    1  get -temp\r    22\r>
-    2  test -off\r    OK\r>)])
+>>> command_readers = {
+...     "CannedQueries": {"data": [{"get -temp\r": {"response": ["20\r>", "22\r>"]},
+...                                 "test -off\r": {"response": "OK\r>"}}]}}
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal._readers_["CannedQueries"].serial_df
+           cmd response
+0  get -temp\r    20\r>
+1  get -temp\r    22\r>
+2  test -off\r    OK\r>
 
 Here we look at how to pass additional columns to our constructed DataFrame
 
-.. doctest::
-    :pyversion: >= 3.6
-
-    >>> config = {
-    ...     "canned_queries": {
-    ...         "data": {
-    ...             "`DEFAULT`": {"get -temp\r": {"response": ["20\r>",
-    ...                                                     "22\r>"]},
-    ...                         "test -volt\r": ["5000\r>",
-    ...                                         "6000\r>"]},
-    ...         },
-    ...         "delay": 2,
-    ...         "other_column": [1, 2, 3, 4]
-    ...     }
-    ... }
-    >>> canned_queries = granola.CannedQueries()
-    >>> canned_queries.initialize_config(config)
-    >>> canned_queries.serial_dfs
-    OrderedDict([('`DEFAULT`',             cmd response  delay  other_column
-    0   get -temp\r    20\r>      2             1
-    1   get -temp\r    22\r>      2             2
-    2  test -volt\r  5000\r>      2             3
-    3  test -volt\r  6000\r>      2             4)])
+>>> command_readers = {
+...     "CannedQueries": {
+...         "data": [{"get -temp\r": {"response": ["20\r>", "22\r>"]}, "test -volt\r": ["5000\r>", "6000\r>"]}],
+...         "delay": 2,
+...         "other_column": [1, 2, 3, 4]
+...     },
+... }
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal._readers_["CannedQueries"].serial_df.sort_values(by="cmd")
+            cmd response  delay  other_column
+0   get -temp\r    20\r>      2             1
+1   get -temp\r    22\r>      2             2
+2  test -volt\r  5000\r>      2             3
+3  test -volt\r  6000\r>      2             4
 
 This last example showcases that we can broadcast delay=2 to the whole DataFrame since if
 it is a single value, or we can supply as many values as serial rows (In the list for ``"other_column"``,
@@ -197,35 +150,31 @@ response, so it does not allow that.)
 We can also pass multiple response directly as a list, without having to embed it in a
 dictionary.
 
-Finally, we will look at 2 ways to specify extra fields on individual rows.
-
-.. doctest::
-    :pyversion: >= 3.6
-
-    >>> config = {
-    ...     "canned_queries": {
-    ...         "data": {
-    ...             "`DEFAULT`": {"get -temp\r": {"response": ["20\r>",
-    ...                                                     "22\r>"],
-    ...                                         "delay": [7,
-    ...                                                     6],},
-    ...                         "test -volt\r": {"response": ["5000\r>",
-    ...                                                         ["6000\r>", {"delay": 5}],
-    ...                                                         "5000\r>"],
-    ...                                         "delay": 4},
-    ...                         "test -off\r": {"response": "OK\r>", "delay": 3},
-    ...                         "get -sn\r": "1234|r>"},
-    ...         },
-    ...     }
-    ... }
-    >>> canned_queries = granola.CannedQueries()
-    >>> canned_queries.initialize_config(config)
-    >>> canned_queries.serial_dfs
-    OrderedDict([('`DEFAULT`',             cmd response  delay
-    0   get -temp\r    20\r>    7.0
-    1   get -temp\r    22\r>    6.0
-    2  test -volt\r  5000\r>    4.0
-    3  test -volt\r  6000\r>    5.0
-    4  test -volt\r  5000\r>    4.0
-    5   test -off\r    OK\r>    3.0
-    6     get -sn\r  1234|r>    NaN)])
\ No newline at end of file
+Finally, we will look at 2 ways to specify extra fields on individual rows. We also look how we can pass
+CannedQueries as the actual class instead of a string
+
+>>> command_readers = {
+...     CannedQueries: {
+...         "data": [
+...             {
+...                 "get -temp\r": {
+...                     "response": ["20\r>", "22\r>"],
+...                     "delay": [7, 6],
+...                 },
+...                 "test -volt\r": {"response": ["5000\r>", ["6000\r>", {"delay": 5}], "5000\r>"], "delay": 4},
+...                 "test -off\r": {"response": "OK\r>", "delay": 3},
+...                 "get -sn\r": "1234|r>",
+...             }
+...         ],
+...     },
+... }
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal._readers_["CannedQueries"].serial_df.sort_values(by="cmd")
+            cmd response  delay
+6     get -sn\r  1234|r>    NaN
+0   get -temp\r    20\r>    7.0
+1   get -temp\r    22\r>    6.0
+5   test -off\r    OK\r>    3.0
+2  test -volt\r  5000\r>    4.0
+3  test -volt\r  6000\r>    5.0
+4  test -volt\r  5000\r>    4.0
diff --git a/docs/config/config.rst b/docs/config/config.rst
index b385555..8cd80f9 100644
--- a/docs/config/config.rst
+++ b/docs/config/config.rst
@@ -3,7 +3,67 @@ Configuration Overview
 ########################
 
 .. toctree::
-    :caption: Configuration Sections
+    :maxdepth: 1
+    :caption: In-depth Guides
 
     Canned Queries <canned_queries>
-    Getters and Setters <getters_and_setters>
\ No newline at end of file
+    Getters and Setters <getters_and_setters>
+    Custom Command Readers and Hooks <cr_hooks_cfg>
+
+.. toctree::
+    :maxdepth: 2
+    :caption: Examples
+
+    ../examples/examples_notebooks
+
+**************************
+What is a Command Reader?
+**************************
+
+:mod:`Command Readers <granola.command_readers>` are the objects that handle the processing of individual serial
+commands. Each serial command that comes in is processed by each :mod:`Command Reader <granola.command_readers>`, and
+once a :mod:`Command Reader <granola.command_readers>` returns a valid response, the next
+:mod:`Command Readers <granola.command_readers>` will skip processing the command. This allows you to change the
+behavior of individual commands by defining new :mod:`Command Readers <granola.command_readers>`.
+
+Canned Queries
+==============
+
+The most basic :mod:`Command Reader <granola.command_readers>` is the :class:`~granola.command_readers.CannedQueries` Command Reader.
+This Command Reader has you define a series of commands and a responses, and then when a command comes in, it with return
+the next matching response for that command. It will iterate through the subset of matching commands (ex: all "get sn\r"),
+until it reaches the end. There are ways with :ref:`Hooks <What is a Hook?>` to define the behavior of what happens when it
+reaches the end.
+
+You can see a more in-depth tutorial on :class:`~granola.command_readers.CannedQueries` :ref:`here <Canned Queries Configuration>`.
+
+Getters And Setters
+===================
+
+Another useful :mod:`Command Reader <granola.command_readers>` is the :class:`~granola.command_readers.GettersAndSetters` Command Reader.
+This Command Reader manages the state of a number of attributes which you can access with getters and set with setters.
+It initializes some default values and stores those values in the attribute. These attributes can then be modified with
+setters, and then grabbed from the getters. Both getters and setters allow configuration with using :std:doc:`Jinja2 <jinja2:intro>` formatting.
+
+You can see a more in-depth tutorial on :class:`~granola.command_readers.GettersAndSetters` :ref:`here <Getters and Setters Configuration>`.
+
+
+
+****************
+What is a Hook?
+****************
+
+A :mod:`Hook <granola.hooks.hooks>` is class that is associated with a :mod:`Command Reader <granola.command_readers>` or set of Command Readers and runs extra code at predefined
+location. Before each :mod:`Command Readers's <granola.command_readers>` :meth:`~granola.command_readers.BaseCommandReaders.get_reading`
+method, a Hook can be ran, via :meth:`~granola.hook.base_hook.BaseHook.pre_reading`, processing the incoming data,
+saving information about it for later, modifying it, or any other useful actions. Alternatively, a Hook can run after
+:meth:`~granola.command_readers.BaseCommandReaders.get_reading`, via :meth:`~granola.hook.base_hook.BaseHook.post_reading`,
+altering the resulting response, using information from prior state, or other more sophisticated techniques.
+
+For information on creating your own hooks, see :mod:`~granola.hooks.base_hook`
+
+.. todo::
+    Add more in depth guide to Hooks
+
+.. todo::
+    Add the ability for Hooks to be ran directly on Breakfast Cereal
diff --git a/docs/config/cr_hooks_cfg.rst b/docs/config/cr_hooks_cfg.rst
new file mode 100644
index 0000000..30b0b66
--- /dev/null
+++ b/docs/config/cr_hooks_cfg.rst
@@ -0,0 +1,73 @@
+=========================================
+Command Readers and Hooks Configuration
+=========================================
+
+
+***********************
+Custom Command Readers
+***********************
+
+To pass custom :mod:`Command Readers <granola.command_readers>`, as well as to change the
+order they are processed in, you can define ``"command_readers"`` in your configuration.
+
+That would look like this
+
+
+>>> from granola import BaseCommandReaders, Cereal, register_hook, CannedQueries
+
+>>> class CommandReader(BaseCommandReaders):
+...     def get_reading(self, data):
+...         return "Command Reader"
+
+>>> command_readers = [CommandReader]
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal.write(b"\r")
+1
+>>> cereal.read(20)
+b'Command Reader'
+
+You can either specify the CommandReader directly, or the name of it as a string. If you specify
+it as a string, then the CommandReader needs to be imported or defined before you initialize :class:`~granola.breakfast_cereal.Cereal`
+
+****************
+Custom Hooks
+****************
+
+To pass custom :mod:`~granola.hooks.hooks`, as well as to change the
+order they are processed in, you can define ``"hooks"`` in your configuration.
+
+That would look like this
+
+
+>>> @register_hook(hook_type_enum="post_reading", hooked_classes=[CannedQueries])
+... def hook(self, hooked, result, data, **kwargs):
+...     return "1"
+
+>>> command_readers = {"CannedQueries": {"data": [{"cmd\r": "response"}]}}
+>>> hooks = [hook]
+>>> cereal = Cereal(command_readers=command_readers, hooks=hooks)
+>>> cereal.write(b"cmd\r")
+4
+>>> cereal.read(cereal.in_waiting)
+b'1'
+
+You can either specify the Hook directly, or the name of it as a string. If you specify
+it as a string, then the Hook needs to be imported or defined before you initialize :class:`~granola.breakfast_cereal.Cereal`
+
+You can also pass addition arguments to a hook like this.
+
+>>> command_readers = {"CannedQueries": {"data": [{"cmd\r": ["1", "2"]}]}}
+>>> hooks = {"StickCannedQueries": {"attributes": ["cmd\r"], "include_or_exclude": "include"}}
+>>> cereal = Cereal(command_readers=command_readers, hooks=hooks)
+>>> cereal.write(b"cmd\r")
+4
+>>> cereal.read(cereal.in_waiting)
+b'1'
+>>> cereal.write(b"cmd\r")
+4
+>>> cereal.read(cereal.in_waiting)
+b'2'
+>>> cereal.write(b"cmd\r")
+4
+>>> cereal.read(cereal.in_waiting)
+b'2'
diff --git a/docs/config/getters_and_setters.rst b/docs/config/getters_and_setters.rst
index adbad74..3db44eb 100644
--- a/docs/config/getters_and_setters.rst
+++ b/docs/config/getters_and_setters.rst
@@ -2,9 +2,6 @@
 Getters and Setters Configuration
 =================================
 
-.. todo::
-    Allow different eol characters
-
 To use the :mod:`Command Reader <granola.command_readers>` :class:`~granola.command_readers.GettersAndSetters`, you must define
 ``"getters_and_setters"`` as a dictionary in your configuration.
 
@@ -27,7 +24,7 @@ getter before you call a setter.
 
 
 The getter serial commands and their responses. The getter commands are specified as a string with your eol characters,
-and the response is a string with :doc:`Jinja2 <jinja2:usage/extensions/intersphinx>` variable formatting.
+and the response is a string with :std:doc:`Jinja2 <jinja2:intro>` variable formatting.
 
 This allows you to do basic equations inside the response, such as combining multiple attributes,
 doing unit conversion, and other things.
@@ -36,7 +33,7 @@ doing unit conversion, and other things.
 ------------------
 
 The setter serial commands and their responses. The setter commands change the stored attributes you initially set up
-inside ``"default_values"``. Both the commands and responses uses :doc:`Jinja2 <jinja2:usage/extensions/intersphinx>`
+inside ``"default_values"``. Both the commands and responses uses :std:doc:`Jinja2 <jinja2:intro>`
 variable formatting.
 
 .. caution::
@@ -46,47 +43,42 @@ variable formatting.
 Example
 ------------------
 
-.. doctest::
-    :pyversion: >= 3.6
-
-    >>> config = {
-    ...     "getters_and_setters": {"default_values": {"sn": "42",
-    ...                                                "temp": "20.0",
-    ...                                                "conversion_factor": "7.0"},
-    ...                             "getters": [{"cmd": "get -sn\r",
-    ...                                          "response": "{{ sn }}\r>"},
-    ...                                         {"cmd": "get -temp\r",
-    ...                                          "response": "{{ temp }}\r>"},
-    ...                                         {"cmd": "get -tempf\r",
-    ...                                          "response": "{{ temp|float * (9/5) + 32 }}\r>"},
-    ...                                         {"cmd": "get temp_convt\r",
-    ...                                          "response": "{{ (temp|float + conversion_factor|float) / 2 }}"}],
-    ...                             "setters": [{"cmd": "set -sn {{sn}}\r",
-    ...                                          "response": "OK\r>"},
-    ...                                         {"cmd": "set sn and temp {{ sn }} {{ temp }}\r",
-    ...                                          "response": "serial number {{ sn }} temp: {{temp}}"},
-    ...                                         {"cmd": "set -temp {{ temp }}\r",
-    ...                                          "response": "{{ temp }}"},
-    ...                                         {"cmd": "set convt {{ conversion_factor }}\r",
-    ...                                          "response": "Conversion Factor {{ conversion_factor }}"}]}
-    ... }
-    >>> cereal = granola.Cereal(config)
-    >>> cereal.write(b"get -sn\r")
-    8
-    >>> cereal.read(10)
-    b'42\r>'
-    >>> cereal.write(b"set -sn granola42\r")
-    18
-    >>> cereal.read(10)
-    b'OK\r>'
-    >>> cereal.write(b"get -sn\r")
-    8
-    >>> cereal.read(10)
-    b'granola42\r'
-    >>> cereal.write(b"get -tempf\r")
-    11
-    >>> cereal.read(10)
-    b'68.0\r>'
+>>> from granola import Cereal
+
+>>> command_readers = {
+...     "GettersAndSetters": {
+...         "default_values": {"sn": "42", "temp": "20.0", "conversion_factor": "7.0"},
+...         "getters": [
+...             {"cmd": "get -sn\r", "response": "{{ sn }}\r>"},
+...             {"cmd": "get -temp\r", "response": "{{ temp }}\r>"},
+...             {"cmd": "get -tempf\r", "response": "{{ temp|float * (9/5) + 32 }}\r>"},
+...             {"cmd": "get temp_convt\r", "response": "{{ (temp|float + conversion_factor|float) / 2 }}"},
+...         ],
+...         "setters": [
+...             {"cmd": "set -sn {{sn}}\r", "response": "OK\r>"},
+...             {"cmd": "set sn and temp {{ sn }} {{ temp }}\r", "response": "serial number {{ sn }} temp: {{temp}}"},
+...             {"cmd": "set -temp {{ temp }}\r", "response": "{{ temp }}"},
+...             {"cmd": "set convt {{ conversion_factor }}\r", "response": "Conversion Factor {{ conversion_factor }}"},
+...         ],
+...     }
+... }
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal.write(b"get -sn\r")
+8
+>>> cereal.read(cereal.in_waiting)
+b'42\r>'
+>>> cereal.write(b"set -sn granola42\r")
+18
+>>> cereal.read(cereal.in_waiting)
+b'OK\r>'
+>>> cereal.write(b"get -sn\r")
+8
+>>> cereal.read(cereal.in_waiting)
+b'granola42\r>'
+>>> cereal.write(b"get -tempf\r")
+11
+>>> cereal.read(cereal.in_waiting)
+b'68.0\r>'
 
 Customizing Jinja
 ------------------
@@ -94,26 +86,22 @@ Customizing Jinja
 If the default jinja2 templating characters are incompatible with your serial commands, you can configure those
 in your configuration dictionary as so.
 
-.. doctest::
-    :pyversion: >= 3.6
-
-    >>> config = {
-    ...     "getters_and_setters": {"default_values": {"sn": "42"},
-    ...                             "getters": [{"cmd": "get -sn\r",
-    ...                                          "response": "`sn`\r>"}],
-    ...                             "setters": [{"cmd": "set -sn `sn`\r",
-    ...                                          "response": "OK\r>"}],
-    ...                             "variable_start_string": "`",
-    ...                             "variable_end_string": "`"
-    ...                             }
-    ... }
-    >>>
-    >>> cereal = granola.Cereal(config)
-    >>> cereal.write(b"set -sn granola42\r")
-    18
-    >>> cereal.read(10)
-    b'OK\r>'
-    >>> cereal.write(b"get -sn\r")
-    8
-    >>> cereal.read(10)
-    b'granola42\r'
\ No newline at end of file
+>>> command_readeres = {
+...     "GettersAndSetters": {
+...         "default_values": {"sn": "42"},
+...         "getters": [{"cmd": "get -sn\r", "response": "`sn`\r>"}],
+...         "setters": [{"cmd": "set -sn `sn`\r", "response": "OK\r>"}],
+...         "variable_start_string": "`",
+...         "variable_end_string": "`",
+...     }
+... }
+>>>
+>>> cereal = Cereal(command_readers=command_readers)
+>>> cereal.write(b"set -sn granola42\r")
+18
+>>> cereal.read(cereal.in_waiting)
+b'OK\r>'
+>>> cereal.write(b"get -sn\r")
+8
+>>> cereal.read(cereal.in_waiting)
+b'granola42\r>'
\ No newline at end of file
diff --git a/docs/examples/basic_usage.ipynb b/docs/examples/basic_usage.ipynb
new file mode 100644
index 0000000..ede36da
--- /dev/null
+++ b/docs/examples/basic_usage.ipynb
@@ -0,0 +1,284 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Basic Overview of Mock Cereal and API\n",
+    "\n",
+    "This tutorial will go over the basics of initializing and using the GRANOLA mock Cereal class (also known as Breakfast Cereal)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from granola.utils import check_min_package_version"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def read_all(cereal):\n",
+    "    \"\"\"convenience function since pyserial pre 3.0 doesn't have read_all\"\"\"\n",
+    "    if check_min_package_version(\"pyserial\", \"3.0\"):\n",
+    "        print(repr(cereal.read_all()))\n",
+    "    else:\n",
+    "        print(repr(cereal.read(cereal.in_waiting)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 1. Overview of Breakfast Cereal Configuration\n",
+    "\n",
+    "Mock Cereal takes a number Command Readers, Hooks (optional), and additional tweaking parameters."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### a) Command Readers\n",
+    "\n",
+    "[Command Readers](https://granola.readthedocs.io/en/latest/command_readers.html) are the backbone of mock Cereal. Each [Command Reader](https://granola.readthedocs.io/en/latest/command_readers.html) receives a serial command and then returns a response. Any given [Command Reader](https://granola.readthedocs.io/en/latest/command_readers.html) is free to process that command in any way that is so chooses. \n",
+    "\n",
+    "[Canned Queries](https://granola.readthedocs.io/en/latest/config/canned_queries.html) is a type of [Command Reader](https://granola.readthedocs.io/en/latest/command_readers.html) that takes a command and loops through a list of responses for that command. It can loop through those responses in order or randomly. [Canned Queries](https://granola.readthedocs.io/en/latest/config/canned_queries.html) also by default loop back to the beginning of their list of responses when they reach the end.\n",
+    "\n",
+    "[Getters and Setters](https://granola.readthedocs.io/en/latest/config/getters_and_setters.html) is a type of [Command Reader](https://granola.readthedocs.io/en/latest/command_readers.html) that manages the state of a number of attributes which you can access with getters and set with setters."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### b) Hooks\n",
+    "\n",
+    "Hooks allow the modification of the processing of all or a subset of serial commands. Some examples are:\n",
+    "\n",
+    "- `LoopCannedQueries` is a Hook that will loop [Canned Queries](https://granola.readthedocs.io/en/latest/config/canned_queries.html) responses when it has exhausted all    responses. This hook is enabled by default on all [Canned Queries](https://granola.readthedocs.io/en/latest/config/canned_queries.html). \n",
+    "\n",
+    "- `StickCannedQueries` is a Hook that will take specified [Canned Queries](https://granola.readthedocs.io/en/latest/config/canned_queries.html) commands, and once the command has exhausted all of its responses, all future queries of that command will only return the last response. A practical example of this might be a serial command of some measurement that is approaching a value, such as a temperature reading.\n",
+    "\n",
+    "- `ApproachHook` is a Hook that works with [Getters and Setters](https://granola.readthedocs.io/en/latest/config/getters_and_setters.html) commands, and allows you to set a new value for a command, but instead of the value being instantly set, it is set over time, allowing you slowly approach that value. This can be practical for a simulating a device that is controlling the temperature of some chamber, but when you set it, it takes a while to warm up."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### c) Other Parameters\n",
+    "\n",
+    "To see a full list of configuration parameters, see [Breakfast Cereal](https://granola.readthedocs.io/en/latest/bk_cereal.html)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 2. Initializing Breakfast Cereal"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from granola import Cereal\n",
+    "\n",
+    "command_readers = {\n",
+    "    # We will first set up queries that will just execute as written, allowing multiple responses per command.\n",
+    "    # By default when a particular command reaches the end of its responses, it will loop back to the beginning,\n",
+    "    # but this behavior can be changed on individual queries.\n",
+    "    \"CannedQueries\": {\n",
+    "        \"data\": [\n",
+    "            {\n",
+    "                \"1\\r\": \"1\",  # a command that only has a single response `1` defined\n",
+    "                \"2\\r\": {\"response\": \"2\"},  # equivalent to the above\n",
+    "                \"3\\r\": {\"response\": [\"3a\", \"3b\"]},  # a command that has 2 responses defined, `3a` and `3b`\n",
+    "                \"4\\r\": [\"4a\", \"4b\"],  # equivalent to the above\n",
+    "            }\n",
+    "        ],\n",
+    "    },\n",
+    "    # We will also define a getter and setter style query. This is a query that you can retrieve a value by\n",
+    "    # calling the getter, but you can also set a new value by calling the setter.\n",
+    "    \"GettersAndSetters\": {\n",
+    "        \"default_values\": {\"sn\": \"42\"},  # We first initialize a default\n",
+    "        \"getters\": [{\"cmd\": \"get sn\\r\", \"response\": \"{{ sn }}\\r>\"}],  # we define default\n",
+    "        \"setters\": [{\"cmd\": \"set sn {{ sn }}\\r\", \"response\": \"OK\\r>\"}],  # and we define a setter\n",
+    "    },\n",
+    "}\n",
+    "cereal = Cereal(command_readers)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 3. Using Breakfast Cereal"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Canned Queries will first find the command you entered, and the iterate through the responses defined for that command until they reach the end for that command. By default, they then loop back to the beginning of that sequence"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "b'3a'\n",
+      "b'3b'\n",
+      "b'3a'\n"
+     ]
+    }
+   ],
+   "source": [
+    "cereal.write(b\"3\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"3\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"3\\r\")\n",
+    "read_all(cereal)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Getters And Setters use jinja2 formatting to specify the location of where the setter attribute is in the setter command, and where it is in the getter response."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "b'42\\r>'\n",
+      "b'OK\\r>'\n",
+      "b'something something\\r>'\n"
+     ]
+    }
+   ],
+   "source": [
+    "cereal.write(b\"get sn\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"set sn something something\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"get sn\\r\")\n",
+    "read_all(cereal)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "If we pass in an undefined command, then we get back an unsupported response."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "b'Unsupported\\r>'\n"
+     ]
+    }
+   ],
+   "source": [
+    "cereal.write(b\"fake command\\r\")\n",
+    "read_all(cereal)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We can configure what our unsupported response is in our initial initialization"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "cereal = Cereal(unsupported_response=\"ERROR!!!\")\n",
+    "cereal.write(b\"fake command\\r\")\n",
+    "read_all(cereal)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Because in this case, we didn't define any Command Readers, every command will be unsupported"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "b'ERROR!!!'\n"
+     ]
+    }
+   ],
+   "source": [
+    "cereal.write(b\"get sn\\r\")\n",
+    "read_all(cereal)"
+   ]
+  }
+ ],
+ "metadata": {
+  "interpreter": {
+   "hash": "5565e1745fb70237b2948a0c9c6f2fd8438f6300b16a2c63269685c5165d3440"
+  },
+  "kernelspec": {
+   "display_name": "Python 3.8.10 ('venv38': venv)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  },
+  "orig_nbformat": 4
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docs/examples/examples_notebooks.rst b/docs/examples/examples_notebooks.rst
new file mode 100644
index 0000000..9bf32e3
--- /dev/null
+++ b/docs/examples/examples_notebooks.rst
@@ -0,0 +1,15 @@
+=========
+Examples
+=========
+
+Here are some example Jupyter Notebooks that showcase Breakfast Cereal and some of its configuration options in
+practice.
+
+.. toctree::
+    :maxdepth: 1
+    :glob:
+
+    *
+
+.. todo::
+    add more example notebooks
\ No newline at end of file
diff --git a/docs/examples/getters_and_setters_jinja2.ipynb b/docs/examples/getters_and_setters_jinja2.ipynb
new file mode 100644
index 0000000..79bca6c
--- /dev/null
+++ b/docs/examples/getters_and_setters_jinja2.ipynb
@@ -0,0 +1,295 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Advanced Getters and Setters"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 45,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from granola.utils import check_min_package_version"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Convenience function for pre pyserial 3 and current pyserial compatibility"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 43,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def read_all(cereal):\n",
+    "    \"\"\"convenience function since pyserial pre 3.0 doesn't have read_all\"\"\"\n",
+    "    if check_min_package_version(\"pyserial\", \"3.0\"):\n",
+    "        print(repr(cereal.read_all()))\n",
+    "    else:\n",
+    "        print(repr(cereal.read(cereal.in_waiting)))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 1. Getters and Setters Configuration Overview\n",
+    "\n",
+    "A Getters and Setters configuration set up by passing into `command_readers` a nested dictionary of Getters and Setters configuration options. This nested dictionaries key should either be the string \"GettersAndSetters\" or the class `GettersAndSetters`.\n",
+    "\n",
+    "They are configured with a number of attributes with their states being saved for you to set or retrieve from. There attributes must first be defined with `\"default_values\"`. This would look like this in your nested dictionary: \n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "`\"default_values\": {\"sn\": \"42\", \"temp\": \"20.0\"}`"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "\n",
+    "\n",
+    "You can call the attributes whatever you want. They don't have to be related to the commands they are called in. It is just for nice bookkeeping.\n",
+    "\n",
+    "You can then define getters or setters to modify those attributes. Getters and setters are both defined with a `\"cmd\"` and `\"response\"` key. The getter key needs to be a static string, but the response can include [jinja2](https://jinja.palletsprojects.com) formatting, see [2. Adding Advanced Jinja2 Formatting](#jinja2_formatting)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 46,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from granola import GettersAndSetters\n",
+    "\n",
+    "command_readers = {\n",
+    "    \"GettersAndSetters\": {\n",
+    "        \"default_values\": {\"sn\": \"42\"},\n",
+    "        \"getters\": [{\"cmd\": \"get -sn\\r\", \"response\": \"{{ sn }}\\r>\"}],\n",
+    "        \"setters\": [{\"cmd\": \"set -sn {{ sn }}\\r\", \"response\": \"OK\\r>\"}],\n",
+    "    }\n",
+    "}\n",
+    "command_readers = {\n",
+    "    GettersAndSetters: {\n",
+    "        \"default_values\": {\"sn\": \"42\"},\n",
+    "        \"getters\": [{\"cmd\": \"get -sn\\r\", \"response\": \"{{ sn }}\\r>\"}],\n",
+    "        \"setters\": [{\"cmd\": \"set -sn {{ sn }}\\r\", \"response\": \"OK\\r>\"}],\n",
+    "    }\n",
+    "}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "<a id='jinja2_formatting'></a>\n",
+    "## 2. Adding Advanced Jinja2 Formatting\n",
+    "\n",
+    "For full configurability with getters and setters, we use [jinja2](https://jinja.palletsprojects.com) for our formatting. This allows you to not only specify which attribute to return or set, but also to include equations in your responses (setter commands that use jinja2 formatting currently are just to specify what attribute to use and can't use anything like an equation. Equations are limited to getter and setter responses)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 54,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from granola import Cereal\n",
+    "\n",
+    "command_readers = {\n",
+    "    \"GettersAndSetters\": {\n",
+    "        \"default_values\": {\"devtype\": \"Cereal\", \"sn\": \"42\", \"fw_ver\": \"0.0.0\", \"temp\": \"20.0\", \"volts\": \"2000.0\"},\n",
+    "        \"getters\": [\n",
+    "            {\"cmd\": \"get sn\\r\", \"response\": \"sn: {{ sn }}\\r>\"},\n",
+    "            {\"cmd\": \"get ver\\r\", \"response\": \"{{ fw_ver }}\\r>\"},\n",
+    "            {\"cmd\": \"show\\r\", \"response\": \"{{ devtype }} - {{ fw_ver }} - {{ sn }}\\r>\"},\n",
+    "            {\"cmd\": \"get temp\\r\", \"response\": \"{{ temp }}\\r>\"},\n",
+    "            # jinja attributes are substituted as strings, so to do math operations on them, we convert them first\n",
+    "            {\"cmd\": \"get tempf\\r\", \"response\": \"{{ temp|float * (9/5) + 32 }}\\r>\"},\n",
+    "            {\"cmd\": \"get volts\\r\", \"response\": \"{{ volts }}\"},\n",
+    "            #\n",
+    "            {\"cmd\": \"get volt_temp\\r\", \"response\": \"{{ (temp|float + volts|float) / 2 }}\"},\n",
+    "        ],\n",
+    "        \"setters\": [\n",
+    "            {\"cmd\": \"set sn {{sn}}\\r\", \"response\": \"OK\\r>\"},\n",
+    "            {\"cmd\": \"set both {{ sn }} {{ fw_ver }}\\r\", \"response\": \"OK\\r>\"},\n",
+    "            {\"cmd\": \"set temp {{ temp }}\\r\", \"response\": \"temp: {{ temp }}\"},\n",
+    "            {\"cmd\": \"set volts {{ volts }}\\r\", \"response\": \"Volt Calculation {{ volts|float / 2 }}\"},\n",
+    "        ],\n",
+    "    }\n",
+    "}\n",
+    "cereal = Cereal(command_readers)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Below we can see an example of two queries that return the `temp` attribute. The first ust returns it directly, while the second converts the string to a float, multiplies it by 9/5 and adds 32, converting it from celsius to fahrenheit."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 52,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "b'temp: 20.0\\r>'\n",
+      "b'68.0\\r>'\n"
+     ]
+    }
+   ],
+   "source": [
+    "cereal.write(b\"get temp\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"get tempf\\r\")\n",
+    "read_all(cereal)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "If we set the temperature, we get back a response of the `temp: ` followed by the temperature we set. \n",
+    "\n",
+    "And if we issue the two commands we did above, they work with the new temperature"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 55,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "b'temp: 100'\n",
+      "b'100\\r>'\n",
+      "b'212.0\\r>'\n"
+     ]
+    }
+   ],
+   "source": [
+    "cereal.write(b\"set temp 100\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"get temp\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"get tempf\\r\")\n",
+    "read_all(cereal)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Our getters and setters can also do calculations involving multiple attributes. `get volt_temp` does calculations on both `volts` and `temp`\n",
+    "\n",
+    "We also se more examples of using calculations with `set volts` which returns a `\"Volt Calculation\"` which is just half of what you put in."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 56,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "b'2000.0'\n",
+      "b'1050.0'\n",
+      "b'Volt Calculation 50.0'\n",
+      "b'100'\n",
+      "b'100.0'\n"
+     ]
+    }
+   ],
+   "source": [
+    "cereal.write(b\"get volts\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"get volt_temp\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"set volts 100\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"get volts\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"get volt_temp\\r\")\n",
+    "read_all(cereal)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Commands can also get multiple attributes and set multiple attributes at once, as in the example below.\n",
+    "\n",
+    "Also notice above in the config that devtype only has a getter associated with it, and no setters. You are allowed to define read only properties."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 57,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "b'Cereal - 0.0.0 - 42\\r>'\n",
+      "b'OK\\r>'\n",
+      "b'Cereal - 1.2.3 - abc\\r>'\n"
+     ]
+    }
+   ],
+   "source": [
+    "cereal.write(b\"show\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"set both abc 1.2.3\\r\")\n",
+    "read_all(cereal)\n",
+    "cereal.write(b\"show\\r\")\n",
+    "read_all(cereal)"
+   ]
+  }
+ ],
+ "metadata": {
+  "interpreter": {
+   "hash": "5565e1745fb70237b2948a0c9c6f2fd8438f6300b16a2c63269685c5165d3440"
+  },
+  "kernelspec": {
+   "display_name": "Python 3.8.10 ('venv38': venv)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  },
+  "orig_nbformat": 4
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docs/index.rst b/docs/index.rst
index 53aadd6..72661b9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -2,8 +2,8 @@
 GRANOLA
 #######
 
-GRANOLA (Generating Real-time Autonomous N-device Output without Linked Apparatuses) is a package aiming to mock pyserial's Serial
-class with the goal of enabling automated testing, faster QA, and faster delivery. The core of this package is the class
+GRANOLA (Generating Real-time Autonomous N-device Output without Linked Apparatuses) is a package aiming to mock :std:doc:`Pyserial's <pyserial:index>`
+:class:`Serial <pyserial:serial.Serial>` class with the goal of enabling automated testing, faster QA, and faster delivery. The core of this package is the class
 Cereal. Cereal allows you to define complicated command and response sequences, getters and setters for commands that set attributes
 on your device (such as the serial number), a hook system to inject your own needed functionality, and a Serial Sniffer to capture serial
 command output from a real device as the basis of mocking later.
@@ -12,26 +12,18 @@ command output from a real device as the basis of mocking later.
 A Simple Example
 ****************
 
-.. doctest::
+>>> from granola import Cereal
+>>> command_readers = {"CannedQueries": {"data": [{"1\r": "1", "2\r": ["2a", "2b"]}]}}
+>>> mock_cereal = Cereal(command_readers)
 
-    >>> from granola import Cereal
-
-    >>> config = {
-    ...     "canned_queries": {
-    ...         "data": {
-    ...             "`DEFAULT`": {"1\r": "1",
-    ...                           "2\r": ["2a", "2b"]}
-    ...         }}}
-    ... mock_cereal = Cereal(config)
-
-    >>> mock_cereal.write(b"2\r")
-    2
-    >>> mock_cereal.read(10)
-    b'2a'
-    >>> mock_cereal.write(b"1\r")
-    2
-    >>> mock_cereal.read(10)
-    b'1'
+>>> mock_cereal.write(b"2\r")
+2
+>>> mock_cereal.read(10)
+b'2a'
+>>> mock_cereal.write(b"1\r")
+2
+>>> mock_cereal.read(10)
+b'1'
 
 
 .. warning::
@@ -45,6 +37,7 @@ A Simple Example
     :name: mastertoc
     :titlesonly:
 
-    Configuration <config/config>
+    config/config
     API Overview <api>
+    examples/examples_notebooks
     logging
diff --git a/granola/__init__.py b/granola/__init__.py
index 71bd03b..8e5de78 100644
--- a/granola/__init__.py
+++ b/granola/__init__.py
@@ -1,8 +1,8 @@
 from granola._version import get_versions
 from granola.breakfast_cereal import Cereal, PortNotOpenError
 from granola.command_readers import (
+    BaseCommandReaders,
     CannedQueries,
-    DefaultDF,
     GettersAndSetters,
     RandomizeResponse,
     SerialCmds,
@@ -15,6 +15,7 @@
     StickCannedQueries,
     register_hook,
 )
+from granola.main import MockSerial  # deprecated
 from granola.serial_sniffer import SerialSniffer
 
 __version__ = get_versions()["version"]
@@ -24,9 +25,10 @@
 __all__ = [
     "__version__",
     "RandomizeResponse",
+    "BaseCommandReaders",
     "SerialCmds",
     "Cereal",
-    "DefaultDF",
+    "MockSerial",  # deprecated
     "PortNotOpenError",
     "GettersAndSetters",
     "CannedQueries",
diff --git a/granola/breakfast_cereal.py b/granola/breakfast_cereal.py
index 7f62a5a..b08ebb1 100644
--- a/granola/breakfast_cereal.py
+++ b/granola/breakfast_cereal.py
@@ -12,7 +12,6 @@
     _run_post_reading_hooks,
     _run_pre_reading_hooks,
 )
-from granola.hooks.hooks import LoopCannedQueries
 from granola.utils import (
     IS_PYTHON3,
     SENTINEL,
@@ -20,7 +19,6 @@
     add_created_at,
     check_min_package_version,
     decode_bytes,
-    deprecation,
     deunicodify_hook,
     encode_to_bytes,
     fixpath,
@@ -39,73 +37,168 @@
 
 class Cereal(Serial):
     r"""
-    Mock pyserial Serial class that takes CSVs of serial commands to use for mocking.
-    Serial commands are routed to various CSVs based on which command is passed in.
-    A serial command is checked against a dictionary of ``SerialCmds``s for if that serial command
-    is a regex match to the keys in that dictionary. If it is, then it uses that CSV, if it doesn't,
-    it uses the a default serial command file under the key ```DEFAULT```.
-
-    Mock serials initialization follows a 2 step process. The first is with the normal `__init__`
+    Mock :std:doc:`Pyserial's <pyserial:index>` :class:`Serial <pyserial:serial.Serial>` class
+    that is capable of mocking serial commands a responses to for both simple and sophisticated
+    neeeds. Breakfast Cereal allows you to defined simple
+    :class:`~granola.command_readersCannedQueries` that are are predefined
+    serial commands and responses for various commands. As well as more complicated
+    :class:`~granola.command_readers.GettersAndSetters` which allow on the fly changing
+    the state of stored attributes. It also allows customizing the behavior of how
+    individual or any set of commands are processed through :mod:`~granola.hooks.hooks`.
+
+    Mock serials initialization follows a 2 step process. The first is with the normal ``__init__``
     method where you pass arguments to Cereal, you can do this before your project is going,
     as part of the setup. At this point, Cereal will be able to mock but it won't have many
     pyserial Serial attributes defined such as port, baudrate. Cereal is
     then able to be "initialized" a second time, following the signature of pyserial Serial class.
-    This allows you to setup Cereal with its configuration before and, and then not have to
-    change your code that initializes Pyserial much or at all. Ways to get Cereal in place of
-    Pyserial are depencency injection or monkey patching.
+    This allows you to setup Cereal with its configuration before and then not have to
+    change your code that initializes Pyserial much or at all through means of depencency injection
+    or patching.
 
     The CSVs must have the columns cmd and response in them, it can have other columns as well.
 
     Args:
-        config_key = (str) Which instrument in the config json to pull out for this particular mock
-            serial.
-
-        config_path: (str) Path to configuration json file. You can define multiple instruments in
-            this json file. Which instrument is used for this mock serial will be the one that
-            is passed in config_key.
-
-            If you don't specify a config_path, it will default to config.json in the root directory
-            of your python path.
-
-            The key "canned_queries" (optional) is a dictionary of csv files of serial
-            cmd and responses. inside "canned_queries", and then put
-            another dictionary with all the files where the keys are regexes to map incoming
-            serial commands to the CSVs and the values are paths to those CSVs. The CSV path with
-            the key "`DEFAULT`" will be the default csv looked in if a serial command comes in
-            and doesn't match any regex.
-
-            The key "unsupported_response" (optional, defaults to Unsupported\r>) defines the
-            response when passed into the mock serial device that is not defined.
-
-            The key "encoding" (optional, defaults to ascii), defines what encoding and decoding
-            to use.
-
-            The key "getters_and_setters" (optional) defines the getters and setters. See `GettersAndSetters`
-            for more information on formatting for those keys. Also examples are given in the
-            config.json
+        command_readers (dict[BaseCommandReaders|str] | list[BaseCommandReaders|str], optional):
+            Dictionary or list of :mod:`Command Readers <granola.command_readers>`. Command Readers
+            can be passed in as a string representation, or you may pass in the class itself.
+            Passing in a dictionary allows specifying your initialization options as well.
+            Using a list allows you to pass in already initialized Command Reader, or a not
+            initialized Command Reader that will be initialized to default values.
+            You can pass in custom Command Readers as well. See
+            :ref:`Command Readers and Hooks Configuration`.
+            defaults to ``(GettersAndSetters(), CannedQueries())``
+
+        hooks (dict[BaseHook|str] | list[BaseHook|str], optional):
+            Dictionary or list of :mod:`Hooks <granola.hooks.hook>`. Hooks
+            can be passed in as a string representation, or you may pass in the class itself.
+            Passing in a dictionary allows specifying your initialization options as well.
+            Using a list allows you to pass in already initialized Hook, or a not
+            initialized Hook that will be initialized to default values.
+            You can pass in custom Hooks as well. See
+            :ref:`Command Readers and Hooks Configuration`.
+            defaults to ``[]``
+
+        data_path_root (str | Path, optional): Path to the where all data file paths (for
+            Command Readers or Custom Hooks) will be referenced from.
+
+            .. todo::
+
+                probably just make this a thing inside the needed command readers
+
+        unsupported_response(str, optional):
+            The response returned for any command that does not have a defined response.
+            Defaults to "Unsupported\r>"
+
+        encoding(str, optional): The encoding scheme used to encode the serial commands and responses
+            Defaults to "ascii"
+
+    See Also
+    --------
+    :meth:`.mock_from_json` : Constructor from external configuration file
+
+    :meth:`.BaseCommandReaders.get_reading` : Base Command Reader that defines the interface for other Command Readers
+
+    :ref:`Configuration Overview` : Configuration Instructions and
+
+    :ref:`Basic Overview of Mock Cereal and API` : Intro Tutorial
+
+    Examples
+    --------
+    Initialize Breakfast Cereal with CannedQueries and GettersAndSetters Command Readers
+
+    >>> command_readers = {
+    ...     "CannedQueries": {"data": [{"1\r": "1", "2\r": ["2a", "2b"]}]},
+    ...     "GettersAndSetters": {
+    ...         "default_values": {"sn": "42"},  # We first initialize a default
+    ...         "getters": [{"cmd": "get sn\r", "response": "{{ sn }}\r>"}],  # we define default
+    ...         "setters": [{"cmd": "set sn {{ sn }}\r", "response": "OK\r>"}],  # and we define a setter
+    ...     },
+    ... }
+    >>> cereal = Cereal(command_readers=command_readers)
+
+    CannedQueries returns the correct response and by default it loops when it reaches the end of the defined responses
+
+    >>> cereal.write(b"2\r")
+    2
+    >>> cereal.read(cereal.in_waiting)
+    b'2a'
+    >>> cereal.write(b"2\r")
+    2
+    >>> cereal.read(cereal.in_waiting)
+    b'2b'
+    >>> cereal.write(b"2\r")
+    2
+    >>> cereal.read(cereal.in_waiting)
+    b'2a'
+
+    Our GettersAndSetters initializes values to the defaults we specified, and then we can set them to new values
+
+    >>> cereal.write(b"get sn\r")
+    7
+    >>> cereal.read(cereal.in_waiting)
+    b'42\r>'
+    >>> cereal.write(b"set sn our fancy new sn\r")  # if we don't read in between commands, it clears the buffer
+    24
+    >>> cereal.write(b"get sn\r")
+    7
+    >>> cereal.read(cereal.in_waiting)
+    b'our fancy new sn\r>'
+
+    If we send a command that is not defined, it sends back an Unsupported response, which is configurable
+
+    >>> cereal.write(b"nonesense\r")
+    10
+    >>> cereal.read(cereal.in_waiting)
+    b'Unsupported\r>'
+
+    We can change the behavior of certain commands by adding :mod:`Hooks <granola.hooks.hooks>`.
+    The :class:`Hooks <granola.hooks.hooks.StickCannedQueries>` causes the last defined response to not loop, but to
+    stick on it and just repeat.
+
+    >>> from granola import StickCannedQueries
+    >>> hooks = {"StickCannedQueries": {"attributes": ["2\r"], "include_or_exclude": "include"}}
+    >>> cereal = Cereal(command_readers=command_readers, hooks=hooks)
+    >>> cereal.write(b"2\r")
+    2
+    >>> cereal.read(cereal.in_waiting)
+    b'2a'
+    >>> cereal.write(b"2\r")
+    2
+    >>> cereal.read(cereal.in_waiting)
+    b'2b'
+    >>> cereal.write(b"2\r")  # You'll notice here that it doesn't loop
+    2
+    >>> cereal.read(cereal.in_waiting)  # It just repeats the last response (it sticks)
+    b'2b'
+    >>> cereal.write(b"2\r")
+    2
+    >>> cereal.read(cereal.in_waiting)
+    b'2b'
     """
 
     @add_created_at
-    def __init__(self, config=None, config_path=None, command_readers=None, hooks=None, config_key=None):
-
-        self._config_path = config_path if config_path is not None else os.path.join(os.getcwd(), "config.json")
-
-        config = self._check_and_normalize_config_deprecation(config, config_key)
-
+    def __init__(
+        self,
+        command_readers=None,
+        hooks=None,
+        data_path_root=None,
+        unsupported_response="Unsupported\r>",
+        write_terminator="\r",
+        encoding="ascii",
+    ):
+        self._data_path_root = (
+            data_path_root if data_path_root is not None else os.path.join(os.getcwd(), "config.json")
+        )
         hooks = hooks if hooks is not None else []
-        if command_readers is None:  # Set default command readers and hooks
-            command_readers = (GettersAndSetters(), CannedQueries())
-            if not hooks:
-                hooks.append(LoopCannedQueries())
+        command_readers = command_readers if command_readers is not None else []
 
-        self._config = config
-        self._unsupported_response = self._config.get("unsupported_response", "Unsupported\r>")
-        self._encoding = self._config.get("encoding", "ascii")
-        self._write_terminator = "\r"  # TODO madeline, make this something to pass in from config.
+        self._unsupported_response = unsupported_response
+        self._encoding = encoding
+        self._write_terminator = write_terminator
 
-        self._readers = self._setup_command_readers(command_readers, hooks)
+        self._readers_ = self._setup_command_readers_and_hooks(command_readers, hooks)
 
-        self._hooks = []
+        self._hooks_ = []
         self._next_read = ""  # The next read for this "serial" device
         self._next_write = ""  # The current write buffer to the serial device
 
@@ -115,20 +208,22 @@ def __init__(self, config=None, config_path=None, command_readers=None, hooks=No
             self._isOpen = True
 
     @classmethod
-    def mock_from_file(cls, config_key, config_path="config.json", *args, **kwargs):
-        config = cls._load_config(config_key=config_key, config_path=config_path)
-        return cls(config=config, config_path=config_path, *args, **kwargs)
-
-    @classmethod
-    def _load_config(cls, config_key, config_path):
+    def mock_from_json(cls, config_key, config_path="config.json", **kwargs):
         """
-        Load configuration dictionary based on self._config_key class variable
-
-        Loads CSV files into self.serial_cmd_files.
+        Load configuration dictionary based on config_key and config_path and return a
+        Breakfast Cereal class.
 
-        If you don't pass in where self._config_path is, it will default to "config.json" in the
+        If you don't pass in where config_path is, it will default to "config.json" in the
             root directory of your python path.
         """
+        config = cls._load_json_config(config_key=config_key, config_path=config_path)
+        kwargs.update(config)
+        if "data_path_root" not in kwargs:
+            kwargs["data_path_root"] = config_path
+        return cls(**kwargs)
+
+    @classmethod
+    def _load_json_config(cls, config_key, config_path="config.json"):
         path = get_path(config_path)
         config_path = fixpath(path)
 
@@ -144,6 +239,21 @@ def _load_config(cls, config_key, config_path):
         return config
 
     def __call__(self, *args, **kwargs):
+        """
+        Method to initialize pyserial Serial object. Serperating out the initialization of
+        Breakfast Cereal into its own ``__init__`` method and then initializing pyserial in its
+        __call__ method allows you to setup up Breakfast Cereal configuration outside of your
+        code, and then have your code initialize it again to initialize the pyserial part of
+        Breakfast Cereal. That way you can modify your own code as minimally as possible.
+
+        Args:
+            args: args to pass to pyserial
+            kwargs: keyword arguments to pass to pyserial
+
+        Raises:
+            TypeError if you use __call__ more than once (and therefore pyserial is already
+            initialized).
+        """
         if not hasattr(self, "_port"):
 
             logger.debug("%s initializing Mock Serial with Serial args %s and kwargs %s", self, args, kwargs)
@@ -162,7 +272,7 @@ def __str__(self):
         return "{device}{port}".format(device=self.__class__.__name__, port=port_str)
 
     def read(self, size=1):
-        """Mock pyserial.Serial `read`. Return number of bytes in self._next_read based on `size`"""
+        """Mock :meth:`pyserial:serial.Serial.read`. Return number of bytes in self._next_read based on `size`"""
         read = bytearray()
         if size > 0:
             self._next_read = encode_to_bytes(self._next_read, self._encoding)
@@ -177,7 +287,7 @@ def read(self, size=1):
 
     def write(self, data):
         """
-        Mock pyserial.Serial `write` by seeding a serial command generator
+        Mock :meth:`pyserial:serial.Serial.write` by seeding a serial command generator
 
         Args:
             data (byte_str): serial command
@@ -197,7 +307,7 @@ def write(self, data):
                 next_read = None
                 _run_pre_reading_hooks(hooked=self, data=self._next_write)
 
-                for reader in self._readers.values():
+                for reader in self._readers_.values():
                     next_read = reader.get_reading(data=self._next_write)
                     if next_read is not None:
                         self._next_read = next_read
@@ -306,88 +416,83 @@ def _verify_open(self):
         if not self._is_open:
             raise PortNotOpenError
 
-    def _setup_command_readers(self, command_readers, hooks):
-        config_hooks = self._read_file_config_hooks()
-        if config_hooks:
-            hooks = config_hooks
-        config_command_readers = self._read_file_config_command_readers()
-        if config_command_readers:
-            command_readers = config_command_readers
-
+    def _setup_command_readers_and_hooks(self, command_readers, hooks):
         readers = OrderedDict()
-        for reader in command_readers:
-            reader.initialize_config(self._config, self._config_path)
-
-            readers[reader.__class__.__name__] = reader
+        config_command_readers = self._read_file_config_command_readers(command_readers)
+        if config_command_readers:
+            for reader in config_command_readers:
+                readers[reader.__class__.__name__] = reader
+        else:
+            command_readers = (GettersAndSetters, CannedQueries)
+            for reader in command_readers:
+                cr = reader()
+                readers[cr.__class__.__name__] = cr
 
+        config_hooks = self._read_file_config_hooks(hooks)
+        if config_hooks:
+            hooks = config_hooks
         for hook in hooks:
             if inspect.isclass(hook):  # initialize uninitialized hook to their default args
                 hook = hook()
             for cls in hook.hooked_classes:
-                readers[cls.__name__]._hooks.append(hook)
+                readers[cls.__name__].register_hook(hook)
+
+        for reader in readers.values():
+            reader.assign_default_hook()
 
         return readers
 
-    def _read_file_config_from_classes(key, subkey, baseclass):
+    def _read_file_config_from_classes(baseclass):
         """
-        closure to generate functions that will grab ``key`` from config and then ``subkey`` is used
-        to match to a subclass of ``baseclass``. This allows users to pass in classes they want to use
-        for things like hooks or other things inside a configuration dictionary (JSON).
+        closure to generate functions that will match passed in configuration option classes
+        to a subclass of ``baseclass``. This allows users to pass in classes they want to use
+        for things like Hooks, CommandReaders or other things inside a configuration dictionary (JSON).
 
         Args:
-            key (str): The outer key to look up in the config dictionary
-            subkey (str): The inner key to look up in the config dictionary to find the class name
-            baseclass (class): Class to check all subclasses of when you know the class name from
-                ``subkey``
+            baseclass (class): Class to check all subclasses of when you know class from config_options
         """
 
-        def _inner(self):
-            config_options = self._config.get(key, [])
+        def _inner(self, config_options):
             subclasses = _get_subclasses(baseclass)
             classes = []
-            for cls in config_options:
-                if isinstance(cls, dict):
-                    c = subclasses[cls.pop(subkey)]
-                    c = c(**cls)
-                else:
-                    c = subclasses[cls]()
-                classes.append(c)
+            if isinstance(config_options, list):
+                for cls in config_options:
+                    if isinstance(cls, str):
+                        c = subclasses[cls]()
+                    elif inspect.isclass(cls):  # initialize uninitialized class
+                        c = cls()
+                    else:
+                        c = cls
+                    classes.append(c)
+            else:
+                for cls, options in config_options.items():
+                    # add config path, but will be overwritten if defined in options
+                    opts = {"data_path_root": self._data_path_root}
+                    opts.update(options)
+                    if isinstance(cls, str):
+                        c = subclasses[cls](**opts)
+                    elif inspect.isclass(cls):  # initialize uninitialized class
+                        c = cls(**opts)
+                    else:
+                        c = cls
+                    classes.append(c)
             return classes
 
         return _inner
 
-    _read_file_config_hooks = _read_file_config_from_classes("hooks", "hook_type", BaseHook)
-
-    _read_file_config_command_readers = _read_file_config_from_classes(
-        "command_readers", "reader_type", BaseCommandReaders
-    )
-
-    def _check_and_normalize_config_deprecation(self, config, config_key):
-        if isinstance(config, str):
-            config_key = config
-        if config_key is not None:
-            deprecation(
-                "Instantiating Cereal with `config_path` and `config_key` is deprecated"
-                " and will be removed in a future release."
-                "\nPlease use `Cereal.mock_from_file(config_key, config_path)` instead"
-            )
-            config = self._load_config(config_key=config_key, config_path=self._config_path)
-
-            # Check for old form of variable substitution pre jinja
-            getters_and_setters = config.get("getters_and_setters", {})
-            start_not_in = "variable_start_string" not in getters_and_setters
-            end_not_in = "variable_end_string" not in getters_and_setters
-            if getters_and_setters and start_not_in and end_not_in:
-                deprecation(
-                    "'getters_and_setters' variable declaration follows old format"
-                    " that will be removed in a future release."
-                    "\nEither switch to traditional jinja2 formatting ({{ var }})"
-                    "\nor specify explicitly your variable_start_string and variable_end_string inside"
-                    " getters and setters (ex: 'variable_start_string': '`')"
-                )
-                getters_and_setters["variable_start_string"] = "`"
-                getters_and_setters["variable_end_string"] = "`"
-
-        if config is None:
-            raise AttributeError("Cereal __init__ requires a config dictionary")
-        return config
+    _read_file_config_hooks = _read_file_config_from_classes(BaseHook)
+
+    _read_file_config_command_readers = _read_file_config_from_classes(BaseCommandReaders)
+
+
+__doc__ = """
+This module provides the core of GRANOLA with the Cereal class (affectionately called Breakfast Cereal).
+Breakfast Cereal is the core Mock Serial object that handles mocking pyserial's Serial class. It does
+this with a number of :mod:`Command Readers <granola.command_readers>` (:ref:`Guide <Configuration Overview>`)
+that each are responsible for processing a number of serial commands in different ways. This allows
+very extensible functionality with different types of serial commands, and user specific customization
+by defining your own Command Readers.
+
+You can further customize the behavior of Command Readers by adding in :mod:`Hook <granola.hooks.hooks>`
+that run a preditermined spots. Also see :ref:`Configuration Overview`.
+"""
diff --git a/granola/command_readers.py b/granola/command_readers.py
index 01d1c00..3e4b73f 100644
--- a/granola/command_readers.py
+++ b/granola/command_readers.py
@@ -8,51 +8,26 @@
 from collections import OrderedDict
 from pathlib import Path
 
-import attr
 import jinja2
 import jinja2.meta
 import pandas as pd
 
-from granola.hooks.base_hook import BaseHook, wrap_in_hooks
-from granola.utils import (
-    ABC,
-    IS_PYTHON3,
-    SENTINEL,
-    deprecation,
-    fixpath,
-    load_serial_df,
-)
-
-try:
-    from enum import Enum, auto
-except ImportError:
-    from aenum import Enum, auto
+import granola.hooks
+from granola.enums import RandomizeResponse, get_attribute_from_enum, validate_enum
+from granola.hooks.base_hook import wrap_in_hooks
+from granola.utils import ABC, IS_PYTHON3, SENTINEL, fixpath, load_serial_df
 
 logger = logging.getLogger(__name__)
 
-DefaultDF = "`DEFAULT`"  # Default dataframe key
-DeprecatedDefaultDF = "_default_csv_"  # Default dataframe key
 
-
-class RandomizeResponse(Enum):
-    """Randomize response enum"""
-
-    not_randomized = auto()  # Don't randomize
-    randomized = auto()  # Randomize and replace
-    randomized_and_remove = auto()  # Randomize and remove
-
-
-@attr.s
 class SerialCmds(object):
     """
     Serial Command DataFrame container object. Required columns for DataFrame are `cmd` and `response`,
     Other columns are optional, and may or may not be used by certain Command Readers.
 
     Args:
-        data (pd.DataFrame): DataFrame containing serial command, response pairs, as well potentially other
-            columns.
-
-        --- Not currently being used ---
+        data (list[pd.DataFrame]): list of DataFrames containing serial command, response pairs,
+            as well potentially other columns.
         will_randomize_responses (RandomizeResponse): Whether the responses of each command will be
             randomized or not.
 
@@ -63,42 +38,43 @@ class SerialCmds(object):
         Config guide :ref:`Canned Queries Configuration`
     """
 
-    data = attr.ib(type=pd.DataFrame)
-    will_randomize_responses = attr.ib(
-        type=RandomizeResponse,
-        default=RandomizeResponse.not_randomized,
-    )
+    def __init__(self, data=None, will_randomize_responses=RandomizeResponse.not_randomized):
+        self.data = data if data is not None else []  # type: list[pd.DataFrame]
+        self.will_randomize_responses = get_attribute_from_enum(will_randomize_responses, "name")
+        validate_enum(self.will_randomize_responses, RandomizeResponse)
+
+    def add_dataframe(self, df):
+        """Add dataframe to data"""
+        self.data.append(df)
 
-    @classmethod
-    def from_file(cls, file, config_path=None, extra_fields=None):
+    def add_df_from_file(self, file, data_path_root=None, **kwargs):
         """
-        Generate a `SerialCmds` from a csv file path
+        Add a df to data from a csv file path
 
         Args:
-            file (str): Path to csv file. Required
-            config_path (str): Path to configuration. Required if file path is not an absolute path
-            extra_fields (dict): Dictionary of extra fields to add to DataFrame of commands. Either
+            file (str): Path to csv file.
+            data_path_root (str, optional): Path to configuration. Required if file path is not an absolute path
+            extra_fields (dict, optional): Dictionary of extra fields to add to DataFrame of commands. Either
                 key will be mapped to a new column in the DataFrame, and each value can either be
                 a single value, in which case it will be broadcast to all rows. Or a list of values
                 the same length as the number of rows in the data.
         """
         file = str(file)
         if not os.path.isabs(file):
-            if config_path is None:
-                raise TypeError("`config_path` must be specified when using a relative file path")
-            config_path = str(config_path)
-            file = os.path.join(os.path.dirname(config_path), file)
+            if data_path_root is None:
+                raise TypeError("`data_path_root` must be specified when using a relative file path")
+            data_path_root = str(data_path_root)
+            file = os.path.join(os.path.dirname(data_path_root), file)
         file = fixpath(file)
         logger.debug("Canned query path %s: ", file)
         df = load_serial_df(file)
-        if extra_fields is not None:
-            cls._append_extra_fields_to_df(df, extra_fields)
-        return cls(data=df)
+        if kwargs:
+            self._append_extra_fields_to_df(df, **kwargs)
+        return self.add_dataframe(df)
 
-    @classmethod
-    def from_dict(cls, dic, extra_fields=None):
+    def add_df_from_dict(self, dic, **kwargs):
         """
-        Generate a `SerialCmds` from a dictionary of serial commands. The format for this dictionary
+        Add a df to data from a dictionary of serial commands. The format for this dictionary
         can take a few forms. It can either be expressed in a JSON configuration or passed in python
         to Cereal. Here is the most basic form, where each command is mapped directly to a single response.
 
@@ -112,19 +88,18 @@ def from_dict(cls, dic, extra_fields=None):
         data = []
         d = copy.deepcopy(dic)
         for cmd, value in d.items():
-            cls._transverse_dict_and_append(data, cmd, value)
+            self._transverse_dict_and_append(data, cmd, value)
         df = pd.DataFrame.from_records(data)
-        if extra_fields is not None:
-            cls._append_extra_fields_to_df(df, extra_fields)
-        return cls(data=df)
+        if kwargs:
+            self._append_extra_fields_to_df(df, **kwargs)
+        return self.add_dataframe(df)
 
-    @classmethod
-    def _transverse_dict_and_append(cls, data, cmd, value, **extra_fields):
+    def _transverse_dict_and_append(self, data, cmd, value, **extra_fields):
         if isinstance(value, (str, int, float)):  # End of node
-            cls._append_to_list_of_rows(data, cmd, value, **extra_fields)
+            self._append_to_list_of_rows(data, cmd, value, **extra_fields)
             return
 
-        inner_extra_fields, response = cls._extract_respose(value)
+        inner_extra_fields, response = self._extract_respose(value)
 
         if isinstance(response, (list, tuple)):
             for index, item in enumerate(response):
@@ -157,11 +132,11 @@ def _transverse_dict_and_append(cls, data, cmd, value, **extra_fields):
                     }
                 else:
                     raise ValueError("Invalid JSON format for canned_queries")
-                cls._append_to_list_of_rows(data, cmd, response_, **extra_fields_)
+                self._append_to_list_of_rows(data, cmd, response_, **extra_fields_)
         else:
             response_ = response
             extra_fields_ = inner_extra_fields
-            cls._append_to_list_of_rows(data, cmd, response_, **extra_fields_)
+            self._append_to_list_of_rows(data, cmd, response_, **extra_fields_)
 
     @staticmethod
     def _extract_respose(value):
@@ -179,35 +154,35 @@ def _append_to_list_of_rows(data, cmd, response, **ex_fields):
         data.append(dict(cmd=cmd, response=response, **ex_fields))
 
     @staticmethod
-    def _append_extra_fields_to_df(data, extra_fields):
-        for field, value in extra_fields.items():
+    def _append_extra_fields_to_df(data, **kwargs):
+        for field, value in kwargs.items():
             if field in data.columns:
                 data.loc[data[field].isna(), field] = value
             else:
                 data[field] = value
 
 
-@attr.s
 class InstrumentAttribute(object):
-    name = attr.ib(type=str)
-    value = attr.ib(type=str)
-    meta_data = attr.ib(type=dict, default=attr.Factory(OrderedDict))
+    def __init__(self, name, value, meta_data=None):
+        self.name = name
+        self.value = value
+        self.def_value = value
+        self.meta_data = meta_data if meta_data is not None else OrderedDict()
 
 
-@attr.s
 class BaseCommandReaders(ABC):
     """
     BaseCommandReaders Class that sets the interface for other CommandReaders. The basic form of
-    a command reader is that Cereal will interact with it solely through `initialize_config`
-    and `get_reading`. `initialize_config` happens durring Cereal's `__init__` method where
-    it passes its config to the CommandReader. The CommandReader is the able to parse out whatever
-    parameter's it needs from said config. And then in `get_reading`, Cereal passes in the
-    serial command and the CommandReader processes it however it does, and returns it.
+    a command reader is that Cereal will interact with it solely through its ``__init__``
+    and :meth:`granola.command_readers.BaseCommandReaders.get_reading`. The ``get_reading``
+    method is the method that will be called, handed in a serial command, processed in some way,
+    and returned. If the Command Reader can not process the serial command, it instead returns
+    None or a SENTINEL object (more on that below) to indicate that the supplied serial command
+    isn't defined for that Command Reader, allowing any other Command Readers to process it instead.
 
-    The `get_reading` commands are also decorated with @wrap_hooks, which will run the attributes
-    `pre_hooks` (iterable type of functions) before `get_reading` and
-    `post_hooks` (iterable type of functions) after `get_reading`, allowing you to modify the
-    behavior of the `get_reading` method for certain serial commands.
+    The `get_reading` commands are also decorated with @wrap_in_hooks, which will run ``_hooks_``
+    attributes (of type :mod:`~granola.hooks.hooks`) that have a defined ``prehook`` method before
+    ``get_reading`` and ``_hooks_`` with a defined ``posthook`` method after after ``get_reading``.
 
     Because "" is a valid serial command response, we use None to signify an unsupported response.
     We also have a defined SENTINEL object for response that are undefined in other ways.
@@ -218,18 +193,25 @@ class BaseCommandReaders(ABC):
     as well.
 
     Args:
-        pre_hooks (list or tuple): iterable of hooks to run before `get_readings`
-        post_hooks (list or tuple): iterable of hooks to run after `get_readings`
+        hooks(list[BaseHook], optional): List of Hooks to run on this Command Reader.
+            Defaults to `[]`
+        data_path_root(str | Path, optional): Path to the where all data file paths for this command reader
+            (such as defining the serial commands) will be referenced from. Not every Command
+            Reader, or every way to initialize a certain Command Reader uses data files, so
+            this doesn't apply to every Command Reader.
+            Defaults to current working directory.
+
+    See Also
+    --------
+    :class:`.Cereal` : Breakfast Cereal class used for mocking
+    :ref:`Command Readers and Hooks Configuration` : Configuration Overview
+    :ref:`Basic Overview of Mock Cereal and API` : Intro Tutorial
     """
 
-    _hooks = attr.ib(type=BaseHook, default=attr.Factory(list))
-
-    _config = attr.ib(type=dict, init=False)
-    _config_path = attr.ib(type=dict, init=False)
-
-    def initialize_config(self, config, config_path=None):
-        self._config = config
-        self._config_path = config_path if config_path is not None else os.getcwd()
+    def __init__(self, hooks=None, data_path_root=None, *args, **kwargs):
+        super(BaseCommandReaders, self).__init__()
+        self._hooks_ = hooks if hooks is not None else []
+        self._data_path_root = data_path_root if data_path_root is not None else os.getcwd()
 
     @wrap_in_hooks
     @abc.abstractmethod
@@ -247,87 +229,66 @@ def get_reading(self, data):
                 by the hooks and CommandReader, dependent on the individual CommandReader.
         """
 
+    def register_hook(self, hook):
+        self._hooks_.append(hook)
+
+    def assign_default_hook(self):
+        """If self._hooks_ hooks is empty, add any default hooks for this Command Reader."""
+        if not self._hooks_:
+            pass
+
 
-@attr.s
 class GettersAndSetters(BaseCommandReaders):
     """
-    Helper class that processes getters and setters. It initializes the default values set
-    in the config and stores those values in the attribute `self.instrument_attributes`. These
-    attributes can then be modified with setters defined in the setters from the config and stored
-    in `self.setters`, and then grabbed from the getters defined in getters from the config and
-    stored in `self.getters`.
+    Command Reader to handle getters and setters. It initializes the default values and stores those
+    values in the attribute `self.instrument_attributes`. These attributes can then be modified with
+    setters defined in the setters in `self.setters`, and then grabbed from the getters stored in
+    `self.getters`.
 
-    Inside the getters and setters, the values pulled out will be the values
-    inside matching backticks (`).
+    Inside the getters and setters, the formatting follows Jinja2 formatting syntax.
 
     Args:
         arguments for BaseCommandReaders
-    """
 
-    instrument_attributes = attr.ib(type=dict, default=attr.Factory(OrderedDict), init=False)
-    # format := {getter_cmd: {"attributes": list_of_instrument_attributes,
-    #                         "response": getter_response_format}}
-    getters = attr.ib(type=dict, default=attr.Factory(OrderedDict), init=False)
-    # setter regex has named match groups created at initialization for ease of book keeping
-    setters = attr.ib(type=dict, default=attr.Factory(OrderedDict), init=False)
-    jinja_env = attr.ib(type=jinja2.Environment, default=attr.Factory(OrderedDict), init=False)
-
-    def initialize_config(self, config, config_path):
-        r"""
-        Initialize config, the config dictionary that has optionally has the key
-        `"getters_and_setters"` defined in it. If `"canned_queries"` is defined,
-        it should be in the form::
-
-            .. code-block:: JSON
-
-                {
-                    "getters_and_setters": {
-                        "default_values": {
-                            "devtype": "Cereal",
-                            "sn": "42",
-                            "fw_ver":"0.0.0"
-                        },
-                        "getters": [
-                            {
-                                "cmd": "get -sn\r",
-                                "response": "{{ sn }}\r>"
-                            },
-                            {
-                                "cmd": "show\r",
-                                "response": "{{ devtype }} {{ fw_ver }} {{ sn }}\r>"
-                            }
-                        ],
-                        "setters": [
-                            {
-                                "cmd": "set -sn {{ sn }}\r",
-                                "response": "OK\r>"
-                            }
-                        ]
-                    }
-                }
+    See Also
+    --------
+    :ref:`Getters and Setters Configuration`
 
-            The {{ sn }} in "cmd": "set -sn {{ sn }}\r" pulls out the attribute defined in
-            "getters_and_setters" and is gotten when you issue either "get -sn\r",
-            or "show\r" (which will also return fw_ver and devtype).
+    :ref:`Advanced Getters and Setters`
+    """
 
-        """
-        super(GettersAndSetters, self).initialize_config(config, config_path)
-        self._variable_start_string = config.get("getters_and_setters", {}).get("variable_start_string", "{{")
-        self._variable_end_string = config.get("getters_and_setters", {}).get("variable_end_string", "}}")
+    def __init__(
+        self,
+        default_values=None,
+        getters=None,
+        setters=None,
+        variable_start_string="{{",
+        variable_end_string="}}",
+        **kwargs
+    ):
+        super(GettersAndSetters, self).__init__(**kwargs)
+        self._variable_start_string = variable_start_string
+        self._variable_end_string = variable_end_string
         self.jinja_env = jinja2.Environment(
-            variable_start_string=self._variable_start_string,
-            variable_end_string=self._variable_end_string,
+            variable_start_string=variable_start_string,
+            variable_end_string=variable_end_string,
             loader=jinja2.BaseLoader(),
         )
-
-        self._load_getters_and_setters()
+        default_values = default_values if default_values is not None else OrderedDict()
+        getters = getters if getters is not None else OrderedDict()
+        setters = setters if setters is not None else OrderedDict()
+        self.instrument_attributes = OrderedDict()
+        self.getters = OrderedDict()
+        self.setters = OrderedDict()
+        self._load_getters_and_setters(default_values, getters, setters)
 
     @wrap_in_hooks
     def get_reading(self, data):
         """
         Processes incoming serial command data and sends it to `self._process_getter` to see if
         it is a getter and get its response, if not, sends it to `self._process_setter`,
-        to see if it is a setter and get its response, if not, it returns None.
+        to see if it is a setter and then set the respective setter and get its response,
+        if not, it returns None.
 
         Args:
             data (str): Incoming serial command
@@ -359,52 +320,34 @@ def render_template(self, string, attribute_vals=None):
         result = template.render(**attribute_vals).replace("_\\r_", "\r").replace("_\\n_", "\n")
         return result
 
-    def _load_getters_and_setters(self):
+    def _load_getters_and_setters(self, default_values, getters, setters):
         """Loads default values, getters and setters"""
 
-        getters_and_setters = self._config.get("getters_and_setters", OrderedDict())
-        if "default_values" not in getters_and_setters:
+        if not default_values:
             return
-        self._initialize_default_instrument_attributes(getters_and_setters)
-        self._initialize_getters(getters_and_setters)
-        self._initialize_setters(getters_and_setters)
+        self._initialize_default_instrument_attributes(default_values)
+        self._initialize_getters(getters)
+        self._initialize_setters(setters)
 
-    def _initialize_default_instrument_attributes(self, getters_and_setters):
+    def _initialize_default_instrument_attributes(self, default_values):
         """Loads default values into `self.instrument_attributes`"""
-        for attribute, default_value in getters_and_setters["default_values"].items():
+        for attribute, default_value in default_values.items():
             self.instrument_attributes[attribute] = InstrumentAttribute(name=attribute, value=default_value)
 
-    def _initialize_getters(self, getters_and_setters):
+    def _initialize_getters(self, getters):
         """
         Loads all getters from getters_and_setters["getters"] into `self.getters`. If getter
         does not get an attribute in self.instrument_attributes, raise Value Error"""
-        for getter in getters_and_setters.get("getters", {}):
-            if "getter" in getter:
-                deprecation(
-                    "Using 'getter' key inside 'getter_and_setters'"
-                    "(config['getters_and_setters']['getters']['getter']"
-                    "is deprecated and will be removed in a future release."
-                    "\nSwitch to using the key 'cmd' instead."
-                )
-                getter["cmd"] = getter.pop("getter")
-
+        for getter in getters:
             self._check_attributes_are_valid("getters", getter)
 
             self.getters[getter["cmd"]] = getter["response"]
 
-    def _initialize_setters(self, getters_and_setters):
+    def _initialize_setters(self, setters):
         """
         Loads all setters from getters_and_setters["setters"] into `self.setters`. If setter
         does not get an attribute in self.instrument_attributes, raise Value Error"""
-        for setter in getters_and_setters.get("setters", {}):
-            if "setter" in setter:
-                deprecation(
-                    "Using 'setter' key inside 'getter_and_setters'"
-                    "(config['getters_and_setters']['setters']['setter']"
-                    "is deprecated and will be removed in a future release."
-                    "\nSwitch to using the key 'cmd' instead."
-                )
-                setter["cmd"] = setter.pop("setter")
+        for setter in setters:
             self._check_attributes_are_valid("setters", setter)
 
             regex = "{variable_start}(.*?){variable_end}".format(
@@ -485,41 +428,67 @@ def _check_attributes_are_valid(self, attribute_type, template_string):
                     )
 
 
-@attr.s
 class CannedQueries(BaseCommandReaders):
     """
     Helper class that processes canned queries (queries defined ahead of time from a list
-    such as a csv).
+    such as a csv or configuration dictionary).
 
     It stores a dictionary of :class:`~granola.command_readers.SerialCmds`, which represents
     canned queries as pandas DataFrames of the serial commands. And then as a serial command comes in,
     it is directed to the correct :class:`~granola.command_readers.SerialCmds` and then creates a slice of
     your DataFrame to get only the matching serial commands to iterate over.
 
-    See :ref:`Canned Queries Configuration` for examples on configuration formatting.
+    See Also
+    --------
+    :ref:`Canned Queries Configuration` for examples on configuration formatting.
+
+    :ref:`Command Readers and Hooks Configuration` : Command Readers and Hook Overviews
     """
 
-    serial_cmd_files = attr.ib(type=dict, default=attr.Factory(OrderedDict), init=False)
-    serial_dfs = attr.ib(type=dict, default=attr.Factory(OrderedDict), init=False)
-    serial_generator = attr.ib(type=dict, default=attr.Factory(OrderedDict), init=False)
+    def __init__(self, data=None, data_path_root=None, **kwargs):
+
+        super(CannedQueries, self).__init__(data_path_root=data_path_root, **kwargs)
+        self.data = data if data is not None else OrderedDict()
+        self.serial_df = pd.DataFrame(columns=["cmd", "response"])  # default empty df
+        serial_cmd_files_kwargs = self._extract_serial_cmd_file_kw_from_config(kwargs)
+        self.serial_cmd_file = SerialCmds(**serial_cmd_files_kwargs)
+        self.serial_generator = OrderedDict()
+
+        for maybe_file in self.data:
+            if isinstance(maybe_file, (str, Path)):
+                self.serial_cmd_file.add_df_from_file(maybe_file, self._data_path_root, **kwargs)
+            elif isinstance(maybe_file, dict):
+                self.serial_cmd_file.add_df_from_dict(maybe_file, **kwargs)
 
-    def initialize_config(self, config, config_path=None):
-        super(CannedQueries, self).initialize_config(config, config_path)
-        self._load_canned_queries(self._config_path)
         self._seed_serial_dfs()
 
+    def assign_default_hook(self):
+        if not self._hooks_:
+            self._hooks_ = [granola.hooks.hooks.LoopCannedQueries()]
+
     @wrap_in_hooks
     def get_reading(self, data):
         """
-        Process incoming canned queries and route them to the appropriate df and create
-        a generator of that df queries to iterate through them.
+        Process incoming canned queries by extracting just the serial commands that match the
+        incoming serial command and then create a generator of those matching serial commands
+        and response to iterate through.
 
         Args:
             data (str): Incoming serial command
 
         Returns:
-            str | None: the response from matching serial df. If no matching matching command is found,
-            then it returns None.
+            str | None | SENTINEL: the response from matching serial df.
+            If no matching matching command is found, then it returns None.
+            If The generator is exhausted, and no Hook is activate to reseed the generator or
+            alter the behavior in some other way, return SENTINEL.
+
+        See Also
+        --------
+        :mod:`Hooks <granola.hooks.hooks>` : Hook API information
+
+        :ref:`Canned Queries Configuration` for examples on configuration formatting.
+
+        :ref:`Command Readers and Hooks Configuration` : Command Readers and Hook Overviews
         """
         try:
             if data not in self.serial_generator:
@@ -530,37 +499,12 @@ def get_reading(self, data):
         return next_read
 
     def _start_serial_generator(self, cmd):
-        df, serial_cmd_file = self._get_matching_df(cmd)
-        serial_df = self._filter_serial_df(df=df, cmd=cmd)  # type: pd.DataFrame
+        serial_df = self._filter_serial_df(cmd=cmd)  # type: pd.DataFrame
         if not serial_df.empty:
-            serial_df_generator = self._get_generator_from_df(serial_df, serial_cmd_file)
+            serial_df_generator = self._get_generator_from_df(serial_df, self.serial_cmd_file.will_randomize_responses)
             self.serial_generator[cmd] = serial_df_generator
 
-    def _load_canned_queries(self, config_path):
-        # kwargs = self._extract_serial_cmd_file_kw_from_config()
-        canned_queries = self._config.get("canned_queries", {})
-
-        if "files" in canned_queries and isinstance(canned_queries["files"], dict):
-            deprecation("canned_queries key 'files' has been deprecated. Use the key 'data' instead.")
-            data = canned_queries.pop("files")
-            canned_queries["data"] = data
-
-        extra_fields = {key: item for key, item in canned_queries.items() if key != "data"}
-
-        data = canned_queries.get("data", {})
-
-        if DeprecatedDefaultDF in data:
-            deprecation("canned_queries['data'] key '_default_csv_' has been deprecated, Use '`DEFAULT`'")
-            default = data.pop(DeprecatedDefaultDF)
-            data[DefaultDF] = default
-
-        for key, maybe_file in canned_queries.get("data", {}).items():
-            if isinstance(maybe_file, (str, Path)):
-                self.serial_cmd_files[key] = SerialCmds.from_file(maybe_file, config_path, extra_fields=extra_fields)
-            elif isinstance(maybe_file, dict):
-                self.serial_cmd_files[key] = SerialCmds.from_dict(maybe_file, extra_fields=extra_fields)
-
-    def _extract_serial_cmd_file_kw_from_config(self):
+    def _extract_serial_cmd_file_kw_from_config(self, kw):
         """
         extract signature of SerialCmds and recursively search `self._config` for matching
         keys ad return all found key value pairs.
@@ -572,46 +516,18 @@ def _extract_serial_cmd_file_kw_from_config(self):
         kwargs = OrderedDict()
         serial_cmd_args = inspection_getargspec(SerialCmds.__init__).args
         for arg in serial_cmd_args:
-            if self._config.get(arg) is not None:
-                kwargs[arg] = self._config[arg]
-            if self._config.get("canned_queries", {}).get(arg) is not None:
-                kwargs[arg] = self._config["canned_queries"][arg]
+            if kw.get(arg) is not None:
+                kwargs[arg] = kw[arg]
+            if kw.get("canned_queries", {}).get(arg) is not None:
+                kwargs[arg] = kw["canned_queries"][arg]
         logger.debug("%s extracted SerialCmds kwargs: %s", self, kwargs)
         return kwargs
 
     def _seed_serial_dfs(self):
-        if self._config.get("canned_queries"):
-            self.serial_dfs = OrderedDict(
-                (key, serial_cmd_file.data) for key, serial_cmd_file in self.serial_cmd_files.items()
-            )
-        else:
-            self.serial_dfs[DefaultDF] = pd.DataFrame(columns=["cmd", "response"])
-
-    def _get_matching_df(self, cmd):
-        """
-        Get matching DataFrame from cmd by regex search.
-
-        Each DataFrame in `self.serial_df` has a key that is a regex corresponding to what commands
-        they map to. If that matches, return that DataFrame.
-
-        Args:
-            cmd (AnyStr): Command to compare against.
+        if self.serial_cmd_file.data:
+            self.serial_df = pd.concat(objs=[df for df in self.serial_cmd_file.data])
 
-        Returns:
-            Tuple[pd.DataFrame, SerialCmds]: Returns matching DataFrame,
-                otherwise it returns default values.
-
-        Raises:
-            KeyError if cmd is never found is not found in DataFrames in no DefaultDF is defined.
-        """
-        for key, df in self.serial_dfs.items():
-            not_default_df = key != DefaultDF
-            if not_default_df and re.match(key, cmd):
-                return df, self.serial_cmd_files[key]
-        return self.serial_dfs[DefaultDF], self.serial_cmd_files[DefaultDF]
-
-    @staticmethod
-    def _filter_serial_df(df, cmd):
+    def _filter_serial_df(self, cmd):
         """
         Filter DataFrame to only the commands that match the inputted command
 
@@ -625,13 +541,14 @@ def _filter_serial_df(df, cmd):
         Returns:
             pd.DataFrame: Filtered DataFrame.
         """
+        df = self.serial_df
         conditions = df["cmd"] == cmd
         columns = ["response"]
         serial_df = df.loc[conditions, columns]
         return serial_df
 
     @staticmethod
-    def _get_generator_from_df(df, serial_cmd_file):
+    def _get_generator_from_df(df, will_randomize_responses):
         """Create a generator for the df so that when you call next on that generator, it
         gives you the next serial command each time, and not the first one. Allowing you to
         continue through the list of serial commands. Also gives the option to randomize results.
@@ -642,27 +559,24 @@ def _get_generator_from_df(df, serial_cmd_file):
                 a straight iteration.
         """
         # TODO(madeline) only pass in randomize enum? Also clean up and make subfunctions?
-        if serial_cmd_file.will_randomize_responses == RandomizeResponse.not_randomized:
+        if will_randomize_responses == RandomizeResponse.not_randomized.name:
             for _, rows in df.iterrows():
                 yield rows["response"]
         if (
-            serial_cmd_file.will_randomize_responses == RandomizeResponse.randomized
-            or serial_cmd_file.will_randomize_responses == RandomizeResponse.randomized_and_remove
+            will_randomize_responses == RandomizeResponse.randomized_w_replacement.name
+            or will_randomize_responses == RandomizeResponse.randomize_and_remove.name
         ):
             while df.shape[0] > 0:
                 df_length = df.shape[0]
                 row = random.randint(0, df_length - 1)
                 yield df["response"].iloc[row]
-                if serial_cmd_file.will_randomize_responses == RandomizeResponse.randomized_and_remove:
+                if will_randomize_responses == RandomizeResponse.randomize_and_remove.name:
                     df = df.drop(row)
 
 
-config = {
-    "getters_and_setters": {
-        "default_values": {"sn": "42"},
-        "getters": [{"cmd": "get -sn\r", "response": "`sn`\r>"}],
-        "setters": [{"cmd": "set -sn `sn`\r", "response": "OK\r>"}],
-        "variable_start_string": "`",
-        "variable_end_string": "`",
-    }
-}
+__doc__ = """
+Command Readers are the objects that handle the processing of individual serial
+commands. Each serial command that comes in is processed by each Command Reader and
+once a Command Reader returns a valid response, the next Command Readers will skip processing the command.
+This allows you to change the behavior of individual commands by defining new Command Readers.
+"""
diff --git a/granola/enums.py b/granola/enums.py
index 5cfc0af..35dd874 100644
--- a/granola/enums.py
+++ b/granola/enums.py
@@ -25,9 +25,7 @@ def validate_enum(value, enum):
         value (Enum or enum key)
         enum: the enum to validate against
     """
-    error = ValueError(
-        "Invalid Enum Option" "\nEnum -- %s" "\nOption supplied -- %s" % (enum.str_enum_options(), value)
-    )
+    error = ValueError("Invalid Enum Option\nEnum -- %s\nOption supplied -- %s" % (enum.str_enum_options(), value))
     if isinstance(value, Enum):
         if value not in enum:
             raise error
@@ -36,6 +34,21 @@ def validate_enum(value, enum):
 
 
 class DocumentedEnum(Enum):
+    """
+    Enum Base class to inherit from that replaces the value option with a description option.
+
+    Examples:
+        >>> class HookTypes(DocumentedEnum):
+        ...     pre_reading = "Hook to run before `get_reading` methods"
+        ...     post_reading = "Hook to run after `get_reading` methods"
+
+        >>> HookTypes.pre_reading.name
+        'pre_reading'
+
+        >>> HookTypes.post_reading.description
+        'Hook to run after `get_reading` methods'
+    """
+
     def __init__(self, description):
         self.description = description
 
@@ -49,6 +62,17 @@ def str_enum_options(cls):
         return string
 
 
+class RandomizeResponse(DocumentedEnum):
+    """
+    The different ways to specify to randomize or not randomize your
+    :class:`~granola.command_readers.CannedQueries` response
+    """
+
+    not_randomized = "Don't randomize the response"
+    randomized_w_replacement = "Randomize with replacement"
+    randomize_and_remove = "Randomize and remove"
+
+
 class HookTypes(DocumentedEnum):
     """
     Allowed hook types for ``BaseHook`` methods or for a ``register_hook``
diff --git a/granola/hooks/base_hook.py b/granola/hooks/base_hook.py
index 4f7ce80..644435e 100644
--- a/granola/hooks/base_hook.py
+++ b/granola/hooks/base_hook.py
@@ -25,7 +25,11 @@ def pre_reading(self, hooked, data, **kwargs):
         Args:
             hooked: instance of hooked class
             data (str): Serial command
+
+        Returns:
+            data (str)
         """
+        return data
 
     def post_reading(self, hooked, result, data, **kwargs):
         """
@@ -35,7 +39,11 @@ def post_reading(self, hooked, result, data, **kwargs):
             hooked: instance of hooked class
             result (str): Returned result from serial command.
             data (str): Serial command
+
+        Returns:
+            result (str)
         """
+        return result
 
 
 def register_hook(hook_type_enum, hooked_classes):  # TODO madeline allow passing in attributes and including
@@ -96,12 +104,13 @@ def hook(self, **kwargs):
 
 
 def _run_pre_reading_hooks(hooked, data, **kwargs):
-    for hook in hooked._hooks:
-        hook.pre_reading(hooked=hooked, data=data, **kwargs)
+    for hook in hooked._hooks_:
+        data = hook.pre_reading(hooked=hooked, data=data, **kwargs)
+    return data
 
 
 def _run_post_reading_hooks(hooked, result, data, **kwargs):
-    for hook in hooked._hooks:
+    for hook in hooked._hooks_:
         result = hook.post_reading(hooked=hooked, result=result, data=data, **kwargs)
     return result
 
@@ -118,7 +127,7 @@ def wrap_in_hooks(func):
 
     @functools.wraps(func)
     def wrapper(hooked, data, **kwargs):
-        _run_pre_reading_hooks(hooked=hooked, data=data, **kwargs)
+        data = _run_pre_reading_hooks(hooked=hooked, data=data, **kwargs)
         result = func(hooked, data, **kwargs)
         result = _run_post_reading_hooks(hooked=hooked, result=result, data=data, **kwargs)
         return result
@@ -147,36 +156,34 @@ def wrapper(hooked, data, **kwargs):
 Hooks as Classes
 ****************
 
-Here is an example of a hook class being created::
-
-    .. code-block:: python
-
-        from granola import GettersAndSetters, CannedQueries
-
-        class MyHook(BaseHook):
-            hooked_classes=[GettersAndSetters, CannedQueries]
-            '''This is the list of classes that the hook will be run on.'''
-
-            def __init__(self, attributes=["get -sn\r"], include_or_exclude="exclude", some_parameter="default_val"):
-                '''attributes and include_or_exclude are parameters (with defaults) that come from
-                BaseHook. This specifies which serial commands or other type of attribute will be
-                include or exclude on your hook (which will be ran). By saying attributes=["get -sn\r"]
-                and include_or_exclude="include" we are saying to exclude "get -sn\r" and
-                include all other commands.
-                '''
-
-                super(MyHook, self).__init__(attributes=attributes, include_or_exclude=include_or_exclude)
-                self.some_parameter = some_parameter
-                self.calculated_value = None
-
-            def pre_reading(self, hooked, data, **kwargs):
-                '''store the length of the the incoming command (data) * ``self.some_parameter``
-                self.calculated_value = self.some_parameter * len(data)
-
-            def post_reading(self, hooked, result, data, **kwargs):
-                '''Change the serial result to the previous ``self.calculated_value`` * ``len(result)``
-                result = self.calculated_value * len(result)
-                return result
+Here is an example of a hook class being created
+
+>>> from granola import GettersAndSetters, CannedQueries, BaseHook
+
+>>> class MyHook(BaseHook):
+...     hooked_classes=[GettersAndSetters, CannedQueries]
+...     '''This is the list of classes that the hook will be run on.'''
+...
+...     def __init__(self, attributes=["get -sn\r"], include_or_exclude="exclude", some_parameter="default_val"):
+...         '''attributes and include_or_exclude are parameters (with defaults) that come from
+...         BaseHook. This specifies which serial commands or other type of attribute will be
+...         include or exclude on your hook (which will be ran). By saying attributes=["get -sn\r"]
+...         and include_or_exclude="include" we are saying to exclude "get -sn\r" and
+...         include all other commands.
+...         '''
+...
+...         super(MyHook, self).__init__(attributes=attributes, include_or_exclude=include_or_exclude)
+...         self.some_parameter = some_parameter
+...         self.calculated_value = None
+...
+...     def pre_reading(self, hooked, data, **kwargs):
+...         '''store the length of the the incoming command (data) * ``self.some_parameter``'''
+...         self.calculated_value = self.some_parameter * len(data)
+...
+...     def post_reading(self, hooked, result, data, **kwargs):
+...         '''Change the serial result to the previous ``self.calculated_value`` * ``len(result)``'''
+...         result = self.calculated_value * len(result)
+...         return result
 
 Doing a hook this way, as a class definition allows you to specify a ``pre_reading`` and ``post_reading``
 method on the same hook. Some hooks (such as the approach hook) rely on it being ran before and after the reading
@@ -189,27 +196,29 @@ def post_reading(self, hooked, result, data, **kwargs):
 If your hook doesn't require both a ``pre_reading`` and ``post_reading``, you can instead just write
 a function and decorate it with the :func:`~granola.hooks.base_hook.register_hook` decorator.
 
-Here is an example of a hook being create like that::
+Here is an example of a hook being created
 
-    .. code-block:: python
+>>> from granola import register_hook, CannedQueries
 
-        from granola.hooks.base_hook import register_hook
+>>> @register_hook(hook_type_enum="post_reading", hooked_classes=[CannedQueries])
+... def LoopCannedQueries(hooked, result, data, **kwargs):
+...     if result is SENTINEL:
+...         hooked._start_serial_generator(data)
+...         result = next(hooked.serial_generator[data])
+...         return result
+...     return result
 
-        @register_hook(hook_enum=HookTypes.post_reading, hooked_classes=[CannedQueries])
-        def LoopCannedQueries(hooked, result, data, **kwargs):
-            if result is SENTINEL:
-                hooked._start_serial_generator(data)
-                result = next(hooked.serial_generator[data])
-                return result
-            return result
+This is the actual implementation of :class:`~granola.hooks.hooks.LoopCannedQueries` the looping hook that by
+default loops all canned queries (minus the docstring)
 
-This is the actual implementation of the looping hook that by default loops all canned queries
-(minus the docstring), you can find the documentation for that hook at the bottom of this page.
+:func:`~granola.hooks.base_hook.register_hook` takes two arguments, the ``hook_type_enum``
+(see :class:`~granola.enums.HookTypes` for options and more details), which tells you if it is a
+pre or post_reading hook (can be passed in as a direct enum object or as a string), and the ``hooked_classes``,
+which tell you which :mod:`~granola.command_readers` the hook applies to.
+After that it is just your function definition.
 
-``register_hook`` takes two arguments, the ``hook_type_enum``, which tells you if it is a pre or
-post_reading hook (can be passed in as a direct enum object or as a string), and the ``hooked_classes``,
-which tell you which classes the hook applies to. After that is just your function definition,
-which the arguments for that follow the ``post_reading`` method for :class:`~granola.hooks.base_hook.BaseHook`.
+The signature of your function should match the signature of base method you are using
+(``post_reading``, ``post_reading``, etc.) from :class:`~granola.hooks.base_hook.BaseHook`.
 
 After you have "registered" your hook function, it can then be initialized like a hook class.
 (i.e. substantiating it before you pass it to ``Cereal``). If you don't instantiate it, then Cereal
@@ -220,20 +229,22 @@ def LoopCannedQueries(hooked, result, data, **kwargs):
 Hook Examples
 ******************
 
-.. code-block:: python
+>>> from granola import LoopCannedQueries, Cereal
+>>> command_readers = {"CannedQueries": {"data": [{"cmd\r": "response"}]}}
 
-    >>> # this way works by initializing LoopCannedQueries to non default values
-    >>> hook = LoopCannedQueries(attributes={"get batt\r"}, include_or_exclude="include")
-    >>> bk_cereal = Cereal(some_config, hooks=[hook])
+>>> # this way works by initializing LoopCannedQueries to non default values
+>>> hook = LoopCannedQueries(attributes={"get batt\r"}, include_or_exclude="include")
+>>> cereal = Cereal(command_readers=command_readers, hooks=[hook])
 
-    >>> # This version we initialize it to its default values
-    >>> hook = LoopCannedQueries()
-    >>> bk_cereal = Cereal(some_config, hooks=[hook])
+>>> # This version we initialize it to its default values
+>>> hook = LoopCannedQueries()
+>>> ccereal = Cereal(command_readers=command_readers, hooks=[hook])
 
-    >>> # This version we don't initialize it at all and let Cereal initialize it to its default values
-    >>> bk_cereal = Cereal(some_config, hooks=[LoopCannedQueries])
+>>> # This version we don't initialize it at all and let Cereal initialize it to its default values
+>>> hook = LoopCannedQueries
+>>> cereal = Cereal(command_readers=command_readers, hooks=[hook])
 
-This last example also shows how to pass hooks to ``Cereal``. You pass hooks in an iterable to
+These examples also shows how to pass hooks to ``Cereal``. You pass hooks in an iterable to
 ``Cereal``, and ``Cereal`` will associate the hook with whatever class it needs to
 based on the classes listed in your hook's ``hooked_classes``. In these example, ``LoopCannedQueries`` would
 only run on ``CannedQueries``, but ``MyHook`` would run on ``CannedQueries`` and ``GettersAndSetters``.
diff --git a/granola/hooks/hooks.py b/granola/hooks/hooks.py
index c137e63..8640f1b 100644
--- a/granola/hooks/hooks.py
+++ b/granola/hooks/hooks.py
@@ -65,9 +65,9 @@ def pre_reading(self, hooked, data, **kwargs):
             data (str): Serial command
         """
         regex_match, response = hooked._get_matching_setter(data)
-        if not response:
-            return
-        self._process_setter_approach_hook_helper(hooked, regex_match, hooked.attribute_vals)
+        if response:
+            self._process_setter_approach_hook_helper(hooked, regex_match, hooked.attribute_vals)
+        return data
 
     def post_reading(self, hooked, result, data, **kwargs):
         """
@@ -129,7 +129,7 @@ def validate_attribute_type(self, hooked, attribute):
             float(hooked.instrument_attributes[attribute].value)
         except ValueError:
             raise ValueError(
-                "ApproachHook can only be applied with a float attribute." "\nproblem attribute: %s" % attribute
+                "ApproachHook can only be applied with a float attribute.\nproblem attribute: %s" % attribute
             )
 
 
diff --git a/granola/main.py b/granola/main.py
new file mode 100644
index 0000000..6465e97
--- /dev/null
+++ b/granola/main.py
@@ -0,0 +1,136 @@
+"""
+This module is deprecated and will be removed in version 1.0. Please use  :mod:`~granola.breakfast_cereal` instead
+"""
+from granola.breakfast_cereal import Cereal
+from granola.utils import deprecation
+
+
+class MockSerial(Cereal):
+    """
+    Deprecated version of :class:`~granola.breakfast_cereal.Cereal`. Will be removed in 1.0
+
+    Switch to using :class:`~granola.breakfast_cereal.Cereal` and the new interface.
+    """
+
+    def __init__(self, config_key, config_path="config.json", command_readers=None, hooks=None):
+        deprecation("MockSerial is deprecated. Please use granola.breakfast_cereal.Cereal instead.", "1.0")
+        command_readers = command_readers or []
+        hooks = hooks or []
+        config = self._load_json_config(config_key=config_key, config_path=config_path)
+
+        config = self._check_and_normalize_config_deprecation(config)
+
+        config = self._check_and_normalize_command_readers_deprecations(config, command_readers)
+        config = self._check_and_normalize_hook_deprecations(config, hooks)
+
+        super(MockSerial, self).__init__(data_path_root=config_path, **config)
+
+    def _check_and_normalize_config_deprecation(self, config):
+        command_readers = config.setdefault("command_readers", {})
+        if "canned_queries" in config:
+            self._check_and_normalize_canned_queries_deprecation(config, command_readers)
+
+        if "getters_and_setters" in config:
+            self._check_and_normalize_getters_and_setters_deprecation(config, command_readers)
+
+        return config
+
+    def _check_and_normalize_getters_and_setters_deprecation(self, config, command_readers):
+        deprecation(
+            "Specifically GettersAnd_setters Command Reader through the outermost config key"
+            " 'getters_and_setters is deprecated. Please use the 'command_reader' section instead."
+            " See https://granola.readthedocs.io/en/latest/config/config.html for more details."
+        )
+
+        # Check for old form of variable substitution pre jinja
+        getters_and_setters = config["getters_and_setters"]
+        start_not_in = "variable_start_string" not in getters_and_setters
+        end_not_in = "variable_end_string" not in getters_and_setters
+        if start_not_in and end_not_in:
+            deprecation(
+                "'GettersAndSetters' variable declaration follows old format"
+                "\nSwitch to using ``Cereal``, which defaults to traditional jinja2 formatting ({{ var }}),"
+                "\nor specify explicitly your variable_start_string and variable_end_string inside"
+                " getters and setters (ex: 'variable_start_string': '`')",
+                "1.0",
+            )
+            # specify getters and setters variable start and end as the old way
+            getters_and_setters["variable_start_string"] = "`"
+            getters_and_setters["variable_end_string"] = "`"
+
+            # swap canned_queries for "command_readers": {"CannedQueries": ...}
+        command_readers["GettersAndSetters"] = config.pop("getters_and_setters")
+
+        getters = command_readers["GettersAndSetters"].get("getters", [])
+        for getter in getters:
+            if "getter" in getter:
+                deprecation(
+                    "Using 'getter' key inside"
+                    " config['getters_and_setters']['getters']['getter']"
+                    "is deprecated and will be removed in a future release."
+                    "\nSwitch to using the key 'cmd' instead.",
+                    "1.0",
+                )
+                # swap getters key for cmd
+                getter["cmd"] = getter.pop("getter")
+        setters = command_readers["GettersAndSetters"].get("setters", [])
+        for setter in setters:
+            if "setter" in setter:
+                deprecation(
+                    "Using 'setter' key inside "
+                    " config['getters_and_setters']['setters']['setter']"
+                    "is deprecated and will be removed in a future release."
+                    "\nSwitch to using the key 'cmd' instead.",
+                    "1.0",
+                )
+                # swap setters key for cmd
+                setter["cmd"] = setter.pop("setter")
+
+    def _check_and_normalize_canned_queries_deprecation(self, config, command_readers):
+        deprecation(
+            "Specifically CannedQueries Command Reader through the outermost config key 'canned_queries"
+            " is deprecated. Please use the 'command_reader' section instead."
+            " See https://granola.readthedocs.io/en/latest/config/config.html for more details.",
+            "1.0",
+        )
+        # swap canned_queries for "command_readers": {"CannedQueries": ...}
+        command_readers["CannedQueries"] = config.pop("canned_queries")
+
+        cr = command_readers["CannedQueries"]
+
+        if "files" in cr:
+            deprecation("canned_queries key 'files' has been deprecated. Use the key 'data' instead.", "1.0")
+            # swap file key for data
+            cr["data"] = cr.pop("files")
+
+        data = cr.get("data", [])
+        if isinstance(data, dict):
+            deprecation(
+                "canned_queries['data'] as a dictionary has been deprecated."
+                "\nEither use a list of files or list of dictionaries of cmds and responses instead"
+                "See configuration section of documentation.",
+                "1.0",
+            )
+            # Turn dictionary of keys mapped to their values into just a list
+            new_data = [value for value in data.values()]
+            cr["data"] = new_data
+
+    def _check_and_normalize_command_readers_deprecations(self, config, command_readers):
+        config_readers = config["command_readers"]
+        for command_reader in command_readers:
+            str_not_in = command_reader not in config_readers
+            obj_not_in = getattr(command_reader, "__name__", "") not in config_readers
+            cls_not_in = command_reader.__class__.__name__ not in config_readers
+            if str_not_in and obj_not_in and cls_not_in:
+                config["command_readers"][command_reader] = {}
+        return config
+
+    def _check_and_normalize_hook_deprecations(self, config, hooks):
+        config_hooks = config.setdefault("hooks", {})
+        for hook in hooks:
+            str_not_in = hook not in config_hooks
+            obj_not_in = getattr(hook, "__name__", "") not in config_hooks
+            cls_not_in = hook.__class__.__name__ not in config_hooks
+            if str_not_in and obj_not_in and cls_not_in:
+                config["hooks"][hook] = {}
+        return config
diff --git a/granola/serial_sniffer.py b/granola/serial_sniffer.py
index 3307699..c643956 100644
--- a/granola/serial_sniffer.py
+++ b/granola/serial_sniffer.py
@@ -17,6 +17,7 @@
     encode_escape_char,
     get_path,
     is_terminated_with,
+    make_path,
 )
 
 logger = logging.getLogger(__name__)
@@ -35,6 +36,10 @@ class SerialSniffer(Serial):
         quotechar : passed to csv.writer()
         write_terminator (str) : The character sequence used to indicate that a serial command is complete
         read_terminator (str) : The character sequence used to indicate that a serial response is complete
+
+    .. todo::
+
+        Change how this is done to the same way the Breakfast Cereal is done (init then __call__)
     """
 
     outfile = ""
@@ -54,6 +59,7 @@ def __init__(self, *args, **kwargs):
                 self.outfile = datetime.now().strftime("%Y-%m-%dT%H-%M-%S") + "_serial_commands.csv"
 
         self.outpath = get_path(self.outfile)
+        make_path(self.outpath)
 
         logger.debug("%s outpath: %s", self, self.outpath)
 
@@ -195,3 +201,15 @@ def _open_csv_writer(path, mode, **kwargs):
         yield csvwriter
     finally:
         csvfile.close()
+
+
+__doc__ = """
+``SerialSniffer`` is a class that can be used in place of pyserial Serial to track every write and read for pyserial.
+All it does is capture every incoming write and read, writes it out to a csv, and then passes it along to
+pyserial.
+
+You can easily run ``SerialSniffer`` in your code for a one off to just capture some commands for mocking later
+by replacing your pyserial Serial import with::
+
+    from granola import SerialSniffer as Serial
+"""
diff --git a/granola/utils.py b/granola/utils.py
index f4d9e5f..be193aa 100644
--- a/granola/utils.py
+++ b/granola/utils.py
@@ -131,19 +131,22 @@ def check_min_package_version(package, minimum_version, should_trunc_to_same_len
 
 def get_path(path):
     """
-    Helper function that if you pass in a path to a file, returns the absolute path.
-    If the directory to that path does not exist, then it makes all the directories needed.
+    Helper function that if you pass in a path to a file or directory, returns the absolute path.
 
     Args:
-        path (str): path to file
+        path (str): relative or absolute path to file or directory
 
     Returns:
-        str: absolute path to file
+        str: absolute path to file or directory
     """
+    return os.path.abspath(path)
+
+
+def make_path(path):
+    """Make a path if the path does not already exist"""
     dir_name = os.path.dirname(path)
-    if dir_name and not os.path.isdir(dir_name):  # TODO (madeline) make the makedirs into its own function?
+    if dir_name and not os.path.isdir(dir_name):
         os.makedirs(dir_name)
-    return os.path.abspath(path)
 
 
 def int_to_char(int_):
@@ -169,9 +172,12 @@ def is_terminated_with(input, terminator):
 
 
 def _get_subclasses(cls):
+    """Get subclasses of passed in class and return a dictionary of them"""
     return {subclass.__name__: subclass for subclass in cls.__subclasses__()}
 
 
-def deprecation(message):
+def deprecation(message, version_to_remove=None):
     warnings.simplefilter("always", DeprecationWarning)
+    if version_to_remove:
+        message += "\nScheduled to be removed on version {version}".format(version=version_to_remove)
     warnings.warn(message, DeprecationWarning)
diff --git a/pyproject.toml b/pyproject.toml
index 8a0bd2a..efd6945 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,14 +2,19 @@
 profile = "black"
 multi_line_output = 3
 py_version = 39
+skip = ["versioneer.py", "granola/_version.py"]
+skip_glob = ["*env*/*"]
+
 
 [tool.black]
 line-length = 120
+experimental-string-processing=true
 target-version = ['py27']
 # This is a regular expression (hence the \. is escaping the ., and not a literal \.)
 exclude = '''
 (
-    /(versioneer\.py|\.git
+    /(versioneer\.py
+    | \.git
     | \.hg
     | \.mypy_cache
     | \.tox
@@ -23,4 +28,10 @@ exclude = '''
     | granola/_version.py
     | versioneer.py
 )
-'''
\ No newline at end of file
+'''
+
+[tool.pytest.ini_options]
+testpaths = "granola docs"
+addopts = "--doctest-modules --doctest-glob='*.rst'"
+doctest_optionflags = "NORMALIZE_WHITESPACE"
+norecursedirs = "_build"
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index a822bcc..59bd46e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,1 +1,1 @@
--e .[test,dev,docs]
\ No newline at end of file
+-e .[test,dev,docs,examples]
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index b5f0c5d..d79b259 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,11 +41,18 @@ dev =
     pre-commit
     flake8
     black<=21.12b0;python_version>"3"
+    black[jupyter]<=21.12b0;python_version>"3"
     isort
 docs =
     myst-parser;python_version>"3"
     sphinx;python_version>"3"
     sphinx-rtd-theme;python_version>"3"
+    ipython;python_version>"3"
+    ipykernel;python_version>"3"
+    myst-nb;python_version>"3"
+examples =
+    ipython
+    jupyter
 
 [options.packages.find]
 include = granola, granola.*


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index d844f98..3b36494 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -26,30 +26,4 @@ jobs:
         python -m pip install --upgrade pip
         python -m pip install .[test]
     - name: Run Tests
-      run: python -W always::DeprecationWarning -m pytest
-
-
-repos:
-  - repo: local
-    hooks:
-      - id: pytest
-        name: pytest
-        entry: python -m pytest
-        stages: [push]
-        language: system
-        pass_filenames: false
-      - id: black
-        name: black
-        entry: python -m black granola
-        stages: [commit]
-        language: system
-      - id: isort
-        name: isort
-        entry: python -m isort granola
-        stages: [commit]
-        language: system
-      - id: flake8
-        name: flake8
-        entry: python -m flake8 granola
-        stages: [commit]
-        language: system
+      run: python -m pytest
diff --git a/granola/tests/config.json b/granola/tests/config.json
index 8df22f5..51ad7c6 100644
--- a/granola/tests/config.json
+++ b/granola/tests/config.json
@@ -1,171 +1,181 @@
 {
     "cereal": {
         "unsupported_response": "ERROR\r>",
-
-        "canned_queries": { "data": {
-            "`DEFAULT`": "data/cereal_cmds.csv",
-            "sdicmd 1 get-measurement": "data\\sensor_get_reading_cmds.csv",
-            "sdicmd 1": "data\\sensor_cmds.csv"
-            }},
-        "getters_and_setters": {
-            "default_values": {
-                "devtype": "Cereal",
-                "sn": "42",
-                "fw_ver":"0.0.0",
-                "temp": "20.0",
-                "volts": "2000.0"
-            },
-            "getters": [
-                {
-                    "cmd": "get -sn\r",
-                    "response": "{{ sn }}\r>"
-                },
-                {
-                    "cmd": "get ver\r",
-                    "response": "{{ fw_ver }}\r>"
-                },
-                {
-                    "cmd": "show\r",
-                    "response": "{{ devtype }} {{ fw_ver }} {{ sn }}\r>"
-                },
-                {
-                    "cmd": "get -temp\r",
-                    "response": "{{ temp }}\r>"
-                },
-                {
-                    "cmd": "get -tempf\r",
-                    "response": "{{ temp|float * (9/5) + 32 }}\r>"
-                },
-                {
-                    "cmd": "get volt_temp\r",
-                    "response": "{{ (temp|float + volts|float) / 2 }}"
-                }
-            ],
-            "setters": [
-                {
-                    "cmd": "set -sn {{sn}}\r",
-                    "response": "OK\r>"
-                },
-                {
-                    "cmd": "set -some dummy command {{ sn }} {{ fw_ver }}\r",
-                    "response": "OK\r>"
-                },
-                {
-                    "cmd": "set -temp {{ temp }}\r",
-                    "response": "{{ temp }}"
+        "command_readers": {
+            "GettersAndSetters": {
+                "default_values": {
+                    "devtype": "Cereal",
+                    "sn": "42",
+                    "fw_ver":"0.0.0",
+                    "temp": "20.0",
+                    "volts": "2000.0"
                 },
+                "getters": [
+                    {
+                        "cmd": "get -sn\r",
+                        "response": "{{ sn }}\r>"
+                    },
+                    {
+                        "cmd": "get ver\r",
+                        "response": "{{ fw_ver }}\r>"
+                    },
+                    {
+                        "cmd": "show\r",
+                        "response": "{{ devtype }} {{ fw_ver }} {{ sn }}\r>"
+                    },
+                    {
+                        "cmd": "get -temp\r",
+                        "response": "{{ temp }}\r>"
+                    },
+                    {
+                        "cmd": "get -tempf\r",
+                        "response": "{{ temp|float * (9/5) + 32 }}\r>"
+                    },
+                    {
+                        "cmd": "get volt_temp\r",
+                        "response": "{{ (temp|float + volts|float) / 2 }}"
+                    }
+                ],
+                "setters": [
+                    {
+                        "cmd": "set -sn {{sn}}\r",
+                        "response": "OK\r>"
+                    },
+                    {
+                        "cmd": "set -some dummy command {{ sn }} {{ fw_ver }}\r",
+                        "response": "OK\r>"
+                    },
+                    {
+                        "cmd": "set -temp {{ temp }}\r",
+                        "response": "{{ temp }}"
+                    },
+                    {
+                        "cmd": "set volt {{ volts }}\r",
+                        "response": "Volt Calculation {{ volts|float / 2 }}"
+                    }
+                ]
+            },
+            "CannedQueries": { "data": [
+                "data/cereal_cmds.csv",
+                "data\\sensor_get_reading_cmds.csv",
+                "data\\sensor_cmds.csv",
                 {
-                    "cmd": "set volt {{ volts }}\r",
-                    "response": "Volt Calculation {{ volts|float / 2 }}"
+                    "1\r": "1",
+                    "2\r": {"response": "2"},
+                    "3\r": {"response": "3", "delay": 3},
+                    "4\r": {"response": ["4a", "4b"]},
+                    "5\r": {"response": ["5a", "5b"], "delay": 5},
+                    "6\r": {"response": ["6a", "6b"], "delay": [6.1, 6.2]},
+                    "7\r": {"response": [["7a", {"delay": 7}], "7b"]},
+                    "8\r": {"response": [["8a", {"delay": 8.1}], "8b"], "delay": 8},
+                    "9\r": [["9a", {"delay": 9}], "9b"]
                 }
-            ]
+            ]}
         }
     },
 
     "just_getters_and_setters": {
-
-        "getters_and_setters": {
-            "default_values": {
-                "sn": "42",
-                "fw_ver":"123"
-            },
-            "getters": [
-                {
-                    "cmd": "get -sn\r",
-                    "response": "`sn`\r>"
+        "command_readers": {
+            "GettersAndSetters": {
+                "default_values": {
+                    "sn": "42",
+                    "fw_ver":"123"
                 },
-                {
-                    "cmd": "show\r",
-                    "response": "SOMEMACHINE `fw_ver` `sn`\r>"
-                }
-            ],
-            "setters": [
-                {
-                    "cmd": "set -sn `sn`\r",
-                    "response": "OK\r>"
-                }
-            ],
-            "variable_start_string": "`",
-            "variable_end_string": "`"
+                "getters": [
+                    {
+                        "cmd": "get -sn\r",
+                        "response": "`sn`\r>"
+                    },
+                    {
+                        "cmd": "show\r",
+                        "response": "SOMEMACHINE `fw_ver` `sn`\r>"
+                    }
+                ],
+                "setters": [
+                    {
+                        "cmd": "set -sn `sn`\r",
+                        "response": "OK\r>"
+                    }
+                ],
+                "variable_start_string": "`",
+                "variable_end_string": "`"
+            }
         }
     },
 
     "incorrect_getter": {
-        "canned_queries": {"data": {"`DEFAULT`": "data/cereal_cmds.csv"}},
-        "getters_and_setters": {
-            "default_values": {
-                "sn": "42"
-            },
-            "getters": [
-                {
-                    "cmd": "get -sn\r",
-                    "response": "{{ INCORRECT }}"
-                }
-            ]
+        "command_readers":{
+            "CannedQueries": {"data": [{"1\r": "1", "2\r": {"response": "3", "delay": 3}}]},
+            "GettersAndSetters": {
+                "default_values": {
+                    "sn": "42"
+                },
+                "getters": [
+                    {
+                        "cmd": "get -sn\r",
+                        "response": "{{ INCORRECT }}"
+                    }
+                ]
+            }
         }
     },
 
     "incorrect_setter": {
-        "canned_queries": {"data": {"`DEFAULT`": "data/cereal_cmds.csv"}},
-        "getters_and_setters": {
-            "default_values": {
-                "sn": "42"
-            },
-            "setters": [
-                {
-                    "cmd": "set -sn {{ INCORRECT }}\r",
-                    "response": "OK\r>"
-                }
-            ]
+        "command_readers":{
+            "CannedQueries": {"data": [{"1\r": "1", "2\r": {"response": "3", "delay": 3}}]},
+            "GettersAndSetters": {
+                "default_values": {
+                    "sn": "42"
+                },
+                "setters": [
+                    {
+                        "cmd": "set -sn {{ INCORRECT }}\r",
+                        "response": "OK\r>"
+                    }
+                ]
+            }
         }
     },
 
     "stick_hook": {
-        "canned_queries": {
-            "data": {
-                "`DEFAULT`": "data/cereal_cmds.csv",
-                "sdicmd 1 get-measurement": "data\\sensor_get_reading_cmds.csv",
-                "sdicmd 1": "data\\sensor_cmds.csv"
-            }
+        "command_readers":{
+            "CannedQueries": {"data": [{"1\n": "1", "2\n": {"response": "3", "delay": 3}}]}
         },
-        "hooks": [
-            {
-                "hook_type": "StickCannedQueries",
-                "attributes": ["get -volt\r"],
+        "hooks": {
+            "StickCannedQueries": {
+                "attributes": ["2\n"],
                 "include_or_exclude": "exclude"
             }
-        ]
+        },
+        "write_terminator": "\n"
     },
 
     "command_readers": {
-        "canned_queries": {
-            "data": {
-                "`DEFAULT`": "data/cereal_cmds.csv",
-                "sdicmd 1 get-measurement": "data\\sensor_get_reading_cmds.csv",
-                "sdicmd 1": "data\\sensor_cmds.csv"
-            }
-        },
-        "command_readers": [
-            {
-                "reader_type": "CannedQueries"
-            }
-        ]
+        "command_readers": {
+            "CannedQueries": {"data": ["data/cereal_cmds.csv"]}
+        }
     },
 
     "dictionary": {
-        "canned_queries": {
-            "data": {
-                "`DEFAULT`": {"get -temp\r": {"response": ["20\r>",
-                                                           "22\r>"]},
-                              "get -sn\r": "sn1234\r>",
-                              "test -off\r": {"response": "OK\r>"}}}
+        "command_readers":{
+            "CannedQueries": {
+                "data": [
+                    {"get -temp\r": {"response": ["20\r>",
+                                                  "22\r>"]},
+                     "get -sn\r": "sn1234\r>",
+                     "test -off\r": {"response": "OK\r>"}}
+                ]
+            }
         }
     },
-    "incorrect_jinja_template_for_deprecation": {
-        "getters_and_setters": {
-            "default_values": {"sn": "42"},
-            "getters": [{"cmd": "get -sn\r",
-                         "response": "`sn`\r>"}]
+
+    "incorrect_jinja_template_for_deprecation2": {
+        "command_readers":{
+            "GettersAndSetters": {
+                "default_values": {"sn": "42"},
+                "getters": [{"cmd": "get -sn\r",
+                            "response": "`sn`\r>"}]
+            }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/granola/tests/config_deprecations.json b/granola/tests/config_deprecations.json
new file mode 100644
index 0000000..fd18b9d
--- /dev/null
+++ b/granola/tests/config_deprecations.json
@@ -0,0 +1,64 @@
+{
+    "deprecated": {
+        "unsupported_response": "ERROR\r>",
+        "canned_queries": { "files": {
+            "_default_csv_": "data/cereal_cmds.csv",
+            "sdicmd 1 get-measurement": "data\\sensor_get_reading_cmds.csv",
+            "sdicmd 1": "data\\sensor_cmds.csv"
+            }},
+        "getters_and_setters": {
+            "default_values": {
+                "devtype": "Cereal",
+                "sn": "42",
+                "fw_ver":"0.0.0",
+                "temp": "20.0",
+                "volts": "2000.0"
+            },
+            "getters": [
+                {
+                    "getter": "get -sn\r",
+                    "response": "`sn`\r>"
+                },
+                {
+                    "getter": "get ver\r",
+                    "response": "`fw_ver`\r>"
+                },
+                {
+                    "getter": "show\r",
+                    "response": "`devtype` `fw_ver` `sn`\r>"
+                },
+                {
+                    "getter": "get -temp\r",
+                    "response": "`temp`\r>"
+                },
+                {
+                    "getter": "get -tempf\r",
+                    "response": "`temp|float * (9/5) + 32`\r>"
+                },
+                {
+                    "getter": "get volt_temp\r",
+                    "response": "`(temp|float + volts|float) / 2`"
+                }
+            ],
+            "setters": [
+                {
+                    "setter": "set -sn `sn`\r",
+                    "response": "OK\r>"
+                },
+                {
+                    "setter": "set -some dummy command `sn` `fw_ver`\r",
+                    "response": "OK\r>"
+                },
+                {
+                    "setter": "set -temp `temp`\r",
+                    "response": "`temp`"
+                },
+                {
+                    "setter": "set volt `volts`\r",
+                    "response": "Volt Calculation `volts|float / 2`"
+                }
+            ]
+        }
+
+    }
+}
diff --git a/granola/tests/conftest.py b/granola/tests/conftest.py
index 72315b0..b8f4e30 100644
--- a/granola/tests/conftest.py
+++ b/granola/tests/conftest.py
@@ -1,6 +1,5 @@
 import functools
 import os
-import warnings
 
 import pytest
 
@@ -14,6 +13,7 @@
 
 current_dir = os.path.dirname(os.path.realpath(__file__))
 CONFIG_PATH = os.path.join(current_dir, "config.json")
+CONFIG_PATH_DEPRECATIONS = os.path.join(current_dir, "config_deprecations.json")
 
 
 def assert_filled_all(mask):
@@ -26,8 +26,19 @@ def assert_filled_any(mask):
     assert mask.any()
 
 
-def query_device(bk_cereal, cmd):
-    bk_cereal.write(("{cmd}\r".format(cmd=cmd)).encode(bk_cereal._encoding))
+def all_equal(iterator):
+    iterator = iter(iterator)
+    try:
+        first = next(iterator)
+    except StopIteration:
+        return True
+    return all(first == x for x in iterator)
+
+
+def query_device(bk_cereal, cmd, write_terminator="\r"):
+    bk_cereal.write(
+        ("{cmd}{write_terminator}".format(cmd=cmd, write_terminator=write_terminator)).encode(bk_cereal._encoding)
+    )
     return bk_cereal.read(1000)
 
 
@@ -42,10 +53,10 @@ def check_deprecation(*msgs):
 
         @functools.wraps(func)
         def _inner(*args, **kwargs):
-            with warnings.catch_warnings(record=True) as w:
+            with pytest.deprecated_call() as dep:
                 r = func(*args, **kwargs)
-                assert issubclass(w[-1].category, Warning)
-                assert msg in str(w[-1].message).lower()
+                assert issubclass(dep.list[-1].category, Warning)
+                assert msg in str(dep.list[-1].message).lower()
             return r
 
         return _inner
@@ -54,11 +65,11 @@ def _inner(*args, **kwargs):
         def _check_deprecation(func):
             @functools.wraps(func)
             def _inner(*args, **kwargs):
-                with warnings.catch_warnings(record=True) as w:
+                with pytest.deprecated_call() as dep:
                     r = func(*args, **kwargs)
                     found_warning = False
                     for msg in msgs:
-                        for warning in w:
+                        for warning in dep.list:
                             if issubclass(warning.category, Warning) and msg.lower() in str(warning.message).lower():
                                 found_warning = True
                                 break
@@ -76,12 +87,12 @@ class SerialSnifferTester(SerialSniffer):
 
 @pytest.fixture
 def mock_cereal():
-    return Cereal.mock_from_file("cereal", config_path=CONFIG_PATH)()
+    return Cereal.mock_from_json("cereal", config_path=CONFIG_PATH)()
 
 
 @pytest.fixture
 def bk_cereal_only_getters_and_setters():
-    return Cereal.mock_from_file("just_getters_and_setters", config_path=CONFIG_PATH)()
+    return Cereal.mock_from_json("just_getters_and_setters", config_path=CONFIG_PATH)()
 
 
 @pytest.fixture
@@ -104,11 +115,11 @@ def mock_read():
 
 
 @pytest.fixture
-def canned_queries_config():
-    canned_queries = {
-        "canned_queries": {
-            "data": {
-                "`DEFAULT`": {
+def canned_queries_command_readers():
+    command_readers = {
+        "CannedQueries": {
+            "data": [
+                {
                     "1\r": "1",
                     "2\r": {"response": "2"},
                     "3\r": {"response": "3", "delay": 3},
@@ -119,8 +130,8 @@ def canned_queries_config():
                     "8\r": {"response": [["8a", {"delay": 8.1}], "8b"], "delay": 8},
                     "9\r": [["9a", {"delay": 9}], "9b"],
                 }
-            },
+            ],
             "delay": 0,
         }
     }
-    return canned_queries
+    return command_readers
diff --git a/granola/tests/serial_tests/test_canned_queries.py b/granola/tests/serial_tests/test_canned_queries.py
index 07da23f..7f980e7 100644
--- a/granola/tests/serial_tests/test_canned_queries.py
+++ b/granola/tests/serial_tests/test_canned_queries.py
@@ -42,3 +42,17 @@ def test_multipart_writes_with_canned_queries_only_fire_on_carriage_return(mock_
 
     # Then the correct result,and only the correct result is returned
     assert mock_cereal.read(1000) == output
+
+
+def test_that_you_can_use_both_files_and_dictionary_of_cmds_and_response_in_same_config(mock_cereal):
+    # Given a mock cerial device that has canned queries defined a both paths to csvs and dictionaries
+    # of cmds and responses
+
+    # When we query from a command from one of the files
+    ok = query_device(mock_cereal, "reset")
+    # one we query from a command from the dictionary of cmds
+    one = query_device(mock_cereal, "1")
+
+    # Then they both return the correct response
+    assert ok == b"OK\r>"
+    assert one == b"1"
diff --git a/granola/tests/serial_tests/test_getters_and_setters.py b/granola/tests/serial_tests/test_getters_and_setters.py
index 48c4928..3526a9b 100644
--- a/granola/tests/serial_tests/test_getters_and_setters.py
+++ b/granola/tests/serial_tests/test_getters_and_setters.py
@@ -13,7 +13,7 @@ def test_get_sn_getter_from_should_default_to_42(mock_cereal):
 
     # Then the serial number should be the default
     decoded_sn = decode_response(sn, mock_cereal)
-    true_default = mock_cereal._config["getters_and_setters"]["default_values"]["sn"] + "\r>"
+    true_default = mock_cereal._readers_["GettersAndSetters"].instrument_attributes["sn"].def_value + "\r>"
     assert true_default == decoded_sn
 
 
@@ -54,8 +54,8 @@ def test_set_command_that_sets_multiple_attributes_should_set_multiple_attribute
     query_device(mock_cereal, "set -some dummy command {new_sn} {new_fw}".format(new_sn=new_sn, new_fw=new_fw))
 
     # Then the devices instrument attributes should match those new_sn and new_fw
-    device_sn = mock_cereal._readers["GettersAndSetters"].instrument_attributes["sn"].value
-    device_fw = mock_cereal._readers["GettersAndSetters"].instrument_attributes["fw_ver"].value
+    device_sn = mock_cereal._readers_["GettersAndSetters"].instrument_attributes["sn"].value
+    device_fw = mock_cereal._readers_["GettersAndSetters"].instrument_attributes["fw_ver"].value
     assert new_sn == device_sn
     assert new_fw == device_fw
 
@@ -64,7 +64,7 @@ def test_that_a_bk_cereal_can_be_used_with_the_default_path_from_root():
     # Given a mock serial we set up from the default path
 
     # When we initialize it
-    Cereal.mock_from_file("fake device")()
+    Cereal.mock_from_json("fake device")()
     # Then It should be fine, and nothing should error out
 
 
@@ -72,14 +72,14 @@ def test_that_a_bk_cereal_can_be_used_with_the_default_path_from_root_and_use_ge
     # Given a mock serial we set up from the default path
 
     # When we initialize it and query from it
-    default_path_serial = Cereal.mock_from_file("fake device")()
+    default_path_serial = Cereal.mock_from_json("fake device")()
     value = query_device(default_path_serial, "get -my value")
     # or try an undefined command
     unsupported = query_device(default_path_serial, "garbage query")
 
     # then the returned real query should match the expected response
     decoded_value = decode_response(value, default_path_serial)
-    true_value = default_path_serial._config["getters_and_setters"]["default_values"]["my_value"]
+    true_value = default_path_serial._readers_["GettersAndSetters"].instrument_attributes["my_value"].value
     assert true_value == decoded_value
     # and the returned unsupported response should match the expected unsupported response
     true_unsupported = b"Unsupported\r>"
@@ -124,8 +124,10 @@ def test_a_bk_cereal_with_no_canned_queries_should_still_be_able_to_use_getters(
     # Then the serial number should be the default and show should return the machine name, fw version, and sn
     decoded_sn = decode_response(sn, bk_cereal_only_getters_and_setters)
     decoded_show = decode_response(show, bk_cereal_only_getters_and_setters)
-    true_sn = bk_cereal_only_getters_and_setters._config["getters_and_setters"]["default_values"]["sn"]
-    true_fw_ver = bk_cereal_only_getters_and_setters._config["getters_and_setters"]["default_values"]["fw_ver"]
+    true_sn = bk_cereal_only_getters_and_setters._readers_["GettersAndSetters"].instrument_attributes["sn"].value
+    true_fw_ver = (
+        bk_cereal_only_getters_and_setters._readers_["GettersAndSetters"].instrument_attributes["fw_ver"].value
+    )
 
     assert true_sn + "\r>" == decoded_sn
     assert "SOMEMACHINE %s %s\r>" % (true_fw_ver, true_sn) == decoded_show
@@ -181,7 +183,7 @@ def test_incorrect_getter_will_raise_value_error():
 
     # When you try to initialize it
     with pytest.raises(ValueError):
-        Cereal.mock_from_file(config_key=config_key, config_path=CONFIG_PATH)
+        Cereal.mock_from_json(config_key=config_key, config_path=CONFIG_PATH)
         # Then it should raise a value error
 
 
@@ -191,5 +193,5 @@ def test_incorrect_setter_will_raise_value_error():
 
     # When you try to initialize it
     with pytest.raises(ValueError):
-        Cereal.mock_from_file(config_key=config_key, config_path=CONFIG_PATH)
+        Cereal.mock_from_json(config_key=config_key, config_path=CONFIG_PATH)
         # Then it should raise a value error
diff --git a/granola/tests/serial_tests/test_hooks.py b/granola/tests/serial_tests/test_hooks.py
index 5d55015..7011a34 100644
--- a/granola/tests/serial_tests/test_hooks.py
+++ b/granola/tests/serial_tests/test_hooks.py
@@ -22,7 +22,7 @@ def test_loop_hook_should_loop_included_queries_only():
     # Given a mock serial with loop hook to loop over get batt\r
     loop_hook = LoopCannedQueries(attributes={"get batt\r"}, include_or_exclude="include")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[loop_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[loop_hook])
 
     # When we query a command not in our looping commands that is only entered once,
     bk_cereal.write(b"start\r")
@@ -53,7 +53,7 @@ def test_loop_hook_should_not_loop_excluded_queries():
     # Given a mock serial with loop hook to not loop over get batt\r, but no other excluded queries
     loop_hook = LoopCannedQueries(attributes={"get batt\r"}, include_or_exclude="exclude")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[loop_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[loop_hook])
 
     # When we query a our excluded response
     bk_cereal.write(b"get batt\r")
@@ -84,7 +84,7 @@ def test_loop_hook_with_no_commands_included_should_return_unsupported_after_loo
     # Given a mock serial with loop hook to include no commands
     loop_hook = LoopCannedQueries(attributes=set(), include_or_exclude="include")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[loop_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[loop_hook])
 
     # When we query any command
     bk_cereal.write(b"get batt\r")
@@ -109,7 +109,7 @@ def test_loop_hook_with_cmd_that_is_excluded_should_log_a_warning_msg(caplog):
     # Given a mock serial with loop hook to not loop over get batt\r, but no other excluded queries
     loop_hook = LoopCannedQueries(attributes={"get batt\r"}, include_or_exclude="exclude")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[loop_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[loop_hook])
 
     # When we query a our excluded response past the generator
     for _ in range(10):
@@ -125,7 +125,7 @@ def test_stick_hook_should_stick_on_command_after_the_generator_expires():
     # Given a mock serial with the stick hook to stick on adc30 3\r after the generator expires
     stick_hook = StickCannedQueries(attributes={"scan adc30 3\r"}, include_or_exclude="include")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[stick_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[stick_hook])
 
     # When we query it enough to ensure it has exhausted the generator
     for _ in range(50):
@@ -145,7 +145,7 @@ def test_approach_hook_should_set_a_greater_value_and_subsequent_gets_should_inc
     # Given a mock serial with the approach hook defined for temp attribute
     approach_hook = ApproachHook(attributes={"temp"}, include_or_exclude="include")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
 
     # When we set the temperature higher
     query_device(bk_cereal, "set -temp 25")
@@ -164,7 +164,7 @@ def test_approach_hook_should_not_impact_attributes_not_specified():
     # Given a mock serial with the approach hook defined for temp attribute
     approach_hook = ApproachHook(attributes={"temp"}, include_or_exclude="include")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
 
     # When we set the sn
     query_device(bk_cereal, "set -sn 123456789")
@@ -187,7 +187,7 @@ def test_approach_hook_should_reach_a_certain_value():
         attributes={"temp"}, include_or_exclude="include", transition_asc_scaling=time_for_1_degree
     )
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
 
     # When we set the temperature 1 degree higher
     degree_pls_1 = float(query_device(bk_cereal, "get -temp")[:-2]) + 1
@@ -206,7 +206,7 @@ def test_approach_hook_should_throw_ValueError_if_you_use_it_on_a_non_numeric_at
     # Given a mock serial with the approach hook defined for sn attribute
     approach_hook = ApproachHook(attributes={"sn"}, include_or_exclude="include")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
 
     # When we set the sn, which is a query in the approach hook
     with pytest.raises(ValueError):
@@ -218,7 +218,7 @@ def test_approach_hook_should_not_work_for_if_you_exclude_a_query():
     # Given a mock serial with the approach hook defined to exclude temp
     approach_hook = ApproachHook(attributes={"temp"}, include_or_exclude="exclude")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
 
     # When we set the temperature higher (instantly since it is excluding temp)
     query_device(bk_cereal, "set -temp 25")
@@ -236,7 +236,7 @@ def test_approach_hook_should_work_for_if_you_use_a_attribute_not_excluded():
     # Given a mock serial with the approach hook defined to exclude temp
     approach_hook = ApproachHook(attributes={"sn"}, include_or_exclude="exclude")
 
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[approach_hook])
 
     # When we set the temperature higher (not instantly since it is doesn't exclude temp)
     query_device(bk_cereal, "set -temp 25")
@@ -262,9 +262,9 @@ def hook2(hooked, result, data, **kwargs):
         return "2"
 
     # when we initialize 2 different mock serials, one with the first hook and one with the second
-    bk_cereal1 = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[hook1])
+    bk_cereal1 = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[hook1])
 
-    bk_cereal2 = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, hooks=[hook2])
+    bk_cereal2 = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, hooks=[hook2])
 
     # and we query anything from the devices
     response1 = query_device(bk_cereal1, "any command")
@@ -277,7 +277,7 @@ def hook2(hooked, result, data, **kwargs):
 
 def test_getter_setter_response_can_be_templated():
     # Given a mock serial with getters and setters defined for temp attribute, and set temp returning temp
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH)
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH)
     volts = 3000
     temp = 100
 
diff --git a/granola/tests/test_deprecations.py b/granola/tests/test_deprecations.py
index a4e9b0b..0bb4fcc 100644
--- a/granola/tests/test_deprecations.py
+++ b/granola/tests/test_deprecations.py
@@ -1,17 +1,25 @@
-from granola import Cereal
+import time
+
+from granola import (
+    ApproachHook,
+    BaseCommandReaders,
+    CannedQueries,
+    MockSerial,
+    register_hook,
+)
 from granola.tests.conftest import (
-    CONFIG_PATH,
+    CONFIG_PATH_DEPRECATIONS,
     check_deprecation,
     decode_response,
     query_device,
 )
 
 
-@check_deprecation("Instantiating Cereal with `config_path` and `config_key`")
-def test_config_key_deprecation():
+@check_deprecation("MockSerial is deprecated. Please use granola.breakfast_cereal.Cereal instead")
+def test_mock_serial_deprecation():
     # Given 2 mock serials using deprecated config_key style instantiation
-    mock1 = Cereal("just_getters_and_setters", CONFIG_PATH)
-    mock2 = Cereal(config_key="just_getters_and_setters", config_path=CONFIG_PATH)
+    mock1 = MockSerial("deprecated", config_path=CONFIG_PATH_DEPRECATIONS)
+    mock2 = MockSerial(config_key="deprecated", config_path=CONFIG_PATH_DEPRECATIONS)
 
     # When we set the serial number
     for new_sn, mock in enumerate([mock1, mock2]):
@@ -24,27 +32,26 @@ def test_config_key_deprecation():
         assert new_sn + "\r>" == decoded_sn
 
 
-@check_deprecation("canned_queries['data'] key '_default_csv_'")
+@check_deprecation(
+    "canned_queries['data'] as a dictionary has been deprecated."
+    "\nEither use a list of files or list of dictionaries of cmds and responses instead"
+)
 def test_default_df_key_deprecation():
     # Given a mock serial with deprecated "files" and "_default_csv_" keys
-    config = {"canned_queries": {"files": {"_default_csv_": {"1\r": "1", "2\r": {"response": "2"}}}, "delay": 0}}
-    # When we initialize it, internally these keys are remapped to the non deprecated version
-    mock = Cereal(config=config)()
+    mock = MockSerial("deprecated", CONFIG_PATH_DEPRECATIONS)
 
-    one = query_device(mock, "1")
-    two = query_device(mock, "2")
+    fortytwo = query_device(mock, "get -sn")
 
     # Then it should still work as expected
-    assert one == b"1"
-    assert two == b"2"
+    assert fortytwo == b"42\r>"
 
 
-@check_deprecation("'getters_and_setters' variable declaration follows old format")
+@check_deprecation("'GettersAndSetters' variable declaration follows old format")
 def test_jinja2_variable_start_and_end_deprecation():
     # Given a mock serial that passes config path and key in directly and old pre-jinja variable identifiers
 
     # When we initialize it, internally the variable identifiers are remapped to jinja ones
-    mock = Cereal(config_key="incorrect_jinja_template_for_deprecation", config_path=CONFIG_PATH)
+    mock = MockSerial(config_key="deprecated", config_path=CONFIG_PATH_DEPRECATIONS)
 
     sn = query_device(mock, "get -sn")
 
@@ -52,18 +59,10 @@ def test_jinja2_variable_start_and_end_deprecation():
     assert sn == b"42\r>"
 
 
-@check_deprecation("Using 'getter' key inside 'getter_and_setters'", "Using 'setter' key inside 'getter_and_setters'")
+@check_deprecation("Using 'getter' key inside", "Using 'setter' key inside")
 def test_getter_and_setter_keys_deprecation():
     # Given a mock serial with "getter" and "setter" keys
-    config = {
-        "getters_and_setters": {
-            "default_values": {"sn": "42"},
-            "getters": [{"getter": "get -sn\r", "response": "{{ sn }}\r>"}],
-            "setters": [{"setter": "set -sn {{ sn }}\r", "response": "OK\r>"}],
-        }
-    }
-    # When we initialize it, internally these keys are remapped to the non deprecated version
-    mock = Cereal(config=config)()
+    mock = MockSerial(config_key="deprecated", config_path=CONFIG_PATH_DEPRECATIONS)
 
     sn = query_device(mock, "get -sn")
 
@@ -71,4 +70,51 @@ def test_getter_and_setter_keys_deprecation():
     assert sn == b"42\r>"
 
 
-config = {"canned_queries": {"data": {"`DEFAULT`": {"1\r": "1", "2\r": ["2a", "2b"]}}}}
+def test_passing_in_command_readers_deprecation():
+    # Given a mock serial with 2 command readers, one of them custom
+    class CommandReader(BaseCommandReaders):
+        def get_reading(self, data):
+            if "1" in data:
+                return "Command Reader"
+
+    mock = MockSerial(
+        config_key="deprecated", config_path=CONFIG_PATH_DEPRECATIONS, command_readers=[CannedQueries, CommandReader]
+    )
+
+    sn = query_device(mock, "get -sn")
+    one = query_device(mock, "1")
+
+    # Then it should trigger our 2 different command readers
+    assert sn == b"42\r>"
+    assert one == b"Command Reader"
+
+
+def test_passing_in_hook_deprecation():
+    # Given a mock serial with 2 hooks, one of them custom
+    @register_hook(hook_type_enum="post_reading", hooked_classes=[CannedQueries])
+    def hook(self, hooked, result, data, **kwargs):
+        if "1" in data:
+            return "hook"
+        return result
+
+    approach_hook = ApproachHook(attributes={"temp"}, include_or_exclude="include")
+
+    mock = MockSerial(config_key="deprecated", config_path=CONFIG_PATH_DEPRECATIONS, hooks=[approach_hook, hook])
+
+    # When we set the temperature higher (not instantly since it is doesn't exclude temp)
+    query_device(mock, "set -temp 25")
+    # and then query it repeatedly
+    old_value = float(query_device(mock, "get -temp")[:-2])
+    for _ in range(5):
+        time.sleep(0.001)
+        # we grab the numeric part of the temp, leaving behind the write terminator
+        new_value = float(query_device(mock, "get -temp")[:-2])
+
+        # then each new value is strictly larger than the old value
+        assert new_value > old_value
+
+    # And when we query something with a 1
+    hk = query_device(mock, "1")
+
+    # Then it should trigger the custom hook and return "hook"
+    assert hk == b"hook"
diff --git a/granola/tests/test_serial_cmd_obj.py b/granola/tests/test_serial_cmd_obj.py
index c966f53..2817f5c 100644
--- a/granola/tests/test_serial_cmd_obj.py
+++ b/granola/tests/test_serial_cmd_obj.py
@@ -1,12 +1,19 @@
-from granola import Cereal
-from granola.tests.conftest import CONFIG_PATH, assert_filled_all, query_device
+import pandas as pd
 
+from granola import CannedQueries, Cereal, RandomizeResponse
+from granola.tests.conftest import (
+    CONFIG_PATH,
+    all_equal,
+    assert_filled_all,
+    query_device,
+)
 
-def test_that_you_can_pass_canned_queries_directly_instead_of_as_file_paths(canned_queries_config):
+
+def test_that_you_can_pass_canned_queries_directly_instead_of_as_file_paths(canned_queries_command_readers):
     # Given a mock pyserial class defined by a python dictionary configuration with canned queries directly
 
     # When we initialize it
-    mock = Cereal(config=canned_queries_config)()
+    mock = Cereal(command_readers=canned_queries_command_readers)()
     # and query it
     one = query_device(mock, "1")
 
@@ -14,11 +21,11 @@ def test_that_you_can_pass_canned_queries_directly_instead_of_as_file_paths(cann
     assert one == b"1"
 
 
-def test_that_you_can_pass_canned_queries_directly_with_multiple_response(canned_queries_config):
+def test_that_you_can_pass_canned_queries_directly_with_multiple_response(canned_queries_command_readers):
     # Given a mock pyserial class defined by a python dictionary configuration with canned queries directly
 
     # When we initialize it
-    mock = Cereal(config=canned_queries_config)()
+    mock = Cereal(command_readers=canned_queries_command_readers)()
     # and issue get temp twice
     a4 = query_device(mock, "4")
     b4 = query_device(mock, "4")
@@ -28,11 +35,11 @@ def test_that_you_can_pass_canned_queries_directly_with_multiple_response(canned
     assert b4 == b"4b"
 
 
-def test_that_you_can_pass_canned_queries_directly_with_multiple_commands(canned_queries_config):
+def test_that_you_can_pass_canned_queries_directly_with_multiple_commands(canned_queries_command_readers):
     # Given a mock pyserial class defined by a python dictionary configuration with canned queries directly
 
     # When we initialize it
-    mock = Cereal(config=canned_queries_config)()
+    mock = Cereal(command_readers=canned_queries_command_readers)()
     # and issue all of our serial commands
     a4 = query_device(mock, "4")
     b4 = query_device(mock, "4")
@@ -50,7 +57,7 @@ def test_that_you_can_pass_canned_queries_directly_with_multiple_commands_from_j
     # Given a mock pyserial class defined by a json dictionary configuration with canned queries directly
 
     # When we initialize it
-    mock = Cereal.mock_from_file("dictionary", config_path=CONFIG_PATH)()
+    mock = Cereal.mock_from_json("dictionary", config_path=CONFIG_PATH)()
     # and issue all of our serial commands
     twenty = query_device(mock, "get -temp")
     twenty_two = query_device(mock, "get -temp")
@@ -66,39 +73,41 @@ def test_that_you_can_pass_canned_queries_directly_with_multiple_commands_from_j
 
 def test_that_you_can_specify_a_delay_on_one_command():
     # Given a dictionary canned queries with a delay on only one command
-    config = {
-        "canned_queries": {
-            "data": {"`DEFAULT`": {"1\r": "1", "2\r": {"response": "3", "delay": 3}}},
+    command_readers = {
+        "CannedQueries": {
+            "data": [{"1\r": "1", "2\r": {"response": "3", "delay": 3}}],
         }
     }
 
     # When we initialize it
-    mock = Cereal(config=config)()
-    df = mock._readers["CannedQueries"].serial_dfs["`DEFAULT`"]
+    mock = Cereal(command_readers=command_readers)()
+    df = mock._readers_["CannedQueries"].serial_df
 
     # Then 2 has a delay of 3 but get -sn does not have any delay (nan)
     assert_filled_all(df.loc[(df.cmd == "2\r")]["delay"] == 3)
     assert_filled_all(df.loc[(df.cmd == "1\r")]["delay"].isna())
 
 
-def test_that_you_can_specify_a_delay_on_one_command_and_a_broadcasting_delay_for_the_rest(canned_queries_config):
+def test_that_you_can_specify_a_delay_on_one_command_and_a_broadcasting_delay_for_the_rest(
+    canned_queries_command_readers,
+):
     # Given a dictionary canned queries with a default delay and a specific delay
 
     # When we initialize it
-    mock = Cereal(config=canned_queries_config)()
-    df = mock._readers["CannedQueries"].serial_dfs["`DEFAULT`"]
+    mock = Cereal(command_readers=canned_queries_command_readers)()
+    df = mock._readers_["CannedQueries"].serial_df
 
     # Then 3 gets the default delay of 3, but 2 has a delay of 0 since we that is the default
     assert_filled_all(df.loc[(df.cmd == "3\r")]["delay"] == 3)
     assert_filled_all(df.loc[(df.cmd == "2\r")]["delay"] == 0)
 
 
-def test_that_you_can_specify_a_inside_a_response_list(canned_queries_config):
+def test_that_you_can_specify_a_inside_a_response_list(canned_queries_command_readers):
     # Given a dictionary canned queries with a default delay and a specific delay
 
     # When we initialize it
-    mock = Cereal(config=canned_queries_config)()
-    df = mock._readers["CannedQueries"].serial_dfs["`DEFAULT`"]
+    mock = Cereal(command_readers=canned_queries_command_readers)()
+    df = mock._readers_["CannedQueries"].serial_df
 
     # THen the delays inside lists should specify columns
     assert_filled_all(df.loc[(df.cmd == "1\r")]["delay"] == 0)
@@ -107,12 +116,14 @@ def test_that_you_can_specify_a_inside_a_response_list(canned_queries_config):
     assert_filled_all(df.loc[(df.cmd == "7\r") & (df.response == "7b")]["delay"] == 0)
 
 
-def test_that_that_all_off_the_ways_to_specify_canned_queries_inside_dicts_can_go_together(canned_queries_config):
+def test_that_that_all_off_the_ways_to_specify_canned_queries_inside_dicts_can_go_together(
+    canned_queries_command_readers,
+):
     # Given a dictionary canned queries with a default delay and a specific delay
 
     # When we initialize it
-    mock = Cereal(config=canned_queries_config)()
-    df = mock._readers["CannedQueries"].serial_dfs["`DEFAULT`"]
+    mock = Cereal(command_readers=canned_queries_command_readers)()
+    df = mock._readers_["CannedQueries"].serial_df
 
     # THen we should get get the delays inside lists should specify columns
     assert_filled_all(df.loc[(df.cmd == "1\r")]["delay"] == 0)
@@ -128,3 +139,59 @@ def test_that_that_all_off_the_ways_to_specify_canned_queries_inside_dicts_can_g
     assert_filled_all(df.loc[(df.cmd == "8\r") & (df.response == "8b")]["delay"] == 8)
     assert_filled_all(df.loc[(df.cmd == "9\r") & (df.response == "9a")]["delay"] == 9)
     assert_filled_all(df.loc[(df.cmd == "9\r") & (df.response == "9b")]["delay"] == 0)
+
+
+def test_random_responses():
+    # When we have a dataframe and and randomized response enum
+    df = pd.DataFrame(data=dict(cmd=[1, 2, 3], response=[1, 2, 3]))
+    will_randomize_responses = RandomizeResponse.randomized_w_replacement.name
+
+    # When we randomize our response 100 times
+    # (we choose 100, just to be pretty sure that it will give us difference respones, even if luck isn't on our side)
+    randomized_responses = []
+    for _ in range(100):
+        randomized_responses.append(next(CannedQueries._get_generator_from_df(df.copy(), will_randomize_responses)))
+
+    # instead of always getting the 1st response, we should get others as well
+    assert len(set(randomized_responses)) != 1
+
+
+def test_random_responses_with_removal():
+
+    # When we have a dataframe and and randomized response enum
+    df = pd.DataFrame(data=dict(cmd=[1, 2, 3], response=[1, 2, 3]))
+    will_randomize_responses = RandomizeResponse.randomize_and_remove.name
+
+    # When we randomize our response 100 times
+    # (we choose 100, just to be pretty sure that it will give us difference respones, even if luck isn't on our side)
+    randomized_responses = []
+    for _ in range(100):
+        responses = []
+        for _ in range(len(df)):
+            responses.append(next(CannedQueries._get_generator_from_df(df.copy(), will_randomize_responses)))
+        randomized_responses.append(responses)
+
+    # instead of always getting the 1st response, we should get others as well
+    for responses in randomized_responses:
+        assert len(responses) == 3
+    assert not all_equal(randomized_responses)
+
+
+def test_pass_in_randomize_response_in_canned_queries():
+    # Given a CannedQueries config with a randomized responses
+    command_readers = {
+        "CannedQueries": {"data": [{"1\r": ["1a", "1b", "1c"]}], "will_randomize_responses": "randomized_w_replacement"}
+    }
+
+    # When we query the entire list of our 1\r command 100 times
+    # (we choose 100, just to be pretty sure that it will give us difference respones, even if luck isn't on our side)
+    randomized_responses = []
+    for _ in range(100):
+        mock = Cereal(command_readers=command_readers)()
+        responses = []
+        for _ in range(3):
+            responses.append(query_device(mock, "1"))
+        randomized_responses.append(responses)
+
+    # Then the order the 3 respones is not the same accross all 100 tries
+    assert not all_equal(randomized_responses)
diff --git a/granola/tests/test_setup.py b/granola/tests/test_setup.py
index 75d3ff4..863bb64 100644
--- a/granola/tests/test_setup.py
+++ b/granola/tests/test_setup.py
@@ -2,7 +2,15 @@
 import os
 from pathlib import Path
 
-from granola import CannedQueries, Cereal, GettersAndSetters, SerialSniffer
+from granola import (
+    BaseCommandReaders,
+    CannedQueries,
+    Cereal,
+    GettersAndSetters,
+    HookTypes,
+    SerialSniffer,
+    register_hook,
+)
 from granola.tests.conftest import CONFIG_PATH, decode_response, query_device
 from granola.utils import IS_PYTHON3
 
@@ -18,7 +26,7 @@ def test_a_bk_cereal_on_COM1_should_print_that_it_on_COM1():
     com2 = "COM2"
 
     # When we initialize them and get their __str__s
-    bk_cereal = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH)(com1)
+    bk_cereal = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH)(com1)
     mock_str = str(bk_cereal)
     try:
         with patch("serial.Serial.open"):
@@ -39,7 +47,7 @@ def test_a_bk_cereal_with_no_canned_queries_should_be_fine():
     # Given a mock pyserial class defined with no canned queries in its keys
 
     # When we initialize it
-    Cereal.mock_from_file("just_getters_and_setters", config_path=CONFIG_PATH)
+    Cereal.mock_from_json("just_getters_and_setters", config_path=CONFIG_PATH)
 
     # Then it shouldn't throw any errors
 
@@ -52,7 +60,7 @@ def test_that_a_config_paths_work_when_cwd_is_changed():
         config_path = "../config.json"
 
         # When we initialize a device with canned queries
-        fake_device = Cereal.mock_from_file(config_key="fake device", config_path=config_path)
+        fake_device = Cereal.mock_from_json(config_key="fake device", config_path=config_path)
 
         # Then The canned queries are loaded from the path relative to the config, not CWD
         assert query_device(fake_device, "reset") == b"OK\r>"
@@ -64,11 +72,11 @@ def test_that_a_config_paths_work_when_cwd_is_changed():
 
 def test_a_bk_cereal_with_on_GettersAndSetters_command_readers_should_still_work_with_those_commands():
     # Given a mock pyserial class defined with just a GettersAndSetters command_readers
-    command_readers = [GettersAndSetters()]
+    command_readers = [GettersAndSetters]
     new_sn = "2.718"
 
     # When we initialize it
-    mock = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, command_readers=command_readers)()
+    mock = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, command_readers=command_readers)()
     # and issue get and set sn commands
     query_device(mock, "set -sn %s" % new_sn)
     sn = query_device(mock, "get -sn")
@@ -81,10 +89,10 @@ def test_a_bk_cereal_with_on_GettersAndSetters_command_readers_should_still_work
 
 def test_a_bk_cereal_with_on_GettersAndSetters_command_readers_should_throw_error_on_bad_query():
     # Given a mock pyserial class defined with just a GettersAndSetters command_readers
-    command_readers = [GettersAndSetters()]
+    command_readers = [GettersAndSetters]
 
     # When we initialize it
-    mock = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, command_readers=command_readers)()
+    mock = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, command_readers=command_readers)()
     # and it is passed a query not defined
     response = query_device(mock, "dummy command")
 
@@ -95,10 +103,10 @@ def test_a_bk_cereal_with_on_GettersAndSetters_command_readers_should_throw_erro
 
 def test_a_bk_cereal_with_no_command_readers_should_still_be_able_to_throw_an_unsupported_response():
     # Given a mock pyserial class defined with just a GettersAndSetters command_readers
-    command_readers = [GettersAndSetters()]
+    command_readers = [GettersAndSetters]
 
     # When we initialize it
-    mock = Cereal.mock_from_file("cereal", config_path=CONFIG_PATH, command_readers=command_readers)()
+    mock = Cereal.mock_from_json("cereal", config_path=CONFIG_PATH, command_readers=command_readers)()
     # and it is passed a query not defined
     response = query_device(mock, "dummy command")
 
@@ -109,17 +117,17 @@ def test_a_bk_cereal_with_no_command_readers_should_still_be_able_to_throw_an_un
 
 def test_that_a_python_dictionary_config_is_just_as_good_as_json():
     # Given a mock pyserial class defined by a python dictionary configuration
-    command_readers = [CannedQueries()]
-    config = {
-        "canned_queries": {
-            "data": {
-                "`DEFAULT`": Path(__file__).resolve().parent / "data/cereal_cmds.csv",
-            }
+    command_readers = [CannedQueries]
+    command_readers = {
+        CannedQueries: {
+            "data": [
+                Path(__file__).resolve().parent / "data/cereal_cmds.csv",
+            ]
         }
     }
 
     # When we initialize it
-    mock = Cereal(config=config, command_readers=command_readers)()
+    mock = Cereal(command_readers=command_readers)()
     # and issue test off
     ok = query_device(mock, "reset")
 
@@ -131,65 +139,68 @@ def test_that_a_python_dictionary_config_is_just_as_good_as_json():
 
 def test_that_a_python_dictionary_config_lets_you_use_getters_and_setters():
     # Given a mock pyserial class defined by a python dictionary configuration of getters and setters
-    command_readers = [GettersAndSetters()]
-    config = {
-        "getters_and_setters": {
-            "default_values": {
-                "sn": "42",
-            },
-            "getters": [{"cmd": "get -sn\r", "response": "{{ sn }}\r>"}],
-            "setters": [{"cmd": "set -sn {{ sn }}\r", "response": "OK\r>"}],
+    command_readers = {
+        "GettersAndSetters": {
+            "default_values": {"sn": "42"},
+            "getters": [{"cmd": "get -sn\n", "response": "{{ sn }}\n>"}],
+            "setters": [{"cmd": "set -sn {{ sn }}\n", "response": "OK\n>"}],
         }
     }
     new_sn = "2.718"
 
     # When we initialize it
-    mock = Cereal(config=config, command_readers=command_readers)()
+    mock = Cereal(command_readers=command_readers, write_terminator="\n")()
     # and issue get and set sn commands
-    query_device(mock, "set -sn %s" % new_sn)
-    sn = query_device(mock, "get -sn")
+    query_device(mock, "set -sn %s" % new_sn, write_terminator="\n")
+    sn = query_device(mock, "get -sn", write_terminator="\n")
 
     # Then the serial number should be what we set it to
-    true_response = new_sn + "\r>"
+    true_response = new_sn + "\n>"
     decoded_sn = decode_response(sn, mock)
     assert true_response == decoded_sn
 
 
 def test_a_json_config_can_specify_a_hook___stick_hook_specifically():
     # Given a mock serial with a json config that specifies a stick hook
-    bk_cereal = Cereal.mock_from_file("stick_hook", config_path=CONFIG_PATH)
+    # defined to stick on the cmd 1\r
+    bk_cereal = Cereal.mock_from_json("stick_hook", config_path=CONFIG_PATH)
 
     # When we query it enough to ensure it has exhausted the generator
     for _ in range(50):
-        bk_cereal.write(b"scan adc30 3\r")
+        bk_cereal.write(b"1\n")
         bk_cereal.read(100)
 
     responses = set()
     # Then all future queries should be the same
     for _ in range(10):
-        bk_cereal.write(b"scan adc30 3\r")
+        bk_cereal.write(b"1\n")
         responses.add(bk_cereal.read(100))
 
         assert len(responses) == 1
+        # and we get back the expected response
+        assert b"1" in responses
 
 
 def test_a_json_config_can_specify_a_hook_arguments_to_exclude_a_query___stick_hook_specifically(caplog):
-    # Given a mock serial with a json config that specifies a stick hook, but to exclude get -volt
-    bk_cereal = Cereal.mock_from_file("stick_hook", config_path=CONFIG_PATH)
+    # Given a mock serial with a json config that specifies a stick hook, but to exclude 2\r
+    bk_cereal = Cereal.mock_from_json("stick_hook", config_path=CONFIG_PATH)
 
     # When we query a our excluded response past the generator
+    responses = []
     for _ in range(10):
         with caplog.at_level(logging.WARNING):
-            bk_cereal.write(b"get -volt\r")
-            bk_cereal.read(100)
+            bk_cereal.write(b"2\n")
+            responses.append(bk_cereal.read(100))
 
-    # Then we capture in our log that there was an unhandled response from the hooks
+    # Then we capture in our log that there was an unhandled response from the hook
     assert "unhandled response return from hooks" in caplog.text
+    # and we get back the responses as an Unsupported\r> response
+    assert responses[-1] == b"Unsupported\r>"
 
 
 def test_a_json_config_can_specify_command_readers_as_well():
     # Given a mock serial with a json config that specifies a CannedQueries command reader
-    mock = Cereal.mock_from_file("command_readers", config_path=CONFIG_PATH)
+    mock = Cereal.mock_from_json("command_readers", config_path=CONFIG_PATH)
 
     # When we issue a number of commands
     ok1 = query_device(mock, "reset")
@@ -200,3 +211,54 @@ def test_a_json_config_can_specify_command_readers_as_well():
     assert ok1 == b"OK\r>"
     assert one_hund == b"100\r>"
     assert ok2 == b"OK\r>"
+
+
+def test_that_you_can_pass_in_a_custom_hook_through_the_config_dict_as_a_str_and_obj():
+    # Given custom hooks
+    @register_hook(hook_type_enum=HookTypes.post_reading, hooked_classes=[CannedQueries])
+    def hook1(self, hooked, result, data, **kwargs):
+        return "01"
+
+    @register_hook(hook_type_enum=HookTypes.post_reading, hooked_classes=[CannedQueries])
+    def hook2(self, hooked, result, data, **kwargs):
+        return "02"
+
+    # and a config dictionary that references hooks by name and by the actual object
+    hooks = {
+        "hook1": {"attributes": ["3\r"], "include_or_exclude": "include"},
+        hook2: {"attributes": ["4\r"], "include_or_exclude": "include"},
+    }
+
+    # When we initialize it and issue our serial command
+    mock = Cereal(hooks=hooks)()
+    zero1 = query_device(mock, "3")
+    zero2 = query_device(mock, "4")
+
+    # Then the hooks returns 01 and 02
+    assert zero1 == b"01"
+    assert zero2 == b"02"
+
+
+def test_that_you_can_pass_in_a_custom_command_reader_through_the_config_dict_as_a_str_and_obj():
+    # Given custom command readers
+    class CommandReader1(BaseCommandReaders):
+        def get_reading(self, data):
+            if "1" in data:
+                return "Command Reader1"
+
+    class CommandReader2(BaseCommandReaders):
+        def get_reading(self, data):
+            if "2" in data:
+                return "Command Reader2"
+
+    # and a config dictionary that references the command readers by name and by the actual object
+    command_readers = ["CommandReader1", CommandReader2]
+
+    # When we initialize it and issue our serial command
+    mock = Cereal(command_readers=command_readers)()
+    cr1 = query_device(mock, "1")
+    cr2 = query_device(mock, "2")
+
+    # Then the hooks returns b"Command Reader1" and "Command Reader2"
+    assert cr1 == b"Command Reader1"
+    assert cr2 == b"Command Reader2"
diff --git a/granola/tests/test_utils.py b/granola/tests/test_utils.py
index f6c7239..4200c1a 100644
--- a/granola/tests/test_utils.py
+++ b/granola/tests/test_utils.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from granola.utils import IS_PYTHON3, fixpath, get_path
+from granola.utils import IS_PYTHON3, fixpath, get_path, make_path
 
 if IS_PYTHON3:
     from unittest.mock import patch
@@ -37,19 +37,21 @@ def test_using_get_path_with_just_a_file_should_return_absolute_path_to_root_to_
     assert true_path == path
 
 
-def test_using_get_path_with_directories_that_dont_exist_should_create_those_directories():
+def test_using_make_path_with_directories_that_dont_exist_should_create_those_directories():
     # Given a path to a file name
     path_to_file = os.path.join("Toby", "Tali", "test.csv")
 
-    # When we call `get_path` on it
-    path = get_path(path_to_file)
+    # When we call `make_path` on it
+    make_path(path_to_file)
 
-    # Then it should have created the directories Toby and Tali
-    dir_name = os.path.dirname(path)
-    assert os.path.isdir(dir_name)
+    try:
+        # Then it should have created the directories Toby and Tali
+        dir_name = os.path.dirname(path_to_file)
+        assert os.path.isdir(dir_name)
 
-    # cleanup by removing created directories
-    shutil.rmtree("Toby")
+    finally:
+        # cleanup by removing created directories
+        shutil.rmtree("Toby")
 
 
 @pytest.mark.parametrize("path", PATHS_TO_TEST)