Commit 52686fb2 authored by Martin Claus's avatar Martin Claus
Browse files

Merge branch 'release-v0.1.0' into 'master'

Release v0.1.0

See merge request !5
parents 13184d5c e3a8d877
Loading
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+6 −0
Original line number Diff line number Diff line
__pycache__
build
dist
namelist.egg-info
*.pyc
.coverage

.gitlab-ci.yml

0 → 100644
+11 −0
Original line number Diff line number Diff line
image: continuumio/miniconda3:latest

before_script:
  - env
  - source activate root
  - conda install pytest pytest-cov

test:
  stage: test
  script:
    - pytest -v --cov --cov-report term-missing .
+26 −14
Original line number Diff line number Diff line
[![pipeline status](https://git.geomar.de/martin-claus/py-namelist/badges/develop/pipeline.svg)](https://git.geomar.de/martin-claus/py-namelist/commits/develop)
[![coverage report](https://git.geomar.de/martin-claus/py-namelist/badges/develop/coverage.svg)](https://git.geomar.de/martin-claus/py-namelist/commits/develop)

py-namelist
===========
Parse Fortran Namelists to dict-like objects and back

## Download
To get the latest git version do
To get the latest release do
```bash
git clone https://git.geomar.de:martin-claus/py-namelist.git --branch v0.1.0 --depth=1
```
$ git clone https://github.com/martinclaus/py-namelist.git
or, if you prefer the latest unstable version
```bash
git clone https://git.geomar.de:martin-claus/py-namelist.git
```


## Usage
To parse a namelist file you call `parse_namelist(fobj)` where fobj is a file-like object offering a `read()` and `seek()` method (usually the standard python file object).
```
To parse a namelist file you call `parse_namelist_file(fobj)` where fobj is a file-like object offering a `read()` and `seek()` method
(usually the standard python file object). Alternatively, you can parse a string using `parse_namelist_string(str)`.
```python
import namelist
with open(your_nml_file) as fobj:
	nmls = namelist.parse_namelist_file(fobj)
```
`nmls` will be a list of instances of the `Namelist` class.

`Namelist` is a subclass of `dict` or `OrderedDict` if you use Python >= 2.7. A `Namelist` instance, `nml`, is initialized with an name and optionally with initial values.
```
`Namelist` is a subclass of `OrderedDict` (or `dict` if you use Python < 2.7).
A `Namelist` instance, `nml`, is initialized with an name and optionally with initial values.

```python
nml = Namelist("param", (("key1", val1), ...))
```

The name attribute will set the read-only property name of `nml`. To change, add or delete values the same methods are available as for dict.
```
The name attribute will set the read-only property name of `nml`. To change, add or delete values the
same methods are available as for `dict`.
```python
print nml.name
nml.update({"eggs": 1, "spam": [1, 2, 3]})
del(nml["param"])
```

To create a Fortran readable string representation of the Namelist instance, just use the `str()` build-in
```
To create a Fortran readable string representation of the `Namelist` instance, just use the `str()` build-in
```python
s = str(nml)
```
or just
```
```python
print(nml)
```

A string representation of the Namelist instance that can be used by `eval()` to create copies of it can be created by `repr()`
```
A string representation of the `Namelist` instance that can be used by `eval()` to create copies of it can be created by `repr()`
```python
print repr(nml)
```

**Note**: The parsing of namelist does not have to strictly follow the Fortran standard. Hence, some namelists that are perfectly accepted by some Fortran version are not guaranteed to be correctly parsed by `parse_namelist()`. Always check the content of your Namelist object.
**Note**: The parsing of namelist does not have to strictly follow the Fortran standard. Hence, some namelists that are perfectly accepted by some Fortran version are not guaranteed to be correctly parsed by `parse_namelist_string()`. Always check the content of your `Namelist` object. If you do find a namelist that does not work, please create an issue at <https://git.geomar.de/martin-claus/py-namelist/> together with the namelist that does not work.

namelist/__init__.py

0 → 100644
+9 −0
Original line number Diff line number Diff line
"""namelist: Parsing Fortran namelists to Python dictionaries and back."""

__name__ = "namelist"
__version__ = "0.0.1"

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
Loading