diff --git a/src/spec2nexus/spec.py b/src/spec2nexus/spec.py
index 53df0ea..a236888 100644
--- a/src/spec2nexus/spec.py
+++ b/src/spec2nexus/spec.py
@@ -27,6 +27,15 @@ specifying the file reference (by path reference as needed)
 and the internal routines will take care of all that is necessary
 to read and interpret the information.
 
+.. autosummary::
+   
+    ~is_spec_file
+    ~is_spec_file_with_header
+    ~SpecDataFile
+    ~SpecDataFileHeader
+    ~SpecDataFileScan
+
+
 ..  -----------------------------------------------------------------------------------------
     old documentation
     -----------------------------------------------------------------------------------------
@@ -203,7 +212,22 @@ def is_spec_file_with_header(filename):
 class SpecDataFile(object):
 
     """
-    contents of a spec data file
+    contents of a SPEC data file
+
+    .. autosummary::
+
+        ~update_available
+        ~dissect_file
+        ~read
+        ~getScan
+        ~getScanNumbers
+        ~getScanNumbersChronological
+        ~getMinScanNumber
+        ~getMaxScanNumber
+        ~getFirstScanNumber
+        ~getLastScanNumber
+        ~getScanCommands
+
     """
 
     fileName = ''
@@ -217,16 +241,33 @@ class SpecDataFile(object):
         self.headers = []
         self.scans = OrderedDict()
         self.readOK = -1
-        if not os.path.exists(filename):
-            raise SpecDataFileNotFound('file does not exist: ' + str(filename))
-        if not is_spec_file(filename):
-            raise NotASpecDataFile('not a SPEC data file: ' + str(filename))
-        self.fileName = filename
-
-        self.read()
+        self.last_scan = None
+        self.mtime = 0
+        self.num_lines = 0
+
+        if filename is not None:
+            if not os.path.exists(filename):
+                raise SpecDataFileNotFound(
+                    'file does not exist: ' + str(filename))
+            if not is_spec_file(filename):
+                raise NotASpecDataFile(
+                    'not a SPEC data file: ' + str(filename))
+            self.fileName = filename
+
+            self.read()
     
     def __str__(self):
         return self.fileName or 'None'
+    
+    @property
+    def update_available(self):
+        """
+        Has the file been updated since the last time it was read?
+        
+        Reference file modification time is stored *after* 
+        file is read in :meth:`read()` method.
+        """
+        return self.mtime != os.path.getmtime(self.fileName)
 
     def _read_file_(self, spec_file_name):
         """Reads a spec data file"""
@@ -260,11 +301,12 @@ class SpecDataFile(object):
             text with one of the above control lines at its start 
         
         """
-        buf = self._read_file_(self.fileName)
+        buf = self._read_file_(self.fileName).splitlines()
+        self.num_lines = len(buf)
         
         sections, block = [], []
         
-        for _line_num, text in enumerate(buf.splitlines()):
+        for _line_num, text in enumerate(buf):
             if len(text.strip()) > 0:
                 f = text.split()[0]
                 if len(f) == 2 and f in ("#E", "#F", "#S"):
@@ -299,6 +341,9 @@ class SpecDataFile(object):
         # fix any missing parts
         if not hasattr(self, "specFile"):
             self.specFile = self.fileName
+
+        self.last_scan = self.getLastScanNumber()
+        self.mtime = os.path.getmtime(self.fileName)
     
     def getScan(self, scan_number=0):
         """return the scan number indicated, None if not found"""
@@ -358,7 +403,18 @@ class SpecDataFile(object):
 
 
 class SpecDataFileHeader(object):
-    """contents of a spec data file header (#E) section"""
+    """
+    contents of a spec data file header (#E) section
+
+    .. autosummary::
+
+        ~get_macro_name
+        ~interpret
+        ~addPostProcessor
+        ~addH5writer
+        ~getLatestScan
+
+    """
 
     def __init__(self, buf, parent = None):
         #----------- initialize the instance variables
@@ -430,7 +486,19 @@ class SpecDataFileHeader(object):
 #-------------------------------------------------------------------------------------------
 
 class SpecDataFileScan(object):
-    """contents of a spec data file scan (#S) section"""
+    """
+    contents of a spec data file scan (#S) section
+
+    .. autosummary::
+
+        ~get_macro_name
+        ~interpret
+        ~add_interpreter_comment
+        ~get_interpreter_comments
+        ~addPostProcessor
+        ~addH5writer
+
+    """
 
     def __init__(self, header, buf, parent=None):
         self.parent = parent        # instance of SpecDataFile

Feature suggestion:  Monitor for file change???
Just a thought, ever think about adding code in the SpecDataFile to monitor for changes in the file & send out a signal?  Since this is a file that is expected to change it might be good to add monitoring so that Apps could take advantage of this.
diff --git a/tests/test_spec.py b/tests/test_spec.py
index 3b08c7d..d04f317 100644
--- a/tests/test_spec.py
+++ b/tests/test_spec.py
@@ -12,8 +12,12 @@ unit tests for the spec module
 # The full license is in the file LICENSE.txt, distributed with this software.
 #-----------------------------------------------------------------------------
 
+import os
+import shutil
+import sys
+import tempfile
+import time
 import unittest
-import os, sys
 
 _test_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 _path = os.path.abspath(os.path.join(_test_path, 'src'))
@@ -25,70 +29,68 @@ from spec2nexus import spec, utils
 
 
 class Test(unittest.TestCase):
-
-    def setUp(self):
-        self.basepath = os.path.join(_path, 'spec2nexus')
-        self.datapath = os.path.join(self.basepath, 'data')
-
-#     def tearDown(self):
-#         pass
-
-#     def testName(self):
-#         pass
     
     def abs_data_fname(self, fname):
-        return os.path.join(self.datapath, fname)
+        return os.path.join(_path, 'spec2nexus', 'data', fname)
     
     def test_strip_first_word(self):
         self.assertEqual(utils.strip_first_word('one two three'), 'two three')
         
     def test_isSpecFileThis(self):
         self.assertFalse(spec.is_spec_file('this_does_not_exist'))
-        self.assertFalse(spec.is_spec_file(self.basepath))
+        self.assertFalse(spec.is_spec_file(os.path.join(_path, 'spec2nexus')))
         self.assertFalse(spec.is_spec_file(__file__))
         self.assertTrue( spec.is_spec_file(self.abs_data_fname('APS_spec_data.dat')))
+        self.assertFalse(spec.is_spec_file_with_header('file does not exist'))
+        self.assertTrue( spec.is_spec_file_with_header(self.abs_data_fname('APS_spec_data.dat')))
+        self.assertFalse(spec.is_spec_file_with_header(self.abs_data_fname('spec_from_spock.spc')))
     
     def is_spec_file(self, fname):
         return spec.is_spec_file(self.abs_data_fname(fname))
         
     def test_isSpecFile(self):
         '''test all the known data files to see if they are SPEC'''
-        self.assertTrue( self.is_spec_file('33bm_spec.dat'))
-        self.assertTrue( self.is_spec_file('33id_spec.dat'))
-        self.assertTrue( self.is_spec_file('APS_spec_data.dat'))
-        self.assertTrue( self.is_spec_file('CdSe'))
-        self.assertTrue( self.is_spec_file('lmn40.spe'))
-        self.assertTrue( self.is_spec_file('YSZ011_ALDITO_Fe2O3_planar_fired_1.spc'))
-        self.assertFalse(self.is_spec_file('uxml'))             # directory
-        self.assertFalse(self.is_spec_file('README.txt'))       # text file
-        self.assertFalse(self.is_spec_file('33bm_spec.hdf5'))
-        self.assertFalse(self.is_spec_file('33id_spec.hdf5'))
-        self.assertFalse(self.is_spec_file('APS_spec_data.hdf5'))
-        self.assertFalse(self.is_spec_file('compression.h5'))
-        self.assertFalse(self.is_spec_file('Data_Q.h5'))
-        self.assertFalse(self.is_spec_file('lmn40.hdf5'))
-        self.assertFalse(self.is_spec_file('writer_1_3.h5'))
-    
-    def cannot_find_spec_data_file(self):
-        spec.SpecDataFile('cannot_find_this_file')
-    
-    def not_a_spec_data_file(self):
-        spec.SpecDataFile(__file__)
+        files = {
+            "33bm_spec.dat": True,
+            "33id_spec.dat": True,
+            "APS_spec_data.dat": True,
+            "CdSe": True,
+            "lmn40.spe": True,
+            "YSZ011_ALDITO_Fe2O3_planar_fired_1.spc": True,
+            "uxml": False,             # directory
+            "README.txt": False,       # text file
+            "33bm_spec.hdf5": False,
+            "33id_spec.hdf5": False,
+            "APS_spec_data.hdf5": False,
+            "compression.h5": False,
+            "Data_Q.h5": False,
+            "lmn40.hdf5": False,
+            "writer_1_3.h5": False,
+        }
+        for item, expected in files.items():
+            self.assertEqual(self.is_spec_file(item), expected, item)
     
     def test_custom_exceptions(self):
-        self.assertRaises(Exception, spec.SpecDataFileNotFound())
-        self.assertRaises(Exception, spec.SpecDataFileCouldNotOpen())
-        self.assertRaises(Exception, spec.DuplicateSpecScanNumber())
-        self.assertRaises(Exception, spec.NotASpecDataFile())
-        self.assertRaises(Exception, spec.UnknownSpecFilePart())
-
-    def spec_data_file(self):
-        spec.SpecDataFile(self.abs_data_fname('03_05_UImg.dat'))
+        with self.assertRaises(IOError):
+            raise spec.SpecDataFileNotFound()
+        with self.assertRaises(IOError):
+            raise spec.SpecDataFileCouldNotOpen()
+        with self.assertRaises(Exception):
+            raise spec.DuplicateSpecScanNumber()
+        with self.assertRaises(Exception):
+            raise spec.NotASpecDataFile()
+        with self.assertRaises(Exception):
+            raise spec.UnknownSpecFilePart()
 
     def test_file_initial_exceptions(self):
-        self.assertRaises(TypeError, spec.SpecDataFile)
-        self.assertRaises(spec.SpecDataFileNotFound, self.cannot_find_spec_data_file)
-        self.assertRaises(spec.NotASpecDataFile, self.not_a_spec_data_file)
+        with self.assertRaises(TypeError):
+            spec.SpecDataFile()
+        with self.assertRaises(spec.SpecDataFileNotFound):
+            spec.SpecDataFile('cannot_find_this_file')
+        with self.assertRaises(spec.SpecDataFileNotFound):
+            spec.SpecDataFile(self.abs_data_fname('03_05_UImg.dat'))
+        with self.assertRaises(spec.NotASpecDataFile):
+            spec.SpecDataFile(__file__)
 
     def test_33bm_spec(self):
         fname = self.abs_data_fname('33bm_spec.dat')
@@ -315,7 +317,7 @@ class Test(unittest.TestCase):
         scan = sfile.getScan(scan_number)
         self.assertTrue(scan is not None)
         self.assertEqual(scan.T, "0", "received expected count time")
-        self.assertTrue("Seco nds" in scan.data, "found counting base")
+        self.assertIn("Seco nds", scan.data, "found counting base")
         self.assertEqual(
             scan.data["Seco nds"][0], 
             1, 
@@ -334,25 +336,75 @@ class Test(unittest.TestCase):
             "400000", 
             "received expected monitor count")
         self.assertTrue(hasattr(scan, 'MCA'), "MCA found")
-        self.assertTrue("ROI" in scan.MCA, "MCA ROI found")
+        self.assertIn("ROI", scan.MCA, "MCA ROI found")
         roi_dict = scan.MCA["ROI"]
         key = "FeKa(mca1 R1)"
-        self.assertTrue(key in roi_dict, "MCA ROI config found")
+        self.assertIn(key, roi_dict, "MCA ROI config found")
         roi = roi_dict[key]
         self.assertEqual(roi["first_chan"], 377, "MCA ROI first channel")
         self.assertEqual(roi["last_chan"], 413, "MCA ROI last channel")
 
-        self.assertTrue(key in scan.data, "MCA ROI data found")
+        self.assertIn(key, scan.data, "MCA ROI data found")
         self.assertEqual(
             len(scan.data[key]), 
             61, 
             "embedded comment not part of data")
 
+    def test_str(self):
+        specFile = os.path.join(
+            os.path.dirname(__file__), 
+            'data', 
+            'issue109_data.txt')
+        sdf = spec.SpecDataFile(specFile)
+        self.assertEqual(str(sdf), sdf.fileName)
+        sdf = spec.SpecDataFile(None)
+        self.assertEqual(str(sdf), 'None')
+
+
+class TestFileUpdate(unittest.TestCase):
+
+    def setUp(self):
+        self.data_file = tempfile.NamedTemporaryFile(
+            suffix='.dat', delete=False)
+        self.data_file.close()
+
+    def tearDown(self):
+        os.remove(self.data_file.name)
+    
+    def test_update_available(self):
+        # test the mtime function first
+        # and setup the modifiable SPEC data file
+        self.assertTrue(os.path.exists(self.data_file.name))
+        mt0 = os.path.getmtime(self.data_file.name)
+        time.sleep(0.02)        # at least a clock tick (1/60 s)
+        shutil.copy(
+            os.path.join(_test_path, "tests", "data", "issue82_data.txt"),
+            self.data_file.name)
+        mt1 = os.path.getmtime(self.data_file.name)
+        self.assertGreater(mt1, mt0)
+        
+        # test the ``update_available`` property
+        sdf = spec.SpecDataFile(self.data_file.name)
+        self.assertFalse(sdf.update_available)
+        self.assertEqual(sdf.num_lines, 164)
+        self.assertEqual(sdf.last_scan, sdf.getLastScanNumber())
+        self.assertEqual(sdf.last_scan, '17')
+
+        # update the file with a trivial edit
+        with open(self.data_file.name, "a") as fp:
+            fp.write("\n#C comment\n")
+        time.sleep(0.02)        # at least a clock tick (1/60 s)
+
+        mt2 = os.path.getmtime(self.data_file.name)
+        self.assertGreater(mt2, mt1)
+        self.assertTrue(sdf.update_available)
+
 
 def suite(*args, **kw):
     test_suite = unittest.TestSuite()
     test_list = [
         Test,
+        TestFileUpdate,
         ]
     for test_case in test_list:
         test_suite.addTest(unittest.makeSuite(test_case))

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
contourpy==1.3.0
coverage==7.8.0
cycler==0.12.1
docopt==0.6.2
exceptiongroup==1.2.2
execnet==2.1.1
fonttools==4.56.0
h5py==3.13.0
importlib_resources==6.5.2
iniconfig==2.1.0
kiwisolver==1.4.7
lxml==5.3.1
matplotlib==3.9.4
numpy==2.0.2
packaging==24.2
pillow==11.1.0
pluggy==1.5.0
pyparsing==3.2.3
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-xdist==3.6.1
python-dateutil==2.9.0.post0
six==1.17.0
-e git+https://github.com/prjemian/spec2nexus.git@4f8e469b81834dee5f1b5e95b045c59a6da8b4ce#egg=spec2nexus
tomli==2.2.1
typing_extensions==4.13.0
zipp==3.21.0

name: spec2nexus
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - contourpy==1.3.0
      - coverage==7.8.0
      - cycler==0.12.1
      - docopt==0.6.2
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - fonttools==4.56.0
      - h5py==3.13.0
      - importlib-resources==6.5.2
      - iniconfig==2.1.0
      - kiwisolver==1.4.7
      - lxml==5.3.1
      - matplotlib==3.9.4
      - numpy==2.0.2
      - packaging==24.2
      - pillow==11.1.0
      - pluggy==1.5.0
      - pyparsing==3.2.3
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - pytest-xdist==3.6.1
      - python-dateutil==2.9.0.post0
      - six==1.17.0
      - tomli==2.2.1
      - typing-extensions==4.13.0
      - zipp==3.21.0
prefix: /opt/conda/envs/spec2nexus

tests/test_spec.py::Test::test_str tests/test_spec.py::TestFileUpdate::test_update_available
tests/test_spec.py::Test::test_33bm_spec
tests/test_spec.py::Test::test_33id_spec tests/test_spec.py::Test::test_APS_spec_data tests/test_spec.py::Test::test_CdSe tests/test_spec.py::Test::test_YSZ011_ALDITO_Fe2O3_planar_fired_1 tests/test_spec.py::Test::test_custom_exceptions tests/test_spec.py::Test::test_extra_control_line_content__issue109 tests/test_spec.py::Test::test_file_initial_exceptions tests/test_spec.py::Test::test_isSpecFile tests/test_spec.py::Test::test_isSpecFileThis tests/test_spec.py::Test::test_lmn40 tests/test_spec.py::Test::test_strip_first_word
src/spec2nexus/spec.py
src/spec2nexus/spec.py