├── .bumpversion.cfg ├── .coveragerc ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── environment.yml ├── pymolprobity ├── __init__.py ├── colors.py ├── flips.py ├── gui.py ├── kinemage.py ├── main.py ├── points.py └── utils.py ├── requirements.txt └── tests ├── __init__.py ├── colors_tests.py ├── context.py ├── flips_tests.py ├── gui_tests.py ├── kinemage_tests.py ├── main_tests.py ├── points_tests.py └── utils_tests.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.5 3 | 4 | [bumpversion:file:pymolprobity/__init__.py] 5 | 6 | [bumpversion:file:CHANGELOG.md] 7 | search = ## Unreleased 8 | replace = ## Unreleased 9 | - 10 | -## v{new_version} 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = ./pymolprobity 3 | include = ./* 4 | 5 | # vim: ft=config: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | *.pyc 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 15 | 16 | 17 | ## Unreleased 18 | 19 | ## v0.1.5 20 | 21 | ### Added 22 | - Config file for use with `bumpversion` utility. 23 | 24 | ### Changed 25 | - Format of this changelog file. 26 | 27 | ### Fixed 28 | - Improved instructions for incentive build installation and PATH setup. 29 | 30 | 31 | 32 | ## v0.1.4 33 | 34 | ### Added 35 | - Citation instructions and release DOIs via GitHub/Zenodo. 36 | - This changelog file. 37 | 38 | 39 | 40 | ## v0.1.3 41 | 42 | ### Added 43 | - Python3 compatibility by Thomas Holder ([@speleo3][]). 44 | - Instructions for symlinking to Phenix-installed Reduce and Probe. 45 | 46 | ### Changed 47 | - Installation instructions to use Github master zip file by default. 48 | 49 | ### Fixed 50 | - Several bugs related to Tk interface, by Thomas Holder ([@speleo3][]). 51 | 52 | ### Removed 53 | - `make` targets "updateenv" and "run". 54 | 55 | 56 | 57 | ## v0.1.2 58 | 59 | ### Fixed 60 | - Parsing of Flipkin output for some atoms ("invalid literal for float" error). 61 | 62 | 63 | 64 | ## v0.1.1 65 | 66 | ### Changed 67 | - Redirect some low-level output to logger instead of console. 68 | 69 | ### Fixed 70 | - Provide a more informative error message when required executables are not found in PATH. 71 | - An error that occurred when trying to load a non-existant object. 72 | 73 | 74 | 75 | ## v0.1.0 76 | 77 | - Initial release 78 | 79 | 80 | 81 | [@speleo3]: https://github.com/speleo3 82 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jared M. Sampson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "Possible targets:" 3 | @echo " init: install dependencies using conda or pip" 4 | @echo " test: run tests (requires \`nose\`)" 5 | @echo " testcov: run tests and determine coverage (requires \`coverage\')" 6 | @echo " cov: full coverage report (requires \`coverage\`)" 7 | @echo " clean: delete \`.pyc\` files" 8 | 9 | init: 10 | conda env create -f environment.yml || pip install -r requirements.txt 11 | 12 | test: 13 | export PYTHONPATH=${PYMOL_HOME}; nosetests tests 14 | 15 | testcov: 16 | export PYTHONPATH=${PYMOL_HOME}; nosetests --with-coverage --cover-erase tests 17 | 18 | cov: 19 | make testcov 20 | coverage report -m pymolprobity/*.py 21 | 22 | clean: 23 | rm pymolprobity/*.pyc 24 | rm tests/*.pyc 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyMOLProbity 2 | 3 | #### A MolProbity-style visualization plugin for PyMOL 4 | 5 | The PyMOLProbity plugin allows a [PyMOL][] user to produce MolProbity-style 6 | visualization of atomic interactions within a structure (e.g. H-bonds, van der 7 | Waals interactions and clashes) directly within a PyMOL session. The plugin 8 | runs local copies of several executable programs from the [Richardson Lab][] at 9 | Duke University, authors of the [MolProbity][] software, parses the output, and 10 | displays the results in the PyMOL viewport. There are both a graphical user 11 | interface (GUI) for general point-and-click use, and a command-line interface 12 | (CLI) suitable for scripting. 13 | 14 | [PyMOL]: http://www.pymol.org/ 15 | [Richardson Lab]: http://kinemage.biochem.duke.edu/ 16 | [MolProbity]: http://molprobity.biochem.duke.edu/ 17 | 18 | 19 | ## Getting Started 20 | 21 | ### Linux/MacOS only 22 | 23 | Because the Reduce, Probe, and Prekin executables are only available for Linux 24 | and MacOS, unfortunately PyMOLProbity is not currently useful on Windows. 25 | However, if there is sufficient demand, it wouldn't be too difficult to add 26 | support to read PDB and Kinemage files generated by the MolProbity server and 27 | downloaded to the local filesystem. Please submit a [feature request][] if 28 | this interests you. 29 | 30 | [feature request]: https://github.com/jaredsampson/pymolprobity/issues 31 | 32 | ### Prerequisites 33 | 34 | #### PyMOL 35 | 36 | To use this PyMOL plugin, of course you will need to have PyMOL installed on 37 | your machine. Incentive PyMOL users can download precompiled binaries; for 38 | those who don't have an Incentive PyMOL license, there is Open Source PyMOL. 39 | Detailed instructions are available via the [PyMOL Wiki][]. 40 | 41 | [PyMOL Wiki]: http://www.pymolwiki.org/index.php/Category:Installation 42 | 43 | **Only PyMOL v2.0 or later is supported.** 44 | 45 | 46 | #### MolProbity programs 47 | 48 | To work properly, PyMOLProbity requires 4 programs from the MolProbity software 49 | package: Reduce, Probe, Flipkin (actually a Perl script), and Prekin, which are 50 | available from the Richardson Lab [Github account][]. There are two general 51 | ways to satisfy these requirements. 52 | 53 | [Github account]: https://github.com/rlabduke 54 | 55 | 1. **With PHENIX.** If you have [PHENIX][] 56 | installed and properly configured, you already have Reduce and Probe 57 | available as `phenix.reduce` and `phenix.probe`, so you only need Flipkin and 58 | Prekin. 59 | 60 | * **Direct download links (right click, Save as...):** 61 | * Flipkin: [PHENIX-compatible version][fkp] 62 | * Prekin: [Linux (64-bit)][pkl64], [Linux (32-bit)][pkl32], [MacOS][pkm] 63 | 64 | You will need to create either aliases or symlinks to the `phenix.xxx` 65 | executables so PyMOLProbity can find them by calling `reduce` and `probe` from 66 | the shell. For example, the following added to `~/.bashrc` should be 67 | sufficient in most cases. 68 | 69 | ``` 70 | alias reduce=phenix.reduce 71 | alias probe=phenix.probe 72 | ``` 73 | 74 | 75 | [PHENIX]: http://www.phenix-online.org/ 76 | 77 | 2. **Without PHENIX.** If you do not wish to install PHENIX, you'll need to 78 | download all 4 programs. Note that the appropriate Flipkin in this case is an 79 | older version of the script, as the latest version specifies 80 | `phenix.reduce` and `phenix.probe` explicitly. 81 | 82 | * **Direct download links (right click, Save as...):** 83 | * Reduce: [Linux][rl], [MacOS][rm] 84 | * Probe: [Linux][pl], [MacOS][pmac] 85 | * Flipkin: [Non-PHENIX version][fkold] 86 | * Prekin: [Linux (64-bit)][pkl64], [Linux (32-bit)][pkl32], [MacOS][pkm] 87 | 88 | [rl]: https://github.com/rlabduke/MolProbity/raw/master/bin/linux/reduce 89 | [rm]: https://github.com/rlabduke/MolProbity/raw/master/bin/macosx/reduce 90 | [pl]: https://github.com/rlabduke/MolProbity/raw/master/bin/linux/probe 91 | [pmac]: https://github.com/rlabduke/MolProbity/raw/master/bin/macosx/probe 92 | [fkp]: https://raw.githubusercontent.com/rlabduke/MolProbity/master/bin/flipkin 93 | [fkold]: https://github.com/rlabduke/MolProbity/raw/4f09d8a7e888d865fecec99a9b7338303b0cbc51/bin/flipkin 94 | [pkl64]: https://github.com/rlabduke/MolProbity/raw/master/bin/linux/prekin_64 95 | [pkl32]: https://github.com/rlabduke/MolProbity/raw/master/bin/linux/prekin_32 96 | [pkm]: https://github.com/rlabduke/MolProbity/raw/master/bin/macosx/prekin 97 | 98 | Place the downloaded files in a directory on the shell `PATH` such as 99 | `/usr/local/bin` and make sure they are executable: 100 | 101 | ``` 102 | cd /usr/local/bin 103 | chmod +x flipkin prekin probe reduce 104 | ``` 105 | 106 | You may also wish to download the Richardson group's "slightly modified version 107 | of the connectivity table published by the PDB" from the [Reduce GitHub repo][rp], `reduce_wwPDB_het_dict.txt`. This file should be placed in `/usr/local`. If you don't, you'll 108 | probably get the following error when you run Reduce: 109 | 110 | [rp]: https://github.com/rlabduke/reduce/raw/master/reduce_wwPDB_het_dict.txt 111 | 112 | ``` 113 | ERROR CTab(/usr/local/reduce_wwPDB_het_dict.txt): could not open 114 | ``` 115 | 116 | 117 | ### Installation 118 | 119 | Installation of PyMOLProbity itself should be straightforward using PyMOL's 120 | [Plugin Manager][pm]. (Mac users note: Plugins are only available for 1.x 121 | versions of MacPyMOL if you use the X11 hybrid [tweak][], which entails simply 122 | renaming or copying `MacPyMOL.app` to `PyMOLX11Hybrid.app` in your Applications 123 | folder. For PyMOL version 2.0 and newer, plugins are built-in and you will not 124 | need to do this.) 125 | 126 | [pm]: http://www.pymolwiki.org/index.php/Plugin_Manager 127 | [tweak]: http://www.pymolwiki.org/index.php/Plugins#Plugins_on_OS_X 128 | 129 | Launch PyMOL, open the Plugin Manager from the menu (*Plugin* > *Plugin 130 | Manager*), and navigate to the *Install New Plugin* tab. Paste the following 131 | URL into the *URL* box and click *Fetch*. 132 | 133 | https://github.com/jaredsampson/pymolprobity/archive/master.zip 134 | 135 | Confirm you wish to proceed with the download and the plugin will be installed 136 | automatically. Alternatively, or if the URL method fails, you can [download][] 137 | the zipped plugin archive file (or a specific [version][], if desired) and 138 | select it using the file chooser. 139 | 140 | [download]: https://github.com/jaredsampson/pymolprobity/archive/master.zip 141 | [version]: https://github.com/jaredsampson/pymolprobity/releases 142 | 143 | *Note:* If you are using PyMOL version earlier than 2.3.3, the URL-based option 144 | above will fail due to the presence of thpe `tests` directory, so you will need 145 | to download the `master.zip` file from the link above, unzip it, delete the 146 | `tests` directory, and re-zip the file. This can all be done via the following 147 | `bash` commands: 148 | 149 | ``` 150 | curl -o pymolprobity-master.zip -O https://github.com/jaredsampson/pymolprobity/archive/master.zip \ 151 | && unzip pymolprobity-master.zip \ 152 | && rm -r pymolprobity-master/tests \ 153 | && rm pymolprobity-master.zip \ 154 | && zip -r pymolprobity-master.zip pymolprobity-master 155 | ``` 156 | 157 | One final step is required if you are using an incentive build and launching 158 | from an app icon rather than from the terminal. You will need to ensure that 159 | the path to the Reduce/Probe executables is included in the shell PATH within 160 | the PyMOL app environment. This can be done by editing your `pymolrc` file via 161 | *File* > *Edit pymolrc* and adding the following lines, changing the path to 162 | point to the actual location of your MolProbity programs if you have not used 163 | `/usr/local/bin`. 164 | 165 | ``` 166 | # for PyMOLProbity plugin, path to reduce/probe/flipkin/prekin executables 167 | import os 168 | os.environ['PATH'] += os.pathsep + '/usr/local/bin' 169 | ``` 170 | 171 | 172 | ### First run 173 | 174 | Now you can open PyMOL, load or fetch a structure, and launch PyMOLProbity from 175 | the Plugin menu. Use the *Add Hydrogens* tab to add hydrogens with Reduce. 176 | This will also calculate which N/Q/H residue side chains should be flipped. If 177 | you would like to examine these more closely, use the *Review Flips* tab to 178 | examine each flippable residue and choose the ones you wish to flip or keep. 179 | Save these using the *Save Selections* button. Finally, use the *Visualize 180 | Contacts* tab to run Probe on the modified coordinates and generate contact 181 | dots and clash vectors for all the atoms in your object. 182 | 183 | 184 | ## Development 185 | 186 | Users who wish to inspect or tinker with the code or contribute to development 187 | may wish to clone the repository. 188 | 189 | ``` 190 | git clone https://github.com/jaredsampson/pymolprobity.git 191 | cd pymolprobity 192 | ``` 193 | 194 | ### Running the unit tests 195 | 196 | A unit testing suite is included in the `tests` subdirectory. 197 | 198 | Running the tests requires the Python packages `nose`, `mock`, and optionally 199 | `coverage`. If you have these installed already, you can skip this step; if 200 | not, run the following command to install the three packages and their 201 | dependencies using `conda` if available, and `pip` otherwise. 202 | 203 | ``` 204 | make init 205 | ``` 206 | 207 | In the case of `conda`, the packages will be created in a new environment 208 | called `mp`, which you should then activate with: 209 | 210 | ``` 211 | conda activate mp 212 | ``` 213 | 214 | and deactivate when you are finished with: 215 | 216 | ``` 217 | conda deactivate 218 | ``` 219 | 220 | `virtualenv` users can run `make init` from within an activated virtualenv to 221 | keep these packages out of your global Python environment as well. 222 | 223 | Once `nose` and `mock` are installed, run the unit tests with 224 | 225 | ``` 226 | make test 227 | ``` 228 | 229 | To run the tests and determine code coverage: 230 | 231 | ``` 232 | make testcov 233 | ``` 234 | 235 | To see a full coverage report, including line numbers missed by the tests: 236 | 237 | ``` 238 | make cov 239 | ``` 240 | 241 | ### Troubleshooting 242 | 243 | If you run the tests and get errors like: 244 | 245 | ``` 246 | ImportError: No module named pymol 247 | ImportError: No module named chempy 248 | ``` 249 | 250 | it means your PyMOL installation's Python modules are not being found by 251 | Python. The easiest way to solve this (which is accounted for in the Makefile) 252 | is to set `PYMOL_HOME` in your `~/.bashrc` file and start a new terminal 253 | session. 254 | 255 | ``` 256 | export PYMOL_HOME=/path/to/pymol-installation/libexec/lib/python2.7/site-packages 257 | ``` 258 | 259 | where `/path/to/pymol-installation/bin/pymol` is the absolute path to your 260 | PyMOL executable. 261 | 262 | 263 | ## Contributing 264 | 265 | For a closer look at the plugin, to run tests yourself, or to help improve this 266 | plugin, please feel free to clone or fork this repository. Bug reports and 267 | pull requests are definitely welcome! 268 | 269 | 270 | ## Versioning 271 | 272 | PyMOLProbity uses [semantic versioning](http://semver.org/). For the versions 273 | available, see the [tags on this 274 | repository](https://github.com/jaredsampson/pymolprobity/tags). 275 | 276 | 277 | ## Authors 278 | 279 | ### Primary Author 280 | 281 | * **Jared Sampson** wrote this plugin as part of his 2014-2015 [Warren L. 282 | DeLano PyMOL Open Source Fellowship][posf], while working as a technician at 283 | [NYU Langone Medical Center][nyu] and then as a Ph.D. student at [Columbia 284 | University][cu]. 285 | 286 | [posf]: http://www.pymol.org/fellowship/ 287 | [nyu]: http://www.med.nyu.edu/ 288 | [cu]: http://www.columbia.edu/ 289 | 290 | ### Contributors 291 | 292 | * **Thomas Holder** - "quad" dots visualization style and a number of other 293 | improvements and bugfixes. 294 | 295 | 296 | ## Citing PyMOLProbity 297 | 298 | Please cite the PyMOLProbity plugin with the following information: 299 | 300 | Sampson, Jared M. PyMOLProbity: a MolProbity-style visualization plugin for PyMOL, . (). 301 | 302 | where: 303 | 304 | * `` is the version number for the [release][releases] you used (e.g. v0.1.4); 305 | * `` is the date that release version was made available; and 306 | * `` is either: 307 | * the address of this repository on GitHub (https://github.com/jaredsampson/pymolprobity); or 308 | * the DOI associated with the release, available on the [releases][] page, or via this Zenodo [record][], which will always resolve to the latest release and display a list of available releases and DOIs. 309 | 310 | [releases]: https://github.com/jaredsampson/pymolprobity/releases 311 | [record]: https://zenodo.org/record/3366407 312 | 313 | For example, a BibTeX-formatted citation might look like this: 314 | 315 | ``` 316 | @software{pymolprobity, 317 | author = {Sampson, Jared M.}, 318 | title = {PyMOLProbity: a MolProbity-style visualization plugin for PyMOL}, 319 | url = {https://github.com/jaredsampson/pymolprobity}, 320 | version = {0.1.4}, 321 | date = {2019-08-12}, 322 | } 323 | 324 | ``` 325 | 326 | Remember also that PyMOLProbity uses the Reduce, Probe, Flipkin and Prekin 327 | programs from the Richardson group's MolProbity package, so please cite those 328 | as well. Some relevant information may be found [here][mpabout]. 329 | 330 | [mpabout]: http://molprobity.biochem.duke.edu/help/about.html 331 | 332 | 333 | ## License 334 | 335 | This project is licensed under the MIT License - see [LICENSE.md](LICENSE.md) 336 | for details 337 | 338 | ## Acknowledgments 339 | 340 | The PyMOLProbity plugin was initially developed as part of my 2014-15 [Warren 341 | L. DeLano Memorial PyMOL Open Source Fellowship][posf]. I would like to thank 342 | [Schrodinger][] for sponsoring my POSF fellowship and opportunity to work on 343 | this software. Specifically, I am grateful to the PyMOL developers for their 344 | help and patience. 345 | 346 | [Schrodinger]: http://www.schrodinger.com/ 347 | 348 | Additional thanks go to David and Jane Richardson and members of the 349 | [Richardson Lab][] for providing the MolProbity software and server and the 350 | programs on which this plugin depends, and to Bradley Hintze and Christopher 351 | Williams in particular for their help in answering questions I had about the 352 | programs. 353 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: mp 2 | dependencies: 3 | - coverage=4.2=py27_0 4 | - funcsigs=1.0.2=py27_0 5 | - mock=2.0.0=py27_0 6 | - nose=1.3.7=py27_1 7 | - openssl=1.0.2h=2 8 | - pbr=1.10.0=py27_0 9 | - pip=8.1.2=py27_0 10 | - python=2.7.12=1 11 | - readline=6.2=2 12 | - setuptools=25.1.6=py27_0 13 | - six=1.10.0=py27_0 14 | - sqlite=3.13.0=0 15 | - tk=8.5.18=0 16 | - wheel=0.29.0=py27_0 17 | - zlib=1.2.8=3 18 | -------------------------------------------------------------------------------- /pymolprobity/__init__.py: -------------------------------------------------------------------------------- 1 | '''pymolprobity: A MolProbity plugin for PyMOL 2 | 3 | REQUIREMENTS 4 | 5 | The PyMOLProbity Plugin depends on programs from the Richardson lab at Duke 6 | University, which must be installed locally. These programs, Reduce, 7 | Prekin, Flipkin, and Probe, are available for download from the Richardson 8 | lab website: 9 | 10 | http://kinemage.biochem.duke.edu/software/ 11 | 12 | They are available as precompiled binaries for Linux and MacOS (or, in the 13 | case of Flipkin, a Perl script), and Reduce and Probe are also included 14 | with Phenix as phenix.reduce and phenix.probe, so if you have an existing 15 | Phenix installation, you don't need to install them again. 16 | 17 | The easiest way to set the programs up to run is probably to place the 18 | binaries (or symlinks to them) in a directory on the shell PATH, e.g. in 19 | `/usr/local/bin`. Otherwise, save them wherever you like and update the 20 | appropriate plugin setting from the PyMOL command line: 21 | 22 | mpset reduce_path, /path/to/reduce 23 | mp_save_settings 24 | 25 | You may also wish to download the Richardson group's "slightly modified 26 | version of the connectivity table published by the PDB" from the Reduce 27 | software page above. This file should be placed in /usr/local. (If you 28 | don't, you'll probably get the following error when you run Reduce. 29 | 30 | ERROR CTab(/usr/local/reduce_wwPDB_het_dict.txt): could not open 31 | 32 | 33 | 34 | 35 | 36 | ''' 37 | 38 | from __future__ import absolute_import 39 | 40 | __author__ = 'Jared Sampson' 41 | __version__ = '0.1.5' 42 | 43 | 44 | import logging 45 | 46 | from . import gui 47 | 48 | 49 | logging.basicConfig(level=logging.INFO) 50 | 51 | def __init__(self): 52 | self.menuBar.addmenuitem('Plugin', 'command', 'PyMOLProbity', 53 | label = 'PyMOLProbity', 54 | command = lambda s=self: gui.PyMOLProbity(s)) 55 | 56 | -------------------------------------------------------------------------------- /pymolprobity/colors.py: -------------------------------------------------------------------------------- 1 | '''Color functions for PyMOLProbity plugin.''' 2 | 3 | from pymol import cmd 4 | 5 | 6 | def get_pymol_color(color): 7 | """Return the PyMOL color corresponding to a Kinemage color name.""" 8 | 9 | color_list = { 10 | # Color names defined in the KiNG source that aren't included in a 11 | # standard PyMOL installation are listed here with the names of 12 | # (approximately) equivalent PyMOL named colors. 13 | 14 | #"king_color": "pymol_color", 15 | "sea": "teal", 16 | "sky": "skyblue", 17 | "peach": "yelloworange", 18 | "lilac": "arsenic", 19 | "pinktint": "lightpink", 20 | "peachtint": "lightorange", 21 | "yellowtint": "paleyellow", 22 | "greentint": "palegreen", 23 | "bluetint": "lightblue", 24 | "lilactint": "lithium", 25 | "deadwhite": "white", 26 | "deadblack": "black", 27 | } 28 | 29 | try: 30 | return color_list[color] 31 | except: 32 | return color 33 | 34 | 35 | def get_color_rgb(color): 36 | """Given a color name, returns the RGB values.""" 37 | index = cmd.get_color_index(color) 38 | rgb = cmd.get_color_tuple(index) 39 | return rgb # e.g. (1.0, 1.0, 1.0) 40 | 41 | -------------------------------------------------------------------------------- /pymolprobity/flips.py: -------------------------------------------------------------------------------- 1 | '''Flips for PyMOLProbity plugin.''' 2 | 3 | from __future__ import print_function 4 | 5 | import logging 6 | import re 7 | 8 | # from pymol import cmd 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class Flip(object): 14 | """Store information related to a bond rotation optimized by Reduce.""" 15 | def macro(self): 16 | '''Return an atom selection macro for the Flip.''' 17 | c = self.chain or '' 18 | i = self.resi or '' 19 | n = self.name or '' 20 | r = self.resn or '' 21 | sep = '`' if (i and r) else '' 22 | macro = '{}/{}{}{}/{}'.format(c, r, sep, i, n) 23 | return macro 24 | 25 | def __init__(self): 26 | self.flip_class = None 27 | self.set = None 28 | self.set_index = None 29 | 30 | self.chain = None 31 | self.resi = None 32 | self.resn = None 33 | self.name = None 34 | self.alt = None 35 | 36 | self.flip_type = None 37 | self.descr = None 38 | self.best_score = None 39 | self.best_has_bad_bump = None 40 | 41 | # for "methyl" type (H added to freely rotatable bond) 42 | self.init_score = None 43 | self.init_has_bad_bump = None 44 | 45 | # for "canflip" type (e.g. NQH flips) 46 | self.code = None 47 | self.orig_score = None 48 | self.orig_has_bad_bump = None 49 | self.flip_score = None 50 | self.flip_has_bad_bump = None 51 | 52 | # Track user choices for making/reverting flips 53 | self.reduce_flipped = None 54 | self.user_flipped = None 55 | 56 | def __str__(self): 57 | return 'Flip: {}'.format(self.macro()) 58 | 59 | 60 | # Based on: http://stackoverflow.com/a/4914089/501277 61 | def slices(input_str, col_lengths): 62 | '''Split a string into columns of the lengths in the col_lengths list.''' 63 | position = 0 64 | for length in col_lengths: 65 | yield input_str[position:position + length] 66 | position += length 67 | 68 | 69 | def parse_func_group(func_group): 70 | '''Parse functional group info from second section of a flip. 71 | 72 | Returns 5 strings: chain, resi, resn, name, alt 73 | 74 | Functional group strings are written by Reduce via the source code: 75 | ``` 76 | ::sprintf(descrbuf, "%-2.2s%4s%c%-3.3s%-4.4s%c", 77 | hr.chain(), hr.Hy36resno(), hr.insCode(), 78 | hr.resname(), c1.atomname(), hr.alt()); 79 | std::string descr = descrbuf; 80 | ``` 81 | 82 | and generally look something like, e.g. " A 254 THR OG1 ". 83 | 84 | ''' 85 | # Set columns based on above source code excerpt, with residue number and 86 | # insertion code merged into the 2nd column 87 | col_lengths = [2, 5, 3, 4, 1] 88 | 89 | # Default is 15-character string. 90 | if len(func_group) != 15: 91 | # Allow for different chain ID lengths (which can apparently happen, 92 | # according to the `flipkin` script). 93 | if len(func_group) == 14: 94 | # 1-character chain ID field 95 | col_lengths[0] = 1 96 | elif len(func_group) == 17: 97 | # 4-character chain ID field 98 | col_lengths[0] = 4 99 | else: 100 | # Non-standard functional group string format 101 | msg = 'Non-standard length ({}) of functional group string: `{}`' 102 | raise ValueError(msg.format(len(func_group), func_group)) 103 | 104 | # Slice it up & strip whitespace 105 | return [col.strip() for col in list(slices(func_group, col_lengths))] 106 | 107 | 108 | def parse_methyl_score(groups): 109 | '''Parse the score section of a methyl flip match.''' 110 | best_score = float(groups[0].strip()) 111 | best_has_bad_bump = (groups[1] == '!') 112 | init_score = float(groups[2]) 113 | init_has_bad_bump = (groups[3] == '!') 114 | return best_score, best_has_bad_bump, init_score, init_has_bad_bump 115 | 116 | 117 | def parse_canflip_score(groups): 118 | '''Parse the score section of a canflip flip match.''' 119 | best_score = float(groups[0]) 120 | best_has_bad_bump = (groups[1] == '!') 121 | code = groups[2] 122 | orig_score = float(groups[3]) 123 | orig_has_bad_bump = (groups[4] == '!') 124 | flip_score = float(groups[5]) 125 | flip_has_bad_bump = (groups[6] == '!') 126 | return (best_score, best_has_bad_bump, code, orig_score, 127 | orig_has_bad_bump, flip_score, flip_has_bad_bump) 128 | 129 | 130 | def parse_other_score(groups): 131 | '''Parse the score section of an other flip match.''' 132 | best_score = float(groups[0]) 133 | best_has_bad_bump = (groups[1] == '!') 134 | return best_score, best_has_bad_bump 135 | 136 | 137 | def parse_flips(user_mod): 138 | '''Parse an array of USER MOD lines and return a list of Flips.''' 139 | ret = [] 140 | set_re = re.compile(r"Set\s?([0-9]*)\.([0-9])") 141 | for line in user_mod: 142 | if line.startswith('Single') or line.startswith('Set'): 143 | try: 144 | flip_class, func_group, descr, score = line.split(':') 145 | except ValueError: 146 | logger.error("Malformed USER MOD line: `{}`".format(line)) 147 | raise 148 | else: # pragma: nocover 149 | continue 150 | 151 | # new flip 152 | f = Flip() 153 | 154 | # Determine flip class 155 | if flip_class.startswith('Single'): 156 | f.flip_class = 'single' 157 | else: 158 | set_match = set_re.match(flip_class) 159 | if set_match: 160 | f.flip_class = 'set' 161 | g = set_match.groups() 162 | f.set = g[0] 163 | f.set_index = g[1] 164 | else: 165 | msg = 'Malformed Set flip: {}'.format(flip_class) 166 | raise ValueError(msg) 167 | 168 | # Parse functional group (i.e. residue/atom info) 169 | f.chain, f.resi, f.resn, f.name, f.alt = parse_func_group(func_group) 170 | 171 | # Store orientation description as-is 172 | f.descr = descr 173 | 174 | # Determine flip type (canflip, methyl, other) from score section 175 | methyl_score = re.compile( 176 | r"sc=(.{8})" # best score 177 | r"(!| ) " # 2 spaces # bad clash indicator for best score 178 | r"\(180deg=([\-\.0-9]*)" # initial score 179 | r"(!?)\)" # bad clash indicator (may be empty) 180 | ) 181 | canflip_score = re.compile( 182 | r"sc=(.{8})" # best score 183 | r"(!| ) " # 1 space # bad clash indicator for best score 184 | r"(.)" # class code (Reduce's recommendation) 185 | r"\(o=([\-\.0-9]*)" # original max score 186 | r"(!?)," # bad clash indicator for original 187 | r"f=([\-\.0-9]*)" # flipped max score 188 | r"(!?)\)" # bad clash indicator for flipped 189 | ) 190 | other_score = re.compile( 191 | # Note: This matches the preceding two types of flip records, 192 | # and therefore must be checked last. 193 | r"sc=(.{8})" # best score 194 | r"(!?)" # optional bad clash indicator 195 | ) 196 | 197 | # Match to regex, then process the score section. 198 | # Methyl 199 | m = methyl_score.match(score) 200 | if m: 201 | f.flip_type = 'methyl' 202 | (f.best_score, f.best_score_has_bad_bump, f.init_score, 203 | f.init_has_bad_bump) = parse_methyl_score(m.groups()) 204 | else: 205 | # Canflip 206 | m = canflip_score.match(score) 207 | if m: 208 | f.flip_type = 'canflip' 209 | (f.best_score, f.best_score_has_bad_bump, 210 | f.code, f.orig_score, f.orig_has_bad_bump, f.flip_score, 211 | f.flip_has_bad_bump) = parse_canflip_score(m.groups()) 212 | f.reduce_flipped = 1 if (f.code == 'F') else 0 213 | else: 214 | # Other 215 | m = other_score.match(score) 216 | if m: 217 | f.flip_type = 'other' 218 | (f.best_score, 219 | f.best_score_has_bad_bump) = parse_other_score(m.groups()) 220 | else: 221 | raise ValueError("UNABLE TO PARSE FLIP!\n" 222 | "raw flip record:\n" 223 | ">>{}<<".format(user_mod)) 224 | ret.append(f) 225 | 226 | return ret 227 | -------------------------------------------------------------------------------- /pymolprobity/gui.py: -------------------------------------------------------------------------------- 1 | '''GUI for PyMOLProbity plugin.''' 2 | 3 | from __future__ import absolute_import 4 | 5 | import logging 6 | import Pmw 7 | import sys 8 | 9 | if sys.version_info[0] < 3: 10 | import Tkinter as tk 11 | import tkMessageBox 12 | else: 13 | import tkinter as tk 14 | import tkinter.messagebox as tkMessageBox 15 | 16 | from pymol import cmd 17 | 18 | from . import main 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | def popup_on_exception(func): 24 | '''Decorator for GUI error handling''' 25 | def wrapper(*args, **kwargs): 26 | try: 27 | return func(*args, **kwargs) 28 | except Exception as e: 29 | tkMessageBox.showerror('Error', str(e)) 30 | 31 | return wrapper 32 | 33 | 34 | class PyMOLProbity: 35 | '''The main PyMOLProbity plugin dialog.''' 36 | def __init__(self, app): 37 | 38 | def get_object_list(): 39 | all_obj = cmd.get_names_of_type('object:molecule') 40 | # filter to exclude plugin-generated objects 41 | ret = [o for o in all_obj if not o.startswith('mp_')] 42 | logger.debug('available objects are: {}'.format(ret)) 43 | return ret 44 | 45 | def update_combo(): 46 | input_combo.setlist(get_object_list()) 47 | input_combo.selectitem(mpobj.get()) 48 | 49 | def refresh_page(page): 50 | try: 51 | logger.debug('Refreshing page {}.'.format(page)) 52 | # Refresh the combo box in case something has changed 53 | update_combo(input_combo) 54 | nb.setnaturalsize() 55 | except Exception as e: 56 | logger.debug('Failed to refresh page {}.'.format(page)) 57 | 58 | def set_mpobj(val): 59 | mpobj.set(val) 60 | cmd.zoom(val, 3) 61 | logger.debug('mpobj set to {}'.format(val)) 62 | # Get active flipkin 63 | flipkin = self.flipkin_radio.getvalue() 64 | o = main.get_object(val) 65 | if o.kin[flipkin] is not None: 66 | logger.debug('populating flips group...') 67 | populate_flips_grp(flipkin) 68 | else: 69 | 70 | msg = "No {} for object {}...clearing flips.".format( 71 | flipkin, val) 72 | logger.debug(msg) 73 | clear_child_widgets(flips_grp.interior()) 74 | 75 | @popup_on_exception 76 | def reduce_mpobj(): 77 | main.reduce_object(mpobj.get()) 78 | 79 | @popup_on_exception 80 | def flipkin_mpobj(): 81 | main.flipkin_object(mpobj.get()) 82 | frame = flipkin_ctl.interior() 83 | self.flipkin_radio = gen_flipkin_radioselect(frame) 84 | self.flipkin_group_chk = gen_flipkin_group_checkbuttons(frame) 85 | self.flipkin_radio.invoke('flipkinNQ') # always start with NQ 86 | animate_mpobj() 87 | 88 | def select_flipkin(flipkin): 89 | o = main.get_object(mpobj.get()) 90 | o.solo_kin(flipkin) 91 | 92 | # Set the proper PDB based on the active group 93 | try: 94 | val = self.flipkin_group_chk.getvalue() 95 | except: 96 | # first time running, default to NQ 97 | val = 'flipkinNQ' 98 | logger.debug('self.flipkin_group_chk = {} during select_flipkin'.format(val)) 99 | if 'flip' in val: 100 | o.solo_pdb(flipkin) 101 | if 'reduce' in val: 102 | o.enable_pdb('reduce') 103 | else: 104 | o.solo_pdb('reduce') 105 | 106 | # Set up the view controls 107 | frame = flipkin_ctl.interior() 108 | self.flipkin_subgroups_chk = gen_flipkin_subgroup_checkbuttons(frame) 109 | self.flipkin_masters_chk = gen_flipkin_master_checkbuttons(frame) 110 | # self.flipkin_pointmasters_chk = gen_flipkin_pointmaster_checkbuttons(frame) 111 | 112 | # Show and populate the flips group for this flipkin 113 | populate_flips_grp(flipkin) 114 | nb.setnaturalsize() 115 | 116 | def enable_flipkin_group(group, enable=True): 117 | o = main.get_object(mpobj.get()) 118 | 119 | # use full names (e.g. 'flipkinNQ') for PDBs 120 | if group.startswith('flip'): 121 | pdb = self.flipkin_radio.getvalue() 122 | suffix = pdb.replace('flipkin','') # yields 'NQ' or 'H' 123 | assert suffix in ['NQ', 'H'] 124 | kin_grp = '{}{}'.format(group, suffix) 125 | else: 126 | # unless something is wrong with the flipkin, group is 'reduce' 127 | pdb = group 128 | kin_grp = group 129 | 130 | # enable (or disable if enable=False) 131 | if enable: 132 | logger.debug('enabling {} and {}'.format(kin_grp, pdb)) 133 | o.enable_flipkin_group(kin_grp) 134 | o.enable_pdb(pdb) 135 | else: 136 | logger.debug('disabling {} and {}'.format(kin_grp, pdb)) 137 | o.disable_flipkin_group(kin_grp) 138 | o.disable_pdb(pdb) 139 | 140 | def animate_mpobj(): 141 | o = main.get_object(mpobj.get()) 142 | # Ensure only one is checked 143 | val = self.flipkin_group_chk.getvalue() 144 | if len(val) == 1: 145 | # Toggle both buttons 146 | self.flipkin_group_chk.invoke('reduce') 147 | self.flipkin_group_chk.invoke('flip') 148 | elif len(val) in [0, 2]: 149 | # If they're both checked or unchecked, only toggle one 150 | self.flipkin_group_chk.invoke('reduce') 151 | 152 | def toggle_subgroup(subgroup, enable): 153 | SEL = { 154 | '1->2': 'mp_*.1to2.*', 155 | '2->1': 'mp_*.2to1.*', 156 | } 157 | if enable: 158 | cmd.enable(SEL[subgroup]) 159 | else: 160 | cmd.disable(SEL[subgroup]) 161 | 162 | def toggle_master(master, enable): 163 | SEL = { 164 | 'vdw contact': 'mp_*vdw_contact', 165 | 'small overlap': 'mp_*small_overlap', 166 | 'bad overlap': 'mp_*bad_overlap', 167 | 'H-bonds': 'mp_*H_bonds', 168 | } 169 | if enable: 170 | cmd.enable(SEL[master]) 171 | # Keep buttons on Flipkin and Probe tabs in sync. 172 | try: 173 | self.flipkin_masters_chk.button(master).select() 174 | self.probe_masters_chk.button(master).select() 175 | except: 176 | # don't choke if the button doesn't exist 177 | pass 178 | 179 | else: 180 | cmd.disable(SEL[master]) 181 | try: 182 | self.flipkin_masters_chk.button(master).deselect() 183 | self.probe_masters_chk.button(master).deselect() 184 | except: 185 | pass 186 | 187 | def toggle_pointmaster(pointmaster, enable): 188 | pass 189 | 190 | def zoom_and_refresh(sel): 191 | cmd.orient(sel) 192 | cmd.zoom(sel, 3) 193 | cmd.refresh() 194 | 195 | def clear_child_widgets(parent): 196 | # TODO may be better to keep the widgets and use grid_forget() 197 | for widget in parent.winfo_children(): 198 | widget.destroy() 199 | logger.debug('cleared contents of {}'.format(parent)) 200 | 201 | def populate_flips_grp(flipkin): 202 | parent = flips_grp.interior() 203 | # Clear any existing child widgets 204 | clear_child_widgets(parent) 205 | # Recreate the scrolling frame to house the flip rows 206 | frame = gen_scrolling_frame(parent) 207 | 208 | # Column headers 209 | gen_flips_list_header(frame) 210 | 211 | # Repopulate with flips 212 | o = main.get_object(mpobj.get()) 213 | if not o.views[flipkin]: 214 | kin = o.kin[flipkin] 215 | o.views[flipkin] = kin.viewids() 216 | logger.debug('begin populating flip rows') 217 | for i, v in enumerate(o.views[flipkin]): 218 | gen_flips_list_row(frame, o, flipkin, i+1, v) 219 | logger.debug('finished populating flip rows') 220 | nb.setnaturalsize() 221 | 222 | @popup_on_exception 223 | def save_flip_selections(): 224 | '''Save the user-selected flips to a new PDB object.''' 225 | o = main.get_object(mpobj.get()) 226 | flipkin = self.flipkin_radio.getvalue() 227 | reduce_obj = o.pdb['reduce'] 228 | flipkin_obj = o.pdb[flipkin] 229 | userflips_obj = o.pdb['userflips'] 230 | # Create a new userflips object even if it already exists in case 231 | # previous selections have been changed. 232 | v = cmd.get_view() 233 | cmd.create(userflips_obj, reduce_obj) 234 | cmd.set_view(v) 235 | 236 | for i, v in enumerate(o.views[flipkin]): 237 | # If reduce value and user value are different, copy the 238 | # coordinates from the current flipkin molecule 239 | if v['reduce_chk_val'].get() != v['user_chk_val'].get(): 240 | # Do it the hard way, by combining objects. This is plenty 241 | # fast (we typically won't have too many flips to switch) 242 | # and doesn't result in atom name mismatch errors for 243 | # differently protonated HIS residues the way the 244 | # load_coords method does. 245 | flipped_sel = '({} and chain {} and resi {})'.format( 246 | flipkin_obj, v['chain'], v['resi']) 247 | userflips_sel = '({} and not (chain {} and resi {}))'.format( 248 | userflips_obj, v['chain'], v['resi']) 249 | combined_sel = '{} or {}'.format( 250 | userflips_sel, flipped_sel) 251 | v = cmd.get_view() 252 | cmd.create(userflips_obj, combined_sel) 253 | cmd.set_view(v) 254 | msg = 'added flip for {} to {}'.format(flipped_sel, 255 | userflips_obj) 256 | logger.debug(msg) 257 | o.solo_pdb('userflips') 258 | 259 | @popup_on_exception 260 | def probe_mpobj(): 261 | main.probe_object(mpobj.get()) 262 | frame = probe_ctl.interior() 263 | # Set up the view controls 264 | # self.probe_group_chk = gen_flipkin_group_checkbuttons(frame) 265 | # self.probe_subgroups_chk = gen_probe_subgroup_checkbuttons(frame) 266 | self.probe_masters_chk = gen_probe_master_checkbuttons(frame) 267 | # self.probe_pointmasters_chk = gen_probe_pointmaster_checkbuttons(frame) 268 | nb.setnaturalsize() 269 | 270 | 271 | 272 | ############################################################################### 273 | # 274 | # Widget Generators 275 | # 276 | ############################################################################### 277 | 278 | def gen_input_obj_combo(parent): 279 | '''Show the available loaded PyMOL objects in a ComboBox.''' 280 | obj_list = get_object_list() 281 | 282 | # Stop if there are no available objects. 283 | if len(obj_list) == 0: 284 | txt = 'No objects loaded! Please load a structure and restart the plugin.' 285 | w = tk.Label(parent, text=txt, padx=20, pady=20) 286 | w.pack() 287 | return None 288 | 289 | # Create a frame container 290 | frame = tk.Frame(parent) 291 | frame.pack(fill='x') 292 | 293 | # Create and populate the combo box 294 | combo = Pmw.ComboBox(frame, 295 | label_text = 'Base object name:', 296 | labelpos = 'w', 297 | selectioncommand = set_mpobj, 298 | scrolledlist_items = obj_list, 299 | ) 300 | combo.grid(row=0, sticky='ew') 301 | 302 | # Create a refresh button 303 | btn = tk.Button(frame, text='Refresh List', command=update_combo) 304 | btn.grid(row=0, column=1) 305 | 306 | # Select first object by default if not previously set 307 | if not mpobj.get(): 308 | combo.selectitem(obj_list[0]) 309 | mpobj.set(obj_list[0]) 310 | return combo 311 | 312 | def gen_flipkin_radioselect(parent): 313 | # destroy the previous one if it exists 314 | try: 315 | self.flipkin_radio.destroy() 316 | except AttributeError: 317 | pass 318 | 319 | # Radio buttons to select a flipkin. 320 | select = Pmw.RadioSelect(parent, 321 | labelpos='w', 322 | label_text='Select a flipkin:', 323 | buttontype='radiobutton', 324 | command=select_flipkin) 325 | select.add('flipkinNQ') 326 | select.add('flipkinH') 327 | select.grid(sticky='ew') 328 | return select 329 | 330 | def gen_flipkin_group_checkbuttons(parent): 331 | # destroy the previous version, if it exists 332 | try: 333 | self.flipkin_group_chk_frame.destroy() 334 | except AttributeError: 335 | pass 336 | frame = tk.Frame(parent) 337 | frame.grid(sticky='ew') 338 | self.flipkin_group_chk_frame = frame 339 | 340 | # Checkbuttons to choose flipkin group(s). 341 | select = Pmw.RadioSelect(frame, 342 | labelpos='w', 343 | label_text='Select flipkin group(s):', 344 | buttontype='checkbutton', 345 | command=enable_flipkin_group) 346 | select.add('reduce') 347 | select.add('flip') 348 | select.grid(row=0, column=1) 349 | # Animate button 350 | btn = tk.Button(frame, text='Animate', command=animate_mpobj) 351 | btn.grid(row=0, column=2) 352 | # Return only the RadioSelect widget for access to values & names 353 | return select 354 | 355 | def gen_flipkin_subgroup_checkbuttons(parent): 356 | # destroy the previous version 357 | try: 358 | self.flipkin_subgroups_chk.destroy() 359 | except AttributeError: 360 | pass 361 | 362 | chk = Pmw.RadioSelect(parent, 363 | buttontype='checkbutton', 364 | labelpos='w', 365 | label_text='Subgroups:', 366 | command=toggle_subgroup) 367 | chk.grid(sticky='ew') 368 | for btn in ['1->2', '2->1']: 369 | chk.add(btn) 370 | chk.invoke(btn) 371 | # o = main.get_object(mpobj.get()) 372 | # kinNQ = o.kin['flipkinNQ'] 373 | # kinNQ_sg = [x[1] for x in kinNQ.kin_subgroups()] 374 | # kinH = o.kin['flipkinH'] 375 | # kinH_sg = [x[1] for x in kinH.kin_subgroups()] 376 | 377 | # Get sorted list union of all subgroups (should both be the same) 378 | # subgroups = sorted(set().union(kinNQ_sg, kinH_sg)) 379 | # for sg in subgroups: 380 | # chk.add(sg) 381 | return chk 382 | 383 | def gen_flipkin_master_checkbuttons(parent): 384 | # destroy the previous version if it exists 385 | try: 386 | self.flipkin_masters_chk.destroy() 387 | except AttributeError: 388 | pass 389 | 390 | chk = Pmw.RadioSelect(parent, 391 | buttontype='checkbutton', 392 | labelpos='w', 393 | label_text='Masters:', 394 | command=toggle_master) 395 | chk.grid(sticky='w') 396 | 397 | # Bonds 398 | # bond_masters = ['mainchain', 'sidechain', 'hydrogens', 'hets', 399 | # 'waters'] 400 | 401 | # Contacts 402 | contact_masters = ['vdw contact', 'small overlap', 403 | 'bad overlap', 'H-bonds'] 404 | for btn in contact_masters: 405 | chk.add(btn) 406 | chk.invoke(btn) 407 | 408 | return chk 409 | 410 | # def gen_flipkin_pointmaster_checkbuttons(parent): 411 | # # destroy the previous version if it exists 412 | # try: 413 | # self.flipkin_pointmasters_chk.destroy() 414 | # except AttributeError: 415 | # pass 416 | 417 | # chk = Pmw.RadioSelect(parent, 418 | # buttontype='checkbutton', 419 | # labelpos='w', 420 | # label_text='Pointmasters:', 421 | # command=toggle_pointmaster) 422 | # chk.grid(sticky='ew') 423 | # o = main.get_object(mpobj.get()) 424 | # kinNQ = o.kin['flipkinNQ'] 425 | # kinNQ_sg = [x[1] for x in kinNQ.pointmasters()] 426 | # kinH = o.kin['flipkinH'] 427 | # kinH_sg = [x[1] for x in kinH.pointmasters()] 428 | 429 | # # Get sorted list union of all subgroups (should both be the same) 430 | # masters = list(set().union(kinNQ_sg, kinH_sg)) 431 | # for m in masters: 432 | # chk.add(m) 433 | # return chk 434 | 435 | # Set up scrollbar, canvas and data frame 436 | # Based on http://stackoverflow.com/a/3092341 437 | def gen_scrolling_frame(parent, **canvas_options): 438 | '''Return a frame + scrollbar packed within a Tkinter Canvas.''' 439 | def onFrameConfigure(canvas): 440 | '''Reset the scroll region to encompass the inner frame.''' 441 | canvas.configure(scrollregion=canvas.bbox('all')) 442 | canvas = tk.Canvas(parent, **canvas_options) 443 | frame = tk.Frame(canvas) 444 | vsb = tk.Scrollbar(parent, orient='vertical', command=canvas.yview) 445 | canvas.configure(yscrollcommand=vsb.set) 446 | vsb.pack(side='right', fill='y') 447 | canvas.pack(side='left', fill='both', expand=1) 448 | canvas.create_window((0,0), window=frame, anchor='nw') 449 | frame.bind('', 450 | lambda event, canvas=canvas: onFrameConfigure(canvas)) 451 | return frame 452 | 453 | def gen_flips_list_header(frame): 454 | # Formatting for all labels 455 | labels = ['Flipped Residue', 'Zoom', 'Flipped by Reduce', 456 | 'User Selection'] # TODO add 'Current'? 457 | for i, label in enumerate(labels): 458 | w = tk.Label(frame, text=label, 459 | relief='ridge', fg='#ffffff', bg='#555555') 460 | w.grid(row=0, column=i, 461 | sticky='nsew', ipadx=4, ipady=4) 462 | 463 | def gen_flips_list_row(frame, o, flipkin, i, v): 464 | '''Generate a row in the flips list frame for the given view. 465 | 466 | PARAMETERS 467 | 468 | frame The target frame where the widgets should be drawn. 469 | 470 | o The MPObject instance to which the viewid belongs. 471 | 472 | i The row index where the widgets should be gridded. 473 | 474 | v The viewid dictionary. 475 | 476 | ''' 477 | # TODO make this conversion when processing flipkin 478 | RESN = {'N': 'ASN', 'Q': 'GLN', 'H': 'HIS'} 479 | resn = RESN[v['resn']] 480 | macro = '{}/{}`{}'.format(v['chain'], resn, v['resi']) 481 | if v['flipped']: 482 | label_text = '{}*'.format(macro) 483 | else: 484 | label_text = macro 485 | 486 | # Flip macro label 487 | label = tk.Label(frame, text=label_text) 488 | label.grid(row=i, column=0) 489 | logger.debug('made label', label_text) 490 | 491 | # Zoom button 492 | zoom_obj = o.pdb[flipkin] 493 | logger.debug('got zoom_obj', zoom_obj) 494 | zoom_sel = '/{}//{} and sidechain'.format(zoom_obj, macro) 495 | logger.debug('got zoom_sel', zoom_sel) 496 | zoom_cmd = (lambda s=zoom_sel: zoom_and_refresh(s)) 497 | logger.debug('got zoom_cmd', zoom_cmd) 498 | zoom_btn = tk.Button(frame, text='Zoom', command=zoom_cmd) 499 | zoom_btn.grid(row=i, column=1) 500 | 501 | # Flipped by Reduce checkbutton 502 | # Store value in a Tkinter IntVar (first time only) 503 | if 'reduce_chk_val' not in v.keys(): 504 | v['reduce_chk_val'] = tk.IntVar() 505 | v['reduce_chk_val'].set(v['flipped']) 506 | reduce_chk = tk.Checkbutton(frame, variable=v['reduce_chk_val'], 507 | state='disabled') 508 | reduce_chk.grid(row=i, column=2) 509 | 510 | # User-selected flip checkbutton 511 | # Create user flip selection storage variable, default to same 512 | # as 'reduce' value. 513 | if 'user_chk_val' not in v.keys(): 514 | v['user_chk_val'] = tk.IntVar() 515 | v['user_chk_val'].set(v['flipped']) 516 | user_chk = tk.Checkbutton(frame, variable=v['user_chk_val']) 517 | user_chk.grid(row=i, column=3) 518 | 519 | logger.debug('...added flip {}'.format(macro)) 520 | 521 | def gen_probe_master_checkbuttons(parent): 522 | # destroy the previous version if it exists 523 | try: 524 | self.probe_masters_chk.destroy() 525 | except AttributeError: 526 | pass 527 | 528 | chk = Pmw.RadioSelect(parent, 529 | buttontype='checkbutton', 530 | labelpos='w', 531 | label_text='Masters:', 532 | command=toggle_master) 533 | chk.grid(sticky='w') 534 | 535 | # Contacts 536 | contact_masters = ['vdw contact', 'small overlap', 537 | 'bad overlap', 'H-bonds'] 538 | for btn in contact_masters: 539 | chk.add(btn) 540 | chk.invoke(btn) 541 | 542 | return chk 543 | 544 | 545 | ####################################################################### 546 | # 547 | # Instance Variable 548 | # 549 | ####################################################################### 550 | 551 | root = app.root 552 | 553 | # The current MPObject name 554 | mpobj = tk.StringVar(root) 555 | 556 | 557 | ####################################################################### 558 | # 559 | # Main dialog & tabs 560 | # 561 | ####################################################################### 562 | 563 | # Main window 564 | dialog = Pmw.Dialog(root, title="PyMOLProbity", buttons=[]) 565 | 566 | # Add the mpobj selection combo box 567 | input_combo = gen_input_obj_combo(dialog.interior()) 568 | 569 | # If no objects are loaded, stop loading the GUI (works for now) 570 | if input_combo is None: 571 | return 572 | 573 | # Add notebook with tabs 574 | nb = Pmw.NoteBook(dialog.interior(), 575 | raisecommand=refresh_page) 576 | nb.pack(fill='both', expand=1) 577 | 578 | # Page names 579 | reduce_page_name = 'Add Hydrogens' 580 | flipkin_page_name = 'Review Flips' 581 | probe_page_name = 'Visualize Contacts' 582 | 583 | # Create pages 584 | reduce_page = nb.add(reduce_page_name) 585 | flipkin_page = nb.add(flipkin_page_name) 586 | probe_page = nb.add(probe_page_name) 587 | 588 | 589 | ####################################################################### 590 | # 591 | # Reduce Page 592 | # 593 | ####################################################################### 594 | 595 | reduce_button = tk.Button(reduce_page, text='Run Reduce', 596 | command=reduce_mpobj) 597 | reduce_button.pack(anchor='n', fill='x', expand=1) 598 | 599 | 600 | ####################################################################### 601 | # 602 | # Flipkin Page 603 | # 604 | ####################################################################### 605 | 606 | flipkin_button = tk.Button(flipkin_page, text='Run Flipkin', 607 | command=flipkin_mpobj) 608 | flipkin_button.pack(anchor='n', fill='x', expand=1) 609 | 610 | # Controls group 611 | flipkin_ctl = Pmw.Group(flipkin_page, tag_text='View Options') 612 | flipkin_ctl.pack(anchor='n', fill='x', expand=1) 613 | self.flipkin_radio = None 614 | self.flipkin_group_chk = None 615 | 616 | # Flips group 617 | flips_grp = Pmw.Group(flipkin_page, tag_text='Select Flips') 618 | flips_grp.pack(anchor='n', fill='x', expand=1) 619 | 620 | # Save flip selections 621 | save_flips_btn = tk.Button(flipkin_page, 622 | text='Save Selections', command=save_flip_selections) 623 | save_flips_btn.pack(anchor='n', fill='x', expand=1) 624 | 625 | 626 | ####################################################################### 627 | # 628 | # Probe Page 629 | # 630 | ####################################################################### 631 | 632 | probe_button = tk.Button(probe_page, text='Run Probe', 633 | command=probe_mpobj) 634 | probe_button.pack(anchor='n', fill='x', expand=1) 635 | 636 | # Controls group 637 | probe_ctl = Pmw.Group(probe_page, tag_text='View Options') 638 | probe_ctl.pack(anchor='n', fill='x', expand=1) 639 | self.probe_radio = None 640 | self.probe_group_chk = None 641 | -------------------------------------------------------------------------------- /pymolprobity/kinemage.py: -------------------------------------------------------------------------------- 1 | '''Kinemage handling for PyMOLProbity plugin.''' 2 | 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | import copy 7 | import logging 8 | import re 9 | 10 | from pymol import cmd 11 | 12 | from . import points 13 | from . import utils 14 | 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | 20 | ############################################################################### 21 | # 22 | # KINEMAGE 23 | # 24 | ############################################################################### 25 | 26 | class Kinemage(object): 27 | '''Store Kinemage data.''' 28 | 29 | def draw(self, kin_group, dot_mode=0): 30 | '''Draw the Kinemage dots and clashes.''' 31 | logger.debug('Drawing Kinemage {}...'.format(self)) 32 | view = cmd.get_view() 33 | 34 | # Set group_auto_mode for easier naming 35 | gam = cmd.get('group_auto_mode') 36 | cmd.set('group_auto_mode', 2) 37 | 38 | # Speed up CGO sphere rendering 39 | if dot_mode == 0: 40 | cmd.set('cgo_use_shader', 0) 41 | cmd.set('cgo_sphere_quality', 0) 42 | 43 | # Create dict to store cgolists to be extended 44 | cgolists = {} 45 | 46 | # Populate a Pointmaster code lookup dict 47 | pm_lookup = {} 48 | for pm in self.pointmasters(): 49 | if pm['code'] not in pm_lookup.keys(): 50 | pm_lookup[pm['code']] = pm['label'] 51 | 52 | # Get CGO from dotlists 53 | for dotlist in self.dotlists(): 54 | # Skip non-contact dotlists (if there is such a thing) 55 | if dotlist[0].dotlist_name != 'x': 56 | continue 57 | 58 | try: 59 | dg = dotlist[0].group[0] 60 | except TypeError: 61 | dg = 'no_group' # probe output typically doesn't have a group 62 | 63 | ds = dotlist[0].subgroup[1] 64 | dm = dotlist[0].master 65 | for dot in dotlist: 66 | dpm = pm_lookup[dot.pm] 67 | dcgo = dot.get_cgo(dot_mode) 68 | dname = '{}.{}.{}.{}'.format(dg, ds, dm, dpm) 69 | try: 70 | # Extend existing cgo list 71 | cgolists[dname].extend(dcgo) 72 | except KeyError: 73 | # Create new cgo list 74 | cgolists[dname] = dcgo 75 | 76 | # Get CGO from vectorlists 77 | # TODO combine this with the dotlist version into a separate function 78 | for vectorlist in self.vectorlists(): 79 | # Skip non-clash vectorlists (e.g. coordinates) 80 | if vectorlist[0].vectorlist_name != 'x': 81 | continue 82 | 83 | try: 84 | vg = vectorlist[0].group[0] 85 | except TypeError: 86 | vg = 'no_group' # probe output typically doesn't have a group 87 | 88 | vs = vectorlist[0].subgroup[1] 89 | vm = vectorlist[0].master 90 | for vector in vectorlist: 91 | vpm = pm_lookup[vector.pm[0]] # 2 stored, use first 92 | vcgo = vector.get_cgo() 93 | vname = '{}.{}.{}.{}'.format(vg, vs, vm, vpm) 94 | try: 95 | # Extend existing cgo list 96 | cgolists[vname].extend(vcgo) 97 | except KeyError: 98 | # Create new cgo list 99 | cgolists[vname] = vcgo 100 | 101 | # Create CGO objects 102 | for name, cgolist in cgolists.items(): 103 | objname = '{}.{}'.format(kin_group, name) 104 | logger.debug('loading cgo for object {}'.format(objname)) 105 | cmd.load_cgo(cgolist, objname) 106 | 107 | # Restore initial view. 108 | cmd.set_view(view) 109 | 110 | # Restore initial group_auto_mode setting 111 | cmd.set('group_auto_mode', gam) 112 | 113 | logger.debug('Finished drawing Kinemage.') 114 | 115 | def get_all_keywords_of_type(self, kw): 116 | l = [] 117 | for i, k in self.keywords.items(): 118 | if k['keyword'] == kw: 119 | l.append(k['data']) 120 | return l 121 | 122 | def get_unique_keywords_of_type(self, kw): 123 | l = [] 124 | for i, k in self.keywords.items(): 125 | if k['keyword'] == kw: 126 | if k['data'] not in l: 127 | l.append(k['data']) 128 | return l 129 | 130 | # TODO add filtering to these methods 131 | # e.g. kin.vectorlists(filter={'group': 'flipNQ'}) to get only those 132 | # vectorlists in group flipNQ. 133 | 134 | def viewids(self): 135 | return self.get_all_keywords_of_type('viewid') 136 | 137 | def groups(self): 138 | return self.get_all_keywords_of_type('group') 139 | 140 | def subgroups(self): 141 | return self.get_unique_keywords_of_type('subgroup') 142 | 143 | def kin_subgroups(self): 144 | subgroups = self.subgroups() 145 | return [sg for sg in subgroups if sg[0] == 'dominant'] 146 | 147 | def masters(self): 148 | return self.get_unique_keywords_of_type('master') 149 | 150 | def pointmasters(self): 151 | return self.get_unique_keywords_of_type('pointmaster') 152 | # l = [] 153 | # for i, k in self.keywords.items(): 154 | # if k['keyword'] == 'pointmaster': 155 | # pm = k['data']['label'] 156 | # if pm not in l: 157 | # l.append(pm) 158 | # return l 159 | 160 | def dotlists(self): 161 | return self.get_all_keywords_of_type('dotlist') 162 | 163 | def vectorlists(self): 164 | return self.get_all_keywords_of_type('vectorlist') 165 | 166 | def __init__(self): 167 | self.keywords = {} 168 | 169 | 170 | 171 | def single_line_keyword_check(lines): 172 | '''Call this for keywords that should only be a single line. 173 | 174 | If the list has more than one line, print a warning. If the input is not 175 | properly formatted, raise a ValueError. 176 | ''' 177 | if type(lines) is not list: 178 | msg = 'Expected a list of keyword lines but got: {}'.format(lines) 179 | raise ValueError(msg) 180 | if len(lines) > 1: 181 | msg = ' Only using the first line of multiline keyword: {}' 182 | kw = lines[0].split()[0] 183 | logger.warning(msg.format(kw)) 184 | 185 | 186 | VIEWID_RE = re.compile( 187 | r'''(\d*)viewid ''' # view number 188 | r'''{([ \*])''' # is flipped by Reduce? (* or space) 189 | r'''([\w])''' # single-letter AA resn 190 | r'''([ \w]{2,5})''' # 1-4 digit residue number plus insertion code 191 | r'''([ \w])''' # alt 192 | r'''([ \w])}''') # chain id 193 | def process_viewid(lines, context): 194 | single_line_keyword_check(lines) 195 | line = lines[0] 196 | m = VIEWID_RE.match(line) 197 | if m: 198 | return { 199 | 'view_num': (int(m.group(1)) if m.group(1) else 1), 200 | 'flipped': m.group(2) == '*', 201 | 'resn': m.group(3).strip(), 202 | 'resi': m.group(4).strip(), 203 | 'alt': m.group(5).strip(), 204 | 'chain': m.group(6).strip(), 205 | } 206 | else: 207 | logger.warning('Unrecognized viewid format: "{}"'.format(line)) 208 | return None 209 | 210 | 211 | MASTER_RE = re.compile(r'''master {([^}]*)}''') 212 | def process_master(lines, context): 213 | single_line_keyword_check(lines) 214 | line = lines[0] 215 | m = MASTER_RE.match(line) 216 | if m: 217 | return utils.slugify(m.group(1)) 218 | 219 | 220 | POINTMASTER_RE = re.compile( 221 | r'''pointmaster '(\w*)' ''' # pointmaster code 222 | r'''{([\w\s]*)}''' # pointmaster label 223 | r'''(?: (\w+))?''') # default state 224 | def process_pointmaster(lines, context): 225 | single_line_keyword_check(lines) 226 | line = lines[0] 227 | m = POINTMASTER_RE.match(line) 228 | if m: 229 | pm = { 230 | 'code': m.group(1), 231 | 'label': utils.slugify(m.group(2)), 232 | 'enable': 0 if m.group(3) == 'off' else 1, # default to "on" 233 | } 234 | return pm 235 | 236 | 237 | KINEMAGE_RE = re.compile(r'''kinemage ([\w\d]+)''') 238 | def process_kinemage_keyword(lines, context): 239 | single_line_keyword_check(lines) 240 | line = lines[0] 241 | m = KINEMAGE_RE.match(line) 242 | if m: 243 | return m.group(1) 244 | 245 | 246 | GROUP_RE = re.compile(r'''group {([^}]*)} (dominant|animate)''') 247 | def process_group(lines, context): 248 | single_line_keyword_check(lines) 249 | line = lines[0] 250 | m = GROUP_RE.match(line) 251 | if m: 252 | return [m.group(1), m.group(2)] 253 | 254 | 255 | SUBGROUP_RE = re.compile(r'''subgroup(?: (\w*))? {([^}]*)}(?: (\w+))?(?: (\w+))?''') 256 | def process_subgroup(lines, context): 257 | single_line_keyword_check(lines) 258 | line = lines[0] 259 | m = SUBGROUP_RE.match(line) 260 | if m: 261 | g2 = m.group(2).replace('->', 'to').replace(' ', '_') 262 | return [m.group(1), g2, m.group(3), m.group(4)] 263 | 264 | 265 | def process_kinemage(kinstr): 266 | '''Parse a Probe output string and return a Kinemage object.''' 267 | KEYWORD_HANDLERS = { 268 | # MASTERS, ASPECTS, AND COLORS 269 | 'master': process_master, 270 | 'pointmaster': process_pointmaster, 271 | 272 | # KINEMAGES, GROUPS AND SUBGROUPS 273 | 'kinemage': process_kinemage_keyword, 274 | 'group': process_group, 275 | 'subgroup': process_subgroup, 276 | 277 | # LISTS 278 | 'dotlist': points.process_dotlist, 279 | 'vectorlist': points.process_vectorlist, 280 | 281 | # VIEWS 282 | # may be preceded by a number, e.g. `@2viewid` 283 | 'viewid': process_viewid, 284 | } 285 | 286 | SKIPPED_KEYWORDS = [ 287 | # METADATA 288 | 'text', 'title', 'copyright', 'caption', 'mage', 'prekin', 289 | 'pdbfile', 'command', 'dimensions', 'dimminmax', 'dimscale', 290 | 'dimoffset', 291 | 292 | # DISPLAY OPTIONS 293 | 'whitebackground', 'onewidth', 'thinline', 'perspective', 'flat', 294 | 'listcolordominant', 'lens', 295 | 296 | # MASTERS, ASPECTS, AND COLORS 297 | 'colorset', 'hsvcolor', 'hsvcolour', 298 | 299 | # LISTS 300 | 'labellist', 'ringlist', 'balllist', 'spherelist', 'trianglelist', 301 | 'ribbonlist', 'marklist', 'arrowlist', 302 | 303 | # VIEWS 304 | # may be preceded by a number, e.g. `@2span` 305 | 'span', 'zslab', 'center', 306 | ] 307 | 308 | kin = Kinemage() 309 | 310 | commands = kinstr.lstrip('@').split('\n@') 311 | 312 | # Track context 313 | context = { 314 | 'kinemage': None, 315 | 'group': None, 316 | 'subgroup': None, 317 | 'animate': 0, 318 | } 319 | for i, command in enumerate(commands): 320 | lines = command.strip().split("\n") 321 | keyword = lines[0].split(" ")[0] # First word after "@" 322 | base_keyword = re.sub(r'\d', '', keyword) # remove any digits 323 | if base_keyword in KEYWORD_HANDLERS.keys(): 324 | 325 | # Process keyword lines with the function set in KEYWORD_HANDLERS 326 | logger.debug('Processing keyword {}: {} as {}...'.format(i, 327 | keyword, base_keyword)) 328 | data = KEYWORD_HANDLERS[base_keyword](lines, copy.copy(context)) 329 | kin.keywords[i] = { 330 | 'keyword': base_keyword, 331 | 'data': data 332 | } 333 | 334 | logger.debug(" Stored keyword {}: {}.".format(i, keyword)) 335 | 336 | # Manage context after kinemage, group, and subgroup keywords 337 | if base_keyword == 'kinemage': 338 | context['kinemage'] = data 339 | msg = 'entering kinemage #{kinemage}'.format(**context) 340 | logger.debug(msg) 341 | 342 | elif base_keyword == 'group': 343 | context['group'] = copy.deepcopy(data) 344 | try: 345 | if 'animate' in data: 346 | context['animate'] = 1 347 | else: 348 | context['animate'] = 0 349 | except TypeError: 350 | context['animate'] = 0 351 | 352 | msg = 'entering group: {group} (animate = {animate})' 353 | logger.debug(msg.format(**context)) 354 | 355 | elif base_keyword == 'subgroup': 356 | context['subgroup'] = copy.deepcopy(data) 357 | logger.debug('entering subgroup: {subgroup}'.format(**context)) 358 | 359 | 360 | elif base_keyword in SKIPPED_KEYWORDS: 361 | logger.debug('Skipping keyword {}: {}'.format(i, keyword)) 362 | 363 | else: 364 | logger.warning('Unknown keyword: {}'.format(keyword)) 365 | 366 | return kin 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | -------------------------------------------------------------------------------- /pymolprobity/main.py: -------------------------------------------------------------------------------- 1 | '''Main functions for PyMOLProbity plugin.''' 2 | 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | import logging 7 | import os 8 | import re 9 | import subprocess 10 | import tempfile 11 | 12 | from pymol import cmd #cgo, cmd, plugins 13 | from pymol import CmdException 14 | 15 | # import colors 16 | # from commands import command_line_output, save_to_tempfile 17 | from . import flips 18 | from . import kinemage 19 | from . import points 20 | # import settings 21 | # import utils 22 | 23 | 24 | # Set up logger 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | # # Load saved settings from ~/.pymolpluginsrc if available 29 | # settings.load_settings() 30 | 31 | 32 | 33 | ############################################################################### 34 | # 35 | # EXCEPTIONS 36 | # 37 | ############################################################################### 38 | 39 | class MPException(CmdException): 40 | pass 41 | 42 | 43 | ############################################################################### 44 | # 45 | # MPOBJECT 46 | # 47 | ############################################################################### 48 | 49 | class MPObject(object): 50 | '''Container for PyMOLProbity data associated with a loaded PyMOL object.''' 51 | 52 | def get_kin_cgo_group(self, kin): 53 | '''Return the name of the PyMOL group containing the kinemage CGO objects. 54 | 55 | PARAMETERS 56 | 57 | kin (str) Name of the kinemage. Possible values are 58 | 'flipkinNQ', 'flipkinH', or 'probe'. 59 | 60 | ''' 61 | if kin in self.kin.keys(): 62 | return '{}.{}'.format(self.mp_group, kin) 63 | else: 64 | msg = 'Unexpected kinemage name specified: {}'.format(kin) 65 | logger.warning(msg) 66 | return None 67 | 68 | def draw(self, kin, dot_mode=0): 69 | '''Draw kinemage CGO objects for this MPObject. 70 | 71 | PARAMETERS 72 | 73 | kin (str) The kinemage to draw. Possible values are 74 | 'reduce', 'flipkinNQ', 'flipkinH', or 'probe'. 75 | 76 | dot_mode (int) Set the style of the kinemage dots: 0 = spheres, 77 | 1 = quads. (Default: 0) 78 | 79 | ''' 80 | # Disable (toggle off) all kinemages from this object 81 | self.disable_kin() 82 | 83 | # Draw the Kinemage CGO into the proper PyMOL object group 84 | source_kin = self.kin[kin] 85 | kin_grp = self.get_kin_cgo_group(kin) 86 | source_kin.draw(kin_grp, dot_mode=dot_mode) 87 | 88 | # Show only the current kinemage and its corresponding molecule. 89 | self.solo_pdb(kin) 90 | self.solo_kin(kin) 91 | 92 | if kin.startswith('flipkin'): 93 | self.animate() 94 | 95 | def enable_pdb(self, pdb): 96 | '''Enable an MPObject structure object. 97 | 98 | PARAMETERS 99 | 100 | pdb (str) The structure to be enabled. Possible values are 101 | 'reduce', 'flipkinNQ', 'flipkinH', 'userflips', or 'probe'. 102 | 103 | ''' 104 | pdb_obj = self.pdb[pdb] 105 | cmd.enable(pdb_obj) 106 | 107 | def disable_pdb(self, pdb='all'): 108 | '''Disable an MPObject structure object. 109 | 110 | PARAMETERS 111 | 112 | pdb (str) The structure to be disabled. Possible values are 113 | 'reduce', 'flipkinNQ', 'flipkinH', 'userflips', 'probe', or 114 | 'all'. (Default: all) 115 | 116 | ''' 117 | if pdb == 'all': 118 | for p in self.pdb.keys(): 119 | self.disable_pdb(p) 120 | else: 121 | pdb_obj = self.pdb[pdb] 122 | logger.debug('disabling {}'.format(pdb_obj)) 123 | cmd.disable(pdb_obj) 124 | 125 | def solo_pdb(self, pdb): 126 | '''Enable an MPObject structure object and disable all the others. 127 | 128 | PARAMETERS 129 | 130 | pdb (str) The structure to be enabled. Possible values are 131 | 'reduce', 'flipkinNQ', 'flipkinH', 'userflips', or 'probe'. 132 | 133 | ''' 134 | self.disable_pdb('all') 135 | self.enable_pdb(pdb) 136 | 137 | def enable_kin(self, kin): 138 | '''Enable an MPObject kinemage. 139 | 140 | PARAMETERS 141 | 142 | kin (str) The kinemage to be enabled. Possible values are 143 | 'flipkinNQ', 'flipkinH', or 'probe'. 144 | 145 | ''' 146 | kin_group = self.get_kin_cgo_group(kin) 147 | cmd.enable(kin_group) 148 | 149 | def disable_kin(self, kin='all'): 150 | '''Disable an MPObject kinemage. 151 | 152 | PARAMETERS 153 | 154 | kin (str) The kinemage to be disabled. Possible values are 155 | 'flipkinNQ', 'flipkinH', 'probe', or 'all'. (Default: all) 156 | 157 | ''' 158 | if kin == 'all': 159 | for k in self.kin.keys(): 160 | self.disable_kin(k) 161 | else: 162 | kin_grp = self.get_kin_cgo_group(kin) 163 | logger.debug('disabling'.format(kin_grp)) 164 | cmd.disable(kin_grp) 165 | 166 | def solo_kin(self, kin): 167 | '''Enable an MPObject kinemage and disable all the others. 168 | 169 | PARAMETERS 170 | 171 | kin (str) The kinemage to be enabled. Possible values are 172 | 'flipkinNQ', 'flipkinH', or 'probe'. 173 | 174 | ''' 175 | self.disable_kin('all') 176 | self.enable_kin(kin) 177 | 178 | def enable_flipkin_group(self, group='reduce'): 179 | '''Enable a flipkin kinemage group. 180 | 181 | PARAMETERS 182 | 183 | group (str) The group to be enabled. Possible values are 184 | 'reduce', 'flipNQ', 'flipH', or 'both'. (Default: both) 185 | 186 | ''' 187 | if group in ['reduce', 'flipNQ', 'flipH']: 188 | sel = '{}.*.{}'.format(self.mp_group, group) 189 | else: 190 | msg = 'not a typical flipkin group name: {}'.format(group) 191 | logger.warning(msg) 192 | sel = '{}.*.{}*'.format(self.mp_group, group) 193 | logger.debug('enabling "{}"'.format(sel)) 194 | cmd.enable(sel) 195 | 196 | def disable_flipkin_group(self, group='all'): 197 | '''Disable a flipkin kinemage group. 198 | 199 | PARAMETERS 200 | 201 | group (str) The group to be disabled. Possible values are 202 | 'reduce', 'flipNQ', 'flipH', or 'all'. (Default: both) 203 | 204 | ''' 205 | GROUPS = ['reduce', 'flipNQ', 'flipH'] 206 | if group == 'all': 207 | # disable each one recursively 208 | for g in GROUPS: 209 | self.disable_flipkin_group(g) 210 | if group not in GROUPS: 211 | msg = 'not a typical flipkin group name: {}'.format(group) 212 | logger.warning(msg) 213 | sel = '{}.*.{}'.format(self.mp_group, group) 214 | logger.debug('disabling "{}"'.format(sel)) 215 | cmd.disable(sel) 216 | 217 | def solo_flipkin_group(self, grp): 218 | '''Enable a flipkin kinemage group and disable all the others. 219 | 220 | PARAMETERS 221 | 222 | group (str) The group to be enabled. Possible values are 223 | 'reduce', 'flipNQ', or 'flipH'. 224 | 225 | ''' 226 | self.disable_flipkin_group('all') 227 | self.enable_flipkin_group(grp) 228 | 229 | def animate(self, no_recurse=False): 230 | '''Toggle between 'reduce' and 'flip' flipkin kinemage groups. 231 | 232 | The 'flip' group is either 'flipNQ' or 'flipH', depending on which 233 | kinemage is enabled. 234 | 235 | Note: Using this method is not recommended if you're using the GUI, as 236 | it doesn't currently affect the state of the flipkin group checkboxes. 237 | 238 | ''' 239 | # TODO integrate GUI animate button with this function. 240 | # Check which flipkin kinemages are enabled. 241 | logger.debug('checking which kinemages are enabled') 242 | enabled_flipkins = [] 243 | for fk in ['flipkinNQ', 'flipkinH']: 244 | kin_grp = self.get_kin_cgo_group(fk) 245 | if kin_grp in cmd.get_names(enabled_only=1): 246 | logger.debug(' {} flipkin is enabled.'.format(fk)) 247 | enabled_flipkins.append(fk) 248 | continue 249 | 250 | if not enabled_flipkins: 251 | if no_recurse: 252 | logger.debug('endless recursion, something went wrong') 253 | return 254 | 255 | logger.debug(' no flipkin kinemages enabled...enabling flipkinNQ') 256 | self.solo_kin('flipkinNQ') 257 | self.animate(True) 258 | return 259 | 260 | # Regex to match `mp_myobj.*.reduce` (flipkin 'reduce' group) 261 | reduce_group_regex = re.compile(re.escape(self.mp_group) + r'\.[^\.]+\.reduce$') 262 | 263 | # If at least one flipkin kinemage is enabled AND either the 'reduce' 264 | # molecule or any 'reduce' kinemage CGO groups are enabled, solo the 265 | # 'flipX' groups of both flipkin kinemages and the 'flip' molecule of 266 | # whichever flipkin kinemages are enabled. 267 | logger.debug('checking if any reduce molecule or kinemage is enabled...') 268 | reduce_is_enabled = 0 269 | for name in cmd.get_names(enabled_only=1): 270 | logger.debug(' checking {} for reduce group match'.format(name)) 271 | if reduce_group_regex.match(name) or name == self.pdb['reduce']: 272 | logger.debug(' match! {}') 273 | reduce_is_enabled = 1 274 | continue 275 | if reduce_is_enabled: 276 | logger.debug('reduce was enabled, switching to flips.') 277 | self.disable_pdb('reduce') 278 | # Enable the flipkin 'flip' groups 279 | self.solo_flipkin_group('flip') 280 | # And the molecules for each enabled flipkin. 281 | for fk in enabled_flipkins: 282 | self.enable_pdb(fk) 283 | 284 | # If, on the other hand, no 'reduce' group or molecule is enabled, solo 285 | # the 'reduce' groups for all kinemages and the 'reduce' 286 | # coordinates for this MPObject. 287 | else: 288 | self.solo_pdb('reduce') 289 | self.solo_flipkin_group('reduce') 290 | 291 | def get_pdbstr(self, pdb='reduce'): 292 | '''Return a PDB string for the specified structure object.''' 293 | # TODO this needs some work 294 | name = self.pdb[pdb] 295 | if name in cmd.get_names(): 296 | return cmd.get_pdbstr(name) 297 | else: 298 | msg = 'Sorry, no object loaded called {}!'.format(name) 299 | logger.warning(msg) 300 | return None 301 | 302 | def __init__(self, name): 303 | self.name = name 304 | group = 'mp_{}'.format(name) 305 | self.mp_group = group 306 | 307 | # Reduce output (need USER MOD headers to pass to flipkin) 308 | self.reduce_output = None 309 | self.flips = None 310 | 311 | # Coordinate object names 312 | self.pdb = { 313 | 'reduce': '{}.{}_reduce'.format(group, name), 314 | 'flipkinNQ': '{}.{}_flipkinNQ'.format(group, name), 315 | 'flipkinH': '{}.{}_flipkinH'.format(group, name), 316 | 'userflips': '{}.{}_userflips'.format(group, name), 317 | 'probe': '{}.{}_probe'.format(group, name), 318 | } 319 | 320 | # Processed Kinemage instances from Flipkin & Probe 321 | self.kin = { 322 | 'flipkinNQ': None, 323 | 'flipkinH': None, 324 | 'probe': None, 325 | } 326 | 327 | # Store viewids for persistence in GUI 328 | self.views = { 329 | 'flipkinNQ': None, 330 | 'flipkinH': None, 331 | 'probe': None, 332 | } 333 | 334 | 335 | def __str__(self): 336 | return "" % self.name 337 | 338 | 339 | ############################################################################### 340 | # 341 | # GENERAL FUNCTIONS 342 | # 343 | ############################################################################### 344 | 345 | def get_object(obj): 346 | '''Return the matching MPObject instance if it exists. 347 | 348 | PARAMETERS 349 | 350 | obj (str) The name of a loaded PyMOL structure object. (Also 351 | accepts an MPObject instance.) 352 | 353 | ''' 354 | if type(obj) is str: 355 | try: 356 | return objects[obj] 357 | except KeyError: 358 | msg = "get_object: '{}' not in plugin objects dict.".format(obj) 359 | logger.error(msg) 360 | raise MPException(msg) 361 | elif type(obj) is MPObject: 362 | try: 363 | if obj.name not in objects.keys(): 364 | msg = ("get_object: {}'s name attribute not in plugin objects " 365 | 'dict.').format(obj) 366 | raise ValueError(msg) 367 | if objects[obj.name] is obj: 368 | return obj 369 | else: 370 | msg = ('get_object: MPObject {} is not listed in the plugin ' 371 | 'objects dict under {}!').format(obj, obj.name) 372 | raise ValueError(msg) 373 | except AttributeError: 374 | msg = "get_object: '{}' not in plugin objects dict.".format(obj) 375 | logger.error(msg) 376 | raise MPException(msg) 377 | else: 378 | msg = "get_object: `obj` must be either a string or MPObject instance." 379 | raise ValueError(msg) 380 | 381 | 382 | def get_or_create_object(obj): 383 | '''Return the matching MPObject instance if it exists, or create it. 384 | 385 | PARAMETERS 386 | 387 | obj (str) Name of a loaded PyMOL structure object. 388 | 389 | ''' 390 | # Check input 391 | if not type(obj) is str: 392 | msg = "get_or_create_object: `obj` must be a string" 393 | raise TypeError(msg) 394 | 395 | # Get the object... 396 | if obj in objects.keys(): 397 | logger.debug('Using existing MPObject: {}'.format(obj)) 398 | return get_object(obj) 399 | # Or create it 400 | else: 401 | logger.debug('Creating MPObject: {}'.format(obj)) 402 | objects[obj] = MPObject(obj) 403 | return get_object(obj) 404 | 405 | 406 | def save_to_tempfile(data_str): 407 | """Save a selection to a temporary PDB file and return the file name.""" 408 | # text mode (py3 compatibility) 409 | tf = tempfile.NamedTemporaryFile("w", suffix=".pdb", dir=".", delete=False) 410 | tf.write(data_str) 411 | tf.close() 412 | return tf.name 413 | 414 | 415 | def run_command(args, input_str=None): 416 | """Run a command with the given arguments and optional piped STDIN input 417 | string, and return STDOUT as a string. 418 | """ 419 | try: 420 | process = subprocess.Popen(args, stdin=subprocess.PIPE, 421 | stderr=subprocess.PIPE, 422 | universal_newlines=True, # text mode (py3 compatibility) 423 | stdout=subprocess.PIPE) 424 | output, stderr = process.communicate(input_str or "") 425 | except OSError: 426 | msg = ("Unable to run the following command:\n\t`{}`\nPlease make " 427 | "sure {} is installed and can be found on the shell PATH.") 428 | raise MPException(msg.format(" ".join(args), args[0])) 429 | 430 | logger.debug("===== BEGIN OUTPUT =====\n%s", output) 431 | logger.debug("===== END OUTPUT =====") 432 | 433 | # Error check 434 | if process.returncode != 0: 435 | msg = '{} returned {}'.format(args[0], str(process.returncode)) 436 | logger.warning(msg) 437 | 438 | if stderr: 439 | logger.warning(stderr) 440 | 441 | return output 442 | 443 | 444 | 445 | ############################################################################### 446 | # 447 | # REDUCE 448 | # 449 | ############################################################################### 450 | 451 | def get_reduce_args(h=1, flip=1, quiet=1, addflags=None): 452 | """Return a list of arguments to be used to run Reduce locally. 453 | 454 | Prepare arguments to run Reduce locally using the specified options. Note 455 | that the path to the `reduce` executable must be set in the MolProbity 456 | plugin settings (see [mpset], [mpget]). 457 | 458 | With no keyword arguments, uses Reduce defaults, to perform NQH flips and 459 | add hydrogens. 460 | 461 | Any additional options passed via `addflags` will take precedence over 462 | other options specified by the normal keyword arguments. 463 | 464 | USAGE: 465 | 466 | get_reduce_args [h=1, [flip=1, [quiet=1, [addflags=None]]]] 467 | 468 | ARGUMENTS: 469 | 470 | h: 471 | 1 = Add hydrogens. (default) 472 | 0 = Remove ("trim") hydrogens if present. 473 | 474 | flip: 475 | If unset, no NQH flips will be performed. (default=1) 476 | 477 | quiet: 478 | Suppress reduce's normal console output. (default=1) 479 | 480 | addflags: 481 | A space-separated string of additional flags to pass directly to 482 | Reduce (e.g. "-DENSITY24 -SHOWSCORE"). (default=None) 483 | 484 | """ 485 | # Check inputs 486 | msg = "get_reduce_args: `{}` must be of type (or castable as) `{}`" 487 | try: h = int(h) 488 | except: raise TypeError(msg.format('h', int)) 489 | try: flip = int(flip) 490 | except: raise TypeError(msg.format('flip', int)) 491 | try: quiet = int(quiet) 492 | except: raise TypeError(msg.format('quiet', int)) 493 | 494 | # Handle nonbinary values 495 | if h != 0: h = 1 496 | if flip != 0: flip = 1 497 | if quiet != 0: quiet = 1 498 | 499 | if addflags is not None: 500 | if type(addflags) is not str: 501 | msg = ("get_reduce_args: `addflags` should be a space-separated " 502 | "string of options to pass to Reduce.") 503 | raise TypeError(msg) 504 | # logger.debug('reduce_object inputs are ok.') 505 | 506 | # Begin with the path to `reduce`. 507 | args = ['reduce'] # [settings.mpgetq('reduce_path')] 508 | 509 | # Handle keyword arguments 510 | if quiet: 511 | args.append('-Quiet') 512 | 513 | if flip: 514 | args.append('-FLIP') 515 | else: 516 | args.append('-NOFLIP') 517 | 518 | if h == 0: 519 | args.append('-Trim') 520 | 521 | # Any user-specified flags are added last. These will take precedence over 522 | # any of the previous flags from keyword arguments. 523 | if addflags: 524 | args.extend(addflags.split(' ')) 525 | 526 | # Pass the pdbstr from STDIN 527 | args.append('-') 528 | 529 | return args 530 | 531 | 532 | def generate_reduce_output(pdbstr, flip_type=1): 533 | '''Generate Reduce output from the given PDB string and flip_type. 534 | 535 | ARGUMENTS 536 | pdbstr (str) A loaded PyMOL object (typically an automatically 537 | generated temporary one, without hydrogens). 538 | 539 | flip_type (int) Indicates which type of result to make. 540 | 0 = no flips 541 | 1 = recommended flips (default) 542 | 2 = all flips 543 | 544 | ''' 545 | lookup = { 546 | 0: {'flip': 0}, # no flips 547 | 1: {}, # default flips 548 | 2: {'addflags': '-NOBUILD0'} # all scorable flips 549 | } 550 | args = get_reduce_args(**lookup[flip_type]) 551 | output = run_command(args, pdbstr) 552 | return output 553 | 554 | 555 | def process_reduce_output(reduced_pdbstr): 556 | '''Process Reduce output and return a Reduce result dict.''' 557 | prefix = 'USER MOD ' 558 | if not type(reduced_pdbstr) is str: 559 | msg = 'process_reduce_output: argument `reduced_pdbstr` must be a string.' 560 | raise TypeError(msg) 561 | if not reduced_pdbstr.startswith(prefix): 562 | raise ValueError('process_reduce_output: malformed input string') 563 | lines = reduced_pdbstr.split('\n') 564 | 565 | # Collect USER MOD records and remove the prefix from each line. 566 | user_mod = [l.replace(prefix, '') for l in lines if prefix in l] 567 | 568 | flips_list = flips.parse_flips(user_mod) 569 | 570 | return flips_list 571 | 572 | 573 | def reduce_object(obj, flip=1): 574 | """Add hydrogens to a copy of a loaded PyMOL object with Reduce. 575 | 576 | TODO: more doc here 577 | 578 | """ 579 | # Run reduce with specified flips 580 | pdbstr = cmd.get_pdbstr(obj) 581 | reduced_pdbstr = generate_reduce_output(pdbstr, flip_type=flip) 582 | 583 | # Fail gracefully if no output is generated. 584 | if not reduced_pdbstr: 585 | msg = "Failed to generate Reduce output for {}.".format(obj) 586 | logger.error(msg) 587 | return 588 | 589 | withflips = " with flips" if flip else "" 590 | logger.info("Generated Reduce output{} for '{}'.".format(withflips, obj)) 591 | 592 | # Process the output string for flips 593 | flips_list = process_reduce_output(reduced_pdbstr) 594 | logger.info("Processed Reduce output to extract list of flips.") 595 | 596 | # Store flips list and raw reduced_pdbstr in MPObject 597 | o = get_or_create_object(obj) 598 | o.flips = flips_list 599 | o.reduce_output = reduced_pdbstr 600 | 601 | # Store current group_auto_mode setting 602 | gam = cmd.get('group_auto_mode') 603 | cmd.set('group_auto_mode', 2) 604 | 605 | # Load the output PDB into a copy of the original and disable the original. 606 | name = o.pdb['reduce'] 607 | cmd.create(name, obj) # duplicate original to preserve representation 608 | cmd.read_pdbstr(reduced_pdbstr, name, state=1) 609 | cmd.disable(obj) 610 | 611 | # Restore original group_auto_mode 612 | cmd.set('group_auto_mode', gam) 613 | 614 | 615 | 616 | ############################################################################### 617 | # 618 | # FLIPKIN 619 | # 620 | ############################################################################### 621 | 622 | def generate_flipkin_output(filename, his=False): 623 | flipkin_path = 'flipkin' # TODO settings 624 | args = [flipkin_path] 625 | if his: 626 | args.append('-h') 627 | args.append(filename) 628 | output = run_command(args) 629 | return output 630 | 631 | 632 | def process_flipkin_output(kinstr): 633 | '''Currently just a wrapper for kinemage.process_kinemage().''' 634 | return kinemage.process_kinemage(kinstr) 635 | 636 | 637 | def create_object_with_flipkin_coords(mpobj, which_flips='NQ'): 638 | '''Duplicate the mpobj molecule and apply the flipNQ/H group coordinates. 639 | 640 | PARAMETERS 641 | 642 | mpobj An MPObject instance 643 | 644 | which_flips Either 'NQ' or 'H' to specify which flipkin to use 645 | 646 | ''' 647 | flipkin_name = 'flipkin{}'.format(which_flips) 648 | flipkin = mpobj.kin[flipkin_name] 649 | flip_group = 'flip{}'.format(which_flips) 650 | 651 | reduced_obj = mpobj.pdb['reduce'] 652 | flipped_obj = mpobj.pdb[flipkin_name] 653 | cmd.create(flipped_obj, reduced_obj) 654 | 655 | for vl in flipkin.vectorlists(): 656 | # Skip if not coordinates 657 | if vl[0].vectorlist_name == 'x': 658 | msg = 'skipping non-coords vectorlist {}' 659 | logger.debug(msg.format(vl[0].vectorlist_name)) 660 | continue 661 | 662 | # Skip everything except the'flipNQ' (or 'flipH') group 663 | if vl[0].group[0] != flip_group: 664 | msg = 'skipping non-{} vectorlist'.format(flip_group) 665 | logger.debug(msg) 666 | continue 667 | 668 | logger.debug('begin flipping vectorlist') 669 | for v in vl: 670 | if not (v.atom[0] and v.atom[1]): 671 | msg = 'vector {} was missing atoms: {}'.format(v, v.atom) 672 | logger.warning(msg) 673 | continue 674 | macro = v.macro(1) 675 | sel = v.sel(1) 676 | logger.debug('atom to be flipped: {}\n sel: {}'.format(macro, sel)) 677 | source_coords = v.coords[1] 678 | 679 | target_sel = '{} and {}'.format(flipped_obj, sel) 680 | msg = 'flipping atom {} to {}'.format(target_sel, source_coords) 681 | logger.debug(msg) 682 | 683 | 684 | ret = cmd.load_coords([source_coords], target_sel) 685 | if ret == -1: 686 | success = 0 687 | a = v.atom[1] 688 | 689 | if a['resn'] == 'HIS': 690 | his_h = { 691 | 'HD1': { 692 | 'old_h': 'HE2', 693 | 'old_n': 'NE2', 694 | 'new_h': 'HD1', 695 | 'new_n': 'ND1', 696 | }, 697 | 'HE2': { 698 | 'old_h': 'HD1', 699 | 'old_n': 'ND1', 700 | 'new_h': 'HE2', 701 | 'new_n': 'NE2', 702 | } 703 | } 704 | if a['name'] in his_h.keys(): 705 | # Reduce has switched which N is protonated. Let's move 706 | # the old H atom and rename it. 707 | resi_sel = '{} and chain {} and resi {}'.format( 708 | flipped_obj, a['chain'], a['resi']) 709 | h = his_h[a['name']] 710 | old_h = '{} and name {}'.format(resi_sel, h['old_h']) 711 | old_n = '{} and name {}'.format(resi_sel, h['old_n']) 712 | new_h = '{} and name {}'.format(resi_sel, h['new_h']) 713 | new_n = '{} and name {}'.format(resi_sel, h['new_n']) 714 | 715 | # Break the old bond 716 | cmd.unbond(old_h, old_n) 717 | 718 | # Rename the atom 719 | cmd.alter(old_h, 'name="{}"'.format(a['name'])) 720 | 721 | # Make the new bond 722 | cmd.bond(new_h, new_n) 723 | 724 | # Retry loading coordinates 725 | ret = cmd.load_coords([source_coords], target_sel) 726 | if not ret == -1: 727 | success = 1 728 | 729 | if not success: 730 | msg = 'failed to load coords for {}!'.format(macro) 731 | logger.warning(msg) 732 | 733 | logger.debug('end flipping vectorlist') 734 | 735 | 736 | def flipkin_object(obj): 737 | '''Run flipkin to generate Asn/Gln and His flip kinemages. 738 | 739 | ARGUMENTS 740 | 741 | obj (str) 742 | 743 | Name of a loaded PyMOL object that has already been passed as an 744 | argument to `reduce_object()` (`reduce_obj` from the PyMOL command 745 | line). 746 | 747 | Uses the coordinates output from a previous run of reduce_object(). 748 | ''' 749 | o = get_object(obj) 750 | 751 | # Save a tempfile 752 | tf = save_to_tempfile(o.reduce_output) 753 | 754 | # Run flipkin to get NQ and H flip kinemages 755 | flipkinNQ_raw = generate_flipkin_output(tf) 756 | if not flipkinNQ_raw: 757 | msg = 'Failed to generate Flipkin NQ output for {}.'.format(obj) 758 | raise MPException(msg) 759 | 760 | flipkinH_raw = generate_flipkin_output(tf, his=True) 761 | if not flipkinH_raw: 762 | msg = 'Failed to generate Flipkin H output for {}.'.format(obj) 763 | raise MPException(msg) 764 | 765 | # Cleanup 766 | os.unlink(tf) 767 | 768 | logger.info("Generated Flipkin output for '{}'.".format(obj)) 769 | 770 | # Process flipkins 771 | o.kin['flipkinNQ'] = process_flipkin_output(flipkinNQ_raw) 772 | o.kin['flipkinH'] = process_flipkin_output(flipkinH_raw) 773 | 774 | create_object_with_flipkin_coords(o, 'NQ') 775 | create_object_with_flipkin_coords(o, 'H') 776 | 777 | o.draw('flipkinNQ') 778 | o.draw('flipkinH') 779 | 780 | o.disable_kin('all') 781 | o.animate() 782 | 783 | logger.info("Stored Flipkin output for '{}'.".format(obj)) 784 | 785 | 786 | 787 | ############################################################################### 788 | # 789 | # PROBE 790 | # 791 | ############################################################################### 792 | 793 | def get_probe_args(pdb_file): 794 | '''Return arguments for running Probe on the given the PDB filename.''' 795 | probe_path = 'probe' # TODO settings.mpgetq('probe_path') 796 | return [probe_path, '-Quiet', '-Self', 'ALL', pdb_file] 797 | 798 | 799 | def generate_probe_output(pdbstr): 800 | '''Generate Probe output from the given PDB string. 801 | 802 | ARGUMENTS 803 | pdbstr (str) PDB coordinates from a loaded PyMOL object. 804 | 805 | ''' 806 | # Probe doesn't accept input via STDIN, so we need to write a tempfile. 807 | tf = save_to_tempfile(pdbstr) 808 | assert os.path.isfile(tf) 809 | args = get_probe_args(tf) 810 | output = run_command(args) 811 | os.unlink(tf) 812 | assert not os.path.isfile(tf) 813 | return output 814 | 815 | 816 | def process_probe_output(kinstr): 817 | '''Process Probe output and return lists of dots and clashes.''' 818 | 819 | return kinemage.process_kinemage(kinstr) 820 | 821 | 822 | def probe_object(obj): 823 | '''Run Probe on the "Reduce-d" coordinates of a loaded PyMOL object. 824 | 825 | ARGUMENTS 826 | 827 | obj (str) 828 | 829 | Name of a loaded PyMOL object that has already been passed as an 830 | argument to `reduce_object()` (or `reduce_obj` from the PyMOL 831 | command line). 832 | 833 | NOTE 834 | 835 | Reduce_object() must be run prior to probe_object() in order to set up 836 | an MPObject instance in the `objects` dictionary. Running 837 | probe_object() on a plain PyMOL object will fail. Also, accordingly, 838 | keep in mind that the coordinates probe_object() uses are those of the 839 | Reduce-modified version. For an object `myobj`, this will typically 840 | be `mp_myobj.myobj_reduce`. 841 | 842 | ''' 843 | 844 | 845 | o = get_object(obj) 846 | 847 | # Clear previous results 848 | cmd.delete(o.pdb['probe']) # coords 849 | cmd.delete(o.get_kin_cgo_group('probe')) # cgo 850 | 851 | # Create the PDB to use with the 'probe' kinemage 852 | if o.pdb['userflips'] in cmd.get_names(): 853 | which = 'userflips' 854 | else: 855 | which = 'reduce' 856 | v = cmd.get_view() 857 | cmd.create(o.pdb['probe'], o.pdb[which]) 858 | cmd.set_view(v) 859 | 860 | pdbstr = o.get_pdbstr('probe') 861 | output = generate_probe_output(pdbstr) 862 | 863 | # Fail gracefully if probe call returns no output. 864 | if not output: 865 | msg = 'Failed to generate Probe output for {}.'.format(obj) 866 | logger.error(msg) 867 | return 868 | 869 | logger.info("Generated Probe output for '{}'.".format(obj)) 870 | 871 | # Store list of dots and vectors 872 | o.kin['probe'] = process_probe_output(output) 873 | 874 | # Draw dots and vectors 875 | o.draw('probe') 876 | cmd.set_view(v) 877 | 878 | 879 | # Set up a module-level `objects` storage variable the first time only. 880 | try: # pragma: no cover 881 | len(objects) 882 | logger.info('Using existing MolProbity objects list.') 883 | except: 884 | objects = {} 885 | logger.info('Set up MolProbity objects list.') 886 | 887 | 888 | 889 | # ############################################################################### 890 | # # 891 | # # Set up CLI 892 | # # 893 | # ############################################################################### 894 | 895 | cmd.extend('reduce_obj', reduce_object) 896 | cmd.extend('flipkin_obj', flipkin_object) 897 | cmd.extend('probe_obj', probe_object) 898 | 899 | logger.info('Finished loading MolProbity plugin.') 900 | -------------------------------------------------------------------------------- /pymolprobity/points.py: -------------------------------------------------------------------------------- 1 | '''Points (dots, vectors) for PyMOLProbity plugin.''' 2 | 3 | from __future__ import absolute_import 4 | 5 | import copy 6 | import logging 7 | import re 8 | 9 | from chempy import cpv 10 | #from pymol import cmd 11 | from pymol import cgo 12 | 13 | from . import colors 14 | # from settings import mpgetq 15 | from . import utils 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | ############################################################################### 22 | # 23 | # CGO UTILS 24 | # 25 | ############################################################################### 26 | 27 | def _cgo_color(color): 28 | '''Return a CGO list specifying a color.''' 29 | r, g, b = colors.get_color_rgb(color) 30 | return [cgo.COLOR, r, g, b] 31 | 32 | 33 | def _cgo_sphere(pos, radius): 34 | '''Return a CGO list specifying a sphere.''' 35 | x, y, z = pos 36 | return [cgo.SPHERE, x, y, z, radius] 37 | 38 | 39 | def _perp_vec(vec): 40 | '''Return a vector orthogonal to `vec`.''' 41 | if abs(vec[0]) > 1e-6: 42 | return [-(vec[1] + vec[2]) / vec[0], 1., 1.] 43 | if abs(vec[1]) > 1e-6: 44 | return [1., -(vec[0] + vec[2]) / vec[1], 1.] 45 | return [1., 1., -(vec[0] + vec[1]) / vec[2]] 46 | 47 | 48 | def _cgo_quad(pos, normal, radius): 49 | '''Return a CGO list specifying a quad.''' 50 | v1 = cpv.normalize(_perp_vec(normal)) 51 | v2 = cpv.cross_product(normal, v1) 52 | v1 = cpv.scale(v1, radius) 53 | v2 = cpv.scale(v2, radius) 54 | obj = [ cgo.BEGIN, 55 | cgo.TRIANGLE_STRIP, 56 | cgo.NORMAL] 57 | obj.extend(normal) 58 | obj.append(cgo.VERTEX) 59 | obj.extend(cpv.add(pos, v1)) 60 | obj.append(cgo.VERTEX) 61 | obj.extend(cpv.add(pos, v2)) 62 | obj.append(cgo.VERTEX) 63 | obj.extend(cpv.sub(pos, v2)) 64 | obj.append(cgo.VERTEX) 65 | obj.extend(cpv.sub(pos, v1)) 66 | obj.append(cgo.END) 67 | return obj 68 | 69 | 70 | def _cgo_cylinder(pos0, pos1, radius, rgb0, rgb1): 71 | '''Return a CGO list specifying a cylinder.''' 72 | cgolist = [cgo.CYLINDER] 73 | cgolist.extend(pos0) # start 74 | cgolist.extend(pos1) # end 75 | cgolist.append(radius) 76 | cgolist.extend(rgb0) # start color 77 | cgolist.extend(rgb1) # end color 78 | return cgolist 79 | 80 | 81 | 82 | ############################################################################## 83 | # 84 | # ATOM INFO STRINGS 85 | # 86 | ############################################################################### 87 | 88 | # KEY: N=name, A=alt, R=resn, I=resi, X=ins code, C=chain, O=occ, b=Bfac 89 | 90 | # Simple dotlist atom: 15-char total, with 2-char chain ID 91 | # e.g. "NNNNARRRIIIIXCC" 92 | BASIC_ATOM_RE = re.compile( 93 | r"^([\w\? ]{4})" # group 1: atom name (e.g. " CA ") 94 | r"([\w ])" # group 2: alternate conformation ID (e.g. "A") 95 | r"([\w]{3})" # group 3: residue name (e.g. "ARG") 96 | r"([\d ]{4}[\w ])" # group 4: residue number + insertion code 97 | r"([\w ]{1,4})" # group 5: chain ID (1-, 2-, or 4-char) 98 | ) 99 | 100 | def process_basic_atom_string(atom_str): 101 | m = BASIC_ATOM_RE.match(atom_str) 102 | if m: 103 | name, alt, resn, resi, chain = [g.strip().upper() for g in m.groups()] 104 | # Assemble into dict 105 | atom_dict = { 106 | 'chain': chain, 107 | 'resn': resn, 108 | 'resi': resi, 109 | 'name': name, 110 | 'alt': alt, 111 | } 112 | return atom_dict 113 | else: 114 | return None 115 | 116 | 117 | # Bonds vectorlist with occupancy (optional), B-factor, and input file name 118 | # e.g. "NNNNARRRCCIIIIX OOOOBbbbbbb filename" 119 | BONDS_VECTORLIST_ATOM_RE = re.compile( 120 | r"^([\w\? ]{4})" # group 1: atom name (e.g. " CA ") 121 | r"([\w ])" # group 2: alternate conformation ID (e.g. "A") 122 | r"([\w]{3})" # group 3: residue name (e.g. "ARG") 123 | r"([\w ]{2})" # group 4: chain ID 124 | r"([\d ]{4}[\w ]) " # group 5: residue number + ins code 125 | r"([\d\.]{4}|) ?" # group 6: 4-char occupancy (optional) + space? 126 | r"B([\d\.]{4,6}) " # group 7: 4- to 6-char B-factor + space 127 | r".+$" # input file name 128 | ) 129 | 130 | def process_bonds_vectorlist_atom(atom_str): 131 | m = BONDS_VECTORLIST_ATOM_RE.match(atom_str) 132 | if m: 133 | (name, alt, resn, chain, 134 | resi, occ, b) = [g.strip().upper() for g in m.groups()] 135 | # Convert occupancy and B-factor to floats 136 | if not occ: 137 | occ = 1.00 138 | else: 139 | occ = float(occ) 140 | if not b: 141 | b = 0.00 142 | else: 143 | b = float(b) 144 | # Assemble into dict 145 | atom_dict = { 146 | 'chain': chain, 147 | 'resn': resn, 148 | 'resi': resi, 149 | 'name': name, 150 | 'alt': alt, 151 | 'occ': occ, 152 | 'b': b, 153 | } 154 | return atom_dict 155 | else: 156 | return None 157 | 158 | 159 | 160 | ############################################################################### 161 | # 162 | # DOTS 163 | # 164 | ############################################################################### 165 | 166 | class Dot(object): 167 | """Python representation of a Molprobity-style kinemage dot.""" 168 | 169 | # def set_draw(self): 170 | # self.draw = 1 171 | 172 | # def unset_draw(self): 173 | # self.draw = 0 174 | 175 | # def toggle_draw(self): 176 | # if self.draw: 177 | # self.draw = 0 178 | # else: 179 | # self.draw = 1 180 | 181 | def get_cgo(self, dot_mode=0, dot_radius=0.03): 182 | """Generate a CGO list for a dot.""" 183 | cgolist = [] 184 | 185 | # COLOR 186 | cgolist.extend(_cgo_color(self.color)) 187 | 188 | if dot_mode == 0: # spheres 189 | logger.debug("Adding dot to cgolist...") 190 | cgolist.extend(_cgo_sphere(self.coords, dot_radius)) 191 | logger.debug("Finished adding dot to cgolist.") 192 | 193 | if dot_mode == 1: # quads 194 | logger.debug("Adding quad to cgolist...") 195 | normal = cpv.normalize(cpv.sub(self.coords, self.atom['coords'])) 196 | cgolist.extend(_cgo_quad(self.coords, normal, dot_radius * 1.5)) 197 | logger.debug("Finished adding quad to cgolist.") 198 | 199 | return cgolist 200 | 201 | def __init__(self, 202 | atom=None, color=None, pointmaster=None, coords=None, draw=1): 203 | # Atom 204 | self.atom = atom 205 | 206 | # Dot info 207 | self.color = colors.get_pymol_color(color) 208 | self.pm = pointmaster 209 | self.coords = coords 210 | self.draw = draw 211 | 212 | # Dotlist info 213 | self.dotlist_name = None 214 | self.dotlist_color = None 215 | self.master = None 216 | 217 | # # Bind to PyMOL atom 218 | # #self.atom_selection = None 219 | # #self.atom_id = None 220 | # #if self.atom is not None: 221 | # #self.atom_selection = self._get_atom_selection() 222 | # #self.atom_id = cmd.id_atom(self.atom_selection) 223 | # #print self.atom_selection, self.atom_id 224 | 225 | 226 | DOTLIST_HEADER_RE = re.compile( 227 | r"dotlist " # dotlist keyword + space 228 | r"{([^}]*)} " # atom info section + space 229 | r"color=([^\s]*) " # color 230 | r"master={([^}]*)}" # master 231 | ) 232 | 233 | def _parse_dotlist_header(line): 234 | """Parse the header line of a kinemage `@dotlist` keyword. 235 | 236 | Header lines are in the following format: 237 | 238 | dotlist {x} color=white master={vdw contact} 239 | dotlist {x} color=sky master={vdw contact} 240 | dotlist {x} color=red master={H-bonds} 241 | 242 | Where "x" is an arbitrary name for the dotlist (currently hard-coded as "x" 243 | in the Probe source), the color is the default color of the dots, and 244 | master is the type of interaction depicted by the dots. 245 | 246 | """ 247 | m = DOTLIST_HEADER_RE.match(line) 248 | 249 | # name, color, master 250 | return m.group(1), m.group(2), utils.slugify(m.group(3)) 251 | 252 | 253 | DOTLIST_BODY_RE = re.compile( 254 | r"{([^}]*)}" # atom info string 255 | r"(\w*)\s*" # color + optional space(s) 256 | r"'(\w)' " # pointmaster 257 | r"([0-9.,\-]*)" # coordinates 258 | ) 259 | 260 | def _parse_dotlist_body(lines): 261 | """Parse the non-header lines of a kinemage `@dotlist` keyword. 262 | 263 | Body lines are in the following format[*]: 264 | 265 | { CA SER 26 A}blue 'O' 61.716,59.833,8.961 266 | { OE1 GLU 31 A}blue 'S' 61.865,58.936,17.234 267 | { OE2 GLU 31 A}blue 'S' 61.108,60.399,15.044 268 | { H? HOH 293 A}greentint 'O' 57.884,59.181,7.525 [**] 269 | { O HOH 435 A}greentint 'O' 56.838,61.938,21.538 270 | { O HOH 450 A}greentint 'O' 55.912,56.611,17.956 271 | 272 | Or generally: 273 | 274 | {AAAABCCCDDDDEFF}colorname 'G' X,Y,Z 275 | 276 | where: 277 | 278 | AAAA = atom name 279 | B = alt conf 280 | CCC = residue name (3-letter) 281 | DDDD = residue number (typically 3 digits) 282 | E = insertion code 283 | FF = chain (typically a single letter) 284 | G = pointmaster code (e.g. ScSc, McSc, McMc, Hets) 285 | 286 | * Note that the text within the braces is not space-delimited, but is 287 | arranged in fixed-width columns. 288 | 289 | ** Also, 'H?' for water hydrogens is problematic and simply stripped away. 290 | 291 | """ 292 | active_atom = None 293 | dots = [] 294 | 295 | # Parse lines to generate Dots 296 | for i, l in enumerate(lines): 297 | m = DOTLIST_BODY_RE.match(l) 298 | 299 | # Atom selection in kinemages is only written explicitly the first 300 | # time for a given set of dots. Afterward, it inherits from the 301 | # previous line via a single double-quote character ("). 302 | if m.group(1) == '"': 303 | atom = active_atom 304 | else: 305 | # e.g.: " O HOH 450 A" 306 | logger.debug('m.group(1) match is: %s' % m.group(1)) 307 | atom_sel = m.group(1) 308 | # TODO: Check this formatting in probe documentation 309 | name = atom_sel[0:4].strip().replace('?','') 310 | alt = atom_sel[4:5].strip().upper() 311 | resn = atom_sel[5:8].strip().upper() 312 | resi = atom_sel[8:13].strip().upper() 313 | chain = atom_sel[13:15].strip().upper() 314 | 315 | logger.debug('Generating atom for dot %i...' % i) 316 | # TODO don't create duplicate atoms (track in MPObject) 317 | atom = {'name': name, 318 | 'alt': alt, 319 | 'resn': resn, 320 | 'resi': resi, 321 | 'chain': chain} 322 | logger.debug('Finished generating atom for dot %i.' % i) 323 | 324 | active_atom = atom 325 | 326 | color = m.group(2) 327 | pointmaster = m.group(3) 328 | coords = [float(c) for c in m.group(4).split(',')] 329 | 330 | # Create the Dot 331 | dot = Dot(atom, color, pointmaster, coords) 332 | dots.append(dot) 333 | 334 | return dots 335 | 336 | 337 | # #def _get_dot_atom_selection(dot): 338 | # #assert type(dot) is Dot 339 | # #obj = "%s" % dot.dotlist.result.obj 340 | # #if dot.atom['chain']: 341 | # #sele = "%s and chain %s" % (sele, dot.chain) 342 | # #if dot.atom['resn']: 343 | # #sele = "%s and resn %s" % (sele, dot.resn) 344 | # #if dot.atom['resi']: 345 | # #sele = "%s and resi %s" % (sele, dot.resi) 346 | 347 | # ## Hack: Probe gives HOH hydrogens atom names of 'H?', which, even when the 348 | # ## '?' is stripped, doesn't work with PyMOL, which numbers them 'H1' and 349 | # ## 'H2'. 350 | # #if dot.atom['name'] and not dot.atom['resn'] == 'HOH': # hack 351 | # #sele = "%s and name %s" % (sele, dot.name) 352 | # #return sele 353 | 354 | 355 | def process_dotlist(lines, context): 356 | '''Process a list of dotlist lines and return a list of Dots. 357 | 358 | Given a list of lines from a Kinemage file comprising a dotlist, parse the 359 | first line as the header, and the remaining lines as the body. Create a 360 | Dot() instance for each line and return a list of Dots. 361 | 362 | ''' 363 | logger.debug("Parsing dotlist header...") 364 | name, color, master = _parse_dotlist_header(lines[0]) 365 | logger.debug("Parsing dotlist body...") 366 | dots = _parse_dotlist_body(lines[1:]) 367 | 368 | logger.debug("Adding Dotlist info...") 369 | # Add Dotlist info to each dot. 370 | for d in dots: 371 | # From dotlist header 372 | d.dotlist_name = name 373 | d.dotlist_color = color 374 | d.master = master 375 | 376 | # From context 377 | d.kinemage = context['kinemage'] 378 | d.group = context['group'] 379 | d.subgroup = context['subgroup'] 380 | d.animate = context['animate'] 381 | 382 | logger.debug("Finished adding Dotlist info.") 383 | 384 | return dots 385 | 386 | 387 | 388 | 389 | # ############################################################################### 390 | # # 391 | # # VECTORS 392 | # # 393 | # ############################################################################### 394 | 395 | class Vector(object): 396 | """Python representation of a Molprobity-style kinemage vector.""" 397 | 398 | # def set_draw(self): 399 | # self.draw = 1 400 | 401 | # def unset_draw(self): 402 | # self.draw = 0 403 | 404 | # def toggle_draw(self): 405 | # if self.draw: 406 | # self.draw = 0 407 | # else: 408 | # self.draw = 1 409 | 410 | def get_cgo(self, radius=0.03): 411 | """Generate a CGO list for a vector.""" 412 | cgolist = [] 413 | 414 | # Set colors 415 | rgb0 = colors.get_color_rgb(self.color[0]) 416 | rgb1 = colors.get_color_rgb(self.color[1]) 417 | 418 | if True: # cylinders 419 | logger.debug("Adding vector to cgolist...") 420 | # Cylinder 421 | cgolist.extend(_cgo_cylinder(self.coords[0], self.coords[1], 422 | radius, rgb0, rgb1)) 423 | # Caps 424 | cgolist.extend(_cgo_color(self.color[0])) 425 | cgolist.extend(_cgo_sphere(self.coords[0], radius)) 426 | cgolist.extend(_cgo_color(self.color[1])) 427 | cgolist.extend(_cgo_sphere(self.coords[1], radius)) 428 | 429 | logger.debug("Finished adding vector to cgolist.") 430 | 431 | return cgolist 432 | 433 | def macro(self, i): 434 | a = copy.copy(self.atom[i]) 435 | if a is None: 436 | return None 437 | if a['alt']: 438 | a['alt'] = '`{}'.format(a['alt']) 439 | else: 440 | a['alt'] = '' 441 | return '{chain}/{resn}`{resi}/{name}{alt}'.format(**a) 442 | 443 | def sel(self, i): 444 | a = copy.copy(self.atom[i]) 445 | if a is None: 446 | return None 447 | c = 'chain {chain} and '.format(**a) if a['chain'] else '' 448 | i = 'resi {resi} and '.format(**a) if a['resi'] else '' 449 | n = 'name {name} and '.format(**a) if a['name'] else '' 450 | alt = 'alt {alt} and '.format(**a) if a['alt'] else '' 451 | 452 | sel = '{}{}{}{}'.format(c, i, n, alt) 453 | return sel[:-5] # strip last " and " 454 | 455 | 456 | def __init__(self, 457 | atom0=None, color0=None, pointmaster0=None, coords0=None, 458 | atom1=None, color1=None, pointmaster1=None, coords1=None, draw=1): 459 | # Atom 460 | self.atom = [atom0, atom1] 461 | 462 | # Vector info 463 | c0 = colors.get_pymol_color(color0) 464 | c1 = colors.get_pymol_color(color1) 465 | self.color = [c0, c1] 466 | self.pm = [pointmaster0, pointmaster1] 467 | self.coords = [coords0, coords1] 468 | self.draw = draw 469 | 470 | # Vectorlist info 471 | self.vectorlist_name = None 472 | self.vectorlist_color = None 473 | self.master = None 474 | 475 | # # Bind to PyMOL atom 476 | # #self.atom_selection = None 477 | # #self.atom_id = None 478 | # #if self.atom is not None: 479 | # #self.atom_selection = self._get_atom_selection() 480 | # #self.atom_id = cmd.id_atom(self.atom_selection) 481 | # #print self.atom_selection, self.atom_id 482 | 483 | def __str__(self): 484 | vectorlist_info = '[{},{}]'.format(self.vectorlist_name, self.master) 485 | return '{}: {}--{}'.format(vectorlist_info, self.macro(0), self.macro(1)) 486 | 487 | 488 | VECTORLIST_HEADER_RE = re.compile( 489 | r"vectorlist " 490 | r"{([^}]*)} " # group 1: name 491 | r"color= *([^\s]*) *" # group 2: color 492 | r"(\w+ )*" # group 3: other text (e.g. nobutton) 493 | r"master= *{([^}]*)}" # group 4: master 494 | ) 495 | def _parse_vectorlist_header(line): 496 | """Parse the header line of a kinemage `@vectorlist` keyword. 497 | 498 | Header lines are in the following format: 499 | vectorlist {x} color=white master={small overlap} 500 | 501 | Where "x" is an arbitrary name for the list (currently hard-coded as "x" 502 | in the Probe source), the color is the default color of the vectors, and 503 | master is the type of interaction depicted by the vectors. 504 | 505 | """ 506 | logger.debug('parsing line: "{}"'.format(line)) 507 | 508 | NAME = 1 509 | COLOR = 2 510 | OTHER = 3 511 | MASTER = 4 512 | m = VECTORLIST_HEADER_RE.match(line) 513 | 514 | logger.debug('vectorlist header: {}'.format(m.groups())) 515 | 516 | # name, color, master 517 | return m.group(NAME), m.group(COLOR), utils.slugify(m.group(MASTER)) 518 | 519 | 520 | # Probe v2.16 (20-May-13) format 521 | VECTORLIST_CLASH_RE = re.compile( 522 | r"{([^}]*)}" # group 1: atom description 523 | r"(\w*) " # group 2: color 524 | r"([A-Z] )*" # group 3: optional L or P character followed by space 525 | r" *" # allow extra space before pointmaster 526 | r"'(\w)' " # group 4: pointmaster 527 | r"([0-9.,\-]*)" # group 5: coordinates as a single string 528 | ) 529 | def _parse_clash_vectorlist_body(lines): 530 | """Parse the non-header lines of a vectorlist containing clash spikes. 531 | 532 | Body lines are in the following format: 533 | { CB SER 26 A}yellowtint P 'O' 57.581,59.168,8.642 {"}yellowtint 'O' 57.589,59.163,8.646 534 | 535 | Or generally: 536 | 537 | {AAAA BBB CCCD E}colorname 'F' X,Y,Z (x 2) 538 | 539 | where: 540 | 541 | AAAA = atom name 542 | BBB = residue name (3-letter) 543 | CCC = residue number 544 | D = insertion code ??? TODO: check this 545 | E = chain 546 | F = pointmaster code (e.g. ScSc, McSc, McMc, Hets) 547 | 548 | * Note that the text within the braces is not space-delimited, but is 549 | arranged in fixed-width columns. 550 | 551 | Also, 'H?' for water hydrogens is problematic. # TODO 552 | 553 | """ 554 | 555 | ATOM = 1 556 | COLOR = 2 557 | LP = 3 558 | PM = 4 559 | COORDS = 5 560 | 561 | active_atom = None 562 | vectors = [] 563 | 564 | # Parse lines to generate Vectors 565 | for i, l in enumerate(lines): 566 | matches = VECTORLIST_CLASH_RE.finditer(l) 567 | 568 | v = [] 569 | for m in matches: 570 | logger.debug('match: {}'.format(m.group(0))) 571 | logger.debug('clash vectorlist body line: {}'.format(m.groups())) 572 | logger.debug('beginning match...') 573 | # Atom selection in kinemages is only written explicitly the first 574 | # time for a given list of points. Afterward, it inherits from the 575 | # previous point via a single double-quote character ("). 576 | atom_sel = m.group(ATOM) 577 | if atom_sel == '"': 578 | #logger.debug('using active atom...') 579 | atom = active_atom 580 | else: 581 | # TODO don't create duplicate atoms (track in MPObject) 582 | logger.debug('Generating atom for vector %i point...' % i) 583 | atom = process_basic_atom_string(atom_sel) 584 | logger.debug('Finished generating atom for vector %i point.' % i) 585 | 586 | active_atom = atom 587 | 588 | color = m.group(COLOR) 589 | pointmaster = m.group(PM) 590 | coords = [float(c) for c in m.group(COORDS).split(',')] 591 | 592 | v.append({ 593 | 'atom': atom, 594 | 'color': color, 595 | 'pm': pointmaster, 596 | 'coords': coords}) 597 | 598 | # Create the Vector 599 | vector = Vector(v[0]['atom'], v[0]['color'], v[0]['pm'], v[0]['coords'], 600 | v[1]['atom'], v[1]['color'], v[1]['pm'], v[1]['coords']) 601 | vectors.append(vector) 602 | 603 | return vectors 604 | 605 | 606 | VECTORLIST_BONDS_RE = re.compile( 607 | r"{([^}]*)}" # group 1: atom description 608 | r" ?" # optional space 609 | r"([LP]) " # group 2: single L or P character (TODO: what is this?) 610 | r"(?:'(\w)' )*" # group 3: pointmaster (optional) 611 | r"(?:(\w+) )*" # group 4: other non-quoted word, e.g. 'ghost' (optional) 612 | r"([\d,\.\- ]+)" # group 5: coordinates as a single string 613 | ) 614 | 615 | def _parse_bonds_vectorlist_body(lines): 616 | '''Parse vectorlist lines that describe bonds.''' 617 | # TODO: merge this with _parse_clashes_vectorlist_body() 618 | 619 | ATOM = 1 620 | LP = 2 # not used 621 | PM = 3 622 | OTHER = 4 # not used 623 | COORDS = 5 624 | 625 | # active_atom = None 626 | vectors = [] 627 | 628 | for i, l in enumerate(lines): 629 | matches = tuple(VECTORLIST_BONDS_RE.finditer(l)) 630 | 631 | # Set up new vector points list 632 | v = [] 633 | 634 | # If a continuation point, reuse the second point from the previous vector 635 | if len(matches) == 1: 636 | try: 637 | prev_v = vectors[-1] 638 | v.append({ 639 | 'atom': prev_v.atom[1], 640 | 'color': prev_v.color[1], 641 | 'pm': prev_v.pm[1], 642 | 'coords': prev_v.coords[1]}) 643 | except IndexError: 644 | logger.error('only 1 point given, but no previous points to use') 645 | raise 646 | elif len(matches) < 1 or len(matches) > 2: 647 | # <1 or >2 shouldn't happen 648 | raise ValueError('malformed line: {}'.format(l)) 649 | 650 | for j, m in enumerate(matches): 651 | logger.debug('match {} of {}: {}'.format(j+1, len(matches), m.group(0))) 652 | logger.debug('bond vectorlist body line: {}'.format(m.groups())) 653 | logger.debug('beginning match...') 654 | # # Atom selection in kinemages is only written explicitly the first 655 | # # time for a given set of dots. Afterward, it inherits from the 656 | # # previous line via a single double-quote character ("). 657 | atom_sel = m.group(ATOM) 658 | if atom_sel == '"': 659 | logger.debug('using active atom...') 660 | atom = active_atom 661 | else: 662 | try: 663 | # TODO don't create duplicate atoms (track in MPObject) 664 | msg = 'Generating atom for vector {} point {}...'.format(i, j) 665 | logger.debug(msg) 666 | 667 | atom = process_bonds_vectorlist_atom(atom_sel) 668 | except: 669 | msg = 'Atom info string `{}` could not be parsed. Skipping.' 670 | logger.error(msg.format(atom_sel)) 671 | 672 | logger.debug('Finished generating atom for vector %i point %i.' % (i, j)) 673 | 674 | # active_atom = atom 675 | 676 | color = None 677 | pointmaster = m.group(PM) 678 | coords = [float(c) for c in m.group(COORDS).split(',')] 679 | 680 | v.append({ 681 | 'atom': atom, 682 | 'color': color, 683 | 'pm': pointmaster, 684 | 'coords': coords}) 685 | 686 | # logger.debug('coords0: {}'.format(v[0]['coords'])) 687 | # logger.debug('coords1: {}'.format(v[1]['coords'])) 688 | 689 | # Create the Vector 690 | vector = Vector(v[0]['atom'], v[0]['color'], v[0]['pm'], v[0]['coords'], 691 | v[1]['atom'], v[1]['color'], v[1]['pm'], v[1]['coords']) 692 | 693 | vectors.append(vector) 694 | 695 | return vectors 696 | 697 | 698 | 699 | 700 | # #def _get_dot_atom_selection(dot): 701 | # #assert type(dot) is Dot 702 | # #obj = "%s" % dot.dotlist.result.obj 703 | # #if dot.atom['chain']: 704 | # #sele = "%s and chain %s" % (sele, dot.chain) 705 | # #if dot.atom['resn']: 706 | # #sele = "%s and resn %s" % (sele, dot.resn) 707 | # #if dot.atom['resi']: 708 | # #sele = "%s and resi %s" % (sele, dot.resi) 709 | 710 | # ## Hack: Probe gives HOH hydrogens atom names of 'H?', which, even when the 711 | # ## '?' is stripped, doesn't work with PyMOL, which numbers them 'H1' and 712 | # ## 'H2'. 713 | # #if dot.atom['name'] and not dot.atom['resn'] == 'HOH': # hack 714 | # #sele = "%s and name %s" % (sele, dot.name) 715 | # #return sele 716 | 717 | 718 | def process_vectorlist(lines, context): 719 | """Process a list of dotlist lines and return a list of Vectors. 720 | 721 | Given a list of lines from a Kinemage file comprising a dotlist, parse the 722 | first line as the header, and the remaining lines as the body. Create a 723 | Dot() instance for each line and return a list of Dots. 724 | 725 | """ 726 | logger.debug("Parsing vectorlist header...") 727 | name, color, master = _parse_vectorlist_header(lines[0]) 728 | logger.debug("Parsing vectorlist body...") 729 | if name == 'x': 730 | vectors = _parse_clash_vectorlist_body(lines[1:]) 731 | else: 732 | vectors = _parse_bonds_vectorlist_body(lines[1:]) 733 | 734 | logger.debug("Adding vectorlist info...") 735 | # Add Dotlist info to each dot. 736 | for v in vectors: 737 | # From vectorlist header 738 | v.vectorlist_name = name 739 | v.vectorlist_color = color 740 | v.master = master 741 | 742 | # From context 743 | v.kinemage = context['kinemage'] 744 | v.group = context['group'] 745 | v.subgroup = context['subgroup'] 746 | v.animate = context['animate'] 747 | 748 | logger.debug("Finished adding Vectorlist info.") 749 | 750 | return vectors 751 | 752 | 753 | -------------------------------------------------------------------------------- /pymolprobity/utils.py: -------------------------------------------------------------------------------- 1 | ''' Utility functions for PyMOLProbity plugin. ''' 2 | import re 3 | 4 | 5 | def quote_str(var, quote="'"): 6 | '''Enclose a str variable in quotes for printing.''' 7 | if type(var) is str: 8 | return '%s%s%s' % (quote, var, quote) 9 | else: 10 | return var 11 | 12 | 13 | def slugify(s, sep='_'): 14 | '''Eliminate troublesome characters in a string.''' 15 | try: 16 | # Replace anything not alphanumeric with a separator. 17 | slug = re.sub(r'[^A-Za-z0-9]+', sep, s).strip(sep) 18 | 19 | # Condense multiple adjacent separators into one. 20 | mult_sep = r'[' + re.escape(sep) + ']+' 21 | slug = re.sub(mult_sep, sep, slug) 22 | 23 | return slug 24 | 25 | except TypeError: 26 | # Don't slugify non-string input. 27 | return s 28 | 29 | 30 | def to_number(var): 31 | '''Convert a variable to a number if possible, and return it. 32 | 33 | Convert the passed variable to a number if possible and return the 34 | number. Otherwise, return the original variable. 35 | ''' 36 | try: 37 | # int (e.g. '1') 38 | return int(str(var)) 39 | except: 40 | # float (e.g. '1.23') 41 | try: 42 | return float(str(var)) 43 | except: 44 | # not a number 45 | return var 46 | 47 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==4.2 2 | funcsigs==1.0.2 3 | mock==2.0.0 4 | nose==1.3.7 5 | pbr==1.10.0 6 | six==1.10.0 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredsampson/pymolprobity/ac32531387bbae5dce2beda92c7b22b7229d183d/tests/__init__.py -------------------------------------------------------------------------------- /tests/colors_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import mock 4 | 5 | from .context import pymolprobity 6 | import pymolprobity.colors as c 7 | 8 | 9 | class GetPymolColorTests(unittest.TestCase): 10 | def test_with_color_list_color(self): 11 | inp = 'sea' 12 | ref = 'teal' 13 | res = c.get_pymol_color(inp) 14 | self.assertEqual(res, ref) 15 | 16 | def test_with_non_color_list_color(self): 17 | inp = 'somecolor' 18 | ref = 'somecolor' 19 | res = c.get_pymol_color(inp) 20 | self.assertEqual(res, ref) 21 | 22 | def test_with_None(self): 23 | inp = None 24 | ref = None 25 | res = c.get_pymol_color(inp) 26 | self.assertEqual(res, ref) 27 | 28 | 29 | class GetColorRGBTests(unittest.TestCase): 30 | @mock.patch('pymolprobity.colors.cmd') 31 | def test_workflow(self, mock_cmd): 32 | mock_cmd.get_color_index.return_value = 0 33 | mock_cmd.get_color_tuple.return_value = (1.0, 1.0, 1.0) 34 | inp = 'white' 35 | ref = (1.0, 1.0, 1.0) 36 | res = c.get_color_rgb(inp) 37 | self.assertEqual(res, ref) 38 | mock_cmd.get_color_index.assert_called_with(inp) 39 | mock_cmd.get_color_tuple.assert_called_with(0) 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath('..')) 4 | 5 | import pymolprobity 6 | -------------------------------------------------------------------------------- /tests/flips_tests.py: -------------------------------------------------------------------------------- 1 | '''Tests for PyMOLProbity flips module.''' 2 | 3 | import mock 4 | import unittest 5 | 6 | from .context import pymolprobity 7 | import pymolprobity.flips as flips 8 | 9 | 10 | 11 | class TestFlip(unittest.TestCase): 12 | def test_base_init(self): 13 | res = flips.Flip() 14 | self.assertEqual(res.flip_class, None) 15 | self.assertEqual(res.set, None) 16 | self.assertEqual(res.set_index, None) 17 | self.assertEqual(res.chain, None) 18 | self.assertEqual(res.resi, None) 19 | self.assertEqual(res.resn, None) 20 | self.assertEqual(res.name, None) 21 | self.assertEqual(res.alt, None) 22 | self.assertEqual(res.flip_type, None) 23 | self.assertEqual(res.descr, None) 24 | self.assertEqual(res.best_score, None) 25 | self.assertEqual(res.best_has_bad_bump, None) 26 | self.assertEqual(res.init_score, None) 27 | self.assertEqual(res.init_has_bad_bump, None) 28 | self.assertEqual(res.code, None) 29 | self.assertEqual(res.orig_score, None) 30 | self.assertEqual(res.orig_has_bad_bump, None) 31 | self.assertEqual(res.flip_score, None) 32 | self.assertEqual(res.flip_has_bad_bump, None) 33 | self.assertEqual(res.reduce_flipped, None) 34 | self.assertEqual(res.user_flipped, None) 35 | 36 | def test_macro(self): 37 | f = flips.Flip() 38 | f.chain = 'A' 39 | f.resi = '100' 40 | f.resn = 'THR' 41 | f.name = 'CA' 42 | res = f.macro() 43 | ref = 'A/THR`100/CA' 44 | self.assertEqual(res, ref) 45 | 46 | @mock.patch('pymolprobity.flips.Flip.macro') 47 | def test_str(self, mock_macro): 48 | mock_macro.return_value = 'macro' 49 | f = flips.Flip() 50 | res = f.__str__() 51 | ref = 'Flip: macro' 52 | self.assertEqual(res, ref) 53 | 54 | 55 | @mock.patch('pymolprobity.flips.parse_other_score') 56 | @mock.patch('pymolprobity.flips.parse_canflip_score') 57 | @mock.patch('pymolprobity.flips.parse_methyl_score') 58 | @mock.patch('pymolprobity.flips.parse_func_group') 59 | class TestParseFlips(unittest.TestCase): 60 | def setUp(self): 61 | self.set = ['Set10.1: B 358 ASN : amide:sc= -2.71! C(o=-1.3!,f=-3.4!)'] 62 | self.single = ['Single : A 41 ASN :FLIP amide:sc= -0.0583 X(o=-0.064,f=-0.058)'] 63 | 64 | def test_user_mod_with_too_few_sections(self, mock_pfunc, 65 | mock_parse_methyl_score, mock_parse_canflip_score, 66 | mock_parse_other_score): 67 | user_mod = ['Set:2'] 68 | with self.assertRaises(ValueError): 69 | flips.parse_flips(user_mod) 70 | 71 | def test_user_mod_with_too_many_sections(self, mock_pfunc, 72 | mock_parse_methyl_score, mock_parse_canflip_score, 73 | mock_parse_other_score): 74 | user_mod = ['Set:2:3:4:5'] 75 | with self.assertRaises(ValueError): 76 | flips.parse_flips(user_mod) 77 | 78 | def test_with_set(self, mock_pfunc, 79 | mock_parse_methyl_score, mock_parse_canflip_score, 80 | mock_parse_other_score): 81 | mock_pfunc.return_value = [1, 2, 3, 4, 5] 82 | mock_parse_canflip_score.return_value = [1, 2, 3, 4, 5, 6, 7] 83 | user_mod = self.set 84 | res = flips.parse_flips(user_mod) 85 | assert type(res) is list 86 | assert len(res) == 1 87 | f = res[0] 88 | assert f.flip_class == 'set' 89 | assert f.set == '10' 90 | assert f.set_index == '1' 91 | 92 | def test_with_single(self, mock_pfunc, 93 | mock_parse_methyl_score, mock_parse_canflip_score, 94 | mock_parse_other_score): 95 | mock_pfunc.return_value = [1, 2, 3, 4, 5] 96 | mock_parse_canflip_score.return_value = [1, 2, 3, 4, 5, 6, 7] 97 | user_mod = self.single 98 | res = flips.parse_flips(user_mod) 99 | assert type(res) is list 100 | assert len(res) == 1 101 | f = res[0] 102 | assert f.flip_class == 'single' 103 | assert f.set is None 104 | assert f.set_index is None 105 | 106 | def test_with_non_single_non_set(self, mock_pfunc, 107 | mock_parse_methyl_score, mock_parse_canflip_score, 108 | mock_parse_other_score): 109 | '''Parsing USER MOD with no flip lines should return an empty list.''' 110 | user_mod = ['blah'] # not Set or Single 111 | res = flips.parse_flips(user_mod) 112 | mock_pfunc.assert_not_called() 113 | mock_parse_methyl_score.assert_not_called() 114 | mock_parse_canflip_score.assert_not_called() 115 | mock_parse_other_score.assert_not_called() 116 | self.assertEqual(res, []) 117 | 118 | def test_with_set_not_matching_set_number_regex(self, mock_pfunc, 119 | mock_parse_methyl_score, mock_parse_canflip_score, 120 | mock_parse_other_score): 121 | user_mod = ['Set blah:2:3:4'] 122 | with self.assertRaises(ValueError): 123 | flips.parse_flips(user_mod) 124 | 125 | 126 | def test_calls_parse_func_group(self, mock_pfunc, 127 | mock_parse_methyl_score, mock_parse_canflip_score, 128 | mock_parse_other_score): 129 | mock_pfunc.return_value = [1, 2, 3, 4, 5] 130 | mock_parse_canflip_score.return_value = [1, 2, 3, 4, 5, 6, 7] 131 | user_mod = self.single 132 | res = flips.parse_flips(user_mod) 133 | mock_pfunc.assert_called_once_with(' A 41 ASN ') 134 | 135 | def test_sets_flip_func_group_info(self, mock_pfunc, 136 | mock_parse_methyl_score, mock_parse_canflip_score, 137 | mock_parse_other_score): 138 | mock_pfunc.return_value = ['A', '100B', 'THR', 'OG1', ''] 139 | mock_parse_canflip_score.return_value = [1, 2, 3, 4, 5, 6, 7] 140 | user_mod = self.single 141 | res = flips.parse_flips(user_mod) 142 | f = res[0] 143 | assert f.chain == 'A' 144 | assert f.resi == '100B' 145 | assert f.resn == 'THR' 146 | assert f.name == 'OG1' 147 | assert f.alt == '' 148 | 149 | def test_single_methyl_calls_parse_methyl_score(self, mock_pfunc, 150 | mock_parse_methyl_score, mock_parse_canflip_score, 151 | mock_parse_other_score): 152 | mock_pfunc.return_value = [1, 2, 3, 4, 5] 153 | mock_parse_methyl_score.return_value = [1, 2, 3, 4] 154 | user_mod = ['Single : A 57 LYS NZ :NH3+ 180:sc= 0 (180deg=0)'] 155 | flips.parse_flips(user_mod) 156 | assert mock_parse_methyl_score.called 157 | 158 | def test_single_canflip_calls_parse_canflip_score(self, mock_pfunc, 159 | mock_parse_methyl_score, mock_parse_canflip_score, 160 | mock_parse_other_score): 161 | mock_pfunc.return_value = [1, 2, 3, 4, 5] 162 | mock_parse_canflip_score.return_value = [1, 2, 3, 4, 5, 6, 7] 163 | user_mod = ['Single : A 41 ASN :FLIP amide:sc= -0.0583 X(o=-0.064,f=-0.058)'] 164 | flips.parse_flips(user_mod) 165 | assert mock_parse_canflip_score.called 166 | 167 | def test_single_other_calls_parse_other_score(self, mock_pfunc, 168 | mock_parse_methyl_score, mock_parse_canflip_score, 169 | mock_parse_other_score): 170 | mock_pfunc.return_value = [1, 2, 3, 4, 5] 171 | mock_parse_other_score.return_value = [1, 2] 172 | user_mod = ['Single : A 89 SER OG : rot 180:sc= 0'] 173 | flips.parse_flips(user_mod) 174 | assert mock_parse_other_score.called 175 | 176 | def test_unparseable_score(self, mock_pfunc, 177 | mock_parse_methyl_score, mock_parse_canflip_score, 178 | mock_parse_other_score): 179 | mock_pfunc.return_value = [1, 2, 3, 4, 5] 180 | user_mod = ['Single : A 89 SER OG : rot 180:bad score section'] 181 | with self.assertRaises(ValueError): 182 | flips.parse_flips(user_mod) 183 | 184 | 185 | class TestParseFuncGroup(unittest.TestCase): 186 | def test_with_1_char_chain_ids(self): 187 | fg = 'B 510 HIS ' # len 14 188 | ref = ['B','510','HIS','',''] 189 | res = flips.parse_func_group(fg) 190 | self.assertEqual(res, ref) 191 | 192 | def test_with_2_char_chain_ids(self): 193 | fg = ' B 510 HIS ' # len 15 194 | ref = ['B','510','HIS','',''] 195 | res = flips.parse_func_group(fg) 196 | self.assertEqual(res, ref) 197 | 198 | def test_with_4_char_chain_ids(self): 199 | fg = ' B 510 HIS ' # len 17 200 | ref = ['B','510','HIS','',''] 201 | res = flips.parse_func_group(fg) 202 | self.assertEqual(res, ref) 203 | 204 | def test_with_res_and_name(self): 205 | fg = ' B 528 THR OG1 ' 206 | ref = ['B', '528', 'THR', 'OG1', ''] 207 | res = flips.parse_func_group(fg) 208 | self.assertEqual(res, ref) 209 | 210 | def test_with_altconf(self): 211 | fg = ' B 528 THR OG1A' 212 | ref = ['B', '528', 'THR', 'OG1', 'A'] 213 | res = flips.parse_func_group(fg) 214 | self.assertEqual(res, ref) 215 | 216 | def test_with_bad_input_length(self): 217 | with self.assertRaises(ValueError): 218 | flips.parse_func_group('blah') 219 | 220 | 221 | class ParseMethylTests(unittest.TestCase): 222 | def test_with_basic_input(self): 223 | groups = (' 0', ' ', '0', '') 224 | ref = (0.0, False, 0.0, False) 225 | res = flips.parse_methyl_score(groups) 226 | self.assertEqual(res, ref) 227 | 228 | def test_with_actual_numbers_and_1_clash(self): 229 | groups = (' 0.728', ' ', '-1.25', '!') 230 | ref = (0.728, False, -1.25, True) 231 | res = flips.parse_methyl_score(groups) 232 | self.assertEqual(res, ref) 233 | 234 | 235 | class ParseCanflipTests(unittest.TestCase): 236 | def test_with_basic_input(self): 237 | groups = (' -0.0583', ' ', 'X', '-0.064', '', '-0.058', '') 238 | ref = (-0.0583, False, 'X', -0.064, False, -0.058, False) 239 | res = flips.parse_canflip_score(groups) 240 | self.assertEqual(res, ref) 241 | 242 | def test_with_some_clashes(self): 243 | groups = (' -2.71', '!', 'C', '-1.3', '!', '-3.4', '!') 244 | ref = (-2.71, True, 'C', -1.3, True, -3.4, True) 245 | res = flips.parse_canflip_score(groups) 246 | self.assertEqual(res, ref) 247 | 248 | 249 | class ParseOtherTests(unittest.TestCase): 250 | def test_with_basic_input(self): 251 | groups = (' 0', '') 252 | ref = (0, False) 253 | res = flips.parse_other_score(groups) 254 | self.assertEqual(res, ref) 255 | 256 | def test_with_number_and_clash(self): 257 | groups = (' -1.2', '!') 258 | ref = (-1.2, True) 259 | res = flips.parse_other_score(groups) 260 | self.assertEqual(res, ref) 261 | 262 | 263 | 264 | 265 | if __name__ == '__main__': 266 | unittest.main() 267 | 268 | -------------------------------------------------------------------------------- /tests/gui_tests.py: -------------------------------------------------------------------------------- 1 | '''GUI tests for PyMOLProbity plugin.''' 2 | 3 | import mock 4 | import unittest 5 | 6 | from .context import pymolprobity 7 | import pymolprobity.gui as gui 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /tests/kinemage_tests.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import unittest 3 | 4 | import mock 5 | 6 | from .context import pymolprobity 7 | import pymolprobity.kinemage as kin 8 | 9 | 10 | 11 | class KinemageTests(unittest.TestCase): 12 | def setUp(self): 13 | self.kw_types = ['viewid', 'group', 'subgroup', 'master', 14 | 'pointmaster', 'dotlist', 'vectorlist'] 15 | self.kin = kin.Kinemage() 16 | for i, kw in enumerate(self.kw_types): 17 | self.kin.keywords[i] = {'keyword': kw, 18 | 'data': '{} data'.format(kw)} 19 | # duplicate each item to test get_unique 20 | self.kin2 = copy.deepcopy(self.kin) 21 | for i in range(0, 7): 22 | self.kin2.keywords[i-7] = self.kin2.keywords[i] 23 | 24 | def tearDown(self): 25 | self.kin = None 26 | 27 | def test_get_all_keywords_of_type(self): 28 | for kw in self.kw_types: 29 | res = self.kin.get_all_keywords_of_type(kw) 30 | self.assertEqual(len(res), 1) 31 | self.assertEqual(res[0], '{} data'.format(kw)) 32 | 33 | def test_get_unique_keywords_of_type(self): 34 | for kw in self.kw_types: 35 | res = self.kin2.get_unique_keywords_of_type(kw) 36 | self.assertEqual(len(res), 1) 37 | self.assertEqual(res[0], '{} data'.format(kw)) 38 | 39 | @mock.patch('pymolprobity.kinemage.Kinemage.get_all_keywords_of_type') 40 | def test_viewids(self, mock_get_all): 41 | res = self.kin.viewids() 42 | ref = mock_get_all.return_value 43 | mock_get_all.assert_called_once_with('viewid') 44 | self.assertEqual(res, ref) 45 | 46 | @mock.patch('pymolprobity.kinemage.Kinemage.get_all_keywords_of_type') 47 | def test_groups(self, mock_get_all): 48 | res = self.kin.groups() 49 | ref = mock_get_all.return_value 50 | mock_get_all.assert_called_once_with('group') 51 | self.assertEqual(res, ref) 52 | 53 | @mock.patch('pymolprobity.kinemage.Kinemage.get_unique_keywords_of_type') 54 | def test_subgroups(self, mock_get_unique): 55 | res = self.kin.subgroups() 56 | ref = mock_get_unique.return_value 57 | mock_get_unique.assert_called_once_with('subgroup') 58 | self.assertEqual(res, ref) 59 | 60 | @mock.patch('pymolprobity.kinemage.Kinemage.get_unique_keywords_of_type') 61 | def test_masters(self, mock_get_unique): 62 | res = self.kin.masters() 63 | ref = mock_get_unique.return_value 64 | mock_get_unique.assert_called_once_with('master') 65 | self.assertEqual(res, ref) 66 | 67 | @mock.patch('pymolprobity.kinemage.Kinemage.get_unique_keywords_of_type') 68 | def test_pointmasters(self, mock_get_unique): 69 | res = self.kin.pointmasters() 70 | ref = mock_get_unique.return_value 71 | mock_get_unique.assert_called_once_with('pointmaster') 72 | self.assertEqual(res, ref) 73 | 74 | @mock.patch('pymolprobity.kinemage.Kinemage.get_all_keywords_of_type') 75 | def test_dotlists(self, mock_get_all): 76 | res = self.kin.dotlists() 77 | ref = mock_get_all.return_value 78 | mock_get_all.assert_called_once_with('dotlist') 79 | self.assertEqual(res, ref) 80 | 81 | @mock.patch('pymolprobity.kinemage.Kinemage.get_all_keywords_of_type') 82 | def test_vectorlists(self, mock_get_all): 83 | res = self.kin.vectorlists() 84 | ref = mock_get_all.return_value 85 | mock_get_all.assert_called_once_with('vectorlist') 86 | self.assertEqual(res, ref) 87 | 88 | 89 | class KinemageDrawMethodTests(unittest.TestCase): 90 | # TODO 91 | pass 92 | 93 | 94 | class ProcessKinemageTests(unittest.TestCase): 95 | def setUp(self): 96 | self.context = { 97 | 'kinemage': None, 98 | 'group': None, 99 | 'subgroup': None, 100 | 'animate': 0, 101 | } 102 | 103 | @mock.patch('pymolprobity.kinemage.points.process_dotlist') 104 | def test_calls_process_dotlist_with_dotlists(self, 105 | mock_proc_dotlist): 106 | inp = '@dotlist blah' 107 | mock_proc_dotlist.return_value = 'val' 108 | k = kin.process_kinemage(inp) 109 | mock_proc_dotlist.assert_called_once_with( 110 | ['dotlist blah'], self.context) 111 | ref = {'keyword': 'dotlist', 'data': 'val'} 112 | self.assertEqual(k.keywords[0], ref) 113 | 114 | @mock.patch('pymolprobity.kinemage.points.process_vectorlist') 115 | def test_calls_process_vectorlist_with_vectorlists(self, 116 | mock_proc_vectorlist): 117 | inp = '@vectorlist blah' 118 | mock_proc_vectorlist.return_value = 'val' 119 | k = kin.process_kinemage(inp) 120 | mock_proc_vectorlist.assert_called_once_with( 121 | ['vectorlist blah'], self.context) 122 | ref = {'keyword': 'vectorlist', 'data': 'val'} 123 | self.assertEqual(k.keywords[0], ref) 124 | 125 | @mock.patch('pymolprobity.kinemage.process_viewid') 126 | def test_calls_process_viewid_with_viewids(self, 127 | mock_proc_viewid): 128 | inp = '@viewid blah' 129 | mock_proc_viewid.return_value = 'val' 130 | k = kin.process_kinemage(inp) 131 | mock_proc_viewid.assert_called_once_with( 132 | ['viewid blah'], self.context) 133 | ref = {'keyword': 'viewid', 'data': 'val'} 134 | self.assertEqual(k.keywords[0], ref) 135 | 136 | @mock.patch('pymolprobity.kinemage.process_master') 137 | def test_calls_process_master_with_master(self, 138 | mock_proc_master): 139 | inp = '@master blah' 140 | mock_proc_master.return_value = 'val' 141 | k = kin.process_kinemage(inp) 142 | mock_proc_master.assert_called_once_with( 143 | ['master blah'], self.context) 144 | ref = {'keyword': 'master', 'data': 'val'} 145 | self.assertEqual(k.keywords[0], ref) 146 | 147 | @mock.patch('pymolprobity.kinemage.process_pointmaster') 148 | def test_calls_process_pointmaster_with_pointmaster(self, 149 | mock_proc_pm): 150 | inp = '@pointmaster blah' 151 | mock_proc_pm.return_value = 'val' 152 | k = kin.process_kinemage(inp) 153 | mock_proc_pm.assert_called_once_with( 154 | ['pointmaster blah'], self.context) 155 | ref = {'keyword': 'pointmaster', 'data': 'val'} 156 | self.assertEqual(k.keywords[0], ref) 157 | 158 | @mock.patch('pymolprobity.kinemage.process_kinemage_keyword') 159 | def test_calls_process_kinemage_keyword_with_kinemage(self, 160 | mock_proc_kin): 161 | inp = '@kinemage blah' 162 | mock_proc_kin.return_value = 'val' 163 | k = kin.process_kinemage(inp) 164 | mock_proc_kin.assert_called_once_with( 165 | ['kinemage blah'], self.context) 166 | ref = {'keyword': 'kinemage', 'data': 'val'} 167 | self.assertEqual(k.keywords[0], ref) 168 | 169 | @mock.patch('pymolprobity.kinemage.process_group') 170 | def test_calls_process_group_with_group(self, 171 | mock_proc): 172 | inp = '@group blah' 173 | mock_proc.return_value = 'val' 174 | k = kin.process_kinemage(inp) 175 | mock_proc.assert_called_once_with( 176 | ['group blah'], self.context) 177 | ref = {'keyword': 'group', 'data': 'val'} 178 | self.assertEqual(k.keywords[0], ref) 179 | 180 | @mock.patch('pymolprobity.kinemage.process_subgroup') 181 | def test_calls_process_subgroup_with_subgroup(self, 182 | mock_proc): 183 | inp = '@subgroup blah' 184 | context = {} 185 | mock_proc.return_value = 'data' 186 | k = kin.process_kinemage(inp) 187 | mock_proc.assert_called_once_with( 188 | ['subgroup blah'], self.context) 189 | ref = {'keyword': 'subgroup', 'data': 'data'} 190 | self.assertEqual(k.keywords[0], ref) 191 | 192 | @mock.patch('pymolprobity.kinemage.points.process_vectorlist') 193 | @mock.patch('pymolprobity.kinemage.points.process_dotlist') 194 | def test_with_skipped_keyword(self, mock_proc_dotlist, 195 | mock_proc_vectorlist): 196 | inp = '@text something' 197 | kin.process_kinemage(inp) 198 | mock_proc_dotlist.assert_not_called() 199 | mock_proc_vectorlist.assert_not_called() 200 | # TODO: test prints debug message 201 | 202 | @mock.patch('pymolprobity.kinemage.logger') 203 | def test_with_unknown_keyword(self, mock_logger): 204 | inp = '@not_a_keyword blah' 205 | k = kin.process_kinemage(inp) 206 | mock_logger.warning.assert_called_with('Unknown keyword: not_a_keyword') 207 | 208 | @mock.patch('pymolprobity.kinemage.process_master') 209 | @mock.patch('pymolprobity.kinemage.process_kinemage_keyword') 210 | def test_kinemage_keyword_updates_context(self, mock_proc_kin, 211 | mock_proc_master): 212 | inp = '@kinemage blah\n@master blah' 213 | context = self.context 214 | context['kinemage'] = mock_proc_kin.return_value 215 | kin.process_kinemage(inp) 216 | mock_proc_master.assert_called_once_with(['master blah'], context) 217 | 218 | @mock.patch('pymolprobity.kinemage.process_master') 219 | @mock.patch('pymolprobity.kinemage.process_group') 220 | def test_group_updates_context(self, mock_proc_group, 221 | mock_proc_master): 222 | inp = '@group blah\n@master blah' 223 | context = self.context 224 | context['group'] = mock_proc_group.return_value 225 | kin.process_kinemage(inp) 226 | mock_proc_master.assert_called_once_with(['master blah'], context) 227 | 228 | @mock.patch('pymolprobity.kinemage.process_master') 229 | @mock.patch('pymolprobity.kinemage.process_group') 230 | def test_none_group_updates_context(self, mock_proc_group, 231 | mock_proc_master): 232 | inp = '@group blah\n@group blah\n@master blah' 233 | mock_proc_group.side_effect = ( ['reduce', 'animate'], None ) 234 | 235 | context1 = copy.deepcopy(self.context) 236 | context1['group'] = ['reduce', 'animate'] 237 | context1['animate'] = 1 238 | 239 | context2 = copy.deepcopy(context1) 240 | context2['group'] = None # from 2nd group 241 | context2['animate'] = 0 242 | 243 | kin.process_kinemage(inp) 244 | 245 | mock_proc_group.assert_has_calls( 246 | [mock.call(['group blah'], self.context), 247 | mock.call(['group blah'], context1)]) 248 | mock_proc_master.assert_called_once_with(['master blah'], context2) 249 | 250 | @mock.patch('pymolprobity.kinemage.process_master') 251 | @mock.patch('pymolprobity.kinemage.process_group') 252 | def test_animate_group_updates_context(self, mock_proc_group, 253 | mock_proc_master): 254 | inp = '@group blah\n@master blah' 255 | mock_proc_group.return_value = ['reduce', 'animate'] 256 | context = self.context 257 | context['group'] = mock_proc_group.return_value 258 | context['animate'] = 1 259 | kin.process_kinemage(inp) 260 | mock_proc_master.assert_called_once_with(['master blah'], context) 261 | 262 | @mock.patch('pymolprobity.kinemage.process_master') 263 | @mock.patch('pymolprobity.kinemage.process_group') 264 | def test_non_animate_group_updates_context(self, mock_proc_group, 265 | mock_proc_master): 266 | inp = '@group blah\n@group blah\n@master blah' 267 | # first call sets animate = 1, second should reset it to 0 268 | mock_proc_group.side_effect = [['animate'], ['blah']] 269 | context = self.context 270 | context['group'] = ['blah'] 271 | context['animate'] = 0 272 | kin.process_kinemage(inp) 273 | mock_proc_master.assert_called_once_with(['master blah'], context) 274 | 275 | @mock.patch('pymolprobity.kinemage.process_master') 276 | @mock.patch('pymolprobity.kinemage.process_subgroup') 277 | def test_subgroup_updates_context(self, mock_proc_subgroup, 278 | mock_proc_master): 279 | inp = '@subgroup blah\n@master blah' 280 | context = self.context 281 | context['subgroup'] = mock_proc_subgroup.return_value 282 | kin.process_kinemage(inp) 283 | mock_proc_master.assert_called_once_with(['master blah'], context) 284 | 285 | 286 | 287 | 288 | 289 | 290 | @mock.patch('pymolprobity.kinemage.logger') 291 | class SingleLineKeywordCheckTests(unittest.TestCase): 292 | def test_with_single_line(self, mock_logger): 293 | inp = ['line 1'] 294 | kin.single_line_keyword_check(inp) 295 | self.assertFalse(mock_logger.warning.called) 296 | 297 | def test_with_multiple_lines(self, mock_logger): 298 | inp = ['line 1', 'line 2'] 299 | kin.single_line_keyword_check(inp) 300 | self.assertTrue(mock_logger.warning.called) 301 | 302 | def test_with_non_list_input(self, mock_logger): 303 | inp = 42 304 | with self.assertRaises(ValueError): 305 | kin.single_line_keyword_check(inp) 306 | 307 | 308 | class ProcessViewidTests(unittest.TestCase): 309 | def setUp(self): 310 | self.base_context = { 311 | 'kinemage': None, 312 | 'group': None, 313 | 'subgroup': None, 314 | 'animate': 0, 315 | } 316 | 317 | @mock.patch('pymolprobity.kinemage.single_line_keyword_check') 318 | def test_calls_single_line_keyword_check(self, mock_check): 319 | inp = ['blah'] 320 | res = kin.process_viewid(inp, self.base_context) 321 | self.assertTrue(mock_check.called) 322 | 323 | def test_with_first_viewid(self): 324 | inp = ['viewid { Q28 A}'] 325 | res = kin.process_viewid(inp, self.base_context) 326 | ref = { 327 | 'view_num': 1, 328 | 'flipped': False, 329 | 'resn': 'Q', 330 | 'resi': '28', 331 | 'alt': '', 332 | 'chain': 'A', 333 | } 334 | self.assertEqual(res, ref) 335 | 336 | def test_with_second_or_later_viewid(self): 337 | inp = ['2viewid { Q32 A}'] 338 | res = kin.process_viewid(inp, self.base_context) 339 | self.assertEqual(res['view_num'], 2) 340 | 341 | def test_with_3_digit_resnum(self): 342 | inp = ['19viewid { Q277 A}'] 343 | ref = { 344 | 'view_num': 19, 345 | 'flipped': False, 346 | 'resn': 'Q', 347 | 'resi': '277', 348 | 'alt': '', 349 | 'chain': 'A', 350 | } 351 | res = kin.process_viewid(inp, self.base_context) 352 | self.assertEqual(res, ref) 353 | 354 | def test_with_flipped_asterisk(self): 355 | inp = ['2viewid {*Q32 A}'] 356 | res = kin.process_viewid(inp, self.base_context) 357 | self.assertTrue(res['flipped']) 358 | 359 | def test_with_insertion_code(self): 360 | inp = ['2viewid { Q32A A}'] 361 | res = kin.process_viewid(inp, self.base_context) 362 | self.assertEqual(res['resi'], '32A') 363 | 364 | @mock.patch('pymolprobity.kinemage.logger') 365 | def test_with_bad_format(self, mock_logger): 366 | inp = ['viewid bad bad bad'] 367 | res = kin.process_viewid(inp, self.base_context) 368 | self.assertTrue(mock_logger.warning.called) 369 | self.assertIsNone(res) 370 | 371 | 372 | @mock.patch('pymolprobity.kinemage.single_line_keyword_check') 373 | class ProcessMasterTests(unittest.TestCase): 374 | def setUp(self): 375 | self.base_context = { 376 | 'kinemage': None, 377 | 'group': None, 378 | 'subgroup': None, 379 | 'animate': 0, 380 | } 381 | 382 | def test_calls_single_line_keyword_check(self, mock_check): 383 | inp = ['blah'] 384 | res = kin.process_master(inp, self.base_context) 385 | self.assertTrue(mock_check.called) 386 | 387 | def test_with_well_formed_master(self, mock_check): 388 | inp = ['master {something}'] 389 | res = kin.process_master(inp, self.base_context) 390 | self.assertEqual(res, 'something') 391 | 392 | 393 | @mock.patch('pymolprobity.kinemage.single_line_keyword_check') 394 | class ProcessPointmasterTests(unittest.TestCase): 395 | def setUp(self): 396 | self.base_context = { 397 | 'kinemage': None, 398 | 'group': None, 399 | 'subgroup': None, 400 | 'animate': 0, 401 | } 402 | 403 | def test_calls_single_line_keyword_check(self, mock_check): 404 | inp = ['blah'] 405 | res = kin.process_pointmaster(inp, self.base_context) 406 | self.assertTrue(mock_check.called) 407 | 408 | def test_with_well_formed_pointmaster(self, mock_check): 409 | inp = ["pointmaster 'a' {something}"] 410 | res = kin.process_pointmaster(inp, self.base_context) 411 | ref = {'code': 'a', 'label': 'something', 'enable': 1} 412 | self.assertEqual(res, ref) 413 | 414 | def test_with_on_statement(self, mock_check): 415 | inp = ["pointmaster 'a' {something} on"] 416 | res = kin.process_pointmaster(inp, self.base_context) 417 | ref = {'code': 'a', 'label': 'something', 'enable': 1} 418 | self.assertEqual(res, ref) 419 | 420 | def test_with_off_statement(self, mock_check): 421 | inp = ["pointmaster 'a' {something} off"] 422 | res = kin.process_pointmaster(inp, self.base_context) 423 | ref = {'code': 'a', 'label': 'something', 'enable': 0} 424 | self.assertEqual(res, ref) 425 | 426 | 427 | @mock.patch('pymolprobity.kinemage.single_line_keyword_check') 428 | class ProcessKinemageKeywordTests(unittest.TestCase): 429 | def setUp(self): 430 | self.base_context = { 431 | 'kinemage': None, 432 | 'group': None, 433 | 'subgroup': None, 434 | 'animate': 0, 435 | } 436 | 437 | def test_calls_single_line_keyword_check(self, mock_check): 438 | inp = ['blah'] 439 | res = kin.process_kinemage_keyword(inp, self.base_context) 440 | self.assertTrue(mock_check.called) 441 | 442 | def test_with_well_formed_kinemage(self, mock_check): 443 | inp = ['kinemage 1'] 444 | res = kin.process_kinemage_keyword(inp, self.base_context) 445 | self.assertEqual(res, '1') 446 | 447 | 448 | @mock.patch('pymolprobity.kinemage.single_line_keyword_check') 449 | class ProcessGroupTests(unittest.TestCase): 450 | def setUp(self): 451 | self.base_context = { 452 | 'kinemage': None, 453 | 'group': None, 454 | 'subgroup': None, 455 | 'animate': 0, 456 | } 457 | 458 | def test_calls_single_line_keyword_check(self, mock_check): 459 | inp = ['blah'] 460 | res = kin.process_group(inp, self.base_context) 461 | self.assertTrue(mock_check.called) 462 | 463 | def test_with_dominant_group(self, mock_check): 464 | inp = ['group {something} dominant'] 465 | res = kin.process_group(inp, self.base_context) 466 | ref = ['something', 'dominant'] 467 | self.assertEqual(res, ref) 468 | 469 | def test_with_animate_group(self, mock_check): 470 | inp = ['group {something} animate'] 471 | res = kin.process_group(inp, self.base_context) 472 | ref = ['something', 'animate'] 473 | self.assertEqual(res, ref) 474 | 475 | 476 | @mock.patch('pymolprobity.kinemage.single_line_keyword_check') 477 | class ProcessSubgroupTests(unittest.TestCase): 478 | def setUp(self): 479 | self.base_context = { 480 | 'kinemage': None, 481 | 'group': None, 482 | 'subgroup': None, 483 | 'animate': 0, 484 | } 485 | 486 | def test_calls_single_line_keyword_check(self, mock_check): 487 | inp = ['blah'] 488 | res = kin.process_subgroup(inp, self.base_context) 489 | self.assertTrue(mock_check.called) 490 | 491 | def test_with_dominant_subgroup(self, mock_check): 492 | inp = ['subgroup {something} dominant'] 493 | res = kin.process_subgroup(inp, self.base_context) 494 | ref = [None, 'something', 'dominant', None] 495 | self.assertEqual(res, ref) 496 | 497 | def test_with_dominant_before_name_subgroup(self, mock_check): 498 | inp = ['subgroup dominant {something}'] 499 | res = kin.process_subgroup(inp, self.base_context) 500 | ref = ['dominant', 'something', None, None] 501 | self.assertEqual(res, ref) 502 | 503 | def test_with_nobutton_dominant_subgroup(self, mock_check): 504 | inp = ['subgroup {something} nobutton dominant'] 505 | res = kin.process_subgroup(inp, self.base_context) 506 | ref = [None, 'something', 'nobutton', 'dominant'] 507 | self.assertEqual(res, ref) 508 | 509 | 510 | 511 | -------------------------------------------------------------------------------- /tests/main_tests.py: -------------------------------------------------------------------------------- 1 | '''Basic tests for PyMOLProbity plugin.''' 2 | 3 | import mock 4 | import unittest 5 | 6 | # PyMOL API setup 7 | import __main__ 8 | __main__.pymol_argv = ['pymol','-qkc'] 9 | import pymol 10 | from pymol import cmd 11 | 12 | from .context import pymolprobity 13 | import pymolprobity.main as mp 14 | import pymolprobity.flips as flp 15 | 16 | 17 | ############################################################################### 18 | # 19 | # GENERAL FUNCTION TESTS 20 | # 21 | ############################################################################### 22 | 23 | class GetObjectTests(unittest.TestCase): 24 | def setUp(self): 25 | '''Create an MPObject and register it with the module-level objects dict.''' 26 | self.obj = mp.MPObject('test') 27 | mp.objects['test'] = self.obj 28 | 29 | def tearDown(self): 30 | del(self.obj) 31 | mp.objects.clear() 32 | 33 | def test_string_existing_object(self): 34 | '''Retrieve the MPObject by its string name.''' 35 | o = mp.get_object('test') 36 | assert o == self.obj 37 | assert o.name == 'test' 38 | 39 | def test_string_nonexistent_object(self): 40 | '''A string not in the objects dict returns None.''' 41 | with self.assertRaises(AttributeError): 42 | mp.get_objects('blah') 43 | 44 | def test_mpobject_in_objects_dict(self): 45 | '''Retrieve the MPObject itself.''' 46 | assert mp.get_object(self.obj) == self.obj 47 | 48 | def test_mpobject_not_in_objects_dict(self): 49 | '''An MPObject not in the module level objects dict also returns None.''' 50 | other_obj = mp.MPObject('other') 51 | with self.assertRaises(ValueError): 52 | mp.get_object(other_obj) 53 | 54 | def test_other_input_type(self): 55 | '''Non-string and non-MPObject input returns None.''' 56 | with self.assertRaises(ValueError): 57 | mp.get_object(1) 58 | 59 | 60 | @mock.patch('pymolprobity.main.MPObject') 61 | @mock.patch('pymolprobity.main.get_object') 62 | class GetOrCreateObjectTests(unittest.TestCase): 63 | def setUp(self): 64 | # should probably mock out main.objects dict instead 65 | mp.objects['test'] = mp.MPObject('test') 66 | 67 | def tearDown(self): 68 | mp.objects.clear() 69 | 70 | def test_get_existing_object(self, mock_get, mock_MPObj): 71 | res = mp.get_or_create_object('test') 72 | ref = mock_get.return_value 73 | self.assertEqual(res, ref) 74 | mock_MPObj.assert_not_called() 75 | 76 | def test_with_nonexisting_object(self, mock_get, mock_MPObj): 77 | res = mp.get_or_create_object('blah') 78 | ref = mock_get.return_value 79 | self.assertEqual(res, ref) 80 | mock_get.assert_called_once_with('blah') 81 | 82 | def test_non_string_input(self, mock_get, mock_MPObj): 83 | with self.assertRaises(TypeError): 84 | mp.get_or_create_object(2) 85 | 86 | 87 | class RunCommandTests(unittest.TestCase): 88 | def test_with_args_only(self): 89 | output = mp.run_command(['printf', 'blah']) 90 | assert output == "blah" 91 | 92 | def test_with_args_and_input_str(self): 93 | output = mp.run_command(['cat', '-'], 'blah') 94 | assert output == "blah" 95 | 96 | @mock.patch('pymolprobity.main.logger') 97 | def test_nonzero_return_code(self, mock_logger): 98 | # TODO mock subprocess 99 | mp.run_command(['false']) 100 | self.assertTrue(mock_logger.warning.called) 101 | 102 | @mock.patch('pymolprobity.main.logger') 103 | def test_with_missing_executable(self, mock_logger): 104 | mp.run_command(['not_a_command']) 105 | self.assertTrue(mock_logger.error.called) 106 | 107 | 108 | @mock.patch('pymolprobity.main.cmd.save') 109 | @mock.patch('pymolprobity.main.tempfile') 110 | class SaveToTempfileTests(unittest.TestCase): 111 | def test_basic(self, mock_tempfile, mock_save): 112 | inp = 'mystr' 113 | fn = 'somefile.pdb' 114 | tf = mock_tempfile.NamedTemporaryFile.return_value 115 | tf.name = 'somefile.pdb' 116 | res = mp.save_to_tempfile(inp) 117 | tf.write.assert_called_once_with(inp) 118 | self.assertEqual(res, fn) 119 | 120 | 121 | 122 | ############################################################################### 123 | # 124 | # MPOBJECT TESTS 125 | # 126 | ############################################################################### 127 | 128 | class MPObjectTests(unittest.TestCase): 129 | def setUp(self): 130 | self.o = mp.MPObject('test') 131 | 132 | @mock.patch('pymolprobity.main.cmd') 133 | def test_get_pdbstr_with_defaults(self, mock_cmd): 134 | mock_cmd.get_names.return_value = ['mp_test.test_reduce'] 135 | mock_cmd.get_pdbstr.return_value = 'pdbstr' 136 | res = self.o.get_pdbstr() 137 | self.assertEqual(res, 'pdbstr') 138 | mock_cmd.get_pdbstr.assert_called_once_with('mp_test.test_reduce') 139 | 140 | @mock.patch('pymolprobity.main.cmd') 141 | def test_get_pdbstr_with_explicit_reduce(self, mock_cmd): 142 | mock_cmd.get_names.return_value = ['mp_test.test_reduce'] 143 | mock_cmd.get_pdbstr.return_value = 'pdbstr' 144 | res = self.o.get_pdbstr('reduce') 145 | self.assertEqual(res, 'pdbstr') 146 | mock_cmd.get_pdbstr.assert_called_once_with('mp_test.test_reduce') 147 | 148 | @mock.patch('pymolprobity.main.cmd') 149 | def test_get_pdbstr_with_missing_object(self, mock_cmd): 150 | mock_cmd.get_names.return_value = ['not here'] 151 | res = self.o.get_pdbstr() 152 | mock_cmd.get_pdbstr.assert_not_called() 153 | self.assertEqual(res, None) 154 | 155 | def test_get_kin_cgo_group_with_valid_kin(self): 156 | res = self.o.get_kin_cgo_group('flipkinNQ') 157 | ref = 'mp_test.flipkinNQ' 158 | self.assertEqual(res, ref) 159 | 160 | @mock.patch('pymolprobity.main.logger') 161 | def test_get_kin_cgo_group_with_invalid_kin(self, mock_logger): 162 | res = self.o.get_kin_cgo_group('reduce') 163 | ref = None 164 | self.assertEqual(res, ref) 165 | self.assertTrue(mock_logger.warning.called) 166 | 167 | # @mock.patch('pymolprobity.main.cmd') 168 | # def test_get_kin_group_cgo_group_with_valid_kin(self, mock_cmd): 169 | # ref = 'mp_test.flipkinNQ.reduce' 170 | # mock_cmd.get_names.return_value = [ref] 171 | # res = self.o.get_kin_group_cgo_group('flipkinNQ', 'reduce') 172 | # self.assertEqual(res, ref) 173 | 174 | # @mock.patch('pymolprobity.main.logger') 175 | # @mock.patch('pymolprobity.main.cmd') 176 | # def test_get_kin_group_cgo_group_with_invalid_kin(self, mock_cmd, mock_logger): 177 | # mock_cmd.get_names.return_value = [] 178 | # res = self.o.get_kin_group_cgo_group('flipkinNQ', 'blah') 179 | # ref = None 180 | # self.assertEqual(res, ref) 181 | # self.assertTrue(mock_logger.warning.called) 182 | 183 | @mock.patch('pymolprobity.main.MPObject.solo_pdb') 184 | @mock.patch('pymolprobity.main.MPObject.solo_kin') 185 | @mock.patch('pymolprobity.main.MPObject.get_kin_cgo_group') 186 | @mock.patch('pymolprobity.main.kinemage.Kinemage', autospec=True) 187 | @mock.patch('pymolprobity.main.MPObject.disable_kin') 188 | def test_draw_with_no_args(self, mock_disable_kin, mock_kin, mock_get_grp, 189 | mock_solo_kin, mock_solo_pdb): 190 | k = 'probe' 191 | self.o.kin[k] = mock_kin.return_value 192 | self.o.draw(k) 193 | mock_disable_kin.assert_called_once_with() 194 | mock_get_grp.assert_called_once_with(k) 195 | grp = mock_get_grp.return_value 196 | self.o.kin[k].draw.assert_called_with(grp, dot_mode=0) 197 | mock_solo_pdb.assert_called_once_with(k) 198 | mock_solo_kin.assert_called_once_with(k) 199 | 200 | @mock.patch('pymolprobity.main.MPObject.animate') 201 | @mock.patch('pymolprobity.main.MPObject.solo_pdb') 202 | @mock.patch('pymolprobity.main.MPObject.solo_kin') 203 | @mock.patch('pymolprobity.main.MPObject.get_kin_cgo_group') 204 | @mock.patch('pymolprobity.main.kinemage.Kinemage', autospec=True) 205 | @mock.patch('pymolprobity.main.MPObject.disable_kin') 206 | def test_draw_with_flipkin_arg(self, mock_disable_kin, mock_kin, mock_get_grp, 207 | mock_solo_kin, mock_solo_pdb, mock_animate): 208 | k = 'flipkinNQ' 209 | self.o.kin[k] = mock_kin.return_value 210 | self.o.draw(k) 211 | grp = mock_get_grp.return_value 212 | self.assertTrue(self.o.kin[k].draw.called) 213 | self.assertTrue(mock_animate.called) 214 | 215 | @mock.patch('pymolprobity.main.cmd') 216 | def test_disable_pdb_with_default_all_input(self, mock_cmd): 217 | for p in self.o.pdb.keys(): 218 | self.o.pdb[p] = '{}_pdb_obj'.format(p) 219 | self.o.disable_pdb() # default is pdb='all' 220 | mock_cmd.disable.assert_has_calls( 221 | [mock.call('reduce_pdb_obj'), 222 | mock.call('flipkinNQ_pdb_obj'), 223 | mock.call('flipkinH_pdb_obj'), 224 | mock.call('probe_pdb_obj')], any_order=True) 225 | 226 | @mock.patch('pymolprobity.main.cmd') 227 | def test_disable_pdb_with_valid_single_input(self, mock_cmd): 228 | inp = 'reduce' 229 | self.o.pdb[inp] = 'pdb_obj' 230 | self.o.disable_pdb(inp) 231 | mock_cmd.disable.assert_called_once_with('pdb_obj') 232 | 233 | @mock.patch('pymolprobity.main.cmd') 234 | def test_disable_pdb_with_invalid_single_input(self, mock_cmd): 235 | inp = 'blah' 236 | with self.assertRaises(KeyError): 237 | self.o.disable_pdb(inp) 238 | 239 | @mock.patch('pymolprobity.main.cmd') 240 | def test_enable_pdb(self, mock_cmd): 241 | inp = 'reduce' 242 | self.o.pdb[inp] = 'obj' 243 | self.o.enable_pdb(inp) 244 | mock_cmd.enable.assert_called_once_with('obj') 245 | 246 | @mock.patch('pymolprobity.main.MPObject.enable_pdb') 247 | @mock.patch('pymolprobity.main.MPObject.disable_pdb') 248 | def test_solo_pdb(self, mock_disable, mock_enable): 249 | inp = 'pdb' 250 | self.o.solo_pdb(inp) 251 | mock_disable.assert_called_once_with('all') 252 | mock_enable.assert_called_once_with(inp) 253 | 254 | @mock.patch('pymolprobity.main.cmd') 255 | @mock.patch('pymolprobity.main.MPObject.get_kin_cgo_group', autospec=True) 256 | def test_disable_kin_with_default(self, mock_get_grp, mock_cmd): 257 | grp = mock_get_grp.return_value 258 | self.o.disable_kin() 259 | mock_get_grp.assert_has_calls( 260 | [mock.call(self.o, 'flipkinNQ'), 261 | mock.call(self.o, 'flipkinH'), 262 | mock.call(self.o, 'probe')], any_order=True) 263 | mock_cmd.disable.assert_has_calls( 264 | [mock.call(grp), 265 | mock.call(grp), 266 | mock.call(grp)]) 267 | 268 | @mock.patch('pymolprobity.main.cmd') 269 | @mock.patch('pymolprobity.main.MPObject.get_kin_cgo_group', autospec=True) 270 | def test_disable_kin_with_kin_key(self, mock_get_grp, mock_cmd): 271 | inp = 'flipkinNQ' 272 | grp = mock_get_grp.return_value 273 | self.o.disable_kin(inp) 274 | mock_get_grp.assert_called_once_with(self.o, inp) 275 | mock_cmd.disable.assert_called_once_with(grp) 276 | 277 | @mock.patch('pymolprobity.main.cmd') 278 | @mock.patch('pymolprobity.main.MPObject.get_kin_cgo_group', autospec=True) 279 | def test_enable_kin_with_kin_key(self, mock_get_grp, mock_cmd): 280 | inp = 'flipkinNQ' 281 | grp = mock_get_grp.return_value 282 | self.o.enable_kin(inp) 283 | mock_get_grp.assert_called_once_with(self.o, inp) 284 | mock_cmd.enable.assert_called_once_with(grp) 285 | 286 | @mock.patch('pymolprobity.main.MPObject.enable_kin', autospec=True) 287 | @mock.patch('pymolprobity.main.MPObject.disable_kin', autospec=True) 288 | def test_solo_kin_with_kin_key(self, mock_disable, mock_enable): 289 | inp = 'flipkinNQ' 290 | self.o.solo_kin(inp) 291 | mock_disable.assert_called_once_with(self.o, 'all') 292 | mock_enable.assert_called_once_with(self.o, inp) 293 | 294 | @mock.patch('pymolprobity.main.cmd') 295 | def test_enable_flipkin_group_with_default_reduce(self, mock_cmd): 296 | self.o.enable_flipkin_group() 297 | ref = 'mp_test.*.reduce' 298 | mock_cmd.enable.assert_called_once_with(ref) 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | # @mock.patch('pymolprobity.main.MPObject.get_vectors_cgo') 313 | # @mock.patch('pymolprobity.main.MPObject.get_dots_cgo') 314 | # @mock.patch('pymolprobity.main.cmd') 315 | # def test_draw_workflow_with_defaults(self, mock_cmd, mock_dots_cgo, 316 | # mock_vectors_cgo): 317 | # mock_cmd.get_view.return_value = 'view' 318 | # dots_cgo = mock_dots_cgo.return_value 319 | # vectors_cgo = mock_vectors_cgo.return_value 320 | 321 | # self.o.draw() 322 | 323 | # mock_cmd.get_view.assert_called_once_with() 324 | # mock_cmd.set.assert_has_calls( 325 | # [mock.call('cgo_use_shader', 0), 326 | # mock.call('cgo_sphere_quality', 0)]) 327 | # mock_dots_cgo.assert_called_once_with(dot_mode=0) 328 | # mock_vectors_cgo.assert_called_once_with() 329 | # mock_cmd.load_cgo.assert_has_calls( 330 | # [mock.call(dots_cgo, 'test_dots' ), 331 | # mock.call(vectors_cgo, 'test_clashes')]) 332 | # mock_cmd.group.assert_has_calls( 333 | # [mock.call('mp_test', 'test_dots'), 334 | # mock.call('mp_test', 'test_clashes')]) 335 | # mock_cmd.set_view.assert_called_once_with('view') 336 | 337 | # @mock.patch('pymolprobity.main.MPObject.get_vectors_cgo') 338 | # @mock.patch('pymolprobity.main.MPObject.get_dots_cgo') 339 | # @mock.patch('pymolprobity.main.cmd') 340 | # def test_draw_with_dot_mode_1(self, mock_cmd, mock_dots_cgo, 341 | # mock_vectors_cgo): 342 | # dots_cgo = mock_dots_cgo.return_value 343 | # self.o.draw(dot_mode=1) 344 | # mock_dots_cgo.assert_called_once_with(dot_mode=1) 345 | 346 | # @mock.patch('pymolprobity.main.points.Dot') 347 | # def test_get_dots_cgo(self, mock_dot): 348 | # o = mp.MPObject('test') 349 | # o.dots = [mock_dot, mock_dot, mock_dot] 350 | # mock_dot.get_cgo.return_value = ['dots_cgo'] 351 | # res = o.get_dots_cgo() 352 | # ref = ['dots_cgo', 'dots_cgo', 'dots_cgo'] 353 | # self.assertEqual(res, ref) 354 | 355 | # @mock.patch('pymolprobity.main.points.Vector') 356 | # def test_get_vectors_cgo(self, mock_vec): 357 | # o = mp.MPObject('test') 358 | # o.vectors = [mock_vec, mock_vec, mock_vec] 359 | # mock_vec.get_cgo.return_value = ['vectors_cgo'] 360 | # res = o.get_vectors_cgo() 361 | # ref = ['vectors_cgo', 'vectors_cgo', 'vectors_cgo'] 362 | # self.assertEqual(res, ref) 363 | 364 | # def test_get_flip_matching_atom(self): 365 | # f0 = flp.Flip() 366 | # f0.chain, f0.resi, f0.resn, f0.alt = ['A', '1', 'ASN', ''] 367 | # f1 = flp.Flip() 368 | # f1.chain, f1.resi, f1.resn, f1.alt = ['A', '100', 'GLN', ''] 369 | 370 | # o = mp.MPObject('test') 371 | # o.flips = [f0, f1] 372 | 373 | # atom = {'chain': 'A', 'resi': '100', 'resn': 'GLN', 'alt': ''} 374 | # res = o.get_flip_matching_atom(atom) 375 | # self.assertEqual(res, f1) 376 | 377 | # @mock.patch('pymolprobity.main.logger') 378 | # def test_get_flip_matching_atom_with_no_match(self, mock_logger): 379 | # f0 = flp.Flip() 380 | # f0.chain, f0.resi, f0.resn, f0.alt = ['A', '1', 'ASN', ''] 381 | # f1 = flp.Flip() 382 | # f1.chain, f1.resi, f1.resn, f1.alt = ['A', '100', 'GLN', ''] 383 | 384 | # o = mp.MPObject('test') 385 | # o.flips = [f0, f1] 386 | 387 | # atom = {'chain': 'A', 'resi': '2', 'resn': 'GLN', 'alt': ''} 388 | # res = o.get_flip_matching_atom(atom) 389 | # self.assertEqual(res, None) 390 | 391 | # @mock.patch('pymolprobity.main.logger') 392 | # def test_get_flip_matching_atom_with_multiple_matches(self, mock_logger): 393 | # f0 = flp.Flip() 394 | # f0.chain, f0.resi, f0.resn, f0.alt = ['A', '100', 'ASN', ''] 395 | 396 | # o = mp.MPObject('test') 397 | # o.flips = [f0, f0] 398 | 399 | # atom = {'chain': 'A', 'resi': '100', 'resn': 'ASN', 'alt': ''} 400 | # res = o.get_flip_matching_atom(atom) 401 | # self.assertEqual(res, None) 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | ############################################################################### 410 | # 411 | # REDUCE TESTS 412 | # 413 | ############################################################################### 414 | 415 | class GetReduceArgsTests(unittest.TestCase): 416 | def test_default_values(self): 417 | '''Default arguments''' 418 | ref = ['reduce', '-Quiet', '-FLIP', '-'] 419 | args = mp.get_reduce_args() 420 | assert args == ref 421 | 422 | def test_no_h(self): 423 | ref = ['reduce', '-Quiet', '-FLIP', '-Trim', '-'] 424 | args = mp.get_reduce_args(h=0) 425 | assert args == ref 426 | 427 | def test_no_flips(self): 428 | ref = ['reduce', '-Quiet', '-NOFLIP', '-'] 429 | args = mp.get_reduce_args(flip=0) 430 | assert args == ref 431 | 432 | def test_no_quiet(self): 433 | '''Without -Quiet flag.''' 434 | ref = ['reduce', '-FLIP', '-'] 435 | args = mp.get_reduce_args(quiet=0) 436 | assert args == ref 437 | 438 | def test_with_addflags_string(self): 439 | ref = ['reduce', '-Quiet', '-FLIP', 'added', '-'] 440 | args = mp.get_reduce_args(addflags='added') 441 | assert args == ref 442 | 443 | def test_with_h_nonint(self): 444 | with self.assertRaises(TypeError): 445 | mp.get_reduce_args(h='str') 446 | 447 | def test_with_h_nonzero_int(self): 448 | ref = ['reduce', '-Quiet', '-FLIP', '-'] 449 | args = mp.get_reduce_args(h=2) 450 | 451 | def test_with_flip_nonint(self): 452 | with self.assertRaises(TypeError): 453 | mp.get_reduce_args(flip='str') 454 | 455 | def test_with_flip_nonzero_int(self): 456 | ref = ['reduce', '-Quiet', '-FLIP', '-'] 457 | args = mp.get_reduce_args(flip=2) 458 | 459 | def test_with_quiet_nonint(self): 460 | with self.assertRaises(TypeError): 461 | mp.get_reduce_args(quiet='str') 462 | 463 | def test_with_quiet_nonzero_int(self): 464 | ref = ['reduce', '-Quiet', '-FLIP', '-'] 465 | args = mp.get_reduce_args(quiet=2) 466 | 467 | def test_with_addflags_nonstring(self): 468 | with self.assertRaises(TypeError): 469 | mp.get_reduce_args(addflags=1) 470 | 471 | 472 | @mock.patch('pymolprobity.main.run_command') 473 | @mock.patch('pymolprobity.main.get_reduce_args') 474 | class GenerateReduceResultTests(unittest.TestCase): 475 | 476 | def test_with_flip_type_0(self, mock_args, mock_run): 477 | mp.generate_reduce_output('pdbstr', 0) 478 | mock_args.assert_called_once_with(flip=0) 479 | 480 | def test_with_flip_type_1(self, mock_args, mock_run): 481 | mp.generate_reduce_output('pdbstr', 1) 482 | mock_args.assert_called_once_with() 483 | 484 | def test_with_flip_type_2(self, mock_args, mock_run): 485 | mp.generate_reduce_output('pdbstr', 2) 486 | mock_args.assert_called_once_with(addflags='-NOBUILD0') 487 | 488 | def test_run_reduce_call(self, mock_args, mock_run): 489 | mock_args.return_value = ['args'] 490 | mp.generate_reduce_output('pdbstr', 1) 491 | mock_run.assert_called_once_with(['args'], 'pdbstr') 492 | 493 | 494 | @mock.patch('pymolprobity.main.logger') 495 | @mock.patch('pymolprobity.main.process_reduce_output') 496 | @mock.patch('pymolprobity.main.generate_reduce_output') 497 | @mock.patch('pymolprobity.main.cmd') 498 | class ReduceObjectTests(unittest.TestCase): 499 | def test_default_with_flips(self, mock_cmd, mock_gen, mock_proc, 500 | mock_logger): 501 | mock_cmd.get_pdbstr.return_value = 'pdbstr' 502 | mock_gen.return_value = 'reduced_pdbstr' 503 | mock_proc.return_value = ['flip1', 'flip2'] 504 | mp.reduce_object('obj') 505 | mock_cmd.get_pdbstr.assert_called_once_with('obj') 506 | mock_gen.assert_called_once_with('pdbstr', flip_type=1) 507 | mock_proc.assert_called_once_with('reduced_pdbstr') 508 | 509 | def test_without_flips(self, mock_cmd, mock_gen, mock_proc, mock_logger): 510 | mock_cmd.get_pdbstr.return_value = 'pdbstr' 511 | mp.reduce_object('obj', flip=0) 512 | mock_cmd.get_pdbstr.assert_called_once_with('obj') 513 | mock_gen.assert_called_once_with('pdbstr', flip_type=0) 514 | 515 | def test_no_reduce_output(self, mock_cmd, mock_gen, mock_proc, 516 | mock_logger): 517 | mock_gen.return_value = None 518 | mp.reduce_object('obj') 519 | self.assertTrue(mock_logger.error.called) 520 | self.assertFalse(mock_proc.called) 521 | 522 | 523 | @mock.patch('pymolprobity.flips.parse_flips') 524 | class ProcessReduceOutputTests(unittest.TestCase): 525 | def test_with_normal_string_input(self, mock_parse_flips): 526 | val = 'USER MOD blah blah\nHEADER blah\nUSER MOD blah blah blah' 527 | user_mod_ref = ['blah blah', 'blah blah blah'] 528 | mp.process_reduce_output(val) 529 | mock_parse_flips.assert_called_with(user_mod_ref) 530 | 531 | def test_with_nonstring_input(self, mock_parse): 532 | '''Raise a TypeError with non-string input.''' 533 | bad_val = 12345 534 | with self.assertRaises(TypeError): 535 | mp.process_reduce_output(bad_val) 536 | 537 | def test_with_malformed_string_input(self, mock_parse): 538 | '''Raise a ValueError if the input string doesn't look like normal 539 | Reduce output.''' 540 | bad_val = "This doesn't start with USER MOD." 541 | with self.assertRaises(ValueError): 542 | mp.process_reduce_output(bad_val) 543 | 544 | def test_calls_parse_flips(self, mock_parse): 545 | val = "USER MOD blah blah\nHEADER blah\nUSER MOD blah blah blah" 546 | user_mod_ref = ["blah blah", "blah blah blah"] 547 | mp.process_reduce_output(val) 548 | mock_parse.assert_called_once_with(user_mod_ref) 549 | 550 | 551 | ############################################################################### 552 | # 553 | # FLIPKIN TESTS 554 | # 555 | ############################################################################### 556 | 557 | @mock.patch('pymolprobity.main.logger') 558 | @mock.patch('pymolprobity.main.os.unlink') 559 | @mock.patch('pymolprobity.main.process_flipkin_output') 560 | @mock.patch('pymolprobity.main.generate_flipkin_output') 561 | @mock.patch('pymolprobity.main.save_to_tempfile') 562 | @mock.patch('pymolprobity.main.get_object') 563 | class FlipkinObjectTests(unittest.TestCase): 564 | def test_workflow_for_nq_defaults(self, mock_get_obj, mock_tf, mock_gen, 565 | mock_proc, mock_unlink, mock_logger): #, mock_apply): 566 | o = mock_get_obj.return_value 567 | o.reduce_output = 'pdbstr' 568 | mock_tf.return_value = 'tempfile.pdb' 569 | mock_gen.side_effect = ['flipkin_nq', 'flipkin_h'] 570 | mock_proc.side_effect = ['proc_nq', 'proc_h'] 571 | 572 | mp.flipkin_object('test') 573 | 574 | mock_get_obj.assert_called_once_with('test') 575 | mock_tf.assert_called_once_with('pdbstr') 576 | mock_gen.assert_has_calls( 577 | [mock.call('tempfile.pdb'), 578 | mock.call('tempfile.pdb', his=True)]) 579 | mock_proc.assert_has_calls( 580 | [mock.call('flipkin_nq'), 581 | mock.call('flipkin_h')]) 582 | mock_unlink.assert_called_once_with('tempfile.pdb') 583 | # mock_apply.assert_has_calls( 584 | # [mock.call(o, 'proc_nq'), 585 | # mock.call(o, 'proc_h')]) 586 | 587 | def test_no_flipkinNQ_output(self, mock_get_obj, mock_tf, mock_gen, 588 | mock_proc, mock_unlink, mock_logger): 589 | mock_gen.side_effect = (None, 'flipkin_h') 590 | mp.flipkin_object('test') 591 | self.assertTrue(mock_logger.error.called) 592 | self.assertFalse(mock_proc.called) 593 | 594 | def test_no_flipkinH_output(self, mock_get_obj, mock_tf, mock_gen, 595 | mock_proc, mock_unlink, mock_logger): 596 | mock_gen.side_effect = ('flipkin_nq', None) 597 | mp.flipkin_object('test') 598 | self.assertTrue(mock_logger.error.called) 599 | self.assertFalse(mock_proc.called) 600 | 601 | 602 | 603 | 604 | @mock.patch('pymolprobity.main.run_command') 605 | class GenerateFlipkinOutputTests(unittest.TestCase): 606 | def test_workflow_with_default_nq(self, mock_run): 607 | mock_run.return_value = 'output' 608 | res = mp.generate_flipkin_output('filename') 609 | mock_run.assert_called_once_with(['flipkin', 'filename']) 610 | ref = 'output' 611 | self.assertEqual(res, ref) 612 | 613 | def test_workflow_with_his(self, mock_run): 614 | mp.generate_flipkin_output('filename', his=True) 615 | mock_run.assert_called_once_with(['flipkin', '-h', 'filename']) 616 | 617 | 618 | @mock.patch('pymolprobity.main.kinemage.process_kinemage') 619 | class ProcessFlipkinOutputTests(unittest.TestCase): 620 | def test_workflow(self, mock_proc_kin): 621 | kin = mock_proc_kin.return_value 622 | res = mp.process_flipkin_output('kinstr') 623 | mock_proc_kin.assert_called_once_with('kinstr') 624 | 625 | 626 | # @mock.patch('pymolprobity.points.Vector') 627 | # @mock.patch('pymolprobity.flips.Flip') 628 | # @mock.patch('pymolprobity.main.kinemage.Kinemage') 629 | # @mock.patch('pymolprobity.main.MPObject') 630 | # class ApplyFlipkinToMPObject(unittest.TestCase): 631 | # def test_with_animated_coordinate_vectorlist_input(self, mock_Obj, mock_Kin, 632 | # mock_Flip, mock_Vector): 633 | # mpobj = mock_Obj('myobj') 634 | # f = mock_Flip() 635 | # f.flipped = False 636 | # mpobj.get_flip_matching_atom.return_value = f 637 | # kin = mock_Kin() 638 | # vec = mock_Vector() 639 | # vec.atom = [{'name': 'ca', 'coords': [1, 2, 3], 'resn': 'GLN'}, 640 | # {'name': 'cb', 'coords': [2, 3, 4], 'resn': 'ASN'}] 641 | # kin.keywords = { 642 | # '1': {'keyword': 'group', 'data': ['reduce', 'animate']}, 643 | # '2': {'keyword': 'vectorlist', 'data': [vec]}, 644 | # } 645 | # mp.apply_flipkin_to_mpobject(mpobj, kin) 646 | # mpobj.get_flip_matching_atom.assert_called_once_with(vec.atom[1]) 647 | 648 | # def test_with_coordinates_vectorlist_input(self, mock_Obj, mock_Kin, 649 | # mock_Flip, mock_Vector): 650 | # '''should ignore vectorlist within main coordinates group''' 651 | # mpobj = mock_Obj('myobj') 652 | # kin = mock_Kin() 653 | # vec = mock_Vector() 654 | # kin.keywords = { 655 | # '1': {'keyword': 'group', 'data': ['objname', 'dominant']}, 656 | # '2': {'keyword': 'vectorlist', 'data': [vec]}, 657 | # } 658 | # mp.apply_flipkin_to_mpobject(mpobj, kin) 659 | # self.assertFalse(mpobj.get_flip_matching_atom.called) 660 | 661 | # @mock.patch('pymolprobity.main.logger') 662 | # def test_with_kinemage_keyword_input(self, mock_logger, mock_Obj, 663 | # mock_Kin, mock_Flip, mock_Vector): 664 | # mpobj = mock_Obj('myobj') 665 | # kin = mock_Kin() 666 | # vec = mock_Vector() 667 | # kin.keywords = { 668 | # '1': {'keyword': 'kinemage', 'data': '1'}} 669 | # mp.apply_flipkin_to_mpobject(mpobj, kin) 670 | # self.assertTrue(mock_logger.debug.called) 671 | 672 | # @mock.patch('pymolprobity.main.logger') 673 | # def test_with_subgroup_input(self, mock_logger, mock_Obj, 674 | # mock_Kin, mock_Flip, mock_Vector): 675 | # mpobj = mock_Obj('myobj') 676 | # kin = mock_Kin() 677 | # vec = mock_Vector() 678 | # kin.keywords = { 679 | # '1': {'keyword': 'subgroup', 'data': ['foo', 'bar']}} 680 | # mp.apply_flipkin_to_mpobject(mpobj, kin) 681 | # mock_logger.debug.assert_called_with('entering subgroup: bar') 682 | 683 | 684 | 685 | 686 | 687 | 688 | ############################################################################### 689 | # 690 | # PROBE TESTS 691 | # 692 | ############################################################################### 693 | 694 | class GetProbeArgsTests(unittest.TestCase): 695 | def test(self): 696 | fn = 'temp.pdb' 697 | ref = ['probe', '-Quiet', '-Self', 'ALL', 'temp.pdb'] 698 | res = mp.get_probe_args(fn) 699 | self.assertEqual(res, ref) 700 | 701 | 702 | @mock.patch('pymolprobity.main.run_command') 703 | @mock.patch('pymolprobity.main.get_probe_args') 704 | @mock.patch('pymolprobity.main.os') 705 | @mock.patch('pymolprobity.main.save_to_tempfile') 706 | class GenerateProbeOutputTests(unittest.TestCase): 707 | def test_workflow(self, mock_tf, mock_os, mock_args, 708 | mock_run): 709 | fn = 'somefile.pdb' 710 | pdb = 'pdbstr' 711 | mock_tf.return_value = fn 712 | mock_os.path.isfile.side_effect = [True, False] 713 | mock_args.return_value = [1,2,3] 714 | mock_run.return_value = 'output' 715 | 716 | res = mp.generate_probe_output(pdb) 717 | self.assertEqual(res, 'output') 718 | 719 | mock_tf.assert_called_once_with(pdb) 720 | self.assertTrue(mock_args.called) 721 | mock_run.assert_called_once_with([1, 2, 3]) 722 | mock_os.unlink.assert_called_once_with(fn) 723 | call = mock.call(fn) 724 | mock_os.path.isfile.assert_has_calls([call, call]) 725 | 726 | 727 | @mock.patch('pymolprobity.main.logger') 728 | @mock.patch('pymolprobity.main.cmd') 729 | @mock.patch('pymolprobity.main.process_probe_output') 730 | @mock.patch('pymolprobity.main.generate_probe_output') 731 | @mock.patch('pymolprobity.main.get_object') 732 | class ProbeObjectTests(unittest.TestCase): 733 | def test_workflow(self, mock_get_obj, mock_gen, mock_proc, 734 | mock_cmd, mock_logger): 735 | obj = 'obj' 736 | o = mock_get_obj.return_value 737 | # o.pdb['userflips'] == 'userflips_obj' 738 | # o.pdb['reduce'] == 'reduce_obj' 739 | o.get_pdbstr.return_value = 'pdbstr' 740 | mock_gen.return_value = 'output' 741 | mock_proc.return_value = ('dots_list', 'clashes_list') 742 | 743 | mp.probe_object(obj) 744 | 745 | mock_get_obj.assert_called_once_with(obj) 746 | mock_gen.assert_called_once_with('pdbstr') 747 | self.assertTrue(o.draw.called) 748 | 749 | def test_no_probe_output(self, mock_get_obj, mock_gen, mock_proc, 750 | mock_cmd, mock_logger): 751 | '''Fail gracefully if `probe` call returns no output.''' 752 | mock_gen.return_value = None 753 | mp.probe_object('obj') 754 | self.assertTrue(mock_logger.error.called) 755 | self.assertFalse(mock_proc.called) 756 | 757 | 758 | 759 | 760 | class ProcessProbeOutputTests(unittest.TestCase): 761 | @mock.patch('pymolprobity.main.kinemage.process_kinemage') 762 | def test_workflow(self, mock_proc_kin): 763 | kin = mock_proc_kin.return_value 764 | inp = '@some\n@kinemage\n@string' 765 | res = mp.process_probe_output(inp) 766 | mock_proc_kin.assert_called_once_with(inp) 767 | self.assertEqual(res, kin) 768 | 769 | 770 | 771 | 772 | 773 | if __name__ == '__main__': 774 | unittest.main() -------------------------------------------------------------------------------- /tests/points_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import mock 4 | 5 | from .context import pymolprobity 6 | import pymolprobity.points as pt 7 | 8 | 9 | ############################################################################### 10 | # 11 | # CGO Utils 12 | # 13 | ############################################################################### 14 | 15 | class CgoColorTests(unittest.TestCase): 16 | @mock.patch('pymolprobity.points.cgo') 17 | @mock.patch('pymolprobity.points.colors.get_color_rgb') 18 | def test(self, mock_get_rgb, mock_cgo): 19 | mock_get_rgb.return_value = (1.0, 1.0, 1.0) 20 | mock_cgo.COLOR = 'COLOR' 21 | inp = 'white' 22 | ref = ['COLOR', 1.0, 1.0, 1.0] 23 | res = pt._cgo_color(inp) 24 | self.assertEqual(res, ref) 25 | 26 | 27 | class CgoSphereTests(unittest.TestCase): 28 | @mock.patch('pymolprobity.points.cgo') 29 | def test(self, mock_cgo): 30 | mock_cgo.SPHERE = 'SPHERE' 31 | pos = (0.0, 1.0, 2.0) 32 | rad = 0.03 33 | ref = ['SPHERE', 0.0, 1.0, 2.0, 0.03] 34 | res = pt._cgo_sphere(pos, rad) 35 | self.assertEqual(res, ref) 36 | 37 | 38 | class PerpVecTests(unittest.TestCase): 39 | def test_with_nonzero_first_coord(self): 40 | inp = (1, 0, 0) 41 | ref = [0, 1.0, 1.0] 42 | res = pt._perp_vec(inp) 43 | self.assertEqual(res, ref) 44 | 45 | def test_with_nonzero_second_coord(self): 46 | inp = (0, 1, 0) 47 | ref = [1.0, 0, 1.0] 48 | res = pt._perp_vec(inp) 49 | self.assertEqual(res, ref) 50 | 51 | def test_with_nonzero_third_coord(self): 52 | inp = (0, 0, 1) 53 | ref = [1.0, 1.0, 0] 54 | res = pt._perp_vec(inp) 55 | self.assertEqual(res, ref) 56 | 57 | def test_with_all_nonzero_coords(self): 58 | inp = (1, 1, 1) 59 | ref = [-2.0, 1.0, 1.0] 60 | res = pt._perp_vec(inp) 61 | self.assertEqual(res, ref) 62 | 63 | class CgoQuadTests(unittest.TestCase): 64 | @mock.patch('pymolprobity.points.cpv') 65 | @mock.patch('pymolprobity.points.cgo') 66 | def test(self, mock_cgo, mock_cpv): 67 | mock_cgo.BEGIN = 'BEGIN' 68 | mock_cgo.TRIANGLE_STRIP = 'TRISTRIP' 69 | mock_cgo.NORMAL = 'NORMAL' 70 | mock_cgo.VERTEX = 'VERTEX' 71 | mock_cgo.END = 'END' 72 | mock_cpv.add.side_effect = [['v1'], ['v2']] 73 | mock_cpv.sub.side_effect = [['v3'], ['v4']] 74 | pos = (0, 1, 2) 75 | normal = (3, 4, 5) 76 | radius = 0.03 77 | ref = ['BEGIN', 'TRISTRIP', 'NORMAL', 3, 4, 5, 'VERTEX', 'v1', 78 | 'VERTEX', 'v2', 'VERTEX', 'v3', 'VERTEX', 'v4', 'END'] 79 | res = pt._cgo_quad(pos, normal, radius) 80 | self.assertEqual(res, ref) 81 | 82 | class CgoCylinderTests(unittest.TestCase): 83 | @mock.patch('pymolprobity.points.cgo') 84 | def test(self, mock_cgo): 85 | mock_cgo.CYLINDER = 'CYLINDER' 86 | p1 = (0, 1, 2) 87 | p2 = (3, 4, 5) 88 | rad = 0.02 89 | c1 = (0, 0, 0) 90 | c2 = (1, 1, 1) 91 | ref = ['CYLINDER', 0, 1, 2, 3, 4, 5, 0.02, 0, 0, 0, 1, 1, 1] 92 | res = pt._cgo_cylinder(p1, p2, rad, c1, c2) 93 | self.assertEqual(res, ref) 94 | 95 | 96 | ############################################################################### 97 | # 98 | # ATOM INFO STRINGS 99 | # 100 | ############################################################################### 101 | 102 | class ProcessBasicAtomStringTests(unittest.TestCase): 103 | def test_all(self): 104 | inputs = [ 105 | {'inp': " CA BSER 126 A", 106 | 'ref': {'chain': 'A', 'resn': 'SER', 'resi': '126', 'name': 'CA', 'alt': 'B'}}, 107 | {'inp': "HD13 LEU 30A A", 108 | 'ref': {'chain': 'A', 'resn': 'LEU', 'resi': '30A', 'name': 'HD13', 'alt': ''}}, 109 | ] 110 | for i in inputs: 111 | res = pt.process_basic_atom_string(i['inp']) 112 | self.assertEqual(res, i['ref']) 113 | 114 | 115 | class ProcessBondsVectorlistAtomTests(unittest.TestCase): 116 | def test_matches(self): 117 | inputs = [ 118 | {'inp': ' og aser A 120 0.73B9.22 tmpcA2P6n', 119 | 'ref': {'chain': 'A', 'resn': 'SER', 'resi': '120', 'name': 'OG', 'alt': 'A', 'occ': 0.73, 'b': 9.22}}, 120 | {'inp': ' ne2agln A1104 0.59B23.79 1cexFH', 121 | 'ref': {'chain': 'A', 'resn': 'GLN', 'resi': '1104', 'name': 'NE2', 'alt': 'A', 'occ': 0.59, 'b': 23.79}}, 122 | {'inp': ' nd2 asn A 106B B24.83 1cexFH', 123 | 'ref': {'chain': 'A', 'resn': 'ASN', 'resi': '106B', 'name': 'ND2', 'alt': '', 'occ': 1.00, 'b': 24.83}}, 124 | ] 125 | for i in inputs: 126 | res = pt.process_bonds_vectorlist_atom(i['inp']) 127 | self.assertEqual(res, i['ref']) 128 | 129 | 130 | 131 | ############################################################################### 132 | # 133 | # DOTLISTS 134 | # 135 | ############################################################################### 136 | 137 | class ProcessDotlistTests(unittest.TestCase): 138 | def setUp(self): 139 | self.base_context = { 140 | 'kinemage': None, 141 | 'group': None, 142 | 'subgroup': None, 143 | 'animate': 0, 144 | } 145 | 146 | @mock.patch('pymolprobity.points._parse_dotlist_body') 147 | @mock.patch('pymolprobity.points._parse_dotlist_header') 148 | def test_workflow(self, mock_parse_header, mock_parse_body): 149 | mock_parse_header.return_value = ('name', 'color', 'master') 150 | mock_parse_body.return_value = [pt.Dot(), pt.Dot(), pt.Dot()] 151 | inp = ['dotlist blah', 'line 1', 'line 2', 'line 3'] 152 | res = pt.process_dotlist(inp, self.base_context) 153 | mock_parse_header.assert_called_once_with(inp[0]) 154 | mock_parse_body.assert_called_once_with(inp[1:]) 155 | for dot in res: 156 | self.assertEqual(dot.dotlist_name, 'name') 157 | self.assertEqual(dot.dotlist_color, 'color') 158 | self.assertEqual(dot.master, 'master') 159 | self.assertEqual(dot.kinemage, self.base_context['kinemage']) 160 | self.assertEqual(dot.group, self.base_context['group']) 161 | self.assertEqual(dot.subgroup, self.base_context['subgroup']) 162 | self.assertEqual(dot.animate, self.base_context['animate']) 163 | 164 | 165 | class DotlistBodyReTests(unittest.TestCase): 166 | def setUp(self): 167 | self.re = pt.DOTLIST_BODY_RE 168 | 169 | def test_matches(self): 170 | inputs = [ 171 | '''{ CA SER 26 A}blue 'O' 61.716,59.833,8.961''', # typical 172 | '''{ H? HOH 293 A}greentint 'O' 57.884,59.181,7.525''', # H? 173 | ] 174 | for i in inputs: 175 | self.assertRegexpMatches(i, self.re) 176 | 177 | 178 | class ParseDotlistHeaderTests(unittest.TestCase): 179 | def test1(self): 180 | inp = 'dotlist {x} color=white master={vdw contact}' 181 | ref = ('x', 'white', 'vdw_contact') 182 | res = pt._parse_dotlist_header(inp) 183 | self.assertEqual(res, ref) 184 | 185 | def test2(self): 186 | inp = 'dotlist {x} color=red master={H-bonds}' 187 | ref = ('x', 'red', 'H_bonds') 188 | res = pt._parse_dotlist_header(inp) 189 | self.assertEqual(res, ref) 190 | 191 | 192 | @mock.patch('pymolprobity.points.colors.get_pymol_color') 193 | class ParseDotlistBodyTests(unittest.TestCase): 194 | def test_general_form(self, mock_get_color): 195 | mock_get_color.return_value = 'colorname' 196 | inp = ["{AAAABCCCDDDDEFF}colorname 'G' 0.0,1.0,2.0"] 197 | ref_atom = {'name': 'AAAA', 198 | 'alt': 'B', 199 | 'resn': 'CCC', 200 | 'resi': 'DDDDE', 201 | 'chain': 'FF'} 202 | res = pt._parse_dotlist_body(inp) 203 | self.assertEqual(res[0].atom, ref_atom) 204 | self.assertEqual(res[0].color, 'colorname') 205 | self.assertEqual(res[0].pm, 'G') 206 | self.assertEqual(res[0].coords, [0.0, 1.0, 2.0]) 207 | 208 | def test_with_spaces_in_atom_desc(self, mock_get_color): 209 | '''Should handle spaces in atom name, alt, resn, resi, and chain.''' 210 | mock_get_color.return_value = 'colorname' 211 | inp = ["{ CA SER 26 A}colorname 'G' 0.0,1.0,2.0"] 212 | ref_atom = {'name': 'CA', 213 | 'alt': '', 214 | 'resn': 'SER', 215 | 'resi': '26', 216 | 'chain': 'A'} 217 | res = pt._parse_dotlist_body(inp) 218 | self.assertEqual(res[0].atom, ref_atom) 219 | 220 | def test_with_question_mark_in_atom_desc(self, mock_get_color): 221 | '''Should handle ? in atom name e.g. "H?" for water hydrogen.''' 222 | mock_get_color.return_value = 'colorname' 223 | inp = ["{ H? HOH 293 A}colorname 'G' 0.0,1.0,2.0"] 224 | ref_atom = {'name': 'H', 225 | 'alt': '', 226 | 'resn': 'HOH', 227 | 'resi': '293', 228 | 'chain': 'A'} 229 | res = pt._parse_dotlist_body(inp) 230 | self.assertEqual(res[0].atom, ref_atom) 231 | 232 | def test_with_negative_coords(self, mock_get_color): 233 | '''Handle negative coordinates.''' 234 | mock_get_color.return_value = 'colorname' 235 | inp = ["{AAAABCCCDDDDEFF}colorname 'G' -0.0,-1.0,-2.0"] 236 | res = pt._parse_dotlist_body(inp) 237 | self.assertEqual(res[0].coords, [-0.0, -1.0, -2.0]) 238 | 239 | def test_with_quotation_mark_atom(self, mock_get_color): 240 | '''Handle repeated atoms indicated by `{"}`.''' 241 | mock_get_color.return_value = 'colorname' 242 | inp = ['''{AAAABCCCDDDDEFF}colorname 'G' 0.0,1.0,2.0''', 243 | '''{"}colorname 'G' -0.0,-1.0,-2.0'''] 244 | res = pt._parse_dotlist_body(inp) 245 | self.assertEqual(res[0].atom, res[1].atom) 246 | 247 | 248 | ############################################################################### 249 | # 250 | # VECTORLISTS 251 | # 252 | ############################################################################### 253 | 254 | class ProcessVectorlistTests(unittest.TestCase): 255 | def setUp(self): 256 | self.base_context = { 257 | 'kinemage': None, 258 | 'group': None, 259 | 'subgroup': None, 260 | 'animate': 0, 261 | } 262 | 263 | @mock.patch('pymolprobity.points._parse_clash_vectorlist_body') 264 | @mock.patch('pymolprobity.points._parse_vectorlist_header') 265 | def test_workflow_with_clash_vectorlist(self, mock_parse_header, 266 | mock_parse_body): 267 | mock_parse_header.return_value = ('x', 'color', 'master') 268 | mock_parse_body.return_value = [pt.Vector(), pt.Vector(), pt.Vector()] 269 | inp = ['vectorlist blah', 'line 1', 'line 2', 'line 3'] 270 | res = pt.process_vectorlist(inp, self.base_context) 271 | mock_parse_header.assert_called_once_with(inp[0]) 272 | mock_parse_body.assert_called_once_with(inp[1:]) 273 | for v in res: 274 | self.assertEqual(v.vectorlist_name, 'x') 275 | self.assertEqual(v.vectorlist_color, 'color') 276 | self.assertEqual(v.master, 'master') 277 | self.assertEqual(v.kinemage, self.base_context['kinemage']) 278 | self.assertEqual(v.group, self.base_context['group']) 279 | self.assertEqual(v.subgroup, self.base_context['subgroup']) 280 | self.assertEqual(v.animate, self.base_context['animate']) 281 | 282 | 283 | @mock.patch('pymolprobity.points._parse_bonds_vectorlist_body') 284 | @mock.patch('pymolprobity.points._parse_vectorlist_header') 285 | def test_workflow_with_bonds_vectorlist(self, mock_parse_header, 286 | mock_parse_body): 287 | mock_parse_header.return_value = ('mc', 'color', 'master') 288 | mock_parse_body.return_value = [pt.Vector(), pt.Vector(), pt.Vector()] 289 | inp = ['vectorlist blah', 'line 1', 'line 2', 'line 3'] 290 | res = pt.process_vectorlist(inp, self.base_context) 291 | mock_parse_header.assert_called_once_with(inp[0]) 292 | mock_parse_body.assert_called_once_with(inp[1:]) 293 | for v in res: 294 | self.assertEqual(v.vectorlist_name, 'mc') 295 | self.assertEqual(v.vectorlist_color, 'color') 296 | self.assertEqual(v.master, 'master') 297 | self.assertEqual(v.kinemage, self.base_context['kinemage']) 298 | self.assertEqual(v.group, self.base_context['group']) 299 | self.assertEqual(v.subgroup, self.base_context['subgroup']) 300 | self.assertEqual(v.animate, self.base_context['animate']) 301 | 302 | 303 | class ParseVectorlistHeaderTests(unittest.TestCase): 304 | def test_general_form(self): 305 | inp = 'vectorlist {x} color=foo master={bar}' 306 | ref = ('x', 'foo', 'bar') 307 | res = pt._parse_vectorlist_header(inp) 308 | self.assertEqual(res, ref) 309 | 310 | 311 | @mock.patch('pymolprobity.points.process_basic_atom_string') 312 | @mock.patch('pymolprobity.points.colors.get_pymol_color') 313 | class ParseClashVectorlistBodyTests(unittest.TestCase): 314 | def test_clash_general_form(self, mock_get_color, mock_proc_atom): 315 | mock_get_color.return_value = 'somecolor' 316 | mock_proc_atom.side_effect = ['atom1', 'atom2'] 317 | inp = ['''{atom1}somecolor P 'O' 0.0,1.0,2.0 {"}somecolor 'O' 3.0,4.0,5.0'''] 318 | res = pt._parse_clash_vectorlist_body(inp) 319 | mock_proc_atom.assert_called_once_with('atom1') 320 | self.assertEqual(res[0].atom, ['atom1', 'atom1']) 321 | self.assertEqual(res[0].color, ['somecolor', 'somecolor']) 322 | self.assertEqual(res[0].pm, ['O', 'O']) 323 | self.assertEqual(res[0].coords, [[0.0, 1.0, 2.0], [3.0, 4.0, 5.0]] ) 324 | 325 | def test_with_negative_coords(self, mock_get_color, mock_proc): 326 | '''Handle negative coordinates.''' 327 | mock_proc.side_effect = ['crina', None] 328 | inp = ['''{atomstr}somecolor P 'O' -0.0,-1.0,-2.0 {"}somecolor 'O' -3.0,-4.0,-5.0'''] 329 | res = pt._parse_clash_vectorlist_body(inp) 330 | self.assertEqual(res[0].coords, [[-0.0, -1.0, -2.0], [-3.0, -4.0, -5.0]]) 331 | 332 | 333 | @mock.patch('pymolprobity.points.process_bonds_vectorlist_atom') 334 | class ParseBondsVectorlistBody(unittest.TestCase): 335 | def test_bond_vector_general_form(self, mock_proc_atom): 336 | mock_proc_atom.side_effect = ['atom1', 'atom2'] 337 | inp = ['''{atom1} P 'O' 0.0,1.0,2.0 {atom2} P 'O' 3.0,4.0,5.0'''] 338 | res = pt._parse_bonds_vectorlist_body(inp) 339 | mock_proc_atom.assert_has_calls([mock.call('atom1'), mock.call('atom2')]) 340 | self.assertEqual(res[0].atom, ['atom1', 'atom2']) 341 | self.assertEqual(res[0].color, [None, None]) 342 | self.assertEqual(res[0].pm, ['O', 'O']) 343 | self.assertEqual(res[0].coords, [[0.0, 1.0, 2.0], [3.0, 4.0, 5.0]] ) 344 | 345 | def test_with_continuous_bonds_carryover_second_atom(self, mock_proc_atom): 346 | inp = ['''{atom1} P 'O' 0.0,1.0,2.0 {atom2} P 'O' 3.0,4.0,5.0''', 347 | '''{atom3} P 'O' 0.0,1.0,2.0 '''] 348 | mock_proc_atom.side_effect = ['atom1', 'atom2', 'atom3'] 349 | res = pt._parse_bonds_vectorlist_body(inp) 350 | self.assertEqual(res[0].atom, ['atom1', 'atom2']) 351 | self.assertEqual(res[1].atom, ['atom2', 'atom3']) 352 | 353 | def test_raises_indexerror_when_first_line_has_only_one_point(self, 354 | mock_proc_atom): 355 | inp = ['''{atom1} P 'O' 0.0,1.0,2.0'''] 356 | with self.assertRaises(IndexError): 357 | pt._parse_bonds_vectorlist_body(inp) 358 | 359 | def test_raises_valueerror_with_zero_matches(self, mock_proc_atom): 360 | inp = ['''not a match'''] 361 | with self.assertRaises(ValueError): 362 | pt._parse_bonds_vectorlist_body(inp) 363 | 364 | def test_raises_valueerror_with_more_than_2_matches(self, mock_proc_atom): 365 | inp = ['''{atom1} P 'O' 0.0,1.0,2.0 {atom2} P 'O' 0.0,1.0,2.0 {atom3} P 'O' 3.0,4.0,5.0'''] 366 | with self.assertRaises(ValueError): 367 | pt._parse_bonds_vectorlist_body(inp) 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | class DotTests(unittest.TestCase): 376 | @mock.patch('pymolprobity.points.colors.get_pymol_color') 377 | def test_init(self, mock_get_color): 378 | mock_get_color.return_value = 'somecolor' 379 | d = pt.Dot() 380 | self.assertEqual(d.atom, None) 381 | self.assertEqual(d.color, 'somecolor') 382 | self.assertEqual(d.pm, None) 383 | self.assertEqual(d.coords, None) 384 | self.assertEqual(d.draw, 1) 385 | self.assertEqual(d.dotlist_name, None) 386 | self.assertEqual(d.dotlist_color, None) 387 | self.assertEqual(d.master, None) 388 | 389 | @mock.patch('pymolprobity.points._cgo_sphere') 390 | @mock.patch('pymolprobity.points._cgo_color') 391 | def test_get_cgo_workflow_with_defaults(self, mock_color, mock_sphere): 392 | mock_color.return_value = ['color'] 393 | mock_sphere.return_value = ['sphere'] 394 | d = pt.Dot() 395 | d.color = 'white' 396 | d.coords = (0.0, 1.0, 2.0) 397 | 398 | res = d.get_cgo() 399 | ref = ['color', 'sphere'] 400 | self.assertEqual(res, ref) 401 | 402 | mock_color.assert_called_once_with('white') 403 | mock_sphere.assert_called_once_with((0, 1, 2), 0.03) 404 | 405 | @mock.patch('pymolprobity.points.cpv') 406 | @mock.patch('pymolprobity.points._cgo_quad') 407 | @mock.patch('pymolprobity.points._cgo_color') 408 | def test_get_cgo_with_dot_mode_1(self, mock_color, mock_quad, mock_cpv): 409 | mock_color.return_value = ['color'] 410 | mock_quad.return_value = ['quad'] 411 | mock_cpv.normalize.return_value = (1, 0, 0) 412 | d = pt.Dot() 413 | d.color = 'white' 414 | d.coords = (0.0, 1.0, 2.0) 415 | d.atom = {'coords': (3.0, 4.0, 5.0)} 416 | 417 | res = d.get_cgo(dot_mode=1) 418 | 419 | ref = ['color', 'quad'] 420 | self.assertEqual(res, ref) 421 | 422 | mock_color.assert_called_once_with('white') 423 | mock_quad.assert_called_once_with((0.0, 1.0, 2.0), (1, 0, 0), 0.03 * 424 | 1.5) 425 | mock_cpv.sub.assert_called_once_with((0.0, 1.0, 2.0), (3.0, 4.0, 5.0)) 426 | 427 | 428 | 429 | class VectorTests(unittest.TestCase): 430 | @mock.patch('pymolprobity.points.colors.get_pymol_color') 431 | def test_init(self, mock_get_color): 432 | mock_get_color.return_value = None 433 | v = pt.Vector() 434 | self.assertEqual(v.atom, [None, None]) 435 | self.assertEqual(v.color, [None, None]) 436 | self.assertEqual(v.pm, [None, None]) 437 | self.assertEqual(v.coords, [None, None]) 438 | self.assertEqual(v.draw, 1) 439 | self.assertEqual(v.vectorlist_name, None) 440 | self.assertEqual(v.vectorlist_color, None) 441 | self.assertEqual(v.master, None) 442 | 443 | @mock.patch('pymolprobity.points._cgo_sphere') 444 | @mock.patch('pymolprobity.points._cgo_color') 445 | @mock.patch('pymolprobity.points._cgo_cylinder') 446 | @mock.patch('pymolprobity.points.colors.get_color_rgb') 447 | def test_get_cgo_workflow_with_defaults(self, mock_get_rgb, mock_cylinder, 448 | mock_color, mock_sphere): 449 | mock_get_rgb.return_value = 'rgb' 450 | mock_cylinder.return_value = ['cylinder'] 451 | mock_color.return_value = ['color'] 452 | mock_sphere.return_value = ['sphere'] 453 | 454 | v = pt.Vector() 455 | v.color = ['white', 'white'] 456 | v.coords = [(0.0, 1.0, 2.0), (3.0, 4.0, 5.0)] 457 | 458 | res = v.get_cgo() 459 | ref = ['cylinder', 'color', 'sphere', 'color', 'sphere'] 460 | self.assertEqual(res, ref) 461 | 462 | mock_color.assert_has_calls([mock.call('white'), mock.call('white')]) 463 | cyl_args = [(0.0, 1.0, 2.0), (3.0, 4.0, 5.0), 0.03, 'rgb', 'rgb'] 464 | mock_cylinder.assert_called_once_with(*cyl_args) 465 | mock_sphere.assert_has_calls([mock.call( (0.0, 1.0, 2.0), 0.03 ), 466 | mock.call( (3.0, 4.0, 5.0), 0.03 )]) 467 | 468 | @mock.patch('pymolprobity.points.cpv') 469 | @mock.patch('pymolprobity.points._cgo_quad') 470 | @mock.patch('pymolprobity.points._cgo_color') 471 | def test_get_cgo_with_dot_mode_1(self, mock_color, mock_quad, mock_cpv): 472 | mock_color.return_value = ['color'] 473 | mock_quad.return_value = ['quad'] 474 | mock_cpv.normalize.return_value = (1, 0, 0) 475 | d = pt.Dot() 476 | d.color = 'white' 477 | d.coords = (0.0, 1.0, 2.0) 478 | d.atom = {'coords': (3.0, 4.0, 5.0)} 479 | 480 | res = d.get_cgo(dot_mode=1) 481 | 482 | ref = ['color', 'quad'] 483 | self.assertEqual(res, ref) 484 | 485 | mock_color.assert_called_once_with('white') 486 | mock_quad.assert_called_once_with((0.0, 1.0, 2.0), (1, 0, 0), 0.03 * 487 | 1.5) 488 | mock_cpv.sub.assert_called_once_with((0.0, 1.0, 2.0), (3.0, 4.0, 5.0)) 489 | 490 | 491 | 492 | if __name__ == '__main__': 493 | unittest.main() 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | -------------------------------------------------------------------------------- /tests/utils_tests.py: -------------------------------------------------------------------------------- 1 | '''utils tests for pymolprobity plugin''' 2 | 3 | import mock 4 | import unittest 5 | 6 | from .context import pymolprobity 7 | import pymolprobity.utils as ut 8 | 9 | 10 | class QuoteStrTests(unittest.TestCase): 11 | def test_with_string_input(self): 12 | res = ut.quote_str("inp") 13 | ref = "'inp'" 14 | self.assertEqual(res, ref) 15 | 16 | def test_with_nonstring_input(self): 17 | res = ut.quote_str(1) 18 | self.assertEqual(res, 1) 19 | 20 | def test_with_custom_quote(self): 21 | res = ut.quote_str("inp", '?') 22 | ref = "?inp?" 23 | self.assertEqual(res, ref) 24 | 25 | 26 | class SlugifyTests(unittest.TestCase): 27 | def test_basic(self): 28 | inp = 'Some string 1' 29 | res = ut.slugify(inp) 30 | ref = 'Some_string_1' 31 | self.assertEqual(res, ref) 32 | 33 | def test_leading_trailing_junk(self): 34 | inp = ' this string had leading and trailing space ' 35 | res = ut.slugify(inp) 36 | ref = 'this_string_had_leading_and_trailing_space' 37 | self.assertEqual(res, ref) 38 | 39 | def test_multiple_adjacent_separators(self): 40 | inp = 'this string had extra spaces' 41 | res = ut.slugify(inp) 42 | ref = 'this_string_had_extra_spaces' 43 | self.assertEqual(res, ref) 44 | 45 | def test_custom_separator(self): 46 | inp = 'some string' 47 | res = ut.slugify(inp, sep='!') 48 | ref = 'some!string' 49 | self.assertEqual(res, ref) 50 | 51 | def test_non_string_input(self): 52 | inp = 1 53 | res = ut.slugify(inp) 54 | ref = 1 55 | self.assertEqual(res, ref) 56 | 57 | 58 | class ToNumberTests(unittest.TestCase): 59 | def test_with_int_str(self): 60 | inp = '1' 61 | res = ut.to_number(inp) 62 | ref = 1 63 | self.assertEqual(res, ref) 64 | 65 | def test_with_float_str(self): 66 | inp = '1.23' 67 | res = ut.to_number(inp) 68 | ref = 1.23 69 | self.assertEqual(res, ref) 70 | 71 | def test_with_scientific_notation_str(self): 72 | inp = '1e-2' 73 | res = ut.to_number(inp) 74 | ref = 0.01 75 | self.assertEqual(res, ref) 76 | 77 | def test_with_non_numeric_str(self): 78 | inp = 'foo' 79 | res = ut.to_number(inp) 80 | ref = inp 81 | self.assertEqual(res, ref) 82 | 83 | 84 | 85 | if __name__ == '__main__': 86 | unittest.main() 87 | 88 | --------------------------------------------------------------------------------