Repository: hed-standard/hed-python (language: python)

Issue:
Corrections to generate_sidecar_entry and some renaming
The generate_sidecar_entry did not create json sidecars that could be validated when there were categorical column values that were used in multiple columns (like 0, 1, 2)...   The generation was updated to include the column name in the dummy HED tagging.


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
index 50fbc0cfb..0ae795047 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -8,6 +8,7 @@ omit =
         **/test/
         */test*
         **/__init__.py
+        **/_version.py
 
 
 [report]
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..e79aa686a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,1 @@
+hed/_version.py export-subst
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 000000000..775fc2ddf
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include versioneer.py
+include hed/_version.py
+include requirements.txt
\ No newline at end of file
diff --git a/docs/requirements.txt b/docs/requirements.txt
index daefc74d8..cdd00a8ed 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,28 +1,29 @@
-myst-parser==0.17.0
-Pygments==2.11.2
-numpydoc==1.2
-click==8.0.4
-coverage==6.3.1
+
+Pygments==2.12.0
+numpydoc==1.3.1
+click==8.1.3
+coverage==6.3.2
 defusedxml==0.7.1
 et-xmlfile==1.1.0
-Flask==2.0.3
-Flask-WTF==1.0.0
-inflect==5.4.0
-itsdangerous==2.1.0
+Flask==2.1.2
+Flask-WTF==1.0.1
+inflect==5.5.2
+itsdangerous==2.1.2
 jdcal==1.4.1
-Jinja2==3.0.3
-MarkupSafe==2.1.0
-numpy==1.21.5
+Jinja2>=3.0.0
+MarkupSafe==2.1.1
+numpy==1.21.6
 openpyxl==3.0.9
 pandas==1.3.5
 portalocker==2.4.0
 python-dateutil==2.8.2
-pytz==2021.3
+pytz==2022.1
 semantic_version==2.9.0
 six==1.16.0
-Sphinx==4.4.0
+Sphinx>=4,<5
 SphinxExtensions==0.2.0
 sphinx_rtd_theme==1.0.0
-Werkzeug==2.0.3
+Werkzeug==2.1.2
 WTForms==3.0.1
 xlrd==2.0.1
+myst-parser==0.17.2
diff --git a/docs/source/api.rst b/docs/source/api.rst
index afafa7433..d1a038c11 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -18,7 +18,10 @@ HED models
    hed.models.DefinitionMapper
    hed.models.EventsInput
    hed.models.HedGroup
+   hed.models.HedGroupBase
+   hed.models.HedGroupFrozen
    hed.models.HedInput
+   hed.models.HedOps
    hed.models.HedString
    hed.models.HedTag
    hed.models.OnsetMapper
@@ -61,6 +64,7 @@ HED tools
    hed.tools.BidsEventFile
    hed.tools.BidsEventFiles
    hed.tools.BidsFile
+   hed.tools.BidsFileDictionary
    hed.tools.BidsJsonFile
    hed.tools.BidsSidecarFile
    hed.tools.BidsTsvDictionary
@@ -71,7 +75,6 @@ HED tools
    hed.tools.HedLogger
    hed.tools.KeyMap
    hed.tools.KeyTemplate
-   hed.tools.SummaryEntry
    hed.tools.TagSummary
    hed.tools.annotation.annotation_util
 
@@ -105,11 +108,3 @@ HED validators
 
 .. currentmodule:: hed
 
-Modules
---------------
-.. toctree::
-   :maxdepth: 6
-
-   generated/modules
-
-.. _calibration_ref2:
\ No newline at end of file
diff --git a/docs/source/generated/hed.errors.rst b/docs/source/generated/hed.errors.rst
deleted file mode 100644
index 4169973f3..000000000
--- a/docs/source/generated/hed.errors.rst
+++ /dev/null
@@ -1,53 +0,0 @@
-hed.errors package
-==================
-
-Submodules
-----------
-
-hed.errors.error\_handlers module
----------------------------------
-
-.. automodule:: hed.errors.error_handlers
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.errors.error\_messages module
----------------------------------
-
-.. automodule:: hed.errors.error_messages
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.errors.error\_reporter module
----------------------------------
-
-.. automodule:: hed.errors.error_reporter
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.errors.error\_types module
-------------------------------
-
-.. automodule:: hed.errors.error_types
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.errors.exceptions module
-----------------------------
-
-.. automodule:: hed.errors.exceptions
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: hed.errors
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/generated/hed.models.BaseInput.rst b/docs/source/generated/hed.models.BaseInput.rst
index c71e0161e..f0506a2b4 100644
--- a/docs/source/generated/hed.models.BaseInput.rst
+++ b/docs/source/generated/hed.models.BaseInput.rst
@@ -16,6 +16,7 @@
       ~BaseInput.convert_to_long
       ~BaseInput.convert_to_short
       ~BaseInput.extract_definitions
+      ~BaseInput.get_assembled
       ~BaseInput.get_def_and_mapper_issues
       ~BaseInput.get_worksheet
       ~BaseInput.iter_dataframe
@@ -24,7 +25,7 @@
       ~BaseInput.set_cell
       ~BaseInput.to_csv
       ~BaseInput.to_excel
-      ~BaseInput.update_definition_mapper_with_file
+      ~BaseInput.update_definition_mapper
       ~BaseInput.validate_file
    
    
diff --git a/docs/source/generated/hed.models.EventsInput.rst b/docs/source/generated/hed.models.EventsInput.rst
index 52117c7bf..742afe433 100644
--- a/docs/source/generated/hed.models.EventsInput.rst
+++ b/docs/source/generated/hed.models.EventsInput.rst
@@ -17,6 +17,7 @@
       ~EventsInput.convert_to_short
       ~EventsInput.create_def_mapper
       ~EventsInput.extract_definitions
+      ~EventsInput.get_assembled
       ~EventsInput.get_def_and_mapper_issues
       ~EventsInput.get_worksheet
       ~EventsInput.iter_dataframe
@@ -26,7 +27,7 @@
       ~EventsInput.set_cell
       ~EventsInput.to_csv
       ~EventsInput.to_excel
-      ~EventsInput.update_definition_mapper_with_file
+      ~EventsInput.update_definition_mapper
       ~EventsInput.validate_file
       ~EventsInput.validate_file_sidecars
    
diff --git a/docs/source/generated/hed.models.HedGroup.rst b/docs/source/generated/hed.models.HedGroup.rst
index 25b75aaef..43357cb0f 100644
--- a/docs/source/generated/hed.models.HedGroup.rst
+++ b/docs/source/generated/hed.models.HedGroup.rst
@@ -15,24 +15,24 @@
       ~HedGroup.__init__
       ~HedGroup.append
       ~HedGroup.cascade_mutable
-      ~HedGroup.check_if_in_original_tags_and_groups
+      ~HedGroup.check_if_in_original
       ~HedGroup.find_def_tags
       ~HedGroup.find_exact_tags
       ~HedGroup.find_placeholder_tag
       ~HedGroup.find_tags
+      ~HedGroup.find_tags_with_term
       ~HedGroup.get_all_groups
       ~HedGroup.get_all_tags
       ~HedGroup.get_as_form
       ~HedGroup.get_as_long
       ~HedGroup.get_as_short
-      ~HedGroup.get_direct_children
       ~HedGroup.get_frozen
       ~HedGroup.get_original_hed_string
       ~HedGroup.groups
       ~HedGroup.lower
       ~HedGroup.make_tag_mutable
-      ~HedGroup.remove_groups
-      ~HedGroup.replace_tag
+      ~HedGroup.remove
+      ~HedGroup.replace
       ~HedGroup.tags
    
    
@@ -43,6 +43,7 @@
 
    .. autosummary::
    
+      ~HedGroup.children
       ~HedGroup.is_group
       ~HedGroup.span
    
diff --git a/docs/source/generated/hed.models.HedInput.rst b/docs/source/generated/hed.models.HedInput.rst
index 638fc321d..9d39261f0 100644
--- a/docs/source/generated/hed.models.HedInput.rst
+++ b/docs/source/generated/hed.models.HedInput.rst
@@ -16,6 +16,7 @@
       ~HedInput.convert_to_long
       ~HedInput.convert_to_short
       ~HedInput.extract_definitions
+      ~HedInput.get_assembled
       ~HedInput.get_def_and_mapper_issues
       ~HedInput.get_worksheet
       ~HedInput.iter_dataframe
@@ -24,7 +25,7 @@
       ~HedInput.set_cell
       ~HedInput.to_csv
       ~HedInput.to_excel
-      ~HedInput.update_definition_mapper_with_file
+      ~HedInput.update_definition_mapper
       ~HedInput.validate_file
    
    
diff --git a/docs/source/generated/hed.models.HedString.rst b/docs/source/generated/hed.models.HedString.rst
index 3badb9a14..1772a689e 100644
--- a/docs/source/generated/hed.models.HedString.rst
+++ b/docs/source/generated/hed.models.HedString.rst
@@ -16,33 +16,32 @@
       ~HedString.append
       ~HedString.apply_funcs
       ~HedString.cascade_mutable
-      ~HedString.check_if_in_original_tags_and_groups
+      ~HedString.check_if_in_original
       ~HedString.convert_to_canonical_forms
       ~HedString.convert_to_long
       ~HedString.convert_to_original
       ~HedString.convert_to_short
-      ~HedString.create_from_other
       ~HedString.find_def_tags
       ~HedString.find_exact_tags
       ~HedString.find_placeholder_tag
       ~HedString.find_tags
+      ~HedString.find_tags_with_term
       ~HedString.find_top_level_tags
       ~HedString.get_all_groups
       ~HedString.get_all_tags
       ~HedString.get_as_form
       ~HedString.get_as_long
       ~HedString.get_as_short
-      ~HedString.get_direct_children
       ~HedString.get_frozen
       ~HedString.get_original_hed_string
       ~HedString.groups
       ~HedString.lower
       ~HedString.make_tag_mutable
       ~HedString.remove_definitions
-      ~HedString.remove_groups
-      ~HedString.replace_tag
+      ~HedString.remove
+      ~HedString.replace
       ~HedString.split_hed_string
-      ~HedString.split_hed_string_into_groups
+      ~HedString.split_into_groups
       ~HedString.tags
       ~HedString.validate
    
@@ -56,6 +55,7 @@
    
       ~HedString.CLOSING_GROUP_CHARACTER
       ~HedString.OPENING_GROUP_CHARACTER
+      ~HedString.children
       ~HedString.is_group
       ~HedString.span
    
diff --git a/docs/source/generated/hed.models.HedTag.rst b/docs/source/generated/hed.models.HedTag.rst
index 1c9014b31..d9c780f16 100644
--- a/docs/source/generated/hed.models.HedTag.rst
+++ b/docs/source/generated/hed.models.HedTag.rst
@@ -49,6 +49,7 @@
       ~HedTag.short_base_tag
       ~HedTag.short_tag
       ~HedTag.tag
+      ~HedTag.tag_terms
       ~HedTag.unit_classes
       ~HedTag.value_classes
    
diff --git a/docs/source/generated/hed.models.rst b/docs/source/generated/hed.models.rst
index e93a8b521..b62606ebe 100644
--- a/docs/source/generated/hed.models.rst
+++ b/docs/source/generated/hed.models.rst
@@ -1,133 +1,5 @@
-hed.models package
-==================
-
-Submodules
-----------
-
-hed.models.base\_input module
------------------------------
-
-.. automodule:: hed.models.base_input
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.column\_mapper module
---------------------------------
-
-.. automodule:: hed.models.column_mapper
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.column\_metadata module
-----------------------------------
-
-.. automodule:: hed.models.column_metadata
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.def\_dict module
----------------------------
-
-.. automodule:: hed.models.def_dict
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.def\_mapper module
------------------------------
-
-.. automodule:: hed.models.def_mapper
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.events\_input module
--------------------------------
-
-.. automodule:: hed.models.events_input
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.hed\_group module
-----------------------------
-
-.. automodule:: hed.models.hed_group
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.hed\_group\_base module
-----------------------------------
-
-.. automodule:: hed.models.hed_group_base
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.hed\_input module
-----------------------------
-
-.. automodule:: hed.models.hed_input
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.hed\_ops module
---------------------------
-
-.. automodule:: hed.models.hed_ops
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.hed\_string module
------------------------------
-
-.. automodule:: hed.models.hed_string
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.hed\_tag module
---------------------------
-
-.. automodule:: hed.models.hed_tag
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.model\_constants module
-----------------------------------
-
-.. automodule:: hed.models.model_constants
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.onset\_mapper module
--------------------------------
-
-.. automodule:: hed.models.onset_mapper
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.models.sidecar module
--------------------------
-
-.. automodule:: hed.models.sidecar
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
+models module
+=============
 
 .. automodule:: hed.models
-   :members:
-   :undoc-members:
-   :show-inheritance:
+   :members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.rst b/docs/source/generated/hed.rst
deleted file mode 100644
index 67afb61a2..000000000
--- a/docs/source/generated/hed.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-hed package
-===========
-
-Subpackages
------------
-
-.. toctree::
-   :maxdepth: 4
-
-   hed.errors
-   hed.models
-   hed.schema
-   hed.tools
-   hed.util
-   hed.validator
-
-Module contents
----------------
-
-.. automodule:: hed
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/generated/hed.schema.HedSchema.rst b/docs/source/generated/hed.schema.HedSchema.rst
index 301394d05..1e50993ab 100644
--- a/docs/source/generated/hed.schema.HedSchema.rst
+++ b/docs/source/generated/hed.schema.HedSchema.rst
@@ -19,7 +19,6 @@
       ~HedSchema.find_tag_entry
       ~HedSchema.get_all_schema_tags
       ~HedSchema.get_all_tag_attributes
-      ~HedSchema.get_all_tags_with_attribute
       ~HedSchema.get_all_unknown_attributes
       ~HedSchema.get_as_mediawiki_string
       ~HedSchema.get_as_xml_string
@@ -28,6 +27,7 @@
       ~HedSchema.get_tag_attribute_names
       ~HedSchema.get_tag_description
       ~HedSchema.get_tag_entry
+      ~HedSchema.get_tags_with_attribute
       ~HedSchema.get_unit_class_units
       ~HedSchema.save_as_mediawiki
       ~HedSchema.save_as_xml
diff --git a/docs/source/generated/hed.schema.HedSchemaGroup.rst b/docs/source/generated/hed.schema.HedSchemaGroup.rst
index 1a71e060c..06de09d8e 100644
--- a/docs/source/generated/hed.schema.HedSchemaGroup.rst
+++ b/docs/source/generated/hed.schema.HedSchemaGroup.rst
@@ -15,8 +15,8 @@
       ~HedSchemaGroup.__init__
       ~HedSchemaGroup.check_compliance
       ~HedSchemaGroup.find_tag_entry
-      ~HedSchemaGroup.get_all_tags_with_attribute
       ~HedSchemaGroup.get_tag_entry
+      ~HedSchemaGroup.get_tags_with_attribute
       ~HedSchemaGroup.schema_for_prefix
    
    
diff --git a/docs/source/generated/hed.schema.io.rst b/docs/source/generated/hed.schema.io.rst
deleted file mode 100644
index 0e03e1bcf..000000000
--- a/docs/source/generated/hed.schema.io.rst
+++ /dev/null
@@ -1,61 +0,0 @@
-hed.schema.io package
-=====================
-
-Submodules
-----------
-
-hed.schema.io.schema2wiki module
---------------------------------
-
-.. automodule:: hed.schema.io.schema2wiki
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.io.schema2xml module
--------------------------------
-
-.. automodule:: hed.schema.io.schema2xml
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.io.wiki2schema module
---------------------------------
-
-.. automodule:: hed.schema.io.wiki2schema
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.io.wiki\_constants module
-------------------------------------
-
-.. automodule:: hed.schema.io.wiki_constants
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.io.xml2schema module
--------------------------------
-
-.. automodule:: hed.schema.io.xml2schema
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.io.xml\_constants module
------------------------------------
-
-.. automodule:: hed.schema.io.xml_constants
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: hed.schema.io
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/generated/hed.schema.rst b/docs/source/generated/hed.schema.rst
index eb59f15c5..f0af8b746 100644
--- a/docs/source/generated/hed.schema.rst
+++ b/docs/source/generated/hed.schema.rst
@@ -1,93 +1,5 @@
-hed.schema package
-==================
-
-Subpackages
------------
-
-.. toctree::
-   :maxdepth: 4
-
-   hed.schema.io
-
-Submodules
-----------
-
-hed.schema.hed\_cache module
-----------------------------
-
-.. automodule:: hed.schema.hed_cache
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.hed\_schema module
------------------------------
-
-.. automodule:: hed.schema.hed_schema
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.hed\_schema\_constants module
-----------------------------------------
-
-.. automodule:: hed.schema.hed_schema_constants
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.hed\_schema\_entry module
-------------------------------------
-
-.. automodule:: hed.schema.hed_schema_entry
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.hed\_schema\_group module
-------------------------------------
-
-.. automodule:: hed.schema.hed_schema_group
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.hed\_schema\_io module
----------------------------------
-
-.. automodule:: hed.schema.hed_schema_io
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.hed\_schema\_section module
---------------------------------------
-
-.. automodule:: hed.schema.hed_schema_section
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.schema\_compliance module
-------------------------------------
-
-.. automodule:: hed.schema.schema_compliance
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.schema.schema\_validation\_util module
-------------------------------------------
-
-.. automodule:: hed.schema.schema_validation_util
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
+schema module
+=============
 
 .. automodule:: hed.schema
-   :members:
-   :undoc-members:
-   :show-inheritance:
+   :members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsFileDictionary.rst b/docs/source/generated/hed.tools.BidsFileDictionary.rst
new file mode 100644
index 000000000..bbe0630e3
--- /dev/null
+++ b/docs/source/generated/hed.tools.BidsFileDictionary.rst
@@ -0,0 +1,47 @@
+﻿hed.tools.BidsFileDictionary
+============================
+
+.. currentmodule:: hed.tools
+
+.. autoclass:: hed.tools.BidsFileDictionary
+   
+   .. automethod:: __init__
+
+   
+   .. rubric:: Methods
+
+   .. autosummary::
+   
+      ~BidsFileDictionary.__init__
+      ~BidsFileDictionary.create_file_dict
+      ~BidsFileDictionary.create_split_dict
+      ~BidsFileDictionary.get_file_path
+      ~BidsFileDictionary.iter_files
+      ~BidsFileDictionary.key_diffs
+      ~BidsFileDictionary.make_bids_file_dict
+      ~BidsFileDictionary.make_file_dict
+      ~BidsFileDictionary.make_key
+      ~BidsFileDictionary.make_query
+      ~BidsFileDictionary.match_query
+      ~BidsFileDictionary.output_files
+      ~BidsFileDictionary.split_dict_by_entity
+   
+   
+
+   
+   
+   .. rubric:: Attributes
+
+   .. autosummary::
+   
+      ~BidsFileDictionary.file_list
+      ~BidsFileDictionary.key_list
+      ~BidsFileDictionary.name
+   
+   
+
+.. autoclass:: hed.tools.BidsFileDictionary
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.BidsTsvDictionary.rst b/docs/source/generated/hed.tools.BidsTsvDictionary.rst
index 59e8f6c0f..28b5309fc 100644
--- a/docs/source/generated/hed.tools.BidsTsvDictionary.rst
+++ b/docs/source/generated/hed.tools.BidsTsvDictionary.rst
@@ -14,7 +14,10 @@
    
       ~BidsTsvDictionary.__init__
       ~BidsTsvDictionary.count_diffs
+      ~BidsTsvDictionary.create_file_dict
       ~BidsTsvDictionary.create_split_dict
+      ~BidsTsvDictionary.get_file_path
+      ~BidsTsvDictionary.get_info
       ~BidsTsvDictionary.iter_files
       ~BidsTsvDictionary.iter_tsv_info
       ~BidsTsvDictionary.key_diffs
@@ -23,7 +26,7 @@
       ~BidsTsvDictionary.make_key
       ~BidsTsvDictionary.make_query
       ~BidsTsvDictionary.match_query
-      ~BidsTsvDictionary.print_files
+      ~BidsTsvDictionary.output_files
       ~BidsTsvDictionary.split_dict_by_entity
    
    
@@ -36,6 +39,7 @@
    
       ~BidsTsvDictionary.file_list
       ~BidsTsvDictionary.key_list
+      ~BidsTsvDictionary.name
    
    
 
diff --git a/docs/source/generated/hed.tools.BidsTsvSummary.rst b/docs/source/generated/hed.tools.BidsTsvSummary.rst
index 81f37c177..7969b042e 100644
--- a/docs/source/generated/hed.tools.BidsTsvSummary.rst
+++ b/docs/source/generated/hed.tools.BidsTsvSummary.rst
@@ -14,9 +14,9 @@
    
       ~BidsTsvSummary.__init__
       ~BidsTsvSummary.extract_sidecar_template
+      ~BidsTsvSummary.get_columns_info
       ~BidsTsvSummary.get_number_unique_values
       ~BidsTsvSummary.make_combined_dicts
-      ~BidsTsvSummary.print
       ~BidsTsvSummary.update
       ~BidsTsvSummary.update_summary
    
diff --git a/docs/source/generated/hed.tools.FileDictionary.rst b/docs/source/generated/hed.tools.FileDictionary.rst
index 7d91f726f..4913e3ded 100644
--- a/docs/source/generated/hed.tools.FileDictionary.rst
+++ b/docs/source/generated/hed.tools.FileDictionary.rst
@@ -13,11 +13,13 @@
    .. autosummary::
    
       ~FileDictionary.__init__
+      ~FileDictionary.create_file_dict
+      ~FileDictionary.get_file_path
       ~FileDictionary.iter_files
       ~FileDictionary.key_diffs
       ~FileDictionary.make_file_dict
       ~FileDictionary.make_key
-      ~FileDictionary.print_files
+      ~FileDictionary.output_files
    
    
 
@@ -29,6 +31,7 @@
    
       ~FileDictionary.file_list
       ~FileDictionary.key_list
+      ~FileDictionary.name
    
    
 
diff --git a/docs/source/generated/hed.tools.HedLogger.rst b/docs/source/generated/hed.tools.HedLogger.rst
index 34d44aec9..1c4452628 100644
--- a/docs/source/generated/hed.tools.HedLogger.rst
+++ b/docs/source/generated/hed.tools.HedLogger.rst
@@ -16,7 +16,10 @@
       ~HedLogger.add
       ~HedLogger.get_log
       ~HedLogger.get_log_keys
+      ~HedLogger.get_log_string
+      ~HedLogger.load_log
       ~HedLogger.print_log
+      ~HedLogger.save_log
    
    
 
diff --git a/docs/source/generated/hed.tools.KeyTemplate.rst b/docs/source/generated/hed.tools.KeyTemplate.rst
index 315763f65..e621bfd60 100644
--- a/docs/source/generated/hed.tools.KeyTemplate.rst
+++ b/docs/source/generated/hed.tools.KeyTemplate.rst
@@ -14,7 +14,6 @@
    
       ~KeyTemplate.__init__
       ~KeyTemplate.make_template
-      ~KeyTemplate.print
       ~KeyTemplate.resort
       ~KeyTemplate.update
       ~KeyTemplate.update_by_tuple
diff --git a/docs/source/generated/hed.tools.SummaryEntry.rst b/docs/source/generated/hed.tools.SummaryEntry.rst
deleted file mode 100644
index 14792b40e..000000000
--- a/docs/source/generated/hed.tools.SummaryEntry.rst
+++ /dev/null
@@ -1,30 +0,0 @@
-﻿hed.tools.SummaryEntry
-======================
-
-.. currentmodule:: hed.tools
-
-.. autoclass:: hed.tools.SummaryEntry
-   
-   .. automethod:: __init__
-
-   
-   .. rubric:: Methods
-
-   .. autosummary::
-   
-      ~SummaryEntry.__init__
-      ~SummaryEntry.extract_anchored_group
-      ~SummaryEntry.extract_summary_info
-      ~SummaryEntry.separate_anchored_groups
-   
-   
-
-   
-   
-   
-
-.. autoclass:: hed.tools.SummaryEntry
-   :members:
-   :undoc-members:
-   :show-inheritance:
-   :inherited-members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.tools.TagSummary.rst b/docs/source/generated/hed.tools.TagSummary.rst
index 74a57ecd7..386351ec2 100644
--- a/docs/source/generated/hed.tools.TagSummary.rst
+++ b/docs/source/generated/hed.tools.TagSummary.rst
@@ -13,6 +13,7 @@
    .. autosummary::
    
       ~TagSummary.__init__
+      ~TagSummary.extract_summary_info
       ~TagSummary.get_design_matrices
    
    
diff --git a/docs/source/generated/hed.tools.annotation.annotation_util.rst b/docs/source/generated/hed.tools.annotation.annotation_util.rst
index d5afc911c..bf5234a21 100644
--- a/docs/source/generated/hed.tools.annotation.annotation_util.rst
+++ b/docs/source/generated/hed.tools.annotation.annotation_util.rst
@@ -15,10 +15,12 @@
    
       check_df_columns
       df_to_hed
-      extract_tag
+      extract_tags
       generate_sidecar_entry
       hed_to_df
       merge_hed_dict
+      trim_back
+      trim_front
    
    
 
diff --git a/docs/source/generated/hed.tools.annotation.rst b/docs/source/generated/hed.tools.annotation.rst
deleted file mode 100644
index 444194043..000000000
--- a/docs/source/generated/hed.tools.annotation.rst
+++ /dev/null
@@ -1,77 +0,0 @@
-hed.tools.annotation package
-============================
-
-Submodules
-----------
-
-hed.tools.annotation.annotation\_util module
---------------------------------------------
-
-.. automodule:: hed.tools.annotation.annotation_util
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.annotation.file\_dictionary module
---------------------------------------------
-
-.. automodule:: hed.tools.annotation.file_dictionary
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.annotation.key\_map module
-------------------------------------
-
-.. automodule:: hed.tools.annotation.key_map
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.annotation.key\_template module
------------------------------------------
-
-.. automodule:: hed.tools.annotation.key_template
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.annotation.map\_summary module
-----------------------------------------
-
-.. automodule:: hed.tools.annotation.map_summary
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.annotation.summary\_entry module
-------------------------------------------
-
-.. automodule:: hed.tools.annotation.summary_entry
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.annotation.summary\_util module
------------------------------------------
-
-.. automodule:: hed.tools.annotation.summary_util
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.annotation.tag\_summary module
-----------------------------------------
-
-.. automodule:: hed.tools.annotation.tag_summary
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: hed.tools.annotation
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/generated/hed.tools.bids.rst b/docs/source/generated/hed.tools.bids.rst
deleted file mode 100644
index 80ce88ade..000000000
--- a/docs/source/generated/hed.tools.bids.rst
+++ /dev/null
@@ -1,101 +0,0 @@
-hed.tools.bids package
-======================
-
-Submodules
-----------
-
-hed.tools.bids.bids\_dataset module
------------------------------------
-
-.. automodule:: hed.tools.bids.bids_dataset
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_dataset\_summary module
---------------------------------------------
-
-.. automodule:: hed.tools.bids.bids_dataset_summary
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_event\_file module
----------------------------------------
-
-.. automodule:: hed.tools.bids.bids_event_file
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_event\_files module
-----------------------------------------
-
-.. automodule:: hed.tools.bids.bids_event_files
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_file module
---------------------------------
-
-.. automodule:: hed.tools.bids.bids_file
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_file\_dictionary module
---------------------------------------------
-
-.. automodule:: hed.tools.bids.bids_file_dictionary
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_json\_file module
---------------------------------------
-
-.. automodule:: hed.tools.bids.bids_json_file
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_sidecar\_file module
------------------------------------------
-
-.. automodule:: hed.tools.bids.bids_sidecar_file
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_tsv\_dictionary module
--------------------------------------------
-
-.. automodule:: hed.tools.bids.bids_tsv_dictionary
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_tsv\_file module
--------------------------------------
-
-.. automodule:: hed.tools.bids.bids_tsv_file
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.tools.bids.bids\_tsv\_summary module
-----------------------------------------
-
-.. automodule:: hed.tools.bids.bids_tsv_summary
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: hed.tools.bids
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/generated/hed.tools.rst b/docs/source/generated/hed.tools.rst
index 1c32b3cd2..f748d902a 100644
--- a/docs/source/generated/hed.tools.rst
+++ b/docs/source/generated/hed.tools.rst
@@ -1,30 +1,5 @@
-hed.tools package
-=================
-
-Subpackages
------------
-
-.. toctree::
-   :maxdepth: 4
-
-   hed.tools.annotation
-   hed.tools.bids
-
-Submodules
-----------
-
-hed.tools.hed\_logger module
-----------------------------
-
-.. automodule:: hed.tools.hed_logger
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
+tools module
+=============
 
 .. automodule:: hed.tools
-   :members:
-   :undoc-members:
-   :show-inheritance:
+   :members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.util.data_util.rst b/docs/source/generated/hed.util.data_util.rst
index 87fce9596..4d03d5d80 100644
--- a/docs/source/generated/hed.util.data_util.rst
+++ b/docs/source/generated/hed.util.data_util.rst
@@ -20,6 +20,7 @@
       get_key_hash
       get_new_dataframe
       get_row_hash
+      get_value_dict
       make_info_dataframe
       remove_quotes
       reorder_columns
diff --git a/docs/source/generated/hed.util.file_util.rst b/docs/source/generated/hed.util.file_util.rst
index 5f511e740..f42cdd1b8 100644
--- a/docs/source/generated/hed.util.file_util.rst
+++ b/docs/source/generated/hed.util.file_util.rst
@@ -20,7 +20,6 @@
       url_to_string
       write_errors_to_file
       write_strings_to_file
-      write_text_iter_to_file
       write_xml_tree_2_xml_file
    
    
diff --git a/docs/source/generated/hed.util.io_util.rst b/docs/source/generated/hed.util.io_util.rst
index 4df3b8fd8..6b9336293 100644
--- a/docs/source/generated/hed.util.io_util.rst
+++ b/docs/source/generated/hed.util.io_util.rst
@@ -14,11 +14,13 @@
    .. autosummary::
    
       check_filename
+      extract_suffix_path
       generate_filename
       get_dir_dictionary
       get_file_list
       get_filtered_list
       get_path_components
+      make_path
       parse_bids_filename
    
    
diff --git a/docs/source/generated/hed.util.rst b/docs/source/generated/hed.util.rst
index 7b09615c2..31ead42cd 100644
--- a/docs/source/generated/hed.util.rst
+++ b/docs/source/generated/hed.util.rst
@@ -1,37 +1,5 @@
-hed.util package
-================
-
-Submodules
-----------
-
-hed.util.data\_util module
---------------------------
-
-.. automodule:: hed.util.data_util
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.util.file\_util module
---------------------------
-
-.. automodule:: hed.util.file_util
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.util.io\_util module
-------------------------
-
-.. automodule:: hed.util.io_util
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
+util module
+=============
 
 .. automodule:: hed.util
-   :members:
-   :undoc-members:
-   :show-inheritance:
+   :members:
\ No newline at end of file
diff --git a/docs/source/generated/hed.validator.rst b/docs/source/generated/hed.validator.rst
index 0a11a23e3..b62606ebe 100644
--- a/docs/source/generated/hed.validator.rst
+++ b/docs/source/generated/hed.validator.rst
@@ -1,37 +1,5 @@
-hed.validator package
-=====================
+models module
+=============
 
-Submodules
-----------
-
-hed.validator.hed\_validator module
------------------------------------
-
-.. automodule:: hed.validator.hed_validator
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.validator.tag\_validator module
------------------------------------
-
-.. automodule:: hed.validator.tag_validator
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-hed.validator.tag\_validator\_util module
------------------------------------------
-
-.. automodule:: hed.validator.tag_validator_util
-   :members:
-   :undoc-members:
-   :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: hed.validator
-   :members:
-   :undoc-members:
-   :show-inheritance:
+.. automodule:: hed.models
+   :members:
\ No newline at end of file
diff --git a/docs/source/generated/modules.rst b/docs/source/generated/modules.rst
deleted file mode 100644
index 4715c82f2..000000000
--- a/docs/source/generated/modules.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-hed-python
-==========
-
-.. toctree::
-   :maxdepth: 4
-
-   hed
diff --git a/hed/__init__.py b/hed/__init__.py
index df20a62f8..3adb69b30 100644
--- a/hed/__init__.py
+++ b/hed/__init__.py
@@ -24,3 +24,6 @@
 #     "util",
 #     "validator"
 # ]
+
+from . import _version
+__version__ = _version.get_versions()['version']
diff --git a/hed/_version.py b/hed/_version.py
new file mode 100644
index 000000000..da383d668
--- /dev/null
+++ b/hed/_version.py
@@ -0,0 +1,658 @@
+
+# This file helps to compute a version number in source trees obtained from
+# git-archive tarball (such as those provided by githubs download-from-tag
+# feature). Distribution tarballs (built by setup.py sdist) and build
+# directories (produced by setup.py build) will contain a much shorter file
+# that just contains the computed version number.
+
+# This file is released into the public domain. Generated by
+# versioneer-0.22 (https://github.com/python-versioneer/python-versioneer)
+
+"""Git implementation of _version.py."""
+
+import errno
+import os
+import re
+import subprocess
+import sys
+from typing import Callable, Dict
+import functools
+
+
+def get_keywords():
+    """Get the keywords needed to look up the version information."""
+    # these strings will be replaced by git during git-archive.
+    # setup.py/versioneer.py will grep for the variable names, so they must
+    # each be defined on a line of their own. _version.py will just call
+    # get_keywords().
+    git_refnames = "$Format:%d$"
+    git_full = "$Format:%H$"
+    git_date = "$Format:%ci$"
+    keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
+    return keywords
+
+
+class VersioneerConfig:
+    """Container for Versioneer configuration parameters."""
+
+
+def get_config():
+    """Create, populate and return the VersioneerConfig() object."""
+    # these strings are filled in when 'setup.py versioneer' creates
+    # _version.py
+    cfg = VersioneerConfig()
+    cfg.VCS = "git"
+    cfg.style = "pep440"
+    cfg.tag_prefix = ""
+    cfg.parentdir_prefix = "hedtools-"
+    cfg.versionfile_source = "hed/_version.py"
+    cfg.verbose = False
+    return cfg
+
+
+class NotThisMethod(Exception):
+    """Exception raised if a method is not valid for the current scenario."""
+
+
+LONG_VERSION_PY: Dict[str, str] = {}
+HANDLERS: Dict[str, Dict[str, Callable]] = {}
+
+
+def register_vcs_handler(vcs, method):  # decorator
+    """Create decorator to mark a method as the handler of a VCS."""
+    def decorate(f):
+        """Store f in HANDLERS[vcs][method]."""
+        if vcs not in HANDLERS:
+            HANDLERS[vcs] = {}
+        HANDLERS[vcs][method] = f
+        return f
+    return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
+                env=None):
+    """Call the given command(s)."""
+    assert isinstance(commands, list)
+    process = None
+
+    popen_kwargs = {}
+    if sys.platform == "win32":
+        # This hides the console window if pythonw.exe is used
+        startupinfo = subprocess.STARTUPINFO()
+        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+        popen_kwargs["startupinfo"] = startupinfo
+
+    for command in commands:
+        try:
+            dispcmd = str([command] + args)
+            # remember shell=False, so use git.cmd on windows, not just git
+            process = subprocess.Popen([command] + args, cwd=cwd, env=env,
+                                       stdout=subprocess.PIPE,
+                                       stderr=(subprocess.PIPE if hide_stderr
+                                               else None), **popen_kwargs)
+            break
+        except OSError:
+            e = sys.exc_info()[1]
+            if e.errno == errno.ENOENT:
+                continue
+            if verbose:
+                print("unable to run %s" % dispcmd)
+                print(e)
+            return None, None
+    else:
+        if verbose:
+            print("unable to find command, tried %s" % (commands,))
+        return None, None
+    stdout = process.communicate()[0].strip().decode()
+    if process.returncode != 0:
+        if verbose:
+            print("unable to run %s (error)" % dispcmd)
+            print("stdout was %s" % stdout)
+        return None, process.returncode
+    return stdout, process.returncode
+
+
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+    """Try to determine the version from the parent directory name.
+
+    Source tarballs conventionally unpack into a directory that includes both
+    the project name and a version string. We will also support searching up
+    two directory levels for an appropriately named parent directory
+    """
+    rootdirs = []
+
+    for _ in range(3):
+        dirname = os.path.basename(root)
+        if dirname.startswith(parentdir_prefix):
+            return {"version": dirname[len(parentdir_prefix):],
+                    "full-revisionid": None,
+                    "dirty": False, "error": None, "date": None}
+        rootdirs.append(root)
+        root = os.path.dirname(root)  # up a level
+
+    if verbose:
+        print("Tried directories %s but none started with prefix %s" %
+              (str(rootdirs), parentdir_prefix))
+    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+
+
+@register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+    """Extract version information from the given file."""
+    # the code embedded in _version.py can just fetch the value of these
+    # keywords. When used from setup.py, we don't want to import _version.py,
+    # so we do it with a regexp instead. This function is not used from
+    # _version.py.
+    keywords = {}
+    try:
+        with open(versionfile_abs, "r") as fobj:
+            for line in fobj:
+                if line.strip().startswith("git_refnames ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["refnames"] = mo.group(1)
+                if line.strip().startswith("git_full ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["full"] = mo.group(1)
+                if line.strip().startswith("git_date ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["date"] = mo.group(1)
+    except OSError:
+        pass
+    return keywords
+
+
+@register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+    """Get version information from git keywords."""
+    if "refnames" not in keywords:
+        raise NotThisMethod("Short version file found")
+    date = keywords.get("date")
+    if date is not None:
+        # Use only the last line.  Previous lines may contain GPG signature
+        # information.
+        date = date.splitlines()[-1]
+
+        # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
+        # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
+        # -like" string, which we must then edit to make compliant), because
+        # it's been around since git-1.5.3, and it's too difficult to
+        # discover which version we're using, or to work around using an
+        # older one.
+        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
+    refnames = keywords["refnames"].strip()
+    if refnames.startswith("$Format"):
+        if verbose:
+            print("keywords are unexpanded, not using")
+        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
+    refs = {r.strip() for r in refnames.strip("()").split(",")}
+    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+    TAG = "tag: "
+    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
+    if not tags:
+        # Either we're using git < 1.8.3, or there really are no tags. We use
+        # a heuristic: assume all version tags have a digit. The old git %d
+        # expansion behaves like git log --decorate=short and strips out the
+        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
+        # between branches and tags. By ignoring refnames without digits, we
+        # filter out many common branch names like "release" and
+        # "stabilization", as well as "HEAD" and "master".
+        tags = {r for r in refs if re.search(r'\d', r)}
+        if verbose:
+            print("discarding '%s', no digits" % ",".join(refs - tags))
+    if verbose:
+        print("likely tags: %s" % ",".join(sorted(tags)))
+    for ref in sorted(tags):
+        # sorting will prefer e.g. "2.0" over "2.0rc1"
+        if ref.startswith(tag_prefix):
+            r = ref[len(tag_prefix):]
+            # Filter out refs that exactly match prefix or that don't start
+            # with a number once the prefix is stripped (mostly a concern
+            # when prefix is '')
+            if not re.match(r'\d', r):
+                continue
+            if verbose:
+                print("picking %s" % r)
+            return {"version": r,
+                    "full-revisionid": keywords["full"].strip(),
+                    "dirty": False, "error": None,
+                    "date": date}
+    # no suitable tags, so version is "0+unknown", but full hex is still there
+    if verbose:
+        print("no suitable tags, using unknown + full revision id")
+    return {"version": "0+unknown",
+            "full-revisionid": keywords["full"].strip(),
+            "dirty": False, "error": "no suitable tags", "date": None}
+
+
+@register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
+    """Get version from 'git describe' in the root of the source tree.
+
+    This only gets called if the git-archive 'subst' keywords were *not*
+    expanded, and _version.py hasn't already been rewritten with a short
+    version string, meaning we're inside a checked out source tree.
+    """
+    GITS = ["git"]
+    if sys.platform == "win32":
+        GITS = ["git.cmd", "git.exe"]
+
+    # GIT_DIR can interfere with correct operation of Versioneer.
+    # It may be intended to be passed to the Versioneer-versioned project,
+    # but that should not change where we get our version from.
+    env = os.environ.copy()
+    env.pop("GIT_DIR", None)
+    runner = functools.partial(runner, env=env)
+
+    _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
+                   hide_stderr=True)
+    if rc != 0:
+        if verbose:
+            print("Directory %s not under git control" % root)
+        raise NotThisMethod("'git rev-parse --git-dir' returned error")
+
+    MATCH_ARGS = ["--match", "%s*" % tag_prefix] if tag_prefix else []
+
+    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+    # if there isn't one, this yields HEX[-dirty] (no NUM)
+    describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty",
+                                     "--always", "--long", *MATCH_ARGS],
+                              cwd=root)
+    # --long was added in git-1.5.5
+    if describe_out is None:
+        raise NotThisMethod("'git describe' failed")
+    describe_out = describe_out.strip()
+    full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
+    if full_out is None:
+        raise NotThisMethod("'git rev-parse' failed")
+    full_out = full_out.strip()
+
+    pieces = {}
+    pieces["long"] = full_out
+    pieces["short"] = full_out[:7]  # maybe improved later
+    pieces["error"] = None
+
+    branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
+                             cwd=root)
+    # --abbrev-ref was added in git-1.6.3
+    if rc != 0 or branch_name is None:
+        raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
+    branch_name = branch_name.strip()
+
+    if branch_name == "HEAD":
+        # If we aren't exactly on a branch, pick a branch which represents
+        # the current commit. If all else fails, we are on a branchless
+        # commit.
+        branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
+        # --contains was added in git-1.5.4
+        if rc != 0 or branches is None:
+            raise NotThisMethod("'git branch --contains' returned error")
+        branches = branches.split("\n")
+
+        # Remove the first line if we're running detached
+        if "(" in branches[0]:
+            branches.pop(0)
+
+        # Strip off the leading "* " from the list of branches.
+        branches = [branch[2:] for branch in branches]
+        if "master" in branches:
+            branch_name = "master"
+        elif not branches:
+            branch_name = None
+        else:
+            # Pick the first branch that is returned. Good or bad.
+            branch_name = branches[0]
+
+    pieces["branch"] = branch_name
+
+    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+    # TAG might have hyphens.
+    git_describe = describe_out
+
+    # look for -dirty suffix
+    dirty = git_describe.endswith("-dirty")
+    pieces["dirty"] = dirty
+    if dirty:
+        git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+    # now we have TAG-NUM-gHEX or HEX
+
+    if "-" in git_describe:
+        # TAG-NUM-gHEX
+        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+        if not mo:
+            # unparsable. Maybe git-describe is misbehaving?
+            pieces["error"] = ("unable to parse git-describe output: '%s'"
+                               % describe_out)
+            return pieces
+
+        # tag
+        full_tag = mo.group(1)
+        if not full_tag.startswith(tag_prefix):
+            if verbose:
+                fmt = "tag '%s' doesn't start with prefix '%s'"
+                print(fmt % (full_tag, tag_prefix))
+            pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
+                               % (full_tag, tag_prefix))
+            return pieces
+        pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+        # distance: number of commits since tag
+        pieces["distance"] = int(mo.group(2))
+
+        # commit: short hex revision ID
+        pieces["short"] = mo.group(3)
+
+    else:
+        # HEX: no tags
+        pieces["closest-tag"] = None
+        count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
+        pieces["distance"] = int(count_out)  # total number of commits
+
+    # commit date: see ISO-8601 comment in git_versions_from_keywords()
+    date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
+    # Use only the last line.  Previous lines may contain GPG signature
+    # information.
+    date = date.splitlines()[-1]
+    pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
+
+    return pieces
+
+
+def plus_or_dot(pieces):
+    """Return a + if we don't already have one, else return a ."""
+    if "+" in pieces.get("closest-tag", ""):
+        return "."
+    return "+"
+
+
+def render_pep440(pieces):
+    """Build up version string, with post-release "local version identifier".
+
+    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+    Exceptions:
+    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += plus_or_dot(pieces)
+            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0+untagged.%d.g%s" % (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_branch(pieces):
+    """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
+
+    The ".dev0" means not master branch. Note that .dev0 sorts backwards
+    (a feature branch will appear "older" than the master branch).
+
+    Exceptions:
+    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            if pieces["branch"] != "master":
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0"
+        if pieces["branch"] != "master":
+            rendered += ".dev0"
+        rendered += "+untagged.%d.g%s" % (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def pep440_split_post(ver):
+    """Split pep440 version string at the post-release segment.
+
+    Returns the release segments before the post-release and the
+    post-release version number (or -1 if no post-release segment is present).
+    """
+    vc = str.split(ver, ".post")
+    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
+
+
+def render_pep440_pre(pieces):
+    """TAG[.postN.devDISTANCE] -- No -dirty.
+
+    Exceptions:
+    1: no tags. 0.post0.devDISTANCE
+    """
+    if pieces["closest-tag"]:
+        if pieces["distance"]:
+            # update the post release segment
+            tag_version, post_version = pep440_split_post(pieces["closest-tag"])
+            rendered = tag_version
+            if post_version is not None:
+                rendered += ".post%d.dev%d" % (post_version+1, pieces["distance"])
+            else:
+                rendered += ".post0.dev%d" % (pieces["distance"])
+        else:
+            # no commits, use the tag as the version
+            rendered = pieces["closest-tag"]
+    else:
+        # exception #1
+        rendered = "0.post0.dev%d" % pieces["distance"]
+    return rendered
+
+
+def render_pep440_post(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+    The ".dev0" means dirty. Note that .dev0 sorts backwards
+    (a dirty tree will appear "older" than the corresponding clean one),
+    but you shouldn't be releasing software with -dirty anyways.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%s" % pieces["short"]
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+        rendered += "+g%s" % pieces["short"]
+    return rendered
+
+
+def render_pep440_post_branch(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
+
+    The ".dev0" means not master branch.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["branch"] != "master":
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%s" % pieces["short"]
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["branch"] != "master":
+            rendered += ".dev0"
+        rendered += "+g%s" % pieces["short"]
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_old(pieces):
+    """TAG[.postDISTANCE[.dev0]] .
+
+    The ".dev0" means dirty.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+    return rendered
+
+
+def render_git_describe(pieces):
+    """TAG[-DISTANCE-gHEX][-dirty].
+
+    Like 'git describe --tags --dirty --always'.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render_git_describe_long(pieces):
+    """TAG-DISTANCE-gHEX[-dirty].
+
+    Like 'git describe --tags --dirty --always -long'.
+    The distance/hash is unconditional.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render(pieces, style):
+    """Render the given version pieces into the requested style."""
+    if pieces["error"]:
+        return {"version": "unknown",
+                "full-revisionid": pieces.get("long"),
+                "dirty": None,
+                "error": pieces["error"],
+                "date": None}
+
+    if not style or style == "default":
+        style = "pep440"  # the default
+
+    if style == "pep440":
+        rendered = render_pep440(pieces)
+    elif style == "pep440-branch":
+        rendered = render_pep440_branch(pieces)
+    elif style == "pep440-pre":
+        rendered = render_pep440_pre(pieces)
+    elif style == "pep440-post":
+        rendered = render_pep440_post(pieces)
+    elif style == "pep440-post-branch":
+        rendered = render_pep440_post_branch(pieces)
+    elif style == "pep440-old":
+        rendered = render_pep440_old(pieces)
+    elif style == "git-describe":
+        rendered = render_git_describe(pieces)
+    elif style == "git-describe-long":
+        rendered = render_git_describe_long(pieces)
+    else:
+        raise ValueError("unknown style '%s'" % style)
+
+    return {"version": rendered, "full-revisionid": pieces["long"],
+            "dirty": pieces["dirty"], "error": None,
+            "date": pieces.get("date")}
+
+
+def get_versions():
+    """Get version information or return default if unable to do so."""
+    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
+    # __file__, we can work backwards from there to the root. Some
+    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
+    # case we can only use expanded keywords.
+
+    cfg = get_config()
+    verbose = cfg.verbose
+
+    try:
+        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
+                                          verbose)
+    except NotThisMethod:
+        pass
+
+    try:
+        root = os.path.realpath(__file__)
+        # versionfile_source is the relative path from the top of the source
+        # tree (where the .git directory might live) to this file. Invert
+        # this to find the root from __file__.
+        for _ in cfg.versionfile_source.split('/'):
+            root = os.path.dirname(root)
+    except NameError:
+        return {"version": "0+unknown", "full-revisionid": None,
+                "dirty": None,
+                "error": "unable to find root of source tree",
+                "date": None}
+
+    try:
+        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
+        return render(pieces, cfg.style)
+    except NotThisMethod:
+        pass
+
+    try:
+        if cfg.parentdir_prefix:
+            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+    except NotThisMethod:
+        pass
+
+    return {"version": "0+unknown", "full-revisionid": None,
+            "dirty": None,
+            "error": "unable to compute version", "date": None}
diff --git a/hed/errors/error_messages.py b/hed/errors/error_messages.py
index b305fa590..433871605 100644
--- a/hed/errors/error_messages.py
+++ b/hed/errors/error_messages.py
@@ -145,9 +145,12 @@ def val_error_sidecar_key_missing(invalid_key, category_keys):
 def val_error_def_unmatched(tag):
     return f"A data-recording’s Def tag cannot be matched to definition.  Tag: '{tag}'", {}
 
+
 @hed_tag_error(ValidationErrors.HED_DEF_EXPAND_INVALID)
 def val_error_bad_def_expand(tag, actual_def, found_def):
-    return f"A data-recording’s Def-expand tag does not match the given definition.  Tag: '{tag}'.  Actual Def: {actual_def}.  Found Def: {found_def}", {}
+    return f"A data-recording’s Def-expand tag does not match the given definition." + \
+           f"Tag: '{tag}'.  Actual Def: {actual_def}.  Found Def: {found_def}", {}
+
 
 @hed_tag_error(ValidationErrors.HED_DEF_VALUE_MISSING, actual_code=ValidationErrors.HED_DEF_VALUE_INVALID)
 def val_error_def_value_missing(tag):
@@ -192,7 +195,7 @@ def val_warning_default_units_used(tag, default_unit):
 
 
 @hed_error(SchemaErrors.HED_SCHEMA_DUPLICATE_NODE)
-def schema_error_HED_SCHEMA_DUPLICATE_NODE(tag, duplicate_tag_list, section):
+def schema_error_hed_duplicate_node(tag, duplicate_tag_list, section):
     tag_join_delimiter = "\n\t"
     return f"Duplicate term '{str(tag)}' used {len(duplicate_tag_list)} places in '{section}' section schema as: " + \
            f"{tag_join_delimiter} {tag_join_delimiter.join(duplicate_tag_list)}", {}
@@ -257,8 +260,9 @@ def sidecar_error_unknown_column(column_name):
 
 @hed_tag_error(DefinitionErrors.DEF_TAG_IN_DEFINITION, actual_code=ValidationErrors.HED_DEFINITION_INVALID)
 def def_error_def_tag_in_definition(tag, def_name):
-    return f"Invalid tag {tag} found in definition for {def_name}.  Def and Def-expand tags cannot be in definitions."\
-        , {}
+    return f"Invalid tag {tag} found in definition for {def_name}. " +\
+           f"Def and Def-expand tags cannot be in definitions.", {}
+
 
 @hed_error(DefinitionErrors.WRONG_NUMBER_GROUP_TAGS, actual_code=ValidationErrors.HED_DEFINITION_INVALID)
 def def_error_wrong_group_tags(def_name, tag_list):
diff --git a/hed/errors/error_reporter.py b/hed/errors/error_reporter.py
index 20e956b1d..6644936f0 100644
--- a/hed/errors/error_reporter.py
+++ b/hed/errors/error_reporter.py
@@ -540,7 +540,7 @@ def _format_single_context_string(context_type, context, tab_count=0):
     ----------
     context_type : str
         The context type of this entry
-    context : str
+    context : str or HedString
         The value of this context
     tab_count : int
         Number of tabs to name_prefix each line with.
diff --git a/hed/models/__init__.py b/hed/models/__init__.py
index e3ce24e58..7b46701e6 100644
--- a/hed/models/__init__.py
+++ b/hed/models/__init__.py
@@ -3,11 +3,13 @@
 from .base_input import BaseInput
 from .column_mapper import ColumnMapper
 from .column_metadata import ColumnMetadata, ColumnType
-from .def_dict import DefDict, DefEntry
-from .def_mapper import DefinitionMapper
+from .definition_dict import DefinitionDict, DefinitionEntry
+from .def_mapper import DefMapper
 from .events_input import EventsInput
-from .hed_group import HedGroup
+from .hed_group import HedGroup, HedGroupFrozen
+from .hed_group_base import HedGroupBase
 from .hed_input import HedInput
+from .hed_ops import HedOps
 from .hed_string import HedString
 from .hed_tag import HedTag
 from .onset_mapper import OnsetMapper
diff --git a/hed/models/base_input.py b/hed/models/base_input.py
index 0fb42f615..40e0f3076 100644
--- a/hed/models/base_input.py
+++ b/hed/models/base_input.py
@@ -3,7 +3,7 @@
 import pandas
 import copy
 
-from hed.models.def_dict import DefDict
+from hed.models.definition_dict import DefinitionDict
 from hed.models.column_mapper import ColumnMapper
 from hed.errors.exceptions import HedFileError, HedExceptions
 from hed.errors.error_types import ErrorContext, ErrorSeverity
@@ -11,12 +11,12 @@
 from hed.models import model_constants
 from hed.models.hed_ops import translate_ops
 from hed.models.onset_mapper import OnsetMapper
-from hed.models.hed_string import HedString
-from hed.models.def_mapper import DefinitionMapper
+from hed.models.hed_string_comb import HedStringComb
+from hed.models.def_mapper import DefMapper
 
 
 class BaseInput:
-    """Represents a spreadsheet file."""
+    """ Superclass representing a basic spreadsheet file. """
 
     TEXT_EXTENSION = ['.tsv', '.txt']
     EXCEL_EXTENSION = ['.xlsx']
@@ -30,22 +30,20 @@ def __init__(self, file, file_type=None, worksheet_name=None, has_column_names=T
                  name=None):
         """Constructor for the BaseInput class.
 
-         Parameters
-         ----------
-         file: str or file like
-             An xlsx/tsv file to open.
-         file_type: str
-            ".xlsx" for excel, ".tsv" or ".txt" for tsv. data.  Derived from file if file is a str.
-         worksheet_name: str or None
-             The name of the Excel workbook worksheet that contains the HED tags.  Not applicable to tsv files.
-         has_column_names: bool
-             True if file has column names. The validation will skip over the first line of the file. False, if
-             otherwise.
-         mapper: ColumnMapper
-             Pass in a built column mapper(see HedInput or EventsInput for examples), or None to just
-             retrieve all columns as hed tags.
-         name: str or None
-            Optional field for how this file will report errors.
+        Args:
+            file (str or file-like): An xlsx/tsv file to open.
+            file_type (str or None): ".xlsx" (Excel), ".tsv" or ".txt" (tab-separated text).
+                Derived from file if file is a filename.
+            worksheet_name (str or None): Name of Excel workbook worksheet name to use.
+                (Not applicable to tsv files.)
+            has_column_names (bool): True if file has column names.
+            mapper (ColumnMapper or None):  Indicates which columns have HED tags.
+            name (str or None): Optional field for how this file will report errors.
+
+        Notes:
+            The validation will skip over the first line of the file.
+            See HedInput or EventsInput for examples of how to use built-in a ColumnMapper.
+
          """
         if mapper is None:
             mapper = ColumnMapper()
@@ -86,12 +84,11 @@ def __init__(self, file, file_type=None, worksheet_name=None, has_column_names=T
         self.reset_mapper(mapper)
 
     def reset_mapper(self, new_mapper):
-        """
-            Set the column mapper to the passed in one, allowing you to view the file differently.
+        """ Set the column mapper to allow a different view of the file.
+
+        Args:
+            new_mapper (ColumnMapper): A column mapper to be associated with this base input.
 
-        Parameters
-        ----------
-        new_mapper : ColumnMapper
         """
         self._mapper = new_mapper
         if not self._mapper:
@@ -103,7 +100,7 @@ def reset_mapper(self, new_mapper):
 
         self.file_def_dict = self.extract_definitions()
 
-        self.update_definition_mapper_with_file(self.file_def_dict)
+        self.update_definition_mapper(self.file_def_dict)
 
     @property
     def dataframe(self):
@@ -126,86 +123,67 @@ def worksheet_name(self):
         return self._worksheet_name
 
     def _convert_to_form(self, hed_schema, tag_form, error_handler):
+        """ Convert all tags in a given spreadsheet to the specified form.
+
+        Args:
+            hed_schema (HedSchema): The schema to use to convert tags.
+            tag_form (str): The form to convert the tags to (short_tag, long_tag, base_tag, etc).
+            error_handler (ErrorHandler or None): The error handler to use for context or default if none.
+
+        Returns:
+            dict: A list of issue dictionaries corresponding to issues found during conversion.
+
         """
-        Converts all tags in a given spreadsheet to a given form
-
-        Parameters
-        ----------
-        hed_schema : HedSchema
-            The schema to use to convert tags.
-        tag_form: str
-            The form to convert the tags to.  (short_tag, long_tag, base_tag, etc)
-        error_handler : ErrorHandler
-            The error handler to use for context, uses a default one if none.
-
-        Returns
-        -------
-        issues_list: [{}]
-            A list of issues found during conversion
-        """
-        if error_handler is None:
-            error_handler = ErrorHandler()
         error_list = []
-        for row_number, column_to_hed_tags_dictionary in self:
-            error_handler.push_error_context(ErrorContext.ROW, row_number)
+        for row_number, row_dict in self.iter_dataframe(hed_ops=hed_schema, return_row_dict=True,
+                                                        remove_definitions=False, error_handler=error_handler):
+            column_to_hed_tags_dictionary = row_dict[model_constants.COLUMN_TO_HED_TAGS]
+            error_list += row_dict[model_constants.ROW_ISSUES]
             for column_number in column_to_hed_tags_dictionary:
-                error_handler.push_error_context(ErrorContext.COLUMN, column_number)
                 column_hed_string = column_to_hed_tags_dictionary[column_number]
-                error_list += column_hed_string.convert_to_canonical_forms(hed_schema)
                 self.set_cell(row_number, column_number, column_hed_string,
                               include_column_prefix_if_exist=False, tag_form=tag_form)
-                error_handler.pop_error_context()
-            error_handler.pop_error_context()
 
         return error_list
 
     def convert_to_short(self, hed_schema, error_handler=None):
-        """
-        Converts all tags in a given spreadsheet to short form
-
-        Parameters
-        ----------
-        hed_schema : HedSchema
-            The schema to use to convert tags.
-        error_handler : ErrorHandler
-            The error handler to use for context, uses a default one if none.
-
-        Returns
-        -------
-        issues_list: [{}]
-            A list of issues found during conversion
+        """ Convert all tags in a given spreadsheet to short form.
+
+        Args:
+            hed_schema (HedSchema): The schema to use to convert tags.
+            error_handler (ErrorHandler): The error handler to use for context, uses a default if none.
+
+        Returns:
+            dict: A list of issue dictionaries corresponding to issues found during conversion.
+
         """
         return self._convert_to_form(hed_schema, "short_tag", error_handler)
 
     def convert_to_long(self, hed_schema, error_handler=None):
-        """
-        Converts all tags in a given spreadsheet to long form
-
-        Parameters
-        ----------
-        hed_schema : HedSchema
-            The schema to use to convert tags.
-        error_handler : ErrorHandler
-            The error handler to use for context, uses a default one if none.
-
-        Returns
-        -------
-        issues_list: [{}]
-            A list of issues found during conversion
+        """ Convert all tags in a given spreadsheet to long form.
+
+        Args:
+            hed_schema (HedSchema): The schema to use to convert tags.
+            error_handler (ErrorHandler): The error handler to use for context, uses a default if none.
+
+        Returns:
+            dict: A list of issue dictionaries corresponding to issues found during conversion.
+
         """
         return self._convert_to_form(hed_schema, "long_tag", error_handler)
 
     def to_excel(self, file, output_processed_file=False):
-        """
+        """ Output to an Excel file.
 
-        Parameters
-        ----------
-        file : str or file like
-            Location to save this file.  Can be file, or stream/file like.
-        output_processed_file : bool
-            Replace all definitions and labels in HED columns as appropriate.  Also fills in things like categories.
-        Returns
-        -------
+        Args:
+            file (str or file-like):      Location to save this base input.
+            output_processed_file (bool): If True, replace definitions and labels in HED columns.
+
+        Notes:
+            Also fills in things like categories.
+
+        Raises:
+            HedFileError if empty file object or file cannot be opened.
 
         """
         if not file:
@@ -234,17 +212,16 @@ def to_excel(self, file, output_processed_file=False):
             output_file._dataframe.to_excel(file, header=self._has_column_names)
 
     def to_csv(self, file=None, output_processed_file=False):
-        """
-            Returns the file as a csv string.
+        """ Write the data of this base input object to file or return as a string.
 
-        Parameters
-        ----------
-        file : str or file like or None
-            Location to save this file.  Can be file, or stream/file like.
-        output_processed_file : bool
+        Args:
+            file (str, file-like, or None): Location to save this file. If None, return as string.
+            output_processed_file (bool):
             Replace all definitions and labels in HED columns as appropriate.  Also fills in things like categories.
-        Returns
-        -------
+
+        Returns:
+            None or str:  None if file is given or the contents as a str if file is None.
+
         """
         # For now just make a copy if we want to save a formatted copy.  Could optimize this further.
         if output_processed_file:
@@ -256,28 +233,27 @@ def to_csv(self, file=None, output_processed_file=False):
         return csv_string_if_filename_none
 
     def __iter__(self):
+        """ Iterate over the underlying dataframe using iter_dataframe. """
         return self.iter_dataframe()
 
     def iter_raw(self, hed_ops=None, error_handler=None, **kwargs):
-        """Generates an iterator that goes over every row in the file without modification.
-
-           This is primarily for altering or re-saving the original file.(eg convert short tags to long)
-
-        Parameters
-        ----------
-        hed_ops : [func or HedOps] or func or HedOps
-            A list of HedOps of funcs to apply to the hed strings before returning
-        error_handler: ErrorHandler
-            The error handler to use for context, uses a default one if none.
-        kwargs:
-            See models.hed_ops.translate_ops or the specific hed_ops for additional options
-
-        Yields
-        ------
-        row_number: int
-            The current row number
-        column_to_hed_tags_dictionary: dict
-            A dict with keys column_number, value the cell at that position.
+        """ Iterate over (row, column value dict) without modifications or substitutions.
+
+        Args:
+            hed_ops (list, func, HedOps, or None): A func, a HedOps or a list of these to apply to the
+                hed strings before returning.
+            error_handler (ErrorHandler or None): Handler to use for context or a default one if None.
+            kwargs:
+
+        Yields:
+            int: The current row number.
+            dict: A dict with column_number keys and values corresponding to the cell at that position.
+
+        Notes:
+            See models.hed_ops.translate_ops or the specific hed_ops for additional options.
+            Primarily for altering or re-saving the original file (e.g., convert short tags to long).
+            Used for initial processing when trying to find definitions.
+
         """
         if error_handler is None:
             error_handler = ErrorHandler()
@@ -288,34 +264,26 @@ def iter_raw(self, hed_ops=None, error_handler=None, **kwargs):
 
     def iter_dataframe(self, mapper=None, return_row_dict=False, hed_ops=None, run_string_ops_on_columns=False,
                        error_handler=None, expand_defs=False, remove_definitions=True, **kwargs):
-        """
-        Generates a list of parsed rows based on the given column mapper.
-
-        Parameters
-        ----------
-        mapper : ColumnMapper
-            The column name to column number mapper
-        return_row_dict: bool
-            If True, this returns the full row_dict including issues.
-            If False, returns just the HedStrings for each column
-        error_handler : ErrorHandler
-            The error handler to use for context, uses a default one if none.
-        hed_ops : [func or HedOps] or func or HedOps
-            A list of HedOps of funcs to apply to the hed strings before returning
-        run_string_ops_on_columns: bool
-            If true, run all tag and string ops on columns, rather than columns then rows.
-        expand_defs: bool
-            If True, this will expand def tags into def-expand groups
-        remove_definitions: bool
-            If true, this will remove all definition tags found.
-        kwargs:
-            See models.hed_ops.translate_ops or the specific hed_ops for additional options
-        Yields
-        -------
-        row_number: int
-            The current row number
-        row_dict: dict
-            A dict containing the parsed row, including: "HED", "column_to_hed_tags", and possibly "column_issues"
+        """ Iterate over (row number, parsed row dictionary) based on the given column mapper.
+
+        Args:
+            mapper (ColumnMapper or None): The column name to column number mapper (or internal mapper if None).
+            return_row_dict (bool): If True, return the full row_dict including issues,
+                otherwise just the HedStrings for each column.
+            hed_ops (list, func, HedOps, or None): A func, a HedOps or a list of these to apply to the
+                hed strings before returning.
+            run_string_ops_on_columns (bool): If true, run all tag and string ops on columns,
+                rather than columns then rows.
+            error_handler (ErrorHandler or None): The error handler to use for context or a default if None.
+            expand_defs (bool):  If True, expand def tags into def-expand groups.
+            remove_definitions (bool): If true, remove all definition tags found.
+            kwargs:     See models.hed_ops.translate_ops or the specific hed_ops for additional options.
+
+        Yields:
+            int: The current row number.
+            dict: A dict containing the parsed row, including keys: "HED", "column_to_hed_tags",
+                and possibly "column_issues".
+
         """
         if error_handler is None:
             error_handler = ErrorHandler()
@@ -333,47 +301,66 @@ def iter_dataframe(self, mapper=None, return_row_dict=False, hed_ops=None, run_s
 
         # Iter tuples is ~ 25% faster compared to iterrows in our use case
         for row_number, text_file_row in enumerate(self._dataframe.itertuples(index=False)):
-            # todo: Double check if we need a check here for blank lines to avoid crashing.
-
-            row_dict = mapper.expand_row_tags(text_file_row)
-            column_to_hed_tags = row_dict[model_constants.COLUMN_TO_HED_TAGS]
-            expansion_column_issues = row_dict.get(model_constants.COLUMN_ISSUES, {})
             error_handler.push_error_context(ErrorContext.ROW, row_number)
-            row_issues = []
-            if tag_funcs:
-                row_issues += self._run_column_ops(column_to_hed_tags, tag_funcs,
-                                                   expansion_column_issues,
-                                                   error_handler)
-            if return_row_dict:
-                final_hed_string = HedString.create_from_other(column_to_hed_tags.values())
-                if string_funcs:
-                    row_issues += self._run_row_ops(final_hed_string, string_funcs, error_handler)
-                row_dict[model_constants.ROW_ISSUES] = row_issues
-                row_dict[model_constants.ROW_HED_STRING] = final_hed_string
-                yield row_number + start_at_one, row_dict
-            else:
-                yield row_number + start_at_one, column_to_hed_tags
+            yield row_number + start_at_one, self._expand_row_internal(text_file_row, tag_funcs, string_funcs,
+                                                                       error_handler=error_handler,
+                                                                       mapper=mapper, return_row_dict=return_row_dict)
             error_handler.pop_error_context()
 
+    def _expand_row_internal(self, text_file_row, tag_funcs, string_funcs, error_handler,
+                             mapper=None, return_row_dict=False):
+        row_dict = mapper.expand_row_tags(text_file_row)
+        column_to_hed_tags = row_dict[model_constants.COLUMN_TO_HED_TAGS]
+        expansion_column_issues = row_dict.get(model_constants.COLUMN_ISSUES, {})
+
+        row_issues = []
+        if tag_funcs:
+            row_issues += self._run_column_ops(column_to_hed_tags, tag_funcs,
+                                               expansion_column_issues,
+                                               error_handler)
+        if return_row_dict:
+            final_hed_string = HedStringComb(column_to_hed_tags.values())
+            if string_funcs:
+                row_issues += self._run_row_ops(final_hed_string, string_funcs, error_handler)
+            row_dict[model_constants.ROW_ISSUES] = row_issues
+            row_dict[model_constants.ROW_HED_STRING] = final_hed_string
+            return row_dict
+        else:
+            return column_to_hed_tags
+
+    def get_assembled(self, row_number, expand_defs, shrink_defs, remove_definitions=False, error_handler=None):
+        if error_handler is None:
+            error_handler = ErrorHandler()
+
+        mapper = self._mapper
+        # this could be cached
+        tag_funcs, _ = self._translate_ops([],
+                                           run_string_ops_on_columns=True,
+                                           expand_defs=expand_defs, shrink_defs=shrink_defs,
+                                           remove_definitions=remove_definitions, error_handler=error_handler)
+
+        adj_row_number = 1
+        if self._has_column_names:
+            adj_row_number += 1
+
+        text_file_row = self._dataframe.iloc[row_number - adj_row_number]
+        return self._expand_row_internal(text_file_row, tag_funcs, None, error_handler=error_handler,
+                                         mapper=mapper, return_row_dict=False)
+
     def set_cell(self, row_number, column_number, new_string_obj, include_column_prefix_if_exist=False,
                  tag_form="short_tag"):
-        """
+        """ Replace the specified cell of the spreadsheet with transformed text.
+
+        Args:
+            row_number (int):    The row number of the spreadsheet to set.
+            column_number (int): The column number of the spreadsheet to set.
+            new_string_obj (HedString): Object with text to put in the given cell.
+            include_column_prefix_if_exist (bool): If True and the column matches one from mapper
+                _column_prefix_dictionary, remove the prefix.
+            tag_form (str): Version of the tags (short_tag, long_tag, base_tag, etc)
 
-        Parameters
-        ----------
-        row_number : int
-            The row number of the spreadsheet to set
-        column_number : int
-            The column number of the spreadsheet to set
-        new_string_obj : HedString
-            Text to put in the given cell
-        include_column_prefix_if_exist : bool
-            If true and the column matches one from mapper _column_prefix_dictionary, remove the name_prefix
-        tag_form: str
-            The version of the tags we would like to use from the hed string.(short_tag, long_tag, base_tag, etc)
-            Any attribute of a HedTag that returns a string is valid.
-        Returns
-        -------
+        Notes:
+             Any attribute of a HedTag that returns a string is a valid value of tag_form.
 
         """
         if self._dataframe is None:
@@ -390,16 +377,14 @@ def set_cell(self, row_number, column_number, new_string_obj, include_column_pre
         self._dataframe.iloc[row_number - adj_row_number, column_number - 1] = new_text
 
     def get_worksheet(self, worksheet_name=None):
-        """
-            Returns the requested worksheet from the workbook by name
-
-        Parameters
-        ----------
-        worksheet_name : str
-            Returns the requested worksheet by name, or the first one if no name passed in.
-        Returns
-        -------
-        worksheet
+        """ Get the requested worksheet from the workbook by name or the first worksheet if None.
+
+        Args:
+            worksheet_name (str or None): The name of the requested worksheet by name or the first one if None.
+
+        Returns:
+            openpyxl.workbook.Workbook: The workbook request.
+
         """
         if worksheet_name and self._loaded_workbook:
             # return self._loaded_workbook.get_sheet_by_name(worksheet_name)
@@ -410,19 +395,15 @@ def get_worksheet(self, worksheet_name=None):
             return None
 
     def get_def_and_mapper_issues(self, error_handler, check_for_warnings=False):
-        """
-            Returns formatted issues found with definitions and columns.
-
-        Parameters
-        ----------
-        error_handler : ErrorHandler
-            The error handler to use
-        check_for_warnings: bool
-            If True this will check for and return warnings as well
-        Returns
-        -------
-        issues_list: [{}]
-            A list of definition and mapping issues.
+        """ Return formatted issues found with definitions and columns.
+
+        Args:
+            error_handler (ErrorHandler): The error handler to use.
+            check_for_warnings (bool): If True check for and return warnings as well as errors.
+
+        Returns:
+            dict: A list of definition and mapping issues. Each issue is a dictionary.
+
         """
         issues = []
         issues += self.file_def_dict.get_definition_issues()
@@ -436,13 +417,11 @@ def get_def_and_mapper_issues(self, error_handler, check_for_warnings=False):
         return issues
 
     def _get_processed_copy(self):
-        """
-        Returns a copy of this file with processing applied(definitions replaced, columns expanded, etc)
+        """ Return a copy of this file with processing applied (definitions replaced, columns expanded, etc).
+
+        Returns:
+            BaseInput: The copy.
 
-        Returns
-        -------
-        file_copy: BaseInput
-            The copy.
         """
         output_file = copy.deepcopy(self)
         for row_number, column_to_hed_tags_dictionary in self:
@@ -454,20 +433,15 @@ def _get_processed_copy(self):
 
     @staticmethod
     def _get_dataframe_from_worksheet(worksheet, has_headers):
-        """
-        Creates a pandas dataframe from the given worksheet object
-
-        Parameters
-        ----------
-        worksheet : Worksheet
-            The loaded worksheet to convert
-        has_headers : bool
-            If this worksheet has column headers or not.
-
-        Returns
-        -------
-        dataframe: DataFrame
-            The converted data frame.
+        """ Create a pandas dataframe from the given worksheet object.
+
+        Args:
+            worksheet (Worksheet): The loaded worksheet to convert.
+            has_headers (bool): True if this worksheet has column headers.
+
+        Returns:
+            DataFrame: The converted data frame.
+
         """
         if has_headers:
             data = worksheet.values
@@ -523,26 +497,19 @@ def _run_row_ops(self, row_hed_string, row_ops, error_handler):
         return row_issues
 
     def validate_file(self, hed_ops, name=None, error_handler=None, check_for_warnings=True, **kwargs):
-        """Run the given hed_ops on all columns and rows of the spreadsheet
-
-        Parameters
-        ----------
-
-        hed_ops : [func or HedOps] or func or HedOps
-            A list of HedOps of funcs to apply.
-        name: str
-            If present, will use this as the filename for context, rather than using the actual filename
-            Useful for temp filenames.
-        error_handler : ErrorHandler or None
-            Used to report errors.  Uses a default one if none passed in.
-        check_for_warnings: bool
-            If True this will check for and return warnings as well
-        kwargs:
-            See models.hed_ops.translate_ops or the specific hed_ops for additional options
-        Returns
-        -------
-        validation_issues: [{}]
-            The list of validation issues found
+        """ Run the given hed_ops on all columns and rows of the spreadsheet
+
+        Args:
+            hed_ops (func, HedOps, or list of func and/or HedOps): The HedOps of funcs to apply.
+            name (str): If present, use this as the filename for context, rather than using the actual filename
+                Useful for temp filenames.
+            error_handler (ErrorHandler or None): Used to report errors a default one if None.
+            check_for_warnings (bool): If True check for and return warnings as well as errors.
+            kwargs: See models.hed_ops.translate_ops or the specific hed_ops for additional options.
+
+        Returns:
+            list: The list of validation issues found. The list elements are dictionaries.
+
         """
         if not name:
             name = self.name
@@ -553,7 +520,7 @@ def validate_file(self, hed_ops, name=None, error_handler=None, check_for_warnin
             error_handler = ErrorHandler()
 
         error_handler.push_error_context(ErrorContext.FILE_NAME, name)
-        validation_issues = self.get_def_and_mapper_issues(error_handler, check_for_warnings)
+        validation_issues = self.get_def_and_mapper_issues(error_handler, check_for_warnings=check_for_warnings)
         validation_issues += self._run_validators(hed_ops, error_handler=error_handler,
                                                   check_for_warnings=check_for_warnings, **kwargs)
         error_handler.pop_error_context()
@@ -561,39 +528,34 @@ def validate_file(self, hed_ops, name=None, error_handler=None, check_for_warnin
         return validation_issues
 
     def extract_definitions(self, error_handler=None):
-        """
-        Gathers and validates all definitions found in this spreadsheet
+        """ Gather and validate all definitions found in this object.
 
-        Parameters
-        ----------
-        error_handler : ErrorHandler
-            The error handler to use for context, uses a default one if none.
+        Args:
+            error_handler (ErrorHandler): The error handler to use for context or a default if None.
+
+        Returns:
+            DefinitionDict: Contains all the definitions located in the file.
 
-        Returns
-        -------
-        def_dict: DefDict
-            Contains all the definitions located in the file
         """
         if error_handler is None:
             error_handler = ErrorHandler()
-        new_def_dict = DefDict()
+        new_def_dict = DefinitionDict()
         hed_ops = [new_def_dict]
         _ = self._run_validators(hed_ops, run_on_raw=True, error_handler=error_handler)
         return new_def_dict
 
-    def update_definition_mapper_with_file(self, def_dict):
-        """
-        Adds label definitions gathered from the given list of inputs if this has a definition mapper.
+    def update_definition_mapper(self, def_dict):
+        """ Add label definitions gathered from the given input if this has a definition mapper.
+
+        Args:
+            def_dict (list or DefinitionDict): Add the DefDict or list of DefDict to the internal definition mapper.
 
-        Parameters
-        ----------
-        def_dict : DefDict
-            The gathered definitions to add to the mapper.
         """
         if self._def_mapper is not None:
             self._def_mapper.add_definitions(def_dict)
 
     def _translate_ops(self, hed_ops, run_string_ops_on_columns, expand_defs, remove_definitions, **kwargs):
+
         tag_funcs = []
         string_funcs = []
         if hed_ops or expand_defs or remove_definitions:
@@ -603,6 +565,7 @@ def _translate_ops(self, hed_ops, run_string_ops_on_columns, expand_defs, remove
             if not run_string_ops_on_columns:
                 self._add_def_onset_mapper(hed_ops)
                 tag_funcs, string_funcs = translate_ops(hed_ops, split_tag_and_string_ops=True, expand_defs=expand_defs,
+                                                        remove_definitions=remove_definitions,
                                                         **kwargs)
             else:
                 tag_funcs = translate_ops(hed_ops, expand_defs=expand_defs, **kwargs)
@@ -610,7 +573,7 @@ def _translate_ops(self, hed_ops, run_string_ops_on_columns, expand_defs, remove
         return tag_funcs, string_funcs
 
     def _add_def_onset_mapper(self, hed_ops):
-        if not any(isinstance(hed_op, DefinitionMapper) for hed_op in hed_ops):
+        if not any(isinstance(hed_op, DefMapper) for hed_op in hed_ops):
             if self._def_mapper:
                 hed_ops.append(self._def_mapper)
                 hed_ops.append(OnsetMapper(self._def_mapper))
diff --git a/hed/models/column_mapper.py b/hed/models/column_mapper.py
index 66d34496d..5094491c3 100644
--- a/hed/models/column_mapper.py
+++ b/hed/models/column_mapper.py
@@ -12,35 +12,42 @@
 
 
 class ColumnMapper:
-    """Container class for mapping columns in event files into HED tags.
+    """ Container class for mapping columns in event files into HED tags.
 
+    Notes:
         Private Functions and variables column and row indexing starts at 0.
-        Public functions and variables indexing starts at 1 (or 2 if has column names)"""
+        Public functions and variables indexing starts at 1 (or 2 if has column names)
+
+    """
 
     def __init__(self, sidecars=None, tag_columns=None, column_prefix_dictionary=None,
                  attribute_columns=None, optional_tag_columns=None):
-        """Constructor for ColumnMapper
-
-        Parameters
-        ----------
-        sidecars : Sidecar or string or list
-            A list of ColumnDefinitionGroups or filenames to gather ColumnDefinitions from.
+        """ Constructor for ColumnMapper.
+
+        Args:
+            sidecars (Sidecar, string, or list of these): A list of Sidecars or
+                 filenames to gather ColumnDefinitions from.
+            tag_columns: (list):  A list of ints or strings containing the columns that contain the HED tags.
+                Sidecar column definitions will take precedent if there is a conflict with tag_columns.
+            column_prefix_dictionary (dict): Dictionary with keys that are column numbers and values are HED tag
+                prefixes to prepend to the tags in that column before processing.
+            attribute_columns (str, int or list): A column name, column number or a list of column names
+                or numbers to treat as attributes.
+            optional_tag_columns (list): A list of ints or strings containing the columns that contain
+                the HED tags. If the column is otherwise unspecified, convert this column type to HEDTags.
+
+        Notes:
             Sidecars later in the list override those earlier in the list.
-        tag_columns: [int or str]
-             A list of ints or strings containing the columns that contain the HED tags.
-             If the column is otherwise unspecified, it will convert this column type to HEDTags
-        column_prefix_dictionary: dict
-            A dictionary with keys pertaining to the required HED tag columns that correspond to tags that need to be
-            prefixed with a parent tag path. For example, prefixed_needed_tag_columns = {3: 'Event/Description',
-            4: 'Event/Label/', 5: 'Event/Category/'} The third column contains tags that need Event/Description/
-            prepended to them, the fourth column contains tags that need Event/Label/ prepended to them,
-            and the fifth column contains tags that needs Event/Category/ prepended to them.
-        attribute_columns: str or int or [str] or [int]
-             A list of column names or numbers to treat as attributes.
-        optional_tag_columns: [int or str]
-             A list of ints or strings containing the columns that contain the HED tags.
-             If the column is otherwise unspecified, it will convert this column type to HEDTags
+
+
+        Examples:
+            column_prefix_dictionary = {3: 'Description/', 4: 'Label/'}
+
+            The third column contains tags that need Description/ tag prepended, while the fourth column
+            contains tag that needs Label/ prepended.
+
         """
+
         # This points to column_type entries based on column names or indexes if columns have no column_name.
         self.column_data = {}
         # Maps column number to column_entry.  This is what's actually used by most code.
@@ -65,13 +72,11 @@ def __init__(self, sidecars=None, tag_columns=None, column_prefix_dictionary=Non
         self._finalize_mapping()
 
     def add_sidecars(self, sidecars):
-        """
-        Gathers column definitions from a list of files and adds them to the column mapper.
+        """ Gather column definitions from a list of sidecars and add them to the column mapper.
+
+        Args:
+            sidecars (list): A list of filenames or loaded sidecar files in any mix.
 
-        Parameters
-        ----------
-        sidecars : [str or Sidecar]
-            A list of filenames or loaded files in any mix
         """
         self._has_sidecars = True
         sidecars = Sidecar.load_multiple_sidecars(sidecars)
@@ -80,18 +85,16 @@ def add_sidecars(self, sidecars):
                 self._add_column_data(column_data)
 
     def set_column_prefix_dict(self, column_prefix_dictionary, finalize_mapping=True):
-        """Adds the given columns as hed tag columns with the required name_prefix if it does not already exist.
-
-        Parameters
-        ----------
-        column_prefix_dictionary: dict
-            A dictionary with keys pertaining to the required HED tag columns that correspond to tags that need to be
-            prefixed with a parent tag path. For example, prefixed_needed_tag_columns = {3: 'Event/Description',
-            4: 'Event/Label/', 5: 'Event/Category/'} The third column contains tags that need Event/Description/
-            prepended to them, the fourth column contains tags that need Event/Label/ prepended to them,
-            and the fifth column contains tags that needs Event/Category/ prepended to them.
-        finalize_mapping : bool
-            If True, will re-generate the internal mapping. If False, this function has no effect until you do finalize.
+        """ Add the columns specified by the column_prefix_dictionary as HED tag columns with required prefix.
+
+        Args:
+            column_prefix_dictionary (dict):  Dictionary with keys that are column numbers and values are HED tag
+                prefixes to prepend to the tags in that column before processing.
+            finalize_mapping (bool): Re-generate the internal mapping if True, otherwise no effect until finalize.
+
+        Returns:
+            list:  List of issues that occurred during this process. Each issue is a dictionary.
+
         """
         if column_prefix_dictionary:
             self._column_prefix_dictionary = self._subtract_1_from_dictionary_keys(column_prefix_dictionary)
@@ -100,16 +103,17 @@ def set_column_prefix_dict(self, column_prefix_dictionary, finalize_mapping=True
         return []
 
     def set_tag_columns(self, tag_columns=None, optional_tag_columns=None, finalize_mapping=True):
-        """Sets the current tag columns to the passed in values, clearing the current ones if None.
-
-        Parameters
-        ----------
-        tag_columns : [str or int]
-            A list of ints or strings containing the columns that contain the HED tags.
-        optional_tag_columns : [str or int]
-            A list of ints or strings containing the columns that contain the HED tags, but not an error if missing.
-        finalize_mapping :
-            If True, will re-generate the internal mapping. If False, this function has no effect until you do finalize.
+        """ Set the current tag columns, clearing the current ones if None.
+
+        Args:
+            tag_columns (list): A list of ints or strings containing the columns that contain the HED tags.
+            optional_tag_columns (list): A list of ints or strings containing the columns that contain the HED tags,
+                but not an error if missing.
+            finalize_mapping (bool): Re-generate the internal mapping if True, otherwise no effect until finalize.
+
+        Returns:
+            list: List of issues that occurred during this process. Each issue is a dictionary.
+
         """
         self._tag_columns = []
         self._optional_tag_columns = []
@@ -123,13 +127,15 @@ def set_tag_columns(self, tag_columns=None, optional_tag_columns=None, finalize_
         return []
 
     def set_column_map(self, new_column_map=None):
-        """Pass in the column number to column_name mapping to finalize
+        """ Set the column number to column_name mapping.
+
+        Args:
+            new_column_map (list or dict):  Either an ordered list of the column names or column_number:column name
+                dictionary. In both cases column numbers start at 1.
+
+        Returns:
+            list: List of issues. Each issue is a dictionary.
 
-        Parameters
-        ----------
-        new_column_map : list or dict
-            If list, should be each column name with column numbers assumed to start at 1.
-            If dict, should be column_number : column name.  Column number should start at 1.
         """
         if isinstance(new_column_map, dict):
             column_map = self._subtract_1_from_dictionary_keys(new_column_map)
@@ -140,15 +146,12 @@ def set_column_map(self, new_column_map=None):
         return self._finalize_mapping()
 
     def add_columns(self, column_names_or_numbers, column_type=ColumnType.Attribute):
-        """
-        Add a list of blank columns of the given type.
-
-        Parameters
-        ----------
-        column_names_or_numbers : [int or str]
-            A list of column names or numbers to add as the specified type.
-        column_type : ColumnType, default Attribute
-            The type of this column.  Generally will be Attribute
+        """ Add a list of blank columns in the given column category.
+
+        Args:
+            column_names_or_numbers (list): A list of column names or numbers to add as the specified type.
+            column_type (ColumnType property): The category of column these should be.
+
         """
         if column_names_or_numbers:
             if not isinstance(column_names_or_numbers, list):
@@ -158,25 +161,19 @@ def add_columns(self, column_names_or_numbers, column_type=ColumnType.Attribute)
                 self._add_column_data(new_def)
 
     def _expand_column(self, column_number, input_text):
+        """ Expand the specified text based on the rules for expanding the specified column.
+
+        Args:
+            column_number (int): The column number this text should be treated as from.
+            input_text (str): The text to expand, generally from a single cell of a spreadsheet.
+
+        Returns:
+            str or None: The text after expansion or None if this column is undefined or the given text is null.
+            False or str: Depends on the value of first return value. If None, this is an error message.
+                If string, this is an attribute name that should be stored separately.
+
         """
-        Expand the given text based on the rules for expanding this column
-
-        Parameters
-        ----------
-        column_number : int
-            The column number this text should be treated as from
-        input_text : str
-            The text to expand, generally a single cell of a spreadsheet.
-
-        Returns
-        -------
-        expanded_text : str or None
-            The text after expansion.  Returns None if this column is undefined or the given text is null.
-        attribute_name_or_error_message: False or str
-            Depends on the value of first return value.
-            If None, this is an error message.
-            If string, this is an attribute name, that should be stored separately.
-        """
+
         # Default 1-1 mapping if we don't have specific behavior.
         if not self._final_column_map:
             return HedString(input_text), False
@@ -192,22 +189,19 @@ def _expand_column(self, column_number, input_text):
         return column_entry.expand(input_text)
 
     def expand_row_tags(self, row_text):
-        """
-        Expands all mapped columns from a given row
-
-        Parameters
-        ----------
-        row_text : [str]
-            The text for the given row, one entry per column number.
-
-        Returns
-        -------
-        expanded_dict: {str: }
-            A dictionary containing:
-                "HED": The entire expanded row
-                "column_to_hed_tags": Each expanded column separately as a list of strings.
-                (attribute name): The attribute value from the spreadsheet column,
-                                    only present when a given column is an attribute
+        """ Expand all mapped columns from a given row.
+
+        Args:
+            row_text (list): The text for the given row, one list entry per column number.
+
+        Returns:
+            dict: A dictionary containing the keys HED, column_to_hed_tags, and attribute.
+
+        Notes:
+            The value of the "HED" entry is the entire expanded row.
+            The "column_to_hed_tags" is each expanded column given separately as a list of strings.
+            The attribute is the value from the spreadsheet column only present when a given column is an attribute.
+
         """
         result_dict = {}
         column_to_hed_tags_dictionary = {}
@@ -236,16 +230,14 @@ def expand_row_tags(self, row_text):
         return result_dict
 
     def get_prefix_remove_func(self, column_number):
-        """
-        Returns a function that will remove the name_prefix for the given column.
-
-        Parameters
-        ----------
-        column_number : int
-            numbered column to use name_prefix check from.
-        Returns
-        -------
-            A function taking a tag and string, returning a string.
+        """ Return a function that removes the name_prefix for the given column.
+
+        Args:
+            column_number (int): Column number to look up in the prefix dictionary.
+
+        Returns:
+            func: A function taking a tag and string, returning a string.
+
         """
         column_number -= 1
         if column_number not in self._final_column_map:
@@ -258,31 +250,31 @@ def get_prefix_remove_func(self, column_number):
         return entry.remove_prefix_if_needed
 
     def _add_column_data(self, new_column_entry):
-        """
-        Add a column definition to this column mapper.
+        """ Add the metadata of a column to this column mapper.
 
-        Note if an entry with the same name exists, the new entry will replace it.
+        Args:
+            new_column_entry (ColumnMetadata): The column definition to add.
+
+        Notes:
+            If an entry with the same name exists, the new entry will replace it.
 
-        Parameters
-        ----------
-        new_column_entry : ColumnMetadata
-            The column definition to add
         """
         column_name = new_column_entry.column_name
         self.column_data[column_name] = copy.deepcopy(new_column_entry)
 
     def _set_column_prefix(self, column_number, new_required_prefix):
-        """
-        Internal function to add this as a required name_prefix to a column
+        """ Internal function to add this as a required name_prefix to a column
+
+        Args:
+            column_number (int): The column number with this name_prefix.
+            new_required_prefix (str): The name_prefix to add to the column when loading from a spreadsheet.
+
+        Raises:
+            TypeError if column number is passed as a str rather an an int.
 
-        If the column is not known to the mapper, it will be added as a HEDTags column.
+        Notes:
+            If the column is not known to the mapper, it will be added as a HEDTags column.
 
-        Parameters
-        ----------
-        column_number : int
-            The column number with this name_prefix
-        new_required_prefix : str
-            The name_prefix to add to the column when loading from a spreadsheet.
         """
         if isinstance(column_number, str):
             raise TypeError("Must pass in a column number not column_name to _set_column_prefix")
@@ -297,11 +289,17 @@ def _set_column_prefix(self, column_number, new_required_prefix):
             column_entry.column_type = ColumnType.HEDTags
 
     def _finalize_mapping(self):
-        """
+        """ Set the final column mapping information.
+
         Internal function that gathers all the various sources of column rules and puts them
         in a list mapping from column number to definition.
 
-        This needs to be called after all definitions and columns are added.
+        Returns:
+            list: A list of issues that occurred when creating the mapping. Each issue is a dictionary.
+
+        Notes:
+            This needs to be called after all definitions and columns are added.
+
         """
         self._final_column_map = {}
         found_named_tag_columns = {}
@@ -349,43 +347,36 @@ def _finalize_mapping(self):
         return self._finalize_mapping_issues
 
     def get_def_dicts(self):
-        """
-            Return a list of all def dicts from every column description
+        """ Return a list of all def dicts from every column description.
+
+        Returns:
+           list:   A list of DefinitionDict objects corresponding to each column entry.
 
-        Returns
-        -------
-        def_dicts: [DefDict]
-            A list of def dicts, corresponding to each column entry.
         """
         def_dicts = [entry.def_dict for entry in self.column_data.values()]
         return def_dicts
 
     def get_column_mapping_issues(self):
-        """
-            Gets all the issues with finalizing column mapping.  Primarily a missing required column.
-        Returns
-        -------
-        col_mapping_issues: [{}]
-            A list of all issues found from mapping column names to numbers.
+        """ Get all the issues with finalizing column mapping.  Primarily a missing required column.
+
+        Returns:
+            list: A list dictionaries of all issues found from mapping column names to numbers.
+
         """
         return self._finalize_mapping_issues
 
     def validate_column_data(self, hed_ops, error_handler=None, **kwargs):
-        """
-        Validates all column definitions that are being used and column definition hed strings
-
-        Parameters
-        ----------
-        hed_ops : [func or HedOps] or func or HedOps
-            A list of HedOps of funcs to apply to the hed strings in the sidecars.
-        error_handler : ErrorHandler or None
-            Used to report errors.  Uses a default one if none passed in.
-        kwargs:
-            See models.hed_ops.translate_ops or the specific hed_ops for additional options
-        Returns
-        -------
-        validation_issues : [{}]
-            A list of syntax and semantic issues found in the definitions.
+        """ Validate all column definitions that are being used and column definition hed strings
+
+        Args:
+            hed_ops (list, func, or HedOps): A func, a HedOps or a list of these to apply to the
+                hed strings in the sidecars.
+            error_handler (ErrorHandler or None): Used to report errors.  Uses a default one if none passed in.
+            kwargs: See models.hed_ops.translate_ops or the specific hed_ops for additional options.
+
+        Returns:
+            list: A list of syntax and semantic issues found in the definitions. Each issue is a dictionary.
+
         """
         if error_handler is None:
             error_handler = ErrorHandler()
@@ -399,32 +390,26 @@ def validate_column_data(self, hed_ops, error_handler=None, **kwargs):
 
     @staticmethod
     def _subtract_1_from_dictionary_keys(int_key_dictionary):
-        """Subtracts 1 from each dictionary key.
+        """ Subtracts 1 from each dictionary key.
 
-        Parameters
-        ----------
-        int_key_dictionary: {}
-            A dictionary with int keys.
-        Returns
-        -------
-        adjusted_dict: {}
-            A dictionary with the keys subtracted by 1.
+        Args:
+            int_key_dictionary (dict): A dictionary with int keys.
+
+        Returns:
+            dict: A dictionary with the keys subtracted by 1.
 
         """
         return {key - 1: value for key, value in int_key_dictionary.items()}
 
     @staticmethod
     def _subtract_1_from_list_elements(int_list):
-        """Subtracts 1 from each int in a list.
-
-        Parameters
-        ----------
-        int_list: [int]
-            A list of ints.
-        Returns
-        -------
-        adjusted_list: [int]
-            A list of containing each element subtracted by 1.
+        """ Subtracts 1 from each int in a list.
+
+        Args:
+            int_list (list): A list of ints.
+
+        Returns:
+            list: A list of ints where corresponding elements are 1 less than int_list.
 
         """
         return [x if isinstance(x, str) else x - 1 for x in int_list]
diff --git a/hed/models/column_metadata.py b/hed/models/column_metadata.py
index ce725eb99..61f5f9bb5 100644
--- a/hed/models/column_metadata.py
+++ b/hed/models/column_metadata.py
@@ -1,6 +1,6 @@
 from enum import Enum
 from hed.models.hed_string import HedString
-from hed.models.def_dict import DefDict
+from hed.models.definition_dict import DefinitionDict
 from hed.errors.error_types import SidecarErrors, ErrorContext, ValidationErrors
 from hed.errors import error_reporter
 from hed.errors.error_reporter import ErrorHandler
@@ -26,27 +26,24 @@ class ColumnType(Enum):
 
 
 class ColumnMetadata:
-    """Represents a single column in a ColumnMapper or Sidecar."""
-
-    def __init__(self, column_type=None, name=None, hed_dict=None, column_prefix=None,
-                 error_handler=None):
-        """
-        A single column entry in the column mapper.  Each column you want to retrieve data from will have one.
-
-        Parameters
-        ----------
-        column_type : ColumnType
-            How to treat this column when reading data
-        name : str or int
-            The column_name or column number of this column.  If name is a string, you'll need to use a column map
-            to set the number later.
-        hed_dict : dict
-            The loaded data (usually from json) for the given def
-            At a minimum, this needs "HED" in the dict for several ColumnType
-        column_prefix : str
-            If present, prepend the given name_prefix to all hed tags in the columns.  Only works on ColumnType HedTags
-        error_handler : ErrorHandler or None
-            Used to report errors.  Uses a default one if none passed in.
+    """ Class representing a single column in a ColumnMapper or top-level dict in Sidecar. """
+
+    def __init__(self, column_type=None, name=None, hed_dict=None, column_prefix=None, error_handler=None):
+        """ A single column entry in the column mapper.
+
+        Args:
+            column_type (ColumnType or None): How to treat this column when reading data.
+            name (str, int, or None): The column_name or column number identifying this column.
+                If name is a string, you'll need to use a column map to set the number later.
+            hed_dict (dict or None): The loaded data (usually from json) for the given def
+                At a minimum, this needs "HED" in the dict for several ColumnType
+            column_prefix (str or None): If present, prepend the given column_prefix to all hed tags in the columns.
+                Only works on ColumnType HedTags.
+            error_handler (ErrorHandler or None): Used to report errors.  Uses a default if None.
+
+        Notes:
+            Each column from which data is retrieved must have a ColumnMetadata representing its contents.
+            The column_prefix dictionaries are used when the column is processed.
         """
         if column_type is None or column_type == ColumnType.Unknown:
             column_type = ColumnMetadata._detect_column_type(hed_dict)
@@ -63,48 +60,37 @@ def __init__(self, column_type=None, name=None, hed_dict=None, column_prefix=Non
 
     @property
     def def_dict(self):
-        """
-            Returns the definition dictionary for this column
+        """ Return the definition dictionary for this column.
+
+        Returns:
+            DefinitionDict: Contains all the definitions located in the column.
 
-        Returns
-        -------
-        def_dict: DefDict
-            Contains all the definitions located in the file
         """
         return self._def_dict
 
     @property
     def hed_dict(self):
-        """
-            The loaded dict for any given entry.
+        """ The loaded dict for any given entry.
+
+        Returns:
+            dict: A dict which generally contains a "HED" entry and optional others like description.
 
-        Returns
-        -------
-        dict_for_entry: dict
-            A dict which generally contains a "HED" entry and optional others like description.
         """
         return self._hed_dict
 
     def hed_string_iter(self, hed_ops=None, error_handler=None, **kwargs):
-        """
-        Return iterator to loop over all hed strings in this column definition
-
-        Parameters
-        ----------
-        hed_ops : [func or HedOps] or func or HedOps
-            A list of HedOps of funcs to apply to the hed strings before returning
-        error_handler : ErrorHandler
-            The error handler to use for context, uses a default one if none.
-        kwargs:
-            See models.hed_ops.translate_ops or the specific hed_ops for additional options
-        Yields
-        -------
-        hed_string : HedString
-            hed_string at a given column and key position
-        position: str
-            Indicates where hed_string was loaded from so it can be later set by the user
-        issues: [{}]
-            List of issues found applying hed_ops
+        """ Iterator that yields all hed strings in this column metadata.
+
+        Args:
+            hed_ops (func, HedOps, or list of these): The HedOps or funcs to apply to the hed strings before returning.
+            error_handler (ErrorHandler): The error handler to use for context, uses a default one if none.
+            kwargs: See models.hed_ops.translate_ops or the specific hed_ops for additional options
+
+        Yields:
+            HedString: The hed string at a given column and key position.
+            str: Indication of the where hed string was loaded from so it can be later set by the user.
+            list: List of issues found applying hed_ops. Each issue is a dictionary.
+
         """
         if error_handler is None:
             error_handler = ErrorHandler()
@@ -135,19 +121,15 @@ def hed_string_iter(self, hed_ops=None, error_handler=None, **kwargs):
             error_handler.pop_error_context()
 
     def _hed_iter(self, also_return_bad_types=False):
-        """
-            Iterates over all the hed string entries, returning HedString objects.
-
-        Parameters
-        ----------
-        also_return_bad_types: bool
-            If true, this will also yield types other than HedString
-
-        Yields
-        -------
-        hed_string: HedString
-        position: str
-            The position to pass back to set this string.
+        """ Iterate over all the hed string entries, returning HedString objects.
+
+        Args:
+            also_return_bad_types (bool): If true, this can yield types other than HedString, otherwise skips these.
+
+        Yields:
+            HedString: Individual hed strings for different entries.
+            str: The position to pass back to set this string.
+
         """
         hed_strings = self._hed_dict.get("HED", None)
         if isinstance(hed_strings, dict):
@@ -163,17 +145,13 @@ def _hed_iter(self, also_return_bad_types=False):
             yield hed_string, None
 
     def set_hed_string(self, new_hed_string, position=None, set_def_removed=False):
-        """
-            Set a hed string in a provided category key/etc
-
-        Parameters
-        ----------
-        new_hed_string : str or HedString
-            The new hed_string to replace the value at position.
-        position : str, optional
-            This should only be a value returned from hed_string_iter
-        set_def_removed: bool
-            If True, set the version with definitions removed, rather than the normal version
+        """ Set a hed string in a provided category key/etc.
+
+        Args:
+            new_hed_string (str or HedString): The new hed_string to replace the value at position.
+            position (str, optional): This should only be a value returned from hed_string_iter.
+            set_def_removed (bool): If True, set the version with definitions removed, rather than the normal version.
+
         """
         hed_strings = self._hed_dict.get("HED", None)
         if isinstance(hed_strings, dict):
@@ -196,17 +174,14 @@ def set_hed_string(self, new_hed_string, position=None, set_def_removed=False):
             raise TypeError("Error: Trying to set a HED string on a column_type that doesn't support it.")
 
     def _get_category_hed_string(self, category):
-        """Fetches the hed string from a given category key.
+        """ Fetch the hed string from a given category key.
+
+        Args:
+            category (str): The category key to retrieve the string from.
 
-        Parameters
-        ----------
-        category : str
-            The category key to retrieve the string from
+        Returns:
+            str: The hed string for a given category entry in a category column.
 
-        Returns
-        -------
-        hed_string: str
-            The hed string for a given category entry in a category column
         """
         if self.column_type != ColumnType.Categorical:
             return None
@@ -214,12 +189,11 @@ def _get_category_hed_string(self, category):
         return self._def_removed_hed_dict.get(category, None)
 
     def _get_value_hed_string(self):
-        """Fetches the hed_string from a given value column
+        """ Fetch the hed string from a given value column.
+
+        Returns:
+            str: The hed string for a given value column.
 
-        Returns
-        -------
-        hed_string: str
-            The hed string for a given value column
         """
         if self.column_type != ColumnType.Value:
             return None
@@ -227,22 +201,19 @@ def _get_value_hed_string(self):
         return self._def_removed_hed_dict
 
     def expand(self, input_text):
-        """
-            Expands the input_text based on the rules for this column.
-            Eg adding name_prefix, inserting a column hed_string from key, etc.
-
-        Parameters
-        ----------
-        input_text : str
-            Text to expand(generally from a single cell in a spreadsheet)
-
-        Returns
-        -------
-        hed_string: str
-            The expanded column as a hed_string
-        attribute_name_or_error_message: str or {}
-            If this is a string, contains the name of this column as an attribute.
-            If the first return value is None, this is an error message dict.
+        """ Expand the input_text based on the rules for this column.
+
+        Args:
+            input_text (str): Text to expand (generally from a single cell in a spreadsheet).
+
+        Returns:
+            str: The expanded column as a hed_string.
+            attribute_name_or_error_message (str or dict): If this is a string, contains the name of this column
+                as an attribute. If the first return value is None, this is an error message dictionary.
+
+        Notes:
+            An example is adding name_prefix, inserting a column hed_string from key, etc.
+
         """
         column_type = self.column_type
 
@@ -270,18 +241,16 @@ def expand(self, input_text):
 
     @staticmethod
     def _prepend_prefix_to_required_tag_column_if_needed(required_tag_column_tags, required_tag_prefix):
-        """Prepends the tag paths to the required tag column tags that need them.
-        Parameters
-        ----------
-        required_tag_column_tags: HedString
-            A string containing HED tags associated with a required tag column that may need a tag
+        """ Prepend the tag paths to the required tag column tags that need them.
+
+        Args:
+            required_tag_column_tags (HedString): A string containing HED tags associated with a
+                required tag column that may need a tag
             name_prefix prepended to its tags.
-        required_tag_prefix: str
-            A string that will be added if missing to any given tag.
-        Returns
-        -------
-        HedString
-            A comma separated string that contains the required HED tags with the tag name_prefix prepended to them if
+            required_tag_prefix (str): A string that will be added if missing to any given tag.
+
+        Returns:
+            HedString: A comma separated string that contains the required HED tags with the tag name_prefix prepended to them if
             needed.
 
         """
@@ -294,20 +263,14 @@ def _prepend_prefix_to_required_tag_column_if_needed(required_tag_column_tags, r
         return required_tag_column_tags
 
     def remove_prefix_if_needed(self, original_tag, current_tag_text):
-        """
-        Remove name_prefix from all tags in given hed string if this column has a required name_prefix
-
-        Parameters
-        ----------
-        original_tag: HedTag
-            The original hed tag being written.
-        current_tag_text : str
-            A single tag as a string, in any form.
-
-        Returns
-        -------
-        prefix_removed_text: str
-            original_text with required prefixes removed from all hed tags
+        """ Remove column_prefix if present from the given tag text.
+
+        Args:
+            original_tag (HedTag): The original hed tag being written.
+            current_tag_text (str): A single tag as a string, in any form.
+
+        Returns:
+            str: current_tag_text with required prefixes removed
         """
         prefix_to_remove = self.column_prefix
         if not prefix_to_remove:
@@ -319,18 +282,15 @@ def remove_prefix_if_needed(self, original_tag, current_tag_text):
 
     @staticmethod
     def _detect_column_type(dict_for_entry):
-        """
-        Determines the ColumnType of a given json entry.
+        """ Determine the ColumnType of a given json entry.
+
+        Args:
+            dict_for_entry (dict): The loaded json entry a specific column.
+                Generally has a "HED" entry among other optional ones.
 
-        Parameters
-        ----------
-        dict_for_entry : dict
-            The loaded json entry a specific column.  Generally has a "HED" entry among other optional ones.
+        Returns:
+            ColumnType: The determined type of given column.  Returns None if unknown.
 
-        Returns
-        -------
-        column_type: ColumnType
-            The determined type of given column.  Returns None if unknown.
         """
         if not dict_for_entry or not isinstance(dict_for_entry, dict):
             return ColumnType.Attribute
@@ -352,32 +312,26 @@ def _detect_column_type(dict_for_entry):
         return ColumnType.Value
 
     def get_definition_issues(self):
-        """
-        Returns the issues found extracting definitions from this column.
+        """ Return the issues found from extracting definitions from this column.
+
+        Returns:
+            list: A list of issues found when parsing definitions. Indiviudal issues are dictionaries.
 
-        Returns
-        -------
-        issues_list: [{}]
-            A list of issues found when parsing definitions
         """
         return self._def_dict.get_definition_issues()
 
     def validate_column(self, hed_ops, error_handler, **kwargs):
-        """
-            Run the given hed_ops on this column
-
-        Parameters
-        ----------
-        hed_ops : [func or HedOps] or func or HedOps
-            A list of HedOps of funcs to apply to the hed strings in the columns.
-        error_handler : ErrorHandler or None
-            Used to report errors.  Uses a default one if none passed in.
-        kwargs:
-            See models.hed_ops.translate_ops or the specific hed_ops for additional options
-        Returns
-        -------
-        col_issues: [{}]
-            A list of issues found by the given hed_ops.
+        """ Run the given hed_ops on this column.
+
+        Args:
+            hed_ops (list or func or HedOps) A list of HedOps of funcs or a HedOps or func to apply
+                to the hed strings in the columns.
+            error_handler (ErrorHandler or None): Used to report errors.  Uses a default one if none passed in.
+            kwargs: See models.hed_ops.translate_ops or the specific hed_ops for additional options.
+
+        Returns:
+            list: Issues found by the given hed_ops. Each issue is a dictionary.
+
         """
         if error_handler is None:
             error_handler = error_reporter.ErrorHandler()
@@ -396,15 +350,13 @@ def validate_column(self, hed_ops, error_handler, **kwargs):
         return col_validation_issues
 
     def _validate_column_structure(self, error_handler):
-        """
-            This checks primarily for type errors, such as expecting a string and getting a list in a json sidecar.
+        """ Checks primarily for type errors such as expecting a string and getting a list in a json sidecar.
 
-        Parameters
-        ----------
-        error_handler
+        Args:
+            error_handler (ErrorHandler)  Sets the context for the error reporting. Cannot be None.
 
-        Returns
-        -------
+        Returns:
+            list:  Issues in performing the operations. Each issue is a dictionary.
 
         """
         val_issues = []
@@ -438,18 +390,18 @@ def _run_ops(self, hed_ops, error_handler, **kwargs):
         return col_validation_issues
 
     def _validate_pound_sign_count(self, hed_string):
-        """Checks if a given hed string in the column has the correct number of pound signs
+        """ Check if a given hed string in the column has the correct number of pound signs.
+
+        Args:
+            hed_string (str or HedString): HED string to be checked.
 
-        This normally should be either 0 or 1, but sometimes will be higher due to the presence of definition tags.
+        Returns:
+            list: Issues due to pound sign errors. Each issue is a dictionary.
 
-        Parameters
-        ----------
-        hed_string : str or HedString
+        Notes:
+            Normally the number of # should be either 0 or 1, but sometimes will be higher due to the
+            presence of definition tags.
 
-        Returns
-        -------
-        issues_list: [{}]
-            A list of the pound sign errors(always 0 or 1 item in the list)
         """
         if self.column_type == ColumnType.Value or self.column_type == ColumnType.Attribute:
             expected_pound_sign_count = 1
@@ -470,22 +422,18 @@ def _validate_pound_sign_count(self, hed_string):
         return []
 
     def extract_definitions(self, error_handler=None):
-        """
-        Gathers and validates all definitions found in this spreadsheet
+        """ Gather and validate all definitions found in this column.
+
+        Args:
+            error_handler (ErrorHandler): The error handler to use for context, uses a default one if None.
 
-        Parameters
-        ----------
-        error_handler : ErrorHandler
-            The error handler to use for context, uses a default one if none.
+        Returns:
+            DefinitionDict: Contains all the definitions located in the column.
 
-        Returns
-        -------
-        def_dict: DefDict
-            Contains all the definitions located in the file
         """
         if error_handler is None:
             error_handler = ErrorHandler()
-        new_def_dict = DefDict()
+        new_def_dict = DefinitionDict()
         hed_ops = []
         hed_ops.append(new_def_dict)
         hed_ops.append(HedString.remove_definitions)
diff --git a/hed/models/def_mapper.py b/hed/models/def_mapper.py
index 4e34cc8dc..77f5c7785 100644
--- a/hed/models/def_mapper.py
+++ b/hed/models/def_mapper.py
@@ -1,26 +1,30 @@
 from hed.models.hed_string import HedString
 from hed.models.hed_tag import HedTag
-from hed.models.def_dict import DefDict
+from hed.models.definition_dict import DefinitionDict
 from hed.models.model_constants import DefTagNames
 from hed.errors.error_types import ValidationErrors
 from hed.errors.error_reporter import ErrorHandler
 from hed.models.hed_ops import HedOps
 
 
-class DefinitionMapper(HedOps):
-    """Class for handling definitions in hed strings.
+class DefMapper(HedOps):
+    """ Class for handling Def/ and Def-expand/ in hed strings.
 
-       The class is responsible for gather and removing definitions.
-       The class also replaces labels in hed strings with the gathered definitions."""
+    Notes:
+       The class provides string funcs but no tag funcs when extending HedOps.
+       The class can expand or shrink definitions in hed strings via Def/XXX and (Def-expand/XXX ...).
+
+    """
 
     def __init__(self, def_dicts=None):
-        """
-        Class to handle mapping definitions from hed strings to fill them in with their values.
+        """ Initialize mapper for definitions in hed strings to fill them in with their values.
+
+        Args:
+            def_dicts (list): DefDicts containing the definitions this mapper should initialize with.
+
+        Notes:
+            More definitions can be added later.
 
-        Parameters
-        ----------
-        def_dicts: [DefDict]
-            DefDicts containing all the definitions this mapper should initialize with.  More can be added later.
         """
         super().__init__()
         self._gathered_defs = {}
@@ -33,72 +37,68 @@ def __init__(self, def_dicts=None):
             self.add_definitions(def_dicts)
 
     def get_def_entry(self, def_name):
-        return self._gathered_defs.get(def_name.lower())
+        """ Get the definition entry for the definition name.
 
-    def clear_temporary_definitions(self):
-        """
-            Removes any previously added temporary definitions
+        Args:
+            def_name (str):  Name of the definition to retrieve.
 
-        Returns
-        -------
+        Returns:
+            DefinitionEntry:  Definition entry for the requested definition.
 
         """
+
+        return self._gathered_defs.get(def_name.lower())
+
+    def clear_temporary_definitions(self):
+        """ Remove any previously added temporary definitions. """
         for def_name in self._temporary_def_names:
             del self._gathered_defs[def_name]
         self._temporary_def_names = set()
 
     def add_definitions_from_string_as_temp(self, hed_string_obj):
-        """
-            Checks the given string and adds any definitions found to this mapper as temp.
+        """ Add any definitions found in the hed string to this mapper as temporary definitions.
+
+        Args:
+            hed_string_obj (HedString):  Hed string object to search for definitions
 
-        Parameters
-        ----------
-        hed_string_obj : HedString
+        Returns:
+            list:  List of issues due to invalid definitions found in this string. Each issue is a dictionary.
 
-        Returns
-        -------
-        issues_list: [{}]
-            A list of issues related to definitions found
         """
-        this_string_def_dict = DefDict()
+        this_string_def_dict = DefinitionDict()
         validation_issues = this_string_def_dict.check_for_definitions(hed_string_obj)
         self.add_definitions(this_string_def_dict, add_as_temp=True)
         return validation_issues
 
     def add_definitions(self, def_dicts, add_as_temp=False):
-        """
-        Adds all definitions found in hed strings in the given input
+        """ Add the definitions found in the given definition dictionaries to this mapper.
+
+        Args:
+            def_dicts (list or DefinitionDict): DefDict or list of DefDicts whose definitions should be added.
+            add_as_temp (bool):          If true, mark these new definitions as temporary (easily purged).
 
-        Parameters
-        ----------
-        def_dicts : [] or DefDict
-        add_as_temp: bool
-            If true, mark these new definitions as temporary(easily purged)
         """
         if not isinstance(def_dicts, list):
             def_dicts = [def_dicts]
         for def_dict in def_dicts:
-            if isinstance(def_dict, DefDict):
+            if isinstance(def_dict, DefinitionDict):
                 self._add_definitions_from_dict(def_dict, add_as_temp)
             else:
-                print(f"Invalid input type '{type(def_dict)} passed to DefinitionMapper.  Skipping.")
+                print(f"Invalid input type '{type(def_dict)} passed to DefMapper.  Skipping.")
 
     def _add_definitions_from_dict(self, def_dict, add_as_temp=False):
-        """
-            Gather definitions from a single def group and add it to the mapper
+        """ Add the definitions found in the given definition dictionary to this mapper.
 
-        Parameters
-        ----------
-        def_dict : DefDict
+         Args:
+             def_dict (DefinitionDict): DefDict whose definitions should be added.
+             add_as_temp (bool): If true, mark these new definitions as temporary (easily purged).
 
-        Returns
-        -------
         """
         for def_tag, def_value in def_dict:
             if def_tag in self._gathered_defs:
                 # todo: add detection here possibly.  Right now the first definition found is used.
                 # This is a warning.  Errors from a duplicate definition in a single source will be reported
-                # by DefDict
+                # by DefinitionDict
                 print(f"WARNING: Duplicate definition found for '{def_tag}'.")
                 continue
             self._gathered_defs[def_tag] = def_value
@@ -106,21 +106,22 @@ def _add_definitions_from_dict(self, def_dict, add_as_temp=False):
                 self._temporary_def_names.add(def_tag)
 
     def expand_def_tags(self, hed_string_obj, expand_defs=True, shrink_defs=False):
-        """Validates def and def-expand tags, and optionally expands or shrinks them.
-
-        Parameters
-        ----------
-        hed_string_obj : HedString
-            The hed string to modify.
-        expand_defs: bool
-            If true, convert all located def tags to def-expand tags, and plug in the definitions.
-        shrink_defs: bool
-            If True, this will shrink all def-expand groups to def tags
-        Returns
-        -------
-        def_issues: []
-            Issues found related to validating defs.  Usually mismatched placeholders, or a missing definition.
+        """ Validate Def and Def-expand tags, and optionally expands or shrinks them.
+
+        Args:
+            hed_string_obj (HedString): The hed string to process.
+            expand_defs (bool): If true, convert def tags to def-expand tag groups that include definition content.
+            shrink_defs (bool): If True, replace all def-expand groups with corresponding def tags.
+
+        Returns:
+            list: Issues found related to validating defs. Each issue is a dictionary.
+
+        Notes:
+            Usually issues are mismatched placeholders or a missing definition.
+            The expand_defs and shrink_defs cannot both be True.
+
         """
+
         # First see if the "def" is found at all.  This covers def and def-expand.
         hed_string_lower = hed_string_obj.lower()
         if self._label_tag_name not in hed_string_lower:
@@ -133,36 +134,31 @@ def expand_def_tags(self, hed_string_obj, expand_defs=True, shrink_defs=False):
             if def_expand_group is def_tag:
                 if def_contents is not None and expand_defs:
                     def_tag.short_base_tag = DefTagNames.DEF_EXPAND_ORG_KEY
-                    def_group.replace_tag(def_tag, def_contents)
+                    def_group.replace(def_tag, def_contents)
             else:
                 if def_contents is not None and shrink_defs:
                     def_tag.short_base_tag = DefTagNames.DEF_ORG_KEY
-                    def_group.replace_tag(def_expand_group, def_tag)
+                    def_group.replace(def_expand_group, def_tag)
 
         return def_issues
 
     def expand_and_remove_definitions(self, hed_string_obj, check_for_definitions=False, expand_defs=True,
                                       shrink_defs=False, remove_definitions=True):
-        """
-            Validate any def tags and remove any definitions found in the given hed string.
-
-        Parameters
-        ----------
-        hed_string_obj : HedString
-            The string to validate.
-        check_for_definitions : bool
-            If True, this will first check the hed string for any definitions.  This mostly applies to validate
-                hed strings individually, not as part of a file.
-        expand_defs : bool
-            If true, convert all located def tags to def-expand tags, and plug in the definitions.
-        shrink_defs: bool
-            If True, this will shrink all def-expand groups to def tags
-        remove_definitions: bool
-            If true, this will remove all definition tags found.
-        Returns
-        -------
-        def_issues: [{}]
-            The issues found with def tags.
+        """ Validate any Definition tags and remove any definitions found in the given hed string.
+
+        Args:
+            hed_string_obj (HedString): The string to search for definitions.
+            check_for_definitions  (bool): If True, this will first check the hed string for any definitions.
+            expand_defs (bool):        If True, replace Def tags to Def-expand tag groups.
+            shrink_defs (bool):        If True, replace Def-expand groups with Def tags.
+            remove_definitions (bool): If true, this will remove all Definition tag groups.
+
+        Returns:
+            def_issues (list): A list of issues for definition-related tags in this string. Each issue is a dictionary.
+
+        Notes:
+            The check_for_definitions is mainly used for individual HedStrings in isolation.
+
         """
         def_issues = []
         if check_for_definitions:
@@ -176,21 +172,17 @@ def expand_and_remove_definitions(self, hed_string_obj, check_for_definitions=Fa
         return def_issues
 
     def _get_definition_contents(self, def_tag, def_expand_group, def_issues):
-        """
-            Checks for issues with expanding a tag from def to def-expand, and returns the expanded tag.
+        """ Check for issues with expanding a tag from Def to a Def-expand tag group and return the expanded tag group.
 
-        Parameters
-        ----------
-        def_tag : HedTag
-            Source hed tag that may be a Def or Def-expand tag.
-        def_expand_group: HedGroup or HedTag
+        Args:
+            def_tag (HedTag): Source hed tag that may be a Def or Def-expand tag.
+            def_expand_group (HedGroup or HedTag):
             Source group for this def-expand tag.  Same as def_tag if this is not a def-expand tag.
         def_issues : [{}]
             List of issues to append any new issues to
 
-        Returns
-        -------
-        def_contents: [HedTag or HedGroup]
+        Returns:
+            def_contents: [HedTag or HedGroup]
             The contents to replace the previous def-tag with.
         """
         # todo: This check could be removed for optimizing
@@ -222,17 +214,18 @@ def _get_definition_contents(self, def_tag, def_expand_group, def_issues):
                     def_issues += ErrorHandler.format_error(ValidationErrors.HED_DEF_VALUE_EXTRA, tag=def_tag)
             pass
         else:
-            raise ValueError("Internal error in DefinitionMapper")
+            raise ValueError("Internal error in DefMapper")
         return None
 
     def __get_string_funcs__(self, **kwargs):
+        """ String funcs for processing definitions. """
         string_funcs = []
         expand_defs = kwargs.get("expand_defs")
         shrink_defs = kwargs.get("shrink_defs")
         remove_definitions = kwargs.get("remove_definitions")
         check_for_definitions = kwargs.get("check_for_definitions")
         if shrink_defs and expand_defs:
-            raise ValueError("Cannot pass both shrink_defs and expand_defs to DefinitionMapper")
+            raise ValueError("Cannot pass both shrink_defs and expand_defs to DefMapper")
         from functools import partial
         string_funcs.append(partial(self.expand_and_remove_definitions,
                                     check_for_definitions=check_for_definitions,
diff --git a/hed/models/def_dict.py b/hed/models/definition_dict.py
similarity index 68%
rename from hed/models/def_dict.py
rename to hed/models/definition_dict.py
index 1a24be46b..38ba5f4e5 100644
--- a/hed/models/def_dict.py
+++ b/hed/models/definition_dict.py
@@ -9,22 +9,17 @@
 from hed.models.hed_ops import HedOps
 
 
-class DefEntry:
-    """Represents a single definition tag"""
+class DefinitionEntry:
+    """ Represents a single definition."""
 
     def __init__(self, name, contents, takes_value, source_context):
-        """Contains info for a single definition tag
-
-        Parameters
-        ----------
-        name : str
-            The label portion of this name(not including definition/)
-        contents: HedGroup
-            The contents of this definition
-        takes_value : bool
-            If True, expects ONE tag to have a single # sign in it.
-        source_context: {}
-            Info about where this definition was declared.
+        """ Initialize info for a single definition.
+
+        Args:
+            name (str):            The label portion of this name (not including Definition/).
+            contents (HedGroup):   The contents of this definition.
+            takes_value (bool):    If True, expects ONE tag to have a single # sign in it.
+            source_context (dict): Info about where this definition was declared.
         """
         self.name = name
         self.contents = contents
@@ -37,20 +32,21 @@ def __init__(self, name, contents, takes_value, source_context):
             add_group_to_dict(contents, self.tag_dict)
 
     def get_definition(self, replace_tag, placeholder_value=None):
-        """
-            Returns a copy of the definition with the tag expanded and the placeholder plugged in
+        """ Return a copy of the definition with the tag expanded and the placeholder plugged in.
 
-        Parameters
-        ----------
-        replace_tag : HedTag
-            The def hed tag to replace with an expanded version
-        placeholder_value : str or None
-            If present and required, will replace any pound signs in the definition contents
+        Args:
+            replace_tag (HedTag): The def hed tag to replace with an expanded version
+            placeholder_value (str or None): If present and required, will replace any pound signs in the definition contents
 
-        Returns
-        -------
-        expanded_tag_name, def_contents: str, [HedGroup or HedTag]
+        Returns:
+            str:          The expanded def tag name
+            HedGroup:
+            expanded_tag_name, def_contents: str, [HedGroup or HedTag]
             The expanded def tag name, and the contents of this definition(including the def tag itself)
+
+        Raises:
+            ValueError:  If a placeholder_value is passed, but this definition doesn't have a placehold.
+
         """
         if self.takes_value == (placeholder_value is None):
             return None, []
@@ -76,20 +72,17 @@ def get_definition(self, replace_tag, placeholder_value=None):
         return f"{DefTagNames.DEF_EXPAND_ORG_KEY}/{name}", output_contents
 
 
-class DefDict(HedOps):
+class DefinitionDict(HedOps):
     """Class responsible for gathering and storing a group of definitions to be considered a single source.
 
-        A bids_old style file might have many of these(one for each json dict, and another for the actual file)
+        A bids_old style file might have many of these(one for each json dict, and another for the actual file).
+
+        This class extends HedOps because it has string_funcs to check for definitions. It has no tag_funcs.
+
     """
 
     def __init__(self):
-        """
-        Class responsible for gathering and storing a group of definitions to be considered a single source.
-
-        A bids_old style file might have many of these(one for each json dict, and another for the actual file)
-        Parameters
-        ----------
-        """
+        """ Initialize the class. gathering and storing a group of definitions to be considered a single source. """
         super().__init__()
         self._defs = {}
 
@@ -97,15 +90,10 @@ def __init__(self):
         self._extract_def_issues = []
 
     def get_definition_issues(self):
-        """
-            Returns definition errors found during extraction
+        """ Return definition errors found during extraction.
 
-        Parameters
-        ----------
-        Returns
-        -------
-        issues_list: [{}]
-            List of DefinitionErrors found.
+        Returns:
+            list: List of DefinitionErrors issues found. Each issue is a dictionary.
         """
         return self._extract_def_issues
 
@@ -115,7 +103,7 @@ def defs(self):
             Provides direct access to internal dictionary.  Alter at your own risk.
         Returns
         -------
-        def_dict: {str: DefEntry}
+        def_dict: {str: DefinitionEntry}
         """
         return self._defs
 
@@ -130,20 +118,18 @@ def __get_tag_funcs__(self, **kwargs):
         return []
 
     def check_for_definitions(self, hed_string_obj, error_handler=None):
-        """
-        Check a given hed string for definition tags, and add them to the dictionary if so.
-
-        Parameters
-        ----------
-        hed_string_obj : HedString
-            A single hed string to gather definitions from
-        error_handler: ErrorHandler
-            Used to note where definitions are found.  Optional.
-        Returns
-        ----------
+        """ Check a given HedString for definition tags, and add them to the dictionary if so.
+
+        Args:
+            hed_string_obj (HedString): A single hed string to gather definitions from.
+            error_handler (ErrorHandler or None): Error context used to identify where definitions are found.
+
+        Returns:
+            list:  List of issues encountered in checking for definitions. Each issue is a dictionary.
+
         """
         new_def_issues = []
-        for definition_tag, group in hed_string_obj.find_top_level_tags(anchors={DefTagNames.DEFINITION_KEY}):
+        for definition_tag, group in hed_string_obj.find_top_level_tags(anchor_tags={DefTagNames.DEFINITION_KEY}):
             def_tag_name = definition_tag.extension_or_value_portion
 
             # initial validation
@@ -155,10 +141,12 @@ def check_for_definitions(self, hed_string_obj, error_handler=None):
                                                            def_name=def_tag_name, tag_list=groups)
                 continue
             if len(group.tags()) != 1:
-                new_def_issues += ErrorHandler.format_error_with_context(error_handler,
-                                                                         DefinitionErrors.WRONG_NUMBER_GROUP_TAGS,
-                                                                         def_name=def_tag_name,
-                                                                         tag_list=[tag for tag in group.tags() if tag is not definition_tag])
+                new_def_issues += \
+                    ErrorHandler.format_error_with_context(error_handler,
+                                                           DefinitionErrors.WRONG_NUMBER_GROUP_TAGS,
+                                                           def_name=def_tag_name,
+                                                           tag_list=[tag for tag in group.tags()
+                                                                     if tag is not definition_tag])
                 continue
 
             group_tag = groups[0] if groups else None
@@ -207,25 +195,27 @@ def check_for_definitions(self, hed_string_obj, error_handler=None):
                                                                          DefinitionErrors.DUPLICATE_DEFINITION,
                                                                          def_name=def_tag_name)
                 continue
-            self._defs[def_tag_lower] = DefEntry(name=def_tag_name, contents=group_tag,
-                                                 takes_value=def_takes_value,
-                                                 source_context=context)
+            self._defs[def_tag_lower] = DefinitionEntry(name=def_tag_name, contents=group_tag,
+                                                        takes_value=def_takes_value,
+                                                        source_context=context)
 
         self._extract_def_issues += new_def_issues
         return new_def_issues
 
 
-# This may be moved later
 def add_group_to_dict(group, tag_dict=None):
-    """
-        Note: Expects tags to have forms calculated already.
-    Parameters:
-        group: HedGroup
-            contents to add to the tag dict
-        tag_dict: {}
-            Output dictionary
-    Returns: dict
-        Dictionary of tags with a list of values
+    """ Add the tags and their values from a HED group to a tag dictionary.
+
+    Args:
+        group (HedGroup):   Contents to add to the tag dict.
+        tag_dict (dict):    The starting tag dictionary to which to add to.
+
+    Returns:
+        dict:  The updated tag_dict containing the tags with a list of values.
+
+    Notes:
+        Expects tags to have forms calculated already.
+
     """
     if tag_dict is None:
         tag_dict = {}
diff --git a/hed/models/events_input.py b/hed/models/events_input.py
index bd07fad8f..16ec5e04b 100644
--- a/hed/models/events_input.py
+++ b/hed/models/events_input.py
@@ -1,7 +1,7 @@
 from hed.models.column_mapper import ColumnMapper
 from hed.models.base_input import BaseInput
 from hed.models.sidecar import Sidecar
-from hed.models.def_mapper import DefinitionMapper
+from hed.models.def_mapper import DefMapper
 
 
 class EventsInput(BaseInput):
@@ -22,8 +22,8 @@ def __init__(self, file=None, sidecars=None, attribute_columns=None, extra_def_d
         attribute_columns: str or int or [str] or [int]
             A list of column names or numbers to treat as attributes.
             Default: ["duration", "onset"]
-        extra_def_dicts: [DefDict] or DefDict or None
-            DefDict objects containing all the definitions this file should use other than the ones coming from the file
+        extra_def_dicts: [DefinitionDict] or DefinitionDict or None
+            DefinitionDict objects containing all the definitions this file should use other than the ones coming from the file
             itself and from the column def groups.  These are added as the last entries, so names will override
             earlier ones.
         also_gather_defs: bool
@@ -59,11 +59,11 @@ def create_def_mapper(self, column_mapper, extra_def_dicts=None):
         ----------
         column_mapper : ColumnMapper
             The column mapper to gather definitions from
-        extra_def_dicts : DefDict or [DefDict]
+        extra_def_dicts : DefinitionDict or [DefinitionDict]
             Optional. Adds any definitions in these to the def mapper as well, in addition to any found in the columns.
         Returns
         -------
-        def mapper: DefinitionMapper
+        def mapper: DefMapper
             A class to validate or expand definitions with the given def dicts.
         """
         def_dicts = []
@@ -74,7 +74,7 @@ def mapper: DefinitionMapper
             extra_def_dicts = [extra_def_dicts]
         if extra_def_dicts:
             def_dicts += extra_def_dicts
-        def_mapper = DefinitionMapper(def_dicts)
+        def_mapper = DefMapper(def_dicts)
 
         return def_mapper
 
diff --git a/hed/models/expression_parser.py b/hed/models/expression_parser.py
new file mode 100644
index 000000000..5f00bf47f
--- /dev/null
+++ b/hed/models/expression_parser.py
@@ -0,0 +1,354 @@
+import re
+
+
+# Syntax:
+# and = "and" or ","
+# or = "or"
+# logical operation grouping = "(" and ")"
+# inexact grouping = "[" and "]"
+# same group grouping = "[[" and "]]"
+# not = "~"
+
+class Token:
+    And = 0
+    Tag = 1
+    ExactGroup = 2
+    ExactGroupEnd = 3
+    ContainingGroup = 8
+    ContainingGroupEnd = 9
+    Or = 4
+    LogicalGroup = 5
+    LogicalGroupEnd = 6
+    LogicalNegation = 7
+
+    def __init__(self, text):
+        tokens = {
+            ",": Token.And,
+            "and": Token.And,
+            "or": Token.Or,
+            "[[": Token.ExactGroup,
+            "]]": Token.ExactGroupEnd,
+            "[": Token.ContainingGroup,
+            "]": Token.ContainingGroupEnd,
+            "(": Token.LogicalGroup,
+            ")": Token.LogicalGroupEnd,
+            "~": Token.LogicalNegation,
+        }
+        self.kind = tokens.get(text, Token.Tag)
+        self.text = text
+
+    def __str__(self):
+        return self.text
+
+    def __eq__(self, other):
+        if self.kind == other:
+            return True
+        return False
+
+
+class Expression:
+    def __init__(self, token, left=None, right=None):
+        self.left = left
+        self.right = right
+        self.token = token
+
+    def __str__(self):
+        output_str = ""
+        if self.left:
+            output_str += str(self.left)
+        output_str += " " + str(self.token)
+        if self.right:
+            output_str += str(self.right)
+        return output_str
+
+    def handle_expr(self, hed_group, exact=False):
+        # groups_found = hed_group.find_exact_tags([self.token.text], recursive=True)
+        groups_found = hed_group.find_tags_with_term(self.token.text, recursive=True, include_groups=1)
+        if exact:
+            all_found_groups = groups_found
+        else:
+            all_found_groups = []
+            for group in groups_found:
+                while group:
+                    all_found_groups.append(group)
+                    group = group._parent
+        return all_found_groups
+
+
+class ExpressionAnd(Expression):
+    def handle_expr(self, hed_group, exact=False):
+        groups1 = self.left.handle_expr(hed_group, exact=exact)
+        if not groups1:
+            return groups1
+        groups2 = self.right.handle_expr(hed_group, exact=exact)
+        return [group for group in groups1 if group in groups2]
+
+
+class ExpressionOr(Expression):
+    def handle_expr(self, hed_group, exact=False):
+        groups1 = self.left.handle_expr(hed_group, exact=exact)
+        # Don't early out as we need to gather all groups incase tags appear more than once etc
+        groups2 = self.right.handle_expr(hed_group, exact=exact)
+        return groups1 + groups2
+
+
+class ExpressionNegation(Expression):
+    def handle_expr(self, hed_group, exact=False):
+        groups = self.right.handle_expr(hed_group, exact=exact)
+
+        # todo: This probably needs fixing
+        negated_groups = [group for group in hed_group.get_all_groups() if group not in groups]
+        # Possible alternate if only groups with tags are valid results(is this a real case?)
+        #negated_groups = [group for group in hed_group.get_all_groups() if group not in groups if group.tags()]
+
+        return negated_groups
+
+
+class ExpressionLogicalGroup(Expression):
+    def handle_expr(self, hed_group, exact=False):
+        return self.right.handle_expr(hed_group, exact=exact)
+
+
+class ExpressionExactGroup(Expression):
+    def handle_expr(self, hed_group, exact=False):
+        result = self.right.handle_expr(hed_group, exact=True)
+        found_groups = result
+        if result:
+            found_parent_groups = list()
+            for group in found_groups:
+                if not group.is_group:
+                    continue
+                if group._parent:
+                    found_parent_groups.append(group._parent)
+
+            if found_parent_groups:
+                return found_parent_groups
+
+        return []
+
+
+class ExpressionContainingGroup(Expression):
+    def handle_expr(self, hed_group, exact=False):
+        result = self.right.handle_expr(hed_group)
+        found_groups = result
+        found_parent_groups = list()
+        if result:
+            for group in found_groups:
+                if not group.is_group:
+                    continue
+                if group._parent:
+                    found_parent_groups.append(group._parent)
+
+
+        if found_parent_groups:
+            return found_parent_groups
+        return []
+
+
+class TagExpressionParser:
+    """Parse a search expression into a form than can be used to search a hed string."""
+    def __init__(self, expression_string):
+        self.tokens = []
+        self.at_token = -1
+        self.tree = self._parse(expression_string.lower())
+        self._org_string = expression_string
+
+    def _get_next_token(self):
+        self.at_token += 1
+        if self.at_token >= len(self.tokens):
+            raise ValueError("Parse error in get next token")
+        return self.tokens[self.at_token]
+
+    def _next_token_is(self, kinds):
+        if self.at_token + 1 >= len(self.tokens):
+            return None
+        if self.tokens[self.at_token + 1].kind in kinds:
+            return self._get_next_token()
+        return None
+
+    def _handle_and_op(self):
+        expr = self._handle_negation()
+        next_token = self._next_token_is([Token.And, Token.Or])
+        while next_token:
+            right = self._handle_negation()
+            if next_token.kind == Token.And:
+                expr = ExpressionAnd(next_token, expr, right)
+            else:
+                expr = ExpressionOr(next_token, expr, right)
+            next_token = self._next_token_is([Token.And, Token.Or])
+
+        return expr
+
+    def _handle_negation(self):
+        next_token = self._next_token_is([Token.LogicalNegation])
+        if next_token == Token.LogicalNegation:
+            interior = self._handle_grouping_op()
+            expr = ExpressionNegation(next_token, right=interior)
+            return expr
+        else:
+            return self._handle_grouping_op()
+
+    def _handle_grouping_op(self):
+        next_token = self._next_token_is([Token.ExactGroup, Token.LogicalGroup, Token.ContainingGroup])
+        if next_token == Token.ExactGroup:
+            interior = self._handle_and_op()
+            expr = ExpressionExactGroup(next_token, right=interior)
+            next_token = self._next_token_is([Token.ExactGroupEnd])
+            if next_token != Token.ExactGroupEnd:
+                raise ValueError("Parse error: Missing closing square brackets")
+        elif next_token == Token.LogicalGroup:
+            interior = self._handle_and_op()
+            expr = ExpressionLogicalGroup(next_token, right=interior)
+            next_token = self._next_token_is([Token.LogicalGroupEnd])
+            if next_token != Token.LogicalGroupEnd:
+                raise ValueError("Parse error: Missing closing paren")
+        elif next_token == Token.ContainingGroup:
+            interior = self._handle_and_op()
+            expr = ExpressionContainingGroup(next_token, right=interior)
+            next_token = self._next_token_is([Token.ContainingGroupEnd])
+            if next_token != Token.ContainingGroupEnd:
+                raise ValueError("Parse error: Missing closing square bracket")
+        else:
+            next_token = self._get_next_token()
+            if next_token:
+                expr = Expression(next_token)
+
+        return expr
+
+    def _parse(self, expression_string):
+        self.tokens = self._tokenize(expression_string)
+
+        expr = self._handle_and_op()
+
+        if self.at_token + 1 != len(self.tokens):
+            raise ValueError("Parse error in search string")
+
+        return expr
+
+    def _tokenize(self, expression_string):
+        grouping_re = r"\[\[|\[|,|\]\]|\]"
+        paren_re = "\)|\(|~"
+        word_re = r"\band\b|\bor\b|[_\-a-zA-Z0-9/.^#]+"
+        re_string = fr"({grouping_re}|{paren_re}|{word_re})"
+        token_re = re.compile(re_string)
+
+        tokens = token_re.findall(expression_string)
+        tokens = [Token(token) for token in tokens]
+
+        return tokens
+
+    def search_hed_string(self, hed_string_obj):
+        current_node = self.tree
+
+        result = current_node.handle_expr(hed_string_obj)
+        return result
+
+
+if __name__ == '__main__':
+    from hed.models.hed_string import HedStringFrozen
+    from hed.schema.hed_schema_io import load_schema_version
+    hed_schema = load_schema_version(xml_version='8.0.0')
+    my_string = "Sensory-event,(Intended-effect,Cue),((Def-expand/Circle-only," + \
+                "(Visual-presentation,(Foreground-view,((White,Circle)," + \
+                "(Center-of,Computer-screen))),(Background-view,Black))),Onset)," + \
+                "((Def-expand/Face-image,(Visual-presentation,(Foreground-view,((Image,Face,Hair)," + \
+                "Color/Grayscale),((White,Cross),(Center-of,Computer-screen)))," + \
+                "(Background-view,Black))),Offset),((Def-expand/Blink-inhibition-task," + \
+                "(Task,Experiment-participant,Inhibit-blinks)),Offset)," + \
+                "((Def-expand/Fixation-task,(Task,Experiment-participant,(Fixate,Cross)" + \
+                ")),Offset),Experimental-trial/1,(Image,Pathname/circle.bmp)"
+    hed_string = HedStringFrozen(my_string, hed_schema=hed_schema)
+    my_string1 = "sensory-event,(intended-effect,cue)"
+    hed_string1 = HedStringFrozen(my_string1, hed_schema=hed_schema)
+    expression1 = TagExpressionParser("sensory-event")
+    print(expression1.search_hed_string(hed_string1), True)
+    expression2 = TagExpressionParser("sensory-event, cue")
+    print(expression2.search_hed_string(hed_string1), True)
+    expression3 = TagExpressionParser("onset")
+    print(expression3.search_hed_string(hed_string), True)
+    expression = TagExpressionParser("[[a,b]]")
+    hed_string = HedStringFrozen("A, C, D, B")
+    x = expression.search_hed_string(hed_string)
+    print(expression.search_hed_string(hed_string), False)  # False
+    hed_string = HedStringFrozen("(A, B, C, D)")
+    y = expression.search_hed_string(hed_string)
+    print(expression.search_hed_string(hed_string), True)  # True
+    hed_string = HedStringFrozen("(A, C, D, B)")
+    print(expression.search_hed_string(hed_string), True)  # True
+
+    # Doesn't handle and/or precedence well(right to left and equal precedent currently)
+    expression = TagExpressionParser("a and b or c")
+    hed_string = HedStringFrozen("B")
+    print(expression.search_hed_string(hed_string), False)  # False
+    hed_string = HedStringFrozen("A, B")
+    print(expression.search_hed_string(hed_string), True)  # True
+    hed_string = HedStringFrozen("A, C")
+    print(expression.search_hed_string(hed_string), True)  # True
+    hed_string = HedStringFrozen("C")
+    print(expression.search_hed_string(hed_string), True)  # TRue
+
+    expression = TagExpressionParser("a or b and c")
+    hed_string = HedStringFrozen("B")
+    print(expression.search_hed_string(hed_string), False)  # False
+    hed_string = HedStringFrozen("A, B")
+    print(expression.search_hed_string(hed_string), False)  # False
+    hed_string = HedStringFrozen("A, C")
+    print(expression.search_hed_string(hed_string), True)  # True
+    hed_string = HedStringFrozen("C")
+    print(expression.search_hed_string(hed_string), False)  # false
+    hed_string = HedStringFrozen("A")
+    print(expression.search_hed_string(hed_string), False)  # false
+
+    expression = TagExpressionParser("a and (b or c)")
+    hed_string = HedStringFrozen("A, C")
+    print(expression.search_hed_string(hed_string), True)  # True
+
+    expression = TagExpressionParser("[[a,b]] or [[c, d]]")
+    hed_string = HedStringFrozen("(c, d)")
+    print(expression.search_hed_string(hed_string), True)  # True
+    hed_string = HedStringFrozen("(b, d)")
+    print(expression.search_hed_string(hed_string), False)  # False
+    hed_string = HedStringFrozen("(b, a)")
+    print(expression.search_hed_string(hed_string), True)  # True
+
+    expression = TagExpressionParser("[[a,b]] and [[c, d]]")
+    hed_string = HedStringFrozen("(c, d, a, b)")
+    print(expression.search_hed_string(hed_string), True)  # True
+    hed_string = HedStringFrozen("(c, d, a)")
+    print(expression.search_hed_string(hed_string), False)  # False
+    hed_string = HedStringFrozen("(c, d), (a, b)")
+    print(expression.search_hed_string(hed_string), True)  # True
+    breakHere = 3
+
+    expression = TagExpressionParser("[[a,b]] and d")
+    hed_string = HedStringFrozen("(a, b), d")
+    print(expression.search_hed_string(hed_string), True)  # True
+
+    expression = TagExpressionParser("[[a,b]] and d")
+    hed_string = HedStringFrozen("(a, b), c")
+    print(expression.search_hed_string(hed_string), False)  # False
+
+    expression = TagExpressionParser("[[a,b]] or d")
+    hed_string = HedStringFrozen("d")
+    print(expression.search_hed_string(hed_string), True)  # True
+
+    hed_string = HedStringFrozen("a, b")
+    print(expression.search_hed_string(hed_string), False)  # False
+
+    expression = TagExpressionParser("[[a,b,[[c,d]]]]")
+    # # Should this be valid?  It's iffy.
+    hed_string = HedStringFrozen("(a, b, c, d)")
+    print(expression.search_hed_string(hed_string), False)  # False
+
+    hed_string = HedStringFrozen("(a, b, (c, d))")
+    y = expression.search_hed_string(hed_string)
+    print(expression.search_hed_string(hed_string), True)  # True
+
+    expression = TagExpressionParser("[[a,b,[[c,d, [[e, f]]]]]]")
+    hed_string = HedStringFrozen("(a, b, ((e, f), c, d))")
+    print(expression.search_hed_string(hed_string), True)  # True
+
+    expression = TagExpressionParser("[[a,b,[[c,d, [[e, f]]]]]]")
+    hed_string = HedStringFrozen("(a, b, (e, f, c, d))")
+    print(expression.search_hed_string(hed_string), False)  # False
+
diff --git a/hed/models/hed_group.py b/hed/models/hed_group.py
index 61f0baaa9..487089228 100644
--- a/hed/models/hed_group.py
+++ b/hed/models/hed_group.py
@@ -6,22 +6,20 @@
 class HedGroup(HedGroupBase):
     """ Represents a single parenthesized hed string. """
 
-    def __init__(self, hed_string="", startpos=None, endpos=None,
-                 contents=None):
-        """
-        Returns an empty HedGroup object
-
-        Parameters
-        ----------
-        hed_string : str
-            Source hed string for this group
-        startpos : int
-            Starting index of group(including parentheses) in hed_string
-        endpos : int
-            Ending index of group(including parentheses) in hed_string
-        contents : [HedTag or HedGroup]
-            A list of tags and groups that will be set as the contents of this group.
+    def __init__(self, hed_string="", startpos=None, endpos=None, contents=None):
+        """ Return an empty HedGroup object.
+
+        Args:
+            hed_string (str or None): Source hed string for this group.
+            startpos (int or None):   Starting index of group(including parentheses) in hed_string.
+            endpos (int or None):     Position after the end (including parentheses) in hed_string.
+            contents (list or None):  A list of HedTags and/or HedGroups that will be set as the contents of this group.
+
+        Notes:
+            contents is mainly used for processing definitions.
+
         """
+
         super().__init__(hed_string=hed_string, startpos=startpos, endpos=endpos)
         if contents:
             self._children = contents
@@ -31,38 +29,55 @@ def __init__(self, hed_string="", startpos=None, endpos=None,
         self.mutable = True  # If False, this group is potentially referenced in other places and should not be altered
 
     def __eq__(self, other):
+        """ Test if the same object or a HedGroup with same contents.
+
+            Args:
+                other (object):  Any object.
+
+            Returns:
+                bool:  True if other is the same as this HedGroup or is a HedGroup with the same contents.
+
+        TODO: check to see if can be moved to HedGroupBase.
+
+        """
+
         if self is other:
             return True
-
         if not isinstance(other, HedGroup):
             return False
 
-        if self._children != other._children:
+        if self.children != other.children:
             return False
         if self.is_group != other.is_group:
             return False
         return True
 
-    def append(self, new_tag_or_group):
-        """
-        Add a tag or group to this group.
+    def append(self, tag_or_group):
+        """ Add a tag or group to this group.
+
+        Args:
+            tag_or_group (HedTag or HedGroup): The new object to add to this group.
+
+        Raises:
+            ValueError: If a HedGroupFrozen.
 
-        Parameters
-        ----------
-        new_tag_or_group : HedTag or HedGroup
-            The new object to add
         """
         if not self.mutable:
             raise ValueError("Trying to alter immutable group")
 
-        self._children.append(new_tag_or_group)
+        self._children.append(tag_or_group)
 
-    def check_if_in_original_tags_and_groups(self, tag_or_group):
-        """
-            Checks if the tag or group was in the original string, even if it has since been removed.
+    def check_if_in_original(self, tag_or_group):
+        """ Check if the tag or group was in the original string, even if it has since been removed.
+
+        Args:
+            tag_or_group (HedTag or HedGroup): The HedTag or HedGroup to be looked for in this group.
+
+        Returns:
+            bool:  True if in this group.
 
-        Returns
-        -------
+        Notes:
+            Creates a list of full tags and groups contained in this group.
 
         """
         node_list = [self]
@@ -77,15 +92,16 @@ def check_if_in_original_tags_and_groups(self, tag_or_group):
 
         return self._check_in_group(tag_or_group, final_list)
 
-    def replace_tag(self, tag_or_group, new_contents):
-        """ Replaces an existing tag in the group with a new tag, list, or group
+    def replace(self, item_to_replace, new_contents):
+        """ Replace an existing tag or group in the group with a new tag, list, or group.
+
+        Args:
+            item_to_replace (HedTag or HedGroup): The item to  replace must exist or this will raise an error.
+            new_contents (HedTag, HedGroup or list of HedTag and/or HedGroup): Replacement contents.
+
+        Raises:
+            ValueError:  If this HedGroup is not mutable.
 
-        Parameters
-        ----------
-        tag_or_group : HedTag or HedGroup
-            The tag to replace.  It must exist or this will raise an error.
-        new_contents : HedTag or HedGroup or [HedTag or HedGroup]
-            What to replace the tag with.
         """
         if not self.mutable:
             raise ValueError("Trying to alter immutable group")
@@ -95,19 +111,22 @@ def replace_tag(self, tag_or_group, new_contents):
 
         replace_index = -1
         for i, child in enumerate(self._children):
-            if tag_or_group is child:
+            if item_to_replace is child:
                 replace_index = i
                 break
         self._children[replace_index] = new_contents
 
     def __copy__(self):
-        """
-            Return a shallow copy with a new _children list, rather than the same list
+        """ Return a shallow mutable copy with a new _children list, rather than the same list.
 
-            Also mark the copy as mutable.
+        Returns:
+            HedGroup:  A mutable shallow copy of this HedGroup.
 
-        Returns
-        -------
+        Notes:
+            The child HedTags and HedGroups are the same objects (similar to list copy).
+            This operation is mainly used for handling definitions.
+
+        TODO: Check to see if can be moved to HedGroupBase.
 
         """
         cls = self.__class__
@@ -118,16 +137,11 @@ def __copy__(self):
         return result
 
     def cascade_mutable(self, value):
-        """
-            Sets the mutable property for all child tags and groups.
+        """ Set the mutable property for all child tags and groups.
 
-        Parameters
-        ----------
-        value: bool
-            If they should be mutable
+        Args:
+            value (bool):  True if all should be mutable, False if not.
 
-        Returns
-        -------
         """
         self.mutable = value
         for child in self._children:
@@ -136,50 +150,67 @@ def cascade_mutable(self, value):
             else:
                 child.mutable = value
 
-    def remove_groups(self, remove_groups):
-        """
-            Remove any tags/groups in remove_groups found in this hed string.
+    def remove(self, items_to_remove):
+        """Remove any tags/groups in items_to_remoe found in this HedGroup.
+
+        Args:
+            items_to_remove (list):  List of HedGroups and/or HedTags to remove.
 
+        Notes:
             Any groups that become empty will also be pruned.
+            Identity, not equivalence is used in determining whether or not to remove.
 
-        Parameters
-        ----------
-        remove_groups : [HedGroup or HedTag]
-            A list of groups or tags to remove.
         """
-        empty_groups = []
         all_groups = self.get_all_groups()
-        for remove_child in remove_groups:
+        self._remove(items_to_remove, all_groups)
+
+    def _remove(self, items_to_remove, all_groups):
+        """ Needs to be documented.
+
+        Args:
+            items_to_remove ( ):
+            all_groups ( ):
+
+        Returns:
+
+        Raise:
+            ValueError: ?
+
+        """
+        empty_groups = []
+        for remove_child in items_to_remove:
             for group in all_groups:
-                if remove_child in group._children:
+                # only proceed if we have an EXACT match for this child
+                if any(remove_child is child for child in group._children):
                     if not group.mutable:
                         raise ValueError("Trying to alter immutable group")
                     if group._original_children is group._children:
                         group._original_children = group._children.copy()
 
                     group._children = [child for child in group._children if child is not remove_child]
+                    # If this was the last child, flag this group to be removed on a second pass
                     if not group._children and group is not self:
                         empty_groups.append(group)
                     break
 
         if empty_groups:
-            return self.remove_groups(empty_groups)
+            return self.remove(empty_groups)
 
     def make_tag_mutable(self, tag):
-        """
-            Replace the given tag in the group with a tag you can alter.
+        """ Replace the given tag in this group with a tag that can be altered and return new version.
+
+        Args:
+            tag (HedTag): The tag to make mutable.
 
-            This is more or less exclusively for def expansion.
+        Returns:
+            HedTag: A mutable new copy of the tag, or the old mutable one.
 
-        Parameters
-        ----------
-        tag: HedTag
-            The tag you want to make mutable
+        Raises:
+            ValueError:  If this HedGroup is not already mutable, the operation fails.
+
+        Notes:
+            This method is more or less exclusively for definition expansion.
 
-        Returns
-        -------
-        mutable_tag: HedTag
-            A mutable new copy of the tag, or the old mutable one.
         """
         if not self.mutable:
             raise ValueError("Trying to alter immutable group")
@@ -187,59 +218,73 @@ def make_tag_mutable(self, tag):
         if not tag.mutable:
             tag_copy = copy.copy(tag)
             tag_copy.mutable = True
-            self.replace_tag(tag, tag_copy)
+            self.replace(tag, tag_copy)
             return tag_copy
         return tag
 
     def get_frozen(self):
-        """
-            Returns a frozen copy of this HedGroup.
+        """ Returns a frozen (non-mutable) copy of this HedGroup.
 
-        Returns: HedGroupFrozen
-            A frozen copy of this HedGroup.  Note: tags still point to the same place.  Do not alter them.
+        Returns:
+            HedGroupFrozen: A frozen copy of this HedGroup.
+
+        Notes:
+            The tags still point to the same place.  Do not alter them.
 
         """
         return HedGroupFrozen(self)
 
 
 class HedGroupFrozen(HedGroupBase):
-    """
-        This is the same as a HedGroup, except it is frozen as sets and cannot be altered further.
+    """ This is the same as a HedGroup, except it is frozen as sets and cannot be altered further.
+
+        Searching and getting all tags/groups will be faster.
+
+        Additionally, HedGroupFrozen is order-agnostic: (a, b) = (b, a)
 
-        Searching and getting all tags/groups will be faster.  Additionally, HedGroupFrozen is order-agnostic.
-        (a, b) = (b, a)
     """
     def __init__(self, contents, hed_string=None):
-        """
+        """ Initialize a frozen group.
 
         Args:
-            contents: HedGroup or [HedGroup or HedTag]
-                If this is a group, get the source hed_string from the group.
-                If a list, it gets the source hed_string from the parameter.
-            hed_string: str or None
-                If using a list, this is the raw string the contents came from
+            contents (HedGroupBase or list): Used to set the contents of this group.
+                If HedGroupBase, get the source hed_string from the group.
+                If a list, may be a mixture of HedTags and HedGroups. Use the hed_string parameter as the source.
+            hed_string (str or None): If contents is a list, this is the raw string the contents came from.
+
         """
         if isinstance(contents, HedGroupBase):
             span = contents.span
             hed_string = contents._hed_string
-            contents = contents._children
+            contents = contents.children
         else:
             if hed_string:
                 span = 0, len(hed_string)
             else:
                 span = 0, 0
 
+        # Possibly temporary: Have frozen groups always know their parents
+        self._parent = None
+
         super().__init__(hed_string=hed_string, startpos=span[0], endpos=span[1])
         if contents is not None:
             self._children = frozenset(child if not isinstance(child, HedGroupBase)
                                        else HedGroupFrozen(child) for child in contents)
+            for child in self.groups():
+                child._parent = self
         else:
             self._children = None
 
+
     def __hash__(self):
+        """ Get a hash of this HedFrozenGroup."""
         return hash((self._children, self.is_group))
 
     def __eq__(self, other):
+        """
+        TODO:  Put in the HedGroupBase
+
+        """
         if self is other:
             return True
 
@@ -253,34 +298,28 @@ def __eq__(self, other):
             return False
         return True
 
-    # Cache these values as we know this is frozen
     def get_all_tags(self):
-        """
-        Returns all the tags, including descendants.
+        """ Return all the HedTags in this group, including descendants.
+
+        Returns:
+            list: A list of all the HedTags in this group including descendants.
+
+        Notes:
+            This list is cached when initially called since its contents never change.
 
-        Returns
-        -------
-        tag_list: [HedTag]
-            The list of all tags in this group, including descendants.
         """
         if not hasattr(self, "_tags"):
             self._tags = super().get_all_tags()
-
         return self._tags
 
     def get_all_groups(self, also_return_depth=False):
-        """
-        Returns all the HedGroups, including descendants and self.
+        """ Return all the HedGroups, including descendants and self.
 
-        Parameters
-        ----------
-        also_return_depth : bool
-            If True, this yields tuples(group, depth) rather than just groups
+        Args:
+            also_return_depth (bool): If True, this yields tuples (group, depth) rather than just groups.
 
-        Returns
-        -------
-        group_list: [HedGroup]
-            The list of all HedGroups in this group, including descendants and self.
+        Returns:
+            list: The list of all HedGroups in this group, including descendants and self.
         """
         if also_return_depth:
             if not hasattr(self, "_groups1"):
@@ -292,11 +331,12 @@ def get_all_groups(self, also_return_depth=False):
             return self._groups2
 
     def get_frozen(self):
-        """
-            Returns this frozen group.  This is just used for the hed validator.
+        """ Return this frozen group.
 
-        Returns: HedGroupFrozen
-            This same group.
+        Returns:
+            HedGroupFrozen: This same group.
 
+        Notes:
+            The base
         """
         return self
diff --git a/hed/models/hed_group_base.py b/hed/models/hed_group_base.py
index 41c0c10e8..e23068194 100644
--- a/hed/models/hed_group_base.py
+++ b/hed/models/hed_group_base.py
@@ -2,20 +2,16 @@
 
 
 class HedGroupBase:
-    """ This is an abstract baseclass used for HedGroup API, so it's shared with HedGroupFrozen """
+    """ An abstract baseclass used for HedGroup API that is shared with HedGroupFrozen. """
 
     def __init__(self, hed_string="", startpos=None, endpos=None):
-        """
-        Returns an empty HedGroup object
-
-        Parameters
-        ----------
-        hed_string : str
-            Source hed string for this group
-        startpos : int
-            Starting index of group(including parentheses) in hed_string
-        endpos : int
-            Ending index of group(including parentheses) in hed_string
+        """ Return an empty HedGroupBase object.
+
+        Args:
+            hed_string (str): Source hed string for this group.
+            startpos (int): Starting index of group (including parentheses) in hed_string.
+            endpos (int): Ending index of group (including parentheses) in hed_string.
+
         """
         self._startpos = startpos
         self._endpos = endpos
@@ -25,32 +21,18 @@ def __init__(self, hed_string="", startpos=None, endpos=None):
         # placeholder just to make IDE not complain
         self._children = None
 
+    @property
+    def children(self):
+        """ Return a list of the direct children of this string. """
+        return self._children
+
     @property
     def is_group(self):
-        """
-            Returns True if this is a group with parenthesis.
-        """
+        """ Return True if this is a group with parenthesis. """
         return True
 
-    def get_direct_children(self):
-        """
-        Returns an iterator over all HedTags and HedGroups that are in this group.  Not recursive.
-
-        Returns
-        -------
-        iterator over the direct children
-        """
-        return self._children
-
     def get_all_tags(self):
-        """
-        Returns all the tags, including descendants.
-
-        Returns
-        -------
-        tag_list: [HedTag]
-            The list of all tags in this group, including descendants.
-        """
+        """ Return all the HedTags in this group, including descendants. """
         node_list = [self]
         final_list = []
 
@@ -58,24 +40,20 @@ def get_all_tags(self):
         while node_list:
             current_group_or_tag = node_list.pop(0)
             if isinstance(current_group_or_tag, HedGroupBase):
-                node_list = list(current_group_or_tag._children) + node_list
+                node_list = list(current_group_or_tag.children) + node_list
             else:
                 final_list.append(current_group_or_tag)
         return final_list
 
     def get_all_groups(self, also_return_depth=False):
-        """
-        Returns all the HedGroups, including descendants and self.
+        """ Return all the HedGroups, including descendants and self.
 
-        Parameters
-        ----------
-        also_return_depth : bool
-            If True, this yields tuples(group, depth) rather than just groups
+        Args:
+            also_return_depth (bool): If True, yield tuples (group, depth) rather than just groups.
+
+        Returns:
+            group_list (list): The list of all HedGroups in this group, including descendants and self.
 
-        Returns
-        -------
-        group_list: [HedGroup]
-            The list of all HedGroups in this group, including descendants and self.
         """
         node_list = [self]
         final_list = []
@@ -84,7 +62,7 @@ def get_all_groups(self, also_return_depth=False):
         while node_list:
             current_group_or_tag = node_list.pop(0)
             if isinstance(current_group_or_tag, HedGroupBase):
-                node_list = list(current_group_or_tag._children) + node_list
+                node_list = list(current_group_or_tag.children) + node_list
                 final_list.append(current_group_or_tag)
 
         if also_return_depth:
@@ -95,130 +73,127 @@ def get_all_groups(self, also_return_depth=False):
 
     @staticmethod
     def _check_in_group(group, group_list):
+        """ Return true if the group is in the group list.
+
+        Args:
+            group ():
+            group_list ():
+
+        Returns:
+            bool: True if group is in the group_list.
+
+        """
         for val in group_list:
             if val is group:
                 return True
         return False
 
     def tags(self):
-        """
-        Returns the direct child tags of this group, filtering out HedGroup children
+        """ Return the direct child tags of this group, filtering out HedGroup children.
+
+        Returns:
+            tag_list (list): The list of all tags directly in this group.
 
-        Returns
-        -------
-        tag_list: [HedTag]
-            The list of all tags directly in this group.
         """
-        return [tag for tag in self._children if isinstance(tag, HedTag)]
+        return [tag for tag in self.children if isinstance(tag, HedTag)]
 
     def groups(self):
-        """
-        Returns the direct child groups of this group, filtering out HedTag children
+        """ Return the direct child groups of this group, filtering out HedTag children.
+
+        Return:
+            group_list (list): The list of all groups directly in this group.
 
-        Returns
-        -------
-        group_list: [HedGroup]
-            The list of all tags directly in this group.
         """
-        return [group for group in self._children if isinstance(group, HedGroupBase)]
+        return [group for group in self.children if isinstance(group, HedGroupBase)]
 
     def get_original_hed_string(self):
-        """
-            Get the original hed string with zero modification
+        """ Get the original hed string with zero modification.
+
+        Returns:
+            str: The original string.
 
-        Returns
-        -------
-        hed_string: str
-            The original string
         """
         return self._hed_string[self._startpos:self._endpos]
 
     @property
     def span(self):
-        """
-        Return the source span of this group from the source hed_string
-        Returns
-        -------
-        span: (int, int)
-            The start and end index of the group(including parentheses) from the source string
+        """ Return the source span of this group from the source hed string.
+
+        Return:
+            int: start index of the group (including parentheses) from the source string.
+            int: end index of the group (including parentheses) from the source string.
+
         """
         return self._startpos, self._endpos
 
     def __str__(self):
-        """
-        Convert this HedGroup to a string
+        """ Convert this HedGroup to a string.
+
+        Returns:
+            str: The group as a string, including any modified HedTags.
 
-        Returns
-        -------
-        str
-            Returns the group as a string, including any modified HedTags
         """
         if self.is_group:
-            return "(" + ",".join([str(child) for child in self._children]) + ")"
-        return ",".join([str(child) for child in self._children])
+            return "(" + ",".join([str(child) for child in self.children]) + ")"
+        return ",".join([str(child) for child in self.children])
 
     def get_as_short(self):
-        """
-        Convert this HedGroup to a short tag string
+        """ Return this HedGroup as a short tag string.
+
+        Returns:
+            str: The group as a string with all tags as short tags.
 
-        Returns
-        -------
-        str
-            Returns the group as a string, returning all tags as short tags.
         """
         return self.get_as_form("short_tag")
 
     def get_as_long(self):
-        """
-        Convert this HedGroup to a long tag string
+        """ Return this HedGroup as a long tag string.
+
+        Returns:
+            str: The group as a string with all tags as long tags.
 
-        Returns
-        -------
-        str
-            Returns the group as a string, returning all tags as long tags.
         """
         return self.get_as_form("long_tag")
 
     def get_as_form(self, tag_attribute, tag_transformer=None):
-        """
+        """ Get the string corresponding to the specified form of this group after transformation.
+
+        Args:
+            tag_attribute (str): The hed_tag property to use to construct the string (usually short_tag or long_tag).
+            tag_transformer (func or None): A function that is applied to each tag string before returning.
+
+        Returns:
+            str: The constructed string.
+
+        Notes:
+            The signature of a tag_transformer is str def(HedTag, str).
 
-        Parameters
-        ----------
-        tag_attribute : str
-            The hed_tag property to use to construct the string.  Most commonly short_tag or long_tag.
-        tag_transformer: func or None
-            A function that is applied to each tag string before returning.
-            signature: str def(HedTag, str)
-        Returns
-        -------
-        group_as_string: str
-            The constructed string
         """
         if tag_transformer:
             result = ",".join([tag_transformer(child, child.__getattribute__(tag_attribute))
                                if isinstance(child, HedTag) else child.get_as_form(tag_attribute, tag_transformer)
-                               for child in self._children])
+                               for child in self.children])
         else:
             result = ",".join([child.__getattribute__(tag_attribute) if isinstance(child, HedTag) else
-                               child.get_as_form(tag_attribute) for child in self._children])
+                               child.get_as_form(tag_attribute) for child in self.children])
         if self.is_group:
             return f"({result})"
         return result
 
     def lower(self):
-        """Convenience function, equivalent to str(self).lower()"""
+        """ Convenience function, equivalent to str(self).lower(). """
+
         return str(self).lower()
 
     def find_placeholder_tag(self):
-        """
-            If a placeholder tag is present, this will return it.
+        """ Return a placeholder tag, if present in this group.
 
+        Returns:
+            HedTag or None: The placeholder tag if found.
+
+        Notes:
             Assumes a valid HedString with no erroneous "#" characters.
 
-        Returns
-        -------
-        found_tag: HedTag or None
-            The placeholder tag if found.
         """
         for tag in self.get_all_tags():
             if "#" in tag.org_tag:
@@ -229,29 +204,27 @@ def find_placeholder_tag(self):
     def __bool__(self):
         return bool(self._children)
 
-    def find_tags(self, anchors, recursive=False, include_groups=2):
-        """
-            Find tags given anchors.
-
-            Note: This can only find identified tags.  By default, definition, def, def-expand, onset, and offset
-                are identified, even without a schema.
+    def find_tags(self, search_tags, recursive=False, include_groups=2):
+        """ Find the search tags and their containing groups that are in this group.
 
         Args:
-            anchors: container
-                A container of short_base_tags to locate
-            recursive: bool
-                If true, also check subgroups.
-            include_groups: 0, 1 or 2
-                If 0: Return only tags
-                If 1: return only groups
-                If 2 or any other value: return both
-                
-        returns:
-        list:
-        tag: HedTag
-            The located tag
-        group: HedGroup
-            The group the located tag is in
+            search_tags (container):    A container of short_base_tags to locate
+            recursive (bool):           If true, also check subgroups.
+            include_groups (0, 1 or 2): Specify return values.
+
+        Returns:
+            list:
+            tag (HedTag) The located tag
+            group (HedGroup) The group the located tag is in.
+
+        Notes:
+            This can only find identified tags.
+            By default, definition, def, def-expand, onset, and offset are identified, even without a schema.
+            The rules for include_groups are:
+                If 0: returns a list of HedTags corresponding to found search tags.
+                If 1: return a list of containing HedGroups for found search tags.
+                If 2 or any other value: returns a list of tuples (tag, containing_group)
+
         """
         found_tags = []
         if recursive:
@@ -261,7 +234,7 @@ def find_tags(self, anchors, recursive=False, include_groups=2):
 
         for sub_group in groups:
             for tag in sub_group.tags():
-                if tag.short_base_tag.lower() in anchors:
+                if tag.short_base_tag.lower() in search_tags:
                     found_tags.append((tag, sub_group))
                     
         if include_groups == 0 or include_groups == 1:
@@ -269,29 +242,28 @@ def find_tags(self, anchors, recursive=False, include_groups=2):
         return found_tags
 
     def find_exact_tags(self, tags_or_groups, recursive=False):
-        """
-            Find the given tags_or_groups.  Note if you pass in groups it will only find EXACT matches.
+        """  Find the given tags or groups.
+
+        Args:
+            tags_or_groups (HedTag, HedGroupBase): A container of tags to locate.
+            recursive (bool): If true, also check subgroups.
+
+        Returns:
+            list: A list of HedGroupBases the given tags/groups were found in.
+
+        Notes:
+            If you pass in groups it will only find EXACT matches.
+            This can only find identified tags.
+            By default, definition, def, def-expand, onset, and offset are identified, even without a schema.
 
             If this is a HedGroup, order matters.  (b, a) != (a, b)
 
             If this is a HedGroupFrozen:
-            if "(a, b)" in tags_or_groups, then it will match 1 and 2, but not 3.
-            1. (a, b)
-            2. (b, a)
-            3. (a, b, c)
-
-            Note: This can only find identified tags.  By default, definition, def, def-expand, onset, and offset
-                are identified, even without a schema.
+                if "(a, b)" in tags_or_groups, then it will match 1 and 2, but not 3.
+                1. (a, b)
+                2. (b, a)
+                3. (a, b, c)
 
-        Args:
-            tags_or_groups: [HedTag or HedGroupBase]
-                A container of tags to locate
-            recursive: bool
-                If true, also check subgroups.
-
-        returns:
-        group_list: [HedGroupBase]
-            A list of all groups the given tags/groups were found in.
         """
         found_tags = []
         if recursive:
@@ -301,33 +273,37 @@ def find_exact_tags(self, tags_or_groups, recursive=False):
 
         for sub_group in groups:
             for search_tag in tags_or_groups:
-                if search_tag in sub_group._children:
+                if search_tag in sub_group.children:
                     found_tags.append(sub_group)
 
         return found_tags
 
     def find_def_tags(self, recursive=False, include_groups=3):
-        """
-            Find any def and def-expand tags in the group.
+        """ Find any def and def-expand tags in the group.
 
         Args:
-            recursive: bool
-                If true, also check subgroups.
-            include_groups: int, 0, 1, 2, 3
-                If 0: Return only def and def expand tags
-                If 1: Return only def tags and def-expand groups
-                If 2: Return only groups containing defs, or def-expand groups
-                If 3 or any other value: Return all 3 as a tuple.
+            recursive (bool): If true, also check subgroups.
+            include_groups (int, 0, 1, 2, 3): options for how to expand or include groups
+
         Returns:
-        list:
+            tuple (HedTag
+            list:
+
         def_tag: HedTag
             The located def tag
         def_expand_group: HedGroup or None
             If this is a def-expand rather than def tag, this will be the entire def-expand group.
         group: HedGroup
             The group the def tag or def expand group is in.
+
+        Notes: The include_groups option controls the tag expansion as follows:
+            If 0: Return only def and def expand tags/
+            If 1: Return only def tags and def-expand groups
+            If 2: Return only groups containing defs, or def-expand groups
+            If 3 or any other value: Return all 3 as a tuple.
+
         """
-        from hed.models.def_dict import DefTagNames
+        from hed.models.definition_dict import DefTagNames
         if recursive:
             groups = self.get_all_groups()
         else:
@@ -335,7 +311,7 @@ def find_def_tags(self, recursive=False, include_groups=3):
 
         def_tags = []
         for group in groups:
-            for child in group.get_direct_children():
+            for child in group.children:
                 if isinstance(child, HedTag):
                     if child.short_base_tag.lower() == DefTagNames.DEF_KEY:
                         def_tags.append((child, child, group))
@@ -347,3 +323,53 @@ def find_def_tags(self, recursive=False, include_groups=3):
         if include_groups == 0 or include_groups == 1 or include_groups == 2:
             return [tag[include_groups] for tag in def_tags]
         return def_tags
+
+    def find_tags_with_term(self, term, recursive=False, include_groups=2):
+        """  Find any tags that contain the given term.
+
+            Note: This can only find identified tags.
+
+        Args:
+            term (str): A single term to search for.
+            recursive (bool): If true, recursively check subgroups.
+            include_groups: 0, 1 or 2
+                If 0: Return only tags
+                If 1: return only groups
+                If 2 or any other value: return both
+
+           recursive (bool): If true, also check subgroups.
+            include_groups (int, 0, 1, 2, 3): options for how to expand or include groups
+
+        Returns:
+            list:
+
+        def_tag: HedTag
+            The located def tag
+        def_expand_group: HedGroup or None
+            If this is a def-expand rather than def tag, this will be the entire def-expand group.
+        group: HedGroup
+            The group the def tag or def expand group is in.
+
+        Notes: The include_groups option controls the tag expansion as follows:
+            If 0: Return only def and def expand tags/
+            If 1: Return only def tags and def-expand groups
+            If 2: Return only groups containing defs, or def-expand groups
+            If 3 or any other value: Return all 3 as a tuple.
+
+        """
+
+        found_tags = []
+        if recursive:
+            groups = self.get_all_groups()
+        else:
+            groups = (self,)
+
+        search_for = term.lower()
+        for sub_group in groups:
+            for tag in sub_group.tags():
+                if search_for in tag.tag_terms:
+                    found_tags.append((tag, sub_group))
+
+        if include_groups == 0 or include_groups == 1:
+            return [tag[include_groups] for tag in found_tags]
+        return found_tags
diff --git a/hed/models/hed_input.py b/hed/models/hed_input.py
index f80c2c1d2..e2914bc91 100644
--- a/hed/models/hed_input.py
+++ b/hed/models/hed_input.py
@@ -1,37 +1,34 @@
 from hed.models.column_mapper import ColumnMapper
 from hed.models.base_input import BaseInput
-from hed.models.def_mapper import DefinitionMapper
+from hed.models.def_mapper import DefMapper
 
 
 class HedInput(BaseInput):
-    """ Represents a spreadsheet of HED tags."""
+    """ Class representing a spreadsheet of HED tags."""
 
     def __init__(self, file=None, file_type=None, worksheet_name=None, tag_columns=None,
                  has_column_names=True, column_prefix_dictionary=None,
                  def_dicts=None, name=None):
         """Constructor for the HedInput class.
 
-        Parameters
-        ----------
-        file: str or file like
-             An xlsx/tsv file to open.
-        file_type: str
-            ".xlsx" for excel, ".tsv" or ".txt" for tsv. data.  Derived from file if file is a str.
-        worksheet_name: str
-            The name of the Excel workbook worksheet that contains the HED tags.  Not applicable to tsv files.
-        tag_columns: []
-            A list of ints containing the columns that contain the HED tags. The default value is the 2nd column.
-        has_column_names: bool
-            True if file has column names. The validation will skip over the first line of the file. False, if
-            otherwise.
-        column_prefix_dictionary: {}
-            A dictionary with keys pertaining to the required HED tag columns that correspond to tags that need to be
-            prefixed with a parent tag path. For example, prefixed_needed_tag_columns = {3: 'Event/Description',
-            4: 'Event/Label/', 5: 'Event/Category/'} The third column contains tags that need Event/Description/
-            prepended to them, the fourth column contains tags that need Event/Label/ prepended to them,
-            and the fifth column contains tags that needs Event/Category/ prepended to them.
-        def_dicts: [DefDict] or DefDict
-            The gathered definitions to use for this file, other than the ones that come from itself.
+        Args:
+            file (str or file like): An xlsx/tsv file to open or a File object.
+            file_type (str or None): ".xlsx" for excel, ".tsv" or ".txt" for tsv. data. If file is a string, the
+            worksheet_name (str or None): The name of the Excel workbook worksheet that contains the HED tags.
+                Not applicable to tsv files. If omitted for Excel, the first worksheet is assumed.
+            tag_columns (list): A list of ints containing the columns that contain the HED tags.
+                The default value is [2] indicating only the second column has tags.
+            has_column_names (bool): True if file has column names. Validation will skip over the
+                first line of the file if the spreadsheet as column names.
+            column_prefix_dictionary (dict): A dictionary with column number keys and prefix values.
+            def_dicts (DefinitionDict or list):  A DefinitionDict or list of DefDicts containing definitions for this
+                object other than the ones extracted from the HedInput object itself.
+
+        Examples:
+            A prefix dictionary {3: 'Label/', 5: 'Description/'} indicates that column 3 and 5 have HED tags
+            that need to be prefixed by Label/ and Description/ respectively.
+            Column numbers 3 and 5 should also be included in the tag_columns list.
+
         """
         if tag_columns is None:
             tag_columns = [2]
@@ -40,7 +37,7 @@ def __init__(self, file=None, file_type=None, worksheet_name=None, tag_columns=N
 
         new_mapper = ColumnMapper(tag_columns=tag_columns, column_prefix_dictionary=column_prefix_dictionary)
 
-        def_mapper = DefinitionMapper(def_dicts)
+        def_mapper = DefMapper(def_dicts)
 
         super().__init__(file, file_type, worksheet_name, has_column_names, new_mapper, def_mapper=def_mapper,
                          name=name)
diff --git a/hed/models/hed_ops.py b/hed/models/hed_ops.py
index 7f05ba32c..682ba66fd 100644
--- a/hed/models/hed_ops.py
+++ b/hed/models/hed_ops.py
@@ -16,8 +16,7 @@
 
 
 def translate_ops(hed_ops, split_tag_and_string_ops=False, **kwargs):
-    """
-        Takes a list of hed_ops and/or functions and returns a list of functions to apply to a hed string object
+    """ Given a list of hed_ops and/or functions, returns a list of functions to apply to a hed string object.
 
     Parameters
     ----------
@@ -39,6 +38,7 @@ def translate_ops(hed_ops, split_tag_and_string_ops=False, **kwargs):
     -------
     tag_funcs, string_funcs: ([func], [func]) or [func]
         A list of functions to apply.  Tag vs string primarily applies to spreadsheets.
+
     """
     if not isinstance(hed_ops, list):
         hed_ops = [hed_ops]
diff --git a/hed/models/hed_string.py b/hed/models/hed_string.py
index f4f45a566..1f1a051d1 100644
--- a/hed/models/hed_string.py
+++ b/hed/models/hed_string.py
@@ -12,26 +12,24 @@
 
 
 class HedString(HedGroup):
-    """Represents a hed string."""
+    """Class representing a HED string."""
 
     OPENING_GROUP_CHARACTER = '('
     CLOSING_GROUP_CHARACTER = ')'
 
     def __init__(self, hed_string, hed_schema=None):
-        """Constructor for the HedString class.
-
-        Parameters
-        ----------
-        hed_string: str
-            A HED string consisting of tags and tag groups.
-        Returns
-        -------
+        """ Constructor for the HedString class.
+
+        Args:
+            hed_string (str): A HED string consisting of tags and tag groups.
+
+        Notes:
+            The HedString object parses its component tags and groups into a tree-like structure.
+
         """
-        # The sub HedStrings that make up this object.  Empty if this is the lowest level one.
-        self._component_strings = []
-        # This is a tree like structure containing the entire hed string
+
         try:
-            contents = self.split_hed_string_into_groups(hed_string, hed_schema)
+            contents = self.split_into_groups(hed_string, hed_schema)
         except ValueError:
             contents = []
 
@@ -39,56 +37,21 @@ def __init__(self, hed_string, hed_schema=None):
 
     @property
     def is_group(self):
-        """
-            Returns True if this is a group with parenthesis.
-        """
+        """ Returns False since the underlying string is not a group with parentheses. """
         return False
 
-    @staticmethod
-    def create_from_other(hed_string_obj_list):
-        """
-            Creates a hed string from a list of hed strings.
-
-            Note: This does not allocate new tags for the new strings, they point to the same internal tags.
-
-        Parameters
-        ----------
-        hed_string_obj_list : [HedString]
-            The list of strings to combine
-
-        Returns
-        -------
-        combined_hed_string_obj: HedString
-            The combined hed string, containing all tags and delimiters from the list
-        """
-        hed_string_obj_list = [hed_string_obj for hed_string_obj in hed_string_obj_list if hed_string_obj is not None]
-        new_hed_string_obj = HedString("")
-        for hed_string_obj in hed_string_obj_list:
-            new_hed_string_obj._children += hed_string_obj._children
-        new_hed_string_obj._component_strings = list(hed_string_obj_list)
-
-        hed_string = ",".join([hed_string_obj._hed_string for hed_string_obj in hed_string_obj_list])
-        new_hed_string_obj._hed_string = hed_string
-        new_hed_string_obj._startpos = 0
-        new_hed_string_obj._endpos = len(hed_string)
-
-        return new_hed_string_obj
-
     def convert_to_canonical_forms(self, hed_schema):
-        """
-            Identify all tags using the given schema.  If no schema, still identify "key" tags such as definitions.
+        """ Identify all tags using the given schema. If no schema, still identify "key" tags such as definitions.
 
-            Also sets the "isDefinition" property on tags and groups.
+        Args:
+            hed_schema (HedSchema or None): The schema to use to validate/convert tags.
+
+        Returns:
+            list: A list of issues found while converting the string. Each issue is a dictionary.
 
-        Parameters
-        ----------
-        hed_schema : HedSchema or None
-            The schema to use to validate/convert tags.
+        Notes:
+            Sets the "isDefinition" property on tags and groups.
 
-        Returns
-        -------
-        conversion_issues: [{}]
-            A list of issues found while converting the string.
         """
         validation_issues = []
         for tag in self.get_all_tags():
@@ -97,88 +60,79 @@ def convert_to_canonical_forms(self, hed_schema):
         return validation_issues
 
     def remove_definitions(self):
-        """
-            Removes any definition tags and groups from this string.
+        """ Remove definition tags and groups from this string.
+
+        Returns:
+            list: An empty list as there are no possible issues, this list is always blank.
 
-        Returns
-        -------
-        issues: [{}]
-            There are no possible issues, this list is always blank.
         """
         definition_groups = self.find_top_level_tags({DefTagNames.DEFINITION_KEY}, include_groups=1)
         if definition_groups:
-            self.remove_groups(definition_groups)
+            self.remove(definition_groups)
 
         return []
 
     def convert_to_short(self, hed_schema):
-        """
-            Compute string canonical forms and return the long form and issues.
-
-        Parameters
-        ----------
-        hed_schema : HedSchema or None
-            The schema to use to calculate forms.
-        Returns
-        -------
-        short_form: str
-            The string with all tags converted to short form
-        conversion_issues: [{}]
-            Issues found during conversion.  No issues will be found if no schema is passed.
+        """ Compute string canonical forms and return the short form and issues.
+
+        Args:
+            hed_schema (HedSchema or None): The schema to use to calculate forms.
+
+        Returns:
+            str:   The string with all tags converted to short form.
+            list:  A list of issues found during conversion. Each issue is a dictionary.
+
+        Notes:
+            No issues will be found if no schema is passed.
+
         """
         conversion_issues = self.convert_to_canonical_forms(hed_schema)
         short_string = self.get_as_short()
         return short_string, conversion_issues
 
     def convert_to_long(self, hed_schema):
-        """
-            Compute string canonical forms and return the long form and issues.
-
-        Parameters
-        ----------
-        hed_schema : HedSchema or None
-            The schema to use to calculate forms.
-        Returns
-        -------
-        long_form: str
-            The string with all tags converted to long form
-        conversion_issues: [{}]
-            Issues found during conversion.  No issues will be found if no schema is passed.
+        """ Compute string canonical forms and return the long form and issues.
+
+        Args:
+            hed_schema (HedSchema or None): The schema to use to calculate forms.
+
+        Returns:
+            str:   The string with all tags converted to long form.
+            list:  A list of issues found during conversion.  Each issue is a dictionary.
+
+        Notes:
+            No issues will be found if no schema is passed.
+
         """
         conversion_issues = self.convert_to_canonical_forms(hed_schema)
         short_string = self.get_as_long()
         return short_string, conversion_issues
 
     def convert_to_original(self):
-        """
-            Returns the original form of this string, though potentially with some extraneous spaces removed.
+        """ Return the original form of this string, though potentially with some extraneous spaces removed.
+
+        Returns:
+            str: The string with all the tags in their original form.
 
-        Returns
-        -------
-        org_string: str
-            The string with all the tags in their original form.
         """
         return self.get_as_form("org_tag")
 
     @staticmethod
-    def split_hed_string_into_groups(hed_string, hed_schema=None):
-        """Splits the hed_string into a tree of tag groups, tags, and delimiters.
-
-        Parameters
-        ----------
-        hed_string
-            A hed string consisting of tags and tag groups.
-        hed_schema
-            Schema to use to identify tags.
-        Returns
-        -------
-        group: HedGroup
-            The group containing this hed_string.
-
-        raises: ValueError
-            Raises ValueError if the string is significantly malformed, such as mis-matched parenthesis.
+    def split_into_groups(hed_string, hed_schema=None):
+        """ Split the HED string into a tree of tag groups, tags, and delimiters.
+
+        Args:
+            hed_string (str): A hed string consisting of tags and tag groups to be processed.
+            hed_schema (HedSchema or None): Hed schema to use to identify tags.
+
+        Returns:
+            list:  A list of HedTag and/or HedGroupBase.
+
+        Raises:
+            ValueError: If the string is significantly malformed, such as mismatched parentheses.
+
         """
-        # Stack variable while processing
+
         current_tag_group = [[]]
 
         input_tags = HedString.split_hed_string(hed_string)
@@ -218,62 +172,42 @@ def split_hed_string_into_groups(hed_string, hed_schema=None):
         return current_tag_group[0]
 
     def _get_org_span(self, tag_or_group):
-        """
-            If this tag or group was in the original hed string, find it's original span.
+        """ If this tag or group was in the original hed string, find its original span.
 
-            This handles all cases of strings, including hed strings made of other hed strings.
+        Args:
+            tag_or_group (HedTag or HedGroup): The hed tag to locate in this string.
 
-            If the hed tag or group was not in the original string, returns (None, None)
+        Returns:
+            int or None:   Starting position of the given item in the original string.
+            int or None:   Ending position of the given item in the original string.
 
-        Parameters
-        ----------
-        tag_or_group : HedTag or HedGroup
-            The hed tag to locate in this string.
+        Notes:
+            If the hed tag or group was not in the original string, returns (None, None).
 
-        Returns
-        -------
-        tag_span: (int or None, int or None)
-            The starting and ending index of the given tag in the original string
         """
-        strings_list = self._component_strings
-        found_string = None
-        string_start_index = 0
-        if not self._component_strings:
-            if self.check_if_in_original_tags_and_groups(tag_or_group):
-                return tag_or_group.span
-        for string in strings_list:
-            if string.check_if_in_original_tags_and_groups(tag_or_group):
-                found_string = string
-                break
-            # Add 1 for comma
-            string_start_index += string.span[1] + 1
-
-        if not found_string:
-            return None, None
-
-        return tag_or_group.span[0] + string_start_index, tag_or_group.span[1] + string_start_index
+        if self.check_if_in_original(tag_or_group):
+            return tag_or_group.span
+
+        return None, None
 
     @staticmethod
     def split_hed_string(hed_string):
-        """
-        Takes a hed string and splits it into delimiters and tags
-
-        Note: This does not validate tags or delimiters in any form.
-
-        Parameters
-        ----------
-            hed_string: string
-                the hed string to split
-        Returns
-        -------
-        [tuple]
-            each tuple: (is_hed_tag, (start_pos, end_pos))
-            is_hed_tag: bool
-                This is a (possible) hed tag if true, delimiter if not
-            start_pos: int
-                index of start of string in hed_string
-            end_pos: int
-                index of end of string in hed_string
+        """ Split a HED string into delimiters and tags.
+
+        Args:
+            hed_string (str): The HED string to split.
+
+        Returns:
+            list:  A list of tuples where each tuple is (is_hed_tag, (start_pos, end_pos)).
+
+        Notes:
+            The tuple format is as follows
+                is_hed_tag (bool): A (possible) hed tag if true, delimiter if not.
+                start_pos (int):   Index of start of string in hed_string.
+                end_pos (int):     Index of end of string in hed_string
+
+            This function does not validate tags or delimiters in any form.
+
         """
         tag_delimiters = ",()"
         current_spacing = 0
@@ -325,20 +259,17 @@ def split_hed_string(hed_string):
         return result_positions
 
     def apply_funcs(self, string_funcs):
-        """
-            Run the list of functions on this string and gather issues found.
+        """ Run the list of functions on this string and gather issues found.
 
-            This potentially modifies the hed string object.
+        Args:
+            string_funcs (list): A list of functions that take a hed string object and return a list of issues.
+
+        Returns:
+            list: A list of issues found by these operations. Each issue is a dictionary.
 
-        Parameters
-        ----------
-        string_funcs : [func]
-            A list of functions that take a hed string object and return a list of issues.
+        Notes:
+            This method potentially modifies the hed string object.
 
-        Returns
-        -------
-        issues_list: [{}]
-            A list of issues found by these operations
         """
         string_issues = []
         for string_func in string_funcs:
@@ -350,20 +281,19 @@ def apply_funcs(self, string_funcs):
         return string_issues
 
     def validate(self, hed_ops=None, error_handler=None, **kwargs):
-        """
-            Run the given hed_ops on this string.
+        """ Run the given hed_ops on this string.
+
+        Args:
+            hed_ops: (func, HedOps, or list): Operations to apply to this object.
+            error_handler (ErrorHandler or None): Used to report errors in context.  Uses a default if None.
+            kwargs:
+                See models.hed_ops.translate_ops or the specific hed_ops for additional options
 
-        Parameters
-        ----------
-        hed_ops : [func or HedOps] or func or HedOps
-            A list of HedOps of funcs to apply to the hed strings in this sidecar.
-        error_handler : ErrorHandler or None
-            Used to report errors.  Uses a default one if none passed in.
-        kwargs:
-            See models.hed_ops.translate_ops or the specific hed_ops for additional options
+        Returns:
+            list:  A list of issues encountered in applying these operations. Each issue is a dictionary.
 
-        Returns
-        -------
+        Notes:
+            Although this function is called validation, the HedOps can represent other transformations.
 
         """
         if error_handler is None:
@@ -378,38 +308,42 @@ def validate(self, hed_ops=None, error_handler=None, **kwargs):
         return issues
 
     def get_frozen(self):
-        """
-            Returns a frozen copy of this HedString.
+        """ Return a frozen copy of this HedString.
+
+        Returns:
+            HedStringFrozen: A frozen copy of this HedString.
 
-        Returns: HedStringFrozen
-            A frozen copy of this HedString.  Note: tags still point to the same place.  Do not alter them.
+        Notes:
+            Tags in frozen hed strings still point to the same place.  Do not alter them.
 
         """
         return HedStringFrozen(self)
 
-    def find_top_level_tags(self, anchors, include_groups=2):
-        """
-            Find top level groups containing the given anchor tags.
+    def find_top_level_tags(self, anchor_tags, include_groups=2):
+        """ Find top level groups containing the given anchor tags.
 
-            Max of 1 tag located her top level group.
         Args:
-            anchors: container
-                A list/set/etc of short_base_tags to find groups by.
-            include_groups: 0, 1 or 2
-                If 0: Return only tags
-                If 1: return only groups
-                If 2 or any other value: return both
+            anchor_tags (container):     A list/set/etc of short_base_tags to find groups by.
+            include_groups (0, 1 or 2):  Parameter indicating what return values to include.
+
         Returns:
-        list:
-        tag: HedTag
-            The located tag
-        group: HedGroup
-            The group the located tag is in
+            list:
+            tag (HedTag): The located tag
+            group (HedGroup): The group the located tag is in.
+
+        Notes:
+            The include_groups parameter meanings are:
+                If 0: return only tags.
+                If 1: return only groups.
+                If 2 or any other value: return both.
+
+            A max of 1 tag located her top level group.
+
         """
         top_level_tags = []
         for group in self.groups():
             for tag in group.tags():
-                if tag.short_base_tag.lower() in anchors:
+                if tag.short_base_tag.lower() in anchor_tags:
                     top_level_tags.append((tag, group))
                     # Only capture a max of 1 per group.  These are implicitly unique.
                     break
@@ -420,24 +354,21 @@ def find_top_level_tags(self, anchors, include_groups=2):
 
 
 class HedStringFrozen(HedGroupFrozen):
-    """
-        Represents an immutable hed string.
-    """
+    """ Class representing an immutable hed string. """
+
     def __init__(self, hed_string, hed_schema=None):
-        """
+        """ Initialize a frozen hed string object.
 
         Args:
-            hed_string: HedGroupBase or str
-                Source string
-            hed_schema: HedSchema or HedSchemaGroup or None
-                HedSchema to use to identify tags if hed_string is a str.
+            hed_string (HedGroupBase or str): The source HED string.
+            hed_schema (HedSchema, HedSchemaGroup or None): HedSchema to use to identify tags if hed_string is a str.
         """
         if isinstance(hed_string, HedGroupBase):
             contents = hed_string
             hed_string = hed_string._hed_string
         else:
             try:
-                contents = HedString.split_hed_string_into_groups(hed_string, hed_schema)
+                contents = HedString.split_into_groups(hed_string, hed_schema)
             except ValueError:
                 contents = []
 
@@ -445,9 +376,7 @@ def __init__(self, hed_string, hed_schema=None):
 
     @property
     def is_group(self):
-        """
-            Returns True if this is a group with parenthesis.
-        """
+        """ Return False since this does not have parentheses. """
         return False
 
     # Other functions from HedString we want.
diff --git a/hed/models/hed_string_comb.py b/hed/models/hed_string_comb.py
new file mode 100644
index 000000000..1c0998716
--- /dev/null
+++ b/hed/models/hed_string_comb.py
@@ -0,0 +1,136 @@
+"""
+    This module is used to easily concatenate multiple hed strings in place
+"""
+from hed.models.hed_string import HedString
+
+
+class HedStringComb(HedString):
+    """Represents a hed string made from other hed strings(generally multiple columns)."""
+
+    def __init__(self, hed_string_obj_list):
+        """Constructor for the HedStringComb class.
+
+        Parameters
+        ----------
+        hed_string_obj_list: [HedString]
+            A list of component HedStrings for this combined
+
+        Returns
+        -------
+        """
+        super().__init__("")
+        self._children = list(hed_string for hed_string in hed_string_obj_list if hed_string is not None)
+        self._original_children = self._children
+
+    def get_original_hed_string(self):
+        return "".join([group._hed_string for group in self._children])
+
+    @property
+    def span(self):
+        """ Return the source span of this group from the source hed string.
+
+        Return:
+            int: start index of the group (including parentheses) from the source string.
+            int: end index of the group (including parentheses) from the source string.
+        """
+        return 0, len(self.get_original_hed_string())
+
+    @property
+    def children(self):
+        """ Returns the direct children of this string.
+
+        Returns:
+            The list of direct children of this group
+        """
+        return [child for sub_string in self._children for child in sub_string._children]
+
+    def remove(self, items_to_remove):
+        """
+            Remove any tags/groups in remove found in this hed string.
+
+            Any groups that become empty will also be pruned.
+
+            Note: this goes by identity, not equivalence.
+
+        Parameters
+        ----------
+        items_to_remove : [HedGroup or HedTag]
+            A list of groups or tags to remove.
+        """
+        all_groups = [group for sub_group in self._children for group in sub_group.get_all_groups()]
+        self._remove(items_to_remove, all_groups)
+        # Remove any lingering empty HedStrings
+        if any(not hed_string for hed_string in self._children):
+            if self._original_children is self._children:
+                self._original_children = self._children.copy()
+            self._children = [child for child in self._children if child]
+
+    def replace(self, item_to_replace, new_contents):
+        """ Replaces an existing tag in the group with a new tag, list, or group
+
+        Parameters
+        ----------
+        item_to_replace : HedTag or HedGroup
+            The tag to replace.  It must exist or this will raise an error.
+        new_contents : HedTag or HedGroup or [HedTag or HedGroup]
+            What to replace the tag with.
+        """
+        if not self.mutable:
+            raise ValueError("Trying to alter immutable group")
+
+        # this needs to pass the tag off to the appropriate group
+        replace_sub_string = None
+        for sub_string in self._children:
+            for i, child in enumerate(sub_string.children):
+                if item_to_replace is child:
+                    replace_sub_string = sub_string
+                    break
+
+        replace_sub_string.replace(item_to_replace, new_contents)
+
+    def __copy__(self):
+        """
+            Returns a copy of this combined string converted to HedString
+
+        Returns
+        -------
+
+        """
+        # When we copy a combined string, just turn it into a normal string.
+        result = HedString.__new__(HedString)
+        result.__dict__.update(self.__dict__)
+        result._startpos, result._endpos = self.span
+        result._hed_string = self.get_original_hed_string()
+        result._children = self.children.copy()
+        result.mutable = True
+        return result
+
+    def _get_org_span(self, tag_or_group):
+        """
+            If this tag or group was in the original hed string, find it's original span.
+
+            If the hed tag or group was not in the original string, returns (None, None)
+
+        Parameters
+        ----------
+        tag_or_group : HedTag or HedGroup
+            The hed tag to locate in this string.
+
+        Returns
+        -------
+        tag_span: (int or None, int or None)
+            The starting and ending index of the given tag in the original string
+        """
+        found_string = None
+        string_start_index = 0
+        for string in self._children:
+            if string.check_if_in_original(tag_or_group):
+                found_string = string
+                break
+            # Add 1 for comma
+            string_start_index += string.span[1] + 1
+
+        if not found_string:
+            return None, None
+
+        return tag_or_group.span[0] + string_start_index, tag_or_group.span[1] + string_start_index
diff --git a/hed/models/hed_tag.py b/hed/models/hed_tag.py
index 168ee166f..1a6460853 100644
--- a/hed/models/hed_tag.py
+++ b/hed/models/hed_tag.py
@@ -40,8 +40,9 @@ def __init__(self, hed_string, span=None, hed_schema=None):
 
         self._extension_value = ""
 
-        self.mutable = True  # Note this only keeps track of tags also used in definitions.  You should also not
-                             # alter any tags used in a HedGroupFrozen
+        # Note this only keeps track of tags also used in definitions.  You should also not
+        # alter any tags used in a HedGroupFrozen
+        self.mutable = True
 
         if hed_schema:
             self.convert_to_canonical_forms(hed_schema)
@@ -76,14 +77,14 @@ def short_tag(self):
 
     @property
     def base_tag(self):
-        """
-            Returns the long version of the tag, without value or extension
+        """ Returns the long version of the tag, without value or extension.
+
+        Returns:
+            base_tag (str): The long version of the tag, without value or extension.
+
+        Notes:
+            Only valid after calling convert_to_canonical_forms.
 
-            Note: only valid after calling convert_to_canonical_forms
-        Returns
-        -------
-        base_tag: str
-            The long version of the tag, without value or extension
         """
         if self._schema_entry:
             return self._schema_entry.long_tag_name
@@ -91,15 +92,14 @@ def base_tag(self):
 
     @property
     def short_base_tag(self):
-        """
-            Returns just the short non-extension portion of a tag.
+        """ Returns just the short non-extension portion of a tag.
 
-            e.g. ParentNodes/Def/DefName would return just "Def"
+        Returns:
+            base_tag (str): The short non-extension port of a tag.
+
+        Notes:
+            ParentNodes/Def/DefName would return just "Def".
 
-        Returns
-        -------
-        base_tag: str
-            The short non-extension port of a tag.
         """
         if self._schema_entry:
             return self._schema_entry.short_tag_name
@@ -134,17 +134,17 @@ def short_base_tag(self, new_tag_val):
 
     @property
     def org_base_tag(self):
-        """
-            Returns the original version of the tag, without value or extension
+        """ Return the original version of the tag, without value or extension.
+
+        Returns:
+            base_tag (str): The original version of the tag, without value or extension.
 
+        Notes:
             Warning: This could be empty if the original tag had a name_prefix prepended.
                 eg a column where "Label/" is prepended, thus the column value has zero base portion.
 
-            Note: only valid after calling convert_to_canonical_forms
-        Returns
-        -------
-        base_tag: str
-            The original version of the tag, without value or extension
+            Only valid after calling convert_to_canonical_forms.
+
         """
         if self._schema_entry:
             extension_len = len(self._extension_value)
@@ -251,6 +251,24 @@ def org_tag(self):
         """
         return self._hed_string[self.span[0]:self.span[1]]
 
+    @property
+    def tag_terms(self):
+        """
+            Returns a tuple of all the terms in this tag, not counting any extension.  Lowercase.
+
+            Returns empty tuple for unidentified tag.
+
+        Returns
+        -------
+        tag_terms: (str)
+            Tuple of terms
+        """
+        if self._schema_entry:
+            return self._schema_entry.tag_terms
+
+        ## todo: Potentially remove this.  It's just a quick hack for testing and should not be on the main branch.
+        return tuple(str(self).lower())
+
     def __str__(self):
         """
         Convert this HedTag to a string
@@ -261,7 +279,7 @@ def __str__(self):
             Return the original tag if we haven't set a new tag.(e.g. short to long)
         """
         if self._schema_entry:
-            return self.long_tag
+            return self.short_tag
 
         if self._tag:
             return self._tag
@@ -275,24 +293,25 @@ def _str_no_long_tag(self):
         return self._hed_string[self.span[0]:self.span[1]]
 
     def add_prefix_if_not_present(self, required_prefix):
-        """Add a name_prefix to this tag *unless* the tag is already formatted.
+        """ Add a name_prefix to this tag *unless* the tag is already formatted.
 
-        This means we verify the tag does not have the required name_prefix, or any partial name_prefix
+        Args:
+            required_prefix (str): The full name_prefix to add if not present.
 
-        Ex:
-        Required: KnownTag1/KnownTag2
-        Case 1: KnownTag1/KnownTag2/ColumnValue
-            Will not be changed, has name_prefix already
-        Case 2: KnownTag2/ColumnValue
-            Will not be changed, has partial name_prefix already
-        Case 3: ColumnValue
-            Prefix will be added.
+        Notes:
+            This means we verify the tag does not have the required name_prefix, or any partial name_prefix.
+
+            Ex:
+            Required: KnownTag1/KnownTag2
+            Case 1: KnownTag1/KnownTag2/ColumnValue
+                Will not be changed, has name_prefix already
+            Case 2: KnownTag2/ColumnValue
+                Will not be changed, has partial name_prefix already
+            Case 3: ColumnValue
+                Prefix will be added.
 
-        Parameters
-        ----------
-        required_prefix : str
-            The full name_prefix to add if not present
         """
+
         # if not self.mutable:
         #     raise ValueError("Trying to alter immutable tag")
 
@@ -305,25 +324,20 @@ def add_prefix_if_not_present(self, required_prefix):
                 break
             checking_prefix = checking_prefix[slash_index:]
         self.tag = required_prefix + self.org_tag
-        # self._tag = required_prefix + self.org_tag
 
     def lower(self):
         """Convenience function, equivalent to str(self).lower()"""
         return str(self).lower()
 
     def convert_to_canonical_forms(self, hed_schema):
-        """
-            This updates the internal tag states from the schema, and sets the schema entry.
+        """ Update the internal tag states from the schema and set the schema entry.
 
-        Parameters
-        ----------
-        hed_schema : HedSchema
-            The schema to use to validate this tag
+        Args:
+            hed_schema (HedSchema): The schema to use to validate this tag
+
+        Returns:
+            list:  A list of issues found during conversion. Each element is a dictionary.
 
-        Returns
-        -------
-        conversion_issues: [{}]
-            A list of issues found during conversion
         """
         if not hed_schema:
             return self._convert_key_tags_to_canonical_form()
@@ -415,58 +429,55 @@ def attributes(self):
         return {}
 
     def tag_exists_in_schema(self):
-        """
-            Gets the schema entry for this tag.
+        """ Get the schema entry for this tag.
 
-            Note: This does NOT assure this is a valid tag.
-        Returns
-        -------
-        tag_exists: bool
-            Returns true if this tag exists
+        Returns:
+            (bool): True if this tag exists.
+
+        Notes:
+            This does NOT assure this is a valid tag.
         """
         return bool(self._schema_entry)
 
     def is_takes_value_tag(self):
-        """
-            Returns true if this is a takes value tag
-        Returns
-        -------
-        is_value_tag: bool
+        """ Returns true if this is a takes value tag.
+
+        Returns:
+            bool: True if this is a takes value tag.
+
         """
         if self._schema_entry:
             return self._schema_entry.has_attribute(HedKey.TakesValue)
         return False
 
     def is_unit_class_tag(self):
-        """
-            Returns true if this is a unit class tag
-        Returns
-        -------
-        is_unit_class_tag: bool
+        """ Returns true if this is a unit class tag.
+
+        Returns:
+            bool: True if this is a unit class tag.
+
         """
         if self._schema_entry:
             return bool(self._schema_entry.unit_classes)
         return False
 
     def is_value_class_tag(self):
-        """
-            Returns true if this is a value class tag
+        """ Returns true if this is a value class tag.
+
+        Returns:
+            bool:  True if this is a a tag with a value class.
 
-        Returns
-        -------
-        is_value_class_tag: bool
         """
         if self._schema_entry:
             return bool(self._schema_entry.value_classes)
         return False
 
     def is_basic_tag(self):
-        """
-            Returns true if this is a known tag with no extension or value.
+        """  Returns true if this is a known tag with no extension or value.
+
+        Returns:
+            bool:  True if this is a known tag without extension or value.
 
-        Returns
-        -------
-        is_basic_tag: bool
         """
         return bool(self._schema_entry and not self.extension_or_value_portion)
 
@@ -568,6 +579,7 @@ def any_parent_has_attribute(self, attribute):
         -------
         tag_has_attribute: bool
             True if the tag has the given attribute. False, if otherwise.
+
         """
         if self._schema_entry:
             return self._schema_entry.any_parent_has_attribute(attribute=attribute)
diff --git a/hed/models/onset_mapper.py b/hed/models/onset_mapper.py
index 8363062f1..725101cd2 100644
--- a/hed/models/onset_mapper.py
+++ b/hed/models/onset_mapper.py
@@ -1,5 +1,5 @@
 from hed.models.model_constants import DefTagNames
-from hed.models.hed_group import HedTag, HedGroup
+from hed.models.hed_group import HedGroup
 from hed.errors.error_reporter import ErrorHandler
 from hed.errors.error_types import OnsetErrors
 from hed.models.hed_ops import HedOps
@@ -14,17 +14,17 @@ def __init__(self, def_mapper):
         self._onsets = {}
 
     def check_for_onset_offset(self, hed_string_obj):
-        """
-            Checks for an onset or offset tag in the given string and adds it to the current context if found.
-        Parameters
-        ----------
-        hed_string_obj : HedString
-            The hed string to check.  Finds a maximum of one onset tag.
-
-        Returns
-        -------
-        onset_issues: [{}]
-            Issues found validating onsets.  Out of order onsets, unknown def names, etc.
+        """ Check for an onset or offset tag in the given string and add it to the current context if found.
+
+        Args:
+            hed_string_obj (HedString): The hed string to check.  Finds a maximum of one onset tag.
+
+        Returns:
+            list: A list of issues found in validating onsets (i.e., out of order onsets, unknown def names).
+
+        Notes:
+            Each issue in the return list is a dictionary.
+
         """
         onset_issues = []
         for found_onset, found_group in self._find_onset_tags(hed_string_obj):
@@ -46,7 +46,7 @@ def check_for_onset_offset(self, hed_string_obj):
             def_tag, def_group, _ = def_tags[0]
             if def_group is None:
                 def_group = def_tag
-            children = [child for child in found_group.get_direct_children() if
+            children = [child for child in found_group.children if
                         def_group is not child and found_onset is not child]
             max_children = 1
             if found_onset.short_base_tag.lower() == DefTagNames.OFFSET_KEY:
@@ -54,7 +54,7 @@ def check_for_onset_offset(self, hed_string_obj):
             if len(children) > max_children:
                 onset_issues += ErrorHandler.format_error(OnsetErrors.ONSET_WRONG_NUMBER_GROUPS,
                                                           def_tag,
-                                                          found_group.get_direct_children())
+                                                          found_group.children)
                 continue
 
             if children:
@@ -71,7 +71,7 @@ def check_for_onset_offset(self, hed_string_obj):
         return onset_issues
 
     def _find_onset_tags(self, hed_string_obj):
-        return hed_string_obj.find_top_level_tags(anchors={DefTagNames.ONSET_KEY, DefTagNames.OFFSET_KEY})
+        return hed_string_obj.find_top_level_tags(anchor_tags={DefTagNames.ONSET_KEY, DefTagNames.OFFSET_KEY})
 
     def _handle_onset_or_offset(self, def_tag, onset_offset_tag):
         is_onset = onset_offset_tag.short_base_tag.lower() == DefTagNames.ONSET_KEY
diff --git a/hed/models/sidecar.py b/hed/models/sidecar.py
index 166c9b8bc..2d9ac2e1a 100644
--- a/hed/models/sidecar.py
+++ b/hed/models/sidecar.py
@@ -5,11 +5,11 @@
 from hed.errors import ErrorHandler
 from hed.errors.exceptions import HedFileError, HedExceptions
 from hed.models.hed_string import HedString
-from hed.models.def_mapper import DefinitionMapper
+from hed.models.def_mapper import DefMapper
 
 
 class Sidecar:
-    """Contents of a single JSON file with definition dictionaries."""
+    """ Contents of a single JSON file with definition dictionaries."""
 
     def __init__(self, file, name=None):
         """
@@ -51,12 +51,11 @@ def save_as_json(self, save_filename):
             json.dump(output_dict, fp, indent=4)
 
     def get_as_json_string(self):
-        """
-        Returns this entire column definition group as a json string.
-        Returns
-        -------
-        json_string: str
-            The json string representing this column definition group.
+        """ Return this entire column definition group as a json string.
+
+        Returns:
+            str: The json string representing this column definition group.
+
         """
         output_dict = {}
         for entry in self._column_data.values():
@@ -154,7 +153,7 @@ def hed_string_iter(self, hed_ops=None, error_handler=None, expand_defs=False, r
             If True, remove all definitions found in the string.
         allow_placeholders: bool
             If False, placeholders will be marked as validation warnings.
-        extra_def_dicts: [DefDict] or DefDict or None
+        extra_def_dicts: [DefinitionDict] or DefinitionDict or None
             Extra dicts to add to the list
         kwargs:
             See models.hed_ops.translate_ops or the specific hed_ops for additional options
@@ -206,9 +205,9 @@ def _add_definition_mapper(self, hed_ops, extra_def_dicts=None):
         if not isinstance(hed_ops, list):
             hed_ops = [hed_ops]
         hed_ops = hed_ops.copy()
-        if not any(isinstance(hed_op, DefinitionMapper) for hed_op in hed_ops):
+        if not any(isinstance(hed_op, DefMapper) for hed_op in hed_ops):
             def_dicts = self.get_def_dicts(extra_def_dicts)
-            def_mapper = DefinitionMapper(def_dicts)
+            def_mapper = DefMapper(def_dicts)
             hed_ops.append(def_mapper)
         return hed_ops
 
@@ -234,11 +233,11 @@ def get_def_dicts(self, extra_def_dicts=None):
 
         Parameters
         ----------
-        extra_def_dicts: [DefDict] or DefDict or None
+        extra_def_dicts: [DefinitionDict] or DefinitionDict or None
             Extra dicts to add to the list
         Returns
         -------
-        def_dicts: [DefDict]
+        def_dicts: [DefinitionDict]
             A list of def dicts for each column plus any found in extra_def_dicts.
         """
         def_dicts = [column_entry.def_dict for column_entry in self]
@@ -260,7 +259,7 @@ def validate_entries(self, hed_ops=None, name=None, extra_def_dicts=None,
         name: str
             If present, will use this as the filename for context, rather than using the actual filename
             Useful for temp filenames.
-        extra_def_dicts: [DefDict] or DefDict or None
+        extra_def_dicts: [DefinitionDict] or DefinitionDict or None
             If present, also use these in addition to the sidecars def dicts.
         error_handler : ErrorHandler or None
             Used to report errors.  Uses a default one if none passed in.
diff --git a/hed/schema/hed_schema.py b/hed/schema/hed_schema.py
index 7c68ca3de..d2db19c10 100644
--- a/hed/schema/hed_schema.py
+++ b/hed/schema/hed_schema.py
@@ -338,6 +338,7 @@ def __eq__(self, other):
         if self.epilogue != other.epilogue:
             return False
         if self._sections != other._sections:
+            # This block is useful for debugging when modifying the schema class itself.
             # for section1, section2 in zip(self._sections.values(), other._sections.values()):
             #     if section1 != section2:
             #         dict1 = section1.all_names
@@ -379,9 +380,9 @@ def get_unit_class_units(self, unit_class_type):
             return unit_class_entry.unit_class_units
         return []
 
-    def get_all_tags_with_attribute(self, key, section_key=HedSectionKey.AllTags):
+    def get_tags_with_attribute(self, key, section_key=HedSectionKey.AllTags):
         """
-            Returns a list of all tags with the given attribute.
+            Returns a list of all tag entries with the given attribute.
 
             Note: Result is cached so will be fast after first call.
 
@@ -397,7 +398,8 @@ def get_all_tags_with_attribute(self, key, section_key=HedSectionKey.AllTags):
         tag_list: [str]
             A list of all tags with this attribute
         """
-        return self._sections[section_key].get_entries_with_attribute(key, return_name_only=True)
+        return self._sections[section_key].get_entries_with_attribute(key, return_name_only=True,
+                                                                      library_prefix=self._library_prefix)
 
     def get_tag_entry(self, name, key_class=HedSectionKey.AllTags, library_prefix=""):
         """
@@ -424,66 +426,84 @@ def get_tag_entry(self, name, key_class=HedSectionKey.AllTags, library_prefix=""
         return self._sections[key_class].get(name)
 
     def find_tag_entry(self, tag, library_prefix=""):
-        """
-        This takes a source tag and finds the schema entry for it.
+        """ Find the schema entry for a given source tag.
 
-        Works right to left.(mostly relevant for errors)
+        Args:
+            tag (str, HedTag): Any form of tag to look up.  Can have an extension, value, etc.
+        library_prefix (str):  The prefix the library, if any.
 
-        Parameters
-        ----------
-        tag : str or HedTag
-            Any form of tag to look up.  Can have an extension, value, etc.
-        library_prefix: str
-            The prefix the library, if any.
+        Returns:
+            HedTagEntry: The located tag entry for this tag.
+            str: The remainder of the tag that isn't part of the base tag.
+            list: A list of errors while converting.
+
+        Notes:
+            Works right to left (which is mostly relevant for errors).
 
-        Returns
-        -------
-        tag_entry: HedTagEntry
-            The located tag entry for this tag.
-        remainder: str
-            The remainder of the tag that isn't part of the base tag.
-        errors: list
-            a list of errors while converting
         """
         clean_tag = str(tag)
         prefix = library_prefix
         clean_tag = clean_tag[len(prefix):]
         prefix_tag_adj = len(prefix)
         working_tag = clean_tag.lower()
-        found_entry = None
-        remainder = ""
 
-        # this handles the one special case where the actual tag contains "/#" instead of something specific.
-        if working_tag.endswith("/#"):
-            working_tag = working_tag[:-2]
-            remainder = "/#"
+        # Most tags are in the schema directly, so test that first
+        found_entry = self.get_tag_entry(working_tag)
+        if found_entry:
+            # this handles the one special case where the actual tag contains "/#" instead of something specific.
+            if working_tag.endswith("/#"):
+                remainder = working_tag[-2:]
+            else:
+                remainder = ""
+
+            return found_entry, remainder, []
+
+        current_slash_index = -1
+        current_entry = None
+
+        # Loop left to right, checking each word.  Once we find an invalid word, we stop.
         while True:
-            tag_entry = self.get_tag_entry(working_tag)
-            parent_name, _, child_name = working_tag.rpartition("/")
-            if tag_entry is None:
-                if self.get_tag_entry(child_name):
-                    error = ErrorHandler.format_error(ValidationErrors.INVALID_PARENT_NODE,
-                                                      tag,
-                                                      index_in_tag=len(parent_name) + 1 + prefix_tag_adj,
-                                                      index_in_tag_end=len(parent_name) + 1 +
-                                                                       len(child_name) + prefix_tag_adj,
-                                                      expected_parent_tag=self.all_tags[child_name].name)
-                    return None, None, error
-                if not parent_name:
+            next_index = working_tag.find("/", current_slash_index + 1)
+            if next_index == -1:
+                next_index = len(working_tag)
+            parent_name = working_tag[:next_index]
+            parent_entry = self.get_tag_entry(parent_name)
+
+            if not parent_entry:
+                # We haven't found any tag at all yet
+                if current_entry is None:
                     error = ErrorHandler.format_error(ValidationErrors.NO_VALID_TAG_FOUND,
                                                       tag,
-                                                      index_in_tag=len(parent_name) + prefix_tag_adj,
-                                                      index_in_tag_end=len(parent_name)
-                                                                       + len(child_name) + prefix_tag_adj)
+                                                      index_in_tag=prefix_tag_adj,
+                                                      index_in_tag_end=prefix_tag_adj + next_index)
                     return None, None, error
-                working_tag = parent_name
-                remainder = clean_tag[len(working_tag):][:len(child_name) + 1] + remainder
-                continue
-
-            if remainder and tag_entry.takes_value_child_entry:
-                tag_entry = tag_entry.takes_value_child_entry
-            found_entry = tag_entry
-            break
+                # If this is not a takes value node, validate each term in the remainder.
+                if not current_entry.takes_value_child_entry:
+                    child_names = working_tag[current_slash_index + 1:].split("/")
+                    word_start_index = current_slash_index + 1 + prefix_tag_adj
+                    for name in child_names:
+                        if self.get_tag_entry(name):
+                            error = ErrorHandler.format_error(ValidationErrors.INVALID_PARENT_NODE,
+                                                              tag,
+                                                              index_in_tag=word_start_index,
+                                                              index_in_tag_end=word_start_index + len(name),
+                                                              expected_parent_tag=self.all_tags[name].name)
+                            return None, None, error
+                        word_start_index += len(name) + 1
+                break
+
+            current_entry = parent_entry
+            current_slash_index = next_index
+            if next_index == len(working_tag):
+                break
+            continue
+
+        remainder = None
+        if current_slash_index != -1:
+            remainder = clean_tag[current_slash_index:]
+        if remainder and current_entry.takes_value_child_entry:
+            current_entry = current_entry.takes_value_child_entry
+        found_entry = current_entry
 
         return found_entry, remainder, []
 
diff --git a/hed/schema/hed_schema_entry.py b/hed/schema/hed_schema_entry.py
index 032938708..05fa692f5 100644
--- a/hed/schema/hed_schema_entry.py
+++ b/hed/schema/hed_schema_entry.py
@@ -119,6 +119,9 @@ def __eq__(self, other):
     def __hash__(self):
         return hash((self.name, self._section._section_key))
 
+    def __str__(self):
+        return self.name
+
 
 class UnitClassEntry(HedSchemaEntry):
     """
@@ -196,23 +199,23 @@ def __init__(self, *args, **kwargs):
         self.short_tag_name = None
         self.takes_value_child_entry = None  # this is a child takes value tag, if one exists
         self._parent_tag = None
+        self.tag_terms = tuple()
 
     @staticmethod
     def get_fake_tag_entry(tag, tags_to_identify):
-        """
-            Given a tag and a list of possible short tags, create a tag entry if we find a match.
+        """ Create a tag entry if a given a tag has a match in a list of possible short tags.
 
         Args:
-            tag: str
-                The short/mid/long form tag to identify
-            tags_to_identify: [str]
-                A list of lowercase short tags to identify.  Goes through tag left to right.
+            tag (str): The short/mid/long form tag to identify.
+            tags_to_identify (list): A list of lowercase short tags to identify.
 
         Returns:
-        tag_entry: HedTagEntry or None
-            The fake entry, showing the short tag name as the found tag
-        remainder: str
-            The remaining text after the located short tag.  May be empty.
+            (HedTagEntry or None): The fake entry showing the short tag name as the found tag.
+            (str): The remaining text after the located short tag, which may be empty.
+
+        Notes:
+             The match is done left to right.
+
         """
         split_names = tag.split("/")
         index = 0
@@ -228,17 +231,19 @@ def get_fake_tag_entry(tag, tags_to_identify):
         return None, ""
 
     def any_parent_has_attribute(self, attribute):
-        """Checks to see if the tag (or any of its parents) have the given attribute.
+        """ Check if the tag (or any of its parents) has the given attribute.
 
-            This is mostly used to check extension allowed.  Could be cached.
-        Parameters
-        ----------
+        Args:
         attribute: str
             The name of the attribute to check for.
         Returns
         -------
         tag_has_attribute: bool
             True if the tag has the given attribute. False, if otherwise.
+
+        Notes:
+             This is mostly used to check extension allowed.  Could be cached.
+
         """
         iter_entry = self
         while iter_entry is not None:
@@ -284,6 +289,7 @@ def finalize_entry(self, schema):
             parent_tag = schema.get_tag_entry(parent_name)
         self._parent_tag = parent_tag
         self.takes_value_child_entry = schema.get_tag_entry(self.name + "/#")
+        self.tag_terms = tuple(self.long_tag_name.lower().split("/"))
 
         if self.name.endswith("/#"):
             if HedKey.UnitClass in self.attributes:
diff --git a/hed/schema/hed_schema_group.py b/hed/schema/hed_schema_group.py
index 849c3f070..d2f07aaad 100644
--- a/hed/schema/hed_schema_group.py
+++ b/hed/schema/hed_schema_group.py
@@ -128,10 +128,10 @@ def check_compliance(self, also_check_for_warnings=True, name=None,
             issues_list += schema.check_compliance(also_check_for_warnings, name, error_handler)
         return issues_list
 
-    def get_all_tags_with_attribute(self, key):
+    def get_tags_with_attribute(self, key):
         all_tags = set()
         for schema in self._schemas.values():
-            all_tags.update(schema.get_all_tags_with_attribute(key))
+            all_tags.update(schema.get_tags_with_attribute(key))
         return all_tags
 
     # todo: maybe tweak this API so you don't have to pass in library prefix?
diff --git a/hed/schema/hed_schema_io.py b/hed/schema/hed_schema_io.py
index 91cf03de1..b2c26f85c 100644
--- a/hed/schema/hed_schema_io.py
+++ b/hed/schema/hed_schema_io.py
@@ -10,23 +10,17 @@
 from hed.util import file_util
 
 
-
 def from_string(schema_string, file_type=".xml", library_prefix=None):
-    """
-        Creates a schema from the given string as if it was loaded from the given file type.
+    """ Create a schema from the given string as if it was loaded from the given file type.
+
+    Args:
+        schema_string (str):         An XML or mediawiki file as a single long string.
+        file_type (str):             The extension(including the .) corresponding to a file source.
+        library_prefix (str, None):  The name_prefix all tags in this schema will accept.
+
+    Returns:
+        (HedSchema):  The loaded schema.
 
-    Parameters
-    ----------
-    schema_string : str
-        An XML or mediawiki file as a single long string.
-    file_type : str
-        The extension(including the .) we should treat this string as
-    library_prefix : str or None
-        The name_prefix all tags in this schema will accept.
-    Returns
-    -------
-    schema: HedSchema
-        The loaded schema
     """
     if not schema_string:
         raise HedFileError(HedExceptions.BAD_PARAMETERS, "Empty string passed to HedSchema.from_string",
@@ -87,7 +81,7 @@ def load_schema(hed_path=None, library_prefix=None):
 
 # todo: this could be updated to also support .mediawiki format.
 def get_hed_xml_version(hed_xml_file_path):
-    """Gets the version number from a HED XML file.
+    """ Gets the version number from a HED XML file.
     Parameters
     ----------
     hed_xml_file_path: str
diff --git a/hed/schema/hed_schema_section.py b/hed/schema/hed_schema_section.py
index acf28afe2..ad98fd683 100644
--- a/hed/schema/hed_schema_section.py
+++ b/hed/schema/hed_schema_section.py
@@ -46,12 +46,9 @@ def _add_to_dict(self, name):
 
         return new_entry
 
-    def get_entries_with_attribute(self, attribute_name, return_name_only=False):
+    def get_entries_with_attribute(self, attribute_name, return_name_only=False, library_prefix=""):
         """
-            Returns an iterator of all entries with the given attribute
-
-            If this is called outside of schema validation, you may wish to cache this result or update
-            this function to have a cache.
+            Returns a list of all entries(or names) with the given attribute
 
         Parameters
         ----------
@@ -59,6 +56,8 @@ def get_entries_with_attribute(self, attribute_name, return_name_only=False):
             The name of the attribute(generally a HedKey entry)
         return_name_only: bool
             If true, return the name as a string rather than the tag entry.
+        library_prefix: str
+            Only valid if returning names.  Prepends given prefix to each name
         Returns
         -------
         [HedSchemaEntry] or [str]
@@ -69,7 +68,7 @@ def get_entries_with_attribute(self, attribute_name, return_name_only=False):
 
         cache_val = self._attribute_cache[attribute_name]
         if return_name_only:
-            return [tag_entry.name for tag_entry in cache_val]
+            return [f"{library_prefix}{tag_entry.name}" for tag_entry in cache_val]
         return cache_val
 
     # ===============================================
diff --git a/hed/schema/schema_compliance.py b/hed/schema/schema_compliance.py
index 328bfa27b..6740d685e 100644
--- a/hed/schema/schema_compliance.py
+++ b/hed/schema/schema_compliance.py
@@ -9,7 +9,6 @@
 ALLOWED_DESC_CHARS = "-_:;,./()+ ^"
 
 
-
 def check_compliance(hed_schema, also_check_for_warnings=True, name=None,
                      error_handler=None):
     """
diff --git a/hed/tools/__init__.py b/hed/tools/__init__.py
index 6368f4bcc..9456a7b7b 100644
--- a/hed/tools/__init__.py
+++ b/hed/tools/__init__.py
@@ -4,8 +4,6 @@
 from .annotation.file_dictionary import FileDictionary
 from .annotation.key_map import KeyMap
 from .annotation.key_template import KeyTemplate
-
-from .annotation.summary_entry import SummaryEntry
 from .annotation.tag_summary import TagSummary
 from .annotation.annotation_util import \
     check_df_columns, extract_tags, generate_sidecar_entry, hed_to_df, df_to_hed, merge_hed_dict
diff --git a/hed/tools/annotation/annotation_util.py b/hed/tools/annotation/annotation_util.py
index 367e95c8e..8e4a77f4a 100644
--- a/hed/tools/annotation/annotation_util.py
+++ b/hed/tools/annotation/annotation_util.py
@@ -1,6 +1,8 @@
 """ Utilities to facilitate annotation of events in BIDS. """
+
+import re
 from pandas import DataFrame
-from hed.errors import HedFileError
+from hed.errors.exceptions import HedFileError
 
 
 def check_df_columns(df, required_cols=('column_name', 'column_value', 'description', 'HED')):
@@ -79,7 +81,7 @@ def extract_tags(hed_string, search_tag):
 
 
 def generate_sidecar_entry(column_name, column_values=None):
-    """  Creates the sidecar column dictionary for a given column name
+    """  Create the sidecar column dictionary for a given column name
 
     Args:
         column_name (str):       Name of the column
@@ -87,19 +89,22 @@ def generate_sidecar_entry(column_name, column_values=None):
 
      Returns:
          dict   A dictionary representing a template for a sidecar entry.
+
     """
 
+    name_label = re.sub(r'[^A-Za-z0-9-]+', '_', column_name)
     sidecar_entry = {"Description": f"Description for {column_name}", "HED": ""}
     if not column_values:
-        sidecar_entry["HED"] = "Label/#"
+        sidecar_entry["HED"] = f"(Label/{name_label}, Label/#)"
     else:
         levels = {}
         hed = {}
         for column_value in column_values:
             if column_value == "n/a":
                 continue
-            levels[column_value] = f"Description for {column_value}"
-            hed[column_value] = f"Label/{column_value}"
+            value_label = re.sub(r'[^A-Za-z0-9-]+', '_', column_value)
+            levels[column_value] = f"Description for {column_value} of {column_name}"
+            hed[column_value] = f"(Label/{name_label}, Label/{value_label})"
         sidecar_entry["Levels"] = levels
         sidecar_entry["HED"] = hed
     return sidecar_entry
diff --git a/hed/tools/annotation/file_dictionary.py b/hed/tools/annotation/file_dictionary.py
index 791555fdc..618f12df1 100644
--- a/hed/tools/annotation/file_dictionary.py
+++ b/hed/tools/annotation/file_dictionary.py
@@ -12,16 +12,14 @@ def __init__(self, collection_name, file_list, key_indices=(0, 2), separator='_'
 
         Args:
             collection_name (str): Name of the file collection for reference
-            file_list (list):      List containing full paths of files of interest
-            key_indices (tuple):   List of indices into base file names of pieces to assemble for the key
+            file_list (list, None):      List containing full paths of files of interest
+            key_indices (tuple, None):   List of indices into base file names of pieces to assemble for the key
             separator (str):       Character used to separate pieces of key name
 
         """
         self.collection_name = collection_name
-        if key_indices:
-            self.file_dict = self.make_file_dict(file_list, key_indices=key_indices, separator=separator)
-        else:
-            self.file_dict = {}
+        self.file_dict = {}
+        self.create_file_dict(file_list, key_indices, separator)
 
     @property
     def name(self):
@@ -35,6 +33,10 @@ def key_list(self):
     def file_list(self):
         return list(self.file_dict.values())
 
+    def create_file_dict(self, file_list, key_indices, separator):
+        if key_indices:
+            self.file_dict = self.make_file_dict(file_list, key_indices=key_indices, separator=separator)
+
     def get_file_path(self, key):
         return self.file_dict.get(key, None)
 
@@ -56,15 +58,27 @@ def key_diffs(self, other_dict):
         return list(diffs)
 
     def output_files(self, title=None, logger=None):
+        """ Returns a str with the output of the list
+        Args:
+            title (None, str)    Optional title.
+            logger (HedLogger)   Optional HED logger for recording.
+        Returns: (str)
+            Output the dictionary in string form. The logger is updated if available.
+
+        """
+        output_list = []
         if title:
-            print(f"{title} ({len(self.key_list)} files)")
+            output_list.append(f"{title} ({len(self.key_list)} files)")
         for key, value in self.file_dict.items():
-            print(f"{key}: {os.path.basename(value)}")
+            basename = os.path.basename(self.get_file_path(key))
+            output_list.append(f"{key}: {basename}")
             if logger:
-                logger.add(key, f"{self.name}: {os.path.basename(value)}")
+                logger.add(key, f"{self.name}: {basename}")
+        return "\n".join(output_list)
 
     @staticmethod
     def make_file_dict(file_list, key_indices=(0, 2), separator='_'):
+
         file_dict = {}
         for the_file in file_list:
             the_file = os.path.realpath(the_file)
diff --git a/hed/tools/annotation/key_map.py b/hed/tools/annotation/key_map.py
index 4547f0bfc..bcec81bfe 100644
--- a/hed/tools/annotation/key_map.py
+++ b/hed/tools/annotation/key_map.py
@@ -84,7 +84,7 @@ def _remap(self, df):
         for index, row in df.iterrows():
             key = get_row_hash(row, self.key_cols)
             key_value = self.map_dict.get(key, None)
-            if key_value != None:
+            if key_value is not None:
                 result = self.col_map.iloc[key_value]
                 row[self.target_cols] = result[self.target_cols].values
                 df.iloc[index] = row
diff --git a/hed/tools/annotation/key_template.py b/hed/tools/annotation/key_template.py
index dfcc43414..b2067ce1b 100644
--- a/hed/tools/annotation/key_template.py
+++ b/hed/tools/annotation/key_template.py
@@ -23,6 +23,13 @@ def __init__(self, columns, name=''):
         self.map_dict = {}
         self.count_dict = {}
 
+    def __str__(self):
+        temp_list = [f"{self.name} counts for key [{str(self.columns)}]:"]
+        for index, row in self.col_map.iterrows():
+            key_hash = get_row_hash(row, self.columns)
+            temp_list.append(f"{str(list(row.values))}\t{self.count_dict[key_hash]}")
+        return "\n".join(temp_list)
+
     def make_template(self, additional_cols=[]):
         if additional_cols and set(self.columns).intersection(additional_cols):
             raise HedFileError("AdditionalColumnsNotDisjoint",
@@ -34,6 +41,13 @@ def make_template(self, additional_cols=[]):
             df[additional_cols] = 'n/a'
         return df
 
+    def resort(self):
+        """Sort the col_map in place by the key columns """
+        self.col_map.sort_values(by=self.columns, inplace=True, ignore_index=True)
+        for index, row in self.col_map.iterrows():
+            key_hash = get_row_hash(row, self.columns)
+            self.map_dict[key_hash] = index
+
     def update_by_tuple(self, key_tuple):
         key_hash = get_key_hash(key_tuple)
         if key_hash not in self.map_dict:
@@ -74,15 +88,3 @@ def _update(self, base_df):
                 self.col_map = self.col_map.append(row[self.columns], ignore_index=True)
                 self.count_dict[key] = 0
             self.count_dict[key] += 1
-
-    def resort(self):
-        self.col_map.sort_values(by=self.columns, inplace=True, ignore_index=True)
-        for index, row in self.col_map.iterrows():
-            key_hash = get_row_hash(row, self.columns)
-            self.map_dict[key_hash] = index
-
-    def print(self, file=None):
-        print(f"Counts for key [{str(self.columns)}]:", file=file)
-        for index, row in self.col_map.iterrows():
-            key_hash = get_row_hash(row, self.columns)
-            print(f"{str(list(row.values))}\t{self.count_dict[key_hash]}", file=file)
diff --git a/hed/tools/annotation/summary_entry.py b/hed/tools/annotation/summary_entry.py
deleted file mode 100644
index bc4199d7d..000000000
--- a/hed/tools/annotation/summary_entry.py
+++ /dev/null
@@ -1,147 +0,0 @@
-import json
-from hed.models.hed_group import HedGroup
-from hed.models.model_constants import DefTagNames
-from hed.models.sidecar import Sidecar
-from hed.schema.hed_schema_io import load_schema_version
-from hed.tools.annotation.summary_util import add_tag_list_to_dict, breakout_tags, extract_dict_values
-
-
-class SummaryEntry:
-    """ Holds the summary of a group such as a HED definition. """
-
-    def __init__(self, name, contents, anchor_name=None):
-        """Contains info for a HedGroup
-
-        Parameters
-        ----------
-        name : str
-            The label for this entry
-        contents: HedGroup or None
-            The contents of this group
-       anchor_tag: str
-             Lowercase version of tag that anchars this group
-
-        For Definition groups, the label portion of the Definition tag not including the Definition/ is the name
-        and the group is the contents of the Definition.
-        """
-        self.name = name
-        self.anchor_name = anchor_name
-        self.group = contents
-        self.tag_dict = {}
-        add_tag_list_to_dict([self.group], self.tag_dict)
-
-    @staticmethod
-    def extract_anchored_group(group, anchor_name=DefTagNames.DEFINITION_KEY):
-        """ Extract a list of non-anchored groups and return a SummaryEntry for the extracted group
-
-        Parameters:
-            group: HedGroup
-                Group from which to extract the summary entry
-            anchor_name: str
-                Short tag name (without value) of the anchor tag
-        Returns:
-            SummaryEntry
-
-        This assumes that the canonical forms have already been set.
-        """
-
-        tags = group.tags()
-        if len(tags) != 1 or not anchor_name:
-            return None
-        index = tags[0].short_tag.lower().find(anchor_name.lower())
-        if index == -1:
-            return None
-        tag_name = tags[0].short_tag[(index + len(anchor_name) + 1):]
-        groups = group.groups()
-        return SummaryEntry(tag_name, groups[0], anchor_name)
-
-    @staticmethod
-    def extract_summary_info(summary_entry_dict, tag_name):
-        dict_info = {}
-        for key, entry in summary_entry_dict.items():
-            tags = list(entry.tag_dict.keys())
-            tag_names, tag_present =  extract_dict_values(entry.tag_dict, tag_name, tags)
-            if not tag_present:
-                continue
-            descriptions, tag_present = extract_dict_values(entry.tag_dict, 'Description', tags)
-            dict_info[entry.name] = {'tag_name': tag_name, 'def_name': entry.name, 'tags': tags,
-                                     'description': ' '.join(descriptions), 'tag_values': tag_names}
-        return dict_info
-
-    @staticmethod
-    def separate_anchored_groups(hed_string_obj, entry_dict, anchor_name=DefTagNames.DEFINITION_KEY):
-        """ Removes the anchored groups from a HedString and puts them in a separate dictionary
-
-        Parameters:
-            hed_string_obj:  HedString
-                HedString object whose children have their canonical forms already set
-            entry_dict: dict
-                dictionary of information for anchored tag groups
-            anchor_name: str
-                Short tag name (without value) of the anchor tag
-
-        Returns:
-            list:  a list of direct children of HedString with the anchor groups removed
-
-        """
-        groups = hed_string_obj.groups()
-        tags_no_anchor = hed_string_obj.tags()
-        for group in groups:
-            def_entry = SummaryEntry.extract_anchored_group(group, anchor_name=anchor_name)
-            if def_entry:
-                entry_dict[def_entry.name] = def_entry
-            else:
-                tags_no_anchor.append(group)
-        return tags_no_anchor
-
-
-if __name__ == '__main__':
-    from hed.models import DefDict
-    from hed.models.def_dict import add_group_to_dict
-    the_path = '../../../tests/data/bids/eeg_ds003654s_hed/task-FacePerception_events.json'
-    sidecar = Sidecar(the_path)
-    schema = load_schema_version(xml_version='8.0.0')
-
-    tag_dict_no_defs = {}
-    tag_dict_with_defs = {}
-    def_dict = DefDict()
-    for hed_string_obj, _, _ in sidecar.hed_string_iter([schema, def_dict]):
-        add_group_to_dict(hed_string_obj, tag_dict_with_defs)
-        hed_string_obj.remove_definitions()
-        add_group_to_dict(hed_string_obj, tag_dict_no_defs)
-
-    # tag_dict = {}
-    # no_defs_list = []
-    # def_dict = {}
-    # for column in sidecar:
-    #     for hed, key in column._hed_iter():
-    #         hed.convert_to_canonical_forms(schema)
-    #         no_defs_list += SummaryEntry.separate_anchored_groups(hed, def_dict, DefTagNames.DEFINITION_KEY)
-    # add_tag_list_to_dict(no_defs_list, tag_dict)
-    #
-    # hed_list = []
-    # for column in sidecar:
-    #     for hed, key in column._hed_iter():
-    #         hed.convert_to_canonical_forms(schema)
-    #         hed_list.append(hed)
-    #
-    # only_dict = {}
-    # add_tag_list_to_dict(hed_list, only_dict )
-    # print(only_dict.keys())
-    #
-    # # Todo: remove this block
-    # if tag_dict_with_defs != only_dict:
-    #     raise ValueError("def dict!")
-    # if tag_dict_no_defs != tag_dict:
-    #     raise ValueError("tag dict!")
-
-    json_path = "../../../tests/data/curation/tag_summary_template.json5"
-    with open(json_path) as fp:
-        rules = json.load(fp)
-    breakout_list = rules["Tag-categories"]
-    breakout_dict = breakout_tags(schema, tag_dict_with_defs.keys(), breakout_list)
-    with open('d:/temp1.json', "w") as fp:
-        json.dump(breakout_dict, fp, indent=4)
-
-
-
diff --git a/hed/tools/annotation/summary_util.py b/hed/tools/annotation/summary_util.py
index e8c2ebcec..6230bd4a6 100644
--- a/hed/tools/annotation/summary_util.py
+++ b/hed/tools/annotation/summary_util.py
@@ -1,13 +1,22 @@
 """ Utilities used in computing dataset annotation. """
+
 from hed.models.hed_tag import HedTag
 
 
 def breakout_tags(schema, tag_list, breakout_list):
-    """ Create a dictionary where the tags are broken up into specific tags
-    Parameters:
-    -----------
-        schema: HedSchema
+    """ Create a dictionary where the tags are broken up into specific groups.
+
+    Args:
+        schema (HedSchema, HedSchemas):   Schemas to use to break out the tags.
+        tag_list (list, dict):            Iteratable of tags to be broken out.
+        breakout_list (list):             List of hed tag node strings that should be summarized separately.
 
+    Returns:
+        dict:  Dictionary where the keys are the tags from tag_list and all of their parents. The values are
+               each a list of all of the tags from the breakout_list and their parents.
+
+    Notes:
+        The tags that aren't in the breakout list appear in the returned dictionary under "leftovers".
 
     """
     breakout_dict = {}
@@ -29,15 +38,40 @@ def breakout_tags(schema, tag_list, breakout_list):
 
 
 def extract_dict_values(tag_dict, tag_name, tags):
-        if tag_name not in tag_dict:
-            return [], False
-        tags.remove(tag_name)
-        return list(tag_dict[tag_name].keys()), True
+    """ Get the tags associated with tag name from the tag dictionary.
+
+    Args:
+        tag_dict (dict):  Dictionary with keys that a tag node names and values are dictionaries.
+        tag_name (str):   Name of a node.
+        tags (list):      List of tags left to process.
+
+    Returns:
+        list:             Tags associated with tag_name.
+        bool:             True if tag_name was found in tag_dict.
+
+    Notes:
+        Side-effect the tags list is modified.
+    """
+    if tag_name not in tag_dict:
+        return [], False
+    tags.remove(tag_name)
+    return list(tag_dict[tag_name].keys()), True
 
 
 def get_schema_entries(hed_schema, tag, library_prefix=""):
+    """ Get a list of schema entries corresponding to tag and to its parents.
+
+    Args:
+        hed_schema (HedSchema, HedSchemaGroup):  The schemas in which to search for tag.
+        tag (HedTag, str):       The HED tag to look for.
+        library_prefix (str):    The library prefix to use in the search.
+
+    Returns:
+        list:  A list of HedTagEntry objects for the tag and its parent nodes in the schema.
+    """
+
     entry_list = []
-    tag_entry, remainder, tag_issues=hed_schema.find_tag_entry(tag, library_prefix)
+    tag_entry, remainder, tag_issues = hed_schema.find_tag_entry(tag, library_prefix)
     while tag_entry is not None:
         entry_list.append(tag_entry)
         tag_entry = tag_entry._parent_tag
@@ -47,16 +81,16 @@ def get_schema_entries(hed_schema, tag, library_prefix=""):
 
 
 def add_tag_list_to_dict(tag_list, tag_dict, hed_schema=None):
-    """ Convert a list of tags and groups into a single list of tags and create a dictionary
+    """ Convert a list of tags and groups into a dictionary with short tag as key and dict of values as the value.
 
-    Parameters:
-        tag_list: list
-            List of HedTag and HedGroup to transform
-        hed_schema: HedSchema or HedSchemaGroup
-            Hed schema to use to convert tags to canonical form. If None, assumes already converted
+    Args:
+        tag_list (list): List of HedTag and HedGroup objects to transform.
+        tag_dict (dict):
+        hed_schema (HedSchema or HedSchemaGroup):  Hed schema to use to convert tags to canonical form.
+            If None, assumes already converted
 
-    Returns: dict
-        Dictionary of tags with a list of values
+    Returns:
+        dict: Dictionary of tags as keys with a dict of values as the value.
     """
     unfolded_list = unfold_tag_list(tag_list)
     for tag in unfolded_list:
@@ -72,7 +106,15 @@ def add_tag_list_to_dict(tag_list, tag_dict, hed_schema=None):
 
 
 def unfold_tag_list(tag_list):
-    """ Unfold a list consisting of HedTag and HedGroup objects """
+    """ Unfold a list consisting of HedTag and HedGroup objects making it a single level of HedTags.
+
+     Args:
+         tag_list (list):  List of HedTags and HedGroup objects.
+
+     Returns:
+         list:    A list of HedTags.
+
+    """
     unfolded_list = []
     for element in tag_list:
         if isinstance(element, HedTag):
diff --git a/hed/tools/annotation/tag_summary.py b/hed/tools/annotation/tag_summary.py
index 5abff5df5..2f2b17faf 100644
--- a/hed/tools/annotation/tag_summary.py
+++ b/hed/tools/annotation/tag_summary.py
@@ -1,13 +1,9 @@
-""" Creates a summary from a list containing HedTags and HedGroups but no definitions """
 import os
 import json
-from hed.models.model_constants import DefTagNames
 from hed.tools.bids.bids_dataset import BidsDataset
-
-from hed.tools.annotation.summary_entry import SummaryEntry
-from hed.tools.annotation.summary_util import add_tag_list_to_dict, breakout_tags
-from hed.models.def_dict import add_group_to_dict
-from hed.models import DefDict
+from hed.tools.annotation.summary_util import breakout_tags, extract_dict_values
+from hed.models.definition_dict import add_group_to_dict
+from hed.models import DefinitionDict
 
 DEFAULT_BREAKOUT_LIST = [
     "Sensory-event", "Agent-action", "Event", "Action", "Task-event-role", "Task-action-type",
@@ -17,7 +13,22 @@
 
 
 class TagSummary:
-    """ Holds a HED tag summary for a BIDS dataset. """
+    """ Creates a HED tag summary for a BIDS dataset.
+
+    Args:
+        dataset (BidsDataset)        Contains the information for a BIDS dataset.
+        breakout_list (list, None):  List of the tags to be explicitly broken out.
+
+    Attributes:
+        dataset (BidsDataset):  The BIDS dataset to be summarized.
+        all_tags_dict (dict):   The keys are all of the unique tags in the BIDS dataset. The values
+             are dictionaries of the unique values that these tags take on.
+        breakout_list (list):  The tag nodes that are to be specially summarized.
+        breakout_dict (dict):  The keys are the breakout nodes. The values are dictionaries of the
+             child nodes and the nodes themselves that appear in the dataset.
+        task_dict (dict):      The keys are definition names and the values are dictionaries with info.
+        cond_dict (dict):      The keys are definition names and the values are dictionaries with info.
+    """
     def __init__(self, dataset, breakout_list=None):
         self.dataset = dataset
         self.all_tags_dict = {}
@@ -26,12 +37,12 @@ def __init__(self, dataset, breakout_list=None):
         if not breakout_list:
             self.breakout_list = DEFAULT_BREAKOUT_LIST
         self.breakout_dict = breakout_tags(self.dataset.schemas, self.all_tags_dict, breakout_list)
-        self.task_dict = SummaryEntry.extract_summary_info(self.all_defs_dict, 'Task')
-        self.cond_dict = SummaryEntry.extract_summary_info(self.all_defs_dict, 'Condition-variable')
+        self.task_dict = self.extract_summary_info(self.all_defs_dict, 'Task')
+        self.cond_dict = self.extract_summary_info(self.all_defs_dict, 'Condition-variable')
 
     def _set_all_tags(self):
         schema = self.dataset.schemas
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         self.all_defs_dict = def_dict.defs
         for bids_sidecar in self.dataset.event_files.sidecar_dict.values():
             sidecar = bids_sidecar.contents
@@ -39,6 +50,14 @@ def _set_all_tags(self):
                 add_group_to_dict(hed_string_obj, self.all_tags_dict)
 
     def get_design_matrices(self):
+        """ Returns a dictionary with condition variables.
+
+        Returns: (dict, list, list)
+            Dictionary with condition variable levels corresponding to a design matrix.
+            List with the other condition variables that aren't associated with levels.
+            List of errors.
+
+        """
         cond_dict = self.cond_dict
         design_dict = {}
         other_list = []
@@ -59,6 +78,26 @@ def get_design_matrices(self):
                 error_list.append(level_info)
         return design_dict, other_list, error_list
 
+    @staticmethod
+    def extract_summary_info(entry_dict, tag_name):
+        """ Extracts the summary of tag that is stored in the entry dictionary.
+
+        Args:
+            entry_dict (dict):  Keys are individual tag node names
+            tag_name (str):     Name of an individual node.
+
+        """
+        dict_info = {}
+        for key, entry in entry_dict.items():
+            tags = list(entry.tag_dict.keys())
+            tag_names, tag_present = extract_dict_values(entry.tag_dict, tag_name, tags)
+            if not tag_present:
+                continue
+            descriptions, tag_present = extract_dict_values(entry.tag_dict, 'Description', tags)
+            dict_info[entry.name] = {'tag_name': tag_name, 'def_name': entry.name, 'tags': tags,
+                                     'description': ' '.join(descriptions), 'tag_values': tag_names}
+        return dict_info
+
 
 if __name__ == '__main__':
     root_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
@@ -67,10 +106,9 @@ def get_design_matrices(self):
     json_path = "../../../tests/data/curation/tag_summary_template.json5"
     with open(json_path) as fp:
         rules = json.load(fp)
-    breakout_list = rules["Tag-categories"]
-
-    summary = TagSummary(BidsDataset(root_path), breakout_list)
-    design_dict, other_list, error_list = summary.get_design_matrices()
-
+    breakouts = rules["Tag-categories"]
 
+    summary = TagSummary(BidsDataset(root_path), breakouts)
+    designs, others, errors = summary.get_design_matrices()
 
+    print("to here")
\ No newline at end of file
diff --git a/hed/tools/bids/bids_event_file.py b/hed/tools/bids/bids_event_file.py
index 7227c8002..eb51aaa60 100644
--- a/hed/tools/bids/bids_event_file.py
+++ b/hed/tools/bids/bids_event_file.py
@@ -4,7 +4,10 @@
 
 
 class BidsEventFile(BidsTsvFile):
-    """Represents a BIDS event file including its associated sidecars."""
+    """ Represents a BIDS event file including its associated sidecars.
+
+
+    """
 
     def __init__(self, file_path):
         super().__init__(file_path, set_contents=False)
diff --git a/hed/tools/bids/bids_event_files.py b/hed/tools/bids/bids_event_files.py
index 56a05b9e6..845add98a 100644
--- a/hed/tools/bids/bids_event_files.py
+++ b/hed/tools/bids/bids_event_files.py
@@ -61,12 +61,36 @@ def get_sidecars_from_path(self, obj):
         return sidecar_list
 
     def summarize(self, value_cols=None, skip_cols=None):
-        col_info = BidsTsvSummary(value_cols=None, skip_cols=None)
+        """
+
+        Args:
+            value_cols (list):  Column names designated as value columns.
+            skip_cols (list):   Column names designated as columns to skip.
+
+        Returns:
+            BidsTsvSummary:  A summary of the number of values in different columns.
+
+        Notes: The columns that are not value_cols or skip_col are summarized by counting
+        the number of times each unique value appears in that column.
+
+        """
+        info = BidsTsvSummary(value_cols=value_cols, skip_cols=skip_cols)
         for event_obj in self.events_dict.values():
-            col_info.update(event_obj.file_path)
-        return col_info
+            info.update(event_obj.file_path)
+        return info
 
     def validate(self, validators, check_for_warnings=True, keep_events=False):
+        """ Validate the events files and return an error list.
+
+        Args:
+            validators ([func or HedOps], func, HedOps):  Validation functions to apply.
+            check_for_warnings (bool):  If True, include warnings in the check.
+            keep_events (bool):         If True, the underlying event files are read and their contents retained.
+
+        Returns:
+            (list):    A list of validation issues found.
+
+        """
         issues = []
         for json_obj in self.sidecar_dict.values():
             issues += json_obj.contents.validate_entries(hed_ops=validators, check_for_warnings=check_for_warnings)
@@ -101,4 +125,3 @@ def validate(self, validators, check_for_warnings=True, keep_events=False):
     # print(f"Issues: {issue_str}")
 
     col_info = bids.summarize()
-    col_info.print()
diff --git a/hed/tools/bids/bids_file.py b/hed/tools/bids/bids_file.py
index 2c0e620b0..31c0795c1 100644
--- a/hed/tools/bids/bids_file.py
+++ b/hed/tools/bids/bids_file.py
@@ -3,7 +3,18 @@
 
 
 class BidsFile:
-    """Represents the entity and file names for a BIDs file."""
+    """ Represents the entity dictionary and file names for a BIDs file.
+
+    Args:
+        file_path (str):   Full path of the file.
+
+    Attributes:
+        file_path (str):     Real path of the file.
+        suffix (str):        Suffix part of the filename.
+        ext (str):           Extension (including the .).
+        entity_dict (dict):  Dictionary of entity-names (keys) and entity-values (values).
+
+    """
 
     def __init__(self, file_path):
         self.file_path = os.path.realpath(file_path)
diff --git a/hed/tools/bids/bids_file_dictionary.py b/hed/tools/bids/bids_file_dictionary.py
index 56284c36d..35e7543a5 100644
--- a/hed/tools/bids/bids_file_dictionary.py
+++ b/hed/tools/bids/bids_file_dictionary.py
@@ -1,4 +1,3 @@
-import os
 from hed.errors.exceptions import HedFileError
 from hed.tools import BidsFile
 from hed.tools.annotation.file_dictionary import FileDictionary
@@ -14,16 +13,13 @@ def __init__(self, collection_name, file_list=None, entities=('sub', 'ses', 'tas
 
         Args:
             collection_name (str): Name of this collection
-            file_list (list):      List containing full paths of files of interest
+            file_list (list, None):      List containing full paths of files of interest
             entities (tuple:  List of indices into base file names of pieces to assemble for the key
         """
-        self.collection_name = collection_name
+        super().__init__(collection_name, None, None, separator='_')
         self.entities = entities
         if file_list:
             self.file_dict = self.make_bids_file_dict(file_list, entities)
-        else:
-            self.file_dict = {}
-
 
     @property
     def key_list(self):
@@ -62,14 +58,6 @@ def make_query(self, query_dict={'sub': '*', 'run': ['*']}):
                 response_dict[key] = file
         return response_dict
 
-    def output_files(self, title=None, logger=None):
-        if title:
-            print(f"{title} ({len(self.key_list)} files)")
-        for key, value in self.file_dict.items():
-            print(f"{key}: {os.path.basename(value.file_path)}")
-            if logger:
-                logger.add(key, f"{self.name}: {os.path.basename(value.file_path)}")
-
     def _create_dict_obj(self, collection_name, file_dict):
         dict_obj = BidsFileDictionary(collection_name, file_list=None, entities=self.entities)
         dict_obj.file_dict = file_dict
diff --git a/hed/tools/bids/bids_tsv_summary.py b/hed/tools/bids/bids_tsv_summary.py
index 5421fb39c..bba767744 100644
--- a/hed/tools/bids/bids_tsv_summary.py
+++ b/hed/tools/bids/bids_tsv_summary.py
@@ -1,5 +1,6 @@
-from hed.errors.exceptions import HedFileError
-from hed.util.data_util import get_new_dataframe
+
+from hed.errors import HedFileError
+from hed.util import get_new_dataframe
 from hed.tools.annotation.annotation_util import generate_sidecar_entry
 
 
@@ -10,9 +11,9 @@ def __init__(self, value_cols=None, skip_cols=None, name=''):
         """ .
 
         Args:
-            value_cols (list):   List of columns to be treated as value columns
-            skip_cols (list):    List of columns to be skipped
-            name (str):          Name associated with the dictionary
+            value_cols (list, None):  List of columns to be treated as value columns.
+            skip_cols (list, None):   List of columns to be skipped.
+            name (str):               Name associated with the dictionary.
 
         """
 
@@ -32,8 +33,34 @@ def __init__(self, value_cols=None, skip_cols=None, name=''):
         self.total_files = 0
         self.total_events = 0
 
+    def __str__(self):
+        indent = "   "
+        summary_list = [f"Summary for column dictionary {self.name}:"]
+        sorted_keys = sorted(self.categorical_info.keys())
+        summary_list.append(f"{indent}Categorical columns ({len(sorted_keys)}):")
+        for key in sorted_keys:
+            value_dict = self.categorical_info[key]
+            sorted_v_keys = sorted(list(value_dict))
+            summary_list.append(f"{indent * 2}{key} ({len(sorted_v_keys)} distinct values):")
+            for v_key in sorted_v_keys:
+                summary_list.append(f"{indent * 3}{v_key}: {value_dict[v_key]}")
+
+        sorted_cols = sorted(map(str, list(self.value_info)))
+        summary_list.append(f"{indent}Value columns ({len(sorted_cols)}):")
+        for key in sorted_cols:
+            summary_list.append(f"{indent * 2}{key}: {self.value_info[key]}")
+        return "\n".join(summary_list)
+
     def get_number_unique_values(self, column_names=None):
-        """ Extract the number of unique values the specified columns if available ."""
+        """ Extract the number of unique values in each of the specified columns if available .
+
+        Args:
+            column_names (list, None):   A list of column names to analyze or all columns if None.
+
+        Returns: (dict)
+            Dictionary with a column name as the key and the number of unique values in the column as the value.
+
+        """
         if not column_names:
             column_names = list(self.categorical_info.keys())
         counts = {}
@@ -56,44 +83,8 @@ def extract_sidecar_template(self):
             side_dict[column_name] = generate_sidecar_entry(column_name, [])
         return side_dict
 
-    def print(self, title=None, indent="  "):
-        if not title:
-            title = f"Summary for column dictionary {self.name}:"
-        print(title)
-        sorted_keys = sorted(self.categorical_info.keys())
-        print(f"{indent}Categorical columns ({len(sorted_keys)}):")
-        for key in sorted_keys:
-            value_dict = self.categorical_info[key]
-            sorted_v_keys = sorted(list(value_dict))
-            print(f"{indent * 2}{key} ({len(sorted_v_keys)} distinct values):")
-            for v_key in sorted_v_keys:
-                print(f"{indent * 3}{v_key}: {value_dict[v_key]}")
-
-        sorted_cols = sorted(map(str, list(self.value_info)))
-        print(f"{indent}Value columns ({len(sorted_cols)}):")
-        for key in sorted_cols:
-            print(f"{indent * 2}{key}: {self.value_info[key]}")
-
-    # def tojson(self, max= indent=4):
-    #     if not title:
-    #         title = f"Summary for column dictionary {self.name}:"
-    #     print(title)
-    #     sorted_keys = sorted(self.categorical_info.keys())
-    #     print(f"{indent}Categorical columns ({len(sorted_keys)}):")
-    #     for key in sorted_keys:
-    #         value_dict = self.categorical_info[key]
-    #         sorted_v_keys = sorted(list(value_dict))
-    #         print(f"{indent * 2}{key} ({len(sorted_v_keys)} distinct values):")
-    #         for v_key in sorted_v_keys:
-    #             print(f"{indent * 3}{v_key}: {value_dict[v_key]}")
-    #
-    #     sorted_cols = sorted(map(str, list(self.value_info)))
-    #     print(f"{indent}Value columns ({len(sorted_cols)}):")
-    #     for key in sorted_cols:
-    #         print(f"{indent * 2}{key}: {self.value_info[key]}")
-
     def update(self, data):
-        """ Extracts the number of times each unique value appears in each column.
+        """ Extract the number of times each unique value appears in each column.
 
         Args:
             data (DataFrame, str, or list):    DataFrame to be analyzed or the full path of a tsv file.
diff --git a/hed/tools/curation/tsv_reports.py b/hed/tools/curation/tsv_reports.py
index b8b1abaaf..c89019cc5 100644
--- a/hed/tools/curation/tsv_reports.py
+++ b/hed/tools/curation/tsv_reports.py
@@ -8,46 +8,50 @@ def report_tsv_diffs(tsv_dict1, tsv_dict2, logger):
         tsv_dict2 (BidsTsvDictionary): A dictionary representing BIDS-keyed tsv files
         logger (HedLogger):            A HedLogger object for reporting the values by key
 
-    Returns:
+    Returns: (str)                     A string with the differences
 
     """
-    logger.add("overall", f"{tsv_dict1.name} has {len(tsv_dict1.file_list)} event files")
-    logger.add("overall", f"{tsv_dict2.name} has {len(tsv_dict2.file_list)} event files")
-    print(f"Summarizing {tsv_dict1.name}...")
-    tsv_dict1.output_files(title=f"\n{tsv_dict1.name} event files", logger=logger)
-    tsv_dict2.output_files(title=f"\n{tsv_dict2.name} event files", logger=logger)
+    report_list = [f"{tsv_dict1.name} has {len(tsv_dict1.file_list)} event files"]
+    logger.add("overall", f"{report_list[-1]}")
+    report_list.append(f"{tsv_dict2.name} has {len(tsv_dict2.file_list)} event files")
+    logger.add("overall", f"{report_list[-1]}")
+
+    report_list.append(tsv_dict1.output_files(title=f"\n{tsv_dict1.name} event files", logger=logger))
+    report_list.append(tsv_dict2.output_files(title=f"\n{tsv_dict2.name} event files", logger=logger))
 
     # Make sure there are the same number of files in both collections
     if len(tsv_dict1.key_list) != len(tsv_dict2.key_list):
-        msg = f"{tsv_dict1.name} has {len(tsv_dict1.file_list)} files and " +\
-              f"{tsv_dict2.name} has {len(tsv_dict2.file_list)} files"
-        logger.add("overall", msg, level="ERROR", also_print=True)
+        report_list.append(f"{tsv_dict1.name} has {len(tsv_dict1.file_list)} files and " +\
+              f"{tsv_dict2.name} has {len(tsv_dict2.file_list)} files")
+        logger.add("overall", f"{report_list[-1]}", level="ERROR")
 
     # Compare keys from the two dictionaries to make sure they have the same keys
     key_diff = tsv_dict1.key_diffs(tsv_dict2)
     if key_diff:
-        logger.add("overall", f"File key differences {str(key_diff)}", level="ERROR", also_print=True)
+        report_list.append(f"File key differences {str(key_diff)}")
+        logger.add("overall", f"{report_list[-1]}", level="ERROR")
 
     # Output the column names for each type of event file
-    print(f"\n{tsv_dict1.name} event file columns:")
+    report_list.append(f"\n{tsv_dict1.name} event file columns:")
     for key, file, rowcount, columns in tsv_dict1.iter_tsv_info():
-        logger.add(key, f"{tsv_dict1.name}: [{rowcount} events] {str(columns)}", also_print=True)
+        report_list.append(f"{tsv_dict1.name}: [{rowcount} events] {str(columns)}")
+        logger.add(key, f"{report_list[-1]}")
 
-    print(f"\n{tsv_dict2.name} event file columns:")
     for key, file, rowcount, columns in tsv_dict2.iter_tsv_info():
-        logger.add(key, f"{tsv_dict2.name}: [{rowcount} events] {str(columns)}", also_print=True)
+        report_list.append(f"{tsv_dict2.name}: [{rowcount} events] {str(columns)}")
+        logger.add(key, f"{report_list[-1]}")
 
     # Output keys for files in which the BIDS and EEG.events have different numbers of events
     count_diffs = tsv_dict1.count_diffs(tsv_dict2)
     if count_diffs:
-        print(f"\nThe number of {tsv_dict1.name} events and {tsv_dict2.name} events differ for the following files:")
+        report_list.append(f"\nThe number of {tsv_dict1.name} events and {tsv_dict2.name} events" +\
+                           f"differ for the following files:")
         for item in count_diffs:
-            msg = f"The {tsv_dict1.name} file has {item[1]} rows and " +\
-                  f"the {tsv_dict2.name} event file has {item[2]} rows"
-            logger.add(item[0], msg, level="ERROR", also_print=True)
+            report_list.append(f"The {tsv_dict1.name} file has {item[1]} rows and " +\
+                  f"the {tsv_dict2.name} event file has {item[2]} rows")
+            logger.add(item[0], f"{report_list[-1]}", level="ERROR")
     else:
-        logger.add("overall",
-                   f"The {tsv_dict1.name} event files and the {tsv_dict2.name} files have the same number of rows",
-                   also_print=True)
-
+        report_list.append(f"\nThe {tsv_dict1.name} and {tsv_dict2.name} files have the same number of rows")
+        logger.add("overall", f"{report_list[-1]}")
 
+    return "\n".join(report_list)
diff --git a/hed/tools/hed_logger.py b/hed/tools/hed_logger.py
index 392e4261c..3eab8f21a 100644
--- a/hed/tools/hed_logger.py
+++ b/hed/tools/hed_logger.py
@@ -5,20 +5,11 @@
 import json
 
 
-# class HedLogEntry:
-#     """ Class containing the log entry """
-#
-#     def __init__(self, key, msg, level):
-#         self.dict = {"key": key, "msg": msg, "level": msg}
-#
-#     def toJSON(self):
-#         return json.dumps({"key": self.key, "msg": self.msg, "level": self.msg}, indent=4)
-
-
 class HedLogger:
     """ Class to log status messages organized by key """
-    def __init__(self):
+    def __init__(self, name=""):
         self.log = {}
+        self.name = name
 
     def add(self, key, msg, level="", also_print=False):
         if key not in self.log:
@@ -27,15 +18,26 @@ def add(self, key, msg, level="", also_print=False):
         if also_print:
             print(f"{key} [{level}]: {msg}")
 
-    def get_log_keys(self):
-        return list(self.log.keys())
-
     def get_log(self, key):
         if key in self.log:
             return self.log[key]
         else:
             return []
 
+    def get_log_keys(self):
+        return list(self.log.keys())
+
+    def get_log_string(self, level=None):
+        """ Return the log as a string, with entries separated by newlines. """
+        log_lines = [f"{self.name}: Level {str(level)}"]
+        for key, item in self.log.items():
+            log_lines.append(f"{key}:")
+            if item:
+                for entry in item:
+                    if not level or (entry["level"] == level):
+                        log_lines.append(f"\t[{entry['level']} {entry['msg']}]")
+        return "\n".join(log_lines)
+
     def load_log(self, root_path, sub_path='code', log_name='hed_log.json'):
         filename = os.path.realpath(os.path.join(os.path.join(root_path, sub_path), log_name))
         if os.path.isfile(filename):
diff --git a/hed/util/__init__.py b/hed/util/__init__.py
index f3e648149..4a1c47d21 100644
--- a/hed/util/__init__.py
+++ b/hed/util/__init__.py
@@ -2,4 +2,4 @@
 
 from .data_util import get_new_dataframe, get_value_dict, replace_values
 from .file_util import get_file_extension
-from .io_util import generate_filename, extract_suffix_path, get_file_list, make_path
+from .io_util import check_filename, generate_filename, extract_suffix_path, get_file_list, make_path
diff --git a/hed/util/data_util.py b/hed/util/data_util.py
index 35eab0934..3b4123b39 100644
--- a/hed/util/data_util.py
+++ b/hed/util/data_util.py
@@ -6,12 +6,12 @@
 
 
 def add_columns(df, column_list, value='n/a'):
-    """ Add specified columns to df if df they are not already there
+    """ Add specified columns to df if they are not already there.
 
     Args:
-        df (DataFrame):      Pandas dataframe
-        column_list (list):  List of columns
-        value (str):         Default value to place in string
+        df (DataFrame):      Pandas dataframe.
+        column_list (list):  List of columns to append to the dataframe.
+        value (str):         Default fill value for the column.
 
     """
 
@@ -21,15 +21,15 @@ def add_columns(df, column_list, value='n/a'):
 
 
 def check_match(ds1, ds2, numeric=False):
-    """ Check that the two series match
+    """ Check that two Pandas data series have the same values.
 
     Args:
-        ds1 (DataSeries):      Pandas data series
-        ds2 (DataSeries):      Pandas data series
-        numeric (bool):        If true, treat as numeric and do close-to comparison
+        ds1 (DataSeries):      Pandas data series to check.
+        ds2 (DataSeries):      Pandas data series to check.
+        numeric (bool):        If true, treat as numeric and do close-to comparison.
 
     Returns:
-        list                   List of error messages or empty if match
+        list: Error messages indicating the mismatch or empty if the series match.
 
     """
 
@@ -42,17 +42,20 @@ def check_match(ds1, ds2, numeric=False):
             return f"Series differ at positions {list(ds1.loc[np.logical_not(close_test)].index)}"
     else:
         unequal = ds1.map(str) != ds2.map(str)
-        if sum(unequal):
+        if sum(unequal) > 0:
             return f"Series differ at positions {list(ds1.loc[unequal].index)}"
     return []
 
 
 def delete_columns(df, column_list):
-    """ Delete specified columns from df if df has
+    """ Delete the specified columns from a dataframe.
 
     Args:
-        df (DataFrame):      Pandas dataframe
-        column_list (list):  List of columns
+        df (DataFrame):      Pandas dataframe from which to delete columns.
+        column_list (list):  List of candidate column names for deletion.
+
+    Notes: The deletion of columns is done in place.
+           This does not raise an error if df does not have a column in the list.
 
     """
 
@@ -61,34 +64,34 @@ def delete_columns(df, column_list):
 
 
 def delete_rows_by_column(df, value, column_list=None):
-    """ Delete rows in which the specified column of df has particular values
+    """ Delete rows in which the specified columns of a dataframe has a particular value.
 
     Args:
-        df (DataFrame):      Pandas dataframe
-        value (str):         Specified value
-        column_list (list):  List of columns to search for value
+        df (DataFrame):      Pandas dataframe from which to delete rows.
+        value (str):         Specified value to indicate row should be deleted.
+        column_list (list):  List of columns to search for value.
 
-    Note: all values are convert to string before test.
+    Notes: All values are converted to string before testing.
+           Deletion is done in place.
     """
     if column_list:
         cols = list(set(column_list).intersection(set(list(df))))
     else:
         cols = list(df)
 
-    # Remove boundary type events
     for col in cols:
         map_col = df[col].map(str) == str(value)
         df.drop(df[map_col].index, axis=0, inplace=True)
 
 
 def get_key_hash(key_tuple):
-    """ Calculates the key_hash for key_tuple. If the key_hash in map_dict, also return the key value.
+    """ Calculate a hash key for the indicated tuple of values.
 
     Args:
-        key_tuple (tuple):       A tuple with the key values in the correct order for lookup
+        key_tuple (tuple, list):  The key values in the correct order for lookup.
 
     Returns:
-        key_hash (int)              Hash key for the tuple
+        int:  A hash key for the tuple.
 
     """
 
@@ -96,13 +99,17 @@ def get_key_hash(key_tuple):
 
 
 def get_new_dataframe(data):
-    """ Returns a new dataframe representing an event file or template
+    """ Get a new dataframe representing a tsv file.
 
     Args:
-        data (DataFrame or str):      DataFrame or filename representing an events file
+        data (DataFrame or str):  DataFrame or filename representing a tsv file.
 
     Returns:
-        DataFrame containing with a tsv file
+        DataFrame:  A dataframe containing the contents of the tsv file or if data was
+             a DataFrame to start with, a new copy of the DataFrame.
+
+    Raises:
+        HedFileError: If a filename is given and it cannot be read into a Dataframe.
 
     """
 
@@ -116,26 +123,43 @@ def get_new_dataframe(data):
 
 
 def get_row_hash(row, key_list):
+    """ Get a hash key from the values in row corresponding to the column names in a list of keys.
+
+    Args:
+        row (DataSeries)   A Pandas data series corresponding to a row in a spreadsheet.
+        key_list (list)    List of column names to create the hash value from.
+
+    Returns:
+        str: Hash key constructed from the entries of row in the columns specified by key_list.
+
+    Raises:
+        HedFileError: If row doesn't have all of the columns in key_list HedFileError is raised.
+
+    """
     columns_present, columns_missing = separate_columns(list(row.index.values), key_list)
     if columns_missing:
         raise HedFileError("lookup_row", f"row must have all keys, missing{str(columns_missing)}", "")
     return get_key_hash(row[key_list])
 
 
-def get_value_dict(srate_path, key_col='file_basename', value_col='sampling_rate'):
-    """ Return a dictionary of values from two columns a dataframe
+def get_value_dict(tsv_path, key_col='file_basename', value_col='sampling_rate'):
+    """ Get a dictionary created from two columns of a dataframe.
 
     Args:
-        srate_path (str)   Path to a tsv file with a header row
-        key_col (str)      Name of the column which should be the key
-        value_col (str)    Name of the column which should be the value
+        tsv_path (str)   Path to a tsv file with a header row to be read into a DataFrame.
+        key_col (str)    Name of the column which should be the key.
+        value_col (str)  Name of the column which should be the value.
 
-    Returns: (dict)
-        Dictionary with key_col: value_col
+    Returns:
+        dict:  Dictionary with key_col values as the keys and the corresponding
+               value_col values as the values.
+
+    Raises:
+        HedFileError: When tsv_path does not correspond to a file that can be read into a DataFrame.
     """
 
     value_dict = {}
-    df = get_new_dataframe(srate_path)
+    df = get_new_dataframe(tsv_path)
     for index, row in df.iterrows():
         if row[key_col] in value_dict:
             raise HedFileError("DuplicateKeyInValueDict", "The key column must have unique values", "")
@@ -144,14 +168,16 @@ def get_value_dict(srate_path, key_col='file_basename', value_col='sampling_rate
 
 
 def make_info_dataframe(col_info, selected_col):
-    """ Return a dataframe containing the column information for the selected column
+    """ Get a dataframe containing the column information from a dictionary for the selected column.
 
     Args:
-        col_info (dict):      Dictionary of column values and counts
-        selected_col (str):   Name of the column
+        col_info (dict):      Dictionary of dictionaries of column values and counts.
+        selected_col (str):   Name of the column used as top level key for col_info.
 
     Returns:
-        dataframe:  A dictionary of simplified, path-independent key names and full paths values.
+        dataframe:  A two-column dataframe with first column containing values from the
+           dictionary whose key is selected_col and whose second column are the corresponding counts.
+           The returned value is None if selected_col is not a top-level key in col_info.
     """
     col_dict = col_info.get(selected_col, None)
     if not col_dict:
@@ -161,17 +187,17 @@ def make_info_dataframe(col_info, selected_col):
     return df
 
 
-def replace_values(df, values=[''], replace_value='n/a', column_list=None):
-    """ Replace specified string values in specified columns of df with replace_value
+def replace_values(df, values=None, replace_value='n/a', column_list=None):
+    """ Replace specified string values in specified columns of a dataframe with indicated value.
 
     Args:
-        df (DataFrame):      Pandas dataframe
-        values (list):       List of strings to replace
-        replace_value (str): String replacement value
-        column_list (list):  List of columns in which to do replacement
+        df (DataFrame):            Dataframe whose values will replaced.
+        values (list, None):       List of strings to replace. If None, only empty strings are replaced.
+        replace_value (str):       String replacement value.
+        column_list (list, None):  List of columns in which to do replacement. If None all columns are processed.
 
-    Returns: (int)
-        number of values replaced
+    Returns:
+        int: number of values replaced.
     """
 
     num_replaced = 0
@@ -179,20 +205,25 @@ def replace_values(df, values=[''], replace_value='n/a', column_list=None):
         cols = list(set(column_list).intersection(set(list(df))))
     else:
         cols = list(df)
+    if not values:
+        values = ['']
     for col in cols:
         for value in values:
             value_mask = df[col].map(str) == str(value)
             num_replaced += sum(value_mask)
             index = df[value_mask].index
-            df.loc[index, col] = 'n/a'
+            df.loc[index, col] = replace_value
     return num_replaced
 
 
 def remove_quotes(df):
-    """ Remove quotes from the entries of the specified columns in a dataframe or from all columns if no list provided.
+    """ Remove quotes from the entries for all columns of dataframe of type string or object.
 
     Args:
-        df (Dataframe):             Dataframe to process by removing specified quotes
+        df (Dataframe):   Dataframe to process by removing quotes.
+
+    Notes:
+        Replacement is done in place.
     """
 
     col_types = df.dtypes
@@ -203,15 +234,18 @@ def remove_quotes(df):
 
 
 def reorder_columns(data, col_order, skip_missing=True):
-    """ Takes a dataframe or filename representing event file and reorders columns to desired order
-
+    """ Create a new dataframe with columns of data reordered.
     Args:
-        data (DataFrame, str) :        Represents mapping
-        col_order (list):              List of column names for desired order
-        skip_missing (bool):           If true, col_order columns missing from data are skipped, otherwise error
+        data (DataFrame, str) :        Dataframe or filename of dataframe whose columns are to be reordered.
+        col_order (list):              List of column names in desired order.
+        skip_missing (bool):           If true, col_order columns missing from data are skipped, otherwise error.
 
     Returns:
-        DataFrame                      A new reordered dataframe
+        DataFrame                      A new reordered dataframe.
+
+    Raises:
+        HedFileError:  If col_order contains columns not in data and skip_missing is False or if
+            data corresponds to a filename from which a dataframe cannot be created.
     """
     df = get_new_dataframe(data)
     present_cols, missing_cols = separate_columns(df.columns.values.tolist(), col_order)
@@ -222,17 +256,18 @@ def reorder_columns(data, col_order, skip_missing=True):
 
 
 def separate_columns(base_cols, target_cols):
-    """ Takes a list of column names and a list of target columns and returns list of present and missing targets.
-
-    Computes the set difference of target_cols and base_cols and returns a list of columns of
-    target_cols that are in df and a list of those missing.
+    """ Get a list of target columns that are present and missing from the base list.
 
      Args:
-         base_cols (list) :        List of columns in base object
-         target_cols (list):       List of desired column names
+         base_cols (list) :        List of columns to be tested.
+         target_cols (list):       List of desired column names.
+
+     Returns: (list, list):
+         list:  Target columns present in base_cols.
+         list:  Target columns missing from base_cols.
 
-     Returns:
-         tuple (list, list):            Returns two lists one with
+     Notes:  The function computes the set difference of target_cols and base_cols and returns a list
+         of columns of target_cols that are in base_cols and a list of those missing.
      """
 
     if not target_cols:
diff --git a/hed/util/file_util.py b/hed/util/file_util.py
index 220acf3fc..700f48e48 100644
--- a/hed/util/file_util.py
+++ b/hed/util/file_util.py
@@ -1,4 +1,4 @@
-"""Utilities for writing content to files and other file manipulation."""
+"""Utilities for writing content to files and for other file manipulation."""
 
 import tempfile
 import os
@@ -10,17 +10,13 @@
 
 
 def delete_file_if_it_exists(file_path):
-    """Deletes a file if it exist.
+    """Deletes a file if it exists.
 
-    Parameters
-    ----------
-    file_path: str
-        The path the file.
+    Args:
+        file_path (str):    Path of the file to be deleted.
 
-    Returns
-    -------
-    bool
-        True if the file exists and was deleted.
+    Returns:
+        bool:  True if the file exists and was deleted.
     """
     if file_path is None:
         return False
@@ -31,34 +27,26 @@ def delete_file_if_it_exists(file_path):
     return False
 
 
-def get_file_extension(file_name_or_path):
-    """Get the other extension from the specified filename. This can be the full path or just the name.
+def get_file_extension(file):
+    """Get the extension from the specified filename. This can be the full path or just the name.
 
-       Parameters
-       ----------
-       file_name_or_path: str
-           The name or full path of a file.
+        Args:
+            file (str):    Full path or filename with extension of a file.
 
-       Returns
-       -------
-       str
-           The extension of the other.
+        Returns:
+            str: The extension if there is one.
        """
-    return os.path.splitext(file_name_or_path)[1]
+    return os.path.splitext(file)[1]
 
 
 def get_version_from_xml(hed_xml_tree):
-    """Gets version info from root node if present
+    """Get version info from root node of an XML tree if present
 
-       Parameters
-       ----------
-       hed_xml_tree: Element
-           The root node of an XML tree.
+        Args:
+            hed_xml_tree (Element):  The root node of an XML tree.
 
-       Returns
-       -------
-        str
-            The version of the HED schema.
+        Returns:
+            str: The version of the HED schema (e.g. "8.0.0").
 
         TODO: This should be moved to the schema module
     """
@@ -73,17 +61,13 @@ def get_version_from_xml(hed_xml_tree):
 
 
 def url_to_file(resource_url):
-    """Write data from a URL resource into a file. Data is decoded as unicode.
+    """ Write data from a URL resource into a file. Data is decoded as unicode.
 
-    Parameters
-    ----------
-    resource_url: str
-        The URL to the resource.
+    Args:
+        resource_url (str): The URL to the resource.
 
-    Returns
-    -------
-    str
-        The local temporary filename for downloaded file
+    Returns:
+        str: The local temporary filename for the downloaded file,
     """
     url_request = urllib.request.urlopen(resource_url)
     suffix = get_file_extension(resource_url)
@@ -94,17 +78,13 @@ def url_to_file(resource_url):
 
 
 def url_to_string(resource_url):
-    """Get the data from the specified url as a string
+    """ Get the data from the specified url as a string.
 
-    Parameters
-    ----------
-    resource_url: str
-        The URL to the resource.
+    Args:
+        resource_url (str): The URL to the resource.
 
-    Returns
-    -------
-    str
-       The data at the target url
+    Returns:
+        str: The data at the target url.
     """
     url_request = urllib.request.urlopen(resource_url)
     url_data = str(url_request.read(), 'utf-8')
@@ -112,20 +92,14 @@ def url_to_string(resource_url):
 
 
 def write_errors_to_file(issues, extension=".txt"):
-    """
-    Writes an array of issue dictionaries to a temporary file.
-
-    Parameters
-    ----------
-    issues: list
-        List of 2-element dictionaries containing code and message keys.
-    extension: str
-        Desired file extension.
-
-    Returns
-    -------
-    str
-        The name of the temporary file
+    """ Write an array of issue dictionaries to a temporary file.
+
+    Args:
+        issues (list):    List of 2-element dictionaries containing code and message keys.
+        extension (str):  Desired file extension.
+
+    Returns:
+        str: The name of the temporary file.
     """
     with tempfile.NamedTemporaryFile(suffix=extension, mode='w', delete=False, encoding='utf-8') as error_file:
         for line in issues:
@@ -134,21 +108,14 @@ def write_errors_to_file(issues, extension=".txt"):
 
 
 def write_strings_to_file(output_strings, extension=None):
-    """
-    Writes a list of output strings to a temporary file and returns the open file.
+    """ Write a list of output strings to a temporary file and return the open file.
 
-    Parameters
-    ----------
-    output_strings: [str] or str
-        List of strings to output one per line.
-    extension: str
-        Desired file extension.
-
-    Returns
-    -------
-        file
-            Opened temporary file
+    Args:
+        output_strings ([str], str):  Strings to output one per line.
+        extension (str):              File extension of the temporary file.
 
+    Returns:
+        file: Opened temporary file.
 
     """
     if isinstance(output_strings, str):
@@ -160,40 +127,17 @@ def write_strings_to_file(output_strings, extension=None):
         return opened_file.name
 
 
-def write_text_iter_to_file(text_iter, extension=".txt"):
-    """
-    Writes a text file based on an iterator.
-
-    Parameters
-    ----------
-    text_iter: text iterator
-        Iterates over values and writes to temporary file one per line.  Adds newlines.
-    extension: str
-        Desired file extension.
-
-    Returns
-    -------
-        str
-            Name of the temporary file.
-    """
-    with tempfile.NamedTemporaryFile(suffix=extension, mode='w', delete=False, encoding='utf-8') as hed_xml_file:
-        for line in text_iter:
-            hed_xml_file.write(f"{line}\n")
-        return hed_xml_file.name
-
-
 def write_xml_tree_2_xml_file(xml_tree, extension=".xml"):
-    """Writes a XML element tree object into a XML file.
+    """ Write an XML element tree object into a XML file.
+
+    Args:
+        xml_tree (Element):  An element representing an XML file.
+        extension (string):  The file extension to use for the temporary file.
 
-    Parameters
-    ----------
-    xml_tree: Element
-        A element representing an XML file.
-    extension: string
-        The file extension to use for temporary file.
+    Returns:
+        str:  Name of the temporary file.
 
-    Returns
-    -------
+    TODO:  Should this be in the schema module?
 
     """
     with tempfile.NamedTemporaryFile(suffix=extension, mode='w', delete=False, encoding='utf-8') as hed_xml_file:
@@ -203,17 +147,15 @@ def write_xml_tree_2_xml_file(xml_tree, extension=".xml"):
 
 
 def _xml_element_2_str(elem):
-    """Converts an XML element to an XML string.
+    """ Convert an XML element to an XML string.
+
+    Args:
+        elem (Element):  An XML element.
 
-    Parameters
-    ----------
-    elem: Element
-        An XML element.
+    Returns:
+        str: An XML string representing the XML element.
 
-    Returns
-    -------
-    str
-        A XML string representing the XML element.
+    TODO: Shouldn't this be with the schema?
 
     """
     rough_string = ElementTree.tostring(elem, method='xml')
diff --git a/hed/util/io_util.py b/hed/util/io_util.py
index 9423f8dc6..fb41e562c 100644
--- a/hed/util/io_util.py
+++ b/hed/util/io_util.py
@@ -6,40 +6,44 @@
 
 
 def check_filename(test_file, name_prefix=None, name_suffix=None, extensions=None):
-    """ Determines whether test_file has correct extension, name_suffix, and name_prefix.
+    """ Determine whether test_file has correct extension, name suffix, and name prefix.
 
     Everything is converted to lower case prior to testing so this test should be case insensitive.
 
      Args:
-         test_file (str) :           Path of filename to test
-         name_prefix (str):          An optional name_prefix for the base filename
-         name_suffix (str):          An optional name_suffix for the base file name
-         extensions (list):     An optional list of file extensions
+         test_file (str) :           Path of filename to test.
+         name_prefix (str, None):     An optional name_prefix for the base filename
+         name_suffix (str, None):     An optional name_suffix for the base file name
+         extensions (list, None):     An optional list of file extensions
 
      Returns:
          tuple (list, list):            Returns two lists one with
      """
 
-    file_split = os.path.splitext(test_file.lower())
-    is_name = True
-    if extensions and file_split[1] not in [x.lower() for x in extensions]:
-        is_name = False
-    elif name_suffix and not file_split[0].endswith(name_suffix.lower()):
-        is_name = False
-    elif name_prefix and not file_split[0].startswith(name_prefix.lower()):
-        is_name = False
-    return is_name
+    basename = os.path.basename(test_file.lower())
+    if name_prefix and not basename.startswith(name_prefix.lower()):
+        return False
+    if extensions:
+        ext = list(filter(basename.endswith, extensions))
+        if not ext:
+            return False
+        basename = basename[:-len(ext[0])]
+    else:
+        basename = os.path.splitext(basename)[0]
+    if name_suffix and not basename.endswith(name_suffix.lower()):
+        return False
+    return True
 
 
 def extract_suffix_path(path, prefix_path):
     """ Return the suffix path of path after prefix path has been removed
 
     Args:
-        path (str)   path of the root directory
-        prefix_path (str)    sub-path relative to the root directory
+        path (str)           path of the root directory.
+        prefix_path (str)    sub-path relative to the root directory.
 
-    Returns: (str)
-        Suffix path.
+    Returns:
+        str:   Suffix path.
 
     Notes: This function is useful for creating files within BIDS datasets
 
@@ -54,20 +58,16 @@ def extract_suffix_path(path, prefix_path):
 
 
 def generate_filename(base_name, name_prefix=None, name_suffix=None, extension=None):
-    """Generates a filename for the attachment of the form prefix_basename_suffix + extension.
-
-    Parameters
-    ----------
-   base_name: str
-        The name of the base, usually the name of the file that the issues were generated from
-    name_prefix: str
-        The name_prefix prepended to the front of the base_name
-    name_suffix: str
-        The name_suffix appended to the end of the base_name
-    Returns
-    -------
-    string
-        The name of the attachment other containing the issues.
+    """ Generate a filename for the attachment of the form prefix_basename_suffix + extension.
+
+    Args:
+        base_name (str):   Name of the base, usually the name of the file that the issues were generated from.
+        name_prefix (str): Prefix prepended to the front of the base name.
+        name_suffix (str): Suffix appended to the end of the base name.
+        extension (str):   Extension to use.
+
+    Returns:
+        str:  Name of the attachment other containing the issues.
     """
 
     pieces = []
@@ -77,19 +77,14 @@ def generate_filename(base_name, name_prefix=None, name_suffix=None, extension=N
         pieces.append(os.path.splitext(base_name)[0])
     if name_suffix:
         pieces = pieces + [name_suffix]
-
-    if not pieces:
-        return ''
-    filename = pieces[0]
-    for name in pieces[1:]:
-        filename = filename + name
-    if extension:
+    filename = "".join(pieces)
+    if filename and extension:
         filename = filename + extension
     return secure_filename(filename)
 
 
 def get_dir_dictionary(dir_path, name_prefix=None, name_suffix=None, extensions=None, skip_empty=True):
-    """ Traverses a directory tree and creates dictionary with keys that are directory paths a.
+    """ Traverse a directory tree and create dictionary with keys that are directory paths.
 
     Args:
         dir_path (str):               Full path of the directory tree to be traversed (no ending slash)
@@ -114,58 +109,54 @@ def get_dir_dictionary(dir_path, name_prefix=None, name_suffix=None, extensions=
 
 
 def get_filtered_list(file_list, name_prefix=None, name_suffix=None, extensions=None):
-    """ Returns a new list with the filenames in file_list satisfying the criteria
+    """ Get a new list with the filenames in file_list satisfying the criteria
 
     Everything is converted to lower case prior to testing so this test should be case insensitive.
 
      Args:
-         file_list (list):           List of files to test
-         name_prefix (str):          An optional name_prefix for the base filename
-         name_suffix (str):          An optional name_suffix for the base filename
-         extensions (list):     An optional list of file extensions
+         file_list (list):      List of files to test.
+         name_prefix (str):     Optional name_prefix for the base filename.
+         name_suffix (str):     Optional name_suffix for the base filename.
+         extensions (list):     Optional list of file extensions (allows two periods (.tsv.gz)
 
      Returns:
-         tuple (list, list):            Returns two lists one with
+         list:  A list of filtered file names
      """
     filtered_files = []
     for r_file in file_list:
-        file_split = os.path.splitext(r_file.lower())
-        if extensions and file_split[1] not in [x.lower() for x in extensions]:
-            continue
-        elif name_suffix and not file_split[0].endswith(name_suffix.lower()):
-            continue
-        elif name_prefix and not file_split[0].startswith(name_prefix.lower()):
-            continue
-        filtered_files.append(r_file)
+        if check_filename(r_file, name_prefix=name_prefix, name_suffix=name_suffix, extensions=extensions):
+            filtered_files.append(r_file)
     return filtered_files
 
 
-def get_file_list(root_path, name_prefix=None, name_suffix=None, extensions=None, exclude_dirs=[]):
-    """ Traverses a directory tree and returns a list of paths to files ending with a particular name_suffix.
+def get_file_list(root_path, name_prefix=None, name_suffix=None, extensions=None, exclude_dirs=None):
+    """ Traverse a directory tree and return a list of paths of files satisfying various conditions.
 
     Args:
-        root_path (str):              Full path of the directory tree to be traversed (no ending slash)
-        name_prefix (str, None):     An optional name_prefix for the base filename
-        name_suffix (str, None):     The name_suffix of the paths to be extracted
-        extensions (list, None):     A list of extensions to be selected
-        exclude_dirs (list, None):    A list of paths to be excluded
+        root_path (str):              Full path of the directory tree to be traversed (no ending slash).
+        name_prefix (str, None):      An optional name_prefix for the base filename.
+        name_suffix (str, None):      The name_suffix of the paths to be extracted.
+        extensions (list, None):      A list of extensions to be selected.
+        exclude_dirs (list, None):    A list of paths to be excluded.
 
     Returns:
-        list:             A list of full paths
+        list:  A list of full paths.
     """
     file_list = []
+    if not exclude_dirs:
+        exclude_dirs = []
     for root, dirs, files in os.walk(root_path, topdown=True):
         dirs[:] = [d for d in dirs if d not in exclude_dirs]
         for r_file in files:
             if check_filename(r_file, name_prefix, name_suffix, extensions):
-                file_list.append(os.path.join(os.path.realpath(root), r_file))
+                file_list.append(os.path.realpath(os.path.join(root, r_file)))
     return file_list
 
 
 def get_path_components(this_path, root_path):
-    """ Return a list with root_path and remaining components
+    """ Get a list with root_path and remaining components.
+
 
-    this_path must be a descendant of root_path
 
     Args:
         this_path (str):      The path of a file or directory descendant of root_path
@@ -173,7 +164,10 @@ def get_path_components(this_path, root_path):
 
     Returns:
         list or None:         A list with the first element being root_path and the
-                              remaining elements directory components to the file.
+            remaining elements directory components to the file.
+
+    Notes: this_path must be a descendant of root_path.
+
     """
 
     base_path = os.path.realpath(root_path)
@@ -190,7 +184,7 @@ def get_path_components(this_path, root_path):
 
 
 def make_path(root_path, sub_path, filename):
-    """ Return the complete path for a file, making sure all path components exist.
+    """ Get the complete path for a file, making sure all path components exist.
 
     Args:
         root_path (str)   path of the root directory
@@ -210,17 +204,18 @@ def make_path(root_path, sub_path, filename):
 
 
 def parse_bids_filename(file_path):
-    """ Split a filename into its BIDS suffix, extension, and entity_dict
+    """ Split a filename into its BIDS suffix, extension, and a dictionary of entity name-value pairs.
 
         Args:
-            file_path (str)     Path to be parsed
+            file_path (str)     Path to be parsed.
 
         Returns: dict
-            suffix (str)        BIDS suffix name
-            ext (str)           File extension (including the .)
-            entity_dict (dict)  Dictionary with key-value pair being (entity type, entity value)
+            suffix (str)        BIDS suffix name.
+            ext (str)           File extension (including the .).
+            entity_dict (dict)  Dictionary with key-value pair being (entity type, entity value).
 
-        Raises HedFileError when filename does not conform to name-value_suffix format.
+        Raises:
+            HedFileError when filename does not conform to name-value_suffix format.
 
     """
 
@@ -234,7 +229,8 @@ def parse_bids_filename(file_path):
     entity_pieces = basename.split('_')
     split_dict = _split_entity(entity_pieces[-1])
     if "bad" in split_dict:
-        raise HedFileError("BadSuffixPiece", f"The basename for {entity_pieces[-1]} has bad {ent_dict['bad']}", "")
+        raise HedFileError("BadSuffixPiece",
+                           f"The basename for {entity_pieces[-1]} has bad {split_dict['bad']}", "")
     if "suffix" in split_dict:
         suffix = split_dict["suffix"]
     else:
@@ -249,9 +245,13 @@ def parse_bids_filename(file_path):
 
 
 def _split_entity(piece):
-    """Splits an piece into an entity or suffix
+    """Splits an piece into an entity or suffix.
 
-        Returns: dict
+        Args:
+            piece (str):   A string to be parsed.
+
+        Returns:
+            dict:  with entities as keys as well as the key "bad" and the key "suffix".
     """
     piece = piece.strip()
     if not piece:
diff --git a/hed/validator/hed_validator.py b/hed/validator/hed_validator.py
index b88b6e489..13b72ef44 100644
--- a/hed/validator/hed_validator.py
+++ b/hed/validator/hed_validator.py
@@ -87,11 +87,12 @@ def _validate_groups_in_hed_string(self, hed_string_obj):
 
     def _check_for_duplicate_groups(self, original_group):
         # Todo: This could be optimized in various ways.  It repeatedly converts subgroups to HedGroupFrozen
+        # This could also be accomplished by sorting the entire hed string.
         frozen_group_or_string = original_group.get_frozen()
-        if len(frozen_group_or_string._children) != len(original_group._children):
+        if len(frozen_group_or_string._children) != len(original_group.children):
             validation_issues = []
             valid_content = set(frozen_group_or_string._children)
-            for child in original_group._children:
+            for child in original_group.children:
                 if isinstance(child, HedTag):
                     error_code = ValidationErrors.HED_TAG_REPEATED
                     frozen_child = child
@@ -138,9 +139,9 @@ def _validate_individual_tags_in_hed_string(self, hed_string_obj, allow_placehol
              The issues associated with the individual tags in the HED string.
 
          """
-        from hed.models.def_dict import DefTagNames
+        from hed.models.definition_dict import DefTagNames
         validation_issues = []
-        def_groups = hed_string_obj.find_top_level_tags(anchors={DefTagNames.DEFINITION_KEY}, include_groups=1)
+        def_groups = hed_string_obj.find_top_level_tags(anchor_tags={DefTagNames.DEFINITION_KEY}, include_groups=1)
         all_def_groups = [group for sub_group in def_groups for group in sub_group.get_all_groups()]
         for group in hed_string_obj.get_all_groups():
             is_definition = group in all_def_groups
diff --git a/hed/validator/tag_validator.py b/hed/validator/tag_validator.py
index 67f3778a7..832e69949 100644
--- a/hed/validator/tag_validator.py
+++ b/hed/validator/tag_validator.py
@@ -527,34 +527,32 @@ def check_for_required_tags(self, tags):
             A validation issues list. If no issues are found then an empty list is returned.
         """
         validation_issues = []
-        required_tag_prefixes = self._hed_schema.get_all_tags_with_attribute(HedKey.Required)
-        for capitalized_required_tag_prefix in required_tag_prefixes:
-            required_tag_prefix = capitalized_required_tag_prefix.lower()
-            if sum([x.lower().startswith(required_tag_prefix) for x in tags]) < 1:
+        required_prefixes = self._hed_schema.get_tags_with_attribute(HedKey.Required)
+        for required_prefix in required_prefixes:
+            if not any(tag.long_tag.lower().startswith(required_prefix.lower()) for tag in tags):
                 validation_issues += ErrorHandler.format_error(ValidationErrors.HED_REQUIRED_TAG_MISSING,
-                                                               tag_prefix=capitalized_required_tag_prefix)
+                                                               tag_prefix=required_prefix)
         return validation_issues
 
-    def check_multiple_unique_tags_exist(self, original_tag_list):
+    def check_multiple_unique_tags_exist(self, tags):
         """Reports a validation error if two or more tags start with a tag name_prefix that has the 'unique' attribute.
 
         Parameters
         ----------
-        original_tag_list: [HedTag]
-            A list containing tags that are used to report the error.
+        tags: [HedTag]
+            A list containing the tags.
         Returns
         -------
         []
             A validation issues list. If no issues are found then an empty list is returned.
         """
         validation_issues = []
-        unique_tag_prefixes = self._hed_schema.get_all_tags_with_attribute(HedKey.Unique)
-        for capitalized_unique_tag_prefix in unique_tag_prefixes:
-            unique_tag_prefix = capitalized_unique_tag_prefix.lower()
-            unique_tag_prefix_bool_mask = [x.lower().startswith(unique_tag_prefix) for x in original_tag_list]
+        unique_prefixes = self._hed_schema.get_tags_with_attribute(HedKey.Unique)
+        for unique_prefix in unique_prefixes:
+            unique_tag_prefix_bool_mask = [x.long_tag.lower().startswith(unique_prefix.lower()) for x in tags]
             if sum(unique_tag_prefix_bool_mask) > 1:
                 validation_issues += ErrorHandler.format_error(ValidationErrors.HED_TAG_NOT_UNIQUE,
-                                                               tag_prefix=capitalized_unique_tag_prefix)
+                                                               tag_prefix=unique_prefix)
         return validation_issues
 
     # ==========================================================================
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..8bc13baf6
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=42", "versioneer-518"]
+build-backend = "setuptools.build_meta"
diff --git a/readthedocs.yml b/readthedocs.yml
index 74423ca2e..273b977cc 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -13,9 +13,9 @@ sphinx:
   configuration: docs/source/conf.py
   fail_on_warning: false
 
-# Optionally set the version of Python and requirements required to _build your docs
+
 python:
-  version: "3.7"
+  version: 3.7
   install:
-    - requirements: docs/requirements.txt
+   - requirements: docs/requirements.txt
   system_packages: true
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 01c33ff1c..be3b25668 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,13 @@
 defusedxml==0.7.1
 et-xmlfile==1.1.0
-inflect==5.4.0
+inflect==5.5.1
 jdcal==1.4.1
-numpy==1.21.5
+numpy==1.21.6
 openpyxl==3.0.9
-pandas==1.3.4
-portalocker==2.3.2
+pandas==1.3.5
+portalocker==2.4.0
 python-dateutil==2.8.2
-pytz==2021.3
+pytz==2022.1
 semantic-version==2.9.0
 six==1.16.0
-Werkzeug==2.0.3
+Werkzeug==2.1.2
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 000000000..0e334ca31
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,50 @@
+[metadata]
+name = hedtools
+author = VisLab, Ian Callanan, Jeremy Cockfield, Alexander Jones, Owen Winterberg, Kay Robbins
+author_email = Kay.Robbins@utsa.edu
+description = Utilities for converting among different representations of the HED
+              (Hierarchical Event Descriptor) specification.
+long_description = file: README.md
+long_description_content_type = text/markdown
+url = https://github.com/hed-standard/hed-python/
+# project_urls =
+#     Bug Tracker = https://github.com/pypa/sampleproject/issues
+classifiers =
+    Programming Language :: Python :: 3
+    License :: OSI Approved :: MIT License
+    Operating System :: OS Independent
+
+
+
+[options]
+package_dir =
+    = .
+packages = find_namespace:
+python_requires = >=3.7
+install_requires =
+    openpyxl
+    defusedxml
+    et-xmlfile
+    inflect
+    jdcal
+    numpy
+    openpyxl
+    pandas
+    portalocker
+    python-dateutil
+    pytz
+    semantic-version
+    six
+    Werkzeug
+
+
+[options.packages.find]
+include = hed*
+
+[versioneer]
+VCS = git
+style = pep440
+versionfile_source = hed/_version.py
+versionfile_build = hed/_version.py
+tag_prefix =
+parentdir_prefix = hedtools-
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 80174a593..2341aa921 100644
--- a/setup.py
+++ b/setup.py
@@ -1,23 +1,8 @@
-import setuptools
-
-with open("README.md", "r") as fh:
-    long_description = fh.read()
-
-setuptools.setup(
-    name="hedtools",
-    version="0.0.1",
-    author="VisLab, Ian Callanan, Jeremy Cockfield, Alexander Jones, Owen Winterberg, Kay Robbins",
-    author_email="Kay.Robbins@utsa.edu",
-    description="Utilities for converting among different representations of the HED " +
-                "(Hierarchical Event Descriptor) specification.",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    url="https://github.com/hed-standard/hed-python/",
-    packages=setuptools.find_namespace_packages(include=["hed.*"]),
-    classifiers=[
-        "Programming Language :: Python :: 3",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-    ],
-    python_requires='>=3.7',
-)
+# dummy setup file for versioneer
+
+import versioneer
+from setuptools import setup
+
+if __name__ == "__main__":
+    setup(version=versioneer.get_version(),
+          cmdclass=versioneer.get_cmdclass())
\ No newline at end of file
diff --git a/setup_temp.cfg b/setup_temp.cfg
deleted file mode 100644
index 670316ac6..000000000
--- a/setup_temp.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-# setup_temp.cfg
-
-[metadata]
-name = hedtools
-version = 0.0.1
-
-[options]
-packages = hed
diff --git a/versioneer.py b/versioneer.py
new file mode 100644
index 000000000..a142bf53e
--- /dev/null
+++ b/versioneer.py
@@ -0,0 +1,2140 @@
+
+# Version: 0.22
+
+"""The Versioneer - like a rocketeer, but for versions.
+
+The Versioneer
+==============
+
+* like a rocketeer, but for versions!
+* https://github.com/python-versioneer/python-versioneer
+* Brian Warner
+* License: Public Domain
+* Compatible with: Python 3.6, 3.7, 3.8, 3.9, 3.10 and pypy3
+* [![Latest Version][pypi-image]][pypi-url]
+* [![Build Status][travis-image]][travis-url]
+
+This is a tool for managing a recorded version number in distutils/setuptools-based
+python projects. The goal is to remove the tedious and error-prone "update
+the embedded version string" step from your release process. Making a new
+release should be as easy as recording a new tag in your version-control
+system, and maybe making new tarballs.
+
+
+## Quick Install
+
+* `pip install versioneer` to somewhere in your $PATH
+* add a `[versioneer]` section to your setup.cfg (see [Install](INSTALL.md))
+* run `versioneer install` in your source tree, commit the results
+* Verify version information with `python setup.py version`
+
+## Version Identifiers
+
+Source trees come from a variety of places:
+
+* a version-control system checkout (mostly used by developers)
+* a nightly tarball, produced by build automation
+* a snapshot tarball, produced by a web-based VCS browser, like github's
+  "tarball from tag" feature
+* a release tarball, produced by "setup.py sdist", distributed through PyPI
+
+Within each source tree, the version identifier (either a string or a number,
+this tool is format-agnostic) can come from a variety of places:
+
+* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows
+  about recent "tags" and an absolute revision-id
+* the name of the directory into which the tarball was unpacked
+* an expanded VCS keyword ($Id$, etc)
+* a `_version.py` created by some earlier build step
+
+For released software, the version identifier is closely related to a VCS
+tag. Some projects use tag names that include more than just the version
+string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool
+needs to strip the tag prefix to extract the version identifier. For
+unreleased software (between tags), the version identifier should provide
+enough information to help developers recreate the same tree, while also
+giving them an idea of roughly how old the tree is (after version 1.2, before
+version 1.3). Many VCS systems can report a description that captures this,
+for example `git describe --tags --dirty --always` reports things like
+"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the
+0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has
+uncommitted changes).
+
+The version identifier is used for multiple purposes:
+
+* to allow the module to self-identify its version: `myproject.__version__`
+* to choose a name and prefix for a 'setup.py sdist' tarball
+
+## Theory of Operation
+
+Versioneer works by adding a special `_version.py` file into your source
+tree, where your `__init__.py` can import it. This `_version.py` knows how to
+dynamically ask the VCS tool for version information at import time.
+
+`_version.py` also contains `$Revision$` markers, and the installation
+process marks `_version.py` to have this marker rewritten with a tag name
+during the `git archive` command. As a result, generated tarballs will
+contain enough information to get the proper version.
+
+To allow `setup.py` to compute a version too, a `versioneer.py` is added to
+the top level of your source tree, next to `setup.py` and the `setup.cfg`
+that configures it. This overrides several distutils/setuptools commands to
+compute the version when invoked, and changes `setup.py build` and `setup.py
+sdist` to replace `_version.py` with a small static file that contains just
+the generated version data.
+
+## Installation
+
+See [INSTALL.md](./INSTALL.md) for detailed installation instructions.
+
+## Version-String Flavors
+
+Code which uses Versioneer can learn about its version string at runtime by
+importing `_version` from your main `__init__.py` file and running the
+`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can
+import the top-level `versioneer.py` and run `get_versions()`.
+
+Both functions return a dictionary with different flavors of version
+information:
+
+* `['version']`: A condensed version string, rendered using the selected
+  style. This is the most commonly used value for the project's version
+  string. The default "pep440" style yields strings like `0.11`,
+  `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section
+  below for alternative styles.
+
+* `['full-revisionid']`: detailed revision identifier. For Git, this is the
+  full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac".
+
+* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the
+  commit date in ISO 8601 format. This will be None if the date is not
+  available.
+
+* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that
+  this is only accurate if run in a VCS checkout, otherwise it is likely to
+  be False or None
+
+* `['error']`: if the version string could not be computed, this will be set
+  to a string describing the problem, otherwise it will be None. It may be
+  useful to throw an exception in setup.py if this is set, to avoid e.g.
+  creating tarballs with a version string of "unknown".
+
+Some variants are more useful than others. Including `full-revisionid` in a
+bug report should allow developers to reconstruct the exact code being tested
+(or indicate the presence of local changes that should be shared with the
+developers). `version` is suitable for display in an "about" box or a CLI
+`--version` output: it can be easily compared against release notes and lists
+of bugs fixed in various releases.
+
+The installer adds the following text to your `__init__.py` to place a basic
+version in `YOURPROJECT.__version__`:
+
+    from ._version import get_versions
+    __version__ = get_versions()['version']
+    del get_versions
+
+## Styles
+
+The setup.cfg `style=` configuration controls how the VCS information is
+rendered into a version string.
+
+The default style, "pep440", produces a PEP440-compliant string, equal to the
+un-prefixed tag name for actual releases, and containing an additional "local
+version" section with more detail for in-between builds. For Git, this is
+TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags
+--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the
+tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and
+that this commit is two revisions ("+2") beyond the "0.11" tag. For released
+software (exactly equal to a known tag), the identifier will only contain the
+stripped tag, e.g. "0.11".
+
+Other styles are available. See [details.md](details.md) in the Versioneer
+source tree for descriptions.
+
+## Debugging
+
+Versioneer tries to avoid fatal errors: if something goes wrong, it will tend
+to return a version of "0+unknown". To investigate the problem, run `setup.py
+version`, which will run the version-lookup code in a verbose mode, and will
+display the full contents of `get_versions()` (including the `error` string,
+which may help identify what went wrong).
+
+## Known Limitations
+
+Some situations are known to cause problems for Versioneer. This details the
+most significant ones. More can be found on Github
+[issues page](https://github.com/python-versioneer/python-versioneer/issues).
+
+### Subprojects
+
+Versioneer has limited support for source trees in which `setup.py` is not in
+the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are
+two common reasons why `setup.py` might not be in the root:
+
+* Source trees which contain multiple subprojects, such as
+  [Buildbot](https://github.com/buildbot/buildbot), which contains both
+  "master" and "slave" subprojects, each with their own `setup.py`,
+  `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI
+  distributions (and upload multiple independently-installable tarballs).
+* Source trees whose main purpose is to contain a C library, but which also
+  provide bindings to Python (and perhaps other languages) in subdirectories.
+
+Versioneer will look for `.git` in parent directories, and most operations
+should get the right version string. However `pip` and `setuptools` have bugs
+and implementation details which frequently cause `pip install .` from a
+subproject directory to fail to find a correct version string (so it usually
+defaults to `0+unknown`).
+
+`pip install --editable .` should work correctly. `setup.py install` might
+work too.
+
+Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in
+some later version.
+
+[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking
+this issue. The discussion in
+[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the
+issue from the Versioneer side in more detail.
+[pip PR#3176](https://github.com/pypa/pip/pull/3176) and
+[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve
+pip to let Versioneer work correctly.
+
+Versioneer-0.16 and earlier only looked for a `.git` directory next to the
+`setup.cfg`, so subprojects were completely unsupported with those releases.
+
+### Editable installs with setuptools <= 18.5
+
+`setup.py develop` and `pip install --editable .` allow you to install a
+project into a virtualenv once, then continue editing the source code (and
+test) without re-installing after every change.
+
+"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a
+convenient way to specify executable scripts that should be installed along
+with the python package.
+
+These both work as expected when using modern setuptools. When using
+setuptools-18.5 or earlier, however, certain operations will cause
+`pkg_resources.DistributionNotFound` errors when running the entrypoint
+script, which must be resolved by re-installing the package. This happens
+when the install happens with one version, then the egg_info data is
+regenerated while a different version is checked out. Many setup.py commands
+cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into
+a different virtualenv), so this can be surprising.
+
+[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes
+this one, but upgrading to a newer version of setuptools should probably
+resolve it.
+
+
+## Updating Versioneer
+
+To upgrade your project to a new release of Versioneer, do the following:
+
+* install the new Versioneer (`pip install -U versioneer` or equivalent)
+* edit `setup.cfg`, if necessary, to include any new configuration settings
+  indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details.
+* re-run `versioneer install` in your source tree, to replace
+  `SRC/_version.py`
+* commit any changed files
+
+## Future Directions
+
+This tool is designed to make it easily extended to other version-control
+systems: all VCS-specific components are in separate directories like
+src/git/ . The top-level `versioneer.py` script is assembled from these
+components by running make-versioneer.py . In the future, make-versioneer.py
+will take a VCS name as an argument, and will construct a version of
+`versioneer.py` that is specific to the given VCS. It might also take the
+configuration arguments that are currently provided manually during
+installation by editing setup.py . Alternatively, it might go the other
+direction and include code from all supported VCS systems, reducing the
+number of intermediate scripts.
+
+## Similar projects
+
+* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time
+  dependency
+* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of
+  versioneer
+* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools
+  plugin
+
+## License
+
+To make Versioneer easier to embed, all its code is dedicated to the public
+domain. The `_version.py` that it creates is also in the public domain.
+Specifically, both are released under the Creative Commons "Public Domain
+Dedication" license (CC0-1.0), as described in
+https://creativecommons.org/publicdomain/zero/1.0/ .
+
+[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg
+[pypi-url]: https://pypi.python.org/pypi/versioneer/
+[travis-image]:
+https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg
+[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer
+
+"""
+# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring
+# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements
+# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error
+# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with
+# pylint:disable=attribute-defined-outside-init,too-many-arguments
+
+import configparser
+import errno
+import json
+import os
+import re
+import subprocess
+import sys
+from typing import Callable, Dict
+import functools
+
+
+class VersioneerConfig:
+    """Container for Versioneer configuration parameters."""
+
+
+def get_root():
+    """Get the project root directory.
+
+    We require that all commands are run from the project root, i.e. the
+    directory that contains setup.py, setup.cfg, and versioneer.py .
+    """
+    root = os.path.realpath(os.path.abspath(os.getcwd()))
+    setup_py = os.path.join(root, "setup.py")
+    versioneer_py = os.path.join(root, "versioneer.py")
+    if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
+        # allow 'python path/to/setup.py COMMAND'
+        root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0])))
+        setup_py = os.path.join(root, "setup.py")
+        versioneer_py = os.path.join(root, "versioneer.py")
+    if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
+        err = ("Versioneer was unable to run the project root directory. "
+               "Versioneer requires setup.py to be executed from "
+               "its immediate directory (like 'python setup.py COMMAND'), "
+               "or in a way that lets it use sys.argv[0] to find the root "
+               "(like 'python path/to/setup.py COMMAND').")
+        raise VersioneerBadRootError(err)
+    try:
+        # Certain runtime workflows (setup.py install/develop in a setuptools
+        # tree) execute all dependencies in a single python process, so
+        # "versioneer" may be imported multiple times, and python's shared
+        # module-import table will cache the first one. So we can't use
+        # os.path.dirname(__file__), as that will find whichever
+        # versioneer.py was first imported, even in later projects.
+        my_path = os.path.realpath(os.path.abspath(__file__))
+        me_dir = os.path.normcase(os.path.splitext(my_path)[0])
+        vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0])
+        if me_dir != vsr_dir:
+            print("Warning: build in %s is using versioneer.py from %s"
+                  % (os.path.dirname(my_path), versioneer_py))
+    except NameError:
+        pass
+    return root
+
+
+def get_config_from_root(root):
+    """Read the project setup.cfg file to determine Versioneer config."""
+    # This might raise OSError (if setup.cfg is missing), or
+    # configparser.NoSectionError (if it lacks a [versioneer] section), or
+    # configparser.NoOptionError (if it lacks "VCS="). See the docstring at
+    # the top of versioneer.py for instructions on writing your setup.cfg .
+    setup_cfg = os.path.join(root, "setup.cfg")
+    parser = configparser.ConfigParser()
+    with open(setup_cfg, "r") as cfg_file:
+        parser.read_file(cfg_file)
+    VCS = parser.get("versioneer", "VCS")  # mandatory
+
+    # Dict-like interface for non-mandatory entries
+    section = parser["versioneer"]
+
+    cfg = VersioneerConfig()
+    cfg.VCS = VCS
+    cfg.style = section.get("style", "")
+    cfg.versionfile_source = section.get("versionfile_source")
+    cfg.versionfile_build = section.get("versionfile_build")
+    cfg.tag_prefix = section.get("tag_prefix")
+    if cfg.tag_prefix in ("''", '""'):
+        cfg.tag_prefix = ""
+    cfg.parentdir_prefix = section.get("parentdir_prefix")
+    cfg.verbose = section.get("verbose")
+    return cfg
+
+
+class NotThisMethod(Exception):
+    """Exception raised if a method is not valid for the current scenario."""
+
+
+# these dictionaries contain VCS-specific tools
+LONG_VERSION_PY: Dict[str, str] = {}
+HANDLERS: Dict[str, Dict[str, Callable]] = {}
+
+
+def register_vcs_handler(vcs, method):  # decorator
+    """Create decorator to mark a method as the handler of a VCS."""
+    def decorate(f):
+        """Store f in HANDLERS[vcs][method]."""
+        HANDLERS.setdefault(vcs, {})[method] = f
+        return f
+    return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
+                env=None):
+    """Call the given command(s)."""
+    assert isinstance(commands, list)
+    process = None
+
+    popen_kwargs = {}
+    if sys.platform == "win32":
+        # This hides the console window if pythonw.exe is used
+        startupinfo = subprocess.STARTUPINFO()
+        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+        popen_kwargs["startupinfo"] = startupinfo
+
+    for command in commands:
+        try:
+            dispcmd = str([command] + args)
+            # remember shell=False, so use git.cmd on windows, not just git
+            process = subprocess.Popen([command] + args, cwd=cwd, env=env,
+                                       stdout=subprocess.PIPE,
+                                       stderr=(subprocess.PIPE if hide_stderr
+                                               else None), **popen_kwargs)
+            break
+        except OSError:
+            e = sys.exc_info()[1]
+            if e.errno == errno.ENOENT:
+                continue
+            if verbose:
+                print("unable to run %s" % dispcmd)
+                print(e)
+            return None, None
+    else:
+        if verbose:
+            print("unable to find command, tried %s" % (commands,))
+        return None, None
+    stdout = process.communicate()[0].strip().decode()
+    if process.returncode != 0:
+        if verbose:
+            print("unable to run %s (error)" % dispcmd)
+            print("stdout was %s" % stdout)
+        return None, process.returncode
+    return stdout, process.returncode
+
+
+LONG_VERSION_PY['git'] = r'''
+# This file helps to compute a version number in source trees obtained from
+# git-archive tarball (such as those provided by githubs download-from-tag
+# feature). Distribution tarballs (built by setup.py sdist) and build
+# directories (produced by setup.py build) will contain a much shorter file
+# that just contains the computed version number.
+
+# This file is released into the public domain. Generated by
+# versioneer-0.22 (https://github.com/python-versioneer/python-versioneer)
+
+"""Git implementation of _version.py."""
+
+import errno
+import os
+import re
+import subprocess
+import sys
+from typing import Callable, Dict
+import functools
+
+
+def get_keywords():
+    """Get the keywords needed to look up the version information."""
+    # these strings will be replaced by git during git-archive.
+    # setup.py/versioneer.py will grep for the variable names, so they must
+    # each be defined on a line of their own. _version.py will just call
+    # get_keywords().
+    git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s"
+    git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s"
+    git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s"
+    keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
+    return keywords
+
+
+class VersioneerConfig:
+    """Container for Versioneer configuration parameters."""
+
+
+def get_config():
+    """Create, populate and return the VersioneerConfig() object."""
+    # these strings are filled in when 'setup.py versioneer' creates
+    # _version.py
+    cfg = VersioneerConfig()
+    cfg.VCS = "git"
+    cfg.style = "%(STYLE)s"
+    cfg.tag_prefix = "%(TAG_PREFIX)s"
+    cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s"
+    cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s"
+    cfg.verbose = False
+    return cfg
+
+
+class NotThisMethod(Exception):
+    """Exception raised if a method is not valid for the current scenario."""
+
+
+LONG_VERSION_PY: Dict[str, str] = {}
+HANDLERS: Dict[str, Dict[str, Callable]] = {}
+
+
+def register_vcs_handler(vcs, method):  # decorator
+    """Create decorator to mark a method as the handler of a VCS."""
+    def decorate(f):
+        """Store f in HANDLERS[vcs][method]."""
+        if vcs not in HANDLERS:
+            HANDLERS[vcs] = {}
+        HANDLERS[vcs][method] = f
+        return f
+    return decorate
+
+
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
+                env=None):
+    """Call the given command(s)."""
+    assert isinstance(commands, list)
+    process = None
+
+    popen_kwargs = {}
+    if sys.platform == "win32":
+        # This hides the console window if pythonw.exe is used
+        startupinfo = subprocess.STARTUPINFO()
+        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+        popen_kwargs["startupinfo"] = startupinfo
+
+    for command in commands:
+        try:
+            dispcmd = str([command] + args)
+            # remember shell=False, so use git.cmd on windows, not just git
+            process = subprocess.Popen([command] + args, cwd=cwd, env=env,
+                                       stdout=subprocess.PIPE,
+                                       stderr=(subprocess.PIPE if hide_stderr
+                                               else None), **popen_kwargs)
+            break
+        except OSError:
+            e = sys.exc_info()[1]
+            if e.errno == errno.ENOENT:
+                continue
+            if verbose:
+                print("unable to run %%s" %% dispcmd)
+                print(e)
+            return None, None
+    else:
+        if verbose:
+            print("unable to find command, tried %%s" %% (commands,))
+        return None, None
+    stdout = process.communicate()[0].strip().decode()
+    if process.returncode != 0:
+        if verbose:
+            print("unable to run %%s (error)" %% dispcmd)
+            print("stdout was %%s" %% stdout)
+        return None, process.returncode
+    return stdout, process.returncode
+
+
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+    """Try to determine the version from the parent directory name.
+
+    Source tarballs conventionally unpack into a directory that includes both
+    the project name and a version string. We will also support searching up
+    two directory levels for an appropriately named parent directory
+    """
+    rootdirs = []
+
+    for _ in range(3):
+        dirname = os.path.basename(root)
+        if dirname.startswith(parentdir_prefix):
+            return {"version": dirname[len(parentdir_prefix):],
+                    "full-revisionid": None,
+                    "dirty": False, "error": None, "date": None}
+        rootdirs.append(root)
+        root = os.path.dirname(root)  # up a level
+
+    if verbose:
+        print("Tried directories %%s but none started with prefix %%s" %%
+              (str(rootdirs), parentdir_prefix))
+    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+
+
+@register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+    """Extract version information from the given file."""
+    # the code embedded in _version.py can just fetch the value of these
+    # keywords. When used from setup.py, we don't want to import _version.py,
+    # so we do it with a regexp instead. This function is not used from
+    # _version.py.
+    keywords = {}
+    try:
+        with open(versionfile_abs, "r") as fobj:
+            for line in fobj:
+                if line.strip().startswith("git_refnames ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["refnames"] = mo.group(1)
+                if line.strip().startswith("git_full ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["full"] = mo.group(1)
+                if line.strip().startswith("git_date ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["date"] = mo.group(1)
+    except OSError:
+        pass
+    return keywords
+
+
+@register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+    """Get version information from git keywords."""
+    if "refnames" not in keywords:
+        raise NotThisMethod("Short version file found")
+    date = keywords.get("date")
+    if date is not None:
+        # Use only the last line.  Previous lines may contain GPG signature
+        # information.
+        date = date.splitlines()[-1]
+
+        # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant
+        # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601
+        # -like" string, which we must then edit to make compliant), because
+        # it's been around since git-1.5.3, and it's too difficult to
+        # discover which version we're using, or to work around using an
+        # older one.
+        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
+    refnames = keywords["refnames"].strip()
+    if refnames.startswith("$Format"):
+        if verbose:
+            print("keywords are unexpanded, not using")
+        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
+    refs = {r.strip() for r in refnames.strip("()").split(",")}
+    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+    TAG = "tag: "
+    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
+    if not tags:
+        # Either we're using git < 1.8.3, or there really are no tags. We use
+        # a heuristic: assume all version tags have a digit. The old git %%d
+        # expansion behaves like git log --decorate=short and strips out the
+        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
+        # between branches and tags. By ignoring refnames without digits, we
+        # filter out many common branch names like "release" and
+        # "stabilization", as well as "HEAD" and "master".
+        tags = {r for r in refs if re.search(r'\d', r)}
+        if verbose:
+            print("discarding '%%s', no digits" %% ",".join(refs - tags))
+    if verbose:
+        print("likely tags: %%s" %% ",".join(sorted(tags)))
+    for ref in sorted(tags):
+        # sorting will prefer e.g. "2.0" over "2.0rc1"
+        if ref.startswith(tag_prefix):
+            r = ref[len(tag_prefix):]
+            # Filter out refs that exactly match prefix or that don't start
+            # with a number once the prefix is stripped (mostly a concern
+            # when prefix is '')
+            if not re.match(r'\d', r):
+                continue
+            if verbose:
+                print("picking %%s" %% r)
+            return {"version": r,
+                    "full-revisionid": keywords["full"].strip(),
+                    "dirty": False, "error": None,
+                    "date": date}
+    # no suitable tags, so version is "0+unknown", but full hex is still there
+    if verbose:
+        print("no suitable tags, using unknown + full revision id")
+    return {"version": "0+unknown",
+            "full-revisionid": keywords["full"].strip(),
+            "dirty": False, "error": "no suitable tags", "date": None}
+
+
+@register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
+    """Get version from 'git describe' in the root of the source tree.
+
+    This only gets called if the git-archive 'subst' keywords were *not*
+    expanded, and _version.py hasn't already been rewritten with a short
+    version string, meaning we're inside a checked out source tree.
+    """
+    GITS = ["git"]
+    if sys.platform == "win32":
+        GITS = ["git.cmd", "git.exe"]
+
+    # GIT_DIR can interfere with correct operation of Versioneer.
+    # It may be intended to be passed to the Versioneer-versioned project,
+    # but that should not change where we get our version from.
+    env = os.environ.copy()
+    env.pop("GIT_DIR", None)
+    runner = functools.partial(runner, env=env)
+
+    _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
+                   hide_stderr=True)
+    if rc != 0:
+        if verbose:
+            print("Directory %%s not under git control" %% root)
+        raise NotThisMethod("'git rev-parse --git-dir' returned error")
+
+    MATCH_ARGS = ["--match", "%%s*" %% tag_prefix] if tag_prefix else []
+
+    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+    # if there isn't one, this yields HEX[-dirty] (no NUM)
+    describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty",
+                                     "--always", "--long", *MATCH_ARGS],
+                              cwd=root)
+    # --long was added in git-1.5.5
+    if describe_out is None:
+        raise NotThisMethod("'git describe' failed")
+    describe_out = describe_out.strip()
+    full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
+    if full_out is None:
+        raise NotThisMethod("'git rev-parse' failed")
+    full_out = full_out.strip()
+
+    pieces = {}
+    pieces["long"] = full_out
+    pieces["short"] = full_out[:7]  # maybe improved later
+    pieces["error"] = None
+
+    branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
+                             cwd=root)
+    # --abbrev-ref was added in git-1.6.3
+    if rc != 0 or branch_name is None:
+        raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
+    branch_name = branch_name.strip()
+
+    if branch_name == "HEAD":
+        # If we aren't exactly on a branch, pick a branch which represents
+        # the current commit. If all else fails, we are on a branchless
+        # commit.
+        branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
+        # --contains was added in git-1.5.4
+        if rc != 0 or branches is None:
+            raise NotThisMethod("'git branch --contains' returned error")
+        branches = branches.split("\n")
+
+        # Remove the first line if we're running detached
+        if "(" in branches[0]:
+            branches.pop(0)
+
+        # Strip off the leading "* " from the list of branches.
+        branches = [branch[2:] for branch in branches]
+        if "master" in branches:
+            branch_name = "master"
+        elif not branches:
+            branch_name = None
+        else:
+            # Pick the first branch that is returned. Good or bad.
+            branch_name = branches[0]
+
+    pieces["branch"] = branch_name
+
+    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+    # TAG might have hyphens.
+    git_describe = describe_out
+
+    # look for -dirty suffix
+    dirty = git_describe.endswith("-dirty")
+    pieces["dirty"] = dirty
+    if dirty:
+        git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+    # now we have TAG-NUM-gHEX or HEX
+
+    if "-" in git_describe:
+        # TAG-NUM-gHEX
+        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+        if not mo:
+            # unparsable. Maybe git-describe is misbehaving?
+            pieces["error"] = ("unable to parse git-describe output: '%%s'"
+                               %% describe_out)
+            return pieces
+
+        # tag
+        full_tag = mo.group(1)
+        if not full_tag.startswith(tag_prefix):
+            if verbose:
+                fmt = "tag '%%s' doesn't start with prefix '%%s'"
+                print(fmt %% (full_tag, tag_prefix))
+            pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'"
+                               %% (full_tag, tag_prefix))
+            return pieces
+        pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+        # distance: number of commits since tag
+        pieces["distance"] = int(mo.group(2))
+
+        # commit: short hex revision ID
+        pieces["short"] = mo.group(3)
+
+    else:
+        # HEX: no tags
+        pieces["closest-tag"] = None
+        count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
+        pieces["distance"] = int(count_out)  # total number of commits
+
+    # commit date: see ISO-8601 comment in git_versions_from_keywords()
+    date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip()
+    # Use only the last line.  Previous lines may contain GPG signature
+    # information.
+    date = date.splitlines()[-1]
+    pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
+
+    return pieces
+
+
+def plus_or_dot(pieces):
+    """Return a + if we don't already have one, else return a ."""
+    if "+" in pieces.get("closest-tag", ""):
+        return "."
+    return "+"
+
+
+def render_pep440(pieces):
+    """Build up version string, with post-release "local version identifier".
+
+    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+    Exceptions:
+    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += plus_or_dot(pieces)
+            rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_branch(pieces):
+    """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
+
+    The ".dev0" means not master branch. Note that .dev0 sorts backwards
+    (a feature branch will appear "older" than the master branch).
+
+    Exceptions:
+    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            if pieces["branch"] != "master":
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0"
+        if pieces["branch"] != "master":
+            rendered += ".dev0"
+        rendered += "+untagged.%%d.g%%s" %% (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def pep440_split_post(ver):
+    """Split pep440 version string at the post-release segment.
+
+    Returns the release segments before the post-release and the
+    post-release version number (or -1 if no post-release segment is present).
+    """
+    vc = str.split(ver, ".post")
+    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
+
+
+def render_pep440_pre(pieces):
+    """TAG[.postN.devDISTANCE] -- No -dirty.
+
+    Exceptions:
+    1: no tags. 0.post0.devDISTANCE
+    """
+    if pieces["closest-tag"]:
+        if pieces["distance"]:
+            # update the post release segment
+            tag_version, post_version = pep440_split_post(pieces["closest-tag"])
+            rendered = tag_version
+            if post_version is not None:
+                rendered += ".post%%d.dev%%d" %% (post_version+1, pieces["distance"])
+            else:
+                rendered += ".post0.dev%%d" %% (pieces["distance"])
+        else:
+            # no commits, use the tag as the version
+            rendered = pieces["closest-tag"]
+    else:
+        # exception #1
+        rendered = "0.post0.dev%%d" %% pieces["distance"]
+    return rendered
+
+
+def render_pep440_post(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+    The ".dev0" means dirty. Note that .dev0 sorts backwards
+    (a dirty tree will appear "older" than the corresponding clean one),
+    but you shouldn't be releasing software with -dirty anyways.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%%d" %% pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%%s" %% pieces["short"]
+    else:
+        # exception #1
+        rendered = "0.post%%d" %% pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+        rendered += "+g%%s" %% pieces["short"]
+    return rendered
+
+
+def render_pep440_post_branch(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
+
+    The ".dev0" means not master branch.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%%d" %% pieces["distance"]
+            if pieces["branch"] != "master":
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%%s" %% pieces["short"]
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0.post%%d" %% pieces["distance"]
+        if pieces["branch"] != "master":
+            rendered += ".dev0"
+        rendered += "+g%%s" %% pieces["short"]
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_old(pieces):
+    """TAG[.postDISTANCE[.dev0]] .
+
+    The ".dev0" means dirty.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%%d" %% pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+    else:
+        # exception #1
+        rendered = "0.post%%d" %% pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+    return rendered
+
+
+def render_git_describe(pieces):
+    """TAG[-DISTANCE-gHEX][-dirty].
+
+    Like 'git describe --tags --dirty --always'.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render_git_describe_long(pieces):
+    """TAG-DISTANCE-gHEX[-dirty].
+
+    Like 'git describe --tags --dirty --always -long'.
+    The distance/hash is unconditional.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render(pieces, style):
+    """Render the given version pieces into the requested style."""
+    if pieces["error"]:
+        return {"version": "unknown",
+                "full-revisionid": pieces.get("long"),
+                "dirty": None,
+                "error": pieces["error"],
+                "date": None}
+
+    if not style or style == "default":
+        style = "pep440"  # the default
+
+    if style == "pep440":
+        rendered = render_pep440(pieces)
+    elif style == "pep440-branch":
+        rendered = render_pep440_branch(pieces)
+    elif style == "pep440-pre":
+        rendered = render_pep440_pre(pieces)
+    elif style == "pep440-post":
+        rendered = render_pep440_post(pieces)
+    elif style == "pep440-post-branch":
+        rendered = render_pep440_post_branch(pieces)
+    elif style == "pep440-old":
+        rendered = render_pep440_old(pieces)
+    elif style == "git-describe":
+        rendered = render_git_describe(pieces)
+    elif style == "git-describe-long":
+        rendered = render_git_describe_long(pieces)
+    else:
+        raise ValueError("unknown style '%%s'" %% style)
+
+    return {"version": rendered, "full-revisionid": pieces["long"],
+            "dirty": pieces["dirty"], "error": None,
+            "date": pieces.get("date")}
+
+
+def get_versions():
+    """Get version information or return default if unable to do so."""
+    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
+    # __file__, we can work backwards from there to the root. Some
+    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
+    # case we can only use expanded keywords.
+
+    cfg = get_config()
+    verbose = cfg.verbose
+
+    try:
+        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
+                                          verbose)
+    except NotThisMethod:
+        pass
+
+    try:
+        root = os.path.realpath(__file__)
+        # versionfile_source is the relative path from the top of the source
+        # tree (where the .git directory might live) to this file. Invert
+        # this to find the root from __file__.
+        for _ in cfg.versionfile_source.split('/'):
+            root = os.path.dirname(root)
+    except NameError:
+        return {"version": "0+unknown", "full-revisionid": None,
+                "dirty": None,
+                "error": "unable to find root of source tree",
+                "date": None}
+
+    try:
+        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
+        return render(pieces, cfg.style)
+    except NotThisMethod:
+        pass
+
+    try:
+        if cfg.parentdir_prefix:
+            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+    except NotThisMethod:
+        pass
+
+    return {"version": "0+unknown", "full-revisionid": None,
+            "dirty": None,
+            "error": "unable to compute version", "date": None}
+'''
+
+
+@register_vcs_handler("git", "get_keywords")
+def git_get_keywords(versionfile_abs):
+    """Extract version information from the given file."""
+    # the code embedded in _version.py can just fetch the value of these
+    # keywords. When used from setup.py, we don't want to import _version.py,
+    # so we do it with a regexp instead. This function is not used from
+    # _version.py.
+    keywords = {}
+    try:
+        with open(versionfile_abs, "r") as fobj:
+            for line in fobj:
+                if line.strip().startswith("git_refnames ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["refnames"] = mo.group(1)
+                if line.strip().startswith("git_full ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["full"] = mo.group(1)
+                if line.strip().startswith("git_date ="):
+                    mo = re.search(r'=\s*"(.*)"', line)
+                    if mo:
+                        keywords["date"] = mo.group(1)
+    except OSError:
+        pass
+    return keywords
+
+
+@register_vcs_handler("git", "keywords")
+def git_versions_from_keywords(keywords, tag_prefix, verbose):
+    """Get version information from git keywords."""
+    if "refnames" not in keywords:
+        raise NotThisMethod("Short version file found")
+    date = keywords.get("date")
+    if date is not None:
+        # Use only the last line.  Previous lines may contain GPG signature
+        # information.
+        date = date.splitlines()[-1]
+
+        # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
+        # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
+        # -like" string, which we must then edit to make compliant), because
+        # it's been around since git-1.5.3, and it's too difficult to
+        # discover which version we're using, or to work around using an
+        # older one.
+        date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
+    refnames = keywords["refnames"].strip()
+    if refnames.startswith("$Format"):
+        if verbose:
+            print("keywords are unexpanded, not using")
+        raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
+    refs = {r.strip() for r in refnames.strip("()").split(",")}
+    # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
+    # just "foo-1.0". If we see a "tag: " prefix, prefer those.
+    TAG = "tag: "
+    tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
+    if not tags:
+        # Either we're using git < 1.8.3, or there really are no tags. We use
+        # a heuristic: assume all version tags have a digit. The old git %d
+        # expansion behaves like git log --decorate=short and strips out the
+        # refs/heads/ and refs/tags/ prefixes that would let us distinguish
+        # between branches and tags. By ignoring refnames without digits, we
+        # filter out many common branch names like "release" and
+        # "stabilization", as well as "HEAD" and "master".
+        tags = {r for r in refs if re.search(r'\d', r)}
+        if verbose:
+            print("discarding '%s', no digits" % ",".join(refs - tags))
+    if verbose:
+        print("likely tags: %s" % ",".join(sorted(tags)))
+    for ref in sorted(tags):
+        # sorting will prefer e.g. "2.0" over "2.0rc1"
+        if ref.startswith(tag_prefix):
+            r = ref[len(tag_prefix):]
+            # Filter out refs that exactly match prefix or that don't start
+            # with a number once the prefix is stripped (mostly a concern
+            # when prefix is '')
+            if not re.match(r'\d', r):
+                continue
+            if verbose:
+                print("picking %s" % r)
+            return {"version": r,
+                    "full-revisionid": keywords["full"].strip(),
+                    "dirty": False, "error": None,
+                    "date": date}
+    # no suitable tags, so version is "0+unknown", but full hex is still there
+    if verbose:
+        print("no suitable tags, using unknown + full revision id")
+    return {"version": "0+unknown",
+            "full-revisionid": keywords["full"].strip(),
+            "dirty": False, "error": "no suitable tags", "date": None}
+
+
+@register_vcs_handler("git", "pieces_from_vcs")
+def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
+    """Get version from 'git describe' in the root of the source tree.
+
+    This only gets called if the git-archive 'subst' keywords were *not*
+    expanded, and _version.py hasn't already been rewritten with a short
+    version string, meaning we're inside a checked out source tree.
+    """
+    GITS = ["git"]
+    if sys.platform == "win32":
+        GITS = ["git.cmd", "git.exe"]
+
+    # GIT_DIR can interfere with correct operation of Versioneer.
+    # It may be intended to be passed to the Versioneer-versioned project,
+    # but that should not change where we get our version from.
+    env = os.environ.copy()
+    env.pop("GIT_DIR", None)
+    runner = functools.partial(runner, env=env)
+
+    _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
+                   hide_stderr=True)
+    if rc != 0:
+        if verbose:
+            print("Directory %s not under git control" % root)
+        raise NotThisMethod("'git rev-parse --git-dir' returned error")
+
+    MATCH_ARGS = ["--match", "%s*" % tag_prefix] if tag_prefix else []
+
+    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
+    # if there isn't one, this yields HEX[-dirty] (no NUM)
+    describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty",
+                                     "--always", "--long", *MATCH_ARGS],
+                              cwd=root)
+    # --long was added in git-1.5.5
+    if describe_out is None:
+        raise NotThisMethod("'git describe' failed")
+    describe_out = describe_out.strip()
+    full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
+    if full_out is None:
+        raise NotThisMethod("'git rev-parse' failed")
+    full_out = full_out.strip()
+
+    pieces = {}
+    pieces["long"] = full_out
+    pieces["short"] = full_out[:7]  # maybe improved later
+    pieces["error"] = None
+
+    branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
+                             cwd=root)
+    # --abbrev-ref was added in git-1.6.3
+    if rc != 0 or branch_name is None:
+        raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
+    branch_name = branch_name.strip()
+
+    if branch_name == "HEAD":
+        # If we aren't exactly on a branch, pick a branch which represents
+        # the current commit. If all else fails, we are on a branchless
+        # commit.
+        branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
+        # --contains was added in git-1.5.4
+        if rc != 0 or branches is None:
+            raise NotThisMethod("'git branch --contains' returned error")
+        branches = branches.split("\n")
+
+        # Remove the first line if we're running detached
+        if "(" in branches[0]:
+            branches.pop(0)
+
+        # Strip off the leading "* " from the list of branches.
+        branches = [branch[2:] for branch in branches]
+        if "master" in branches:
+            branch_name = "master"
+        elif not branches:
+            branch_name = None
+        else:
+            # Pick the first branch that is returned. Good or bad.
+            branch_name = branches[0]
+
+    pieces["branch"] = branch_name
+
+    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
+    # TAG might have hyphens.
+    git_describe = describe_out
+
+    # look for -dirty suffix
+    dirty = git_describe.endswith("-dirty")
+    pieces["dirty"] = dirty
+    if dirty:
+        git_describe = git_describe[:git_describe.rindex("-dirty")]
+
+    # now we have TAG-NUM-gHEX or HEX
+
+    if "-" in git_describe:
+        # TAG-NUM-gHEX
+        mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+        if not mo:
+            # unparsable. Maybe git-describe is misbehaving?
+            pieces["error"] = ("unable to parse git-describe output: '%s'"
+                               % describe_out)
+            return pieces
+
+        # tag
+        full_tag = mo.group(1)
+        if not full_tag.startswith(tag_prefix):
+            if verbose:
+                fmt = "tag '%s' doesn't start with prefix '%s'"
+                print(fmt % (full_tag, tag_prefix))
+            pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
+                               % (full_tag, tag_prefix))
+            return pieces
+        pieces["closest-tag"] = full_tag[len(tag_prefix):]
+
+        # distance: number of commits since tag
+        pieces["distance"] = int(mo.group(2))
+
+        # commit: short hex revision ID
+        pieces["short"] = mo.group(3)
+
+    else:
+        # HEX: no tags
+        pieces["closest-tag"] = None
+        count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
+        pieces["distance"] = int(count_out)  # total number of commits
+
+    # commit date: see ISO-8601 comment in git_versions_from_keywords()
+    date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
+    # Use only the last line.  Previous lines may contain GPG signature
+    # information.
+    date = date.splitlines()[-1]
+    pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
+
+    return pieces
+
+
+def do_vcs_install(manifest_in, versionfile_source, ipy):
+    """Git-specific installation logic for Versioneer.
+
+    For Git, this means creating/changing .gitattributes to mark _version.py
+    for export-subst keyword substitution.
+    """
+    GITS = ["git"]
+    if sys.platform == "win32":
+        GITS = ["git.cmd", "git.exe"]
+    files = [manifest_in, versionfile_source]
+    if ipy:
+        files.append(ipy)
+    try:
+        my_path = __file__
+        if my_path.endswith(".pyc") or my_path.endswith(".pyo"):
+            my_path = os.path.splitext(my_path)[0] + ".py"
+        versioneer_file = os.path.relpath(my_path)
+    except NameError:
+        versioneer_file = "versioneer.py"
+    files.append(versioneer_file)
+    present = False
+    try:
+        with open(".gitattributes", "r") as fobj:
+            for line in fobj:
+                if line.strip().startswith(versionfile_source):
+                    if "export-subst" in line.strip().split()[1:]:
+                        present = True
+                        break
+    except OSError:
+        pass
+    if not present:
+        with open(".gitattributes", "a+") as fobj:
+            fobj.write(f"{versionfile_source} export-subst\n")
+        files.append(".gitattributes")
+    run_command(GITS, ["add", "--"] + files)
+
+
+def versions_from_parentdir(parentdir_prefix, root, verbose):
+    """Try to determine the version from the parent directory name.
+
+    Source tarballs conventionally unpack into a directory that includes both
+    the project name and a version string. We will also support searching up
+    two directory levels for an appropriately named parent directory
+    """
+    rootdirs = []
+
+    for _ in range(3):
+        dirname = os.path.basename(root)
+        if dirname.startswith(parentdir_prefix):
+            return {"version": dirname[len(parentdir_prefix):],
+                    "full-revisionid": None,
+                    "dirty": False, "error": None, "date": None}
+        rootdirs.append(root)
+        root = os.path.dirname(root)  # up a level
+
+    if verbose:
+        print("Tried directories %s but none started with prefix %s" %
+              (str(rootdirs), parentdir_prefix))
+    raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
+
+
+SHORT_VERSION_PY = """
+# This file was generated by 'versioneer.py' (0.22) from
+# revision-control system data, or from the parent directory name of an
+# unpacked source archive. Distribution tarballs contain a pre-generated copy
+# of this file.
+
+import json
+
+version_json = '''
+%s
+'''  # END VERSION_JSON
+
+
+def get_versions():
+    return json.loads(version_json)
+"""
+
+
+def versions_from_file(filename):
+    """Try to determine the version from _version.py if present."""
+    try:
+        with open(filename) as f:
+            contents = f.read()
+    except OSError:
+        raise NotThisMethod("unable to read _version.py")
+    mo = re.search(r"version_json = '''\n(.*)'''  # END VERSION_JSON",
+                   contents, re.M | re.S)
+    if not mo:
+        mo = re.search(r"version_json = '''\r\n(.*)'''  # END VERSION_JSON",
+                       contents, re.M | re.S)
+    if not mo:
+        raise NotThisMethod("no version_json in _version.py")
+    return json.loads(mo.group(1))
+
+
+def write_to_version_file(filename, versions):
+    """Write the given version number to the given _version.py file."""
+    os.unlink(filename)
+    contents = json.dumps(versions, sort_keys=True,
+                          indent=1, separators=(",", ": "))
+    with open(filename, "w") as f:
+        f.write(SHORT_VERSION_PY % contents)
+
+    print("set %s to '%s'" % (filename, versions["version"]))
+
+
+def plus_or_dot(pieces):
+    """Return a + if we don't already have one, else return a ."""
+    if "+" in pieces.get("closest-tag", ""):
+        return "."
+    return "+"
+
+
+def render_pep440(pieces):
+    """Build up version string, with post-release "local version identifier".
+
+    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
+    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
+
+    Exceptions:
+    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += plus_or_dot(pieces)
+            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0+untagged.%d.g%s" % (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_branch(pieces):
+    """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
+
+    The ".dev0" means not master branch. Note that .dev0 sorts backwards
+    (a feature branch will appear "older" than the master branch).
+
+    Exceptions:
+    1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            if pieces["branch"] != "master":
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0"
+        if pieces["branch"] != "master":
+            rendered += ".dev0"
+        rendered += "+untagged.%d.g%s" % (pieces["distance"],
+                                          pieces["short"])
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def pep440_split_post(ver):
+    """Split pep440 version string at the post-release segment.
+
+    Returns the release segments before the post-release and the
+    post-release version number (or -1 if no post-release segment is present).
+    """
+    vc = str.split(ver, ".post")
+    return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
+
+
+def render_pep440_pre(pieces):
+    """TAG[.postN.devDISTANCE] -- No -dirty.
+
+    Exceptions:
+    1: no tags. 0.post0.devDISTANCE
+    """
+    if pieces["closest-tag"]:
+        if pieces["distance"]:
+            # update the post release segment
+            tag_version, post_version = pep440_split_post(pieces["closest-tag"])
+            rendered = tag_version
+            if post_version is not None:
+                rendered += ".post%d.dev%d" % (post_version+1, pieces["distance"])
+            else:
+                rendered += ".post0.dev%d" % (pieces["distance"])
+        else:
+            # no commits, use the tag as the version
+            rendered = pieces["closest-tag"]
+    else:
+        # exception #1
+        rendered = "0.post0.dev%d" % pieces["distance"]
+    return rendered
+
+
+def render_pep440_post(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX] .
+
+    The ".dev0" means dirty. Note that .dev0 sorts backwards
+    (a dirty tree will appear "older" than the corresponding clean one),
+    but you shouldn't be releasing software with -dirty anyways.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%s" % pieces["short"]
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+        rendered += "+g%s" % pieces["short"]
+    return rendered
+
+
+def render_pep440_post_branch(pieces):
+    """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
+
+    The ".dev0" means not master branch.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["branch"] != "master":
+                rendered += ".dev0"
+            rendered += plus_or_dot(pieces)
+            rendered += "g%s" % pieces["short"]
+            if pieces["dirty"]:
+                rendered += ".dirty"
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["branch"] != "master":
+            rendered += ".dev0"
+        rendered += "+g%s" % pieces["short"]
+        if pieces["dirty"]:
+            rendered += ".dirty"
+    return rendered
+
+
+def render_pep440_old(pieces):
+    """TAG[.postDISTANCE[.dev0]] .
+
+    The ".dev0" means dirty.
+
+    Exceptions:
+    1: no tags. 0.postDISTANCE[.dev0]
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"] or pieces["dirty"]:
+            rendered += ".post%d" % pieces["distance"]
+            if pieces["dirty"]:
+                rendered += ".dev0"
+    else:
+        # exception #1
+        rendered = "0.post%d" % pieces["distance"]
+        if pieces["dirty"]:
+            rendered += ".dev0"
+    return rendered
+
+
+def render_git_describe(pieces):
+    """TAG[-DISTANCE-gHEX][-dirty].
+
+    Like 'git describe --tags --dirty --always'.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        if pieces["distance"]:
+            rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render_git_describe_long(pieces):
+    """TAG-DISTANCE-gHEX[-dirty].
+
+    Like 'git describe --tags --dirty --always -long'.
+    The distance/hash is unconditional.
+
+    Exceptions:
+    1: no tags. HEX[-dirty]  (note: no 'g' prefix)
+    """
+    if pieces["closest-tag"]:
+        rendered = pieces["closest-tag"]
+        rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
+    else:
+        # exception #1
+        rendered = pieces["short"]
+    if pieces["dirty"]:
+        rendered += "-dirty"
+    return rendered
+
+
+def render(pieces, style):
+    """Render the given version pieces into the requested style."""
+    if pieces["error"]:
+        return {"version": "unknown",
+                "full-revisionid": pieces.get("long"),
+                "dirty": None,
+                "error": pieces["error"],
+                "date": None}
+
+    if not style or style == "default":
+        style = "pep440"  # the default
+
+    if style == "pep440":
+        rendered = render_pep440(pieces)
+    elif style == "pep440-branch":
+        rendered = render_pep440_branch(pieces)
+    elif style == "pep440-pre":
+        rendered = render_pep440_pre(pieces)
+    elif style == "pep440-post":
+        rendered = render_pep440_post(pieces)
+    elif style == "pep440-post-branch":
+        rendered = render_pep440_post_branch(pieces)
+    elif style == "pep440-old":
+        rendered = render_pep440_old(pieces)
+    elif style == "git-describe":
+        rendered = render_git_describe(pieces)
+    elif style == "git-describe-long":
+        rendered = render_git_describe_long(pieces)
+    else:
+        raise ValueError("unknown style '%s'" % style)
+
+    return {"version": rendered, "full-revisionid": pieces["long"],
+            "dirty": pieces["dirty"], "error": None,
+            "date": pieces.get("date")}
+
+
+class VersioneerBadRootError(Exception):
+    """The project root directory is unknown or missing key files."""
+
+
+def get_versions(verbose=False):
+    """Get the project version from whatever source is available.
+
+    Returns dict with two keys: 'version' and 'full'.
+    """
+    if "versioneer" in sys.modules:
+        # see the discussion in cmdclass.py:get_cmdclass()
+        del sys.modules["versioneer"]
+
+    root = get_root()
+    cfg = get_config_from_root(root)
+
+    assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg"
+    handlers = HANDLERS.get(cfg.VCS)
+    assert handlers, "unrecognized VCS '%s'" % cfg.VCS
+    verbose = verbose or cfg.verbose
+    assert cfg.versionfile_source is not None, \
+        "please set versioneer.versionfile_source"
+    assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix"
+
+    versionfile_abs = os.path.join(root, cfg.versionfile_source)
+
+    # extract version from first of: _version.py, VCS command (e.g. 'git
+    # describe'), parentdir. This is meant to work for developers using a
+    # source checkout, for users of a tarball created by 'setup.py sdist',
+    # and for users of a tarball/zipball created by 'git archive' or github's
+    # download-from-tag feature or the equivalent in other VCSes.
+
+    get_keywords_f = handlers.get("get_keywords")
+    from_keywords_f = handlers.get("keywords")
+    if get_keywords_f and from_keywords_f:
+        try:
+            keywords = get_keywords_f(versionfile_abs)
+            ver = from_keywords_f(keywords, cfg.tag_prefix, verbose)
+            if verbose:
+                print("got version from expanded keyword %s" % ver)
+            return ver
+        except NotThisMethod:
+            pass
+
+    try:
+        ver = versions_from_file(versionfile_abs)
+        if verbose:
+            print("got version from file %s %s" % (versionfile_abs, ver))
+        return ver
+    except NotThisMethod:
+        pass
+
+    from_vcs_f = handlers.get("pieces_from_vcs")
+    if from_vcs_f:
+        try:
+            pieces = from_vcs_f(cfg.tag_prefix, root, verbose)
+            ver = render(pieces, cfg.style)
+            if verbose:
+                print("got version from VCS %s" % ver)
+            return ver
+        except NotThisMethod:
+            pass
+
+    try:
+        if cfg.parentdir_prefix:
+            ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
+            if verbose:
+                print("got version from parentdir %s" % ver)
+            return ver
+    except NotThisMethod:
+        pass
+
+    if verbose:
+        print("unable to compute version")
+
+    return {"version": "0+unknown", "full-revisionid": None,
+            "dirty": None, "error": "unable to compute version",
+            "date": None}
+
+
+def get_version():
+    """Get the short version string for this project."""
+    return get_versions()["version"]
+
+
+def get_cmdclass(cmdclass=None):
+    """Get the custom setuptools/distutils subclasses used by Versioneer.
+
+    If the package uses a different cmdclass (e.g. one from numpy), it
+    should be provide as an argument.
+    """
+    if "versioneer" in sys.modules:
+        del sys.modules["versioneer"]
+        # this fixes the "python setup.py develop" case (also 'install' and
+        # 'easy_install .'), in which subdependencies of the main project are
+        # built (using setup.py bdist_egg) in the same python process. Assume
+        # a main project A and a dependency B, which use different versions
+        # of Versioneer. A's setup.py imports A's Versioneer, leaving it in
+        # sys.modules by the time B's setup.py is executed, causing B to run
+        # with the wrong versioneer. Setuptools wraps the sub-dep builds in a
+        # sandbox that restores sys.modules to it's pre-build state, so the
+        # parent is protected against the child's "import versioneer". By
+        # removing ourselves from sys.modules here, before the child build
+        # happens, we protect the child from the parent's versioneer too.
+        # Also see https://github.com/python-versioneer/python-versioneer/issues/52
+
+    cmds = {} if cmdclass is None else cmdclass.copy()
+
+    # we add "version" to both distutils and setuptools
+    try:
+        from setuptools import Command
+    except ImportError:
+        from distutils.core import Command
+
+    class cmd_version(Command):
+        description = "report generated version string"
+        user_options = []
+        boolean_options = []
+
+        def initialize_options(self):
+            pass
+
+        def finalize_options(self):
+            pass
+
+        def run(self):
+            vers = get_versions(verbose=True)
+            print("Version: %s" % vers["version"])
+            print(" full-revisionid: %s" % vers.get("full-revisionid"))
+            print(" dirty: %s" % vers.get("dirty"))
+            print(" date: %s" % vers.get("date"))
+            if vers["error"]:
+                print(" error: %s" % vers["error"])
+    cmds["version"] = cmd_version
+
+    # we override "build_py" in both distutils and setuptools
+    #
+    # most invocation pathways end up running build_py:
+    #  distutils/build -> build_py
+    #  distutils/install -> distutils/build ->..
+    #  setuptools/bdist_wheel -> distutils/install ->..
+    #  setuptools/bdist_egg -> distutils/install_lib -> build_py
+    #  setuptools/install -> bdist_egg ->..
+    #  setuptools/develop -> ?
+    #  pip install:
+    #   copies source tree to a tempdir before running egg_info/etc
+    #   if .git isn't copied too, 'git describe' will fail
+    #   then does setup.py bdist_wheel, or sometimes setup.py install
+    #  setup.py egg_info -> ?
+
+    # we override different "build_py" commands for both environments
+    if 'build_py' in cmds:
+        _build_py = cmds['build_py']
+    elif "setuptools" in sys.modules:
+        from setuptools.command.build_py import build_py as _build_py
+    else:
+        from distutils.command.build_py import build_py as _build_py
+
+    class cmd_build_py(_build_py):
+        def run(self):
+            root = get_root()
+            cfg = get_config_from_root(root)
+            versions = get_versions()
+            _build_py.run(self)
+            # now locate _version.py in the new build/ directory and replace
+            # it with an updated value
+            if cfg.versionfile_build:
+                target_versionfile = os.path.join(self.build_lib,
+                                                  cfg.versionfile_build)
+                print("UPDATING %s" % target_versionfile)
+                write_to_version_file(target_versionfile, versions)
+    cmds["build_py"] = cmd_build_py
+
+    if 'build_ext' in cmds:
+        _build_ext = cmds['build_ext']
+    elif "setuptools" in sys.modules:
+        from setuptools.command.build_ext import build_ext as _build_ext
+    else:
+        from distutils.command.build_ext import build_ext as _build_ext
+
+    class cmd_build_ext(_build_ext):
+        def run(self):
+            root = get_root()
+            cfg = get_config_from_root(root)
+            versions = get_versions()
+            _build_ext.run(self)
+            if self.inplace:
+                # build_ext --inplace will only build extensions in
+                # build/lib<..> dir with no _version.py to write to.
+                # As in place builds will already have a _version.py
+                # in the module dir, we do not need to write one.
+                return
+            # now locate _version.py in the new build/ directory and replace
+            # it with an updated value
+            target_versionfile = os.path.join(self.build_lib,
+                                              cfg.versionfile_build)
+            print("UPDATING %s" % target_versionfile)
+            write_to_version_file(target_versionfile, versions)
+    cmds["build_ext"] = cmd_build_ext
+
+    if "cx_Freeze" in sys.modules:  # cx_freeze enabled?
+        from cx_Freeze.dist import build_exe as _build_exe
+        # nczeczulin reports that py2exe won't like the pep440-style string
+        # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g.
+        # setup(console=[{
+        #   "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION
+        #   "product_version": versioneer.get_version(),
+        #   ...
+
+        class cmd_build_exe(_build_exe):
+            def run(self):
+                root = get_root()
+                cfg = get_config_from_root(root)
+                versions = get_versions()
+                target_versionfile = cfg.versionfile_source
+                print("UPDATING %s" % target_versionfile)
+                write_to_version_file(target_versionfile, versions)
+
+                _build_exe.run(self)
+                os.unlink(target_versionfile)
+                with open(cfg.versionfile_source, "w") as f:
+                    LONG = LONG_VERSION_PY[cfg.VCS]
+                    f.write(LONG %
+                            {"DOLLAR": "$",
+                             "STYLE": cfg.style,
+                             "TAG_PREFIX": cfg.tag_prefix,
+                             "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+                             "VERSIONFILE_SOURCE": cfg.versionfile_source,
+                             })
+        cmds["build_exe"] = cmd_build_exe
+        del cmds["build_py"]
+
+    if 'py2exe' in sys.modules:  # py2exe enabled?
+        from py2exe.distutils_buildexe import py2exe as _py2exe
+
+        class cmd_py2exe(_py2exe):
+            def run(self):
+                root = get_root()
+                cfg = get_config_from_root(root)
+                versions = get_versions()
+                target_versionfile = cfg.versionfile_source
+                print("UPDATING %s" % target_versionfile)
+                write_to_version_file(target_versionfile, versions)
+
+                _py2exe.run(self)
+                os.unlink(target_versionfile)
+                with open(cfg.versionfile_source, "w") as f:
+                    LONG = LONG_VERSION_PY[cfg.VCS]
+                    f.write(LONG %
+                            {"DOLLAR": "$",
+                             "STYLE": cfg.style,
+                             "TAG_PREFIX": cfg.tag_prefix,
+                             "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+                             "VERSIONFILE_SOURCE": cfg.versionfile_source,
+                             })
+        cmds["py2exe"] = cmd_py2exe
+
+    # we override different "sdist" commands for both environments
+    if 'sdist' in cmds:
+        _sdist = cmds['sdist']
+    elif "setuptools" in sys.modules:
+        from setuptools.command.sdist import sdist as _sdist
+    else:
+        from distutils.command.sdist import sdist as _sdist
+
+    class cmd_sdist(_sdist):
+        def run(self):
+            versions = get_versions()
+            self._versioneer_generated_versions = versions
+            # unless we update this, the command will keep using the old
+            # version
+            self.distribution.metadata.version = versions["version"]
+            return _sdist.run(self)
+
+        def make_release_tree(self, base_dir, files):
+            root = get_root()
+            cfg = get_config_from_root(root)
+            _sdist.make_release_tree(self, base_dir, files)
+            # now locate _version.py in the new base_dir directory
+            # (remembering that it may be a hardlink) and replace it with an
+            # updated value
+            target_versionfile = os.path.join(base_dir, cfg.versionfile_source)
+            print("UPDATING %s" % target_versionfile)
+            write_to_version_file(target_versionfile,
+                                  self._versioneer_generated_versions)
+    cmds["sdist"] = cmd_sdist
+
+    return cmds
+
+
+CONFIG_ERROR = """
+setup.cfg is missing the necessary Versioneer configuration. You need
+a section like:
+
+ [versioneer]
+ VCS = git
+ style = pep440
+ versionfile_source = src/myproject/_version.py
+ versionfile_build = myproject/_version.py
+ tag_prefix =
+ parentdir_prefix = myproject-
+
+You will also need to edit your setup.py to use the results:
+
+ import versioneer
+ setup(version=versioneer.get_version(),
+       cmdclass=versioneer.get_cmdclass(), ...)
+
+Please read the docstring in ./versioneer.py for configuration instructions,
+edit setup.cfg, and re-run the installer or 'python versioneer.py setup'.
+"""
+
+SAMPLE_CONFIG = """
+# See the docstring in versioneer.py for instructions. Note that you must
+# re-run 'versioneer.py setup' after changing this section, and commit the
+# resulting files.
+
+[versioneer]
+#VCS = git
+#style = pep440
+#versionfile_source =
+#versionfile_build =
+#tag_prefix =
+#parentdir_prefix =
+
+"""
+
+OLD_SNIPPET = """
+from ._version import get_versions
+__version__ = get_versions()['version']
+del get_versions
+"""
+
+INIT_PY_SNIPPET = """
+from . import {0}
+__version__ = {0}.get_versions()['version']
+"""
+
+
+def do_setup():
+    """Do main VCS-independent setup function for installing Versioneer."""
+    root = get_root()
+    try:
+        cfg = get_config_from_root(root)
+    except (OSError, configparser.NoSectionError,
+            configparser.NoOptionError) as e:
+        if isinstance(e, (OSError, configparser.NoSectionError)):
+            print("Adding sample versioneer config to setup.cfg",
+                  file=sys.stderr)
+            with open(os.path.join(root, "setup.cfg"), "a") as f:
+                f.write(SAMPLE_CONFIG)
+        print(CONFIG_ERROR, file=sys.stderr)
+        return 1
+
+    print(" creating %s" % cfg.versionfile_source)
+    with open(cfg.versionfile_source, "w") as f:
+        LONG = LONG_VERSION_PY[cfg.VCS]
+        f.write(LONG % {"DOLLAR": "$",
+                        "STYLE": cfg.style,
+                        "TAG_PREFIX": cfg.tag_prefix,
+                        "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+                        "VERSIONFILE_SOURCE": cfg.versionfile_source,
+                        })
+
+    ipy = os.path.join(os.path.dirname(cfg.versionfile_source),
+                       "__init__.py")
+    if os.path.exists(ipy):
+        try:
+            with open(ipy, "r") as f:
+                old = f.read()
+        except OSError:
+            old = ""
+        module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0]
+        snippet = INIT_PY_SNIPPET.format(module)
+        if OLD_SNIPPET in old:
+            print(" replacing boilerplate in %s" % ipy)
+            with open(ipy, "w") as f:
+                f.write(old.replace(OLD_SNIPPET, snippet))
+        elif snippet not in old:
+            print(" appending to %s" % ipy)
+            with open(ipy, "a") as f:
+                f.write(snippet)
+        else:
+            print(" %s unmodified" % ipy)
+    else:
+        print(" %s doesn't exist, ok" % ipy)
+        ipy = None
+
+    # Make sure both the top-level "versioneer.py" and versionfile_source
+    # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so
+    # they'll be copied into source distributions. Pip won't be able to
+    # install the package without this.
+    manifest_in = os.path.join(root, "MANIFEST.in")
+    simple_includes = set()
+    try:
+        with open(manifest_in, "r") as f:
+            for line in f:
+                if line.startswith("include "):
+                    for include in line.split()[1:]:
+                        simple_includes.add(include)
+    except OSError:
+        pass
+    # That doesn't cover everything MANIFEST.in can do
+    # (http://docs.python.org/2/distutils/sourcedist.html#commands), so
+    # it might give some false negatives. Appending redundant 'include'
+    # lines is safe, though.
+    if "versioneer.py" not in simple_includes:
+        print(" appending 'versioneer.py' to MANIFEST.in")
+        with open(manifest_in, "a") as f:
+            f.write("include versioneer.py\n")
+    else:
+        print(" 'versioneer.py' already in MANIFEST.in")
+    if cfg.versionfile_source not in simple_includes:
+        print(" appending versionfile_source ('%s') to MANIFEST.in" %
+              cfg.versionfile_source)
+        with open(manifest_in, "a") as f:
+            f.write("include %s\n" % cfg.versionfile_source)
+    else:
+        print(" versionfile_source already in MANIFEST.in")
+
+    # Make VCS-specific changes. For git, this means creating/changing
+    # .gitattributes to mark _version.py for export-subst keyword
+    # substitution.
+    do_vcs_install(manifest_in, cfg.versionfile_source, ipy)
+    return 0
+
+
+def scan_setup_py():
+    """Validate the contents of setup.py against Versioneer's expectations."""
+    found = set()
+    setters = False
+    errors = 0
+    with open("setup.py", "r") as f:
+        for line in f.readlines():
+            if "import versioneer" in line:
+                found.add("import")
+            if "versioneer.get_cmdclass()" in line:
+                found.add("cmdclass")
+            if "versioneer.get_version()" in line:
+                found.add("get_version")
+            if "versioneer.VCS" in line:
+                setters = True
+            if "versioneer.versionfile_source" in line:
+                setters = True
+    if len(found) != 3:
+        print("")
+        print("Your setup.py appears to be missing some important items")
+        print("(but I might be wrong). Please make sure it has something")
+        print("roughly like the following:")
+        print("")
+        print(" import versioneer")
+        print(" setup( version=versioneer.get_version(),")
+        print("        cmdclass=versioneer.get_cmdclass(),  ...)")
+        print("")
+        errors += 1
+    if setters:
+        print("You should remove lines like 'versioneer.VCS = ' and")
+        print("'versioneer.versionfile_source = ' . This configuration")
+        print("now lives in setup.cfg, and should be removed from setup.py")
+        print("")
+        errors += 1
+    return errors
+
+
+if __name__ == "__main__":
+    cmd = sys.argv[1]
+    if cmd == "setup":
+        errors = do_setup()
+        errors += scan_setup_py()
+        if errors:
+            sys.exit(1)


Test patch:
diff --git a/tests/data/curation/bcit_basline_driving_samplingRates.tsv b/tests/data/curation/bcit_baseline_driving_samplingRates.tsv
similarity index 100%
rename from tests/data/curation/bcit_basline_driving_samplingRates.tsv
rename to tests/data/curation/bcit_baseline_driving_samplingRates.tsv
diff --git a/tests/data/model_tests/no_column_header.tsv b/tests/data/model_tests/no_column_header.tsv
new file mode 100644
index 000000000..99733a0e6
--- /dev/null
+++ b/tests/data/model_tests/no_column_header.tsv
@@ -0,0 +1,2 @@
+Geometric-object	Event
+Square	Item
diff --git a/tests/data/model_tests/no_column_header_definition.tsv b/tests/data/model_tests/no_column_header_definition.tsv
new file mode 100644
index 000000000..d6fc19cb0
--- /dev/null
+++ b/tests/data/model_tests/no_column_header_definition.tsv
@@ -0,0 +1,2 @@
+Geometric-object	Event, (Definition/DefTest1, (InvalidDefTag))
+Square	Item, Def/DefTest1
diff --git a/tests/data/model_tests/no_column_header_definition_long.tsv b/tests/data/model_tests/no_column_header_definition_long.tsv
new file mode 100644
index 000000000..68aafa49e
--- /dev/null
+++ b/tests/data/model_tests/no_column_header_definition_long.tsv
@@ -0,0 +1,2 @@
+Item/Object/Geometric-object	Event,(Property/Organizational-property/Definition/DefTest1,(InvalidDefTag))
+Item/Object/Geometric-object/2D-shape/Rectangle/Square	Item,Property/Organizational-property/Def/DefTest1
diff --git a/tests/data/model_tests/no_column_header_long.tsv b/tests/data/model_tests/no_column_header_long.tsv
new file mode 100644
index 000000000..b8287124b
--- /dev/null
+++ b/tests/data/model_tests/no_column_header_long.tsv
@@ -0,0 +1,2 @@
+Item/Object/Geometric-object	Event
+Item/Object/Geometric-object/2D-shape/Rectangle/Square	Item
diff --git a/tests/models/test_column_mapper.py b/tests/models/test_column_mapper.py
index 5d03e1058..1e1506f4c 100644
--- a/tests/models/test_column_mapper.py
+++ b/tests/models/test_column_mapper.py
@@ -16,7 +16,7 @@ def setUpClass(cls):
         cls.zero_based_row_column_count = 3
         cls.column_prefix_dictionary = {3: 'Event/Description/', 4: 'Event/Label/', 5: 'Event/Category/'}
         cls.category_key = 'Event/Category/'
-        cls.category_partipant_and_stimulus_tags = \
+        cls.category_participant_and_stimulus_tags = \
             HedString('Event/Category/Participant response, Event/Category/Stimulus')
         cls.category_tags = HedString('Participant response, Stimulus')
         cls.row_with_hed_tags = ['event1', 'tag1', 'tag2']
@@ -172,7 +172,7 @@ def test__prepend_prefix_to_required_tag_column_if_needed(self):
         prepended_hed_string = ColumnMetadata._prepend_prefix_to_required_tag_column_if_needed(
             self.category_tags, self.category_key)
         self.assertIsInstance(prepended_hed_string, HedString)
-        self.assertEqual(str(prepended_hed_string), str(self.category_partipant_and_stimulus_tags))
+        self.assertEqual(str(prepended_hed_string), str(self.category_participant_and_stimulus_tags))
 
     # Verify reading/writing a short tag to a file column with a name_prefix works
     def test_add_prefix_verify_short_tag_conversion(self):
diff --git a/tests/models/test_def_dict.py b/tests/models/test_def_dict.py
index f1c66fe9d..48e0535ab 100644
--- a/tests/models/test_def_dict.py
+++ b/tests/models/test_def_dict.py
@@ -1,6 +1,6 @@
 import unittest
 
-from hed.models.def_dict import DefDict
+from hed.models.definition_dict import DefinitionDict
 from hed.errors import ErrorHandler, DefinitionErrors
 from hed.models.hed_string import HedString
 from hed import HedTag
@@ -9,7 +9,7 @@
 class TestDefBase(unittest.TestCase):
     def check_def_base(self, test_strings, expected_issues):
         for test_key in test_strings:
-            def_dict = DefDict()
+            def_dict = DefinitionDict()
             hed_string_obj = HedString(test_strings[test_key])
             hed_string_obj.convert_to_canonical_forms(None)
             test_issues = def_dict.check_for_definitions(hed_string_obj)
@@ -32,7 +32,7 @@ class TestDefDict(TestDefBase):
     placeholder_def_string = f"(Definition/TestDefPlaceholder/#,{placeholder_def_contents})"
 
     def test_check_for_definitions(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         original_def_count = len(def_dict._defs)
         hed_string_obj = HedString(self.basic_definition_string)
         hed_string_obj.validate(def_dict)
@@ -40,7 +40,7 @@ def test_check_for_definitions(self):
         self.assertGreater(new_def_count, original_def_count)
 
     def test_check_for_definitions_placeholder(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         original_def_count = len(def_dict._defs)
         hed_string_obj = HedString(self.placeholder_def_string)
         hed_string_obj.validate(def_dict)
diff --git a/tests/models/test_def_mapper.py b/tests/models/test_def_mapper.py
index 38a4b7533..8dbd0c564 100644
--- a/tests/models/test_def_mapper.py
+++ b/tests/models/test_def_mapper.py
@@ -2,7 +2,7 @@
 import os
 
 from hed import schema
-from hed.models import DefDict, DefinitionMapper, HedString
+from hed.models import DefinitionDict, DefMapper, HedString
 from hed.validator import HedValidator
 
 
@@ -56,12 +56,12 @@ def base_def_validator(self, test_strings, result_strings, valid_strings, expand
                            basic_definition_string=None):
         if not basic_definition_string:
             basic_definition_string = self.basic_definition_string
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(basic_definition_string)
         def_string.convert_to_canonical_forms(None)
         def_dict.check_for_definitions(def_string)
 
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         hed_ops = []
         if extra_ops:
             hed_ops += extra_ops
@@ -118,23 +118,23 @@ def test_expand_def_tags(self):
 
     # special case test
     def test_changing_tag_then_def_mapping(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.basic_definition_string)
         def_string.convert_to_canonical_forms(None)
         def_dict.check_for_definitions(def_string)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         validator = HedValidator(self.hed_schema)
         hed_ops = [validator, def_mapper]
 
         test_string = HedString(self.label_def_string)
-        tag = test_string.get_direct_children()[0]
+        tag = test_string.children[0]
         tag.tag = "Organizational-property/" + str(tag)
         def_issues = test_string.validate(hed_ops, expand_defs=True)
         self.assertFalse(def_issues)
         self.assertEqual(test_string.get_as_short(), f"{self.expanded_def_string}")
 
         test_string = HedString(self.label_def_string)
-        tag = test_string.get_direct_children()[0]
+        tag = test_string.children[0]
         tag.tag = "Organizational-property22/" + str(tag)
         def_issues = test_string.validate(hed_ops, expand_defs=True)
         self.assertTrue(def_issues)
@@ -196,11 +196,11 @@ def test_expand_def_tags_placeholder(self):
                                 extra_ops=extra_ops)
 
     def test_expand_def_tags_placeholder_invalid(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.convert_to_canonical_forms(None)
         def_dict.check_for_definitions(def_string)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
 
         placeholder_label_def_string_no_placeholder = "def/TestDefPlaceholder"
 
@@ -210,11 +210,11 @@ def test_expand_def_tags_placeholder_invalid(self):
         self.assertEqual(str(test_string), placeholder_label_def_string_no_placeholder)
         self.assertTrue(def_issues)
 
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.basic_definition_string)
         def_string.convert_to_canonical_forms(None)
         def_dict.check_for_definitions(def_string)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
 
         label_def_string_has_invalid_placeholder = "def/TestDef/54687"
 
@@ -225,11 +225,11 @@ def test_expand_def_tags_placeholder_invalid(self):
         self.assertTrue(def_issues)
 
     def test_bad_def_expand(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.convert_to_canonical_forms(None)
         def_dict.check_for_definitions(def_string)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
 
         valid_placeholder = HedString(self.placeholder_expanded_def_string)
         def_issues = valid_placeholder.validate(def_mapper)
@@ -240,11 +240,11 @@ def test_bad_def_expand(self):
         self.assertTrue(bool(def_issues))
 
     def test_def_no_content(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString("(Definition/EmptyDef)")
         def_string.convert_to_canonical_forms(None)
         def_dict.check_for_definitions(def_string)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
 
         valid_empty = HedString("Def/EmptyDef")
         def_issues = valid_empty.validate(def_mapper, expand_defs=True)
@@ -256,14 +256,13 @@ def test_def_no_content(self):
         self.assertFalse(def_issues)
 
     def test_mutable(self):
-        import copy
         basic_definition_string = "(Definition/TestDef, (Keypad-key/TestDef1,Keyboard-key/TestDef2))"
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(basic_definition_string)
         def_string.convert_to_canonical_forms(self.hed_schema)
         def_dict.check_for_definitions(def_string)
 
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         hed_ops = []
         hed_ops.append(self.hed_schema)
         hed_ops.append(def_mapper)
@@ -276,15 +275,14 @@ def test_mutable(self):
         test_string2 = HedString(label_def_string)
         def_issues = test_string.validate(hed_ops, expand_defs=True)
         def_issues = test_string2.validate(hed_ops, expand_defs=True)
-        tags_to_remove = test_string.find_tags(anchors=["keypad-key"], recursive=True)
+        tags_to_remove = test_string.find_tags(search_tags=["keypad-key"], recursive=True)
 
         for tag, group in tags_to_remove:
-            test_string.remove_groups([tag])
+            test_string.remove([tag])
 
 
         self.assertEqual(test_string, test_string2)
 
-        breakHEre = 3
 
 
 if __name__ == '__main__':
diff --git a/tests/models/test_hed_file_input.py b/tests/models/test_hed_file_input.py
index 47a92d9b9..b7b4caaa3 100644
--- a/tests/models/test_hed_file_input.py
+++ b/tests/models/test_hed_file_input.py
@@ -5,6 +5,8 @@
 from hed.errors import HedFileError
 from hed.models import EventsInput, HedInput, model_constants, Sidecar
 import shutil
+from hed import schema
+
 
 # TODO: Add tests about correct handling of 'n/a'
 
@@ -12,6 +14,9 @@
 class Test(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
+        hed_xml_file = os.path.join(cls.base_data_dir, "schema_test_data/HED8.0.0t.xml")
+        cls.hed_schema = schema.load_schema(hed_xml_file)
         cls.default_test_file_name = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                                   "../data/validator_tests/ExcelMultipleSheets.xlsx")
         cls.generic_file_input = HedInput(cls.default_test_file_name)
@@ -61,7 +66,8 @@ def test_file_as_string(self):
         events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events.tsv')
 
-        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/validator_tests/bids_events.json")
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                 "../data/validator_tests/bids_events.json")
         sidecar = Sidecar(json_path)
         self.assertEqual(len(sidecar.validate_entries(expand_defs=True)), 0)
         input_file = EventsInput(events_path, sidecars=sidecar)
@@ -122,7 +128,8 @@ def test_loading_and_reloading(self):
     def test_loading_and_reset_mapper(self):
         events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                                    '../data/validator_tests/bids_events.tsv')
-        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../data/validator_tests/bids_events.json")
+        json_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                 "../data/validator_tests/bids_events.json")
         sidecar = Sidecar(json_path)
         self.assertEqual(len(sidecar.validate_entries()), 0)
         input_file_1 = EventsInput(events_path, sidecars=sidecar)
@@ -139,6 +146,42 @@ def test_loading_and_reset_mapper(self):
             self.assertTrue(len(column_dict2) == 11,
                             f"The reset column dictionary for row {row_number2} should have the right length")
 
+    def test_no_column_header_and_convert(self):
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                   '../data/model_tests/no_column_header.tsv')
+        hed_input = HedInput(events_path, has_column_names=False, tag_columns=[1, 2])
+        hed_input.convert_to_long(self.hed_schema)
+
+        events_path_long = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                        '../data/model_tests/no_column_header_long.tsv')
+        hed_input_long = HedInput(events_path_long, has_column_names=False, tag_columns=[1, 2])
+        for column1, column2 in zip(hed_input, hed_input_long):
+            self.assertEqual(column1, column2)
+
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                   '../data/model_tests/no_column_header.tsv')
+        hed_input = HedInput(events_path, has_column_names=False, tag_columns=[1, 2])
+        events_path_long = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                        '../data/model_tests/no_column_header_long.tsv')
+        hed_input_long = HedInput(events_path_long, has_column_names=False, tag_columns=[1, 2])
+        hed_input_long.convert_to_short(self.hed_schema)
+        for column1, column2 in zip(hed_input, hed_input_long):
+            self.assertEqual(column1, column2)
+
+    def test_convert_short_long_with_definitions(self):
+        # Verify behavior works as expected even if definitions are present
+        events_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                   '../data/model_tests/no_column_header_definition.tsv')
+        hed_input = HedInput(events_path, has_column_names=False, tag_columns=[1, 2])
+        hed_input.convert_to_long(self.hed_schema)
+
+        events_path_long = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                        '../data/model_tests/no_column_header_definition_long.tsv')
+        hed_input_long = HedInput(events_path_long, has_column_names=False, tag_columns=[1, 2])
+        for column1, column2 in zip(hed_input, hed_input_long):
+            self.assertEqual(column1, column2)
+
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/models/test_hed_group.py b/tests/models/test_hed_group.py
new file mode 100644
index 000000000..b0bff2cd7
--- /dev/null
+++ b/tests/models/test_hed_group.py
@@ -0,0 +1,67 @@
+import unittest
+import os
+
+from hed import schema
+from hed.models import HedString
+
+class Test(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
+        hed_xml_file = os.path.join(cls.base_data_dir, "schema_test_data/HED8.0.0t.xml")
+        cls.hed_schema = schema.load_schema(hed_xml_file)
+
+    def test_remove_groups(self):
+        from hed.models.definition_dict import DefTagNames
+        basic_definition_string = "(Definition/TestDef, (Keypad-key/TestDef1,Keyboard-key/TestDef2))"
+        basic_definition_string_repeated = f"{basic_definition_string},{basic_definition_string}"
+        def_string_with_repeat = HedString(basic_definition_string_repeated, self.hed_schema)
+        definition_tags = def_string_with_repeat.find_tags({DefTagNames.DEFINITION_KEY}, recursive=True, include_groups=1)
+        definition_tag2 = definition_tags[1]
+        def_string_with_repeat.remove([definition_tag2])
+        remaining_children = def_string_with_repeat.get_all_groups()
+        for child in remaining_children:
+            if child is definition_tag2:
+                self.assertFalse(False, "Definition tag not removed successfully")
+
+        basic_definition_string_repeated_subgroup = f"{basic_definition_string},{basic_definition_string}, ({basic_definition_string})"
+        def_string_with_repeat = HedString(basic_definition_string_repeated_subgroup, self.hed_schema)
+        definition_tags = def_string_with_repeat.find_tags({DefTagNames.DEFINITION_KEY}, recursive=True, include_groups=1)
+        definition_tag3 = definition_tags[2]
+        def_string_with_repeat.remove([definition_tag3])
+        remaining_children = def_string_with_repeat.get_all_groups()
+        for child in remaining_children:
+            if child is definition_tag3:
+                self.assertFalse(False, "Nested definition tag not removed successfully")
+
+        basic_definition_string_repeated_subgroup = f"{basic_definition_string},{basic_definition_string}, ({basic_definition_string})"
+        def_string_with_repeat = HedString(basic_definition_string_repeated_subgroup, self.hed_schema)
+        definition_tags = def_string_with_repeat.find_tags({DefTagNames.DEFINITION_KEY}, recursive=True, include_groups=1)
+        definition_tag2 = definition_tags[1]
+        def_string_with_repeat.remove([definition_tag2])
+        remaining_children = def_string_with_repeat.get_all_groups()
+        for child in remaining_children:
+            if child is definition_tag2:
+                self.assertFalse(False, "Nested definition tag not removed successfully")
+
+
+    def test_find_tags_with_term(self):
+        basic_hed_string = "(Keypad-key/TestDef1,Keyboard-key/TestDef2, Item/Object, Event), Event, Object, Geometric-object"
+        basic_hed_string_obj = HedString(basic_hed_string, self.hed_schema)
+        # works
+        located_tags = basic_hed_string_obj.find_tags_with_term("Object", recursive=True, include_groups=0)
+        self.assertEqual(len(located_tags), 5)
+        # located tags now has found all 5 hed tags
+
+        # This will find no tags
+        located_tags = basic_hed_string_obj.find_tags_with_term("bject", recursive=True, include_groups=0)
+        self.assertEqual(len(located_tags), 0)
+
+        # this will also find no tags
+        located_tags = basic_hed_string_obj.find_tags_with_term("Item/Object", recursive=True, include_groups=0)
+        self.assertEqual(len(located_tags), 0)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/models/test_hed_string.py b/tests/models/test_hed_string.py
index ea3de77a6..894668d5e 100644
--- a/tests/models/test_hed_string.py
+++ b/tests/models/test_hed_string.py
@@ -54,12 +54,12 @@ def test_constructor(self):
 class HedTagLists(TestHedStrings):
     def test_type(self):
         hed_string = 'Event/Category/Experimental stimulus,Item/Object/Vehicle/Train,Attribute/Visual/Color/Purple'
-        result = HedString.split_hed_string_into_groups(hed_string)
+        result = HedString.split_into_groups(hed_string)
         self.assertIsInstance(result, list)
 
     def test_top_level_tags(self):
         hed_string = 'Event/Category/Experimental stimulus,Item/Object/Vehicle/Train,Attribute/Visual/Color/Purple'
-        result = HedString.split_hed_string_into_groups(hed_string)
+        result = HedString.split_into_groups(hed_string)
         tags_as_strings = [str(tag) for tag in result]
         self.assertCountEqual(tags_as_strings, ['Event/Category/Experimental stimulus', 'Item/Object/Vehicle/Train',
                                                 'Attribute/Visual/Color/Purple'])
@@ -68,8 +68,8 @@ def test_group_tags(self):
         hed_string = '/Action/Reach/To touch,(/Attribute/Object side/Left,/Participant/Effect/Body part/Arm),' \
                      '/Attribute/Location/Screen/Top/70 px,/Attribute/Location/Screen/Left/23 px '
         string_obj = HedString(hed_string)
-        # result = HedString.split_hed_string_into_groups(hed_string)
-        tags_as_strings = [str(tag) for tag in string_obj.get_direct_children()]
+        # result = HedString.split_into_groups(hed_string)
+        tags_as_strings = [str(tag) for tag in string_obj.children]
         self.assertCountEqual(tags_as_strings,
                               ['/Action/Reach/To touch',
                                '(/Attribute/Object side/Left,/Participant/Effect/Body part/Arm)',
@@ -81,8 +81,8 @@ def test_group_tags(self):
     #     double_quote_string = 'Event/Category/Experimental stimulus,"Item/Object/Vehicle/Train",' \
     #                           'Attribute/Visual/Color/Purple '
     #     normal_string = 'Event/Category/Experimental stimulus,Item/Object/Vehicle/Train,Attribute/Visual/Color/Purple'
-    #     double_quote_result = HedString.split_hed_string_into_groups(double_quote_string)
-    #     normal_result = HedString.split_hed_string_into_groups(normal_string)
+    #     double_quote_result = HedString.split_into_groups(double_quote_string)
+    #     normal_result = HedString.split_into_groups(normal_string)
     #     self.assertEqual(double_quote_result, normal_result)
 
     def test_blanks(self):
@@ -110,7 +110,7 @@ def test_blanks(self):
         }
 
         def test_function(string):
-            return [str(child) for child in HedString.split_hed_string_into_groups(string)]
+            return [str(child) for child in HedString.split_into_groups(string)]
 
         self.validator_list(test_strings, expected_results, test_function)
 
diff --git a/tests/models/test_hed_string_comb.py b/tests/models/test_hed_string_comb.py
new file mode 100644
index 000000000..ec0ffbde4
--- /dev/null
+++ b/tests/models/test_hed_string_comb.py
@@ -0,0 +1,68 @@
+import unittest
+import os
+
+from hed import schema
+from hed.models import HedString
+from hed.models import HedTag
+from hed.models.hed_string_comb import HedStringComb
+import copy
+
+
+class Test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
+        hed_xml_file = os.path.join(cls.base_data_dir, "schema_test_data/HED8.0.0t.xml")
+        cls.hed_schema = schema.load_schema(hed_xml_file)
+
+    def test_remove_groups(self):
+        string1 = HedString("Item/Object", self.hed_schema)
+        string2 = HedString("Event, (Event, Square)", self.hed_schema)
+        comb_string = HedStringComb([string1, string2])
+        self.assertEqual(len(comb_string.get_all_tags()), 4)
+        self.assertEqual(len(string1.get_all_tags()), 1)
+        self.assertEqual(len(string2.get_all_tags()), 3)
+        tags = comb_string.find_tags(["Object".lower()], include_groups=0)
+        comb_string.remove(tags)
+        self.assertEqual(len(string1.get_all_tags()), 0)
+        self.assertEqual(len(string2.get_all_tags()), 3)
+        self.assertEqual(len(comb_string.get_all_tags()), 3)
+
+        tags = comb_string.find_tags(["Event".lower()], recursive=True, include_groups=0)
+        comb_string.remove(tags)
+        self.assertEqual(len(string2.get_all_tags()), 1)
+        self.assertEqual(len(comb_string.get_all_tags()), 1)
+
+        tags = comb_string.find_tags(["Square".lower()], recursive=True, include_groups=0)
+        comb_string.remove(tags)
+        self.assertEqual(len(string2.get_all_tags()), 0)
+        self.assertEqual(len(comb_string.get_all_tags()), 0)
+
+    def test_replace(self):
+        string1 = HedString("Item/Object", self.hed_schema)
+        string2 = HedString("Event, (Event, Square)", self.hed_schema)
+        new_contents = HedTag("Def/TestTag", hed_schema=self.hed_schema)
+        comb_string = HedStringComb([string1, string2])
+        self.assertEqual(len(string1.get_all_tags()), 1)
+        self.assertEqual(len(string2.get_all_tags()), 3)
+        self.assertEqual(len(comb_string.get_all_tags()), 4)
+        tags = comb_string.find_tags(["Object".lower()], include_groups=0)
+        comb_string.replace(tags[0], copy.copy(new_contents))
+
+        self.assertEqual(len(string1.get_all_tags()), 1)
+        self.assertEqual(len(string2.get_all_tags()), 3)
+        self.assertEqual(len(comb_string.get_all_tags()), 4)
+
+        tags = comb_string.find_tags(["Event".lower()], include_groups=0)
+        comb_string.replace(tags[0], copy.copy(new_contents))
+        self.assertEqual(len(string1.get_all_tags()), 1)
+        self.assertEqual(len(string2.get_all_tags()), 3)
+        self.assertEqual(len(comb_string.get_all_tags()), 4)
+
+        tag_group = comb_string.find_tags(["Event".lower()], recursive=True, include_groups=2)
+        tag, group = tag_group[0][0], tag_group[0][1]
+        group.replace(tag, copy.copy(new_contents))
+        self.assertEqual(len(string1.get_all_tags()), 1)
+        self.assertEqual(len(string2.get_all_tags()), 3)
+        self.assertEqual(len(comb_string.get_all_tags()), 4)
+
diff --git a/tests/models/test_onset_mapper.py b/tests/models/test_onset_mapper.py
index 7389918ac..4eaf0209a 100644
--- a/tests/models/test_onset_mapper.py
+++ b/tests/models/test_onset_mapper.py
@@ -2,7 +2,7 @@
 import os
 
 from hed.errors import ErrorHandler, OnsetErrors, ErrorContext, ValidationErrors
-from hed.models import DefinitionMapper, HedString, OnsetMapper, DefDict
+from hed.models import DefMapper, HedString, OnsetMapper, DefinitionDict
 from hed import schema
 from hed.validator import HedValidator
 
@@ -57,10 +57,10 @@ def _test_issues_no_context(self, test_strings, test_issues, hed_ops):
             self.assertCountEqual(onset_issues, issues)
 
     def test_basic_onset_errors(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.validate(def_dict)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         onset_mapper = OnsetMapper(def_mapper)
 
         test_strings = [
@@ -107,10 +107,10 @@ def test_basic_onset_errors(self):
         self._test_issues_base(test_strings, test_issues, expected_context, [onset_mapper])
 
     def test_basic_onset_errors_with_def_mapper(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.validate(def_dict)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         onset_mapper = OnsetMapper(def_mapper)
         hed_ops = [def_mapper, onset_mapper]
 
@@ -159,12 +159,12 @@ def test_basic_onset_errors_with_def_mapper(self):
         self._test_issues_base(test_strings, test_issues, expected_context, hed_ops, expand_defs=False)
 
     def test_basic_onset_errors_expanded(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.validate(def_dict)
         def_string = HedString(self.definition_string)
         def_string.validate(def_dict)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         onset_mapper = OnsetMapper(def_mapper)
 
         test_strings = [
@@ -210,12 +210,12 @@ def test_basic_onset_errors_expanded(self):
         self._test_issues_base(test_strings, test_issues, expected_context, [onset_mapper])
 
     def test_test_interleaving_onset_offset(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.validate(def_dict)
         def_string = HedString(self.definition_string)
         def_string.validate(def_dict)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         onset_mapper = OnsetMapper(def_mapper)
 
         test_strings = [
@@ -251,10 +251,10 @@ def test_test_interleaving_onset_offset(self):
         self._test_issues_base(test_strings, test_issues, expected_context, [onset_mapper])
 
     def test_onset_with_defs_in_them(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.validate(def_dict)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         onset_mapper = OnsetMapper(def_mapper)
 
         test_strings = [
@@ -272,12 +272,12 @@ def test_onset_with_defs_in_them(self):
         self._test_issues_base(test_strings, test_issues, expected_context, [onset_mapper])
 
     def test_onset_multiple_or_misplaced_errors(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.validate(def_dict)
         def_string = HedString(self.definition_string)
         def_string.validate(def_dict)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         onset_mapper = OnsetMapper(def_mapper)
         hed_validator = HedValidator(hed_schema=self.hed_schema)
         hed_ops = [hed_validator, def_mapper, onset_mapper]
@@ -288,8 +288,8 @@ def test_onset_multiple_or_misplaced_errors(self):
             f"({self.placeholder_label_def_string},Onset, Offset)",
         ]
         # count of issues the line generates
-        onset_list = ['Property/Data-property/Data-marker/Temporal-marker/Onset']
-        offset_list = ['Property/Data-property/Data-marker/Temporal-marker/Offset']
+        onset_list = ['Onset']
+        offset_list = ['Offset']
         test_issues = [
             self.format_error(ValidationErrors.HED_TOP_LEVEL_TAG, tag=1),
             self.format_error(ValidationErrors.HED_TAG_REPEATED, tag=2)
@@ -314,12 +314,12 @@ def test_onset_multiple_or_misplaced_errors(self):
         self._test_issues_no_context(test_strings, test_issues, hed_validator)
 
     def test_onset_multiple_or_misplaced_errors_no_validator(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.validate(def_dict)
         def_string = HedString(self.definition_string)
         def_string.validate(def_dict)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         onset_mapper = OnsetMapper(def_mapper)
         hed_ops = [def_mapper, onset_mapper]
 
@@ -356,12 +356,12 @@ def test_onset_multiple_or_misplaced_errors_no_validator(self):
         self._test_issues_no_context(test_strings, test_issues, [hed_ops[1]])
 
     def test_onset_two_in_one_line(self):
-        def_dict = DefDict()
+        def_dict = DefinitionDict()
         def_string = HedString(self.placeholder_definition_string)
         def_string.validate(def_dict)
         def_string = HedString(self.definition_string)
         def_string.validate(def_dict)
-        def_mapper = DefinitionMapper(def_dict)
+        def_mapper = DefMapper(def_dict)
         onset_mapper = OnsetMapper(def_mapper)
 
         test_strings = [
diff --git a/tests/models/test_sidecar.py b/tests/models/test_sidecar.py
index 760c55026..51e729ff9 100644
--- a/tests/models/test_sidecar.py
+++ b/tests/models/test_sidecar.py
@@ -6,7 +6,7 @@
 from hed.models import ColumnMetadata, HedString, Sidecar
 from hed.validator import HedValidator
 from hed import schema
-from hed.models import DefDict
+from hed.models import DefinitionDict
 
 
 class Test(unittest.TestCase):
@@ -104,7 +104,7 @@ def test_validate_column_group(self):
         self.assertEqual(len(validation_issues), 1)
 
         hed_string = HedString("(Definition/JsonFileDef/#, (Item/JsonDef1/#,Item/JsonDef1))")
-        extra_def_dict = DefDict()
+        extra_def_dict = DefinitionDict()
         hed_string.validate(extra_def_dict)
 
         validation_issues = self.json_without_definitions_sidecar.validate_entries(validator, check_for_warnings=True,
diff --git a/tests/models/tests_expression_parser.py b/tests/models/tests_expression_parser.py
new file mode 100644
index 000000000..06c400dc7
--- /dev/null
+++ b/tests/models/tests_expression_parser.py
@@ -0,0 +1,375 @@
+import unittest
+from hed.models.hed_string import HedStringFrozen
+from hed.models.expression_parser import TagExpressionParser
+import os
+from hed import schema
+
+
+class TestParser(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.base_data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../data/')
+        hed_xml_file = os.path.join(cls.base_data_dir, "schema_test_data/HED8.0.0t.xml")
+        cls.hed_schema = schema.load_schema(hed_xml_file)
+
+    def base_test(self, parse_expr, search_strings):
+        expression = TagExpressionParser(parse_expr)
+
+        # print(f"Search Pattern: {expression._org_string}")
+        for string, expected_result in search_strings.items():
+            hed_string = HedStringFrozen(string, self.hed_schema)
+            result = expression.search_hed_string(hed_string)
+            # print(f"\tSearching string '{str(hed_string)}'")
+            # if result:
+            #    print(f"\t\tFound as group(s) {str([str(r) for r in result])}")
+            self.assertEqual(bool(result), expected_result)
+
+    def test_broken_search_strings(self):
+        test_search_strings = [
+            "A and",
+            "(A and B",
+            "and B",
+            "A, ",
+            ", A",
+            "A)"
+        ]
+        for string in test_search_strings:
+            try:
+                parser = TagExpressionParser(string)
+                self.assertFalse(True)
+            except ValueError:
+                continue
+
+    def test_finding_tags(self):
+        test_strings = {
+            "Item, (Clear-throat)": True,
+            "(Item, (Clear-throat))": True,
+            "Item, Clear-throat": True,
+            "Agent, Clear-throat": True,
+            "Agent, Event": False,
+        }
+        self.base_test("(Item or Agent) and Action", test_strings)
+
+    def test_finding_tags2(self):
+        test_strings = {
+            "Item, (Clear-throat)": True,
+            "(Item, (Clear-throat))": True,
+            "Item, Clear-throat": False,
+            "Agent, Clear-throat": False,
+            "Agent, Event": False,
+            "Agent, (Event)": True,
+            "(Item), (Event)": True
+        }
+        self.base_test("(Item or Agent) and [[Action or Event]]", test_strings)
+
+    def test_exact_group(self):
+        test_strings = {
+            "A, B": False,
+            "(A, B)": True,
+            "(A, (B))": False,
+            "(A, (B, C))": False,
+            "(A), (A, B)": True,
+            "(A, B), (A)": True,
+        }
+        self.base_test("[[a, b]]", test_strings)
+
+    def test_exact_group_simple_complex(self):
+        test_strings = {
+            "(A, C)": False,
+            "(A, (C))": True,
+            "((A, C))": False,
+            "A, B, C, D": False,
+            "(A, B, C, D)": False,
+            "(A, B, (C, D))": True,
+            "(A, B, ((C, D)))": False,
+            "(E, F, (A, B, (C, D)))": True,
+            "(A, B, (E, F, (C, D)))": False,  # TODO: Should this be True?  [[c]] isn't directly inside an a group.
+        }
+        self.base_test("[[a, [[c]] ]]", test_strings)
+
+    def test_exact_group_complex(self):
+        test_strings = {
+            "A, B, C, D": False,
+            "(A, B, C, D)": False,
+            "(A, B, (C, D))": True,
+            "(A, B, ((C, D)))": False,
+            "(E, F, (A, B, (C, D)))": True,
+        }
+        self.base_test("[[a, b, [[c, d]] ]]", test_strings)
+
+    def test_exact_group_complex_split(self):
+        test_strings = {
+            "A, B, C, D": False,
+            "(A, B, C, D)": False,
+            "(A, B, (C, D))": False,
+            "(A, B, ((C, D)))": False,
+            "(E, F, (A, B, (C, D)))": False,
+            "((A, B), (C, D))": True,
+        }
+        self.base_test("[[ [[a, b]], [[c, d]] ]]", test_strings)
+
+    def test_mixed_group_split(self):
+        test_strings = {
+            "(A, D)": False,
+            "((A), (D))": True,
+            "((A), ((D)))": True,
+            "((A, D))": True,  # TODO: should this be true?  Probably not.
+        }
+        self.base_test("[[ [a], [d] ]]", test_strings)
+
+    def test_exact_group_split(self):
+        test_strings = {
+            "(A, D)": False,
+            "((A), (D))": True,
+            "((A), ((D)))": False,
+            "((A, D))": True,  # TODO: should this be true?  Probably not.
+        }
+        self.base_test("[[ [[a]], [[d]] ]]", test_strings)
+
+    def test_exact_group_split_or(self):
+        test_strings = {
+            "(A, D)": False,
+            "((A), (D))": True,
+            "((A), ((D)))": True,
+            "((A, D))": True,
+        }
+        self.base_test("[[ [[a]] or [[d]] ]]", test_strings)
+
+    def test_exact_group_split_or_negation(self):
+        test_strings = {
+            "(A, D)": False,
+            "((A), (D))": True,
+            "((A))": False,
+            "((A), ((D)))": True,
+            "((A, D))": False,
+        }
+        self.base_test("[[ [[~a]] ]]", test_strings)
+
+    def test_exact_group_split_or_negation_dual(self):
+        test_strings = {
+            "(A, B)": False,
+            "((A), (B))": False,
+            "((A))": False,
+            "((A), ((B)))": True,  # TODO: must all result groups have tags?  True because of ((B)) group with no tags.
+            "((A, B))": False,
+            "((A), (C))": True,
+            "((A), (B, C))": False,
+            "((A), ((B), C))": True,
+        }
+        self.base_test("[[ [[~a and ~b]] ]]", test_strings)
+
+    def test_exact_group_split_or_negation_dual2(self):
+        test_strings = {
+            "(A, B)": False,
+            "((A), (B))": False,
+            "((A))": False,
+            "((A), ((B)))": True,
+            "((A, B))": False,
+            "((A), (C))": True,
+            "((A), (B, C))": False,
+        }
+        self.base_test("[[ [[~(a or b)]] ]]", test_strings)
+
+    def test_exact_group_split_or_negation_complex(self):
+        test_strings = {
+            "(A, B), (D)": False,
+            "((A), (B)), (D)": False,
+            "((A)), (D)": False,
+            "((A), ((B))), (D)": True,
+            "((A), ((B))), (H)": True,
+            "((A, B)), (D)": False,
+            "((A), (C)), (D)": True,
+            "((A), (B, C)), (D)": False,
+            "((A), (B, C)), (H)": False,
+        }
+        self.base_test("[[ [[~(a or b)]] ]] and [[D or ~F]]", test_strings)
+
+    # TODO: Should this work, and what should it mean?
+    # Right now this is always true, since there is at least one group without ", (a)" in every string.
+    def test_exact_group_negation(self):
+        test_strings = {
+            "(A, D)": True,
+            "((A), (D))": True,
+            "((A))": True,
+            "((A), ((D)))": True,
+            "((A, D))": True,
+        }
+        self.base_test("[[ ~[[a]] ]]", test_strings)
+
+    def test_exact_group_negation2(self):
+        test_strings = {
+            "(A, D, B)": True,
+            "((A), (D), B)": False,
+            "((A))": False,
+            "((A), ((D, B)))": True,
+            "((A, D))": False,
+        }
+        self.base_test("[[ ~[[a]], b]]", test_strings)
+
+    def test_mixed_group_complex_split(self):
+        test_strings = {
+            "A, B, C, D": False,
+            "(A, B), (C, D)": False,
+            "(A, B, C, D)": False,
+            "(A, B, (C, D))": False,
+            "(A, B, ((C, D)))": False,
+            "(E, F, (A, B, (C, D)))": True,
+            "((A, B), (C, D))": True,
+            "((A, B, C, D))": True,  # TODO: should this be true?  Probably not.
+        }
+        self.base_test("[[ [a, b], [c, d] ]]", test_strings)
+
+    def test_exact_group_complex2(self):
+        test_strings = {
+            "A, B, C": False,
+            "(A, B, C)": False,
+            "(A, B, (C)), (A, B)": True,
+            "(A, B), (A, B, (C))": True,
+            "(A, B), (B, (C))": False,
+            "(B, (C)), (A, B, (C))": True
+        }
+        self.base_test("[[a, b, [[c]] ]]", test_strings)
+
+    def test_containing_group_complex2(self):
+        test_strings = {
+            "A, B, C": False,
+            "(A, B, C)": False,
+            "(A, B, (C)), (A, B)": True,
+            "(A, B), (A, B, (C))": True,
+            "(A, B), (B, (C))": False,
+            "(B, (C)), (A, B, (C))": True
+        }
+        self.base_test("[a, b, [c] ]", test_strings)
+
+    def test_containing_group(self):
+        test_strings = {
+            "A, B": False,
+            "(A, B)": True,
+            "(A, B), (A, B)": True,
+            "(A, (B))": True,
+            "(A, (B, C))": True,
+            "(A), (B)": False
+        }
+        self.base_test("[a, b]", test_strings)
+
+    def test_containing_group_simple_complex(self):
+        test_strings = {
+            "A, B, C, D": False,
+            "(A, C)": False,
+            "(A, B, (C, D))": True,
+            "(A, (B))": False,
+            "(A, (C))": True,
+            "(A, (B, C))": True,
+            "(A), (B)": False,
+            "(C, (A))": False,
+            "(A, ((C)))": True
+        }
+        self.base_test("[a, [c] ]", test_strings)
+
+    def test_containing_group_complex(self):
+        test_strings = {
+            "A, B, C, D": False,
+            "(A, B, C, D)": False,
+            "(A, B, (C, D))": True,
+            "(A, (B))": False,
+            "(A, (B, C))": False,
+            "(A), (B)": False
+        }
+        self.base_test("[a, b, [c, d] ]", test_strings)
+
+    def test_mixed_groups(self):
+        test_strings = {
+            "(A, B), (C, D, (E, F))": True
+        }
+        self.base_test("[[a]], [[ [[e, f]] ]]", test_strings)
+
+        # This example works because it finds the group containing (c, d, (e, f)), rather than the ef group
+        self.base_test("[[a]], [e, [[f]] ]", test_strings)
+
+    def test_and_or(self):
+        test_strings = {
+            "A": False,
+            "B": False,
+            "C": True,
+            "A, B": True,
+            "A, C": True,
+            "B, C": True
+        }
+        self.base_test("a and b or c", test_strings)
+
+        test_strings = {
+            "A": False,
+            "B": False,
+            "C": True,
+            "A, B": True,
+            "A, C": True,
+            "B, C": True
+        }
+        self.base_test("(a and b) or c", test_strings)
+
+        test_strings = {
+            "A": False,
+            "B": False,
+            "C": False,
+            "A, B": True,
+            "A, C": True,
+            "B, C": False
+        }
+        self.base_test("a and (b or c)", test_strings)
+
+        test_strings = {
+            "A": False,
+            "B": False,
+            "C": False,
+            "A, B": False,
+            "A, C": True,
+            "B, C": True
+        }
+        self.base_test("a or b and c", test_strings)
+
+        test_strings = {
+            "A": True,
+            "B": False,
+            "C": False,
+            "A, B": True,
+            "A, C": True,
+            "B, C": True
+        }
+        self.base_test("a or (b and c)", test_strings)
+
+        test_strings = {
+            "A": False,
+            "B": False,
+            "C": False,
+            "A, B": False,
+            "A, C": True,
+            "B, C": True
+        }
+        self.base_test("(a or b) and c", test_strings)
+
+    def test_logical_negation(self):
+        expression = TagExpressionParser("~a")
+        hed_string = HedStringFrozen("A")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), False)
+        hed_string = HedStringFrozen("B")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), True)
+
+        expression = TagExpressionParser("~a and b")
+        hed_string = HedStringFrozen("A")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), False)
+        hed_string = HedStringFrozen("B")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), True)
+        hed_string = HedStringFrozen("A, B")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), False)
+
+        expression = TagExpressionParser("~( (a or b) and c)")
+        hed_string = HedStringFrozen("A")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), True)
+        hed_string = HedStringFrozen("B")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), True)
+        hed_string = HedStringFrozen("C")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), True)
+        hed_string = HedStringFrozen("A, B")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), True)
+        hed_string = HedStringFrozen("A, C")
+        self.assertEqual(bool(expression.search_hed_string(hed_string)), False)
diff --git a/tests/schema/test_convert_tags.py b/tests/schema/test_convert_tags.py
index b1b677da7..058d23fd3 100644
--- a/tests/schema/test_convert_tags.py
+++ b/tests/schema/test_convert_tags.py
@@ -66,16 +66,29 @@ def test_tag(self):
 
     def test_tag_takes_value(self):
         test_strings = {
+            # these should all be allowed by initial identifiers.  Validators should flag the second two for '/' not being an allowed char.
+            'singleNodeTakesPlaceholder': 'Item-count/#',
+            'existingNodeValue': 'Label/Red',
+            'existingNodeSubValue': 'Label/UniqueValue/Red',
+            'existingNodeSubValue2': 'Label/Red/UniqueValue',
             'uniqueValue': 'Label/Unique Value',
             'multiLevel': 'Label/Long Unique Value With/Slash Marks',
             'partialPath': 'Informational-property/Label/Unique Value',
         }
         expected_results = {
+            'singleNodeTakesPlaceholder': 'Property/Data-property/Data-value/Quantitative-value/Item-count/#',
+            'existingNodeValue': 'Property/Informational-property/Label/Red',
+            'existingNodeSubValue': 'Property/Informational-property/Label/UniqueValue/Red',
+            'existingNodeSubValue2': 'Property/Informational-property/Label/Red/UniqueValue',
             'uniqueValue': 'Property/Informational-property/Label/Unique Value',
             'multiLevel': 'Property/Informational-property/Label/Long Unique Value With/Slash Marks',
             'partialPath': 'Property/Informational-property/Label/Unique Value',
         }
         expected_errors = {
+            'singleNodeTakesPlaceholder': [],
+            'existingNodeValue': [],
+            'existingNodeSubValue': [],
+            'existingNodeSubValue2': [],
             'uniqueValue': [],
             'multiLevel': [],
             'partialPath': [],
@@ -142,22 +155,13 @@ def test_tag_invalid_extension(self):
                 self.format_error(ValidationErrors.INVALID_PARENT_NODE, tag=0,
                                   index_in_tag=25, index_in_tag_end=41,
                                   expected_parent_tag='Item/Object/Geometric-object'),
-            # old errors if we're doing left to right
-            # 'twoLevels':
-            #     self.format_error(ValidationErrors.INVALID_PARENT_NODE, tag=0,
-            #                                      index_in_tag=19, index_in_tag_end=35,
-            #                                      expected_parent_tag='Item/Object/Geometric-object'),
-            # 'partialDuplicate':
-            #     self.format_error(ValidationErrors.INVALID_PARENT_NODE, tag=0,
-            #                                      index_in_tag=10, index_in_tag_end=14, expected_parent_tag='Item'),
             'twoLevels':
                 self.format_error(ValidationErrors.INVALID_PARENT_NODE, tag=0,
-                                  index_in_tag=36, index_in_tag_end=41,
-                                  expected_parent_tag='Event'),
+                                                 index_in_tag=19, index_in_tag_end=35,
+                                                 expected_parent_tag='Item/Object/Geometric-object'),
             'partialDuplicate':
                 self.format_error(ValidationErrors.INVALID_PARENT_NODE, tag=0,
-                                  index_in_tag=29, index_in_tag_end=45,
-                                  expected_parent_tag='Item/Object/Geometric-object')
+                                                 index_in_tag=17, index_in_tag_end=21, expected_parent_tag='Item'),
         }
         self.validator(test_strings, expected_results, expected_errors)
 
@@ -165,11 +169,13 @@ def test_tag_invalid(self):
         test_strings = {
             'single': 'InvalidEvent',
             'invalidChild': 'InvalidEvent/InvalidExtension',
+            'invalidChildValidParent': 'Event/Event',
             'validChild': 'InvalidEvent/Event',
         }
         expected_results = {
             'single': 'InvalidEvent',
             'invalidChild': 'InvalidEvent/InvalidExtension',
+            'invalidChildValidParent': 'Event/Event',
             'validChild': 'InvalidEvent/Event',
         }
         expected_errors = {
@@ -177,10 +183,11 @@ def test_tag_invalid(self):
                                         tag=0, index_in_tag=0, index_in_tag_end=12),
             'invalidChild': self.format_error(ValidationErrors.NO_VALID_TAG_FOUND,
                                               tag=0, index_in_tag=0, index_in_tag_end=12),
-
-            'validChild': self.format_error(ValidationErrors.INVALID_PARENT_NODE,
-                                            tag=0, index_in_tag=13, index_in_tag_end=18,
+            'invalidChildValidParent': self.format_error(ValidationErrors.INVALID_PARENT_NODE,
+                                            tag=0, index_in_tag=6, index_in_tag_end=11,
                                             expected_parent_tag="Event"),
+            'validChild': self.format_error(ValidationErrors.NO_VALID_TAG_FOUND,
+                                        tag=0, index_in_tag=0, index_in_tag_end=12),
         }
         self.validator(test_strings, expected_results, expected_errors)
 
@@ -241,6 +248,8 @@ def test_tag_takes_value(self):
             'placeholder': 'Informational-property/Label/#',
             # Todo: This should maybe be disallowed, but for now just make sure it works as expected
             'placeholderAfterValue': 'Informational-property/Label/UniqueValue/#',
+            'singleLevelKnown': 'Property/Informational-property/Label/Event',
+            'multiLevelKnown': 'Property/Informational-property/Label/Event/Sensory-event',
         }
         expected_results = {
             'uniqueValue': 'Label/Unique Value',
@@ -248,40 +257,17 @@ def test_tag_takes_value(self):
             'partialPath': 'Label/Unique Value',
             'placeholder': 'Label/#',
             'placeholderAfterValue': 'Label/UniqueValue/#',
+            'singleLevelKnown': 'Label/Event',
+            'multiLevelKnown': 'Label/Event/Sensory-event',
         }
         expected_errors = {
             'uniqueValue': [],
             'multiLevel': [],
             'partialPath': [],
             'placeholder': [],
-            'placeholderAfterValue': []
-        }
-        self.validator(test_strings, expected_results, expected_errors)
-
-    def test_tag_takes_value_invalid(self):
-        test_strings = {
-            'singleLevel': 'Property/Informational-property/Label/Event',
-            'multiLevel': 'Property/Informational-property/Label/Event/Sensory-event',
-            'mixed': 'Item/Sound/Event/Sensory-event/Environmental-sound',
-        }
-        expected_results = {
-            'singleLevel': 'Property/Informational-property/Label/Event',
-            'multiLevel': 'Property/Informational-property/Label/Event/Sensory-event',
-            'mixed': 'Item/Sound/Event/Sensory-event/Environmental-sound',
-        }
-        expected_errors = {
-            'singleLevel':
-                self.format_error(ValidationErrors.INVALID_PARENT_NODE,
-                                  tag=0, index_in_tag=38, index_in_tag_end=43,
-                                  expected_parent_tag='Event'),
-            'multiLevel':
-                self.format_error(ValidationErrors.INVALID_PARENT_NODE,
-                                  tag=0, index_in_tag=44, index_in_tag_end=57,
-                                  expected_parent_tag='Event/Sensory-event'),
-            'mixed':
-                self.format_error(ValidationErrors.INVALID_PARENT_NODE,
-                                  tag=0, index_in_tag=31, index_in_tag_end=50,
-                                  expected_parent_tag='Item/Sound/Environmental-sound'),
+            'placeholderAfterValue': [],
+            'singleLevelKnown': [],
+            'multiLevelKnown': [],
         }
         self.validator(test_strings, expected_results, expected_errors)
 
@@ -345,11 +331,11 @@ def test_tag_invalid_extension(self):
                                   tag=0, index_in_tag=19, index_in_tag_end=35,
                                   expected_parent_tag='Item/Object/Geometric-object'),
             'twoLevels': self.format_error(ValidationErrors.INVALID_PARENT_NODE,
-                                           tag=0, index_in_tag=42, index_in_tag_end=47,
-                                           expected_parent_tag='Event'),
+                                           tag=0, index_in_tag=25, index_in_tag_end=41,
+                                           expected_parent_tag='Item/Object/Geometric-object'),
             'duplicate': self.format_error(ValidationErrors.INVALID_PARENT_NODE,
-                                           tag=0, index_in_tag=41, index_in_tag_end=57,
-                                           expected_parent_tag='Item/Object/Geometric-object')
+                                           tag=0, index_in_tag=29, index_in_tag_end=33,
+                                           expected_parent_tag='Item')
         }
         self.validator(test_strings, expected_results, expected_errors)
 
@@ -369,31 +355,31 @@ def test_tag_invalid(self):
             'invalidWithExtension': 'InvalidEvent/InvalidExtension',
         }
         expected_errors = {
+            # Old errors if we go back to right to left processing
+            # 'invalidParentWithExistingGrandchild': self.format_error(
+            #     ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=32, index_in_tag_end=48,
+            #     expected_parent_tag="Item/Object/Geometric-object"),
+            # 'invalidChildWithExistingGrandchild': self.format_error(
+            #     ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=19, index_in_tag_end=35,
+            #     expected_parent_tag="Item/Object/Geometric-object"),
+            # 'invalidParentWithExistingChild': self.format_error(
+            #     ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=13, index_in_tag_end=29,
+            #     expected_parent_tag="Item/Object/Geometric-object"),
+            # 'invalidSingle': self.format_error(
+            #     ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
+            # 'invalidWithExtension': self.format_error(
+            #     ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
             'invalidParentWithExistingGrandchild': self.format_error(
-                ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=32, index_in_tag_end=48,
-                expected_parent_tag="Item/Object/Geometric-object"),
+                ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
             'invalidChildWithExistingGrandchild': self.format_error(
                 ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=19, index_in_tag_end=35,
                 expected_parent_tag="Item/Object/Geometric-object"),
             'invalidParentWithExistingChild': self.format_error(
-                ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=13, index_in_tag_end=29,
-                expected_parent_tag="Item/Object/Geometric-object"),
+                ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
             'invalidSingle': self.format_error(
                 ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
             'invalidWithExtension': self.format_error(
                 ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
-            # Old errors if we go back to left to right processing.
-            # 'invalidParentWithExistingGrandchild': self.format_error(
-            #     ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
-            # 'invalidChildWithExistingGrandchild': self.format_error(
-            #     ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=19, index_in_tag_end=28,
-            #     expected_parent_tag="Item/Object/Geometric-object"),
-            # 'invalidParentWithExistingChild': self.format_error(
-            #     ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
-            # 'invalidSingle': self.format_error(
-            #     ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
-            # 'invalidWithExtension': self.format_error(
-            #     ValidationErrors.NO_VALID_TAG_FOUND, tag=0, index_in_tag=0, index_in_tag_end=12),
         }
         self.validator(test_strings, expected_results, expected_errors)
 
diff --git a/tests/tools/annotation/test_annotation_util.py b/tests/tools/annotation/test_annotation_util.py
index 792f416f1..886dc4a2c 100644
--- a/tests/tools/annotation/test_annotation_util.py
+++ b/tests/tools/annotation/test_annotation_util.py
@@ -5,10 +5,10 @@
 from pandas import DataFrame
 from hed import schema as hedschema
 from hed.models import Sidecar
-from hed.tools import BidsTsvSummary, check_df_columns, df_to_hed, extract_tags, generate_sidecar_entry, \
-    hed_to_df, merge_hed_dict
+from hed.tools import BidsTsvSummary, check_df_columns, df_to_hed, extract_tags, hed_to_df, merge_hed_dict
 from hed.tools.annotation.annotation_util import _find_last_pos, _find_first_pos, \
-    _flatten_cat_col, _flatten_val_col, _get_value_entry, trim_back, trim_front, _tag_list_to_str, _update_cat_dict
+    _flatten_cat_col, _flatten_val_col, _get_value_entry, trim_back, trim_front, _tag_list_to_str, _update_cat_dict, \
+    generate_sidecar_entry
 from hed.util import get_file_list
 from hed.validator import HedValidator
 
@@ -173,6 +173,21 @@ def test_generate_sidecar_entry(self):
         self.assertIsInstance(entry2['HED'], str,
                               "generate_sidecar_entry HED entry should be str when no column values")
 
+    def test_generate_sidecar_entry_non_letters(self):
+        entry1 = generate_sidecar_entry('my !#$-123_10', column_values=['apple 1', '@banana', 'grape%cherry&'])
+        self.assertIsInstance(entry1, dict,
+                              "generate_sidecar_entry is a dictionary when column values and special chars.")
+        self.assertIn('HED', entry1,
+                      "generate_sidecar_entry has a HED key when column values and special chars")
+        hed_entry1 = entry1['HED']
+        self.assertEqual(hed_entry1['apple 1'], '(Label/my_-123_10, Label/apple_1)',
+                         "generate_sidecar_entry HED entry should convert labels correctly when column values")
+        entry2 = generate_sidecar_entry('my !#$-123_10')
+        self.assertIsInstance(entry2, dict,
+                              "generate_sidecar_entry is a dictionary when no column values and special chars.")
+        self.assertEqual(entry2['HED'], '(Label/my_-123_10, Label/#)',
+                         "generate_sidecar_entry HED entry has correct label when no column values and special chars.")
+
     def test_hed_to_df(self):
         df1a = hed_to_df(self.sidecar1a, col_names=None)
         self.assertIsInstance(df1a, DataFrame)
diff --git a/tests/tools/annotation/test_key_template.py b/tests/tools/annotation/test_key_template.py
index 0b057c6a7..b47c6bfc4 100644
--- a/tests/tools/annotation/test_key_template.py
+++ b/tests/tools/annotation/test_key_template.py
@@ -111,14 +111,15 @@ def test_update_map_duplicate_keys(self):
         self.assertEqual(len(t_map.count_dict), len(t_map.map_dict),
                          "The count dictionary and key dictionary should have same number of values")
 
-    def test_print(self):
-        from io import StringIO
-        t_map = KeyTemplate(self.key_cols+self.target_cols)
-        t_map.update(self.stern_map_path)
+    def test__str__(self):
+        t_map = KeyTemplate(self.key_cols+self.target_cols, name="Test map")
+        map_str1 = str(t_map)
+
+        self.assertTrue(map_str1, "__str__ should return a non-empty string")
+        self.assertEqual(-1, map_str1.find("\n"), "__str__ should not have a new line if no contents")
         t_map.update(self.stern_map_path)
-        with mock.patch('sys.stdout', new=StringIO()):
-            t_map.print()
-            print("This should be eaten by the StringIO")
+        map_str2 = str(t_map)
+        self.assertTrue(map_str2, "__str__ should return a non-empty string")
 
     def test_update_map_missing_key(self):
         keys = self.key_cols + ['another']
diff --git a/tests/tools/annotation/test_summary_entry.py b/tests/tools/annotation/test_summary_entry.py
deleted file mode 100644
index caa6b6240..000000000
--- a/tests/tools/annotation/test_summary_entry.py
+++ /dev/null
@@ -1,116 +0,0 @@
-import unittest
-from hed.models.hed_group import HedGroup
-from hed.models.hed_string import HedString
-from hed.schema.hed_schema_io import load_schema_version
-from hed.tools.annotation.summary_entry import SummaryEntry
-
-
-class Test(unittest.TestCase):
-
-    @classmethod
-    def setUpClass(cls):
-        cls.schema = load_schema_version(xml_version="8.0.0")
-        cls.str1 = f"(Task,Experiment-participant,(See,Face),(Discriminate,(Face, Symmetrical))," \
-                   f"(Press,Keyboard-key),Description/Evaluate degree of image symmetry and respond with key" \
-                   f"press evaluation.)"
-        cls.str2 = f"Sensory-event, (Intended-effect, Cue), (Def/Cross-only, Onset), (Def/Fixation-task, Onset)," \
-                   f"(Def/Circle-only, Offset)"
-        cls.str_with_def = f"(Definition/First-show-cond, ((Condition-variable/Repetition-type," \
-                           f" (Item-count/1, Face), Item-interval/0), " \
-                           f"Description/Factor level indicating the first display of this face.))"
-        cls.str_with_defs = f"(Definition/First-show-cond, ((Condition-variable/Repetition-type," \
-                            f" (Item-count/1, Face), Item-interval/0), " \
-                            f"Description/Factor level indicating the first display of this face.))" \
-                            f"(Definition/Immediate-repeat-cond, ((Condition-variable/Repetition-type, " \
-                            f"(Item-count/2, Face), Item-interval/1), " \
-                            f"Description/Factor level indicating this face was the same as previous one.))" \
-                            f"(Definition/Delayed-repeat-cond, (Condition-variable/Repetition-type, " \
-                            f"(Item-count/2, Face), (Item-interval, (Greater-than-or-equal-to, Item-interval/5)), " \
-                            f"Description/Factor level indicating face was seen 5 to 15 trials ago.))"
-        cls.str_with_def_expand = f"(Def-expand/First-show-cond, ((Condition-variable/Repetition-type," \
-                                  f" (Item-count/1, Face), Item-interval/0), " \
-                                  f"Description/Factor level indicating the first display of this face.))"
-
-    def test_constructor(self):
-        hed1 = HedString(self.str1)
-        hed1.convert_to_canonical_forms(self.schema)
-        entry1 = SummaryEntry("Face-symmetry-evaluation-task", hed1)
-        self.assertIsInstance(entry1, SummaryEntry, "SummaryEntry constructor should create an object")
-        self.assertEqual(entry1.name, "Face-symmetry-evaluation-task",
-                         "SummaryEntry constructor should have the right name")
-        self.assertIsInstance(entry1.group, HedGroup, "SummaryEntry constructor group should be a HedGroup")
-        self.assertTrue(entry1.tag_dict, "SummaryEntry constructor group should have a tag dictionary until set")
-        self.assertTrue('Description' in entry1.tag_dict, "SummaryEntry should not parse values when schema is None")
-
-        entry2 = SummaryEntry("Dummy", HedGroup(""))
-        self.assertIsInstance(entry2, SummaryEntry,
-                              "SummaryEntry constructor should create an object when contents are empty")
-        self.assertEqual(entry2.name, "Dummy",
-                         "SummaryEntry constructor should have the right name when contents empty")
-        self.assertFalse(entry2.group, "SummaryEntry constructor group should be a None when contents empty")
-        self.assertFalse(entry2.tag_dict, "SummaryEntry should not have a tag dictionary when contents empty")
-
-        entry3 = SummaryEntry("NoSchema",  HedString(self.str1))
-        self.assertIsInstance(entry3, SummaryEntry,
-                              "SummaryEntry constructor should create an object when schema is None")
-        self.assertEqual(entry3.name, "NoSchema",
-                         "SummaryEntry constructor should have the right name when schema is None")
-        self.assertTrue(entry3.group, "SummaryEntry constructor group should be non-empty when schema is None")
-        self.assertTrue(entry3.tag_dict, "SummaryEntry should have a tag dictionary when schema is None")
-        self.assertFalse('Description' in entry3.tag_dict, "SummaryEntry should not parse values when schema is None")
-
-    def test_extract_anchored_group(self):
-        hed1 = HedString(self.str1)
-        hed1.convert_to_canonical_forms(self.schema)
-        no_entry = SummaryEntry.extract_anchored_group(hed1)
-        self.assertFalse(no_entry, "extract_anchored_group should return None if no anchored group")
-
-        hed2 = HedString(self.str_with_def)
-        hed2.convert_to_canonical_forms(self.schema)
-        groups2 = hed2.groups()
-        definition_entry = SummaryEntry.extract_anchored_group(groups2[0])
-        self.assertIsInstance(definition_entry, SummaryEntry,
-                              "extract_anchored_group should return a SummaryEntry if anchored group")
-
-        hed3 = HedString(self.str_with_def_expand)
-        hed3.convert_to_canonical_forms(self.schema)
-        groups3 = hed3.groups()
-        def_expand_entry = SummaryEntry.extract_anchored_group(groups3[0], "def-expand")
-        self.assertIsInstance(def_expand_entry, SummaryEntry,
-                              "extract_anchored_group should return a SummaryEntry if definition not the anchor")
-
-        def_expand_entry_case = SummaryEntry.extract_anchored_group(groups3[0], "Def-expand")
-        self.assertIsInstance(def_expand_entry_case, SummaryEntry,
-                              "extract_anchored_group should return a SummaryEntry anchor and is not case sensitive")
-
-    def test_extract_anchored_groups(self):
-        hed1 = HedString(self.str1)
-        hed1.convert_to_canonical_forms(self.schema)
-        entry_dict1 = {}
-        hed1_tags = SummaryEntry.separate_anchored_groups(hed1, entry_dict1, anchor_name="definition")
-        self.assertIsInstance(hed1_tags, list, "separate_anchored_groups returns a list when no anchor")
-        self.assertEqual(len(hed1_tags), 1, "separate_anchored_groups returns a list of right length when no anchor")
-        self.assertFalse(entry_dict1, "separate_anchored_groups returns an empty dictionary if no anchored groups")
-
-        hed2 = HedString(self.str2)
-        hed2.convert_to_canonical_forms(self.schema)
-        entry_dict2 = {}
-        hed2_tags = SummaryEntry.separate_anchored_groups(hed2, entry_dict2, anchor_name="definition")
-        self.assertIsInstance(hed2_tags, list, "separate_anchored_groups returns a list when no anchor and tags")
-        self.assertEqual(len(hed2_tags), 5,
-                         "separate_anchored_groups returns a list of right length when no anchor and tags")
-
-        hed3 = HedString(self.str_with_defs)
-        hed3.convert_to_canonical_forms(self.schema)
-        entry_dict3 = {}
-        hed3_tags = SummaryEntry.separate_anchored_groups(hed3, entry_dict3, anchor_name="definition")
-        self.assertIsInstance(hed3_tags, list,
-                              "separate_anchored_groups returns a list when multiple definitions no tags")
-        self.assertEqual(len(hed3_tags), 0,
-                         "separate_anchored_groups returns a list of right length when no anchor and tags")
-        self.assertEqual(len(entry_dict3), 3,
-                         "separate_anchored_groups returns a anchor dictionary of right length when definitions")
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/tools/bids/test_bids_tsv_summary.py b/tests/tools/bids/test_bids_tsv_summary.py
index c92a3aa34..ca5e82f36 100644
--- a/tests/tools/bids/test_bids_tsv_summary.py
+++ b/tests/tools/bids/test_bids_tsv_summary.py
@@ -43,17 +43,16 @@ def test_get_number_unique_values(self):
         self.assertEqual(len(count_dict), 10, "get_number_unique_values should have the correct number of entries")
         self.assertEqual(count_dict['onset'], 551, "get_number_unique_values should have the right number of unique")
 
-    def test_print(self):
+    def test__str__(self):
         from io import StringIO
-        t_map = BidsTsvSummary()
+        t_map = BidsTsvSummary(name="My output")
         t_map.update(self.stern_map_path)
         df = get_new_dataframe(self.stern_map_path)
         t_map.update(self.stern_map_path)
         self.assertEqual(len(t_map.categorical_info.keys()), len(df.columns),
                          "BidsTsvSummary should have all columns as categorical if no value or skip are given")
-        with mock.patch('sys.stdout', new=StringIO()):
-            t_map.print()
-            print("This should be eaten by the StringIO")
+        t_map_str = str(t_map)
+        self.assertTrue(t_map_str, "__str__ returns a non-empty string when the map has content.")
 
     def test_update(self):
         dict1 = BidsTsvSummary()
diff --git a/tests/tools/test_hed_logger.py b/tests/tools/test_hed_logger.py
index d6fe5c3dd..9d24de56e 100644
--- a/tests/tools/test_hed_logger.py
+++ b/tests/tools/test_hed_logger.py
@@ -32,6 +32,17 @@ def test_get_keys(self):
         self.assertEqual(len(keys), 1, "It should have one key after one message")
         self.assertEqual(keys[0], "baloney", "It should have the correct key after insertion")
 
+    def test_get_log_string(self):
+        status = HedLogger()
+        status.add("baloney", "Test message 1", level="ERROR")
+        status.add("baloney", "Test message 2")
+        stuff1 = status.get_log_string()
+        self.assertGreater(len(stuff1), 0, "get_log should contain messages.")
+        self.assertEqual(stuff1.count('\n'), 3, "get_log should output right number of lines.")
+        status.add("banana", "")
+        stuff2 = status.get_log_string(level="ERROR")
+        self.assertEqual(stuff2.count('\n'), 3, "get_log should output right number of lines when level is given.")
+
     def test_print_log(self):
         status = HedLogger()
         with mock.patch('sys.stdout', new=StringIO()) as fake_out1:
diff --git a/tests/util/test_data_util.py b/tests/util/test_data_util.py
index 75fce9ca5..ec64d5229 100644
--- a/tests/util/test_data_util.py
+++ b/tests/util/test_data_util.py
@@ -17,8 +17,9 @@ def setUpClass(cls):
         cls.stern_test1_path = os.path.join(curation_base_dir, "sternberg_test_events.tsv")
         cls.stern_test2_path = os.path.join(curation_base_dir, "sternberg_with_quotes_events.tsv")
         cls.stern_test3_path = os.path.join(curation_base_dir, "sternberg_no_quotes_events.tsv")
-        cls.attention_shift_path = os.path.join(curation_base_dir, "sub-001_task-AuditoryVisualShiftHed2_run-01_events.tsv")
-        cls.sampling_rate_path = os.path.join(curation_base_dir, "bcit_basline_driving_samplingRates.tsv")
+        cls.attention_shift_path = os.path.join(curation_base_dir,
+                                                "sub-001_task-AuditoryVisualShiftHed2_run-01_events.tsv")
+        cls.sampling_rate_path = os.path.join(curation_base_dir, "bcit_baseline_driving_samplingRates.tsv")
 
     def test_add_column(self):
         data = {'Name': ['n/a', '', 'tom', 'alice', 0, 1],
@@ -111,9 +112,16 @@ def test_replace_values(self):
         data = {'Name': ['n/a', '', 'tom', 'alice', 0, 1],
                 'Age': [np.nan, 10, '', 'n/a', '0', '10']}
         df1 = DataFrame(data)
-        num_replaced = replace_values(df1, values=['', 0])
-        self.assertEqual(df1.loc[0, 'Name'], 'n/a', "replace_values should replace the empty string with n/a")
-        self.assertEqual(4, num_replaced, "replace_values should replace the correct number of values")
+        self.assertFalse(df1.loc[1, 'Name'], "replace_values before replacement value is empty")
+        num_replaced1 = replace_values(df1, values=['', 0])
+        self.assertEqual(df1.loc[1, 'Name'], 'n/a', "replace_values should replace the empty string with n/a")
+        self.assertEqual(4, num_replaced1, "replace_values should replace the correct number of values")
+        df2 = DataFrame(data)
+        num_replaced2 = replace_values(df2, replace_value='Baloney', column_list=['Name'])
+        self.assertEqual(df2.loc[1, 'Name'], 'Baloney',
+                         "replace_values should replace the empty string with specified value in correct columns")
+        self.assertEqual(1, num_replaced2,
+                         "replace_values should replace the correct number of values when columns are specified")
 
     def test_remove_quotes(self):
         df1 = get_new_dataframe(self.stern_test2_path)
diff --git a/tests/util/test_io_util.py b/tests/util/test_io_util.py
index f0b5feb46..b7ab19caa 100644
--- a/tests/util/test_io_util.py
+++ b/tests/util/test_io_util.py
@@ -1,7 +1,7 @@
 import os
 import unittest
 from hed.errors.exceptions import HedFileError
-from hed.util.io_util import extract_suffix_path, generate_filename, get_dir_dictionary, get_file_list, \
+from hed.util.io_util import check_filename, extract_suffix_path, generate_filename, get_dir_dictionary, get_file_list, \
     get_path_components
 
 from hed.util.io_util import parse_bids_filename, _split_entity
@@ -20,6 +20,27 @@ def setUpClass(cls):
         cls.stern_test3_path = os.path.join(stern_base_dir, "sternberg_no_quotes_events.tsv")
         cls.attention_shift_path = os.path.join(att_base_dir, "sub-001_task-AuditoryVisualShiftHed2_run-01_events.tsv")
 
+    def test_check_filename(self):
+        name1 = "/user/local/task_baloney.gz_events.nii"
+        check1a = check_filename(name1, extensions=[".txt", ".nii"])
+        self.assertTrue(check1a, "check_filename should return true if has required extension")
+        check1b = check_filename(name1, name_prefix="apple", extensions=[".txt", ".nii"])
+        self.assertFalse(check1b, "check_filename should return false if right extension but wrong prefix")
+        check1c = check_filename(name1, name_suffix='_events')
+        self.assertTrue(check1c, "check_filename should return true if has a default extension and correct suffix")
+        name2 = "/user/local/task_baloney.gz_events.nii.gz"
+        check2a = check_filename(name2, extensions=[".txt", ".nii"])
+        self.assertFalse(check2a, "check_filename should return false if extension does not match")
+        check2b = check_filename(name2, extensions=[".txt", ".nii.gz"])
+        self.assertTrue(check2b, "check_filename should return true if extension with gz matches")
+        check2c = check_filename(name2, name_suffix="_events", extensions=[".txt", ".nii.gz"])
+        self.assertTrue(check2c, "check_filename should return true if suffix after extension matches")
+        name3 = "Changes"
+        check3a = check_filename(name3, name_suffix="_events", extensions=None)
+        self.assertFalse(check3a, "check_filename should be false if it doesn't match with no extension")
+        check3b = check_filename(name3, name_suffix="es", extensions=None)
+        self.assertTrue(check3b, "check_filename should be true if match with no extension.")
+
     def test_extract_suffix_path(self):
         suffix_path = extract_suffix_path('c:/myroot/temp.tsv', 'c:')
         self.assertTrue(suffix_path.endswith('temp.tsv'), "extract_suffix_path has the right path")
diff --git a/tests/validator/test_hed_validator.py b/tests/validator/test_hed_validator.py
index b4ae1d92b..46c1e4725 100644
--- a/tests/validator/test_hed_validator.py
+++ b/tests/validator/test_hed_validator.py
@@ -4,7 +4,7 @@
 #from hed import
 from hed.errors import ErrorContext
 from hed import schema
-from hed.models import DefinitionMapper, HedString, HedInput, EventsInput, Sidecar
+from hed.models import DefMapper, HedString, HedInput, EventsInput, Sidecar
 from hed.validator import HedValidator
 
 
@@ -89,12 +89,12 @@ def test_complex_file_validation_invalid(self):
                                  "../data/validator_tests/bids_events_bad_defs.json")
         validator = HedValidator(hed_schema=hed_schema)
         sidecar = Sidecar(json_path)
-        issues = sidecar.validate_entries(hed_ops=validator, check_for_warnings=True)
-        self.assertEqual(len(issues), 4)
+        # issues = sidecar.validate_entries(hed_ops=validator, check_for_warnings=True)
+        # self.assertEqual(len(issues), 4)
         input_file = EventsInput(events_path, sidecars=sidecar)
-
-        validation_issues = input_file.validate_file_sidecars(validator, check_for_warnings=True)
-        self.assertEqual(len(validation_issues), 4)
+        #
+        # validation_issues = input_file.validate_file_sidecars(validator, check_for_warnings=True)
+        # self.assertEqual(len(validation_issues), 4)
 
         validation_issues = input_file.validate_file(validator, check_for_warnings=True)
         self.assertEqual(len(validation_issues), 42)
@@ -156,7 +156,7 @@ def test_error_spans_from_file_and_missing_required_column(self):
     def test_org_tag_missing(self):
         test_string_obj = HedString("Event, Item/NotItem")
         removed_tag = test_string_obj.tags()[0]
-        test_string_obj.remove_groups([removed_tag])
+        test_string_obj.remove([removed_tag])
         from hed import HedTag
         source_span = test_string_obj._get_org_span(removed_tag)
         self.assertEqual(source_span, (0, 5))
@@ -169,13 +169,26 @@ def test_def_mapping_single_line(self):
                                    '../data/schema_test_data/HED8.0.0.mediawiki')
         hed_schema = schema.load_schema(schema_path)
         validator = HedValidator(hed_schema=hed_schema)
-        def_mapper = DefinitionMapper()
+        def_mapper = DefMapper()
         string_with_def = \
             '(Definition/TestDefPlaceholder/#,(Item/TestDef1/#,Item/TestDef2)), def/TestDefPlaceholder/2471'
         test_string = HedString(string_with_def)
         issues = test_string.validate([validator, def_mapper], check_for_definitions=True)
         self.assertEqual(len(issues), 0)
 
+    def test_duplicate_group_in_definition(self):
+        schema_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                   '../data/schema_test_data/HED8.0.0.mediawiki')
+        hed_schema = schema.load_schema(schema_path)
+        validator = HedValidator(hed_schema=hed_schema)
+        def_mapper = DefMapper()
+        string_with_def = \
+            '(Definition/TestDef,(Item/TestDef1,Item/TestDef1))'
+        test_string = HedString(string_with_def)
+        issues = test_string.validate([validator, def_mapper], check_for_definitions=False)
+        self.assertEqual(len(issues), 1)
+
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/validator/test_tag_validator.py b/tests/validator/test_tag_validator.py
index 71300b1e8..9c8a06fd3 100644
--- a/tests/validator/test_tag_validator.py
+++ b/tests/validator/test_tag_validator.py
@@ -24,9 +24,9 @@ def test_exist_in_schema(self):
             'invalidExtension': 'Agent/Red',
             'invalidExtension2': 'Agent/Red/Extension2',
             'usedToBeIllegalComma': 'Label/This is a label,This/Is/A/Tag',
-            'illegalDef': 'Def/Item',
-            'illegalDefExpand': 'Def-expand/Item',
-            'illegalDefinition': 'Definition/Item',
+            'legalDef': 'Def/Item',
+            'legalDefExpand': 'Def-expand/Item',
+            'legalDefinition': 'Definition/Item',
         }
         expected_results = {
             'takesValue': True,
@@ -37,9 +37,9 @@ def test_exist_in_schema(self):
             'invalidExtension': False,
             'invalidExtension2': False,
             'usedToBeIllegalComma': False,
-            'illegalDef': False,
-            'illegalDefExpand': False,
-            'illegalDefinition': False,
+            'legalDef': True,
+            'legalDefExpand': True,
+            'legalDefinition': True,
         }
         expected_issues = {
             'takesValue': [],
@@ -57,14 +57,9 @@ def test_exist_in_schema(self):
                                     "/Color/CSS-color/Red-color/Red"),
             'usedToBeIllegalComma': self.format_error(ValidationErrors.NO_VALID_TAG_FOUND, tag=1,
                                                       index_in_tag=0, index_in_tag_end=4),
-            'illegalDef': self.format_error(ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=4,
-                                            index_in_tag_end=8, expected_parent_tag="Item"),
-            'illegalDefExpand': self.format_error(ValidationErrors.INVALID_PARENT_NODE, tag=0,
-                                                  index_in_tag=11, index_in_tag_end=15,
-                                                  expected_parent_tag="Item"),
-            'illegalDefinition': self.format_error(ValidationErrors.INVALID_PARENT_NODE, tag=0,
-                                                   index_in_tag=11, index_in_tag_end=15,
-                                                   expected_parent_tag="Item"),
+            'legalDef': [],
+            'legalDefExpand': [],
+            'legalDefinition': []
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, False)
 
@@ -428,10 +423,10 @@ def test_topLevelTagGroup_validation(self):
             'invalid2': self.format_error(ValidationErrors.HED_TOP_LEVEL_TAG, tag=1),
             'invalidTwoInOne': self.format_error(
                 ValidationErrors.HED_MULTIPLE_TOP_TAGS, tag=0,
-                multiple_tags="Property/Organizational-property/Definition/InvalidDef3".split(", ")),
+                multiple_tags="Definition/InvalidDef3".split(", ")),
             'invalid2TwoInOne': self.format_error(
                 ValidationErrors.HED_MULTIPLE_TOP_TAGS, tag=0,
-                multiple_tags="Property/Data-property/Data-marker/Temporal-marker/Onset".split(", ")),
+                multiple_tags="Onset".split(", ")),
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, False)
 
diff --git a/tests/validator/test_tag_validator_library.py b/tests/validator/test_tag_validator_library.py
index f224649ae..2cba6da7c 100644
--- a/tests/validator/test_tag_validator_library.py
+++ b/tests/validator/test_tag_validator_library.py
@@ -15,7 +15,7 @@ class TestHed3(TestValidatorBase):
 
     @classmethod
     def setUpClass(cls):
-        schema_file = '../data/schema_test_data/HED8.0.0t.xml'
+        schema_file = '../data/validator_tests/HED8.0.0_added_tests.mediawiki'
         hed_xml = os.path.join(os.path.dirname(os.path.realpath(__file__)), schema_file)
         hed_schema1 = schema.load_schema(hed_xml)
         hed_schema2 = schema.load_schema(hed_xml, library_prefix="tl:")
@@ -56,9 +56,9 @@ def test_exist_in_schema(self):
             'invalidExtension': 'tl:Agent/Red',
             'invalidExtension2': 'tl:Agent/Red/Extension2',
             'usedToBeIllegalComma': 'tl:Label/This is a label,tl:This/Is/A/Tag',
-            'illegalDef': 'tl:Def/Item',
-            'illegalDefExpand': 'tl:Def-expand/Item',
-            'illegalDefinition': 'tl:Definition/Item',
+            'legalDef': 'tl:Def/Item',
+            'legalDefExpand': 'tl:Def-expand/Item',
+            'legalDefinition': 'tl:Definition/Item',
             'unknownPrefix': 'ul:Definition/Item'
         }
         expected_results = {
@@ -70,9 +70,9 @@ def test_exist_in_schema(self):
             'invalidExtension': False,
             'invalidExtension2': False,
             'usedToBeIllegalComma': False,
-            'illegalDef': False,
-            'illegalDefExpand': False,
-            'illegalDefinition': False,
+            'legalDef': True,
+            'legalDefExpand': True,
+            'legalDefinition': True,
             'unknownPrefix': False
         }
         expected_issues = {
@@ -91,15 +91,9 @@ def test_exist_in_schema(self):
                                     "/Color/CSS-color/Red-color/Red"),
             'usedToBeIllegalComma': self.format_error(ValidationErrors.NO_VALID_TAG_FOUND, tag=1,
                                                       index_in_tag=3, index_in_tag_end=7),
-            'illegalDef': self.format_error(
-                ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=7, index_in_tag_end=11,
-                expected_parent_tag="Item"),
-            'illegalDefExpand': self.format_error(
-                ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=14,
-                index_in_tag_end=18, expected_parent_tag="Item"),
-            'illegalDefinition': self.format_error(
-                ValidationErrors.INVALID_PARENT_NODE, tag=0, index_in_tag=14,
-                index_in_tag_end=18, expected_parent_tag="Item"),
+            'legalDef': [],
+            'legalDefExpand': [],
+            'legalDefinition': [],
             'unknownPrefix': self.format_error(
                 ValidationErrors.HED_LIBRARY_UNMATCHED, tag=0, unknown_prefix="ul:", known_prefixes=["", "tl:"]),
         }
@@ -379,10 +373,10 @@ def test_topLevelTagGroup_validation(self):
             'invalid2': self.format_error(ValidationErrors.HED_TOP_LEVEL_TAG, tag=1),
             'invalidTwoInOne': self.format_error(
                 ValidationErrors.HED_MULTIPLE_TOP_TAGS, tag=0,
-                multiple_tags="tl:Property/Organizational-property/Definition/InvalidDef3".split(", ")),
+                multiple_tags="tl:Definition/InvalidDef3".split(", ")),
             'invalid2TwoInOne': self.format_error(
                 ValidationErrors.HED_MULTIPLE_TOP_TAGS, tag=0,
-                multiple_tags="tl:Property/Data-property/Data-marker/Temporal-marker/Onset".split(", ")),
+                multiple_tags="tl:Onset".split(", ")),
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, False)
 
@@ -420,6 +414,64 @@ def test_taggroup_validation(self):
         }
         self.validator_semantic(test_strings, expected_results, expected_issues, False)
 
+class RequiredTags(TestHed3):
+    @staticmethod
+    def string_obj_func(validator, check_for_warnings):
+        return partial(validator._validate_tags_in_hed_string, check_for_warnings=check_for_warnings)
+
+    def test_includes_all_required_tags(self):
+        test_strings = {
+            'complete': 'Animal-agent, Action, tl:Animal-agent, tl:Action',
+            'missingAgent': 'Action, tl:Animal-agent, tl:Action',
+            'missingAction': 'Animal-agent, tl:Animal-agent, tl:Action',
+            'inSubGroup': 'Animal-agent, (Action, tl:Animal-agent, tl:Action)',
+            'missingAll': 'Event'
+        }
+        expected_results = {
+            'complete': True,
+            'missingAgent': False,
+            'missingAction': False,
+            'inSubGroup': True,
+            'missingAll': False,
+        }
+        expected_issues = {
+            'complete': [],
+            'missingAgent': self.format_error(ValidationErrors.HED_REQUIRED_TAG_MISSING,
+                                              tag_prefix='Agent/Animal-agent'),
+            'missingAction': self.format_error(ValidationErrors.HED_REQUIRED_TAG_MISSING, tag_prefix='Action'),
+            'inSubGroup': [],
+            'missingAll':
+                self.format_error(ValidationErrors.HED_REQUIRED_TAG_MISSING, tag_prefix='Action')
+                + self.format_error(ValidationErrors.HED_REQUIRED_TAG_MISSING, tag_prefix='Agent/Animal-agent')
+                + self.format_error(ValidationErrors.HED_REQUIRED_TAG_MISSING, tag_prefix='tl:Action')
+                + self.format_error(ValidationErrors.HED_REQUIRED_TAG_MISSING, tag_prefix='tl:Agent/Animal-agent'),
+        }
+        self.validator_semantic(test_strings, expected_results, expected_issues, True)
+
+    def test_multiple_copies_unique_tags(self):
+        test_strings = {
+            'legal': 'tl:Event-context,'
+                     '(Vehicle,Event)',
+            'multipleDesc': 'tl:Event-context,'
+                            'tl:Event-context,'
+                            'Vehicle,(Vehicle,tl:Event-context)',
+            'multipleDescIncShort': 'tl:Event-context,'
+                                    'tl:Organizational-property/Event-context'
+        }
+        expected_results = {
+            'legal': True,
+            'multipleDesc': False,
+            'multipleDescIncShort': False
+        }
+        expected_issues = {
+            'legal': [],
+            'multipleDesc': self.format_error(ValidationErrors.HED_TAG_NOT_UNIQUE,
+                                              tag_prefix='tl:Property/Organizational-property/Event-context'),
+            'multipleDescIncShort': self.format_error(ValidationErrors.HED_TAG_NOT_UNIQUE,
+                                                      tag_prefix='tl:Property/Organizational-property/Event-context'),
+        }
+        self.validator_semantic(test_strings, expected_results, expected_issues, False)
+
 
 if __name__ == '__main__':
     unittest.main()