diff --git a/.gitignore b/.gitignore index 8e4d4e7..3f3f3af 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ dask-worker-space *nc .pytest_cache -contact_map/version.py +contact_map/_installed_version.py cover # Byte-compiled / optimized / DLL files diff --git a/.travis.yml b/.travis.yml index 9c66969..e9ba691 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,13 @@ +version: ~> 1.0 + language: python python: - - '3.6' + - '3.7' - '2.7' - - '3.5' + - '3.6' + +notifications: + webhooks: https://coveralls.io/webhook branches: only: @@ -12,7 +17,7 @@ branches: env: global: - - CANONICAL_PYTHON="3.6" + - CANONICAL_PYTHON="3.7" - CANONICAL_MDTRAJ="release" - CODECLIMATE="" - TWINE_USERNAME="dwhswenson" @@ -24,13 +29,14 @@ env: - MDTRAJ="release" - MDTRAJ="dev" -matrix: +jobs: exclude: - env: MDTRAJ="dev" - python: "3.5" + python: "3.6" before_install: - echo "before install" + - echo "$TRAVIS_BRANCH $TRAVIS_EVENT_TYPE" - git fetch --tags install: @@ -44,75 +50,15 @@ install: script: - export MPLBACKEND=SVG - python -c "import contact_map" - - python autorelease_check.py --branch ${TRAVIS_BRANCH} --event ${TRAVIS_EVENT_TYPE} --allow-patch-skip #TODO remove allow-patch-skip + - python autorelease_check.py --branch ${TRAVIS_BRANCH} --event ${TRAVIS_EVENT_TYPE} #--allow-patch-skip # allow-patch-skip if there was a testpypi problem - py.test -vv --cov=contact_map --cov-report xml:cov.xml after_success: + - export COVERALLS_PARALLEL=true - coveralls - python-codacy-coverage -r cov.xml -jobs: - include: - - stage: deploy testpypi - # This stage runs when you make a PR to stable. It tests that the - # deployment to testpypi works. - if: "(branch = stable) and (type = pull_request)" - python: '3.6' - addons: - apt_packages: - - pandoc - install: - - pip install twine - script: - - pandoc --from=markdown --to=rst --output=README.rst README.md - - python setup.py sdist bdist_wheel - - twine upload --repository-url https://test.pypi.org/legacy/ dist/* - after_success: skip - - stage: test testpypi - # This stage run when you make a PR to stable; after the package has - # been deployed to testpypi. It checks that the deployed package - # works. - if: "(branch = stable) and (type = pull_request)" - python: '3.6' - install: - - source ci/pip-install/install_requirements.sh - - pip install -U -r ci/pip-install/testing_requirements.txt - - pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple contact_map - script: - - cd ~ - - python -c "import contact_map" - - py.test --pyargs contact_map - after_success: skip - - stage: cut release - # This stage runs after you merge a PR into stable. It cuts the GitHub - # release based on the current stable branch and the release notes - # from the last PR merged into stable. - if: "(branch = stable) and (not type in (pull_request, cron))" - python: '3.6' - install: - - pip install autorelease - script: - - VERSION=`python setup.py --version` - - PROJECT=`python setup.py --name` - - autorelease-release --project $PROJECT --version $VERSION --token $AUTORELEASE_TOKEN - after_success: skip - - stage: deploy pypi - # This stage runs when a version-labelled tag is made. It deploys the - # package to PyPI. - if: tag =~ ^v[0-9]+\. - addons: - apt_packages: - - pandoc - install: skip - script: - - pandoc --from=markdown --to=rst --output=README.rst README.md - after_success: true - deploy: - provider: pypi - distributions: sdist bdist_wheel - skip_cleanup: true # need the readme.rst from the script stage - user: dwhswenson - on: - tags: true - password: - secure: "UN99BfNgMlE1qOwkbvJYMEdPGVF02dnZ5iF9HZY5HwJr7nlW1ixEGn6SzICS7WwLzthfqpbwXaqVGEsbsn1YwsvQe72JyCZxftoxZC2bVi5JV/OZka5uUTx0NXtzukZp33AUTDA9J2vushIxZCHSJG+GFqx4c+L8Y194V0QS4sw/lbtGC3qZ8jmszCnRgfPbb0tmCe4Jj1snclspPXhYtXL2z9Xh4r+ImCfosBFfThtsUs6t37/Jrx1srmDrt0eaGGBm1NAcyIVP6x03TdVVUFt/ud3U7+o0iHhqdEZKfUc2Dp6YN08Ck6Zyr/dR/A0Sc3/pWvytXwXLzOj+d7BtaAmPApcAc6v5GEmwWc4mkdsD+CEEW1q3CEFbWq6RTL+oa2zvEO6k0VrnHMCni3KqsWGmF6B209tHhy8jFAmCIruD2foUz7NxEKWNEOR4bqIjOGGIgGp4mfH+1l73JrI995ZmB1PqiIkxVeX0nPrZUZtBxOUyxg6aiL+NHMaYr5NCq2uCzDYhtwWnk26Oq6/JAEMxN+SCiC+O+DSvdQaUgUp6eS5whHGvf27ZxRMkmTSYTIMN6V1/PQ9WWSFsy0QBUYB7UJf/JLFnTi5xHj08jFNzUVOmeyq9MBzJF/ztnTTBSJgzNDROV+2qsZC0Ulm/hesMe0Swxqu/S5PelzrgwCA=" + +import: + - dwhswenson/autorelease:autorelease-travis.yml@v0.1.0 + diff --git a/MANIFEST.in b/MANIFEST.in index 13d8124..2216b6e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include contact_map/tests/trajectory.pdb +include contact_map/tests/*.pdb include LICENSE diff --git a/README.md b/README.md index b3c6c99..0389f4c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f7f3cf53698e4655ac8895f13fa5dea6)](https://www.codacy.com/app/dwhswenson/contact_map?utm_source=github.com&utm_medium=referral&utm_content=dwhswenson/contact_map&utm_campaign=Badge_Grade) [![Maintainability](https://api.codeclimate.com/v1/badges/84768756d594176d8da6/maintainability)](https://codeclimate.com/github/dwhswenson/contact_map/maintainability) -# Contact Maps +# Contact Map Explorer This package provides tools for analyzing and exploring contacts (residue-residue and atom-atom) from a trajectory generated by molecular @@ -20,9 +20,9 @@ processes involving biomolecules. For example, an analysis of contacts can be particularly useful when defining bound states during a binding processes between proteins, DNA, and small molecules (such as potential drugs). -The contacts analyzed by `contact_map` can be either intermolecular or -intramolecular, and can be analyzed on a residue-residue basis or an -atom-atom basis. +The contacts analyzed by Contact Map Explorer can be either intermolecular or +intramolecular, and can be analyzed on a residue-residue basis or an atom-atom +basis. This package makes it very easy to answer questions like: @@ -59,7 +59,7 @@ for details. ## Support and development -`contact_map` is an open source project, released under the GNU LGPL, +Contact Map Explorer is an open source project, released under the GNU LGPL, version 2.1 or (at your option) any later version. Development takes place in public at https://github.com/dwhswenson/contact_map; your contributions would be welcome! diff --git a/appveyor.yml b/appveyor.yml index 856193b..d2eda24 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,10 +21,10 @@ environment: CONDA_PY: "27" ARCH: '64' - PYTHON: "C:\\Miniconda36" - CONDA_PY: "36" + CONDA_PY: "36" # No MDTraj for py37/Win32 ARCH: '32' - - PYTHON: "C:\\Miniconda36-x64" - CONDA_PY: "36" + - PYTHON: "C:\\Miniconda37-x64" + CONDA_PY: "37" ARCH: '64' build: false @@ -32,13 +32,15 @@ build: false install: # install python - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% - # test python version - - "%CMD_IN_ENV% conda config --add channels omnia" # TODO: switch to conda-forge? - - "%CMD_IN_ENV% conda update -yq --all" - - "%CMD_IN_ENV% conda install -yq \"conda-build<3.0.26\"" - - "%CMD_IN_ENV% python --version" - - "%CMD_IN_ENV% python -c \"import struct; print(struct.calcsize('P') * 8)\"" - # now we do our own install + - conda config --add channels conda-forge + - conda install -yq conda=4.5 conda-build=3.10 test_script: - - "%CMD_IN_ENV% conda build --quiet ci\\conda-recipe" + #- "%CMD_IN_ENV% activate base" + #- "%CMD_IN_ENV% python --version" + #- "%CMD_IN_ENV% python -c \"import struct; print(struct.calcsize('P') * 8)\"" + #- "%CMD_IN_ENV% conda build --quiet ci\\conda-recipe" + - activate base + - python --version + - python -c "import struct; print(struct.calcsize('P') * 8)" + - conda build --quiet ci\conda-recipe diff --git a/autorelease_check.py b/autorelease_check.py index 2d1bfb2..9ac0536 100644 --- a/autorelease_check.py +++ b/autorelease_check.py @@ -3,17 +3,19 @@ import setup import contact_map from autorelease import DefaultCheckRunner, conda_recipe_version +from autorelease.version import get_setup_version from packaging.version import Version repo_path = '.' +SETUP_VERSION = get_setup_version(None, directory='.') versions = { 'package': contact_map.version.version, - 'setup.py': setup.PACKAGE_VERSION, + 'setup.py': SETUP_VERSION, 'conda-recipe': conda_recipe_version('ci/conda-recipe/meta.yaml'), } RELEASE_BRANCHES = ['stable'] -RELEASE_TAG = "v" + Version(setup.PACKAGE_VERSION).base_version +RELEASE_TAG = "v" + Version(SETUP_VERSION).base_version if __name__ == "__main__": checker = DefaultCheckRunner( diff --git a/ci/conda-recipe/meta.yaml b/ci/conda-recipe/meta.yaml index 51a4275..8ea4c9d 100644 --- a/ci/conda-recipe/meta.yaml +++ b/ci/conda-recipe/meta.yaml @@ -1,13 +1,12 @@ package: name: contact_map # add ".dev0" for unreleased versions - version: "0.4.0" + version: "0.5.0" source: path: ../../ build: - preserve_egg_dir: True number: 0 script: python setup.py install --single-version-externally-managed --record record.txt @@ -21,7 +20,7 @@ requirements: - mdtraj - numpy - scipy - - pandas + - pandas<1.0 test: requires: diff --git a/contact_map/contact_map.py b/contact_map/contact_map.py index 7c145f1..ea069c6 100644 --- a/contact_map/contact_map.py +++ b/contact_map/contact_map.py @@ -61,6 +61,9 @@ def _atom_slice(traj, indices): if traj._have_unitcell: unitcell_lengths = traj._unitcell_lengths.copy() unitcell_angles = traj._unitcell_angles.copy() + else: + unitcell_lengths = None + unitcell_angles = None time = traj._time.copy() # Hackish to make the smart slicing work diff --git a/contact_map/plot_utils.py b/contact_map/plot_utils.py index ba2ff56..4d86260 100644 --- a/contact_map/plot_utils.py +++ b/contact_map/plot_utils.py @@ -42,7 +42,7 @@ def ranged_colorbar(cmap, norm, cbmin, cbmax, name="Partial Map"): new_norm = matplotlib.colors.Normalize(vmin=cbmin, vmax=cbmax) sm = plt.cm.ScalarMappable(cmap=new_cmap, norm=new_norm) sm._A = [] - cb = plt.colorbar(sm) + cb = plt.colorbar(sm, fraction=0.046, pad=0.04) return cb diff --git a/contact_map/tests/test_contact_map.py b/contact_map/tests/test_contact_map.py index 26f1b2b..2d02445 100644 --- a/contact_map/tests/test_contact_map.py +++ b/contact_map/tests/test_contact_map.py @@ -1,6 +1,7 @@ import os import collections import mdtraj as md +import copy # pylint: disable=wildcard-import, missing-docstring, protected-access # pylint: disable=attribute-defined-outside-init, invalid-name, no-self-use @@ -11,7 +12,7 @@ # stuff to be testing in this file from contact_map.contact_map import * -from contact_map.contact_count import ContactCount, HAS_MATPLOTLIB +from contact_map.contact_count import HAS_MATPLOTLIB traj = md.load(find_testfile("trajectory.pdb")) @@ -39,9 +40,10 @@ test_file = "test_file.p" + def pdb_topology_dict(): serial = {str(i): i+1 for i in range(10)} - name = {str(i): "C" + str(i%2 + 1) for i in range(10)} + name = {str(i): "C" + str(i % 2 + 1) for i in range(10)} element = {str(i): "C" for i in range(10)} res_seq = {str(i): str(i/2 + 1) for i in range(10)} res_name = {str(i): "XXX" for i in range(10)} @@ -56,19 +58,23 @@ def pdb_topology_dict(): 'segmentID': seg_id} return dct + def counter_of_inner_list(ll): return collections.Counter([frozenset(i) for i in ll]) + def check_most_common_order(most_common): for i in range(len(most_common) - 1): assert most_common[i][1] >= most_common[i+1][1] + def check_use_atom_slice(m, use_atom_slice, expected): if use_atom_slice is not None: assert m._use_atom_slice == use_atom_slice else: assert m._use_atom_slice == expected[m] + def _contact_object_compare(m, m2): """Compare two contact objects (with asserts). @@ -95,6 +101,7 @@ def _check_contacts_dict_names(contact_object): for name in names: assert contacts.counter == contact_object.contacts[name].counter + def test_residue_neighborhood(): top = traj.topology residues = list(top.residues) @@ -262,15 +269,16 @@ def test_saving(self, idx): @pytest.mark.parametrize("use_atom_slice", [True, False, None]) def test_atom_slice(self, idx, use_atom_slice): - #Set class variable before init + # Set class variable before init class_default = ContactMap._class_use_atom_slice ContactMap._class_use_atom_slice = use_atom_slice map0q = ContactMap(traj[0], query=[1, 4, 5, 6], cutoff=0.075, n_neighbors_ignored=0) map0h = ContactMap(traj[0], haystack=[1, 4, 5, 6], cutoff=0.075, n_neighbors_ignored=0) - map0b = ContactMap(traj[0], query=[1, 4, 5, 6], haystack=[1,4,5,6], - cutoff=0.075, n_neighbors_ignored=0) + map0b = ContactMap(traj[0], query=[1, 4, 5, 6], + haystack=[1, 4, 5, 6], cutoff=0.075, + n_neighbors_ignored=0) maps = [map0q, map0h, map0b] atoms = {map0q: list(range(10)), map0h: list(range(10)), @@ -305,11 +313,26 @@ def test_atom_slice(self, idx, use_atom_slice): # Reset class variable (as imports are not redone between function # calls) ContactMap._class_use_atom_slice = class_default + def test_contacts_dict(self, idx): _check_contacts_dict_names(self.maps[idx]) + def test_no_unitcell(self, idx): + temptraj = copy.deepcopy(traj) + # Strip unitcell + temptraj.unitcell_vectors = None + + # Activate atom_slice + atoms = [1, 4, 5, 6] + mapi = ContactMap(temptraj[idx], cutoff=0.075, n_neighbors_ignored=0, + query=atoms, haystack=atoms) + expected_atom_contacts = {0: [[1, 4], [4, 6], [5, 6]], + 4: [[1, 4], [4, 6], [5, 6]]} + expected = counter_of_inner_list(expected_atom_contacts[idx]) + assert mapi._atom_contacts == expected # TODO: add tests for ContactObject._check_consistency + class TestContactFrequency(object): def setup(self): self.atoms = [0, 1, 4, 5, 6, 7, 8, 9] @@ -355,7 +378,7 @@ def test_check_compatibility_true(self): map2 = ContactFrequency(trajectory=traj[0:2], cutoff=0.075, n_neighbors_ignored=0) - assert self.map._check_compatibility(map2) == True + assert self.map._check_compatibility(map2) is True @pytest.mark.parametrize("diff", [ {'trajectory': traj.atom_slice([0, 1, 2, 3])}, @@ -512,10 +535,10 @@ def test_add_contact_frequency(self): start.add_contact_frequency(add_in) assert start.atom_contacts.counter == \ - self.map.atom_contacts.counter + self.map.atom_contacts.counter assert start.residue_contacts.counter == \ - self.map.residue_contacts.counter + self.map.residue_contacts.counter def test_subtract_contact_frequency(self): first_four = ContactFrequency(trajectory=traj[:4], @@ -531,14 +554,14 @@ def test_subtract_contact_frequency(self): test_subject.subtract_contact_frequency(first_four) assert test_subject.atom_contacts.counter == \ - last_frame.atom_contacts.counter + last_frame.atom_contacts.counter assert test_subject.residue_contacts.counter == \ - last_frame.residue_contacts.counter + last_frame.residue_contacts.counter @pytest.mark.parametrize("use_atom_slice", [True, False, None]) def test_use_atom_slice(self, use_atom_slice): - #Set class default before init + # Set class default before init class_default = ContactFrequency._class_use_atom_slice ContactFrequency._class_use_atom_slice = use_atom_slice mapq = ContactFrequency(trajectory=traj, cutoff=0.075, @@ -575,6 +598,7 @@ def test_use_atom_slice(self, use_atom_slice): # Reset class default as pytest does not re-import ContactFrequency._class_use_atom_slice = class_default + class TestContactDifference(object): def test_diff_traj_frame(self): ttraj = ContactFrequency(traj[0:4], cutoff=0.075, @@ -605,11 +629,11 @@ def test_diff_traj_frame(self): assert diff_1.atom_contacts.counter == expected_atom_count assert diff_2.atom_contacts.counter == \ - {k: -v for (k, v) in expected_atom_count.items()} + {k: -v for (k, v) in expected_atom_count.items()} assert diff_1.residue_contacts.counter == expected_residue_count assert diff_2.residue_contacts.counter == \ - {k: -v for (k, v) in expected_residue_count.items()} + {k: -v for (k, v) in expected_residue_count.items()} @pytest.mark.parametrize("intermediate", ["dict", "json"]) def test_serialization_cycle(self, intermediate): diff --git a/contact_map/version.py b/contact_map/version.py new file mode 100644 index 0000000..37d99b4 --- /dev/null +++ b/contact_map/version.py @@ -0,0 +1,141 @@ +# This file vendored from Autorelease +import os +import subprocess + +try: + from configparser import ConfigParser, NoSectionError, NoOptionError +except ImportError: + # py2 + from ConfigParser import ConfigParser, NoSectionError, NoOptionError + +try: + from ._installed_version import _installed_version + from ._installed_version import _installed_git_hash + from ._installed_version import _version_setup_depth +except ImportError: + _installed_version = "Unknown" + _installed_git_hash = "Unknown" + _version_setup_depth = -1 + + +def get_git_version(): + """ + Return the git hash as a string. + + Apparently someone got this from numpy's setup.py. It has since been + modified a few times. + """ + # Return the git revision as a string + # copied from numpy setup.py + def _minimal_ext_cmd(cmd): + # construct minimal environment + env = {} + for k in ['SYSTEMROOT', 'PATH']: + v = os.environ.get(k) + if v is not None: + env[k] = v + # LANGUAGE is used on win32 + env['LANGUAGE'] = 'C' + env['LANG'] = 'C' + env['LC_ALL'] = 'C' + with open(os.devnull, 'w') as err_out: + out = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=err_out, # maybe debug later? + env=env).communicate()[0] + return out + + try: + git_dir = os.path.dirname(os.path.realpath(__file__)) + out = _minimal_ext_cmd(['git', '-C', git_dir, 'rev-parse', 'HEAD']) + GIT_REVISION = out.strip().decode('ascii') + except OSError: + GIT_REVISION = 'Unknown' + + return GIT_REVISION + +def _seek_parent_dirs_for_file(filename): + rel_directory = None + my_dir = os.path.dirname(os.path.abspath(__file__)) + rel_directory_arr = [] + while not rel_directory: + expected_dir = os.path.join(*rel_directory_arr) \ + if rel_directory_arr else '.' + expected = os.path.join(expected_dir, filename) + if os.path.isfile(os.path.normpath(expected)): + rel_directory = expected_dir + else: + rel_directory_arr.append('..') + + if len(rel_directory_arr) > len(my_dir.split(os.sep)): + rel_directory_arr = [] + break + + return rel_directory + + +def _find_rel_path_for_file(depth, filename): + rel_directory = None + if depth == 0: + rel_directory = '.' + elif depth >= 1: + rel_directory = os.sep.join(['..'] * depth) + else: + rel_directory = _seek_parent_dirs_for_file(filename) + + if rel_directory: + return os.path.normpath(os.path.join(rel_directory, filename)) + else: + return None + + +def get_setup_cfg(directory, filename="setup.cfg"): + """Load the setup.cfg as a dict-of-dict. + + Parameters + ---------- + directory : str + directory for setup.cfg, relative to cwd; default '.' + filename : str + filename for setup.cfg; default 'setup.cfg' + """ + if isinstance(directory, int): + rel_path = _find_rel_path_for_file(directory, filename) + start_dir = os.path.abspath(os.path.dirname(__file__)) + setup_cfg = os.path.normpath(os.path.join(start_dir, rel_path)) + else: + setup_cfg = os.path.join(directory, filename) + + conf = None + if os.path.exists(setup_cfg): + conf = ConfigParser() + conf.read(setup_cfg) + + return conf + + +def get_setup_version(default_version, directory, filename="setup.cfg"): + version = default_version + conf = get_setup_cfg(directory, filename) + try: + version = conf.get('metadata', 'version') + except (NoSectionError, NoOptionError): + pass # version (or metadata) not defined in setup.cfg + except AttributeError: + pass # no setup.cfg found (conf is None) + return version + + +short_version = get_setup_version(_installed_version, + directory=_version_setup_depth) +_git_version = get_git_version() +_is_repo = (_git_version != '' and _git_version != "Unknown") + +if _is_repo: + git_hash = _git_version + full_version = short_version + "+g" + _git_version[:7] + version = full_version +else: + git_hash = "Unknown" + full_version = short_version + "+g" + _installed_git_hash[:7] + '.install' + version = short_version diff --git a/docs/conf.py b/docs/conf.py index 72bc1d2..0edcb3c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# contact_map documentation build configuration file, created by +# Contact Map Explorer documentation build configuration file, created by # sphinx-quickstart on Sun Oct 8 16:23:29 2017. # # This file is execfile()d with the current directory set to its @@ -100,9 +100,9 @@ master_doc = 'index' # General information about the project. -project = u'contact_map' -copyright = u'2017, David W.H. Swenson' -author = u'David W.H. Swenson' +project = u'Contact Map Explorer' +copyright = u'2017-2019, David W.H. Swenson and Sander Roet' +author = u'David W.H. Swenson and Sander Roet' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -304,8 +304,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'contact_map.tex', u'contact\\_map Documentation', - u'David W.H. Swenson', 'manual'), + (master_doc, 'contact_map.tex', u'Contact Map Explorer Documentation', + u'David W.H. Swenson and Sander Roet', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -346,7 +346,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'contact_map', u'contact_map Documentation', + (master_doc, 'contact_map', u'Contact Map Explorer Documentation', [author], 1) ] @@ -361,7 +361,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'contact_map', u'contact_map Documentation', + (master_doc, 'contact_map', u'Contact Map Explorer Documentation', author, 'contact_map', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/index.rst b/docs/index.rst index 0a89e91..d8bd08f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,8 +5,8 @@ .. currentmodule:: contact_map -``contact_map`` -=============== +Contact Map Explorer +==================== This package provides tools for analyzing and exploring contacts (residue-residue and atom-atom) from a trajectory generated by molecular @@ -18,8 +18,8 @@ processes involving biomolecules. For example, an analysis of contacts can be particularly useful when defining bound states during a binding processes between proteins, DNA, and small molecules (such as potential drugs). -The contacts analyzed by ``contact_map`` can be either intermolecular or -intramolecular, and can be analyzed on a residue-residue basis or an +The contacts analyzed by Contact Map Explorer can be either intermolecular +or intramolecular, and can be analyzed on a residue-residue basis or an atom-atom basis. This package makes it very easy to answer questions like: @@ -45,7 +45,7 @@ representing the fraction of trajectory time that the contact was present. * :ref:`genindex` -``contact_map`` is an open source project, released under the GNU LGPL, +Contact Map Explorer is an open source project, released under the GNU LGPL, version 2.1 or (at your option) any later version. Development takes place in public at https://github.com/dwhswenson/contact_map; your contributions would be welcome! diff --git a/docs/installing.rst b/docs/installing.rst index 07aaab9..13d24a2 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -101,7 +101,7 @@ Additional functionality ======================== Installing some additional packages will immediately enable additional -features in ``contact_map``. To get all the functionality, install the +features in Contact Map Explorer. To get all the functionality, install the packages in ``optional_installs.txt``, either with ``pip install -r optional_installs.txt`` or ``conda install -y --file optional_installs.txt``. @@ -133,5 +133,5 @@ suite. This can be done by installing ``pytest`` (using either ``pip`` or py.test --pyargs contact_map -v -This will run the tests on the installed version of ``contact_map``. All -tests should either pass or skip. +This will run the tests on the installed version of Contact Map Explorer. +All tests should either pass or skip. diff --git a/examples/concurrences.ipynb b/examples/concurrences.ipynb index 71c1e2f..895da5d 100644 --- a/examples/concurrences.ipynb +++ b/examples/concurrences.ipynb @@ -6,7 +6,7 @@ "source": [ "# Concurrences\n", "\n", - "One of the tools in `contact_map` is the ability to look at simultaneous contacts. The idea is that you might have a set of contacts that is likely to happen concurrently, and that this set of contacts might help you define a stable state. This is managed in `contact_map` by what we call contact concurrences.\n", + "One of the tools in Contact Map Explorer is the ability to look at simultaneous contacts. The idea is that you might have a set of contacts that is likely to happen concurrently, and that this set of contacts might help you define a stable state. This is managed in Contact Map Explorer by what we call contact concurrences.\n", "\n", "To start, we'll look at a trajectory of a specific inhibitor during its binding process to GSK3B." ] @@ -224,7 +224,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.2" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/contact_map.ipynb b/examples/contact_map.ipynb index 8130622..55d6df1 100644 --- a/examples/contact_map.ipynb +++ b/examples/contact_map.ipynb @@ -6,9 +6,9 @@ "source": [ "# Contact Maps\n", "\n", - "The `contact_map` package includes some tricks to study contact maps in protein dynamics, based on tools in MDTraj. This notebook shows examples and serves as documentation.\n", + "Contact Map Explorer includes some tricks to study contact maps in protein dynamics, based on tools in MDTraj. This notebook shows examples and serves as documentation.\n", "\n", - "As an example, we'll use part of a trajectory of the KRas protein bound to GTP, which was provided by Sander Roet. KRas is a protein that plays a role in many cancers. For simplicity, the waters were removed from the trajectory (although ions are still included). To run this notebook, download the example files from https://figshare.com/s/453b1b215cf2f9270769 (total download size about 1.2 MB). Download all files, and extract in the same directory that you started Jupyer from (so that you have a directory called `5550217` in your current working directory)." + "As an example, we'll use part of a trajectory of the KRas protein bound to GTP, which was provided by Sander Roet. KRas is a protein that plays a role in many cancers. For simplicity, the waters were removed from the trajectory (although ions are still included). To run this notebook, download the example files from https://figshare.com/s/453b1b215cf2f9270769 (total download size about 1.2 MB). Download all files, and extract in the same directory that you started Jupyter from (so that you have a directory called `5550217` in your current working directory)." ] }, { @@ -1032,7 +1032,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/contact_map_without_atom_slice.ipynb b/examples/contact_map_without_atom_slice.ipynb index 98de8a0..59cef36 100644 --- a/examples/contact_map_without_atom_slice.ipynb +++ b/examples/contact_map_without_atom_slice.ipynb @@ -282,21 +282,21 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 2", + "display_name": "Python 3", "language": "python", - "name": "python2" + "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.15" + "pygments_lexer": "ipython3", + "version": "3.7.3" } }, "nbformat": 4, diff --git a/examples/dask_contact_frequency.ipynb b/examples/dask_contact_frequency.ipynb index a6077dd..bb58479 100644 --- a/examples/dask_contact_frequency.ipynb +++ b/examples/dask_contact_frequency.ipynb @@ -191,21 +191,21 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 2", + "display_name": "Python 3", "language": "python", - "name": "python2" + "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.14" + "pygments_lexer": "ipython3", + "version": "3.7.3" } }, "nbformat": 4, diff --git a/requirements.txt b/requirements.txt index 6cd0d95..2bc28c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ future numpy scipy -pandas +pandas<1.0 mdtraj diff --git a/setup.cfg b/setup.cfg index 3c6e79c..d6415dd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,33 @@ +[metadata] +name = contact_map +version = 0.5.0 +description = Contact maps based on MDTraj +long_description = file: README.md +long_description_content_type = text/markdown +author = David W.H. Swenson and Sander Roet +author_email = dwhs@hyperblazer.net, sanderroet@hotmail.com +license = LGPL 2.1+ +url = https://github.com/dwhswenson/contact_map +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Science/Research + License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) + Operating System :: POSIX + Operating System :: Microsoft :: Windows + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Topic :: Scientific/Engineering :: Bio-Informatics + Topic :: Scientific/Engineering :: Chemistry + +[options] +include_package_data = True +install_requires = + future + numpy + mdtraj + scipy + pandas<1.0 +packages = find: + [bdist_wheel] universal=1 diff --git a/setup.py b/setup.py index 8ef8d25..2a7d0df 100644 --- a/setup.py +++ b/setup.py @@ -1,183 +1,129 @@ -""" -setup.py for contact_map -""" +# This file is vendored from Autorelease import os -import subprocess -import inspect +import ast +import sys + +import fnmatch # Py 2 + from setuptools import setup -####################### USER SETUP AREA ################################# -# * VERSION: base version (do not include .dev0, etc -- that's automatic) -# * IS_RELEASE: whether this is a release -VERSION = "0.4.0" -IS_RELEASE = True - -DEV_NUM = 0 # always 0: we don't do public (pypi) .dev releases -PRE_TYPE = "" # a, b, or rc (although we rarely release such versions) -PRE_NUM = 0 - -# REQUIREMENTS should list any required packages -REQUIREMENTS=['future', 'numpy', 'mdtraj', 'scipy', 'pandas'] - -# PACKAGES should list any subpackages of the code. The assumption is that -# package.subpackage is located at package/subpackage -PACKAGES=['contact_map', 'contact_map.tests'] - -# This DESCRIPTION is only used if a README.rst hasn't been made from the -# markdown version -DESCRIPTION=""" -Contact maps based on MDTraj; useful for studying for intramolecular and -intermolecular contacts (atom-atom or residue-residue) from simulations of -biomolecular systems. For a more detailed description, see package -documentation at http://contact-map.readthedocs.io/ -""" -SHORT_DESCRIPTION="Contact maps based on MDTraj" - -# note: leave the triple quotes on separate lines from the classifiers -CLASSIFIERS=""" -Development Status :: 4 - Beta -Intended Audience :: Science/Research -License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) -Operating System :: POSIX -Operating System :: Microsoft :: Windows -Programming Language :: Python :: 2.7 -Programming Language :: Python :: 3 -Topic :: Scientific/Engineering :: Bio-Informatics -Topic :: Scientific/Engineering :: Chemistry -""" -####################### USER SETUP AREA ################################# - - -# * VERSION: the release version number -# * __version__: ?? how is this used ?? -# * PACKAGE_VERSION: the version used in setup.py info -PACKAGE_VERSION = VERSION -if PRE_TYPE != "": - PACKAGE_VERSION += "." + PRE_TYPE + str(PRE_NUM) -if not IS_RELEASE: - PACKAGE_VERSION += ".dev" + str(DEV_NUM) -__version__ = PACKAGE_VERSION - -if os.path.isfile('README.rst'): - DESCRIPTION = open('README.rst').read() - -################################################################################ -# Writing version control information to the module -################################################################################ -def get_git_version(): - """ - Return the git hash as a string. - - Apparently someone got this from numpy's setup.py. It has since been - modified a few times. - """ - # Return the git revision as a string - # copied from numpy setup.py - def _minimal_ext_cmd(cmd): - # construct minimal environment - env = {} - for k in ['SYSTEMROOT', 'PATH']: - v = os.environ.get(k) - if v is not None: - env[k] = v - # LANGUAGE is used on win32 - env['LANGUAGE'] = 'C' - env['LANG'] = 'C' - env['LC_ALL'] = 'C' - with open(os.devnull, 'w') as err_out: - out = subprocess.Popen(cmd, - stdout=subprocess.PIPE, - stderr=err_out, # maybe debug later? - env=env).communicate()[0] - return out - - try: - git_dir = os.path.dirname(os.path.realpath(__file__)) - out = _minimal_ext_cmd(['git', '-C', git_dir, 'rev-parse', 'HEAD']) - GIT_REVISION = out.strip().decode('ascii') - except OSError: - GIT_REVISION = 'Unknown' - - return GIT_REVISION - -# TODO: this may get moved into another file -VERSION_PY_CONTENT = """ -# This file is automatically generated by setup.py -\"\"\" -Version info for contact_map. - -``full_version`` gives the most information about the current state. It is -always the short (PEP440) version string, followed by a git hash as -``+gGITHASH``. If the install is not in a live git repository, that hash is -followed by ``.install``, and represents the commit that was installed. In a -live repository, it represents the active state. -\"\"\" +def _glob_glob_recursive(directory, pattern): + # python 2 glob.glob doesn't have a recursive keyword + # this implements for the specific case that we want an exact match + # See also https://stackoverflow.com/a/2186565 + matches = [] + for root, dirname, filenames in os.walk(directory): + matches.extend([os.path.join(root, filename) + for filename in fnmatch.filter(filenames, pattern)]) + return matches + + +class VersionPyFinder(object): + _VERSION_PY_FUNCTIONS = ['get_git_version', 'get_setup_cfg'] + def __init__(self, filename='version.py', max_depth=2): + self.filename_base = filename + self.max_depth = max_depth + self.depth = None + self.filename = os.getenv("AUTORELEASE_VERSION_PY", + self._first_eligible()) + self.functions = self._get_functions(self.filename) + + def _find_files(self): + # all_files = glob.glob("**/" + self.filename_base, recursive=True) + all_files = _glob_glob_recursive('.', self.filename_base) + meets_depth = [fname for fname in all_files + if len(fname.split(os.sep)) <= self.max_depth + 1] + return meets_depth + + def _is_eligible(self, filename): + with open(filename, mode='r') as f: + contents = f.read() + + tree = ast.parse(contents) + # we requrie that our functions be defined at module level -- we + # know that's how we wrote them, at least! + all_functions = [node.name for node in tree.body + if isinstance(node, ast.FunctionDef)] + return all(func in all_functions + for func in self._VERSION_PY_FUNCTIONS) + + def _first_eligible(self): + all_files = self._find_files() + for fname in all_files: + if self._is_eligible(fname): + return fname + return None + + @property + def version_setup_depth(self): + def get_depth(fname): + return len(os.path.abspath(fname).split(os.sep)) + + # we assume thta setup.py is in the same dir as setup.cfg + diff = get_depth(self.filename) - get_depth(__file__) + return diff + + def _get_functions(self, filename): + with open(self.filename, mode='r') as f: + contents = f.read() + + tree = ast.parse(contents) + + class MakeImportError(ast.NodeTransformer): + """converts a from x import y into an import error""" + def __init__(self, import_name): + self.import_name = import_name + + def visit_ImportFrom(self, node): + if node.module == self.import_name: + replacement = ast.Raise(exc=ast.Call( + func=ast.Name(id='ImportError', ctx=ast.Load()), + args=[], + keywords=[], + ), cause=None) + return ast.copy_location(replacement, node) + else: + return node + + import_remover = MakeImportError("_installed_version") + tree = import_remover.visit(tree) + ast.fix_missing_locations(tree) + + locs = dict(globals()) + exec(compile(tree, filename="version.py", mode='exec'), locs) + return {f: locs[f] for f in self._VERSION_PY_FUNCTIONS} + + +def write_installed_version_py(filename="_installed_version.py", + src_dir=None): + version_finder = VersionPyFinder() + directory = os.path.dirname(version_finder.filename) + depth = version_finder.version_setup_depth + get_git_version = version_finder.functions['get_git_version'] + get_setup_cfg = version_finder.functions['get_setup_cfg'] + + installed_version = os.path.join(directory, "_installed_version.py") + content = "_installed_version = '{vers}'\n" + content += "_installed_git_hash = '{git}'\n" + content += "_version_setup_depth = {depth}\n" + + # question: if I use the __file__ attribute in something I compile from + # here, what is the file? + my_dir = os.path.abspath(os.path.dirname(__file__)) + conf = get_setup_cfg(directory=my_dir, filename='setup.cfg') + # conf = get_setup_cfg(directory=my_dir, filename='new_setup.cfg') + version = conf.get('metadata', 'version') + git_rev = get_git_version() + + if src_dir is None: + src_dir = conf.get('metadata', 'name') + + with open (os.path.join(src_dir, filename), 'w') as f: + f.write(content.format(vers=version, git=git_rev, depth=depth)) -import os -import subprocess - -# this is automatically generated from the code in setup.py -%(git_version_code)s - -short_version = '%(version)s' -version = '%(version)s' -installed_git_hash = '%(git_revision)s' -full_version = version + '+g' + installed_git_hash[:7] + '.install' -release = %(is_release)s -git_hash = 'Unknown' # default - -if not release: - git_hash = get_git_version() - if git_hash != '' and git_hash != 'Unknown': - full_version = version + '+g' + git_hash[:7] - - version = full_version -""" - -def write_version_py(filename): - # Adding the git rev number needs to be done inside write_version_py(), - # otherwise the import of numpy.version messes up the build under Python 3. - git_version_code = inspect.getsource(get_git_version) - if os.path.exists('.git'): - GIT_REVISION = get_git_version() - else: - GIT_REVISION = 'Unknown' - - content = VERSION_PY_CONTENT % { - 'version': PACKAGE_VERSION, - 'git_revision': GIT_REVISION, - 'is_release': str(IS_RELEASE), - 'git_version_code': str(git_version_code) - } - - with open(filename, 'w') as version_file: - version_file.write(content) - - -################################################################################ -# Installation -################################################################################ if __name__ == "__main__": - write_version_py(os.path.join('contact_map', 'version.py')) - setup( - name="contact_map", - author="David W.H. Swenson", - author_email="dwhs@hyperblazer.net", - version=PACKAGE_VERSION, - license="LGPL-2.1+", - url="http://github.com/dwhswenson/contact_map", - packages=PACKAGES, - package_dir={p: '/'.join(p.split('.')) for p in PACKAGES}, - package_data={'contact_map': ['tests/*pdb']}, - ext_modules=[], - scripts=[], - description=SHORT_DESCRIPTION, - long_description=DESCRIPTION, - platforms=['Linux', 'Mac OS X', 'Unix', 'Windows'], - install_requires=REQUIREMENTS, - requires=REQUIREMENTS, - tests_require=["pytest", "pytest-cov", "python-coveralls"], - classifiers=CLASSIFIERS.split('\n')[1:-1] - ) - + # TODO: only write version.py under special circumstances + write_installed_version_py() + # write_version_py(os.path.join('autorelease', 'version.py')) + setup()