duecredit-0.6.0/000077500000000000000000000000001273067304300134725ustar00rootroot00000000000000duecredit-0.6.0/.coveragerc000066400000000000000000000001161273067304300156110ustar00rootroot00000000000000[run] branch = True source = duecredit include = duecredit/* examples/* duecredit-0.6.0/.github_changelog_generator000066400000000000000000000001101273067304300210220ustar00rootroot00000000000000future-release=0.5.0 exclude-tags-regex="(debian/.*|\d+\.\d+\.\d+\..*)" duecredit-0.6.0/.gitignore000066400000000000000000000001611273067304300154600ustar00rootroot00000000000000__pycache__ *.py[co] /build /dist /MANIFEST /duecredit/version.py /venv* /.tox .duecredit.p .coverage *.egg-info duecredit-0.6.0/.travis.yml000066400000000000000000000027401273067304300156060ustar00rootroot00000000000000# vim ft=yaml # travis-ci.org definition for DueCredit build language: python sudo: false python: - "2.7" # - "3.2" - "3.3" - "3.4" - "3.5" # - "pypy" # - "pypy3" cache: - pip before_install: # - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then travis_retry sudo apt-get update -qq; fi git: depth: 99999 install: - if [[ $TRAVIS_PYTHON_VERSION == pypy ]] ; then dpkg --compare-versions $(pypy --version 2>&1 | awk '/PyPy/{print $2;}') ge 2.5.1 || { d=$PWD; cd /tmp; wget --no-check-certificate https://bitbucket.org/pypy/pypy/downloads/pypy-2.5.1-linux64.tar.bz2; tar -xjvf pypy*bz2; cd pypy-*/bin/; export PATH=$PWD:$PATH; cd $d; } ; fi - travis_retry pip install -q coveralls codecov - python setup.py --help # just to trigger generation of .version - pip install -e '.[tests]' - pip install flake8 script: - nosetests --with-doctest --with-cov --cover-package duecredit --logging-level=INFO -v - python setup.py install # test installation # for now flaking only the stub.py - flake8 duecredit/stub.py after_success: - coveralls - codecov deploy: provider: pypi distributions: sdist user: yarikoptic password: secure: mTxbioGS+sdfxnJRbAGZCxjWlaGJx+KqXPfYGESKcg6IVaSUM9D4CUhxgHHW88FYSnkmCvwuu57w7AAot9FyG6Q/1q656gluCbEJzfDJerSH1S06HqAEmjSPJvIEG/zwvPIUm3RPc+8j9XtedztM3aVWkqBHAzvUzEnsX1jJpic= on: tags: true branch: master repo: duecredit/duecredit condition: "$TRAVIS_PYTHON_VERSION == 2.7 && $TRAVIS_TAG =~ ^[0-9][.][0-9][.0-9]*" duecredit-0.6.0/CHANGELOG.md000066400000000000000000000312231273067304300153040ustar00rootroot00000000000000# Change Log ## [0.6.0](https://github.com/duecredit/duecredit/tree/0.6.0) (2016-06-16) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.5.0...0.6.0) **Implemented enhancements:** - Support system-specific references [\#81](https://github.com/duecredit/duecredit/issues/81) - export to bibtex doesn't support tags yet [\#19](https://github.com/duecredit/duecredit/issues/19) - ENH: support DUECREDIT\_REPORT\_ALL=1 to report all citations, not only with functionality used [\#92](https://github.com/duecredit/duecredit/pull/92) ([yarikoptic](https://github.com/yarikoptic)) **Fixed bugs:** - Outputting to bibtex doesn't filter by used citations [\#68](https://github.com/duecredit/duecredit/issues/68) - references package even if no cited functions/methods used [\#48](https://github.com/duecredit/duecredit/issues/48) - When injecting multiple citations at the same point, only one referenced [\#47](https://github.com/duecredit/duecredit/issues/47) **Merged pull requests:** - BF: allow multiple injections at the same path, avoid resetting \_orig\_import if already deactivated [\#91](https://github.com/duecredit/duecredit/pull/91) ([yarikoptic](https://github.com/yarikoptic)) - DOC: Update readme to reflect current output of duecredit summary [\#89](https://github.com/duecredit/duecredit/pull/89) ([mvdoc](https://github.com/mvdoc)) - enable codecov coverage reports [\#87](https://github.com/duecredit/duecredit/pull/87) ([yarikoptic](https://github.com/yarikoptic)) - REF,ENH: refactor {BibTeX,Text}Output into Output class with subclasses [\#86](https://github.com/duecredit/duecredit/pull/86) ([mvdoc](https://github.com/mvdoc)) ## [0.5.0](https://github.com/duecredit/duecredit/tree/0.5.0) (2016-05-11) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.8...0.5.0) **Fixed bugs:** - test\_noincorrect\_import\_if\_no\_lxml fails on my laptop \(and on travis\) [\#84](https://github.com/duecredit/duecredit/issues/84) - zenodo and "unofficial" bibtex entry types [\#77](https://github.com/duecredit/duecredit/issues/77) **Closed issues:** - duecredit on nipype [\#72](https://github.com/duecredit/duecredit/issues/72) **Merged pull requests:** - BF: workaround for zenodo bibtex entries imported with import\_doi [\#85](https://github.com/duecredit/duecredit/pull/85) ([mvdoc](https://github.com/mvdoc)) - enable testing under python 3.5 on travis [\#79](https://github.com/duecredit/duecredit/pull/79) ([yarikoptic](https://github.com/yarikoptic)) - ENH: appveyor configuration \(based on shablona's\) based on mix of conda and pip [\#70](https://github.com/duecredit/duecredit/pull/70) ([yarikoptic](https://github.com/yarikoptic)) ## [0.4.8](https://github.com/duecredit/duecredit/tree/0.4.8) (2016-05-04) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.7...0.4.8) **Closed issues:** - Referencing articles with no DOI [\#74](https://github.com/duecredit/duecredit/issues/74) - doi importer doesn't work with zenodo dois [\#73](https://github.com/duecredit/duecredit/issues/73) **Merged pull requests:** - BF: change request command to make it work with zenodo too [\#76](https://github.com/duecredit/duecredit/pull/76) ([mvdoc](https://github.com/mvdoc)) - DOC: Show that user can also enter BibTeX entries [\#75](https://github.com/duecredit/duecredit/pull/75) ([mvdoc](https://github.com/mvdoc)) ## [0.4.7](https://github.com/duecredit/duecredit/tree/0.4.7) (2016-04-21) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.6...0.4.7) ## [0.4.6](https://github.com/duecredit/duecredit/tree/0.4.6) (2016-04-19) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.5...0.4.6) **Fixed bugs:** - In PyMVPA, fail to handle failures if lxml, types not available [\#64](https://github.com/duecredit/duecredit/issues/64) **Merged pull requests:** - Primarily PEP8 for stub.py \(the rest needs more work\) [\#69](https://github.com/duecredit/duecredit/pull/69) ([yarikoptic](https://github.com/yarikoptic)) - Use HTTPS for GitHub URL [\#67](https://github.com/duecredit/duecredit/pull/67) ([jwilk](https://github.com/jwilk)) - Fix typos [\#66](https://github.com/duecredit/duecredit/pull/66) ([jwilk](https://github.com/jwilk)) ## [0.4.5](https://github.com/duecredit/duecredit/tree/0.4.5) (2015-12-03) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.4...0.4.5) **Merged pull requests:** - Make duecredit import and stub more robust to failures with e.g. import of lxml [\#65](https://github.com/duecredit/duecredit/pull/65) ([yarikoptic](https://github.com/yarikoptic)) ## [0.4.4](https://github.com/duecredit/duecredit/tree/0.4.4) (2015-11-08) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.3...0.4.4) ## [0.4.3](https://github.com/duecredit/duecredit/tree/0.4.3) (2015-09-28) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.2...0.4.3) **Implemented enhancements:** - Make "conditions" even more powerful [\#36](https://github.com/duecredit/duecredit/issues/36) **Merged pull requests:** - add mod\_ files for nibabel, nipy, nipype [\#62](https://github.com/duecredit/duecredit/pull/62) ([jgors](https://github.com/jgors)) - fixed headers for injections/mod\_ files and added item to .gitignore [\#60](https://github.com/duecredit/duecredit/pull/60) ([jgors](https://github.com/jgors)) - ENH+BF: recently introduced entries got fixed up [\#59](https://github.com/duecredit/duecredit/pull/59) ([yarikoptic](https://github.com/yarikoptic)) - add mod\_\* files [\#58](https://github.com/duecredit/duecredit/pull/58) ([jgors](https://github.com/jgors)) - ENH: versions -- provide dumps, keys, \_\_contains\_\_ [\#57](https://github.com/duecredit/duecredit/pull/57) ([yarikoptic](https://github.com/yarikoptic)) - ENH: Two more module level injections [\#56](https://github.com/duecredit/duecredit/pull/56) ([yarikoptic](https://github.com/yarikoptic)) ## [0.4.2](https://github.com/duecredit/duecredit/tree/0.4.2) (2015-09-03) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.1...0.4.2) **Closed issues:** - we should output description \(not just path\) in the listing [\#49](https://github.com/duecredit/duecredit/issues/49) **Merged pull requests:** - BF: print description, not just path. Closes \#49 [\#52](https://github.com/duecredit/duecredit/pull/52) ([yarikoptic](https://github.com/yarikoptic)) - Overhaul conditions -- "and" logic \(all must be met\) + allow to access attributes of the arguments [\#50](https://github.com/duecredit/duecredit/pull/50) ([yarikoptic](https://github.com/yarikoptic)) - BF: Fix get\_text\_rendering when Citation is passed with Doi [\#46](https://github.com/duecredit/duecredit/pull/46) ([mvdoc](https://github.com/mvdoc)) ## [0.4.1](https://github.com/duecredit/duecredit/tree/0.4.1) (2015-08-27) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.4.0...0.4.1) ## [0.4.0](https://github.com/duecredit/duecredit/tree/0.4.0) (2015-08-21) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.3.0...0.4.0) **Fixed bugs:** - Cross-referencing does not work [\#30](https://github.com/duecredit/duecredit/issues/30) **Closed issues:** - DUECREDIT\_ENABLE doesn't work anymore [\#45](https://github.com/duecredit/duecredit/issues/45) - test\_no\_double\_activation on injector fails on travis and locally with Python 2.7.{6,9} [\#43](https://github.com/duecredit/duecredit/issues/43) - possible bug \(race condition\) in injector's \_\_import\_\_ handling [\#40](https://github.com/duecredit/duecredit/issues/40) **Merged pull requests:** - ENH+DOC: always check if \_orig\_import is not None \(Closes \#40\) [\#44](https://github.com/duecredit/duecredit/pull/44) ([yarikoptic](https://github.com/yarikoptic)) - \[Injections\] Add all references for sklearn.cluster [\#42](https://github.com/duecredit/duecredit/pull/42) ([mvdoc](https://github.com/mvdoc)) - REF: text output, divide "model" from "view" [\#41](https://github.com/duecredit/duecredit/pull/41) ([mvdoc](https://github.com/mvdoc)) - RF to provide \_\_main\_\_ so we could do python -m duecredit existing script [\#39](https://github.com/duecredit/duecredit/pull/39) ([yarikoptic](https://github.com/yarikoptic)) ## [0.3.0](https://github.com/duecredit/duecredit/tree/0.3.0) (2015-08-05) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.2.2...0.3.0) **Implemented enhancements:** - automagically upload releases to pypi from travis [\#6](https://github.com/duecredit/duecredit/issues/6) **Fixed bugs:** - while "dump"ing -- references shouldn't be duplicated even if used in multiple modules [\#23](https://github.com/duecredit/duecredit/issues/23) **Closed issues:** - Syntax error in test\_utils.py with python 2.7.6 [\#26](https://github.com/duecredit/duecredit/issues/26) - Travis skips tests [\#25](https://github.com/duecredit/duecredit/issues/25) **Merged pull requests:** - RF: cite-on-import -\> cite-module since we might be dealing with other languages etc [\#37](https://github.com/duecredit/duecredit/pull/37) ([yarikoptic](https://github.com/yarikoptic)) - Few tune ups to injection and more to its testing [\#35](https://github.com/duecredit/duecredit/pull/35) ([yarikoptic](https://github.com/yarikoptic)) - RF: Donate -\> Url [\#34](https://github.com/duecredit/duecredit/pull/34) ([yarikoptic](https://github.com/yarikoptic)) - TST: check reference numbers are consistent [\#29](https://github.com/duecredit/duecredit/pull/29) ([mvdoc](https://github.com/mvdoc)) - PY3+make vcr optional: more concise use of six, it might take a while for vcr to come to debian [\#28](https://github.com/duecredit/duecredit/pull/28) ([yarikoptic](https://github.com/yarikoptic)) - BF: give correct ref numbers for citations [\#24](https://github.com/duecredit/duecredit/pull/24) ([mvdoc](https://github.com/mvdoc)) - Fix typo in README.md [\#21](https://github.com/duecredit/duecredit/pull/21) ([lesteve](https://github.com/lesteve)) ## [0.2.2](https://github.com/duecredit/duecredit/tree/0.2.2) (2015-07-27) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.2.1...0.2.2) ## [0.2.1](https://github.com/duecredit/duecredit/tree/0.2.1) (2015-07-27) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.2.0...0.2.1) ## [0.2.0](https://github.com/duecredit/duecredit/tree/0.2.0) (2015-07-27) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.1.1...0.2.0) **Closed issues:** - RFC: either rename "kind" and "level" into some thing more descriptive [\#8](https://github.com/duecredit/duecredit/issues/8) - add "classes" \(or tags?\) to citations on what citation is about [\#5](https://github.com/duecredit/duecredit/issues/5) - version for modules still doesn't work [\#2](https://github.com/duecredit/duecredit/issues/2) **Merged pull requests:** - BF: circular import of injector and duecredit [\#17](https://github.com/duecredit/duecredit/pull/17) ([mvdoc](https://github.com/mvdoc)) - Add six to the requirements [\#15](https://github.com/duecredit/duecredit/pull/15) ([mvdoc](https://github.com/mvdoc)) - ENH: conditions kwarg for dcite to condition when to trigger the citation given arguments to the function call [\#14](https://github.com/duecredit/duecredit/pull/14) ([yarikoptic](https://github.com/yarikoptic)) - \[WIP\] Start adding more injections [\#13](https://github.com/duecredit/duecredit/pull/13) ([mvdoc](https://github.com/mvdoc)) - RF arguments for cite: kind -\> tags, level -\> path, use -\> desc [\#12](https://github.com/duecredit/duecredit/pull/12) ([yarikoptic](https://github.com/yarikoptic)) - ENH: try to use new container based Travis infrastructure [\#11](https://github.com/duecredit/duecredit/pull/11) ([yarikoptic](https://github.com/yarikoptic)) - WiP NF: core to implement "injection" of duecredit entries into other modules [\#10](https://github.com/duecredit/duecredit/pull/10) ([yarikoptic](https://github.com/yarikoptic)) - coveralls call should be without any args, also test installation now [\#9](https://github.com/duecredit/duecredit/pull/9) ([yarikoptic](https://github.com/yarikoptic)) ## [0.1.1](https://github.com/duecredit/duecredit/tree/0.1.1) (2015-06-26) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.1.0...0.1.1) ## [0.1.0](https://github.com/duecredit/duecredit/tree/0.1.0) (2015-06-21) [Full Changelog](https://github.com/duecredit/duecredit/compare/0.0.0...0.1.0) **Closed issues:** - fix badges in README.md \(it is not ,rst ;\)\) [\#4](https://github.com/duecredit/duecredit/issues/4) **Merged pull requests:** - Stub tests pass [\#1](https://github.com/duecredit/duecredit/pull/1) ([mvdoc](https://github.com/mvdoc)) ## [0.0.0](https://github.com/duecredit/duecredit/tree/0.0.0) (2013-12-06) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* duecredit-0.6.0/CONTRIBUTING.md000066400000000000000000000156771273067304300157430ustar00rootroot00000000000000Contributing to DueCredit ========================= [gh-duecredit]: https://github.com/duecredit/duecredit Files organization ------------------ - `duecredit/` is the main Python module where major development is happening, with major submodules being: - `cmdline/` contains commands for the command line interface. See any of the `cmd_*.py` files here for an example - `tests/` all unit- and regression- tests - `utils.py` provides convenience helpers used by unit-tests such as `@with_tree`, `@serve_path_via_http` and other decorators - `tools/` might contain helper utilities used during development, testing, and benchmarking of DueCredit. Implemented in any most appropriate language (Python, bash, etc.) How to contribute ----------------- The preferred way to contribute to the DueCredit code base is to fork the [main repository][gh-duecredit] on GitHub. Here we outline the workflow used by the developers: 0. Have a clone of our main [project repository][gh-duecredit] as `origin` remote in your git: git clone git://github.com/duecredit/duecredit 1. Fork the [project repository][gh-duecredit]: click on the 'Fork' button near the top of the page. This creates a copy of the code base under your account on the GitHub server. 2. Add your forked clone as a remote to the local clone you already have on your local disk: git remote add gh-YourLogin git@github.com:YourLogin/duecredit.git git fetch gh-YourLogin To ease addition of other github repositories as remotes, here is a little bash function/script to add to your `~/.bashrc`: ghremote () { url="$1" proj=${url##*/} url_=${url%/*} login=${url_##*/} git remote add gh-$login $url git fetch gh-$login } thus you could simply run: ghremote git@github.com:YourLogin/duecredit.git to add the above `gh-YourLogin` remote. 3. Create a branch (generally off the `origin/master`) to hold your changes: git checkout -b nf-my-feature and start making changes. Ideally, use a prefix signaling the purpose of the branch - `nf-` for new features - `bf-` for bug fixes - `rf-` for refactoring - `doc-` for documentation contributions (including in the code docstrings). We recommend to not work in the ``master`` branch! 4. Work on this copy on your computer using Git to do the version control. When you're done editing, do: git add modified_files git commit to record your changes in Git. Ideally, prefix your commit messages with the `NF`, `BF`, `RF`, `DOC` similar to the branch name prefixes, but you could also use `TST` for commits concerned solely with tests, and `BK` to signal that the commit causes a breakage (e.g. of tests) at that point. Multiple entries could be listed joined with a `+` (e.g. `rf+doc-`). See `git log` for examples. If a commit closes an existing DueCredit issue, then add to the end of the message `(Closes #ISSUE_NUMER)` 5. Push to GitHub with: git push -u gh-YourLogin nf-my-feature Finally, go to the web page of your fork of the DueCredit repo, and click 'Pull request' (PR) to send your changes to the maintainers for review. This will send an email to the committers. You can commit new changes to this branch and keep pushing to your remote -- github automagically adds them to your previously opened PR. (If any of the above seems like magic to you, then look up the [Git documentation](http://git-scm.com/documentation) on the web.) Quality Assurance ----------------- It is recommended to check that your contribution complies with the following rules before submitting a pull request: - All public methods should have informative docstrings with sample usage presented as doctests when appropriate. - All other tests pass when everything is rebuilt from scratch. - New code should be accompanied by tests. ### Tests All tests are available under `duecredit/tests`. To execute tests, the codebase needs to be "installed" in order to generate scripts for the entry points. For that, the recommended course of action is to use `virtualenv`, e.g. ```sh virtualenv --system-site-packages venv-tests source venv-tests/bin/activate pip install -e '.[tests]' ``` On Debian-based systems you might need to install some C-libraries to guarantee installation (building) of some Python modules we use. So for `lxml` please first ```sh sudo apt-get install libxml2-dev libxslt1-dev ``` On Mac OS X Yosemite additional steps are required to make `lxml` work properly (see [this stackoverflow answer](https://stackoverflow.com/questions/19548011/cannot-install-lxml-on-mac-os-x-10-9/26544099#26544099?newreg=d3394d8210cc4779accfac05fe5c9b21)). We recommend using homebrew to install the same dependencies (see the [Homebrew website](http://brew.sh/) to install it), then run ```sh brew install libxml2 libxslt brew link libxml2 --force brew link libxslt --force ``` note that this will override the default libraries installed with Mac OS X. Then use that virtual environment to run the tests, via ```sh python -m nose -s -v duecredit ``` or similarly, ```sh nosetests -s -v duecredit ``` then to later deactivate the virtualenv just simply enter ```sh deactivate ``` ### Coverage You can also check for common programming errors with the following tools: - Code with good unittest coverage (at least 80%), check with: pip install nose coverage nosetests --with-coverage duecredit ### Linting We are not (yet) fully PEP8 compliant, so please use these tools as guidelines for your contributions, but not to PEP8 entire code base. [beyond-pep8]: https://www.youtube.com/watch?v=wf-BqAjZb8M *Sidenote*: watch [Raymond Hettinger - Beyond PEP 8][beyond-pep8] - No pyflakes warnings, check with: pip install pyflakes pyflakes path/to/module.py - No PEP8 warnings, check with: pip install pep8 pep8 path/to/module.py - AutoPEP8 can help you fix some of the easy redundant errors: pip install autopep8 autopep8 path/to/pep8.py Also, some team developers use [PyCharm community edition](https://www.jetbrains.com/pycharm) which provides built-in PEP8 checker and handy tools such as smart splits/joins making it easier to maintain code following the PEP8 recommendations. NeuroDebian provides `pycharm-community-sloppy` package to ease pycharm installation even further. Easy Issues ----------- A great way to start contributing to DueCredit is to pick an item from the list of [Easy issues](https://github.com/duecredit/duecredit/labels/easy) in the issue tracker. Resolving these issues allows you to start contributing to the project without much prior knowledge. Your assistance in this area will be greatly appreciated by the more experienced developers as it helps free up their time to concentrate on other issues. duecredit-0.6.0/LICENSE000066400000000000000000000030421273067304300144760ustar00rootroot00000000000000Copyright 2015 Yaroslav Halchenko, Matteo Visconti di Oleggio Castello. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the copyright holder. duecredit-0.6.0/MANIFEST.in000066400000000000000000000002751273067304300152340ustar00rootroot00000000000000include LICENSE README.md CHANGELOG.md requirements.txt tox.ini CONTRIBUTING.md setup.cfg include .coveragerc .travis.yml include examples/example_scipy.py include duecredit/tests/envs/*/* duecredit-0.6.0/README.md000066400000000000000000000266421273067304300147630ustar00rootroot00000000000000duecredit ========= [![Build Status](https://travis-ci.org/duecredit/duecredit.svg?branch=master)](https://travis-ci.org/duecredit/duecredit) [![Coverage Status](https://coveralls.io/repos/duecredit/duecredit/badge.svg)](https://coveralls.io/r/duecredit/duecredit) duecredit is being conceived to address the problem of inadequate citation of scientific software and methods, and limited visibility of donation requests for open-source software. It provides a simple framework (at the moment for Python only) to embed publication or other references in the original code so they are automatically collected and reported to the user at the necessary level of reference detail, i.e. only references for actually used functionality will be presented back if software provides multiple citeable implementations. duecredit 101 ============= You can already start "registering" citations using duecredit in your Python modules and even registering citations (we call this approach "injections") for modules which do not (yet) use duecredit. duecredit will remain an optional dependency, i.e. your software will work correctly even without duecredit installed. "Native" use of duecredit (recommended) --------------------------------------- For using duecredit in your software 1. copy `duecredit/stub.py` to your codebase, e.g. wget -q -O /path/tomodule/yourmodule/due.py \ https://raw.githubusercontent.com/duecredit/duecredit/master/duecredit/stub.py **Note** that it might be better to avoid naming it duecredit.py to avoid shadowing installed duecredit. 2. Then use `duecredit` import due and necessary entries in your code as from .due import due, Doi, BibTeX to provide reference for the entire module just use e.g. due.cite(Doi("1.2.3/x.y.z"), description="Solves all your problems", path="magicpy") To provide a reference for a function or a method, use dcite decorator @due.dcite(Doi("1.2.3/x.y.z"), description="Resolves constipation issue") def pushit(): ... References can also be entered as BibTeX entries due.cite(BibTeX(""" @article{mynicearticle, title={A very cool paper}, author={Happy, Author and Lucky, Author}, journal={The Journal of Serendipitous Discoveries} } """), description="Solves all your problems", path="magicpy") Add injections for other existing modules ----------------------------------------- We hope that eventually this somewhat cruel approach will not be necessary. But until other packages support duecredit "natively" we have provided a way to "inject" citations for modules and/or functions and methods via injections: citations will be added to the corresponding functionality upon those modules import. All injections are collected under [duecredit/injections](https://github.com/duecredit/duecredit/tree/master/duecredit/injections). See any file there with `mod_` prefix for a complete example. But overall it is just a regular Python module defining a function `inject(injector)` which will then add new entries to the injector, which will in turn add those entries to the duecredit whenever the corresponding module gets imported. User-view --------- By default `duecredit` does exactly nothing -- all decorators do not decorate, all `cite` functions just return, so there should be no fear that it would break anything. Then whenever anyone runs their analysis which uses your code and sets `DUECREDIT_ENABLE=yes` environment variable or uses `python -m duecredit`, and invokes any of the cited function/methods, at the end of the run all collected bibliography will be presented to the screen and pickled into `.duecredit.p` file in current directory: $> python -m duecredit examples/example_scipy.py I: Simulating 4 blobs I: Done clustering 4 blobs DueCredit Report: - Scientific tools library / numpy (v 1.10.4) [1] - Scientific tools library / scipy (v 0.14) [2] - Single linkage hierarchical clustering / scipy.cluster.hierarchy:linkage (v 0.14) [3] 2 packages cited 0 modules cited 1 function cited References ---------- [1] Van Der Walt, S., Colbert, S.C. & Varoquaux, G., 2011. The NumPy array: a structure for efficient numerical computation. Computing in Science & Engineering, 13(2), pp.22–30. [2] Jones, E. et al., 2001. SciPy: Open source scientific tools for Python. [3] Sibson, R., 1973. SLINK: an optimally efficient algorithm for the single-link cluster method. The Computer Journal, 16(1), pp.30–34. Incremental runs of various software would keep enriching that file. Then you can use `duecredit summary` command to show that information again (stored in `.duecredit.p` file) or export it as a BibTeX file ready for reuse, e.g.: $> duecredit summary --format=bibtex @article{van2011numpy, title={The NumPy array: a structure for efficient numerical computation}, author={Van Der Walt, Stefan and Colbert, S Chris and Varoquaux, Gael}, journal={Computing in Science \& Engineering}, volume={13}, number={2}, pages={22--30}, year={2011}, publisher={AIP Publishing} } @Misc{JOP+01, author = {Eric Jones and Travis Oliphant and Pearu Peterson and others}, title = {{SciPy}: Open source scientific tools for {Python}}, year = {2001--}, url = "http://www.scipy.org/", note = {[Online; accessed 2015-07-13]} } @article{sibson1973slink, title={SLINK: an optimally efficient algorithm for the single-link cluster method}, author={Sibson, Robin}, journal={The Computer Journal}, volume={16}, number={1}, pages={30--34}, year={1973}, publisher={Br Computer Soc} } and if by default only references for "implementation" are listed, we can enable listing of references for other tags as well (e.g. "edu" depicting instructional materials -- textbooks etc on the topic): $> DUECREDIT_REPORT_TAGS=* duecredit summary DueCredit Report: - Scientific tools library / numpy (v 1.10.4) [1] - Scientific tools library / scipy (v 0.14) [2] - Hierarchical clustering / scipy.cluster.hierarchy (v 0.14) [3, 4, 5, 6, 7, 8, 9] - Single linkage hierarchical clustering / scipy.cluster.hierarchy:linkage (v 0.14) [10, 11] 2 packages cited 1 module cited 1 function cited References ---------- [1] Van Der Walt, S., Colbert, S.C. & Varoquaux, G., 2011. The NumPy array: a structure for efficient numerical computation. Computing in Science & Engineering, 13(2), pp.22–30. [2] Jones, E. et al., 2001. SciPy: Open source scientific tools for Python. [3] Sneath, P.H. & Sokal, R.R., 1962. Numerical taxonomy. Nature, 193(4818), pp.855–860. [4] Batagelj, V. & Bren, M., 1995. Comparing resemblance measures. Journal of classification, 12(1), pp.73–90. [5] Sokal, R.R., Michener, C.D. & University of Kansas, 1958. A Statistical Method for Evaluating Systematic Relationships, University of Kansas. [6] Jain, A.K. & Dubes, R.C., 1988. Algorithms for clustering data, Prentice-Hall, Inc.. [7] Johnson, S.C., 1967. Hierarchical clustering schemes. Psychometrika, 32(3), pp.241–254. [8] Edelbrock, C., 1979. Mixture model tests of hierarchical clustering algorithms: the problem of classifying everybody. Multivariate Behavioral Research, 14(3), pp.367–384. [9] Fisher, R.A., 1936. The use of multiple measurements in taxonomic problems. Annals of eugenics, 7(2), pp.179–188. [10] Gower, J.C. & Ross, G., 1969. Minimum spanning trees and single linkage cluster analysis. Applied statistics, pp.54–64. [11] Sibson, R., 1973. SLINK: an optimally efficient algorithm for the single-link cluster method. The Computer Journal, 16(1), pp.30–34. The `DUECREDIT_REPORT_ALL` flag allows one to output all the references for the modules that lack objects or functions with citations. Compared to the previous example, the following output additionally shows a reference for scikit-learn since `example_scipy.py` uses an uncited function from that package. $> DUECREDIT_REPORT_TAGS=* DUECREDIT_REPORT_ALL=1 duecredit summary DueCredit Report: - Scientific tools library / numpy (v 1.10.4) [1] - Scientific tools library / scipy (v 0.14) [2] - Hierarchical clustering / scipy.cluster.hierarchy (v 0.14) [3, 4, 5, 6, 7, 8, 9] - Single linkage hierarchical clustering / scipy.cluster.hierarchy:linkage (v 0.14) [10, 11] - Machine Learning library / sklearn (v 0.15.2) [12] 3 packages cited 1 module cited 1 function cited References ---------- [1] Van Der Walt, S., Colbert, S.C. & Varoquaux, G., 2011. The NumPy array: a structure for efficient numerical computation. Computing in Science & Engineering, 13(2), pp.22–30. [2] Jones, E. et al., 2001. SciPy: Open source scientific tools for Python. [3] Sneath, P.H. & Sokal, R.R., 1962. Numerical taxonomy. Nature, 193(4818), pp.855–860. ... Ultimate goals ============== Reduce demand for prima ballerina projects ------------------------------------------ **Problem**: Scientific software is often developed to gain citations for original publication through the use of the software implementing it. Unfortunately such established procedure discourages contributions to existing projects and fosters new projects to be developed from scratch. **Solution**: With easy ways to provide all-and-only relevant references for used functionality within a large(r) framework, scientific developers will prefer to contribute to already existing projects. **Benefits**: As a result, scientific developers will immediately benefit from adhering to proper development procedures (codebase structuring, testing, etc) and already established delivery and deployment channels existing projects already have. This will increase efficiency and standardization of scientific software development, thus addressing many (if not all) core problems with scientific software development everyone likes to bash about (reproducibility, longevity, etc.). Adequately reference core libraries ----------------------------------- **Problem**: Scientific software often, if not always, uses 3rd party libraries (e.g., NumPy, SciPy, atlas) which might not even be visible at the user level. Therefore they are rarely referenced in the publications despite providing the fundamental core for solving a scientific problem at hands. **Solution**: With automated bibliography compilation for all used libraries, such projects and their authors would get a chance to receive adequate citability. **Benefits**: Adequate appreciation of the scientific software developments. Coupled with a solution for "prima ballerina" problem, more contributions will flow into the core/foundational projects making new methodological developments readily available to even wider audiences without proliferation of the low quality scientific software. Similar/related projects ======================== [sempervirens](https://github.com/njsmith/sempervirens) -- *an experimental prototype for gathering anonymous, opt-in usage data for open scientific software*. Eventually in duecredit we aim either to provide similar functionality (since we are collecting such information as well) or just interface/report to sempervirens. duecredit-0.6.0/appveyor.yml000066400000000000000000000026051273067304300160650ustar00rootroot00000000000000environment: matrix: - PYTHON: "C:\\Python27" PYTHON_VERSION: "2.7.8" PYTHON_ARCH: "32" MINICONDA: C:\Miniconda # TODO: # Reenable whenever lxml whl becomes available for those # otherwise it is a pain # # - PYTHON: "C:\\Python33" # PYTHON_VERSION: "3.3.5" # PYTHON_ARCH: "32" # MINICONDA: C:\Miniconda3 # # - PYTHON: "C:\\Python34" # PYTHON_VERSION: "3.4.1" # PYTHON_ARCH: "32" # MINICONDA: C:\Miniconda3 # # - PYTHON: "C:\\Python35" # PYTHON_VERSION: "3.5.1" # PYTHON_ARCH: "32" # MINICONDA: C:\Miniconda35 init: - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH% %MINICONDA%" # Not a .NET project, we build in the install step instead build: false install: - "set PATH=%MINICONDA%;%MINICONDA%\\Scripts;%PATH%" - conda config --set always_yes yes --set changeps1 no - conda update -q conda - conda info -a - "conda create -q -n test-environment python=%PYTHON_VERSION% mock contextlib2 nose coverage requests six" - activate test-environment - pip install -r requirements.txt - pip install -e . test_script: - nosetests --with-doctest -v duecredit - python setup.py install # for interactive debugging upon completion (have 30 min to react) #on_finish: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) duecredit-0.6.0/duecredit/000077500000000000000000000000001273067304300154425ustar00rootroot00000000000000duecredit-0.6.0/duecredit/__init__.py000066400000000000000000000013251273067304300175540ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Module/app to automate collection of relevant to analysis publications. Please see README.md shipped along with duecredit to get a better idea about its functionality """ from .entries import Doi, BibTeX, Url from .version import __version__, __release_date__ from .dueswitch import due __all__ = ['Doi', 'BibTeX', 'Url', 'due']duecredit-0.6.0/duecredit/__main__.py000066400000000000000000000055471273067304300175470ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Helper to use duecredit as a "runnable" module with -m duecredit""" import sys from . import due, __version__ from .log import lgr def usage(outfile, executable=sys.argv[0]): if '__main__.py' in executable: # That was -m duecredit way to launch executable = "%s -m duecredit" % sys.executable outfile.write("""Usage: %s [OPTIONS] [ARGS] Meta-options: --help Display this help then exit. --version Output version information then exit. """ % executable) def runctx(cmd, globals=None, locals=None): if globals is None: globals = {} if locals is None: locals = {} try: exec(cmd, globals, locals) finally: # good opportunity to avoid atexit I guess. pass for now pass def main(argv=None): import os import getopt if argv is None: argv = sys.argv try: opts, prog_argv = getopt.getopt(argv[1:], "", ["help", "version"]) # TODO: support options for whatever we would support ;) # probably needs to hook in somehow into commands/options available # under cmdline/ except getopt.error as msg: sys.stderr.write("%s: %s\n" % (sys.argv[0], msg)) sys.stderr.write("Try `%s --help' for more information\n" % sys.argv[0]) sys.exit(1) # and now we need to execute target script "manually" # Borrowing up on from trace.py for opt, val in opts: if opt == "--help": usage(sys.stdout, executable=argv[0]) sys.exit(0) if opt == "--version": sys.stdout.write("duecredit %s\n" % __version__) sys.exit(0) sys.argv = prog_argv progname = prog_argv[0] sys.path[0] = os.path.split(progname)[0] try: with open(progname) as fp: code = compile(fp.read(), progname, 'exec') # try to emulate __main__ namespace as much as possible globs = { '__file__': progname, '__name__': '__main__', '__package__': None, '__cached__': None, } # Since used explicitly -- activate the beast due.activate(True) runctx(code, globs, globs) # TODO: see if we could hide our presence from the final tracebacks if execution fails except IOError as err: lgr.error("Cannot run file %r because: %s" % (sys.argv[0], err)) sys.exit(1) except SystemExit: pass if __name__ == '__main__': main()duecredit-0.6.0/duecredit/cmdline/000077500000000000000000000000001273067304300170555ustar00rootroot00000000000000duecredit-0.6.0/duecredit/cmdline/__init__.py000066400000000000000000000007451273067304300211740ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ """ __docformat__ = 'restructuredtext' from . import cmd_summary from . import cmd_test duecredit-0.6.0/duecredit/cmdline/cmd_summary.py000066400000000000000000000033171273067304300217530ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Spit out the summary of the citations which were collected. """ import sys import os from ..log import lgr from ..config import DUECREDIT_FILE from ..collector import CollectorSummary from ..io import TextOutput, BibTeXOutput __docformat__ = 'restructuredtext' # magic line for manpage summary # man: -*- % summary of collected citations def setup_parser(parser): parser.add_argument( "-f", "--filename", default=DUECREDIT_FILE, help="Filename containing collected citations. Default: %(default)s") parser.add_argument( "--style", choices=("apa", "harvard1"), default="harvard1", help="Style to be used for listing citations") parser.add_argument( "--format", choices=("text", "bibtex"), default="text", help="Way to present the summary") def run(args): from ..io import PickleOutput if not os.path.exists(args.filename): lgr.debug("File {0} doesn't exist. No summary available".format( args.filename)) return 1 due = PickleOutput.load(args.filename) #CollectorSummary(due).dump() if args.format == "text": out = TextOutput(sys.stdout, due, args.style) elif args.format == "bibtex": out = BibTeXOutput(sys.stdout, due) else: raise ValueError("unknown to treat %s" % args.format) out.dump() duecredit-0.6.0/duecredit/cmdline/cmd_test.py000066400000000000000000000015241273067304300212330ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Run internal DueCredit (unit)tests to verify correct operation on the system""" __docformat__ = 'restructuredtext' # magic line for manpage summary # man: -*- % run unit-tests from .helpers import parser_add_common_args def setup_parser(parser): # TODO -- pass options such as verbosity etc pass def run(args): import duecredit import nose raise NotImplementedError("Just use nosetests duecredit for now") #duecredit.test() duecredit-0.6.0/duecredit/cmdline/common_args.py000066400000000000000000000024401273067304300217330ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ """ __docformat__ = 'restructuredtext' # argument spec template # = ( # , # {} #) from ..cmdline.helpers import HelpAction, LogLevelAction help = ( 'help', ('-h', '--help', '--help-np'), dict(nargs=0, action=HelpAction, help="""show this help message and exit. --help-np forcefully disables the use of a pager for displaying the help.""") ) version = ( 'version', ('--version',), dict(action='version', help="show program's version and license information and exit") ) log_level = ( 'log-level', ('-l', '--log-level'), dict(action=LogLevelAction, choices=['critical', 'error', 'warning', 'info', 'debug'] + [str(x) for x in range(1, 10)], default='warning', help="""level of verbosity. Integers provide even more debugging information""") ) duecredit-0.6.0/duecredit/cmdline/helpers.py000066400000000000000000000072731273067304300211020ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ """ __docformat__ = 'restructuredtext' import argparse import re import sys from ..utils import is_interactive class HelpAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): # import pydb; pydb.debugger() if is_interactive() and option_string == '--help': # lets use the manpage on mature systems ... try: import subprocess subprocess.check_call( 'man %s 2> /dev/null' % parser.prog.replace(' ', '-'), shell=True) sys.exit(0) except (subprocess.CalledProcessError, OSError): # ...but silently fall back if it doesn't work pass if option_string == '-h': helpstr = "%s\n%s" \ % (parser.format_usage(), "Use '--help' to get more comprehensive information.") else: helpstr = parser.format_help() # better for help2man helpstr = re.sub(r'optional arguments:', 'options:', helpstr) # yoh: TODO for duecredit + help2man #helpstr = re.sub(r'positional arguments:\n.*\n', '', helpstr) # convert all heading to have the first character uppercase headpat = re.compile(r'^([a-z])(.*):$', re.MULTILINE) helpstr = re.subn(headpat, lambda match: r'{0}{1}:'.format(match.group(1).upper(), match.group(2)), helpstr)[0] # usage is on the same line helpstr = re.sub(r'^usage:', 'Usage:', helpstr) if option_string == '--help-np': usagestr = re.split(r'\n\n[A-Z]+', helpstr, maxsplit=1)[0] usage_length = len(usagestr) usagestr = re.subn(r'\s+', ' ', usagestr.replace('\n', ' '))[0] helpstr = '%s\n%s' % (usagestr, helpstr[usage_length:]) print(helpstr) sys.exit(0) class LogLevelAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): from ..log import LoggerHelper LoggerHelper().set_level(level=values) def parser_add_common_args(parser, pos=None, opt=None, **kwargs): from . import common_args for i, args in enumerate((pos, opt)): if args is None: continue for arg in args: arg_tmpl = getattr(common_args, arg) arg_kwargs = arg_tmpl[2].copy() arg_kwargs.update(kwargs) if i: parser.add_argument(*arg_tmpl[i], **arg_kwargs) else: parser.add_argument(arg_tmpl[i], **arg_kwargs) def parser_add_common_opt(parser, opt, names=None, **kwargs): from . import common_args opt_tmpl = getattr(common_args, opt) opt_kwargs = opt_tmpl[2].copy() opt_kwargs.update(kwargs) if names is None: parser.add_argument(*opt_tmpl[1], **opt_kwargs) else: parser.add_argument(*names, **opt_kwargs) class RegexpType(object): """Factory for creating regular expression types for argparse DEPRECATED AFAIK -- now things are in the config file... but we might provide a mode where we operate solely from cmdline """ def __call__(self, string): if string: return re.compile(string) else: return None duecredit-0.6.0/duecredit/cmdline/main.py000066400000000000000000000170261273067304300203610ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. Originates from datalad package distributed # under MIT license # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """""" __docformat__ = 'restructuredtext' import argparse import logging import os import sys import textwrap from .. import __version__ from ..log import lgr import duecredit.cmdline as duecmd from . import helpers from ..utils import setup_exceptionhook def _license_info(): return """\ Copyright 2015-2016 Yaroslav Halchenko, Matteo Visconti di Oleggio Castello. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the copyright holder. """ def get_commands(): return sorted([c for c in dir(duecmd) if c.startswith('cmd_')]) def setup_parser(): # setup cmdline args parser # main parser parser = argparse.ArgumentParser( fromfile_prefix_chars='@', # usage="%(prog)s ...", description="""\ DueCredit simplifies citation of papers describing methods, software and data used by any given analysis script/pipeline. """, epilog='"Your Credit is Due"', formatter_class=argparse.RawDescriptionHelpFormatter, add_help=False ) # common options helpers.parser_add_common_opt(parser, 'help') helpers.parser_add_common_opt(parser, 'log_level') helpers.parser_add_common_opt(parser, 'version', version='duecredit %s\n\n%s' % (__version__, _license_info())) if __debug__: parser.add_argument( '--dbg', action='store_true', dest='common_debug', help="do not catch exceptions and show exception traceback") # yoh: atm we only dump to console. Might adopt the same separation later on # and for consistency will call it --verbose-level as well for now # log-level is set via common_opts ATM # parser.add_argument('--log-level', # choices=('critical', 'error', 'warning', 'info', 'debug'), # dest='common_log_level', # help="""level of verbosity in log files. By default # everything, including debug messages is logged.""") #parser.add_argument('-l', '--verbose-level', # choices=('critical', 'error', 'warning', 'info', 'debug'), # dest='common_verbose_level', # help="""level of verbosity of console output. By default # only warnings and errors are printed.""") # subparsers subparsers = parser.add_subparsers() # for all subcommand modules it can find cmd_short_description = [] for cmd in get_commands(): cmd_name = cmd[4:] subcmdmod = getattr(__import__('duecredit.cmdline', globals(), locals(), [cmd], 0), cmd) # deal with optional parser args if 'parser_args' in subcmdmod.__dict__: parser_args = subcmdmod.parser_args else: parser_args = dict() # use module description, if no explicit description is available if not 'description' in parser_args: parser_args['description'] = subcmdmod.__doc__ # create subparser, use module suffix as cmd name subparser = subparsers.add_parser(cmd_name, add_help=False, **parser_args) # all subparser can report the version helpers.parser_add_common_opt( subparser, 'version', version='duecredit %s %s\n\n%s' % (cmd_name, __version__, _license_info())) # our own custom help for all commands helpers.parser_add_common_opt(subparser, 'help') helpers.parser_add_common_opt(subparser, 'log_level') # let module configure the parser subcmdmod.setup_parser(subparser) # logger for command # configure 'run' function for this command subparser.set_defaults(func=subcmdmod.run, logger=logging.getLogger('duecredit.%s' % cmd)) # store short description for later sdescr = getattr(subcmdmod, 'short_description', parser_args['description'].split('\n')[0]) cmd_short_description.append((cmd_name, sdescr)) # create command summary cmd_summary = [] for cd in cmd_short_description: cmd_summary.append('%s\n%s\n\n' \ % (cd[0], textwrap.fill(cd[1], 75, initial_indent=' ' * 4, subsequent_indent=' ' * 4))) parser.description = '%s\n%s\n\n%s' \ % (parser.description, '\n'.join(cmd_summary), textwrap.fill("""\ Detailed usage information for individual commands is available via command-specific help options, i.e.: %s --help""" % sys.argv[0], 75, initial_indent='', subsequent_indent='')) return parser def main(args=None): parser = setup_parser() # parse cmd args args = parser.parse_args(args) # run the function associated with the selected command if args.common_debug or os.environ.get('DUECREDIT_DEBUG', None): # So we could see/stop clearly at the point of failure setup_exceptionhook() args.func(args) else: # Otherwise - guard and only log the summary. Postmortem is not # as convenient if being caught in this ultimate except try: args.func(args) except Exception as exc: lgr.error('%s (%s)' % (str(exc), exc.__class__.__name__)) sys.exit(1) duecredit-0.6.0/duecredit/collector.py000066400000000000000000000421541273067304300200100ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Citation and citations Collector classes""" import os import sys from functools import wraps from six import iteritems, itervalues from .config import DUECREDIT_FILE from .entries import DueCreditEntry from .stub import InactiveDueCreditCollector from .io import TextOutput, PickleOutput from .utils import never_fail, borrowdoc from .versions import external_versions from collections import namedtuple import logging lgr = logging.getLogger('duecredit.collector') CitationKey = namedtuple('CitationKey', ['path', 'entry_key']) class Citation(object): """Encapsulates citations and information on their use""" def __init__(self, entry, description=None, path=None, version=None, cite_module=False, tags=['implementation']): """Cite a reference Parameters ---------- entry: str or DueCreditEntry The entry to use, either identified by its id or a new one (to be added) description: str, optional Description of what this functionality provides path: str Path to the object which this citation associated with. Format is "module[.submodules][:[class.]method]", i.e. ":" is used to separate module path from the path within the module. version: str or tuple, version Version of the beast (e.g. of the module) where applicable cite_module: bool, optional If it is a module citation, setting it to True would make that module citeable even without internal duecredited functionality invoked. Should be used only for core packages whenever it is reasonable to assume that its import constitute its use (e.g. numpy) tags: list of str, optional Tags to associate with the given code/reference combination. Some tags have associated semantics in duecredit, e.g. - "implementation" [default] tag describes as an implementation of the cited method - "reference-implementation" tag describes as the original implementation (ideally by the authors of the paper) of the cited method - "another-implementation" tag describes some other implementation of the method - "use" tag points to publications demonstrating a worthwhile noting use the method - "edu" references to tutorials, textbooks and other materials useful to learn more - "donate" should be commonly used with Url entries to point to the websites describing how to contribute some funds to the referenced project """ if path is None: raise ValueError('Must specify path') self._entry = entry self._description = description # We might want extract all the relevant functionality into a separate class self._path = path self._cite_module = cite_module self.tags = tags or [] self.version = version self.count = 0 def __repr__(self): args = [repr(self._entry)] if self._description: args.append("description={0}".format(repr(self._description))) if self._path: args.append("path={0}".format(repr(self._path))) if self._cite_module: args.append("cite_module={0}".format(repr(self._cite_module))) if args: args = ", ".join(args) else: args = "" return self.__class__.__name__ + '({0})'.format(args) @property def path(self): return self._path @property def cite_module(self): return self._cite_module @path.setter def path(self, path): # TODO: verify value, if we are not up for it -- just make _path public self._path = path @property def entry(self): return self._entry @property def description(self): return self._description @property def cites_module(self): if not self.path: return None return not (':' in self.path) @property def module(self): if not self.path: return None return self.path.split(':', 1)[0] @property def package(self): module = self.module if not module: return None return module.split('.', 1)[0] @property def objname(self): if not self.path: return None spl = self.path.split(':', 1) if len(spl) > 1: return spl[1] else: return None def __contains__(self, entry): """Checks if provided entry 'contained' in this one given its path If current entry is associated with a module, contained will be an entry of - the same module - submodule of the current module or function within If current entry is associated with a specific function/class, it can contain another entry if it really contains it as an attribute """ if self.cites_module: return ((self.path == entry.path) or (entry.path.startswith(self.path + '.')) or (entry.path.startswith(self.path + ':'))) else: return entry.path.startswith(self.path + '.') @property def key(self): return CitationKey(self.path, self.entry.key) @staticmethod def get_key(path, entry_key): return CitationKey(path, entry_key) def set_entry(self, newentry): self._entry = newentry class DueCreditCollector(object): """Collect the references The mighty beast which will might become later a proxy on the way to talk to a real collector Parameters ---------- entries : list of DueCreditEntry, optional List of reference items (BibTeX, Doi, etc) known to the collector citations : list of Citation, optional List of citations -- associations between references and particular code, with a description for its use, tags etc """ # TODO? rename "entries" to "references"? or "references" is closer to "citations" def __init__(self, entries=None, citations=None): self._entries = entries or {} self.citations = citations or {} @never_fail def add(self, entry): """entry should be a DueCreditEntry object""" if isinstance(entry, list): for e in entry: self.add(e) else: key = entry.get_key() self._entries[key] = entry lgr.log(1, "Collector added entry %s", key) @never_fail def load(self, src): """Loads references from a file or other recognizable source ATM supported only - .bib files """ # raise NotImplementedError if isinstance(src, str): if src.endswith('.bib'): self._load_bib(src) else: raise NotImplementedError('Format not yet supported') else: raise ValueError('Must be a string') def _load_bib(self, src): lgr.debug("Loading %s" % src) # # TODO: figure out what would be the optimal use for the __call__ # def __call__(self, *args, **kwargs): # # TODO: how to determine and cite originating module??? # # - we could use inspect but many people complain # # that it might not work with other Python # # implementations # pass # raise NotImplementedError @never_fail @borrowdoc(Citation, "__init__") def cite(self, entry, **kwargs): # TODO: if cite is invoked but no path is provided -- we must figure it out # I guess from traceback, otherwise how would we know later to associate it # with modules??? path = kwargs.get('path', None) if path is None: raise ValueError('path must be provided') if isinstance(entry, DueCreditEntry): # new one -- add it self.add(entry) entry_ = self._entries[entry.get_key()] else: entry_ = self._entries[entry] entry_key = entry_.get_key() citation_key = Citation.get_key(path=path, entry_key=entry_key) try: citation = self.citations[citation_key] except KeyError: self.citations[citation_key] = citation = Citation(entry_, **kwargs) assert(citation.key == citation_key) # update citation count citation.count += 1 # TODO: theoretically version shouldn't differ if we don't preload previous results if not citation.version: version = kwargs.get('version', None) if not version and citation.path: modname = citation.path.split('.', 1)[0] if '.' in modname: package = modname.split('.', 1)[0] else: package = modname # package_loaded = sys.modules.get(package) # if package_loaded: # # find the citation for that module # for citation in itervalues(self.citations): # if citation.package == package \ # and not citation.version: version = external_versions[package] citation.version = version return citation def _citations_fromentrykey(self): """Return a dictionary with the current citations indexed by the entry key""" citations_key = dict() for (path, entry_key), citation in iteritems(self.citations): if entry_key not in citations_key: citations_key[entry_key] = citation return citations_key @staticmethod def _args_match_conditions(conditions, *fargs, **fkwargs): """Helper to identify when to trigger citation given parameters to the function call """ for (argpos, kwarg), values in iteritems(conditions): # main logic -- assess default and if get to the next one if # given argument is not present if not ((len(fargs) > argpos) or (kwarg in fkwargs)): if not ('DC_DEFAULT' in values): # if value was specified but not provided and not default # conditions are not satisfied return False continue # "extract" the value. Must be defined here value = "__duecredit_magical_undefined__" if len(fargs) > argpos: value = fargs[argpos] if kwarg in fkwargs: value = fkwargs[kwarg] assert(value != "__duecredit_magical_undefined__") if '.' in kwarg: # we were requested to condition based on the value of the attribute # of the value. So get to the attribute(s) value for attr in kwarg.split('.')[1:]: value = getattr(value, attr) # Value is present but not matching if not (value in values): return False # if checks passed -- we must have matched conditions return True @never_fail @borrowdoc(Citation, "__init__", replace="PLUGDOCSTRING") def dcite(self, *args, **kwargs): """Decorator for references. PLUGDOCSTRING Parameters ---------- conditions: dict, optional If reference should be cited whenever parameters to the function call satisfy given values (all of the specified). Each key in the dictionary is a 2 element tuple with first element, integer, pointing to a position of the argument in the original function call signature, while second provides the name, thus if used as a keyword argument. Use "DC_DEFAULT" keyword as a value to depict default value (e.g. if no explicit value was provided for that positional or keyword argument). If "keyword argument" is of the form "obj.attr1.attr2", then actual value for comparison would be taken by extracting attr1 (and then attr2) attributes from the provided value. So, if desired to condition of the state of the object, you can use `(0, "self.attr1") : {...values...}` Examples -------- >>> from duecredit import due >>> @due.dcite('XXX00', description="Provides an answer for meaningless existence") ... def purpose_of_life(): ... return None Conditional citation given argument to the function >>> @due.dcite('XXX00', description="Relief through the movement", ... conditions={(1, 'method'): {'purge', 'DC_DEFAULT'}}) ... @due.dcite('XXX01', description="Relief through the drug treatment", ... conditions={(1, 'method'): {'drug'}}) ... def relief(x, method='purge'): ... if method == 'purge': return "crap" ... elif method == 'drug': return "swallow" >>> relief("doesn't matter") 'crap' Conditional based on the state of the object >>> class Citeable(object): ... def __init__(self, param=None): ... self.param = param ... @due.dcite('XXX00', description="The same good old relief", ... conditions={(0, 'self.param'): {'magic'}}) ... def __call__(self, data): ... return sum(data) >>> Citeable('magic')([1, 2]) 3 """ def func_wrapper(func): conditions = kwargs.pop('conditions', {}) path = kwargs.get('path', None) if not path: # deduce path from the actual function which was decorated # TODO: must include class name but can't !!!??? modname = func.__module__ path = kwargs['path'] = '%s:%s' % (modname, func.__name__) else: # TODO: we indeed need to separate path logic outside modname = path.split(':', 1)[0] # if decorated function was invoked, it means that we need # to cite that even if it is a module. But do not override # value if user explicitly stated if 'cite_module' not in kwargs: kwargs['cite_module'] = True # TODO: might make use of inspect.getmro # see e.g. # http://stackoverflow.com/questions/961048/get-class-that-defined-method lgr.debug("Decorating func %s within module %s" % (func.__name__, modname)) # TODO: unittest for all the __version__ madness # TODO: check if we better use wrapt module which provides superior "correctness" # of decorating. vcrpy uses wrapt, and that thing seems to wrap @wraps(func) def cite_wrapper(*fargs, **fkwargs): try: if not conditions \ or self._args_match_conditions(conditions, *fargs, **fkwargs): citation = self.cite(*args, **kwargs) except Exception as e: lgr.warning("Failed to cite due to %s" % (e,)) return func(*fargs, **fkwargs) cite_wrapper.__duecredited__ = func return cite_wrapper return func_wrapper @never_fail def __repr__(self): args = [] if self.citations: args.append("citations={0}".format(repr(self.citations))) if self._entries: args.append("entries={0}".format(repr(self._entries))) if args: args = ", ".join(args) else: args = "" return self.__class__.__name__ + '({0})'.format(args) @never_fail def __str__(self): return self.__class__.__name__ + \ ' {0:d} entries, {1:d} citations'.format( len(self._entries), len(self.citations)) # TODO: redo heavily -- got very messy class CollectorSummary(object): """A helper which would take care about exporting citations upon its Death """ def __init__(self, collector, outputs="stdout,pickle", fn=DUECREDIT_FILE): self._due = collector self.fn = fn # for now decide on output "format" right here self._outputs = [ self._get_output_handler( type_.lower().strip(), collector, fn=fn) for type_ in os.environ.get('DUECREDIT_OUTPUTS', outputs).split(',') if type_ ] @staticmethod def _get_output_handler(type_, collector, fn=None): # just a little factory if type_ in ("stdout", "stderr"): return TextOutput(getattr(sys, type_), collector) elif type_ == "pickle": return PickleOutput(collector, fn=fn) else: raise NotImplementedError() def dump(self): for output in self._outputs: output.dump() # TODO: provide HTML, MD, RST etc formattings duecredit-0.6.0/duecredit/config.py000066400000000000000000000011251273067304300172600ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Configuration handling for duecredit""" import os # For now just hardcoded variables CACHE_DIR = os.path.expanduser(os.path.join('~', '.cache', 'duecredit', 'bibtex')) DUECREDIT_FILE = '.duecredit.p' duecredit-0.6.0/duecredit/dueswitch.py000066400000000000000000000103521273067304300200140ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Provides an adapter to switch between two (active, inactive) collectors """ import os import atexit from .log import lgr from .utils import never_fail def _get_duecredit_enable(): env_enable = os.environ.get('DUECREDIT_ENABLE', 'no') if not env_enable.lower() in ('0', '1', 'yes', 'no', 'true', 'false'): lgr.warning("Misunderstood value %s for DUECREDIT_ENABLE. " "Use 'yes' or 'no', or '0' or '1'") return env_enable.lower() in ('1', 'yes', 'true') @never_fail def _get_inactive_due(): # keeping duplicate but separate so later we could even place it into a separate # submodule to possibly minimize startup time impact even more from .collector import InactiveDueCreditCollector return InactiveDueCreditCollector() @never_fail def _get_active_due(): from .config import CACHE_DIR, DUECREDIT_FILE from duecredit.collector import CollectorSummary, DueCreditCollector from .io import load_due import atexit # where to cache bibtex entries if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR) # TODO: this needs to move to atexit handling, that we load previous # one and them merge with new ones. Informative bits could be -- how # many new citations we got if os.path.exists(DUECREDIT_FILE): try: due_ = load_due(DUECREDIT_FILE) except Exception as e: lgr.warning("Failed to load previously collected %s. " "DueCredit will not be active for this session." % DUECREDIT_FILE) return _get_inactive_due() else: due_ = DueCreditCollector() return due_ class DueSwitch(object): """Adapter between two types of collectors -- Inactive and Active Once activated though, cannot be fully deactivated since it would inject duecredit decorators and register an event atexit. """ def __init__(self, inactive, active, activate=False): self.__active = None self.__collectors = {False: inactive, True: active} self.__activations_done = False self.activate(activate) @property def active(self): return self.__active @never_fail def _dump_collector_summary(self): from duecredit.collector import CollectorSummary due_summary = CollectorSummary(self.__collectors[True]) due_summary.dump() def __prepare_exit_and_injections(self): # Wrapper to create and dump summary... passing method doesn't work: # probably removes instance too early atexit.register(self._dump_collector_summary) # Deal with injector from .injections import DueCreditInjector injector = DueCreditInjector(collector=self.__collectors[True]) injector.activate() @never_fail def activate(self, activate=True): # 1st step -- if activating/deactivating switch between the two collectors if self.__active is not activate: # we need to switch the state is_public = lambda x: not x.startswith('_') # Clean up current bindings first for k in filter(is_public, dir(self)): if k not in ('activate', 'active'): delattr(self, k) new_due = self.__collectors[activate] for k in filter(is_public, dir(new_due)): setattr(self, k, getattr(new_due, k)) self.__active = activate # 2nd -- if activating, we might still need to have activations done if activate and not self.__activations_done: try: self.__prepare_exit_and_injections() except Exception as e: lgr.error("Failed to prepare injections etc: %s" % str(e)) finally: self.__activations_done = True due = DueSwitch(_get_inactive_due(), _get_active_due(), _get_duecredit_enable())duecredit-0.6.0/duecredit/entries.py000066400000000000000000000040541273067304300174700ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. Originates from datalad package distributed # under MIT license # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import re from six import PY2 class DueCreditEntry(object): def __init__(self, rawentry, key=None): self._rawentry = rawentry self._key = key or rawentry.lower() def get_key(self): return self._key @property def key(self): return self.get_key() @property def rawentry(self): if PY2: return unicode(self._rawentry) else: return self._rawentry def _process_rawentry(self): pass def __repr__(self): args = [repr(self._rawentry), "key={0}".format(repr(self._key))] args = ", ".join(args) return self.__class__.__name__ + '({0})'.format(args) class BibTeX(DueCreditEntry): def __init__(self, bibtex, key=None): super(BibTeX, self).__init__(bibtex.strip()) self._key = None self._reference = None self._process_rawentry() def _process_rawentry(self): reg = re.match("\s*@(?P\S*)\s*\{\s*(?P\S*)\s*,.*", self._rawentry, flags=re.MULTILINE) assert(reg) matches = reg.groupdict() self._key = matches['key'] def format(self): # TODO: return nice formatting of the entry return str(self._rawentry) class FreeTextEntry(DueCreditEntry): pass # nothing special I guess class Doi(DueCreditEntry): def __init__(self, doi, key=None): super(Doi, self).__init__(doi, key) self.doi = doi # TODO class Url(DueCreditEntry): def __init__(self, url, key=None): super(Url, self).__init__(url, key) self.url = url duecredit-0.6.0/duecredit/injections/000077500000000000000000000000001273067304300176075ustar00rootroot00000000000000duecredit-0.6.0/duecredit/injections/__init__.py000066400000000000000000000010351273067304300217170ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Facility to automagically decorate with references other modules """ __docformat__ = 'restructuredtext' from .injector import DueCreditInjector duecredit-0.6.0/duecredit/injections/injector.py000066400000000000000000000410131273067304300217750ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Importer which would also call decoration on a module upon import """ __docformat__ = 'restructuredtext' import pdb import os from os.path import basename, join as pathjoin, dirname from glob import glob import sys from functools import wraps import logging from ..log import lgr from six import iteritems if sys.version_info < (3,): import __builtin__ else: import builtins as __builtin__ __all__ = ['DueCreditInjector', 'find_object'] # TODO: move elsewhere def _short_str(obj, l=30): """Return a shortened str of an object -- for logging""" s = str(obj) if len(s) > l: return s[:l-3] + "..." else: return s def get_modules_for_injection(): """Get local modules which provide "inject" method to provide delayed population of injector """ return sorted([basename(x)[:-3] for x in glob(pathjoin(dirname(__file__), "mod_*.py")) ]) def find_object(mod, path): """Finds object among present within module "mod" given path specification within Returns ------- parent, obj_name, obj """ obj = mod # we will look first within module for obj_name in path.split('.'): parent = obj obj = getattr(parent, obj_name) return parent, obj_name, obj # We will keep a very original __import__ to mitigate cases of buggy python # behavior, see e.g. # https://github.com/duecredit/duecredit/issues/40 # But we will also keep the __import__ as of 'activate' call state so we could # stay friendly to anyone else who might decorate __import__ as well _very_orig_import = __builtin__.__import__ class DueCreditInjector(object): """Takes care about "injecting" duecredit references into 3rd party modules upon their import First entries to be "injected" need to be add'ed to the instance. To not incur significant duecredit startup penalty, those entries are added for a corresponding package only whenever corresponding top-level module gets imported. """ # Made as a class variable to assure being singleton and available upon del __orig_import = None # and interact with its value through the property to ease tracking of actions # performed on it @property def _orig_import(self): return DueCreditInjector.__orig_import @_orig_import.setter def _orig_import(self, value): lgr.log(2, "Reassigning _orig_import from %r to %r", DueCreditInjector.__orig_import, value) DueCreditInjector.__orig_import = value def __init__(self, collector=None): if collector is None: from duecredit import due collector = due self._collector = collector self._delayed_injections = {} self._entry_records = {} # dict: modulename: {object: [('entry', cite kwargs)]} self._processed_modules = set() # We need to process modules only after we are done with all nested imports, otherwise we # might be trying to process them too early -- whenever they are not yet linked to their # parent's namespace. So we will keep track of import level and set of modules which # would need to be processed whenever we are back at __import_level == 1 self.__import_level = 0 self.__queue_to_process = set() self.__processing_queue = False self._active = False lgr.debug("Created injector %r", self) def _populate_delayed_injections(self): self._delayed_injections = {} for inj_mod_name in get_modules_for_injection(): assert(inj_mod_name.startswith('mod_')) mod_name = inj_mod_name[4:] lgr.debug("Adding delayed injection for %s", (mod_name,)) self._delayed_injections[mod_name] = inj_mod_name def add(self, modulename, obj, entry, min_version=None, max_version=None, **kwargs): """Add a citation for a given module or object within it Parameters ---------- modulename : string Name of the module (possibly a sub-module) obj : string or None Name of the object (function, method within a class) or None (if for entire module) min_version, max_version : string or tuple, optional Min (inclusive) / Max (exclusive) version of the module where this citation is applicable **kwargs Keyword arguments to be passed into cite. Note that "path" will be automatically set if not provided """ lgr.debug("Adding citation entry %s for %s:%s", _short_str(entry), modulename, obj) if modulename not in self._entry_records: self._entry_records[modulename] = {} if obj not in self._entry_records[modulename]: self._entry_records[modulename][obj] = [] obj_entries = self._entry_records[modulename][obj] if 'path' not in kwargs: kwargs['path'] = modulename + ((":%s" % obj) if obj else "") obj_entries.append({'entry': entry, 'kwargs': kwargs, 'min_version': min_version, 'max_version': max_version}) @property def _import_level_prefix(self): return "." * self.__import_level def _process_delayed_injection(self, mod_name): lgr.debug("%sProcessing delayed injection for %s", self._import_level_prefix, mod_name) inj_mod_name = self._delayed_injections[mod_name] assert(not hasattr(self._orig_import, '__duecredited__')) try: inj_mod_name_full = "duecredit.injections." + inj_mod_name lgr.log(3, "Importing %s", inj_mod_name_full) # Mark it is a processed already, to avoid its processing etc self._processed_modules.add(inj_mod_name_full) inj_mod = self._orig_import(inj_mod_name_full, fromlist=["duecredit.injections"]) except Exception as e: if os.environ.get('DUECREDIT_ALLOW_FAIL', False): raise raise RuntimeError("Failed to import %s: %r" % (inj_mod_name, e)) # TODO: process min/max_versions etc assert(hasattr(inj_mod, 'inject')) lgr.log(3, "Calling injector of %s", inj_mod_name_full) inj_mod.inject(self) def process(self, mod_name): """Process import of the module, possibly decorating some methods with duecredit entries """ assert(self.__import_level == 0) # we should never process while nested within imports # We need to mark that module as processed EARLY, so we don't try to re-process it # while doing _process_delayed_injection self._processed_modules.add(mod_name) if mod_name in self._delayed_injections: # should be hit only once, "theoretically" unless I guess reimport is used etc self._process_delayed_injection(mod_name) if mod_name not in self._entry_records: return total_number_of_citations = sum(map(len, self._entry_records[mod_name].values())) lgr.log(logging.DEBUG + 5, "Process %d citation injections for %d objects for module %s", total_number_of_citations, len(self._entry_records[mod_name]), mod_name) try: mod = sys.modules[mod_name] except KeyError: lgr.warning("Failed to access module %s among sys.modules" % mod_name) return # go through the known entries and register them within the collector, and # decorate corresponding methods # There could be multiple records per module for obj_path, obj_entry_records in iteritems(self._entry_records[mod_name]): parent, obj_name = None, None if obj_path: # so we point to an object within the mod try: parent, obj_name, obj = find_object(mod, obj_path) except (KeyError, AttributeError) as e: lgr.warning("Could not find %s in module %s: %s" % (obj_path, mod, e)) continue # there could be multiple per func lgr.log(4, "Considering %d records for decoration of %s:%s", len(obj_entry_records), parent, obj_name) for obj_entry_record in obj_entry_records: entry = obj_entry_record['entry'] # Add entry explicitly self._collector.add(entry) if obj_path: # if not entire module -- decorate! decorator = self._collector.dcite(entry.get_key(), **obj_entry_record['kwargs']) lgr.debug("Decorating %s:%s with %s", parent, obj_name, decorator) obj_decorated = decorator(obj) setattr(parent, obj_name, obj_decorated) # override previous obj with the decorated one if there are multiple decorators obj = obj_decorated else: lgr.log(3, "Citing directly %s:%s since obj_path is empty", parent, obj_name) self._collector.cite(entry.get_key(), **obj_entry_record['kwargs']) lgr.log(3, "Done processing injections for module %s", mod_name) def _mitigate_None_orig_import(self, name, *args, **kwargs): lgr.error("For some reason self._orig_import is None" ". Importing using stock importer to mitigate and adjusting _orig_import") self._orig_import = _very_orig_import return _very_orig_import(name, *args, **kwargs) def activate(self, retrospect=True): """ Parameters ---------- retrospect : bool, optional Either consider already loaded modules """ if not self._orig_import: # for paranoid Yarik so we have assurance we are not somehow # overriding our decorator if hasattr(__builtin__.__import__, '__duecredited__'): raise RuntimeError("__import__ is already duecredited") self._orig_import = __builtin__.__import__ @wraps(__builtin__.__import__) def __import(name, *args, **kwargs): if self.__processing_queue or name in self._processed_modules or name in self.__queue_to_process: lgr.debug("Performing undecorated import of %s", name) # return right away without any decoration in such a case if self._orig_import: return _very_orig_import(name, *args, **kwargs) else: return self._mitigate_None_orig_import(name, *args, **kwargs) import_level_prefix = self._import_level_prefix lgr.log(1, "%sProcessing request to import %s", import_level_prefix, name) # importing submodule might result in importing a new one and # name here is not sufficient to determine which module would actually # get imported unless level=0 (absolute import), but that one rarely used # could be old-style or new style relative import! # args[0] -> globals, [1] -> locals(), [2] -> fromlist, [3] -> level level = args[3] if len(args) >= 4 else kwargs.get('level', -1) # fromlist = args[2] if len(args) >= 3 else kwargs.get('fromlist', []) if not retrospect and not self._processed_modules: # we were asked to not consider those modules which were already loaded # so let's assume that they were all processed already self._processed_modules = set(sys.modules) mod = None try: self.__import_level += 1 # TODO: safe-guard all our logic so # if anything goes wrong post-import -- we still return imported module if self._orig_import: mod = self._orig_import(name, *args, **kwargs) else: mod = self._mitigate_None_orig_import(name, *args, **kwargs) self._handle_fresh_imports(name, import_level_prefix, level) finally: self.__import_level -= 1 if self.__import_level == 0 and self.__queue_to_process: self._process_queue() lgr.log(1, "%sReturning %s", import_level_prefix, mod) return mod __import.__duecredited__ = True self._populate_delayed_injections() if retrospect: lgr.debug("Considering previously loaded %d modules", len(sys.modules)) # operate on keys() (not iterator) since we might end up importing delayed injection modules etc for mod_name in sys.modules.keys(): self.process(sys.modules[mod_name]) lgr.debug("Assigning our importer") __builtin__.__import__ = __import self._active = True else: lgr.warning("Seems that we are calling duecredit_importer twice." " No harm is done but shouldn't happen") def _handle_fresh_imports(self, name, import_level_prefix, level): """Check which modules were imported since last point we checked and add them to the queue """ new_imported_modules = set(sys.modules.keys()) - self._processed_modules - self.__queue_to_process if new_imported_modules: lgr.log(4, "%s%d new modules were detected upon import of %s (level=%s)", import_level_prefix, len(new_imported_modules), name, level) # lgr.log(2, "%s%d new modules were detected: %s, upon import of %s (level=%s)", # import_level_prefix, len(new_imported_modules), new_imported_modules, name, level) for imported_mod in new_imported_modules: if imported_mod in self.__queue_to_process: # we saw it already continue # lgr.log(1, "Name %r was imported as %r (path: %s). fromlist: %s, level: %s", # name, mod.__name__, getattr(mod, '__path__', None), fromlist, level) # package package = imported_mod.split('.', 1)[0] if package != imported_mod \ and package not in self._processed_modules \ and package not in self.__queue_to_process: # if its parent package wasn't yet imported before lgr.log(3, "%sParent of %s, %s wasn't yet processed, adding to the queue", import_level_prefix, imported_mod, package) self.__queue_to_process.add(package) self.__queue_to_process.add(imported_mod) def _process_queue(self): """Process the queue of collected imported modules """ # process the queue lgr.debug("Processing queue of imported %d modules", len(self.__queue_to_process)) # We need first to process top-level modules etc, so delayed injections get picked up, # let's sort by the level queue_with_levels = sorted([(m.count('.'), m) for m in self.__queue_to_process]) self.__processing_queue = True try: sorted_queue = [x[1] for x in queue_with_levels] while sorted_queue: mod_name = sorted_queue.pop(0) self.process(mod_name) self.__queue_to_process.remove(mod_name) assert (not len(self.__queue_to_process)) finally: self.__processing_queue = False def deactivate(self): if not self._orig_import: lgr.warning("_orig_import is not yet known, so we haven't decorated default importer yet." " Nothing TODO") return if not self._active: # pragma: no cover lgr.error("Must have not happened, but we will survive!") lgr.debug("Assigning original importer") __builtin__.__import__ = self._orig_import self._orig_import = None self._active = False def __del__(self): lgr.debug("%s is asked to be deleted", self) if self._active: self.deactivate() try: super(self.__class__, self).__del__() except Exception: pass duecredit-0.6.0/duecredit/injections/mod_biosig.py000066400000000000000000000014201273067304300222710ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Automatic injection of bibliography entries for biosig module """ from ..entries import Doi # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('biosig', None, Doi("10.1109/MC.2008.407"), description="I/O library for biosignal data formats") duecredit-0.6.0/duecredit/injections/mod_dipy.py000066400000000000000000000016331273067304300217700ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ Automatic injection of bibliography entries for dipy module """ from ..entries import Doi, BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): #http://nipy.org/dipy/cite.html#a-note-on-citing-our-work injector.add('dipy', None, Doi('10.3389/fninf.2014.00008'), description='Dipy, a library for the analysis of diffusion MRI data.', tags=['implementation']) duecredit-0.6.0/duecredit/injections/mod_mdp.py000066400000000000000000000107451273067304300216070ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ Automatic injection of bibliography entries for mdp module """ from ..entries import Doi, BibTeX, Url # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('mdp', None, Doi('10.3389/neuro.11.008.2008'), description="Modular toolkit for Data Processing (MDP): a Python data processing framework", tags=['implementation']) injector.add('mdp.nodes', 'PCANode.train', Doi('10.1007/b98835'), description="Principal Component Analysis (and filtering)", tags=['implementation']) injector.add('mdp.nodes', 'NIPALSNode.train', BibTeX(""" @incollection{Word1966, author={Wold, H.}, title={Nonlinear estimation by iterative least squares procedures.}, booktitle={Research Papers in Statistics}, publisher={Wiley} year={1966}, editor={David, F.}, pages={411--444}, } """), description="Principal Component Analysis using the NIPALS algorithm.", tags=['edu']) injector.add('mdp.nodes', 'FastICANode.train', Doi('10.1109/72.761722'), description="Independent Component Analysis using the FastICA algorithm", tags=['implementation']) injector.add('mdp.nodes', 'CuBICANode.train', Doi('10.1109/TSP.2004.826173'), description='Independent Component Analysis using the CuBICA algorithm.', tags=['implementation']) injector.add('mdp.nodes', 'NIPALSNode.train', BibTeX(""" @conference{ZieheMuller1998, author={Ziehe, Andreas and Muller, Klaus-Robert}, title={TDSEP an efficient algorithm for blind separation using time structure.}, booktitle={Proc. 8th Int. Conf. Artificial Neural Networks}, year={1998}, editor={Niklasson, L, Boden, M, and Ziemke, T}, publisher={ICANN} } """), description='Independent Component Analysis using the TDSEP algorithm', tags=['edu']) injector.add('mdp.nodes', 'JADENode.train', Doi('10.1049/ip-f-2.1993.0054'), description='Independent Component Analysis using the JADE algorithm', tags=['implementation']) injector.add('mdp.nodes', 'JADENode.train', Doi('10.1162/089976699300016863'), description='Independent Component Analysis using the JADE algorithm', tags=['implementation']) injector.add('mdp.nodes', 'SFANode.train', Doi('10.1162/089976602317318938'), description='Slow Feature Analysis', tags=['implementation']) injector.add('mdp.nodes', 'SFA2Node.train', Doi('10.1162/089976602317318938'), description='Slow Feature Analysis (via the space of inhomogeneous polynomials)', tags=['implementation']) injector.add('mdp.nodes', 'ISFANode.train', Doi('10.1007/978-3-540-30110-3_94'), description='Independent Slow Feature Analysis', tags=['implementation']) injector.add('mdp.nodes', 'XSFANode.train', BibTeX(""" @article{SprekelerZitoWiskott2009, author={Sprekeler, H., Zito, T., and Wiskott, L.}, title={An Extension of Slow Feature Analysis for Nonlinear Blind Source Separation.}, journal={Journal of Machine Learning Research.}, year={2009}, volume={15}, pages={921--947}, } """), description="Non-linear Blind Source Separation using Slow Feature Analysis", tags=['edu']) injector.add('mdp.nodes', 'FDANode.train', BibTeX(""" @book{Bishop2011, author={Bishop, Christopher M.}, title={Neural Networks for Pattern Recognition}, publisher={Oxford University Press, Inc} year={2011}, pages={105--112}, } """), description="(generalized) Fisher Discriminant Analysis", tags=['edu']) # etc... duecredit-0.6.0/duecredit/injections/mod_mne.py000066400000000000000000000021001273067304300215700ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ Automatic injection of bibliography entries for mne module """ from ..entries import Doi, BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): #http://martinos.org/mne/stable/cite.html injector.add('mne', None, Doi('10.1016/j.neuroimage.2013.10.027'), description='MNE software for processing MEG and EEG data.', tags=['implementation']) injector.add('mne', None, Doi('10.3389/fnins.2013.00267'), description='MEG and EEG data analysis with MNE-Python.', tags=['implementation']) duecredit-0.6.0/duecredit/injections/mod_nibabel.py000066400000000000000000000015141273067304300224150ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Automatic injection of bibliography entries for nibabel module """ from ..entries import Url # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('nibabel', None, Url('http://nipy.org/nibabel'), description="Access a cacophony of neuro-imaging file formats", tags=['implementation']) duecredit-0.6.0/duecredit/injections/mod_nipy.py000066400000000000000000000022121273067304300217740ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Automatic injection of bibliography entries for nipy module """ from ..entries import Doi # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('nipy', None, Doi('10.1016/S1053-8119(09)72223-2'), description="Library fMRI data analysis", tags=['implementation']) for f, d in [('spectral_decomposition', 'PCA decomposition of symbolic HRF shifted over time'), ('taylor_approx', 'A Taylor series approximation of an HRF shifted over time')]: injector.add('nipy.modalities.fmri.fmristat.hrf', f, Doi('10.1006/nimg.2002.1096'), description=d, tags=['implementation'])duecredit-0.6.0/duecredit/injections/mod_nipype.py000066400000000000000000000051011273067304300223210ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ Automatic injection of bibliography entries for nipype module """ from ..entries import Doi, BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): #http://nipy.org/nipype/about.html injector.add('nipype', None, Doi('10.3389/fninf.2011.00013'), description='Nipype: a flexible, lightweight and extensible neuroimaging data processing framework in Python', tags=['implementation']) #http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/ injector.add('nipype.interfaces', 'fsl', Doi('10.1016/j.neuroimage.2004.07.051'), description='Advances in functional and structural MR image analysis and implementation as FSL', tags=['implementation']) injector.add('nipype.interfaces', 'fsl', Doi('10.1016/j.neuroimage.2008.10.055'), description='Bayesian analysis of neuroimaging data in FSL', tags=['implementation']) injector.add('nipype.interfaces', 'fsl', Doi('10.1016/j.neuroimage.2011.09.015'), description='FSL.', tags=['implementation']) #http://www.fil.ion.ucl.ac.uk/spm injector.add('nipype.interfaces', 'spm', BibTeX(""" @book{FrackowiakFristonFrithDolanMazziotta1997, author={R.S.J. Frackowiak, K.J. Friston, C.D. Frith, R.J. Dolan, and J.C. Mazziotta}, title={Human Brain Function}, publisher={Academic Press USA} year={1997}, } """), description='The fundamental text on Statistical Parametric Mapping (SPM)', tags=['implementation']) #http://surfer.nmr.mgh.harvard.edu/fswiki/FreeSurferMethodsCitation # there are a bunch, not sure what is primary #injector.add('nipype.interfaces', 'freesurfer', Doi(''), #description='', #tags=['implementation']) #http://afni.nimh.nih.gov/afni/about/citations/ # there are a bunch, not sure what is primary #injector.add('nipype.interfaces', 'afni', Doi(''), #description='', #tags=['implementation']) duecredit-0.6.0/duecredit/injections/mod_numpy.py000066400000000000000000000022361273067304300221730ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Automatic injection of bibliography entries for numpy module """ from ..entries import Doi, BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('numpy', None, BibTeX(""" @article{van2011numpy, title={The NumPy array: a structure for efficient numerical computation}, author={Van Der Walt, Stefan and Colbert, S Chris and Varoquaux, Gael}, journal={Computing in Science \& Engineering}, volume={13}, number={2}, pages={22--30}, year={2011}, publisher={AIP Publishing} } """), tags=['implementation'], cite_module=True, description="Scientific tools library") duecredit-0.6.0/duecredit/injections/mod_pandas.py000066400000000000000000000022201273067304300222620ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Automatic injection of bibliography entries for pandas module """ from ..entries import Doi, BibTeX, Url # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('pandas', None, BibTeX(""" @InProceedings{ mckinney-proc-scipy-2010, author = { McKinney, Wes }, title = { Data Structures for Statistical Computing in Python }, booktitle = { Proceedings of the 9th Python in Science Conference }, pages = { 51 -- 56 }, year = { 2010 }, editor = { van der Walt, St\'efan and Millman, Jarrod } } """), description="Data analysis library for tabular data")duecredit-0.6.0/duecredit/injections/mod_psychopy.py000066400000000000000000000020531273067304300226760ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ Automatic injection of bibliography entries for psychopy module """ from ..entries import Doi, BibTeX, Url # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('psychopy', None, Doi('doi:10.1016/j.jneumeth.2006.11.017'), description="PsychoPy -- Psychophysics software in Python.", tags=['implementation']) injector.add('psychopy', None, Doi('10.3389/neuro.11.010.2008'), description="Generating stimuli for neuroscience using PsychoPy.", tags=['implementation']) duecredit-0.6.0/duecredit/injections/mod_scipy.py000066400000000000000000000144361273067304300221570ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Automatic injection of bibliography entries for scipy module """ from ..entries import Doi, BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('scipy', None, BibTeX(""" @Misc{JOP+01, author = {Eric Jones and Travis Oliphant and Pearu Peterson and others}, title = {{SciPy}: Open source scientific tools for {Python}}, year = {2001--}, url = "http://www.scipy.org/", note = {[Online; accessed 2015-07-13]} }"""), description="Scientific tools library", tags=['implementation']) # scipy.cluster.hierarchy general references # TODO: we should allow to pass a list of entries injector.add('scipy.cluster.hierarchy', None, BibTeX(""" @article{johnson1967hierarchical, title={Hierarchical clustering schemes}, author={Johnson, Stephen C}, journal={Psychometrika}, volume={32}, number={3}, pages={241--254}, year={1967}, publisher={Springer} }"""), min_version='0.4.3', description="Hierarchical clustering", tags=['edu']) injector.add('scipy.cluster.hierarchy', None, BibTeX(""" @article{sneath1962numerical, title={Numerical taxonomy}, author={Sneath, Peter HA and Sokal, Robert R}, journal={Nature}, volume={193}, number={4818}, pages={855--860}, year={1962}, publisher={Nature Publishing Group} }"""), description="Hierarchical clustering", min_version='0.4.3', tags=['edu']) injector.add('scipy.cluster.hierarchy', None, BibTeX(""" @article{batagelj1995comparing, title={Comparing resemblance measures}, author={Batagelj, Vladimir and Bren, Matevz}, journal={Journal of classification}, volume={12}, number={1}, pages={73--90}, year={1995}, publisher={Springer} }"""), description="Hierarchical clustering", min_version='0.4.3', tags=['edu']) injector.add('scipy.cluster.hierarchy', None, BibTeX(""" @book{sokal1958statistical, author = {Sokal, R R and Michener, C D and {University of Kansas}}, title = {{A Statistical Method for Evaluating Systematic Relationships}}, publisher = {University of Kansas}, year = {1958}, series = {University of Kansas science bulletin} }"""), description="Hierarchical clustering", min_version='0.4.3', tags=['edu']) injector.add('scipy.cluster.hierarchy', None, BibTeX(""" @article{edelbrock1979mixture, title={Mixture model tests of hierarchical clustering algorithms: the problem of classifying everybody}, author={Edelbrock, Craig}, journal={Multivariate Behavioral Research}, volume={14}, number={3}, pages={367--384}, year={1979}, publisher={Taylor \& Francis} }"""), description="Hierarchical clustering", min_version='0.4.3', tags=['edu']) injector.add('scipy.cluster.hierarchy', None, BibTeX(""" @book{jain1988algorithms, title={Algorithms for clustering data}, author={Jain, Anil K and Dubes, Richard C}, year={1988}, publisher={Prentice-Hall, Inc.} }"""), description="Hierarchical clustering", min_version='0.4.3', tags=['edu']) injector.add('scipy.cluster.hierarchy', None, BibTeX(""" @article{fisher1936use, title={The use of multiple measurements in taxonomic problems}, author={Fisher, Ronald A}, journal={Annals of eugenics}, volume={7}, number={2}, pages={179--188}, year={1936}, publisher={Wiley Online Library} }"""), description="Hierarchical clustering", min_version='0.4.3', tags=['edu']) # Here options for linkage injector.add('scipy.cluster.hierarchy', 'linkage', BibTeX(""" @article{ward1963hierarchical, title={Hierarchical grouping to optimize an objective function}, author={Ward Jr, Joe H}, journal={Journal of the American statistical association}, volume={58}, number={301}, pages={236--244}, year={1963}, publisher={Taylor \& Francis} }"""), conditions={(1, 'method'): {'ward'}}, description="Ward hierarchical clustering", min_version='0.4.3', tags=['reference']) injector.add('scipy.cluster.hierarchy', 'linkage', BibTeX(""" @article{gower1969minimum, title={Minimum spanning trees and single linkage cluster analysis}, author={Gower, John C and Ross, GJS}, journal={Applied statistics}, pages={54--64}, year={1969}, publisher={JSTOR} }"""), conditions={(1, 'method'): {'single', 'DC_DEFAULT'}}, description="Single linkage hierarchical clustering", min_version='0.4.3', tags=['reference']) injector.add('scipy.cluster.hierarchy', 'linkage', BibTeX(""" @article{sibson1973slink, title={SLINK: an optimally efficient algorithm for the single-link cluster method}, author={Sibson, Robin}, journal={The Computer Journal}, volume={16}, number={1}, pages={30--34}, year={1973}, publisher={Br Computer Soc} }"""), conditions={(1, 'method'): {'single', 'DC_DEFAULT'}}, description="Single linkage hierarchical clustering", min_version='0.4.3', tags=['implementation']) duecredit-0.6.0/duecredit/injections/mod_skimage.py000066400000000000000000000015521273067304300224430ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ Automatic injection of bibliography entries for skimage module """ from ..entries import Doi, BibTeX # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): #http://scikit-image.org injector.add('skimage', None, Doi('10.7717/peerj.453'), description='scikit-image: Image processing in Python.', tags=['implementation']) duecredit-0.6.0/duecredit/injections/mod_sklearn.py000066400000000000000000000112471273067304300224640ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Automatic injection of bibliography entries for numpy module """ from ..entries import Doi, BibTeX, Url # If defined, would determine from which to which version of the corresponding # module to care about min_version = None max_version = None def inject(injector): injector.add('sklearn', None, BibTeX(""" @article{pedregosa2011scikit, title={Scikit-learn: Machine learning in Python}, author={Pedregosa, Fabian and Varoquaux, Ga{\"e}l and Gramfort, Alexandre and Michel, Vincent and Thirion, Bertrand and Grisel, Olivier and Blondel, Mathieu and Prettenhofer, Peter and Weiss, Ron and Dubourg, Vincent and others}, journal={The Journal of Machine Learning Research}, volume={12}, pages={2825--2830}, year={2011}, publisher={JMLR.org} } """), description="Machine Learning library") # sklearn.cluster.affinity_propagation_ injector.add('sklearn.cluster.affinity_propagation_', None, Doi('10.1126/science.1136800'), description="Affinity propagation clustering algorithm", tags=['implementation']) # sklearn.cluster.bicluster injector.add('sklearn.cluster.bicluster', 'SpectralCoclustering._fit', Doi('10.1101/gr.648603'), description="Spectral Coclustering algorithm", tags=['implementation']) injector.add('sklearn.cluster.bicluster', 'SpectralBiclustering._fit', Doi('10.1101/gr.648603'), description="Spectral Biclustering algorithm", tags=['implementation']) # sklearn.cluster.birch injector.add('sklearn.cluster.birch', 'Birch._fit', Doi('10.1145/233269.233324'), description="BIRCH clustering algorithm", tags=['implementation']) injector.add('sklearn.cluster.birch', 'Birch._fit', Url('https://code.google.com/p/jbirch/'), description="Java implementation of BIRCH clustering algorithm", tags=['another-implementation']) # sklearn.cluster.dbscan_ injector.add('sklearn.cluster.dbscan_', 'dbscan', BibTeX("""@inproceedings{ester1996density, title={A density-based algorithm for discovering clusters in large spatial databases with noise.}, author={Ester, Martin and Kriegel, Hans-Peter and Sander, J{\"o}rg and Xu, Xiaowei}, booktitle={Kdd}, volume={96}, number={34}, pages={226--231}, year={1996} }"""), description="dbscan clustering algorithm", tags=['implementation']) # sklearn.cluster.mean_shift_ injector.add('sklearn.cluster.mean_shift_', 'mean_shift', Doi('10.1109/34.1000236'), description="Mean shift clustering algorithm", tags=['implementation']) # sklearn.cluster.spectral injector.add('sklearn.cluster.spectral', 'discretize', Doi('10.1109/ICCV.2003.1238361'), description="Multiclass spectral clustering", tags=['reference']) injector.add('sklearn.cluster.spectral', 'spectral_clustering', Doi('10.1109/34.868688'), description="Spectral clustering", tags=['implementation']) injector.add('sklearn.cluster.spectral', 'spectral_clustering', Doi('10.1007/s11222-007-9033-z'), description="Spectral clustering", tags=['implementation']) # sklearn.ensemble.forest and tree Breiman_2001 = Doi("10.1023/A:1010933404324") Breiman_1984 = BibTeX("""@BOOK{breiman-friedman-olshen-stone-1984, author = {L. Breiman and J. Friedman and R. Olshen and C. Stone}, title = {{Classification and Regression Trees}}, publisher = {Wadsworth and Brooks}, address = {Monterey, CA}, year = {1984}, }""") # Not clear here though if those are the original publication on the topic # or just an educational references (books), most probably both ;) injector.add('sklearn.ensemble.forest', 'RandomForestClassifier.predict_proba', Breiman_2001, description="Random forest classifiers", tags=['implementation', 'edu']) injector.add('sklearn.ensemble.forest', 'RandomForestRegressor.predict', Breiman_2001, description="Random forest regressions", tags=['implementation', 'edu']) injector.add('sklearn.tree.tree', 'DecisionTreeClassifier.predict_proba', Breiman_1984, description="Classification and regression trees", tags=['implementation', 'edu']) duecredit-0.6.0/duecredit/io.py000066400000000000000000000274201273067304300164300ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # Just for testing of robust operation import os if 'DUECREDIT_TEST_EARLY_IMPORT_ERROR' in os.environ.keys(): raise ImportError("DUECREDIT_TEST_EARLY_IMPORT_ERROR") import locale import time from collections import defaultdict, Iterator import copy from os.path import dirname, exists import pickle import requests import tempfile from six import PY2, itervalues, iteritems import warnings from .config import CACHE_DIR, DUECREDIT_FILE from .entries import BibTeX, Doi from .log import lgr _PREFERRED_ENCODING = locale.getpreferredencoding() def get_doi_cache_file(doi): return os.path.join(CACHE_DIR, doi) def import_doi(doi, sleep=0.5, retries=10): cached = get_doi_cache_file(doi) if exists(cached): with open(cached) as f: doi = f.read() if PY2: return doi.decode('utf-8') return doi # else -- fetch it headers = {'Accept': 'application/x-bibtex; charset=utf-8'} url = 'http://dx.doi.org/' + doi while retries > 0: r = requests.get(url, headers=headers) r.encoding = 'UTF-8' bibtex = r.text.strip() if bibtex.startswith('@'): # no more retries necessary break lgr.warning("Failed to obtain bibtex from doi.org, retrying...") time.sleep(sleep) # give some time to the server retries -= 1 status_code = r.status_code if not bibtex.startswith('@'): raise ValueError('Query %(url)s for BibTeX for a DOI %(doi)s (wrong doi?) has failed. ' 'Response code %(status_code)d. ' #'BibTeX response was: %(bibtex)s' % locals()) if not exists(cached): cache_dir = dirname(cached) if not exists(cache_dir): os.makedirs(cache_dir) with open(cached, 'w') as f: if PY2: f.write(bibtex.encode('utf-8')) else: f.write(bibtex) return bibtex def _is_contained(toppath, subpath): if ':' not in toppath: return ((toppath == subpath) or (subpath.startswith(toppath + '.')) or (subpath.startswith(toppath + ':'))) else: return subpath.startswith(toppath + '.') class Output(object): """A generic class for setting up citations that then will be outputted differently (e.g., Bibtex, Text, etc.)""" def __init__(self, fd, collector): self.fd = fd self.collector = collector def _get_collated_citations(self, tags=None, all_=None): """Given all the citations, filter only those that the user wants and those that were actually used""" if not tags: tags = os.environ.get('DUECREDIT_REPORT_TAGS', 'reference-implementation,implementation').split(',') if all_ is None: # consult env var all_ = os.environ.get('DUECREDIT_REPORT_ALL', '').lower() in {'1', 'true', 'yes', 'on'} tags = set(tags) citations = self.collector.citations if tags != {'*'}: # Filter out citations based on tags citations = dict((k, c) for k, c in iteritems(citations) if tags.intersection(c.tags)) packages = defaultdict(list) modules = defaultdict(list) objects = defaultdict(list) # store the citations according to their path and divide them into # the right level for (path, entry_key), citation in iteritems(citations): if ':' in path: objects[path].append(citation) elif '.' in path: modules[path].append(citation) else: packages[path].append(citation) # now we need to filter out the packages that don't have modules # or objects cited, or are specifically requested cited_packages = list(packages) cited_modobj = list(modules) + list(objects) for package in cited_packages: package_citations = packages[package] if all_ or \ any(filter(lambda x: x.cite_module, package_citations)) or \ any(filter(lambda x: _is_contained(package, x), cited_modobj)): continue else: # we don't need it del packages[package] return packages, modules, objects def dump(self, tags=None): raise NotImplementedError class TextOutput(Output): def __init__(self, fd, collector, style=None): super(TextOutput, self).__init__(fd, collector) self.style = style if 'DUECREDIT_STYLE' in os.environ.keys(): self.style = os.environ['DUECREDIT_STYLE'] else: self.style = 'harvard1' @staticmethod def _format_citations(citations, citation_nr): descriptions = map(str, set(str(r.description) for r in citations)) versions = map(str, set(str(r.version) for r in citations)) refnrs = map(str, [citation_nr[c.entry.key] for c in citations]) path = citations[0].path return '- {0} / {1} (v {2}) [{3}]\n'.format( ", ".join(descriptions), path, ', '.join(versions), ', '.join(refnrs)) def dump(self, tags=None): # get 'model' of citations packages, modules, objects = self._get_collated_citations(tags) # put everything into a single dict pmo = {} pmo.update(packages) pmo.update(modules) pmo.update(objects) # get all the paths paths = sorted(list(pmo)) # get all the entry_keys in order entry_keys = [c.entry.key for p in paths for c in pmo[p]] # make a dictionary entry_key -> citation_nr citation_nr = defaultdict(int) refnr = 1 for entry_key in entry_keys: if entry_key not in citation_nr: citation_nr[entry_key] = refnr refnr += 1 self.fd.write('\nDueCredit Report:\n') start_refnr = 1 for path in paths: # since they're lexicographically sorted by path, dependencies # should be maintained cit = pmo[path] if ':' in path or '.' in path: self.fd.write(' ') self.fd.write(self._format_citations(cit, citation_nr)) start_refnr += len(cit) # Print out some stats stats = [(len(packages), 'package'), (len(modules), 'module'), (len(objects), 'function')] for n, cit_type in stats: self.fd.write('\n{0} {1} cited'.format(n, cit_type if n == 1 else cit_type + 's')) # now print out references printed_keys = [] if len(pmo) > 0: self.fd.write('\n\nReferences\n' + '-' * 10 + '\n') for path in paths: for cit in pmo[path]: ek = cit.entry.key if ek not in printed_keys: self.fd.write('\n[{0}] '.format(citation_nr[ek])) self.fd.write(get_text_rendering(cit, style=self.style)) printed_keys.append(ek) self.fd.write('\n') def get_text_rendering(citation, style='harvard1'): from .collector import Citation entry = citation.entry if isinstance(entry, Doi): bibtex_rendering = get_bibtex_rendering(entry) bibtex_citation = copy.copy(citation) bibtex_citation.set_entry(bibtex_rendering) return get_text_rendering(bibtex_citation) elif isinstance(entry, BibTeX): return format_bibtex(entry, style=style) else: return str(entry) def get_bibtex_rendering(entry): if isinstance(entry, Doi): return BibTeX(import_doi(entry.doi)) elif isinstance(entry, BibTeX): return entry else: raise ValueError("Have no clue how to get bibtex out of %s" % entry) def format_bibtex(bibtex_entry, style='harvard1'): try: from citeproc.source.bibtex import BibTeX as cpBibTeX import citeproc as cp except ImportError as e: raise RuntimeError( "For formatted output we need citeproc and all of its dependencies " "(such as lxml) but there is a problem while importing citeproc: %s" % str(e)) key = bibtex_entry.get_key() # need to save it temporarily to use citeproc-py fname = tempfile.mktemp(suffix='.bib') try: with open(fname, 'wt') as f: bibtex = bibtex_entry.rawentry # XXX: workaround atm to fix zenodo bibtexs, convert @data to @misc # and also ; into and if bibtex.startswith('@data'): bibtex = bibtex.replace('@data', '@misc', 1) bibtex = bibtex.replace(';', ' and') bibtex = bibtex.replace(u'\u2013', '--') + "\n" # TODO: manage to save/use UTF-8 if PY2: bibtex = bibtex.encode('ascii', 'ignore') f.write(bibtex) # We need to avoid cpBibTex spitting out warnings old_filters = warnings.filters[:] # store a copy of filters warnings.simplefilter('ignore', UserWarning) try: bib_source = cpBibTeX(fname) except Exception as e: lgr.error("Failed to process BibTeX file %s: %s" % (fname, e)) return "ERRORED: %s" % str(e) finally: # return warnings back warnings.filters = old_filters bib_style = cp.CitationStylesStyle(style, validate=False) # TODO: specify which tags of formatter we want bibliography = cp.CitationStylesBibliography(bib_style, bib_source, cp.formatter.plain) citation = cp.Citation([cp.CitationItem(key)]) bibliography.register(citation) finally: if not os.environ.get("DUECREDIT_KEEPTEMP"): os.unlink(fname) biblio_out = bibliography.bibliography() assert(len(biblio_out) == 1) biblio_out = ''.join(biblio_out[0]) return biblio_out # if biblio_out else str(bibtex_entry) # TODO: harmonize order of arguments class PickleOutput(object): def __init__(self, collector, fn=DUECREDIT_FILE): self.collector = collector self.fn = fn def dump(self): with open(self.fn, 'wb') as f: pickle.dump(self.collector, f) @classmethod def load(cls, filename=DUECREDIT_FILE): with open(filename, 'rb') as f: return pickle.load(f) class BibTeXOutput(Output): def __init__(self, fd, collector): super(BibTeXOutput, self).__init__(fd, collector) def dump(self, tags=None): packages, modules, objects = self._get_collated_citations(tags) # get all the citations in order pmo = {} pmo.update(packages) pmo.update(modules) pmo.update(objects) # get all the paths paths = sorted(list(pmo)) entries = [] for path in paths: for c in pmo[path]: if c.entry not in entries: entries.append(c.entry) for entry in entries: try: bibtex = get_bibtex_rendering(entry) except: lgr.warning("Failed to generate bibtex for %s" % entry) continue self.fd.write(bibtex.rawentry + "\n") def load_due(filename): return PickleOutput.load(filename) duecredit-0.6.0/duecredit/log.py000066400000000000000000000174721273067304300166100ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. This file originates from datalad distributed # under MIT license. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import logging, os, sys, platform import logging.handlers from .utils import is_interactive __all__ = ['ColorFormatter', 'log'] # Snippets from traceback borrowed from PyMVPA upstream/2.4.0-39-g69ad545 MIT license import traceback import re from os.path import basename, dirname def mbasename(s): """Custom function to include directory name if filename is too common Also strip .py at the end """ base = basename(s) if base.endswith('.py'): base = base[:-3] if base in set(['base', '__init__']): base = basename(dirname(s)) + '.' + base return base class TraceBack(object): """Customized traceback to be included in debug messages """ def __init__(self, collide=False): """Initialize TraceBack metric Parameters ---------- collide : bool if True then prefix common with previous invocation gets replaced with ... """ self.__prev = "" self.__collide = collide def __call__(self): ftb = traceback.extract_stack(limit=100)[:-2] entries = [[mbasename(x[0]), str(x[1])] for x in ftb if mbasename(x[0]) != 'logging.__init__'] entries = [ e for e in entries if e[0] != 'unittest' ] # lets make it more concise entries_out = [entries[0]] for entry in entries[1:]: if entry[0] == entries_out[-1][0]: entries_out[-1][1] += ',%s' % entry[1] else: entries_out.append(entry) sftb = '>'.join(['%s:%s' % (mbasename(x[0]), x[1]) for x in entries_out]) if self.__collide: # lets remove part which is common with previous invocation prev_next = sftb common_prefix = os.path.commonprefix((self.__prev, sftb)) common_prefix2 = re.sub('>[^>]*$', '', common_prefix) if common_prefix2 != "": sftb = '...' + sftb[len(common_prefix2):] self.__prev = prev_next return sftb # Recipe from http://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output # by Brandon Thomson # Adjusted for automagic determination either coloring is needed and # prefixing of multiline log lines class ColorFormatter(logging.Formatter): BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) RESET_SEQ = "\033[0m" COLOR_SEQ = "\033[1;%dm" BOLD_SEQ = "\033[1m" COLORS = { 'WARNING': YELLOW, 'INFO': WHITE, 'DEBUG': BLUE, 'CRITICAL': YELLOW, 'ERROR': RED } def __init__(self, use_color=None, log_name=False): if use_color is None: # if 'auto' - use color only if all streams are tty use_color = is_interactive() self.use_color = use_color and platform.system() != 'Windows' # don't use color on windows msg = self.formatter_msg(self._get_format(log_name), self.use_color) self._tb = TraceBack(collide=os.environ.get('DUECREDIT_LOGTRACEBACK', '') == 'collide') \ if os.environ.get('DUECREDIT_LOGTRACEBACK', False) else None logging.Formatter.__init__(self, msg) def _get_format(self, log_name=False): return ("$BOLD%(asctime)-15s$RESET " + ("%(name)-15s " if log_name else "") + "[%(levelname)s] " "%(message)s " "($BOLD%(filename)s$RESET:%(lineno)d)") def formatter_msg(self, fmt, use_color=False): if use_color: fmt = fmt.replace("$RESET", self.RESET_SEQ).replace("$BOLD", self.BOLD_SEQ) else: fmt = fmt.replace("$RESET", "").replace("$BOLD", "") return fmt def format(self, record): if record.msg.startswith('| '): # If we already log smth which supposed to go without formatting, like # output for running a command, just return the message and be done return record.msg levelname = record.levelname if self.use_color and levelname in self.COLORS: fore_color = 30 + self.COLORS[levelname] levelname_color = self.COLOR_SEQ % fore_color + "%-7s" % levelname + self.RESET_SEQ record.levelname = levelname_color record.msg = record.msg.replace("\n", "\n| ") if self._tb: record.msg = self._tb() + " " + record.msg return logging.Formatter.format(self, record) class LoggerHelper(object): """Helper to establish and control a Logger""" def __init__(self, name='duecredit'): self.name = name self.lgr = logging.getLogger(name) def _get_environ(self, var, default=None): return os.environ.get(self.name.upper() + '_%s' % var.upper(), default) def set_level(self, level=None, default='WARNING'): """Helper to set loglevel for an arbitrary logger By default operates for 'duecredit'. TODO: deduce name from upper module name so it could be reused without changes """ if level is None: # see if nothing in the environment level = self._get_environ('LOGLEVEL') if level is None: level = default try: # it might be a string which still represents an int log_level = int(level) except ValueError: # or a string which corresponds to a constant;) log_level = getattr(logging, level.upper()) self.lgr.setLevel(log_level) def get_initialized_logger(self, logtarget=None): """Initialize and return the logger Parameters ---------- target: string, optional Which log target to request logger for logtarget: { 'stdout', 'stderr', str }, optional Where to direct the logs. stdout and stderr stand for standard streams. Any other string is considered a filename. Multiple entries could be specified comma-separated Returns ------- logging.Logger """ # By default mimic previously talkative behavior logtarget = self._get_environ('LOGTARGET', logtarget or 'stdout') # Allow for multiple handlers being specified, comma-separated if ',' in logtarget: for handler_ in logtarget.split(','): self.get_initialized_logger(logtarget=handler_) return self.lgr if logtarget.lower() in ('stdout', 'stderr') : loghandler = logging.StreamHandler(getattr(sys, logtarget.lower())) use_color = is_interactive() # explicitly decide here else: # must be a simple filename # Use RotatingFileHandler for possible future parametrization to keep # log succinct and rotating loghandler = logging.handlers.RotatingFileHandler(logtarget) use_color = False # I had decided not to guard this call and just raise exception to go # out happen that specified file location is not writable etc. # But now improve with colors and useful information such as time loghandler.setFormatter( ColorFormatter(use_color=use_color, log_name=self._get_environ("LOGNAME", False))) #logging.Formatter('%(asctime)-15s %(levelname)-6s %(message)s')) self.lgr.addHandler(loghandler) self.set_level() # set default logging level return self.lgr lgr = LoggerHelper().get_initialized_logger() duecredit-0.6.0/duecredit/parsers.py000066400000000000000000000015411273067304300174740ustar00rootroot00000000000000import re def extract_references_from_rst(rst): # for now will be very simple, just trying to separate # then up until the end or another section starting pass def test_extract_references_from_rst(): # some obscure examples of how people specify references samples = [ """ References ---------- .. [1] line1 line2 .. [2] line11 line12 """, """ References ---------- - line1 line2 """, """ References ---------- .. [xyz1] line1 line2 .. [xyz2] line11 line12 Buga duga --------- """, """ References ---------- line1 line2 line11 line12 """ ] extract_references_from_rst(samples[0]) duecredit-0.6.0/duecredit/stub.py000066400000000000000000000036541273067304300170010ustar00rootroot00000000000000# emacs: at the end of the file # ex: set sts=4 ts=4 sw=4 et: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### # """ Stub file for a guaranteed safe import of duecredit constructs: if duecredit is not available. To use it, place it into your project codebase to be imported, e.g. copy as cp stub.py /path/tomodule/module/due.py Note that it might be better to avoid naming it duecredit.py to avoid shadowing installed duecredit. Then use in your code as from .due import due, Doi, BibTeX See https://github.com/duecredit/duecredit/blob/master/README.md for examples. Origin: Originally a part of the duecredit Copyright: 2015-2016 DueCredit developers License: BSD-2 """ __version__ = '0.0.5' class InactiveDueCreditCollector(object): """Just a stub at the Collector which would not do anything""" def _donothing(self, *args, **kwargs): """Perform no good and no bad""" pass def dcite(self, *args, **kwargs): """If I could cite I would""" def nondecorating_decorator(func): return func return nondecorating_decorator cite = load = add = _donothing def __repr__(self): return self.__class__.__name__ + '()' def _donothing_func(*args, **kwargs): """Perform no good and no bad""" pass try: from duecredit import due, BibTeX, Doi, Url if 'due' in locals() and not hasattr(due, 'cite'): raise RuntimeError( "Imported due lacks .cite. DueCredit is now disabled") except Exception as e: if type(e).__name__ != 'ImportError': import logging logging.getLogger("duecredit").error( "Failed to import duecredit due to %s" % str(e)) # Initiate due stub due = InactiveDueCreditCollector() BibTeX = Doi = Url = _donothing_func # Emacs mode definitions # Local Variables: # mode: python # py-indent-offset: 4 # tab-width: 4 # indent-tabs-mode: nil # End: duecredit-0.6.0/duecredit/tests/000077500000000000000000000000001273067304300166045ustar00rootroot00000000000000duecredit-0.6.0/duecredit/tests/__init__.py000066400000000000000000000000001273067304300207030ustar00rootroot00000000000000duecredit-0.6.0/duecredit/tests/envs/000077500000000000000000000000001273067304300175575ustar00rootroot00000000000000duecredit-0.6.0/duecredit/tests/envs/nolxml/000077500000000000000000000000001273067304300210705ustar00rootroot00000000000000duecredit-0.6.0/duecredit/tests/envs/nolxml/lxml.py000066400000000000000000000000221273067304300224100ustar00rootroot00000000000000raise ImportError duecredit-0.6.0/duecredit/tests/envs/stubbed/000077500000000000000000000000001273067304300212075ustar00rootroot00000000000000duecredit-0.6.0/duecredit/tests/envs/stubbed/README.txt000066400000000000000000000001741273067304300227070ustar00rootroot00000000000000A sample script with our stub to verify that it runs without complete failure even if there are internal duecredit problems duecredit-0.6.0/duecredit/tests/envs/stubbed/due.py000077700000000000000000000000001273067304300245212../../../stub.pyustar00rootroot00000000000000duecredit-0.6.0/duecredit/tests/envs/stubbed/script.py000066400000000000000000000004771273067304300230750ustar00rootroot00000000000000from due import due, Doi kwargs = dict( entry=Doi("10.1007/s12021-008-9041-y"), description="Multivariate pattern analysis of neural data", tags=["use"] ) import numpy as np due.cite(path="test", **kwargs) @due.dcite(**kwargs) def method(arg): return arg+1 assert(method(1) == 2) print("done123") duecredit-0.6.0/duecredit/tests/mod/000077500000000000000000000000001273067304300173635ustar00rootroot00000000000000duecredit-0.6.0/duecredit/tests/mod/__init__.py000066400000000000000000000001641273067304300214750ustar00rootroot00000000000000"""Module to test various duecredit functionality, e.g. injections""" from .imported import * __version__ = '0.5' duecredit-0.6.0/duecredit/tests/mod/imported.py000066400000000000000000000011131273067304300215540ustar00rootroot00000000000000def testfunc1(arg1, kwarg1=None): """custom docstring""" return "testfunc1: %s, %s" % (arg1, kwarg1) class TestClass1(object): """wrong custom docstring""" def testmeth1(self, arg1, kwarg1=None): """custom docstring""" return "TestClass1.testmeth1: %s, %s" % (arg1, kwarg1) class TestClass12(object): """wrong custom docstring""" class Embed(object): """wrong custom docstring""" def testmeth1(self, arg1, kwarg1=None): """custom docstring""" return "TestClass12.Embed.testmeth1: %s, %s" % (arg1, kwarg1)duecredit-0.6.0/duecredit/tests/mod/submod.py000066400000000000000000000003771273067304300212350ustar00rootroot00000000000000"""Some test submodule""" def testfunc(arg1, kwarg1=None): """testfunc docstring""" return "testfunc: %s, %s" % (arg1, kwarg1) class TestClass(object): def testmeth(self, arg1, kwarg1=None): return "testmeth: %s, %s" % (arg1, kwarg1)duecredit-0.6.0/duecredit/tests/test__main__.py000066400000000000000000000027421273067304300216030ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import atexit import sys from mock import patch from six.moves import StringIO from nose.tools import assert_raises, assert_equal from .. import __main__, __version__ from .. import due from ..utils import with_tempfile @patch('sys.stdout', new_callable=StringIO) def test_main_help(stdout): assert_raises(SystemExit, __main__.main, ['__main__.py', '--help']) assert( stdout.getvalue().startswith( "Usage: %s -m duecredit [OPTIONS] [ARGS]\n" % sys.executable )) @patch('sys.stdout', new_callable=StringIO) def test_main_version(stdout): assert_raises(SystemExit, __main__.main, ['__main__.py', '--version']) assert_equal(stdout.getvalue().rstrip(), "duecredit %s" % __version__) @patch.object(due, 'activate') @patch('sys.stdout', new_callable=StringIO) @with_tempfile(content='print("Running the script")\n'.encode()) def test_main_run_a_script(stdout, mock_activate, f): __main__.main(['__main__.py', f]) assert_equal(stdout.getvalue().rstrip(), "Running the script") # And we have "activated" the due mock_activate.assert_called_once_with(True) duecredit-0.6.0/duecredit/tests/test_api.py000066400000000000000000000121311273067304300207640ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from duecredit.collector import DueCreditCollector from duecredit.stub import InactiveDueCreditCollector from duecredit.entries import BibTeX, Doi from ..utils import on_windows from .utils import KnownFailure from nose.tools import assert_equal from nose.tools import assert_in from nose import SkipTest def _test_api(due): # add references due.add(BibTeX('@article{XXX00, ...}')) # could even be by DOI -- we need to fetch and cache those due.add(Doi("xxx.yyy/zzz.1", key="XXX01")) # and/or load multiple from a file due.load('/home/siiioul/deep/good_intentions.bib') # Cite entire module due.cite('XXX00', description="Answers to existential questions", path="module") # Cita some method within some submodule due.cite('XXX01', description="More answers to existential questions", path="module.submodule:class1.whoknowswhat2.func1") # dcite for decorator cite # cite specific functionality if/when it gets called up @due.dcite('XXX00', description="Provides an answer for meaningless existence") def purpose_of_life(): return None class Child(object): # Conception process is usually way too easy to be referenced def __init__(self): pass # including functionality within/by the methods @due.dcite('XXX00') def birth(self, gender): return "Rachel was born" kid = Child() kid.birth("female") def test_api(): yield _test_api, DueCreditCollector() yield _test_api, InactiveDueCreditCollector() import os import sys from os.path import dirname, join as pathjoin, pardir, normpath from mock import patch from subprocess import Popen, PIPE badlxml_path = pathjoin(dirname(__file__), 'envs', 'nolxml') stubbed_script = pathjoin(dirname(__file__), 'envs', 'stubbed', 'script.py') def run_python_command(cmd=None, script=None): """Just a tiny helper which runs command and returns exit code, stdout, stderr""" assert(bool(cmd) != bool(script)) # one or another, not both args = ['-c', cmd] if cmd else [script] python = Popen([sys.executable] + args, stdout=PIPE, stderr=PIPE) stdout, stderr = python.communicate() # wait() ret = python.poll() return ret, stdout.decode(), stderr.decode() mock_env_nolxml = {'PYTHONPATH': "%s:%s" % (badlxml_path, os.environ.get('PYTHONPATH', ''))} # Since duecredit and possibly lxml already loaded, let's just test # ability to import in absence of lxml via external call to python def test_noincorrect_import_if_no_lxml(): if on_windows: raise KnownFailure("Fails for some reason on Windows") with patch.dict(os.environ, mock_env_nolxml): # make sure out mocking works here ret, out, err = run_python_command('import lxml') assert_equal(ret, 1) assert_in('ImportError', err) # # make sure out mocking works here ret, out, err = run_python_command('import duecredit') assert_equal(err, '') assert_equal(out, '') assert_equal(ret, 0) def check_noincorrect_import_if_no_lxml_numpy(kwargs, env): # Now make sure that we would not crash entire process at the end when unable to # produce sensible output when we have something to cite # we do inject for numpy try: import numpy except ImportError: raise SkipTest("We need to have numpy to test correct operation") mock_env_nolxml_ = mock_env_nolxml.copy() mock_env_nolxml_.update(env) with patch.dict(os.environ, mock_env_nolxml_): ret, out, err = run_python_command(**kwargs) assert_equal(err, '') if os.environ.get('DUECREDIT_ENABLE', False): # we enabled duecredit assert_in('For formatted output we need citeproc', out) assert_in('done123', out) elif os.environ.get('DUECREDIT_TEST_EARLY_IMPORT_ERROR'): assert_in('ImportError', out) assert_in('DUECREDIT_TEST_EARLY_IMPORT_ERROR', out) assert_in('done123', out) else: assert_equal('done123\n', out) assert_equal(ret, 0) # but we must not fail overall regardless def test_noincorrect_import_if_no_lxml_numpy(): for kwargs in ( # direct command to evaluate {'cmd': 'import duecredit; import numpy as np; print("done123")'}, # script with decorated funcs etc -- should be importable {'script': stubbed_script} ): yield check_noincorrect_import_if_no_lxml_numpy, kwargs, {} yield check_noincorrect_import_if_no_lxml_numpy, kwargs, {'DUECREDIT_ENABLE': 'yes'} yield check_noincorrect_import_if_no_lxml_numpy, kwargs, {'DUECREDIT_TEST_EARLY_IMPORT_ERROR': 'yes'} if __name__ == '__main__': from duecredit import due _test_api(due)duecredit-0.6.0/duecredit/tests/test_cmdline.py000066400000000000000000000026221273067304300216320ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import sys from mock import patch from six.moves import StringIO from nose.tools import assert_raises, assert_equal from .. import __version__ from ..cmdline import main def test_import(): import duecredit.cmdline import duecredit.cmdline.main @patch('sys.stdout', new_callable=StringIO) def test_main_help(stdout): assert_raises(SystemExit, main.main, ['--help']) assert(stdout.getvalue().lstrip().startswith("Usage: ")) # differs among Python versions -- catch both @patch('sys.std' + ('err' if sys.version_info < (3, 4) else 'out'), new_callable=StringIO) def test_main_version(out): assert_raises(SystemExit, main.main, ['--version']) assert_equal((out.getvalue()).split('\n')[0], "duecredit %s" % __version__) # smoke test the cmd_summary # TODO: carry sample .duecredit.p, point to that file, mock TextOutput and BibTeXOutput .dumps def test_smoke_cmd_summary(): main.main(['summary']) # test the not implemented cmd_test def test_cmd_test(): assert_raises(SystemExit, main.main, ['test'])duecredit-0.6.0/duecredit/tests/test_collector.py000066400000000000000000000251711273067304300222110ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from ..collector import DueCreditCollector, InactiveDueCreditCollector, \ CollectorSummary, Citation from ..entries import BibTeX, Doi from ..io import PickleOutput from ..utils import with_tempfile from mock import patch from nose.tools import assert_equal, assert_is_instance, assert_raises, assert_true from nose.tools import assert_false import os import tempfile def _test_entry(due, entry): due.add(entry) _sample_bibtex = """ @ARTICLE{XXX0, author = {Halchenko, Yaroslav O. and Hanke, Michael}, title = {Open is not enough. Let{'}s take the next step: An integrated, community-driven computing platform for neuroscience}, journal = {Frontiers in Neuroinformatics}, year = {2012}, volume = {6}, number = {00022}, doi = {10.3389/fninf.2012.00022}, issn = {1662-5196}, localfile = {HH12.pdf}, } """ _sample_bibtex2 = """ @ARTICLE{Atkins_2002, title = {title}, volume = {666}, url = {http://dx.doi.org/10.1038/nrd842}, DOI = {10.1038/nrd842}, number = {3009}, journal = {My Fancy. Journ.}, publisher = {The Publisher}, author = {Atkins, Joshua H. and Gershell, Leland J.}, year = {2002}, month = {Jul}, } """ _sample_doi = "10.3389/fninf.2012.00022" def test_citation_paths(): entry = BibTeX(_sample_bibtex) cit1 = Citation(entry, path="somemodule") assert_true(cit1.cites_module) assert_equal(cit1.module, "somemodule") cit2 = Citation(entry, path="somemodule.submodule") assert_true(cit2.cites_module) assert_equal(cit2.module, "somemodule.submodule") assert_true(cit1 in cit1) assert_true(cit2 in cit1) assert_false(cit1 in cit2) cit3 = Citation(entry, path="somemodule.submodule:class2.func2") assert_false(cit3.cites_module) assert_equal(cit3.module, "somemodule.submodule") assert_true(cit2 in cit1) assert_true(cit3 in cit1) assert_true(cit3 in cit2) assert_false(cit2 in cit3) cit4 = Citation(entry, path="somemodule2:class2.func2") assert_false(cit4.cites_module) assert_equal(cit4.module, "somemodule2") assert_false(cit1 in cit4) assert_false(cit4 in cit1) def test_entry(): entry = BibTeX(_sample_bibtex) yield _test_entry, DueCreditCollector(), entry entries = [BibTeX(_sample_bibtex), BibTeX(_sample_bibtex), Doi(_sample_doi)] yield _test_entry, DueCreditCollector(), entries def _test_dcite_basic(due, callable): assert_equal(callable("magical", 1), "load") # verify that @wraps correctly passes all the docstrings etc assert_equal(callable.__name__, "method") assert_equal(callable.__doc__, "docstring") def test_dcite_method(): # Test basic wrapping that we don't mask out the arguments for due in [DueCreditCollector(), InactiveDueCreditCollector()]: active = isinstance(due, DueCreditCollector) due.add(BibTeX(_sample_bibtex)) @due.dcite("XXX0", path='method') def method(arg1, kwarg2="blah"): """docstring""" assert_equal(arg1, "magical") assert_equal(kwarg2, 1) return "load" class SomeClass(object): @due.dcite("XXX0", path='someclass:method') def method(self, arg1, kwarg2="blah"): """docstring""" assert_equal(arg1, "magical") assert_equal(kwarg2, 1) return "load" if active: assert_equal(due.citations, {}) assert_equal(len(due._entries), 1) yield _test_dcite_basic, due, method if active: assert_equal(len(due.citations), 1) assert_equal(len(due._entries), 1) citation = due.citations[("method", "XXX0")] assert_equal(citation.count, 1) # TODO: this is probably incomplete path but unlikely we would know # any better assert_equal(citation.path, "method") instance = SomeClass() yield _test_dcite_basic, due, instance.method if active: assert_equal(len(due.citations), 2) assert_equal(len(due._entries), 1) # TODO: we should actually get path/counts pairs so here citation = due.citations[("someclass:method", "XXX0")] assert_equal(citation.path, "someclass:method") assert_equal(citation.count, 1) # And we explicitly stated that module need to be cited assert_true(citation.cite_module) class SomeClass2(object): @due.dcite("XXX0", path="some.module.without.method") def method2(self, arg1, kwarg2="blah"): assert_equal(arg1, "magical") return "load" # and a method pointing to the module instance2 = SomeClass() yield _test_dcite_basic, due, instance2.method if active: assert_equal(len(due.citations), 2) # different paths assert_equal(len(due._entries), 1) # the same entry # TODO: we should actually get path/counts pairs so here # it is already a different path # And we still explicitly stated that module need to be cited assert_true(citation.cite_module) def _test_args_match_conditions(conds): args_match_conditions = DueCreditCollector._args_match_conditions assert_true(args_match_conditions(conds)) assert_true(args_match_conditions(conds, None)) assert_true(args_match_conditions(conds, someirrelevant=True)) assert_true(args_match_conditions(conds, method='purge')) assert_true(args_match_conditions(conds, method='fullpurge')) assert_true(args_match_conditions(conds, None, 'purge')) assert_true(args_match_conditions(conds, None, 'fullpurge')) assert_true(args_match_conditions(conds, None, 'fullpurge', someirrelevant="buga")) assert_false(args_match_conditions(conds, None, 'push')) assert_false(args_match_conditions(conds, method='push')) if len(conds) < 2: return # got compound case assert_true(args_match_conditions(conds, scope='life')) assert_false(args_match_conditions(conds, scope='someother')) # should be "and", so if one not matching -- both not matching assert_false(args_match_conditions(conds, method="wrong", scope='life')) assert_false(args_match_conditions(conds, method="purge", scope='someother')) #assert_true(args_match_conditions(conds, None, None, 'life')) # ambiguous/conflicting def test_args_match_conditions(): yield _test_args_match_conditions, {(1, 'method'): {'purge', 'fullpurge', 'DC_DEFAULT'}} yield _test_args_match_conditions, {(1, 'method'): {'purge', 'fullpurge', 'DC_DEFAULT'}, (2, 'scope'): {'life', 'DC_DEFAULT'}} def _test_dcite_match_conditions(due, callable, path): assert_equal(due.citations, {}) assert_equal(len(due._entries), 1) assert_equal(callable("magical", "unknown"), "load unknown") assert_equal(due.citations, {}) assert_equal(len(due._entries), 1) assert_equal(callable("magical"), "load blah") assert_equal(len(due.citations), 1) assert_equal(len(due._entries), 1) entry = due._entries['XXX0'] assert_equal(due.citations[(path, 'XXX0')].count, 1) # Cause the same citation assert_equal(callable("magical", "blah"), "load blah") # Nothing should change assert_equal(len(due.citations), 1) assert_equal(len(due._entries), 1) assert_equal(due.citations[(path, 'XXX0')].count, 2) # Besides the count # Now cause new citation given another value assert_equal(callable("magical", "boo"), "load boo") assert_equal(len(due.citations), 2) assert_equal(len(due._entries), 2) assert_equal(due.citations[(path, 'XXX0')].count, 2) # Count should stay the same for XXX0 assert_equal(due.citations[(path, "10.3389/fninf.2012.00022")].count, 1) # but we get a new one def test_dcite_match_conditions_function(): due = DueCreditCollector() due.add(BibTeX(_sample_bibtex)) @due.dcite("XXX0", path='callable', conditions={(1, "kwarg2"): {"blah", "DC_DEFAULT"}}) @due.dcite(Doi(_sample_doi), path='callable', conditions={(1, "kwarg2"): {"boo"}}) def method(arg1, kwarg2="blah"): """docstring""" assert_equal(arg1, "magical") return "load %s" % kwarg2 _test_dcite_match_conditions(due, method, 'callable') def test_dcite_match_conditions_method(): due = DueCreditCollector() due.add(BibTeX(_sample_bibtex)) class Citeable(object): def __init__(self, param=None): self.param = param @due.dcite("XXX0", path='obj.callable', conditions={(2, "kwarg2"): {"blah", "DC_DEFAULT"}, (0, 'self.param'): {"paramvalue"} # must be matched }) @due.dcite(Doi(_sample_doi), path='obj.callable', conditions={(2, "kwarg2"): {"boo"}}) def method(self, arg1, kwarg2="blah"): """docstring""" assert_equal(arg1, "magical") return "load %s" % kwarg2 citeable = Citeable(param="paramvalue") _test_dcite_match_conditions(due, citeable.method, 'obj.callable') # now test for self.param - @with_tempfile def test_get_output_handler_method(f): with patch.dict(os.environ, {'DUECREDIT_OUTPUTS': 'pickle'}): entry = BibTeX(_sample_bibtex) collector = DueCreditCollector() collector.cite(entry, path='module') summary = CollectorSummary(collector, fn=f) handlers = [summary._get_output_handler(type_, collector) for type_ in ['pickle']] #assert_is_instance(handlers[0], TextOutput) assert_is_instance(handlers[0], PickleOutput) assert_raises(NotImplementedError, summary._get_output_handler, 'nothing', collector) def test_collectors_uniform_API(): get_api = lambda obj: [x for x in sorted(dir(obj)) if not x.startswith('_') or x in ('__call__')] assert_equal(get_api(DueCreditCollector), get_api(InactiveDueCreditCollector)) def _test__docs__(method): assert("entry:" in method.__doc__) assert("tags: " in method.__doc__) def test__docs__(): yield _test__docs__, DueCreditCollector.cite yield _test__docs__, DueCreditCollector.dcite yield _test__docs__, Citation.__init__ duecredit-0.6.0/duecredit/tests/test_dueswitch.py000066400000000000000000000023071273067304300222160ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from mock import patch import atexit from nose import SkipTest from ..injections.injector import DueCreditInjector from ..dueswitch import due @patch.object(DueCreditInjector, 'activate') @patch.object(atexit, 'register') def test_dueswitch_activate(mock_register, mock_activate): was_active = due.active # atexit.register(crap) # injector.activate() due.activate() if was_active: # we can only test that mocked methods do not invoked second time mock_activate.assert_not_called() mock_register.assert_not_called() raise SkipTest("due is already active, can't test more at this point") # was not active, so should have called activate of the injector class mock_activate.assert_called_once_with() mock_register.assert_called_once_with(due._dump_collector_summary) duecredit-0.6.0/duecredit/tests/test_import_doi000066400000000000000000000420201273067304300217310ustar00rootroot00000000000000interactions: - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.8.1] method: GET uri: http://dx.doi.org/10.1038/nrd842 response: body: {string: !!python/unicode 'Handle Redirect http://data.crossref.org/10.1038%2Fnrd842'} headers: content-length: ['167'] content-type: [text/html;charset=utf-8] date: ['Wed, 04 May 2016 18:52:39 GMT'] expires: ['Wed, 04 May 2016 19:51:05 GMT'] location: ['http://data.crossref.org/10.1038%2Fnrd842'] server: [Apache-Coyote/1.1] vary: [Accept] status: {code: 303, message: See Other} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.8.1] method: GET uri: http://data.crossref.org/10.1038%2Fnrd842 response: body: {string: !!python/unicode "@article{Atkins_2002,\n\tdoi = {10.1038/nrd842},\n\turl = {http://dx.doi.org/10.1038/nrd842},\n\tyear = 2002,\n\tmonth = {jul},\n\tpublisher = {Nature Publishing Group},\n\tvolume = {1},\n\tnumber = {7},\n\tpages = {491--492},\n\tauthor = {Joshua H. Atkins and Leland J. Gershell},\n\ttitle = {From the analyst{\\textquotesingle}s couch: Selective anticancer drugs},\n\tjournal = {Nature Reviews Drug Discovery}\n}"} headers: access-control-allow-headers: ['X-Requested-With, Accept, Accept-Encoding, Accept-Charset, Accept-Language, Accept-Ranges, Cache-Control'] access-control-allow-origin: ['*'] access-control-expose-headers: [Link] content-length: ['390'] content-type: [application/x-bibtex] date: ['Wed, 04 May 2016 18:52:40 GMT'] link: ['; rel="canonical"'] server: [http-kit] status: {code: 200, message: OK} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.8.1] method: GET uri: http://dx.doi.org/fasljfdldaksj response: body: {string: !!python/unicode "\n\n\nError: DOI Not Found\n\n\n\n\n \n\n\n\n\n\n\n
\n\"Logo\"\n
\n\n
\n
\n
\n\n\n\n\n\n \n \n \n \n
\"\"\n \ HOME  | HANDBOOK  | FACTSHEETS  | FAQs  |  RESOURCES  | USERS  | NEWS  | MEMBERS AREA\n
\n\n\n
\n
\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n
\n\"\"\n
\n\n

DOI Not Found

\n\n
 
\n\n\n\n

10/fasljfdldaksj

\n\n
 
\n\n\n\n\n

This DOI cannot be found in the DOI System. Possible reasons are:

\n\n\n
    \n\n
  • The DOI is incorrect in your source. Search for the item by name, title, or other metadata using a search engine.
  • \n\n
  • The DOI was copied incorrectly. Check to see that the string includes all the characters before and after the slash and no sentence punctuation marks.
  • \n\n
  • The DOI has not been activated yet. Please try again later, and report the problem if the error continues.
  • \n\n
\n\n\n\n
 
\n\n

You may report this error to the responsible DOI Registration Agency using the form below. Include your email address to receive confirmation and feedback.

\n\n
\n\n
\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Your Email Address:
Additional Information About the Error:
\n\n

\n\n
\n
\n\n\n\n\n
\"\"
\n\n
 
\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n
\nDOI System Proxy Server Documentation\n
\n\"DOI_disc_logo\"\n\n®, DOI®, DOI.ORG®, and shortDOI® are trademarks of the International DOI Foundation.\n
\n\n\n"} headers: content-length: ['4980'] content-type: [text/html;charset=utf-8] date: ['Wed, 04 May 2016 18:52:40 GMT'] server: [Apache-Coyote/1.1] status: {code: 404, message: Not Found} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.8.1] method: GET uri: http://dx.doi.org/fasljfdldaksj response: body: {string: !!python/unicode "\n\n\nError: DOI Not Found\n\n\n\n\n \n\n\n\n\n\n\n
\n\"Logo\"\n
\n\n
\n
\n
\n\n\n\n\n\n \n \n \n \n
\"\"\n \ HOME  | HANDBOOK  | FACTSHEETS  | FAQs  |  RESOURCES  | USERS  | NEWS  | MEMBERS AREA\n
\n\n\n
\n
\n\n\n\n \n\n\n\n\n\n\n\n\n\n\n
\n\"\"\n
\n\n

DOI Not Found

\n\n
 
\n\n\n\n

10/fasljfdldaksj

\n\n
 
\n\n\n\n\n

This DOI cannot be found in the DOI System. Possible reasons are:

\n\n\n
    \n\n
  • The DOI is incorrect in your source. Search for the item by name, title, or other metadata using a search engine.
  • \n\n
  • The DOI was copied incorrectly. Check to see that the string includes all the characters before and after the slash and no sentence punctuation marks.
  • \n\n
  • The DOI has not been activated yet. Please try again later, and report the problem if the error continues.
  • \n\n
\n\n\n\n
 
\n\n

You may report this error to the responsible DOI Registration Agency using the form below. Include your email address to receive confirmation and feedback.

\n\n
\n\n
\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Your Email Address:
Additional Information About the Error:
\n\n

\n\n
\n
\n\n\n\n\n
\"\"
\n\n
 
\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n
\nDOI System Proxy Server Documentation\n
\n\"DOI_disc_logo\"\n\n®, DOI®, DOI.ORG®, and shortDOI® are trademarks of the International DOI Foundation.\n
\n\n\n"} headers: content-length: ['4980'] content-type: [text/html;charset=utf-8] date: ['Wed, 04 May 2016 18:52:40 GMT'] server: [Apache-Coyote/1.1] status: {code: 404, message: Not Found} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.8.1] method: GET uri: http://dx.doi.org/10.5281/zenodo.50186 response: body: {string: !!python/unicode 'Handle Redirect http://data.datacite.org/10.5281%2Fzenodo.50186'} headers: content-length: ['179'] content-type: [text/html;charset=utf-8] date: ['Wed, 04 May 2016 18:52:41 GMT'] expires: ['Wed, 04 May 2016 19:47:56 GMT'] location: ['http://data.datacite.org/10.5281%2Fzenodo.50186'] server: [Apache-Coyote/1.1] vary: [Accept] status: {code: 303, message: See Other} - request: body: null headers: Accept: [application/x-bibtex; charset=utf-8] Accept-Encoding: ['gzip, deflate'] Connection: [keep-alive] User-Agent: [python-requests/2.8.1] method: GET uri: http://data.datacite.org/10.5281%2Fzenodo.50186 response: body: {string: !!python/unicode "@data{3835635f-efca-4ec0-949d-7fb5770ec4cb,\n \ doi = {10.5281/zenodo.50186},\n url = {http://dx.doi.org/10.5281/zenodo.50186},\n \ author = {Satrajit Ghosh; Chris Filo Gorgolewski; Oscar Esteban; Erik Ziegler; David Ellis; cindeem; Michael Waskom; Dav Clark; Michael; Fred Loney; Alexandre M. S.; Michael Notter; Hans Johnson; Anisha Keshavan; Yaroslav Halchenko; Carlo Hamalainen; Blake Dewey; Ben Cipollini; Daniel Clark; Julia Huntenburg; Drew Erickson; Michael Hanke; moloney; Jason W; Demian Wassermann; cdla; Nolan Nichols; Chris Markiewicz; Jarrod Millman; Arman Eshaghi; },\n publisher = {Zenodo},\n title = {nipype: Release candidate 1 for version 0.12.0},\n \ year = {2016}\n}"} headers: access-control-allow-headers: ['Content-Type,Accept,Accept-Encoding,Origin,User-Agent,Cache-Control,Keep-Alive'] access-control-allow-origin: ['*'] cache-control: ['no-transform, max-age=3600'] connection: [keep-alive] content-type: [application/x-bibtex] date: ['Wed, 04 May 2016 18:54:54 GMT'] last-modified: ['Thu, 21 Apr 2016 13:58:49 GMT'] server: [nginx/1.1.19] vary: [Accept] status: {code: 200, message: OK} version: 1 duecredit-0.6.0/duecredit/tests/test_injections.py000066400000000000000000000314411273067304300223650ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import gc import os import sys from six import viewvalues, PY2 if PY2: import __builtin__ else: import builtins as __builtin__ _orig__import__ = __builtin__.__import__ from duecredit.collector import DueCreditCollector, InactiveDueCreditCollector from duecredit.entries import BibTeX, Doi from ..injections.injector import DueCreditInjector, find_object, get_modules_for_injection from .. import __version__ from nose import SkipTest from nose.tools import assert_equal from nose.tools import assert_false from nose.tools import assert_true try: import mvpa2 _have_mvpa2 = True except ImportError: _have_mvpa2 = False from logging import getLogger lgr = getLogger('duecredit.tests.injector') class TestActiveInjector(object): def setup(self): lgr.log(5, "Setting up for a TestActiveInjector test") self._cleanup_modules() self.due = DueCreditCollector() self.injector = DueCreditInjector(collector=self.due) self.injector.activate(retrospect=False) # numpy might be already loaded... def teardown(self): lgr.log(5, "Tearing down after a TestActiveInjector test") # gc might not pick up inj after some tests complete # so we will always deactivate explicitly self.injector.deactivate() assert_true(__builtin__.__import__ is _orig__import__) self._cleanup_modules() def _cleanup_modules(self): if 'duecredit.tests.mod' in sys.modules: sys.modules.pop('duecredit.tests.mod') def _test_simple_injection(self, func, import_stmt, func_call=None): assert_false('duecredit.tests.mod' in sys.modules) self.injector.add('duecredit.tests.mod', func, Doi('1.2.3.4'), description="Testing %s" % func, min_version='0.1', max_version='1.0', tags=["implementation", "very custom"]) assert_false('duecredit.tests.mod' in sys.modules) # no import happening assert_equal(len(self.due._entries), 0) assert_equal(len(self.due.citations), 0) globals_, locals_ = {}, {} exec(import_stmt, globals_, locals_) assert_equal(len(self.due._entries), 1) # we should get an entry now assert_equal(len(self.due.citations), 0) # but not yet a citation import duecredit.tests.mod as mod _, _, obj = find_object(mod, func) assert_true(obj.__duecredited__) # we wrapped assert_false(obj.__duecredited__ is obj) # and it is not pointing to the same func assert_equal(obj.__doc__, "custom docstring") # we preserved docstring # TODO: test decoration features -- preserver __doc__ etc exec('ret = %s(None, "somevalue")' % (func_call or func), globals_, locals_) # XXX: awkwardly 'ret' is not found in the scope while running nosetests # under python3.4, although present in locals()... WTF? assert_equal(locals_['ret'], "%s: None, somevalue" % func) assert_equal(len(self.due._entries), 1) assert_equal(len(self.due.citations), 1) # TODO: there must be a cleaner way to get first value citation = list(viewvalues(self.due.citations))[0] # TODO: ATM we don't allow versioning of the submodules -- we should # assert_equal(citation.version, '0.5') # ATM it will be the duecredit's version assert_equal(citation.version, __version__) assert(citation.tags == ['implementation', 'very custom']) def _test_double_injection(self, func, import_stmt, func_call=None): assert_false('duecredit.tests.mod' in sys.modules) # add one injection self.injector.add('duecredit.tests.mod', func, Doi('1.2.3.4'), description="Testing %s" % func, min_version='0.1', max_version='1.0', tags=["implementation", "very custom"]) #add another one self.injector.add('duecredit.tests.mod', func, Doi('1.2.3.5'), description="Testing %s" % func, min_version='0.1', max_version='1.0', tags=["implementation", "very custom"]) assert_false('duecredit.tests.mod' in sys.modules) # no import happening assert_equal(len(self.due._entries), 0) assert_equal(len(self.due.citations), 0) globals_, locals_ = {}, {} exec(import_stmt, globals_, locals_) assert_equal(len(self.due._entries), 2) # we should get two entries now assert_equal(len(self.due.citations), 0) # but not yet a citation import duecredit.tests.mod as mod _, _, obj = find_object(mod, func) assert_true(obj.__duecredited__) # we wrapped assert_false(obj.__duecredited__ is obj) # and it is not pointing to the same func assert_equal(obj.__doc__, "custom docstring") # we preserved docstring # TODO: test decoration features -- preserver __doc__ etc exec('ret = %s(None, "somevalue")' % (func_call or func), globals_, locals_) # XXX: awkwardly 'ret' is not found in the scope while running nosetests # under python3.4, although present in locals()... WTF? assert_equal(locals_['ret'], "%s: None, somevalue" % func) assert_equal(len(self.due._entries), 2) assert_equal(len(self.due.citations), 2) # TODO: there must be a cleaner way to get first value citation = list(viewvalues(self.due.citations))[0] # TODO: ATM we don't allow versioning of the submodules -- we should # assert_equal(citation.version, '0.5') # ATM it will be the duecredit's version assert_equal(citation.version, __version__) assert(citation.tags == ['implementation', 'very custom']) def test_simple_injection(self): yield self._test_simple_injection, "testfunc1", 'from duecredit.tests.mod import testfunc1', None yield self._test_simple_injection, "TestClass1.testmeth1", \ 'from duecredit.tests.mod import TestClass1; c = TestClass1()', 'c.testmeth1' yield self._test_simple_injection, "TestClass12.Embed.testmeth1", \ 'from duecredit.tests.mod import TestClass12; c = TestClass12.Embed()', 'c.testmeth1' def test_double_injection(self): yield self._test_double_injection, "testfunc1", 'from duecredit.tests.mod import testfunc1', None yield self._test_double_injection, "TestClass1.testmeth1", \ 'from duecredit.tests.mod import TestClass1; c = TestClass1()', 'c.testmeth1' yield self._test_double_injection, "TestClass12.Embed.testmeth1", \ 'from duecredit.tests.mod import TestClass12; c = TestClass12.Embed()', 'c.testmeth1' def test_delayed_entries(self): # verify that addition of delayed injections happened modules_for_injection = get_modules_for_injection() assert_equal(len(self.injector._delayed_injections), len(modules_for_injection)) assert_equal(self.injector._entry_records, {}) # but no entries were added assert('scipy' in self.injector._delayed_injections) # We must have it ATM try: # We do have injections for scipy import scipy except ImportError as e: raise SkipTest("scipy was not found: %s" % (e,)) def test_import_mvpa2_suite(self): if not _have_mvpa2: raise SkipTest("no mvpa2 found") # just a smoke test for now import mvpa2.suite as mv def _test_incorrect_path(self, mod, obj): ref = Doi('1.2.3.4') # none of them should lead to a failure self.injector.add(mod, obj, ref) # now cause the import handling -- it must not fail # TODO: catch/analyze warnings exec('from duecredit.tests.mod import testfunc1', {}, {}) def test_incorrect_path(self): yield self._test_incorrect_path, "nonexistingmodule", None yield self._test_incorrect_path, "duecredit.tests.mod.nonexistingmodule", None yield self._test_incorrect_path, "duecredit.tests.mod", "nonexisting" yield self._test_incorrect_path, "duecredit.tests.mod", "nonexisting.whocares" def _test_find_object(mod, path, parent, obj_name, obj): assert_equal(find_object(mod, path), (parent, obj_name, obj)) def test_find_object(): import duecredit.tests.mod as mod yield _test_find_object, mod, 'testfunc1', mod, 'testfunc1', mod.testfunc1 yield _test_find_object, mod, 'TestClass1', mod, 'TestClass1', mod.TestClass1 yield _test_find_object, mod, 'TestClass1.testmeth1', mod.TestClass1, 'testmeth1', mod.TestClass1.testmeth1 yield _test_find_object, mod, 'TestClass12.Embed.testmeth1', \ mod.TestClass12.Embed, 'testmeth1', mod.TestClass12.Embed.testmeth1 def test_no_double_activation(): orig__import__ = __builtin__.__import__ try: due = DueCreditCollector() injector = DueCreditInjector(collector=due) injector.activate() assert_false(__builtin__.__import__ is orig__import__) duecredited__import__ = __builtin__.__import__ # TODO: catch/analyze/swallow warning injector.activate() assert_true(__builtin__.__import__ is duecredited__import__) # we didn't decorate again finally: injector.deactivate() __builtin__.__import__ = orig__import__ def test_get_modules_for_injection(): assert_equal(get_modules_for_injection(), [ 'mod_biosig', 'mod_dipy', 'mod_mdp', 'mod_mne', 'mod_nibabel', 'mod_nipy', 'mod_nipype', 'mod_numpy', 'mod_pandas', 'mod_psychopy', 'mod_scipy', 'mod_skimage', 'mod_sklearn']) def test_cover_our_injections(): # this one tests only import/syntax/api for the injections due = DueCreditCollector() inj = DueCreditInjector(collector=due) for modname in get_modules_for_injection(): mod = __import__('duecredit.injections.' + modname, fromlist=["duecredit.injections"]) mod.inject(inj) def test_no_harm_from_deactivate(): # if we have not activated one -- shouldn't blow if we deactivate it # TODO: catch warning being spitted out DueCreditInjector().deactivate() def test_injector_del(): orig__import__ = __builtin__.__import__ try: due = DueCreditCollector() inj = DueCreditInjector(collector=due) del inj # delete inactive assert_true(__builtin__.__import__ is orig__import__) inj = DueCreditInjector(collector=due) inj.activate(retrospect=False) assert_false(__builtin__.__import__ is orig__import__) assert_false(inj._orig_import is None) del inj # delete active but not used inj = None __builtin__.__import__ = None # We need to do that since otherwise gc will not pick up inj gc.collect() # To cause __del__ assert_true(__builtin__.__import__ is orig__import__) import abc # and new imports work just fine finally: __builtin__.__import__ = orig__import__ def test_injector_delayed_del(): # interesting case -- if we still have an instance of injector hanging around # and then create a new one, activate it but then finally delete/gc old one # it would (currently) reset import back (because atm defined as class var) # which would ruin operation of the new injector orig__import__ = __builtin__.__import__ try: due = DueCreditCollector() inj = DueCreditInjector(collector=due) inj.activate(retrospect=False) assert_false(__builtin__.__import__ is orig__import__) assert_false(inj._orig_import is None) inj.deactivate() assert_true(__builtin__.__import__ is orig__import__) assert_true(inj._orig_import is None) # create 2nd one inj2 = DueCreditInjector(collector=due) inj2.activate(retrospect=False) assert_false(__builtin__.__import__ is orig__import__) assert_false(inj2._orig_import is None) del inj inj = None gc.collect() # To cause __del__ assert_false(__builtin__.__import__ is orig__import__) # would fail if del had side-effect assert_false(inj2._orig_import is None) inj2.deactivate() assert_true(__builtin__.__import__ is orig__import__) import abc # and new imports work just fine finally: __builtin__.__import__ = orig__import__ duecredit-0.6.0/duecredit/tests/test_io.py000066400000000000000000000476751273067304300206470ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import random import re import pickle import os from .test_collector import _sample_bibtex, _sample_bibtex2 from six.moves import StringIO from six import text_type from mock import patch from ..collector import DueCreditCollector, Citation from .test_collector import _sample_bibtex, _sample_doi from ..entries import BibTeX, DueCreditEntry, Doi from ..io import TextOutput, PickleOutput, import_doi, \ get_text_rendering, format_bibtex, _is_contained, Output, BibTeXOutput from ..utils import with_tempfile from nose.tools import assert_equal, assert_is_instance, assert_raises, \ assert_true, assert_false try: import vcr @vcr.use_cassette() def test_import_doi(): doi_good = '10.1038/nrd842' kw = dict(sleep=0.00001, retries=2) assert_is_instance(import_doi(doi_good, **kw), text_type) doi_bad = 'fasljfdldaksj' assert_raises(ValueError, import_doi, doi_bad, **kw) doi_zenodo = '10.5281/zenodo.50186' assert_is_instance(import_doi(doi_zenodo, **kw), text_type) except ImportError: # no vcr, and that is in 2015! pass @with_tempfile def test_pickleoutput(fn): #entry = BibTeX('@article{XXX0, ...}') entry = BibTeX("@article{Atkins_2002,\n" "title=title,\n" "volume=1, \n" "url=http://dx.doi.org/10.1038/nrd842, \n" "DOI=10.1038/nrd842, \n" "number=7, \n" "journal={Nat. Rev. Drug Disc.}, \n" "publisher={Nature Publishing Group}, \n" "author={Atkins, Joshua H. and Gershell, Leland J.}, \n" "year={2002}, \n" "month={Jul}, \n" "pages={491--492}\n}") collector_ = DueCreditCollector() collector_.add(entry) collector_.cite(entry, path='module') # test it doesn't puke with an empty collector collectors = [collector_, DueCreditCollector()] for collector in collectors: pickler = PickleOutput(collector, fn=fn) assert_equal(pickler.fn, fn) assert_equal(pickler.dump(), None) with open(fn, 'rb') as f: collector_loaded = pickle.load(f) assert_equal(collector.citations.keys(), collector_loaded.citations.keys()) # TODO: implement comparison of citations assert_equal(collector._entries.keys(), collector_loaded._entries.keys()) os.unlink(fn) def test_output(): entry = BibTeX(_sample_bibtex) entry2 = BibTeX(_sample_bibtex2) # normal use collector = DueCreditCollector() collector.cite(entry, path='package') collector.cite(entry, path='package.module') output = Output(None, collector) packages, modules, objects = output._get_collated_citations(tags=['*']) assert_equal(len(packages), 1) assert_equal(len(modules), 1) assert_equal(len(objects), 0) assert_equal(packages['package'][0], collector.citations[('package', entry.get_key())]) assert_equal(modules['package.module'][0], collector.citations[('package.module', entry.get_key())]) # no toppackage collector = DueCreditCollector() collector.cite(entry, path='package') collector.cite(entry, path='package2.module') output = Output(None, collector) packages, modules, objects = output._get_collated_citations(tags=['*']) assert_equal(len(packages), 0) assert_equal(len(modules), 1) assert_equal(len(objects), 0) assert_equal(modules['package2.module'][0], collector.citations[('package2.module', entry.get_key())]) # toppackage because required collector = DueCreditCollector() collector.cite(entry, path='package', cite_module=True) collector.cite(entry, path='package2.module') output = Output(None, collector) packages, modules, objects = output._get_collated_citations(tags=['*']) assert_equal(len(packages), 1) assert_equal(len(modules), 1) assert_equal(len(objects), 0) assert_equal(packages['package'][0], collector.citations[('package', entry.get_key())]) assert_equal(modules['package2.module'][0], collector.citations[('package2.module', entry.get_key())]) # check it returns multiple entries collector = DueCreditCollector() collector.cite(entry, path='package') collector.cite(entry2, path='package') collector.cite(entry, path='package.module') output = Output(None, collector) packages, modules, objects = output._get_collated_citations(tags=['*']) assert_equal(len(packages), 1) assert_equal(len(packages['package']), 2) assert_equal(len(modules), 1) assert_equal(len(objects), 0) # sort them in order so we know who is who # entry2 key is Atk... # entry key is XX.. packs = sorted(packages['package'], key=lambda x: x.entry.key) assert_equal(packs[0], collector.citations[('package', entry2.get_key())]) assert_equal(packs[1], collector.citations[('package', entry.get_key())]) assert_equal(modules['package.module'][0], collector.citations[('package.module', entry.get_key())]) # check that filtering works collector = DueCreditCollector() collector.cite(entry, path='package', tags=['edu']) collector.cite(entry2, path='package') collector.cite(entry, path='package.module', tags=['edu']) output = Output(None, collector) packages, modules, objects = output._get_collated_citations(tags=['edu']) assert_equal(len(packages), 1) assert_equal(len(packages['package']), 1) assert_equal(len(modules), 1) assert_equal(len(objects), 0) assert_equal(packages['package'][0], collector.citations[('package', entry.get_key())]) assert_equal(modules['package.module'][0], collector.citations[('package.module', entry.get_key())]) def test_output_return_all(): entry = BibTeX(_sample_bibtex) entry2 = BibTeX(_sample_bibtex2) # normal use collector = DueCreditCollector() collector.cite(entry, path='package') collector.cite(entry2, path='package2') output = Output(None, collector) packages, modules, objects = output._get_collated_citations(tags=['*']) assert_false(packages) assert_false(modules) assert_false(objects) for flag in ['1', 'True', 'TRUE', 'true', 'on', 'yes']: with patch.dict(os.environ, {'DUECREDIT_REPORT_ALL': flag}): # if _all is None then get the environment packages, modules, objects = output._get_collated_citations(tags=['*']) assert_equal(len(packages), 2) assert_false(modules) assert_false(objects) # however if _all is set it shouldn't work packages, modules, objects = output._get_collated_citations(tags=['*'], all_=False) assert_false(packages) assert_false(modules) assert_false(objects) def test_output_tags(): entry = BibTeX(_sample_bibtex) entry2 = BibTeX(_sample_bibtex2) # normal use collector = DueCreditCollector() collector.cite(entry, path='package', cite_module=True, tags=['edu']) collector.cite(entry2, path='package.module', tags=['wip']) output = Output(None, collector) packages, modules, objects = output._get_collated_citations(tags=['*']) assert_true(len(packages) == 1) assert_true(len(modules) == 1) assert_false(objects) packages, modules, objects = output._get_collated_citations() assert_false(packages) assert_false(modules) assert_false(objects) for tags in ['edu', 'wip', 'edu,wip']: with patch.dict(os.environ, {'DUECREDIT_REPORT_TAGS': tags}): # if tags is None then get the environment packages, modules, objects = output._get_collated_citations() assert_true(len(packages) == (1 if 'edu' in tags else 0)) assert_true(len(modules) == (1 if 'wip' in tags else 0)) assert_false(objects) # however if tags is set it shouldn't work packages, modules, objects = output._get_collated_citations(tags=['implementation']) assert_false(packages) assert_false(modules) assert_false(objects) def test_text_output(): entry = BibTeX(_sample_bibtex) entry2 = BibTeX(_sample_bibtex2) # in this case, since we're not citing any module or method, we shouldn't # output anything collector = DueCreditCollector() collector.cite(entry, path='package') strio = StringIO() TextOutput(strio, collector).dump(tags=['*']) value = strio.getvalue() assert_true("0 packages cited" in value, msg="value was %s" % value) assert_true("0 modules cited" in value, msg="value was %s" % value) assert_true("0 functions cited" in value, msg="value was %s" % value) # but it should be cited if cite_module=True collector = DueCreditCollector() collector.cite(entry, path='package', cite_module=True) strio = StringIO() TextOutput(strio, collector).dump(tags=['*']) value = strio.getvalue() assert_true("1 package cited" in value, msg="value was %s" % value) assert_true("0 modules cited" in value, msg="value was %s" % value) assert_true("0 functions cited" in value, msg="value was %s" % value) # in this case, we should be citing the package since we are also citing a # submodule collector = DueCreditCollector() collector.cite(entry, path='package') collector.cite(entry, path='package.module') strio = StringIO() TextOutput(strio, collector).dump(tags=['*']) value = strio.getvalue() assert_true("1 package cited" in value, msg="value was %s" % value) assert_true("1 module cited" in value, msg="value was %s" % value) assert_true("0 functions cited" in value, msg="value was %s" % value) assert_true("Halchenko, Y.O." in value, msg="value was %s" % value) assert_true(value.strip().endswith("Frontiers in Neuroinformatics, 6(22).")) # in this case, we should be citing the package since we are also citing a # submodule collector = DueCreditCollector() collector.cite(entry, path='package') collector.cite(entry2, path='package') collector.cite(entry, path='package.module') strio = StringIO() TextOutput(strio, collector).dump(tags=['*']) value = strio.getvalue() assert_true("1 package cited" in value, msg="value was %s" % value) assert_true("1 module cited" in value, msg="value was %s" % value) assert_true("0 functions cited" in value, msg="value was %s" % value) assert_true("Halchenko, Y.O." in value, msg="value was %s" % value) assert_true('[1, 2]' in value, msg="value was %s" %value) assert_false('[3]' in value, msg="value was %s" %value) def test_text_output_dump_formatting(): due = DueCreditCollector() # XXX: atm just to see if it spits out stuff @due.dcite(BibTeX(_sample_bibtex), description='solution to life', path='mymodule', version='0.0.16') def mymodule(arg1, kwarg2="blah"): """docstring""" assert_equal(arg1, "magical") assert_equal(kwarg2, 1) @due.dcite(BibTeX(_sample_bibtex2), description='solution to life', path='mymodule:myfunction') def myfunction(arg42): pass myfunction('argh') return "load" # check we don't have anything output strio = StringIO() TextOutput(strio, due).dump(tags=['*']) value = strio.getvalue() assert_true('0 modules cited' in value, msg='value was {0}'.format(value)) assert_true('0 functions cited' in value, msg='value was {0}'.format(value)) # now we call it -- check it prints stuff strio = StringIO() mymodule('magical', kwarg2=1) TextOutput(strio, due).dump(tags=['*']) value = strio.getvalue() assert_true('1 package cited' in value, msg='value was {0}'.format(value)) assert_true('1 function cited' in value, msg='value was {0}'.format(value)) assert_true('(v 0.0.16)' in value, msg='value was {0}'.format(value)) assert_equal(len(value.split('\n')), 16, msg='value was {0}'.format(len(value.split('\n')))) # test we get the reference numbering right samples_bibtex = [_generate_sample_bibtex() for x in range(6)] # this sucks but at the moment it's the only way to have multiple # references for a function @due.dcite(BibTeX(samples_bibtex[0]), description='another solution', path='myothermodule', version='0.0.666') def myothermodule(arg1, kwarg2="blah"): """docstring""" assert_equal(arg1, "magical") assert_equal(kwarg2, 1) @due.dcite(BibTeX(samples_bibtex[1]), description='solution to life', path='myothermodule:myotherfunction') @due.dcite(BibTeX(samples_bibtex[2]), description='solution to life', path='myothermodule:myotherfunction') @due.dcite(BibTeX(samples_bibtex[3]), description='solution to life', path='myothermodule:myotherfunction') @due.dcite(BibTeX(samples_bibtex[4]), description='solution to life', path='myothermodule:myotherfunction') @due.dcite(BibTeX(samples_bibtex[5]), description='solution to life', path='myothermodule:myotherfunction') def myotherfunction(arg42): pass myotherfunction('argh') return "load" myothermodule('magical', kwarg2=1) strio = StringIO() TextOutput(strio, due).dump(tags=['*']) value = strio.getvalue() lines = value.split('\n') citation_numbers = [] reference_numbers = [] references = [] for line in lines: match_citation = re.search('\[([0-9, ]+)\]$', line) match_reference = re.search('^\[([0-9])\]', line) if match_citation: citation_numbers.extend(match_citation.group(1).split(', ')) elif match_reference: reference_numbers.append(match_reference.group(1)) references.append(line.replace(match_reference.group(), "")) assert_equal(set(citation_numbers), set(reference_numbers)) assert_equal(len(set(references)), len(set(citation_numbers))) assert_equal(len(citation_numbers), 8) # verify that we have returned to previous state of filters import warnings assert_true(('ignore', None, UserWarning, None, 0) not in warnings.filters) def test_bibtex_output(): entry = BibTeX(_sample_bibtex) entry2 = BibTeX(_sample_bibtex2) # in this case, since we're not citing any module or method, we shouldn't # output anything collector = DueCreditCollector() collector.cite(entry, path='package') strio = StringIO() BibTeXOutput(strio, collector).dump(tags=['*']) value = strio.getvalue() assert_equal(value, '', msg='Value was {0}'.format(value)) # impose citing collector = DueCreditCollector() collector.cite(entry, path='package', cite_module=True) strio = StringIO() BibTeXOutput(strio, collector).dump(tags=['*']) value = strio.getvalue() assert_equal(value.strip(), _sample_bibtex.strip(), msg='Value was {0}'.format(value)) # impose filtering collector = DueCreditCollector() collector.cite(entry, path='package', cite_module=True, tags=['edu']) collector.cite(entry2, path='package.module') strio = StringIO() BibTeXOutput(strio, collector).dump(tags=['edu']) value = strio.getvalue() assert_equal(value.strip(), _sample_bibtex.strip(), msg='Value was {0}'.format(value)) # no filtering strio = StringIO() BibTeXOutput(strio, collector).dump(tags=['*']) value = strio.getvalue() assert_equal(value.strip(), _sample_bibtex.strip() + _sample_bibtex2.rstrip(), msg='Value was {0}'.format(value)) # check the we output only unique bibtex entries collector.cite(entry2, path='package') strio = StringIO() BibTeXOutput(strio, collector).dump(tags=['*']) value = strio.getvalue() value_ = sorted(value.strip().split('\n')) bibtex = sorted((_sample_bibtex.strip() + _sample_bibtex2.rstrip()).split('\n')) assert_equal(value_, bibtex, msg='Value was {0}'.format(value_, bibtex)) def _generate_sample_bibtex(): """ Generate a random sample bibtex to test multiple references """ letters = 'abcdefghilmnopqrstuvxz' numbers = '0123456789' letters_numbers = letters + letters.upper() + numbers letters_numbers_spaces = letters_numbers + ' ' key = "".join(random.sample(letters_numbers, 7)) title = "".join(random.sample(letters_numbers_spaces, 20)) journal = "".join(random.sample(letters_numbers_spaces, 20)) publisher = "".join(random.sample(letters_numbers_spaces, 10)) author = "".join(random.sample(letters, 6)) + ', ' + \ "".join(random.sample(letters, 4)) year = "".join(random.sample(numbers, 4)) elements = [('title', title), ('journal', journal), ('publisher', publisher), ('author', author), ('year', year)] sample_bibtex = "@ARTICLE{%s,\n" % key for string, value in elements: sample_bibtex += "%s={%s},\n" % (string, value) sample_bibtex += "}" return sample_bibtex @patch('duecredit.io.get_bibtex_rendering') @patch('duecredit.io.format_bibtex') def test_get_text_rendering(mock_format_bibtex, mock_get_bibtex_rendering): # mock get_bibtex_rendering to return the same bibtex entry sample_bibtex = BibTeX(_sample_bibtex) mock_get_bibtex_rendering.return_value = sample_bibtex # test if bibtex type is passed citation_bibtex = Citation(sample_bibtex, path='mypath') bibtex_output = get_text_rendering(citation_bibtex) mock_format_bibtex.assert_called_with(citation_bibtex.entry, style='harvard1') mock_format_bibtex.reset_mock() # test if doi type is passed citation_doi = Citation(Doi(_sample_doi), path='mypath') doi_output = get_text_rendering(citation_doi) mock_format_bibtex.assert_called_with(citation_bibtex.entry, style='harvard1') assert_equal(bibtex_output, doi_output) def test_format_bibtex_zenodo_doi(): """ test that we can correctly parse bibtex entries obtained from a zenodo doi """ # this was fetched on 2016-05-10 bibtex_zenodo = """ @data{0b1284ba-5ce5-4367-84f3-c44b4962ad90, doi = {10.5281/zenodo.50186}, url = {http://dx.doi.org/10.5281/zenodo.50186}, author = {Satrajit Ghosh; Chris Filo Gorgolewski; Oscar Esteban; Erik Ziegler; David Ellis; cindeem; Michael Waskom; Dav Clark; Michael; Fred Loney; Alexandre M. S.; Michael Notter; Hans Johnson; Anisha Keshavan; Yaroslav Halchenko; Carlo Hamalainen; Blake Dewey; Ben Cipollini; Daniel Clark; Julia Huntenburg; Drew Erickson; Michael Hanke; moloney; Jason W; Demian Wassermann; cdla; Nolan Nichols; Chris Markiewicz; Jarrod Millman; Arman Eshaghi; }, publisher = {Zenodo}, title = {nipype: Release candidate 1 for version 0.12.0}, year = {2016} } """ assert_equal(format_bibtex(BibTeX(bibtex_zenodo)), """Ghosh, S. et al., 2016. nipype: Release candidate 1 for version 0.12.0.""") def test_is_contained(): toppath = 'package' assert_true(_is_contained(toppath, 'package.module')) assert_true(_is_contained(toppath, 'package.module.submodule')) assert_true(_is_contained(toppath, 'package.module.submodule:object')) assert_true(_is_contained(toppath, 'package:object')) assert_true(_is_contained(toppath, toppath)) assert_false(_is_contained(toppath, 'package2')) assert_false(_is_contained(toppath, 'package2:anotherobject')) assert_false(_is_contained(toppath, 'package2.module:anotherobject')) duecredit-0.6.0/duecredit/tests/test_utils.py000066400000000000000000000022621273067304300213570ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. Originates from datalad package distributed # under MIT license # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import sys from mock import patch from ..utils import is_interactive from nose.tools import assert_false, assert_true def test_is_interactive_crippled_stdout(): class mocked_out(object): """the one which has no isatty """ def write(self, *args, **kwargs): pass class mocked_isatty(mocked_out): def isatty(self): return True for inout in ('in', 'out', 'err'): with patch('sys.std%s' % inout, mocked_out()): assert_false(is_interactive()) # just for paranoids with patch('sys.stdin', mocked_isatty()), \ patch('sys.stdout', mocked_isatty()), \ patch('sys.stderr', mocked_isatty()): assert_true(is_interactive())duecredit-0.6.0/duecredit/tests/test_versions.py000066400000000000000000000064411273067304300220720ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from os import linesep from ..version import __version__ from ..versions import ExternalVersions, StrictVersion from nose.tools import assert_true, assert_false from nose.tools import assert_equal, assert_greater_equal, assert_greater from nose.tools import assert_raises from nose import SkipTest from six import PY3 if PY3: # just to ease testing def cmp(a, b): return (a > b) - (a < b) def test_external_versions_basic(): ev = ExternalVersions() assert_equal(ev._versions, {}) assert_equal(ev['duecredit'], __version__) # and it could be compared assert_greater_equal(ev['duecredit'], __version__) assert_greater(ev['duecredit'], '0.1') assert_equal(list(ev.keys()), ['duecredit']) assert_true('duecredit' in ev) assert_false('unknown' in ev) # StrictVersion might remove training .0 version_str = str(ev['duecredit']) \ if isinstance(ev['duecredit'], StrictVersion) \ else __version__ assert_equal(ev.dumps(), "Versions: duecredit=%s" % version_str) # For non-existing one we get None assert_equal(ev['duecreditnonexisting'], None) # and nothing gets added to _versions for nonexisting assert_equal(set(ev._versions.keys()), {'duecredit'}) # but if it is a module without version, we get it set to UNKNOWN assert_equal(ev['os'], ev.UNKNOWN) # And get a record on that inside assert_equal(ev._versions.get('os'), ev.UNKNOWN) # And that thing is "True", i.e. present assert(ev['os']) # but not comparable with anything besides itself (was above) assert_raises(TypeError, cmp, ev['os'], '0') assert_raises(TypeError, assert_greater, ev['os'], '0') # And we can get versions based on modules themselves from duecredit.tests import mod assert_equal(ev[mod], mod.__version__) # Check that we can get a copy of the versions versions_dict = ev.versions versions_dict['duecredit'] = "0.0.1" assert_equal(versions_dict['duecredit'], "0.0.1") assert_equal(ev['duecredit'], __version__) def test_external_versions_unknown(): assert_equal(str(ExternalVersions.UNKNOWN), 'UNKNOWN') def _test_external(ev, modname): try: exec ("import %s" % modname, globals(), locals()) except ImportError: raise SkipTest("External %s not present" % modname) except Exception as e: raise SkipTest("External %s fails to import: %s" % (modname, e)) assert (ev[modname] is not ev.UNKNOWN) assert_greater(ev[modname], '0.0.1') assert_greater('1000000.0', ev[modname]) # unlikely in our lifetimes def test_external_versions_popular_packages(): ev = ExternalVersions() for modname in ('scipy', 'numpy', 'mvpa2', 'sklearn', 'statsmodels', 'pandas', 'matplotlib', 'psychopy'): yield _test_external, ev, modname # more of a smoke test assert_false(linesep in ev.dumps()) assert_true(ev.dumps(indent=True).endswith(linesep))duecredit-0.6.0/duecredit/tests/utils.py000066400000000000000000000007131273067304300203170ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## from nose import SkipTest class KnownFailure(SkipTest): pass duecredit-0.6.0/duecredit/utils.py000066400000000000000000000277021273067304300171640ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. Originates from datalad package distributed # under MIT license # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import glob import os import logging import sys import platform import tempfile from six import binary_type from os.path import exists, join as opj, isabs, expandvars, expanduser, abspath from os.path import realpath from functools import wraps # # Some useful variables # on_windows = platform.system() == 'Windows' on_osx = platform.system() == 'Darwin' on_linux = platform.system() == 'Linux' try: on_debian_wheezy = platform.system() == 'Linux' \ and platform.linux_distribution()[0] == 'debian' \ and platform.linux_distribution()[1].startswith('7.') except: # pragma: no cover on_debian_wheezy = False lgr = logging.getLogger("duecredit.utils") # # Little helpers # def is_interactive(): """Return True if all in/outs are tty""" if any(not hasattr(inout, 'isatty') for inout in (sys.stdin, sys.stdout, sys.stderr)): lgr.warning("Assuming non interactive session since isatty found missing") return False # TODO: check on windows if hasattr check would work correctly and add value: # return sys.stdin.isatty() and sys.stdout.isatty() and sys.stderr.isatty() def expandpath(path, force_absolute=True): """Expand all variables and user handles in a path. By default return an absolute path """ path = expandvars(expanduser(path)) if force_absolute: path = abspath(path) return path def is_explicit_path(path): """Return whether a path explicitly points to a location Any absolute path, or relative path starting with either '../' or './' is assumed to indicate a location on the filesystem. Any other path format is not considered explicit.""" path = expandpath(path, force_absolute=False) return isabs(path) \ or path.startswith(os.curdir + os.sep) \ or path.startswith(os.pardir + os.sep) def rotree(path, ro=True, chmod_files=True): """To make tree read-only or writable Parameters ---------- path : string Path to the tree/directory to chmod ro : bool, optional Either to make it R/O (default) or RW chmod_files : bool, optional Either to operate also on files (not just directories) """ if ro: chmod = lambda f: os.chmod(f, os.stat(f).st_mode & ~stat.S_IWRITE) else: chmod = lambda f: os.chmod(f, os.stat(f).st_mode | stat.S_IWRITE | stat.S_IREAD) for root, dirs, files in os.walk(path, followlinks=False): if chmod_files: for f in files: fullf = opj(root, f) # might be the "broken" symlink which would fail to stat etc if exists(fullf): chmod(fullf) chmod(root) def rmtree(path, chmod_files='auto', *args, **kwargs): """To remove git-annex .git it is needed to make all files and directories writable again first Parameters ---------- chmod_files : string or bool, optional Either to make files writable also before removal. Usually it is just a matter of directories to have write permissions. If 'auto' it would chmod files on windows by default `*args` : `**kwargs` : Passed into shutil.rmtree call """ # Give W permissions back only to directories, no need to bother with files if chmod_files == 'auto': chmod_files = on_windows if not os.path.islink(path): rotree(path, ro=False, chmod_files=chmod_files) shutil.rmtree(path, *args, **kwargs) else: # just remove the symlink os.unlink(path) def rmtemp(f, *args, **kwargs): """Wrapper to centralize removing of temp files so we could keep them around It will not remove the temporary file/directory if DATALAD_TESTS_KEEPTEMP environment variable is defined """ if not os.environ.get('DATALAD_TESTS_KEEPTEMP'): if not os.path.lexists(f): lgr.debug("Path %s does not exist, so can't be removed" % f) return lgr.log(5, "Removing temp file: %s" % f) # Can also be a directory if os.path.isdir(f): rmtree(f, *args, **kwargs) else: for i in range(10): try: os.unlink(f) except OSError as e: if i < 9: sleep(0.1) continue else: raise break else: lgr.info("Keeping temp file: %s" % f) # # Decorators # # Borrowed from pandas # Copyright: 2011-2014, Lambda Foundry, Inc. and PyData Development Team # License: BSD-3 def optional_args(decorator): """allows a decorator to take optional positional and keyword arguments. Assumes that taking a single, callable, positional argument means that it is decorating a function, i.e. something like this:: @my_decorator def function(): pass Calls decorator with decorator(f, *args, **kwargs)""" @wraps(decorator) def wrapper(*args, **kwargs): def dec(f): return decorator(f, *args, **kwargs) is_decorating = not kwargs and len(args) == 1 and callable(args[0]) if is_decorating: f = args[0] args = [] return dec(f) else: return dec return wrapper def never_fail(f): """Assure that function never fails -- all exceptions are caught""" @wraps(f) def wrapped_func(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: lgr.warning("DueCredit internal failure while running %s: %r. " "Please report to developers at https://github.com/duecredit/duecredit/issues" % (f, e)) if os.environ.get('DUECREDIT_ALLOW_FAIL', False): return f else: return wrapped_func def borrowdoc(cls, methodname=None, replace=None): """Return a decorator to borrow docstring from another `cls`.`methodname` Common use is to borrow a docstring from the class's method for an adapter function (e.g. sphere_searchlight borrows from Searchlight) Examples -------- To borrow `__repr__` docstring from parent class `Mapper`, do:: @borrowdoc(Mapper) def __repr__(self): ... Parameters ---------- cls Usually a parent class methodname : None or str Name of the method from which to borrow. If None, would use the same name as of the decorated method replace : None or str, optional If not None, then not entire docstring gets replaced but only the matching to "replace" value string """ def _borrowdoc(method): """Decorator which assigns to the `method` docstring from another """ if methodname is None: other_method = getattr(cls, method.__name__) else: other_method = getattr(cls, methodname) if hasattr(other_method, '__doc__'): if not replace: method.__doc__ = other_method.__doc__ else: method.__doc__ = method.__doc__.replace(replace, other_method.__doc__) return method return _borrowdoc # TODO: just provide decorators for tempfile.mk* functions. This is ugly! def get_tempfile_kwargs(tkwargs={}, prefix="", wrapped=None): """Updates kwargs to be passed to tempfile. calls depending on env vars """ # operate on a copy of tkwargs to avoid any side-effects tkwargs_ = tkwargs.copy() # TODO: don't remember why I had this one originally # if len(targs)<2 and \ if not 'prefix' in tkwargs_: tkwargs_['prefix'] = '_'.join( ['duecredit_temp'] + ([prefix] if prefix else []) + ([''] if (on_windows or not wrapped) else [wrapped.__name__])) directory = os.environ.get('DUECREDIT_TESTS_TEMPDIR') if directory and 'dir' not in tkwargs_: tkwargs_['dir'] = directory return tkwargs_ @optional_args def with_tempfile(t, content=None, **tkwargs): """Decorator function to provide a temporary file name and remove it at the end Parameters ---------- mkdir : bool, optional (default: False) If True, temporary directory created using tempfile.mkdtemp() content : str or bytes, optional Content to be stored in the file created `**tkwargs`: All other arguments are passed into the call to tempfile.mk{,d}temp(), and resultant temporary filename is passed as the first argument into the function t. If no 'prefix' argument is provided, it will be constructed using module and function names ('.' replaced with '_'). To change the used directory without providing keyword argument 'dir' set DUECREDIT_TESTS_TEMPDIR. Examples -------- @with_tempfile def test_write(tfile): open(tfile, 'w').write('silly test') """ @wraps(t) def newfunc(*arg, **kw): tkwargs_ = get_tempfile_kwargs(tkwargs, wrapped=t) # if DUECREDIT_TESTS_TEMPDIR is set, use that as directory, # let mktemp handle it otherwise. However, an explicitly provided # dir=... will override this. mkdir = tkwargs_.pop('mkdir', False) filename = {False: tempfile.mktemp, True: tempfile.mkdtemp}[mkdir](**tkwargs_) filename = realpath(filename) if content: with open(filename, 'w' + ('b' if isinstance(content, binary_type) else '')) as f: f.write(content) if __debug__: lgr.debug('Running %s with temporary filename %s', t.__name__, filename) try: return t(*(arg + (filename,)), **kw) finally: # glob here for all files with the same name (-suffix) # would be useful whenever we requested .img filename, # and function creates .hdr as well lsuffix = len(tkwargs_.get('suffix', '')) filename_ = lsuffix and filename[:-lsuffix] or filename filenames = glob.glob(filename_ + '*') if len(filename_) < 3 or len(filenames) > 5: # For paranoid yoh who stepped into this already ones ;-) lgr.warning("It is unlikely that it was intended to remove all" " files matching %r. Skipping" % filename_) return for f in filenames: try: rmtemp(f) except OSError: pass if tkwargs.get('mkdir', None) and content is not None: raise ValueError("mkdir=True while providing content makes no sense") return newfunc # # Context Managers # # # Additional handlers # _sys_excepthook = sys.excepthook # Just in case we ever need original one def setup_exceptionhook(): """Overloads default sys.excepthook with our exceptionhook handler. If interactive, our exceptionhook handler will invoke pdb.post_mortem; if not interactive, then invokes default handler. """ def _duecredit_pdb_excepthook(type, value, tb): if is_interactive(): import traceback, pdb traceback.print_exception(type, value, tb) print pdb.post_mortem(tb) else: lgr.warn("We cannot setup exception hook since not in interactive mode") # we are in interactive mode or we don't have a tty-like # device, so we call the default hook #sys.__excepthook__(type, value, tb) _sys_excepthook(type, value, tb) sys.excepthook = _duecredit_pdb_excepthook duecredit-0.6.0/duecredit/versions.py000066400000000000000000000075301273067304300176710ustar00rootroot00000000000000# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- # ex: set sts=4 ts=4 sw=4 noet: # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # # See COPYING file distributed along with the duecredit package for the # copyright and license terms. # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Module to help maintain a registry of versions for external modules etc """ import sys from os import linesep from six import string_types from distutils.version import StrictVersion, LooseVersion # To depict an unknown version, which can't be compared by mistake etc class UnknownVersion: """For internal use """ def __str__(self): return "UNKNOWN" def __cmp__(self, other): if other is self: return 0 raise TypeError("UNKNOWN version is not comparable") class ExternalVersions(object): """Helper to figure out/use versions of the external modules. It maintains a dictionary of `distuil.version.StrictVersion`s to make comparisons easy. If version string doesn't conform the StrictVersion LooseVersion will be used. If version can't be deduced for the module, 'None' is assigned """ UNKNOWN = UnknownVersion() def __init__(self): self._versions = {} @classmethod def _deduce_version(klass, module): version = None for attr in ('__version__', 'version'): if hasattr(module, attr): version = getattr(module, attr) break if isinstance(version, tuple) or isinstance(version, list): # Generate string representation version = ".".join(str(x) for x in version) if version: try: return StrictVersion(version) except ValueError: # let's then go with Loose one return LooseVersion(version) else: return klass.UNKNOWN def __getitem__(self, module): # when ran straight in its source code -- fails to discover nipy's version.. TODO #if module == 'nipy': # import pdb; pdb.set_trace() if not isinstance(module, string_types): modname = module.__name__ else: modname = module module = None if modname not in self._versions: if module is None: if modname not in sys.modules: try: module = __import__(modname) except ImportError: return None else: module = sys.modules[modname] self._versions[modname] = self._deduce_version(module) return self._versions.get(modname, self.UNKNOWN) def keys(self): """Return names of the known modules""" return self._versions.keys() def __contains__(self, item): return item in self._versions @property def versions(self): """Return dictionary (copy) of versions""" return self._versions.copy() def dumps(self, indent=False, preamble="Versions:"): """Return listing of versions as a string Parameters ---------- indent: bool or str, optional If set would instruct on how to indent entries (if just True, ' ' is used). Otherwise returned in a single line preamble: str, optional What preamble to the listing to use """ if indent and (indent is True): indent = ' ' items = ["%s=%s" % (k, self._versions[k]) for k in sorted(self._versions)] out = "%s" % preamble if indent: out += (linesep + indent).join([''] + items) + linesep else: out += " " + ' '.join(items) return out external_versions = ExternalVersions() duecredit-0.6.0/examples/000077500000000000000000000000001273067304300153105ustar00rootroot00000000000000duecredit-0.6.0/examples/example_scipy.py000066400000000000000000000007671273067304300205360ustar00rootroot00000000000000# A tiny analysis script to demonstrate duecredit # # Import of duecredit is not necessary if you just run this script with # python -m duecredit # import duecredit # Just to enable duecredit from scipy.cluster.hierarchy import linkage from scipy.spatial.distance import pdist from sklearn.datasets import make_blobs print("I: Simulating 4 blobs") data, true_label = make_blobs(centers=4) dist = pdist(data, metric='euclidean') Z = linkage(dist, method='single') print("I: Done clustering 4 blobs") duecredit-0.6.0/requirements.txt000066400000000000000000000001511273067304300167530ustar00rootroot00000000000000# install everything among install_requires specified in setup.py # and needed for testing -e .[tests] duecredit-0.6.0/setup.cfg000066400000000000000000000003261273067304300153140ustar00rootroot00000000000000[metadata] description-file = README.md [bdist_rpm] release = 1 packager = Yaroslav Halchenko doc_files = README.md CHANGELOG.md LICENSE CONTRIBUTING.md duecredit-0.6.0/setup.py000077500000000000000000000125501273067304300152120ustar00rootroot00000000000000#!/usr/bin/env python """ duecredit -- publications (donations, etc) tracer """ import re import os import sys import re from datetime import datetime from setuptools import setup from pkgutil import walk_packages from subprocess import Popen, PIPE from os.path import exists # Adopted from citeproc-py # License: BSD-2 # Copyright 2011-2013 Brecht Machiels PACKAGE = 'duecredit' PACKAGE_ABSPATH = os.path.abspath(PACKAGE) VERSION_FILE = PACKAGE + '/version.py' # retrieve the version number from git or VERSION_FILE # inspired by http://dcreager.net/2010/02/10/setuptools-git-version-numbers/ try: if exists('debian/copyright'): print('Generating version.py out of debian/copyright information') # building debian package. Deduce version from debian/copyright with open('debian/changelog', 'r') as f: lines = f.readlines() __version__ = re.sub('(.*)-(.*?)$', r'\1.debian\2', lines[0].split()[1].strip('()') ).replace('-', '.') # TODO: unify format whenever really bored ;) __release_date__ = re.sub('^ -- .*>\s*(.*)', r'\1', list(filter(lambda x: x.startswith(' -- '), lines))[0].rstrip()) else: print('Attempting to get version number from git...') git = Popen(['git', 'describe', '--abbrev=4', '--dirty'], stdout=PIPE, stderr=sys.stderr) if git.wait() != 0: raise OSError line = git.stdout.readlines()[0].strip().decode('ascii') if line.count('-') >= 2: # we should parse it to make version compatible with PEP440 # unfortunately we wouldn't be able to include git treeish # into the version, and thus can have collisions. So let's # release from master only line = '.dev'.join(line.split('-')[:2]) __version__ = line __release_date__ = datetime.now().strftime('%b %d %Y, %H:%M:%S') with open(VERSION_FILE, 'w') as version_file: version_file.write("__version__ = '{0}'\n".format(__version__)) version_file.write("__release_date__ = '{0}'\n".format(__release_date__)) except OSError as e: print('Assume we are running from a source distribution.') # read version from VERSION_FILE if os.path.exists(VERSION_FILE): with open(VERSION_FILE) as version_file: code = compile(version_file.read(), VERSION_FILE, 'exec') exec(code, locals(), globals()) else: __version__ = '0.0.0.dev' print("Version: %s" % __version__) with open('README.md') as file: README = file.read() def find_packages(path, prefix): yield prefix prefix = prefix + "." for _, name, ispkg in walk_packages(path, prefix): if ispkg: yield name setup( name=PACKAGE, version=__version__, packages=list(find_packages([PACKAGE_ABSPATH], PACKAGE)), scripts=[], install_requires=['requests', 'citeproc-py', 'six'], extras_require={ 'tests': [ 'mock', 'nose>=1.3.4', 'vcrpy', 'contextlib2' ] }, include_package_data=True, provides=[PACKAGE], #test_suite='nose.collector', entry_points={ 'console_scripts': [ 'duecredit=duecredit.cmdline.main:main', ], }, author='Yaroslav Halchenko, Matteo Visconti di Oleggio Castello', author_email='yoh@onerussian.com', description='Publications (and donations) tracer', long_description="""\ duecredit is being conceived to address the problem of inadequate citation of scientific software and methods, and limited visibility of donation requests for open-source software. It provides a simple framework (at the moment for Python only) to embed publication or other references in the original code so they are automatically collected and reported to the user at the necessary level of reference detail, i.e. only references for actually used functionality will be presented back if software provides multiple citeable implementations. To get a sense of what duecredit is about, run for example shipped along example script, or your analysis script with `-m duecredit`, e.g. python -m duecredit examples/example_scipy.py """, url='https://github.com/duecredit/duecredit', # Download URL will point to the latest release, thus suffixes removed download_url='https://github.com/duecredit/duecredit/releases/tag/%s' % re.sub('-.*$', '', __version__), keywords=['citation tracing'], license='2-clause BSD License', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Environment :: Other Environment', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'Intended Audience :: Education', 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Legal Industry', 'Intended Audience :: Other Audience', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Topic :: Documentation', 'Topic :: Software Development :: Documentation', 'Topic :: Software Development :: Libraries :: Python Modules', ] ) duecredit-0.6.0/tox.ini000066400000000000000000000011731273067304300150070ustar00rootroot00000000000000[tox] envlist = py27,py33,py34,py35 #,flake8 [testenv] commands = nosetests {posargs} deps = -r{toxinidir}/requirements.txt [testenv:cover] commands = nosetests --with-coverage {posargs} [testenv:flake8] commands = flake8 {posargs} [testenv:venv] commands = {posargs} [flake8] #show-source = True # E265 = comment blocks like @{ section, which it can't handle # E266 = too many leading '#' for block comment # E731 = do not assign a lambda expression, use a def # W293 = Blank line contains whitespace #ignore = E265,W293,E266,E731 max-line-length = 120 include = duecredit exclude = .tox,.venv,venv-debug,build,dist,doc,git/ext/