Repository: bids-standard/pybv

Issue:
"Units" parameter is not checked for valid units.
Passing "μV" fails, because μ is https://www.compart.com/de/unicode/U+03BC, but only µ (https://www.compart.com/de/unicode/U+00B5) is supported in BV according to the spec.

Also passing nonsense like "sV" results in an ungraceful error.

![image](https://user-images.githubusercontent.com/9084751/98238603-e855e280-1f66-11eb-8aa3-9745b5417e20.png)


Comments:

Patch:
diff --git a/README.rst b/README.rst
index b49be74..3345e67 100644
--- a/README.rst
+++ b/README.rst
@@ -22,9 +22,10 @@
 pybv
 ====
 
-``pybv`` is a lightweight exporter to the BrainVision data format. It is meant
-for use with electrophysiology datasets stored in the
-`Brain Imaging Data Structure <https://bids.neuroimaging.io>`_.
+``pybv`` is a lightweight exporter to the BrainVision data format.
+
+The BrainVision data format is a recommended data format
+for use in the `Brain Imaging Data Structure <https://bids.neuroimaging.io>`_.
 
 
 The documentation can be found under the following links:
@@ -64,9 +65,11 @@ as hosted by Brain Products.
 Installation
 ============
 
+``pybv`` runs on Python version 3.6 or higher.
+
 ``pybv``'s only dependency is ``numpy``. However, we currently recommend that
 you install MNE-Python for reading BrainVision data. See their instructions
-`here <https://www.martinos.org/mne/stable/install_mne_python.html>`_.
+`here <https://mne.tools/stable/install/index.html>`_.
 
 After you have a working installation of MNE-Python (or only ``numpy`` if you
 don't want to read data and only write it), you can install ``pybv`` through
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 0939b1a..3bd08ba 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -15,14 +15,20 @@ Here we list a changelog of pybv.
 Current
 =======
 
-- no entries yet
+Changelog
+~~~~~~~~~
+- Passing a "greek small letter mu" instead of a "micro sign" as a ``unit`` is now permitted, because :func:`pybv.write_brainvision` will convert from one to the other, by `Stefan Appelhoff`_ (`#47 <https://github.com/bids-standard/pybv/pull/47>`_)
+
+Authors
+~~~~~~~
+- `Stefan Appelhoff`_
 
 0.3.0
 =====
 
 Changelog
 ~~~~~~~~~
-- Add ``unit`` parameter for exporting signals in a specific unit (V, mV, µV or uV, nV) by `Clemens Brunner`_ (`#39 <https://github.com/bids-standard/pybv/pull/39>`_)
+- Add ``unit`` parameter for exporting signals in a specific unit (V, mV, µV or uV, nV), by `Clemens Brunner`_ (`#39 <https://github.com/bids-standard/pybv/pull/39>`_)
 
 API
 ~~~
@@ -39,8 +45,8 @@ Authors
 
 Changelog
 ~~~~~~~~~
-- Add option to disable writing a meas_date event (which is also the new default) by `Clemens Brunner`_ (`#32 <https://github.com/bids-standard/pybv/pull/32>`_)
-- Support event durations by passing an (N, 3) array to the events parameter (the third column contains the event durations) by `Clemens Brunner`_ (`#33 <https://github.com/bids-standard/pybv/pull/33>`_)
+- Add option to disable writing a meas_date event (which is also the new default), by `Clemens Brunner`_ (`#32 <https://github.com/bids-standard/pybv/pull/32>`_)
+- Support event durations by passing an (N, 3) array to the events parameter (the third column contains the event durations), by `Clemens Brunner`_ (`#33 <https://github.com/bids-standard/pybv/pull/33>`_)
 
 Authors
 ~~~~~~~
@@ -53,7 +59,7 @@ Authors
 Changelog
 ~~~~~~~~~
 - Add measurement date parameter to public API, by `Stefan Appelhoff`_ (`#29 <https://github.com/bids-standard/pybv/pull/29>`_)
-- Add binary format parameter to public API by `Tristan Stenner`_ (`#22 <https://github.com/bids-standard/pybv/pull/22>`_)
+- Add binary format parameter to public API, by `Tristan Stenner`_ (`#22 <https://github.com/bids-standard/pybv/pull/22>`_)
 
 Bug
 ~~~
@@ -71,7 +77,7 @@ Authors
 
 Changelog
 ~~~~~~~~~
-- Support channel-specific scaling factors by `Tristan Stenner`_ (`#17 <https://github.com/bids-standard/pybv/pull/17>`_)
+- Support channel-specific scaling factors, by `Tristan Stenner`_ (`#17 <https://github.com/bids-standard/pybv/pull/17>`_)
 
 Authors
 ~~~~~~~
@@ -84,7 +90,7 @@ Authors
 
 Changelog
 ~~~~~~~~~
-- Initial import from philistine package by `Phillip Alday`_ and removing dependency on MNE-Python, by `Chris Holdgraf`_ and `Stefan Appelhoff`_
+- Initial import from philistine package by `Phillip Alday`_ and removing dependency on MNE-Python, by `Chris Holdgraf`_, and `Stefan Appelhoff`_
 
 Authors
 ~~~~~~~
diff --git a/pybv/io.py b/pybv/io.py
index 1e8f37f..f93d53c 100644
--- a/pybv/io.py
+++ b/pybv/io.py
@@ -13,18 +13,21 @@ import codecs
 import datetime
 import os
 import os.path as op
+import warnings
 
 import numpy as np
 
 from pybv import __version__
 
 # ASCII as future formats
-supported_formats = {
+SUPPORTED_FORMATS = {
     'binary_float32': ('IEEE_FLOAT_32', np.float32),
     'binary_int16': ('INT_16', np.int16),
 }
 
-supported_orients = {'multiplexed'}
+SUPPORTED_ORIENTS = {'multiplexed'}
+
+SUPPORTED_UNITS = ['V', 'mV', 'µV', 'uV', 'nV']
 
 
 def write_brainvision(data, sfreq, ch_names, fname_base, folder_out,
@@ -120,6 +123,15 @@ def write_brainvision(data, sfreq, ch_names, fname_base, folder_out,
 
     _chk_fmt(fmt)
 
+    if unit == 'μV':
+        # this is greek mu: μ
+        # https://www.compart.com/de/unicode/U+03BC
+        warnings.warn(
+            f"Encountered small greek letter mu: 'μ' in unit: {unit} ... "
+            f"converting to micro sign: 'µ': {unit.replace('μ', 'µ')}"
+        )
+        unit = 'µV'
+
     # measurement date
     if not isinstance(meas_date, (str, datetime.datetime, type(None))):
         raise ValueError('`meas_date` must be of type str, datetime.datetime, '
@@ -145,21 +157,21 @@ def write_brainvision(data, sfreq, ch_names, fname_base, folder_out,
 
 def _chk_fmt(fmt):
     """Check that the format string is valid, return BVEF / numpy datatypes."""
-    if fmt not in supported_formats:
+    if fmt not in SUPPORTED_FORMATS:
         errmsg = ('Data format {} not supported.'.format(fmt) +
                   'Currently supported formats are: ' +
-                  ', '.join(supported_formats))
+                  ', '.join(SUPPORTED_FORMATS))
         raise ValueError(errmsg)
-    return supported_formats[fmt]
+    return SUPPORTED_FORMATS[fmt]
 
 
 def _chk_multiplexed(orientation):
     """Validate an orientation, return if it is multiplexed or not."""
     orientation = orientation.lower()
-    if orientation not in supported_orients:
+    if orientation not in SUPPORTED_ORIENTS:
         errmsg = ('Orientation {} not supported.'.format(orientation) +
                   'Currently supported orientations are: ' +
-                  ', '.join(supported_orients))
+                  ', '.join(SUPPORTED_ORIENTS))
         raise ValueError(errmsg)
     return orientation == 'multiplexed'
 
@@ -225,6 +237,12 @@ def _optimize_channel_unit(resolution, unit):
         return resolution / 1e-6, 'µV'
     elif unit == 'nV':
         return resolution / 1e-9, 'nV'
+    else:
+        raise ValueError(
+            f'Encountered unsupported unit: {unit}'
+            '\nUse either "None" for `unit`, or one of the following: '
+            f'{SUPPORTED_UNITS}'
+            )
 
 
 def _write_vhdr_file(vhdr_fname, vmrk_fname, eeg_fname, data, sfreq, ch_names,
diff --git a/setup.py b/setup.py
index b863d9b..3dc4ae6 100644
--- a/setup.py
+++ b/setup.py
@@ -33,8 +33,6 @@ VERSION = version
 
 
 if __name__ == "__main__":
-    if os.path.exists('MANIFEST'):
-        os.remove('MANIFEST')
 
     setup(name=DISTNAME,
           maintainer=MAINTAINER,
@@ -48,16 +46,19 @@ if __name__ == "__main__":
           long_description=open('README.rst').read(),
           long_description_content_type='text/x-rst',
           zip_safe=True,  # the package can run out of an .egg file
-          classifiers=['Intended Audience :: Science/Research',
-                       'Intended Audience :: Developers',
-                       'License :: OSI Approved',
-                       'Programming Language :: Python',
-                       'Topic :: Software Development',
-                       'Topic :: Scientific/Engineering',
-                       'Operating System :: Microsoft :: Windows',
-                       'Operating System :: POSIX',
-                       'Operating System :: Unix',
-                       'Operating System :: MacOS'],
+          python_requires='~=3.6',
+          classifiers=[
+              'Intended Audience :: Science/Research',
+              'Intended Audience :: Developers',
+              'License :: OSI Approved',
+              'Programming Language :: Python',
+              'Topic :: Software Development',
+              'Topic :: Scientific/Engineering',
+              'Operating System :: Microsoft :: Windows',
+              'Operating System :: POSIX',
+              'Operating System :: Unix',
+              'Operating System :: MacOS'
+          ],
           platforms='any',
           keywords='Brain Products BrainVision vhdr vmrk eeg',
           packages=find_packages(),


Test patch:
diff --git a/.github/workflows/python_tests.yml b/.github/workflows/python_tests.yml
index b81e8d7..bb8ef4a 100644
--- a/.github/workflows/python_tests.yml
+++ b/.github/workflows/python_tests.yml
@@ -5,6 +5,12 @@ on:
     branches: [ master ]
   pull_request:
     branches: [ master ]
+  create:
+    branches: [master]
+    tags: ['**']
+  schedule:
+    - cron: "0 4 1 * *"
+
 
 jobs:
   build:
@@ -14,6 +20,10 @@ jobs:
         platform: [ubuntu-18.04, ubuntu-latest, macos-latest, windows-latest]
         python-version: [3.8]
 
+    env:
+      TZ: Europe/Berlin
+      FORCE_COLOR: true
+
     runs-on: ${{ matrix.platform }}
 
     steps:
@@ -36,6 +46,14 @@ jobs:
       run: |
         pip install -U https://api.github.com/repos/mne-tools/mne-python/zipball/master
 
+    - name: Display versions and environment information
+      run: |
+        echo $TZ
+        date
+        python --version
+        which python
+        mne sys_info
+
     - name: Check formatting
       if: "matrix.platform == 'ubuntu-18.04'"
       run: |
diff --git a/pybv/tests/test_bv_writer.py b/pybv/tests/test_bv_writer.py
index c055396..846029d 100644
--- a/pybv/tests/test_bv_writer.py
+++ b/pybv/tests/test_bv_writer.py
@@ -123,10 +123,19 @@ def test_write_read_cycle(meas_date):
     # check that we create a folder that does not yet exist
     tmpdir = op.join(tmpdir, 'newfolder')
 
+    # First fail writing due to wrong unit
+    unsupported_unit = "rV"
+    with pytest.raises(ValueError, match='Encountered unsupported unit'):
+        write_brainvision(data, sfreq, ch_names, fname, tmpdir,
+                          unit=unsupported_unit)
+
     # write and read data to BV format
-    write_brainvision(data, sfreq, ch_names, fname, tmpdir, events=events,
-                      resolution=np.power(10., -np.arange(10)),
-                      meas_date=meas_date)
+    # ensure that greek small letter mu gets converted to micro sign
+    with pytest.warns(UserWarning, match="Encountered small greek letter mu"):
+        write_brainvision(data, sfreq, ch_names, fname, tmpdir, events=events,
+                          resolution=np.power(10., -np.arange(10)),
+                          unit='μV',
+                          meas_date=meas_date)
     vhdr_fname = op.join(tmpdir, fname + '.vhdr')
     raw_written = mne.io.read_raw_brainvision(vhdr_fname, preload=True)
     # delete the first annotation because it's just marking a new segment