Commit 1408366f authored by Martin Claus's avatar Martin Claus
Browse files

Merge branch '2-create-test-cases' into 'develop'

Resolve "Create test cases"

Closes #2

See merge request !3
parents 4dc2e879 d8fc459f
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -2,3 +2,5 @@ __pycache__
build
dist
namelist.egg-info
*.pyc
.coverage
+1 −1
Original line number Diff line number Diff line
@@ -5,7 +5,7 @@ Parse Fortran Namelists to dict-like objects and back
## Download
To get the latest git version do
```
$ git clone https://github.com/martinclaus/py-namelist.git
$ git clone https://git.geomar.de:martin-claus/py-namelist.git
```

## Usage
+4 −1
Original line number Diff line number Diff line
@@ -3,4 +3,7 @@
__name__ = "namelist"
__version__ = "0.0.1"

from .namelist import Namelist, parse_namelist_file
from .namelist import (Namelist,
                       parse_namelist_file,
                       parse_namelist_string,
                       )
+113 −46
Original line number Diff line number Diff line
@@ -4,56 +4,60 @@ Created on Tue Sep 23 10:27:57 2014

@author: mclaus
"""
import sys
import re

if sys.version_info[:2] < (2, 7):
    DictClass = dict
else:
    # If Python 2.7 or higher use OrderedDict to preserve
    # the order of the Namelist
try:
    from collections import OrderedDict as DictClass
except ImportError:  # pragma: no cover # Python < 2.7
    DictClass = dict

MODULE_NAME = "namelist"
NML_LINE_LENGTH = 70
# Config file parser, called from the class initialization
varname = r'[a-zA-Z][a-zA-Z0-9_]*'
valueBool = re.compile(r"(\.(true|false|t|f)\.)", re.I)
quote = re.compile(r"([\'\"]{1}.*[\'\"]{1})")
quote = re.compile(r"([\']{1}[^\']*[\']{1}|[\"]{1}[^\"]*[\"]{1})",
                   re.MULTILINE)
namelistname = re.compile(r"&(" + varname + r")")
paramname = re.compile(r"^(" + varname + r")")
namlistend = re.compile(r"^\$(end)?", re.I)
comment = re.compile(r"#.*")
namlistend = re.compile(r'^(&(end)?|/)$', re.I)
comment = re.compile(r"!.*$", re.MULTILINE)
equalsign = re.compile(r"^=$")
computation = re.compile(r"^([0-9\.e]+\s*[\*\+\-/]{1}\s*)+[0-9\.e]+", re.I)
computation = re.compile(
    r"([\(]*[0-9\.e]+[\)\s]*([\*\+\-/]{1}|[\*]{2})\s*)+[0-9\.e]+[\)]*", re.I
)


class Namelist(DictClass):
    """ Class to handle Fortran Namelists
    Namelist(string) -> new namelist with fortran nml identifier string
    Namelist(string, init_val) -> new initialized namelist with nml identifier
        string and init_val beeing a valid initialisation object for the parent
    """Class to handle Fortran Namelists.

    Namelist(string) -> new namelist with fortran namelist group name
    Namelist(string, init_val) -> new initialized namelist with namelist group
        name and init_val beeing a valid initialisation object for the parent
        class (either OrderedDict for Python >= 2.7 or else dict).
    A fortran readable string representation of the namelist can be generated
    via str() build-in function. A string representation of the Python object
    that can be used with eval or string. Template substitution can be obtained
    by repr() build-in function.
    """

    @property
    def name(self):
        """ Read only property name, representing the fortran namelist
        identifier.
        """
        """Namelist group name."""
        return self._name

    def __init__(self, name, init_val=()):
        """x.__init__(...) initializes x; see help(type(x)) for signature"""
        """Create a `Namelist` instance.

        See help(type(x)) for signature.
        """
        self._name = name
        super(self.__class__, self).__init__(init_val)

    def __str__(self):
        """x.__str__(self) -> Fortran readable string representation of the
        namelist. If a value v is a sequence, an 1D fortran array representation
        """Fortran readable string representation of the namelist.

        If a value v is a sequence, an 1D fortran array representation
        is created using iter(v).
        """
        retstr = "&%s\n" % str(self.name)
@@ -85,13 +89,11 @@ class Namelist(DictClass):
                else:
                    rv = repr(v)
                retstr += "%s = %s\n" % (str(k), rv)
        retstr += "&end\n"
        retstr += "/\n"
        return retstr

    def __repr__(self):
        """x.__repr__(self) -> string that can be used by eval to create a copy
        of x.
        """
        """Return a string that can be used by eval to create a copy."""
        retstr = "%s.%s(%s, (" % (MODULE_NAME, self.__class__.__name__,
                                  repr(self.name))
        for k, v in self.items():
@@ -100,17 +102,55 @@ class Namelist(DictClass):
        return retstr

    def has_name(self, name):
        """x.hasname(self, name) <==> name==x.name"""
        """Return `True` if `name` matches the namelist group name.

        Parameters
        ----------
        name : str
            name to test against.

        Returns
        -------
        bool : `True` if `name` matches the namelist group name.

        """
        return name == self.name


def parse_namelist_file(in_file):
    """ parse_namelist_file(fobj) -> list of Namelist instances. fobj can be
    any object that implements pythons file object API, i.e. that offers a
    read() method.
    """Parse namelists from file object.

    Parameters
    ----------
    in_file : :obj:
        Any object that implements pythons file object API, i.e. that offers a
        `read` and `seek` method.

    Returns
    -------
    :obj:`List` of :obj:`Namelist`

    """
    retlist = []
    content = _tokenize(in_file.read())
    namelist_string = in_file.read()
    in_file.seek(0, 0)
    return parse_namelist_string(namelist_string)


def parse_namelist_string(in_string):
    """Parse namelists from string.

    Parameters
    ----------
    in_string : str
        String containing one or more namelist definitions.

    Returns
    -------
    :obj:`List` of :obj:`Namelist`

    """
    retlist = []
    content = _tokenize(in_string)
    for item in content:
        match = re.match(namelistname, item)
        if match:
@@ -123,9 +163,9 @@ def parse_namelist_file(in_file):
            pname = match.group(1)
            nml[pname] = []
            continue
        for pattern in (namlistend, equalsign):
            match = re.match(pattern, item)
            if match:
        if re.match(namlistend, item):
            continue
        if re.match(equalsign, item):
            continue
        match = re.match(valueBool, item)
        if match:
@@ -140,26 +180,53 @@ def parse_namelist_file(in_file):
        except ValueError:
            pass
        else:
            continue
            continue  # pragma: no cover
        try:
            nml[pname].append(float(item))
        except ValueError:
            pass
        else:
            continue
            continue  # pragma: no cover
        match = re.match(computation, item)
        if match:
            nml[pname].append(eval(item))
    for nml in retlist:
        for k, v in nml.iteritems():
        for k, v in nml.items():
            if len(v) == 1:
                nml[k] = v[0]
    return retlist


def _tokenize(text):
    fs = "$FS$"
    """Extract syntax tokens."""
    fs = "$$$FS$$$"

    # remove comments
    text = re.sub(comment, '', text)
    for char, rep in zip(('\n', r',', ' ', '=', ), (fs, fs, fs, fs+'='+fs)):

    hashed_tokens = {}

    # replace quoted strings by hash
    text = _hash_token(text, quote, hashed_tokens, fs)

    # replace numerical computations by hash
    text = _hash_token(text, computation, hashed_tokens, fs)

    for char, rep in zip(('\n', r',', ' ', '=', '(/', '/)'),
                         (fs, fs, fs, fs+'='+fs, fs, fs)):
        text = text.replace(char, rep)
    text = text.split(fs)
    return [token.strip() for token in text if token.strip() != '']
    tokens = [token.strip() for token in text if token.strip() != '']
    return [hashed_tokens[t] if t in hashed_tokens else t for t in tokens]


def _hash_token(text, pattern, hashed_tokens, fs):
    while True:
        match = re.search(pattern, text)
        if not match:
            break
        matched_str = match.group(0)
        hashed = str(hash(matched_str))
        hashed_tokens[hashed] = matched_str
        text = text.replace(matched_str, fs+hashed+fs, 1)
    return text

tests/context.py

0 → 100644
+14 −0
Original line number Diff line number Diff line
"""Context for tests."""

import os
import sys
sys.path.insert(
    0,
    os.path.abspath(
        os.path.join(os.path.dirname(__file__), '..')
    )
)


import namelist
import io
Loading