From 7bd98a75fa3ab75abbc9d1846613aba7f3bf0e60 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 16 Apr 2019 12:40:47 -0600 Subject: [PATCH 01/15] style: reformat for black --- bmi/__init__.py | 5 +- bmi/_version.py | 154 +++++++++++++++++++++++++++++------------------- 2 files changed, 97 insertions(+), 62 deletions(-) diff --git a/bmi/__init__.py b/bmi/__init__.py index c381d4f..2eb46d8 100644 --- a/bmi/__init__.py +++ b/bmi/__init__.py @@ -1,8 +1,7 @@ +from ._version import get_versions from .bmi import Bmi - __all__ = ["Bmi"] -from ._version import get_versions -__version__ = get_versions()['version'] +__version__ = get_versions()["version"] del get_versions diff --git a/bmi/_version.py b/bmi/_version.py index a33c1cb..19f8ebd 100644 --- a/bmi/_version.py +++ b/bmi/_version.py @@ -1,4 +1,3 @@ - # 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 @@ -58,17 +57,18 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular 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): +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None @@ -76,10 +76,13 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + p = subprocess.Popen( + [c] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), + ) break except EnvironmentError: e = sys.exc_info()[1] @@ -116,16 +119,22 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): for i 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} + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } else: 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)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -181,7 +190,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # 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 = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = set([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 @@ -190,7 +199,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # 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 = set([r for r in refs if re.search(r'\d', r)]) + tags = set([r for r in refs if re.search(r"\d", r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -198,19 +207,26 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 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):] + r = ref[len(tag_prefix) :] if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + 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} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") @@ -225,8 +241,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -234,10 +249,19 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # 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 = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = run_command( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + "%s*" % tag_prefix, + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -260,17 +284,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-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) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -279,10 +302,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 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)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -293,13 +318,13 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) + count_out, rc = run_command(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 = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ + 0 + ].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces @@ -330,8 +355,7 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -445,11 +469,13 @@ def render_git_describe_long(pieces): 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} + 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 @@ -469,9 +495,13 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } def get_versions(): @@ -485,8 +515,7 @@ def get_versions(): verbose = cfg.verbose try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass @@ -495,13 +524,16 @@ def get_versions(): # 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 i in cfg.versionfile_source.split('/'): + for i 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} + 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) @@ -515,6 +547,10 @@ def get_versions(): except NotThisMethod: pass - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } From bb3a5ec474dae37012563febb6bfb271208c287b Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 16 Apr 2019 14:29:22 -0600 Subject: [PATCH 02/15] fix: rename bmi package to bmipy BREAKING CHANGE: rename the packge to avoid name conflicts --- Makefile | 17 ++++++++--------- README.md => README.rst | 0 {bmi => bmipy}/__init__.py | 0 {bmi => bmipy}/_version.py | 0 {bmi => bmipy}/bmi.py | 0 5 files changed, 8 insertions(+), 9 deletions(-) rename README.md => README.rst (100%) rename {bmi => bmipy}/__init__.py (100%) rename {bmi => bmipy}/_version.py (100%) rename {bmi => bmipy}/bmi.py (100%) diff --git a/Makefile b/Makefile index 11a7c71..4362e7f 100644 --- a/Makefile +++ b/Makefile @@ -51,12 +51,11 @@ clean-test: ## remove test and coverage artifacts rm -fr .pytest_cache lint: ## check style with flake8 - flake8 bmi tests + flake8 bmipy pretty: ## reformat files to make them look pretty - find bmi -name '*.py' | xargs isort - find tests -name '*.py' | xargs isort - black setup.py bmi tests + find bmipy -name '*.py' | xargs isort + black setup.py bmipy test: ## run tests quickly with the default Python py.test @@ -65,16 +64,16 @@ test-all: ## run tests on every Python version with tox tox coverage: ## check code coverage quickly with the default Python - pytest --cov=bmi --cov-report= --cov-report=html --cov-config=setup.cfg + pytest --cov=bmipy --cov-report= --cov-report=html --cov-config=setup.cfg $(BROWSER) htmlcov/index.html docs: ## generate Sphinx HTML documentation, including API docs - rm -f docs/api/bmi.rst - rm -f docs/api/modules.rst - sphinx-apidoc --force -o docs/api bmi *tests + rm -f docs/source/bmipy.rst + rm -f docs/source/modules.rst + sphinx-apidoc --force -o docs/source bmipy *tests $(MAKE) -C docs clean $(MAKE) -C docs html - $(BROWSER) docs/_build/html/index.html + $(BROWSER) docs/build/html/index.html servedocs: docs ## compile the docs watching for changes watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . diff --git a/README.md b/README.rst similarity index 100% rename from README.md rename to README.rst diff --git a/bmi/__init__.py b/bmipy/__init__.py similarity index 100% rename from bmi/__init__.py rename to bmipy/__init__.py diff --git a/bmi/_version.py b/bmipy/_version.py similarity index 100% rename from bmi/_version.py rename to bmipy/_version.py diff --git a/bmi/bmi.py b/bmipy/bmi.py similarity index 100% rename from bmi/bmi.py rename to bmipy/bmi.py From 9c497290d68a32f09178b4d1f377a83b9b652b88 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 16 Apr 2019 14:37:47 -0600 Subject: [PATCH 03/15] docs: fix link to ugrid conventions --- bmipy/bmi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bmipy/bmi.py b/bmipy/bmi.py index f115a0a..d594a4f 100644 --- a/bmipy/bmi.py +++ b/bmipy/bmi.py @@ -226,7 +226,7 @@ def get_var_location(self, name: str) -> str: ----- CSDMS uses the `ugrid conventions`_ to define unstructured grids. - .. _UGRID: http://ugrid-conventions.github.io/ugrid-conventions + .. _ugrid conventions: http://ugrid-conventions.github.io/ugrid-conventions """ ... From 86921f7037054002619b3245802ed8f7d762a5c4 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Tue, 16 Apr 2019 14:38:14 -0600 Subject: [PATCH 04/15] docs: add some brief usage instructions --- README.rst | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 3f55ad3..0f2be1c 100644 --- a/README.rst +++ b/README.rst @@ -1,2 +1,32 @@ -# bmi-python -Python bindings for the Basic Model Interface +BMI for Python +============== + +Install +------- + +Install *bmipy* with *pip*, + +.. code-block:: bash + + $ pip install bmipy + +If you're using Anaconda, you can also install *bmipy* +with conda from the *conda-forge* channel, + +.. code-block:: bash + + $ conda install bmipy -c conda-forge + + +Usage +----- + +.. code-block:: python + + from bmipy import Bmi + + + class MyBmi(Bmi): + + def initialize(self, config_file): + # Your implementation goes here From ee272d4ef695059388c170b509436bd47d17bdd8 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 09:19:00 -0600 Subject: [PATCH 05/15] test: add a travis CI config file --- .travis.yml | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3dfff0b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,62 @@ +language: generic +os: +- linux +- osx +env: + matrix: + - CONDA_ENV=3.6 + - CONDA_ENV=3.7 +matrix: + exclude: + - os: osx + env: CONDA_ENV=lint +sudo: false +jobs: + include: + - stage: lint + os: linux + if: env(CONDA_ENV) = "3.7" + script: + - make lint + - stage: docs + os: linux + if: env(CONDA_ENV) = "3.7" + script: + - conda env create -n test_env --file docs/environment.yml + - sphinx-apidoc --force -o docs bmipy *tests + - make -C docs clean html + - stage: deploy + if: tag =~ v.*$ + script: skip + os: linux + deploy: + on: + all_branches: true + provider: pypi + user: mcflugen + password: + secure: EsExpqv8hT9MqAT6eQ8944ePQrpjQGis9cDZsqROLYWp6MMwAcBuNGsp3leki6Gk8VFlA3wqlK+wNPMfWffFyU+1684vpobNnU6+fxP/LM/DzsMtDSatTfy6KS692an26AvppOHlkONL1kkv9GrzrpvUGwpdboTb1fW7x4zxhYFjWG2HQlBJ72+Ljuiy1NfjUQo4uIbvzTIAw7Sm2sNtopGPKuLShtY1QjIGOVBx1cVEgNdpgY0ZyQ/xXSbjmoSisU8/dWENAzcpRhg/9mJzNlwUMHDJi0lZw+zdnv1e7s8Q+umALL1ekuXrT0ptJYkoonpewe+jusAdeL7kwWuVm3MetY9P8xAZyNTcOUeC0FHqiO4O+nUKOlTMBeTlJjY0M/bp+UkN/y56DDUdZC4xtwf6UrttSqxgtiI0Y4eTK7SzyMfnLrZcYWUQ6mc0ZMLjnnXthJQjVTzG02HIzDNPTYqcIztVZyZi9ySkZ9luQa3XF8+pd3HuAZK6mLrjsGMQA/8iQewMT1WpnoStLMOSRMTKEMy+xLvqP6eNkaphpst8m5fUuSWpUknHlP8xA8XWR8sT7nLexzZFzR2dygQx8/3M+kVyJuHE1jaUaOD7OCho1lVDsuOafXyQLYRAlQt2b/8B/1hNtw4xZMXFDr0LMQ31cwr7BaK51uQbKb4pzK0= +before_install: +- | + if [[ $TRAVIS_OS_NAME == "osx" ]]; then + brew remove --force $(brew list) + brew cleanup -s + rm -rf $(brew --cache) + fi +install: +- | + if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then + curl https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh > $HOME/miniconda.sh + else + curl https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh > $HOME/miniconda.sh + fi +- bash $HOME/miniconda.sh -b -p $HOME/anaconda +- export PATH="$HOME/anaconda/bin:$PATH" +- hash -r +- conda config --set always_yes yes --set changeps1 no +- conda create -n test_env python=$CONDA_ENV +- source activate test_env +- pip install . +script: +- pytest --cov=bmipy --cov-report=xml:$(pwd)/coverage.xml -vvv +after_success: coveralls From 6fdb34b955f7f50617d89ad84b860b646e563207 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 09:20:11 -0600 Subject: [PATCH 06/15] docs: add a docs folder with minimal documentation --- docs/Makefile | 19 ++ .../source/_static/powered-by-logo-header.png | Bin 0 -> 15416 bytes docs/source/_templates/links.html | 9 + docs/source/_templates/sidebarintro.html | 4 + docs/source/bmipy.rst | 22 +++ docs/source/conf.py | 170 ++++++++++++++++++ docs/source/environment.yml | 12 ++ docs/source/index.rst | 11 ++ docs/source/modules.rst | 7 + 9 files changed, 254 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/source/_static/powered-by-logo-header.png create mode 100644 docs/source/_templates/links.html create mode 100644 docs/source/_templates/sidebarintro.html create mode 100644 docs/source/bmipy.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/environment.yml create mode 100644 docs/source/index.rst create mode 100644 docs/source/modules.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..69fe55e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/source/_static/powered-by-logo-header.png b/docs/source/_static/powered-by-logo-header.png new file mode 100644 index 0000000000000000000000000000000000000000..46c82d07f6e2f212423e1512dd8462b623ffd7b8 GIT binary patch literal 15416 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�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 literal 0 HcmV?d00001 diff --git a/docs/source/_templates/links.html b/docs/source/_templates/links.html new file mode 100644 index 0000000..03f7f56 --- /dev/null +++ b/docs/source/_templates/links.html @@ -0,0 +1,9 @@ +

Useful Links

+ + diff --git a/docs/source/_templates/sidebarintro.html b/docs/source/_templates/sidebarintro.html new file mode 100644 index 0000000..f09d068 --- /dev/null +++ b/docs/source/_templates/sidebarintro.html @@ -0,0 +1,4 @@ +

About bmipy

+

+ Python bindings for the Basic Model Interface. +

diff --git a/docs/source/bmipy.rst b/docs/source/bmipy.rst new file mode 100644 index 0000000..57aec98 --- /dev/null +++ b/docs/source/bmipy.rst @@ -0,0 +1,22 @@ +bmipy package +============= + +Submodules +---------- + +bmipy.bmi module +---------------- + +.. automodule:: bmipy.bmi + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: bmipy + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..fbe0538 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,170 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'bmipy' +copyright = '2019, Eric Hutton' +author = 'Eric Hutton' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', + 'sphinx.ext.napoleon', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = "_static/powered-by-logo-header.png" + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +html_sidebars = { + "index": [ + "sidebarintro.html", + "links.html", + "sourcelink.html", + "searchbox.html", + ], + "**": [ + "sidebarintro.html", + "links.html", + "sourcelink.html", + "searchbox.html", + ] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'bmidoc' + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True diff --git a/docs/source/environment.yml b/docs/source/environment.yml new file mode 100644 index 0000000..56caecd --- /dev/null +++ b/docs/source/environment.yml @@ -0,0 +1,12 @@ +name: bmipy_docs +channels: + - conda-forge +dependencies: +- python==3.6 +- pandoc +- sphinx>=1.5.1 +- sphinx_rtd_theme +- tornado +- entrypoints +- pip: + - sphinxcontrib_github_alt diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..7346d0f --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,11 @@ +.. title:: BMIpy + +.. include:: ../../README.rst + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 0000000..8f50db5 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +bmipy +===== + +.. toctree:: + :maxdepth: 4 + + bmipy From a93ca9ff661955f1846da27785b656d5f9beacf8 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 09:21:59 -0600 Subject: [PATCH 07/15] chore: rename remaining occurances of bmi to bmipy --- MANIFEST.in | 2 +- Makefile | 2 +- bmipy/_version.py | 4 ++-- setup.cfg | 18 +++++++----------- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index cf15bcd..27d3006 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include versioneer.py -include bmi/_version.py +include bmipy/_version.py diff --git a/Makefile b/Makefile index 4362e7f..dbd6bba 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ clean-test: ## remove test and coverage artifacts rm -fr .pytest_cache lint: ## check style with flake8 - flake8 bmipy + flake8 bmipy tests pretty: ## reformat files to make them look pretty find bmipy -name '*.py' | xargs isort diff --git a/bmipy/_version.py b/bmipy/_version.py index 19f8ebd..4cfe5bf 100644 --- a/bmipy/_version.py +++ b/bmipy/_version.py @@ -41,8 +41,8 @@ def get_config(): cfg.VCS = "git" cfg.style = "pep440" cfg.tag_prefix = "" - cfg.parentdir_prefix = "bmi-" - cfg.versionfile_source = "bmi/_version.py" + cfg.parentdir_prefix = "bmipy-" + cfg.versionfile_source = "bmipy/_version.py" cfg.verbose = False return cfg diff --git a/setup.cfg b/setup.cfg index 37bfd26..58ff3b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,29 +2,25 @@ disable = line-too-long,bad-continuation [flake8] -exclude = docs -ignore = - E203 - E501 - W503 +exclude = docs bmipy/_version.py max-line-length = 88 [versioneer] VCS = git style = pep440 -versionfile_source = bmi/_version.py -versionfile_build = bmi/_version.py +versionfile_source = bmipy/_version.py +versionfile_build = bmipy/_version.py tag_prefix = -parentdir_prefix = bmi- +parentdir_prefix = bmipy- [tool:pytest] minversion = 3.0 -testpaths = bmi tests +testpaths = bmipy tests norecursedirs = .* *.egg* build dist addopts = --ignore setup.py --ignore versioneer.py - --ignore bmi/_version.py + --ignore bmipy/_version.py --tb native --strict --durations 16 @@ -35,4 +31,4 @@ doctest_optionflags = ALLOW_UNICODE [run] -omit = bmi/_version.py +omit = bmipy/_version.py From c0e93524b0c1761db2814b0c2d79787d125dd36b Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 09:23:37 -0600 Subject: [PATCH 08/15] tests: add tests folder with minimal tests --- tests/test_bmipy.py | 134 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/test_bmipy.py diff --git a/tests/test_bmipy.py b/tests/test_bmipy.py new file mode 100644 index 0000000..9527458 --- /dev/null +++ b/tests/test_bmipy.py @@ -0,0 +1,134 @@ +import pytest + +from bmipy import Bmi + + +class EmptyBmi(Bmi): + def __init__(self): + pass + + def initialize(self, config_file): + pass + + def update(self): + pass + + def update_until(self, then): + pass + + def finalize(self): + pass + + def get_var_type(self, var_name): + pass + + def get_var_units(self, var_name): + pass + + def get_var_nbytes(self, var_name): + pass + + def get_var_itemsize(self, name): + pass + + def get_var_location(self, name): + pass + + def get_var_grid(self, var_name): + pass + + def get_grid_rank(self, grid_id): + pass + + def get_grid_size(self, grid_id): + pass + + def get_value_ptr(self, var_name): + pass + + def get_value(self, var_name): + pass + + def get_value_at_indices(self, var_name, indices): + pass + + def set_value(self, var_name, src): + pass + + def set_value_at_indices(self, var_name, src, indices): + pass + + def get_component_name(self): + pass + + def get_input_var_names(self): + pass + + def get_output_var_names(self): + pass + + def get_grid_shape(self, grid_id): + pass + + def get_grid_spacing(self, grid_id): + pass + + def get_grid_origin(self, grid_id): + pass + + def get_grid_type(self, grid_id): + pass + + def get_start_time(self): + pass + + def get_end_time(self): + pass + + def get_current_time(self): + pass + + def get_time_step(self): + pass + + def get_time_units(self): + pass + + def get_grid_edge_count(self, grid): + pass + + def get_grid_edge_nodes(self, grid, edge_nodes): + pass + + def get_grid_face_count(self, grid): + pass + + def get_grid_face_nodes(self, grid, face_nodes): + pass + + def get_grid_node_count(self, grid): + pass + + def get_grid_nodes_per_face(self, grid, nodes_per_face): + pass + + def get_grid_x(self, grid, x): + pass + + def get_grid_y(self, grid, y): + pass + + def get_grid_z(self, grid, z): + pass + + +def test_bmi_not_implemented(): + class MyBmi(Bmi): + pass + + with pytest.raises(TypeError): + Bmi() + + +def test_bmi_implemented(): + assert isinstance(EmptyBmi(), Bmi) From f161d2f8bb1ba2e1a408e4221d4c636f4c38b732 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 09:26:06 -0600 Subject: [PATCH 09/15] chore: update pypi secure password --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3dfff0b..9bce771 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,8 +34,8 @@ jobs: all_branches: true provider: pypi user: mcflugen - password: - secure: EsExpqv8hT9MqAT6eQ8944ePQrpjQGis9cDZsqROLYWp6MMwAcBuNGsp3leki6Gk8VFlA3wqlK+wNPMfWffFyU+1684vpobNnU6+fxP/LM/DzsMtDSatTfy6KS692an26AvppOHlkONL1kkv9GrzrpvUGwpdboTb1fW7x4zxhYFjWG2HQlBJ72+Ljuiy1NfjUQo4uIbvzTIAw7Sm2sNtopGPKuLShtY1QjIGOVBx1cVEgNdpgY0ZyQ/xXSbjmoSisU8/dWENAzcpRhg/9mJzNlwUMHDJi0lZw+zdnv1e7s8Q+umALL1ekuXrT0ptJYkoonpewe+jusAdeL7kwWuVm3MetY9P8xAZyNTcOUeC0FHqiO4O+nUKOlTMBeTlJjY0M/bp+UkN/y56DDUdZC4xtwf6UrttSqxgtiI0Y4eTK7SzyMfnLrZcYWUQ6mc0ZMLjnnXthJQjVTzG02HIzDNPTYqcIztVZyZi9ySkZ9luQa3XF8+pd3HuAZK6mLrjsGMQA/8iQewMT1WpnoStLMOSRMTKEMy+xLvqP6eNkaphpst8m5fUuSWpUknHlP8xA8XWR8sT7nLexzZFzR2dygQx8/3M+kVyJuHE1jaUaOD7OCho1lVDsuOafXyQLYRAlQt2b/8B/1hNtw4xZMXFDr0LMQ31cwr7BaK51uQbKb4pzK0= + password: + secure: d0lVWG54WokY88HQJRll0hshZ9apboEHdrjjs9mZKbCu6qVADp4r6xkboUuS9W4nSAVDfKZ4Q7cPdkplfGunjuhPq4McztF7jWqc1zPwVvKfJiHhGK90vqwUJwLC/zyPn2QR3HYCTmainZeGGMcsGNh5IvO5CcZg9IGR3M/EsPm6WTTu+/e1JG6fixO+cUmYsQbQ/Z2/+NSHUiFkiT8oAlLJZqZDHvvURU4jQCBeISnqRzOqR76Sy00W1gniUYXeyr8DUAZy+SHPgxIQ8tUAkB/Qg6mwNVQ2P+VEkm1feuTNUbrUYyi34XZhcjvP6AQfU1sKIsdMQvCtBTfSO1ALNy+5izKUhi1egmoarqcV/HkMho/okSWDDCkrTiYMb1904rYaTH/x6ixcefFT4X1GBLthsF+ZRBvzCQSKxKPWt/Sqn8P9CyAOIVDSJIut/nII/LfkC50fKdIz4DMIb9vtGgyknHlRxj1a2pmAwTlBF7OXV/BKaEafxapaUmAnj/V+OdFsiHn9iA4xLaQ/YgYWjI6fveiWMD+Pl7KPUDt5eK7WtiI1bfsuyuKMlyTwyunkfqJ5Y24R6DTKBksYqNpuYmzAewHTbNcJkesN/ZPvDp1zSgmYQr/IlObTvxt57ebunxn+1rByN/gO48QeUMOr9LWPSaYdHcjQSN5ZHBMKpqI= before_install: - | if [[ $TRAVIS_OS_NAME == "osx" ]]; then From b6c2711029c4bf2455cd942f9362ab76718ac74d Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 09:32:40 -0600 Subject: [PATCH 10/15] chore: add requirements file for testing --- .travis.yml | 2 +- requirements.txt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml index 9bce771..4ccc104 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,7 +56,7 @@ install: - conda config --set always_yes yes --set changeps1 no - conda create -n test_env python=$CONDA_ENV - source activate test_env -- pip install . +- pip install . -r requirements.txt script: - pytest --cov=bmipy --cov-report=xml:$(pwd)/coverage.xml -vvv after_success: coveralls diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..643c230 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +coveralls +pytest>=3.6 +pytest-cov From 726ea4b3663619fe9dc884710ce062587689bcd5 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 09:55:04 -0600 Subject: [PATCH 11/15] chore: remove conditionals from stages --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4ccc104..54f5c3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,12 +15,10 @@ jobs: include: - stage: lint os: linux - if: env(CONDA_ENV) = "3.7" script: - make lint - stage: docs os: linux - if: env(CONDA_ENV) = "3.7" script: - conda env create -n test_env --file docs/environment.yml - sphinx-apidoc --force -o docs bmipy *tests From cd340ef3c098ced00df91bc46b032a88955258e4 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 10:54:58 -0600 Subject: [PATCH 12/15] chore: add flake8 to requirements file --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 643c230..84678f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ coveralls +flake8 pytest>=3.6 pytest-cov From c1d2a4387de76e81ff224d4132b7cdedbd702f68 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 11:26:04 -0600 Subject: [PATCH 13/15] chore: fix path to docs environment file --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 54f5c3a..3f0a9a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ jobs: - stage: docs os: linux script: - - conda env create -n test_env --file docs/environment.yml + - conda env create -n test_env --file docs/source/environment.yml - sphinx-apidoc --force -o docs bmipy *tests - make -C docs clean html - stage: deploy From b72e16606418bfb0a47c5d46462c7303ace72922 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 12:00:54 -0600 Subject: [PATCH 14/15] chore: add pip as a requirement --- docs/source/environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/environment.yml b/docs/source/environment.yml index 56caecd..6c4e7d4 100644 --- a/docs/source/environment.yml +++ b/docs/source/environment.yml @@ -4,6 +4,7 @@ channels: dependencies: - python==3.6 - pandoc +- pip - sphinx>=1.5.1 - sphinx_rtd_theme - tornado From 933d798cb21412196902efc564b605209ea70528 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Wed, 17 Apr 2019 12:01:16 -0600 Subject: [PATCH 15/15] chore: separate install step for each stage --- .travis.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3f0a9a3..5481a0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,8 @@ os: - osx env: matrix: - - CONDA_ENV=3.6 - CONDA_ENV=3.7 -matrix: - exclude: - - os: osx - env: CONDA_ENV=lint + - CONDA_ENV=3.6 sudo: false jobs: include: @@ -19,8 +15,10 @@ jobs: - make lint - stage: docs os: linux + install: + - conda env create --file docs/source/environment.yml + - conda activate bmipy_docs script: - - conda env create -n test_env --file docs/source/environment.yml - sphinx-apidoc --force -o docs bmipy *tests - make -C docs clean html - stage: deploy @@ -41,7 +39,6 @@ before_install: brew cleanup -s rm -rf $(brew --cache) fi -install: - | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then curl https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh > $HOME/miniconda.sh @@ -54,6 +51,7 @@ install: - conda config --set always_yes yes --set changeps1 no - conda create -n test_env python=$CONDA_ENV - source activate test_env +install: - pip install . -r requirements.txt script: - pytest --cov=bmipy --cov-report=xml:$(pwd)/coverage.xml -vvv